82 KiB
采购计划系统 - 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
└── 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 | 创建时间 |
自定义枚举类型
-- 应用角色(公司类型)
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表 - 入库操作后自动刷新,无需手动刷新页面
- 组件卸载时自动取消订阅
- 使用 Supabase Realtime 订阅
文件变更
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.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.tsHook 未查询products表关联数据PlanGroup.tsx组件未展示 weight、yarn_ratios、production_price 等字段- 纺织厂
PlanOverview.tsx缺少展开详情视图
修复方案
- 数据层:在
usePlanData.ts中添加 products 表查询,获取 weight、image_url 等字段 - 组件层:在
PlanGroup.tsx中添加完整字段展示,包括:- 克重信息(经线/纬线/总克重)
- 纱线配比列表
- 生产采购价
- 产品图片
- 创建时间、计划开始时间、备注
- 页面层:纺织厂
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 图标:
<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 之间没有共享拒绝状态
- 页面刷新后拒绝状态丢失
修复方案
- 本地状态管理:在
PlanOverview.tsx中使用Set<string>跟踪被拒绝的计划ID - 持久化存储:使用 localStorage 存储拒绝状态,键名为
textile_rejected_plans - 跨页面同步:Dashboard 从 localStorage 读取拒绝状态并过滤最近计划
- 视觉反馈:被拒绝的计划显示灰色背景、"已拒绝"标签,并自动折叠
代码变更
src/pages/textile/PlanOverview.tsx- 添加 rejectedPlans 状态和 localStorage 持久化src/pages/textile/Dashboard.tsx- 添加 rejectedPlanIds 状态,过滤最近计划
关键代码
// 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')
修复后仍发现"各工厂库存"数据未正确与纺织厂数据同步。
根本原因
- 变量作用域问题:
planToFactoryName变量在代码块内部定义,但在后面的代码块中被访问,导致undefined错误 - 代码结构混乱:工厂信息查询逻辑分散在多个地方,导致数据流不清晰
- 工厂分组键值错误:按工厂分组库存时使用了错误的键值(
factory_id而非factory_name)
修复方案
- 提前获取工厂信息:将工厂信息查询(
plan_factories+companies)移到fetchData函数开头,确保后续所有逻辑都能访问 - 统一变量命名:避免重复定义相同用途的变量,使用清晰的命名区分不同阶段的映射关系
- 修复分组逻辑:使用工厂名称作为分组键,确保正确汇总各工厂库存
代码变更
- 文件:
src/pages/purchaser/WarehouseManage.tsx - 主要改动:
- 将工厂信息查询提前到数据获取阶段的开头
- 合并重复的查询逻辑,避免多次查询相同数据
- 修复
fabricCodeFactoryInventory的分组键使用 - 确保
planToFactoryName在整个函数作用域内可用
经验教训
- 块级作用域陷阱:JavaScript/TypeScript 的块级作用域容易导致变量访问问题,特别是在复杂的异步数据获取逻辑中
- 数据流清晰性:相关数据查询应该集中在一起,避免分散在代码各处
- 变量命名规范:相同用途的变量应该统一命名,避免重复定义造成混淆
2025-05-23 入库记录弹窗组件
功能描述
创建统一的入库记录弹窗组件 InventoryRecordsModal,用于在采购商和纺织厂页面展示完整的入库记录详情。
组件特性
- 统计摘要:显示总匹数和总米数
- 记录列表:按时间倒序显示所有入库记录
- 详细信息:包含日期时间、计划编号、工厂名称、批号、匹数、米数、来源
- 动画效果:使用 Framer Motion 实现平滑的弹窗动画
使用方式
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 块级作用域导致变量不可访问
修复方案:
- 将变量定义提前到函数作用域顶部
- 确保变量在使用前已定义
示例代码:
// 错误: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缺失
修复方案:
// 错误逻辑
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 不一致
修复步骤:
- 查询不一致的数据:
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);
- 为缺失记录的计划创建仓库(如果不存在)
- 补充缺失的入库记录
相关文件:数据库修复脚本
分类三:空值处理问题
Bug: NaN 显示问题
现象:弹窗中显示 "NaN 米"
根本原因:
- 入库记录中的
meters字段可能为undefined或null - 直接进行数学运算导致
NaN
修复方案:
// 错误
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.tsxsrc/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.tssrc/utils/helpers.tssrc/hooks/useInventoryData.tssrc/hooks/usePlanData.tssrc/components/warehouse/WarehouseNav.tsxsrc/components/warehouse/ProductForm.tsxsrc/components/warehouse/InboundForm.tsxsrc/components/warehouse/ProductList.tsxsrc/components/warehouse/InboundRecords.tsxsrc/components/plans/PlanGroup.tsxsrc/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 类型定义,包含所有数据库字段,同时保留前端展示用的可选扩展字段:
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)
新增状态:
const [stockHistoryModalOpen, setStockHistoryModalOpen] = useState(false);
const [selectedStockHistory, setSelectedStockHistory] = useState<ProductInRecord[]>([]);
新增处理函数:
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 定时器
- 事件监听器未移除
修复方案:
// 错误:未清理订阅
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)
- 策略中使用了错误的字段比较
修复方案:
-- 错误:仅检查 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未启用
修复方案:
// 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
- 首屏加载时间过长
- 代码未压缩
修复方案:
// 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 - 类型推断失败
- 第三方库类型定义缺失
修复方案:
// 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'
修复方案:
// 错误:泛型约束不明确
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: 状态更新异步导致的竞态条件
现象:
- 快速点击按钮时数据不一致
- 表单提交后状态未同步更新
- 多个组件间状态不同步
根本原因:
- 直接修改状态而非使用函数式更新
- 异步操作未正确处理依赖
- 缺少乐观更新
修复方案:
// 错误:直接依赖旧状态
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 无法确定是受控还是非受控组件
修复方案:
// 错误:初始值可能为 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: 表单验证时机问题
现象:
- 提交时才显示验证错误,用户体验差
- 实时验证过于频繁,性能问题
- 异步验证导致表单状态混乱
修复方案:
// 使用防抖进行实时验证
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 配置问题
修复方案:
// 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 的类名顺序问题
- 未理解移动优先原则
修复方案:
<!-- 错误:桌面优先 -->
<div class="hidden md:block sm:hidden">...</div>
<!-- 正确:移动优先 -->
<!-- 默认样式(移动端)→ sm(640px+)→ md(768px+) -->
<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 条时滚动卡顿
- 内存占用高
- 帧率下降
修复方案:
// 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 内容
- 浏览器预览无法加载
根本原因:
- 沙箱网络限制:
net.ipv4.conf.all.forwarding = 0(IP 转发被禁用) - Webpack devServer 配置问题:
host: '127.0.0.1'无法在沙箱中访问static配置指向不存在的public目录devMiddleware.writeToDisk与沙箱环境不兼容
- 与其他项目对比:Vite 在沙箱中可以正常工作,Webpack devServer 存在兼容性问题
修复方案:
// 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,使用默认行为
},
};
关键配置点:
- host: '0.0.0.0' - 必须绑定到所有接口,而不是 127.0.0.1
- 不要配置 static - 让 webpack 使用内存中的编译产物
- 不要配置 devMiddleware.writeToDisk - 沙箱中不需要写入磁盘
- 保持简单 - 避免复杂的 devServer 配置
验证方法:
# 1. 构建验证
pnpm run build
# 2. 检查 dist 目录
ls -la dist/
# 3. 启动开发服务器(沙箱中 curl 可能仍超时,但不影响实际功能)
pnpm run dev
替代方案:
- 使用
pnpm run build生成静态文件 - 在实际部署环境中测试预览功能
- 考虑迁移到 Vite(沙箱兼容性更好)
相关文件:
webpack.config.jspackage.json
分类十七:帮助系统集成问题
Bug: 账号管理页面缺少帮助系统入口
现象:
- 用户手册、用户反馈渠道、关于窗口在账号管理页面中看不到
- 帮助系统组件已创建但未在 MemberManage 页面集成
根本原因:
- MemberManage.tsx 未导入帮助系统组件
- 缺少状态管理控制弹窗显示
- 未在 UI 中添加入口按钮
修复方案:
// 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 之间没有共享拒绝状态
- 没有机制跟踪用户已查看的拒绝弹窗
修复方案:
// 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.tsxsrc/pages/textile/Dashboard.tsx
分类十九:通知中心主题色适配问题
Bug: 通知中心按钮在各工作台颜色不一致
现象:
- 采购商工作台通知按钮为白色,在白色背景下看不清
- 纺织厂和水洗厂工作台通知按钮颜色未适配主题色
- 按钮缺少明显的视觉层次
根本原因:
- 通知中心组件未接收主题色参数
- 各工作台页面未传递正确的主题色配置
- 默认样式使用白色背景,在浅色主题下不显眼
修复方案:
// 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.tsxsrc/pages/purchaser/Dashboard.tsxsrc/pages/textile/Dashboard.tsxsrc/pages/washing/Dashboard.tsx
分类二十:新手指引弹窗位置问题
Bug: 新手指引弹窗位置偏下,不在视口居中
现象:
- 纺织厂工作台的新手指引弹窗显示在页面底部
- 用户需要滚动才能看到完整内容
- 弹窗没有正确居中显示
根本原因:
- 使用了
items-start和pt-[10vh]试图向下偏移 - 但在小屏幕或内容较多时,弹窗会超出可视区域
- 没有使用真正的居中布局
修复方案:
// 错误:使用 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.tsxsrc/pages/purchaser/Dashboard.tsxsrc/pages/washing/Dashboard.tsx
学习反思与最佳实践
1. 环境差异意识
问题:在本地开发环境正常工作的配置,在沙箱环境中可能失效。 教训:
- 沙箱环境有严格的网络限制(IP 转发禁用、localhost 访问受限)
- 需要针对沙箱环境调整开发服务器配置
- 不要假设所有环境行为一致
2. 配置简化原则
问题:过度配置导致兼容性问题。 教训:
- 使用框架/工具的默认配置作为起点
- 只在必要时添加自定义配置
- 复杂的配置组合可能在特定环境中失效
3. 调试方法改进
问题:陷入反复修改配置的循环,没有定位真正原因。 教训:
- 先检查环境限制(
sysctl net.ipv4.conf.all.forwarding) - 对比正常项目和异常项目的差异
- 使用最小化配置验证(逐步添加配置项)
- 接受某些限制(如沙箱中 curl 无法访问)
4. 文档记录重要性
问题:花费大量时间排查已知问题。 教训:
- 及时记录环境特定的配置要求
- 建立常见问题知识库
- 记录失败尝试和成功方案
5. 技术选型考虑
问题:Webpack devServer 在沙箱中存在兼容性问题。 教训:
- 技术选型需要考虑目标运行环境
- Vite 在沙箱环境中兼容性更好
- 对于新项目,优先考虑沙箱友好的工具链
沙箱环境开发指南
已知限制
- 网络访问:localhost/127.0.0.1 访问受限
- IP 转发:
net.ipv4.conf.all.forwarding = 0 - 端口:仅 3015 可用
- 进程限制:某些命令执行超时
推荐配置
// webpack.config.js
devServer: {
port: 3015,
host: '0.0.0.0',
allowedHosts: 'all',
hot: true,
historyApiFallback: true,
}
验证流程
pnpm run typecheck- 类型检查pnpm run build- 构建验证ls -la dist/- 检查产物pnpm run dev- 启动服务器(沙箱中预览可能受限)
故障排查
- 检查端口占用:
lsof -i :3015 - 检查 IP 转发:
sysctl net.ipv4.conf.all.forwarding - 简化配置:移除 static/devMiddleware 等复杂配置
- 对比其他项目:参考正常工作的项目配置
分类十四:浏览器兼容性问题
Bug: 旧版浏览器不支持新特性
现象:
- 在 Safari 13 或 IE11 中白屏
- 某些 API 报错
xxx is not defined
修复方案:
// 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
修复方案:
// 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. 网络请求调试
// 在 supabase 客户端添加日志
const supabase = createClient(url, key, {
db: {
schema: 'public'
},
global: {
fetch: (...args) => {
console.log('Supabase Request:', args[0]);
return fetch(...args);
}
}
});
2. 性能分析
// 使用 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. 错误边界
// 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: 产品码和色号重复问题
现象:
- 可以创建相同产品码的产品
- 同一"成品名称-克重-颜色"组合下可以有重复色号
根本原因:
- 保存产品时未进行唯一性校验
- 自动生成产品码/色号时未查询数据库最大值
修复方案:
// 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)
- 未查询数据库中该前缀的实际最大值
修复方案:
// 产品码生成:查询数据库中该前缀的最大数字
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
修复方案:
// 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.tsxsrc/pages/washing/Dashboard.tsx
分类二十四:登录页角色颜色不一致问题
Bug: 登录页水洗厂颜色与Dashboard不一致
现象:
- 登录页水洗厂演示账号按钮使用
from-cyan-500 to-blue-600(蓝青色) - 水洗厂Dashboard使用
from-violet-400 to-fuchsia-400(紫罗兰色) - 同一角色在不同页面颜色不统一
根本原因:
- 登录页
demoAccounts配置未随Dashboard主题色更新而同步调整 - 缺乏统一的主题色管理规范
修复方案:
// 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 |
柔和紫粉 |
设计原则:
- 色阶统一:所有主题色使用
-400色阶,保持视觉一致性 - 渐变搭配:使用相邻色系的柔和渐变(如violet→fuchsia)
- 全站同步:登录页、Dashboard、MemberManage等页面统一更新
- 组件适配:NotificationCenter等共享组件需支持所有主题色
代码规范:
// 主题色配置模板
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'
}
};
经验教训:
- 组件设计时考虑扩展性:NotificationCenter等共享组件应预留主题色扩展接口
- 变更同步机制:主题色变更需同步更新所有相关页面(登录页、Dashboard、设置页等)
- 类型安全:TypeScript类型定义需与实际配置保持一致,避免运行时错误
- 视觉回归测试:主题色调整后需验证所有页面的视觉效果
项目统计(截至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% |
技术债务分析
高频问题类别:
- UI/UX问题(3个):主题色适配、弹窗位置、移动端适配
- TypeScript类型问题(2个):类型定义不匹配、泛型约束
- Webpack构建问题(2个):热更新失效、产物体积过大
改进建议:
- 建立主题色设计系统,统一管理颜色变量
- 完善TypeScript严格模式配置,提前发现类型问题
- 优化Webpack配置,建立构建性能监控
- 增加组件级单元测试,覆盖主题色渲染场景