# 配方管理模块文档

## 一、功能概述

配方管理模块用于管理生产配方数据，每个配方包含配方名称、设定重量和设定误差等参数。配方数据存储在云端 MySQL 数据库，现场一体机和微信小程序均可访问。

**小程序端约束**：微信小程序（展会版）不提供配方的新增/修改/删除功能，只提供**配方选择 + 发送**功能。配方的新增/修改/删除在现场一体机（MiniUI）上完成。

## 二、功能架构

### 2.1 功能结构图

```
┌─────────────────────────────────────────────────────────────┐
│                       配方管理模块                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────┐    ┌──────────────┐                      │
│  │   小程序端    │    │   API接口    │                      │
│  │  (微信H5)    │    │  (PHP)       │                      │
│  └──────┬───────┘    └──────┬───────┘                      │
│         │                   │                               │
│         └───────────────────┘                               │
│                             │                               │
│                             ▼                               │
│                    ┌──────────────────┐                    │
│                    │   MySQL数据库    │                    │
│                    │   M_RECIPE表     │                    │
│                    └──────────────────┘                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

### 2.2 核心功能列表

> **小程序端发送配方说明**：
> - 小程序端点击"发送配方"时，从底部滑出面板选择配方并输入参数
> - 确认后调用 `POST /api/exhibition/recipe.php?action=send`
> - API 同时写入 `M_PLAN`（STATE=1，计划状态跟踪）和 `M_TRANSFER`（LOCAL_FLAG=1，待一体机执行）
> - 现场一体机定时轮询 `M_TRANSFER`，读取到待发送配方后写入 PLC 执行

## 三、数据模型

### 3.1 数据库表结构 (M_RECIPE)

| 字段名 | 类型 | 长度 | 说明 | 约束 |
|--------|------|------|------|------|
| ID | bigint | - | 主键ID | 自增 |
| RECIPE_NAME | varchar | 30 | 配方名称 | 唯一，非空 |
| WEIGHT1_SET | decimal | 8,3 | 1#设定重量(Kg) | 0 < 重量 ≤ 9 |
| TOLERANCE1_SET | decimal | 8,3 | 1#设定误差(Kg) | 0 < 误差 < 9 |
| WEIGHT2_SET | decimal | 8,3 | 2#设定重量(Kg) | 0 < 重量 ≤ 15 |
| TOLERANCE2_SET | decimal | 8,3 | 2#设定误差(Kg) | 0 < 误差 < 15 |
| WEIGHT3_SET | decimal | 8,3 | 3#设定重量(Kg) | 0 < 重量 ≤ 8 |
| TOLERANCE3_SET | decimal | 8,3 | 3#设定误差(Kg) | 0 < 误差 < 8 |
| SAVE_TIME | varchar | 14 | 保存时间 | yyyyMMddHHmmss |

> 实际表结构支持 3 组称重系统（1#/2#/3#），每套系统独立的设定重量和设定误差。

### 3.2 数据验证规则（小程序端发送配方时）

```
配方名称：
  - 不能为空

车数（BATCH）：
  - 整数，范围 1~5

设定重量（WEIGHT1_SET / WEIGHT2_SET / WEIGHT3_SET）：
  - 1#：大于0，小于等于9
  - 2#：大于0，小于等于15
  - 3#：大于0，小于等于8
  - 单位：Kg
  - 精度：1位小数

设定误差（TOLERANCE1_SET / TOLERANCE2_SET / TOLERANCE3_SET）：
  - 1#：大于0，小于9
  - 2#：大于0，小于15
  - 3#：大于0，小于8
  - 单位：Kg
  - 精度：1位小数
  - 同一秤的设定误差不能大于设定重量
