From 15af405d02a2128e38197a73a97cb51bb91bb82e Mon Sep 17 00:00:00 2001 From: abearxiong Date: Mon, 20 Oct 2025 05:45:19 +0800 Subject: [PATCH] update --- pnpm-lock.yaml | 148 +++++++++ server/code/test/demo/main.ts | 14 + server/package.json | 1 + server/src/cache/index.ts | 6 +- server/src/index.ts | 4 +- server/src/routes/file-code/index.ts | 31 +- server/src/test/common.ts | 5 + server/src/test/test-upload.ts | 48 +++ web/.env.example | 6 + web/package.json | 4 + web/src/apps/login/AuthProvider.tsx | 17 + web/src/apps/login/DashboardApp.tsx | 70 ++++ web/src/apps/login/LoginForm.tsx | 200 +++++++++++ web/src/apps/login/LoginPage.tsx | 23 ++ web/src/apps/login/ProtectedRoute.tsx | 103 ++++++ web/src/apps/login/UserInfo.tsx | 110 ++++++ web/src/apps/muse/base/index.tsx | 70 ++++ web/src/apps/muse/base/mock/collection.ts | 183 ++++++++++ web/src/apps/muse/base/table/DetailModal.tsx | 153 +++++++++ web/src/apps/muse/base/table/README.md | 180 ++++++++++ web/src/apps/muse/base/table/Table.tsx | 306 +++++++++++++++++ web/src/apps/muse/base/table/index.tsx | 281 ++++++++++++++++ web/src/apps/muse/base/table/modal.css | 305 +++++++++++++++++ web/src/apps/muse/base/table/table.css | 312 ++++++++++++++++++ web/src/apps/muse/base/table/types.ts | 58 ++++ web/src/apps/muse/index.tsx | 126 +++++++ web/src/apps/muse/prompts/index.tsx | 190 +++++++++++ web/src/apps/muse/videos/modules/VadVoice.tsx | 130 ++++++++ web/src/apps/muse/videos/modules/style.css | 5 + web/src/env.d.ts | 9 + web/src/lib/pocketbase.ts | 143 ++++++++ web/src/pages/api/login.ts | 85 +++++ web/src/pages/api/logout.ts | 42 +++ web/src/pages/dashboard.astro | 14 + web/src/pages/login.astro | 14 + web/src/pages/muse.astro | 8 + web/src/store/authStore.ts | 171 ++++++++++ 37 files changed, 3570 insertions(+), 5 deletions(-) create mode 100644 server/code/test/demo/main.ts create mode 100644 server/src/test/common.ts create mode 100644 server/src/test/test-upload.ts create mode 100644 web/.env.example create mode 100644 web/src/apps/login/AuthProvider.tsx create mode 100644 web/src/apps/login/DashboardApp.tsx create mode 100644 web/src/apps/login/LoginForm.tsx create mode 100644 web/src/apps/login/LoginPage.tsx create mode 100644 web/src/apps/login/ProtectedRoute.tsx create mode 100644 web/src/apps/login/UserInfo.tsx create mode 100644 web/src/apps/muse/base/index.tsx create mode 100644 web/src/apps/muse/base/mock/collection.ts create mode 100644 web/src/apps/muse/base/table/DetailModal.tsx create mode 100644 web/src/apps/muse/base/table/README.md create mode 100644 web/src/apps/muse/base/table/Table.tsx create mode 100644 web/src/apps/muse/base/table/index.tsx create mode 100644 web/src/apps/muse/base/table/modal.css create mode 100644 web/src/apps/muse/base/table/table.css create mode 100644 web/src/apps/muse/base/table/types.ts create mode 100644 web/src/apps/muse/index.tsx create mode 100644 web/src/apps/muse/prompts/index.tsx create mode 100644 web/src/apps/muse/videos/modules/VadVoice.tsx create mode 100644 web/src/apps/muse/videos/modules/style.css create mode 100644 web/src/env.d.ts create mode 100644 web/src/lib/pocketbase.ts create mode 100644 web/src/pages/api/login.ts create mode 100644 web/src/pages/api/logout.ts create mode 100644 web/src/pages/dashboard.astro create mode 100644 web/src/pages/login.astro create mode 100644 web/src/pages/muse.astro create mode 100644 web/src/store/authStore.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fd4539..20ac5e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@kevisual/noco': specifier: ^0.0.1 version: 0.0.1 + '@kevisual/query': + specifier: ^0.0.29 + version: 0.0.29(zod@3.25.76) '@kevisual/router': specifier: ^0.0.29 version: 0.0.29 @@ -54,6 +57,9 @@ importers: '@astrojs/sitemap': specifier: ^3.6.0 version: 3.6.0 + '@faker-js/faker': + specifier: ^10.1.0 + version: 10.1.0 '@kevisual/noco': specifier: ^0.0.1 version: 0.0.1 @@ -66,6 +72,9 @@ importers: '@kevisual/registry': specifier: ^0.0.1 version: 0.0.1(typescript@5.9.3) + '@ricky0123/vad-web': + specifier: ^0.0.28 + version: 0.0.28 '@tailwindcss/vite': specifier: ^4.1.14 version: 4.1.14(vite@6.3.7(@types/node@24.7.2)(jiti@2.6.1)(lightningcss@1.30.1)) @@ -96,12 +105,18 @@ importers: nanoid: specifier: ^5.1.6 version: 5.1.6 + pocketbase: + specifier: ^0.26.2 + version: 0.26.2 react: specifier: ^19.2.0 version: 19.2.0 react-dom: specifier: ^19.2.0 version: 19.2.0(react@19.2.0) + react-resizable-panels: + specifier: ^3.0.6 + version: 3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) react-toastify: specifier: ^11.0.5 version: 11.0.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -433,6 +448,10 @@ packages: cpu: [x64] os: [win32] + '@faker-js/faker@10.1.0': + resolution: {integrity: sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==} + engines: {node: ^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0, npm: '>=10'} + '@img/colour@1.0.0': resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} engines: {node: '>=18'} @@ -645,6 +664,39 @@ packages: '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@ricky0123/vad-web@0.0.28': + resolution: {integrity: sha512-Hvw8jN3r1SBxmjJa89HITxRcwlT6dc7CQPVtVQLrqfY8EeQcx41QeqKUol4lw8ZCeAIHKwYndHnB1K/4SAQJgQ==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -1388,6 +1440,9 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + flatbuffers@25.9.23: + resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==} + flattie@1.1.1: resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} engines: {node: '>=8'} @@ -1428,6 +1483,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + guid-typescript@1.0.9: + resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} + h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} @@ -1676,6 +1734,9 @@ packages: lodash-es@4.17.21: resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -1960,6 +2021,12 @@ packages: oniguruma-to-es@4.3.3: resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==} + onnxruntime-common@1.23.0: + resolution: {integrity: sha512-Auz8S9D7vpF8ok7fzTobvD1XdQDftRf/S7pHmjeCr3Xdymi4z1C7zx4vnT6nnUjbpelZdGwda0BmWHCCTMKUTg==} + + onnxruntime-web@1.23.0: + resolution: {integrity: sha512-w0bvC2RwDxphOUFF8jFGZ/dYw+duaX20jM6V4BIZJPCfK4QuCpB/pVREV+hjYbT3x4hyfa2ZbTaWx4e1Vot0fQ==} + openai@5.23.2: resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==} hasBin: true @@ -2016,6 +2083,9 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + platform@1.3.6: + resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} + pocketbase@0.26.2: resolution: {integrity: sha512-WA8EOBc3QnSJh8rJ3iYoi9DmmPOMFIgVfAmIGux7wwruUEIzXgvrO4u0W2htfQjGIcyezJkdZOy5Xmh7SxAftw==} @@ -2037,6 +2107,10 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -2072,6 +2146,12 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-resizable-panels@3.0.6: + resolution: {integrity: sha512-b3qKHQ3MLqOgSS+FRYKapNkJZf5EQzuf6+RLiq1/IlTHw99YrZ2NJZLk4hQIzTnnIkRg2LUqyVinu6YWWpUYew==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-toastify@11.0.5: resolution: {integrity: sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==} peerDependencies: @@ -2948,6 +3028,8 @@ snapshots: '@esbuild/win32-x64@0.25.11': optional: true + '@faker-js/faker@10.1.0': {} + '@img/colour@1.0.0': optional: true @@ -3174,6 +3256,33 @@ snapshots: '@oslojs/encoding@1.1.0': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@ricky0123/vad-web@0.0.28': + dependencies: + onnxruntime-web: 1.23.0 + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/plugin-commonjs@28.0.7(rollup@4.52.4)': @@ -3926,6 +4035,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + flatbuffers@25.9.23: {} + flattie@1.1.1: {} fontace@0.3.1: @@ -3964,6 +4075,8 @@ snapshots: graceful-fs@4.2.11: {} + guid-typescript@1.0.9: {} + h3@1.15.4: dependencies: cookie-es: 1.2.2 @@ -4260,6 +4373,8 @@ snapshots: lodash-es@4.17.21: {} + long@5.3.2: {} + longest-streak@3.1.0: {} lru-cache@10.4.3: {} @@ -4794,6 +4909,17 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 + onnxruntime-common@1.23.0: {} + + onnxruntime-web@1.23.0: + dependencies: + flatbuffers: 25.9.23 + guid-typescript: 1.0.9 + long: 5.3.2 + onnxruntime-common: 1.23.0 + platform: 1.3.6 + protobufjs: 7.5.4 + openai@5.23.2(zod@3.25.76): optionalDependencies: zod: 3.25.76 @@ -4846,6 +4972,8 @@ snapshots: picomatch@4.0.3: {} + platform@1.3.6: {} + pocketbase@0.26.2: {} postcss@8.5.6: @@ -4865,6 +4993,21 @@ snapshots: property-information@7.1.0: {} + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 24.7.2 + long: 5.3.2 + queue-microtask@1.2.3: {} radix3@1.1.2: {} @@ -4888,6 +5031,11 @@ snapshots: react-refresh@0.17.0: {} + react-resizable-panels@3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0): + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-toastify@11.0.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: clsx: 2.1.1 diff --git a/server/code/test/demo/main.ts b/server/code/test/demo/main.ts new file mode 100644 index 0000000..09340d9 --- /dev/null +++ b/server/code/test/demo/main.ts @@ -0,0 +1,14 @@ +import { QueryRouterServer } from "@kevisual/router"; + +const app = new QueryRouterServer(); + +app.route({ + path: 'main' +}).define(async (ctx) => { + ctx.body = { + message: 'this is main. filename: test/demo/main.ts', + params: ctx.query + } +}).addTo(app) + +app.wait() \ No newline at end of file diff --git a/server/package.json b/server/package.json index 0086636..f0c2991 100644 --- a/server/package.json +++ b/server/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@kevisual/noco": "^0.0.1", + "@kevisual/query": "^0.0.29", "@kevisual/router": "^0.0.29", "fast-glob": "^3.3.3", "pocketbase": "^0.26.2", diff --git a/server/src/cache/index.ts b/server/src/cache/index.ts index 319eee4..768001d 100644 --- a/server/src/cache/index.ts +++ b/server/src/cache/index.ts @@ -11,13 +11,13 @@ export const storage = createStorage({ export const codeStorage = createStorage({ // @ts-ignore driver: fsLiteDriver({ - base: './code' + base: codeRoot }) }) // storage.setItem('test-ke/test-key.json', 'test-value'); // console.log('Cache test-key:', await storage.getItem('test-key')); -storage.setItem('root/light-code-demo/main.ts', 'test-value2'); -console.log('Cache test-key:', await storage.getItem('root/light-code-demo/main.ts')); +// codeStorage.setItem('root/light-code-demo/main.ts', 'test-value2'); +console.log('Cache test-key:', await codeStorage.getItem('root/light-code-demo/main.ts')); console.log('has', await codeStorage.hasItem('root/light-code-demo/main.ts')); diff --git a/server/src/index.ts b/server/src/index.ts index a533ad0..2bb4e29 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -14,4 +14,6 @@ app.listen(4005, () => { console.log('Server is running on http://localhost:4005') }) -app.onServerRequest(proxyRoute); \ No newline at end of file +app.onServerRequest(proxyRoute); + +export { app } \ No newline at end of file diff --git a/server/src/routes/file-code/index.ts b/server/src/routes/file-code/index.ts index e19a987..4cffc47 100644 --- a/server/src/routes/file-code/index.ts +++ b/server/src/routes/file-code/index.ts @@ -29,11 +29,40 @@ app.route({ ctx.body = files }).addTo(app); - +type UploadProps = { + user: string; + key: string; + files: { + type: 'file' | 'base64'; + filepath: string; + content: string; + }[]; +} app.route({ path: 'file-code', key: 'upload', middleware: ['auth'] }).define(async (ctx) => { + const upload = ctx.query?.upload as UploadProps; + if (!upload || !upload.user || !upload.key || !upload.files) { + ctx.throw(400, 'Invalid upload data'); + } + const user = upload.user; + const key = upload.key; + for (const file of upload.files) { + if (file.type === 'file') { + const fullPath = path.join(codeRoot, user, key, file.filepath); + const dir = path.dirname(fullPath); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(fullPath, file.content, 'utf-8'); + } else if (file.type === 'base64') { + const fullPath = path.join(codeRoot, user, key, file.filepath); + const dir = path.dirname(fullPath); + fs.mkdirSync(dir, { recursive: true }); + const buffer = Buffer.from(file.content, 'base64'); + fs.writeFileSync(fullPath, buffer); + } + } + ctx.body = { success: true }; }).addTo(app) \ No newline at end of file diff --git a/server/src/test/common.ts b/server/src/test/common.ts new file mode 100644 index 0000000..3566829 --- /dev/null +++ b/server/src/test/common.ts @@ -0,0 +1,5 @@ +import { Query } from '@kevisual/query' + +export const query = new Query({ + url: 'http://localhost:4005/api/router', +}) \ No newline at end of file diff --git a/server/src/test/test-upload.ts b/server/src/test/test-upload.ts new file mode 100644 index 0000000..97a3a30 --- /dev/null +++ b/server/src/test/test-upload.ts @@ -0,0 +1,48 @@ +import { query } from './common.ts' + + + +export const testUpload = async () => { + + const res = await query.post({ + path: 'file-code', + key: 'upload', + upload: { + user: 'test', + key: 'demo', + files: [ + { + type: 'file', + filepath: 'main.ts', + content: `import { QueryRouterServer } from "@kevisual/router"; + +const app = new QueryRouterServer(); + +app.route({ + path: 'main' +}).define(async (ctx) => { + ctx.body = { + message: 'this is main. filename: test/demo/main.ts', + params: ctx.query + } +}).addTo(app) + +app.wait()` + }, + ] + } + }) + console.log('Upload response:', res); +} + +// testUpload(); + +const callTestDemo = async () => { + const res = await query.post({ + path: 'call', + filename: 'test/demo/main.ts', + }) + console.log('Call response:', res); +} + +callTestDemo(); \ No newline at end of file diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 0000000..6d3973d --- /dev/null +++ b/web/.env.example @@ -0,0 +1,6 @@ +# PocketBase配置 +VITE_POCKETBASE_URL=http://localhost:8090 + +# 可选:其他配置 +# VITE_APP_NAME=Light Code Center +# VITE_DEBUG=true \ No newline at end of file diff --git a/web/package.json b/web/package.json index e60c35a..e64aff3 100644 --- a/web/package.json +++ b/web/package.json @@ -19,10 +19,12 @@ "@astrojs/mdx": "^4.3.7", "@astrojs/react": "^4.4.0", "@astrojs/sitemap": "^3.6.0", + "@faker-js/faker": "^10.1.0", "@kevisual/noco": "^0.0.1", "@kevisual/query": "^0.0.29", "@kevisual/query-login": "^0.0.6", "@kevisual/registry": "^0.0.1", + "@ricky0123/vad-web": "^0.0.28", "@tailwindcss/vite": "^4.1.14", "astro": "^5.14.4", "class-variance-authority": "^0.7.1", @@ -33,8 +35,10 @@ "lodash-es": "^4.17.21", "lucide-react": "^0.545.0", "nanoid": "^5.1.6", + "pocketbase": "^0.26.2", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-resizable-panels": "^3.0.6", "react-toastify": "^11.0.5", "tailwind-merge": "^3.3.1", "three": "^0.180.0", diff --git a/web/src/apps/login/AuthProvider.tsx b/web/src/apps/login/AuthProvider.tsx new file mode 100644 index 0000000..3ab02e6 --- /dev/null +++ b/web/src/apps/login/AuthProvider.tsx @@ -0,0 +1,17 @@ +import React, { useEffect } from 'react'; +import { useAuthStore } from '@/store/authStore'; + +interface AuthProviderProps { + children: React.ReactNode; +} + +export const AuthProvider: React.FC = ({ children }) => { + const initAuth = useAuthStore(state => state.initAuth); + + useEffect(() => { + // 在应用启动时初始化认证状态 + initAuth(); + }, [initAuth]); + + return <>{children}; +}; \ No newline at end of file diff --git a/web/src/apps/login/DashboardApp.tsx b/web/src/apps/login/DashboardApp.tsx new file mode 100644 index 0000000..7ff4e92 --- /dev/null +++ b/web/src/apps/login/DashboardApp.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { AuthProvider } from './AuthProvider'; +import { ProtectedRoute } from '@/apps/login/ProtectedRoute'; +import { UserInfo } from '@/apps/login/UserInfo'; +import { useAuth } from '../../store/authStore'; +import { UserType } from '../../lib/pocketbase'; +import { ToastContainer } from 'react-toastify'; + +const DashboardContent: React.FC = () => { + const { user } = useAuth(); + + return ( +
+
+
+
+

