iloom-flatten/AGENTS.md

2598 lines
83 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 采购计划系统 - 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 策略命名:`<scope>_<action>_<table>` 英文 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
<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 类型兼容性问题
#### 问题描述
添加图片功能后出现类型错误:
```
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<T, K>``Pick<T, K>` 可以明确表达类型意图
**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';
// 在页面中使用
<InventoryRecordsModal
isOpen={recordsModalOpen}
onClose={() => 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<string, string> = {};
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);
// 显示时
<span>{record.meters || record.quantity || 0} </span>
```
**相关文件**`src/components/InventoryRecordsModal.tsx`
---
### 分类四UI/UX 问题
#### Bug: 移动端表格横向溢出
**现象**:移动端表格内容超出屏幕,需要横向滚动
**根本原因**
- 使用 `<table>` 布局,列数过多
- 没有针对移动端做适配
**修复方案**
- 桌面端:保持表格布局 `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<ProductInRecord[]>([]);
```
**新增处理函数**
```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<HTMLButtonElement>) => { ... };
// 第三方库类型声明
// 创建 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<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
// 正确:添加约束
interface Props<T extends { id: string }> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
// 使用
interface User {
id: string;
name: string;
}
<List<User> items={users} renderItem={(user) => <span>{user.name}</span>} />
```
---
### 分类十:状态管理问题
#### 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<typeof state>) => {
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<string | undefined>();
// 正确:始终提供非 undefined 初始值
const [value, setValue] = useState('');
// 对象表单
const [form, setForm] = useState({
name: '',
email: '',
age: 0
});
// 处理可能为 null 的数据
const [user, setUser] = useState<User | null>(null);
// 渲染时
<input value={user?.name ?? ''} onChange={...} />
```
---
#### 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
<!-- 错误:桌面优先 -->
<div class="hidden md:block sm:hidden">...</div>
<!-- 正确:移动优先 -->
<!-- 默认样式(移动端)→ sm640px+)→ md768px+ -->
<div class="text-sm sm:text-base md:text-lg">
移动端小字 → 平板中字 → 桌面大字
</div>
<!-- 显示/隐藏 -->
<div class="hidden sm:block">
仅在大于 640px 时显示
</div>
<div class="sm:hidden">
仅在小于 640px 时显示
</div>
```
---
### 分类十三:性能优化问题
#### Bug: 大数据列表渲染卡顿
**现象**
- 列表超过 100 条时滚动卡顿
- 内存占用高
- 帧率下降
**修复方案**
```typescript
// 1. 虚拟列表
import { useVirtualizer } from '@tanstack/react-virtual';
const VirtualList = ({ items }) => {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: items.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
});
return (
<div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
<div style={{ height: `${virtualizer.getTotalSize()}px` }}>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${virtualItem.start}px)`
}}
>
{items[virtualItem.index]}
</div>
))}
</div>
</div>
);
};
// 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 <div>{item.name}</div>;
});
```
**相关文件**`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 中添加入口按钮(位于主账号卡片下方)
<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. 环境差异意识
**问题**:在本地开发环境正常工作的配置,在沙箱环境中可能失效。
**教训**
- 沙箱环境有严格的网络限制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: ['<rootDir>/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(<Component />);
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 <ErrorFallback error={this.state.error} />;
}
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<string> => {
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<string> => {
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. 增加组件级单元测试,覆盖主题色渲染场景