```

### 3.3 发送配方相关表

| 表名 | 用途 | 写入方 | 读取方 |
|------|------|--------|--------|
| M_PLAN | 生产计划状态跟踪（STATE=1 已下达） | 小程序API | 现场 PC |
| M_TRANSFER | 配方/计划中转（LOCAL_FLAG=1 待执行） | 小程序API | 现场 PC（轮询后写入 PLC） |
| R_TRANSFER | 发送记录（现场 PC 执行后反向写入） | 现场 PC | 小程序发送记录页 |

## 四、接口文档

### 4.1 获取配方列表（展会版）

```
GET /api/exhibition/recipe.php?action=list
```

**响应示例：**
```json
{
  "code": 0,
  "message": "获取成功",
  "data": [
    {
      "id": 1,
      "recipeName": "配方A",
      "weight1": 5.5,
      "tolerance1": 1.0,
      "weight2": 3.3,
      "tolerance2": 2.0,
      "weight3": 5.5,
      "tolerance3": 3.1,
      "saveTime": "20260418120000"
    }
  ]
}
```

> 小程序端只读取 M_RECIPE，不提供新增/修改/删除功能。

### 4.2 发送配方（展会版）

```
POST /api/exhibition/recipe.php?action=send
Content-Type: application/json

{
  "recipeName": "配方A",
  "batch": 3,
  "weight1": 4.4,
  "tolerance1": 1.0,
  "weight2": 3.3,
  "tolerance2": 2.0,
  "weight3": 5.5,
  "tolerance3": 3.1
}
```

**请求参数校验规则（前端 + 后端双重校验）**：

| 参数 | 校验规则 | 错误提示 |
|------|----------|----------|
| recipeName | 非空 | 请输入配方名称 |
| batch | 整数，1~5 | 车数必须为 1~5 之间的整数 |
| weight1 | >0 且 ≤9 | 1#设定重量须大于0且不超过9 Kg |
| weight2 | >0 且 ≤15 | 2#设定重量须大于0且不超过15 Kg |
| weight3 | >0 且 ≤8 | 3#设定重量须大于0且不超过8 Kg |
| tolerance1 | >0 且 <9 | 1#设定误差须大于0且小于9 Kg |
| tolerance2 | >0 且 <15 | 2#设定误差须大于0且小于15 Kg |
| tolerance3 | >0 且 <8 | 3#设定误差须大于0且小于8 Kg |
| tolerance ≤ weight | 同一秤误差不能大于重量 | X#设定误差不能大于设定重量 |

**后端逻辑**：
1. 校验配方参数合法性（见上表）
2. 生成 `LOT_ID`（格式：`YmdHis` + 4位随机数，如 `202605052358227312`）
3. 获取当前时间 `SAVE_TIME`（格式 `YmdHis`，PHP 时区已设置为 `Asia/Shanghai`）
4. 开启数据库事务
5. 插入 `M_PLAN`（`STATE = 1` 表示已下达/运行中，`BATCH1_FINISH`/`BATCH2_FINISH`/`BATCH3_FINISH` 初始为 0）
6. 插入 `M_TRANSFER`（`LOCAL_FLAG = 1` 表示待一体机读取，含三秤重量/误差设定值）
7. 提交事务
8. 返回 `{ code: 0, message: "配方已下达", data: { lotId: "..." } }`

**双表写入说明**：
- `M_PLAN`：跟踪生产计划执行状态，现场 PC 读取后控制 PLC 执行
- `M_TRANSFER`：中转队列，`LOCAL_FLAG=1` 表示待执行，现场 PC 轮询读取后下发到 PLC
- 两表通过 `LOT_ID` 关联，确保计划与执行一一对应

## 五、界面设计

### 5.1 小程序端发送配方面板 (mobile/exhibition/monitor/index.html 内嵌底部 Sheet)

#### 面板触发方式
点击底部导航栏「发送配方」按钮，从屏幕底部滑出面板（Bottom Sheet）。面板包含遮罩层，点击遮罩层或关闭按钮可收起面板。

#### 布局结构

```
┌──────────────────────────────────────────┐
│ ┌──────────────────────────────────────┐ │
│ │ 发送配方                    ✕ 关闭   │ │  ← 面板头部
│ ├──────────────────────────────────────┤ │
│ │ 配方名称          车数               │ │  ← 第一行（两列）
│ │ ┌────────────┐    ┌────┐             │ │
│ │ │ 配方A     ▼│    │ 3 ▲│             │ │
│ │ └────────────┘    └────┘             │ │
│ │                                      │ │
│ │ 配方参数设定                         │ │  ← 第二行（三列系统卡片）
│ │ ┌─────────────┐ ┌─────────────┐      │ │
│ │ │  1#系统     │ │  2#系统     │      │ │
│ │ │ 设定重量(Kg)│ │ 设定重量(Kg)│      │ │
│ │ │   4.4       │ │   3.3       │      │ │
│ │ │ 设定误差(Kg)│ │ 设定误差(Kg)│      │ │
│ │ │   1.0       │ │   2.0       │      │ │
│ │ └─────────────┘ └─────────────┘      │ │
│ │ ┌─────────────┐                      │ │
│ │ │  3#系统     │                      │ │
│ │ │ 设定重量(Kg)│                      │ │
│ │ │   5.5       │                      │ │
│ │ │ 设定误差(Kg)│                      │ │
│ │ │   3.1       │                      │ │
│ │ └─────────────┘                      │ │
│ │                                      │ │
│ │ ┌────────────┐  ┌────────────┐      │ │
│ │ │   关闭     │  │   下达     │      │ │  ← 底部按钮
│ │ └────────────┘  └────────────┘      │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────┘
```

#### 组件详细说明

| 组件 | 类型 | ID | 说明 |
|------|------|-----|------|
| 配方名称输入框 | `<input type="text">` | `recipeNameInput` | 支持手动输入 + 下拉选择，placeholder="请输入或选择配方" |
| 配方下拉按钮 | `<button>` | `recipeDropdownBtn` | 点击展开配方列表（从 `M_RECIPE` 读取） |
| 配方下拉列表 | `<div>` | `recipeDropdown` | 动态渲染所有配方，点击某项自动填充参数 |
| 车数显示框 | `<input type="text" readonly>` | `batchInput` | 只读，显示当前车数（1~5） |
| 车数增加按钮 | `<button>` | `batchUp` | 原生 spinner 样式小三角（CSS `border` 伪元素实现） |
| 车数减少按钮 | `<button>` | `batchDown` | 同上 |
| 6 个参数输入框 | `<input type="number" step="0.1">` | `pvWeight1~3`, `pvTolerance1~3` | 按系统分组，聚焦时显示原生数字键盘 |
| 关闭按钮 | `<button>` | `cancelBtn` / `sheetClose` | 收起面板并清空输入 |
| 下达按钮 | `<button>` | `sendBtn` | 校验参数后调用 API |

#### 交互逻辑

1. **配方名称自动匹配**
   - `input` 事件：实时检测输入内容，若与配方列表中某条名称完全匹配（忽略多余空格、不区分大小写），自动填充 6 个参数
   - `compositionstart` / `compositionend`：保护中文输入法拼音组合过程，避免拼音阶段误匹配
   - `blur` 事件：输入框失焦时兜底匹配一次
   - 下拉选择：点击 ▼ 展开配方列表，点击某配方直接填充名称和参数

2. **车数步进器**
   - 初始值为 1，范围 1~5
   - 点击 ▲ 增加，点击 ▼ 减少
   - 到达边界时禁用对应按钮（`disabled` 属性）
   - 按钮样式为原生 spinner 小三角（CSS `::before` + `border` 实现），按钮容器宽 26px

3. **参数输入自动格式化**
   - 所有 6 个参数输入框在 `blur` 时自动格式化为 1 位小数：`parseFloat(value).toFixed(1)`
   - 若输入非法（如空值、非数字），`blur` 后显示空字符串

4. **面板关闭与重置**
   - 点击关闭按钮、遮罩层、或下达成功后，面板收起
   - 收起时清空配方名称和 6 个参数输入框（车数重置为 1）

5. **下达流程**
   - 点击「下达」→ 前端校验 → 显示「下达中...」并禁用按钮 → 调用 API → 成功 Toast → 收起面板

## 六、开发说明

### 6.1 文件清单（微信端相关）

| 文件路径 | 说明 |
|----------|------|
| `mobile/exhibition/monitor/index.html` | 小程序监控主页（内含"发送配方"底部弹窗面板） |
| `api/exhibition/recipe.php` | 小程序展会版配方API（`action=list` + `action=send`） |
| `api/exhibition/send-record.php` | 发送记录查询API（查询 `R_TRANSFER`） |

### 6.2 关键代码说明

#### 移动端输入验证
```javascript
// 设定重量：1# 0-9 / 2# 0-15 / 3# 0-8，最多2位小数
function restrictWeightInput(input) {
    // 只允许数字和小数点
    // 整数部分最多2位（最大15）
    // 小数部分最多2位
}