用户仪表板

+ +
+
+
+ +
+
+
+
+

+ 欢迎,{user?.email || '用户'}! +

+ +
+
+

项目管理

+

管理您的代码项目和文件

+
+ +
+

设置

+

配置您的账户设置

+
+
+
+
+
+
+
+ ); +}; + +export const DashboardApp: React.FC = () => { + return ( + + + + + + + ); +}; \ No newline at end of file diff --git a/web/src/apps/login/LoginForm.tsx b/web/src/apps/login/LoginForm.tsx new file mode 100644 index 0000000..4c520dd --- /dev/null +++ b/web/src/apps/login/LoginForm.tsx @@ -0,0 +1,200 @@ +import React, { useState } from 'react'; +import { useAuthStore, useAuth, useAuthActions } from '@/store/authStore'; +import { UserType } from '@/lib/pocketbase'; +import { toast } from 'react-toastify'; + +interface LoginFormData { + email: string; + password: string; + userType: UserType; +} + +export const LoginForm: React.FC = () => { + const { isLoading, error } = useAuth(); + const { login, clearError } = useAuthActions(); + + const [formData, setFormData] = useState({ + email: '', + password: '', + userType: UserType.USER, + }); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value, + })); + + // 清除错误信息 + if (error) { + clearError(); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.email || !formData.password) { + toast.error('请填写邮箱和密码'); + return; + } + + try { + await login(formData); + toast.success('登录成功!'); + + // 登录成功后跳转到首页或仪表板 + window.location.href = formData.userType === UserType.ADMIN ? '/admin' : '/dashboard'; + } catch (error) { + toast.error(error instanceof Error ? error.message : '登录失败'); + } + }; + + const handleUserTypeChange = (userType: UserType) => { + setFormData(prev => ({ + ...prev, + userType, + })); + + if (error) { + clearError(); + } + }; + + return ( +
+
+
+

