# 采购计划系统 - AGENTS.md ## 项目概述 纺织行业采购计划管理系统,支持三种角色(采购商/布行、纺织厂、水洗厂)的全流程协作管理。 ## 系统架构 ### 技术栈 | 层级 | 技术 | 说明 | |------|------|------| | 前端框架 | React 18 + TypeScript | 函数组件 + Hooks | | 构建工具 | Webpack 5 | 开发服务器端口 3015 | | 样式方案 | TailwindCSS 3 + PostCSS | 原子化 CSS | | 动画库 | Framer Motion | 页面过渡、交互动画 | | 图标库 | Lucide React | 现代化图标 | | 图表库 | Recharts | 数据可视化 | | 路由 | React Router v6 | HashRouter 模式 | | 后端服务 | Meoo Cloud (Supabase) | PostgreSQL + Auth + RLS | | 包管理 | pnpm | 依赖管理 | ### 项目目录结构 ``` /home/project/ ├── AGENTS.md # 项目文档(本文件) ├── package.json # 项目依赖配置 ├── webpack.config.js # Webpack 配置 ├── tailwind.config.js # TailwindCSS 配置 ├── postcss.config.js # PostCSS 配置 ├── tsconfig.json # TypeScript 配置 ├── index.html # HTML 模板 ├── migrations/ # 数据库迁移文件 │ ├── 20260523_052639_create_base_tables.sql │ ├── 20260523_052709_create_plan_tables.sql │ ├── 20260523_052735_create_warehouse_tables.sql │ ├── 20260523_052800_create_process_and_payment_tables.sql │ ├── 20260523_052842_enable_rls_all_tables.sql │ ├── 20260523_052936_create_test_users.sql │ ├── 20260523_053030_seed_companies_and_profiles.sql │ ├── 20260523_053110_seed_plans_and_related.sql │ ├── 20260523_124703_add_company_id_to_inventory.sql │ ├── 20260523_124742_create_product_yarn_ratios_table.sql │ └── 20260523_131935_add_production_price_to_products.sql ├── src/ │ ├── index.tsx # 应用入口 │ ├── App.tsx # 根组件(路由配置) │ ├── styles/ │ │ └── index.css # 全局样式 │ ├── types/ │ │ └── index.ts # 类型定义 │ ├── supabase/ │ │ ├── client.ts # Supabase 客户端(自动生成) │ │ └── types.ts # 数据库类型(自动生成) │ ├── utils/ │ │ └── shareCrypto.ts # 分享链接加密工具 │ ├── hooks/ │ │ ├── useResponsive.ts # 响应式 Hook │ │ └── useTheme.ts # 主题 Hook │ ├── contexts/ │ │ └── AuthContext.tsx # 认证上下文 │ ├── components/ │ │ ├── PlanCard.tsx # 计划卡片组件 │ │ ├── ProcessFlow.tsx # 流程节点组件 │ │ ├── ProgressBar.tsx # 进度条组件 │ │ └── StatusBadge.tsx # 状态标签组件 │ └── pages/ │ ├── LoginPage.tsx # 登录页 │ ├── RegisterPage.tsx # 注册页 │ ├── RoleSelectPage.tsx # 角色选择页 │ ├── MemberManage.tsx # 子账号管理页 │ ├── ImportPlanPage.tsx # 导入计划页(分享链接) │ ├── purchaser/ # 采购商页面 │ │ ├── Dashboard.tsx │ │ ├── PlanOverview.tsx │ │ ├── NewPlan.tsx │ │ ├── WarehouseManage.tsx │ │ └── FactoryManage.tsx │ ├── textile/ # 纺织厂页面 │ │ ├── Dashboard.tsx │ │ ├── PlanOverview.tsx │ │ ├── YarnWarehouse.tsx │ │ ├── FabricWarehouse.tsx │ │ └── PaymentPending.tsx │ └── washing/ # 水洗厂页面 │ └── Dashboard.tsx └── skills/ # 技能文档 ├── meoo-cloud/ ├── mobile-dev/ └── react-project/ ``` ### 重构后新增目录结构(2025-05-24) ``` src/ ├── components/ │ ├── warehouse/ # 仓库管理组件 │ │ ├── WarehouseNav.tsx # 仓库导航组件 │ │ ├── ProductForm.tsx # 产品表单模态框 │ │ ├── InboundForm.tsx # 入库表单模态框 │ │ ├── ProductList.tsx # 产品列表组件(桌面+移动端) │ │ └── InboundRecords.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(带缓存) │ ├── useResponsive.ts # 响应式Hook │ ├── useTheme.ts # 主题Hook │ └── usePerformance.ts # 性能检测Hook(动画降级) └── utils/ ├── constants.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 路径 | 页面 | 说明 | |----------|------|------| | `/login` | LoginPage | 登录页 | | `/role-select` | RoleSelectPage | 角色选择页 | | `/purchaser` | PurchaserDashboard | 采购商首页 | | `/purchaser/plans` | PlanOverview | 采购商计划总览 | | `/purchaser/plans/new` | NewPlan | 新建纺织计划 | | `/purchaser/warehouse` | WarehouseManage | 仓库管理 | | `/purchaser/factories` | FactoryManage | 工厂管理 | | `/textile` | TextileDashboard | 纺织厂首页 | | `/textile/plans` | TextilePlanOverview | 纺织厂计划总览 | | `/textile/yarn-warehouse` | YarnWarehouse | 原料纱仓库 | | `/textile/payments` | PaymentPending | 待结款 | | `/washing` | WashingDashboard | 水洗厂首页 | | `/import` | ImportPlanPage | 通过分享链接导入关联计划 | ## 数据安全 ### RLS 策略设计 所有表启用 Row Level Security (RLS),基于公司 ID 进行数据隔离: | 表 | SELECT 策略 | 修改策略 | |---|---|---| | **companies** | 开放访问 | 仅本公司 | | **profiles** | 本公司成员 | 仅自己 | | **production_plans** | 采购商或关联工厂 | 仅采购商 | | **plan_factories** | 开放访问 | 仅采购商 | | **plan_process_steps** | 开放访问 | 采购商或关联工厂 | | **inventory_records** | 相关方 | 仅本公司 | | **yarn_ratios** | 开放访问 | 仅采购商 | | **yarn_stock** | 仅本公司 | 仅本公司 | | **warehouses** | 仅本公司 | 仅本公司 | | **payments** | 付款方或收款方 | 相关方 | | **products** | 仅本公司 | 仅本公司 | ### 辅助函数 - `get_user_master_company_id()` - 获取当前用户(含子账号)的公司 ID ## 数据架构 ### ER 关系图 ``` auth.users (Supabase内置) │ ├── 1:1 ── profiles (用户配置) │ │ │ ├── N:1 ── companies (公司) │ │ │ │ │ ├── 1:N ── company_members (公司成员/子账号) │ │ │ │ │ ├── 1:N ── factories (工厂) │ │ │ │ │ │ │ ├── N:M ── production_plans (通过 plan_factories 关联) │ │ │ │ │ ├── 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 (结款记录) ``` ### 表结构详细设计 #### 1. profiles - 用户配置表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 关联 auth.users.id | | username | TEXT (UNIQUE) | 用户名 | | phone | TEXT | 手机号 | | company_id | UUID (FK) | 所属公司 | | is_master | BOOLEAN | 是否主账号 | | master_id | UUID (FK) | 主账号ID(子账号使用) | | created_at | TIMESTAMPTZ | 创建时间 | #### 2. companies - 公司表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 公司ID | | name | TEXT | 公司名称 | | role | app_role | 公司类型:purchaser/textile/washing | | address | TEXT | 公司地址 | | contact_phone | TEXT | 联系电话 | | created_at | TIMESTAMPTZ | 创建时间 | #### 3. company_members - 公司成员表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 成员记录ID | | company_id | UUID (FK) | 公司ID | | user_id | UUID (FK) | 用户ID | | role | TEXT | 成员角色 | | created_at | TIMESTAMPTZ | 加入时间 | #### 4. factories - 工厂表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 工厂ID | | company_id | UUID (FK) | 所属公司ID | | name | TEXT | 工厂名称 | | type | factory_type | 工厂类型:textile/washing | | address | TEXT | 工厂地址 | | contact_person | TEXT | 联系人 | | contact_phone | TEXT | 联系电话 | | share_link | TEXT | 分享链接 | | created_at | TIMESTAMPTZ | 创建时间 | #### 5. production_plans - 生产计划表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 计划ID | | plan_code | TEXT (UNIQUE) | 系统自动生成计划唯一识别码 | | product_name | TEXT | 成品名称 | | color | TEXT | 颜色 | | fabric_code | TEXT | 坯布唯一识别码(纯字母) | | color_code | TEXT | 色号(数字排序) | | remark | TEXT | 备注(60*40等) | | 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 | 更新时间 | #### 6. plan_factories - 计划与工厂关联表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 关联ID | | plan_id | UUID (FK) | 计划ID | | factory_id | UUID (FK) | 工厂ID | | factory_type | factory_type | 工厂类型:textile/washing | | created_at | TIMESTAMPTZ | 关联时间 | #### 7. yarn_ratios - 纱线配比表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 配比ID | | plan_id | UUID (FK) | 计划ID | | yarn_name | TEXT | 纱线名称 | | ratio | DECIMAL | 配比比例 | | amount_per_meter | DECIMAL | 每米用量(g/m) | | total_amount | DECIMAL | 总用纱量 | | created_at | TIMESTAMPTZ | 创建时间 | #### 8. 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 | 创建时间 | #### 9. warehouses - 仓库表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 仓库ID | | company_id | UUID (FK) | 所属公司ID | | name | TEXT | 仓库名称 | | type | warehouse_type | 类型:raw_fabric/fabric/finished/yarn | | location | TEXT | 仓库位置 | | created_at | TIMESTAMPTZ | 创建时间 | #### 10. inventory_records - 入库记录表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 记录ID | | plan_id | UUID (FK) | 关联计划ID | | warehouse_id | UUID (FK) | 仓库ID | | quantity | DECIMAL | 入库米数 | | warehouse_location | TEXT | 仓库位置 | | operator_id | UUID (FK) | 操作人 | | created_at | TIMESTAMPTZ | 入库时间 | #### 11. yarn_stock - 纱线库存表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 纱线ID | | company_id | UUID (FK) | 所属公司ID | | warehouse_id | UUID (FK) | 仓库ID | | name | TEXT | 纱线名称 | | spec | TEXT | 规格 | | quantity | DECIMAL | 库存数量(kg) | | min_stock | DECIMAL | 最低库存预警(kg) | | unit | TEXT | 单位 | | updated_at | TIMESTAMPTZ | 更新时间 | #### 12. payments - 结款记录表 | 字段 | 类型 | 说明 | |------|------|------| | id | UUID (PK) | 结款ID | | plan_id | UUID (FK) | 关联计划ID | | from_company_id | UUID (FK) | 付款方公司ID | | to_company_id | UUID (FK) | 收款方公司ID | | amount | DECIMAL | 结款金额 | | quantity | DECIMAL | 结款数量(米) | | price_per_meter | DECIMAL | 单价(元/米) | | status | payment_status | 状态:pending/completed | | paid_at | TIMESTAMPTZ | 结款时间 | | created_at | TIMESTAMPTZ | 创建时间 | ### 自定义枚举类型 ```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'); ``` ## 技术栈 - 前端:React 18 + Webpack + TailwindCSS + Framer Motion + Lucide Icons - 后端:Meoo Cloud (Supabase) - PostgreSQL + Auth + RLS - 路由:HashRouter (react-router-dom v6) - 包管理:pnpm ## 开发规范 - 所有数据操作通过 Supabase client,禁止 mock 数据 - RLS 策略命名:`__` 英文 snake_case - 用户认证使用 `{username}@meoo.local` 虚拟邮箱 - 操作结果必须通过 Toast 反馈给用户 - UUID 作为所有表主键 ## 账号安全策略 ### 用户名唯一性 - **profiles 表 username 字段** 已设置数据库级唯一约束(UNIQUE) - **注册时**:检查用户名是否已存在,返回具体错误提示 - **子账号创建时**:同样进行全局用户名唯一性检查,避免与其他主账号或子账号冲突 - **错误提示**:"该用户名已被使用,请更换其他用户名" ## UI/UX 优化记录 ### 2025-05-23 纺织厂工作台优化 #### 动画流畅度优化 - **Framer Motion 动画参数优化**: - 使用 `ease: [0.25, 0.46, 0.45, 0.94]` 替代默认 ease,提供更自然的动画曲线 - 缩短动画时长(0.35s),减少用户等待感 - 添加 `will-change-transform` 启用硬件加速 - 优化 stagger 延迟(0.08s),避免动画过于密集 - **ProcessFlow 组件优化**: - 当前步骤图标使用 `scale: [1, 1.15, 1]` 呼吸动画 - 弹窗使用 `AnimatePresence` 实现平滑进出 - 输入框展开/收起使用 `ease: [0.25, 0.46, 0.45, 0.94]` #### 移动端适配优化 - **响应式断点**: - 使用 `sm:`(640px)、`md:`(768px)断点 - 字体大小:`text-[10px]`(移动端)→ `text-xs`(sm)→ `text-sm`(md) - 间距:`p-3`(移动端)→ `sm:p-4` → `md:p-6` - **触控优化**: - 按钮添加 `whileTap={{ scale: 0.98 }}` 点击反馈 - 输入框添加 `touch-manipulation` 防止双击缩放 - 输入框添加 `inputMode` 和 `pattern` 优化移动端键盘 - 增大触控区域:`min-height: 44px`(CSS) - **布局适配**: - 坯布入库输入框:移动端垂直排列(`flex-col`),桌面端水平排列(`sm:flex-row`) - 统计卡片:3列网格,移动端缩小间距和字体 - 表格:添加 `overflow-x-auto` 支持横向滚动 #### 实时数据更新 - **FabricWarehouse 实时订阅**: - 使用 Supabase Realtime 订阅 `inventory_records` 表 - 入库操作后自动刷新,无需手动刷新页面 - 组件卸载时自动取消订阅 #### 文件变更 - `src/pages/textile/Dashboard.tsx` - 动画和移动端适配 - `src/pages/textile/PlanOverview.tsx` - 动画和移动端适配 - `src/pages/textile/YarnWarehouse.tsx` - 动画和移动端适配 - `src/pages/textile/FabricWarehouse.tsx` - 动画、移动端适配、实时订阅 - `src/pages/textile/PaymentPending.tsx` - 动画和移动端适配 - `src/components/ProcessFlow.tsx` - 动画优化和移动端输入框 - `src/styles/index.css` - 移动端触控优化CSS ## 更新日志 ### 2025-05-28 (v1.0.31) - **动画性能优化** - 创建 `usePerformance` Hook,自动检测设备性能级别 - 支持三种性能模式:高性能/中等性能/低性能 - 自动检测系统减少动画偏好设置 - 简化 PageTransition 组件动画,仅使用 opacity - 优化 Dashboard 页面,使用 CSS transition 替代 Framer Motion - 添加 CSS 硬件加速优化(GPU加速、内容可见性) - 支持低配置电脑流畅运行 ### 2025-05-28 (v1.0.3) - **原料纱仓库出入库记录功能** - 创建 `yarn_stock_records` 表,记录原料纱出入库历史 - 采纱环节自动记录出库,关联到对应生产计划 - 原料纱入库时自动记录入库 - 原料纱仓库页面新增"出入库记录"标签页 - 显示出入库类型、数量、关联计划、时间等信息 - **采纱环节优化** - 添加自动/手动分配模式切换 - 自动匹配失败时提示切换到手动分配 - 手动模式支持输入实际使用纱线名称和重量 - 确认时显示实际消耗统计(各纱线用量+总计) - 库存不足时强制阻止确认,必须先补充库存 - **Bug 修复** - 修复纱线占比显示为10000%的问题 - 修复 YarnWarehouse.tsx TypeScript 类型错误 ### 2025-05-27 (v1.0.2) - **账号管理页面帮助系统集成** - 新增"帮助与支持"卡片区域 - 集成用户手册入口,支持查看详细使用指南 - 集成用户反馈渠道,支持提交问题或建议 - 集成关于窗口,显示版本信息与版权 - **技术文档更新** - 新增 Bug 修复记录:帮助系统集成问题 - 新增 Bug 修复记录:计划拒绝状态管理问题 - 新增 Bug 修复记录:通知中心主题色适配问题 - 新增 Bug 修复记录:新手指引弹窗位置问题 ### 2025-05-26 - **计划详情字段完善** - 采购商和纺织厂计划总览页面统一显示所有技术字段 - 新增显示字段:克重、经线克重、纬线克重、总克重、生产采购价、产品图片、纱线配比、每米纱用量 - 新增时间字段:创建时间、计划开始时间 - 新增备注字段显示 - 纺织厂页面隐藏敏感信息(成品名称、颜色、色号)以保护采购商数据 - **移动端 UI 优化** - 计划卡片移动端适配:缩小图片尺寸(64x64)、字体调整为 text-xs - 字段标签简化(如"计划编号:"→"编号:") - 网格布局优化,避免内容溢出 - 所有工作台退出按钮在移动端变为图标形式(LogOut图标) - **拒绝计划功能增强** - 纺织厂拒绝计划后立即显示灰色卡片和"已拒绝"标签 - 被拒绝的计划自动折叠 - 被拒绝的计划从 Dashboard "最近计划"中消失 - 使用 localStorage 持久化拒绝状态,跨页面保持同步 ### 2025-05-25 - **产品图片上传功能** - 为采购商原坯布仓库产品管理添加图片上传功能 - 支持 JPG、PNG 格式图片上传,最大 5MB - 产品列表展示缩略图,无图片时显示默认占位图标 - 产品表单支持图片预览、重新上传和删除功能 - 使用 Supabase Storage 存储图片文件 - **数据库变更** - `products` 表新增 `image_url` 字段 - 创建 `product_images` 公共存储桶 - 添加存储桶 RLS 策略 - **UI 优化** - 纺织厂工作台"原坯布仓库"更名为"已生产坯布" - 更新首页快捷操作按钮标签 ## 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 ``` #### 代码变更 - `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` 跟踪被拒绝的计划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 类型兼容性问题 #### 问题描述 添加图片功能后出现类型错误: ``` Types of property 'image_url' are incompatible. Type 'string | null | undefined' is not assignable to type 'string | null' ``` #### 根本原因 - `ProductWithInventory` 接口扩展了 `Product` 类型 - 数据库生成的 `Product` 类型中 `image_url` 为 `string | null` - 但前端扩展类型中重复定义了 `image_url?: string | null`,导致类型冲突 #### 修复方案 移除 `ProductWithInventory` 接口中重复的 `image_url` 字段定义,让类型从父接口继承。 #### 代码变更 - **文件**:`src/types/index.ts` - **改动**:删除 `ProductWithInventory` 接口中的 `image_url` 字段 #### 经验教训与最佳实践 **1. 类型扩展原则** - 当接口 `extends` 另一个类型时,避免重复定义父类型中已存在的字段 - 数据库生成的类型(`Tables<'products'>`)是单一事实来源,前端扩展接口只应添加计算属性或关联数据 - 使用 `Omit` 或 `Pick` 可以明确表达类型意图 **2. 可选字段的语义** - `?: string | null` 和 `: string | null` 在 TypeScript 中有不同含义 - 数据库字段通常定义为 `: string | null`(必须存在,可为 null) - 前端表单数据可能使用 `?: string`(可选,不存在或存在) - 混用这两种语义会导致类型不兼容 **3. 类型检查时机** - 在修改类型定义后,立即运行 `pnpm run typecheck` 验证 - 类型错误往往在使用处才暴露,而非定义处 - 建议启用 TypeScript 严格模式(`strict: true`)提前发现问题 **4. 数据库迁移后的类型同步** - 执行数据库迁移后,Supabase 会自动更新 `src/supabase/types.ts` - 需要检查前端自定义类型是否需要相应调整 - 建议建立迁移后的类型检查清单 --- ### 2025-05-23 采购商仓库管理数据同步问题 #### 问题描述 采购商进入仓库管理页面时,系统崩溃报错: ``` Uncaught TypeError: Cannot read properties of undefined (reading '66f0af18-8d29-493a-ab5a-9ded734d71b8') ``` 修复后仍发现"各工厂库存"数据未正确与纺织厂数据同步。 #### 根本原因 1. **变量作用域问题**:`planToFactoryName` 变量在代码块内部定义,但在后面的代码块中被访问,导致 `undefined` 错误 2. **代码结构混乱**:工厂信息查询逻辑分散在多个地方,导致数据流不清晰 3. **工厂分组键值错误**:按工厂分组库存时使用了错误的键值(`factory_id` 而非 `factory_name`) #### 修复方案 1. **提前获取工厂信息**:将工厂信息查询(`plan_factories` + `companies`)移到 `fetchData` 函数开头,确保后续所有逻辑都能访问 2. **统一变量命名**:避免重复定义相同用途的变量,使用清晰的命名区分不同阶段的映射关系 3. **修复分组逻辑**:使用工厂名称作为分组键,确保正确汇总各工厂库存 #### 代码变更 - **文件**:`src/pages/purchaser/WarehouseManage.tsx` - **主要改动**: - 将工厂信息查询提前到数据获取阶段的开头 - 合并重复的查询逻辑,避免多次查询相同数据 - 修复 `fabricCodeFactoryInventory` 的分组键使用 - 确保 `planToFactoryName` 在整个函数作用域内可用 #### 经验教训 - **块级作用域陷阱**:JavaScript/TypeScript 的块级作用域容易导致变量访问问题,特别是在复杂的异步数据获取逻辑中 - **数据流清晰性**:相关数据查询应该集中在一起,避免分散在代码各处 - **变量命名规范**:相同用途的变量应该统一命名,避免重复定义造成混淆 ### 2025-05-23 入库记录弹窗组件 #### 功能描述 创建统一的入库记录弹窗组件 `InventoryRecordsModal`,用于在采购商和纺织厂页面展示完整的入库记录详情。 #### 组件特性 - **统计摘要**:显示总匹数和总米数 - **记录列表**:按时间倒序显示所有入库记录 - **详细信息**:包含日期时间、计划编号、工厂名称、批号、匹数、米数、来源 - **动画效果**:使用 Framer Motion 实现平滑的弹窗动画 #### 使用方式 ```tsx import { InventoryRecordsModal } from '../../components/InventoryRecordsModal'; // 在页面中使用 setRecordsModalOpen(false)} records={selectedRecords} title="产品名称 - 入库记录" /> ``` #### 文件变更 - `src/components/InventoryRecordsModal.tsx` - 新建弹窗组件 - `src/pages/purchaser/WarehouseManage.tsx` - 集成弹窗组件 - `src/pages/textile/FabricWarehouse.tsx` - 集成弹窗组件 ### 2025-05-23 移动端适配优化 #### 纺织厂原坯布仓库移动端适配 - **桌面端**:保持表格布局,完整展示所有字段 - **移动端**:使用卡片式布局,避免横向滚动 - **响应式断点**:使用 `sm:` 断点(640px)区分桌面和移动端 #### 卡片设计规范 - 计划编号和规格横向排列 - 计划/入库数量分开显示 - 最近一次入库记录显示在底部 - "全部记录"按钮简化为"全部 (X条)"节省空间 - 圆角卡片设计,带边框分隔 ## Bug 修复知识库 ### 分类一:JavaScript/TypeScript 作用域问题 #### Bug: 变量提升导致的 undefined 错误 **现象**:`Uncaught TypeError: Cannot read properties of undefined (reading 'xxx')` **根本原因**: - 在代码块内部定义的变量,在后面的代码块中被访问 - JavaScript 块级作用域导致变量不可访问 **修复方案**: - 将变量定义提前到函数作用域顶部 - 确保变量在使用前已定义 **示例代码**: ```typescript // 错误:planToFactoryName 在后面代码块中使用 if (condition) { const planToFactoryName = {}; } // planToFactoryName 在这里访问不到 // 正确:提前定义 const planToFactoryName: Record = {}; if (condition) { // 填充数据 planToFactoryName[id] = name; } // 可以正常访问 ``` **相关文件**:`src/pages/purchaser/WarehouseManage.tsx` --- ### 分类二:数据一致性问题 #### Bug: 入库记录与计划数量不一致 **现象**: - 计划显示已入库 7400 米 - 仓库只显示 4400 米 - 数据不同步 **根本原因**: - 纺织厂入库时,先检查是否有仓库 - 有仓库时才创建 `inventory_records` 记录 - 但无论是否有仓库,都更新 `completed_quantity` - 导致 `completed_quantity` 增加,但 `inventory_records` 缺失 **修复方案**: ```typescript // 错误逻辑 if (warehouse) { await supabase.from('inventory_records').insert({...}); } await supabase.from('production_plans').update({ completed_quantity: newQty }); // 正确逻辑 await supabase.from('production_plans').update({ completed_quantity: newQty }); await supabase.from('inventory_records').insert({ warehouse_id: warehouse?.id || null, // 允许 null ... }); ``` **相关文件**:`src/pages/textile/PlanOverview.tsx` --- #### Bug: 数据修复 - 补充缺失的入库记录 **现象**:历史数据存在 `completed_quantity` 与 `inventory_records` 不一致 **修复步骤**: 1. 查询不一致的数据: ```sql SELECT p.id, p.plan_code, p.completed_quantity, COALESCE(SUM(ir.quantity), 0) as inventory_total FROM production_plans p LEFT JOIN inventory_records ir ON ir.plan_id = p.id GROUP BY p.id HAVING p.completed_quantity != COALESCE(SUM(ir.quantity), 0); ``` 2. 为缺失记录的计划创建仓库(如果不存在) 3. 补充缺失的入库记录 **相关文件**:数据库修复脚本 --- ### 分类三:空值处理问题 #### Bug: NaN 显示问题 **现象**:弹窗中显示 "NaN 米" **根本原因**: - 入库记录中的 `meters` 字段可能为 `undefined` 或 `null` - 直接进行数学运算导致 `NaN` **修复方案**: ```typescript // 错误 const totalMeters = records.reduce((sum, r) => sum + r.meters, 0); // 正确 const totalMeters = records.reduce((sum, r) => sum + (r.meters || r.quantity || 0), 0); // 显示时 {record.meters || record.quantity || 0} 米 ``` **相关文件**:`src/components/InventoryRecordsModal.tsx` --- ### 分类四:UI/UX 问题 #### Bug: 移动端表格横向溢出 **现象**:移动端表格内容超出屏幕,需要横向滚动 **根本原因**: - 使用 `
` 布局,列数过多 - 没有针对移动端做适配 **修复方案**: - 桌面端:保持表格布局 `hidden sm:block` - 移动端:使用卡片式布局 `sm:hidden` - 响应式断点:`sm:`(640px) **相关文件**:`src/pages/textile/FabricWarehouse.tsx` --- ### 分类五:代码组织问题 #### Bug: 重复代码和逻辑分散 **现象**: - 相同功能在多个地方重复实现 - 工厂查询逻辑分散在多处 - 难以维护 **修复方案**: - 提取公共组件(如 `InventoryRecordsModal`) - 将相关数据查询集中在一起 - 统一变量命名规范 **相关文件**: - `src/components/InventoryRecordsModal.tsx`(新建) - `src/pages/purchaser/WarehouseManage.tsx` - `src/pages/textile/FabricWarehouse.tsx` --- ## 开发规范总结 ### 1. 数据一致性原则 - **原子操作**:相关联的数据更新必须在同一事务中完成 - **校验机制**:关键数据变更后,添加校验逻辑确保一致性 - **修复脚本**:建立数据修复机制,处理历史不一致数据 ### 2. 空值处理规范 - **默认值**:所有数值计算必须提供默认值 - **可选链**:使用 `?.` 和 `||` 处理可能为空的属性 - **类型安全**:TypeScript 严格模式,定义完整的接口类型 ### 3. 响应式设计规范 - **移动优先**:先设计移动端,再适配桌面端 - **断点选择**:使用标准断点 `sm:`(640px)、`md:`(768px) - **布局切换**:表格 ↔ 卡片,根据屏幕宽度切换 ### 4. 组件化开发 - **单一职责**:每个组件只做一件事 - **可复用性**:提取公共组件,避免重复代码 - **Props 设计**:清晰的 Props 接口,支持灵活配置 ### 5. 调试技巧 - **日志输出**:关键节点添加 console.log 输出调试信息 - **数据对比**:前后端数据对比,定位不一致问题 - **数据库查询**:直接使用 SQL 查询验证数据状态 ## 2025-05-24 代码重构记录 ### 重构背景 解决代码可维护性问题:单体文件过大(WarehouseManage.tsx 1992行)、代码重复、性能隐患。 ### 重构内容 #### 1. 类型系统重构 **文件**: `src/types/index.ts` - 新增仓库管理相关类型:`ProductPriceHistory`, `ProductYarnRatio`, `ProductInRecord`, `FactoryInventory`, `ProductWithInventory`, `FabricBatch` - 新增计划管理相关类型:`PlanWithSteps`, `PlanGroup`, `StatusConfig` - 新增流程节点类型:`ProcessNode`, `StepTypeConfig` - 新增表单类型:`YarnInput` - 新增通用工具类型:`LoadingState`, `PaginationState` #### 2. 工具函数提取 **文件**: `src/utils/constants.ts` - `statusMap` - 状态映射配置 - `simpleStatusMap` - 简化状态映射 - `stepTypeConfig` - 步骤类型配置 - `stepNames` - 步骤名称映射 - `stepOrder` - 步骤顺序 - `animationConfig` - 动画配置(标准/快速/慢速过渡、弹簧动画) - `paginationConfig` - 分页配置 - `progressThresholds` - 进度阈值 **文件**: `src/utils/helpers.ts` - `calculateProgress` - 计算生产进度百分比 - `formatDate` / `formatDateTime` / `formatTime` - 日期格式化 - `sortByStepType` - 按步骤类型排序 - `findFirstPendingIndex` - 查找第一个待确认节点 - `generateProductCode` - 生成产品码 - `extractParamsFromLink` - 从分享链接提取参数 - `debounce` / `throttle` - 防抖节流函数 - `safeParseNumber` / `safeParseInt` - 安全数字解析 - `groupProductsByNameAndWeight` - 按名称和克重分组产品 - `calculateSummary` - 计算汇总数据 #### 3. 自定义 Hooks 优化 **文件**: `src/hooks/useInventoryData.ts` - 功能:优化的库存数据获取 Hook - 特性: - 5秒数据缓存,避免重复请求 - 并行数据获取(Promise.all) - 实时订阅自动刷新 - 错误处理 **文件**: `src/hooks/usePlanData.ts` - 功能:优化的计划数据获取 Hook - 特性: - 10秒数据缓存 - 分页加载支持 - 操作人名称缓存 - 实时订阅自动刷新 #### 4. 组件拆分 **WarehouseManage.tsx 重构**: - 原文件:1992行 → 新主文件:约300行 - 拆分组件: - `WarehouseNav.tsx` - 仓库类型和子菜单导航 - `ProductForm.tsx` - 产品创建/编辑表单模态框 - `InboundForm.tsx` - 入库记录表单模态框 - `ProductList.tsx` - 产品列表(支持桌面端表格和移动端卡片) - `InboundRecords.tsx` - 入库记录列表 **PlanOverview.tsx 重构**: - 原文件:711行 → 新主文件:约250行 - 拆分组件: - `PlanGroup.tsx` - 工厂分组和计划卡片 - `ShareModal.tsx` - 分享链接弹窗 ### 性能优化 | 优化项 | 实现方式 | 效果 | |--------|----------|------| | 数据缓存 | useRef 缓存数据,5-10秒有效期 | 减少重复请求 | | 分页加载 | 每次加载20条,支持加载更多 | 避免大数据量渲染 | | 并行请求 | Promise.all 并行获取关联数据 | 减少等待时间 | | useMemo/useCallback | 缓存计算结果和函数引用 | 减少重渲染 | | 组件拆分 | 大文件拆分为职责单一组件 | 提升可维护性 | ### 代码质量提升 | 指标 | 重构前 | 重构后 | |------|--------|--------| | 最大文件行数 | 1992行 | 约300行 | | 类型定义分散度 | 多处重复定义 | 统一在 types/index.ts | | 状态映射重复 | 多处硬编码 | 统一在 constants.ts | | 动画配置重复 | 多处硬编码 | 统一在 constants.ts | | 数据获取逻辑 | 分散在组件中 | 封装为自定义 Hooks | ### 文件变更清单 **新增文件**: - `src/utils/constants.ts` - `src/utils/helpers.ts` - `src/hooks/useInventoryData.ts` - `src/hooks/usePlanData.ts` - `src/components/warehouse/WarehouseNav.tsx` - `src/components/warehouse/ProductForm.tsx` - `src/components/warehouse/InboundForm.tsx` - `src/components/warehouse/ProductList.tsx` - `src/components/warehouse/InboundRecords.tsx` - `src/components/plans/PlanGroup.tsx` - `src/components/plans/ShareModal.tsx` **修改文件**: - `src/types/index.ts` - 扩展类型定义 - `src/pages/purchaser/WarehouseManage.tsx` - 重构为精简主文件 - `src/pages/purchaser/PlanOverview.tsx` - 重构为精简主文件 ## 2025-05-24 出入库历史功能开发记录 ### 需求背景 采购商工作台-原坯布仓库的产品管理模块需要增加出入库历史查看功能,方便用户追踪每个产品的库存变动记录。 ### 遇到的问题 #### 问题1:类型定义不匹配 **现象**:`ProductInRecord` 类型定义与数据库实际返回的字段不一致,导致类型检查报错。 **根本原因**: - 原类型定义只包含前端展示需要的字段(`time`, `rolls`, `meters`, `source` 等) - 但数据库查询返回的是完整的表字段(`id`, `product_id`, `company_id`, `created_at` 等) - TypeScript 严格模式下,类型不匹配导致赋值错误 **错误信息**: ``` Type '{ batch_no: string; company_id: string | null; created_at: string; ... }' is missing the following properties from type 'ProductInRecord': time, source ``` **解决方案**: 扩展 `ProductInRecord` 类型定义,包含所有数据库字段,同时保留前端展示用的可选扩展字段: ```typescript export interface ProductInRecord { // 数据库字段(必需) id: string; product_id: string; company_id: string | null; batch_no: string; rolls: number; meters: number; notes: string | null; operator_id: string | null; warehouse_id: string | null; created_at: string; // 前端展示用扩展字段(可选) time?: string; plan_code?: string; factory_name?: string; source?: 'self' | 'textile'; ... } ``` **经验教训**: - 类型定义应该与数据库表结构保持一致 - 前端特有的展示字段应该标记为可选(`?`) - 使用 TypeScript 严格模式可以提前发现这类问题 ### 实现方案 #### 1. 组件层修改(ProductList.tsx) **新增 Props**: - `onViewStockHistory: (product: ProductWithInventory) => void` - 查看出入库历史回调 **桌面端操作列**: - 在"价格历史"按钮前添加"出入库历史"按钮(紫色样式) - 操作按钮顺序:出入库历史 → 价格历史 → 编辑 → 删除 **移动端操作按钮**: - 第一行:出入库历史(紫色)、价格历史(绿色) - 第二行:编辑(蓝色)、删除(红色) - 分两行布局避免按钮过于拥挤 #### 2. 页面层修改(WarehouseManage.tsx) **新增状态**: ```typescript const [stockHistoryModalOpen, setStockHistoryModalOpen] = useState(false); const [selectedStockHistory, setSelectedStockHistory] = useState([]); ``` **新增处理函数**: ```typescript const handleViewStockHistory = useCallback(async (product: ProductWithInventory) => { const { data } = await supabase .from('product_inventory_records') .select('*') .eq('product_id', product.id) .order('created_at', { ascending: false }); setSelectedStockHistory(data || []); setSelectedProductName(`${product.product_name}-${product.weight}g-${product.color}`); setStockHistoryModalOpen(true); }, []); ``` **弹窗设计**: - 标题:{产品名称} - 出入库历史 - 内容展示:序号、时间、批号、匹数、米数、备注 - 空状态:显示"暂无出入库记录" - 样式:与价格历史弹窗保持一致(灰色卡片背景) #### 3. 类型层修改(types/index.ts) **修复 ProductInRecord 类型**: - 添加所有数据库表字段作为必需字段 - 保留原有前端扩展字段作为可选字段 - 确保与 `Tables<'product_inventory_records'>` 兼容 ### 文件变更清单 **修改文件**: - `src/components/warehouse/ProductList.tsx` - 添加出入库历史按钮和回调 - `src/pages/purchaser/WarehouseManage.tsx` - 添加出入库历史弹窗和查询逻辑 - `src/types/index.ts` - 修复 ProductInRecord 类型定义 ### UI 设计规范 **按钮颜色体系**: | 功能 | 颜色 | Tailwind 类 | |------|------|-------------| | 出入库历史 | 紫色 | `text-purple-600`, `bg-purple-50` | | 价格历史 | 绿色 | `text-emerald-600`, `bg-emerald-50` | | 编辑 | 蓝色 | `text-blue-600`, `bg-blue-50` | | 删除 | 红色 | `text-red-600`, `bg-red-50` | **弹窗布局**: - 最大宽度:`max-w-2xl`(约672px) - 最大高度:`max-h-[80vh]`,内容区 `max-h-[60vh]` - 卡片内边距:`p-4` - 列表间距:`space-y-3` ## 分享链接安全策略 ### 加密机制 - 分享链接使用 `src/utils/shareCrypto.ts` 进行加密/解密 - 加密内容仅包含:`planId` + `factoryType` + `timestamp` - 使用 Base64 + 盐值混淆 + 字符串反转进行加密 ### 敏感信息保护 - **分享链接不传输**:成品名称、颜色、色号等敏感信息 - **导入页面不展示**:接收方通过链接导入计划时,页面不显示成品名称、颜色、色号 - **仅展示**:计划编号、坯布识别码、计划产量、每米布纱用量、生产采购价、备注 --- ## Bug 修复知识库(扩展) ### 分类六:React 组件生命周期问题 #### Bug: 内存泄漏 - 未清理的订阅和定时器 **现象**: - 页面切换后控制台报错:`Can't perform a React state update on an unmounted component` - 内存占用持续增长 - 实时订阅重复触发 **根本原因**: - 组件卸载时未清理 Supabase Realtime 订阅 - 未清除 setInterval/setTimeout 定时器 - 事件监听器未移除 **修复方案**: ```typescript // 错误:未清理订阅 useEffect(() => { const channel = supabase .channel('inventory_changes') .on('postgres_changes', {...}, callback) .subscribe(); }, []); // 正确:清理订阅 useEffect(() => { const channel = supabase .channel('inventory_changes') .on('postgres_changes', {...}, callback) .subscribe(); return () => { channel.unsubscribe(); }; }, []); // 定时器清理 useEffect(() => { const timer = setInterval(fetchData, 5000); return () => clearInterval(timer); }, []); ``` **相关文件**:`src/hooks/useRealtime.ts`, `src/hooks/useInventoryData.ts` --- ### 分类七:Supabase RLS 策略问题 #### Bug: RLS 策略导致数据无法插入 **现象**: - 插入数据时报错:`new row violates row-level security policy for table "xxx"` - 部分用户无法查看数据 - 子账号无法操作主账号数据 **根本原因**: - RLS 策略条件过于严格 - 未考虑子账号场景(需要通过 master_id 获取 company_id) - 策略中使用了错误的字段比较 **修复方案**: ```sql -- 错误:仅检查 user_id CREATE POLICY users_insert_inventory ON inventory_records FOR INSERT WITH CHECK (auth.uid() = operator_id); -- 正确:检查公司权限(支持子账号) CREATE OR REPLACE FUNCTION get_user_master_company_id() RETURNS UUID AS $$ DECLARE v_company_id UUID; v_master_id UUID; BEGIN SELECT company_id, master_id INTO v_company_id, v_master_id FROM profiles WHERE id = auth.uid(); IF v_master_id IS NOT NULL THEN SELECT company_id INTO v_company_id FROM profiles WHERE id = v_master_id; END IF; RETURN v_company_id; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- 使用辅助函数创建策略 CREATE POLICY company_insert_inventory ON inventory_records FOR INSERT WITH CHECK ( company_id = get_user_master_company_id() ); ``` **相关文件**:`migrations/*_create_*_rls.sql` --- ### 分类八:Webpack 构建问题 #### Bug: 开发服务器热更新失效 **现象**: - 修改代码后页面不自动刷新 - 控制台报错 `WebSocket connection failed` - 热模块替换 (HMR) 不工作 **根本原因**: - Webpack devServer 配置缺少 `hot: true` - `allowedHosts` 配置不正确 - `historyApiFallback` 未启用 **修复方案**: ```javascript // webpack.config.js development: { devServer: { port: 3015, hot: true, // 启用热更新 historyApiFallback: true, // 支持前端路由 allowedHosts: ['all', '.alibaba-inc.com'], // 允许所有主机 client: { overlay: { errors: true, warnings: false } } } } ``` **相关文件**:`webpack.config.js` --- #### Bug: 生产构建产物体积过大 **现象**: - 构建后的 JS 文件超过 2MB - 首屏加载时间过长 - 代码未压缩 **修复方案**: ```javascript // webpack.config.js const TerserPlugin = require('terser-webpack-plugin'); module.exports = { optimization: { minimize: true, minimizer: [new TerserPlugin()], splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } } } }; ``` --- ### 分类九:TypeScript 类型问题 #### Bug: 严格模式下的隐式 any 错误 **现象**: - 编译报错:`Parameter 'xxx' implicitly has an 'any' type` - 类型推断失败 - 第三方库类型定义缺失 **修复方案**: ```typescript // tsconfig.json { "compilerOptions": { "strict": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true } } // 代码中显式声明类型 // 错误 const handleClick = (e) => { ... }; // 正确 const handleClick = (e: React.MouseEvent) => { ... }; // 第三方库类型声明 // 创建 src/types/declarations.d.ts declare module 'some-untyped-lib' { export function doSomething(): void; } ``` --- #### Bug: 泛型约束导致的类型不匹配 **现象**: - 泛型组件使用时类型推断错误 - `Type 'T' is not assignable to type 'xxx'` **修复方案**: ```typescript // 错误:泛型约束不明确 interface Props { items: T[]; renderItem: (item: T) => React.ReactNode; } // 正确:添加约束 interface Props { items: T[]; renderItem: (item: T) => React.ReactNode; } // 使用 interface User { id: string; name: string; } items={users} renderItem={(user) => {user.name}} /> ``` --- ### 分类十:状态管理问题 #### Bug: 状态更新异步导致的竞态条件 **现象**: - 快速点击按钮时数据不一致 - 表单提交后状态未同步更新 - 多个组件间状态不同步 **根本原因**: - 直接修改状态而非使用函数式更新 - 异步操作未正确处理依赖 - 缺少乐观更新 **修复方案**: ```typescript // 错误:直接依赖旧状态 const handleIncrement = () => { setCount(count + 1); // 可能使用过时的 count }; // 正确:使用函数式更新 const handleIncrement = () => { setCount(prev => prev + 1); }; // 复杂状态更新 const [state, setState] = useState({ count: 0, loading: false }); const updateState = useCallback((updates: Partial) => { setState(prev => ({ ...prev, ...updates })); }, []); // 乐观更新示例 const handleAddItem = async (item: Item) => { // 乐观更新 UI setItems(prev => [...prev, { ...item, id: 'temp-' + Date.now() }]); try { const { data } = await supabase.from('items').insert(item).select().single(); // 用真实数据替换临时数据 setItems(prev => prev.map(i => i.id.startsWith('temp-') ? data : i)); } catch (error) { // 回滚 setItems(prev => prev.filter(i => !i.id.startsWith('temp-'))); toast.error('添加失败'); } }; ``` **相关文件**:`src/stores/index.ts` --- ### 分类十一:表单处理问题 #### Bug: 受控组件与非受控组件混用警告 **现象**: - 控制台警告:`A component is changing an uncontrolled input to be controlled` - 表单值初始化后无法修改 **根本原因**: - 初始值为 `undefined` 或 `null`,后续变为有值 - React 无法确定是受控还是非受控组件 **修复方案**: ```typescript // 错误:初始值可能为 undefined const [value, setValue] = useState(); // 正确:始终提供非 undefined 初始值 const [value, setValue] = useState(''); // 对象表单 const [form, setForm] = useState({ name: '', email: '', age: 0 }); // 处理可能为 null 的数据 const [user, setUser] = useState(null); // 渲染时 ``` --- #### Bug: 表单验证时机问题 **现象**: - 提交时才显示验证错误,用户体验差 - 实时验证过于频繁,性能问题 - 异步验证导致表单状态混乱 **修复方案**: ```typescript // 使用防抖进行实时验证 const debouncedValidate = useMemo( () => debounce((value: string) => { const error = validateEmail(value); setErrors(prev => ({ ...prev, email: error })); }, 300), [] ); // 表单提交验证 const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const errors = validateForm(formData); if (Object.keys(errors).length > 0) { setErrors(errors); return; } setSubmitting(true); try { await submitForm(formData); } finally { setSubmitting(false); } }; ``` --- ### 分类十二:CSS/Tailwind 问题 #### Bug: Tailwind 类名未生效 **现象**: - 样式未应用 - 自定义配置的颜色/间距未生效 - 生产构建后样式丢失 **根本原因**: - `tailwind.config.js` 配置错误 - `content` 配置未包含所有文件路径 - PostCSS 配置问题 **修复方案**: ```javascript // tailwind.config.js module.exports = { content: [ './src/**/*.{js,jsx,ts,tsx}', // 确保包含所有组件文件 './index.html' ], theme: { extend: { colors: { primary: { 50: '#eff6ff', 500: '#3b82f6', 600: '#2563eb', } } } } }; // postcss.config.js module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } }; ``` --- #### Bug: 移动端样式被桌面端覆盖 **现象**: - 移动端样式在桌面端显示正常,但在移动端错误 - 响应式断点不生效 **根本原因**: - Tailwind 的类名顺序问题 - 未理解移动优先原则 **修复方案**: ```html
移动端小字 → 平板中字 → 桌面大字
仅在小于 640px 时显示
``` --- ### 分类十三:性能优化问题 #### Bug: 大数据列表渲染卡顿 **现象**: - 列表超过 100 条时滚动卡顿 - 内存占用高 - 帧率下降 **修复方案**: ```typescript // 1. 虚拟列表 import { useVirtualizer } from '@tanstack/react-virtual'; const VirtualList = ({ items }) => { const parentRef = useRef(null); const virtualizer = useVirtualizer({ count: items.length, getScrollElement: () => parentRef.current, estimateSize: () => 50, }); return (
{virtualizer.getVirtualItems().map((virtualItem) => (
{items[virtualItem.index]}
))}
); }; // 2. 分页加载 const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const loadMore = async () => { const { data } = await supabase .from('items') .select('*') .range(page * 20, (page + 1) * 20 - 1); if (data.length < 20) setHasMore(false); setItems(prev => [...prev, ...data]); setPage(p => p + 1); }; // 3. 使用 React.memo 和 useMemo const MemoizedItem = React.memo(({ item }) => { return
{item.name}
; }); ``` **相关文件**:`src/components/VirtualList.tsx` --- ### 分类十六:沙箱环境兼容性问题 #### Bug: Webpack devServer 在沙箱环境中预览加载失败 **现象**: - 开发服务器启动正常,端口 3015 在监听 - 但 curl 请求超时,无法获取 HTML 内容 - 浏览器预览无法加载 **根本原因**: 1. **沙箱网络限制**:`net.ipv4.conf.all.forwarding = 0`(IP 转发被禁用) 2. **Webpack devServer 配置问题**: - `host: '127.0.0.1'` 无法在沙箱中访问 - `static` 配置指向不存在的 `public` 目录 - `devMiddleware.writeToDisk` 与沙箱环境不兼容 3. **与其他项目对比**:Vite 在沙箱中可以正常工作,Webpack devServer 存在兼容性问题 **修复方案**: ```javascript // webpack.config.js - 标准配置 module.exports = { // ...其他配置 devServer: { port: 3015, host: '0.0.0.0', // 必须使用 0.0.0.0 allowedHosts: 'all', hot: true, historyApiFallback: true, // 不要配置 static 和 devMiddleware,使用默认行为 }, }; ``` **关键配置点**: 1. **host: '0.0.0.0'** - 必须绑定到所有接口,而不是 127.0.0.1 2. **不要配置 static** - 让 webpack 使用内存中的编译产物 3. **不要配置 devMiddleware.writeToDisk** - 沙箱中不需要写入磁盘 4. **保持简单** - 避免复杂的 devServer 配置 **验证方法**: ```bash # 1. 构建验证 pnpm run build # 2. 检查 dist 目录 ls -la dist/ # 3. 启动开发服务器(沙箱中 curl 可能仍超时,但不影响实际功能) pnpm run dev ``` **替代方案**: - 使用 `pnpm run build` 生成静态文件 - 在实际部署环境中测试预览功能 - 考虑迁移到 Vite(沙箱兼容性更好) **相关文件**: - `webpack.config.js` - `package.json` --- ### 分类十七:帮助系统集成问题 #### 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 中添加入口按钮(位于主账号卡片下方)

