iloom-flatten/AGENTS.md

1800 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

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