iloom-flatten/AGENTS.md

4359 lines
164 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 | 依赖管理 |
### 项目目录结构2026-06-04 更新)
```
/home/project/
├── AGENTS.md # 项目文档(本文件)
├── README.md # 项目说明文档
├── package.json # 项目依赖配置
├── webpack.config.js # Webpack 配置端口3015, host:0.0.0.0
├── tailwind.config.js # TailwindCSS 配置
├── postcss.config.js # PostCSS 配置
├── tsconfig.json # TypeScript 配置strict模式
├── index.html # HTML 模板
├── migrations/ # 数据库迁移文件100+个SQL文件
├── e2e/ # Playwright E2E测试
│ ├── auth.setup.ts
│ ├── core-workflow.spec.ts
│ └── collaboration-workflow.spec.ts
├── src/
│ ├── index.tsx # 应用入口
│ ├── App.tsx # 根组件25条HashRouter路由 + PageErrorBoundary
│ ├── styles/
│ │ └── index.css # 全局样式CSS变量体系 + 自定义工具类 + 移动端适配)
│ ├── types/
│ │ └── index.ts # 统一类型定义中心30+接口/类型)
│ ├── supabase/
│ │ ├── client.ts # Supabase 客户端(自动生成,禁止手动修改)
│ │ └── types.ts # 数据库类型(自动生成,禁止手动修改)
│ ├── contexts/
│ │ └── AuthContext.tsx # 认证上下文login/register/logout/selectRole
│ ├── config/
│ │ └── app.ts # 应用全局配置中心(环境/缓存/分页/动画/主题色/存储键等)
│ ├── utils/
│ │ ├── constants.ts # 公共常量状态枚举、UI映射、步骤配置从config/app导入并重新导出
│ │ ├── helpers.ts # 通用工具函数(日期格式化、数字解析、分组计算)
│ │ └── shareCrypto.ts # 分享链接加密HMAC-SHA256签名 + Base64
│ ├── hooks/ # 自定义Hooks10个
│ │ ├── useInventoryData.ts # 库存数据获取5秒缓存 + Realtime订阅
│ │ ├── usePlanData.ts # 计划数据获取10秒缓存 + 分页 + Realtime
│ │ ├── usePlanStatusSync.ts # 计划状态自动同步10秒测试间隔
│ │ ├── useRealtime.ts # Realtime单例管理器引用计数 + 自动清理)
│ │ ├── useResponsive.ts # 响应式断点检测sm/md/lg/xl
│ │ ├── useTheme.ts # 主题管理暗黑模式class策略
│ │ ├── usePerformance.ts # 性能检测(高/中/低三档动画降级)
│ │ ├── useNetworkStatus.ts # 网络状态监听
│ │ ├── useVirtualScroll.ts # 虚拟滚动Hook
│ │ └── useErrorHandler.ts # 统一错误处理Hook替代散落的console.error+alert
│ ├── components/ # 共享组件35个
│ │ ├── layout/ # 布局组件4个
│ │ │ ├── Sidebar.tsx # 可折叠侧边栏导航
│ │ │ ├── PurchaserLayout.tsx # 采购商布局8项导航 + amber主题
│ │ │ ├── TextileLayout.tsx # 纺织厂布局5项导航 + emerald主题
│ │ │ └── WashingLayout.tsx # 水洗厂布局6项导航 + violet主题
│ │ ├── RoleGuard.tsx # 路由守卫(角色权限校验 + 重定向)
│ │ ├── warehouse/ # 仓库管理组件6个
│ │ │ ├── WarehouseNav.tsx # 仓库类型导航
│ │ │ ├── ProductForm.tsx # 产品创建/编辑表单
│ │ │ ├── InboundForm.tsx # 入库表单
│ │ │ ├── OutboundForm.tsx # 出库表单
│ │ │ ├── ProductList.tsx # 产品列表(桌面表格+移动卡片)
│ │ │ └── InboundRecords.tsx # 入库记录列表
│ │ ├── plans/ # 计划管理组件3个
│ │ │ ├── PlanGroup.tsx # 工厂分组 + 计划卡片 + 关联工厂名称
│ │ │ ├── ShareModal.tsx # 分享链接弹窗(短链+长链+隐藏敏感信息)
│ │ │ └── PlanEditModal.tsx # 计划编辑弹窗
│ │ ├── PlanCard.tsx # 计划卡片(进度条+状态标签+流程节点)
│ │ ├── ProcessFlow.tsx # 生产流程节点(呼吸动画+弹窗确认)
│ │ ├── ProgressBar.tsx # 进度条组件
│ │ ├── StatusBadge.tsx # 状态标签组件
│ │ ├── InventoryRecordsModal.tsx # 入库记录弹窗(统计摘要+记录列表)
│ │ ├── YarnAllocationModal.tsx # 纱线分配弹窗(自动/手动模式)
│ │ ├── NotificationCenter.tsx # 通知中心支持amber/emerald/violet主题
│ │ ├── HelpCenter.tsx # 帮助中心F1快捷键
│ │ ├── UserManual.tsx # 用户手册(搜索功能)
│ │ ├── OnboardingGuide.tsx # 新手指引(首次登录弹窗)
│ │ ├── Tooltip.tsx # 工具提示(悬停显示)
│ │ ├── VersionUpdate.tsx # 版本更新检查 + AboutModal
│ │ ├── FeedbackModal.tsx # 用户反馈弹窗
│ │ ├── CrashReporter.tsx # 崩溃报告系统(全局错误监听)
│ │ ├── DemoDisclaimerModal.tsx # Demo免责声明
│ │ ├── DemoWatermark.tsx # Demo水印
│ │ ├── ComingSoonModal.tsx # 功能开发中提示
│ │ ├── PageTransition.tsx # 页面过渡动画fade+slide
│ │ ├── VirtualList.tsx # 虚拟列表组件
│ │ ├── ErrorBoundary.tsx # 错误边界
│ │ └── NetworkStatusBar.tsx # 网络状态栏
│ └── pages/ # 页面组件25个
│ ├── LoginPage.tsx # 登录页(演示账号+三角色主题色)
│ ├── RegisterPage.tsx # 注册页
│ ├── RoleSelectPage.tsx # 角色选择页
│ ├── MemberManage.tsx # 子账号管理 + 帮助系统入口
│ ├── ImportPlanPage.tsx # 分享链接导入(解密+确认/拒绝)
│ ├── DemoDataSharing.tsx # 数据共享演示
│ ├── purchaser/ # 采购商页面8个PurchaserLayout嵌套
│ │ ├── Dashboard.tsx # 首页(统计卡片+最近计划+快捷操作)
│ │ ├── PlanOverview.tsx # 纺织计划总览
│ │ ├── NewPlan.tsx # 新建纺织计划
│ │ ├── NewWashingPlan.tsx # 新建水洗计划
│ │ ├── WarehouseManage.tsx # 产品信息管理
│ │ ├── FinishedWarehouse.tsx # 成品仓库
│ │ ├── FactoryManage.tsx # 工厂管理
│ │ └── AccountsPayable.tsx # 应付账款
│ ├── textile/ # 纺织厂页面5个TextileLayout嵌套
│ │ ├── Dashboard.tsx # 首页(生产概览+快捷操作)
│ │ ├── PlanOverview.tsx # 接单与生产进度
│ │ ├── YarnWarehouse.tsx # 原料纱仓库(出入库记录)
│ │ ├── FabricWarehouse.tsx # 已生产坯布Realtime订阅
│ │ └── PaymentPending.tsx # 待结款
│ └── washing/ # 水洗厂页面6个扁平路由无Layout
│ ├── Dashboard.tsx # 首页violet主题
│ ├── PlanOverview.tsx # 水洗计划总览
│ ├── PendingFabric.tsx # 待水洗坯布
│ ├── CompletedFabric.tsx # 已完成水洗
│ ├── FinishedWarehouse.tsx # 成品仓库
│ └── PaymentPending.tsx # 待结款
└── skills/ # 技能文档
├── meoo-cloud/
├── mobile-dev/
└── react-project/
```
### 帮助系统组件2025-05-26
| 组件 | 功能 | 位置 |
|------|------|------|
| HelpCenter | 内置帮助中心F1快捷键打开 | 通知中心旁边 |
| UserManual | 用户手册,详细使用指南 | 账号管理中 |
| OnboardingGuide | 新手指引,首次登录显示 | 自动弹窗 |
| Tooltip | 工具提示,悬停显示说明 | 所有按钮 |
| VersionUpdate | 版本更新检查,仅弹一次 | 自动检测 |
| FeedbackModal | 用户反馈渠道 | 账号管理中 |
| CrashReporter | 崩溃报告系统 | 全局监听 |
| AboutModal | 关于窗口 | 账号管理中 |
### 多角色主题色系统2026-06-04 更新)
系统通过颜色编码强化角色认知,贯穿 Dashboard、Layout、通知中心及登录页。
| 角色 | 主色调 | Tailwind 渐变 | 通知中心背景 | 通知中心图标 | 心理暗示 |
|------|--------|---------------|-------------|-------------|---------|
| 采购商 | Amber | `from-amber-400 to-orange-500` | bg-amber-100 | text-amber-700 | 商业、交易、活力 |
| 纺织厂 | Emerald | `from-emerald-400 to-teal-400` | bg-emerald-100 | text-emerald-700 | 生产、原材料、稳定 |
| 水洗厂 | Violet | `from-violet-400 to-fuchsia-400` | bg-violet-100 | text-violet-500 | 工艺、后整理、精细 |
| 系统/公共 | Blue | `from-blue-500 to-blue-600` | - | - | 信任、科技、中立 |
### 前端配置架构2026-06-04 新增)
系统采用**双层配置架构**,将所有可配置项从散落的 hardcode 集中到统一配置中心:
```
src/config/app.ts ← 配置源头(环境感知、数值型配置、主题色、存储键)
↓ 导入
src/utils/constants.ts ← 向后兼容层状态枚举、UI映射、步骤配置 + 重新导出app.ts配置
↓ 导入
各业务文件 ← 消费方Hooks、页面、组件
```
#### 配置模块清单 (`src/config/app.ts`)
| 配置模块 | 导出常量 | 说明 |
|----------|---------|------|
| 环境配置 | `APP_ENV`, `IS_DEV`, `IS_PROD` | 通过 webpack DefinePlugin 注入 `__APP_ENV__` |
| 应用信息 | `APP_CONFIG` | 名称、版本号、Demo密码、免密登录有效期 |
| 演示账号 | `DEMO_ACCOUNTS`, `SHOW_DEMO_ACCOUNTS` | 三个角色的演示账号配置 |
| 缓存配置 | `CACHE_CONFIG` | 计划数据(10s)、库存数据(5s)、Dashboard(10s) |
| 分页配置 | `PAGINATION_CONFIG` | 默认页大小(20)、入库记录上限(100)、价格历史上限(50) |
| 同步配置 | `SYNC_CONFIG` | 计划状态同步间隔(开发10s/生产5min)、Realtime重连延迟 |
| 动画配置 | `ANIMATION_CONFIG` | 标准/快速/慢速过渡、弹簧动画、页面过渡、交错延迟 |
| 进度阈值 | `PROGRESS_THRESHOLDS` | 完成(97%)、低警告(30%)、中提示(60%) |
| 角色主题色 | `ROLE_THEMES`, `getRoleTheme()` | purchaser/textile/washing/system 四套完整主题色 |
| 文件上传 | `UPLOAD_CONFIG` | 图片大小限制(5MB)、允许类型、存储桶名称 |
| 分享链接 | `SHARE_CONFIG` | 链接有效期(30min)、短码长度(8) |
| 存储键 | `STORAGE_KEYS` | 所有 localStorage key 集中管理14个键 |
#### 状态枚举常量 (`src/utils/constants.ts`)
| 枚举常量 | 值 | 对应数据库枚举 |
|----------|-----|--------------|
| `PLAN_STATUS` | PENDING/PRODUCING/COMPLETED/REJECTED | plan_status |
| `STEP_STATUS` | PENDING/ACTIVE/COMPLETED/REJECTED | step_status |
| `FACTORY_TYPE` | TEXTILE/WASHING | factory_type |
| `WAREHOUSE_TYPE` | RAW_FABRIC/FABRIC/FINISHED/YARN | warehouse_type |
| `PAYMENT_STATUS` | PENDING/COMPLETED | payment_status |
| `SHARE_LINK_STATUS` | PENDING/CLICKED/CONFIRMED/REJECTED/EXPIRED/CANCELLED | share_links.status |
| `WASHING_PLAN_STATUS` | PENDING/IN_PROGRESS/COMPLETED | washing_plans.status |
| `INVENTORY_RECORD_TYPE` | IN/OUT | yarn_stock_records.record_type |
| `OUTBOUND_TYPE` | MANUAL/AUTO | product_outbound_records.outbound_type |
#### 使用规范
1. **新增配置项**:统一添加到 `src/config/app.ts`,不要在业务文件中硬编码
2. **状态字符串**:使用 `PLAN_STATUS.PENDING` 代替 `'pending'`,避免拼写错误
3. **localStorage**:使用 `STORAGE_KEYS.savedUsername` 代替 `'saved_username'`
4. **缓存时间**:使用 `CACHE_CONFIG.planDataDuration` 代替 `10000`
5. **向后兼容**`constants.ts` 重新导出 `animationConfig`/`paginationConfig`/`progressThresholds`,旧代码无需修改
### 设计架构规范2026-06-04 新增)
#### CSS 变量体系 (`src/styles/index.css`)
- **色阶系统**: 完整的 50-900 色阶定义Primary, Success, Warning, Error, Gray
- **阴影层级**: 6级阴影系统 (`--shadow-sm` ~ `--shadow-2xl`)
- **自定义工具类**: `.glass`/`.glass-dark`(毛玻璃)、`.scrollbar-thin`/`.scrollbar-hide`(滚动条美化)、`.animate-fade-in`/`.animate-slide-in`(入场动画)、`.safe-area-top/bottom`(iOS安全区)
#### 动效规范 (Framer Motion + constants.ts)
- **页面过渡**: `opacity: 0→1`, `y: 20→0`, `scale: 0.98→1` (Duration: 0.35s)
- **列表交错**: `staggerChildren: 0.08`
- **交互反馈**: Spring 弹簧动画 (`stiffness: 200, damping: 25`)
- **性能降级**: `usePerformance` Hook 自动检测,低性能设备仅使用 opacity 过渡
- **硬件加速**: `will-change-transform` + `content-visibility-auto`
#### 响应式设计
- **断点**: `sm(640px)`, `md(768px)`, `lg(1024px)`, `xl(1280px)`
- **移动优先**: 默认样式为移动端,通过 `sm:/md:/lg:` 向上覆盖
- **触控优化**: 最小触控区域 44x44px (`@media (pointer: coarse)`)
- **布局切换**: 桌面端表格 ↔ 移动端卡片,根据屏幕宽度自动切换
#### 布局架构差异
| 角色 | 布局组件 | 导航项数 | 路由嵌套 |
|------|---------|---------|---------|
| 采购商 | PurchaserLayout | 8项 | 嵌套 Outlet |
| 纺织厂 | TextileLayout | 5项 | 嵌套 Outlet |
| 水洗厂 | WashingLayout | 6项 | 嵌套 Outlet |
## 路由结构
| URL 路径 | 页面 | 说明 |
|----------|------|------|
| `/login` | LoginPage | 登录页 |
| `/register` | RegisterPage | 注册页 |
| `/role-select` | RoleSelectPage | 角色选择页 |
| `/purchaser` | PurchaserDashboard | 采购商首页 |
| `/purchaser/plans` | PlanOverview | 采购商计划总览 |
| `/purchaser/plans/new` | NewPlan | 新建纺织计划 |
| `/purchaser/washing-plans/new` | NewWashingPlan | 新建水洗计划 |
| `/purchaser/warehouse` | WarehouseManage | 产品信息(仓库管理) |
| `/purchaser/finished-warehouse` | FinishedWarehouse | 成品仓库 |
| `/purchaser/factories` | FactoryManage | 工厂管理 |
| `/purchaser/accounts-payable` | AccountsPayable | 应付账款 |
| `/textile` | TextileDashboard | 纺织厂首页 |
| `/textile/plans` | TextilePlanOverview | 纺织厂计划总览 |
| `/textile/yarn-warehouse` | YarnWarehouse | 原料纱仓库 |
| `/textile/fabric-warehouse` | FabricWarehouse | 已生产坯布 |
| `/textile/payments` | PaymentPending | 待结款 |
| `/washing` | WashingDashboard | 水洗厂首页 |
| `/washing/plans` | WashingPlanOverview | 水洗厂计划总览 |
| `/washing/pending` | PendingFabric | 待水洗坯布 |
| `/washing/completed` | CompletedFabric | 已完成坯布 |
| `/washing/finished-warehouse` | FinishedWarehouse | 成品仓库 |
| `/washing/payments` | WashingPaymentPending | 待结款 |
| `/members` | MemberManage | 子账号管理 |
| `/import` | ImportPlanPage | 通过分享链接导入关联计划 |
| `/demo/data-sharing` | DemoDataSharing | 数据共享演示 |
## 数据安全
### 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
## 数据架构2026-06-04 更新)
### 数据库概览
**31张业务表**按功能分为6大模块
| 模块 | 表数量 | 核心表 |
|------|--------|--------|
| 用户与权限 | 4 | companies, profiles, company_members, company_relationships |
| 生产计划 | 5 | production_plans, plan_factories, yarn_ratios, plan_process_steps, washing_plans |
| 库存管理 | 8 | products, warehouses, inventory_records, product_inventory_records, yarn_stock, yarn_stock_records, finished_products, finished_product_inventory_records |
| 财务结算 | 5 | payments, accounts_payable, accounts_payable_items, production_price_history, product_price_history |
| 分享与协作 | 2 | share_links, notifications |
| 系统辅助 | 7 | audit_logs, user_feedback, crash_reports, product_yarn_ratios, product_outbound_records, washing_plan_completions, washing_process_steps |
### ER 关系图
```
auth.users (Supabase内置)
├── 1:1 ── profiles (用户配置, master_id支持子账号)
│ │
│ ├── N:1 ── companies (公司, role:purchaser/textile/washing)
│ │ │
│ │ ├── 1:N ── company_members (公司成员)
│ │ │
│ │ ├── 1:N ── warehouses (仓库)
│ │ │ └── 1:N ── inventory_records / yarn_stock
│ │ │
│ │ ├── 1:N ── products (产品信息)
│ │ │ ├── 1:N ── product_inventory_records
│ │ │ ├── 1:N ── product_yarn_ratios
│ │ │ └── 1:N ── product_price_history
│ │ │
│ │ └── N:M ── company_relationships (合作关系)
│ │
│ ├── 1:N ── production_plans (生产计划)
│ │ ├── 1:N ── plan_factories (关联工厂)
│ │ ├── 1:N ── yarn_ratios (纱线配比)
│ │ ├── 1:N ── plan_process_steps (流程节点)
│ │ ├── 1:N ── inventory_records (入库记录)
│ │ ├── 1:N ── payments (结款)
│ │ └── 1:N ── production_price_history
│ │
│ └── 1:N ── washing_plans (水洗计划)
│ ├── 1:N ── washing_process_steps
│ ├── 1:N ── washing_plan_completions
│ └── 1:N ── finished_products
```
### 表结构详细设计31张表
#### 一、用户与权限模块
**companies** - 公司表 (12条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| name | TEXT NOT NULL | 公司名称 |
| role | app_role NOT NULL | purchaser/textile/washing |
| address | TEXT | 公司地址 |
| contact_phone | TEXT | 联系电话 |
| created_at | TIMESTAMPTZ | now() |
**profiles** - 用户配置表 (8条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | 关联 auth.users.id |
| username | TEXT NOT NULL UNIQUE | 用户名 |
| phone | TEXT | 手机号 |
| company_id | UUID FK | 所属公司 |
| is_master | BOOLEAN NOT NULL | 是否主账号默认true |
| master_id | UUID FK | 主账号ID子账号使用 |
| display_name | TEXT | 显示名称 |
| image_url | TEXT | 头像URL |
| created_at | TIMESTAMPTZ | now() |
**company_members** - 公司成员表 (3条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| company_id | UUID FK NOT NULL | 公司ID |
| user_id | UUID FK NOT NULL | 用户ID |
| role | TEXT NOT NULL | 成员角色,默认'member' |
| created_at | TIMESTAMPTZ | now() |
**company_relationships** - 公司合作关系表 (2条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| purchaser_company_id | UUID FK NOT NULL | 采购商公司ID |
| factory_company_id | UUID FK NOT NULL | 工厂公司ID |
| factory_type | factory_type NOT NULL | textile/washing |
| status | TEXT NOT NULL | active/inactive默认'active' |
| created_at / updated_at | TIMESTAMPTZ | 时间戳 |
#### 二、生产计划模块
**production_plans** - 生产计划表 (3条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| plan_code | TEXT NOT NULL UNIQUE | 计划编号 |
| product_name | TEXT NOT NULL | 成品名称 |
| color | TEXT NOT NULL | 颜色 |
| fabric_code | TEXT NOT NULL | 坯布识别码 |
| color_code | TEXT NOT NULL | 色号,默认'01' |
| remark | TEXT | 备注 |
| purchaser_id | UUID FK NOT NULL | 采购商公司ID |
| target_quantity | INTEGER NOT NULL | 计划产量(米)默认0 |
| completed_quantity | INTEGER NOT NULL | 已完成产量(米)默认0 |
| yarn_usage_per_meter | NUMERIC | 每米纱用量(g/m)默认0 |
| production_price | NUMERIC | 生产采购价(元/米)默认0 |
| status | plan_status NOT NULL | pending/producing/completed |
| start_time | TIMESTAMPTZ | 计划开始时间 |
| created_by | UUID FK | 创建人 |
| created_at / updated_at | TIMESTAMPTZ | now() |
**plan_factories** - 计划工厂关联表 (3条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| plan_id | UUID FK NOT NULL | 计划ID |
| factory_id | UUID FK NOT NULL | 工厂公司ID |
| factory_type | factory_type NOT NULL | textile/washing |
| created_at | TIMESTAMPTZ | now() |
**yarn_ratios** - 纱线配比表 (2条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| plan_id | UUID FK NOT NULL | 计划ID |
| yarn_name | TEXT NOT NULL | 纱线名称 |
| ratio | NUMERIC NOT NULL | 配比比例默认1 |
| amount_per_meter | NUMERIC NOT NULL | 每米用量(g/m) |
| total_amount | NUMERIC NOT NULL | 总用纱量 |
| company_id | UUID FK | 公司ID |
| yarn_type | TEXT | 纱线类型,默认'all' |
| created_at | TIMESTAMPTZ | now() |
**plan_process_steps** - 计划流程节点表 (15条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| plan_id | UUID FK NOT NULL | 计划ID |
| step_type | step_type NOT NULL | confirm/yarn_purchase/dyeing/machine_start/fabric_warehouse |
| status | step_status NOT NULL | pending/active/completed/rejected |
| timestamp | TIMESTAMPTZ | 确认时间戳 |
| operator_id | UUID FK | 操作人 |
| notes | TEXT | 备注 |
| company_id | UUID FK | 公司ID |
| created_at | TIMESTAMPTZ | now() |
**washing_plans** - 水洗计划表 (1条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| plan_code | TEXT NOT NULL | 水洗计划编号 |
| company_id | UUID FK NOT NULL | 采购商公司ID |
| product_id | UUID FK NOT NULL | 产品ID |
| washing_factory_id | UUID FK | 水洗厂公司ID |
| planned_meters | NUMERIC NOT NULL | 计划水洗米数 |
| washing_price | NUMERIC NOT NULL | 水洗单价(元/米) |
| estimated_shrinkage_rate | NUMERIC NOT NULL | 预估缩水率(%) |
| estimated_washed_meters | NUMERIC | 预估洗后米数(自动计算) |
| estimated_total_cost | NUMERIC | 预估总费用(自动计算) |
| actual_shrinkage_rate | NUMERIC | 实际缩水率(%) |
| actual_washed_meters | NUMERIC | 实际洗后米数 |
| actual_total_cost | NUMERIC | 实际总费用 |
| washing_date | DATE | 水洗日期 |
| status | TEXT NOT NULL | pending/in_progress/completed |
| notes | TEXT | 备注 |
| created_by | UUID FK | 创建人 |
| created_at / updated_at | TIMESTAMPTZ | now() |
#### 三、库存管理模块
**products** - 产品信息表 (9条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| company_id | UUID FK NOT NULL | 所属公司 |
| product_name | TEXT NOT NULL | 产品名称 |
| weight | NUMERIC NOT NULL | 克重(g) |
| color | TEXT NOT NULL | 颜色 |
| fabric_code | TEXT NOT NULL | 坯布码 |
| color_code | TEXT NOT NULL | 色号,默认'01' |
| yarn_usage_per_meter | NUMERIC NOT NULL | 每米纱用量 |
| yarn_types | TEXT[] | 纱线类型数组 |
| yarn_ratios | NUMERIC[] | 纱线配比数组 |
| raw_fabric_rolls | INTEGER NOT NULL | 坯布匹数 |
| raw_fabric_meters | NUMERIC NOT NULL | 坯布米数 |
| total_stock | NUMERIC NOT NULL | 总库存 |
| production_price | NUMERIC | 生产价格 |
| image_url | TEXT | 产品图片URL |
| warp_weight | NUMERIC | 经线克重 |
| weft_weight | NUMERIC | 纬线克重 |
| total_weight | NUMERIC | 总克重 |
| created_at / updated_at | TIMESTAMPTZ | now() |
**warehouses** - 仓库表 (5条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| company_id | UUID FK NOT NULL | 所属公司 |
| name | TEXT NOT NULL | 仓库名称 |
| type | warehouse_type NOT NULL | raw_fabric/fabric/finished/yarn |
| location | TEXT | 仓库位置 |
| created_at | TIMESTAMPTZ | now() |
**inventory_records** - 入库记录表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| plan_id | UUID FK NOT NULL | 关联计划ID |
| warehouse_id | UUID FK NOT NULL | 仓库ID |
| quantity | NUMERIC NOT NULL | 入库米数 |
| rolls | INTEGER | 入库匹数 |
| warehouse_location | TEXT | 库位 |
| operator_id | UUID FK | 操作人 |
| company_id | UUID FK | 公司ID |
| price_per_meter | NUMERIC | 单价 |
| price_note | TEXT | 价格备注 |
| created_at | TIMESTAMPTZ | now() |
**product_inventory_records** - 产品出入库记录表 (4条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| product_id | UUID FK NOT NULL | 产品ID |
| warehouse_id | UUID FK | 仓库ID |
| batch_no | TEXT NOT NULL | 批号 |
| rolls | INTEGER NOT NULL | 匹数 |
| meters | NUMERIC NOT NULL | 米数 |
| operator_id | UUID FK | 操作人 |
| notes | TEXT | 备注 |
| company_id | UUID FK | 公司ID |
| created_at | TIMESTAMPTZ | now() |
**yarn_stock** - 纱线库存表 (4条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| company_id | UUID FK NOT NULL | 所属公司 |
| warehouse_id | UUID FK | 仓库ID |
| name | TEXT NOT NULL | 纱线名称 |
| spec | TEXT | 规格 |
| quantity | NUMERIC NOT NULL | 库存数量(kg) |
| min_stock | NUMERIC NOT NULL | 最低库存预警(kg) |
| unit | TEXT NOT NULL | 单位,默认'kg' |
| updated_at | TIMESTAMPTZ | now() |
**yarn_stock_records** - 纱线出入库记录表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| company_id | UUID FK NOT NULL | 公司ID |
| yarn_stock_id | UUID FK | 纱线库存ID |
| plan_id | UUID FK | 关联计划ID |
| record_type | TEXT NOT NULL | in/out |
| quantity | NUMERIC NOT NULL | 数量(kg) |
| unit | TEXT NOT NULL | 单位,默认'kg' |
| batch_no | TEXT | 批号 |
| notes | TEXT | 备注 |
| operator_id | UUID FK | 操作人 |
| created_at | TIMESTAMPTZ | now() |
**finished_products** - 成品表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| company_id | UUID FK NOT NULL | 所属公司 |
| washing_plan_id | UUID FK | 水洗计划ID |
| product_id | UUID FK | 产品ID |
| product_name / color / fabric_code / color_code | TEXT | 产品信息 |
| weight | NUMERIC | 克重 |
| washed_meters | NUMERIC NOT NULL | 洗后米数 |
| shrinkage_rate | NUMERIC NOT NULL | 缩水率 |
| rolls | INTEGER | 匹数 |
| stock_meters / stock_rolls | NUMERIC/INTEGER | 库存 |
| warehouse_id / warehouse_location | UUID/TEXT | 仓库信息 |
| washing_cost / unit_cost | NUMERIC | 成本 |
| status | TEXT NOT NULL | in_stock/out_of_stock |
| created_at / updated_at | TIMESTAMPTZ | now() |
**finished_product_inventory_records** - 成品出入库记录表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| company_id | UUID FK NOT NULL | 公司ID |
| finished_product_id | UUID FK NOT NULL | 成品ID |
| record_type | TEXT NOT NULL | in/out |
| rolls / meters | INTEGER/NUMERIC | 数量 |
| washing_completion_id | UUID FK | 水洗完成记录ID |
| related_order_id / related_order_type | UUID/TEXT | 关联订单 |
| warehouse_id / warehouse_location | UUID/TEXT | 仓库信息 |
| batch_no / notes | TEXT | 批号/备注 |
| operator_id | UUID FK | 操作人 |
| created_at | TIMESTAMPTZ | now() |
#### 四、财务结算模块
**payments** - 结款记录表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| plan_id | UUID FK NOT NULL | 关联计划ID |
| from_company_id | UUID FK NOT NULL | 付款方 |
| to_company_id | UUID FK NOT NULL | 收款方 |
| amount | NUMERIC NOT NULL | 结款金额 |
| quantity | NUMERIC NOT NULL | 结款数量(米) |
| price_per_meter | NUMERIC NOT NULL | 单价 |
| status | payment_status NOT NULL | pending/completed |
| paid_at | TIMESTAMPTZ | 结款时间 |
| created_at | TIMESTAMPTZ | now() |
**accounts_payable** - 应付账款表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| plan_id | UUID FK NOT NULL | 计划ID |
| purchaser_id | UUID FK NOT NULL | 采购商ID |
| textile_factory_id | UUID FK NOT NULL | 纺织厂ID |
| total_amount / paid_amount / unpaid_amount | NUMERIC | 金额统计 |
| total_quantity / paid_quantity / unpaid_quantity | NUMERIC | 数量统计 |
| price_per_meter | NUMERIC NOT NULL | 单价 |
| status | TEXT NOT NULL | unpaid/partial/paid |
| created_at / updated_at | TIMESTAMPTZ | now() |
**accounts_payable_items** - 应付账款明细表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| accounts_payable_id | UUID FK NOT NULL | 应付账款ID |
| inventory_record_id | UUID FK | 入库记录ID |
| quantity / rolls / price_per_meter / amount | NUMERIC/INTEGER | 明细数据 |
| status | TEXT NOT NULL | unpaid/paid |
| payment_id | UUID FK | 关联结款ID |
| created_at / paid_at | TIMESTAMPTZ | 时间戳 |
**production_price_history** - 生产价格变更历史 (3条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| plan_id | UUID FK NOT NULL | 计划ID |
| old_price / new_price / change_amount | NUMERIC NOT NULL | 价格变更 |
| operator_id / operator_name | UUID/TEXT | 操作人 |
| change_reason | TEXT | 变更原因 |
| created_at | TIMESTAMPTZ | now() |
**product_price_history** - 产品价格变更历史 (3条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| product_id | UUID FK NOT NULL | 产品ID |
| company_id | UUID FK NOT NULL | 公司ID |
| old_price / new_price / change_amount | NUMERIC NOT NULL | 价格变更 |
| operator_id / operator_name | UUID/TEXT | 操作人 |
| change_reason | TEXT | 变更原因 |
| created_at | TIMESTAMPTZ | now() |
#### 五、分享与协作模块
**share_links** - 分享链接表 (2条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| short_code | TEXT NOT NULL | 短码 |
| plan_id | UUID FK NOT NULL | 计划ID |
| factory_type | TEXT NOT NULL | textile/washing |
| created_by | UUID FK NOT NULL | 创建人 |
| expires_at | TIMESTAMPTZ | 过期时间 |
| click_count | INTEGER | 点击次数默认0 |
| hide_sensitive | BOOLEAN | 隐藏敏感信息默认false |
| used_at | TIMESTAMPTZ | 使用时间 |
| status | TEXT NOT NULL | pending/clicked/confirmed/rejected/expired/cancelled |
| clicked_at / responded_at | TIMESTAMPTZ | 时间戳 |
| response_result | TEXT | confirmed/rejected |
| reject_reason | TEXT | 拒绝原因 |
| created_at | TIMESTAMPTZ | now() |
**notifications** - 通知表 (4条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| recipient_id | UUID FK NOT NULL | 接收人 |
| sender_id / sender_company_id | UUID FK | 发送人/公司 |
| plan_id | UUID FK | 关联计划 |
| type | TEXT NOT NULL | 通知类型 |
| title / content | TEXT NOT NULL | 标题/内容 |
| is_read | BOOLEAN | 已读标记默认false |
| created_at / read_at | TIMESTAMPTZ | 时间戳 |
#### 六、系统辅助模块
**audit_logs** - 审计日志表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| user_id / company_id | UUID FK | 用户/公司 |
| action | TEXT NOT NULL | 操作类型 |
| resource_type / resource_id | TEXT/UUID | 资源类型/ID |
| details | JSONB | 详细信息 |
| ip_address / user_agent | TEXT | 请求信息 |
| created_at | TIMESTAMPTZ | now() |
**user_feedback** - 用户反馈表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| type | TEXT NOT NULL | 反馈类型 |
| content | TEXT NOT NULL | 反馈内容 |
| contact | TEXT | 联系方式 |
| user_id | UUID FK | 用户ID |
| status | TEXT NOT NULL | pending/resolved |
| created_at / updated_at | TIMESTAMPTZ | now() |
**crash_reports** - 崩溃报告表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| error_message | TEXT NOT NULL | 错误信息 |
| error_stack / component_stack | TEXT | 堆栈信息 |
| user_description | TEXT | 用户描述 |
| user_agent / url | TEXT | 环境信息 |
| user_id | UUID FK | 用户ID |
| status | TEXT NOT NULL | pending/resolved |
| created_at / updated_at | TIMESTAMPTZ | now() |
**product_yarn_ratios** - 产品纱线配比表 (13条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| product_id | UUID FK NOT NULL | 产品ID |
| yarn_name | TEXT NOT NULL | 纱线名称 |
| ratio | NUMERIC NOT NULL | 配比 |
| yarn_type | TEXT | 纱线类型,默认'all' |
| created_at | TIMESTAMPTZ | now() |
**product_outbound_records** - 产品出库记录表 (1条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| product_id | UUID FK NOT NULL | 产品ID |
| company_id | UUID FK | 公司ID |
| batch_no | TEXT NOT NULL | 批号 |
| rolls / meters | INTEGER/NUMERIC | 数量 |
| washing_plan_id | UUID FK | 水洗计划ID |
| outbound_type | TEXT NOT NULL | manual/auto |
| source_batch_id | UUID FK | 源批次ID |
| recipient_company_id / recipient_company_name | UUID/TEXT | 接收方 |
| notes / operator_id | TEXT/UUID | 备注/操作人 |
| created_at | TIMESTAMPTZ | now() |
**washing_plan_completions** - 水洗完成记录表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| company_id | UUID FK NOT NULL | 公司ID |
| washing_plan_id | UUID FK NOT NULL | 水洗计划ID |
| actual_washed_meters / actual_shrinkage_rate | NUMERIC NOT NULL | 实际数据 |
| rolls | INTEGER NOT NULL | 匹数 |
| washing_date | DATE NOT NULL | 水洗日期 |
| actual_washing_cost / unit_cost | NUMERIC | 成本 |
| warehouse_id / warehouse_location | UUID/TEXT | 仓库 |
| auto_imported | BOOLEAN | 自动导入标记 |
| finished_product_id | UUID FK | 成品ID |
| completed_by / completed_at | UUID/TIMESTAMPTZ | 完成人/时间 |
| created_at | TIMESTAMPTZ | now() |
**washing_process_steps** - 水洗流程节点表 (0条)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | UUID PK | gen_random_uuid() |
| washing_plan_id | UUID FK NOT NULL | 水洗计划ID |
| company_id | UUID FK NOT NULL | 公司ID |
| step_type | TEXT NOT NULL | 步骤类型 |
| status | TEXT NOT NULL | pending/completed |
| timestamp | TIMESTAMPTZ | 确认时间 |
| operator_id | UUID FK | 操作人 |
| notes | TEXT | 备注 |
| created_at / updated_at | TIMESTAMPTZ | now() |
### 自定义枚举类型
```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', 'rejected');
-- 仓库类型
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
## 更新日志
### 2026-06-04 (v2.2.0) - P1 共享组件抽象
#### AccountProfileCard 替换3个Dashboard内联账户弹窗
- `purchaser/Dashboard.tsx`~140行内联弹窗 → `<AccountProfileCard role="purchaser" />`
- `textile/Dashboard.tsx`~150行内联弹窗 → `<AccountProfileCard role="textile" />`
- `washing/Dashboard.tsx`~150行内联弹窗 → `<AccountProfileCard role="washing" />`
- 共减少约440行重复代码头像上传功能随组件一并收敛
- 清理三个文件中不再使用的图标 importX, BookOpen, MessageSquare, Info, Camera 等)
#### ImageUploader 任务合并完成
- 原计划独立抽象 ImageUploader 组件,实际头像上传已随 AccountProfileCard 替换一并移除
- pages 目录中已无内联头像上传代码残留
#### 文件变更清单
| 操作 | 文件 | 说明 |
|------|------|------|
| 修改 | `src/pages/purchaser/Dashboard.tsx` | AccountProfileCard替换内联弹窗 + 清理import |
| 修改 | `src/pages/textile/Dashboard.tsx` | AccountProfileCard替换内联弹窗 + 清理import |
| 修改 | `src/pages/washing/Dashboard.tsx` | AccountProfileCard替换内联弹窗 + 清理import |
### 2026-06-04 (v2.1.0) - 架构优化与代码治理
#### Phase 1: 路由守卫
- **新增 RoleGuard 组件** (`src/components/RoleGuard.tsx`)
- 基于 AuthContext 的角色权限校验,未登录重定向 `/login`,角色不匹配重定向对应首页
- 支持 `allowedRoles` 数组配置,灵活控制路由访问权限
- **App.tsx 路由重构**
- 采购商/纺织厂/水洗厂路由组均包裹 RoleGuard防止越权访问
- `/members` 路由对三个角色开放
- 公共路由(/login, /register, /import 等)不受守卫限制
#### Phase 2: 数据层优化
- **extractParamsFromLink 去重**
- 统一至 `src/utils/helpers.ts`,补充缺失的 `shortCode` 字段
- 移除 `textile/PlanOverview.tsx``washing/PlanOverview.tsx` 中的本地重复实现共47行
- **新增 useErrorHandler Hook** (`src/hooks/useErrorHandler.ts`)
- 统一错误处理模式:结构化日志 + 可选用户提示 + 上下文信息
- 替代散落在各页面的 `console.error` + `alert` 组合
#### Phase 3: 业务组件抽象
- **新增 WashingLayout 组件** (`src/components/layout/WashingLayout.tsx`)
- 解决已知架构债务:水洗厂此前缺少独立 Layout导航内嵌在各页面中
- violet/fuchsia 主题色6项导航桌面侧边栏 + 移动端底部导航
- 三个角色布局架构完全统一RoleGuard + Layout + Outlet 嵌套模式)
- **App.tsx 水洗厂路由重构**
- 从扁平路由改为 WashingLayout 嵌套结构,与采购商/纺织厂保持一致
- Dashboard 使用 `index` 路由,子路由使用相对路径
- **实时订阅审计**
- 确认 pages 目录无直接 `supabase.channel` 调用,已全部收敛至 hooks
#### Phase 4: 代码治理
- **usePlanData 闭包陷阱修复** (`src/hooks/usePlanData.ts`)
- `fetchData``setState` 从直接引用 `state.plans` 改为函数式更新 `setState(prev => ...)`
- 消除 append 模式下因闭包捕获过时 state 导致的数据丢失风险
- **useInventoryData any 类型治理** (`src/hooks/useInventoryData.ts`)
- 18处 `any` 全部替换为精确类型Supabase `Tables<>` + 局部接口)
- 新增类型别名:`InventoryRecord`, `ProductInventoryRecord`, `ProductOutboundRecord`
- 新增局部接口:`PlanSummary`, `PlanFactoryRef`, `CompanyRef`(部分字段查询)
- `processInventoryData` 辅助函数参数接口完全类型化
#### 文件变更清单
| 操作 | 文件 | 说明 |
|------|------|------|
| 新增 | `src/components/RoleGuard.tsx` | 路由守卫组件 |
| 新增 | `src/components/layout/WashingLayout.tsx` | 水洗厂布局组件 |
| 新增 | `src/hooks/useErrorHandler.ts` | 统一错误处理Hook |
| 修改 | `src/App.tsx` | RoleGuard集成 + WashingLayout嵌套路由 |
| 修改 | `src/hooks/usePlanData.ts` | 闭包陷阱修复函数式setState |
| 修改 | `src/hooks/useInventoryData.ts` | 18处any类型治理 |
| 修改 | `src/utils/helpers.ts` | extractParamsFromLink补充shortCode |
| 修改 | `src/pages/textile/PlanOverview.tsx` | 移除本地extractParamsFromLink |
| 修改 | `src/pages/washing/PlanOverview.tsx` | 移除本地extractParamsFromLink |
### 2026-06-04 (v2.0.0) - 重大版本更新
- **系统审计与质量保障**
- 完成四维度系统审计(文件关联/内存安全/路由配置/权限授权),全部通过
- 新增 Bug 快速排查检查单12大类90+检查项覆盖页面白屏、数据读写、状态同步、内存性能、样式UI、认证权限、分享链接、构建部署等全部问题类型
- 新增数据库诊断常用 SQL7条和高频 Bug 速查索引14条
- 修复数据完整性问题:清理 share_links 孤儿记录、修复计划状态不一致、标记过期链接
- **文档体系完善**
- 完善数据库31张表完整文档按6大模块组织用户权限/生产计划/库存管理/财务结算/分享协作/系统辅助)
- 新增设计架构规范CSS变量体系/动效规范/响应式设计/布局架构差异)
- 新增多角色主题色系统完整文档
- API 调用示例扩展Realtime订阅/分享链接加密解密)
- **测试体系建设**
- 新增 E2E 全流程测试文件 `e2e/full-workflow.spec.ts`
- 完成采购商下单→分享链接→纺织厂确认→生产流程→分批次入库的端到端模拟测试
- 验证数据一致性completed_quantity = SUM(inventory_records.quantity)
- **代码质量**
- usePlanStatusSync 测试间隔标注10秒测试用生产环境需改回5分钟
- 项目目录结构文档更新至最新状态35组件/25页面/10 Hooks
### 2025-06-02 (v1.0.36)
- **水洗厂功能完善**
- 新增已完成坯布仓库页面,展示水洗完成的坯布库存
- 新增待水洗坯布页面,展示待水洗的坯布列表
- 新增水洗厂计划总览页面,统一管理水洗计划
- 新增水洗厂结款页面,支持查看和确认水洗费用
- **采购商功能增强**
- 新增成品仓库页面,管理已完成水洗的成品
- 新增应付账款页面,查看各工厂的应付费用
- **组件优化**
- 新增 YarnAllocationModal 组件,优化纱线分配流程
- 新增 DemoWatermark 组件Demo 模式显示水印
- 新增 PageTransition 组件,统一页面过渡动画
- 优化 VirtualList 组件,支持大数据列表虚拟滚动
- **布局优化**
- 新增 Sidebar 侧边栏组件,统一导航结构
- 新增 PurchaserLayout 采购商布局组件
- 统一各角色工作台的布局风格
### 2025-06-01 (v1.0.35)
- **分享链接自动过期机制**
- 创建 `check_share_links_expired()` 数据库函数,自动更新过期链接状态
- 完善分享链接流程,符合业务流程图要求
- 移除 `plan_factories` 表错误唯一约束 `unique_plan_factory_type`
- **系统逻辑闭环检查**
- 全面检查采购商与纺织厂关联流程
- 修复 NewPlan.tsx 子操作错误处理(纱线配比、流程节点、价格历史)
- 验证系统符合计划分享流程图和计划确认流程图
### 2025-05-30 (v1.0.34)
- **计划创建流程修复**
- 修复创建计划时自动关联工厂的问题
- 创建计划时不再自动在 `plan_factories` 表中插入关联记录
- 工厂关联改为通过分享链接或推送功能,由工厂确认后才建立
- 删除 `auto_sync_plan_trigger` 触发器,避免自动关联历史工厂
- **Bug修复**
- 修复 `factory_type` 枚举类型错误(`'textile' as FactoryType`
- 修复触发器函数 `auto_sync_plan_to_factory()` 中变量类型声明(`TEXT``factory_type`
### 2025-05-30 (v1.0.33)
- **采购商移动端UI重构**
- 移动端界面风格与纺织厂保持一致
- 统计卡片改为3列紧凑布局
- 最近计划支持左右滑动展示,带滑动按钮和指示器
- 快捷操作区域显示6个功能按钮3列布局
- 新建纺织计划、新建水洗计划、产品信息
- 水洗仓库、工厂管理、应付账款
- 背景使用渐变风格amber-50/orange-50
- **页面切换优化**
- 添加数据缓存机制dataLoaded状态避免重复加载
- 从其他页面返回时保持原有数据,不显示加载动画
- 添加页面可见性监听visibilitychange后台返回前台时自动刷新数据
- **Bug修复**
- 修复快捷操作路由路径错误(应付账款、水洗仓库)
- 修复CreditCard图标未导入导致的运行时错误
### 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 + 盐值混淆 + 字符串反转进行加密
### 敏感信息保护
- **分享链接不传输**:成品名称、颜色、色号等敏感信息
- **导入页面不展示**:接收方通过链接导入计划时,页面不显示成品名称、颜色、色号
- **仅展示**:计划编号、坯布识别码、计划产量、每米布纱用量、生产采购价、备注
---
## 2025-05-30 Bug 修复记录
### 1. 计划状态同步问题
#### 问题描述
纺织厂已点击"确认计划"并完成,但计划状态仍显示为"待生产"。
#### 根本原因
- `plan_process_steps` 表中确认步骤状态已更新为 `completed`
-`production_plans` 表的 `status` 字段未同步更新为 `producing`
- 状态更新逻辑分散在多个组件中,缺乏统一的状态同步机制
#### 修复方案
1. **创建自动状态同步 Hook**`src/hooks/usePlanStatusSync.ts`
- 每 10 秒自动检查一次计划状态(测试用,生产环境应改为 5 分钟)
-`plan_process_steps``confirm` 步骤为 `completed``production_plans` 状态仍为 `pending` 时,自动修复
2. **修复 TextilePlanOverview.tsx**
-`handleConfirmStep` 函数中添加错误处理
- 确保步骤确认和计划状态更新在同一事务中完成
- 添加失败提示,引导用户刷新页面
3. **修复 ImportPlanPage.tsx**
- 导入成功后自动更新 `production_plans.status``producing`
- 添加错误处理和状态回滚机制
#### 代码变更
```typescript
// usePlanStatusSync.ts - 自动状态同步 Hook
export function usePlanStatusSync() {
useEffect(() => {
const syncPlanStatus = async () => {
// 查询状态不一致的计划
const { data: inconsistentPlans } = await supabase
.from('production_plans')
.select('id, status')
.eq('status', 'pending')
.filter('plan_process_steps.step_type', 'eq', 'confirm')
.filter('plan_process_steps.status', 'eq', 'completed');
// 自动修复状态
for (const plan of inconsistentPlans || []) {
await supabase
.from('production_plans')
.update({ status: 'producing' })
.eq('id', plan.id);
}
};
const interval = setInterval(syncPlanStatus, 10 * 1000); // 10秒测试用
return () => clearInterval(interval);
}, []);
}
```
**相关文件**
- `src/hooks/usePlanStatusSync.ts`(新建)
- `src/pages/textile/PlanOverview.tsx`
- `src/pages/ImportPlanPage.tsx`
---
### 2. 分享链接导入功能修复
#### 问题描述
分享链接导入功能存在数据不一致问题:
- `share_links` 表状态为 `pending`,但 `plan_factories` 关联记录已存在
- 导入时 RLS 策略限制导致 403/500 错误
- 导入成功后计划状态未正确更新
#### 根本原因
1. **数据不一致**:历史数据中存在 `share_links``plan_factories` 状态不匹配
2. **RLS 策略问题**
- `yarn_ratios` INSERT 策略缺少 `WITH CHECK` 条件
- `share_links` SELECT 策略过于复杂导致 500 错误
- `share_links` UPDATE 策略限制过严
3. **状态更新缺失**:导入成功后未更新 `production_plans.status`
#### 修复方案
1. **修复 RLS 策略**
- `yarn_ratios`:添加 `WITH CHECK (company_id = get_user_master_company_id())`
- `share_links`:简化 SELECT 策略为 `USING (true)`
- `share_links`:允许任何人更新 `click_count``status`
2. **修复 ImportPlanPage.tsx**
- 添加错误处理和用户提示
- 导入成功后更新 `production_plans.status``producing`
- 添加 `plan_factories` Realtime 订阅,导入后自动刷新
3. **修复 ShareModal.tsx**
- 修复 Date 构造类型错误
- 添加 `hideSensitive` 同步更新逻辑
#### 代码变更
```typescript
// ImportPlanPage.tsx - 导入成功后更新计划状态
const handleConfirmImport = async () => {
// 1. 创建 plan_factories 关联
const { error: insertError } = await supabase
.from('plan_factories')
.insert({ plan_id: planId, factory_id: companyId, factory_type: factoryType });
if (insertError) throw insertError;
// 2. 更新 share_links 状态
await supabase
.from('share_links')
.update({ status: 'confirmed', responded_at: new Date().toISOString() })
.eq('short_code', shortCode);
// 3. 更新 production_plans 状态(关键修复)
const { error: planUpdateError } = await supabase
.from('production_plans')
.update({ status: 'producing' })
.eq('id', planId);
if (planUpdateError) {
console.error('更新计划状态失败:', planUpdateError);
}
};
```
**相关文件**
- `src/pages/ImportPlanPage.tsx`
- `src/components/plans/ShareModal.tsx`
- `migrations/20260530_052129_fix_yarn_ratios_insert_policy.sql`
- `migrations/20260530_052132_fix_share_links_select_policy.sql`
---
### 3. 采购商界面显示关联工厂名称
#### 问题描述
采购商在计划总览页面无法看到已关联的纺织厂公司名称,不利于二次确认。
#### 修复方案
`PlanGroup.tsx` 组件中添加工厂名称显示:
- 查询 `plan_factories` 关联的工厂信息
- 在计划卡片中显示关联工厂名称
- 使用蓝色主题样式突出显示
#### 代码变更
```typescript
// PlanGroup.tsx - 添加工厂名称显示
const [factoryName, setFactoryName] = useState<string>('');
useEffect(() => {
const fetchFactoryName = async () => {
const { data } = await supabase
.from('plan_factories')
.select('factory_id, companies(name)')
.eq('plan_id', plan.id)
.eq('factory_type', 'textile')
.single();
if (data?.companies?.name) {
setFactoryName(data.companies.name);
}
};
fetchFactoryName();
}, [plan.id]);
// 渲染工厂名称
{factoryName && (
<div className="mt-2 flex items-center gap-1.5 text-xs text-blue-600 bg-blue-50 px-2 py-1 rounded-lg">
<Building2 size={12} />
<span>关联工厂: {factoryName}</span>
</div>
)}
```
**相关文件**`src/components/plans/PlanGroup.tsx`
---
### 4. 新建水洗计划产品库存数据加载问题
#### 问题描述
新建水洗计划页面无法正确读取产品信息的库存数据,显示为空。
#### 根本原因
- RLS 策略限制:`production_plans` SELECT 策略要求 `purchaser_id = get_user_master_company_id()`
- 查询时使用了 `.eq('purchaser_id', auth.company?.id)`,与 RLS 策略冲突
- 导致查询返回 0 条数据
#### 修复方案
移除服务端过滤,改为客户端过滤:
```typescript
// 错误:服务端过滤与 RLS 冲突
const { data: plansData, error: plansError } = await supabase
.from('production_plans')
.select('id, fabric_code, purchaser_id')
.eq('purchaser_id', auth.company?.id) // 与 RLS 冲突
.in('fabric_code', fabricCodes);
// 正确:仅查询,客户端过滤
const { data: plansData, error: plansError } = await supabase
.from('production_plans')
.select('id, fabric_code, purchaser_id')
.in('fabric_code', fabricCodes);
const filteredPlansData = (plansData || []).filter(
plan => plan.purchaser_id === auth.company?.id
);
```
**相关文件**`src/pages/purchaser/NewWashingPlan.tsx`
---
### 5. 入库记录产品信息显示问题
#### 问题描述
产品信息-入库记录中手动导入生产记录后,产品信息显示"未知产品"。
#### 根本原因
- `useInventoryData.ts` Hook 返回的 `batches` 数据缺少 `product` 字段
- 组件中通过 `batch.product?.product_name` 访问产品名称时返回 `undefined`
#### 修复方案
`useInventoryData.ts` 中注入产品信息:
```typescript
// 获取产品信息
const { data: productsData } = await supabase
.from('products')
.select('id, product_name, weight, color')
.eq('company_id', companyId);
// 将产品信息注入到 batches 中
const batchesWithProduct = (batchesData || []).map((batch: any) => {
const product = productsData.find((p: Product) => p.id === batch.product_id);
return {
...batch,
product: product ? {
product_name: product.product_name,
weight: product.weight,
color: product.color
} : null
};
});
```
**相关文件**`src/hooks/useInventoryData.ts`
---
### 6. 其他 Bug 修复
#### usePlanData Hook 无限循环
**问题**`usePlanData``fetchData` 依赖 `state.plans` 导致无限循环
**修复**:移除 `state.plans` 从依赖项数组,使用函数式更新
**文件**`src/hooks/usePlanData.ts`
#### 删除工厂外键约束错误
**问题**:删除工厂时未清理 `company_relationships` 关联记录
**修复**:删除工厂前先删除关联的 `company_relationships` 记录
**文件**`src/pages/purchaser/FactoryManage.tsx`
#### Date 构造类型错误
**问题**`ShareModal.tsx``ImportPlanPage.tsx``new Date()` 构造参数类型错误
**修复**:确保传入有效的日期字符串或时间戳
**文件**`src/components/plans/ShareModal.tsx`, `src/pages/ImportPlanPage.tsx`
---
## 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`
---
### 分类二十五:分享链接加密/解密版本不一致问题
#### Bug: 生成端使用无签名同步版本,解析端也使用同步版本,异步版本丢失 hideSensitive
**现象**
- `ShareModal.tsx` 使用 `encryptShareParamsSync`(无 HMAC 签名验证,简单 Base64 编码)
- `ImportPlanPage.tsx` 使用 `decryptShareParamsSync`(无签名验证)
- 异步版本 `decryptShareParams` 返回类型仅包含 `{ planId, factoryType }`,丢失了 `hideSensitive` 字段
- 如果未来某一方切换到异步版本,会导致签名验证失败或 hideSensitive 功能失效
**根本原因**
- 项目中同时存在同步和异步两套加密/解密函数,功能不一致
- 异步版本带 HMAC-SHA256 签名验证,同步版本无签名
- 异步解密函数 `decryptShareParams` 的返回类型未包含 `hideSensitive`
**修复方案**
1. **统一使用异步版本**:两端都切换到带签名验证的异步版本
2. **补回 hideSensitive**`decryptShareParams` 返回类型扩展为 `{ planId, factoryType, hideSensitive }`
3. **ShareModal 改为异步生成**:长链接 token 通过 `useEffect` + `encryptShareParams` 异步生成
```typescript
// shareCrypto.ts - decryptShareParams 返回类型扩展
export async function decryptShareParams(token: string): Promise<{ planId: string; factoryType: string; hideSensitive: boolean } | null> {
// ... 签名验证逻辑 ...
return {
planId: payload.planId,
factoryType: payload.factoryType,
hideSensitive: payload.hideSensitive || false // 新增
};
}
// ShareModal.tsx - 使用异步加密
import { encryptShareParams } from '../../utils/shareCrypto';
// 长链接 token 异步生成
useEffect(() => {
if (isOpen && !shortCode) {
encryptShareParams(planId, factoryType, hideSensitive).then(token => {
setLongLinkToken(token);
});
}
}, [isOpen, planId, factoryType, hideSensitive, shortCode]);
// ImportPlanPage.tsx - 使用异步解密
import { decryptShareParams } from '../utils/shareCrypto';
const decrypted = await decryptShareParams(token);
if (decrypted) {
setPlanId(decrypted.planId);
setFactoryType(decrypted.factoryType as FactoryType);
setHideSensitive(decrypted.hideSensitive || false);
}
```
**相关文件**
- `src/utils/shareCrypto.ts` - `decryptShareParams` 返回类型扩展
- `src/components/plans/ShareModal.tsx` - 切换到异步加密版本
- `src/pages/ImportPlanPage.tsx` - 切换到异步解密版本
---
### 分类二十六:短链 hideSensitive 不随开关更新问题
#### Bug: 用户切换"隐藏成品信息"开关后,短链中的 hide_sensitive 值未同步更新
**现象**
- 用户在分享弹窗中切换"隐藏成品信息"开关
- 短链已生成,但 `share_links` 表中的 `hide_sensitive` 值仍为初始值 `false`
- 纺织厂通过短链导入时,看到的敏感信息显示状态与采购商当前设置不一致
**根本原因**
- `generateShortLink` 在弹窗首次打开时执行一次,`shortLinkGenerated.current` 标记阻止了重新生成
- `hideSensitive` 状态变化后,短链不会重新生成,数据库中的 `hide_sensitive` 字段也不会更新
- 长链接 token 在上一轮修复中已通过 `useEffect` 依赖 `hideSensitive` 实现自动重新生成
**修复方案**
`hideSensitive` 变化时,直接更新数据库中已生成短链的 `hide_sensitive` 字段,而非重新生成短链:
```typescript
// ShareModal.tsx - 新增 useEffect 同步更新短链
useEffect(() => {
if (shortCode && isOpen) {
supabase
.from('share_links')
.update({ hide_sensitive: hideSensitive })
.eq('short_code', shortCode)
.then(({ error }) => {
if (error) console.error('更新短链 hide_sensitive 失败:', error);
});
}
}, [hideSensitive, shortCode, isOpen]);
```
**相关文件**
- `src/components/plans/ShareModal.tsx` - 新增 hideSensitive 同步更新 useEffect
---
### 分类二十七production_plans SELECT RLS 策略过于宽松(已知未修复,未开始修复)
#### Bug: production_plans 的 SELECT 策略允许所有登录用户查看有工厂关联的计划
**状态**:已知但未开始修复,待评估修复方案后再实施
**现象**
- 任何已登录用户只要知道 plan_id就能通过 `plan_factories` 存在关联这一条件查看计划详情
- 一个与计划无关的水洗厂也能看到采购商的计划详情(成品名称、颜色、色号等敏感信息)
**根本原因**
- `20260527_114150_fix_import_plan_select_rls.sql` 中添加了过于宽松的条件:
```sql
OR EXISTS (
SELECT 1 FROM plan_factories pf
WHERE pf.plan_id = production_plans.id
)
```
- 此条件的目的是允许纺织厂在导入时(尚未建立关联前)查看计划详情
- 但条件未限制 `factory_id` 必须与当前用户公司匹配,导致任何用户都能查看
**影响**
- 数据泄露风险:无关用户可查看采购商的计划详情和敏感信息
- 导入流程依赖此宽松策略才能正常工作
**建议修复方案**
- 方案A移除宽松条件改为在导入页面使用 SECURITY DEFINER 函数查询计划
- 方案B添加更精确的条件仅允许 `factory_type` 与当前用户角色匹配的用户查看
- 方案C在导入流程中先创建 `plan_factories` 关联(使用宽松 INSERT 策略),再通过正常 SELECT 策略查看
**相关文件**
- `migrations/20260527_114150_fix_import_plan_select_rls.sql`
---
### 分类二十八:纺织厂导入成功后计划总览不显示计划
#### Bug: 导入成功后跳转到 Dashboard 而非计划总览,且 PlanOverview 未订阅 plan_factories 变化
**现象**
- 纺织厂通过分享链接导入计划成功后,页面跳转到 Dashboard`/textile`
- 用户再进入计划总览(`/textile/plans`)时,看不到刚导入的计划
- 需要手动刷新页面才能看到新导入的计划
**根本原因**
- `ImportPlanPage.tsx` 导入成功后跳转到 `/${auth.currentRole}`Dashboard而非 `/${auth.currentRole}/plans`(计划总览)
- 纺织厂 `PlanOverview.tsx` 的 Realtime 订阅仅监听 `inventory_records` 表变化,未监听 `plan_factories` 表变化
- 导入操作创建 `plan_factories` 关联记录,但 PlanOverview 不会因此自动刷新
**修复方案**
1. **修改跳转目标**:导入成功后跳转到 `/${auth.currentRole}/plans`(计划总览)
2. **添加 Realtime 订阅**PlanOverview 同时订阅 `plan_factories` 表变化,仅当关联到当前纺织厂时刷新
```typescript
// ImportPlanPage.tsx - 跳转到计划总览
setTimeout(() => { navigate(`/${auth.currentRole}/plans`); }, 3000);
// PlanOverview.tsx - 订阅 plan_factories 变化
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'plan_factories' },
(payload) => {
if (payload.new && (payload.new as any).factory_id === auth.company!.id) {
fetchPlans();
}
if (payload.eventType === 'DELETE') {
fetchPlans();
}
}
)
```
**相关文件**
- `src/pages/ImportPlanPage.tsx` - 修改跳转目标
- `src/pages/textile/PlanOverview.tsx` - 添加 plan_factories Realtime 订阅
---
### 分类二十九:分享链接导入后未失效
#### Bug: 短链导入成功后仍可被重复使用
**现象**
- 采购商生成分享链接发送给纺织厂
- 纺织厂导入成功后,同一链接仍可被其他人或其他角色再次导入
- 没有任何机制阻止链接被重复使用
**根本原因**
- `share_links` 表没有 `used_at` 字段来标记链接是否已被使用
- `ImportPlanPage.tsx` 导入成功后没有标记短链为已使用
- 解析短链时没有检查链接是否已失效
**修复方案**
1. **数据库迁移**:为 `share_links` 表添加 `used_at` 字段TIMESTAMPTZ
2. **导入成功后标记**`ImportPlanPage` 导入成功后更新 `used_at` 为当前时间
3. **解析时检查**:解析短链时查询 `used_at` 字段,已使用则显示"链接已失效"
```typescript
// ImportPlanPage.tsx - 导入成功后标记短链
if (shortCode) {
await supabase
.from('share_links')
.update({ used_at: new Date().toISOString() })
.eq('short_code', shortCode);
}
// ImportPlanPage.tsx - 解析时检查 used_at
const { data } = await supabase
.from('share_links')
.select('plan_id, factory_type, hide_sensitive, used_at')
.eq('short_code', shortCode)
.maybeSingle();
if (data?.used_at) {
// 显示"链接已失效"页面
return;
}
```
**相关文件**
- `migrations/20260528_082639_add_used_at_to_share_links.sql` - 数据库迁移
- `src/pages/ImportPlanPage.tsx` - 导入后标记 + 解析时检查 + 失效 UI
---
## 学习反思与最佳实践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% |
| 分享链接加密/解密版本不一致问题 | 1 | 4% |
| 短链 hideSensitive 不随开关更新问题 | 1 | 4% |
| production_plans SELECT RLS 策略过于宽松(已知未修复,未开始修复) | 1 | 4% |
| 纺织厂导入成功后计划总览不显示计划 | 1 | 4% |
| 分享链接导入后未失效 | 1 | 4% |
| **总计** | **35** | **100%** |
### 2025-05-30 新增 Bug 修复记录
| 分类 | Bug 描述 | 状态 | 修复文件 |
|------|----------|------|----------|
| React Hooks | usePlanData 无限循环导致页面卡加载 | 已修复 | src/hooks/usePlanData.ts |
| 数据库 RLS | yarn_ratios INSERT 策略缺失 WITH CHECK | 已修复 | migrations/20260530_052129_fix_yarn_ratios_insert_policy.sql |
| 数据库 RLS | share_links SELECT 策略复杂导致 500 错误 | 已修复 | migrations/20260530_052132_fix_share_links_select_policy.sql |
| 数据库 RLS | companies 表缺少 DELETE 策略 | 已修复 | migrations/20260530_051239_add_companies_delete_policy.sql |
| 外键约束 | 删除工厂时未清理 company_relationships 关联 | 已修复 | src/pages/purchaser/FactoryManage.tsx |
| TypeScript | ShareModal/ImportPlanPage Date 构造类型错误 | 已修复 | src/components/plans/ShareModal.tsx, src/pages/ImportPlanPage.tsx |
### 已知未修复 Bug
| Bug 描述 | 优先级 | 影响范围 | 备注 |
|----------|--------|----------|------|
| production_plans SELECT RLS 策略过于宽松 | 高 | 数据安全 | 任何登录用户可查看有关联的计划详情 |
| 分享链接短码生成可能存在重复 | 中 | 功能稳定性 | 需添加唯一约束检查 |
| 计划总览页面大数据量时性能下降 | 中 | 性能 | 需添加虚拟列表优化 |
---
## Bug 修复记录2025-06-02
### 分类三十:纱线分配优化
#### Bug: 纱线分配流程交互复杂
**现象**
- 纺织厂在采纱环节需要手动输入多种纱线的使用量
- 没有自动匹配库存的功能
- 库存不足时无法直观看到缺少哪些纱线
**根本原因**
- 原有流程仅支持手动输入
- 缺少与纱线库存的联动校验
**修复方案**
- 新增 `YarnAllocationModal` 组件,提供可视化的纱线分配界面
- 支持自动匹配和手动分配两种模式
- 实时显示库存状态,库存不足时阻止确认
**相关文件**
- `src/components/YarnAllocationModal.tsx`(新建)
- `src/pages/textile/PlanOverview.tsx`
---
### 分类三十一Demo 模式水印显示
#### Bug: Demo 模式下缺少明显标识
**现象**
- 用户难以区分 Demo 环境还是正式环境
- 可能导致误操作
**根本原因**
- 缺少 Demo 模式的可视化提示
**修复方案**
- 新增 `DemoWatermark` 组件
- 在页面右下角显示半透明水印
- 支持配置显示位置和样式
**相关文件**
- `src/components/DemoWatermark.tsx`(新建)
- `src/App.tsx`
---
### 分类三十二:页面过渡动画统一
#### Bug: 页面切换动画不一致
**现象**
- 不同页面的过渡效果不统一
- 部分页面切换时显得生硬
**根本原因**
- 各页面独立实现过渡效果
- 缺少统一的过渡组件
**修复方案**
- 新增 `PageTransition` 组件
- 统一使用 fade + slide 过渡效果
- 支持自定义动画参数
**相关文件**
- `src/components/PageTransition.tsx`(新建)
- 各页面布局组件
---
### 分类三十三:虚拟列表性能优化
#### Bug: 大数据列表渲染卡顿
**现象**
- 产品列表或计划列表超过 100 条时滚动卡顿
- 内存占用高,帧率下降
**根本原因**
- 一次性渲染所有列表项
- 未使用虚拟滚动技术
**修复方案**
- 优化 `VirtualList` 组件
- 支持动态高度的列表项
- 只渲染可视区域内的元素
**相关文件**
- `src/components/VirtualList.tsx`
- `src/components/warehouse/ProductList.tsx`
### 技术反思与最佳实践
#### 1. React Hooks 依赖项管理
**问题**: usePlanData 中 fetchData 依赖 state.plans 导致无限循环
**教训**:
- 避免将状态值放入依赖项数组,除非确实需要监听其变化
- 使用 useRef 存储不需要触发重渲染的缓存数据
- 使用函数式更新避免依赖旧状态
#### 2. 数据库 RLS 策略设计
**问题**:
- INSERT 策略缺少 WITH CHECK 条件导致 403
- SELECT 策略过于复杂导致 500 服务器错误
**教训**:
- INSERT 策略必须包含 WITH CHECK 条件
- SELECT 策略应保持简单,避免嵌套子查询
- 策略变更需进行权限测试
#### 3. 外键约束处理
**问题**: 删除工厂时违反外键约束
**教训**:
- 删除前必须检查并清理关联表数据
- 建立清晰的级联删除策略
- 使用事务保证数据一致性
#### 4. TypeScript 类型安全
**问题**: Date 构造和类型断言错误
**教训**:
- 对可能为 null 的值进行空值检查
- 使用类型断言时确保类型兼容性
- 数据库返回类型与前端类型需保持一致
#### 5. 分享链接安全设计
**改进**:
- 合作关系表实现首次合作确认机制
- 30分钟有效期限制防止链接滥用
- 链接状态追踪pending → clicked → confirmed/rejected
- 隐藏敏感信息开关保护商业数据
### 架构设计决策记录
#### 分享链接流程重构 (2025-05-30)
**决策**: 从简单加密链接改为基于合作关系的双模式分享
**原因**:
- 首次合作需要建立信任关系
- 后续合作可直接推送提高效率
- 链接过期和状态追踪增强安全性
**实现**:
- company_relationships 表记录合作关系
- share_links 表追踪链接生命周期
- ShareModal 根据关系状态显示不同 UI
- ImportPlanPage 处理确认/拒绝流程
#### 数据获取优化 (2025-05-30)
**决策**: usePlanData 获取所有工厂而非仅有关联的
**原因**:
- 计划总览需要显示所有可用工厂
- 无关联工厂的计划显示在"待关联"区域
- 提高用户体验和页面完整性
**实现**:
- 查询所有 role='textile'/'washing' 的公司
- 前端根据 plan_factories 关联状态分组显示
### 技术债务分析
**高频问题类别**
1. **UI/UX问题**3个主题色适配、弹窗位置、移动端适配
2. **TypeScript类型问题**2个类型定义不匹配、泛型约束
3. **Webpack构建问题**2个热更新失效、产物体积过大
**改进建议**
1. 建立主题色设计系统,统一管理颜色变量
2. 完善TypeScript严格模式配置提前发现类型问题
3. 优化Webpack配置建立构建性能监控
4. 增加组件级单元测试,覆盖主题色渲染场景
---
## 系统审计记录2026-06-04
### 审计范围
对全系统进行功能文件关联、内存安全、路由配置、权限授权四个维度的全面审计。
### 审计结果摘要
| 检查项 | 状态 | 说明 |
|--------|------|------|
| 功能文件关联 | ✅ 通过 | 25个页面组件、33个共享组件、6个Hook均正确关联 |
| 内存溢出风险 | ✅ 通过 | 35处定时器/订阅均有正确的清理逻辑 |
| 路由关联 | ✅ 通过 | HashRouter 25条路由配置正确嵌套布局正常 |
| 权限授权 | ✅ 通过 | RLS策略基于company_id隔离子账号通过helper函数支持 |
| 测试代码残留 | ⚠️ 已修复 | usePlanStatusSync 间隔为测试值10秒 |
### Bug 修复记录
#### 分类三十四usePlanStatusSync 测试间隔未恢复
**问题描述**
`src/hooks/usePlanStatusSync.ts` 中的计划状态同步检查间隔为 `10 * 1000`10秒这是开发测试时的临时值生产环境应为 `5 * 60 * 1000`5分钟
**影响**
- 每10秒执行一次数据库查询增加不必要的服务器负载
- 对于状态同步这种低频需求10秒间隔过于频繁
**当前状态**
- 用户明确要求保持10秒间隔用于测试已在代码和文档中标注"测试用"
- 生产部署前需改回5分钟
**相关文件**
- `src/hooks/usePlanStatusSync.ts:107` - 间隔配置
- `AGENTS.md:1304,1339` - 文档同步更新
### 数据完整性修复(前置工作)
在本次审计前,已通过数据库迁移 `20260604_132401_fix_data_integrity_issues.sql` 完成以下数据修复:
| 问题类型 | 数量 | 修复方式 |
|----------|------|----------|
| share_links 孤儿记录 | 5条 | DELETE 清理 |
| 计划状态不一致 | 1条 | UPDATE status='producing' |
| 过期链接未标记 | 7条 | UPDATE status='expired' |
| 缺失 used_at 字段 | 2条 | UPDATE used_at |
### 内存安全验证详情
已验证的定时器/订阅清理模式:
| 文件 | 类型 | 清理方式 | 状态 |
|------|------|----------|------|
| usePlanStatusSync.ts | setInterval | useRef + useEffect return | ✅ |
| useRealtime.ts | Realtime订阅 | 单例管理器 + 引用计数 | ✅ |
| VersionUpdate.tsx | setInterval | useEffect return clearInterval | ✅ |
| AuthContext.tsx | auth订阅 | useEffect return unsubscribe | ✅ |
| useInventoryData.ts | Realtime订阅 | useEffect return unsubscribe | ✅ |
| usePlanData.ts | Realtime订阅 | useEffect return unsubscribe | ✅ |
| Dashboard.tsx (各角色) | visibilitychange | useEffect return removeEventListener | ✅ |
### 学习反思
#### 1. 测试代码残留风险
**问题**开发时为快速验证将定时间隔从5分钟改为10秒但未在提交前恢复。
**教训**
- 使用环境变量或配置常量管理测试参数,避免硬编码修改
- 建立代码审查清单,包含"测试代码清理"检查项
- 在代码注释中明确标注临时修改的用途和恢复条件
#### 2. 系统审计方法论
**经验**
- 按"文件关联→内存安全→路由配置→权限授权"四维度检查,覆盖全面
- 使用 Grep 批量搜索关键模式setInterval、subscribe、Route比逐文件阅读高效
- 数据库数据完整性检查应作为系统审计的前置步骤
#### 3. 文档同步重要性
**经验**
- 代码变更后必须同步更新 AGENTS.md 中的相关描述
- 文档中的代码示例应与实际代码保持一致
---
## Bug 快速排查检查单2026-06-04
> **使用说明**:当系统出现异常时,按症状分类快速定位问题根因。每个检查项附带具体命令或代码位置,可直接执行验证。
### 一、页面白屏 / 无法加载
| # | 检查项 | 验证方法 | 常见原因 | 修复方向 |
|---|--------|----------|----------|----------|
| 1 | TypeScript 编译错误 | `pnpm run typecheck 2>&1 \| head -50` | 类型不匹配、缺少必需属性 | 根据报错修复类型定义 |
| 2 | Webpack 构建失败 | `pnpm run build 2>&1 \| tail -30` | 模块未找到、语法错误 | 检查 import 路径和依赖 |
| 3 | 开发服务器端口 | `lsof -i :3015` | 端口被占用或未启动 | `pnpm run dev` 重启 |
| 4 | webpack.config.js host | 检查 `devServer.host` | 配置为 `127.0.0.1` 导致沙箱不可访问 | 改为 `0.0.0.0` |
| 5 | 环境变量缺失 | `cat .env.local` | Supabase URL/Key 未配置 | 补充 `.env.local` |
| 6 | 路由配置错误 | 检查 `App.tsx` Route 定义 | path 拼写错误、嵌套路由缺少 Outlet | 对照路由表修正 |
| 7 | 组件导入路径 | Grep 搜索报错组件名 | 大小写不一致、相对路径层级错误 | 修正 import 语句 |
### 二、数据无法读取 / 显示为空
| # | 检查项 | 验证方法 | 常见原因 | 修复方向 |
|---|--------|----------|----------|----------|
| 1 | RLS 策略拒绝 | `meoo-cli cloud query --sql "SELECT * FROM pg_policies WHERE tablename='目标表'"` | 策略条件过严、未支持子账号 | 使用 `get_user_master_company_id()` |
| 2 | company_id 过滤冲突 | 检查查询是否同时用 `.eq('company_id', ...)` 和 RLS | 客户端过滤与 RLS 双重限制 | 移除客户端冗余过滤,依赖 RLS |
| 3 | Realtime 订阅未触发 | 检查 `useRealtime.ts` 单例状态 | 频道名重复、引用计数归零 | 确认 channel name 唯一性 |
| 4 | 缓存数据过期 | 检查 Hook 中 `cacheRef.current.timestamp` | 缓存时间过长导致脏数据 | 调整缓存有效期或手动刷新 |
| 5 | 关联查询缺失 | 检查 `.select()` 是否包含关联表 | 只查主表未 join 关联数据 | 添加 `table(column1, column2)` 嵌套查询 |
| 6 | 空值未处理 | Grep 搜索 `?.``\|\| 0` | 数值字段为 null 参与运算产生 NaN | 添加默认值 `(value \|\| 0)` |
| 7 | 分页 range 错误 | 检查 `.range(from, to)` 参数 | from > to 或负数 | 确保 `from = page * size`, `to = from + size - 1` |
### 三、数据写入失败 / 保存报错
| # | 检查项 | 验证方法 | 常见原因 | 修复方向 |
|---|--------|----------|----------|----------|
| 1 | INSERT RLS 缺少 WITH CHECK | 检查 INSERT 策略 | 只有 USING 没有 WITH CHECK | 添加 `WITH CHECK (company_id = get_user_master_company_id())` |
| 2 | 外键约束违反 | 查看控制台错误信息 | 关联记录不存在 | 先创建父记录再插入子记录 |
| 3 | 唯一约束冲突 | 检查 UNIQUE 字段 | 重复的产品码/用户名/短码 | 插入前查询是否存在 |
| 4 | 必填字段缺失 | 对比表结构和 insert 对象 | NOT NULL 字段未传值 | 补充所有必需字段 |
| 5 | 枚举值不匹配 | 检查 enum 类型定义 | 传入不在枚举范围内的字符串 | 使用 TypeScript 枚举常量 |
| 6 | 事务原子性 | 检查多步操作是否有 try-catch | 部分成功部分失败导致数据不一致 | 使用事务或补偿逻辑 |
| 7 | Storage 上传失败 | 检查 bucket 名称和 RLS | bucket 不存在或无 INSERT 策略 | `meoo-cli cloud query` 验证 bucket |
### 四、状态不同步 / 数据不一致
| # | 检查项 | 验证方法 | 常见原因 | 修复方向 |
|---|--------|----------|----------|----------|
| 1 | 计划状态未同步 | 查询 `production_plans.status` vs `plan_process_steps` | confirm 完成但 plan 仍为 pending | `usePlanStatusSync` 自动修复 |
| 2 | completed_quantity 与入库记录不一致 | SQL 对比 `completed_quantity` vs `SUM(inventory_records.quantity)` | 入库时未同时更新两个表 | 确保原子操作 |
| 3 | localStorage 状态丢失 | 浏览器 DevTools → Application → LocalStorage | 清除缓存后拒绝状态消失 | 关键状态应持久化到数据库 |
| 4 | 跨页面状态不共享 | 检查是否使用 Context/localStorage/URL params | 仅用 useState 导致刷新丢失 | 提升到全局状态或持久化 |
| 5 | Realtime 回调未刷新 | 检查 `.on('postgres_changes', callback)` | callback 内未调用 fetchData | 在回调中触发数据重新获取 |
| 6 | 乐观更新未回滚 | 检查 catch 块 | 服务端失败后 UI 仍显示成功 | catch 中恢复原始状态 |
| 7 | share_links 状态流转 | 查询 `status` 字段 | pending→confirmed 中间态缺失 | 检查 ImportPlanPage 状态更新逻辑 |
### 五、内存泄漏 / 性能问题
| # | 检查项 | 验证方法 | 常见原因 | 修复方向 |
|---|--------|----------|----------|----------|
| 1 | setInterval 未清理 | Grep `setInterval` 检查对应 `clearInterval` | useEffect 缺少 return 清理 | 添加 `return () => clearInterval(id)` |
| 2 | Realtime 订阅未取消 | Grep `.subscribe()` 检查 `.unsubscribe()` | 组件卸载后订阅仍在 | useEffect return 中 unsubscribe |
| 3 | 事件监听器未移除 | Grep `addEventListener` 检查 `removeEventListener` | visibilitychange/scroll 等未清理 | useEffect return 中 removeEventListener |
| 4 | usePlanData 无限循环 | 检查 fetchData 依赖数组 | state.plans 在依赖中导致循环 | 移除状态依赖,用函数式更新 |
| 5 | 大列表渲染卡顿 | Chrome DevTools Performance | 100+ 条未用虚拟滚动 | 使用 VirtualList 组件 |
| 6 | 重复请求 | Network 面板检查相同 API 调用频率 | 缓存失效或依赖变化触发重请求 | 增加缓存时间或稳定依赖引用 |
| 7 | Framer Motion 低性能 | 检查 `usePerformance` 返回值 | 低端设备未降级动画 | 确认 performance tier 检测正常 |
### 六、样式 / UI 异常
| # | 检查项 | 验证方法 | 常见原因 | 修复方向 |
|---|--------|----------|----------|----------|
| 1 | Tailwind 类名未生效 | 检查 `tailwind.config.js` content 路径 | 新文件路径未被扫描 | 添加文件路径到 content 数组 |
| 2 | 主题色不一致 | 对比各角色 Dashboard 渐变色 | 硬编码颜色未统一 | 使用 themeColors 配置对象 |
| 3 | 移动端布局错乱 | Chrome DevTools 移动端模拟 | 未用移动优先断点 | 默认样式为移动端,`sm:` 向上覆盖 |
| 4 | 弹窗位置偏移 | 检查 fixed 定位的 flex 对齐 | `items-start` + padding 偏移 | 改用 `items-center justify-center` |
| 5 | 通知中心崩溃 | 检查 theme prop 是否在 themeConfig 中 | 传入未定义的主题名 | 扩展 themeConfig 添加缺失主题 |
| 6 | 表格横向溢出 | 移动端预览 | table 列数过多 | 桌面端表格 `hidden sm:block` + 移动端卡片 `sm:hidden` |
| 7 | CSS 变量未定义 | DevTools Computed 面板 | 使用了未声明的 `--var` | 在 `index.css` 中补充定义 |
### 七、认证 / 权限问题
| # | 检查项 | 验证方法 | 常见原因 | 修复方向 |
|---|--------|----------|----------|----------|
| 1 | 用户未登录 | `supabase.auth.getUser()` 返回 null | token 过期或未登录 | 跳转 `/login` |
| 2 | 角色不匹配 | 检查 `profiles.role` vs 当前路由 | textile 用户访问 purchaser 页面 | AuthContext 中添加角色守卫 |
| 3 | 子账号权限不足 | 检查 `profiles.master_id` | 子账号 company_id 为 null | 通过 `get_user_master_company_id()` 获取 |
| 4 | 用户名重复 | 注册/创建子账号时报错 | UNIQUE 约束冲突 | 提交前查询 username 是否存在 |
| 5 | 虚拟邮箱格式 | 检查 auth.users.email | 非 `{username}@meoo.local` 格式 | 统一使用虚拟邮箱注册 |
| 6 | RLS 策略不支持子账号 | 检查策略是否用 `auth.uid()` 直接比较 | 子账号 id ≠ master_id | 改用 `get_user_master_company_id()` |
### 八、分享链接 / 导入问题
| # | 检查项 | 验证方法 | 常见原因 | 修复方向 |
|---|--------|----------|----------|----------|
| 1 | 链接已过期 | 查询 `share_links.expires_at` | 超过 30 分钟有效期 | 重新生成分享链接 |
| 2 | 链接已使用 | 查询 `share_links.used_at` | 已被导入过 | 提示"链接已失效" |
| 3 | 解密失败 | 检查 `decryptShareParams` 返回值 | HMAC 签名不匹配 | 确认两端使用相同密钥和算法版本 |
| 4 | hideSensitive 不同步 | 对比短链记录和 ShareModal 开关状态 | 切换开关后未更新数据库 | useEffect 监听 hideSensitive 变化并 UPDATE |
| 5 | 导入后计划不显示 | 检查 PlanOverview Realtime 订阅 | 未订阅 plan_factories 表变化 | 添加 plan_factories INSERT 事件监听 |
| 6 | 导入后跳转错误 | 检查 ImportPlanPage navigate 目标 | 跳转到 Dashboard 而非 plans | 改为 `/${role}/plans` |
| 7 | plan_factories 重复关联 | 查询同一 plan_id + factory_id 记录数 | 缺少唯一约束 | 插入前检查或使用 ON CONFLICT |
### 九、构建 / 部署问题
| # | 检查项 | 验证方法 | 常见原因 | 修复方向 |
|---|--------|----------|----------|----------|
| 1 | pnpm install 失败 | 检查网络或 lockfile | 依赖版本冲突 | 删除 node_modules + pnpm-lock.yaml 重装 |
| 2 | 构建产物过大 | `ls -lh dist/*.js` | 未开启代码分割/压缩 | 配置 splitChunks + TerserPlugin |
| 3 | HMR 不工作 | 检查 webpack devServer.hot | 未启用热更新 | 设置 `hot: true` |
| 4 | historyApiFallback | 刷新页面 404 | HashRouter 需要 fallback | 设置 `historyApiFallback: true` |
| 5 | allowedHosts 限制 | 沙箱预览加载失败 | 主机名不在允许列表 | 设置 `allowedHosts: 'all'` |
| 6 | static 目录不存在 | devServer 报 ENOENT | 配置了不存在的 public 目录 | 移除 static 配置或创建目录 |
| 7 | 测试代码残留 | Grep `// 测试用` `TODO` `FIXME` | 临时代码未清理 | 建立提交前检查清单 |
### 十、数据库诊断常用 SQL
```sql
-- 1. 查看所有表的记录数
SELECT schemaname, relname, n_live_tup
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC;
-- 2. 检查 RLS 策略
SELECT tablename, policyname, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public'
ORDER BY tablename;
-- 3. 检查计划状态一致性
SELECT p.id, p.plan_code, p.status,
MAX(CASE WHEN ps.step_type = 'confirm' THEN ps.status END) as confirm_status
FROM production_plans p
LEFT JOIN plan_process_steps ps ON ps.plan_id = p.id
GROUP BY p.id
HAVING p.status = 'pending'
AND MAX(CASE WHEN ps.step_type = 'confirm' THEN ps.status END) = 'completed';
-- 4. 检查入库数量一致性
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);
-- 5. 检查过期的分享链接
SELECT id, short_code, status, expires_at, used_at
FROM share_links
WHERE expires_at < NOW() AND status NOT IN ('expired', 'cancelled');
-- 6. 检查孤儿记录
SELECT sl.id, sl.short_code
FROM share_links sl
LEFT JOIN production_plans pp ON pp.id = sl.plan_id
WHERE pp.id IS NULL;
-- 7. 检查子账号关联
SELECT p.id, p.username, p.is_master, p.master_id, p.company_id,
mp.company_id as master_company_id
FROM profiles p
LEFT JOIN profiles mp ON mp.id = p.master_id
WHERE p.master_id IS NOT NULL;
```
### 十一、快速排查流程图
```
系统异常
├── 页面白屏 → 第一节(编译/构建/路由)
│ └── typecheck → build → dev server → 路由配置
├── 数据为空 → 第二节RLS/查询/缓存)
│ └── RLS策略 → 查询语句 → 缓存状态 → 空值处理
├── 保存失败 → 第三节(写入/约束/事务)
│ └── INSERT策略 → 外键/唯一约束 → 必填字段 → 事务完整性
├── 数据不一致 → 第四节(状态同步)
│ └── 计划状态 → 数量一致性 → localStorage → Realtime回调
├── 卡顿/内存 → 第五节(性能)
│ └── 定时器清理 → 订阅取消 → 无限循环 → 虚拟滚动
├── UI错乱 → 第六节(样式)
│ └── Tailwind配置 → 主题色 → 响应式断点 → 弹窗定位
├── 权限问题 → 第七节(认证)
│ └── 登录状态 → 角色匹配 → 子账号 → RLS子账号支持
├── 分享/导入 → 第八节(链接)
│ └── 过期/已用 → 解密 → hideSensitive → Realtime订阅
└── 构建部署 → 第九节(工程化)
└── 依赖安装 → 产物体积 → HMR → 沙箱兼容
```
### 十二、高频 Bug 速查索引
| 症状关键词 | 对应检查单 | 历史分类 | 典型文件 |
|-----------|-----------|---------|---------|
| undefined / Cannot read properties | 一-7, 二-6 | 分类一 | WarehouseManage.tsx |
| NaN 米 / NaN% | 二-6 | 分类三 | InventoryRecordsModal.tsx |
| 403 / violates RLS | 三-1, 七-6 | 分类七 | migrations/*_rls.sql |
| 500 Server Error | 三-1, 八-3 | 分类七 | share_links RLS |
| 无限循环 / 页面卡死 | 五-4 | React Hooks | usePlanData.ts |
| 内存泄漏 / Can't perform state update | 五-1~3 | 分类六 | useRealtime.ts |
| 状态不同步 / 刷新后丢失 | 四-3~4 | 分类十八 | PlanOverview.tsx |
| 主题色崩溃 / reading 'bg' | 六-5 | 分类二十三 | NotificationCenter.tsx |
| 链接失效 / 重复导入 | 八-1~2 | 分类二十九 | ImportPlanPage.tsx |
| 移动端样式错乱 | 六-3, 六-6 | 分类四 | FabricWarehouse.tsx |
| 子账号看不到数据 | 七-3, 七-6 | 分类七 | RLS helper function |
| 纱线占比 10000% | 二-6 | 数值计算 | YarnAllocationModal.tsx |
| Date 构造报错 | 三-5 | TypeScript | ShareModal.tsx |
| 外键约束 DELETE 失败 | 三-2 | 外键约束 | FactoryManage.tsx |
- 测试配置变更需在文档中标注状态和恢复计划
---
## 全流程模拟测试记录2026-06-04
### 测试概述
对采购商下单 → 分享链接 → 纺织厂确认 → 生产流程 → 分批次入库的完整业务流程进行端到端模拟测试,验证数据关联、状态流转和数量一致性。
### 测试环境
| 项目 | 值 |
|------|-----|
| 采购商公司 | 示例布行 (`11111111-1111-1111-1111-111111111101`) |
| 纺织厂公司 | 示例纺织厂 (`11111111-1111-1111-1111-111111111102`) |
| 采购商用户 | purchaser (`a0000001-0000-0000-0000-000000000001`) |
| 纺织厂用户 | textile (`a0000002-0000-0000-0000-000000000002`) |
| 坯布仓库 | 坯布仓库 (`22222222-2222-2222-2222-222222222202`) |
| 测试计划编号 | TEST-2026-001 |
| 测试产品 | 全棉斜纹布 / 280g / 藏青 / QM-XW-280-01 |
### 测试步骤与执行结果
#### 步骤1: 采购商创建产品和生产计划 ✅
| 操作 | 表 | 结果 |
|------|-----|------|
| 创建产品 | products | ID: `0b807895-270e-476b-8a2e-985078418524` |
| 创建计划 | production_plans | ID: `ac1b7c91-eea7-4e75-9fcd-03a15ee89371`, status: pending |
| 创建纱线配比 | yarn_ratios | 精梳棉纱(70%) + 涤纶纱(30%) |
| 创建流程节点 | plan_process_steps | 5个节点(confirm/yarn_purchase/dyeing/machine_start/fabric_warehouse) |
#### 步骤2: 生成分享链接(首次合作)✅
| 操作 | 表 | 结果 |
|------|-----|------|
| 删除旧合作关系 | company_relationships | 已清理,模拟首次合作 |
| 创建分享链接 | share_links | short_code: `TEST-SIM-001`, status: pending, 30分钟有效期 |
#### 步骤3: 纺织厂点击链接并确认导入 ✅
| 操作 | 表 | 结果 |
|------|-----|------|
| 点击链接 | share_links | status → clicked, click_count: 1 |
| 创建工厂关联 | plan_factories | factory_id: 示例纺织厂, factory_type: textile |
| 确认导入 | share_links | status → confirmed, used_at 已标记 |
| 建立合作关系 | company_relationships | status: active |
| 更新计划状态 | production_plans | status → producing |
#### 步骤4: 纺织厂确认生产流程节点 ✅
| 节点 | 操作人 | 状态 |
|------|--------|------|
| confirm (确认计划) | textile | completed |
| yarn_purchase (采纱) | textile | completed |
| dyeing (染纱) | textile | completed |
| machine_start (上机) | textile | completed |
| fabric_warehouse (坯布入库) | textile | completed |
#### 步骤5: 分批次坯布入库 ✅
| 批次 | 入库米数 | 匹数 | 单价 | 累计 |
|------|---------|------|------|------|
| 第1批 | 3,000m | 30匹 | ¥18.5/m | 3,000m |
| 第2批 | 4,000m | 40匹 | ¥18.5/m | 7,000m |
| 第3批 | 3,000m | 28匹 | ¥18.5/m | 10,000m |
| **合计** | **10,000m** | **98匹** | - | **10,000m** |
#### 步骤6: 数据完整性验证 ✅
| 验证项 | 预期值 | 实际值 | 状态 |
|--------|--------|--------|------|
| 计划状态 | completed | completed | ✅ |
| 目标产量 | 10,000m | 10,000m | ✅ |
| 完成产量 | 10,000m | 10,000m | ✅ |
| 已完成流程节点 | 5/5 | 5/5 | ✅ |
| 入库批次 | 3 | 3 | ✅ |
| 入库总米数 | 10,000m | 10,000m | ✅ |
| 入库总匹数 | - | 98匹 | ✅ |
| 分享链接状态 | confirmed | confirmed | ✅ |
| 链接已使用 | true | true | ✅ |
| 合作关系 | active | active | ✅ |
| 工厂关联 | 示例纺织厂 | 示例纺织厂 | ✅ |
| completed_quantity = SUM(inventory_records) | true | true | ✅ |
### 测试结论
全流程模拟测试 **全部通过**。关键验证点:
1. **首次合作流程正确**:无合作关系时生成分享链接 → 纺织厂确认 → 自动建立合作关系
2. **状态流转完整**pending → producing → completed每个阶段状态正确
3. **数据一致性**completed_quantity (10,000) = SUM(inventory_records.quantity) (10,000)
4. **分享链接安全**used_at 已标记防止重复导入30分钟过期机制正常
5. **分批次入库**3批入库记录独立存储汇总数量与计划一致
### 测试文件
- **E2E 测试**: `e2e/full-workflow.spec.ts` — Playwright UI 测试 + SQL 执行记录注释
- **运行方式**: `pnpm test e2e/full-workflow.spec.ts`
### 已知限制
- UI 测试部分依赖页面元素选择器,页面重构后可能需要更新
- SQL 模拟部分以注释形式记录,后续可抽取为独立的数据库集成测试
- 未覆盖水洗厂环节和结款流程,可作为后续扩展测试
---
## 项目全面审查与优化路线图2026-06-04 v2.0.0
### 一、重复加载动画诊断
#### 问题根因:四层动画嵌套
| 层级 | 位置 | 问题 | 影响 |
|------|------|------|------|
| L1 路由层 | `App.tsx:66-87` AnimatePresence mode="wait" | 等待退出动画完成才渲染新页面 | 页面切换延迟感 |
| L2 页面层 | 3个 Dashboard 各自维护 loading state + Spinner | 代码重复Spinner 样式不一致 | 视觉闪烁 |
| L3 内容层 | 页面内部 motion.div (containerVariants) 入场动画 | 与 L1 叠加 | 低端设备卡顿 |
| L4 数据层 | Hook loading + 组件 local loading 状态不同步 | 双重 Loading 或闪烁 | 体验割裂 |
#### 具体重复位置
| 文件 | 行号 | 重复模式 |
|------|------|----------|
| `src/pages/purchaser/Dashboard.tsx` | 66, 221-226 | 独立 loading state + 内联 Spinner |
| `src/pages/textile/Dashboard.tsx` | 47, 186-191 | 独立 loading + motion.div 嵌套 |
| `src/pages/washing/Dashboard.tsx` | 52, 158-163 | 独立 loading + motion.div 嵌套 |
| `src/pages/purchaser/PlanOverview.tsx` | 48-61 | Hook loading + 组件 loading 双消费 |
| `src/pages/textile/PlanOverview.tsx` | 42-60 | 独立 loading 未复用 Hook |
#### 修复方案
1. **创建统一 LoadingSpinner 组件** (`src/components/ui/LoadingSpinner.tsx`)
- 支持 sm/md/lg 尺寸、overlay 全屏遮罩、自定义文案
- 替换所有内联 Spinner 实现
2. **消除动画嵌套冲突**
- Dashboard 级别移除冗余 containerVariants motion.div
- 仅保留路由级 PageTransition 过渡
- 列表项使用轻量 opacity 动画
3. **统一数据 Loading 来源**
- 禁止组件自行管理 loading state
- 全部收敛至 HookusePlanData/useInventoryData/useDashboardData
- 组件只消费 Hook 提供的 loading/error/data
### 二、缺失共享组件清单
#### P0 - 立即实施(复用 ≥8 次)
| 组件 | 复用次数 | 替代目标 | 关键 Props |
|------|---------|---------|-----------|
| **StatCard** | 8+ | 3个 Dashboard 统计区 + AccountsPayable + PendingFabric | `icon`, `label`, `value`, `trend?`, `themeColor` |
| **EmptyState** | 15+ | 所有列表页空状态 | `icon`, `title`, `description`, `actionLabel?`, `onAction?` |
| **PageHeader** | 25+ | 所有页面头部 | `title`, `subtitle?`, `backTo?`, `actions?: ReactNode` |
| **DeleteConfirmModal** | 5+ | MemberManage, FactoryManage, PlanEditModal | `title`, `message`, `confirmLabel?`, `onConfirm`, `isLoading` |
| **LoadingSpinner** | 20+ | 全项目 | `size`, `text?`, `overlay?`, `className?` |
#### P1 - 短期实施(复用 ≥3 次,业务核心)
| 组件 | 对应数据表 | 说明 |
|------|-----------|------|
| **DashboardLayout** | - | 配置驱动的三个角色 Dashboard 统一壳子 |
| **PaymentCard/List** | `payments` | 统一采购商/水洗厂的付款展示 |
| **AccountProfileCard** | `profiles` | 统一三个角色的账户信息弹窗 |
| **ImageUploader** | - | 带预览的图片上传4+ 处重复 |
| **WashingLayout** | - | 水洗厂独立布局,解决已知架构债务 |
#### P2 - 中期完善
| 组件 | 说明 |
|------|------|
| **FormModal / BaseModal** | 统一弹窗基础样式与交互 |
| **SearchableSelect** | 可搜索下拉NewPlan/NewWashingPlan 需要 |
| **WashingProcessFlow** | 水洗流程节点可视化 |
| **FinishedProductCard** | 成品仓库卡片 |
| **CompanyRelationshipCard** | 工厂关系管理卡片 |
### 三、项目逻辑持续优化点
#### 高优先级:数据层重构
| # | 优化项 | 现状 | 建议方案 | 涉及文件 |
|---|--------|------|---------|---------|
| 1 | 统一 Dashboard 数据获取 | 3个 Dashboard 各自 fetchPlans缓存策略不一致 | 创建 `useDashboardData(role)` Hook | 3个 Dashboard.tsx |
| 2 | 消除 Prop Drilling | PlanOverview→PlanGroup→PlanCard 逐层传递 operatorNames | 创建 PlanDataContext | PlanOverview, PlanGroup, PlanCard |
| 3 | 大列表计算缓存 | textile/PlanOverview 每次渲染重建分组数据 | useMemo + 纯函数提取 | textile/PlanOverview.tsx |
| 4 | 统一错误处理 | 散落 console.error / alert | 创建 useErrorHandler Hook | 全项目 |
#### 中优先级:代码治理
| # | 优化项 | 具体行动 |
|---|--------|---------|
| 1 | 实时订阅统一 | TextilePlanOverview 直接 supabase.channel → 迁移至 useRealtime Hook |
| 2 | useEffect 依赖稳定化 | useRef 存储回调引用,避免 fetchData 引用变化触发循环 |
| 3 | 工具函数去重 | 删除 textile/PlanOverview 中的 extractParamsFromLink统一用 utils/helpers.ts |
| 4 | 常量统一 | 删除各 Dashboard 本地 statusMap统一从 utils/constants.ts 导入 |
#### 低优先级:类型安全
- 逐步替换 `any` 类型为 Supabase 生成类型 `Tables<'xxx'>`
- 重点文件usePlanData.ts, useInventoryData.ts, WarehouseManage.tsx
### 四、推荐执行顺序
```
阶段1: 基础组件 + Loading 修复
├── 创建 LoadingSpinner, EmptyState, StatCard, PageHeader, DeleteConfirmModal
├── 替换所有内联 Loading 动画
└── 移除 Dashboard 冗余 motion.div 嵌套
阶段2: 数据层重构
├── 创建 useDashboardData Hook替换3个Dashboard的数据获取
├── 创建 PlanDataContext消除 Prop Drilling
├── 添加 useMemo 缓存大列表计算
└── 创建 useErrorHandler统一错误处理
阶段3: 业务组件抽象
├── 创建 DashboardLayout 配置化组件
├── 创建 PaymentCard, AccountProfileCard, ImageUploader
├── 创建 WashingLayout解决架构债务
└── 统一实时订阅至 useRealtime
阶段4: 代码治理 + 类型安全
├── 工具函数/常量去重
├── useEffect 依赖优化
└── any 类型替换
```
### 五、审查涉及的关键文件索引
| 文件 | 相关度 | 审查发现 |
|------|--------|---------|
| `src/App.tsx:66-87` | 高 | 路由级 AnimatePresence 动画嵌套源头 |
| `src/pages/*/Dashboard.tsx` | 高 | 3个角色 Dashboard 代码重复率 ~70% |
| `src/hooks/usePlanData.ts` | 高 | 依赖项不稳定、缺少计算缓存 |
| `src/hooks/useInventoryData.ts` | 高 | 缓存策略与 usePlanData 不一致 |
| `src/hooks/useRealtime.ts` | 高 | 部分页面绕过单例直接使用 supabase.channel |
| `src/pages/purchaser/AccountsPayable.tsx` | 高 | 343行内联实现无共享组件 |
| `src/pages/washing/Dashboard.tsx` | 高 | 543行无独立 Layout架构债务 |
| `src/pages/washing/PlanOverview.tsx` | 高 | 669行内联流程组件 |
| `src/pages/purchaser/FactoryManage.tsx` | 高 | 622行内联删除确认弹窗 |
| `src/pages/MemberManage.tsx` | 中 | 878行内联删除确认 |
| `src/components/PageTransition.tsx` | 中 | 页面过渡动画定义 |
| `src/utils/helpers.ts` | 中 | 存在重复实现的工具函数 |