// 设定误差：1# 0-9 / 2# 0-15 / 3# 0-8，不允许负数
function restrictToleranceInput(input) {
    // 只允许数字和小数点（不允许负号）
    // 最大15
}
```

#### 发送面板 - 配方名称自动匹配
```javascript
function autoFillByRecipeName(name) {
    const key = name.replace(/\s+/g, ' ').trim().toLowerCase();
    if (!key) return;
    const recipe = recipes.find(r => r.recipeName.replace(/\s+/g, ' ').trim().toLowerCase() === key);
    if (recipe) {
        document.getElementById('pvWeight1').value = recipe.weight1.toFixed(1);
        // ... 填充其余 5 个参数
    }
}
// 三重触发：input（避开输入法组合）、compositionend、blur
```

#### 发送面板 - 参数自动格式化
```javascript
['pvWeight1','pvWeight2','pvWeight3','pvTolerance1','pvTolerance2','pvTolerance3']
    .forEach(function(id){
        document.getElementById(id).addEventListener('blur', function(){
            var val = parseFloat(this.value);
            this.value = !isNaN(val) ? val.toFixed(1) : '';
        });
    });
```

#### 发送面板 - 车数步进器
```javascript
function changeBatch(delta) {
    const input = document.getElementById('batchInput');
    let val = parseInt(input.value) || 1;
    val += delta;
    if (val < 1) val = 1;
    if (val > 5) val = 5;
    input.value = val;
    document.getElementById('batchUp').disabled = (val >= 5);
    document.getElementById('batchDown').disabled = (val <= 1);
}
```

#### 后端 - 双表事务写入
```php
$pdo->beginTransaction();

