From 231caa3b9aba7f6f49e240f7995d4037b4f9b960 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Wed, 31 Dec 2025 17:54:11 +0800 Subject: [PATCH] feat: Implement view management features with a new UI for editing and listing views - Added a resizable panel layout in the studio app to display the view list alongside the main application. - Refactored the studio store to include new methods for fetching and managing route views. - Introduced a new DataItemForm component for configuring data items in views. - Created a ViewEditor component for adding and editing views, including data items and queries. - Enhanced the ViewList component to support searching, adding, editing, and deleting views. - Updated UI components (Button, Checkbox, Dialog, Input, Label, Table) for better styling and functionality. - Added environment configuration for API URL. - Introduced a new workspace configuration for pnpm. --- package.json | 4 +- pnpm-lock.yaml | 20 ++ web/.env.example | 1 + web/.gitignore | 3 + web/astro.config.mjs | 1 + web/package.json | 13 +- web/pnpm-lock.yaml | 265 ++++++++++++++++-- web/pnpm-workspace.yaml | 2 + web/src/apps/studio/index.tsx | 19 +- web/src/apps/studio/store.ts | 177 ++++++------ web/src/apps/view/components/DataItemForm.tsx | 200 +++++++++++++ web/src/apps/view/components/ViewEditor.tsx | 146 ++++++++++ web/src/apps/view/components/ViewFormItem.tsx | 58 ++++ web/src/apps/view/form.ts | 10 + web/src/apps/view/list.tsx | 129 +++++++++ web/src/components/ui/button.tsx | 6 +- web/src/components/ui/checkbox.tsx | 30 ++ web/src/components/ui/dialog.tsx | 42 +++ web/src/components/ui/input.tsx | 21 ++ web/src/components/ui/label.tsx | 22 ++ web/src/components/ui/table.tsx | 114 ++++++++ web/src/modules/query.ts | 10 +- 22 files changed, 1177 insertions(+), 116 deletions(-) create mode 100644 web/.env.example create mode 100644 web/pnpm-workspace.yaml create mode 100644 web/src/apps/view/components/DataItemForm.tsx create mode 100644 web/src/apps/view/components/ViewEditor.tsx create mode 100644 web/src/apps/view/components/ViewFormItem.tsx create mode 100644 web/src/apps/view/form.ts create mode 100644 web/src/apps/view/list.tsx create mode 100644 web/src/components/ui/checkbox.tsx create mode 100644 web/src/components/ui/dialog.tsx create mode 100644 web/src/components/ui/input.tsx create mode 100644 web/src/components/ui/label.tsx create mode 100644 web/src/components/ui/table.tsx diff --git a/package.json b/package.json index 7458734..e70d511 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,7 @@ { "dependencies": { - "nanoid": "^5.1.6" + "nanoid": "^5.1.6", + "zod": "^4.2.1", + "zod-to-json-schema": "^3.25.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2549325..a96aea6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,12 @@ importers: nanoid: specifier: ^5.1.6 version: 5.1.6 + zod: + specifier: ^4.2.1 + version: 4.2.1 + zod-to-json-schema: + specifier: ^3.25.1 + version: 3.25.1(zod@4.2.1) packages: @@ -19,6 +25,20 @@ packages: engines: {node: ^18 || >=20} hasBin: true + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@4.2.1: + resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} + snapshots: nanoid@5.1.6: {} + + zod-to-json-schema@3.25.1(zod@4.2.1): + dependencies: + zod: 4.2.1 + + zod@4.2.1: {} diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..81e286a --- /dev/null +++ b/web/.env.example @@ -0,0 +1 @@ +VITE_API_URL='http://localhost:4005' \ No newline at end of file diff --git a/web/.gitignore b/web/.gitignore index 1ee6af6..2a79579 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -4,3 +4,6 @@ node_modules .astro dist + +.env +!.env*example \ No newline at end of file diff --git a/web/astro.config.mjs b/web/astro.config.mjs index 06626e0..b80b69b 100644 --- a/web/astro.config.mjs +++ b/web/astro.config.mjs @@ -11,6 +11,7 @@ dotenv.config(); const isDev = process.env.NODE_ENV === 'development'; let target = process.env.VITE_API_URL || 'http://localhost:51515'; +console.log('API Proxy Target:', target); const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' }; let proxy = { '/root/': apiProxy, diff --git a/web/package.json b/web/package.json index 6a682c4..d97bbe6 100644 --- a/web/package.json +++ b/web/package.json @@ -23,11 +23,14 @@ "@astrojs/react": "^4.4.2", "@astrojs/sitemap": "^3.6.0", "@astrojs/vue": "^5.1.3", + "@kevisual/cache": "^0.0.5", "@kevisual/context": "^0.0.4", "@kevisual/query": "^0.0.33", "@kevisual/query-login": "^0.0.7", "@kevisual/registry": "^0.0.1", - "@kevisual/router": "^0.0.51", + "@kevisual/router": "^0.0.52", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-slot": "^1.2.4", "@tailwindcss/vite": "^4.1.18", "@uiw/react-md-editor": "^4.0.11", @@ -39,13 +42,17 @@ "dayjs": "^1.11.19", "es-toolkit": "^1.43.0", "github-markdown-css": "^5.8.1", + "handsontable": "^16.2.0", "highlight.js": "^11.11.1", "lucide-react": "^0.562.0", "marked": "^17.0.1", "marked-highlight": "^2.2.3", "nanoid": "^5.1.6", + "papaparse": "^5.5.3", "react": "^19.2.3", "react-dom": "^19.2.3", + "react-hook-form": "^7.69.0", + "react-resizable-panels": "^4.1.0", "react-toastify": "^11.0.5", "tailwind-merge": "^3.4.0", "vue": "^3.5.26", @@ -55,7 +62,7 @@ "access": "public" }, "devDependencies": { - "@kevisual/api": "^0.0.10", + "@kevisual/api": "^0.0.14", "@kevisual/types": "^0.0.10", "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", @@ -63,7 +70,7 @@ "tailwindcss": "^4.1.18", "tw-animate-css": "^1.4.0" }, - "packageManager": "pnpm@10.26.2", + "packageManager": "pnpm@10.27.0", "onlyBuiltDependencies": [ "@tailwindcss/oxide", "esbuild", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 44dfd10..896d84d 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@astrojs/vue': specifier: ^5.1.3 version: 5.1.3(@types/node@24.7.2)(astro@5.16.6(@types/node@24.7.2)(idb-keyval@6.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.9.3))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.52.4)(vue@3.5.26(typescript@5.9.3)) + '@kevisual/cache': + specifier: ^0.0.5 + version: 0.0.5 '@kevisual/context': specifier: ^0.0.4 version: 0.0.4 @@ -33,8 +36,14 @@ importers: specifier: ^0.0.1 version: 0.0.1(typescript@5.9.3) '@kevisual/router': - specifier: ^0.0.51 - version: 0.0.51 + specifier: ^0.0.52 + version: 0.0.52 + '@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) + '@radix-ui/react-label': + specifier: ^2.1.8 + version: 2.1.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) '@radix-ui/react-slot': specifier: ^1.2.4 version: 1.2.4(@types/react@19.2.7)(react@19.2.3) @@ -46,7 +55,7 @@ importers: version: 4.0.11(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) antd: specifier: ^6.1.3 - version: 6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 6.1.3(moment@2.30.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) astro: specifier: ^5.16.6 version: 5.16.6(@types/node@24.7.2)(idb-keyval@6.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.52.4)(typescript@5.9.3) @@ -68,6 +77,9 @@ importers: github-markdown-css: specifier: ^5.8.1 version: 5.8.1 + handsontable: + specifier: ^16.2.0 + version: 16.2.0 highlight.js: specifier: ^11.11.1 version: 11.11.1 @@ -83,12 +95,21 @@ importers: nanoid: specifier: ^5.1.6 version: 5.1.6 + papaparse: + specifier: ^5.5.3 + version: 5.5.3 react: specifier: ^19.2.3 version: 19.2.3 react-dom: specifier: ^19.2.3 version: 19.2.3(react@19.2.3) + react-hook-form: + specifier: ^7.69.0 + version: 7.69.0(react@19.2.3) + react-resizable-panels: + specifier: ^4.1.0 + version: 4.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react-toastify: specifier: ^11.0.5 version: 11.0.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -103,8 +124,8 @@ importers: version: 5.0.9(@types/react@19.2.7)(react@19.2.3) devDependencies: '@kevisual/api': - specifier: ^0.0.10 - version: 0.0.10 + specifier: ^0.0.14 + version: 0.0.14 '@kevisual/types': specifier: ^0.0.10 version: 0.0.10 @@ -539,6 +560,9 @@ packages: cpu: [x64] os: [win32] + '@handsontable/pikaday@1.0.0': + resolution: {integrity: sha512-1VN6N38t5/DcjJ7y7XUYrDx1LuzvvzlrFdBdMG90Qo1xc8+LXHqbWbsTEm5Ec5gXTEbDEO53vUT35R+2COmOyg==} + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -681,17 +705,20 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@kevisual/api@0.0.10': - resolution: {integrity: sha512-AF5DcXPfVEZtvIJw9EC8EXkhU33dS08v9+b4mIrzCi0ETRvwAlQ2cg8WgfY6exJYzbFg6M4h+POhXvPujrk9mA==} + '@kevisual/api@0.0.14': + resolution: {integrity: sha512-GOs61Jvjxs+7PB8+iSPko9/RGeWENxltHueV75M6W0psRsnx/J+06I48/cO413FwCoqSOqpOoivdRgSENdHM9g==} '@kevisual/cache@0.0.3': resolution: {integrity: sha512-BWEck69KYL96/ywjYVkML974RHjDJTj2ITQND1zFPR+hlBV1H1p55QZgSYRJCObg3EAV1S9Zic/fR2T4pfe8yg==} + '@kevisual/cache@0.0.5': + resolution: {integrity: sha512-fgtUYGUUq/DY0KFV4CkWszNqvQUaA8XvMTUjoR9ZXRpau5IIDolD/Wen2TFsZ7G3Rfy+lef5dnaiZVDkZwdVKg==} + '@kevisual/context@0.0.4': resolution: {integrity: sha512-HJeLeZQLU+7tCluSfOyvkgKLs0HjCZrdJlZgEgKRSa8XTwZfMAUt6J7qZTbrZAHBlPtX68EPu/PI8JMCeu3WAQ==} - '@kevisual/js-filter@0.0.2': - resolution: {integrity: sha512-SS8diRpjrAIEQKT8YMTa1XTucQKuPbG04UChXtp7wd1jPsvQaNKYapErRA8qx4igwoVQt6eAYADwYzXhB1fN2A==} + '@kevisual/js-filter@0.0.3': + resolution: {integrity: sha512-vgUB2fUAWS75GUFr/a/tGSSDrPUUmVDktO38k3hIKwU3ZE4tpuhcVxrpUbkXlFS5i0rbL2mAQeID1C6kIlMGRg==} '@kevisual/load@0.0.6': resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==} @@ -707,8 +734,8 @@ packages: '@kevisual/registry@0.0.1': resolution: {integrity: sha512-//OHu9m4JDrMjgP8o8dcjZd3D3IAUkRVlkTSviouZEH7r5m7mccA3Hvzw0XJ/lelx6exC6LWsyv6c4uV0Dp+gw==} - '@kevisual/router@0.0.51': - resolution: {integrity: sha512-i9qYBeS/um78oC912oWJD3iElB+5NTKyTrz1Hzf4DckiUFnjLL81UPwjIh5I2l9+ul0IZ/Pxx+sFSF99fJkzKg==} + '@kevisual/router@0.0.52': + resolution: {integrity: sha512-Qiv3P1XjzD813Tm79S+atrDb2eickGCI9tuy/aCu512LcoYYJqZhwwkeT4ES0DinnA13Ckqd43QWBR6UmuYkHQ==} '@kevisual/types@0.0.10': resolution: {integrity: sha512-Q73uzzjk9UidumnmCvOpgzqDDvQxsblz22bIFuoiioUFJWwaparx8bpd8ArRyFojicYL1YJoFDzDZ9j9NN8grA==} @@ -763,6 +790,19 @@ packages: '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + 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-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: @@ -838,6 +878,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + 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-portal@1.1.9': resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: @@ -953,6 +1006,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rc-component/async-validator@5.0.4': resolution: {integrity: sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==} engines: {node: '>=14.x'} @@ -1541,6 +1612,9 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1741,6 +1815,9 @@ packages: bcp-47-match@2.0.3: resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + birpc@2.8.0: resolution: {integrity: sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw==} @@ -1793,6 +1870,9 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + chevrotain@6.5.0: + resolution: {integrity: sha512-BwqQ/AgmKJ8jcMEjaSnfMybnKMgGTrtDKowfTP3pX4jwVy0kNjRsT/AP6h+wC3+3NC+X8X15VWBnTCQlX+wQFg==} + chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -1855,6 +1935,9 @@ packages: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + core-js@3.47.0: + resolution: {integrity: sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg==} + cross-fetch@4.0.0: resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} @@ -1977,6 +2060,9 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2157,6 +2243,9 @@ packages: h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} + handsontable@16.2.0: + resolution: {integrity: sha512-4zhMQON9DPyip/6YIPH2G7jN+QEJ0uabCZruhrhOqTqr3Qf/FDjsTInUaEzMCmhhdii5MbA6PGyLfUad6t1sXA==} + hast-util-from-html@2.0.3: resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} @@ -2241,6 +2330,9 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + hyperformula@3.1.1: + resolution: {integrity: sha512-v+yvRPZGL73KinH2lvS4/1QMe2xNviTfgIcVgKjzKGi66xEuvuoDRgQ48ODc4XhD+c+JLNfs9Ln1GnHQ5TDNGA==} + i18next-browser-languagedetector@8.2.0: resolution: {integrity: sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==} @@ -2432,6 +2524,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -2641,6 +2737,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -2694,6 +2793,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + numbro@2.5.0: + resolution: {integrity: sha512-xDcctDimhzko/e+y+Q2/8i3qNC9Svw1QgOkSkQoO0kIPI473tR9QRbo2KP88Ty9p8WbPy+3OpTaAIzehtuHq+A==} + ofetch@1.5.1: resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} @@ -2732,6 +2834,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + papaparse@5.5.3: + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -2824,6 +2929,12 @@ packages: peerDependencies: react: ^19.2.3 + react-hook-form@7.69.0: + resolution: {integrity: sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-i18next@15.7.4: resolution: {integrity: sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==} peerDependencies: @@ -2873,6 +2984,12 @@ packages: '@types/react': optional: true + react-resizable-panels@4.1.0: + resolution: {integrity: sha512-8ZpOwdKQz6bCs2LGnfS6HuBITxkOLelSMzBX4DrWsgHaU3ukTPxmBNAeK8Bsp3LAEdtXeG6ll6UPN7OJNua4sw==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -2926,6 +3043,9 @@ packages: regex@6.0.1: resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + regexp-to-ast@0.4.0: + resolution: {integrity: sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==} + rehype-attr@3.0.3: resolution: {integrity: sha512-Up50Xfra8tyxnkJdCzLBIBtxOcB2M1xdeKe1324U06RAvSjYm7ULSeoM+b/nYPQPVd7jsXJ9+39IG1WAJPXONw==} engines: {node: '>=16'} @@ -3159,6 +3279,9 @@ packages: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} + tiny-emitter@2.1.0: + resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==} + tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} @@ -4037,6 +4160,8 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true + '@handsontable/pikaday@1.0.0': {} + '@img/colour@1.0.0': optional: true @@ -4145,20 +4270,27 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@kevisual/api@0.0.10': + '@kevisual/api@0.0.14': dependencies: - '@kevisual/js-filter': 0.0.2 + '@kevisual/js-filter': 0.0.3 '@kevisual/load': 0.0.6 es-toolkit: 1.43.0 + eventemitter3: 5.0.1 nanoid: 5.1.6 '@kevisual/cache@0.0.3': dependencies: idb-keyval: 6.2.2 + '@kevisual/cache@0.0.5': + dependencies: + idb-keyval: 6.2.2 + lru-cache: 11.2.4 + nanoid: 5.1.6 + '@kevisual/context@0.0.4': {} - '@kevisual/js-filter@0.0.2': {} + '@kevisual/js-filter@0.0.3': {} '@kevisual/load@0.0.6': dependencies: @@ -4190,8 +4322,9 @@ snapshots: - react-native - typescript - '@kevisual/router@0.0.51': + '@kevisual/router@0.0.52': dependencies: + eventemitter3: 5.0.1 path-to-regexp: 8.3.0 selfsigned: 5.4.0 send: 1.2.1 @@ -4328,6 +4461,22 @@ snapshots: '@radix-ui/primitive@1.1.3': {} + '@radix-ui/react-checkbox@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)': + 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-presence': 1.1.5(@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-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-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.3)': dependencies: react: 19.2.3 @@ -4399,6 +4548,15 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-label@2.1.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)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@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) + 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-portal@1.1.9(@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/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) @@ -4485,6 +4643,19 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.7)(react@19.2.3)': + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.7 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.7)(react@19.2.3)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.3) + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.7 + '@rc-component/async-validator@5.0.4': dependencies: '@babel/runtime': 7.28.4 @@ -4648,7 +4819,7 @@ 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(dayjs@1.11.19)(moment@2.30.1)(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) @@ -4659,6 +4830,7 @@ snapshots: react-dom: 19.2.3(react@19.2.3) optionalDependencies: dayjs: 1.11.19 + moment: 2.30.1 '@rc-component/portal@2.0.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: @@ -5091,6 +5263,9 @@ snapshots: dependencies: '@types/node': 17.0.45 + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -5317,7 +5492,7 @@ snapshots: ansi-styles@6.2.3: {} - antd@6.1.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + antd@6.1.3(moment@2.30.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@ant-design/colors': 8.0.0 '@ant-design/cssinjs': 2.0.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -5343,7 +5518,7 @@ 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(dayjs@1.11.19)(moment@2.30.1)(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) @@ -5513,6 +5688,8 @@ snapshots: bcp-47-match@2.0.3: {} + bignumber.js@9.3.1: {} + birpc@2.8.0: {} boolbase@1.0.0: {} @@ -5562,6 +5739,11 @@ snapshots: character-reference-invalid@2.0.1: {} + chevrotain@6.5.0: + dependencies: + regexp-to-ast: 0.4.0 + optional: true + chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -5612,6 +5794,8 @@ snapshots: dependencies: is-what: 5.5.0 + core-js@3.47.0: {} + cross-fetch@4.0.0: dependencies: node-fetch: 2.7.0 @@ -5719,6 +5903,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@3.2.2: dependencies: dom-serializer: 2.0.0 @@ -5930,6 +6118,16 @@ snapshots: ufo: 1.6.1 uncrypto: 0.1.3 + handsontable@16.2.0: + dependencies: + '@handsontable/pikaday': 1.0.0 + core-js: 3.47.0 + dompurify: 3.3.1 + moment: 2.30.1 + numbro: 2.5.0 + optionalDependencies: + hyperformula: 3.1.1 + hast-util-from-html@2.0.3: dependencies: '@types/hast': 3.0.4 @@ -6126,6 +6324,12 @@ snapshots: human-signals@8.0.1: {} + hyperformula@3.1.1: + dependencies: + chevrotain: 6.5.0 + tiny-emitter: 2.1.0 + optional: true + i18next-browser-languagedetector@8.2.0: dependencies: '@babel/runtime': 7.28.4 @@ -6266,6 +6470,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.4: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -6743,6 +6949,8 @@ snapshots: mitt@3.0.1: {} + moment@2.30.1: {} + mrmime@2.0.1: {} ms@2.1.3: {} @@ -6778,6 +6986,10 @@ snapshots: dependencies: boolbase: 1.0.0 + numbro@2.5.0: + dependencies: + bignumber.js: 9.3.1 + ofetch@1.5.1: dependencies: destr: 2.0.5 @@ -6820,6 +7032,8 @@ snapshots: pako@0.2.9: {} + papaparse@5.5.3: {} + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -6910,6 +7124,10 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 + react-hook-form@7.69.0(react@19.2.3): + dependencies: + react: 19.2.3 + react-i18next@15.7.4(i18next@25.6.0(typescript@5.9.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3): dependencies: '@babel/runtime': 7.28.4 @@ -6960,6 +7178,11 @@ snapshots: optionalDependencies: '@types/react': 19.2.7 + react-resizable-panels@4.1.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-style-singleton@2.2.3(@types/react@19.2.7)(react@19.2.3): dependencies: get-nonce: 1.0.1 @@ -7026,6 +7249,9 @@ snapshots: dependencies: regex-utilities: 2.3.0 + regexp-to-ast@0.4.0: + optional: true + rehype-attr@3.0.3: dependencies: unified: 11.0.5 @@ -7396,6 +7622,9 @@ snapshots: throttle-debounce@5.0.2: {} + tiny-emitter@2.1.0: + optional: true + tiny-inflate@1.0.3: {} tinyexec@1.0.2: {} diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml new file mode 100644 index 0000000..2e35c96 --- /dev/null +++ b/web/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - core-js diff --git a/web/src/apps/studio/index.tsx b/web/src/apps/studio/index.tsx index 95b2069..2eed08a 100644 --- a/web/src/apps/studio/index.tsx +++ b/web/src/apps/studio/index.tsx @@ -2,9 +2,18 @@ import { toast, ToastContainer } from 'react-toastify'; import { useStudioStore } from './store.ts'; import { useEffect, useState } from 'react'; import { MonitorPlay, Play } from 'lucide-react'; +import { Panel, Group } from 'react-resizable-panels' +import { ViewList } from '../view/list.tsx'; export const AppProvider = () => { - return
- + return
+ + + + + + + + { - const { routes, getRoutes, run } = useStudioStore(); + const { routes, getRouteList, run } = useStudioStore(); const [expandedIds, setExpandedIds] = useState>(new Set()); const [visibleIds, setVisibleIds] = useState>(new Set()); useEffect(() => { - getRoutes(); + getRouteList(); }, []); const toggleDescription = (id: string) => { @@ -58,7 +67,7 @@ export const App = () => { }; return ( -
+
{routes.map((route: RouteItem) => { const isExpanded = expandedIds.has(route.id); diff --git a/web/src/apps/studio/store.ts b/web/src/apps/studio/store.ts index f7b6126..b0d480e 100644 --- a/web/src/apps/studio/store.ts +++ b/web/src/apps/studio/store.ts @@ -1,10 +1,9 @@ import { create } from 'zustand'; -import { QueryProxy, ProxyItem } from '@kevisual/api' -// import { query } from '@/modules/query.ts' -import { QueryClient } from '@kevisual/query'; +import { QueryProxy, RouterViewData } from '@kevisual/api' +import { query } from '@/modules/query.ts' import { toast } from 'react-toastify'; -import { QueryRouterServer } from '@kevisual/router/src/route.ts' - +import { use } from '@kevisual/context' +import { MyCache } from '@kevisual/cache' type RouteItem = { id: string; @@ -13,58 +12,81 @@ type RouteItem = { description?: string; metadata?: Record; } -// type ProxyItem = { -// title?: string; -// type?: 'api' | 'context' | 'page'; -// description?: string; -// api?: { -// url: string; -// }, -// context?: { -// key: string; -// }, -// page?: {}, -// where?: string; -// whereList?: Array<{ title: string; where: string }>; -// } + +type RouteViewList = Array; + interface StudioState { routes: Array; - getRoutes: () => Promise; + getRouteList: () => Promise; run: (route: RouteItem) => Promise; queryProxy?: QueryProxy; - router?: QueryRouterServer; - init: (opts?: { url?: string }) => Promise<{ router: QueryRouterServer; queryProxy: QueryProxy }>; - - proxy?: ProxyItem; - setProxy?: (proxy: ProxyItem) => void; - proxyList?: ProxyItem[]; - setProxyList?: (list: ProxyItem[]) => void; + init: (force?: boolean) => Promise<{ queryProxy: QueryProxy }>; + routeViewList: RouteViewList; + getViewList: () => Promise; + getCurrentView: () => Promise; + updateRouteView: (view: RouterViewData) => Promise; + deleteRouteView: (id: string) => Promise; + currentView?: RouterViewData; } const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); export const useStudioStore = create((set, get) => ({ routes: [], - getRoutes: async () => { - const state = get(); + getRouteList: async () => { + await get().getCurrentView(); + const state = await get().init(); + let queryProxy = state.queryProxy; - if (!queryProxy) { - const init = await state.init(); - queryProxy = init.queryProxy; - } - console.log('query proxy', queryProxy.router); - const routes: any[] = await queryProxy.listRoutes(() => true, "") - console.log('fetched routes', routes); + const url = new URL(window.location.href); + const viewId = url.searchParams.get('viewId') || 'default'; + const routes: any[] = await queryProxy.listRoutes(() => true, { viewId }); set({ routes }); + get().getViewList(); + }, + getViewList: async () => { + const res = await query.post({ path: 'views', key: 'list' }); + if (res.code === 200) { + const list = res.data.list as RouteViewList || []; + set({ routeViewList: list }); + } + }, + getCurrentView: async () => { + const url = new URL(window.location.href); + const viewId = url.searchParams.get('viewId'); + if (!viewId) { + return; + } + const res = await query.post({ path: 'views', key: 'current', data: { viewId: viewId } }); + if (res.code === 200) { + const view = res.data as RouterViewData; + set({ currentView: view }); + } else { + set({ currentView: undefined }); + } + }, + routeViewList: [], + updateRouteView: async (view: RouterViewData) => { + const res = await query.post({ path: 'views', key: 'update', data: view }); + if (res.code !== 200) { + toast.error(`视图更新失败:${res.message || '未知错误'}`); + return; + } else { + get().getViewList(); + toast.success('视图更新成功'); + } + }, + deleteRouteView: async (id: string) => { + const res = await query.post({ path: 'views', key: 'delete', data: { id } }); + if (res.code !== 200) { + toast.error(`视图删除失败:${res.message || '未知错误'}`); + return; + } + get().getViewList(); + toast.success('视图删除成功'); }, run: async (route: RouteItem) => { - const state = get(); - let queryProxy = state.queryProxy!; - if (!state.queryProxy) { - const init = await state.init(); - queryProxy = init.queryProxy; - } - console.log('running route', route, queryProxy.query.url); + const state = await get().init(); + let queryProxy = state.queryProxy; const res = await queryProxy.run({ path: route.path, key: route.key }); - console.log('route run result', res); if (res.code !== 200) { toast.error(`运行失败:${res.message || '未知错误'}`); } else if (res.code === 200) { @@ -73,51 +95,38 @@ export const useStudioStore = create((set, get) => ({ }, queryProxy: undefined, router: undefined, - init: async () => { - const proxy = get().proxy || localStorageProxy.get(); - const url = proxy.type === 'api' && proxy.api ? proxy.api.url : '/client/router'; + init: async (force?: boolean) => { // let _url = 'http://localhost:52002/api/router'; let _url = 'http://localhost:52000/api/router'; // let _url = '/api/router'; - - const query = new QueryClient({ - url: _url, + let queryProxy = get().queryProxy; + if (queryProxy && !force) { + return { queryProxy }; + } + let currentView: RouterViewData | undefined = get().currentView; + console.log('currentView in init', currentView); + const routerViewData: RouterViewData = currentView || { + views: [{ + id: 'default', + title: '默认视图', + query: `WHERE path = 'file' ` + }], + data: { + items: [] + }, + viewId: 'default', + } + console.log('initializing query proxy with view', routerViewData); + queryProxy = new QueryProxy({ + routerViewData }); - const router = new QueryRouterServer(); - const queryProxy = new QueryProxy({ query, router }); await queryProxy.init(); - set({ queryProxy, router }); - return { router, queryProxy } - }, - proxy: undefined, - setProxy: (proxy: ProxyItem) => { - localStorageProxy.set(proxy); - set({ proxy }); - }, - proxyList: [], - setProxyList: (list: ProxyItem[]) => { - set({ proxyList: list }); + await sleep(500); + set({ queryProxy }); + return { queryProxy } }, })); -export const localStorageProxy = { - get: (): ProxyItem => { - const data = localStorage.getItem('PROXY_CONFIG') - if (data) { - return JSON.parse(data) - } - const defult: ProxyItem = { - title: '默认', - description: '默认', - type: 'api', - api: { - url: '/client/router' - }, - } - localStorageProxy.set(defult) - return defult; - }, - set: (proxy: ProxyItem) => { - localStorage.setItem('PROXY_CONFIG', JSON.stringify(proxy)) - } -} \ No newline at end of file +use('studioStore', () => { + return useStudioStore.getState(); +}); \ No newline at end of file diff --git a/web/src/apps/view/components/DataItemForm.tsx b/web/src/apps/view/components/DataItemForm.tsx new file mode 100644 index 0000000..2dca55e --- /dev/null +++ b/web/src/apps/view/components/DataItemForm.tsx @@ -0,0 +1,200 @@ +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" +import { Checkbox } from "@/components/ui/checkbox" +import { Query } from "@kevisual/query" +import { QueryRouterServer } from "@kevisual/router" +import { nanoid } from "nanoid" + +export type RouterViewItem = RouterViewApi | RouterViewContext | RouterViewWorker; +type RouteViewBase = { + id: string; + title: string; + description: string; + enabled?: boolean; +} +export type RouterViewApi = { + type: 'api', + api: { + url: string, + // 已初始化的query实例,不需要编辑配置 + query?: Query + } +} & RouteViewBase; + +export type RouterViewContext = { + type: 'context', + context: { + key: string, + // 从context中获取router,不需要编辑配置 + router?: QueryRouterServer + } +} & RouteViewBase; +export type RouterViewWorker = { + type: 'worker', + worker: { + type: 'Worker' | 'SharedWorker' | 'serviceWorker', + url: string, + // 已初始化的worker实例,不需要编辑配置 + worker?: Worker | SharedWorker | ServiceWorker, + /** + * worker选项 + * default: { type: 'module' } + */ + workerOptions?: { + type: 'module' | 'classic' + } + } +} & RouteViewBase; +interface DataItemFormProps { + item: RouterViewItem + onChange: (item: any) => void + onRemove: () => void +} + +export const DataItemForm = ({ item, onChange, onRemove }: DataItemFormProps) => { + const handleChange = (field: string, value: any) => { + if (field === 'type') { + const newItem: RouterViewItem = { ...item, type: value } + if (value === 'api' && !('api' in item)) { + (newItem as RouterViewApi).api = { url: '' } + } else if (value === 'context' && !('context' in item)) { + (newItem as RouterViewContext).context = { key: '' } + } else if (value === 'worker' && !('worker' in item)) { + (newItem as RouterViewWorker).worker = { type: 'Worker', url: '', workerOptions: { type: 'module' } } + } + if (!newItem.id) { + newItem.id = nanoid(16) + } + onChange(newItem) + } else { + onChange({ ...item, [field]: value }) + } + } + + const handleNestedChange = (parent: string, field: string, value: any) => { + const parentValue = item[parent as keyof RouterViewItem] as Record | undefined + const newParentValue: Record = { + ...(parentValue || {}), + [field]: value + } + onChange({ ...item, [parent]: newParentValue }) + } + + const handleNestedDeepChange = (parent: string, nestedParent: string, field: string, value: any) => { + const parentValue = item[parent as keyof RouterViewItem] as Record | undefined + const nestedValue = parentValue?.[nestedParent] as Record | undefined + const newNestedValue: Record = { + ...(nestedValue || {}), + [field]: value + } + const newParentValue: Record = { + ...(parentValue || {}), + [nestedParent]: newNestedValue + } + onChange({ ...item, [parent]: newParentValue }) + } + + return ( +
+
+

数据项配置

+ +
+ +
+ + handleChange('title', e.target.value)} + placeholder="输入标题" + /> +
+ +
+ + +
+ +
+ handleChange('enabled', checked)} + /> + +
+ + {(item.type === 'api') && ( +
+ + handleNestedChange('api', 'url', e.target.value)} + placeholder="输入 API 地址" + /> +
+ )} + + {item.type === 'context' && ( +
+ + handleNestedChange('context', 'key', e.target.value)} + placeholder="输入 Context Key" + /> +
+ )} + + {item.type === 'worker' && ( +
+
+ + +
+
+ + handleNestedChange('worker', 'url', e.target.value)} + placeholder="输入 Worker URL" + /> +
+
+ + +
+
+ )} +
+ ) +} diff --git a/web/src/apps/view/components/ViewEditor.tsx b/web/src/apps/view/components/ViewEditor.tsx new file mode 100644 index 0000000..9c30e9b --- /dev/null +++ b/web/src/apps/view/components/ViewEditor.tsx @@ -0,0 +1,146 @@ +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { DataItemForm } from "@/apps/view/components/DataItemForm" +import { ViewFormItem } from "@/apps/view/components/ViewFormItem" +import { nanoid } from "nanoid" + +interface ViewEditorProps { + open: boolean + onOpenChange: (open: boolean) => void + data?: { + id?: string + title?: string + data?: { items: any[] } + views?: any[] + } + onSave: (data: any) => void +} + +export const ViewEditor = ({ open, onOpenChange, data, onSave }: ViewEditorProps) => { + const [title, setTitle] = useState('') + const [dataItems, setDataItems] = useState([]) + const [views, setViews] = useState([]) + + const isUpdate = !!data?.id + + useEffect(() => { + if (open) { + setTitle(data?.title || '') + setDataItems(data?.data?.items || []) + setViews(data?.views || []) + } + }, [open, data]) + + const handleAddDataItem = () => { + setDataItems([...dataItems, { type: 'api', api: { url: '' } }]) + } + + const handleUpdateDataItem = (index: number, item: any) => { + const newItems = [...dataItems] + newItems[index] = item + setDataItems(newItems) + } + + const handleRemoveDataItem = (index: number) => { + setDataItems(dataItems.filter((_, i) => i !== index)) + } + + const handleAddView = () => { + setViews([...views, { id: nanoid(16), title: '', query: '' }]) + } + + const handleUpdateView = (index: number, view: any) => { + const newViews = [...views] + newViews[index] = view + setViews(newViews) + } + + const handleRemoveView = (index: number) => { + setViews(views.filter((_, i) => i !== index)) + } + + const handleSave = () => { + const viewData = { + id: data?.id, + title, + data: { + items: dataItems + }, + views + } + onSave(viewData) + onOpenChange(false) + } + + const handleClose = () => { + onOpenChange(false) + } + + return ( + + + + {isUpdate ? '编辑视图' : '新增视图'} + + +
+
+ + setTitle(e.target.value)} + placeholder="输入视图标题" + /> +
+ +
+
+

数据项配置 (data.items)

+ +
+ {dataItems.map((item, index) => ( + handleUpdateDataItem(index, newItem)} + onRemove={() => handleRemoveDataItem(index)} + /> + ))} +
+ +
+
+

视图配置 (views)

+ +
+ {views.map((view, index) => ( + handleUpdateView(index, newView)} + onRemove={() => handleRemoveView(index)} + /> + ))} +
+
+ + + + + +
+
+ ) +} diff --git a/web/src/apps/view/components/ViewFormItem.tsx b/web/src/apps/view/components/ViewFormItem.tsx new file mode 100644 index 0000000..5b9271f --- /dev/null +++ b/web/src/apps/view/components/ViewFormItem.tsx @@ -0,0 +1,58 @@ +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" + +interface ViewFormProps { + view: any + onChange: (view: any) => void + onRemove: () => void +} + +export const ViewFormItem = ({ view, onChange, onRemove }: ViewFormProps) => { + const handleChange = (field: string, value: any) => { + onChange({ ...view, [field]: value }) + } + + return ( +
+
+

