diff --git a/.env.example b/.env.example
index bc9086d..df7acb0 100644
--- a/.env.example
+++ b/.env.example
@@ -1 +1,2 @@
-NODE_ENV=
\ No newline at end of file
+NODE_ENV=
+API_URL=https://kevisual.xiongxiao.me
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..501a050
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+
+.env
+!.env*example
+dist
+.next
+out
\ No newline at end of file
diff --git a/next.config.ts b/next.config.ts
index bd40853..c310583 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -10,6 +10,10 @@ const nextConfig: NextConfig = {
distDir: 'dist',
basePath: basePath,
trailingSlash: true,
+ transpilePackages: ['@kevisual/api'],
+ images: {
+ unoptimized: true,
+ },
};
export default nextConfig;
diff --git a/package.json b/package.json
index 55abc2a..e9b5a1b 100644
--- a/package.json
+++ b/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",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1414a97..f099063 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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: {}
diff --git a/public/panda.jpg b/public/panda.jpg
new file mode 100644
index 0000000..0f1beff
Binary files /dev/null and b/public/panda.jpg differ
diff --git a/src/app/about/page.tsx b/src/app/about/page.tsx
deleted file mode 100644
index 39c259c..0000000
--- a/src/app/about/page.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-export default function AboutPage() {
- return (
-
-
- About Light Code
-
-
- );
-}
\ No newline at end of file
diff --git a/src/app/apps/app/AIEditorLink.tsx b/src/app/apps/app/AIEditorLink.tsx
new file mode 100644
index 0000000..a759897
--- /dev/null
+++ b/src/app/apps/app/AIEditorLink.tsx
@@ -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 (
+
+
+
+
+ 打开对应的文件夹
+
+ );
+};
diff --git a/src/app/apps/app/page.tsx b/src/app/apps/app/page.tsx
new file mode 100644
index 0000000..39bb2fc
--- /dev/null
+++ b/src/app/apps/app/page.tsx
@@ -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 (
+
+ );
+};
+
+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 App Key is required
;
+ }
+ return (
+
+
+
+
+
+
+ 添加版本
+
+
+
+
+
+
+
+
+
+ 返回
+
+
+
+
+
+
+ {versionStore.list.map((item, index) => {
+ const isPublish = item.version === appVersion;
+ const color = isPublish ? 'bg-green-500' : '';
+ const isRunning = item.status === 'running';
+ return (
+
+
+
{item.version}
+
+
+
+
+
+ {isPublish ? 'published' : ''}
+
+
+
+
+
+
+
+ Delete
+
+
+
+
+
+ 使用当前版本,发布为此版本
+
+
+
+
+
+ To Test App
+
+
+
+
+
+
+ 文件管理
+
+
+
+ );
+ })}
+
+
+
+
+
+ {isUpload && (
+
+
+
+
+
+
+ 返回
+
+
{versionStore.key}
+
+
+
+ )}
+
+
+
+
+ );
+};
+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 (
+ <>
+ version: {versionStore.formData.version}
+
+
+ Files
+
+
+
+
+
+ 从同文件目录下检测已经上传的文件内容
+
+
+
+
+ {versionFiles.map((file, index) => {
+ const prefix = versionStore.formData.key + '/' + versionStore.formData.version + '/';
+ const _path = file.path || '';
+ const path = _path.replace(prefix, '');
+ return (
+
+ {/*
{file.name}
*/}
+
+
+
+
{path}
+
+ );
+ })}
+
+
+
+ >
+ );
+};
+
+export default () => {
+ return
+
+
+};
\ No newline at end of file
diff --git a/src/app/apps/constants.ts b/src/app/apps/constants.ts
new file mode 100644
index 0000000..db5722a
--- /dev/null
+++ b/src/app/apps/constants.ts
@@ -0,0 +1,11 @@
+export const iText = {
+ share: {
+ title: '共享设置',
+ tips: `共享设置
+
+ 1. 设置公共可以直接访问
+ 2. 设置受保护需要登录后访问
+ 3. 设置私有只有自己可以访问。\n
+ 受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。`,
+ },
+};
diff --git a/src/app/apps/layouts/index.tsx b/src/app/apps/layouts/index.tsx
new file mode 100644
index 0000000..9851c52
--- /dev/null
+++ b/src/app/apps/layouts/index.tsx
@@ -0,0 +1,7 @@
+'use client';
+
+import { LayoutMain } from '@/modules/layout';
+
+export const Main = () => {
+ return ;
+};
diff --git a/src/app/apps/modules/AppDeleteModal.tsx b/src/app/apps/modules/AppDeleteModal.tsx
new file mode 100644
index 0000000..baf5bda
--- /dev/null
+++ b/src/app/apps/modules/AppDeleteModal.tsx
@@ -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((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 (
+
+ );
+};
diff --git a/src/app/apps/modules/DatePicker.tsx b/src/app/apps/modules/DatePicker.tsx
new file mode 100644
index 0000000..84283bd
--- /dev/null
+++ b/src/app/apps/modules/DatePicker.tsx
@@ -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(
+ 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 (
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/apps/modules/FileUpload.tsx b/src/app/apps/modules/FileUpload.tsx
new file mode 100644
index 0000000..ef26dd0
--- /dev/null
+++ b/src/app/apps/modules/FileUpload.tsx
@@ -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(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 (
+
+
+
+
+ );
+};
diff --git a/src/app/apps/modules/PermissionManager.tsx b/src/app/apps/modules/PermissionManager.tsx
new file mode 100644
index 0000000..6e65d1b
--- /dev/null
+++ b/src/app/apps/modules/PermissionManager.tsx
@@ -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 (
+
+ );
+};
+
+export const KeyTextField = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
+ return (
+ onChange?.(e.target.value)}
+ className="w-full"
+ />
+ );
+};
+
+type PermissionManagerProps = {
+ value: Record;
+ onChange: (value: Record) => void;
+ className?: string;
+};
+
+export const PermissionManager = ({ value, onChange, className }: PermissionManagerProps) => {
+ const [formData, setFormData] = useState(value);
+ const [keys, setKeys] = useState([]);
+
+ 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 (
+
+
+
+ );
+};
diff --git a/src/app/apps/modules/TagsInput.tsx b/src/app/apps/modules/TagsInput.tsx
new file mode 100644
index 0000000..6d6aae1
--- /dev/null
+++ b/src/app/apps/modules/TagsInput.tsx
@@ -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) => {
+ 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 (
+
+ {value.map((tag, index) => (
+
+ {tag}
+
+
+ ))}
+
setInputValue(e.target.value)}
+ onKeyDown={handleKeyDown}
+ placeholder={value.length === 0 ? placeholder : ""}
+ className="flex-1 min-w-[120px] h-8"
+ />
+
+ )
+}
diff --git a/src/app/apps/modules/key-parse.ts b/src/app/apps/modules/key-parse.ts
new file mode 100644
index 0000000..cdd573c
--- /dev/null
+++ b/src/app/apps/modules/key-parse.ts
@@ -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) {
+ 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) {
+ 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;
+ }
+}
diff --git a/src/app/apps/page.tsx b/src/app/apps/page.tsx
new file mode 100644
index 0000000..67aeea7
--- /dev/null
+++ b/src/app/apps/page.tsx
@@ -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 (
+
+ );
+};
+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 (
+
+ );
+};
+const ShareModal = () => {
+ const [permission, setPermission] = useState(null);
+ const [runtime, setRuntime] = useState([]);
+ 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 (
+
+ );
+};
+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 (
+
+
+
+
+ {
+ userAppStore.setFormData({});
+ userAppStore.setShowEdit(true);
+ }}>
+
+
+
+ 添加一个应用
+
+
+
+ {
+ openLink('/domain/', '_self');
+ }}>
+
+
+
+ 域名自定义绑定
+
+
+
+
+
+
+ {userAppStore.list.map((item) => {
+ const isRunning = item.status === 'running';
+ const hasDescription = !!item.description;
+ // const content = marked.parse(item.description);
+ const content = item.description;
+ return (
+
+
{ }}>
+
+
+
+ {item.title} {item.key}
+
+
+
+ {item.title}
+ {item.key}
+
+
+
+
+
+
+
+ {isRunning ? '网页可正常访问' : '网页被关闭'}
+
+
+
+
+
+
+ {
+ copy(item.id);
+ message.success('复制成功');
+ }}>
+ {item.id}
+
+
+ 复制App ID到剪贴板
+
+
+ {item.version}
+
+
+
+
+
+
+
+
+
+ 编辑
+
+
+
+
+
+ 版本列表
+
+
+
+
+
+ {iText.share.tips}
+
+
+
+
+
+ 重新加载
+
+
+
+
+
+ 打开应用
+
+
+
+
+
+
+ 删除
+
+
+
+ );
+ })}
+
+
+
+
+ {/* {contextHolder} */}
+ < FormModal />
+
+
+
+ );
+};
+
+export default () => {
+ return
+
+
+};
diff --git a/src/app/apps/store/app-version.ts b/src/app/apps/store/app-version.ts
new file mode 100644
index 0000000..931e2b3
--- /dev/null
+++ b/src/app/apps/store/app-version.ts
@@ -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;
+ app: any;
+ getApp: (key: string, force?: boolean) => Promise;
+ updateData: (data: any) => Promise;
+ /**
+ * 删除应用版本
+ * @param id 应用版本id
+ * @param deleteFile 是否删除文件
+ * @returns
+ */
+ deleteData: (id: string, deleteFile?: boolean) => Promise;
+ publishVersion: (data: { id?: string; appKey?: string; version?: string }, opts?: { showToast?: boolean }) => Promise;
+ detectVersionList: (data: { appKey: string; version: string }) => Promise;
+};
+export const useAppVersionStore = create((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;
+ },
+ };
+});
diff --git a/src/app/apps/store/index.ts b/src/app/apps/store/index.ts
new file mode 100644
index 0000000..866e600
--- /dev/null
+++ b/src/app/apps/store/index.ts
@@ -0,0 +1,2 @@
+export * from './user-app';
+export * from './app-version';
diff --git a/src/app/apps/store/user-app.ts b/src/app/apps/store/user-app.ts
new file mode 100644
index 0000000..85d42a0
--- /dev/null
+++ b/src/app/apps/store/user-app.ts
@@ -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;
+ updateData: (data: any) => Promise;
+ /**
+ * 删除用户应用
+ * @param id 用户应用id
+ * @param deleteFile 是否删除文件
+ * @returns
+ */
+ deleteData: (id: string, deleteFile?: boolean) => Promise;
+ showShareEdit: boolean;
+ setShowShareEdit: (showShareEdit: boolean) => void;
+ userApp: any;
+ setUserApp: (userApp: any) => void;
+ getUserApp: (id: string) => Promise;
+};
+export const useUserAppStore = create((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');
+ }
+ },
+ };
+});
diff --git a/src/app/domain/page.tsx b/src/app/domain/page.tsx
new file mode 100644
index 0000000..66b13d4
--- /dev/null
+++ b/src/app/domain/page.tsx
@@ -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 (
+
+
+
+
+ ID
+ 域名
+ 应用ID
+ UID
+ 状态
+ 操作
+
+
+
+ {list.map((domain) => (
+
+ {domain.id}
+ {domain.domain}
+ {domain.appId}
+ {domain.uid}
+ {domain.status}
+
+
+
+
+
+ ))}
+
+
+
+ );
+};
+
+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 (
+
+ );
+};
+
+export const List = () => {
+ const { getDomainList, setShowEditModal, setFormData } = useDomainStore();
+
+ useEffect(() => {
+ getDomainList();
+ }, [getDomainList]);
+
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export default () => {
+ return
;
+}
\ No newline at end of file
diff --git a/src/app/domain/store/index .ts b/src/app/domain/store/index .ts
new file mode 100644
index 0000000..fd1d43c
--- /dev/null
+++ b/src/app/domain/store/index .ts
@@ -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;
+ updateDomain: (data: { domain: string; id: string; [key: string]: any }, opts?: { refresh?: boolean }) => Promise;
+ deleteDomain: (data: { id: string }) => Promise;
+ getDomainDetail: (data: { domain?: string; id?: string }) => Promise;
+ list: Domain[];
+ setList: (list: Domain[]) => void;
+ formData: any;
+ setFormData: (formData: any) => void;
+ showEditModal: boolean;
+ setShowEditModal: (showEditModal: boolean) => void;
+}
+
+export const useDomainStore = create((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 }),
+}));
diff --git a/src/app/globals.css b/src/app/globals.css
index 0bc2dea..0c4336f 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -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; /* 修复交汇时出现的白块 */
+ }
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 4fcdd74..06674c2 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,10 +1,14 @@
+'use client';
+import { LayoutMain } from "@/modules/layout";
export default function Home() {
return (
-
- Light Code
-
+
+
+
);
}
diff --git a/src/assets/panda.jpg b/src/assets/panda.jpg
new file mode 100644
index 0000000..0f1beff
Binary files /dev/null and b/src/assets/panda.jpg differ
diff --git a/src/assets/qrcode-8x8.jpg b/src/assets/qrcode-8x8.jpg
new file mode 100644
index 0000000..b770e44
Binary files /dev/null and b/src/assets/qrcode-8x8.jpg differ
diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..9fb18ca
--- /dev/null
+++ b/src/components/ui/calendar.tsx
@@ -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 & {
+ buttonVariant?: React.ComponentProps["variant"]
+}) {
+ const defaultClassNames = getDefaultClassNames()
+
+ return (
+ 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 (
+
+ )
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === "left") {
+ return (
+
+ )
+ }
+
+ if (orientation === "right") {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+ |
+ )
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ )
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames()
+
+ const ref = React.useRef(null)
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus()
+ }, [modifiers.focused])
+
+ return (
+