1800 lines
55 KiB
Markdown
1800 lines
55 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 | 依赖管理 |
|
||
|
||
### 项目目录结构
|
||
|
||
```
|
||
/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>
|
||
|
||
<!-- 正确:移动优先 -->
|
||
<!-- 默认样式(移动端)→ 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`
|
||
|
||
---
|
||
|
||
## 学习反思与最佳实践
|
||
|
||
### 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` |