+ 登录您的账户 +

+

+ 选择您的账户类型并输入登录信息 +

+
+ +
+ {/* 用户类型选择 */} +
+
+ +
+ + +
+
+ + {/* 邮箱输入 */} +
+ + +
+ + {/* 密码输入 */} +
+ + +
+
+ + {/* 错误信息显示 */} + {error && ( +
+
+
+

+ {error} +

+
+
+
+ )} + + {/* 提交按钮 */} +
+ +
+ + {/* 提示信息 */} +
+

+ {formData.userType === UserType.ADMIN + ? '管理员账户将使用 superuser 权限登录' + : '普通用户账户将使用标准权限登录' + } +

+
+
+
+
+ ); +}; \ No newline at end of file diff --git a/web/src/apps/login/LoginPage.tsx b/web/src/apps/login/LoginPage.tsx new file mode 100644 index 0000000..6926442 --- /dev/null +++ b/web/src/apps/login/LoginPage.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { LoginForm } from './LoginForm'; +import { ToastContainer } from 'react-toastify'; + +export const LoginPage: React.FC = () => { + return ( + <> + + + + ); +}; \ No newline at end of file diff --git a/web/src/apps/login/ProtectedRoute.tsx b/web/src/apps/login/ProtectedRoute.tsx new file mode 100644 index 0000000..9c7ed2a --- /dev/null +++ b/web/src/apps/login/ProtectedRoute.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { useAuth, usePermissions } from '@/store/authStore'; +import { UserType } from '@/lib/pocketbase'; + +interface ProtectedRouteProps { + children: React.ReactNode; + requiredUserType?: UserType; + fallback?: React.ReactNode; + redirectTo?: string; +} + +export const ProtectedRoute: React.FC = ({ + children, + requiredUserType, + fallback, + redirectTo = '/login' +}) => { + const { isAuthenticated, isLoading } = useAuth(); + const { canAccess } = usePermissions(); + + // 加载中显示 + if (isLoading) { + return ( +
+
+
+ 验证身份中... +
+
+ ); + } + + // 未登录 + if (!isAuthenticated) { + if (fallback) { + return <>{fallback}; + } + + // 重定向到登录页面 + if (typeof window !== 'undefined') { + window.location.href = redirectTo; + } + + return ( +
+
+

需要登录

+

请先登录您的账户

+ + 前往登录 + +
+
+ ); + } + + // 检查权限 + if (requiredUserType && !canAccess(requiredUserType)) { + if (fallback) { + return <>{fallback}; + } + + return ( +
+
+

权限不足

+

+ 您需要{requiredUserType === UserType.ADMIN ? '管理员' : '用户'}权限才能访问此页面 +