视图配置

+ +
+ +
+
+ + handleChange('id', e.target.value)} + placeholder="自动生成" + disabled + /> +
+
+ + handleChange('title', e.target.value)} + placeholder="输入视图标题" + /> +
+
+ +
+ + handleChange('query', e.target.value)} + placeholder="输入查询语句" + /> +
+
+ ) +} diff --git a/web/src/apps/view/form.ts b/web/src/apps/view/form.ts new file mode 100644 index 0000000..8900927 --- /dev/null +++ b/web/src/apps/view/form.ts @@ -0,0 +1,10 @@ +import { useForm } from 'react-hook-form'; +import { RouterViewQuery, RouterViewItem } from '@kevisual/api'; +type ViewFormData = { + id?: string; + title: string; + data?: { + items: RouterViewItem[]; + }, + views: RouterViewQuery[]; +}; \ No newline at end of file diff --git a/web/src/apps/view/list.tsx b/web/src/apps/view/list.tsx new file mode 100644 index 0000000..f89d1d4 --- /dev/null +++ b/web/src/apps/view/list.tsx @@ -0,0 +1,129 @@ +import { useState } from "react"; +import { useStudioStore } from '../studio/store.ts'; +import { Search, RotateCw, Plus, MoreHorizontal, Layout, Edit2, Trash2 } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { ViewEditor } from "@/apps/view/components/ViewEditor.tsx"; + +export const ViewList = () => { + const { routeViewList, updateRouteView, deleteRouteView } = useStudioStore(); + const [selectedItems, setSelectedItems] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [editorOpen, setEditorOpen] = useState(false); + const [editingView, setEditingView] = useState(null); + + const filteredViews = routeViewList.filter(view => + (view.title || '未命名视图').toLowerCase().includes(searchTerm.toLowerCase()) || + (view.description || '').toLowerCase().includes(searchTerm.toLowerCase()) + ); + + + const handleRefresh = () => { + // 刷新逻辑 + }; + + const handleAdd = () => { + handleEdit({}); + }; + + const handleEdit = (view: any) => { + setEditingView(view); + setEditorOpen(true); + }; + + const handleDelete = (id: string) => { + if (confirm('确定要删除这个视图吗?')) { + deleteRouteView(id); + } + }; + + const handleSaveView = (viewData: any) => { + updateRouteView(viewData); + }; + + return ( +
+
+
+ setSearchTerm(e.target.value)} + /> + +
+ + +
+ + + + + 视图名称 + + + + + {filteredViews.length === 0 ? ( + + + {searchTerm ? '未找到匹配的视图' : '暂无视图'} + + + ) : ( + filteredViews.map((view) => ( + + +
+ + {view.title || '未命名视图'} +
+
+ +
+ + +
+
+
+ )) + )} +
+
+ + +
+ ); +} \ No newline at end of file diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx index 21409a0..37a7d4b 100644 --- a/web/src/components/ui/button.tsx +++ b/web/src/components/ui/button.tsx @@ -38,8 +38,8 @@ const buttonVariants = cva( function Button({ className, - variant, - size, + variant = "default", + size = "default", asChild = false, ...props }: React.ComponentProps<"button"> & @@ -51,6 +51,8 @@ function Button({ return ( diff --git a/web/src/components/ui/checkbox.tsx b/web/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..0e2a6cd --- /dev/null +++ b/web/src/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx new file mode 100644 index 0000000..701245b --- /dev/null +++ b/web/src/components/ui/dialog.tsx @@ -0,0 +1,42 @@ +import * as React from "react" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +const Dialog = ({ open, onOpenChange, children }: { open: boolean; onOpenChange: (open: boolean) => void; children: React.ReactNode }) => { + if (!open) return null + + return ( +
+
onOpenChange(false)} /> +
+ {children} +
+
+ ) +} + +const DialogHeader = ({ className, children, ...props }: React.HTMLAttributes) => ( +
+ {children} +
+) + +const DialogTitle = ({ className, children, ...props }: React.HTMLAttributes) => ( +

+ {children} +

+) + +const DialogContent = ({ className, children, ...props }: React.HTMLAttributes) => ( +
+ {children} +
+) + +const DialogFooter = ({ className, children, ...props }: React.HTMLAttributes) => ( +
+ {children} +
+) + +export { Dialog, DialogHeader, DialogTitle, DialogContent, DialogFooter } diff --git a/web/src/components/ui/input.tsx b/web/src/components/ui/input.tsx new file mode 100644 index 0000000..8916905 --- /dev/null +++ b/web/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/web/src/components/ui/label.tsx b/web/src/components/ui/label.tsx new file mode 100644 index 0000000..ef7133a --- /dev/null +++ b/web/src/components/ui/label.tsx @@ -0,0 +1,22 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/web/src/components/ui/table.tsx b/web/src/components/ui/table.tsx new file mode 100644 index 0000000..5513a5c --- /dev/null +++ b/web/src/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/web/src/modules/query.ts b/web/src/modules/query.ts index 22b6bd1..15bea98 100644 --- a/web/src/modules/query.ts +++ b/web/src/modules/query.ts @@ -1,4 +1,4 @@ -import { Query } from '@kevisual/query' +import { QueryClient } from '@kevisual/query' const getUrl = () => { const host = window.location.host @@ -10,6 +10,10 @@ const getUrl = () => { return '/client/router' } -export const query = new Query({ - url: getUrl() +export const query = new QueryClient({ + url: '/api/router', +}); + +export const queryClient = new QueryClient({ + url: getUrl(), }); \ No newline at end of file