feat(web): Update config page

This commit is contained in:
Dihubopen 2026-03-06 13:17:13 +08:00
parent 82ff997f2e
commit 686f5d449e
9 changed files with 379 additions and 19 deletions

1
.gitignore vendored
View file

@ -44,6 +44,7 @@ tasks/
# Added by goreleaser init: # Added by goreleaser init:
dist/ dist/
*.vite/
# Windows Application Icon/Resource # Windows Application Icon/Resource
*.syso *.syso

View file

@ -16,6 +16,7 @@
"@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/inter": "^5.2.8",
"@tabler/icons-react": "^3.38.0", "@tabler/icons-react": "^3.38.0",
"@tailwindcss/vite": "^4.2.1", "@tailwindcss/vite": "^4.2.1",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-router": "^1.163.3", "@tanstack/react-router": "^1.163.3",
"@tanstack/react-router-devtools": "^1.163.3", "@tanstack/react-router-devtools": "^1.163.3",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
@ -32,6 +33,7 @@
"react-textarea-autosize": "^8.5.9", "react-textarea-autosize": "^8.5.9",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"shadcn": "^3.8.5", "shadcn": "^3.8.5",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.2.1",
"tw-animate-css": "^1.4.0" "tw-animate-css": "^1.4.0"

View file

@ -17,6 +17,9 @@ importers:
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)) version: 4.2.1(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0))
'@tanstack/react-query':
specifier: ^5.90.21
version: 5.90.21(react@19.2.4)
'@tanstack/react-router': '@tanstack/react-router':
specifier: ^1.163.3 specifier: ^1.163.3
version: 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) version: 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -65,6 +68,9 @@ importers:
shadcn: shadcn:
specifier: ^3.8.5 specifier: ^3.8.5
version: 3.8.5(@types/node@24.11.0)(typescript@5.9.3) version: 3.8.5(@types/node@24.11.0)(typescript@5.9.3)
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
tailwind-merge: tailwind-merge:
specifier: ^3.5.0 specifier: ^3.5.0
version: 3.5.0 version: 3.5.0
@ -1353,66 +1359,79 @@ packages:
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.59.0': '@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.59.0': '@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.59.0': '@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.59.0': '@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.59.0': '@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.59.0': '@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.59.0': '@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.59.0': '@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.59.0': '@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.59.0': '@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.59.0': '@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.59.0': '@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@rollup/rollup-openbsd-x64@4.59.0': '@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
@ -1497,24 +1516,28 @@ packages:
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-arm64-musl@4.2.1': '@tailwindcss/oxide-linux-arm64-musl@4.2.1':
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-linux-x64-gnu@4.2.1': '@tailwindcss/oxide-linux-x64-gnu@4.2.1':
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
'@tailwindcss/oxide-linux-x64-musl@4.2.1': '@tailwindcss/oxide-linux-x64-musl@4.2.1':
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
'@tailwindcss/oxide-wasm32-wasi@4.2.1': '@tailwindcss/oxide-wasm32-wasi@4.2.1':
resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==}
@ -1558,6 +1581,14 @@ packages:
resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==} resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==}
engines: {node: '>=20.19'} engines: {node: '>=20.19'}
'@tanstack/query-core@5.90.20':
resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==}
'@tanstack/react-query@5.90.21':
resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==}
peerDependencies:
react: ^18 || ^19
'@tanstack/react-router-devtools@1.163.3': '@tanstack/react-router-devtools@1.163.3':
resolution: {integrity: sha512-42VMkV/2Z8ro7xzblPBRNZIEmCNXMzm2jD68G52p2qhjXm38wGpg46qneAESN9FtTQeVWk5aSXs47/jt7lkzmw==} resolution: {integrity: sha512-42VMkV/2Z8ro7xzblPBRNZIEmCNXMzm2jD68G52p2qhjXm38wGpg46qneAESN9FtTQeVWk5aSXs47/jt7lkzmw==}
engines: {node: '>=20.19'} engines: {node: '>=20.19'}
@ -2754,24 +2785,28 @@ packages:
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.31.1: lightningcss-linux-arm64-musl@1.31.1:
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.31.1: lightningcss-linux-x64-gnu@1.31.1:
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.31.1: lightningcss-linux-x64-musl@1.31.1:
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
engines: {node: '>= 12.0.0'} engines: {node: '>= 12.0.0'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.31.1: lightningcss-win32-arm64-msvc@1.31.1:
resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==}
@ -3504,6 +3539,12 @@ packages:
sisteransi@1.0.5: sisteransi@1.0.5:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
sonner@2.0.7:
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
source-map-js@1.2.1: source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -5323,6 +5364,13 @@ snapshots:
'@tanstack/history@1.161.4': {} '@tanstack/history@1.161.4': {}
'@tanstack/query-core@5.90.20': {}
'@tanstack/react-query@5.90.21(react@19.2.4)':
dependencies:
'@tanstack/query-core': 5.90.20
react: 19.2.4
'@tanstack/react-router-devtools@1.163.3(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.163.3)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': '@tanstack/react-router-devtools@1.163.3(@tanstack/react-router@1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@tanstack/router-core@1.163.3)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
dependencies: dependencies:
'@tanstack/react-router': 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-router': 1.163.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@ -7570,6 +7618,11 @@ snapshots:
sisteransi@1.0.5: {} sisteransi@1.0.5: {}
sonner@2.0.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
source-map-js@1.2.1: {} source-map-js@1.2.1: {}
source-map@0.6.1: {} source-map@0.6.1: {}

