sync: Add files via upload (A@f25beaaf2f1fcd0c8fca85fa622d846392968047)

This commit is contained in:
github-actions[bot] 2026-05-27 03:17:46 +00:00
parent 6517a3f4cd
commit 37ecf1a056
137 changed files with 7885 additions and 641 deletions

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"total 1152\ndrwxr-xr-x 2 root root 4096 May 25 20:10 .\ndrwxr-xr-x 1 root root 4096 May 25 19:48 ..\n-rw-r--r-- 1 root root 77 May 25 19:45 1feff74faaf0efc6a044\n-rw-r--r-- 1 root root 148268 May 25 19:45 core.6fdc2188.js\n-rw-r--r-- 1 root root 112078 May 25 19:45 framer-motion.022bba29.js\n-rw-r--r-- 1 root root 1256 May 25 20:10 index.html\n-rw-r--r-- 1 root root 669973 May 25 20:10 main.1bee7fcd.js\n-rw-r--r-- 1 root root 1910 May 25 19:45 runtime.6db664f3.js\n-rw-r--r-- 1 root root 21712 May 25 19:45 ui.8f0f0b07.js\n-rw-r--r-- 1 root root 192832 May 25 19:45 vendors.750695a4.js"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 dev /home/project\n> webpack serve -- --port 3015\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n<i> [webpack-dev-server] Project is running at:\n<i> [webpack-dev-server] Loopback: http://localhost:3015/\n<i> [webpack-dev-server] Content not from webpack is served from '/home/project/public' directory\n<i> [webpack-dev-server] 404s will fallback to '/index.html'\n"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"total 128\ndrwxr-xr-x 4 root root 4096 May 25 18:00 .\ndrwxr-xr-x 13 root root 4096 May 25 18:00 ..\n-rw-r--r-- 1 root root 5763 May 25 18:00 ComingSoonModal.tsx\n-rw-r--r-- 1 root root 7598 May 25 18:00 DemoDisclaimerModal.tsx\n-rw-r--r-- 1 root root 316 May 25 18:00 DemoWatermark.tsx\n-rw-r--r-- 1 root root 5020 May 25 18:00 ErrorBoundary.tsx\n-rw-r--r-- 1 root root 8129 May 25 19:43 InventoryRecordsModal.tsx\n-rw-r--r-- 1 root root 3884 May 25 18:00 NetworkStatusBar.tsx\n-rw-r--r-- 1 root root 9933 May 25 18:00 NotificationCenter.tsx\n-rw-r--r-- 1 root root 10814 May 25 18:00 OnboardingGuide.tsx\n-rw-r--r-- 1 root root 4638 May 25 18:00 PageTransition.tsx\n-rw-r--r-- 1 root root 3113 May 25 18:00 PlanCard.tsx\n-rw-r--r-- 1 root root 17566 May 25 19:43 ProcessFlow.tsx\n-rw-r--r-- 1 root root 1327 May 25 18:00 ProgressBar.tsx\n-rw-r--r-- 1 root root 1211 May 25 18:00 StatusBadge.tsx\n-rw-r--r-- 1 root root 7450 May 25 18:00 VirtualList.tsx\ndrwxr-xr-x 2 root root 4096 May 25 18:00 plans\ndrwxr-xr-x 2 root root 4096 May 25 18:00 warehouse"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"total 96\ndrwxr-xr-x 2 root root 4096 May 25 18:00 .\ndrwxr-xr-x 5 root root 4096 May 25 18:00 ..\n-rw-r--r-- 1 root root 8287 May 25 19:43 CompletedFabric.tsx\n-rw-r--r-- 1 root root 15750 May 25 18:00 Dashboard.tsx\n-rw-r--r-- 1 root root 13730 May 25 19:43 FinishedWarehouse.tsx\n-rw-r--r-- 1 root root 6782 May 25 18:00 PaymentPending.tsx\n-rw-r--r-- 1 root root 6464 May 25 18:00 PendingFabric.tsx\n-rw-r--r-- 1 root root 26721 May 25 19:43 PlanOverview.tsx"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"total 172\ndrwxr-xr-x 2 root root 4096 May 25 18:00 .\ndrwxr-xr-x 5 root root 4096 May 25 18:00 ..\n-rw-r--r-- 1 root root 14798 May 25 19:43 AccountsPayable.tsx\n-rw-r--r-- 1 root root 15336 May 25 19:27 Dashboard.tsx\n-rw-r--r-- 1 root root 19115 May 25 18:00 FactoryManage.tsx\n-rw-r--r-- 1 root root 13401 May 25 19:43 FinishedWarehouse.tsx\n-rw-r--r-- 1 root root 20754 May 25 19:38 NewPlan.tsx\n-rw-r--r-- 1 root root 21162 May 25 18:00 NewWashingPlan.tsx\n-rw-r--r-- 1 root root 14074 May 25 18:00 PlanOverview.tsx\n-rw-r--r-- 1 root root 29645 May 25 19:43 WarehouseManage.tsx"}

View File

@ -0,0 +1 @@
{"results":[{"file":"src/types/index.ts","matches":[{"line":29,"content":"export type WashingPlan = Tables<'washing_plans'>;"},{"line":31,"content":"export type WashingPlanCompletion = Tables<'washing_plan_completions'>;"}]},{"file":"src/pages/washing/PlanOverview.tsx","matches":[{"line":36,"content":"export function WashingPlanOverview() {"}]},{"file":"src/pages/purchaser/NewWashingPlan.tsx","matches":[{"line":28,"content":"export function NewWashingPlan() {"}]}],"pattern":"export.*WashingPlan","path":"src","totalMatches":4}

View File

@ -0,0 +1 @@
{"results":[{"file":"src/supabase/types.ts","matches":[{"line":441,"content":" referencedRelation: \"washing_plans\""},{"line":1239,"content":" referencedRelation: \"washing_plans\""},{"line":1244,"content":" washing_plans: {"},{"line":1310,"content":" foreignKeyName: \"washing_plans_company_id_fkey\""},{"line":1316,"content":" foreignKeyName: \"washing_plans_created_by_fkey\""},{"line":1322,"content":" foreignKeyName: \"washing_plans_product_id_fkey\""},{"line":1328,"content":" foreignKeyName: \"washing_plans_washing_factory_id_fkey\""},{"line":1388,"content":" referencedRelation: \"washing_plans\""}]}],"pattern":"washing_plans","path":"src/supabase","totalMatches":8}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 dev /home/project\n> webpack serve -- --port 3015\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n<i> [webpack-dev-server] Project is running at:\n<i> [webpack-dev-server] Loopback: http://localhost:3015/\n<i> [webpack-dev-server] Content not from webpack is served from '/home/project/public' directory\n<i> [webpack-dev-server] 404s will fallback to '/index.html'\n"}

View File

@ -0,0 +1 @@
{"exitCode":-1,"output":"","stderr":"Command execution timed out","timedOut":true}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"ls: cannot access '/home/project/public/': No such file or directory\npublic directory does not exist"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"drwxr-xr-x 2 root root 4096 May 25 20:10 dist"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"total 8\ndrwxr-xr-x 2 root root 4096 May 25 20:20 .\ndrwxr-xr-x 1 root root 4096 May 25 20:20 .."}

View File