// M_PLAN: 跟踪计划执行状态
$stmt1 = $pdo->prepare("INSERT INTO M_PLAN (RECIPE_NAME, BATCH, STATE, SAVE_TIME, LOT_ID, BATCH1_FINISH, BATCH2_FINISH, BATCH3_FINISH) VALUES (?, ?, 1, ?, ?, 0, 0, 0)");
$stmt1->execute([$recipeName, $batch, $saveTime, $lotId]);

// M_TRANSFER: 待一体机读取执行
$stmt2 = $pdo->prepare("INSERT INTO M_TRANSFER (LOT_ID, RECIPE_NAME, BATCH, LOCAL_FLAG, SAVE_TIME, WEIGHT1_SET, TOLERANCE1_SET, WEIGHT2_SET, TOLERANCE2_SET, WEIGHT3_SET, TOLERANCE3_SET) VALUES (?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)");
$stmt2->execute([$lotId, $recipeName, $batch, $saveTime, $weight1, $tolerance1, $weight2, $tolerance2, $weight3, $tolerance3]);

$pdo->commit();
```

### 6.3 注意事项

1. **数据精度**：重量和误差在数据库中存储为DECIMAL类型，避免浮点数精度问题；前端显示和输入均保留 **1 位小数**
2. **时间格式与时区**：数据库中存储为字符串格式 `yyyyMMddHHmmss`。PHP 必须在 `config.php` 中设置 `date_default_timezone_set('Asia/Shanghai')`，否则服务器默认 UTC 时区会导致记录时间与北京时间相差 8 小时
3. **配方下发双表机制**：
   - `M_PLAN`（`STATE=1`）：记录生产计划状态，现场 PC 轮询读取
   - `M_TRANSFER`（`LOCAL_FLAG=1`）：待执行中转队列，现场 PC 读取后下发到 PLC
   - 两表通过 `LOT_ID` 关联，确保计划与执行一一对应
4. **权限分离**：小程序端**只读** `M_RECIPE`，不提供新增/修改/删除配方功能。配方的新增/修改/删除仅在 Web 端（MiniUI）完成
5. **WeChat WebView 缓存**：微信内置浏览器对 H5 页面有强缓存，开发阶段必须在小程序 WebView URL 后追加 `?v=${Date.now()}` 时间戳，或在微信开发者工具中清除缓存
6. **事件绑定方式**：所有交互必须使用 `addEventListener`，**禁止**使用内联 `onclick` 属性。微信 WebView 对内联 `onclick` 的兼容性极差，容易导致点击无响应或页面闪退
7. **中文输入法保护**：配方名称输入框的自动匹配功能必须监听 `compositionstart` / `compositionend` 事件，防止拼音输入过程中触发误匹配
8. **参数输入体验**：6 个参数输入框统一使用 `type="number" step="0.1"`，在移动端唤起原生数字键盘；`blur` 时自动格式化为 1 位小数

## 七、发送配方面板设计流程与开发思路

### 7.1 需求分析与设计目标

**业务场景**：展会现场，操作人员通过微信小程序查看设备状态并下达生产配方。下达后，现场一体机（Site PC）从数据库读取配方参数并写入 PLC 执行。

**核心约束**：
- 微信小程序**不能直接连接 PLC**，所有 PLC 交互必须通过数据库中转
- 配方主数据（M_RECIPE）**只能由现场一体机维护**，小程序端只读
- 操作要简单直观，适合展会现场快速演示

**设计目标**：
1. 一键触达：从监控页底部导航点击"发送配方"，直接弹出面板
2. 最小输入：选择已有配方后自动填充参数，只需调整车数和确认
3. 防错机制：所有参数前端+后端双重校验，错误即时提示
4. 数据可追溯：每次下达生成唯一 LOT_ID，同时写入 M_PLAN 和 M_TRANSFER

### 7.2 UI 设计决策

#### 为什么用底部 Sheet（ Bottom Sheet ）？

初始方案考虑过跳转新页面，但展会场景要求操作路径最短。底部 Sheet 从屏幕底部滑出，不离开当前监控页面，操作完成后立即回到监控状态查看设备响应。

#### 为什么配方名称和车数放在同一行？

两阶段迭代结果：
- **第一阶段**：配方名称独占一行，车数（批次）在其下方。屏幕空间浪费，用户需要滚动才能看到参数区。
- **第二阶段**：配方名称占 2/3 宽度，车数占 1/3 宽度，同一行并排。一屏内可完整展示所有输入项，无需滚动。

#### 为什么车数用 Stepper（步进器）而非 `<select>`？

- `<select>` 在 iOS 上唤起原生滚轮选择器，操作步骤多（点击 → 滚动 → 确定）
- Stepper 的 `+` / `-` 按钮一步完成增减，且范围固定（1~5），越界按钮自动禁用，体验更直接
- 最终迭代为原生 spinner 样式的小三角箭头（CSS border 伪元素），视觉上更精致，与深色主题融合更好

#### 为什么参数区改用系统分组卡片而非 6 宫格？

**第一阶段：6 宫格卡片**
3 套称重系统 ×（重量 + 误差）= 6 个参数，用 3×2 宫格布局。问题是每个参数独立成块，同一系统的重量和误差被拆散，视觉上不连贯。

**第二阶段：竖向系统卡片**
改为每列一个系统卡片（1#系统 / 2#系统 / 3#系统），卡片顶部带系统标题栏，内部上下排列「设定重量」和「设定误差」。同一系统的参数聚合在一起，操作更直观，也更符合"按系统配置"的心智模型。

### 7.3 交互设计决策

#### 配方名称自动匹配（三重触发）

**问题**：用户手动输入配方名称时，如何自动填充参数？

**方案演进**：
1. 仅监听 `input` 事件 → 中文输入法拼音阶段频繁触发，导致误匹配
2. 增加 `blur` 兜底 → 用户不离开输入框就不会触发
3. **最终方案**：`input`（避开输入法组合）+ `compositionend`（选字完成后）+ `blur`（失焦兜底）三重触发

**匹配逻辑**：去除多余空格、不区分大小写、trim 首尾。不强求模糊匹配（避免输入过程中频繁弹窗），只在做完输入动作后做一次精确匹配。

#### 参数自动格式化（1 位小数）

**问题**：用户输入 `4` 或 `4.44`，显示不统一，提交给 API 的数据类型也不一致。

**方案**：所有 6 个参数输入框在 `blur` 时执行 `parseFloat(value).toFixed(1)`。非法输入（空、非数字）置空，由后续校验拦截。

**为什么不限制输入过程？** 限制输入过程（如只允许数字和小数点）在移动端体验差，且不同输入法行为不一致。`blur` 时统一格式化更简单可靠。

### 7.4 数据校验设计

**为什么前后端都要校验？**
- 前端校验：即时反馈，减少无效请求，提升用户体验
- 后端校验：安全兜底，防止绕过前端直接调用 API

**校验规则如何确定？**

| 规则 | 依据 |
|------|------|
| 1#重量 >0 且 ≤9 | 1#设备物理量程限制 |
| 2#重量 >0 且 ≤15 | 2#设备物理量程限制 |
| 3#重量 >0 且 ≤8 | 3#设备物理量程限制 |
| 1#误差 >0 且 <9 | 1#工艺允许的最大误差范围 |
| 2#误差 >0 且 <15 | 2#工艺允许的最大误差范围 |
| 3#误差 >0 且 <8 | 3#工艺允许的最大误差范围 |
| 误差 ≤ 重量 | 逻辑约束：误差不能比目标重量还大 |
| 车数 1~5 | 现场设备一次最多执行 5 车 |

**为什么不校验配方名称是否存在于 M_RECIPE？** 现场可能临时输入一个尚未录入的配方名称进行测试，只要参数合法就允许下达。系统不强求名称必须在配方表中。

### 7.5 数据库设计决策（双表写入）

**为什么同时写入 M_PLAN 和 M_TRANSFER？**

系统有两套关注点的数据消费者：
- **M_PLAN（STATE 字段）**：供现场 PC 跟踪计划执行进度（待执行 → 执行中 → 已完成）
- **M_TRANSFER（LOCAL_FLAG 字段）**：供现场 PC 轮询发现"待执行"任务并写入 PLC

如果只写一张表，既跟踪状态又做任务队列，会导致：
- STATE 变化后无法区分是"新下达的"还是"执行中的"
- 一体机轮询逻辑和状态跟踪逻辑耦合

**LOT_ID 的作用**：
- 格式：`YmdHis` + 4位随机数（如 `202605052358227312`）
- 同一时间不可能出现重复（14位时间戳精确到秒 + 0000~9999 随机）
- M_PLAN 和 M_TRANSFER 通过 LOT_ID 关联，确保计划与执行一一对应

**事务保证**：`$pdo->beginTransaction()` → 插入 M_PLAN → 插入 M_TRANSFER → `$pdo->commit()`。任一步骤失败整体回滚，避免数据不一致。

### 7.6 开发中的关键问题与解决方案

#### 问题 1：WeChat WebView 强缓存

**现象**：修改 H5 代码后，微信预览仍然显示旧版本。

**根因**：微信内置浏览器对静态资源有强缓存策略，默认按 URL 缓存。

**解决**：
- 开发阶段：小程序 WebView URL 追加 `?v=${Date.now()}` 时间戳
- 生产阶段：通过构建工具生成带 hash 的文件名
- 紧急刷新：微信开发者工具 → 清缓存 → 重新编译预览

#### 问题 2：内联 onclick 导致点击无响应

**现象**：部分按钮点击后没有任何反应，或点击后页面闪退。

**根因**：微信 WebView 对内联 `onclick="xxx()"` 的 JavaScript 执行环境不稳定，尤其在 iOS WKWebView 中，内联事件处理函数可能在某些场景下被安全策略拦截。

**解决**：所有交互统一使用 `addEventListener`，彻底移除 HTML 中所有 `onclick` 属性。

#### 问题 3：中文输入法误匹配

**现象**：输入"配方A"（拼音输入法），刚输入 "peifang" 时就触发了匹配逻辑，导致错误填充。

**根因**：`input` 事件在中文拼音输入过程中也会触发（每次拼音字母变化都触发）。

**解决**：监听 `compositionstart`（开始拼音组合）和 `compositionend`（选字完成）事件，在拼音组合过程中屏蔽 `input` 事件的匹配逻辑。

#### 问题 4：PHP 时区导致记录时间差 8 小时

**现象**：晚上 23:00 下达配方，数据库 SAVE_TIME 显示 15:00。

**根因**：服务器 PHP 默认时区为 UTC，比北京时间晚 8 小时。`date('YmdHis')` 返回的是 UTC 时间。

**解决**：在 `api/exhibition/config.php` 入口文件顶部添加 `date_default_timezone_set('Asia/Shanghai')`，确保所有 `date()` 调用使用北京时间。

#### 问题 5：Stepper 按钮在微信中不显示

**现象**：车数区域只显示数字"1"，`+` / `-` 按钮完全看不见。

**根因**：`.stepper` 容器设置了 `overflow: hidden`，且 `.stepper-btns` 没有固定宽度，在某些 WebView 渲染引擎中被压缩到 0 宽度。

**解决**：
- 移除 `.stepper` 的 `overflow: hidden`
- `.stepper-btns` 设置固定宽度 `width: 26px` + `flex-shrink: 0`
- 按钮用 CSS `::before` + `border` 技巧画小三角，替代文字符号

### 7.7 最佳实践总结

1. **移动端表单**：用 `type="number" step="0.1"` 唤起数字键盘，`blur` 时统一格式化
2. **事件绑定**：微信 WebView 中只用 `addEventListener`，禁用内联 `onclick`
3. **中文输入**：涉及实时匹配的功能必须处理 `compositionstart` / `compositionend`
4. **时区管理**：PHP 入口文件强制设置 `Asia/Shanghai`，避免 UTC 偏差
5. **缓存策略**：H5 页面 URL 必须带版本号参数，防止微信缓存旧代码
6. **双表设计**：计划状态跟踪和任务队列分离，通过 LOT_ID 关联，事务保证一致性

## 八、更新记录

| 日期 | 版本 | 更新内容 |
|------|------|----------|
| 2026-05-06 | v1.4 | 参数区改为竖向系统卡片（1#系统/2#系统/3#系统），标题栏商务灰蓝色渐变，卡片背景蓝灰渐变；发送面板增加拖拽条、输入框聚焦光晕、底部按钮阴影等美化 |
| 2026-05-06 | v1.3 | 各系统独立量程：1#重量≤9/误差<9、2#重量≤15/误差<15、3#重量≤8/误差<8，同步更新前端校验与文档 |
| 2026-05-05 | v1.2 | 发送配方面板定稿：步进器改原生 spinner 三角箭头、参数标签统一(Kg)、删除独立单位标签、6项参数校验规则（重量>0≤10/误差>0<10/误差≤重量）、配方名称三重自动匹配（input/compositionend/blur）、PHP时区修复为 Asia/Shanghai、补充开发注意事项（缓存/onclick/输入法/双表写入） |
| 2026-04-29 | v1.1 | 补充小程序展会版发送配方功能，明确小程序端只读不写配方主数据 |
| 2026-04-01 | v1.0 | 配方管理模块初始版本 |

## 九、相关文档

- [系统架构文档](../../architecture/系统架构文档.md)
- [API接口文档](../../api/README.md)
