From 09f5f06baa807c21f9aedb4b9926e343f37cc824 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Mon, 2 Feb 2026 23:30:08 +0800 Subject: [PATCH] feat: add environment variable management page with import/export functionality - Implemented EnvPage component for managing environment variables. - Added functionality to load, add, remove, and update environment variables. - Included validation for empty and duplicate keys. - Implemented import/export features for environment variables in JSON format. - Integrated autocompletion for environment variable keys based on predefined config. feat: create user profile management with edit and password change modals - Developed ProfileCard component to display user information. - Added EditProfileModal for updating user details. - Implemented ChangePasswordModal for password modification. - Integrated user data fetching and state management using Zustand. feat: establish user store for managing user state and actions - Created user store with Zustand for managing user profile state. - Added actions for updating user information and handling loading states. feat: implement login store for user authentication - Developed login store for managing login state and actions. - Added functionality for user login and registration with error handling. feat: create reusable UI components for input groups and comboboxes - Developed InputGroup and related components for enhanced input handling. - Created Combobox component for improved selection functionality. - Added Badge component for displaying contextual information. --- next-env.d.ts | 2 +- next.config.ts | 2 +- package.json | 12 +- pnpm-lock.yaml | 222 ++++++++++---- src/app/config/components/autocomplate.tsx | 168 +++++++++++ src/app/config/env/page.tsx | 324 +++++++++++++++++++++ src/app/config/store/config.ts | 28 +- src/app/user/page.tsx | 292 +++++++++++++++++++ src/app/user/store/index.ts | 57 ++++ src/app/user/store/login.ts | 90 ++++++ src/components/ui/badge.tsx | 48 +++ src/components/ui/combobox.tsx | 310 ++++++++++++++++++++ src/components/ui/input-group.tsx | 170 +++++++++++ src/modules/layout/store/index.ts | 3 +- 14 files changed, 1658 insertions(+), 70 deletions(-) create mode 100644 src/app/config/components/autocomplate.tsx create mode 100644 src/app/config/env/page.tsx create mode 100644 src/app/user/page.tsx create mode 100644 src/app/user/store/index.ts create mode 100644 src/app/user/store/login.ts create mode 100644 src/components/ui/badge.tsx create mode 100644 src/components/ui/combobox.tsx create mode 100644 src/components/ui/input-group.tsx diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..b87975d 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import "./dist/dev/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/next.config.ts b/next.config.ts index c310583..268ec66 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,7 +10,7 @@ const nextConfig: NextConfig = { distDir: 'dist', basePath: basePath, trailingSlash: true, - transpilePackages: ['@kevisual/api'], + transpilePackages: ['@kevisual/api', "@kevisual/use-config"], images: { unoptimized: true, }, diff --git a/package.json b/package.json index 46acee8..4f99cc5 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,11 @@ }, "dependencies": { "@ant-design/icons": "^6.1.0", - "@kevisual/api": "^0.0.28", + "@base-ui/react": "^1.1.0", + "@kevisual/api": "^0.0.41", "@kevisual/cache": "^0.0.5", - "@kevisual/query": "^0.0.38", - "@kevisual/router": "^0.0.63", + "@kevisual/query": "^0.0.39", + "@kevisual/router": "^0.0.66", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -39,7 +40,7 @@ "idb-keyval": "^6.2.2", "lucide-react": "^0.563.0", "marked": "^17.0.1", - "next": "16.1.5", + "next": "16.1.6", "react": "19.2.4", "react-day-picker": "^9.13.0", "react-dom": "19.2.4", @@ -48,11 +49,12 @@ "tailwind-merge": "^3.4.0", "valtio": "^2.3.0", "vaul": "^1.1.2", - "zustand": "^5.0.10" + "zustand": "^5.0.11" }, "devDependencies": { "@kevisual/context": "^0.0.4", "@kevisual/types": "^0.0.12", + "@kevisual/use-config": "^1.0.30", "@tailwindcss/postcss": "^4", "@types/node": "^25", "@types/react": "^19", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5984888..b9b468a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,18 +11,21 @@ importers: '@ant-design/icons': specifier: ^6.1.0 version: 6.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@base-ui/react': + specifier: ^1.1.0 + version: 1.1.0(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@kevisual/api': - specifier: ^0.0.28 - version: 0.0.28 + specifier: ^0.0.41 + version: 0.0.41 '@kevisual/cache': specifier: ^0.0.5 version: 0.0.5 '@kevisual/query': - specifier: ^0.0.38 - version: 0.0.38 + specifier: ^0.0.39 + version: 0.0.39 '@kevisual/router': - specifier: ^0.0.63 - version: 0.0.63(typescript@5.9.3) + specifier: ^0.0.66 + version: 0.0.66(typescript@5.9.3) '@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.4(react@19.2.4))(react@19.2.4) @@ -96,8 +99,8 @@ importers: specifier: ^17.0.1 version: 17.0.1 next: - specifier: 16.1.5 - version: 16.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: 16.1.6 + version: 16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: specifier: 19.2.4 version: 19.2.4 @@ -123,8 +126,8 @@ importers: specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) zustand: - specifier: ^5.0.10 - version: 5.0.10(@types/react@19.2.7)(react@19.2.4) + specifier: ^5.0.11 + version: 5.0.11(@types/react@19.2.7)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: '@kevisual/context': specifier: ^0.0.4 @@ -132,6 +135,9 @@ importers: '@kevisual/types': specifier: ^0.0.12 version: 0.0.12 + '@kevisual/use-config': + specifier: ^1.0.30 + version: 1.0.30(dotenv@17.2.3) '@tailwindcss/postcss': specifier: ^4 version: 4.1.18 @@ -211,6 +217,27 @@ packages: resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} engines: {node: '>=6.9.0'} + '@base-ui/react@1.1.0': + resolution: {integrity: sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@base-ui/utils@0.2.4': + resolution: {integrity: sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + '@date-fns/tz@1.4.1': resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} @@ -407,8 +434,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@kevisual/api@0.0.28': - resolution: {integrity: sha512-WQluRlu2qGM1qktIhPLODie8x382a6jEMfFOcay/rnkCgXK0BRpnqOKwlX7IMLdMqka7GY/BD69kSMnK1Exf5g==} + '@kevisual/api@0.0.41': + resolution: {integrity: sha512-cNKvdhIQ3Pt98LV0ElprZZRjY9h/l+hcIGzubcD63PtR4Yq9CTaV0d9ARggkzeDkWlFsmH1ALpru1qZ5Zwmn4w==} '@kevisual/cache@0.0.5': resolution: {integrity: sha512-fgtUYGUUq/DY0KFV4CkWszNqvQUaA8XvMTUjoR9ZXRpau5IIDolD/Wen2TFsZ7G3Rfy+lef5dnaiZVDkZwdVKg==} @@ -426,66 +453,71 @@ packages: '@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/query@0.0.39': + resolution: {integrity: sha512-3UEPBIvtdykNkrby3hvrgrHdgd17Uq+Pnr4zs+JBzATkU2eKaOqtTUJqdyIEwuySCwzGTxrnlUzWP4tziDQDLQ==} - '@kevisual/router@0.0.63': - resolution: {integrity: sha512-rM/FELiNtTJkjb00sdQ2f8NpWHzfOwrtgQJhsX9Np9KdzAQ5dP6pI5nAHlWvU2QyrC1VH5IibUmsBIeMMV3nWg==} + '@kevisual/router@0.0.66': + resolution: {integrity: sha512-yoiCfKJ8yxrXToh8ud1+/JFqlRexrZmJ0PhofQX3jyfmmyEBQQJFL+2UYewm4FxbG3l7ndBC/NIhu1v5CdwxiQ==} '@kevisual/types@0.0.12': resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==} - '@next/env@16.1.5': - resolution: {integrity: sha512-CRSCPJiSZoi4Pn69RYBDI9R7YK2g59vLexPQFXY0eyw+ILevIenCywzg+DqmlBik9zszEnw2HLFOUlLAcJbL7g==} + '@kevisual/use-config@1.0.30': + resolution: {integrity: sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw==} + peerDependencies: + dotenv: ^17 - '@next/swc-darwin-arm64@16.1.5': - resolution: {integrity: sha512-eK7Wdm3Hjy/SCL7TevlH0C9chrpeOYWx2iR7guJDaz4zEQKWcS1IMVfMb9UKBFMg1XgzcPTYPIp1Vcpukkjg6Q==} + '@next/env@16.1.6': + resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + + '@next/swc-darwin-arm64@16.1.6': + resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.1.5': - resolution: {integrity: sha512-foQscSHD1dCuxBmGkbIr6ScAUF6pRoDZP6czajyvmXPAOFNnQUJu2Os1SGELODjKp/ULa4fulnBWoHV3XdPLfA==} + '@next/swc-darwin-x64@16.1.6': + resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.1.5': - resolution: {integrity: sha512-qNIb42o3C02ccIeSeKjacF3HXotGsxh/FMk/rSRmCzOVMtoWH88odn2uZqF8RLsSUWHcAqTgYmPD3pZ03L9ZAA==} + '@next/swc-linux-arm64-gnu@16.1.6': + resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.1.5': - resolution: {integrity: sha512-U+kBxGUY1xMAzDTXmuVMfhaWUZQAwzRaHJ/I6ihtR5SbTVUEaDRiEU9YMjy1obBWpdOBuk1bcm+tsmifYSygfw==} + '@next/swc-linux-arm64-musl@16.1.6': + resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.1.5': - resolution: {integrity: sha512-gq2UtoCpN7Ke/7tKaU7i/1L7eFLfhMbXjNghSv0MVGF1dmuoaPeEVDvkDuO/9LVa44h5gqpWeJ4mRRznjDv7LA==} + '@next/swc-linux-x64-gnu@16.1.6': + resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.1.5': - resolution: {integrity: sha512-bQWSE729PbXT6mMklWLf8dotislPle2L70E9q6iwETYEOt092GDn0c+TTNj26AjmeceSsC4ndyGsK5nKqHYXjQ==} + '@next/swc-linux-x64-musl@16.1.6': + resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.1.5': - resolution: {integrity: sha512-LZli0anutkIllMtTAWZlDqdfvjWX/ch8AFK5WgkNTvaqwlouiD1oHM+WW8RXMiL0+vAkAJyAGEzPPjO+hnrSNQ==} + '@next/swc-win32-arm64-msvc@16.1.6': + resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.1.5': - resolution: {integrity: sha512-7is37HJTNQGhjPpQbkKjKEboHYQnCgpVt/4rBrrln0D9nderNxZ8ZWs8w1fAtzUx7wEyYjQ+/13myFgFj6K2Ng==} + '@next/swc-win32-x64-msvc@16.1.6': + resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1504,6 +1536,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/spark-md5@3.0.5': + resolution: {integrity: sha512-lWf05dnD42DLVKQJZrDHtWFidcLrHuip01CtnC2/S6AMhX4t9ZlEUj4iuRlAnts0PQk7KESOqKxeGE/b6sIPGg==} + antd@6.2.2: resolution: {integrity: sha512-f5RvWnhjt2gZTpBMW3msHwA3IeaCJBHDwVyEsskYGp0EXcRhhklWrltkybDki0ysBNywkjLPp3wuuWhIKfplcQ==} peerDependencies: @@ -1749,8 +1784,8 @@ packages: engines: {node: ^18 || >=20} hasBin: true - next@16.1.5: - resolution: {integrity: sha512-f+wE+NSbiQgh3DSAlTaw2FwY5yGdVViAtp8TotNQj4kk4Q8Bh1sC/aL9aH+Rg1YAVn18OYXsRDT7U/079jgP7w==} + next@16.1.6: + resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -1770,6 +1805,9 @@ packages: sass: optional: true + path-browserify-esm@1.0.6: + resolution: {integrity: sha512-9nUwYvvu/yq1PYrUyYCihNWmpzacaRYF6gGbjLWErrZ4MRDWyfPN7RpE8E7tsw8eqBU/rr7mcoTXbS+Vih8uUA==} + path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} @@ -1845,6 +1883,9 @@ packages: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -1887,6 +1928,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + spark-md5@3.0.2: + resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} + string-convert@0.2.1: resolution: {integrity: sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==} @@ -1910,6 +1954,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + tailwind-merge@3.4.0: resolution: {integrity: sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==} @@ -1961,6 +2008,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + valtio@2.3.0: resolution: {integrity: sha512-1MfKNcmOIdBSatiJsYgw420n6jnD+jeoI0V+RkOQbCB0ElLh6GKUfPr0hc9uq/KBGeghivDEarRsKFFdSQQnKw==} engines: {node: '>=12.20.0'} @@ -1979,8 +2031,8 @@ packages: react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc - zustand@5.0.10: - resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==} + zustand@5.0.11: + resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' @@ -2061,6 +2113,31 @@ snapshots: '@babel/runtime@7.28.6': {} + '@base-ui/react@1.1.0(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@base-ui/utils': 0.2.4(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/utils': 0.2.10 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + reselect: 5.1.1 + tabbable: 6.4.0 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.7 + + '@base-ui/utils@0.2.4(@types/react@19.2.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@babel/runtime': 7.28.6 + '@floating-ui/utils': 0.2.10 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.7 + '@date-fns/tz@1.4.1': {} '@emnapi/runtime@1.8.1': @@ -2205,14 +2282,17 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@kevisual/api@0.0.28': + '@kevisual/api@0.0.41': dependencies: '@kevisual/js-filter': 0.0.5 '@kevisual/load': 0.0.6 + '@types/spark-md5': 3.0.5 es-toolkit: 1.44.0 eventemitter3: 5.0.4 fuse.js: 7.1.0 nanoid: 5.1.6 + path-browserify-esm: 1.0.6 + spark-md5: 3.0.2 '@kevisual/cache@0.0.5': dependencies: @@ -2239,11 +2319,11 @@ snapshots: dependencies: eventemitter3: 5.0.4 - '@kevisual/query@0.0.38': + '@kevisual/query@0.0.39': dependencies: tslib: 2.8.1 - '@kevisual/router@0.0.63(typescript@5.9.3)': + '@kevisual/router@0.0.66(typescript@5.9.3)': dependencies: '@kevisual/dts': 0.0.3(typescript@5.9.3) hono: 4.11.7 @@ -2252,30 +2332,35 @@ snapshots: '@kevisual/types@0.0.12': {} - '@next/env@16.1.5': {} + '@kevisual/use-config@1.0.30(dotenv@17.2.3)': + dependencies: + '@kevisual/load': 0.0.6 + dotenv: 17.2.3 - '@next/swc-darwin-arm64@16.1.5': + '@next/env@16.1.6': {} + + '@next/swc-darwin-arm64@16.1.6': optional: true - '@next/swc-darwin-x64@16.1.5': + '@next/swc-darwin-x64@16.1.6': optional: true - '@next/swc-linux-arm64-gnu@16.1.5': + '@next/swc-linux-arm64-gnu@16.1.6': optional: true - '@next/swc-linux-arm64-musl@16.1.5': + '@next/swc-linux-arm64-musl@16.1.6': optional: true - '@next/swc-linux-x64-gnu@16.1.5': + '@next/swc-linux-x64-gnu@16.1.6': optional: true - '@next/swc-linux-x64-musl@16.1.5': + '@next/swc-linux-x64-musl@16.1.6': optional: true - '@next/swc-win32-arm64-msvc@16.1.5': + '@next/swc-win32-arm64-msvc@16.1.6': optional: true - '@next/swc-win32-x64-msvc@16.1.5': + '@next/swc-win32-x64-msvc@16.1.6': optional: true '@radix-ui/number@1.1.1': {} @@ -3277,6 +3362,8 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/spark-md5@3.0.5': {} + antd@6.2.2(date-fns@4.1.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@ant-design/colors': 8.0.1 @@ -3506,9 +3593,9 @@ snapshots: nanoid@5.1.6: {} - next@16.1.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@next/env': 16.1.5 + '@next/env': 16.1.6 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.9.11 caniuse-lite: 1.0.30001762 @@ -3517,19 +3604,21 @@ snapshots: react-dom: 19.2.4(react@19.2.4) styled-jsx: 5.1.6(react@19.2.4) optionalDependencies: - '@next/swc-darwin-arm64': 16.1.5 - '@next/swc-darwin-x64': 16.1.5 - '@next/swc-linux-arm64-gnu': 16.1.5 - '@next/swc-linux-arm64-musl': 16.1.5 - '@next/swc-linux-x64-gnu': 16.1.5 - '@next/swc-linux-x64-musl': 16.1.5 - '@next/swc-win32-arm64-msvc': 16.1.5 - '@next/swc-win32-x64-msvc': 16.1.5 + '@next/swc-darwin-arm64': 16.1.6 + '@next/swc-darwin-x64': 16.1.6 + '@next/swc-linux-arm64-gnu': 16.1.6 + '@next/swc-linux-arm64-musl': 16.1.6 + '@next/swc-linux-x64-gnu': 16.1.6 + '@next/swc-linux-x64-musl': 16.1.6 + '@next/swc-win32-arm64-msvc': 16.1.6 + '@next/swc-win32-x64-msvc': 16.1.6 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros + path-browserify-esm@1.0.6: {} + path-parse@1.0.7: {} picocolors@1.1.1: {} @@ -3597,6 +3686,8 @@ snapshots: react@19.2.4: {} + reselect@5.1.1: {} + resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -3690,6 +3781,8 @@ snapshots: source-map-js@1.2.1: {} + spark-md5@3.0.2: {} + string-convert@0.2.1: {} styled-jsx@5.1.6(react@19.2.4): @@ -3701,6 +3794,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + tabbable@6.4.0: {} + tailwind-merge@3.4.0: {} tailwindcss@4.1.18: {} @@ -3734,6 +3829,10 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + use-sync-external-store@1.6.0(react@19.2.4): + dependencies: + react: 19.2.4 + valtio@2.3.0(@types/react@19.2.7)(react@19.2.4): dependencies: proxy-compare: 3.0.1 @@ -3750,7 +3849,8 @@ snapshots: - '@types/react' - '@types/react-dom' - zustand@5.0.10(@types/react@19.2.7)(react@19.2.4): + zustand@5.0.11(@types/react@19.2.7)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): optionalDependencies: '@types/react': 19.2.7 react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) diff --git a/src/app/config/components/autocomplate.tsx b/src/app/config/components/autocomplate.tsx new file mode 100644 index 0000000..eb3247e --- /dev/null +++ b/src/app/config/components/autocomplate.tsx @@ -0,0 +1,168 @@ +"use client" + +import * as React from "react" +import { Check, ChevronsUpDown } from "lucide-react" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" +import { Input } from "@/components/ui/input" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover" + +export interface AutocomplateOption { + value: string + label: string + description?: string + tags?: readonly string[] +} + +interface AutocomplateProps { + value: string + onChange: (value: string) => void + options: AutocomplateOption[] + placeholder?: string + searchPlaceholder?: string + emptyMessage?: string + className?: string +} + +export function Autocomplate({ + value, + onChange, + options, + placeholder = "选择项目...", + searchPlaceholder = "搜索...", + emptyMessage = "未找到结果", + className, +}: AutocomplateProps) { + const [open, setOpen] = React.useState(false) + const [inputValue, setInputValue] = React.useState(value) + const [searchValue, setSearchValue] = React.useState("") + + React.useEffect(() => { + setInputValue(value) + }, [value]) + + const handleInputChange = (e: React.ChangeEvent) => { + const newValue = e.target.value + setInputValue(newValue) + onChange(newValue) + } + + const handleSelect = (selectedValue: string) => { + setInputValue(selectedValue) + onChange(selectedValue) + setSearchValue("") + setOpen(false) + } + + const handleSearchKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && searchValue) { + e.preventDefault() + handleSelect(searchValue) + } + } + + // 随机获取badge variant + const getRandomVariant = (index: number) => { + const variants = ['default', 'secondary', 'outline'] as const + return variants[index % variants.length] + } + + return ( +
+ + + + + + + + + + +
+
{emptyMessage}
+ {searchValue && ( + + )} +
+
+ + {options.map((option) => ( + handleSelect(option.value)} + > + +
+ {option.label} + {option.description && ( + + {option.description} + + )} + {option.tags && option.tags.length > 0 && ( +
+ {option.tags.map((tag, index) => ( + + {tag} + + ))} +
+ )} +
+
+ ))} +
+
+
+
+
+
+ ) +} diff --git a/src/app/config/env/page.tsx b/src/app/config/env/page.tsx new file mode 100644 index 0000000..5918218 --- /dev/null +++ b/src/app/config/env/page.tsx @@ -0,0 +1,324 @@ +'use client'; + +import { configEnvList } from '@kevisual/use-config/env-config.ts'; +import { useConfigStore } from '../store/config'; +import { useEffect, useState, useMemo } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card'; +import { Trash2, Plus, Save, Upload, Download } from 'lucide-react'; +import { toast } from 'sonner'; +import { Autocomplate, type AutocomplateOption } from '@/app/config/components/autocomplate'; + +type EnvItem = { + key: string; + value: string; + id: string; +}; + +export default function EnvPage() { + const { getEnv, updateEnv, envData } = useConfigStore(); + const [envItems, setEnvItems] = useState([]); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + loadEnvData(); + }, []); + + useEffect(() => { + if (envData && typeof envData === 'object') { + const data = envData?.data || {}; + const items = Object.entries(data).map(([key, value], index) => ({ + key, + value: String(value || ''), + id: `env-${index}-${Date.now()}`, + })); + setEnvItems(items); + } + }, [envData]); + + const loadEnvData = async () => { + setIsLoading(true); + try { + await getEnv(); + } finally { + setIsLoading(false); + } + }; + + const addEnvItem = () => { + const newItem: EnvItem = { + key: '', + value: '', + id: `env-new-${Date.now()}`, + }; + setEnvItems([...envItems, newItem]); + }; + + const removeEnvItem = (id: string) => { + setEnvItems(envItems.filter((item) => item.id !== id)); + }; + + const updateEnvItem = (id: string, field: 'key' | 'value', newValue: string) => { + setEnvItems( + envItems.map((item) => (item.id === id ? { ...item, [field]: newValue } : item)) + ); + }; + + const handleSave = async () => { + // 验证是否有空的 key + const hasEmptyKey = envItems.some((item) => !item.key.trim()); + if (hasEmptyKey) { + toast.error('请填写所有环境变量的键名'); + return; + } + + // 验证是否有重复的 key + const keys = envItems.map((item) => item.key); + const uniqueKeys = new Set(keys); + if (keys.length !== uniqueKeys.size) { + toast.error('存在重复的环境变量键名'); + return; + } + + // 转换为对象 + const envObject = envItems.reduce( + (acc, item) => { + if (item.key.trim()) { + acc[item.key] = item.value; + } + return acc; + }, + {} as Record + ); + + setIsLoading(true); + try { + await updateEnv({ ...envData, data: envObject }); + await loadEnvData(); + } finally { + setIsLoading(false); + } + }; + + const getEnvDescription = (key: string): string => { + const config = configEnvList.find((item) => item.title === key); + return config?.description || ''; + }; + + const getEnvTags = (key: string): readonly string[] => { + const config = configEnvList.find((item) => item.title === key); + return config?.tags || []; + }; + + const handleExport = () => { + // 只导出第一级的 key-value + const envObject = envItems.reduce( + (acc, item) => { + if (item.key.trim()) { + acc[item.key] = item.value; + } + return acc; + }, + {} as Record + ); + + const json = JSON.stringify(envObject, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'env-config.json'; + a.click(); + URL.revokeObjectURL(url); + toast.success('导出成功'); + }; + + const handleImport = () => { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = 'application/json'; + input.onchange = (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + try { + const json = JSON.parse(event.target?.result as string); + + // 只读取第一级的 key-value + const items: EnvItem[] = []; + Object.entries(json).forEach(([key, value]) => { + // 只处理第一级,忽略嵌套对象 + if (typeof value !== 'object' || value === null) { + items.push({ + key, + value: String(value || ''), + id: `env-${items.length}-${Date.now()}`, + }); + } else { + // 如果是对象,只取第一级的值 + items.push({ + key, + value: JSON.stringify(value), + id: `env-${items.length}-${Date.now()}`, + }); + } + }); + + setEnvItems(items); + toast.success('导入成功'); + } catch (error) { + toast.error('导入失败,请检查文件格式'); + console.error(error); + } + }; + reader.readAsText(file); + }; + input.click(); + }; + + const comboboxOptions = useMemo(() => { + return configEnvList.map((config) => ({ + value: config.title, + label: config.title, + description: config.description, + tags: config.tags, + })).sort((a, b) => { + // 优先排序:有"常用"标签的排在前面 + const aHasCommon = a.tags?.some(tag => tag === '常用') ?? false; + const bHasCommon = b.tags?.some(tag => tag === '常用') ?? false; + + if (aHasCommon && !bHasCommon) return -1; + if (!aHasCommon && bHasCommon) return 1; + + // 其次按字母顺序排序 + return a.label.localeCompare(b.label); + }); + }, []); + + if (isLoading && envItems.length === 0) { + return ( +
+
加载中...
+
+ ); + } + + return ( +
+
+
+
+
+

环境变量配置

+

管理应用的环境变量配置

+
+
+ + + + +
+
+
+
+ +
+
+ +
+ {envItems.map((item) => ( + + +
+
+
+ + updateEnvItem(item.id, 'key', value)} + options={comboboxOptions} + placeholder="输入或选择环境变量键" + searchPlaceholder="搜索环境变量..." + emptyMessage="未找到匹配的环境变量" + /> +
+ + {item.key && getEnvDescription(item.key) && ( + {getEnvDescription(item.key)} + )} + + {item.key && getEnvTags(item.key).length > 0 && ( +
+ {getEnvTags(item.key).map((tag) => ( + + {tag} + + ))} +
+ )} + +
+ + updateEnvItem(item.id, 'value', e.target.value)} + placeholder="输入环境变量的值" + type={ + item.key.toLowerCase().includes('password') || + item.key.toLowerCase().includes('secret') || + item.key.toLowerCase().includes('key') + ? 'password' + : 'text' + } + /> +
+
+ +
+
+
+ ))} +
+ + {envItems.length === 0 && ( + +
+

暂无环境变量

+ +
+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/app/config/store/config.ts b/src/app/config/store/config.ts index 8b56532..4bf97ee 100644 --- a/src/app/config/store/config.ts +++ b/src/app/config/store/config.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { query } from '@/modules/query'; import { toast } from 'sonner'; -import { QueryConfig } from '@kevisual/api/config'; +import { QueryConfig, Config } from '@kevisual/api/config'; export const queryConfig = new QueryConfig({ query: query as any }); @@ -16,6 +16,10 @@ interface ConfigStore { deleteConfig: (id: string) => Promise; detectConfig: () => Promise; onOpenKey: (key: string) => Promise; + getEnv: () => Promise; + updateEnv: (data: Config) => Promise; + envData: Config; + setEnvData: (envData: Config) => void; } export const useConfigStore = create((set, get) => ({ @@ -75,4 +79,26 @@ export const useConfigStore = create((set, get) => ({ toast.error('获取配置失败'); } }, + getEnv: async () => { + const res = await queryConfig.getByKey('env.json'); + if (res.code === 200) { + const data = res.data; + console.log(data); + set({ envData: data }); + } else { + console.log(res); + toast.error('获取失败'); + } + }, + updateEnv: async (data: any) => { + const res = await queryConfig.updateConfig({ key: 'env.json', ...data }); + if (res.code === 200) { + toast.success('更新成功'); + } else { + console.log(res); + toast.error('更新失败'); + } + }, + envData: {}, + setEnvData: (envData: any) => set({ envData }), })); diff --git a/src/app/user/page.tsx b/src/app/user/page.tsx new file mode 100644 index 0000000..f57b8cd --- /dev/null +++ b/src/app/user/page.tsx @@ -0,0 +1,292 @@ +'use client'; +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { useUserStore } from './store'; +import { useLayoutStore } from '@/modules/layout/store'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Textarea } from '@/components/ui/textarea'; +import { Label } from '@/components/ui/label'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { LayoutMain } from '@/modules/layout'; +import { Pencil, Key, User } from 'lucide-react'; +import PandaPNG from '@/assets/panda.jpg'; + +const ProfileCard = () => { + const { me, getMe } = useLayoutStore(); + const { setShowEdit, setShowChangePassword } = useUserStore(); + + useEffect(() => { + getMe(); + }, [getMe]); + + return ( + + + + + 个人信息 + + 管理您的个人资料和账户设置 + + + {/* Avatar */} +
+
+ {me?.avatar ? ( + avatar + ) : ( + avatar + )} +
+
+

{me?.username || '-'}

+

{me?.description || '暂无描述'}

+
+
+ + {/* User Info Fields */} +
+
+ +
{me?.username || '-'}
+
+ +
+ +
+ {me?.nickname || '-'} + +
+
+ +
+ +
+

{me?.description || '暂无描述'}

+ +
+
+ +
+ +
{me?.id || '-'}
+
+ +
+ + +
+
+ + {/* Edit Profile Button */} +
+ +
+
+
+ ); +}; + +const EditProfileModal = () => { + const { showEdit, setShowEdit, setFormData, updateSelf, loading } = useUserStore(); + const { me, getMe } = useLayoutStore(); + const { + handleSubmit, + reset, + register, + } = useForm(); + + useEffect(() => { + if (showEdit) { + reset({ + nickname: me?.nickname || '', + description: me?.description || '', + }); + } + }, [me, showEdit, reset]); + + const onSubmit = async (data: any) => { + const res = await updateSelf(data); + if (res) { + setShowEdit(false); + setFormData({}); + await getMe(); + } + }; + + return ( + { + setShowEdit(open); + if (!open) setFormData({}); + }}> + + + 编辑个人资料 + +
+
+
+ + +
+
+ +