帮助与支持

{/* 用户手册按钮 */} setShowUserManual(true)}> 用户手册 详细使用指南 {/* 用户反馈按钮 */} setShowFeedback(true)}> 用户反馈 提交问题或建议 {/* 关于按钮 */} setShowAbout(true)}> 关于 版本信息与版权
// 4. 在页面底部添加弹窗组件 setShowUserManual(false)} /> setShowFeedback(false)} /> setShowAbout(false)} /> ``` **设计要点**: - 使用卡片式布局,与页面整体风格一致 - 三个按钮使用不同主题色(蓝/绿/紫)便于区分 - 按钮包含图标、标题和描述,信息清晰 - 位于主账号信息下方、子账号列表上方,位置合理 **相关文件**:`src/pages/MemberManage.tsx` --- ### 分类十八:计划拒绝状态管理问题 #### Bug: 纺织厂拒绝计划后状态未持久化 **现象**: - 纺织厂拒绝计划后,刷新页面或重新进入,计划又变回"待确定"状态 - 被拒绝的计划仍显示在 Dashboard "最近计划"中 - 拒绝弹窗每次进入页面都会重复弹出 **根本原因**: - 拒绝状态仅保存在组件 state 中,页面刷新后丢失 - Dashboard 和 PlanOverview 之间没有共享拒绝状态 - 没有机制跟踪用户已查看的拒绝弹窗 **修复方案**: ```typescript // 1. 使用 localStorage 持久化拒绝状态 const [rejectedPlans, setRejectedPlans] = useState>(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>(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 // 纺织厂 Dashboard // 水洗厂 Dashboard ``` **视觉设计规范**: | 工作台 | 背景色 | 图标色 | 悬停色 | |--------|--------|--------|--------| | 采购商 | 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 偏移
// 正确:使用 items-center 实现真正的居中
``` **设计原则**: - 弹窗应该始终位于视口中央,确保用户第一眼就能看到 - 使用 `items-center justify-center` 实现水平和垂直居中 - 添加 `p-4` 确保在小屏幕上有足够的边距 - 避免使用固定的 padding 值进行偏移 **相关文件**: - `src/pages/textile/Dashboard.tsx` - `src/pages/purchaser/Dashboard.tsx` - `src/pages/washing/Dashboard.tsx` --- ## 学习反思与最佳实践 ### 1. 环境差异意识 **问题**:在本地开发环境正常工作的配置,在沙箱环境中可能失效。 **教训**: - 沙箱环境有严格的网络限制(IP 转发禁用、localhost 访问受限) - 需要针对沙箱环境调整开发服务器配置 - 不要假设所有环境行为一致 ### 2. 配置简化原则 **问题**:过度配置导致兼容性问题。 **教训**: - 使用框架/工具的默认配置作为起点 - 只在必要时添加自定义配置 - 复杂的配置组合可能在特定环境中失效 ### 3. 调试方法改进 **问题**:陷入反复修改配置的循环,没有定位真正原因。 **教训**: - 先检查环境限制(`sysctl net.ipv4.conf.all.forwarding`) - 对比正常项目和异常项目的差异 - 使用最小化配置验证(逐步添加配置项) - 接受某些限制(如沙箱中 curl 无法访问) ### 4. 文档记录重要性 **问题**:花费大量时间排查已知问题。 **教训**: - 及时记录环境特定的配置要求 - 建立常见问题知识库 - 记录失败尝试和成功方案 ### 5. 技术选型考虑 **问题**:Webpack devServer 在沙箱中存在兼容性问题。 **教训**: - 技术选型需要考虑目标运行环境 - Vite 在沙箱环境中兼容性更好 - 对于新项目,优先考虑沙箱友好的工具链 --- ## 沙箱环境开发指南 ### 已知限制 1. **网络访问**:localhost/127.0.0.1 访问受限 2. **IP 转发**:`net.ipv4.conf.all.forwarding = 0` 3. **端口**:仅 3015 可用 4. **进程限制**:某些命令执行超时 ### 推荐配置 ```javascript // webpack.config.js devServer: { port: 3015, host: '0.0.0.0', allowedHosts: 'all', hot: true, historyApiFallback: true, } ``` ### 验证流程 1. `pnpm run typecheck` - 类型检查 2. `pnpm run build` - 构建验证 3. `ls -la dist/` - 检查产物 4. `pnpm run dev` - 启动服务器(沙箱中预览可能受限) ### 故障排查 - 检查端口占用:`lsof -i :3015` - 检查 IP 转发:`sysctl net.ipv4.conf.all.forwarding` - 简化配置:移除 static/devMiddleware 等复杂配置 - 对比其他项目:参考正常工作的项目配置 --- ### 分类十四:浏览器兼容性问题 #### Bug: 旧版浏览器不支持新特性 **现象**: - 在 Safari 13 或 IE11 中白屏 - 某些 API 报错 `xxx is not defined` **修复方案**: ```javascript // webpack.config.js module.exports = { target: ['web', 'es5'], // 编译到 ES5 module: { rules: [ { test: /\.(js|jsx|ts|tsx)$/, use: { loader: 'babel-loader', options: { presets: [ ['@babel/preset-env', { targets: { browsers: ['> 1%', 'last 2 versions', 'not dead'] }, useBuiltIns: 'usage', corejs: 3 }] ] } } } ] } }; // 使用 polyfill import 'core-js/stable'; import 'regenerator-runtime/runtime'; ``` --- ### 分类十五:测试相关问题 #### Bug: 异步测试超时 **现象**: - 测试报错 `Timeout - Async callback was not invoked within the 5000ms` - Supabase 操作未正确 mock **修复方案**: ```typescript // jest.config.js module.exports = { testTimeout: 10000, // 增加超时时间 setupFilesAfterEnv: ['/src/__tests__/setup.ts'] }; // setup.ts import '@testing-library/jest-dom'; // Mock Supabase jest.mock('../supabase/client', () => ({ supabase: { from: jest.fn(() => ({ select: jest.fn().mockReturnThis(), insert: jest.fn().mockReturnThis(), update: jest.fn().mockReturnThis(), delete: jest.fn().mockReturnThis(), eq: jest.fn().mockReturnThis(), single: jest.fn().mockResolvedValue({ data: null, error: null }) })), auth: { getUser: jest.fn().mockResolvedValue({ data: { user: { id: 'test-user-id' } } }) } } })); // 测试用例 import { render, screen, waitFor } from '@testing-library/react'; test('loads data', async () => { render(); await waitFor(() => { expect(screen.getByText('Loaded')).toBeInTheDocument(); }, { timeout: 3000 }); }); ``` --- ## 调试技巧总结 ### 1. 网络请求调试 ```typescript // 在 supabase 客户端添加日志 const supabase = createClient(url, key, { db: { schema: 'public' }, global: { fetch: (...args) => { console.log('Supabase Request:', args[0]); return fetch(...args); } } }); ``` ### 2. 性能分析 ```typescript // 使用 React DevTools Profiler // 或使用 console.time console.time('dataFetch'); const data = await fetchData(); console.timeEnd('dataFetch'); // 使用 Performance API const mark = performance.mark('start'); // ... 操作 performance.measure('operation', 'start'); ``` ### 3. 错误边界 ```typescript // ErrorBoundary.tsx class ErrorBoundary extends React.Component< { children: React.ReactNode }, { hasError: boolean; error: Error | null } > { constructor(props: { children: React.ReactNode }) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error: Error) { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error('Error caught by boundary:', error, errorInfo); // 上报错误到监控服务 } render() { if (this.state.hasError) { return ; } return this.props.children; } } ``` ### 4. 开发环境检查清单 - [ ] 控制台无警告/错误 - [ ] React DevTools 无不必要的重渲染 - [ ] Network 面板无重复请求 - [ ] Lighthouse 评分 > 90 - [ ] 移动端模拟器测试通过 - [ ] 类型检查通过 `pnpm run typecheck` - [ ] 代码规范检查通过 `pnpm run lint` --- ## 2025-05-27 产品信息功能更新记录 ### 功能变更 #### 1. 产品信息页面名称统一 **变更内容**:将"原坯布仓库"统一更名为"产品信息" **涉及文件**: - `src/pages/purchaser/Dashboard.tsx` - 快捷操作按钮标签 - `src/pages/purchaser/WarehouseManage.tsx` - 页面标题 - `src/components/OnboardingGuide.tsx` - 新手指引描述 - `src/components/UserManual.tsx` - 用户手册内容 - `src/pages/purchaser/NewPlan.tsx` - 新建计划页面产品选择标题 #### 2. 产品列表折叠逻辑优化 **变更内容**: - 产品无克重时,直接显示产品列表,不显示克重折叠卡片 - 产品仅有一种克重时,直接展开显示,不需要点击折叠 **涉及文件**: - `src/components/warehouse/ProductList.tsx` #### 3. 产品唯一性校验 **新增校验规则**: - **产品码 (fabric_code)**:全局唯一,保存时检查是否已存在 - **色号 (color_code)**:在"成品名称-克重-颜色"组合中唯一 **涉及文件**: - `src/pages/purchaser/WarehouseManage.tsx` #### 4. 产品码自动生成优化 **变更内容**: - 选择已有产品后,自动生成产品码时查询数据库中该前缀的最大数字 - 生成规则:`前缀 + (最大数字 + 1)` **涉及文件**: - `src/components/warehouse/ProductForm.tsx` #### 5. 色号自动生成优化 **变更内容**: - 选择已有产品后,自动生成色号时查询"产品名称-克重"组合下的最大色号 - 生成规则:`(最大色号数字 + 1)`,保持两位数格式 **涉及文件**: - `src/components/warehouse/ProductForm.tsx` --- ## Bug 修复记录(2025-05-27) ### 分类二十一:产品唯一性校验问题 #### Bug: 产品码和色号重复问题 **现象**: - 可以创建相同产品码的产品 - 同一"成品名称-克重-颜色"组合下可以有重复色号 **根本原因**: - 保存产品时未进行唯一性校验 - 自动生成产品码/色号时未查询数据库最大值 **修复方案**: ```typescript // 1. 产品码全局唯一校验 const { data: existingFabricCode } = await supabase .from('products') .select('id') .eq('company_id', auth.company.id) .eq('fabric_code', productData.fabric_code) .neq('id', editingProduct?.id || '00000000-0000-0000-0000-000000000000') .maybeSingle(); if (existingFabricCode) { alert(`产品码 "${productData.fabric_code}" 已存在`); return; } // 2. 色号在"成品名称-克重-颜色"组合中唯一校验 const { data: existingColorCode } = await supabase .from('products') .select('id') .eq('company_id', auth.company.id) .eq('product_name', productData.product_name) .eq('total_weight', productData.total_weight || 0) .eq('color', productData.color) .eq('color_code', productData.color_code) .neq('id', editingProduct?.id || '00000000-0000-0000-0000-000000000000') .maybeSingle(); if (existingColorCode) { alert(`色号重复`); return; } ``` **相关文件**: - `src/pages/purchaser/WarehouseManage.tsx` --- ### 分类二十二:产品码/色号自动生成问题 #### Bug: 自动生成的产品码/色号可能重复 **现象**: - 选择产品后生成的产品码可能与已有产品码重复 - 色号生成仅基于当前产品递增,未考虑数据库中最大值 **根本原因**: - 原逻辑仅对当前选中的产品码进行简单递增(如 M2 → M3) - 未查询数据库中该前缀的实际最大值 **修复方案**: ```typescript // 产品码生成:查询数据库中该前缀的最大数字 const generateNextFabricCode = async (companyId: string, prefix: string): Promise => { const { data } = await supabase .from('products') .select('fabric_code') .eq('company_id', companyId) .ilike('fabric_code', `${prefix}%`); let maxNum = 0; data?.forEach(item => { const match = item.fabric_code.match(/^(.*?)(\d+)$/); if (match && match[1].toUpperCase() === prefix.toUpperCase()) { const num = parseInt(match[2], 10); if (num > maxNum) maxNum = num; } }); return `${prefix}${maxNum + 1}`; }; // 色号生成:查询"产品名称-克重"组合下的最大色号 const generateNextColorCode = async (companyId: string, productName: string, totalWeight: number | null): Promise => { const { data } = await supabase .from('products') .select('color_code') .eq('company_id', companyId) .eq('product_name', productName) .eq('total_weight', totalWeight || 0); let maxNum = 0; data?.forEach(item => { const num = parseInt(item.color_code, 10); if (!isNaN(num) && num > maxNum) maxNum = num; }); const nextNum = maxNum + 1; return nextNum < 10 ? `0${nextNum}` : String(nextNum); }; ``` **相关文件**: - `src/components/warehouse/ProductForm.tsx` --- ## Bug 修复记录(2025-05-28) ### 分类二十三:NotificationCenter主题色缺失问题 #### Bug: 水洗厂工作台崩溃 - Cannot read properties of undefined (reading 'bg') **现象**: - 用户点击"水洗厂"角色进入工作台时页面白屏 - 控制台报错:`Cannot read properties of undefined (reading 'bg')` - 错误位置:`NotificationCenter.tsx:289:98` **根本原因**: - NotificationCenter组件的`themeConfig`中未定义`violet`主题 - 水洗厂Dashboard传入`theme="violet"`,导致`themeConfig[theme]`返回undefined - 访问`colors.bg`时抛出TypeError **修复方案**: ```typescript // 1. 扩展类型定义 interface NotificationCenterProps { theme?: 'amber' | 'emerald' | 'purple' | 'blue' | 'violet'; // 添加violet } // 2. 添加violet主题配置 const themeConfig = { amber: { bg: 'bg-amber-100', hover: 'hover:bg-amber-200', icon: 'text-amber-700' }, emerald: { bg: 'bg-emerald-100', hover: 'hover:bg-emerald-200', icon: 'text-emerald-700' }, purple: { bg: 'bg-purple-100', hover: 'hover:bg-purple-200', icon: 'text-purple-700' }, blue: { bg: 'bg-blue-100', hover: 'hover:bg-blue-200', icon: 'text-blue-700' }, violet: { bg: 'bg-violet-100', hover: 'hover:bg-violet-200', icon: 'text-violet-500' }, // 新增 }; ``` **相关文件**: - `src/components/NotificationCenter.tsx` - `src/pages/washing/Dashboard.tsx` --- ### 分类二十四:登录页角色颜色不一致问题 #### Bug: 登录页水洗厂颜色与Dashboard不一致 **现象**: - 登录页水洗厂演示账号按钮使用`from-cyan-500 to-blue-600`(蓝青色) - 水洗厂Dashboard使用`from-violet-400 to-fuchsia-400`(紫罗兰色) - 同一角色在不同页面颜色不统一 **根本原因**: - 登录页`demoAccounts`配置未随Dashboard主题色更新而同步调整 - 缺乏统一的主题色管理规范 **修复方案**: ```typescript // LoginPage.tsx const demoAccounts = [ { username: 'purchaser', label: '采购商(布行)', color: 'from-blue-500 to-blue-600', icon: Building2, enabled: true }, { username: 'textile', label: '纺织厂', color: 'from-emerald-500 to-green-600', icon: Factory, enabled: true }, { username: 'washing', label: '水洗厂', color: 'from-violet-400 to-fuchsia-400', icon: Droplets, enabled: true } // 更新为紫罗兰色 ]; ``` **相关文件**: - `src/pages/LoginPage.tsx` --- ## 学习反思与最佳实践(2025-05-28) ### 主题色统一设计模式 **问题背景**: 三个角色(采购商、纺织厂、水洗厂)原本使用不同深度的颜色(-500/-600),视觉上不够协调统一。 **解决方案**: 统一使用`-400`色阶的柔和色调: | 角色 | 原配色 | 新配色 | 特点 | |------|--------|--------|------| | 采购商 | `amber-500/600` | `amber-400/orange-400` | 温暖橙黄 | | 纺织厂 | `emerald-500/600` | `emerald-400/teal-400` | 清新青绿 | | 水洗厂 | `purple-500/600` | `violet-400/fuchsia-400` | 柔和紫粉 | **设计原则**: 1. **色阶统一**:所有主题色使用`-400`色阶,保持视觉一致性 2. **渐变搭配**:使用相邻色系的柔和渐变(如violet→fuchsia) 3. **全站同步**:登录页、Dashboard、MemberManage等页面统一更新 4. **组件适配**:NotificationCenter等共享组件需支持所有主题色 **代码规范**: ```typescript // 主题色配置模板 const themeColors = { purchaser: { primary: 'amber-400', gradient: 'from-amber-400 to-orange-400', button: 'bg-amber-400 hover:bg-amber-500', text: 'text-amber-500', bg: 'bg-amber-50', border: 'border-amber-200', ring: 'focus:ring-amber-400' }, textile: { primary: 'emerald-400', gradient: 'from-emerald-400 to-teal-400', button: 'bg-emerald-400 hover:bg-emerald-500', text: 'text-emerald-500', bg: 'bg-emerald-50', border: 'border-emerald-200', ring: 'focus:ring-emerald-400' }, washing: { primary: 'violet-400', gradient: 'from-violet-400 to-fuchsia-400', button: 'bg-violet-400 hover:bg-violet-500', text: 'text-violet-500', bg: 'bg-violet-50', border: 'border-violet-200', ring: 'focus:ring-violet-400' } }; ``` **经验教训**: 1. **组件设计时考虑扩展性**:NotificationCenter等共享组件应预留主题色扩展接口 2. **变更同步机制**:主题色变更需同步更新所有相关页面(登录页、Dashboard、设置页等) 3. **类型安全**:TypeScript类型定义需与实际配置保持一致,避免运行时错误 4. **视觉回归测试**:主题色调整后需验证所有页面的视觉效果 --- ## 项目统计(截至2025-05-28) ### Bug分类统计 | 分类 | 数量 | 占比 | |------|------|------| | JavaScript/TypeScript作用域问题 | 1 | 4% | | 数据一致性问题 | 2 | 8% | | 空值处理问题 | 1 | 4% | | UI/UX问题 | 2 | 8% | | 代码组织问题 | 1 | 4% | | React组件生命周期问题 | 1 | 4% | | Supabase RLS策略问题 | 1 | 4% | | Webpack构建问题 | 2 | 8% | | TypeScript类型问题 | 2 | 8% | | 状态管理问题 | 1 | 4% | | 表单处理问题 | 1 | 4% | | CSS/Tailwind问题 | 1 | 4% | | 性能优化问题 | 1 | 4% | | 沙箱环境兼容性问题 | 1 | 4% | | 帮助系统集成问题 | 1 | 4% | | 计划拒绝状态管理问题 | 1 | 4% | | 通知中心主题色适配问题 | 1 | 4% | | 新手指引弹窗位置问题 | 1 | 4% | | 浏览器兼容性问题 | 1 | 4% | | 测试相关问题 | 1 | 4% | | 产品唯一性校验问题 | 1 | 4% | | 产品码/色号自动生成问题 | 1 | 4% | | NotificationCenter主题色缺失问题 | 1 | 4% | | 登录页角色颜色不一致问题 | 1 | 4% | | **总计** | **24** | **100%** | ### 技术债务分析 **高频问题类别**: 1. **UI/UX问题**(3个):主题色适配、弹窗位置、移动端适配 2. **TypeScript类型问题**(2个):类型定义不匹配、泛型约束 3. **Webpack构建问题**(2个):热更新失效、产物体积过大 **改进建议**: 1. 建立主题色设计系统,统一管理颜色变量 2. 完善TypeScript严格模式配置,提前发现类型问题 3. 优化Webpack配置,建立构建性能监控 4. 增加组件级单元测试,覆盖主题色渲染场景