4359 lines
164 KiB
Markdown
4359 lines
164 KiB
Markdown
# 采购计划系统 - 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/ # 自定义Hooks(10个)
|
||
│ │ ├── 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行重复代码,头像上传功能随组件一并收敛
|
||
- 清理三个文件中不再使用的图标 import(X, 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、认证权限、分享链接、构建部署等全部问题类型
|
||
- 新增数据库诊断常用 SQL(7条)和高频 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>
|
||
|
||
<!-- 正确:移动优先 -->
|
||
<!-- 默认样式(移动端)→ sm(640px+)→ md(768px+) -->
|
||
<div class="text-sm sm:text-base md:text-lg">
|
||
移动端小字 → 平板中字 → 桌面大字
|
||
</div>
|
||
|
||
<!-- 显示/隐藏 -->
|
||
<div class="hidden sm:block">
|
||
仅在大于 640px 时显示
|
||
</div>
|
||
|
||
<div class="sm:hidden">
|
||
仅在小于 640px 时显示
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
### 分类十三:性能优化问题
|
||
|
||
#### Bug: 大数据列表渲染卡顿
|
||
**现象**:
|
||
- 列表超过 100 条时滚动卡顿
|
||
- 内存占用高
|
||
- 帧率下降
|
||
|
||
**修复方案**:
|
||
```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
|
||
- 全部收敛至 Hook(usePlanData/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` | 中 | 存在重复实现的工具函数 | |