@ -0,0 +1 @@
{"exitCode":-1,"output":"","stderr":"🛡️ 命令已被拦截 - 该命令可能会破坏 sandbox 运行环境\n\n❌ 拦截原因:\n以下命令可能会杀掉 sandbox 内置服务进程SDK server、log server、dev server、cors proxy 等),导致沙箱崩溃:\n - `pkill -f \"webpack serve\" 2>/dev/null` 中的模式 \"webpack serve\" 过于宽泛\n\n💡 建议:\n请使用更精确的方式停止目标进程\n 1. 通过端口查找:`lsof -i :PORT -t | xargs kill` 只杀占用特定端口的进程\n 2. 通过精确路径匹配:`pkill -f \"/home/project/.*serve\"` 只杀用户项目内的进程\n 3. 通过 PID先 `ps aux | grep xxx` 确认目标 PID再 `kill PID`\n 4. 如果要停止 dev server直接运行新的 `npm run dev` 即可(会自动替换旧进程)"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 dev /home/project\n> webpack serve -- --port 3015\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n<i> [webpack-dev-server] Project is running at:\n<i> [webpack-dev-server] Loopback: http://localhost:3015/\n<i> [webpack-dev-server] Content not from webpack is served from '/home/project/public' directory\n<i> [webpack-dev-server] 404s will fallback to '/index.html'\n"}

View File

@ -0,0 +1 @@
{"exitCode":-1,"output":"","stderr":"Command execution timed out","timedOut":true}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nwebpack 25764 root 18u IPv4 260876 0t0 TCP *:3015 (LISTEN)\nwebpack 25764 root 23u IPv4 262983 0t0 TCP localhost:3015->localhost:44306 (ESTABLISHED)\nwebpack 25764 root 25u IPv4 260974 0t0 TCP localhost:3015->localhost:44322 (ESTABLISHED)\nwebpack 25764 root 26u IPv4 260975 0t0 TCP localhost:3015->localhost:44336 (ESTABLISHED)\ncurl 25827 root 5u IPv4 260944 0t0 TCP localhost:44336->localhost:3015 (ESTABLISHED)"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"root 25763 0.0 0.0 2584 896 pts/0 S 20:21 0:00 sh -c webpack serve -- --port 3015\nroot 25764 57.9 4.2 33027056 360500 pts/0 Sl 20:21 0:14 webpack"}

View File

@ -0,0 +1 @@
{"exitCode":-1,"output":"","stderr":"Command execution timed out","timedOut":true}

View File

@ -0,0 +1,25 @@
The file /home/project/webpack.config.js has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
131→ },
132→ resolve: {
133→ extensions: ['.mjs', '.ts', '.tsx', '.js', '.jsx']
134→ },
135→ devServer: {
136→ // 约束 A沙箱只开放一个代理端口所有 builderwebpack/vite/electron统一 3015
137→ port: 3015,
138→ host: '0.0.0.0',
139→ allowedHosts: 'all',
140→ hot: true,
141→ historyApiFallback: {
142→ index: '/index.html',
143→ rewrites: [
144→ { from: /^\/_p\/\d+\//, to: '/index.html' }
145→ ]
146→ }
147→ },
148→ plugins: [
149→ new HtmlWebpackPlugin({
150→ template: './index.html',
151→ inject: 'body',
152→ minify: !isDev ? {

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 build /home/project\n> webpack --mode production\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\nassets by status 1.09 MiB [cached] 7 assets\nasset index.html 1.23 KiB [compared for emit]\nEntrypoint main [big] 1.09 MiB = 6 assets\norphan modules 3.11 MiB [orphan] 1697 modules\nruntime modules 4.72 KiB 9 modules\ncacheable modules 2.62 MiB\n modules by path ./node_modules/.pnpm/lucide-react@0.294.0_react@18.3.1/node_modules/lucide-r...(truncated) 38.7 KiB 67 modules\n modules by path ./node_modules/.pnpm/framer-motion@11.18.2_@emotion+is-prop-valid@1.4.0_react-do...(truncated) 394 KiB 9 modules\n modules by path ./node_modules/.pnpm/style-loader@3.3.4_webpack@5.105.4/node_modules/style-loade...(truncated) 5.84 KiB 6 modules\n modules by path ./node_modules/.pnpm/motion-utils@11.18.1/node_modules/motion-utils...(truncated) 1.63 KiB 5 modules\n modules by path ./node_modules/.pnpm/react@18.3.1/node_modules/react/ 8 KiB 4 modules\n modules by path ./node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/ 131 KiB 3 modules\n modules by path ./src/ 1.18 MiB 2 modules\n modules by path ./node_modules/.pnpm/css-loader@6.11.0_webpack@5.105.4/node_modules/css-loader/d...(truncated) 2.31 KiB 2 modules\n modules by path ./node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/ 4.33 KiB 2 modules\n + 7 modules\n\nWARNING in asset size limit: The following asset(s) exceed the recommended size limit (500 KiB).\nThis can impact web performance.\nAssets: \n main.1bee7fcd.js (654 KiB)\n\nWARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (500 KiB). This can impact web performance.\nEntrypoints:\n main (1.09 MiB)\n runtime.6db664f3.js\n core.6fdc2188.js\n framer-motion.022bba29.js\n ui.8f0f0b07.js\n vendors.750695a4.js\n main.1bee7fcd.js\n\nWARNING in webpack performance recommendations: \nYou can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.\nFor more info visit https://webpack.js.org/guides/code-splitting/\n\nwebpack 5.105.4 compiled with 3 warnings in 10487 ms"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 dev /home/project\n> webpack serve -- --port 3015\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n<i> [webpack-dev-server] Project is running at:\n<i> [webpack-dev-server] Loopback: http://localhost:3015/\n<i> [webpack-dev-server] Content not from webpack is served from '/home/project/public' directory\n<i> [webpack-dev-server] 404s will fallback to '/index.html'\n"}

View File

@ -0,0 +1 @@
{"exitCode":-1,"output":"","stderr":"Command execution timed out","timedOut":true}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nwebpack 26361 root 18u IPv4 269636 0t0 TCP *:3015 (LISTEN)\nwebpack 26361 root 21u IPv4 269867 0t0 TCP localhost:3015->localhost:36858 (ESTABLISHED)\nwebpack 26361 root 25u IPv4 269865 0t0 TCP localhost:3015->localhost:36834 (ESTABLISHED)\nwebpack 26361 root 26u IPv4 269866 0t0 TCP localhost:3015->localhost:36842 (ESTABLISHED)"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":""}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"root 26360 0.0 0.0 2584 896 pts/0 S 20:22 0:00 sh -c webpack serve -- --port 3015\nroot 26361 9.6 2.9 32924056 253052 pts/0 Sl 20:22 0:15 webpack"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME\nwebpack 26361 root 18u IPv4 269636 0t0 TCP *:3015 (LISTEN)\nwebpack 26361 root 19u IPv4 273341 0t0 TCP localhost:3015->localhost:35426 (ESTABLISHED)\nwebpack 26361 root 21u IPv4 269867 0t0 TCP localhost:3015->localhost:36858 (ESTABLISHED)\nwebpack 26361 root 23u IPv4 275503 0t0 TCP localhost:3015->localhost:51008 (ESTABLISHED)\ncurl 26424 root 5u IPv4 268715 0t0 TCP localhost:36858->localhost:3015 (ESTABLISHED)"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"total 1152\ndrwxr-xr-x 2 root root 4096 May 25 20:10 .\ndrwxr-xr-x 1 root root 4096 May 25 20:20 ..\n-rw-r--r-- 1 root root 77 May 25 19:45 1feff74faaf0efc6a044\n-rw-r--r-- 1 root root 148268 May 25 19:45 core.6fdc2188.js\n-rw-r--r-- 1 root root 112078 May 25 19:45 framer-motion.022bba29.js\n-rw-r--r-- 1 root root 1256 May 25 20:10 index.html\n-rw-r--r-- 1 root root 669973 May 25 20:10 main.1bee7fcd.js\n-rw-r--r-- 1 root root 1910 May 25 19:45 runtime.6db664f3.js\n-rw-r--r-- 1 root root 21712 May 25 19:45 ui.8f0f0b07.js\n-rw-r--r-- 1 root root 192832 May 25 19:45 vendors.750695a4.js"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"<!doctype html><html lang=\"zh-CN\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>织机 - 以AI为梭织天地经纬</title><link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css\"><style>body,html{margin:0;padding:0;width:100%;height:100%}html.light,html.light body{background-color:#fff!important;color:#000}html.dark,html.dark body{background-color:#1a1a1a!important;color:#fff}</style><script>!function(){function t(t){document.documentElement.classList.remove(\"light\",\"dark\"),document.documentElement.classList.add(t),document.documentElement.setAttribute(\"data-theme\",t)}window.self!==window.top?window.addEventListener(\"message\",function(e){if(e.data&&\"string\"==typeof e.data.theme){const n=e.data.theme;\"light\"!==n&&\"dark\"!==n||t(n)}}):t(\"light\")}()</script></head><body><div id=\"root\"></div><script defer=\"defer\" src=\"runtime.6db664f3.js\"></script><script defer=\"defer\" src=\"core.6fdc2188.js\"></script><script defer=\"defer\" src=\"framer-motion.022bba29.js\"></script><script defer=\"defer\" src=\"ui.8f0f0b07.js\"></script><script defer=\"defer\" src=\"vendors.750695a4.js\"></script><script defer=\"defer\" src=\"main.1bee7fcd.js\"></script></body></html>"}

View File

@ -0,0 +1 @@
{"exitCode":-1,"output":"","stderr":"Command execution timed out","timedOut":true}

View File

@ -0,0 +1 @@
{"exitCode":-1,"output":"","stderr":"Command execution timed out","timedOut":true}

View File

@ -0,0 +1,32 @@
The file /home/project/webpack.config.js has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
131→ },
132→ resolve: {
133→ extensions: ['.mjs', '.ts', '.tsx', '.js', '.jsx']
134→ },
135→ devServer: {
136→ // 约束 A沙箱只开放一个代理端口所有 builderwebpack/vite/electron统一 3015
137→ port: 3015,
138→ host: '0.0.0.0',
139→ allowedHosts: 'all',
140→ hot: true,
141→ static: {
142→ directory: path.join(__dirname, 'public'),
143→ publicPath: '/',
144→ },
145→ historyApiFallback: {
146→ index: '/index.html',
147→ rewrites: [
148→ { from: /^\/_p\/\d+\//, to: '/index.html' }
149→ ]
150→ },
151→ devMiddleware: {
152→ writeToDisk: true,
153→ },
154→ },
155→ plugins: [
156→ new HtmlWebpackPlugin({
157→ template: './index.html',
158→ inject: 'body',
159→ minify: !isDev ? {

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 dev /home/project\n> webpack serve -- --port 3015\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n<i> [webpack-dev-server] Project is running at:\n<i> [webpack-dev-server] Loopback: http://localhost:3015/\n<i> [webpack-dev-server] Content not from webpack is served from '/home/project/public' directory\n<i> [webpack-dev-server] 404s will fallback to '/index.html'\n"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":""}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":""}

View File

@ -0,0 +1,16 @@
The file /home/project/webpack.config.js has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
137→ port: 3015,
138→ host: '0.0.0.0',
139→ allowedHosts: 'all',
140→ hot: true,
141→ static: {
142→ directory: path.join(__dirname, 'dist'),
143→ publicPath: '/',
144→ },
145→ historyApiFallback: {
146→ index: '/index.html',
147→ rewrites: [
148→ { from: /^\/_p\/\d+\//, to: '/index.html' }
149→ ]

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}

View File

@ -0,0 +1,19 @@
The file /home/project/webpack.config.js has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
8→
9→ return {
10→ mode: isDev ? 'development' : 'production',
11→ entry: './src/index.tsx',
12→ output: {
13→ path: path.resolve(__dirname, 'dist'),
14→ filename: '[name].js',
15→ chunkFilename: '[name].chunk.js',
16→ publicPath: '/',
17→ clean: true,
18→ },
19→ optimization: {
20→ minimize: !isDev,
21→ minimizer: [
22→ new TerserPlugin({
23→ terserOptions: {

View File

@ -0,0 +1 @@
{"exitCode":0,"output":""}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 dev /home/project\n> webpack serve -- --port 3015\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n<i> [webpack-dev-server] Project is running at:\n<i> [webpack-dev-server] Loopback: http://localhost:3015/\n<i> [webpack-dev-server] Content not from webpack is served from '/home/project/public' directory\n<i> [webpack-dev-server] 404s will fallback to '/index.html'\n"}

View File

@ -0,0 +1 @@
{"success":true,"message":"Cloud 已开通页面中通过以下方式获取baseUrl: function getSupabaseUrl(): string {\n return `${(window as any).MEOO_CONFIG?.meoo_app_access_url || location.origin}/sb-api`;\n}anonKey: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwiaWF0IjoxNzc5MzM2NjUyLCJleHAiOjEzMjg5OTc2NjUyfQ.NQrukIeqa-IEBtFEf8YOsVK4vxbi6kQqIi-eyRsb2pA","data":{"enabled":true,"instance":{"id":120671,"appId":"uf9sduwnc7f2","status":2,"name":"077f3cdf-5086-454f-bfeb-2740fc01b755","description":"纺织采购计划管理系统","sbInfo":{"vpcHost":"","anonKey":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJvbGUiOiJhbm9uIiwiaWF0IjoxNzc5MzM2NjUyLCJleHAiOjEzMjg5OTc2NjUyfQ.NQrukIeqa-IEBtFEf8YOsVK4vxbi6kQqIi-eyRsb2pA","serviceKey":""},"pgInfo":{"dbInstanceStorage":null},"appInfos":[{"id":"uf9sduwnc7f2","description":""}],"hasEdgeFunctions":true,"needResume":null},"errCode":null,"errMessage":null}}

View File

@ -0,0 +1 @@
{"results":[{"file":"src/hooks/useNetworkStatus.ts","matches":[{"line":61,"content":" await fetch(`${window.location.origin}/sb-api/health`, { "}]}],"pattern":"/sb-api/health|health.*check|healthCheck","path":"src","totalMatches":1}

View File

@ -0,0 +1 @@
{"results":[{"file":"src/supabase/client.ts","matches":[{"line":10,"content":" return `${(window as any).MEOO_CONFIG?.meoo_app_access_url || location.origin}/sb-api`;"}]}],"pattern":"MEOO_CONFIG|meoo_app_access_url","path":"src","totalMatches":1}

View File

@ -0,0 +1,30 @@
The file /home/project/src/hooks/useNetworkStatus.ts has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
50→
51→ window.addEventListener('online', handleOnline);
52→ window.addEventListener('offline', handleOffline);
53→
54→ // 检测慢网络(使用轻量级 DNS 预解析代替 HTTP 请求)
55→ const checkConnectionSpeed = async () => {
56→ if (!navigator.onLine) return;
57→
58→ // 仅通过 navigator.connection API 检测(如果可用)
59→ const connection = (navigator as any).connection;
60→ if (connection) {
61→ setState(prev => ({
62→ ...prev,
63→ isSlowConnection: connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g'
64→ }));
65→ }
66→ // 如果不支持 Connection API则不进行主动探测避免产生大量 502 错误
67→ };
68→
69→ // 每60秒检测一次网络质量降低频率
70→ const interval = setInterval(checkConnectionSpeed, 60000);
71→ checkConnectionSpeed();
72→
73→ return () => {
74→ window.removeEventListener('online', handleOnline);
75→ window.removeEventListener('offline', handleOffline);
76→ clearInterval(interval);

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}

View File

@ -0,0 +1 @@
{"exitCode":-1,"output":"","stderr":"🛡️ 命令已被拦截 - 该命令可能会破坏 sandbox 运行环境\n\n❌ 拦截原因:\n以下命令可能会杀掉 sandbox 内置服务进程SDK server、log server、dev server、cors proxy 等),导致沙箱崩溃:\n - `pkill -f \"webpack serve\" 2>/dev/null` 中的模式 \"webpack serve\" 过于宽泛\n\n💡 建议:\n请使用更精确的方式停止目标进程\n 1. 通过端口查找:`lsof -i :PORT -t | xargs kill` 只杀占用特定端口的进程\n 2. 通过精确路径匹配:`pkill -f \"/home/project/.*serve\"` 只杀用户项目内的进程\n 3. 通过 PID先 `ps aux | grep xxx` 确认目标 PID再 `kill PID`\n 4. 如果要停止 dev server直接运行新的 `npm run dev` 即可(会自动替换旧进程)"}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"25521"}

View File

@ -0,0 +1 @@
{"results":[],"pattern":"sb-api/health|/health","path":"src","totalMatches":0}

View File

@ -0,0 +1 @@
{"results":[{"file":"src/hooks/useNetworkStatus.ts","matches":[{"line":55,"content":" const checkConnectionSpeed = async () => {"},{"line":70,"content":" const interval = setInterval(checkConnectionSpeed, 60000);"},{"line":71,"content":" checkConnectionSpeed();"}]}],"pattern":"checkConnectionSpeed|setInterval","path":"src/hooks/useNetworkStatus.ts","totalMatches":3}

View File

@ -0,0 +1 @@
{"files":["src/App.tsx","src/pages/MemberManage.tsx","src/pages/RoleSelectPage.tsx","src/pages/RegisterPage.tsx","src/pages/ImportPlanPage.tsx","src/pages/LoginPage.tsx","src/components/ErrorBoundary.tsx","src/components/NotificationCenter.tsx","src/contexts/AuthContext.tsx","src/pages/purchaser/FinishedWarehouse.tsx","src/pages/purchaser/FactoryManage.tsx","src/pages/purchaser/WarehouseManage.tsx","src/pages/purchaser/Dashboard.tsx","src/pages/purchaser/AccountsPayable.tsx","src/pages/purchaser/NewWashingPlan.tsx","src/pages/purchaser/NewPlan.tsx","src/pages/purchaser/PlanOverview.tsx","src/pages/textile/PaymentPending.tsx","src/pages/textile/PlanOverview.tsx","src/pages/textile/Dashboard.tsx","src/pages/textile/YarnWarehouse.tsx","src/pages/textile/FabricWarehouse.tsx","src/pages/washing/CompletedFabric.tsx","src/pages/washing/FinishedWarehouse.tsx","src/pages/washing/PaymentPending.tsx","src/pages/washing/PlanOverview.tsx","src/pages/washing/Dashboard.tsx","src/pages/washing/PendingFabric.tsx"],"pattern":"AuthContext|useAuth|navigate.*login|window\\.location.*reload","path":"src","totalFiles":28}

View File

@ -0,0 +1 @@
{"files":["src/supabase/client.ts","src/hooks/useNetworkStatus.ts"],"pattern":"sb-api|supabase.*error|502|Bad Gateway","path":"src","totalFiles":2}

View File

@ -0,0 +1 @@
{"results":[{"file":"src/pages/MemberManage.tsx","matches":[{"line":170,"content":" window.location.reload();"}]},{"file":"src/components/ErrorBoundary.tsx","matches":[{"line":61,"content":" window.location.reload();"}]}],"pattern":"window\\.location\\.reload|location\\.reload|SIGNED_OUT.*navigate","path":"src","totalMatches":2}

View File

@ -0,0 +1 @@
{"results":[{"file":"src/pages/MemberManage.tsx","matches":[{"line":158,"content":" const { error: passwordError } = await supabase.auth.updateUser({"},{"line":226,"content":" const { data: authData, error: signUpError } = await supabase.auth.signUp({"}]},{"file":"src/contexts/AuthContext.tsx","matches":[{"line":38,"content":" const { data: { session } } = await supabase.auth.getSession();"},{"line":69,"content":" const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {"},{"line":96,"content":" } else if (event === 'SIGNED_OUT') {"},{"line":110,"content":" const { error } = await supabase.auth.signInWithPassword({"},{"line":140,"content":" const { data: authData, error: authError } = await supabase.auth.signUp({"},{"line":191,"content":" await supabase.auth.signOut();"}]},{"file":"src/pages/textile/PlanOverview.tsx","matches":[{"line":355,"content":" const session = (await supabase.auth.getSession()).data.session;"}]}],"pattern":"supabase\\.auth|onAuthStateChange|SIGNED_OUT","path":"src","totalMatches":71}

View File

@ -0,0 +1 @@
{"results":[{"file":"src/pages/LoginPage.tsx","matches":[{"line":1,"content":"import React, { useState, useEffect } from 'react';"},{"line":223,"content":" useEffect(() => {"},{"line":277,"content":" const navigate = useNavigate();"},{"line":293,"content":" navigate('/role-select');"},{"line":305,"content":" navigate('/role-select');"}]}],"pattern":"useEffect|navigate|isLoggedIn","path":"src/pages/LoginPage.tsx","totalMatches":54}

View File

@ -0,0 +1,8 @@
The file /home/project/src/hooks/useNetworkStatus.ts has been updated using line-based fuzzy matching (handled indentation). Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
229→ }, []);
230→
231→ return { optimisticUpdate };
232→}
233→

View File

@ -0,0 +1 @@
{"results":[{"file":"src/hooks/useNetworkStatus.ts","matches":[{"line":64,"content":" // 注意:不再发起 /sb-api/health 请求,该接口在当前环境返回 502"}]}],"pattern":"fetch|sb-api|health","path":"src/hooks/useNetworkStatus.ts","totalMatches":1}

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}

View File

@ -0,0 +1 @@
{"files":["src/pages/RegisterPage.tsx","src/pages/LoginPage.tsx","src/pages/purchaser/WarehouseManage.tsx","src/pages/purchaser/FinishedWarehouse.tsx","src/pages/purchaser/PlanOverview.tsx","src/pages/purchaser/Dashboard.tsx","src/pages/purchaser/NewWashingPlan.tsx","src/pages/purchaser/AccountsPayable.tsx","src/pages/washing/PaymentPending.tsx","src/pages/washing/PlanOverview.tsx","src/pages/washing/Dashboard.tsx","src/pages/textile/PaymentPending.tsx","src/pages/washing/FinishedWarehouse.tsx","src/pages/textile/PlanOverview.tsx","src/pages/textile/YarnWarehouse.tsx","src/pages/textile/Dashboard.tsx","src/pages/textile/FabricWarehouse.tsx"],"pattern":"sm:hidden|flex-col|whitespace-nowrap|truncate","path":"src/pages","totalFiles":17}

View File

@ -0,0 +1,25 @@
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
5→import { supabase } from '../../supabase/client';
6→import { decode } from 'base64-arraybuffer';
7→import type { Product, ProductYarnRatio } from '../../types';
8→
9→interface ProductFormData {
10→ product_name: string;
11→ weight: string;
12→ warp_weight: string;
13→ weft_weight: string;
14→ total_weight: string;
15→ color: string;
16→ fabric_code: string;
17→ color_code: string;
18→ production_price: string;
19→ yarn_ratios: { yarn_name: string; ratio: number; yarn_type: 'warp' | 'weft' | 'all' }[];
20→ image_url?: string;
21→}
22→
23→interface ProductFormProps {
24→ isOpen: boolean;
25→ onClose: () => void;
26→ onSave: () => void;

View File

@ -0,0 +1,18 @@
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
286→ const triggerFileSelect = () => {
287→ fileInputRef.current?.click();
288→ };
289→
290→ const addYarnRatio = () => {
291→ setFormData(prev => ({
292→ ...prev,
293→ yarn_ratios: [...prev.yarn_ratios, { yarn_name: '', ratio: 100, yarn_type: 'all' }]
294→ }));
295→ };
296→
297→ const removeYarnRatio = (index: number) => {
298→ setFormData(prev => ({
299→ ...prev,
300→ yarn_ratios: prev.yarn_ratios.filter((_, i) => i !== index)

View File

@ -0,0 +1,19 @@
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
300→ yarn_ratios: prev.yarn_ratios.filter((_, i) => i !== index)
301→ }));
302→ };
303→
304→ const updateYarnRatio = (index: number, field: 'yarn_name' | 'ratio' | 'yarn_type', value: string | number) => {
305→ setFormData(prev => {
306→ const newRatios = [...prev.yarn_ratios];
307→ newRatios[index] = { ...newRatios[index], [field]: value };
308→ return { ...prev, yarn_ratios: newRatios };
309→ });
310→ };
311→
312→ if (!isOpen) return null;
313→
314→ return (
315→ <motion.div

View File

@ -0,0 +1,31 @@
The file /home/project/src/components/warehouse/ProductForm.tsx has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
489→ />
490→ </div>
491→ </div>
492→
493→ {/* 克重细分:经线克重、纬线克重、总克重 */}
494→ <div>
495→ <label className="flex items-center gap-2 text-sm font-medium text-gray-700 mb-2">
496→ <Hash className="w-4 h-4 text-gray-400" />
497→ 克重m/g
498→ </label>
499→ <div className={`grid ${isMobile ? 'grid-cols-1 gap-3' : 'grid-cols-3 gap-3'}`}>
500→ <div>
501→ <label className="text-xs text-gray-500 mb-1 block">经线克重</label>
502→ <input
503→ type="number"
<system-reminder>
⚠️ REPEATED FILE MODIFICATION WARNING: You have modified "/home/project/src/components/warehouse/ProductForm.tsx" 5 times (Write + Edit combined)!
You keep rewriting or editing the same file. This is a strong sign your current approach is not working.
🛑 REQUIRED ACTIONS:
1. STOP and think: why do you keep modifying the same file?
2. If the content keeps being wrong, try a fundamentally different approach
3. If you're making incremental fixes, consolidate them into ONE write instead of many small changes
⏰ 3 more attempt(s) before modifications to this file are AUTOMATICALLY BLOCKED.
</system-reminder>

View File

@ -0,0 +1 @@
{"exitCode":0,"output":"[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production\n\n> procurement-planning@1.0.0 typecheck /home/project\n> tsc --noEmit\n\n[OpenTelemetry] Trace 已启动serviceName=oneday-sandbox-sdk-pro version=v0.1.0 env=production"}

View File

@ -0,0 +1,46 @@
The file /home/project/AGENTS.md has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
431→- `src/pages/textile/PaymentPending.tsx` - 动画和移动端适配
432→- `src/components/ProcessFlow.tsx` - 动画优化和移动端输入框
433→- `src/styles/index.css` - 移动端触控优化CSS
434→
435→## 更新日志
436→
437→### 2025-05-26
438→- **计划详情字段完善**
439→ - 采购商和纺织厂计划总览页面统一显示所有技术字段
440→ - 新增显示字段:克重、经线克重、纬线克重、总克重、生产采购价、产品图片、纱线配比、每米纱用量
441→ - 新增时间字段:创建时间、计划开始时间
442→ - 新增备注字段显示
443→ - 纺织厂页面隐藏敏感信息(成品名称、颜色、色号)以保护采购商数据
444→- **移动端 UI 优化**
445→ - 计划卡片移动端适配缩小图片尺寸64x64、字体调整为 text-xs
446→ - 字段标签简化(如"计划编号:"→"编号:"
447→ - 网格布局优化,避免内容溢出
448→ - 所有工作台退出按钮在移动端变为图标形式LogOut图标
449→- **拒绝计划功能增强**
450→ - 纺织厂拒绝计划后立即显示灰色卡片和"已拒绝"标签
451→ - 被拒绝的计划自动折叠
452→ - 被拒绝的计划从 Dashboard "最近计划"中消失
453→ - 使用 localStorage 持久化拒绝状态,跨页面保持同步
454→
455→### 2025-05-25
456→- **产品图片上传功能**
457→ - 为采购商原坯布仓库产品管理添加图片上传功能
458→ - 支持 JPG、PNG 格式图片上传,最大 5MB
459→ - 产品列表展示缩略图,无图片时显示默认占位图标
460→ - 产品表单支持图片预览、重新上传和删除功能
461→ - 使用 Supabase Storage 存储图片文件
462→- **数据库变更**
463→ - `products` 表新增 `image_url` 字段
464→ - 创建 `product_images` 公共存储桶
465→ - 添加存储桶 RLS 策略
466→- **UI 优化**
467→ - 纺织厂工作台"原坯布仓库"更名为"已生产坯布"
468→ - 更新首页快捷操作按钮标签
469→
470→## Bug 修复记录
471→
472→### 2025-05-25 TypeScript 类型兼容性问题
473→

View File

@ -0,0 +1,134 @@
The file /home/project/AGENTS.md has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
466→- **UI 优化**
467→ - 纺织厂工作台"原坯布仓库"更名为"已生产坯布"
468→ - 更新首页快捷操作按钮标签
469→
470→## Bug 修复记录
471→
472→### 2025-05-26 计划字段显示不一致问题
473→
474→#### 问题描述
475→采购商和纺织厂的计划总览页面显示字段不一致纺织厂缺少克重、纱线配比、生产采购价等关键字段。
476→
477→#### 根本原因
478→- `usePlanData.ts` Hook 未查询 `products` 表关联数据
479→- `PlanGroup.tsx` 组件未展示 weight、yarn_ratios、production_price 等字段
480→- 纺织厂 `PlanOverview.tsx` 缺少展开详情视图
481→
482→#### 修复方案
483→1. **数据层**:在 `usePlanData.ts` 中添加 products 表查询,获取 weight、image_url 等字段
484→2. **组件层**:在 `PlanGroup.tsx` 中添加完整字段展示,包括:
485→ - 克重信息(经线/纬线/总克重)
486→ - 纱线配比列表
487→ - 生产采购价
488→ - 产品图片
489→ - 创建时间、计划开始时间、备注
490→3. **页面层**:纺织厂 `PlanOverview.tsx` 添加展开详情功能,与采购商保持一致
491→
492→#### 代码变更
493→- `src/hooks/usePlanData.ts` - 添加 products 表关联查询
494→- `src/components/plans/PlanGroup.tsx` - 扩展字段展示
495→- `src/pages/textile/PlanOverview.tsx` - 添加展开详情视图
496→- `src/pages/purchaser/PlanOverview.tsx` - 同步优化字段展示
497→
498→#### 经验教训
499→- **数据一致性**:同一业务对象在不同页面的展示应保持一致,仅根据角色权限控制敏感字段
500→- **Hook 复用**:通过统一的数据获取 Hook 确保各页面数据一致性
501→- **类型安全**:扩展类型定义时注意与数据库生成的类型保持兼容
502→
503→---
504→
505→### 2025-05-26 移动端退出按钮适配问题
506→
507→#### 问题描述
508→工作台右上角退出按钮在移动端显示为"退出登录"文字,占用过多空间,与整体移动端设计不协调。
509→
510→#### 根本原因
511→- 退出按钮未做响应式适配
512→- 移动端空间宝贵,文字按钮过于拥挤
513→
514→#### 修复方案
515→使用响应式设计桌面端显示"退出登录"文字,移动端仅显示 LogOut 图标:
516→```tsx
517→<button className="flex items-center gap-2 ...">
518→ <span className="hidden sm:inline">退出登录</span>
519→ <LogOut className="w-5 h-5" />
520→</button>
521→```
522→
523→#### 代码变更
524→- `src/pages/purchaser/Dashboard.tsx` - 退出按钮响应式优化
525→- `src/pages/textile/Dashboard.tsx` - 退出按钮响应式优化
526→- `src/pages/washing/Dashboard.tsx` - 退出按钮响应式优化
527→
528→---
529→
530→### 2025-05-26 拒绝计划状态同步问题
531→
532→#### 问题描述
533→纺织厂拒绝计划后计划卡片未立即更新视觉状态且仍显示在 Dashboard "最近计划"中。
534→
535→#### 根本原因
536→- 拒绝操作仅更新数据库,未更新本地状态
537→- Dashboard 和 PlanOverview 之间没有共享拒绝状态
538→- 页面刷新后拒绝状态丢失
539→
540→#### 修复方案
541→1. **本地状态管理**:在 `PlanOverview.tsx` 中使用 `Set<string>` 跟踪被拒绝的计划ID
542→2. **持久化存储**:使用 localStorage 存储拒绝状态,键名为 `textile_rejected_plans`
543→3. **跨页面同步**Dashboard 从 localStorage 读取拒绝状态并过滤最近计划
544→4. **视觉反馈**:被拒绝的计划显示灰色背景、"已拒绝"标签,并自动折叠
545→
546→#### 代码变更
547→- `src/pages/textile/PlanOverview.tsx` - 添加 rejectedPlans 状态和 localStorage 持久化
548→- `src/pages/textile/Dashboard.tsx` - 添加 rejectedPlanIds 状态,过滤最近计划
549→
550→#### 关键代码
551→```typescript
552→// PlanOverview.tsx - 拒绝时持久化
553→const handleRejectStep = async (step: PlanProcessStep) => {
554→ // ... 提交拒绝原因到数据库 ...
555→
556→ // 更新本地状态
557→ setRejectedPlans(prev => new Set([...prev, step.plan_id]));
558→
559→ // 持久化到 localStorage
560→ const stored = localStorage.getItem('textile_rejected_plans');
561→ const rejectedArray = stored ? JSON.parse(stored) : [];
562→ if (!rejectedArray.includes(step.plan_id)) {
563→ rejectedArray.push(step.plan_id);
564→ localStorage.setItem('textile_rejected_plans', JSON.stringify(rejectedArray));
565→ }
566→};
567→
568→// Dashboard.tsx - 读取并过滤
569→useEffect(() => {
570→ // ... 获取计划列表 ...
571→
572→ // 从 localStorage 读取拒绝状态
573→ const stored = localStorage.getItem('textile_rejected_plans');
574→ if (stored) {
575→ setRejectedPlanIds(new Set(JSON.parse(stored)));
576→ }
577→}, []);
578→
579→// 过滤最近计划
580→const filteredPlans = plans.filter(p => !rejectedPlanIds.has(p.id));
581→const recentPlans = filteredPlans.slice(0, 5);
582→```
583→
584→#### 经验教训
585→- **状态共享**:跨页面状态可考虑使用 localStorage、URL 参数或全局状态管理
586→- **乐观更新**:用户操作后应立即更新 UI再同步后端状态
587→- **数据过滤**Dashboard 展示的数据应根据业务规则进行过滤,而非直接展示原始数据
588→
589→---
590→
591→### 2025-05-25 TypeScript 类型兼容性问题
592→
593→#### 问题描述
594→添加图片功能后出现类型错误
595→```
596→Types of property 'image_url' are incompatible.

View File

@ -0,0 +1,58 @@
The file /home/project/README.md has been updated. Made 1 replacement.
Here's the result of running `cat -n` on a snippet of the edited file:
209→- Safari 14+
210→- Firefox 88+
211→- Edge 90+
212→
213→## 更新日志
214→
215→### 2025-05-26
216→- **计划详情字段完善**
217→ - 采购商和纺织厂计划总览页面统一显示所有技术字段
218→ - 新增显示字段:克重、经线克重、纬线克重、总克重、生产采购价、产品图片、纱线配比、每米纱用量
219→ - 新增时间字段:创建时间、计划开始时间
220→ - 新增备注字段显示
221→ - 纺织厂页面隐藏敏感信息(成品名称、颜色、色号)以保护采购商数据
222→- **移动端 UI 优化**
223→ - 计划卡片移动端适配缩小图片尺寸64x64、字体调整为 text-xs
224→ - 字段标签简化(如"计划编号:"→"编号:"
225→ - 网格布局优化,避免内容溢出
226→ - 所有工作台退出按钮在移动端变为图标形式LogOut图标
227→- **拒绝计划功能增强**
228→ - 纺织厂拒绝计划后立即显示灰色卡片和"已拒绝"标签
229→ - 被拒绝的计划自动折叠
230→ - 被拒绝的计划从 Dashboard "最近计划"中消失
231→ - 使用 localStorage 持久化拒绝状态,跨页面保持同步
232→
233→### 2025-05-25
234→- **产品图片上传功能**
235→ - 为采购商原坯布仓库产品管理添加图片上传功能
236→ - 支持在产品创建和编辑时上传产品图片JPG、PNG格式最大5MB
237→ - 产品列表展示40x40px缩略图无图片时显示默认占位图标
238→ - 产品表单支持图片预览、重新上传和删除功能
239→ - 使用 Supabase Storage `product_images` 存储桶存储图片
240→ - 数据库 `products` 表新增 `image_url` 字段
241→- **UI 优化**
242→ - 纺织厂工作台"原坯布仓库"更名为"已生产坯布"(页面标题和首页快捷操作)
243→
244→### 2025-05-24
245→- **新增出入库历史功能**
246→ - 采购商原坯布仓库产品列表新增"出入库历史"按钮
247→ - 支持查看每个产品的完整库存变动记录(批号、匹数、米数、时间)
248→ - 桌面端和移动端均支持
249→- **类型系统优化**
250→ - 修复 `ProductInRecord` 类型定义,与数据库表结构保持一致
251→ - 区分数据库必需字段和前端展示扩展字段
252→- **新增组件**
253→ - `OutboundForm.tsx` - 出库表单组件
254→ - `PlanEditModal.tsx` - 计划编辑弹窗
255→ - `NotificationCenter.tsx` - 通知中心
256→ - `NetworkStatusBar.tsx` - 网络状态栏
257→ - `VirtualList.tsx` - 虚拟列表组件
258→ - `ErrorBoundary.tsx` - 错误边界组件
259→
260→### 2025-05-23
261→- 优化纺织厂工作台UI动画流畅度
262→- 全面适配移动端,优化触控体验
263→- 添加实时数据订阅,入库记录自动更新

View File

@ -0,0 +1 @@
{"files":["src/utils/helpers.ts","src/supabase/types.ts","src/pages/DemoDataSharing.tsx","src/utils/notifications.ts","src/__tests__/utils/helpers.test.ts","src/pages/washing/PendingFabric.tsx","src/pages/washing/CompletedFabric.tsx","src/pages/washing/PlanOverview.tsx","src/pages/washing/FinishedWarehouse.tsx","src/pages/purchaser/WarehouseManage.tsx","src/pages/purchaser/NewWashingPlan.tsx","src/pages/purchaser/PlanOverview.tsx","src/pages/purchaser/AccountsPayable.tsx","src/pages/purchaser/FinishedWarehouse.tsx","src/pages/purchaser/NewPlan.tsx","src/pages/purchaser/Dashboard.tsx","src/components/plans/PlanGroup.tsx","src/components/plans/PlanEditModal.tsx","src/components/warehouse/OutboundForm.tsx","src/components/warehouse/ProductForm.tsx","src/components/warehouse/ProductList.tsx","src/components/warehouse/InboundForm.tsx","src/components/warehouse/InboundRecords.tsx"],"pattern":"product_name|产品名称|成品名称","path":"src","totalFiles":23}

View File

@ -0,0 +1,8 @@
[
{
"id": "6",
"status": "completed",
"content": "验证构建和类型检查",
"priority": "high"
}
]

View File

@ -0,0 +1,26 @@
[
{
"id": "1",
"status": "completed",
"content": "纺织厂工作台集成帮助系统",
"priority": "high"
},
{
"id": "2",
"status": "completed",
"content": "水洗厂工作台集成帮助系统",
"priority": "high"
},
{
"id": "3",
"status": "completed",
"content": "通知中心按钮适配主题色",
"priority": "high"
},
{
"id": "4",
"status": "completed",
"content": "运行类型检查",
"priority": "medium"
}
]

View File

@ -0,0 +1,26 @@
[
{
"id": "1",
"content": "修改采购商 Dashboard.tsx 退出按钮移动端变图标",
"status": "completed",
"priority": "high"
},
{
"id": "2",
"content": "修改纺织厂 Dashboard.tsx 退出按钮移动端变图标",
"status": "completed",
"priority": "high"
},
{
"id": "3",
"content": "修改水洗厂 Dashboard.tsx 退出按钮移动端变图标",
"status": "completed",
"priority": "high"
},
{
"id": "4",
"content": "运行类型检查",
"status": "completed",
"priority": "medium"
}
]

View File

@ -0,0 +1,17 @@
[
{
"content": "修改 LoginPage 添加记住密码和免密登录 UI",
"id": "1",
"status": "completed"
},
{
"content": "修改 AuthContext 支持本地存储凭据",
"id": "2",
"status": "completed"
},
{
"content": "实现自动登录逻辑",
"id": "3",
"status": "completed"
}
]

View File

@ -0,0 +1,20 @@
[
{
"id": "1",
"content": "分析库存数据不一致问题记录8000m显示14200m",
"status": "completed",
"priority": "high"
},
{
"id": "2",
"content": "定位数据计算逻辑错误",
"status": "completed",
"priority": "high"
},
{
"id": "3",
"content": "修复库存统计显示问题",
"status": "completed",
"priority": "high"
}
]

View File

@ -0,0 +1,26 @@
[
{
"id": "1",
"status": "completed",
"content": "纺织厂工作台集成帮助系统",
"priority": "high"
},
{
"id": "2",
"status": "completed",
"content": "水洗厂工作台集成帮助系统",
"priority": "high"
},
{
"id": "3",
"status": "completed",
"content": "通知中心按钮适配主题色",
"priority": "high"
},
{
"id": "4",
"status": "completed",
"content": "运行类型检查",
"priority": "medium"
}
]

View File

@ -0,0 +1,20 @@
[
{
"id": "1",
"content": "修改分享功能,添加工厂名和对方公司名称二次确认",
"status": "in_progress",
"priority": "high"
},
{
"id": "2",
"content": "限制采购商和纺织厂之间仅有一条计划折叠项",
"status": "pending",
"priority": "high"
},
{
"id": "3",
"content": "运行类型检查",
"status": "pending",
"priority": "medium"
}
]

View File

@ -0,0 +1,17 @@
[
{
"content": "修改 LoginPage 添加记住密码和免密登录 UI",
"id": "1",
"status": "in_progress"
},
{
"content": "修改 AuthContext 支持本地存储凭据",
"id": "2",
"status": "pending"
},
{
"content": "实现自动登录逻辑",
"id": "3",
"status": "pending"
}
]

View File

@ -0,0 +1,32 @@
[
{
"id": "1",
"content": "更新新建纺织计划页面 - 显示经线克重、纬线克重、总克重、产品图片",
"status": "completed",
"priority": "high"
},
{
"id": "2",
"content": "更新新建纺织计划页面 - 完善纱线类型显示(经线/纬线/全部)",
"status": "completed",
"priority": "high"
},
{
"id": "3",
"content": "更新计划总览页面 - 显示克重、生产采购价、纱线配比",
"status": "completed",
"priority": "high"
},
{
"id": "4",
"content": "更新 PlanGroup 组件 - 显示更多产品详情",
"status": "completed",
"priority": "high"
},
{
"id": "5",
"content": "验证类型检查",
"status": "completed",
"priority": "medium"
}
]

View File

@ -0,0 +1,62 @@
[
{
"id": "1",
"content": "查看原坯布仓库新建产品相关代码",
"status": "completed",
"priority": "high"
},
{
"id": "2",
"content": "修改克重标签为 克重m/g",
"status": "completed",
"priority": "high"
},
{
"id": "3",
"content": "修改色号识别码为 色号",
"status": "completed",
"priority": "high"
},
{
"id": "4",
"content": "修改所需纱线类型及占比为 纱线名称/占比",
"status": "completed",
"priority": "high"
},
{
"id": "5",
"content": "添加经线/纬线/全部选项到纱线名称前",
"status": "completed",
"priority": "high"
},
{
"id": "6",
"content": "修改 WarehouseManage.tsx 表单类型和保存逻辑",
"status": "completed",
"priority": "high"
},
{
"id": "7",
"content": "修改 types/index.ts 中的 ProductYarnRatio 类型",
"status": "completed",
"priority": "high"
},
{
"id": "8",
"content": "修改 NewPlan.tsx 展示纱线类型信息",
"status": "completed",
"priority": "high"
},
{
"id": "9",
"content": "修改 PlanGroup.tsx 展示纱线类型信息",
"status": "completed",
"priority": "high"
},
{
"id": "10",
"content": "验证修改效果",
"status": "in_progress",
"priority": "medium"
}
]

View File

@ -0,0 +1,62 @@
[
{
"id": "1",
"content": "查看原坯布仓库新建产品相关代码",
"status": "completed",
"priority": "high"
},
{
"id": "2",
"content": "修改克重标签为 克重m/g",
"status": "completed",
"priority": "high"
},
{
"id": "3",
"content": "修改色号识别码为 色号",
"status": "completed",
"priority": "high"
},
{
"id": "4",
"content": "修改所需纱线类型及占比为 纱线名称/占比",
"status": "completed",
"priority": "high"
},
{
"id": "5",
"content": "添加经线/纬线/全部选项到纱线名称前",
"status": "completed",
"priority": "high"
},
{
"id": "6",
"content": "修改 WarehouseManage.tsx 表单类型和保存逻辑",
"status": "completed",
"priority": "high"
},
{
"id": "7",
"content": "修改 types/index.ts 中的 ProductYarnRatio 类型",
"status": "completed",
"priority": "high"
},
{
"id": "8",
"content": "修改 NewPlan.tsx 展示纱线类型信息",
"status": "completed",
"priority": "high"
},
{
"id": "9",
"content": "修改 PlanGroup.tsx 展示纱线类型信息",
"status": "completed",
"priority": "high"
},
{
"id": "10",
"content": "验证修改效果",
"status": "completed",
"priority": "medium"
}
]

View File

@ -1,20 +1,8 @@
[
{
"id": "1",
"content": "更新 AGENTS.md 记录 webpack devServer 预览加载失败问题",
"status": "completed",
"priority": "high"
},
{
"id": "2",
"content": "更新 README.md 添加问题排查和解决方案",
"status": "completed",
"priority": "high"
},
{
"id": "3",
"content": "总结学习反思",
"status": "completed",
"priority": "medium"
"content": "验证构建和类型检查",
"id": "6",
"priority": "high",
"status": "completed"
}
]

446
AGENTS.md
View File

@ -102,17 +102,51 @@ src/
│ │ ├── InboundForm.tsx # 入库表单模态框
│ │ ├── ProductList.tsx # 产品列表组件(桌面+移动端)
│ │ └── InboundRecords.tsx # 入库记录列表
│ └── plans/ # 计划管理组件
│ ├── PlanGroup.tsx # 计划分组组件
│ └── ShareModal.tsx # 分享弹窗组件
│ ├── plans/ # 计划管理组件
│ │ ├── PlanGroup.tsx # 计划分组组件
│ │ ├── ShareModal.tsx # 分享弹窗组件
│ │ └── PlanEditModal.tsx # 计划编辑弹窗
│ ├── HelpCenter.tsx # 帮助中心F1快捷键
│ ├── UserManual.tsx # 用户手册
│ ├── OnboardingGuide.tsx # 新手指引
│ ├── Tooltip.tsx # 工具提示组件
│ ├── VersionUpdate.tsx # 版本更新检查
│ ├── FeedbackModal.tsx # 用户反馈弹窗
│ ├── CrashReporter.tsx # 崩溃报告系统
│ ├── NotificationCenter.tsx # 通知中心
│ └── DemoDisclaimerModal.tsx # Demo免责声明
├── hooks/
│ ├── useInventoryData.ts # 库存数据获取Hook带缓存
│ └── usePlanData.ts # 计划数据获取Hook带缓存
│ ├── usePlanData.ts # 计划数据获取Hook带缓存
│ ├── useResponsive.ts # 响应式Hook
│ └── useTheme.ts # 主题Hook
└── utils/
├── constants.ts # 公共常量配置
└── helpers.ts # 通用工具函数
├── helpers.ts # 通用工具函数
└── shareCrypto.ts # 分享链接加密
```
### 帮助系统组件2025-05-26
| 组件 | 功能 | 位置 |
|------|------|------|
| HelpCenter | 内置帮助中心F1快捷键打开 | 通知中心旁边 |
| UserManual | 用户手册,详细使用指南 | 账号管理中 |
| OnboardingGuide | 新手指引,首次登录显示 | 自动弹窗 |
| Tooltip | 工具提示,悬停显示说明 | 所有按钮 |
| VersionUpdate | 版本更新检查,仅弹一次 | 自动检测 |
| FeedbackModal | 用户反馈渠道 | 账号管理中 |
| CrashReporter | 崩溃报告系统 | 全局监听 |
| AboutModal | 关于窗口 | 账号管理中 |
### 通知中心主题色适配
| 工作台 | 背景色 | 图标色 |
|--------|--------|--------|
| 采购商 | bg-amber-100 | text-amber-700 |
| 纺织厂 | bg-emerald-100 | text-emerald-700 |
| 水洗厂 | bg-purple-100 | text-purple-700 |
## 路由结构
| URL 路径 | 页面 | 说明 |
@ -434,6 +468,36 @@ CREATE TYPE payment_status AS ENUM ('pending', 'completed');
## 更新日志
### 2025-05-27 (v1.0.2)
- **账号管理页面帮助系统集成**
- 新增"帮助与支持"卡片区域
- 集成用户手册入口,支持查看详细使用指南
- 集成用户反馈渠道,支持提交问题或建议
- 集成关于窗口,显示版本信息与版权
- **技术文档更新**
- 新增 Bug 修复记录:帮助系统集成问题
- 新增 Bug 修复记录:计划拒绝状态管理问题
- 新增 Bug 修复记录:通知中心主题色适配问题
- 新增 Bug 修复记录:新手指引弹窗位置问题
### 2025-05-26
- **计划详情字段完善**
- 采购商和纺织厂计划总览页面统一显示所有技术字段
- 新增显示字段:克重、经线克重、纬线克重、总克重、生产采购价、产品图片、纱线配比、每米纱用量
- 新增时间字段:创建时间、计划开始时间
- 新增备注字段显示
- 纺织厂页面隐藏敏感信息(成品名称、颜色、色号)以保护采购商数据
- **移动端 UI 优化**
- 计划卡片移动端适配缩小图片尺寸64x64、字体调整为 text-xs
- 字段标签简化(如"计划编号:"→"编号:"
- 网格布局优化,避免内容溢出
- 所有工作台退出按钮在移动端变为图标形式LogOut图标
- **拒绝计划功能增强**
- 纺织厂拒绝计划后立即显示灰色卡片和"已拒绝"标签
- 被拒绝的计划自动折叠
- 被拒绝的计划从 Dashboard "最近计划"中消失
- 使用 localStorage 持久化拒绝状态,跨页面保持同步
### 2025-05-25
- **产品图片上传功能**
- 为采购商原坯布仓库产品管理添加图片上传功能
@ -451,6 +515,125 @@ CREATE TYPE payment_status AS ENUM ('pending', 'completed');
## Bug 修复记录
### 2025-05-26 计划字段显示不一致问题
#### 问题描述
采购商和纺织厂的计划总览页面显示字段不一致,纺织厂缺少克重、纱线配比、生产采购价等关键字段。
#### 根本原因
- `usePlanData.ts` Hook 未查询 `products` 表关联数据
- `PlanGroup.tsx` 组件未展示 weight、yarn_ratios、production_price 等字段
- 纺织厂 `PlanOverview.tsx` 缺少展开详情视图
#### 修复方案
1. **数据层**:在 `usePlanData.ts` 中添加 products 表查询,获取 weight、image_url 等字段
2. **组件层**:在 `PlanGroup.tsx` 中添加完整字段展示,包括:
- 克重信息(经线/纬线/总克重)
- 纱线配比列表
- 生产采购价
- 产品图片
- 创建时间、计划开始时间、备注
3. **页面层**:纺织厂 `PlanOverview.tsx` 添加展开详情功能,与采购商保持一致
#### 代码变更
- `src/hooks/usePlanData.ts` - 添加 products 表关联查询
- `src/components/plans/PlanGroup.tsx` - 扩展字段展示
- `src/pages/textile/PlanOverview.tsx` - 添加展开详情视图
- `src/pages/purchaser/PlanOverview.tsx` - 同步优化字段展示
#### 经验教训
- **数据一致性**:同一业务对象在不同页面的展示应保持一致,仅根据角色权限控制敏感字段
- **Hook 复用**:通过统一的数据获取 Hook 确保各页面数据一致性
- **类型安全**:扩展类型定义时注意与数据库生成的类型保持兼容
---
### 2025-05-26 移动端退出按钮适配问题
#### 问题描述
工作台右上角退出按钮在移动端显示为"退出登录"文字,占用过多空间,与整体移动端设计不协调。
#### 根本原因
- 退出按钮未做响应式适配
- 移动端空间宝贵,文字按钮过于拥挤
#### 修复方案
使用响应式设计,桌面端显示"退出登录"文字,移动端仅显示 LogOut 图标:
```tsx
<button className="flex items-center gap-2 ...">
<span className="hidden sm:inline">退出登录</span>
<LogOut className="w-5 h-5" />
</button>
```
#### 代码变更
- `src/pages/purchaser/Dashboard.tsx` - 退出按钮响应式优化
- `src/pages/textile/Dashboard.tsx` - 退出按钮响应式优化
- `src/pages/washing/Dashboard.tsx` - 退出按钮响应式优化
---
### 2025-05-26 拒绝计划状态同步问题
#### 问题描述
纺织厂拒绝计划后,计划卡片未立即更新视觉状态,且仍显示在 Dashboard "最近计划"中。
#### 根本原因
- 拒绝操作仅更新数据库,未更新本地状态
- Dashboard 和 PlanOverview 之间没有共享拒绝状态
- 页面刷新后拒绝状态丢失
#### 修复方案
1. **本地状态管理**:在 `PlanOverview.tsx` 中使用 `Set<string>` 跟踪被拒绝的计划ID
2. **持久化存储**:使用 localStorage 存储拒绝状态,键名为 `textile_rejected_plans`
3. **跨页面同步**Dashboard 从 localStorage 读取拒绝状态并过滤最近计划
4. **视觉反馈**:被拒绝的计划显示灰色背景、"已拒绝"标签,并自动折叠
#### 代码变更
- `src/pages/textile/PlanOverview.tsx` - 添加 rejectedPlans 状态和 localStorage 持久化
- `src/pages/textile/Dashboard.tsx` - 添加 rejectedPlanIds 状态,过滤最近计划
#### 关键代码
```typescript
// PlanOverview.tsx - 拒绝时持久化
const handleRejectStep = async (step: PlanProcessStep) => {
// ... 提交拒绝原因到数据库 ...
// 更新本地状态
setRejectedPlans(prev => new Set([...prev, step.plan_id]));
// 持久化到 localStorage
const stored = localStorage.getItem('textile_rejected_plans');
const rejectedArray = stored ? JSON.parse(stored) : [];
if (!rejectedArray.includes(step.plan_id)) {
rejectedArray.push(step.plan_id);
localStorage.setItem('textile_rejected_plans', JSON.stringify(rejectedArray));
}
};
// Dashboard.tsx - 读取并过滤
useEffect(() => {
// ... 获取计划列表 ...
// 从 localStorage 读取拒绝状态
const stored = localStorage.getItem('textile_rejected_plans');
if (stored) {
setRejectedPlanIds(new Set(JSON.parse(stored)));
}
}, []);
// 过滤最近计划
const filteredPlans = plans.filter(p => !rejectedPlanIds.has(p.id));
const recentPlans = filteredPlans.slice(0, 5);
```
#### 经验教训
- **状态共享**:跨页面状态可考虑使用 localStorage、URL 参数或全局状态管理
- **乐观更新**:用户操作后应立即更新 UI再同步后端状态
- **数据过滤**Dashboard 展示的数据应根据业务规则进行过滤,而非直接展示原始数据
---
### 2025-05-25 TypeScript 类型兼容性问题
#### 问题描述
@ -1561,6 +1744,259 @@ pnpm run dev
---
### 分类十七:帮助系统集成问题
#### Bug: 账号管理页面缺少帮助系统入口
**现象**
- 用户手册、用户反馈渠道、关于窗口在账号管理页面中看不到
- 帮助系统组件已创建但未在 MemberManage 页面集成
**根本原因**
- MemberManage.tsx 未导入帮助系统组件
- 缺少状态管理控制弹窗显示
- 未在 UI 中添加入口按钮
**修复方案**
```typescript
// 1. 导入帮助系统组件
import { UserManual } from '../components/UserManual';
import { FeedbackModal } from '../components/FeedbackModal';
import { AboutModal } from '../components/VersionUpdate';
// 2. 添加状态管理
const [showUserManual, setShowUserManual] = useState(false);
const [showFeedback, setShowFeedback] = useState(false);
const [showAbout, setShowAbout] = useState(false);
// 3. 在 UI 中添加入口按钮(位于主账号卡片下方)
<div className="bg-white rounded-lg shadow-sm p-4 md:p-6 mb-6">
<div className="flex items-center gap-2 mb-4">
<BookOpen className="w-5 h-5 text-blue-600" />
<h2 className="font-semibold text-gray-800">帮助与支持</h2>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{/* 用户手册按钮 */}
<motion.button onClick={() => setShowUserManual(true)}>
<BookOpen className="w-5 h-5 text-blue-600" />
<span>用户手册</span>
<span>详细使用指南</span>
</motion.button>
{/* 用户反馈按钮 */}
<motion.button onClick={() => setShowFeedback(true)}>
<MessageSquare className="w-5 h-5 text-emerald-600" />
<span>用户反馈</span>
<span>提交问题或建议</span>
</motion.button>
{/* 关于按钮 */}
<motion.button onClick={() => setShowAbout(true)}>
<Info className="w-5 h-5 text-purple-600" />
<span>关于</span>
<span>版本信息与版权</span>
</motion.button>
</div>
</div>
// 4. 在页面底部添加弹窗组件
<UserManual isOpen={showUserManual} onClose={() => setShowUserManual(false)} />
<FeedbackModal isOpen={showFeedback} onClose={() => setShowFeedback(false)} />
<AboutModal isOpen={showAbout} onClose={() => setShowAbout(false)} />
```
**设计要点**
- 使用卡片式布局,与页面整体风格一致
- 三个按钮使用不同主题色(蓝/绿/紫)便于区分
- 按钮包含图标、标题和描述,信息清晰
- 位于主账号信息下方、子账号列表上方,位置合理
**相关文件**`src/pages/MemberManage.tsx`
---
### 分类十八:计划拒绝状态管理问题
#### Bug: 纺织厂拒绝计划后状态未持久化
**现象**
- 纺织厂拒绝计划后,刷新页面或重新进入,计划又变回"待确定"状态
- 被拒绝的计划仍显示在 Dashboard "最近计划"中
- 拒绝弹窗每次进入页面都会重复弹出
**根本原因**
- 拒绝状态仅保存在组件 state 中,页面刷新后丢失
- Dashboard 和 PlanOverview 之间没有共享拒绝状态
- 没有机制跟踪用户已查看的拒绝弹窗
**修复方案**
```typescript
// 1. 使用 localStorage 持久化拒绝状态
const [rejectedPlans, setRejectedPlans] = useState<Set<string>>(new Set());
useEffect(() => {
// 从 localStorage 读取已拒绝的计划
const stored = localStorage.getItem('textile_rejected_plans');
if (stored) {
setRejectedPlans(new Set(JSON.parse(stored)));
}
}, []);
// 2. 拒绝计划时持久化到 localStorage
const handleRejectStep = async (step: PlanProcessStep) => {
// ... 提交拒绝原因到数据库 ...
// 更新本地状态
setRejectedPlans(prev => new Set([...prev, step.plan_id]));
// 持久化到 localStorage
const stored = localStorage.getItem('textile_rejected_plans');
const rejectedArray = stored ? JSON.parse(stored) : [];
if (!rejectedArray.includes(step.plan_id)) {
rejectedArray.push(step.plan_id);
localStorage.setItem('textile_rejected_plans', JSON.stringify(rejectedArray));
}
};
// 3. Dashboard 过滤最近计划
const filteredPlans = plans.filter(p => !rejectedPlanIds.has(p.id));
const recentPlans = filteredPlans.slice(0, 5);
// 4. 使用 Set 跟踪已查看的拒绝弹窗(每个计划只弹一次)
const [viewedRejectedModals, setViewedRejectedModals] = useState<Set<string>>(new Set());
useEffect(() => {
const stored = localStorage.getItem('viewed_rejected_modals');
if (stored) {
setViewedRejectedModals(new Set(JSON.parse(stored)));
}
}, []);
// 显示拒绝弹窗时检查是否已查看
if (isRejected && !viewedRejectedModals.has(plan.id)) {
showRejectedModal(plan);
const newViewed = new Set([...viewedRejectedModals, plan.id]);
setViewedRejectedModals(newViewed);
localStorage.setItem('viewed_rejected_modals', JSON.stringify([...newViewed]));
}
```
**关键设计决策**
- 使用 `textile_rejected_plans` 存储被拒绝的计划ID列表
- 使用 `viewed_rejected_modals` 存储已查看弹窗的计划ID列表
- 被拒绝的计划显示灰色背景和"已拒绝"标签
- 被拒绝的计划自动折叠,减少视觉干扰
**相关文件**
- `src/pages/textile/PlanOverview.tsx`
- `src/pages/textile/Dashboard.tsx`
---
### 分类十九:通知中心主题色适配问题
#### Bug: 通知中心按钮在各工作台颜色不一致
**现象**
- 采购商工作台通知按钮为白色,在白色背景下看不清
- 纺织厂和水洗厂工作台通知按钮颜色未适配主题色
- 按钮缺少明显的视觉层次
**根本原因**
- 通知中心组件未接收主题色参数
- 各工作台页面未传递正确的主题色配置
- 默认样式使用白色背景,在浅色主题下不显眼
**修复方案**
```typescript
// 1. NotificationCenter 组件添加 theme 属性
interface NotificationCenterProps {
theme?: 'amber' | 'emerald' | 'purple' | 'blue';
}
const themeConfig = {
amber: {
bg: 'bg-amber-100',
hover: 'hover:bg-amber-200',
icon: 'text-amber-700',
border: 'border-amber-300'
},
emerald: {
bg: 'bg-emerald-100',
hover: 'hover:bg-emerald-200',
icon: 'text-emerald-700',
border: 'border-emerald-300'
},
purple: {
bg: 'bg-purple-100',
hover: 'hover:bg-purple-200',
icon: 'text-purple-700',
border: 'border-purple-300'
},
};
// 2. 各工作台传递对应主题色
// 采购商 Dashboard
<NotificationCenter theme="amber" />
// 纺织厂 Dashboard
<NotificationCenter theme="emerald" />
// 水洗厂 Dashboard
<NotificationCenter theme="purple" />
```
**视觉设计规范**
| 工作台 | 背景色 | 图标色 | 悬停色 |
|--------|--------|--------|--------|
| 采购商 | bg-amber-100 | text-amber-700 | hover:bg-amber-200 |
| 纺织厂 | bg-emerald-100 | text-emerald-700 | hover:bg-emerald-200 |
| 水洗厂 | bg-purple-100 | text-purple-700 | hover:bg-purple-200 |
**相关文件**
- `src/components/NotificationCenter.tsx`
- `src/pages/purchaser/Dashboard.tsx`
- `src/pages/textile/Dashboard.tsx`
- `src/pages/washing/Dashboard.tsx`
---
### 分类二十:新手指引弹窗位置问题
#### Bug: 新手指引弹窗位置偏下,不在视口居中
**现象**
- 纺织厂工作台的新手指引弹窗显示在页面底部
- 用户需要滚动才能看到完整内容
- 弹窗没有正确居中显示
**根本原因**
- 使用了 `items-start``pt-[10vh]` 试图向下偏移
- 但在小屏幕或内容较多时,弹窗会超出可视区域
- 没有使用真正的居中布局
**修复方案**
```tsx
// 错误:使用 items-start 和 padding 偏移
<div className="fixed inset-0 flex items-start justify-center pt-[10vh] z-50">
<OnboardingGuide />
</div>
// 正确:使用 items-center 实现真正的居中
<div className="fixed inset-0 flex items-center justify-center z-50 p-4">
<OnboardingGuide />
</div>
```
**设计原则**
- 弹窗应该始终位于视口中央,确保用户第一眼就能看到
- 使用 `items-center justify-center` 实现水平和垂直居中
- 添加 `p-4` 确保在小屏幕上有足够的边距
- 避免使用固定的 padding 值进行偏移
**相关文件**
- `src/pages/textile/Dashboard.tsx`
- `src/pages/purchaser/Dashboard.tsx`
- `src/pages/washing/Dashboard.tsx`
---
## 学习反思与最佳实践
### 1. 环境差异意识

View File

@ -212,6 +212,46 @@ import { InventoryRecordsModal } from '../../components/InventoryRecordsModal';
## 更新日志
### 2025-05-27 (v1.0.2)
- **账号管理页面帮助系统集成**
- 新增"帮助与支持"卡片区域,集成用户手册、用户反馈、关于窗口
- 用户手册:详细使用指南,支持搜索功能
- 用户反馈:支持提交问题或建议到数据库
- 关于窗口:显示版本号 v1.0.2、版权信息、技术支持联系方式
- **帮助系统组件完善**
- 内置帮助中心F1快捷键功能介绍、常见操作步骤、快捷键列表
- 新手指引:首次登录自动弹窗,引导完成关键设置
- 工具提示:所有按钮悬停显示功能说明
- 版本更新检查:自动检测新版本,仅弹窗一次
- 崩溃报告系统:全局错误监听,自动收集错误日志
- **通知中心主题色适配**
- 采购商工作台琥珀色主题bg-amber-100/text-amber-700
- 纺织厂工作台翠绿色主题bg-emerald-100/text-emerald-700
- 水洗厂工作台紫色主题bg-purple-100/text-purple-700
- **计划拒绝功能优化**
- 使用 localStorage 持久化拒绝状态,跨页面保持同步
- 被拒绝的计划显示灰色卡片和"已拒绝"标签
- 被拒绝的计划自动从 Dashboard "最近计划"中移除
- 每个被拒绝的计划弹窗仅显示一次
### 2025-05-26
- **计划详情字段完善**
- 采购商和纺织厂计划总览页面统一显示所有技术字段
- 新增显示字段:克重、经线克重、纬线克重、总克重、生产采购价、产品图片、纱线配比、每米纱用量
- 新增时间字段:创建时间、计划开始时间
- 新增备注字段显示
- 纺织厂页面隐藏敏感信息(成品名称、颜色、色号)以保护采购商数据
- **移动端 UI 优化**
- 计划卡片移动端适配缩小图片尺寸64x64、字体调整为 text-xs
- 字段标签简化(如"计划编号:"→"编号:"
- 网格布局优化,避免内容溢出
- 所有工作台退出按钮在移动端变为图标形式LogOut图标
- **拒绝计划功能增强**
- 纺织厂拒绝计划后立即显示灰色卡片和"已拒绝"标签
- 被拒绝的计划自动折叠
- 被拒绝的计划从 Dashboard "最近计划"中消失
- 使用 localStorage 持久化拒绝状态,跨页面保持同步
### 2025-05-25
- **产品图片上传功能**
- 为采购商原坯布仓库产品管理添加图片上传功能
@ -314,8 +354,8 @@ VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
# 应用配置
VITE_APP_NAME=织机Demo版
VITE_APP_VERSION=1.0.0
VITE_APP_NAME=昱森Demo版
VITE_APP_VERSION=1.0.2
```
### 生产环境

View File

@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>织机 - 以AI为梭织天地经纬</title>
<title>昱森 - 以AI为梭织天地经纬</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* 内联样式确保主题色立即生效,避免闪烁 */

View File

@ -0,0 +1,6 @@
-- ============================================
-- 此文件由 Meoo Cloud 自动生成,请勿修改
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
-- ============================================
ALTER TABLE products ADD COLUMN warp_weight DECIMAL DEFAULT NULL; ALTER TABLE products ADD COLUMN weft_weight DECIMAL DEFAULT NULL; ALTER TABLE products ADD COLUMN total_weight DECIMAL DEFAULT NULL;

View File

@ -0,0 +1,6 @@
-- ============================================
-- 此文件由 Meoo Cloud 自动生成,请勿修改
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
-- ============================================
ALTER TABLE product_yarn_ratios ADD COLUMN IF NOT EXISTS yarn_type TEXT DEFAULT 'all';

View File

@ -0,0 +1,6 @@
-- ============================================
-- 此文件由 Meoo Cloud 自动生成,请勿修改
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
-- ============================================
ALTER TABLE yarn_ratios ADD COLUMN IF NOT EXISTS yarn_type TEXT DEFAULT 'all';

View File

@ -0,0 +1,16 @@
-- ============================================
-- 此文件由 Meoo Cloud 自动生成,请勿修改
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
-- ============================================
-- 为 product_outbound_records 表添加生产批号关联和接手公司字段
ALTER TABLE public.product_outbound_records
ADD COLUMN IF NOT EXISTS source_batch_id UUID REFERENCES public.product_inventory_records(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS recipient_company_id UUID REFERENCES public.companies(id) ON DELETE SET NULL,
ADD COLUMN IF NOT EXISTS recipient_company_name TEXT;
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_product_outbound_source_batch ON public.product_outbound_records(source_batch_id);
CREATE INDEX IF NOT EXISTS idx_product_outbound_recipient ON public.product_outbound_records(recipient_company_id);

View File

@ -0,0 +1,6 @@
-- ============================================
-- 此文件由 Meoo Cloud 自动生成,请勿修改
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
-- ============================================
ALTER TYPE public.step_status ADD VALUE 'rejected';

View File

@ -0,0 +1,45 @@
-- ============================================
-- 此文件由 Meoo Cloud 自动生成,请勿修改
-- This file is auto-generated by Meoo Cloud, DO NOT MODIFY
-- ============================================
CREATE TABLE IF NOT EXISTS user_feedback (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type TEXT NOT NULL CHECK (type IN ('suggestion', 'bug', 'question')),
content TEXT NOT NULL,
contact TEXT,
user_id UUID REFERENCES auth.users(id),
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'processing', 'resolved', 'closed')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS crash_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
error_message TEXT NOT NULL,
error_stack TEXT,
component_stack TEXT,
user_description TEXT,
user_agent TEXT,
url TEXT,
user_id UUID REFERENCES auth.users(id),
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'investigating', 'fixed', 'closed')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- 启用RLS
ALTER TABLE user_feedback ENABLE ROW LEVEL SECURITY;
ALTER TABLE crash_reports ENABLE ROW LEVEL SECURITY;
-- 创建RLS策略
CREATE POLICY users_insert_feedback ON user_feedback
FOR INSERT WITH CHECK (auth.uid() = user_id OR user_id IS NULL);
CREATE POLICY users_select_own_feedback ON user_feedback
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY users_insert_crash ON crash_reports
FOR INSERT WITH CHECK (auth.uid() = user_id OR user_id IS NULL);

View File

@ -0,0 +1,403 @@
# 纺织行业采购计划管理系统 - 使用说明书
## 文档鉴别材料 - 使用说明书
**软件名称**:纺织行业采购计划管理系统
**软件简称**:昱森
**版本号**V1.0.0
**编制日期**2025年05月
---
## 目录
1. 软件概述
2. 系统要求
3. 安装与配置
4. 功能使用说明
5. 常见问题
6. 技术支持
---
## 第一章 软件概述
### 1.1 软件简介
纺织行业采购计划管理系统(简称"织机")是一款专为纺织行业设计的全流程协作管理平台,连接采购商(布行)、纺织厂、水洗厂三方,实现从计划制定到成品交付的数字化管理。
### 1.2 主要特点
- **多角色协作**:支持采购商、纺织厂、水洗厂三种角色协同工作
- **实时同步**:数据实时更新,多方共享
- **移动端适配**:支持手机、平板、电脑多端访问
- **数据安全**:采用行级安全策略,确保数据隔离
- **操作简便**:界面友好,易于上手
### 1.3 适用对象
- 纺织采购商(布行)
- 纺织生产厂家
- 水洗加工厂家
- 纺织行业供应链管理人员
---
## 第二章 系统要求
### 2.1 硬件环境
| 项目 | 最低配置 | 推荐配置 |
|------|----------|----------|
| 处理器 | 双核 1.5GHz | 四核 2.0GHz |
| 内存 | 4GB | 8GB |
| 硬盘 | 128GB | 256GB SSD |
| 显示器 | 1280×720 | 1920×1080 |
| 网络 | 宽带 2Mbps | 宽带 10Mbps |
### 2.2 软件环境
**客户端浏览器**
- Chrome 90 及以上版本
- Firefox 88 及以上版本
- Safari 14 及以上版本
- Edge 90 及以上版本
**移动端支持**
- iOS 14 及以上版本
- Android 10 及以上版本
### 2.3 运行环境
- 云服务平台Meoo Cloud
- 数据库PostgreSQL 15+
- Web服务器Nginx 1.24+
---
## 第三章 安装与配置
### 3.1 访问方式
本系统为Web应用无需安装客户端软件直接通过浏览器访问即可。
**访问地址**https://[您的域名]/
### 3.2 首次使用
1. 打开浏览器,输入系统地址
2. 点击"注册"按钮创建账号
3. 选择您的角色(采购商/纺织厂/水洗厂)
4. 填写公司信息和个人信息
5. 完成注册后使用用户名和密码登录
### 3.3 账号管理
**主账号**
- 拥有全部权限
- 可创建子账号
- 可管理公司信息
**子账号**
- 由主账号创建
- 权限由主账号分配
- 共享主账号的公司数据
---
## 第四章 功能使用说明
### 4.1 登录与注册
#### 4.1.1 用户注册
1. 点击登录页面的"注册"按钮
2. 选择角色类型:
- 采购商/布行
- 纺织厂
- 水洗厂
3. 填写注册信息:
- 用户名(唯一标识)
- 密码至少6位
- 公司名称
- 联系人姓名
- 联系电话
4. 点击"注册"完成账号创建
#### 4.1.2 用户登录
1. 输入用户名
2. 输入密码
3. 点击"登录"按钮
4. 首次登录需选择工作角色
#### 4.1.3 退出登录
点击页面右上角的"退出"按钮,安全退出系统。
### 4.2 采购商功能
#### 4.2.1 工作台首页
登录后进入采购商工作台,显示:
- **统计卡片**:所有计划、进行中、已完成数量
- **快捷操作**:常用功能入口
- **最近计划**最近创建的5个计划
#### 4.2.2 创建纺织计划
1. 点击"新建纺织计划"或"计划总览"页面的"新建计划"按钮
2. 填写计划信息:
- 成品名称
- 颜色
- 坯布识别码(系统自动生成)
- 色号
- 计划产量(米)
- 每米布纱用量g/m
- 生产采购价(元/米)
- 备注
3. 配置纱线配比:
- 添加纱线名称
- 设置配比比例
- 系统自动计算总用纱量
4. 选择关联工厂(可选)
5. 点击"创建计划"
#### 4.2.3 计划管理
**查看计划**
- 列表展示所有计划
- 支持按状态筛选
- 点击计划查看详情
**计划详情**
- 基本信息展示
- 生产流程节点跟踪
- 进度可视化
- 关联工厂信息
**分享计划**
1. 打开计划详情
2. 点击"分享"按钮
3. 复制分享链接
4. 发送给纺织厂
#### 4.2.4 仓库管理
**原坯布仓库**
- 按坯布码分组展示
- 查看库存数量
- 查看入库记录
- 查看价格历史
**成品仓库**
- 管理水洗厂内坯布库存
- 跟踪库存变动
#### 4.2.5 工厂管理
**添加工厂**
1. 进入"工厂管理"页面
2. 点击"添加工厂"
3. 选择工厂类型(纺织厂/水洗厂)
4. 填写工厂信息
5. 生成分享链接
**管理工厂**
- 查看已关联工厂
- 编辑工厂信息
- 删除工厂
#### 4.2.6 应付账款
- 查看待结款计划
- 按完成米数自动计算金额
- 确认结款记录
### 4.3 纺织厂功能
#### 4.3.1 工作台首页
显示:
- 关联的计划数量统计
- 快捷操作入口
- 最近更新的计划
#### 4.3.2 接收计划
**通过分享链接导入**
1. 点击分享链接
2. 系统自动识别计划信息
3. 确认导入计划
**通过剪贴板导入**
1. 复制分享链接
2. 进入"导入计划"页面
3. 系统自动识别剪贴板内容
4. 确认导入
#### 4.3.3 计划生产
**查看计划详情**
- 产品信息
- 纱线配比
- 计划产量
**确认生产节点**
1. 打开计划详情
2. 查看生产流程
3. 点击当前节点"确认"按钮
4. 添加备注(可选)
5. 完成确认
**生产流程节点**
- 确认计划
- 采纱
- 染纱
- 上机
- 坯布入库
#### 4.3.4 坯布入库
1. 进入"已生产坯布"页面
2. 选择要入库的计划
3. 填写入库信息:
- 入库米数
- 入库匹数
- 批号
4. 确认入库
#### 4.3.5 纱线仓库
- 查看原料纱库存
- 设置最低库存预警
- 库存不足时系统提醒
#### 4.3.6 待结款
- 查看已完成入库的计划
- 查看应结款金额
- 确认收款
### 4.4 水洗厂功能
#### 4.4.1 工作台首页
显示:
- 水洗计划统计
- 待处理坯布数量
- 快捷操作入口
#### 4.4.2 水洗计划管理
**接收水洗计划**
- 通过分享链接导入
- 查看计划详情
**处理坯布**
1. 查看待处理坯布
2. 确认接收
3. 开始水洗处理
4. 完成处理确认
#### 4.4.3 成品仓库
- 管理水洗后成品
- 跟踪库存状态
- 出库记录
### 4.5 公共功能
#### 4.5.1 子账号管理
**创建子账号**(仅主账号):
1. 进入"账号管理"页面
2. 点击"添加成员"
3. 填写子账号信息
4. 设置权限
5. 完成创建
**管理子账号**
- 查看成员列表
- 编辑权限
- 删除子账号
#### 4.5.2 通知中心
- 查看系统通知
- 计划状态变更提醒
- 库存预警通知
#### 4.5.3 个人设置
- 修改个人信息
- 修改密码
- 查看登录历史
---
## 第五章 常见问题
### 5.1 登录问题
**Q忘记密码怎么办**
A请联系主账号重置密码或联系系统管理员。
**Q无法登录提示"用户名或密码错误"**
A请检查用户名和密码是否正确注意区分大小写。
### 5.2 功能问题
**Q为什么看不到某些功能**
A部分功能仅限主账号使用或需要特定角色权限。
**Q分享链接无法打开**
A请检查链接是否完整或联系计划创建者重新分享。
**Q数据不同步**
A请检查网络连接或刷新页面重新加载。
### 5.3 性能问题
**Q页面加载慢**
A
- 检查网络连接
- 清除浏览器缓存
- 关闭不必要的浏览器标签
**Q移动端显示异常**
A请使用推荐的浏览器版本或尝试刷新页面。
---
## 第六章 技术支持
### 6.1 联系方式
- **技术支持邮箱**support@example.com
- **技术支持电话**400-XXX-XXXX
- **服务时间**:工作日 9:00-18:00
### 6.2 反馈建议
欢迎通过以下方式反馈使用建议:
- 发送邮件至 feedback@example.com
- 在系统内提交反馈
### 6.3 版本更新
系统会定期更新,更新内容包括:
- 功能优化
- 性能提升
- 安全加固
- Bug修复
---
**文档版本**V1.0.0
**最后更新**2025年05月
**编制单位**[您的单位名称]
---
*本说明书版权归软件著作权人所有,未经许可不得复制、传播。*

Binary file not shown.

View File

@ -0,0 +1,417 @@
# 纺织行业采购计划管理系统 - 设计说明书
## 文档鉴别材料 - 设计说明书
**软件名称**:纺织行业采购计划管理系统
**软件简称**:昱森
**版本号**V1.0.0
**编制日期**2025年05月
---
## 目录
1. 系统架构设计
2. 数据库设计
3. 接口设计
4. 安全设计
5. 部署架构
6. 技术选型说明
---
## 第一章 系统架构设计
### 1.1 整体架构
本系统采用前后端分离架构,基于云原生技术栈构建:
```
┌─────────────────────────────────────────────────────────────┐
│ 客户端层 │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Web浏览器 │ │ 移动端 │ │ 平板 │ │
│ │ (React) │ │ (H5) │ │ (响应式) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 云服务层 (Meoo Cloud) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Supabase │ │ 认证服务 │ │ 实时订阅 │ │
│ │ (PostgreSQL)│ │ (Auth) │ │ (Realtime) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 1.2 前端架构
**技术栈**
- **框架**React 18 + TypeScript
- **路由**React Router v6 (HashRouter)
- **状态管理**React Context + Zustand
- **样式方案**TailwindCSS
- **动画库**Framer Motion
- **构建工具**Webpack 5
**目录结构**
```
src/
├── components/ # 公共组件
├── pages/ # 页面组件
│ ├── purchaser/ # 采购商页面
│ ├── textile/ # 纺织厂页面
│ └── washing/ # 水洗厂页面
├── hooks/ # 自定义Hooks
├── contexts/ # React上下文
├── stores/ # 状态管理
├── utils/ # 工具函数
├── types/ # 类型定义
└── supabase/ # Supabase客户端
```
### 1.3 后端架构
**云服务**Meoo Cloud (基于Supabase)
- **数据库**PostgreSQL 15
- **认证**Supabase Auth
- **实时**Supabase Realtime
- **存储**Supabase Storage
---
## 第二章 数据库设计
### 2.1 ER关系图
```
auth.users (Supabase内置)
├── 1:1 ── profiles (用户配置)
│ │
│ ├── N:1 ── companies (公司)
│ │ │
│ │ ├── 1:N ── company_members (公司成员)
│ │ │
│ │ ├── 1:N ── factories (工厂)
│ │ │ │
│ │ │ ├── N:M ── production_plans
│ │ │
│ │ ├── 1:N ── warehouses (仓库)
│ │ │ │
│ │ │ ├── 1:N ── inventory_records
│ │ │
│ │ ├── 1:N ── yarn_stock (纱线库存)
│ │
│ ├── 1:N ── production_plans (采购商创建的计划)
│ │ │
│ │ ├── 1:N ── yarn_ratios (纱线配比)
│ │ │
│ │ ├── 1:N ── plan_process_steps (流程节点)
│ │ │
│ │ ├── N:M ── factories (通过plan_factories关联)
│ │
│ ├── 1:N ── payments (结款记录)
```
### 2.2 核心表结构
#### 2.2.1 companies - 公司表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID (PK) | 公司ID |
| name | TEXT | 公司名称 |
| role | app_role | 公司类型purchaser/textile/washing |
| address | TEXT | 公司地址 |
| contact_phone | TEXT | 联系电话 |
| created_at | TIMESTAMPTZ | 创建时间 |
#### 2.2.2 profiles - 用户配置表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID (PK) | 关联auth.users.id |
| username | TEXT (UNIQUE) | 用户名 |
| display_name | TEXT | 显示名称 |
| phone | TEXT | 手机号(加密存储) |
| company_id | UUID (FK) | 所属公司 |
| is_master | BOOLEAN | 是否主账号 |
| master_id | UUID (FK) | 主账号ID子账号使用 |
| created_at | TIMESTAMPTZ | 创建时间 |
#### 2.2.3 production_plans - 生产计划表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID (PK) | 计划ID |
| plan_code | TEXT (UNIQUE) | 系统自动生成计划唯一识别码 |
| product_name | TEXT | 成品名称 |
| color | TEXT | 颜色 |
| fabric_code | TEXT | 坯布唯一识别码(纯字母) |
| color_code | TEXT | 色号(数字排序) |
| remark | TEXT | 备注 |
| purchaser_id | UUID (FK) | 采购商公司ID |
| target_quantity | INTEGER | 计划产量(米) |
| completed_quantity | INTEGER | 已完成产量(米) |
| yarn_usage_per_meter | DECIMAL | 每米布纱用量(g/m) |
| production_price | DECIMAL | 生产采购价(元/米) |
| status | plan_status | 状态pending/producing/completed |
| start_time | TIMESTAMPTZ | 计划开始时间 |
| created_by | UUID (FK) | 创建人 |
| created_at | TIMESTAMPTZ | 创建时间 |
| updated_at | TIMESTAMPTZ | 更新时间 |
#### 2.2.4 plan_process_steps - 计划流程节点表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID (PK) | 节点ID |
| plan_id | UUID (FK) | 计划ID |
| step_type | step_type | 节点类型confirm/yarn_purchase/dyeing/machine_start/fabric_warehouse |
| status | step_status | 状态pending/active/completed |
| timestamp | TIMESTAMPTZ | 确认时间戳 |
| operator_id | UUID (FK) | 操作人 |
| notes | TEXT | 备注 |
| created_at | TIMESTAMPTZ | 创建时间 |
### 2.3 自定义枚举类型
```sql
-- 应用角色(公司类型)
CREATE TYPE app_role AS ENUM ('purchaser', 'textile', 'washing');
-- 工厂类型
CREATE TYPE factory_type AS ENUM ('textile', 'washing');
-- 计划状态
CREATE TYPE plan_status AS ENUM ('pending', 'producing', 'completed');
-- 流程节点类型
CREATE TYPE step_type AS ENUM ('confirm', 'yarn_purchase', 'dyeing', 'machine_start', 'fabric_warehouse');
-- 流程节点状态
CREATE TYPE step_status AS ENUM ('pending', 'active', 'completed');
-- 仓库类型
CREATE TYPE warehouse_type AS ENUM ('raw_fabric', 'fabric', 'finished', 'yarn');
-- 结款状态
CREATE TYPE payment_status AS ENUM ('pending', 'completed');
```
### 2.4 RLS安全策略
所有表启用Row Level Security (RLS)基于公司ID进行数据隔离
```sql
-- 示例production_plans表RLS策略
CREATE POLICY company_select_plans ON production_plans
FOR SELECT USING (purchaser_id = get_user_master_company_id());
CREATE POLICY company_insert_plans ON production_plans
FOR INSERT WITH CHECK (purchaser_id = get_user_master_company_id());
CREATE POLICY company_update_plans ON production_plans
FOR UPDATE USING (purchaser_id = get_user_master_company_id());
CREATE POLICY company_delete_plans ON production_plans
FOR DELETE USING (purchaser_id = get_user_master_company_id());
```
---
## 第三章 接口设计
### 3.1 认证接口
| 接口 | 方法 | 说明 |
|------|------|------|
| /auth/v1/token | POST | 用户登录 |
| /auth/v1/signup | POST | 用户注册 |
| /auth/v1/logout | POST | 用户退出 |
| /auth/v1/refresh | POST | 刷新Token |
### 3.2 数据接口
基于Supabase自动生成的RESTful API
| 资源 | 操作 | 接口示例 |
|------|------|----------|
| production_plans | CRUD | /rest/v1/production_plans |
| companies | CRUD | /rest/v1/companies |
| profiles | CRUD | /rest/v1/profiles |
| inventory_records | CRUD | /rest/v1/inventory_records |
| plan_process_steps | CRUD | /rest/v1/plan_process_steps |
### 3.3 实时订阅
使用Supabase Realtime进行数据实时同步
```typescript
supabase
.channel('inventory_changes')
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'inventory_records'
}, callback)
.subscribe();
```
---
## 第四章 安全设计
### 4.1 数据安全
**加密存储**
- 手机号使用XOR加密存储
- 敏感数据加密传输HTTPS
**访问控制**
- 基于角色的访问控制RBAC
- 行级安全策略RLS
- 公司数据隔离
### 4.2 认证安全
- JWT Token认证
- Token自动刷新
- Session持久化
- 虚拟邮箱格式保护隐私
### 4.3 分享安全
- 分享链接使用Base64 + 盐值混淆加密
- 链接不包含敏感信息
- 导入页面隐藏成品名称、颜色等敏感字段
---
## 第五章 部署架构
### 5.1 部署环境
**云服务**Meoo Cloud (Supabase)
- 托管数据库
- 托管认证服务
- 托管实时服务
**前端部署**
- 静态资源托管
- CDN加速
- HTTPS安全传输
### 5.2 构建配置
**开发环境**
```bash
pnpm run dev # 启动开发服务器
pnpm run build # 构建生产版本
```
**生产构建**
- Webpack 5优化打包
- 代码分割
- Tree Shaking
- 资源压缩
---
## 第六章 技术选型说明
### 6.1 前端技术选型
| 技术 | 版本 | 选型理由 |
|------|------|----------|
| React | 18.2.0 | 组件化开发,生态丰富 |
| TypeScript | 5.3.3 | 类型安全,提升开发效率 |
| TailwindCSS | 3.3.5 | 原子化CSS快速开发 |
| Framer Motion | 11.16.1 | 流畅动画,用户体验好 |
| React Router | 6.8.0 | 声明式路由,易于管理 |
| Zustand | 4.4.7 | 轻量状态管理,简单易用 |
### 6.2 后端技术选型
| 技术 | 版本 | 选型理由 |
|------|------|----------|
| PostgreSQL | 15+ | 关系型数据库,支持复杂查询 |
| Supabase | 2.98.0 | 开源Firebase替代功能完整 |
| Row Level Security | - | 行级安全,数据隔离 |
### 6.3 开发工具选型
| 工具 | 版本 | 用途 |
|------|------|------|
| Webpack | 5.89.0 | 模块打包 |
| Babel | 7.23.5 | JavaScript编译 |
| Jest | 29.7.0 | 单元测试 |
| Playwright | 1.40.0 | E2E测试 |
| pnpm | 8+ | 包管理 |
---
## 附录
### A. 数据库迁移脚本
```sql
-- 创建公司表
CREATE TABLE companies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
role app_role NOT NULL,
address TEXT,
contact_phone TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 创建用户配置表
CREATE TABLE profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id),
username TEXT UNIQUE NOT NULL,
display_name TEXT,
phone TEXT,
company_id UUID REFERENCES companies(id),
is_master BOOLEAN DEFAULT false,
master_id UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 启用RLS
ALTER TABLE companies ENABLE ROW LEVEL SECURITY;
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
```
### B. 技术架构图
```
┌────────────────────────────────────────────────────────────┐
│ 前端层 │
│ React 18 + TypeScript + TailwindCSS + Framer Motion │
└────────────────────────────────────────────────────────────┘
▼ HTTP/HTTPS
┌────────────────────────────────────────────────────────────┐
│ API网关层 │
│ Supabase REST API / Realtime │
└────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────┐
│ 数据层 │
│ PostgreSQL + Row Level Security + Realtime Subscription │
└────────────────────────────────────────────────────────────┘
```
---
**文档版本**V1.0.0
**最后更新**2025年05月
**编制单位**[您的单位名称]
---
*本说明书版权归软件著作权人所有,未经许可不得复制、传播。*

View File

@ -0,0 +1,467 @@
═══════════════════════════════════════════════════════════════════════════════
程序鉴别材料 - 源代码前30页
纺织行业采购计划管理系统 V1.0.0
═══════════════════════════════════════════════════════════════════════════════
【文件1】src/index.tsx - 应用入口文件
───────────────────────────────────────────────────────────────────────────────
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles/index.css';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
【文件2】src/App.tsx - 根组件与路由配置
───────────────────────────────────────────────────────────────────────────────
import React, { useState } from 'react';
import { HashRouter, Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { AnimatePresence, motion } from 'framer-motion';
import { AuthProvider } from './contexts/AuthContext';
import { ErrorBoundary, PageErrorBoundary } from './components/ErrorBoundary';
import { LoginPage } from './pages/LoginPage';
import { RegisterPage } from './pages/RegisterPage';
import { RoleSelectPage } from './pages/RoleSelectPage';
import { PurchaserDashboard } from './pages/purchaser/Dashboard';
import { TextileDashboard } from './pages/textile/Dashboard';
import { WashingDashboard } from './pages/washing/Dashboard';
import { PlanOverview } from './pages/purchaser/PlanOverview';
import { NewPlan } from './pages/purchaser/NewPlan';
import { WarehouseManage } from './pages/purchaser/WarehouseManage';
import { FactoryManage } from './pages/purchaser/FactoryManage';
import { TextilePlanOverview } from './pages/textile/PlanOverview';
import { YarnWarehouse } from './pages/textile/YarnWarehouse';
import { FabricWarehouse } from './pages/textile/FabricWarehouse';
import { PaymentPending } from './pages/textile/PaymentPending';
import { MemberManage } from './pages/MemberManage';
import { ImportPlanPage } from './pages/ImportPlanPage';
import { NetworkStatusBar } from './components/NetworkStatusBar';
import './styles/index.css';
// 页面过渡动画配置
const pageTransitionVariants = {
initial: { opacity: 0, y: 20, scale: 0.98 },
animate: {
opacity: 1, y: 0, scale: 1,
transition: { duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }
},
exit: {
opacity: 0, y: -10, scale: 0.98,
transition: { duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] }
},
};
function AnimatedRoute({ children, pageName }: { children: React.ReactNode; pageName?: string }) {
return (
<motion.div variants={pageTransitionVariants} initial="initial" animate="animate" exit="exit"
style={{ willChange: 'transform, opacity' }}>
<PageErrorBoundary pageName={pageName}>{children}</PageErrorBoundary>
</motion.div>
);
}
function AppRoutes() {
const location = useLocation();
return (
<AnimatePresence mode="wait">
<Routes location={location} key={location.pathname}>
<Route path="/login" element={<AnimatedRoute pageName="登录"><LoginPage /></AnimatedRoute>} />
<Route path="/register" element={<AnimatedRoute pageName="注册"><RegisterPage /></AnimatedRoute>} />
<Route path="/role-select" element={<AnimatedRoute pageName="角色选择"><RoleSelectPage /></AnimatedRoute>} />
<Route path="/purchaser" element={<AnimatedRoute pageName="采购商首页"><PurchaserDashboard /></AnimatedRoute>} />
<Route path="/purchaser/plans" element={<AnimatedRoute pageName="计划总览"><PlanOverview /></AnimatedRoute>} />
<Route path="/purchaser/plans/new" element={<AnimatedRoute pageName="新建计划"><NewPlan /></AnimatedRoute>} />
<Route path="/purchaser/warehouse" element={<AnimatedRoute pageName="仓库管理"><WarehouseManage /></AnimatedRoute>} />
<Route path="/purchaser/factories" element={<AnimatedRoute pageName="工厂管理"><FactoryManage /></AnimatedRoute>} />
<Route path="/textile" element={<AnimatedRoute pageName="纺织厂首页"><TextileDashboard /></AnimatedRoute>} />
<Route path="/textile/plans" element={<AnimatedRoute pageName="纺织厂计划"><TextilePlanOverview /></AnimatedRoute>} />
<Route path="/textile/fabric-warehouse" element={<AnimatedRoute pageName="坯布仓库"><FabricWarehouse /></AnimatedRoute>} />
<Route path="/textile/yarn-warehouse" element={<AnimatedRoute pageName="纱线仓库"><YarnWarehouse /></AnimatedRoute>} />
<Route path="/textile/payments" element={<AnimatedRoute pageName="待结款"><PaymentPending /></AnimatedRoute>} />
<Route path="/washing" element={<AnimatedRoute pageName="水洗厂首页"><WashingDashboard /></AnimatedRoute>} />
<Route path="/members" element={<AnimatedRoute pageName="子账号管理"><MemberManage /></AnimatedRoute>} />
<Route path="/import" element={<AnimatedRoute pageName="导入计划"><ImportPlanPage /></AnimatedRoute>} />
<Route path="/" element={<Navigate to="/login" replace />} />
</Routes>
</AnimatePresence>
);
}
function App() {
return (
<ErrorBoundary>
<AuthProvider>
<HashRouter>
<NetworkStatusBar />
<AppRoutes />
</HashRouter>
</AuthProvider>
</ErrorBoundary>
);
}
export default App;
【文件3】src/contexts/AuthContext.tsx - 认证上下文(部分)
───────────────────────────────────────────────────────────────────────────────
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
import { supabase } from '../supabase/client';
import type { Profile, Company, UserRole, AuthState } from '../types';
import { encryptPhone, decryptPhone } from '../utils/crypto';
interface RegisterData {
username: string;
password: string;
displayName: string;
companyName: string;
phone: string;
role: UserRole;
}
interface AuthContextType {
auth: AuthState;
login: (username: string, password: string) => Promise<boolean>;
register: (data: RegisterData) => Promise<{ success: boolean; error?: string }>;
logout: () => Promise<void>;
selectRole: (role: UserRole) => void;
loading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [auth, setAuth] = useState<AuthState>({
isLoggedIn: false, user: null, company: null, currentRole: null
});
const [loading, setLoading] = useState(true);
useEffect(() => {
const initAuth = async () => {
const { data: { session } } = await supabase.auth.getSession();
if (session?.user) {
const { data: profile } = await supabase
.from('profiles').select('*').eq('id', session.user.id).maybeSingle();
if (profile) {
let company: Company | null = null;
if (profile.company_id) {
const { data: comp } = await supabase
.from('companies').select('*').eq('id', profile.company_id).maybeSingle();
company = comp;
}
setAuth({ isLoggedIn: true, user: profile, company, currentRole: company?.role || null });
}
}
setLoading(false);
};
initAuth();
const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
if (event === 'SIGNED_IN' && session?.user) {
setTimeout(async () => {
const { data: profile } = await supabase
.from('profiles').select('*').eq('id', session.user.id).maybeSingle();
if (profile) {
let company: Company | null = null;
if (profile.company_id) {
const { data: comp } = await supabase
.from('companies').select('*').eq('id', profile.company_id).maybeSingle();
company = comp;
}
setAuth({ isLoggedIn: true, user: profile, company, currentRole: company?.role || null });
}
}, 0);
} else if (event === 'SIGNED_OUT') {
setAuth({ isLoggedIn: false, user: null, company: null, currentRole: null });
}
});
return () => subscription.unsubscribe();
}, []);
const login = useCallback(async (username: string, password: string): Promise<boolean> => {
const { error } = await supabase.auth.signInWithPassword({
email: `${username}@meoo.local`, password,
});
return !error;
}, []);
const register = useCallback(async (data: RegisterData) => {
try {
const { data: companyData, error: companyError } = await supabase
.from('companies').insert({ name: data.companyName, role: data.role, contact_phone: data.phone })
.select().single();
if (companyError || !companyData) return { success: false, error: '创建公司失败' };
const { data: authData, error: authError } = await supabase.auth.signUp({
email: `${data.username}@meoo.local`, password: data.password,
});
if (authError || !authData.user) {
await supabase.from('companies').delete().eq('id', companyData.id);
return { success: false, error: '注册失败' };
}
const { error: profileError } = await supabase.from('profiles').insert({
id: authData.user.id, username: data.username, display_name: data.displayName,
phone: encryptPhone(data.phone), company_id: companyData.id, is_master: true
});
if (profileError) {
await supabase.from('companies').delete().eq('id', companyData.id);
return { success: false, error: '创建用户资料失败' };
}
return { success: true };
} catch (err: any) {
return { success: false, error: err?.message || '注册失败' };
}
}, []);
const logout = useCallback(async () => {
await supabase.auth.signOut();
setAuth({ isLoggedIn: false, user: null, company: null, currentRole: null });
}, []);
const selectRole = useCallback((role: UserRole) => {
setAuth(prev => ({ ...prev, currentRole: role }));
}, []);
return (
<AuthContext.Provider value={{ auth, login, register, logout, selectRole, loading }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) throw new Error('useAuth must be used within AuthProvider');
return context;
}
【文件4】src/supabase/client.ts - Supabase客户端配置
───────────────────────────────────────────────────────────────────────────────
import { createClient } from '@supabase/supabase-js';
import type { Database } from './types';
export function getSupabaseUrl(): string {
return `${(window as any).MEOO_CONFIG?.meoo_app_access_url || location.origin}/sb-api`;
}
export const supabaseUrl = getSupabaseUrl();
export const supabaseAnonKey = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...';
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
auth: { persistSession: true, autoRefreshToken: true },
});
【文件5】src/types/index.ts - 类型定义(部分)
───────────────────────────────────────────────────────────────────────────────
export type UserRole = 'purchaser' | 'textile' | 'washing';
export type PlanStatus = 'pending' | 'producing' | 'completed';
export type FactoryType = 'textile' | 'washing';
export interface Profile {
id: string;
username: string;
display_name?: string;
phone?: string;
company_id?: string;
is_master: boolean;
master_id?: string;
created_at: string;
}
export interface Company {
id: string;
name: string;
role: UserRole;
address?: string;
contact_phone?: string;
created_at: string;
}
export interface ProductionPlan {
id: string;
plan_code: string;
product_name: string;
color: string;
fabric_code: string;
color_code: string;
remark?: string;
purchaser_id: string;
target_quantity: number;
completed_quantity: number;
yarn_usage_per_meter: number;
production_price: number;
status: PlanStatus;
start_time?: string;
created_by: string;
created_at: string;
updated_at: string;
}
export interface AuthState {
isLoggedIn: boolean;
user: Profile | null;
company: Company | null;
currentRole: UserRole | null;
}
【文件6】src/hooks/useNetworkStatus.ts - 网络状态Hook部分
───────────────────────────────────────────────────────────────────────────────
import { useState, useEffect, useCallback, useRef } from 'react';
interface NetworkState {
isOnline: boolean;
isSlowConnection: boolean;
lastOnlineTime: number | null;
}
interface QueuedOperation {
id: string;
operation: () => Promise<void>;
retryCount: number;
maxRetries: number;
onSuccess?: () => void;
onError?: (error: Error) => void;
}
export function useNetworkStatus() {
const [state, setState] = useState<NetworkState>({
isOnline: navigator.onLine,
isSlowConnection: false,
lastOnlineTime: navigator.onLine ? Date.now() : null
});
const queueRef = useRef<QueuedOperation[]>([]);
const processingRef = useRef(false);
useEffect(() => {
const handleOnline = () => {
setState(prev => ({ ...prev, isOnline: true, lastOnlineTime: Date.now() }));
processQueue();
};
const handleOffline = () => {
setState(prev => ({ ...prev, isOnline: false }));
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
const checkConnectionSpeed = async () => {
const connection = (navigator as any).connection;
if (connection) {
setState(prev => ({
...prev,
isSlowConnection: connection.effectiveType === 'slow-2g' || connection.effectiveType === '2g'
}));
}
};
const interval = setInterval(checkConnectionSpeed, 120000);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
clearInterval(interval);
};
}, []);
const processQueue = useCallback(async () => { /* ... */ }, []);
const enqueueOperation = useCallback((operation: () => Promise<void>, options?: any) => { /* ... */ }, []);
const executeWithRetry = useCallback(async <T>(operation: () => Promise<T>, options?: any) => { /* ... */ }, []);
return { ...state, enqueueOperation, executeWithRetry, queueLength: queueRef.current.length };
}
【文件7】src/components/ErrorBoundary.tsx - 错误边界组件(部分)
───────────────────────────────────────────────────────────────────────────────
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { AlertCircle, RefreshCw, Home } from 'lucide-react';
interface Props { children: ReactNode; fallback?: ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void; }
interface State { hasError: boolean; error: Error | null; errorInfo: ErrorInfo | null; }
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error: Error): Partial<State> {
return { hasError: true, error };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({ errorInfo });
if (this.props.onError) this.props.onError(error, errorInfo);
console.error('ErrorBoundary caught an error:', error, errorInfo);
}
handleReset = () => this.setState({ hasError: false, error: null, errorInfo: null });
handleGoHome = () => window.location.href = '/';
handleReload = () => window.location.reload();
render() {
if (this.state.hasError) {
if (this.props.fallback) return this.props.fallback;
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<div className="bg-white rounded-2xl shadow-lg border border-gray-200 p-6 sm:p-8 max-w-md w-full">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center">
<AlertCircle className="w-6 h-6 text-red-600" />
</div>
<div>
<h2 className="text-lg font-semibold text-gray-900">页面出错了</h2>
<p className="text-sm text-gray-500">抱歉,发生了意外错误</p>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-3 mb-4 overflow-auto max-h-32">
<p className="text-xs text-gray-600 font-mono break-all">{this.state.error?.toString()}</p>
</div>
<div className="space-y-2">
<button onClick={this.handleReset} className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm font-medium">
<RefreshCw className="w-4 h-4" /> 重试
</button>
<div className="flex gap-2">
<button onClick={this.handleGoHome} className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
<Home className="w-4 h-4" /> 返回首页
</button>
<button onClick={this.handleReload} className="flex-1 flex items-center justify-center gap-2 px-4 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors text-sm font-medium">
<RefreshCw className="w-4 h-4" /> 刷新页面
</button>
</div>
</div>
</div>
</div>
);
}
return this.props.children;
}
}
═══════════════════════════════════════════════════════════════════════════════
源代码特征说明
═══════════════════════════════════════════════════════════════════════════════
1. 编程语言TypeScript (.ts, .tsx)
2. 前端框架React 18 + React Router v6
3. 状态管理React Context + Hooks
4. 动画库Framer Motion
5. 样式方案TailwindCSS
6. 后端服务Supabase (PostgreSQL)
7. 构建工具Webpack 5
代码特点:
- 使用函数组件和React Hooks
- 类型安全的TypeScript开发
- 模块化设计,组件化架构
- 响应式设计,支持移动端
- 错误边界处理机制
- 实时数据同步
═══════════════════════════════════════════════════════════════════════════════
第 1 页 / 共 30 页前30页
═══════════════════════════════════════════════════════════════════════════════

View File

@ -0,0 +1,684 @@
═══════════════════════════════════════════════════════════════════════════════
程序鉴别材料 - 源代码后30页
纺织行业采购计划管理系统 V1.0.0
═══════════════════════════════════════════════════════════════════════════════
【文件1】src/utils/crypto.ts - 加密工具模块
───────────────────────────────────────────────────────────────────────────────
/**
* 数据加密/解密工具
* 使用 AES 对称加密保护敏感数据
*/
// 加密密钥 - 生产环境应通过环境变量注入
const ENCRYPTION_KEY = 'Procurement2026SecureKeyForPhoneEncryption';
/**
* 简单的 XOR 加密(用于演示,生产环境建议使用 Web Crypto API
* @param text 明文
* @param key 密钥
* @returns 加密后的 Base64 字符串
*/
function xorEncrypt(text: string, key: string): string {
const textBytes = new TextEncoder().encode(text);
const keyBytes = new TextEncoder().encode(key);
const result = new Uint8Array(textBytes.length);
for (let i = 0; i < textBytes.length; i++) {
result[i] = textBytes[i] ^ keyBytes[i % keyBytes.length];
}
return btoa(String.fromCharCode(...result));
}
/**
* XOR 解密
* @param encryptedBase64 Base64 加密字符串
* @param key 密钥
* @returns 明文
*/
function xorDecrypt(encryptedBase64: string, key: string): string {
try {
const encryptedBytes = new Uint8Array(
atob(encryptedBase64).split('').map(c => c.charCodeAt(0))
);
const keyBytes = new TextEncoder().encode(key);
const result = new Uint8Array(encryptedBytes.length);
for (let i = 0; i < encryptedBytes.length; i++) {
result[i] = encryptedBytes[i] ^ keyBytes[i % keyBytes.length];
}
return new TextDecoder().decode(result);
} catch {
return '';
}
}
/**
* 加密手机号
* @param phone 明文手机号
* @returns 加密后的字符串
*/
export function encryptPhone(phone: string): string {
if (!phone) return '';
return `ENC:${xorEncrypt(phone, ENCRYPTION_KEY)}`;
}
/**
* 解密手机号
* @param encryptedPhone 加密后的手机号
* @returns 明文手机号
*/
export function decryptPhone(encryptedPhone: string): string {
if (!encryptedPhone) return '';
if (!encryptedPhone.startsWith('ENC:')) return encryptedPhone;
const encrypted = encryptedPhone.slice(4);
return xorDecrypt(encrypted, ENCRYPTION_KEY);
}
/**
* 脱敏显示手机号
* @param phone 明文或加密手机号
* @returns 脱敏格式如 138****8888
*/
export function maskPhone(phone: string): string {
if (!phone) return '';
const plainPhone = phone.startsWith('ENC:') ? decryptPhone(phone) : phone;
if (plainPhone.length !== 11) return plainPhone;
return `${plainPhone.slice(0, 3)}****${plainPhone.slice(7)}`;
}
/**
* 验证是否为有效的加密格式
* @param value 待验证的值
* @returns 是否已加密
*/
export function isEncrypted(value: string): boolean {
return value?.startsWith('ENC:') ?? false;
}
【文件2】src/utils/helpers.ts - 工具函数模块(部分)
───────────────────────────────────────────────────────────────────────────────
import type { StepType, StepStatus } from '../types';
import { stepOrder } from './constants';
/**
* 计算生产进度百分比
*/
export function calculateProgress(completed: number, target: number): number {
if (target === 0) return 0;
return Math.round((completed / target) * 100);
}
/**
* 格式化日期显示
*/
export function formatDate(dateString: string | null | undefined): string {
if (!dateString) return '-';
return new Date(dateString).toLocaleDateString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit'
});
}
/**
* 格式化日期时间显示
*/
export function formatDateTime(dateString: string | null | undefined): string {
if (!dateString) return '-';
return new Date(dateString).toLocaleString('zh-CN');
}
/**
* 格式化时间(仅时分)
*/
export function formatTime(dateString: string | null | undefined): string {
if (!dateString) return '-';
return new Date(dateString).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
}
/**
* 按步骤类型排序
*/
export function sortByStepType<T extends { stepType: StepType }>(nodes: T[]): T[] {
return [...nodes].sort((a, b) => {
const indexA = stepOrder.indexOf(a.stepType);
const indexB = stepOrder.indexOf(b.stepType);
return indexA - indexB;
});
}
/**
* 查找第一个待确认的节点索引
*/
export function findFirstPendingIndex(nodes: { status: StepStatus }[]): number {
return nodes.findIndex(n => n.status !== 'completed');
}
/**
* 生成产品码
*/
export function generateProductCode(name: string, weight: string, colorCode: string): string {
const nameLetters = name.replace(/[^a-zA-Z]/g, '').toUpperCase();
const nameCode = nameLetters.slice(0, 2) || 'XX';
const weightNum = weight.replace(/[^0-9]/g, '') || '0';
const colorNum = colorCode.replace(/[^0-9]/g, '') || '00';
return `${nameCode}${weightNum}${colorNum}`;
}
/**
* 从分享链接中提取参数
*/
export function extractParamsFromLink(link: string): { token: string | null; plan: string | null; type: string | null } {
const hashMatch = link.match(/#\/import\?(.+)$/);
if (hashMatch) {
const queryString = hashMatch[1];
const params = new URLSearchParams(queryString);
return {
token: params.get('token'),
plan: params.get('plan'),
type: params.get('type')
};
}
return { token: null, plan: null, type: null };
}
【文件3】src/stores/index.ts - 状态管理模块(部分)
───────────────────────────────────────────────────────────────────────────────
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import type { ProductionPlan, PlanProcessStep, Company, Profile } from '../types';
// ========== 计划状态管理 ==========
interface PlanState {
plans: ProductionPlan[];
selectedPlan: ProductionPlan | null;
planSteps: Record<string, PlanProcessStep[]>;
loading: boolean;
error: string | null;
}
interface PlanActions {
setPlans: (plans: ProductionPlan[]) => void;
addPlan: (plan: ProductionPlan) => void;
updatePlan: (id: string, updates: Partial<ProductionPlan>) => void;
deletePlan: (id: string) => void;
selectPlan: (plan: ProductionPlan | null) => void;
setPlanSteps: (planId: string, steps: PlanProcessStep[]) => void;
updatePlanStep: (planId: string, stepId: string, updates: Partial<PlanProcessStep>) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
}
export const usePlanStore = create<PlanState & PlanActions>()(
immer((set) => ({
plans: [],
selectedPlan: null,
planSteps: {},
loading: false,
error: null,
setPlans: (plans) =>
set((state) => {
state.plans = plans;
}),
addPlan: (plan) =>
set((state) => {
state.plans.unshift(plan);
}),
updatePlan: (id, updates) =>
set((state) => {
const plan = state.plans.find((p) => p.id === id);
if (plan) {
Object.assign(plan, updates);
}
}),
deletePlan: (id) =>
set((state) => {
state.plans = state.plans.filter((p) => p.id !== id);
if (state.selectedPlan?.id === id) {
state.selectedPlan = null;
}
}),
selectPlan: (plan) =>
set((state) => {
state.selectedPlan = plan;
}),
setPlanSteps: (planId, steps) =>
set((state) => {
state.planSteps[planId] = steps;
}),
updatePlanStep: (planId, stepId, updates) =>
set((state) => {
const steps = state.planSteps[planId];
if (steps) {
const step = steps.find((s) => s.id === stepId);
if (step) {
Object.assign(step, updates);
}
}
}),
setLoading: (loading) =>
set((state) => {
state.loading = loading;
}),
setError: (error) =>
set((state) => {
state.error = error;
}),
}))
);
// ========== 库存状态管理 ==========
interface InventoryItem {
id: string;
planId: string;
quantity: number;
rolls: number;
createdAt: string;
}
interface InventoryState {
items: InventoryItem[];
loading: boolean;
error: string | null;
}
interface InventoryActions {
setItems: (items: InventoryItem[]) => void;
addItem: (item: InventoryItem) => void;
updateItem: (id: string, updates: Partial<InventoryItem>) => void;
deleteItem: (id: string) => void;
setLoading: (loading: boolean) => void;
setError: (error: string | null) => void;
}
export const useInventoryStore = create<InventoryState & InventoryActions>()(
immer((set) => ({
items: [],
loading: false,
error: null,
setItems: (items) =>
set((state) => {
state.items = items;
}),
addItem: (item) =>
set((state) => {
state.items.unshift(item);
}),
updateItem: (id, updates) =>
set((state) => {
const item = state.items.find((i) => i.id === id);
if (item) {
Object.assign(item, updates);
}
}),
deleteItem: (id) =>
set((state) => {
state.items = state.items.filter((i) => i.id !== id);
}),
setLoading: (loading) =>
set((state) => {
state.loading = loading;
}),
setError: (error) =>
set((state) => {
state.error = error;
}),
}))
);
// ========== UI 状态管理 ==========
interface UIState {
sidebarOpen: boolean;
modalOpen: boolean;
modalContent: string | null;
toast: { message: string; type: 'success' | 'error' | 'info' } | null;
}
interface UIActions {
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
openModal: (content: string) => void;
closeModal: () => void;
showToast: (message: string, type: 'success' | 'error' | 'info') => void;
hideToast: () => void;
}
export const useUIStore = create<UIState & UIActions>()(
immer((set) => ({
sidebarOpen: false,
modalOpen: false,
modalContent: null,
toast: null,
toggleSidebar: () =>
set((state) => {
state.sidebarOpen = !state.sidebarOpen;
}),
setSidebarOpen: (open) =>
set((state) => {
state.sidebarOpen = open;
}),
openModal: (content) =>
set((state) => {
state.modalOpen = true;
state.modalContent = content;
}),
closeModal: () =>
set((state) => {
state.modalOpen = false;
state.modalContent = null;
}),
showToast: (message, type) =>
set((state) => {
state.toast = { message, type };
}),
hideToast: () =>
set((state) => {
state.toast = null;
}),
}))
);
// ========== 网络状态管理 ==========
interface NetworkState {
isOnline: boolean;
isSlowConnection: boolean;
pendingRequests: number;
}
interface NetworkActions {
setOnline: (online: boolean) => void;
setSlowConnection: (slow: boolean) => void;
incrementPending: () => void;
decrementPending: () => void;
}
export const useNetworkStore = create<NetworkState & NetworkActions>()(
immer((set) => ({
isOnline: true,
isSlowConnection: false,
pendingRequests: 0,
setOnline: (online) =>
set((state) => {
state.isOnline = online;
}),
setSlowConnection: (slow) =>
set((state) => {
state.isSlowConnection = slow;
}),
incrementPending: () =>
set((state) => {
state.pendingRequests++;
}),
decrementPending: () =>
set((state) => {
state.pendingRequests = Math.max(0, state.pendingRequests - 1);
}),
}))
);
【文件4】src/components/ProcessFlow.tsx - 流程节点组件(部分)
───────────────────────────────────────────────────────────────────────────────
import React, { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Check, Clock, AlertCircle } from 'lucide-react';
import type { StepType, StepStatus } from '../types';
interface ProcessNode {
id: string;
stepType: StepType;
status: StepStatus;
timestamp?: string;
operatorName?: string;
notes?: string;
}
interface ProcessFlowProps {
nodes: ProcessNode[];
onConfirm?: (nodeId: string, notes?: string) => void;
canConfirm?: boolean;
currentUserName?: string;
}
const stepConfig: Record<StepType, { label: string; icon: any; color: string }> = {
confirm: { label: '确认计划', icon: Check, color: 'blue' },
yarn_purchase: { label: '采纱', icon: Clock, color: 'amber' },
dyeing: { label: '染纱', icon: Clock, color: 'purple' },
machine_start: { label: '上机', icon: Clock, color: 'indigo' },
fabric_warehouse: { label: '坯布入库', icon: Check, color: 'emerald' },
};
export function ProcessFlow({ nodes, onConfirm, canConfirm, currentUserName }: ProcessFlowProps) {
const [expandedNode, setExpandedNode] = useState<string | null>(null);
const [notes, setNotes] = useState('');
const sortedNodes = [...nodes].sort((a, b) => {
const orderA = ['confirm', 'yarn_purchase', 'dyeing', 'machine_start', 'fabric_warehouse'];
return orderA.indexOf(a.stepType) - orderA.indexOf(b.stepType);
});
const handleConfirm = (nodeId: string) => {
if (onConfirm) {
onConfirm(nodeId, notes);
setNotes('');
setExpandedNode(null);
}
};
return (
<div className="space-y-3">
{sortedNodes.map((node, index) => {
const config = stepConfig[node.stepType];
const Icon = config.icon;
const isActive = node.status === 'active';
const isCompleted = node.status === 'completed';
const isPending = node.status === 'pending';
const isExpanded = expandedNode === node.id;
return (
<motion.div
key={node.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.1 }}
className={`relative rounded-xl border-2 p-4 transition-all ${
isCompleted ? 'border-emerald-200 bg-emerald-50' :
isActive ? 'border-blue-200 bg-blue-50' :
'border-gray-200 bg-gray-50'
}`}
>
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
isCompleted ? 'bg-emerald-500' :
isActive ? 'bg-blue-500' :
'bg-gray-300'
}`}>
<Icon className="w-5 h-5 text-white" />
</div>
<div className="flex-1">
<h4 className="font-medium text-gray-900">{config.label}</h4>
{node.timestamp && (
<p className="text-sm text-gray-500">
{new Date(node.timestamp).toLocaleString('zh-CN')}
{node.operatorName && ` · ${node.operatorName}`}
</p>
)}
</div>
{isActive && canConfirm && (
<button
onClick={() => setExpandedNode(isExpanded ? null : node.id)}
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700 transition-colors"
>
确认
</button>
)}
{isCompleted && <Check className="w-5 h-5 text-emerald-600" />}
</div>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="mt-3 pt-3 border-t border-blue-200"
>
<textarea
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="添加备注(可选)"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
rows={2}
/>
<div className="flex justify-end gap-2 mt-2">
<button
onClick={() => setExpandedNode(null)}
className="px-3 py-1.5 text-gray-600 hover:text-gray-900 text-sm"
>
取消
</button>
<button
onClick={() => handleConfirm(node.id)}
className="px-3 py-1.5 bg-blue-600 text-white rounded-lg text-sm hover:bg-blue-700"
>
确认完成
</button>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
})}
</div>
);
}
【文件5】src/components/PlanCard.tsx - 计划卡片组件(部分)
───────────────────────────────────────────────────────────────────────────────
import React from 'react';
import { motion } from 'framer-motion';
import { FileText, TrendingUp, Package, Clock } from 'lucide-react';
import type { ProductionPlan } from '../types';
interface PlanCardProps {
plan: ProductionPlan;
onClick?: () => void;
showProgress?: boolean;
}
const statusMap = {
pending: { label: '待确定', color: 'text-amber-600', bgColor: 'bg-amber-50', icon: Clock },
producing: { label: '生产中', color: 'text-blue-600', bgColor: 'bg-blue-50', icon: TrendingUp },
completed: { label: '已完成', color: 'text-emerald-600', bgColor: 'bg-emerald-50', icon: Package },
};
export function PlanCard({ plan, onClick, showProgress = true }: PlanCardProps) {
const status = statusMap[plan.status];
const StatusIcon = status.icon;
const progress = plan.target_quantity > 0
? Math.round((plan.completed_quantity / plan.target_quantity) * 100)
: 0;
return (
<motion.div
whileHover={{ scale: 1.02, y: -2 }}
whileTap={{ scale: 0.98 }}
onClick={onClick}
className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 cursor-pointer hover:shadow-md hover:border-blue-200 transition-all"
>
<div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-2">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${status.bgColor}`}>
<StatusIcon className={`w-4 h-4 ${status.color}`} />
</div>
<div>
<h3 className="font-medium text-gray-900 text-sm">{plan.product_name}</h3>
<p className="text-xs text-gray-500">{plan.fabric_code}-{plan.color_code}</p>
</div>
</div>
<span className={`px-2 py-0.5 text-xs rounded-full ${status.bgColor} ${status.color}`}>
{status.label}
</span>
</div>
{showProgress && (
<div className="mt-3">
<div className="flex items-center justify-between text-xs text-gray-500 mb-1">
<span>进度</span>
<span>{progress}%</span>
</div>
<div className="h-1.5 bg-gray-100 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${progress}%` }}
transition={{ duration: 0.5 }}
className="h-full bg-gradient-to-r from-blue-500 to-indigo-500 rounded-full"
/>
</div>
<p className="text-xs text-gray-400 mt-1">
{plan.completed_quantity} / {plan.target_quantity} 米
</p>
</div>
)}
</motion.div>
);
}
═══════════════════════════════════════════════════════════════════════════════
源代码特征说明
═══════════════════════════════════════════════════════════════════════════════
1. 编程语言TypeScript (.ts, .tsx)
2. 状态管理Zustand + Immer
3. 加密算法XOR对称加密
4. 工具函数:日期格式化、进度计算、排序算法
5. 组件设计:函数组件 + Hooks
6. 动画效果Framer Motion
7. 样式方案TailwindCSS
代码特点:
- 使用Zustand进行全局状态管理
- 使用Immer实现不可变状态更新
- 数据加密保护敏感信息
- 组件化设计,高复用性
- 响应式布局支持
- 动画交互流畅
═══════════════════════════════════════════════════════════════════════════════
第 1 页 / 共 30 页后30页
═══════════════════════════════════════════════════════════════════════════════

View File

@ -0,0 +1,180 @@
# 软件著作权登记基本信息
## 一、软件基本信息
**软件全称**:纺织行业采购计划管理系统
**软件简称**:昱森
**版本号**V1.0.0
**开发完成日期**2025年05月
**首次发表日期**2025年05月
## 二、开发的硬件环境
| 项目 | 配置 |
|------|------|
| 处理器 | Intel Core i5/i7 或 AMD Ryzen 5/7 及以上 |
| 内存 | 8GB DDR4 及以上 |
| 硬盘 | 256GB SSD 及以上 |
| 显示器 | 1920×1080 分辨率及以上 |
| 网络 | 宽带互联网连接 |
## 三、允许的硬件环境
| 项目 | 配置 |
|------|------|
| 最低配置 | 双核处理器4GB内存128GB硬盘 |
| 推荐配置 | 四核处理器8GB内存256GB SSD |
| 移动端支持 | 支持iOS 14+、Android 10+的智能手机和平板 |
## 四、开发该软件的操作系统
- **开发操作系统**macOS Sonoma 14.x / Windows 11 / Linux Ubuntu 22.04 LTS
- **开发环境**Node.js 18+ 运行时环境
## 五、软件开发环境/开发工具
| 类别 | 工具/技术 |
|------|-----------|
| 代码编辑器 | Visual Studio Code 1.85+ |
| 版本控制 | Git 2.40+ |
| 包管理器 | pnpm 8+ |
| 构建工具 | Webpack 5.89+ |
| 类型检查 | TypeScript 5.3+ |
| 测试框架 | Jest 29.7+、Playwright 1.40+ |
| 浏览器调试 | Chrome DevTools、Firefox Developer Tools |
## 六、该软件的运行平台/操作系统
| 平台 | 支持版本 |
|------|----------|
| 服务器端 | Linux (Ubuntu 20.04 LTS 及以上) |
| 客户端浏览器 | Chrome 90+、Firefox 88+、Safari 14+、Edge 90+ |
| 移动端浏览器 | iOS Safari 14+、Chrome Mobile 90+ |
## 七、软件运行支撑环境/支持软件
| 类别 | 软件/服务 |
|------|-----------|
| 云服务平台 | Meoo Cloud (基于Supabase) |
| 数据库 | PostgreSQL 15+ |
| Web服务器 | Nginx 1.24+ (反向代理) |
| 运行时环境 | Node.js 18+ (开发环境) |
| 容器化支持 | Docker 24+ (可选) |
## 八、编程语言
| 语言 | 用途 | 占比 |
|------|------|------|
| TypeScript | 前端业务逻辑、类型定义 | 约85% |
| JavaScript | 配置文件、工具脚本 | 约10% |
| SQL | 数据库迁移脚本 | 约5% |
## 九、源程序量
| 统计项 | 数量 |
|--------|------|
| 源代码文件总数 | 73个 |
| 源代码总行数 | 约20,486行 |
| 前端组件文件 | 约45个 |
| 页面组件文件 | 约25个 |
| 工具/配置文件 | 约3个 |
## 十、开发目的
本软件旨在解决纺织行业传统采购计划管理中存在的信息不对称、协作效率低、数据追溯困难等问题。通过数字化手段连接采购商(布行)、纺织厂、水洗厂三方,实现:
1. **全流程数字化**:从计划制定到成品交付的完整数字化管理
2. **多方协同**:打破信息孤岛,实现三方实时协作
3. **数据可视化**:生产进度、库存状态实时可视
4. **效率提升**:减少人工沟通成本,提高业务处理效率
## 十一、面向领域/行业
| 项目 | 说明 |
|------|------|
| **主要行业** | 纺织制造业 |
| **细分领域** | 坯布生产、面料加工、水洗处理 |
| **目标用户** | 纺织采购商(布行)、纺织厂、水洗厂 |
| **应用场景** | 生产计划管理、库存管理、供应链协同 |
## 十二、软件的主要功能
### 1. 多角色协作管理
- **采购商/布行**:创建生产计划、管理坯布库存、跟踪生产进度、处理结款
- **纺织厂**:接收生产计划、管理生产流程、坯布入库、纱线库存管理
- **水洗厂**:接收水洗订单、管理水洗流程
### 2. 计划管理
- 自动生成唯一计划编号和坯布识别码
- 纱线配比配置与用量自动计算
- 生产流程节点跟踪(确认计划 → 采纱 → 染纱 → 上机 → 坯布入库)
- 实时进度可视化支持97%完成度自动提醒
### 3. 库存管理
- **原坯布仓库**:按坯布码分组管理,入库记录追溯
- **原料纱仓库**:库存预警、最低库存提醒
- 实时库存数据同步,入库自动更新
### 4. 结款管理
- 按完成米数自动计算结款金额
- 待结款汇总统计
- 一键确认结款,记录结款时间
### 5. 分享与导入
- 加密分享链接,保护敏感信息
- 支持剪贴板自动识别导入
- 扫码或链接快速关联计划
## 十三、软件的技术特点
### 1. 前端技术特点
- **React 18** + TypeScript类型安全的函数组件开发
- **TailwindCSS**原子化CSS快速构建响应式界面
- **Framer Motion**:流畅的页面过渡和交互动画
- **响应式设计**:适配手机、平板、桌面端
### 2. 后端技术特点
- **Meoo Cloud (Supabase)**:云数据库 + 认证 + 实时订阅
- **PostgreSQL**:关系型数据库,支持复杂查询
- **Row Level Security**行级安全策略基于公司ID的数据隔离
- **Realtime**:实时数据同步,入库记录即时更新
### 3. 安全特性
- **RLS策略**:所有表启用行级安全,严格的数据访问控制
- **公司隔离**基于company_id的数据隔离确保数据安全
- **子账号支持**:主账号/子账号权限继承,统一公司数据访问
- **分享加密**分享链接使用Base64 + 盐值混淆加密
### 4. 移动端优化
- 响应式布局375px基准宽度设计
- 触控优化44px最小触控区域、防双击缩放
- 输入优化:数字键盘自动唤起、输入模式适配
- 动画优化:硬件加速、自然动画曲线
## 十四、程序鉴别材料
程序鉴别材料包括:
1. 源代码前30页程序鉴别材料-源代码前30页.doc
2. 源代码后30页程序鉴别材料-源代码后30页.doc
源代码特征:
- 使用TypeScript语言编写
- 采用React 18函数组件+Hooks架构
- 使用Webpack 5构建工具
- 遵循模块化设计原则
## 十五、文档鉴别材料
文档鉴别材料包括:
1. 软件使用说明书(见:文档鉴别材料-使用说明书.doc
2. 软件设计说明书(见:文档鉴别材料-设计说明书.doc
---
**编制日期**2025年05月
**编制单位**[您的单位名称]

View File

@ -1,4 +1,4 @@
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>织机 - 以AI为梭织天地经纬</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"><style>/* */
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>昱森 - 以AI为梭织天地经纬</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"><style>/* */
html, body {
margin: 0;
padding: 0;

View File

@ -91,7 +91,7 @@ meoo-cli cloud init -d "项目描述"
**示例**
```bash
# 创建消息表
# 1. 创建消息表
meoo-cli cloud migrate --sql "
CREATE TABLE messages (
id SERIAL PRIMARY KEY,

View File

@ -0,0 +1 @@
1

151
skills/meoo-pdf/SKILL.md Normal file
View File

@ -0,0 +1,151 @@
---
name: meoo-pdf
description: 使用此技能处理 PDF 文件的所有操作。包括:读取、提取文本/表格、合并/拆分 PDF、旋转页面、添加水印、创建新 PDF支持中文、填充 PDF 表单、提取图像以及对扫描件进行 OCR。触发场景提及 '.pdf' 文件或要求生成/处理 PDF 成果。本技能已针对 meoo 沙箱环境优化,支持中文字体。
---
# meoo-pdf: PDF 处理专家
## ⚠️ 核心执行原则
1. **环境适配**
- **已预装 (Python)**`pypdf`, `pdfplumber`, `reportlab`, `pandas`
- **已预装 (Node.js)**`pdf-lib`
- **需按需安装**Poppler-utils (用于 `pdftoppm` 预览)、QPDF (用于高级页面操作/解密)、Tesseract-OCR (用于 OCR)。
2. **禁止直接读取**:绝对禁止通过 `read``view_file` 工具直接读取此类二进制文件,这会导致乱码且无法获取有效信息。你**必须**使用本技能推荐的库编写脚本来解析内容。
3. **脚本执行**:你**必须**先将脚本写入 `home/project/tmp/` 目录(例如 `home/project/tmp/process_pdf.py`),然后运行。禁止使用 bash inline python。
4. **中文支持**:沙箱已安装 `fonts-wqy-microhei`。在使用 `reportlab` 生成 PDF 时,必须注册并使用此中文字体。
---
## 📦 环境依赖安装指南
如果在执行过程中提示命令缺失,请先运行以下安装指令:
```bash
apt-get update && apt-get install -y poppler-utils qpdf tesseract-ocr
```
---
## 中文支持配置 (Python - ReportLab)
在生成 PDF 的脚本中注册中文字体:
```python
from reportlab.pdfbase import ttfonts, pdfmetrics
from reportlab.platypus import Paragraph
from reportlab.lib.styles import getSampleStyleSheet
# 注册文泉驿微米黑
pdfmetrics.registerFont(ttfonts.TTFont('WenQuanYi', '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc'))
# 使用示例
styles = getSampleStyleSheet()
styles['Normal'].fontName = 'WenQuanYi'
化学式Paragraph("H<sub>2</sub>O", styles['Normal'])
数学公式Paragraph("x<super>2</super>", styles['Normal'])
```
---
## 🎨 美学与专业美化指南 (Aesthetics Guide)
PDF 的专业感源于“秩序感”与“可访问性”:
### 1. CRAP 设计原则
- **对比 (Contrast)**:标题字体需显著大于或黑于正文(建议正文 11-12pt
- **重复 (Repetition)**:全文页眉、页脚、各级标题颜色必须保持 100% 视觉一致性。
- **对齐 (Alignment)**:所有图片、文字块和表格必须基于隐形网格线对齐,严禁随意摆放位置。
- **亲密性 (Proximity)**:图标与标签、图片与说明文字必须紧凑,不同章节间通过大留白区分。
### 2. 导航与交互
- **可点击目录**:对于 5 页以上的文档必须生成带有超链接的目录TOC和 PDF 书签Bookmarks
- **禁止“图片化”**:确保导出的 PDF 文字可选中、可搜索。严禁将整个文档转为单张大图 PDF。
### 3. 可访问性
- 为所有图表添加 **Alt Text**
- 采用高对比度颜色组合(如黑字白底),确保内容易于阅读。
---
## 进阶功能
### 1. 元数据与加密 (pypdf)
```python
from pypdf import PdfReader, PdfWriter
reader = PdfReader("input.pdf")
writer = PdfWriter()
writer.append(reader)
# 设置元数据
writer.add_metadata({"/Title": "我的文档", "/Author": "meoo"})
# 加密
writer.encrypt("user_password", "owner_password")
with open("output.pdf", "wb") as f:
writer.write(f)
```
### 2. 扫描件 OCR (自动化提取)
如果文档无可选文本,可将其转换为图像并使用 Tesseract
```python
from pdf2image import convert_from_path
import pytesseract
images = convert_from_path('scanned.pdf', dpi=300)
text = ""
for img in images:
text += pytesseract.image_to_string(img, lang='chi_sim+eng')
```
---
## 常用工作流
### 1. 文本与表格提取
优先使用 `pdfplumber` 提取结构化数据:
```python
import pdfplumber
with pdfplumber.open("input.pdf") as pdf:
# 提取表格
table = pdf.pages[0].extract_table()
```
### 2. 合并、拆分与旋转
使用 `pypdf` 进行基础页面操作:
```python
from pypdf import PdfWriter
writer = PdfWriter()
writer.append("doc1.pdf")
writer.write("merged.pdf")
```
### 3. PDF 表单填充与标签
针对需要精准定位的表单填充或批注(基于图像坐标转换),请使用内置脚本:
```bash
python3 scripts/fill_pdf_form_with_annotations.py input.pdf fields.json output.pdf
```
### 4. 图片提取 (Image Extraction)
使用 `pdfimages` (来自 `poppler-utils`) 可以高效提取 PDF 中的原始图像:
```bash
# -j 参数确保将图像提取为 JPEG 格式
pdfimages -j input.pdf image_prefix
```
> [!TIP]
> 提取出的文件将命名为 `image_prefix-000.jpg`, `image_prefix-001.jpg` 等。
### 5. OCR 与扫描件处理
如果需要 OCR需动态安装依赖
```bash
apt-get update && apt-get install -y tesseract-ocr
pip install pytesseract
```
---
## 捆绑资源说明
- `scripts/fill_pdf_form_with_annotations.py`: 解决 PDF 坐标与图像坐标DPI映射的精准填充脚本。
- `scripts/extract_form_field_info.py`: 解析 PDF 表单域信息的辅助工具。

Some files were not shown because too many files have changed in this diff Show More