update
This commit is contained in:
@@ -1 +1,2 @@
|
||||
NODE_ENV=
|
||||
NODE_ENV=
|
||||
API_URL=https://kevisual.xiongxiao.me
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
|
||||
.env
|
||||
!.env*example
|
||||
dist
|
||||
.next
|
||||
out
|
||||
@@ -10,6 +10,10 @@ const nextConfig: NextConfig = {
|
||||
distDir: 'dist',
|
||||
basePath: basePath,
|
||||
trailingSlash: true,
|
||||
transpilePackages: ['@kevisual/api'],
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
27
package.json
27
package.json
@@ -1,17 +1,20 @@
|
||||
{
|
||||
"name": "@kevisual/next-simple-template",
|
||||
"name": "@kevisual/center",
|
||||
"version": "0.1.0",
|
||||
"basename": "/root/next-simple-template",
|
||||
"basename": "/root/center",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"pub": "ev deploy ./dist -k next-simple-template -v 0.1.0 -u -y y",
|
||||
"pub": "ev deploy ./dist -k center -v 0.1.0 -u -y y",
|
||||
"ui": "pnpm dlx shadcn@latest add "
|
||||
},
|
||||
"dependencies": {
|
||||
"@kevisual/query": "^0.0.35",
|
||||
"@kevisual/router": "^0.0.53",
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@kevisual/api": "^0.0.26",
|
||||
"@kevisual/cache": "^0.0.5",
|
||||
"@kevisual/query": "^0.0.38",
|
||||
"@kevisual/router": "^0.0.60",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
@@ -20,25 +23,33 @@
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-select": "^2.2.6",
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"antd": "^6.2.0",
|
||||
"antd": "^6.2.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.2.3",
|
||||
"es-toolkit": "^1.43.0",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"lucide-react": "^0.562.0",
|
||||
"next": "16.1.2",
|
||||
"marked": "^17.0.1",
|
||||
"next": "16.1.4",
|
||||
"react": "19.2.3",
|
||||
"react-day-picker": "^9.13.0",
|
||||
"react-dom": "19.2.3",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"valtio": "^2.3.0",
|
||||
"zustand": "^5.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^25",
|
||||
"@types/react": "^19",
|
||||
|
||||
388
pnpm-lock.yaml
generated
388
pnpm-lock.yaml
generated
@@ -8,12 +8,21 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@ant-design/icons':
|
||||
specifier: ^6.1.0
|
||||
version: 6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@kevisual/api':
|
||||
specifier: ^0.0.26
|
||||
version: 0.0.26
|
||||
'@kevisual/cache':
|
||||
specifier: ^0.0.5
|
||||
version: 0.0.5
|
||||
'@kevisual/query':
|
||||
specifier: ^0.0.35
|
||||
version: 0.0.35
|
||||
specifier: ^0.0.38
|
||||
version: 0.0.38
|
||||
'@kevisual/router':
|
||||
specifier: ^0.0.53
|
||||
version: 0.0.53
|
||||
specifier: ^0.0.60
|
||||
version: 0.0.60
|
||||
'@radix-ui/react-checkbox':
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -38,6 +47,9 @@ importers:
|
||||
'@radix-ui/react-slot':
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-switch':
|
||||
specifier: ^1.2.6
|
||||
version: 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-tabs':
|
||||
specifier: ^1.1.13
|
||||
version: 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -45,8 +57,8 @@ importers:
|
||||
specifier: ^1.2.8
|
||||
version: 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
antd:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1(date-fns@4.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -56,30 +68,45 @@ importers:
|
||||
cmdk:
|
||||
specifier: ^1.1.1
|
||||
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
copy-to-clipboard:
|
||||
specifier: ^3.3.3
|
||||
version: 3.3.3
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
dayjs:
|
||||
specifier: ^1.11.19
|
||||
version: 1.11.19
|
||||
dotenv:
|
||||
specifier: ^17.2.3
|
||||
version: 17.2.3
|
||||
es-toolkit:
|
||||
specifier: ^1.43.0
|
||||
version: 1.43.0
|
||||
specifier: ^1.44.0
|
||||
version: 1.44.0
|
||||
idb-keyval:
|
||||
specifier: ^6.2.2
|
||||
version: 6.2.2
|
||||
jotai:
|
||||
specifier: ^2.16.1
|
||||
version: 2.16.1(@types/react@19.2.7)(react@19.2.3)
|
||||
lucide-react:
|
||||
specifier: ^0.562.0
|
||||
version: 0.562.0(react@19.2.3)
|
||||
marked:
|
||||
specifier: ^17.0.1
|
||||
version: 17.0.1
|
||||
next:
|
||||
specifier: 16.1.1
|
||||
version: 16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
specifier: 16.1.4
|
||||
version: 16.1.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react:
|
||||
specifier: 19.2.3
|
||||
version: 19.2.3
|
||||
react-day-picker:
|
||||
specifier: ^9.13.0
|
||||
version: 9.13.0(react@19.2.3)
|
||||
react-dom:
|
||||
specifier: 19.2.3
|
||||
version: 19.2.3(react@19.2.3)
|
||||
react-hook-form:
|
||||
specifier: ^7.71.1
|
||||
version: 7.71.1(react@19.2.3)
|
||||
sonner:
|
||||
specifier: ^2.0.7
|
||||
version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -93,6 +120,9 @@ importers:
|
||||
specifier: ^5.0.10
|
||||
version: 5.0.10(@types/react@19.2.7)(react@19.2.3)
|
||||
devDependencies:
|
||||
'@kevisual/types':
|
||||
specifier: ^0.0.12
|
||||
version: 0.0.12
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4
|
||||
version: 4.1.18
|
||||
@@ -130,8 +160,8 @@ packages:
|
||||
react: '>=18'
|
||||
react-dom: '>=18'
|
||||
|
||||
'@ant-design/cssinjs@2.0.2':
|
||||
resolution: {integrity: sha512-7KDVIigtqlamOLtJ0hbjECX/sDGDaJXsM/KHala8I/1E4lpl9RAO585kbVvh/k1rIrFAV6JeGkXmdWyYj9XvuA==}
|
||||
'@ant-design/cssinjs@2.0.3':
|
||||
resolution: {integrity: sha512-HAo8SZ3a6G8v6jT0suCz1270na6EA3obeJWM4uzRijBhdwdoMAXWK2f4WWkwB28yUufsfk3CAhN1coGPQq4kNQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.0.0'
|
||||
react-dom: '>=16.0.0'
|
||||
@@ -160,6 +190,9 @@ packages:
|
||||
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@date-fns/tz@1.4.1':
|
||||
resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==}
|
||||
|
||||
'@emnapi/runtime@1.8.1':
|
||||
resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==}
|
||||
|
||||
@@ -337,59 +370,74 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@kevisual/query@0.0.35':
|
||||
resolution: {integrity: sha512-80dyy2LMCmEC72g+X4QWUKlZErhawQPgnGSBNR4yhrBcFgHIJQ14LR1Z+bS5S1I7db+1PDNpaxBTjIaoYoXunw==}
|
||||
'@kevisual/api@0.0.26':
|
||||
resolution: {integrity: sha512-u40PNMeVoWfUrUXWSEOc6/aacmt+flq7gE6kdmJJpgc9rfn8I6mXWlp3z0FPrAuqJopKAraiU9To+U72TL8U9g==}
|
||||
|
||||
'@kevisual/router@0.0.53':
|
||||
resolution: {integrity: sha512-Bw9xYVWyxRhd230nF1ac7cyvzWDYKI/3V+Fr1Ew1Bfr0Ey8KuWb1MgPPopHkRHCCcUcysLtWXfu/JRiTAoBmGA==}
|
||||
'@kevisual/cache@0.0.5':
|
||||
resolution: {integrity: sha512-fgtUYGUUq/DY0KFV4CkWszNqvQUaA8XvMTUjoR9ZXRpau5IIDolD/Wen2TFsZ7G3Rfy+lef5dnaiZVDkZwdVKg==}
|
||||
|
||||
'@next/env@16.1.1':
|
||||
resolution: {integrity: sha512-3oxyM97Sr2PqiVyMyrZUtrtM3jqqFxOQJVuKclDsgj/L728iZt/GyslkN4NwarledZATCenbk4Offjk1hQmaAA==}
|
||||
'@kevisual/js-filter@0.0.5':
|
||||
resolution: {integrity: sha512-+S+Sf3K/aP6XtZI2s7TgKOr35UuvUvtpJ9YDW30a+mY0/N8gRuzyKhieBzQN7Ykayzz70uoMavBXut2rUlLgzw==}
|
||||
|
||||
'@next/swc-darwin-arm64@16.1.1':
|
||||
resolution: {integrity: sha512-JS3m42ifsVSJjSTzh27nW+Igfha3NdBOFScr9C80hHGrWx55pTrVL23RJbqir7k7/15SKlrLHhh/MQzqBBYrQA==}
|
||||
'@kevisual/load@0.0.6':
|
||||
resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==}
|
||||
|
||||
'@kevisual/query@0.0.38':
|
||||
resolution: {integrity: sha512-bfvbSodsZyMfwY+1T2SvDeOCKsT/AaIxlVe0+B1R/fNhlg2MDq2CP0L9HKiFkEm+OXrvXcYDMKPUituVUM5J6Q==}
|
||||
|
||||
'@kevisual/router@0.0.60':
|
||||
resolution: {integrity: sha512-2v/ZzUstsaq+Uqo+tZX9ys5E+/2erPggCtljv9jTb3NA88ZdHsYUAsd5wUFvLtf9QucpJCzyWEt+InDV/98FKw==}
|
||||
|
||||
'@kevisual/types@0.0.12':
|
||||
resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==}
|
||||
|
||||
'@next/env@16.1.4':
|
||||
resolution: {integrity: sha512-gkrXnZyxPUy0Gg6SrPQPccbNVLSP3vmW8LU5dwEttEEC1RwDivk8w4O+sZIjFvPrSICXyhQDCG+y3VmjlJf+9A==}
|
||||
|
||||
'@next/swc-darwin-arm64@16.1.4':
|
||||
resolution: {integrity: sha512-T8atLKuvk13XQUdVLCv1ZzMPgLPW0+DWWbHSQXs0/3TjPrKNxTmUIhOEaoEyl3Z82k8h/gEtqyuoZGv6+Ugawg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-darwin-x64@16.1.1':
|
||||
resolution: {integrity: sha512-hbyKtrDGUkgkyQi1m1IyD3q4I/3m9ngr+V93z4oKHrPcmxwNL5iMWORvLSGAf2YujL+6HxgVvZuCYZfLfb4bGw==}
|
||||
'@next/swc-darwin-x64@16.1.4':
|
||||
resolution: {integrity: sha512-AKC/qVjUGUQDSPI6gESTx0xOnOPQ5gttogNS3o6bA83yiaSZJek0Am5yXy82F1KcZCx3DdOwdGPZpQCluonuxg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.1.1':
|
||||
resolution: {integrity: sha512-/fvHet+EYckFvRLQ0jPHJCUI5/B56+2DpI1xDSvi80r/3Ez+Eaa2Yq4tJcRTaB1kqj/HrYKn8Yplm9bNoMJpwQ==}
|
||||
'@next/swc-linux-arm64-gnu@16.1.4':
|
||||
resolution: {integrity: sha512-POQ65+pnYOkZNdngWfMEt7r53bzWiKkVNbjpmCt1Zb3V6lxJNXSsjwRuTQ8P/kguxDC8LRkqaL3vvsFrce4dMQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.1.1':
|
||||
resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==}
|
||||
'@next/swc-linux-arm64-musl@16.1.4':
|
||||
resolution: {integrity: sha512-3Wm0zGYVCs6qDFAiSSDL+Z+r46EdtCv/2l+UlIdMbAq9hPJBvGu/rZOeuvCaIUjbArkmXac8HnTyQPJFzFWA0Q==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.1.1':
|
||||
resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==}
|
||||
'@next/swc-linux-x64-gnu@16.1.4':
|
||||
resolution: {integrity: sha512-lWAYAezFinaJiD5Gv8HDidtsZdT3CDaCeqoPoJjeB57OqzvMajpIhlZFce5sCAH6VuX4mdkxCRqecCJFwfm2nQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-linux-x64-musl@16.1.1':
|
||||
resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==}
|
||||
'@next/swc-linux-x64-musl@16.1.4':
|
||||
resolution: {integrity: sha512-fHaIpT7x4gA6VQbdEpYUXRGyge/YbRrkG6DXM60XiBqDM2g2NcrsQaIuj375egnGFkJow4RHacgBOEsHfGbiUw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.1.1':
|
||||
resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==}
|
||||
'@next/swc-win32-arm64-msvc@16.1.4':
|
||||
resolution: {integrity: sha512-MCrXxrTSE7jPN1NyXJr39E+aNFBrQZtO154LoCz7n99FuKqJDekgxipoodLNWdQP7/DZ5tKMc/efybx1l159hw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.1.1':
|
||||
resolution: {integrity: sha512-Ncwbw2WJ57Al5OX0k4chM68DKhEPlrXBaSXDCi2kPi5f4d8b3ejr3RRJGfKBLrn2YJL5ezNS7w2TZLHSti8CMw==}
|
||||
'@next/swc-win32-x64-msvc@16.1.4':
|
||||
resolution: {integrity: sha512-JSVlm9MDhmTXw/sO2PE/MRj+G6XOSMZB+BcZ0a7d6KwVFZVpkHcb2okyoYFBaco6LeiL53BBklRlOrDDbOeE5w==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -697,6 +745,19 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-switch@1.2.6':
|
||||
resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-tabs@1.1.13':
|
||||
resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
|
||||
peerDependencies:
|
||||
@@ -983,8 +1044,8 @@ packages:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
|
||||
'@rc-component/resize-observer@1.0.1':
|
||||
resolution: {integrity: sha512-r+w+Mz1EiueGk1IgjB3ptNXLYSLZ5vnEfKHH+gfgj7JMupftyzvUUl3fRcMZe5uMM04x0n8+G2o/c6nlO2+Wag==}
|
||||
'@rc-component/resize-observer@1.1.1':
|
||||
resolution: {integrity: sha512-NfXXMmiR+SmUuKE1NwJESzEUYUFWIDUn2uXpxCTOLwiRUUakd62DRNFjRJArgzyFW8S5rsL4aX5XlyIXyC/vRA==}
|
||||
peerDependencies:
|
||||
react: '>=16.9.0'
|
||||
react-dom: '>=16.9.0'
|
||||
@@ -1068,8 +1129,8 @@ packages:
|
||||
react: '*'
|
||||
react-dom: '*'
|
||||
|
||||
'@rc-component/trigger@3.8.2':
|
||||
resolution: {integrity: sha512-I6idYAk8YY3Ly6v5hB7ONqxfdTYTcVNUmV1ZjtSsGH6N/k5tss9+OAtusr+7zdlIcD7TwnlsoB5etfB14ORtMw==}
|
||||
'@rc-component/trigger@3.9.0':
|
||||
resolution: {integrity: sha512-X8btpwfrT27AgrZVOz4swclhEHTZcqaHeQMXXBgveagOiakTa36uObXbdwerXffgV8G9dH1fAAE0DHtVQs8EHg==}
|
||||
engines: {node: '>=8.x'}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0'
|
||||
@@ -1196,8 +1257,8 @@ packages:
|
||||
'@types/react@19.2.7':
|
||||
resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
|
||||
|
||||
antd@6.2.0:
|
||||
resolution: {integrity: sha512-fwETatwHYExjfzKcV41fBtgPo4kp+g+9gp5YOSSGxwnJHljps8TbXef8WP7ZnaOn5dkcA9xIC0TyUecIybBG7w==}
|
||||
antd@6.2.1:
|
||||
resolution: {integrity: sha512-ycw/XX7So4MdrwYKGfvZJdkGiCYUOSTebAIi+ejE95WJ138b11oy/iJg7iH0qydaD/B5sFd7Tz8XfPBuW7CRmw==}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0'
|
||||
react-dom: '>=18.0.0'
|
||||
@@ -1232,9 +1293,18 @@ packages:
|
||||
compute-scroll-into-view@3.1.1:
|
||||
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
|
||||
|
||||
copy-to-clipboard@3.3.3:
|
||||
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
date-fns-jalali@4.1.0-0:
|
||||
resolution: {integrity: sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==}
|
||||
|
||||
date-fns@4.1.0:
|
||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||
|
||||
dayjs@1.11.19:
|
||||
resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
|
||||
|
||||
@@ -1253,8 +1323,11 @@ packages:
|
||||
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
es-toolkit@1.43.0:
|
||||
resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==}
|
||||
es-toolkit@1.44.0:
|
||||
resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==}
|
||||
|
||||
eventemitter3@5.0.4:
|
||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||
|
||||
get-nonce@1.0.1:
|
||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||
@@ -1263,6 +1336,10 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
hono@4.11.5:
|
||||
resolution: {integrity: sha512-WemPi9/WfyMwZs+ZUXdiwcCh9Y+m7L+8vki9MzDw3jJ+W9Lc+12HGsd368Qc1vZi1xwW8BWMMsnK5efYKPdt4g==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
idb-keyval@6.2.2:
|
||||
resolution: {integrity: sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==}
|
||||
|
||||
@@ -1273,24 +1350,6 @@ packages:
|
||||
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
|
||||
hasBin: true
|
||||
|
||||
jotai@2.16.1:
|
||||
resolution: {integrity: sha512-vrHcAbo3P7Br37C8Bv6JshMtlKMPqqmx0DDREtTjT4nf3QChDrYdbH+4ik/9V0cXA57dK28RkJ5dctYvavcIlg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': '>=7.0.0'
|
||||
'@babel/template': '>=7.0.0'
|
||||
'@types/react': '>=17.0.0'
|
||||
react: '>=17.0.0'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
'@babel/template':
|
||||
optional: true
|
||||
'@types/react':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
|
||||
json2mq@0.2.0:
|
||||
resolution: {integrity: sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==}
|
||||
|
||||
@@ -1364,6 +1423,10 @@ packages:
|
||||
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
||||
lru-cache@11.2.4:
|
||||
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lucide-react@0.562.0:
|
||||
resolution: {integrity: sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==}
|
||||
peerDependencies:
|
||||
@@ -1372,13 +1435,23 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
marked@17.0.1:
|
||||
resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==}
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.11:
|
||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
next@16.1.1:
|
||||
resolution: {integrity: sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==}
|
||||
nanoid@5.1.6:
|
||||
resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
next@16.1.4:
|
||||
resolution: {integrity: sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ==}
|
||||
engines: {node: '>=20.9.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -1412,11 +1485,23 @@ packages:
|
||||
proxy-compare@3.0.1:
|
||||
resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==}
|
||||
|
||||
react-day-picker@9.13.0:
|
||||
resolution: {integrity: sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
|
||||
react-dom@19.2.3:
|
||||
resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==}
|
||||
peerDependencies:
|
||||
react: ^19.2.3
|
||||
|
||||
react-hook-form@7.71.1:
|
||||
resolution: {integrity: sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
|
||||
react-is@18.3.1:
|
||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||
|
||||
@@ -1512,6 +1597,9 @@ packages:
|
||||
resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==}
|
||||
engines: {node: '>=12.22'}
|
||||
|
||||
toggle-selection@1.0.6:
|
||||
resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@@ -1586,13 +1674,13 @@ snapshots:
|
||||
|
||||
'@ant-design/cssinjs-utils@2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@ant-design/cssinjs': 2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@ant-design/cssinjs': 2.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@babel/runtime': 7.28.6
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
'@ant-design/cssinjs@2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
'@ant-design/cssinjs@2.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@emotion/hash': 0.8.0
|
||||
@@ -1628,6 +1716,8 @@ snapshots:
|
||||
|
||||
'@babel/runtime@7.28.6': {}
|
||||
|
||||
'@date-fns/tz@1.4.1': {}
|
||||
|
||||
'@emnapi/runtime@1.8.1':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
@@ -1770,36 +1860,60 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@kevisual/query@0.0.35':
|
||||
'@kevisual/api@0.0.26':
|
||||
dependencies:
|
||||
'@kevisual/js-filter': 0.0.5
|
||||
'@kevisual/load': 0.0.6
|
||||
es-toolkit: 1.44.0
|
||||
eventemitter3: 5.0.4
|
||||
nanoid: 5.1.6
|
||||
|
||||
'@kevisual/cache@0.0.5':
|
||||
dependencies:
|
||||
idb-keyval: 6.2.2
|
||||
lru-cache: 11.2.4
|
||||
nanoid: 5.1.6
|
||||
|
||||
'@kevisual/js-filter@0.0.5': {}
|
||||
|
||||
'@kevisual/load@0.0.6':
|
||||
dependencies:
|
||||
eventemitter3: 5.0.4
|
||||
|
||||
'@kevisual/query@0.0.38':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
'@kevisual/router@0.0.53': {}
|
||||
'@kevisual/router@0.0.60':
|
||||
dependencies:
|
||||
hono: 4.11.5
|
||||
|
||||
'@next/env@16.1.1': {}
|
||||
'@kevisual/types@0.0.12': {}
|
||||
|
||||
'@next/swc-darwin-arm64@16.1.1':
|
||||
'@next/env@16.1.4': {}
|
||||
|
||||
'@next/swc-darwin-arm64@16.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-darwin-x64@16.1.1':
|
||||
'@next/swc-darwin-x64@16.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-gnu@16.1.1':
|
||||
'@next/swc-linux-arm64-gnu@16.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-arm64-musl@16.1.1':
|
||||
'@next/swc-linux-arm64-musl@16.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-gnu@16.1.1':
|
||||
'@next/swc-linux-x64-gnu@16.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-linux-x64-musl@16.1.1':
|
||||
'@next/swc-linux-x64-musl@16.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-arm64-msvc@16.1.1':
|
||||
'@next/swc-win32-arm64-msvc@16.1.4':
|
||||
optional: true
|
||||
|
||||
'@next/swc-win32-x64-msvc@16.1.1':
|
||||
'@next/swc-win32-x64-msvc@16.1.4':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/number@1.1.1': {}
|
||||
@@ -2127,6 +2241,21 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
|
||||
'@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
'@radix-ui/react-use-size': 1.1.1(@types/react@19.2.7)(react@19.2.3)
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
'@types/react-dom': 19.2.3(@types/react@19.2.7)
|
||||
|
||||
'@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.3
|
||||
@@ -2291,7 +2420,7 @@ snapshots:
|
||||
|
||||
'@rc-component/dropdown@1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@rc-component/trigger': 3.8.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
@@ -2334,7 +2463,7 @@ snapshots:
|
||||
'@rc-component/input': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/menu': 1.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/textarea': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.8.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
@@ -2344,7 +2473,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/overflow': 1.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.8.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
@@ -2378,7 +2507,7 @@ snapshots:
|
||||
'@rc-component/overflow@1.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
@@ -2391,16 +2520,17 @@ snapshots:
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
'@rc-component/picker@1.9.0(dayjs@1.11.19)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
'@rc-component/picker@1.9.0(date-fns@4.1.0)(dayjs@1.11.19)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@rc-component/overflow': 1.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.8.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
optionalDependencies:
|
||||
date-fns: 4.1.0
|
||||
dayjs: 1.11.19
|
||||
|
||||
'@rc-component/portal@2.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
@@ -2430,7 +2560,7 @@ snapshots:
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
'@rc-component/resize-observer@1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
'@rc-component/resize-observer@1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
react: 19.2.3
|
||||
@@ -2448,7 +2578,7 @@ snapshots:
|
||||
'@rc-component/select@1.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@rc-component/overflow': 1.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.8.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/virtual-list': 1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
@@ -2479,7 +2609,7 @@ snapshots:
|
||||
'@rc-component/table@1.9.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@rc-component/context': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/virtual-list': 1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
@@ -2491,7 +2621,7 @@ snapshots:
|
||||
'@rc-component/dropdown': 1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/menu': 1.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
@@ -2500,7 +2630,7 @@ snapshots:
|
||||
'@rc-component/textarea@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@rc-component/input': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
@@ -2508,7 +2638,7 @@ snapshots:
|
||||
|
||||
'@rc-component/tooltip@1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@rc-component/trigger': 3.8.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
@@ -2517,7 +2647,7 @@ snapshots:
|
||||
'@rc-component/tour@2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@rc-component/portal': 2.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.8.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
@@ -2541,11 +2671,11 @@ snapshots:
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
'@rc-component/trigger@3.8.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
'@rc-component/trigger@3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@rc-component/motion': 1.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/portal': 2.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
@@ -2568,7 +2698,7 @@ snapshots:
|
||||
'@rc-component/virtual-list@1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
'@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
react: 19.2.3
|
||||
@@ -2659,10 +2789,10 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
antd@6.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
antd@6.2.1(date-fns@4.1.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@ant-design/colors': 8.0.1
|
||||
'@ant-design/cssinjs': 2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@ant-design/cssinjs': 2.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@ant-design/cssinjs-utils': 2.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@ant-design/fast-color': 3.0.0
|
||||
'@ant-design/icons': 6.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -2685,11 +2815,11 @@ snapshots:
|
||||
'@rc-component/mutate-observer': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/notification': 1.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/pagination': 1.2.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/picker': 1.9.0(dayjs@1.11.19)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/picker': 1.9.0(date-fns@4.1.0)(dayjs@1.11.19)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/progress': 1.0.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/qrcode': 1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/rate': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/resize-observer': 1.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/segmented': 1.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/select': 1.5.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/slider': 1.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -2702,7 +2832,7 @@ snapshots:
|
||||
'@rc-component/tour': 2.3.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/tree': 1.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/tree-select': 1.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.8.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/trigger': 3.9.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/upload': 1.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
'@rc-component/util': 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
clsx: 2.1.1
|
||||
@@ -2746,8 +2876,16 @@ snapshots:
|
||||
|
||||
compute-scroll-into-view@3.1.1: {}
|
||||
|
||||
copy-to-clipboard@3.3.3:
|
||||
dependencies:
|
||||
toggle-selection: 1.0.6
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
date-fns-jalali@4.1.0-0: {}
|
||||
|
||||
date-fns@4.1.0: {}
|
||||
|
||||
dayjs@1.11.19: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
@@ -2761,23 +2899,22 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.0
|
||||
|
||||
es-toolkit@1.43.0: {}
|
||||
es-toolkit@1.44.0: {}
|
||||
|
||||
eventemitter3@5.0.4: {}
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
hono@4.11.5: {}
|
||||
|
||||
idb-keyval@6.2.2: {}
|
||||
|
||||
is-mobile@5.0.0: {}
|
||||
|
||||
jiti@2.6.1: {}
|
||||
|
||||
jotai@2.16.1(@types/react@19.2.7)(react@19.2.3):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.7
|
||||
react: 19.2.3
|
||||
|
||||
json2mq@0.2.0:
|
||||
dependencies:
|
||||
string-convert: 0.2.1
|
||||
@@ -2831,6 +2968,8 @@ snapshots:
|
||||
lightningcss-win32-arm64-msvc: 1.30.2
|
||||
lightningcss-win32-x64-msvc: 1.30.2
|
||||
|
||||
lru-cache@11.2.4: {}
|
||||
|
||||
lucide-react@0.562.0(react@19.2.3):
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
@@ -2839,11 +2978,15 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
marked@17.0.1: {}
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
next@16.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
nanoid@5.1.6: {}
|
||||
|
||||
next@16.1.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@next/env': 16.1.1
|
||||
'@next/env': 16.1.4
|
||||
'@swc/helpers': 0.5.15
|
||||
baseline-browser-mapping: 2.9.11
|
||||
caniuse-lite: 1.0.30001762
|
||||
@@ -2852,14 +2995,14 @@ snapshots:
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
styled-jsx: 5.1.6(react@19.2.3)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.1.1
|
||||
'@next/swc-darwin-x64': 16.1.1
|
||||
'@next/swc-linux-arm64-gnu': 16.1.1
|
||||
'@next/swc-linux-arm64-musl': 16.1.1
|
||||
'@next/swc-linux-x64-gnu': 16.1.1
|
||||
'@next/swc-linux-x64-musl': 16.1.1
|
||||
'@next/swc-win32-arm64-msvc': 16.1.1
|
||||
'@next/swc-win32-x64-msvc': 16.1.1
|
||||
'@next/swc-darwin-arm64': 16.1.4
|
||||
'@next/swc-darwin-x64': 16.1.4
|
||||
'@next/swc-linux-arm64-gnu': 16.1.4
|
||||
'@next/swc-linux-arm64-musl': 16.1.4
|
||||
'@next/swc-linux-x64-gnu': 16.1.4
|
||||
'@next/swc-linux-x64-musl': 16.1.4
|
||||
'@next/swc-win32-arm64-msvc': 16.1.4
|
||||
'@next/swc-win32-x64-msvc': 16.1.4
|
||||
sharp: 0.34.5
|
||||
transitivePeerDependencies:
|
||||
- '@babel/core'
|
||||
@@ -2881,11 +3024,22 @@ snapshots:
|
||||
|
||||
proxy-compare@3.0.1: {}
|
||||
|
||||
react-day-picker@9.13.0(react@19.2.3):
|
||||
dependencies:
|
||||
'@date-fns/tz': 1.4.1
|
||||
date-fns: 4.1.0
|
||||
date-fns-jalali: 4.1.0-0
|
||||
react: 19.2.3
|
||||
|
||||
react-dom@19.2.3(react@19.2.3):
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
scheduler: 0.27.0
|
||||
|
||||
react-hook-form@7.71.1(react@19.2.3):
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
|
||||
react-is@18.3.1: {}
|
||||
|
||||
react-remove-scroll-bar@2.3.8(@types/react@19.2.7)(react@19.2.3):
|
||||
@@ -2982,6 +3136,8 @@ snapshots:
|
||||
|
||||
throttle-debounce@5.0.2: {}
|
||||
|
||||
toggle-selection@1.0.6: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tw-animate-css@1.4.0: {}
|
||||
|
||||
BIN
public/panda.jpg
Normal file
BIN
public/panda.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
@@ -1,9 +0,0 @@
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
About Light Code
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
src/app/apps/app/AIEditorLink.tsx
Normal file
47
src/app/apps/app/AIEditorLink.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import { useLayoutStore } from '@/modules/layout/store';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { toast } from 'sonner';
|
||||
import { Folder } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { openLink } from '@/modules/basename';
|
||||
|
||||
type Props = {
|
||||
pathname?: string;
|
||||
};
|
||||
export const AIEditorLink = (props: Props) => {
|
||||
const layoutUser = useLayoutStore(
|
||||
useShallow((state) => ({
|
||||
user: state.me?.username || '',
|
||||
})),
|
||||
);
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
if (!layoutUser.user) {
|
||||
toast.error('请先登录');
|
||||
}
|
||||
if (!window) {
|
||||
return;
|
||||
}
|
||||
let folder = `${layoutUser.user}/resources/${props.pathname}`;
|
||||
if (folder.endsWith('/')) {
|
||||
folder = folder.slice(0, -1);
|
||||
}
|
||||
let baseUri = location.origin;
|
||||
const openUrl = `${baseUri}/root/ai-pages/ai-editor/?folder=${folder}/`;
|
||||
openLink(openUrl, '_blank');
|
||||
}}>
|
||||
<Folder className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>打开对应的文件夹</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
378
src/app/apps/app/page.tsx
Normal file
378
src/app/apps/app/page.tsx
Normal file
@@ -0,0 +1,378 @@
|
||||
'use client';
|
||||
|
||||
import { useAppVersionStore } from '../store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
|
||||
import { Plus, ChevronLeft, Upload, File, Trash2, ExternalLink } from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { isObjectNull } from '@/modules/is-null';
|
||||
import { FileUpload } from '../modules/FileUpload';
|
||||
import clsx from 'clsx';
|
||||
import { toast as message } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { pick } from 'es-toolkit';
|
||||
import { useAppDeleteModalStore, AppDeleteModal } from '../modules/AppDeleteModal';
|
||||
import { AIEditorLink } from './AIEditorLink';
|
||||
import { openLink } from '@/modules/basename';
|
||||
import { LayoutMain } from '@/modules/layout';
|
||||
|
||||
const FormModal = () => {
|
||||
const { control, handleSubmit, reset } = useForm();
|
||||
const containerStore = useAppVersionStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
showEdit: state.showEdit,
|
||||
setShowEdit: state.setShowEdit,
|
||||
formData: state.formData,
|
||||
updateData: state.updateData,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const open = containerStore.showEdit;
|
||||
if (open) {
|
||||
const isNull = isObjectNull(containerStore.formData);
|
||||
if (isNull) {
|
||||
reset({});
|
||||
} else {
|
||||
reset(containerStore.formData);
|
||||
}
|
||||
}
|
||||
}, [containerStore.showEdit]);
|
||||
|
||||
const onFinish = async (values: any) => {
|
||||
const pickValues = pick(values, ['id', 'key', 'version']);
|
||||
containerStore.updateData(pickValues);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
containerStore.setShowEdit(false);
|
||||
reset();
|
||||
};
|
||||
|
||||
const isEdit = containerStore.formData.id;
|
||||
|
||||
return (
|
||||
<Dialog open={containerStore.showEdit} onOpenChange={(open) => containerStore.setShowEdit(open)}>
|
||||
<DialogContent className='w-[800px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? 'Edit' : 'Add'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className='flex flex-col gap-6 py-4' onSubmit={handleSubmit(onFinish)}>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='key'>key</Label>
|
||||
<Controller
|
||||
name='key'
|
||||
control={control}
|
||||
defaultValue=''
|
||||
render={({ field }) => <Input id='key' {...field} disabled />}
|
||||
/>
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='version'>version</Label>
|
||||
<Controller
|
||||
name='version'
|
||||
control={control}
|
||||
defaultValue=''
|
||||
render={({ field }) => <Input id='version' {...field} />}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<Button type='submit'>Submit</Button>
|
||||
<Button variant='outline' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const getAppKey = () => {
|
||||
const [appKey, setAppKey] = useState('');
|
||||
useLayoutEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const url = new URL(window.location.href);
|
||||
const appKey = url.searchParams.get('appKey');
|
||||
setAppKey(appKey || '');
|
||||
}, []);
|
||||
return appKey || '';
|
||||
}
|
||||
export const AppVersionList = () => {
|
||||
const appKey = getAppKey();
|
||||
const versionStore = useAppVersionStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
list: state.list,
|
||||
getList: state.getList,
|
||||
key: state.key,
|
||||
setKey: state.setKey,
|
||||
setShowEdit: state.setShowEdit,
|
||||
formData: state.formData,
|
||||
setFormData: state.setFormData,
|
||||
deleteData: state.deleteData,
|
||||
publishVersion: state.publishVersion,
|
||||
app: state.app,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const appDeleteModalStore = useAppDeleteModalStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
onClickDelete: state.onClickDelete,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const [isUpload, setIsUpload] = useState(false);
|
||||
useEffect(() => {
|
||||
// fetch app version list
|
||||
if (appKey) {
|
||||
versionStore.setKey(appKey);
|
||||
versionStore.getList();
|
||||
}
|
||||
}, [appKey]);
|
||||
const appVersion = useMemo(() => {
|
||||
return versionStore.app?.version || '';
|
||||
}, [versionStore.app?.version]);
|
||||
if (!appKey) {
|
||||
return <div>App Key is required</div>;
|
||||
}
|
||||
return (
|
||||
<div className='w-full h-full flex bg-slate-100'>
|
||||
<div className='p-2 bg-white'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
versionStore.setFormData({ key: appKey });
|
||||
versionStore.setShowEdit(true);
|
||||
}}>
|
||||
<Plus className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>添加版本</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className='grow h-full relative'>
|
||||
<div className='absolute top-2 left-4'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
// navigate('/app/edit/list');
|
||||
history.back();
|
||||
}}>
|
||||
<ChevronLeft className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>返回</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className='w-full h-full p-4 pt-12'>
|
||||
<div className='w-full h-full rounded-lg bg-white'>
|
||||
<div className='flex gap-2 flex-wrap p-4'>
|
||||
{versionStore.list.map((item, index) => {
|
||||
const isPublish = item.version === appVersion;
|
||||
const color = isPublish ? 'bg-green-500' : '';
|
||||
const isRunning = item.status === 'running';
|
||||
return (
|
||||
<div className='w-[300px] bg-white rounded-lg border border-slate-200 shadow-sm p-4' key={index}>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<span className='font-medium'>{item.version}</span>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className={clsx('rounded-full w-4 h-4', color)}></div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{isPublish ? 'published' : ''}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='mt-4 flex gap-1'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={(e) => {
|
||||
appDeleteModalStore.onClickDelete('app-version', item);
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
versionStore.publishVersion({ id: item.id });
|
||||
}}>
|
||||
<Upload className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>使用当前版本,发布为此版本</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
if (isRunning) {
|
||||
const origin = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
const link = new URL(`/test/${item.id}`, origin);
|
||||
openLink(link.toString(), '_blank');
|
||||
} else {
|
||||
message.error('The app is not running');
|
||||
}
|
||||
}}>
|
||||
<ExternalLink className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>To Test App</TooltipContent>
|
||||
</Tooltip>
|
||||
<AIEditorLink pathname={item.key + '/' + item.version} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
versionStore.setFormData(item);
|
||||
setIsUpload(true);
|
||||
}}>
|
||||
<File className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>文件管理</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='shark h-full'>
|
||||
{isUpload && (
|
||||
<div className='bg-white p-2 w-[600px] h-full flex flex-col'>
|
||||
<div className='header flex items-center gap-2'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
setIsUpload(false);
|
||||
}}>
|
||||
<ChevronLeft className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>返回</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className='font-bold'>{versionStore.key}</div>
|
||||
</div>
|
||||
<AppVersionFile />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FormModal />
|
||||
<AppDeleteModal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const AppVersionFile = () => {
|
||||
const versionStore = useAppVersionStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
formData: state.formData,
|
||||
detectVersionList: state.detectVersionList,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const versionFiles = useMemo(() => {
|
||||
if (!versionStore.formData?.data) return [];
|
||||
const files = versionStore.formData.data.files || [];
|
||||
return files as any[];
|
||||
}, [versionStore.formData]);
|
||||
const onDetect = useCallback(async () => {
|
||||
console.log('formData', versionStore.formData);
|
||||
if (!versionStore.formData.key || !versionStore.formData.version) {
|
||||
message.error('请先选择应用和版本');
|
||||
return;
|
||||
}
|
||||
const res = await versionStore.detectVersionList({
|
||||
appKey: versionStore.formData.key,
|
||||
version: versionStore.formData.version,
|
||||
});
|
||||
console.log('res', res);
|
||||
if (res.code === 200) {
|
||||
message.success('检测实际文件成功');
|
||||
} else {
|
||||
message.error(res.message || 'Detect failed');
|
||||
}
|
||||
}, [versionStore.formData]);
|
||||
return (
|
||||
<>
|
||||
<div>version: {versionStore.formData.version}</div>
|
||||
<div className='border border-gray-200 rounded-md my-2 grow overflow-hidden'>
|
||||
<div className='flex gap-2 items-center border-b border-b-gray-200 py-2 px-2'>
|
||||
Files
|
||||
<FileUpload />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant='outline' size='sm' onClick={onDetect}>
|
||||
检测文件
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>从同文件目录下检测已经上传的文件内容</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div
|
||||
className='mt-2 '
|
||||
style={{
|
||||
height: 'calc(100% - 40px)',
|
||||
}}>
|
||||
<div className='h-full overflow-auto mb-4 pb-8 scrollbar'>
|
||||
{versionFiles.map((file, index) => {
|
||||
const prefix = versionStore.formData.key + '/' + versionStore.formData.version + '/';
|
||||
const _path = file.path || '';
|
||||
const path = _path.replace(prefix, '');
|
||||
return (
|
||||
<div className='flex gap-2 px-4 py-2 border-b border-b-gray-200' key={index}>
|
||||
{/* <div className='w-[100px] truncate'>{file.name}</div> */}
|
||||
<div>
|
||||
<File className='h-4 w-4' />
|
||||
</div>
|
||||
<div>{path}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return <LayoutMain>
|
||||
<AppVersionList />
|
||||
</LayoutMain>
|
||||
};
|
||||
11
src/app/apps/constants.ts
Normal file
11
src/app/apps/constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export const iText = {
|
||||
share: {
|
||||
title: '共享设置',
|
||||
tips: `共享设置
|
||||
|
||||
1. 设置公共可以直接访问
|
||||
2. 设置受保护需要登录后访问
|
||||
3. 设置私有只有自己可以访问。\n
|
||||
受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。`,
|
||||
},
|
||||
};
|
||||
7
src/app/apps/layouts/index.tsx
Normal file
7
src/app/apps/layouts/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { LayoutMain } from '@/modules/layout';
|
||||
|
||||
export const Main = () => {
|
||||
return <LayoutMain title='User Apps' />;
|
||||
};
|
||||
86
src/app/apps/modules/AppDeleteModal.tsx
Normal file
86
src/app/apps/modules/AppDeleteModal.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useState } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { useAppVersionStore, useUserAppStore } from '../store';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
|
||||
type AppDeleteModalStore = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
app: any;
|
||||
setApp: (app: any) => void;
|
||||
type: 'user-app' | 'app-version';
|
||||
setType: (type: 'user-app' | 'app-version') => void;
|
||||
onClickDelete: (type: 'user-app' | 'app-version', data: any) => void;
|
||||
};
|
||||
export const useAppDeleteModalStore = create<AppDeleteModalStore>((set) => ({
|
||||
open: false,
|
||||
setOpen: (open) => set({ open }),
|
||||
app: null,
|
||||
setApp: (app) => set({ app }),
|
||||
type: 'user-app',
|
||||
setType: (type) => set({ type }),
|
||||
onClickDelete: (type, data) => {
|
||||
set({ open: true, type, app: data });
|
||||
},
|
||||
}));
|
||||
|
||||
export const AppDeleteModal = () => {
|
||||
const { open, setOpen, app, type } = useAppDeleteModalStore();
|
||||
const userAppStore = useUserAppStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
deleteData: state.deleteData,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const appVersionStore = useAppVersionStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
deleteData: state.deleteData,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const onClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
const onDelete = (deleteFile = false) => {
|
||||
if (type === 'user-app') {
|
||||
userAppStore.deleteData(app.id, deleteFile);
|
||||
} else {
|
||||
appVersionStore.deleteData(app.id, deleteFile);
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tips</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='w-[400px]'>
|
||||
<p className='text-sm text-gray-500'>Delete App Introduce</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant='default' onClick={() => onDelete()}>
|
||||
Delete
|
||||
</Button>
|
||||
<Button variant='outline' onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant='destructive'
|
||||
onClick={() => {
|
||||
onDelete(true);
|
||||
}}>
|
||||
Delete and remove file
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
61
src/app/apps/modules/DatePicker.tsx
Normal file
61
src/app/apps/modules/DatePicker.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Calendar as CalendarIcon } from "lucide-react"
|
||||
import dayjs, { type Dayjs } from "dayjs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
|
||||
|
||||
interface DatePickerProps {
|
||||
className?: string
|
||||
value?: string | Dayjs
|
||||
onChange?: (date: Dayjs) => void
|
||||
}
|
||||
|
||||
export function DatePicker({ className, value, onChange }: DatePickerProps) {
|
||||
const [date, setDate] = React.useState<Date | undefined>(
|
||||
value ? new Date(typeof value === 'string' ? value : value.toISOString()) : undefined
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (value) {
|
||||
const dateValue = typeof value === 'string' ? value : value.toISOString()
|
||||
setDate(new Date(dateValue))
|
||||
}
|
||||
}, [value])
|
||||
|
||||
const handleSelect = (selectedDate: Date | undefined) => {
|
||||
setDate(selectedDate)
|
||||
if (selectedDate && onChange) {
|
||||
onChange(dayjs(selectedDate))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-full justify-start text-left font-normal",
|
||||
!date && "text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{date ? dayjs(date).format("YYYY-MM-DD") : <span>选择日期</span>}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
111
src/app/apps/modules/FileUpload.tsx
Normal file
111
src/app/apps/modules/FileUpload.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useAppVersionStore } from '../store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { toast as message } from 'sonner';
|
||||
|
||||
export type FileType = {
|
||||
name: string;
|
||||
size: number;
|
||||
lastModified: number;
|
||||
webkitRelativePath: string; // 包含name
|
||||
};
|
||||
|
||||
export const FileUpload = () => {
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const appVersionStore = useAppVersionStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
formData: state.formData,
|
||||
setFormData: state.setFormData,
|
||||
updateByFromData: state.updateByFromData,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const onChange = useCallback(
|
||||
async (e: any) => {
|
||||
console.log(e.target.files);
|
||||
// webkitRelativePath
|
||||
let files = Array.from(e.target.files) as any[];
|
||||
console.log(files);
|
||||
if (files.length === 0) {
|
||||
message.error('请选择文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤 文件 .DS_Store
|
||||
files = files.filter((file) => {
|
||||
if (file.webkitRelativePath.startsWith('__MACOSX')) {
|
||||
return false;
|
||||
}
|
||||
// 过滤node_modules
|
||||
if (file.webkitRelativePath.includes('node_modules')) {
|
||||
return false;
|
||||
}
|
||||
// 过滤以.开头的文件
|
||||
return !file.name.startsWith('.');
|
||||
});
|
||||
if (files.length === 0) {
|
||||
console.log('no files');
|
||||
return;
|
||||
}
|
||||
const root = files[0].webkitRelativePath.split('/')[0];
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => {
|
||||
// relativePath 去除第一级
|
||||
const webkitRelativePath = file.webkitRelativePath.replace(root + '/', '');
|
||||
formData.append('file', file, webkitRelativePath); // 保留文件夹路径
|
||||
});
|
||||
const key = appVersionStore.formData.key;
|
||||
const version = appVersionStore.formData.version;
|
||||
formData.append('appKey', key);
|
||||
formData.append('version', version);
|
||||
const res = await fetch('/api/app/upload', {
|
||||
method: 'POST',
|
||||
body: formData, //
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + (typeof window !== 'undefined' ? localStorage.getItem('token') : ''),
|
||||
},
|
||||
}).then((res) => res.json());
|
||||
if (res?.code === 200) {
|
||||
appVersionStore.setFormData(res.data);
|
||||
appVersionStore.updateByFromData();
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
// 清理之前上传的文件
|
||||
e.target.value = '';
|
||||
},
|
||||
[appVersionStore.formData],
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
className='hidden'
|
||||
ref={ref}
|
||||
type='file'
|
||||
// @ts-ignore
|
||||
webkitdirectory='true'
|
||||
multiple
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
const key = appVersionStore.formData.key;
|
||||
const version = appVersionStore.formData.version;
|
||||
if (!key || !version) {
|
||||
message.error('请先选择应用和版本');
|
||||
return;
|
||||
}
|
||||
ref.current!.click();
|
||||
}}>
|
||||
上传目录
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
149
src/app/apps/modules/PermissionManager.tsx
Normal file
149
src/app/apps/modules/PermissionManager.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { KeyParse, getTips } from './key-parse';
|
||||
import { DatePicker } from './DatePicker';
|
||||
import { TagsInput } from './TagsInput';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
|
||||
export const KeyShareSelect = ({ value, onChange }: { value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<Select value={value || ''} onValueChange={(val) => onChange?.(val)}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="选择共享类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='public'>公开</SelectItem>
|
||||
<SelectItem value='protected'>受保护</SelectItem>
|
||||
<SelectItem value='private'>私有</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
export const KeyTextField = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
|
||||
return (
|
||||
<Input
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type PermissionManagerProps = {
|
||||
value: Record<string, any>;
|
||||
onChange: (value: Record<string, any>) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const PermissionManager = ({ value, onChange, className }: PermissionManagerProps) => {
|
||||
const [formData, setFormData] = useState<any>(value);
|
||||
const [keys, setKeys] = useState<any>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const hasShare = value?.share && value?.share === 'protected';
|
||||
setFormData(KeyParse.parse(value || {}));
|
||||
if (hasShare) {
|
||||
setKeys(['password', 'usernames', 'expiration-time']);
|
||||
} else {
|
||||
setKeys([]);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const onChangeValue = (key: string, newValue: any) => {
|
||||
let newFormData = { ...formData, [key]: newValue };
|
||||
if (key === 'share') {
|
||||
if (newValue === 'protected') {
|
||||
newFormData = { ...newFormData, password: '', usernames: [], 'expiration-time': null };
|
||||
onChange(KeyParse.stringify(newFormData));
|
||||
setKeys(['password', 'usernames', 'expiration-time']);
|
||||
} else {
|
||||
delete newFormData.password;
|
||||
delete newFormData.usernames;
|
||||
delete newFormData['expiration-time'];
|
||||
onChange(KeyParse.stringify(newFormData));
|
||||
setKeys([]);
|
||||
}
|
||||
} else {
|
||||
onChange(KeyParse.stringify(newFormData));
|
||||
}
|
||||
};
|
||||
|
||||
const tips = getTips('share');
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<form className={clsx('flex flex-col w-full gap-4', className)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium">共享</label>
|
||||
{tips && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-4 w-4 p-0">
|
||||
<HelpCircle size={16} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="whitespace-pre-wrap">
|
||||
<p>{tips}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<KeyShareSelect value={formData?.share} onChange={(value) => onChangeValue('share', value)} />
|
||||
</div>
|
||||
|
||||
{keys.map((item: any) => {
|
||||
const tips = getTips(item);
|
||||
|
||||
return (
|
||||
<div key={item} className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium">{item}</label>
|
||||
{tips && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-4 w-4 p-0">
|
||||
<HelpCircle size={16} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tips}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
{item === 'expiration-time' && (
|
||||
<DatePicker value={formData[item] || ''} onChange={(date) => onChangeValue(item, date)} />
|
||||
)}
|
||||
{item === 'usernames' && (
|
||||
<TagsInput value={formData[item] || []} onChange={(value: string[]) => onChangeValue(item, value)} />
|
||||
)}
|
||||
{item !== 'expiration-time' && item !== 'usernames' && (
|
||||
<KeyTextField name={item} value={formData[item] || ''} onChange={(value) => onChangeValue(item, value)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</form>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
62
src/app/apps/modules/TagsInput.tsx
Normal file
62
src/app/apps/modules/TagsInput.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { X } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
|
||||
type TagsInputProps = {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function TagsInput({ value, onChange, placeholder = "输入用户名,按回车添加", className }: TagsInputProps) {
|
||||
const [inputValue, setInputValue] = React.useState("")
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter" || e.key === ",") {
|
||||
e.preventDefault()
|
||||
const newValue = inputValue.trim()
|
||||
if (newValue && !value.includes(newValue)) {
|
||||
onChange([...value, newValue])
|
||||
}
|
||||
setInputValue("")
|
||||
} else if (e.key === "Backspace" && !inputValue && value.length > 0) {
|
||||
onChange(value.slice(0, -1))
|
||||
}
|
||||
}
|
||||
|
||||
const removeTag = (tagToRemove: string) => {
|
||||
onChange(value.filter((tag) => tag !== tagToRemove))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-wrap gap-2 w-full", className)}>
|
||||
{value.map((tag, index) => (
|
||||
<div
|
||||
key={`${tag}-${index}`}
|
||||
className="flex items-center gap-1 px-3 py-1 text-sm bg-secondary text-secondary-foreground rounded-md border border-border"
|
||||
>
|
||||
<span>{tag}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTag(tag)}
|
||||
className="flex items-center justify-center w-4 h-4 rounded hover:bg-muted transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={value.length === 0 ? placeholder : ""}
|
||||
className="flex-1 min-w-[120px] h-8"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
109
src/app/apps/modules/key-parse.ts
Normal file
109
src/app/apps/modules/key-parse.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
export const getTips = (key: string, lang?: string) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip) {
|
||||
if (lang === 'en') {
|
||||
return tip.enTips;
|
||||
}
|
||||
return tip.tips;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
export const keysTips = [
|
||||
{
|
||||
key: 'share',
|
||||
tips: `共享设置
|
||||
1. 设置公共可以直接访问
|
||||
2. 设置受保护需要登录后访问
|
||||
3. 设置私有只有自己可以访问。\n
|
||||
受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。 不设置,默认是只能自己访问。`,
|
||||
enTips: `1. Set public to directly access
|
||||
2. Set protected to access after login
|
||||
3. Set private to access only yourself.
|
||||
Protected can set a password and set the username for access. After switching the shared state, you need to reset the password and username. If not set, it defaults to only being accessible to yourself.`,
|
||||
},
|
||||
{
|
||||
key: 'content-type',
|
||||
tips: `内容类型,设置文件的内容类型。默认不要修改。`,
|
||||
enTips: `Content type, set the content type of the file. Default do not modify.`,
|
||||
},
|
||||
{
|
||||
key: 'app-source',
|
||||
tips: `应用来源,上传方式。默认不要修改。`,
|
||||
enTips: `App source, upload method. Default do not modify.`,
|
||||
},
|
||||
{
|
||||
key: 'cache-control',
|
||||
tips: `缓存控制,设置文件的缓存控制。默认不要修改。`,
|
||||
enTips: `Cache control, set the cache control of the file. Default do not modify.`,
|
||||
},
|
||||
{
|
||||
key: 'password',
|
||||
tips: `密码,设置文件的密码。不设置默认是所有人都可以访问。`,
|
||||
enTips: `Password, set the password of the file. If not set, it defaults to everyone can access.`,
|
||||
},
|
||||
{
|
||||
key: 'usernames',
|
||||
tips: `用户名,设置文件的用户名。不设置默认是所有人都可以访问。`,
|
||||
enTips: `Username, set the username of the file. If not set, it defaults to everyone can access.`,
|
||||
parse: (value: string) => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
return value.split(',');
|
||||
},
|
||||
stringify: (value: string[]) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.join(',');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'expiration-time',
|
||||
tips: `过期时间,设置文件的过期时间。不设置默认是永久。`,
|
||||
enTips: `Expiration time, set the expiration time of the file. If not set, it defaults to permanent.`,
|
||||
parse: (value: Date) => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return dayjs(value);
|
||||
},
|
||||
stringify: (value?: dayjs.Dayjs) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
return value.toISOString();
|
||||
},
|
||||
},
|
||||
];
|
||||
export class KeyParse {
|
||||
static parse(metadata: Record<string, any>) {
|
||||
const keys = Object.keys(metadata);
|
||||
const newMetadata = {};
|
||||
keys.forEach((key) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip && tip.parse) {
|
||||
newMetadata[key] = tip.parse(metadata[key]);
|
||||
} else {
|
||||
newMetadata[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newMetadata;
|
||||
}
|
||||
static stringify(metadata: Record<string, any>) {
|
||||
const keys = Object.keys(metadata);
|
||||
const newMetadata = {};
|
||||
keys.forEach((key) => {
|
||||
const tip = keysTips.find((item) => item.key === key);
|
||||
if (tip && tip.stringify) {
|
||||
newMetadata[key] = tip.stringify(metadata[key]);
|
||||
} else {
|
||||
newMetadata[key] = metadata[key];
|
||||
}
|
||||
});
|
||||
return newMetadata;
|
||||
}
|
||||
}
|
||||
494
src/app/apps/page.tsx
Normal file
494
src/app/apps/page.tsx
Normal file
@@ -0,0 +1,494 @@
|
||||
'use client';
|
||||
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useAppVersionStore, useUserAppStore } from './store';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
// import { useModal } from '@kevisual/components/modal/Confirm.tsx';
|
||||
|
||||
import { Plus, Code, Link as LinkIcon, Edit, Trash2, Share2, RefreshCcw, ExternalLink, Folder } from 'lucide-react';
|
||||
import { isObjectNull } from '@/modules/is-null';
|
||||
import clsx from 'clsx';
|
||||
// import { IconButton } from '@kevisual/components/button/index.tsx';
|
||||
// import { Select } from '@kevisual/components/select/index.tsx';
|
||||
import { iText } from './constants';
|
||||
import { PermissionManager } from './modules/PermissionManager';
|
||||
import { toast as message } from 'sonner';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { pick } from 'es-toolkit';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { useLayoutStore } from '@/modules/layout/store';
|
||||
import { useAppDeleteModalStore, AppDeleteModal } from './modules/AppDeleteModal';
|
||||
import { AppWindow, Folder as FolderIcon } from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { AIEditorLink } from './app/AIEditorLink';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { LayoutMain } from '@/modules/layout';
|
||||
import { openLink } from '@/modules/basename';
|
||||
export const IconButton = (props: any) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-slate-100 disabled:opacity-50 disabled:pointer-events-none',
|
||||
props.className,
|
||||
)}
|
||||
{...props}>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
const FormModal = () => {
|
||||
const defaultValues = {
|
||||
id: '',
|
||||
title: '',
|
||||
domain: '',
|
||||
key: '',
|
||||
description: '',
|
||||
proxy: true,
|
||||
status: 'running',
|
||||
};
|
||||
const { control, handleSubmit, reset } = useForm({
|
||||
defaultValues,
|
||||
});
|
||||
const containerStore = useUserAppStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
showEdit: state.showEdit,
|
||||
setShowEdit: state.setShowEdit,
|
||||
userApp: state.userApp,
|
||||
updateData: state.updateData,
|
||||
};
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
const open = containerStore.showEdit;
|
||||
if (open) {
|
||||
const isNull = isObjectNull(containerStore.userApp);
|
||||
if (isNull) {
|
||||
reset(defaultValues);
|
||||
} else {
|
||||
reset(containerStore.userApp);
|
||||
}
|
||||
}
|
||||
}, [containerStore.showEdit, containerStore.userApp]);
|
||||
const onFinish = async (values: any) => {
|
||||
const pickValues = pick(values, ['id', 'title', 'domain', 'key', 'description', 'proxy', 'status']);
|
||||
containerStore.updateData(pickValues);
|
||||
};
|
||||
const onClose = () => {
|
||||
containerStore.setShowEdit(false);
|
||||
reset();
|
||||
};
|
||||
const isEdit = containerStore?.userApp?.id;
|
||||
const isAdmin = useLayoutStore(useShallow((state) => state.isAdmin));
|
||||
|
||||
return (
|
||||
<Dialog open={containerStore.showEdit} onOpenChange={(open) => containerStore.setShowEdit(open)}>
|
||||
<DialogContent className='w-[1000px]'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? '编辑' : '添加'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className='flex flex-col gap-4 pt-2' onSubmit={handleSubmit(onFinish)}>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='title'>标题</Label>
|
||||
<Controller name='title' control={control} render={({ field }) => <Input id='title' {...field} />} />
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='key'>关键字</Label>
|
||||
<Controller name='key' control={control} render={({ field }) => <Input id='key' {...field} />} />
|
||||
</div>
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='description'>描述</Label>
|
||||
<Controller
|
||||
name='description'
|
||||
control={control}
|
||||
render={({ field }) => <Textarea id='description' {...field} rows={4} />}
|
||||
/>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='proxy'>代理</Label>
|
||||
<Controller name='proxy' control={control} render={({ field }) => <Switch id='proxy' checked={field.value} onCheckedChange={field.onChange} />} />
|
||||
</div>
|
||||
)}
|
||||
<div className='grid gap-2'>
|
||||
<Label htmlFor='status'>状态</Label>
|
||||
<Controller
|
||||
name='status'
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='选择状态' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='running'>运行中</SelectItem>
|
||||
<SelectItem value='stop'>已停止</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex gap-2'>
|
||||
<Button type='submit'>提交</Button>
|
||||
<Button variant='outline' type='reset' onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
const ShareModal = () => {
|
||||
const [permission, setPermission] = useState<any>(null);
|
||||
const [runtime, setRuntime] = useState<string[]>([]);
|
||||
const containerStore = useUserAppStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
showEdit: state.showShareEdit,
|
||||
setShowEdit: state.setShowShareEdit,
|
||||
updateData: state.updateData,
|
||||
userApp: state.userApp,
|
||||
};
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
const open = containerStore.showEdit;
|
||||
if (open) {
|
||||
const permission = containerStore.userApp?.data?.permission || {};
|
||||
const runtime = containerStore.userApp?.data?.runtime || [];
|
||||
if (isObjectNull(permission)) {
|
||||
setPermission({ share: 'private' });
|
||||
} else {
|
||||
setPermission(permission);
|
||||
}
|
||||
setRuntime(runtime);
|
||||
}
|
||||
}, [containerStore.showEdit, containerStore.userApp]);
|
||||
const onFinish = async () => {
|
||||
const values = {
|
||||
id: containerStore.userApp.id,
|
||||
data: {
|
||||
permission,
|
||||
runtime,
|
||||
},
|
||||
};
|
||||
containerStore.updateData(values);
|
||||
};
|
||||
const onClose = () => {
|
||||
containerStore.setShowEdit(false);
|
||||
};
|
||||
const isAdmin = useLayoutStore(useShallow((state) => state.isAdmin));
|
||||
return (
|
||||
<Dialog open={containerStore.showEdit} onOpenChange={(open) => containerStore.setShowEdit(open)}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>分享</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className='flex flex-col gap-2 w-[400px] '>
|
||||
<PermissionManager
|
||||
value={permission}
|
||||
onChange={(value) => {
|
||||
setPermission(value);
|
||||
}}
|
||||
/>
|
||||
{isAdmin && (
|
||||
<div className='grid gap-2'>
|
||||
<Label>运行时</Label>
|
||||
<Select
|
||||
value={runtime[0] || ''}
|
||||
onValueChange={(val: string) => {
|
||||
setRuntime((prev) => (prev.includes(val) ? prev.filter((v) => v !== val) : [...prev, val]));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder='选择运行时' />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='node'>Node.js</SelectItem>
|
||||
<SelectItem value='browser'>浏览器</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className='flex gap-1 flex-wrap'>
|
||||
{runtime.map((r) => (
|
||||
<span key={r} className='bg-slate-200 px-2 py-1 rounded text-xs'>
|
||||
{r}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type='submit' onClick={() => { onFinish() }}>提交</Button>
|
||||
<Button variant='outline' type='reset' onClick={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
export const List = () => {
|
||||
// const [modal, contextHolder] = useModal();
|
||||
const userAppStore = useUserAppStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
list: state.list,
|
||||
getList: state.getList,
|
||||
setShowEdit: state.setShowEdit,
|
||||
formData: state.formData,
|
||||
setFormData: state.setFormData,
|
||||
deleteData: state.deleteData,
|
||||
setShowShareEdit: state.setShowShareEdit,
|
||||
getUserApp: state.getUserApp,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const appVersionStore = useAppVersionStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
publishVersion: state.publishVersion,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const appDeleteModalStore = useAppDeleteModalStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
onClickDelete: state.onClickDelete,
|
||||
};
|
||||
}),
|
||||
);
|
||||
useEffect(() => {
|
||||
userAppStore.getList();
|
||||
}, []);
|
||||
return (
|
||||
<div className='w-full h-full flex bg-slate-100'>
|
||||
<div className='p-2 h-full bg-white flex flex-col gap-2'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<IconButton
|
||||
sx={{
|
||||
padding: '8px',
|
||||
}}
|
||||
onClick={() => {
|
||||
userAppStore.setFormData({});
|
||||
userAppStore.setShowEdit(true);
|
||||
}}>
|
||||
<Plus className='h-4 w-4' />
|
||||
</IconButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>添加一个应用</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<IconButton
|
||||
sx={{
|
||||
padding: '8px',
|
||||
}}
|
||||
onClick={() => {
|
||||
openLink('/domain/', '_self');
|
||||
}}>
|
||||
<LinkIcon className='h-4 w-4' />
|
||||
</IconButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>域名自定义绑定</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className='grow'>
|
||||
<div className='w-full h-full p-4'>
|
||||
<div className='w-full h-full bg-white rounded-lg p-2 scrollbar '>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
{userAppStore.list.map((item) => {
|
||||
const isRunning = item.status === 'running';
|
||||
const hasDescription = !!item.description;
|
||||
// const content = marked.parse(item.description);
|
||||
const content = item.description;
|
||||
return (
|
||||
<div className='w-[300px] bg-white rounded-lg border border-slate-200 shadow-sm p-4 relative' key={item.id}>
|
||||
<div className='flex font-bold justify-between mb-3' onClick={() => { }}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
{item.title} <i className='text-xs text-gray-400'>{item.key}</i>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent><pre className=''>
|
||||
<span className='text-sm'>{item.title}</span>
|
||||
<i className='text-xs text-white ml-4'>{item.key}</i>
|
||||
</pre></TooltipContent>
|
||||
</Tooltip>
|
||||
<div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className={`${isRunning ? 'bg-green-500' : 'bg-red-500'} w-4 h-4 rounded-full`}></div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{isRunning ? '网页可正常访问' : '网页被关闭'}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-2 mb-16'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className='text-xs cursor-copy'
|
||||
onClick={() => {
|
||||
copy(item.id);
|
||||
message.success('复制成功');
|
||||
}}>
|
||||
{item.id}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>复制App ID到剪贴板</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className='text-xs text-gray-500'>
|
||||
{item.version}
|
||||
</div>
|
||||
|
||||
<div className={clsx('text-sm border border-slate-200 rounded p-2 max-h-[140px] overflow-auto my-1 scrollbar', !hasDescription && 'hidden')}>
|
||||
{/* <div dangerouslySetInnerHTML={{ __html: content }}></div> */}
|
||||
<div className='text-sm whitespace-pre-wrap'>{content}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-4 pt-3 border-t border-slate-100 flex gap-1 absolute bottom-0 left-0 right-0 px-4 pb-4 bg-white rounded-b-lg'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
userAppStore.getUserApp(item.id);
|
||||
userAppStore.setFormData(item);
|
||||
userAppStore.setShowEdit(true);
|
||||
}}>
|
||||
<Edit className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>编辑</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
const url = `/apps/app?appKey=${item.key}`;
|
||||
openLink(url, '_self');
|
||||
}}
|
||||
|
||||
>
|
||||
<AppWindow className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>版本列表</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
userAppStore.getUserApp(item.id);
|
||||
userAppStore.setFormData(item);
|
||||
userAppStore.setShowShareEdit(true);
|
||||
}}>
|
||||
<Share2 className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="whitespace-pre-wrap">{iText.share.tips}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
appVersionStore.publishVersion({ appKey: item.key, version: item.version }, { showToast: true });
|
||||
}}>
|
||||
<RefreshCcw className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>重新加载</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={() => {
|
||||
if (isRunning) {
|
||||
let baseUri = typeof window !== 'undefined' ? window.location.origin : '';
|
||||
if (item.domain) {
|
||||
if (item.domain.startsWith('http://') || item.domain.startsWith('https://')) {
|
||||
baseUri = item.domain;
|
||||
} else if (item.domain.startsWith('//')) {
|
||||
baseUri = new URL(item.domain).origin;
|
||||
} else {
|
||||
baseUri = new URL('https://' + item.domain).toString();
|
||||
}
|
||||
if (baseUri.endsWith('/')) {
|
||||
openLink(baseUri, '_blank');
|
||||
}
|
||||
console.log('baseUri', baseUri);
|
||||
message.success('success');
|
||||
return;
|
||||
}
|
||||
const link = new URL(`/${item.user}/${item.key}/`, baseUri);
|
||||
openLink(link.toString(), '_blank');
|
||||
} else {
|
||||
message.error('应用未运行');
|
||||
}
|
||||
}}>
|
||||
<ExternalLink className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>打开应用</TooltipContent>
|
||||
</Tooltip>
|
||||
<AIEditorLink pathname={item.key} />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
onClick={(e) => {
|
||||
appDeleteModalStore.onClickDelete('user-app', item);
|
||||
e.stopPropagation();
|
||||
}}>
|
||||
<Trash2 className='h-4 w-4' />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>删除</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
{/* {contextHolder} */}
|
||||
< FormModal />
|
||||
<ShareModal />
|
||||
<AppDeleteModal />
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return <LayoutMain>
|
||||
<List />
|
||||
</LayoutMain>
|
||||
};
|
||||
152
src/app/apps/store/app-version.ts
Normal file
152
src/app/apps/store/app-version.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
'use client';
|
||||
import { isObjectNull } from '@/modules/is-null';
|
||||
import { create } from 'zustand';
|
||||
import { query } from '@/modules/query';
|
||||
import { toast as message } from 'sonner'
|
||||
type AppVersionStore = {
|
||||
showEdit: boolean;
|
||||
setShowEdit: (showEdit: boolean) => void;
|
||||
formData: any;
|
||||
setFormData: (formData: any) => void;
|
||||
updateByFromData: () => void;
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
key: string;
|
||||
setKey: (key: string) => void;
|
||||
list: any[];
|
||||
getList: () => Promise<void>;
|
||||
app: any;
|
||||
getApp: (key: string, force?: boolean) => Promise<void>;
|
||||
updateData: (data: any) => Promise<void>;
|
||||
/**
|
||||
* 删除应用版本
|
||||
* @param id 应用版本id
|
||||
* @param deleteFile 是否删除文件
|
||||
* @returns
|
||||
*/
|
||||
deleteData: (id: string, deleteFile?: boolean) => Promise<void>;
|
||||
publishVersion: (data: { id?: string; appKey?: string; version?: string }, opts?: { showToast?: boolean }) => Promise<any>;
|
||||
detectVersionList: (data: { appKey: string; version: string }) => Promise<any>;
|
||||
};
|
||||
export const useAppVersionStore = create<AppVersionStore>((set, get) => {
|
||||
return {
|
||||
showEdit: false,
|
||||
setShowEdit: (showEdit) => set({ showEdit }),
|
||||
formData: {},
|
||||
setFormData: (formData) => set({ formData }),
|
||||
updateByFromData: () => {
|
||||
const { formData, list } = get();
|
||||
const data = list.map((item) => {
|
||||
if (item.id === formData.id) {
|
||||
return formData;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
set({ list: data });
|
||||
},
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
key: '',
|
||||
setKey: (key) => set({ key }),
|
||||
list: [],
|
||||
getList: async () => {
|
||||
set({ loading: true });
|
||||
const key = get().key;
|
||||
|
||||
const res = await query.post({
|
||||
path: 'app',
|
||||
key: 'list',
|
||||
data: {
|
||||
key,
|
||||
},
|
||||
});
|
||||
get().getApp(key, true);
|
||||
set({ loading: false });
|
||||
if (res.code === 200) {
|
||||
set({ list: res.data });
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
app: {},
|
||||
getApp: async (key, force) => {
|
||||
const { app } = get();
|
||||
if (!force && !isObjectNull(app)) {
|
||||
return;
|
||||
}
|
||||
const res = await query.post({
|
||||
path: 'user-app',
|
||||
key: 'get',
|
||||
data: {
|
||||
key,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
set({ app: res.data });
|
||||
} else {
|
||||
message.error(res.message || '请求失败');
|
||||
}
|
||||
},
|
||||
updateData: async (data) => {
|
||||
const { getList } = get();
|
||||
const res = await query.post({
|
||||
path: 'app',
|
||||
key: 'update',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
set({ showEdit: false, formData: res.data });
|
||||
getList();
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
deleteData: async (id, deleteFile = false) => {
|
||||
const { getList } = get();
|
||||
const res = await query.post({
|
||||
path: 'app',
|
||||
key: 'delete',
|
||||
payload: {
|
||||
id,
|
||||
deleteFile,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
getList();
|
||||
message.success('Success');
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
publishVersion: async (data, opts) => {
|
||||
const showToast = opts?.showToast ?? true;
|
||||
const res = await query.post({
|
||||
path: 'app',
|
||||
key: 'publish',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
if (showToast) {
|
||||
message.success('发布成功');
|
||||
if (get().key) {
|
||||
get().getApp(get().key, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (showToast) {
|
||||
message.error(res.message || '请求失败');
|
||||
}
|
||||
}
|
||||
return res;
|
||||
},
|
||||
detectVersionList: async (data) => {
|
||||
const res = await query.post({
|
||||
path: 'app',
|
||||
key: 'detectVersionList',
|
||||
data,
|
||||
});
|
||||
return res;
|
||||
},
|
||||
};
|
||||
});
|
||||
2
src/app/apps/store/index.ts
Normal file
2
src/app/apps/store/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './user-app';
|
||||
export * from './app-version';
|
||||
101
src/app/apps/store/user-app.ts
Normal file
101
src/app/apps/store/user-app.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
import { create } from 'zustand';
|
||||
import { query } from '@/modules/query';
|
||||
import { toast as message } from 'sonner'
|
||||
type UserAppStore = {
|
||||
showEdit: boolean;
|
||||
setShowEdit: (showEdit: boolean) => void;
|
||||
formData: any;
|
||||
setFormData: (formData: any) => void;
|
||||
loading: boolean;
|
||||
setLoading: (loading: boolean) => void;
|
||||
list: any[];
|
||||
getList: () => Promise<void>;
|
||||
updateData: (data: any) => Promise<void>;
|
||||
/**
|
||||
* 删除用户应用
|
||||
* @param id 用户应用id
|
||||
* @param deleteFile 是否删除文件
|
||||
* @returns
|
||||
*/
|
||||
deleteData: (id: string, deleteFile?: boolean) => Promise<void>;
|
||||
showShareEdit: boolean;
|
||||
setShowShareEdit: (showShareEdit: boolean) => void;
|
||||
userApp: any;
|
||||
setUserApp: (userApp: any) => void;
|
||||
getUserApp: (id: string) => Promise<void>;
|
||||
};
|
||||
export const useUserAppStore = create<UserAppStore>((set, get) => {
|
||||
return {
|
||||
showEdit: false,
|
||||
setShowEdit: (showEdit) => set({ showEdit }),
|
||||
formData: {},
|
||||
setFormData: (formData) => set({ formData }),
|
||||
loading: false,
|
||||
setLoading: (loading) => set({ loading }),
|
||||
list: [],
|
||||
getList: async () => {
|
||||
set({ loading: true });
|
||||
|
||||
const res = await query.post({
|
||||
path: 'user-app',
|
||||
key: 'list',
|
||||
});
|
||||
set({ loading: false });
|
||||
if (res.code === 200) {
|
||||
set({ list: res.data });
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
updateData: async (data) => {
|
||||
const { getList } = get();
|
||||
const res = await query.post({
|
||||
path: 'user-app',
|
||||
key: 'update',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
message.success('Success');
|
||||
set({ showEdit: false, showShareEdit: false, formData: res.data });
|
||||
getList();
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
deleteData: async (id, deleteFile = false) => {
|
||||
const { getList } = get();
|
||||
const res = await query.post({
|
||||
path: 'user-app',
|
||||
key: 'delete',
|
||||
payload: {
|
||||
id,
|
||||
deleteFile,
|
||||
},
|
||||
});
|
||||
if (res.code === 200) {
|
||||
getList();
|
||||
message.success('Success');
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
showShareEdit: false,
|
||||
setShowShareEdit: (showShareEdit) => set({ showShareEdit }),
|
||||
userApp: {},
|
||||
setUserApp: (userApp) => set({ userApp }),
|
||||
getUserApp: async (id) => {
|
||||
set({ userApp: null });
|
||||
const res = await query.post({
|
||||
path: 'user-app',
|
||||
key: 'get',
|
||||
payload: { id }
|
||||
});
|
||||
if (res.code === 200) {
|
||||
set({ userApp: res.data });
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
205
src/app/domain/page.tsx
Normal file
205
src/app/domain/page.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
'use client';
|
||||
import { useEffect } from 'react';
|
||||
import { appDomainStatus, useDomainStore } from './store/index ';
|
||||
import { useForm, Controller } from 'react-hook-form';
|
||||
import { pick } from 'es-toolkit';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Plus, Pencil, Trash2 } from 'lucide-react';
|
||||
import { LayoutUser } from '@/modules/layout/LayoutUser';
|
||||
import { LayoutMain } from '@/modules/layout';
|
||||
|
||||
const TableList = () => {
|
||||
const { list, setShowEditModal, setFormData, deleteDomain } = useDomainStore();
|
||||
useEffect(() => {
|
||||
// Initial load is handled by the parent component
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>域名</TableHead>
|
||||
<TableHead>应用ID</TableHead>
|
||||
<TableHead>UID</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{list.map((domain) => (
|
||||
<TableRow key={domain.id}>
|
||||
<TableCell>{domain.id}</TableCell>
|
||||
<TableCell>{domain.domain}</TableCell>
|
||||
<TableCell>{domain.appId}</TableCell>
|
||||
<TableCell>{domain.uid}</TableCell>
|
||||
<TableCell>{domain.status}</TableCell>
|
||||
<TableCell className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setShowEditModal(true);
|
||||
setFormData(domain);
|
||||
}}>
|
||||
<Pencil className="w-4 h-4 mr-1" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => deleteDomain(domain)}>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
删除
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FomeModal = () => {
|
||||
const { showEditModal, setShowEditModal, formData, updateDomain } = useDomainStore();
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
control,
|
||||
setValue,
|
||||
} = useForm();
|
||||
|
||||
useEffect(() => {
|
||||
if (!showEditModal) return;
|
||||
if (formData?.id) {
|
||||
reset(formData);
|
||||
} else {
|
||||
reset({
|
||||
status: 'running',
|
||||
});
|
||||
}
|
||||
}, [formData, showEditModal, reset]);
|
||||
|
||||
const onSubmit = async (data: any) => {
|
||||
const _formData = pick(data, ['domain', 'appId', 'status', 'id']);
|
||||
if (formData.id) {
|
||||
_formData.id = formData.id;
|
||||
}
|
||||
const res = await updateDomain(_formData);
|
||||
if (res.code === 200) {
|
||||
setShowEditModal(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={showEditModal} onOpenChange={setShowEditModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加域名</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4 w-[500px]">
|
||||
<form className="w-full flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">域名</label>
|
||||
<Input
|
||||
{...control.register('domain', { required: '请输入域名' })}
|
||||
placeholder="请输入域名"
|
||||
className={errors.domain ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.domain && <span className="text-xs text-red-500">{errors.domain.message as string}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">应用ID</label>
|
||||
<Input
|
||||
{...control.register('appId', { required: '请输入应用ID' })}
|
||||
placeholder="请输入应用ID"
|
||||
className={errors.appId ? "border-red-500" : ""}
|
||||
/>
|
||||
{errors.appId && <span className="text-xs text-red-500">{errors.appId.message as string}</span>}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium">状态</label>
|
||||
<Controller
|
||||
name="status"
|
||||
control={control}
|
||||
defaultValue=""
|
||||
render={({ field }) => (
|
||||
<Select value={field.value || ''} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择状态" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{appDomainStatus.map((item) => (
|
||||
<SelectItem key={item} value={item}>{item}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">提交</Button>
|
||||
</form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const List = () => {
|
||||
const { getDomainList, setShowEditModal, setFormData } = useDomainStore();
|
||||
|
||||
useEffect(() => {
|
||||
getDomainList();
|
||||
}, [getDomainList]);
|
||||
|
||||
return (
|
||||
<div className="p-4 w-full h-full">
|
||||
<div className="flex mb-4">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowEditModal(true);
|
||||
setFormData({});
|
||||
}}>
|
||||
<Plus className="w-4 h-4 mr-1" />
|
||||
添加
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
</Dialog>
|
||||
</div>
|
||||
<TableList />
|
||||
<FomeModal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return <LayoutMain><List /></LayoutMain>;
|
||||
}
|
||||
92
src/app/domain/store/index .ts
Normal file
92
src/app/domain/store/index .ts
Normal file
@@ -0,0 +1,92 @@
|
||||
'use strict';
|
||||
import { create } from 'zustand';
|
||||
import { query } from '@/modules/query';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// 审核,通过,驳回
|
||||
export const appDomainStatus = ['audit', 'auditReject', 'auditPending', 'running', 'stop'] as const;
|
||||
|
||||
type AppDomainStatus = (typeof appDomainStatus)[number];
|
||||
type Domain = {
|
||||
id: string;
|
||||
domain: string;
|
||||
appId?: string;
|
||||
status: AppDomainStatus;
|
||||
data?: any;
|
||||
uid?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
interface Store {
|
||||
getDomainList: () => Promise<any>;
|
||||
updateDomain: (data: { domain: string; id: string; [key: string]: any }, opts?: { refresh?: boolean }) => Promise<any>;
|
||||
deleteDomain: (data: { id: string }) => Promise<any>;
|
||||
getDomainDetail: (data: { domain?: string; id?: string }) => Promise<any>;
|
||||
list: Domain[];
|
||||
setList: (list: Domain[]) => void;
|
||||
formData: any;
|
||||
setFormData: (formData: any) => void;
|
||||
showEditModal: boolean;
|
||||
setShowEditModal: (showEditModal: boolean) => void;
|
||||
}
|
||||
|
||||
export const useDomainStore = create<Store>((set, get) => ({
|
||||
getDomainList: async () => {
|
||||
const res = await query.get({
|
||||
path: 'app.domain.manager',
|
||||
key: 'list',
|
||||
});
|
||||
if (res.code === 200) {
|
||||
set({ list: res.data?.list || [] });
|
||||
}
|
||||
return res;
|
||||
},
|
||||
updateDomain: async (data: any, opts?: { refresh?: boolean }) => {
|
||||
const res = await query.post({
|
||||
path: 'app.domain.manager',
|
||||
key: 'update',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
const list = get().list;
|
||||
set({ list: list.map((item) => (item.id === data.id ? res.data : item)) });
|
||||
toast.success('更新成功');
|
||||
if (opts?.refresh ?? true) {
|
||||
get().getDomainList();
|
||||
}
|
||||
} else {
|
||||
toast.error(res.message || '更新失败');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
deleteDomain: async (data: any) => {
|
||||
const res = await query.post({
|
||||
path: 'app.domain.manager',
|
||||
key: 'delete',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
const list = get().list;
|
||||
set({ list: list.filter((item) => item.id !== data.id) });
|
||||
toast.success('删除成功');
|
||||
}
|
||||
return res;
|
||||
},
|
||||
getDomainDetail: async (data: any) => {
|
||||
const res = await query.post({
|
||||
path: 'app.domain.manager',
|
||||
key: 'get',
|
||||
data,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
set({ formData: res.data });
|
||||
}
|
||||
return res;
|
||||
},
|
||||
list: [],
|
||||
setList: (list: any[]) => set({ list }),
|
||||
formData: {},
|
||||
setFormData: (formData: any) => set({ formData }),
|
||||
showEditModal: false,
|
||||
setShowEditModal: (showEditModal: boolean) => set({ showEditModal }),
|
||||
}));
|
||||
@@ -123,3 +123,42 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
html,body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@utility scrollbar {
|
||||
overflow: auto;
|
||||
/* 整个滚动条 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: var(--color-scrollbar-track);
|
||||
}
|
||||
/* 滚动条有滑块的轨道部分 */
|
||||
&::-webkit-scrollbar-track-piece {
|
||||
background-color: transparent;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
/* 滚动条滑块(竖向:vertical 横向:horizontal) */
|
||||
&::-webkit-scrollbar-thumb {
|
||||
cursor: pointer;
|
||||
background-color: var(--color-scrollbar-thumb);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* 滚动条滑块hover */
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--color-scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
/* 同时有垂直和水平滚动条时交汇的部分 */
|
||||
&::-webkit-scrollbar-corner {
|
||||
display: block; /* 修复交汇时出现的白块 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
'use client';
|
||||
import { LayoutMain } from "@/modules/layout";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
Light Code
|
||||
</main>
|
||||
<LayoutMain>
|
||||
<iframe src="/root/router-studio" className="w-full border-0" style={{
|
||||
height: 'calc(100vh - 48px)'
|
||||
}}/>
|
||||
</LayoutMain>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BIN
src/assets/panda.jpg
Normal file
BIN
src/assets/panda.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
BIN
src/assets/qrcode-8x8.jpg
Normal file
BIN
src/assets/qrcode-8x8.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
220
src/components/ui/calendar.tsx
Normal file
220
src/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
DayPicker,
|
||||
getDefaultClassNames,
|
||||
type DayButton,
|
||||
} from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"flex gap-4 flex-col md:flex-row relative",
|
||||
defaultClassNames.months
|
||||
),
|
||||
month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute bg-popover inset-0 opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
caption_label: cn(
|
||||
"select-none font-medium",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("flex w-full mt-2", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"select-none w-(--cell-size)",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] select-none text-muted-foreground",
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
|
||||
props.showWeekNumber
|
||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
|
||||
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn(
|
||||
"rounded-l-md bg-accent",
|
||||
defaultClassNames.range_start
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null)
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus()
|
||||
}, [modifiers.focused])
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton }
|
||||
31
src/components/ui/switch.tsx
Normal file
31
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SwitchPrimitive.Root>) {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
data-slot="switch"
|
||||
className={cn(
|
||||
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
data-slot="switch-thumb"
|
||||
className={cn(
|
||||
"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export { Switch }
|
||||
99
src/components/ui/table.tsx
Normal file
99
src/components/ui/table.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className="text-muted-foreground mt-4 text-sm"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
19
src/components/ui/textarea.tsx
Normal file
19
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -1,8 +1,8 @@
|
||||
const isDev = process.env.NODE_ENV === "development";
|
||||
const BASE_NAME = isDev ? '' : '/root/perler-beads';
|
||||
export const isDev = process.env.NODE_ENV === "development";
|
||||
|
||||
const BASE_NAME = isDev ? '' : '/root/center';
|
||||
|
||||
export const basename = BASE_NAME;
|
||||
|
||||
export const wrapBasename = (path: string) => {
|
||||
const hasEnd = path.endsWith('/')
|
||||
let _basename = basename;
|
||||
@@ -14,5 +14,13 @@ export const wrapBasename = (path: string) => {
|
||||
if (isDev) {
|
||||
return _basename
|
||||
}
|
||||
return _basename + '.html';
|
||||
return !hasEnd ? _basename + '/' : _basename;
|
||||
}
|
||||
export const openLink = (path: string, target: string = '_self') => {
|
||||
if (path.startsWith('http://') || path.startsWith('https://')) {
|
||||
window.open(path, target);
|
||||
return;
|
||||
}
|
||||
const url = wrapBasename(path);
|
||||
window.open(url, target);
|
||||
}
|
||||
9
src/modules/is-null.ts
Normal file
9
src/modules/is-null.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const isObjectNull = (value: any) => {
|
||||
if (value === null || value === undefined) {
|
||||
return true;
|
||||
}
|
||||
if (JSON.stringify(value) === '{}') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
178
src/modules/layout/LayoutUser.tsx
Normal file
178
src/modules/layout/LayoutUser.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
'use strict';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useLayoutStore } from './store';
|
||||
import clsx from 'clsx';
|
||||
import { toast as message } from 'sonner';
|
||||
import { useMemo } from 'react';
|
||||
import { queryLogin } from '../query';
|
||||
import { LogOut, Map, SquareUser, Users, X, ArrowDownLeftFromSquareIcon } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { openLink } from '../basename';
|
||||
|
||||
export const LayoutUser = () => {
|
||||
const { open, setOpen, isAdmin, ...store } = useLayoutStore(
|
||||
useShallow((state) => ({
|
||||
open: state.openUser,
|
||||
setOpen: state.setOpenUser,
|
||||
me: state.me,
|
||||
switchOrg: state.switchOrg,
|
||||
isAdmin: state.isAdmin,
|
||||
})),
|
||||
);
|
||||
const items = useMemo(() => {
|
||||
const orgs = store.me?.orgs || [];
|
||||
return orgs.map((item) => {
|
||||
return {
|
||||
label: item,
|
||||
key: item,
|
||||
icon: <Users size={16} />,
|
||||
};
|
||||
});
|
||||
}, [store.me]);
|
||||
const menu = useMemo(() => {
|
||||
const orgs = store.me?.orgs || [];
|
||||
const hasOrg = orgs.length > 0;
|
||||
type MenuItem = {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
link?: string;
|
||||
isOrg?: boolean;
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
const menuItems: MenuItem[] = [
|
||||
// {
|
||||
// title: '个人中心',
|
||||
// icon: <SquareUser size={16} />,
|
||||
// link: '/user/profile',
|
||||
// },
|
||||
// {
|
||||
// title: '我的组织',
|
||||
// icon: <Users size={16} />,
|
||||
// link: '/org/edit/list',
|
||||
// isOrg: true,
|
||||
// },
|
||||
// {
|
||||
// title: '站点地图',
|
||||
// icon: <Map size={16} />,
|
||||
// link: '/map',
|
||||
// },
|
||||
{
|
||||
title: '域名管理',
|
||||
icon: <ArrowDownLeftFromSquareIcon size={16} />,
|
||||
link: '/domain/',
|
||||
isAdmin: true,
|
||||
},
|
||||
];
|
||||
return menuItems.filter((item) => {
|
||||
if (item.isOrg) {
|
||||
return hasOrg;
|
||||
}
|
||||
if (item.isAdmin) {
|
||||
return isAdmin;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}, [store.me]);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<div className={clsx('w-full h-full absolute z-20 no-drag text-primary', !open && 'hidden')}>
|
||||
<div
|
||||
className='w-full absolute h-full opacity-60 z-0'
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}></div>
|
||||
<div className='w-[400px] bg-white transition-all duration-300 h-full absolute top-0 right-0 rounded-l-lg'>
|
||||
<div className='flex justify-between p-6 mt-4 font-bold items-center border-b'>
|
||||
<div className='flex items-center gap-2'>
|
||||
用户: <span className='text-primary'>{store.me?.username}</span>
|
||||
</div>
|
||||
<div className='flex gap-4'>
|
||||
{items.length > 0 && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='ghost' size='icon'>
|
||||
<Users />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{items.map((item, index) => (
|
||||
<DropdownMenuItem
|
||||
key={index}
|
||||
onClick={() => {
|
||||
store.switchOrg(item.key, 'org');
|
||||
}}>
|
||||
<div className='mr-2'>{item.icon}</div>
|
||||
<div>{item.label}</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>切换组织</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Button variant='ghost' size='icon' onClick={() => setOpen(false)}>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3 font-medium'>
|
||||
{menu.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='flex items-center p-4 hover:bg-secondary hover:text-white cursor-pointer'
|
||||
onClick={() => {
|
||||
if (item.link) {
|
||||
openLink(item.link, '_self');
|
||||
setOpen(false);
|
||||
} else {
|
||||
message.info('即将上线');
|
||||
}
|
||||
}}>
|
||||
<div className='mr-4'>{item.icon}</div>
|
||||
<div>{item.title}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center p-4 hover:bg-secondary hover:text-white cursor-pointer'
|
||||
onClick={async () => {
|
||||
const res = await queryLogin.logout();
|
||||
if (res.success) {
|
||||
const url = new URL(location.origin);
|
||||
url.pathname = '/root/login';
|
||||
openLink(url.toString(), '_self');
|
||||
} else {
|
||||
message.error(res.message || '退出失败');
|
||||
}
|
||||
}}>
|
||||
<div className='mr-4'>
|
||||
<LogOut size={16} />
|
||||
</div>
|
||||
<div>退出登录</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
104
src/modules/layout/Menu.tsx
Normal file
104
src/modules/layout/Menu.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useLayoutStore } from './store';
|
||||
import clsx from 'clsx';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { toast } from 'sonner';
|
||||
import HomeOutlined from '@ant-design/icons/HomeOutlined';
|
||||
import AppstoreOutlined from '@ant-design/icons/AppstoreOutlined';
|
||||
import FolderOutlined from '@ant-design/icons/FolderOutlined';
|
||||
import CodeOutlined from '@ant-design/icons/CodeOutlined';
|
||||
import SwitcherOutlined from '@ant-design/icons/SwitcherOutlined';
|
||||
import SmileOutlined from '@ant-design/icons/SmileOutlined';
|
||||
import { X, Settings } from 'lucide-react';
|
||||
import { Map } from 'lucide-react';
|
||||
import { openLink } from '../basename';
|
||||
|
||||
export const useQuickMenu = () => {
|
||||
return [
|
||||
{
|
||||
title: '首页',
|
||||
icon: <HomeOutlined />,
|
||||
link: '/',
|
||||
},
|
||||
{
|
||||
title: '应用',
|
||||
icon: <AppstoreOutlined />,
|
||||
link: '/apps/',
|
||||
},
|
||||
// {
|
||||
// title: '文件',
|
||||
// icon: <FolderOutlined />,
|
||||
// link: '/file/edit/list',
|
||||
// },
|
||||
];
|
||||
};
|
||||
|
||||
export const LayoutMenu = () => {
|
||||
const meun = [
|
||||
{
|
||||
title: '首页',
|
||||
icon: <HomeOutlined />,
|
||||
link: '/',
|
||||
},
|
||||
// {
|
||||
// title: '应用',
|
||||
// icon: <AppstoreOutlined />,
|
||||
// link: '/app/edit/list',
|
||||
// },
|
||||
// {
|
||||
// title: '文件',
|
||||
// icon: <FolderOutlined />,
|
||||
// link: '/file/edit/list',
|
||||
// },
|
||||
// {
|
||||
// title: '容器',
|
||||
// icon: <CodeOutlined />,
|
||||
// link: '/container/edit/list',
|
||||
// },
|
||||
// { title: '配置', icon: <Settings size={16} />, link: '/config/edit/list' },
|
||||
// { title: '地图', icon: <Map size={16} />, link: '/map' },
|
||||
// {
|
||||
// title: '关于',
|
||||
// icon: <SmileOutlined />,
|
||||
// },
|
||||
];
|
||||
const { open, setOpen } = useLayoutStore(useShallow((state) => ({ open: state.open, setOpen: state.setOpen })));
|
||||
return (
|
||||
<div className={clsx('w-full h-full text-primary absolute z-20 no-drag', !open && 'hidden')}>
|
||||
<div
|
||||
className='bg-white w-full absolute h-full opacity-60 z-0'
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}></div>
|
||||
<div className='w-[300px] h-full absolute top-0 left-0 '>
|
||||
<div className='flex justify-between p-6 mt-4 font-bold items-center'>
|
||||
Envision Center
|
||||
<div>
|
||||
<Button variant='ghost' size='icon' onClick={() => setOpen(false)}>
|
||||
<X />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-3 font-medium'>
|
||||
{meun.map((item, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className='flex items-center p-4 gap-3 cursor-pointer hover:bg-secondary hover:text-white rounded-md'
|
||||
onClick={() => {
|
||||
if (item.link) openLink(item.link, '_self');
|
||||
else {
|
||||
toast.info('关于 Envision Center');
|
||||
}
|
||||
setOpen(false);
|
||||
}}>
|
||||
<div className='w-6 h-6 flex items-center justify-center'>{item.icon}</div>
|
||||
<div>{item.title}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
147
src/modules/layout/index.tsx
Normal file
147
src/modules/layout/index.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { MenuOutlined, SwapOutlined } from '@ant-design/icons';
|
||||
import { LayoutMenu, useQuickMenu } from './Menu';
|
||||
import { useLayoutStore, usePlatformStore } from './store';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { LayoutUser } from './LayoutUser';
|
||||
import PandaPNG from '@/assets/panda.jpg';
|
||||
import QRCodePNG from '@/assets/qrcode-8x8.jpg';
|
||||
import clsx from 'clsx';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
|
||||
export const IconButton = (props: any) => {
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
'inline-flex items-center justify-center rounded-md p-2 transition-colors hover:bg-slate-100 disabled:opacity-50 disabled:pointer-events-none',
|
||||
props.className,
|
||||
)}
|
||||
{...props}>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
import { QrCode } from 'lucide-react';
|
||||
import { openLink } from '../basename';
|
||||
|
||||
type LayoutMainProps = {
|
||||
title?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export const LayoutMain = (props: LayoutMainProps) => {
|
||||
const menuStore = useLayoutStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
open: state.open,
|
||||
setOpen: state.setOpen, //
|
||||
getMe: state.getMe,
|
||||
me: state.me,
|
||||
setOpenUser: state.setOpenUser,
|
||||
switchOrg: state.switchOrg,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const platformStore = usePlatformStore(
|
||||
useShallow((state) => {
|
||||
return {
|
||||
isMac: state.isMac,
|
||||
mount: state.mount,
|
||||
isElectron: state.isElectron,
|
||||
init: state.init,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const { isMac, mount, isElectron } = platformStore;
|
||||
const quickMenu = useQuickMenu();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
platformStore.init();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
menuStore.getMe();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='flex w-full h-full flex-col relative'>
|
||||
<div
|
||||
className={clsx('layout-menu items-center ', !mount && '!invisible')}
|
||||
style={{
|
||||
cursor: isElectron ? 'move' : 'default',
|
||||
}}>
|
||||
<div className='flex grow justify-between pl-4 py-2 items-center bg-gray-200'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className='text-xl font-bold '>{props.title}</div>
|
||||
<div className='flex items-center gap-2 text-sm '>
|
||||
{quickMenu.map((item, index) => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const isActive = location?.pathname === item.link;
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className={clsx('flex items-center gap-2 px-1', isActive && 'border border-white')}
|
||||
onClick={() => {
|
||||
openLink(item.link, '_self');
|
||||
}}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className='cursor-pointer'>{item.icon}</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{item.title}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className='mr-4 flex gap-4 items-center no-drag'>
|
||||
<div className='group relative'>
|
||||
<IconButton>
|
||||
<QrCode size={16} />
|
||||
</IconButton>
|
||||
|
||||
<div className='absolute hidden group-hover:flex bg-white p-2 border shadow-md top-10 -left-15 w-40 z-[9999] flex-col items-center justify-center rounded-md'>
|
||||
<img src={QRCodePNG.src} alt='QR Code' />
|
||||
<div className='text-sm text-black'>逸闻设计</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{menuStore.me?.type === 'org' && (
|
||||
<div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
menuStore.switchOrg('', 'user');
|
||||
}}>
|
||||
<SwapOutlined />
|
||||
</IconButton>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Switch To User</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div className='w-8 h-8 rounded-full avatar cursor-pointer' onClick={() => menuStore.setOpenUser(true)}>
|
||||
{menuStore.me?.avatar ? (
|
||||
<img className='w-8 h-8 rounded-full' src={menuStore.me?.avatar} alt='avatar' />
|
||||
) : (
|
||||
<img className='w-8 h-8 rounded-full' src={PandaPNG.src} alt='avatar' />
|
||||
)}
|
||||
</div>
|
||||
<div className='cursor-pointer' onClick={() => menuStore.setOpenUser(true)}>
|
||||
{menuStore.me?.username}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='flex'
|
||||
style={{
|
||||
height: 'calc(100vh - 3rem)',
|
||||
}}>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
<LayoutUser />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
104
src/modules/layout/store/index.ts
Normal file
104
src/modules/layout/store/index.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
'use strict';
|
||||
import { query, queryLogin } from '@/modules/query';
|
||||
import { create } from 'zustand';
|
||||
import { toast as message } from 'sonner';
|
||||
export const getIsMac = async () => {
|
||||
// @ts-ignore
|
||||
const userAgentData = navigator.userAgentData;
|
||||
if (userAgentData) {
|
||||
const ua = await userAgentData.getHighEntropyValues(['platform']);
|
||||
if (ua.platform === 'macOS') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export const getIsElectron = () => {
|
||||
// 检查 window.process 和 navigator.userAgent 中是否包含 Electron 信息
|
||||
return (
|
||||
// @ts-ignore
|
||||
(typeof window !== 'undefined' && typeof window.process !== 'undefined' && window.process.type === 'renderer') ||
|
||||
(typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0)
|
||||
);
|
||||
};
|
||||
|
||||
type PlatfromStore = {
|
||||
isMac: boolean;
|
||||
setIsMac: (mac: boolean) => void;
|
||||
mount: boolean;
|
||||
isElectron: boolean;
|
||||
init: () => Promise<void>;
|
||||
};
|
||||
export const usePlatformStore = create<PlatfromStore>((set) => {
|
||||
return {
|
||||
isMac: false,
|
||||
mount: false,
|
||||
isElectron: false,
|
||||
setIsMac: (mac) => set({ isMac: mac }),
|
||||
init: async () => {
|
||||
const mac = await getIsMac();
|
||||
// @ts-ignore
|
||||
const isElectron = getIsElectron();
|
||||
set({ isMac: isElectron && mac, isElectron: isElectron, mount: true });
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Me = {
|
||||
id?: string;
|
||||
username?: string;
|
||||
needChangePassword?: boolean;
|
||||
role?: string;
|
||||
description?: string;
|
||||
type?: 'user' | 'org';
|
||||
orgs?: string[];
|
||||
avatar?: string;
|
||||
};
|
||||
export type LayoutStore = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
me: Me;
|
||||
setMe: (me: Me) => void;
|
||||
getMe: () => Promise<void>;
|
||||
openUser: boolean;
|
||||
setOpenUser: (openUser: boolean) => void;
|
||||
switchOrg: (username?: string, type?: 'user' | 'org') => Promise<void>;
|
||||
isAdmin: boolean;
|
||||
setIsAdmin: (isAdmin: boolean) => void;
|
||||
checkHasOrg: () => boolean;
|
||||
};
|
||||
export const useLayoutStore = create<LayoutStore>((set, get) => ({
|
||||
open: false,
|
||||
setOpen: (open) => set({ open }),
|
||||
me: {},
|
||||
setMe: (me) => set({ me }),
|
||||
getMe: async () => {
|
||||
const res = await queryLogin.getMe();
|
||||
if (res.code === 200) {
|
||||
set({ me: res.data });
|
||||
set({ isAdmin: res.data.orgs?.includes('admin') });
|
||||
}
|
||||
},
|
||||
openUser: false,
|
||||
setOpenUser: (openUser) => set({ openUser }),
|
||||
switchOrg: async (username?: string, type?: string) => {
|
||||
const res = await queryLogin.switchUser(username || '');
|
||||
if (res.code === 200) {
|
||||
message.success('Switch success');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
message.error(res.message || 'Request failed');
|
||||
}
|
||||
},
|
||||
isAdmin: false,
|
||||
setIsAdmin: (isAdmin) => set({ isAdmin }),
|
||||
checkHasOrg: () => {
|
||||
const user = get().me || {};
|
||||
if (!user.orgs) {
|
||||
return false;
|
||||
}
|
||||
return user?.orgs?.length > 0;
|
||||
},
|
||||
}));
|
||||
32
src/modules/query.ts
Normal file
32
src/modules/query.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
import { QueryClient } from '@kevisual/query';
|
||||
import { QueryLoginBrowser } from '@kevisual/api/login';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
// Only create instances in browser environment
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
export const query = isBrowser ? new QueryClient({}) : {} as QueryClient;
|
||||
|
||||
export const queryLogin = isBrowser
|
||||
? new QueryLoginBrowser({
|
||||
query: query as any,
|
||||
})
|
||||
: {} as QueryLoginBrowser;
|
||||
|
||||
if (isBrowser) {
|
||||
(query as any).afterResponse = async (res, ctx) => {
|
||||
const newRes = await queryLogin.run401Action(res, ctx, {
|
||||
afterAlso401: () => {},
|
||||
afterCheck: (res: any) => {
|
||||
if (res.code === 200) {
|
||||
toast.success('刷新登陆信息');
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
});
|
||||
return newRes as any;
|
||||
};
|
||||
}
|
||||
@@ -1,34 +1,20 @@
|
||||
{
|
||||
"extends": "@kevisual/types/json/next.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"strict": false,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"baseUrl": "./",
|
||||
"typeRoots": [
|
||||
"node_modules/@types"
|
||||
],
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
@@ -41,4 +27,4 @@
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user