View file

@ -1,4 +1,5 @@
import type { ReactNode } from "react" import type { ReactNode } from "react"
import { Toaster } from "sonner"
import { AppHeader } from "@/components/app-header" import { AppHeader } from "@/components/app-header"
import { AppSidebar } from "@/components/app-sidebar" import { AppSidebar } from "@/components/app-sidebar"
@ -19,7 +20,8 @@ export function AppLayout({ children }: { children: ReactNode }) {
</main> </main>
</div> </div>
</div> </div>
<Toaster position="bottom-center" />
</SidebarProvider> </SidebarProvider>
</TooltipProvider> </TooltipProvider>
) )
} }

View file

@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-6 overflow-hidden rounded-xl bg-card py-6 text-sm text-card-foreground shadow-xs ring-1 ring-foreground/10 has-[>img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-6 group-data-[size=sm]/card:px-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-6 group-data-[size=sm]/card:[.border-b]:pb-4",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"text-base leading-normal font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-6 group-data-[size=sm]/card:px-4", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl px-6 group-data-[size=sm]/card:px-4 [.border-t]:pt-6 group-data-[size=sm]/card:[.border-t]:pt-4",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View file

@ -71,7 +71,9 @@
}, },
"common": { "common": {
"cancel": "Cancel", "cancel": "Cancel",
"save": "Save" "save": "Save",
"saving": "Saving...",
"reset": "Reset"
}, },
"models": { "models": {
"description": "Configure API keys for each AI provider. Only configured models are available for chat.", "description": "Configure API keys for each AI provider. Only configured models are available for chat.",
@ -155,7 +157,17 @@
"description": "Securely manage your API keys and credentials." "description": "Securely manage your API keys and credentials."
}, },
"config": { "config": {
"description": "System configuration and preferences." "description": "System configuration and preferences.",
"raw_json_title": "Raw JSON Configuration",
"raw_json_desc": "Advanced users can directly edit the raw JSON configuration below.",
"json_placeholder": "Enter valid JSON configuration...",
"save_success": "Configuration saved successfully.",
"save_error": "Failed to save configuration.",
"reset_success": "Configuration has been reset.",
"invalid_json": "Invalid JSON format.",
"format_success": "JSON formatted successfully.",
"format_error": "Invalid JSON format.",
"format": "Format"
}, },
"logs": { "logs": {
"description": "System logs and monitoring." "description": "System logs and monitoring."

View file

@ -71,7 +71,9 @@
}, },
"common": { "common": {
"cancel": "取消", "cancel": "取消",
"save": "保存" "save": "保存",
"saving": "保存中...",
"reset": "重置"
}, },
"models": { "models": {
"description": "为每个 AI 服务商配置 API Key。只有已配置的模型可用于对话。", "description": "为每个 AI 服务商配置 API Key。只有已配置的模型可用于对话。",
@ -155,7 +157,17 @@
"description": "安全管理您的 API 密钥与访问凭据。" "description": "安全管理您的 API 密钥与访问凭据。"
}, },
"config": { "config": {
"description": "系统配置和偏好设置。" "description": "系统配置和偏好设置。",
"raw_json_title": "原始 JSON 配置",
"raw_json_desc": "高级用户可以直接编辑下方的原始 JSON 配置。",
"json_placeholder": "请输入有效的 JSON 配置...",
"save_success": "配置保存成功。",
"save_error": "配置保存失败。",
"reset_success": "配置已重置。",
"invalid_json": "JSON 格式无效。",
"format_success": "JSON 格式化成功。",
"format_error": "JSON 格式无效。",
"format": "格式化"
}, },
"logs": { "logs": {
"description": "系统日志和监控。" "description": "系统日志和监控。"

View file

@ -1,3 +1,7 @@
import {
QueryClient,
QueryClientProvider,
} from "@tanstack/react-query"
import { RouterProvider, createRouter } from "@tanstack/react-router" import { RouterProvider, createRouter } from "@tanstack/react-router"
import { StrictMode } from "react" import { StrictMode } from "react"
import ReactDOM from "react-dom/client" import ReactDOM from "react-dom/client"
@ -6,7 +10,14 @@ import "./i18n"
import "./index.css" import "./index.css"
import { routeTree } from "./routeTree.gen" import { routeTree } from "./routeTree.gen"
const router = createRouter({ routeTree }) const queryClient = new QueryClient()
const router = createRouter({
routeTree,
context: {
queryClient,
},
})
declare module "@tanstack/react-router" { declare module "@tanstack/react-router" {
interface Register { interface Register {
@ -19,7 +30,9 @@ if (!rootElement.innerHTML) {
const root = ReactDOM.createRoot(rootElement) const root = ReactDOM.createRoot(rootElement)
root.render( root.render(
<StrictMode> <StrictMode>
<RouterProvider router={router} /> <QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</StrictMode>, </StrictMode>,
) )
} }

View file

@ -1,7 +1,31 @@
import { createFileRoute } from "@tanstack/react-router" import { createFileRoute } from "@tanstack/react-router"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { PageHeader } from "@/components/page-header" import { PageHeader } from "@/components/page-header"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Textarea } from "@/components/ui/textarea"
import { ScrollArea } from "@/components/ui/scroll-area"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { useEffect, useState } from "react"
export const Route = createFileRoute("/config")({ export const Route = createFileRoute("/config")({
component: ConfigPage, component: ConfigPage,
@ -12,19 +36,157 @@ function ConfigPage() {
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<PageHeader title={t("navigation.config", "Config")} /> <PageHeader title={t("navigation.config", "Config")} />
<div className="flex flex-1 items-center justify-center p-8"> <div className="flex-1 overflow-auto p-4 lg:p-8">
<div className="text-center"> <div className="mx-auto max-w-4xl">
<h1 className="text-2xl font-semibold tracking-tight"> <RawJsonPanel />
{t("navigation.config", "Config")}
</h1>
<p className="text-muted-foreground mt-2 text-sm">
{t(
"pages.config.description",
"System configuration and preferences.",
)}
</p>
</div> </div>
</div> </div>
</div> </div>
) )
} }
function RawJsonPanel() {
const { t } = useTranslation()
const queryClient = useQueryClient()
const { data: config, isLoading } = useQuery({
queryKey: ["config"],
queryFn: async () => {
const res = await fetch("/api/config")
if (!res.ok) {
throw new Error("Failed to fetch config")
}
return res.json()
},
})
const mutation = useMutation({
mutationFn: async (newConfig: string) => {
const res = await fetch("/api/config", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: newConfig,
})
if (!res.ok) {
throw new Error("Failed to save config")
}
},
onSuccess: () => {
toast.success(t("pages.config.save_success", "Configuration saved successfully."))
queryClient.invalidateQueries({ queryKey: ["config"] })
},
onError: () => {
toast.error(t("pages.config.save_error", "Failed to save configuration."))
},
})
const [editorValue, setEditorValue] = useState("")
useEffect(() => {
if (config) {
setEditorValue(JSON.stringify(config, null, 2))
}
}, [config])
const handleSave = () => {
try {
// Validate JSON before saving
JSON.parse(editorValue)
mutation.mutate(editorValue)
} catch (e) {
toast.error(t("pages.config.invalid_json", "Invalid JSON format."))
}
}
const handleFormat = () => {
try {
const formatted = JSON.stringify(JSON.parse(editorValue), null, 2)
setEditorValue(formatted)
toast.success(t("pages.config.format_success", "JSON formatted successfully."))
} catch (error) {
toast.error(t("pages.config.format_error", "Invalid JSON format."))
}
}
const [showResetDialog, setShowResetDialog] = useState(false)
const confirmReset = () => {
queryClient.invalidateQueries({ queryKey: ["config"] })
toast.info(t("pages.config.reset_success", "Configuration has been reset."))
setShowResetDialog(false)
}
return (
<Card>
<CardHeader>
<CardTitle>
{t("pages.config.raw_json_title", "Raw JSON Configuration")}
</CardTitle>
<CardDescription>
{t(
"pages.config.raw_json_desc",
"Advanced users can directly edit the raw JSON configuration below.",
)}
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="flex h-64 items-center justify-center">
<p>{t("labels.loading", "Loading...")}</p>
</div>
) : (
<div className="space-y-4">
<div className="bg-muted/30 relative rounded-lg border">
<ScrollArea className="h-[calc(100vh-20rem)] min-h-[200px]">
<Textarea
value={editorValue}
onChange={(e) => setEditorValue(e.target.value)}
className="font-mono text-sm min-h-[200px] resize-none border-0 bg-transparent px-4 py-3 shadow-none focus-visible:ring-0"
placeholder={t(
"pages.config.json_placeholder",
"Enter valid JSON configuration...",
)}
/>
</ScrollArea>
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={handleFormat} disabled={mutation.isPending}>
{t("pages.config.format", "Format")}
</Button>
<AlertDialog open={showResetDialog} onOpenChange={setShowResetDialog}>
<AlertDialogTrigger asChild>
<Button
variant="outline"
disabled={mutation.isPending}
onClick={() => setShowResetDialog(true)}
>
{t("common.reset", "Reset")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{t("pages.config.reset_confirm_title", "Reset Configuration")}</AlertDialogTitle>
<AlertDialogDescription>
{t("pages.config.reset_confirm_desc", "Are you sure you want to reset the configuration? This action cannot be undone.")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel", "Cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={confirmReset}>
{t("common.confirm", "Confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Button onClick={handleSave} disabled={mutation.isPending}>
{mutation.isPending
? t("common.saving", "Saving...")
: t("common.save", "Save")}
</Button>
</div>
</div>
)}
</CardContent>
</Card>
)
}