+ + 返回首页 + +
+
+ ); + } + + return <>{children}; +}; + +// 专门用于管理员页面的保护组件 +export const AdminRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; + +// 专门用于普通用户页面的保护组件 +export const UserRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => { + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/web/src/apps/login/UserInfo.tsx b/web/src/apps/login/UserInfo.tsx new file mode 100644 index 0000000..674351e --- /dev/null +++ b/web/src/apps/login/UserInfo.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { useAuth, useAuthActions, usePermissions } from '@/store/authStore'; +import { UserType } from '@/lib/pocketbase'; +import { toast } from 'react-toastify'; + +export const UserInfo: React.FC = () => { + const { isAuthenticated, user, userType, isLoading } = useAuth(); + const { logout } = useAuthActions(); + const { isAdmin, isUser } = usePermissions(); + + const handleLogout = () => { + logout(); + toast.success('已成功登出'); + window.location.href = '/login'; + }; + + if (isLoading) { + return ( +
+
+ 加载中... +
+ ); + } + + if (!isAuthenticated || !user) { + return ( + + ); + } + + return ( +
+ {/* 用户信息 */} +
+ {/* 用户头像 */} +
+ {user.email?.charAt(0).toUpperCase() || 'U'} +
+ + {/* 用户详情 */} +
+ + {(user as any).name || (user as any).username || user.email} + + + {isAdmin ? '管理员' : '用户'} + +
+
+ + {/* 操作按钮 */} +
+ {/* 仪表板链接 */} + + {isAdmin ? '管理面板' : '仪表板'} + + + {/* 登出按钮 */} + +
+
+ ); +}; + +// 简化版本的用户状态显示组件 +export const UserStatus: React.FC = () => { + const { isAuthenticated, user, userType } = useAuth(); + const { isAdmin } = usePermissions(); + + if (!isAuthenticated || !user) { + return ( + 未登录 + ); + } + + return ( +
+
+ + {isAdmin ? '管理员' : '用户'}: {user.email} + +
+ ); +}; \ No newline at end of file diff --git a/web/src/apps/muse/base/index.tsx b/web/src/apps/muse/base/index.tsx new file mode 100644 index 0000000..576ceb2 --- /dev/null +++ b/web/src/apps/muse/base/index.tsx @@ -0,0 +1,70 @@ +import { useState } from "react"; +import { Base } from "./table/index"; + +const tabs = [ + { + key: 'table', + title: '表格' + }, + { + key: 'graph', + title: '关系图' + }, + { + key: 'world', + title: '世界' + } +]; + +export const BaseApp = () => { + const [activeTab, setActiveTab] = useState('table'); + + const renderContent = () => { + switch (activeTab) { + case 'table': + return ; + case 'graph': + return ( +
+ 关系图模块暂未实现 +
+ ); + case 'world': + return ( +
+ 世界模块暂未实现 +
+ ); + default: + return null; + } + }; + + return ( +
+ {/* Tab 导航栏 */} +
+ +
+ + {/* Tab 内容区域 */} +
+ {renderContent()} +
+
+ ); +} \ No newline at end of file diff --git a/web/src/apps/muse/base/mock/collection.ts b/web/src/apps/muse/base/mock/collection.ts new file mode 100644 index 0000000..c034bbf --- /dev/null +++ b/web/src/apps/muse/base/mock/collection.ts @@ -0,0 +1,183 @@ +import { faker } from '@faker-js/faker'; +import { nanoid, customAlphabet } from 'nanoid'; + +export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'); + +// 类型定义 +export type MarkDataNode = { + id?: string; + content?: string; + type?: string; + title?: string; + position?: { x: number; y: number }; + size?: { width: number; height: number }; + metadata?: Record; + [key: string]: any; +}; + +export type MarkFile = { + id: string; + name: string; + url: string; + size: number; + type: 'self' | 'data' | 'generate'; // generate为生成文件 + query: string; // 'data.nodes[id].content'; + hash: string; + fileKey: string; // 文件的名称, 唯一 +}; + +export type MarkData = { + md?: string; // markdown + mdList?: string[]; // markdown list + type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file + data?: any; + key?: string; // 文件的名称, 唯一 + push?: boolean; // 是否推送到elasticsearch + pushTime?: Date; // 推送时间 + summary?: string; // 摘要 + nodes?: MarkDataNode[]; // 节点 + [key: string]: any; +}; + +export type MarkConfig = { + visibility?: 'public' | 'private' | 'restricted'; + allowComments?: boolean; + allowDownload?: boolean; + password?: string; + expiredAt?: Date; + [key: string]: any; +}; + +export type MarkAuth = { + permissions?: string[]; + roles?: string[]; + userId?: string; + [key: string]: any; +}; + +export type Mark = { + id: string; + title: string; + description: string; + cover: string; + thumbnail: string; + key: string; + markType: string; + link: string; + tags: string[]; + summary: string; + data: MarkData; + uid: string; + puid: string; + config: MarkConfig; + fileList: MarkFile[]; + uname: string; + markedAt: Date; + createdAt: Date; + updatedAt: Date; + version: number; +}; + +// 生成模拟的 MarkDataNode +const generateMarkDataNode = (): MarkDataNode => ({ + id: random(12), + content: faker.lorem.paragraph(), + type: faker.helpers.arrayElement(['text', 'image', 'video', 'code', 'link']), + title: faker.lorem.sentence(), + position: { + x: faker.number.int({ min: 0, max: 1920 }), + y: faker.number.int({ min: 0, max: 1080 }) + }, + size: { + width: faker.number.int({ min: 100, max: 800 }), + height: faker.number.int({ min: 50, max: 600 }) + }, + metadata: { + createdBy: faker.person.fullName(), + lastModified: faker.date.recent(), + isLocked: faker.datatype.boolean() + } +}); + +// 生成模拟的 MarkFile +const generateMarkFile = (): MarkFile => ({ + id: faker.string.uuid(), + name: faker.system.fileName(), + url: faker.internet.url(), + size: faker.number.int({ min: 1024, max: 50 * 1024 * 1024 }), // 1KB to 50MB + type: faker.helpers.arrayElement(['self', 'data', 'generate']), + query: `data.nodes[${random(12)}].content`, + hash: faker.git.commitSha(), + fileKey: faker.system.fileName() +}); + +// 生成模拟的 MarkData +const generateMarkData = (): MarkData => ({ + md: faker.lorem.paragraphs(3, '\n\n'), + mdList: Array.from({ length: faker.number.int({ min: 3, max: 8 }) }, () => faker.lorem.sentence()), + type: faker.helpers.arrayElement(['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file']), + data: { + author: faker.person.fullName(), + category: faker.helpers.arrayElement(['技术', '生活', '工作', '学习', '思考']), + priority: faker.helpers.arrayElement(['low', 'medium', 'high']) + }, + key: faker.system.fileName(), + push: faker.datatype.boolean(), + pushTime: faker.date.recent(), + summary: faker.lorem.paragraph(), + nodes: Array.from({ length: faker.number.int({ min: 2, max: 6 }) }, generateMarkDataNode) +}); + +// 生成模拟的 MarkConfig +const generateMarkConfig = (): MarkConfig => ({ + visibility: faker.helpers.arrayElement(['public', 'private', 'restricted']), + allowComments: faker.datatype.boolean(), + allowDownload: faker.datatype.boolean(), + password: faker.datatype.boolean() ? faker.internet.password() : undefined, + expiredAt: faker.datatype.boolean() ? faker.date.future() : undefined, + theme: faker.helpers.arrayElement(['light', 'dark', 'auto']), + language: faker.helpers.arrayElement(['zh-CN', 'en-US', 'ja-JP']) +}); + +// 生成单个 Mark 记录 +const generateMark = (): Mark => { + const markType = faker.helpers.arrayElement(['markdown', 'json', 'html', 'image', 'video', 'audio', 'code', 'link', 'file']); + const title = faker.lorem.sentence({ min: 3, max: 8 }); + + return { + id: faker.string.uuid(), + title, + description: faker.lorem.paragraph(), + cover: faker.image.url({ width: 800, height: 600 }), + thumbnail: faker.image.url({ width: 200, height: 150 }), + key: faker.system.filePath(), + markType, + link: faker.internet.url(), + tags: Array.from({ length: faker.number.int({ min: 1, max: 5 }) }, () => + faker.helpers.arrayElement(['技术', '前端', '后端', '设计', 'AI', '工具', '教程', '笔记']) + ), + summary: faker.lorem.sentence(), + data: generateMarkData(), + uid: faker.string.uuid(), + puid: faker.string.uuid(), + config: generateMarkConfig(), + fileList: Array.from({ length: faker.number.int({ min: 0, max: 4 }) }, generateMarkFile), + uname: faker.person.fullName(), + markedAt: faker.date.past(), + createdAt: faker.date.past(), + updatedAt: faker.date.recent(), + version: faker.number.int({ min: 1, max: 10 }) + }; +}; + +// 生成 20 条模拟数据 +export const mockMarks: Mark[] = Array.from({ length: 20 }, generateMark); + +// 导出生成器函数 +export { + generateMark, + generateMarkData, + generateMarkFile, + generateMarkDataNode, + generateMarkConfig +}; \ No newline at end of file diff --git a/web/src/apps/muse/base/table/DetailModal.tsx b/web/src/apps/muse/base/table/DetailModal.tsx new file mode 100644 index 0000000..c9279be --- /dev/null +++ b/web/src/apps/muse/base/table/DetailModal.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { Mark } from '../mock/collection'; +import './modal.css'; + +interface DetailModalProps { + visible: boolean; + data: Mark | null; + onClose: () => void; +} + +export const DetailModal: React.FC = ({ visible, data, onClose }) => { + if (!visible || !data) return null; + + return ( +
+
e.stopPropagation()}> +
+

详情信息

+ +
+ +
+
+

基本信息

+
+
+ + {data.title} +
+
+ + {data.markType} +
+
+ + {data.uname} +
+
+ + + {data.config.visibility === 'public' ? '公开' : + data.config.visibility === 'private' ? '私有' : '受限'} + +
+
+
+ +
+

描述

+

{data.description}

+
+ +
+

标签

+
+ {data.tags.map((tag, index) => ( + {tag} + ))} +
+
+ +
+

时间信息

+
+
+ + {new Date(data.markedAt).toLocaleString('zh-CN')} +
+
+ + {new Date(data.createdAt).toLocaleString('zh-CN')} +
+
+ + {new Date(data.updatedAt).toLocaleString('zh-CN')} +
+
+ + v{data.version} +
+
+
+ + {data.fileList.length > 0 && ( +
+

附件文件 ({data.fileList.length})

+
+ {data.fileList.map((file, index) => ( +
+
+ {file.name} + {formatFileSize(file.size)} +
+ {file.type} +
+ ))} +
+
+ )} + +
+

数据摘要

+

{data.data.summary || data.summary}

+
+ + {data.config.allowComments !== undefined && ( +
+

权限设置

+
+
+ + + {data.config.allowComments ? '是' : '否'} + +
+
+ + + {data.config.allowDownload ? '是' : '否'} + +
+ {data.config.expiredAt && ( +
+ + {new Date(data.config.expiredAt).toLocaleString('zh-CN')} +
+ )} +
+
+ )} +
+ +
+ + +
+
+
+ ); +}; + +// 格式化文件大小 +function formatFileSize(bytes: number): string { + if (bytes === 0) return '0 B'; + + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +} \ No newline at end of file diff --git a/web/src/apps/muse/base/table/README.md b/web/src/apps/muse/base/table/README.md new file mode 100644 index 0000000..65fe1e6 --- /dev/null +++ b/web/src/apps/muse/base/table/README.md @@ -0,0 +1,180 @@ +# 数据管理表格组件 + +这是一个功能完整的React表格组件,支持多选、排序、分页、操作等功能,并集成了Mock数据。 + +## 功能特性 + +### ✅ 已实现功能 + +1. **数据展示** + - 支持多种数据类型展示(标题、类型、标签、创建者等) + - 自定义列渲染(类型徽章、标签展示等) + - 响应式设计,适配移动端 + +2. **多选功能** + - 支持单行选择和全选 + - 批量操作(批量删除) + - 选择状态实时显示 + +3. **排序功能** + - 支持多列排序(标题、类型、创建者、创建时间等) + - 升序/降序/取消排序 + - 排序状态可视化指示 + +4. **分页功能** + - 支持页码切换 + - 可调整每页显示数量(10/20/50/100条) + - 显示总数和当前范围 + - 快速跳转页码 + +5. **操作功能** + - 详情查看(弹窗形式) + - 编辑功能 + - 删除功能(单个/批量) + - 删除确认对话框 + +6. **详情模态框** + - 完整的数据信息展示 + - 分区域显示(基本信息、描述、标签、时间信息等) + - 附件文件列表 + - 权限设置显示 + +## 文件结构 + +``` +base/table/ +├── index.tsx # 主组件入口,集成所有功能 +├── Table.tsx # 基础表格组件 +├── DetailModal.tsx # 详情查看模态框 +├── types.ts # TypeScript类型定义 +├── table.css # 表格样式 +└── modal.css # 模态框样式 +``` + +## 使用的Mock数据 + +数据来源:`base/mock/collection.ts` +- 20条模拟的Mark记录 +- 包含完整的用户、文件、配置等信息 +- 支持各种数据类型和状态 + +## 组件特色 + +### 1. 类型安全 +- 完整的TypeScript类型定义 +- 严格的类型检查 +- 良好的IDE支持 + +### 2. 用户体验 +- 直观的操作界面 +- 实时的状态反馈 +- 响应式设计 +- 加载状态和空状态处理 + +### 3. 数据展示 +- 多种数据类型的可视化展示 +- 颜色编码的类型和状态 +- 格式化的时间和文件大小 + +### 4. 交互功能 +- 丰富的操作按钮 +- 确认对话框 +- 详情查看弹窗 +- 批量操作支持 + +## 技术实现 + +### 状态管理 +- 使用React Hooks进行状态管理 +- 分离的数据状态和UI状态 +- 受控组件模式 + +### 样式设计 +- 现代化的UI设计 +- 一致的视觉风格 +- 响应式布局 +- 无障碍访问支持 + +### 数据处理 +- 客户端排序和分页 +- 嵌套数据访问 +- 数据格式化和转换 + +## 快速开始 + +1. 确保已安装依赖: + ```bash + pnpm install + ``` + +2. 启动开发服务器: + ```bash + npm run dev + ``` + +3. 访问表格页面查看效果 + +## 自定义配置 + +### 添加新列 +在 `index.tsx` 中的 `columns` 数组中添加新的列配置: + +```tsx +{ + key: 'newColumn', + title: '新列', + dataIndex: 'fieldName', + width: 120, + sortable: true, + render: (value, record) => { + // 自定义渲染逻辑 + return {value}; + } +} +``` + +### 添加新操作 +在 `actions` 数组中添加新的操作按钮: + +```tsx +{ + key: 'newAction', + label: '新操作', + type: 'primary', + icon: '🔧', + onClick: (record) => { + // 操作逻辑 + } +} +``` + +### 修改分页配置 +调整 `paginationConfig` 对象的属性: + +```tsx +const paginationConfig = { + current: currentPage, + pageSize: pageSize, + total: data.length, + showSizeChanger: true, + showQuickJumper: true, + // 其他配置... +}; +``` + +## 性能优化 + +1. **虚拟化**:对于大量数据,可以考虑实现虚拟滚动 +2. **懒加载**:支持服务端分页和按需加载 +3. **缓存**:实现数据缓存机制 +4. **防抖**:搜索和过滤功能添加防抖处理 + +## 后续优化建议 + +1. **搜索功能**:添加全局搜索和列过滤 +2. **导出功能**:支持数据导出为Excel/CSV +3. **列配置**:支持用户自定义显示列 +4. **主题配置**:支持多主题切换 +5. **国际化**:添加多语言支持 + +这个表格组件提供了现代化的数据管理界面,具备企业级应用所需的核心功能。 \ No newline at end of file diff --git a/web/src/apps/muse/base/table/Table.tsx b/web/src/apps/muse/base/table/Table.tsx new file mode 100644 index 0000000..39dc129 --- /dev/null +++ b/web/src/apps/muse/base/table/Table.tsx @@ -0,0 +1,306 @@ +import React, { useState, useMemo } from 'react'; +import { Mark } from '../mock/collection'; +import { TableProps, SortState } from './types'; +import './table.css'; + +export const Table: React.FC = ({ + data, + columns, + loading = false, + rowSelection, + pagination, + actions, + onSort +}) => { + const [sortState, setSortState] = useState({ field: null, order: null }); + const [currentPage, setCurrentPage] = useState(1); + + // 处理排序 + const handleSort = (field: string) => { + let newOrder: 'asc' | 'desc' | null = 'asc'; + + if (sortState.field === field) { + if (sortState.order === 'asc') { + newOrder = 'desc'; + } else if (sortState.order === 'desc') { + newOrder = null; + } + } + + const newSortState = { field: newOrder ? field : null, order: newOrder }; + setSortState(newSortState); + onSort?.(newSortState.field!, newSortState.order!); + }; + + // 排序后的数据 + const sortedData = useMemo(() => { + if (!sortState.field || !sortState.order) return data; + + return [...data].sort((a, b) => { + const aVal = getNestedValue(a, sortState.field!); + const bVal = getNestedValue(b, sortState.field!); + + if (aVal < bVal) return sortState.order === 'asc' ? -1 : 1; + if (aVal > bVal) return sortState.order === 'asc' ? 1 : -1; + return 0; + }); + }, [data, sortState]); + + // 分页数据 + const paginatedData = useMemo(() => { + if (!pagination) return sortedData; + + const start = (currentPage - 1) * pagination.pageSize; + const end = start + pagination.pageSize; + return sortedData.slice(start, end); + }, [sortedData, currentPage, pagination]); + + // 处理分页变化 + const handlePageChange = (page: number) => { + setCurrentPage(page); + if (pagination && typeof pagination === 'object') { + pagination.onChange?.(page, pagination.pageSize); + } + }; + + // 处理页大小变化 + const handlePageSizeChange = (pageSize: number) => { + setCurrentPage(1); + if (pagination && typeof pagination === 'object') { + pagination.onChange?.(1, pageSize); + } + }; + + // 全选/取消全选 + const handleSelectAll = (checked: boolean) => { + if (!rowSelection) return; + + const allKeys = paginatedData.map(item => item.id); + const selectedKeys = checked ? allKeys : []; + const selectedRows = checked ? paginatedData : []; + + rowSelection.onChange?.(selectedKeys, selectedRows); + }; + + // 单行选择 + const handleRowSelect = (record: Mark, checked: boolean) => { + if (!rowSelection) return; + + const currentKeys = rowSelection.selectedRowKeys || []; + const newKeys = checked + ? [...currentKeys, record.id] + : currentKeys.filter(key => key !== record.id); + + const selectedRows = data.filter(item => newKeys.includes(item.id)); + rowSelection.onChange?.(newKeys, selectedRows); + }; + + // 获取嵌套值 + const getNestedValue = (obj: any, path: string) => { + return path.split('.').reduce((o, p) => o?.[p], obj); + }; + + if (loading) { + return ( +
+
+ 加载中... +
+ ); + } + + const selectedKeys = rowSelection?.selectedRowKeys || []; + const isAllSelected = paginatedData.length > 0 && paginatedData.every(item => selectedKeys.includes(item.id)); + const isIndeterminate = selectedKeys.length > 0 && !isAllSelected; + + return ( +
+ {/* 表格工具栏 */} + {rowSelection && selectedKeys.length > 0 && ( +
+ + 已选择 {selectedKeys.length} 项 + +
+ +
+
+ )} + + {/* 表格 */} +
+ + + + {rowSelection && ( + + )} + {columns.map(column => ( + + ))} + {actions && actions.length > 0 && ( + + )} + + + + {paginatedData.map((record, index) => ( + + {rowSelection && ( + + )} + {columns.map(column => ( + + ))} + {actions && actions.length > 0 && ( + + )} + + ))} + +
+ { + if (input) input.indeterminate = isIndeterminate; + }} + onChange={(e) => handleSelectAll(e.target.checked)} + /> + +
+ {column.title} + {column.sortable && ( +
handleSort(column.dataIndex)} + > + + +
+ )} +
+
操作
+ handleRowSelect(record, e.target.checked)} + disabled={rowSelection.getCheckboxProps?.(record)?.disabled} + /> + + {column.render + ? column.render(getNestedValue(record, column.dataIndex), record, index) + : getNestedValue(record, column.dataIndex) + } + +
+ {actions.map(action => ( + + ))} +
+
+ + {paginatedData.length === 0 && ( +
+
📭
+

暂无数据

+
+ )} +
+ + {/* 分页 */} + {pagination && ( +
+
+ {pagination.showTotal && pagination.showTotal( + pagination.total, + [ + (currentPage - 1) * pagination.pageSize + 1, + Math.min(currentPage * pagination.pageSize, pagination.total) + ] + )} +
+
+ + +
+ {Array.from({ length: Math.ceil(pagination.total / pagination.pageSize) }) + .map((_, i) => i + 1) + .filter(page => { + const distance = Math.abs(page - currentPage); + return distance === 0 || distance <= 2 || page === 1 || page === Math.ceil(pagination.total / pagination.pageSize); + }) + .map((page, index, pages) => { + const prevPage = pages[index - 1]; + const showEllipsis = prevPage && page - prevPage > 1; + + return ( + + {showEllipsis && ...} + + + ); + }) + } +
+ + +
+ + {pagination.showSizeChanger && ( +
+ +
+ )} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/web/src/apps/muse/base/table/index.tsx b/web/src/apps/muse/base/table/index.tsx new file mode 100644 index 0000000..ade951f --- /dev/null +++ b/web/src/apps/muse/base/table/index.tsx @@ -0,0 +1,281 @@ +import React, { useState, useMemo } from 'react'; +import { Table } from './Table'; +import { DetailModal } from './DetailModal'; +import { mockMarks, Mark } from '../mock/collection'; +import { TableColumn, ActionButton } from './types'; + +export const Base = () => { + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [data, setData] = useState(mockMarks); + const [detailModalVisible, setDetailModalVisible] = useState(false); + const [currentRecord, setCurrentRecord] = useState(null); + + // 表格列配置 + const columns: TableColumn[] = [ + { + key: 'title', + title: '标题', + dataIndex: 'title', + width: 300, + sortable: true, + render: (value: string, record: Mark) => ( +
+
+ {value} +
+
+ {record.description.slice(0, 60)}... +
+
+ ) + }, + { + key: 'markType', + title: '类型', + dataIndex: 'markType', + width: 100, + sortable: true, + render: (value: string) => ( + + {value} + + ) + }, + { + key: 'tags', + title: '标签', + dataIndex: 'tags', + width: 200, + render: (tags: string[]) => ( +
+ {tags.slice(0, 3).map((tag, index) => ( + + {tag} + + ))} + {tags.length > 3 && ( + + +{tags.length - 3} + + )} +
+ ) + }, + { + key: 'uname', + title: '创建者', + dataIndex: 'uname', + width: 120, + sortable: true + }, + { + key: 'createdAt', + title: '创建时间', + dataIndex: 'createdAt', + width: 180, + sortable: true, + render: (value: Date) => new Date(value).toLocaleString('zh-CN') + }, + { + key: 'config.visibility', + title: '可见性', + dataIndex: 'config.visibility', + width: 100, + render: (value: string) => ( + + {value === 'public' ? '公开' : value === 'private' ? '私有' : '受限'} + + ) + } + ]; + + // 操作按钮配置 + const actions: ActionButton[] = [ + { + key: 'view', + label: '详情', + type: 'primary', + icon: '👁', + onClick: (record: Mark) => { + handleViewDetail(record); + } + }, + { + key: 'edit', + label: '编辑', + icon: '✏️', + onClick: (record: Mark) => { + handleEdit(record); + } + }, + { + key: 'delete', + label: '删除', + type: 'danger', + icon: '🗑️', + onClick: (record: Mark) => { + handleDelete(record); + } + } + ]; + + // 获取类型颜色 + const getTypeColor = (type: string): string => { + const colors: Record = { + markdown: '#1890ff', + json: '#52c41a', + html: '#fa8c16', + image: '#eb2f96', + video: '#722ed1', + audio: '#13c2c2', + code: '#666', + link: '#1890ff', + file: '#999' + }; + return colors[type] || '#999'; + }; + + // 处理详情查看 + const handleViewDetail = (record: Mark) => { + setCurrentRecord(record); + setDetailModalVisible(true); + }; + + // 处理编辑 + const handleEdit = (record: Mark) => { + alert(`编辑: ${record.title}`); + // 这里可以打开编辑对话框或跳转到编辑页面 + }; + + // 处理删除 + const handleDelete = (record: Mark) => { + if (window.confirm(`确定要删除"${record.title}"吗?`)) { + setData(prevData => prevData.filter(item => item.id !== record.id)); + // 如果当前选中的项包含被删除的项,也要从选中列表中移除 + setSelectedRowKeys(prev => prev.filter(key => key !== record.id)); + } + }; + + // 处理批量删除 + const handleBatchDelete = () => { + if (selectedRowKeys.length === 0) return; + + if (window.confirm(`确定要删除选中的 ${selectedRowKeys.length} 项吗?`)) { + setData(prevData => prevData.filter(item => !selectedRowKeys.includes(item.id))); + setSelectedRowKeys([]); + } + }; + + // 排序处理 + const handleSort = (field: string, order: 'asc' | 'desc' | null) => { + if (!order) { + setData(mockMarks); // 重置为原始顺序 + return; + } + + const sortedData = [...data].sort((a, b) => { + const getNestedValue = (obj: any, path: string) => { + return path.split('.').reduce((o, p) => o?.[p], obj); + }; + + const aVal = getNestedValue(a, field); + const bVal = getNestedValue(b, field); + + if (aVal < bVal) return order === 'asc' ? -1 : 1; + if (aVal > bVal) return order === 'asc' ? 1 : -1; + return 0; + }); + + setData(sortedData); + }; + + // 分页配置 + const paginationConfig = { + current: currentPage, + pageSize: pageSize, + total: data.length, + showSizeChanger: true, + showQuickJumper: true, + showTotal: (total: number, range: [number, number]) => + `第 ${range[0]}-${range[1]} 条,共 ${total} 条`, + onChange: (page: number, size: number) => { + setCurrentPage(page); + setPageSize(size); + } + }; + + return ( +
+
+

数据管理表格

+

+ 支持多选、排序、分页等功能的数据表格示例 +

+
+ + {selectedRowKeys.length > 0 && ( +
+ 已选择 {selectedRowKeys.length} 项 + +
+ )} + + { + setSelectedRowKeys(keys); + } + }} + pagination={paginationConfig} + onSort={handleSort} + /> + + { + setDetailModalVisible(false); + setCurrentRecord(null); + }} + /> + + ); +}; \ No newline at end of file diff --git a/web/src/apps/muse/base/table/modal.css b/web/src/apps/muse/base/table/modal.css new file mode 100644 index 0000000..275dd5c --- /dev/null +++ b/web/src/apps/muse/base/table/modal.css @@ -0,0 +1,305 @@ +/* 模态框样式 */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 20px; +} + +.modal-content { + background: #fff; + border-radius: 8px; + max-width: 800px; + width: 100%; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid #e8e8e8; + background: #fafafa; +} + +.modal-header h3 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #333; +} + +.modal-close { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #666; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s; +} + +.modal-close:hover { + background: #f0f0f0; + color: #333; +} + +.modal-body { + flex: 1; + overflow-y: auto; + padding: 24px; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 24px; + border-top: 1px solid #e8e8e8; + background: #fafafa; +} + +/* 详情部分 */ +.detail-section { + margin-bottom: 24px; +} + +.detail-section h4 { + margin: 0 0 12px 0; + font-size: 16px; + font-weight: 600; + color: #333; + border-bottom: 2px solid #1890ff; + padding-bottom: 8px; +} + +.detail-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 12px; +} + +.detail-item { + display: flex; + align-items: center; + gap: 8px; +} + +.detail-item label { + font-weight: 600; + color: #666; + min-width: 80px; + font-size: 14px; +} + +.detail-item span { + color: #333; + font-size: 14px; +} + +/* 类型徽章 */ +.type-badge { + padding: 4px 8px; + border-radius: 4px; + color: #fff; + font-size: 12px; + font-weight: 500; +} + +.type-markdown { background: #1890ff; } +.type-json { background: #52c41a; } +.type-html { background: #fa8c16; } +.type-image { background: #eb2f96; } +.type-video { background: #722ed1; } +.type-audio { background: #13c2c2; } +.type-code { background: #666; } +.type-link { background: #1890ff; } +.type-file { background: #999; } + +/* 可见性徽章 */ +.visibility-badge { + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.visibility-public { + background: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; +} + +.visibility-private { + background: #fff2f0; + color: #ff4d4f; + border: 1px solid #ffccc7; +} + +.visibility-restricted { + background: #fffbe6; + color: #faad14; + border: 1px solid #ffe58f; +} + +/* 标签容器 */ +.tags-container { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.tag { + padding: 4px 8px; + background: #f0f0f0; + border-radius: 4px; + font-size: 12px; + color: #666; + border: 1px solid #d9d9d9; +} + +/* 文件列表 */ +.file-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.file-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px; + border: 1px solid #e8e8e8; + border-radius: 4px; + background: #fafafa; +} + +.file-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.file-name { + font-weight: 500; + color: #333; + font-size: 14px; +} + +.file-size { + font-size: 12px; + color: #999; +} + +.file-type { + padding: 2px 6px; + border-radius: 2px; + font-size: 10px; + font-weight: 500; + text-transform: uppercase; +} + +.file-type-self { + background: #e6f7ff; + color: #1890ff; +} + +.file-type-data { + background: #f6ffed; + color: #52c41a; +} + +.file-type-generate { + background: #fff7e6; + color: #fa8c16; +} + +/* 摘要文本 */ +.summary-text { + line-height: 1.6; + color: #666; + margin: 0; + padding: 12px; + background: #f9f9f9; + border-radius: 4px; + border-left: 4px solid #1890ff; +} + +/* 权限网格 */ +.permission-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; +} + +.permission-item { + display: flex; + align-items: center; + gap: 8px; +} + +.permission-item label { + font-weight: 600; + color: #666; + font-size: 14px; +} + +.enabled { + color: #52c41a; + font-weight: 500; +} + +.disabled { + color: #ff4d4f; + font-weight: 500; +} + +/* 响应式 */ +@media (max-width: 768px) { + .modal-content { + margin: 10px; + max-height: 95vh; + } + + .modal-header, + .modal-body, + .modal-footer { + padding: 16px; + } + + .detail-grid { + grid-template-columns: 1fr; + } + + .permission-grid { + grid-template-columns: 1fr; + } + + .file-item { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .modal-footer { + flex-direction: column; + } +} \ No newline at end of file diff --git a/web/src/apps/muse/base/table/table.css b/web/src/apps/muse/base/table/table.css new file mode 100644 index 0000000..eaa24cd --- /dev/null +++ b/web/src/apps/muse/base/table/table.css @@ -0,0 +1,312 @@ +/* 表格容器 */ +.table-container { + background: #fff; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; +} + +/* 工具栏 */ +.table-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + background: #f5f5f5; + border-bottom: 1px solid #e8e8e8; +} + +.selected-info { + color: #666; + font-size: 14px; +} + +.bulk-actions { + display: flex; + gap: 8px; +} + +/* 表格主体 */ +.table-wrapper { + overflow-x: auto; +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; +} + +.data-table th, +.data-table td { + padding: 12px 16px; + text-align: left; + border-bottom: 1px solid #e8e8e8; +} + +.data-table th { + background: #fafafa; + font-weight: 600; + color: #333; + position: sticky; + top: 0; + z-index: 10; +} + +.data-table tbody tr:hover { + background: #f5f5f5; +} + +/* 表头 */ +.table-header { + display: flex; + align-items: center; + justify-content: space-between; +} + +.sortable { + cursor: pointer; + user-select: none; +} + +.sort-indicators { + display: flex; + flex-direction: column; + margin-left: 8px; +} + +.sort-arrow { + font-size: 10px; + line-height: 1; + color: #ccc; + cursor: pointer; + transition: color 0.2s; +} + +.sort-arrow.active { + color: #1890ff; +} + +.sort-up { + margin-bottom: 2px; +} + +/* 选择列 */ +.selection-column { + width: 48px; + text-align: center; +} + +.selection-column input[type="checkbox"] { + cursor: pointer; +} + +/* 操作列 */ +.actions-column { + width: 200px; + text-align: right; +} + +.action-buttons { + display: flex; + gap: 8px; + justify-content: flex-end; +} + +/* 按钮样式 */ +.btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + border: 1px solid #d9d9d9; + border-radius: 4px; + background: #fff; + color: #333; + font-size: 12px; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; +} + +.btn:hover { + border-color: #40a9ff; + color: #40a9ff; +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.btn-primary { + background: #1890ff; + border-color: #1890ff; + color: #fff; +} + +.btn-primary:hover:not(:disabled) { + background: #40a9ff; + border-color: #40a9ff; +} + +.btn-danger { + background: #ff4d4f; + border-color: #ff4d4f; + color: #fff; +} + +.btn-danger:hover:not(:disabled) { + background: #ff7875; + border-color: #ff7875; +} + +.btn-icon { + font-size: 12px; +} + +/* 空状态 */ +.empty-state { + text-align: center; + padding: 64px 16px; + color: #999; +} + +.empty-icon { + font-size: 48px; + margin-bottom: 16px; +} + +/* 加载状态 */ +.table-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 64px 16px; + color: #666; +} + +.loading-spinner { + width: 32px; + height: 32px; + border: 3px solid #f0f0f0; + border-top: 3px solid #1890ff; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 16px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* 分页 */ +.pagination-wrapper { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + border-top: 1px solid #e8e8e8; + background: #fafafa; +} + +.pagination-info { + color: #666; + font-size: 14px; +} + +.pagination-controls { + display: flex; + align-items: center; + gap: 8px; +} + +.page-numbers { + display: flex; + align-items: center; + gap: 4px; +} + +.page-btn { + min-width: 32px; + height: 32px; + padding: 0 8px; + border: 1px solid #d9d9d9; + background: #fff; + color: #333; + cursor: pointer; + transition: all 0.2s; +} + +.page-btn:hover { + border-color: #40a9ff; + color: #40a9ff; +} + +.page-btn.active { + background: #1890ff; + border-color: #1890ff; + color: #fff; +} + +.page-ellipsis { + padding: 0 8px; + color: #999; +} + +.page-size-selector { + margin-left: 16px; +} + +.page-size-selector select { + padding: 4px 8px; + border: 1px solid #d9d9d9; + border-radius: 4px; + background: #fff; + color: #333; + font-size: 14px; +} + +/* 响应式 */ +@media (max-width: 768px) { + .table-toolbar { + flex-direction: column; + gap: 12px; + align-items: stretch; + } + + .bulk-actions { + justify-content: flex-end; + } + + .pagination-wrapper { + flex-direction: column; + gap: 12px; + align-items: stretch; + } + + .pagination-controls { + justify-content: center; + } + + .page-size-selector { + margin-left: 0; + text-align: center; + } + + .action-buttons { + flex-direction: column; + gap: 4px; + } + + .actions-column { + width: 120px; + } + + .btn { + font-size: 11px; + padding: 4px 8px; + } +} \ No newline at end of file diff --git a/web/src/apps/muse/base/table/types.ts b/web/src/apps/muse/base/table/types.ts new file mode 100644 index 0000000..e166291 --- /dev/null +++ b/web/src/apps/muse/base/table/types.ts @@ -0,0 +1,58 @@ +import { Mark } from '../mock/collection'; + +// 表格列配置 +export interface TableColumn { + key: string; + title: string; + dataIndex: string; + width?: number; + render?: (value: any, record: T, index: number) => React.ReactNode; + sortable?: boolean; + fixed?: 'left' | 'right'; +} + +// 表格行选择配置 +export interface RowSelection { + type?: 'checkbox' | 'radio'; + selectedRowKeys?: React.Key[]; + onChange?: (selectedRowKeys: React.Key[], selectedRows: T[]) => void; + getCheckboxProps?: (record: T) => { disabled?: boolean }; +} + +// 分页配置 +export interface PaginationConfig { + current: number; + pageSize: number; + total: number; + showSizeChanger?: boolean; + showQuickJumper?: boolean; + showTotal?: (total: number, range: [number, number]) => string; + onChange?: (page: number, pageSize: number) => void; +} + +// 表格操作按钮类型 +export interface ActionButton { + key: string; + label: string; + type?: 'primary' | 'default' | 'danger'; + icon?: React.ReactNode; + onClick: (record: Mark) => void; + disabled?: (record: Mark) => boolean; +} + +// 表格属性 +export interface TableProps { + data: Mark[]; + columns: TableColumn[]; + loading?: boolean; + rowSelection?: RowSelection; + pagination?: PaginationConfig | false; + actions?: ActionButton[]; + onSort?: (field: string, order: 'asc' | 'desc' | null) => void; +} + +// 排序状态 +export interface SortState { + field: string | null; + order: 'asc' | 'desc' | null; +} \ No newline at end of file diff --git a/web/src/apps/muse/index.tsx b/web/src/apps/muse/index.tsx new file mode 100644 index 0000000..3d987ab --- /dev/null +++ b/web/src/apps/muse/index.tsx @@ -0,0 +1,126 @@ +import { ToastContainer } from 'react-toastify'; +import { AuthProvider } from '../login/AuthProvider'; +import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; +import { useState } from 'react'; + +import { VadVoice } from './videos/modules/VadVoice.tsx'; +import { ChatInterface } from './prompts/index.tsx'; +import { BaseApp } from './base/index.tsx'; + +const LeftPanel = () => { + return ( + +
+ +
+
+ ); +}; + +const CenterPanel = () => { + return ( + +
+ +
+
+ ); +}; + +const RightPanel = ({ isVisible }: { isVisible: boolean }) => { + if (!isVisible) return null; + + return ( + +
+

Right Panel

+
+ +
+
+
+ ); +}; + +export const MuseApp = () => { + const [showRightPanel, setShowRightPanel] = useState(true); + const [showLeftPanel, setShowLeftPanel] = useState(true); + const [showCenterPanel, setShowCenterPanel] = useState(true); + + return ( +
+ {/* Panel Controls */} +
+
+ + + +
+
+ + {/* Resizable Panels */} +
+ + {showLeftPanel && } + + {showLeftPanel && showCenterPanel && ( + + )} + + {showCenterPanel && } + + {showCenterPanel && showRightPanel && ( + + )} + + {showRightPanel && } + +
+
+ ); +} + + +export const App: React.FC = () => { + return ( + + + + + ); +}; \ No newline at end of file diff --git a/web/src/apps/muse/prompts/index.tsx b/web/src/apps/muse/prompts/index.tsx new file mode 100644 index 0000000..53344e1 --- /dev/null +++ b/web/src/apps/muse/prompts/index.tsx @@ -0,0 +1,190 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { Send, Bot, User } from 'lucide-react'; + +interface Message { + id: string; + content: string; + role: 'user' | 'assistant'; + timestamp: Date; +} + +export const ChatInterface: React.FC = () => { + const [messages, setMessages] = useState([ + { + id: '1', + content: '你好!我是AI助手,有什么可以帮助您的吗?', + role: 'assistant', + timestamp: new Date() + } + ]); + const [inputValue, setInputValue] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + + // 自动滚动到最新消息 + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + // 发送消息 + const handleSend = async () => { + if (!inputValue.trim() || isLoading) return; + + const userMessage: Message = { + id: Date.now().toString(), + content: inputValue.trim(), + role: 'user', + timestamp: new Date() + }; + + setMessages(prev => [...prev, userMessage]); + setInputValue(''); + setIsLoading(true); + + // 模拟AI回复 + setTimeout(() => { + const aiMessage: Message = { + id: (Date.now() + 1).toString(), + content: `我收到了您的消息:"${userMessage.content}"。这里是我的回复,您还有其他问题吗?`, + role: 'assistant', + timestamp: new Date() + }; + setMessages(prev => [...prev, aiMessage]); + setIsLoading(false); + }, 1000); + }; + + // 处理键盘事件 + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + // 格式化时间 + const formatTime = (date: Date) => { + return date.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }); + }; + + return ( +
+ {/* 头部 */} +
+
+
+ +
+
+

AI 助手

+

在线 · 随时为您服务

+
+
+
+ + {/* 对话列表区域 */} +
+ {messages.map((message) => ( +
+ {message.role === 'assistant' && ( +
+ +
+ )} + +
+
+ {message.content} +
+
+ {formatTime(message.timestamp)} +
+
+ + {message.role === 'user' && ( +
+ +
+ )} +
+ ))} + + {/* 加载状态 */} + {isLoading && ( +
+
+ +
+
+
+
+
+
+
+
+
+ )} + +
+
+ + {/* 输入框区域 */} +
+
+
+