# TODO — Flutter Wisetronic Dart 3 空安全迁移

> ## ✅ Web 构建已成功！（2026-08-01）
> **`flutter build web --no-tree-shake-icons` → ✓ Built build/web/（main.dart.js 4.5M）**
> - `dart analyze lib`: 仅剩 **6 error**，全部在 `utils/util_io.dart`（原生路径，Web 已排除，见 §5 延后）。
> - Web 可达错误: **0**。
> - 峰值 2374 → 6（全部 util_io 原生）。0 语法损坏。分支 `dev2`。
> - 构建脚本: `./build_web.sh`（构建）/ `./build_web.sh run`（Chrome 运行）。
> - **关键修复**: `cross_file` override 到 `0.3.4+2`（旧版 0.3.1+4 用了 Dart3 移除的 `UnmodifiableUint8ListView`；0.3.5+ 需 Dart 3.10）。

---

## 0. 快速恢复（Resume 起点）

```bash
cd /Users/peima/StudioProjects/flutter_wisetronic
git log --oneline -1                       # 确认在最新提交
dart analyze lib | tail -5                 # 当前: 仅 6 error(util_io.dart)
./build_web.sh                             # 构建: ✓ Built build/web/
./build_web.sh run                         # Chrome 运行调试

# 重跑自动修复脚本（安全，带损坏自检，可随时跑）—— 已无 web 可达错误，通常无需再跑
python3 tools/nullfix.py --max-rounds 12
```

**核心原则**：每次改动后用 `dart analyze lib | grep -cE '\s+error\s+-'` 数错误；脚本若报"检测到语法损坏"会自动中止，`git checkout -- lib` 回滚。

---

## 1. 剩余错误分布（118 个）

| 规则 | 数量 | 说明 |
|------|------|------|
| `unchecked_use_of_nullable_value` | 44 | 可空值当非空用（方法调用/条件/迭代器，多为多行） |
| `invalid_assignment` | 34 | 赋值类型不匹配（复杂表达式） |
| `argument_type_not_assignable` | 22 | 参数类型不匹配（三元/比较/Map字面量） |
| `return_of_invalid_type_from_closure` | 4 | 闭包返回类型 |
| `undefined_getter` | 3 | 多为 `.buttonColor`（Flutter 3 移除） |
| `return_of_invalid_type` | 3 | 函数 `return null` 但返回类型非空 |
| `undefined_named_parameter` | 2 | |
| `undefined_method` | 2 | |
| `creation_with_non_type` | 2 | 类名拼错（util_io 内） |
| `extra_positional_arguments_could_be_named` | 1 | |
| `body_might_complete_normally` | 1 | |

**错误最多的文件**（详细见 `dart analyze lib`）：

```
 6  utils/util_io.dart                 ← 延后（仅原生，Web 构建已排除）
 5  widgets/desktop/desktop_edit_address.dart   ← .trim() 多行（见 §3，已知修法）
 5  widgets/mobile/mobile_edit_address.dart     ← 同上
 5  widgets/general/add_remove_button.dart
 5  widgets/general/parabolic_animation_widget.dart
 5  widgets/general/sliding_up_panel.dart
 4  widgets/desktop/desktop_me.dart
 4  widgets/mobile/mobile_attribute_selection.dart
 4  widgets/mobile/mobile_order_detail.dart
 3  widgets/desktop/desktop_coupons.dart / desktop_my_support / desktop_order_detail / shop_products
 3  widgets/general/animation_point_manager / popup_animation_widget
 3  widgets/mobile/mobile_coupons / mobile_me / mobile_my_support / mobile_orders / shopping_cart_bar
 2  约 12 个文件（blog/orders/product_item/shopping_cart 等）
 1  约 25 个文件（单点错误）
```

---

## 2. 推荐处理顺序

1. **edit_address（5+5）** — 已知修法，见 §3，最快。
2. **parabolic/popup_animation（5+3）** — `late` 字段已修，可能还有 unchecked_use（offset 计算方法调用），手动看。
3. **sliding_up_panel（5）** — 第三方面板 widget 适配，手动。
4. **add_remove_button（5）** — badges 相关，手动。
5. **散落的 2-error / 1-error 文件** — 用 `dart analyze lib/<file>` 逐个看，多为加 `!`。
6. **util_io.dart（6）** — 最后处理（见 §5），不影响 Web 构建。

**目标**：先清掉 Web 可达错误（`flutter build web` 的 112），让 Web 能跑；`util_io` 原生路径可延后。

---

## 3. 已知具体修法（直接套用）

### 3.1 edit_address `.trim()` 多行（desktop + mobile，各 5 处）
错误：`A nullable expression can't be used to invoke 'trim'`。代码结构：
```dart
validator: (String? value) {
  if (value          // ← value 是 String?
      .trim()
      .isEmpty) {
```
**修法**：把 `if (value` 行改成 `if (value!`。
```python
# 上次脚本 bug：用了 rstrip() 应该用 strip()。正确版：
for p in ['lib/widgets/desktop/desktop_edit_address.dart','lib/widgets/mobile/mobile_edit_address.dart']:
    lines=open(p).read().split('\n'); n=0
    for i in range(len(lines)):
        if lines[i].strip()=='if (value' and lines[i+1].lstrip().startswith('.trim()'):
            lines[i]=lines[i].rstrip()+'!'; n+=1   # 注意 rstrip() 保留前导缩进
    open(p,'w').write('\n'.join(lines))
```
（`.buttonColor`→`.colorScheme.primary` 已在两个文件修好。）

### 3.2 通用模式（手动加 `!` / `?? 默认` / `== true`）
- `String? → String` 参数：`field` → `field!`（确认运行时非空）。
- `num?/int?/double?` 比较：`subtotal > minPrice` → `subtotal > minPrice!` 或 `(minPrice ?? 0)`。
- `bool?` 当条件：`if (x)` → `if (x == true)`；`if (!x)` → `if (x != true)`。
- `return null;` 但返回类型非空：把函数返回类型加 `?`（如 `CategoryProducts? foo()`）。
- 局部变量赋值 nullable：声明加 `?`（如 `Map<String,dynamic>? x = ...`），下游 nullfix 会补 `!`。
- 多行方法调用 `.toUpperCase()` / `.trim()`：接收者在上一行末尾，在该行末尾加 `!`。

### 3.3 可复用的定向脚本（一次性，贴进 bash 跑）

**a) 条件修复（analyzer 驱动，只修简单 `if (chain)`）** — 已用过，安全：
```python
python3 -c "
import re,subprocess
ERR=re.compile(r'\s+error\s+-\s+([\w/.\-]+):(\d+):(\d+)\s+-\s+(.*?)\s+-\s+([a-z_]+)\s*\$')
pat=re.compile(r'^(\s*if \()(!?)([\w.!\[\]]+)(\)\s*\{?\s*)\$')
out=subprocess.run(['dart','analyze','lib'],capture_output=True,text=True).stdout
tg={}
for ln in out.splitlines():
    m=ERR.match(ln)
    if m and 'used as a condition' in m.group(4): tg.setdefault('lib/'+m.group(1),set()).add(int(m.group(2)))
n=0
for p,ls in tg.items():
    L=open(p).read().split('\n');ch=False
    for i,x in enumerate(L,1):
        if i in ls:
            m=pat.match(x.rstrip('\n'))
            if m: L[i-1]=m.group(1)+m.group(3)+(' != true' if m.group(2) else ' == true')+m.group(4);ch=True;n+=1
    if ch: open(p,'w').write('\n'.join(L))
print('fixed',n)
"
```

**b) 局部变量改可空（严格类型，已验证安全）** — 跑完再 `python3 tools/nullfix.py` 吃级联：
```python
python3 -c "
import re,subprocess
ERR=re.compile(r'\s+error\s+-\s+([\w/.\-]+):(\d+):(\d+)\s+-\s+(.*?)\s+-\s+([a-z_]+)\s*\$')
NAMEQ=re.compile(r\"'([^']+)'\"); TYPE=r'(?:[A-Z]\w*(?:<[^;]*>)?|int|double|String|bool|num|dynamic|List<[^;]*>|Map<[^;]*>|Set<[^;]*>)'
out=subprocess.run(['dart','analyze','lib'],capture_output=True,text=True).stdout;tg={}
for ln in out.splitlines():
    m=ERR.match(ln)
    if m and m.group(5)=='not_assigned_potentially_non_nullable_local_variable':
        nm=NAMEQ.search(m.group(4))
        if nm: tg.setdefault('lib/'+m.group(1),[]).append((int(m.group(2)),nm.group(1)))
dp=re.compile(r'^(\s+(?:final\s+|const\s+)?)('+TYPE+r')\s+(\w+)\s*;\s*\$');n=0
for p,it in tg.items():
    L=open(p).read().split('\n');ch=False;done=set()
    for ul,nm in it:
        if (ul,nm) in done: continue
        done.add((ul,nm))
        for i in range(ul-2,-1,-1):
            m=dp.match(L[i])
            if m and m.group(3)==nm and '?' not in m.group(2): L[i]=m.group(1)+m.group(2)+'? '+nm+';';ch=True;n+=1;break
    if ch: open(p,'w').write('\n'.join(L))
print('fixed',n)
"
```

---

## 4. 工具脚本说明（`tools/`）

### `tools/nullfix.py` — 主力自动修复
```bash
python3 tools/nullfix.py              # 修整个 lib，默认 5 轮
python3 tools/nullfix.py --max-rounds 12
python3 tools/nullfix.py lib/widgets/mobile/shop.dart   # 只修单文件
```
- 每轮重新 `dart analyze lib`，按 error 的 (file,line,col) 精确定位修改。
- **带腐败自检**：每轮后检测 `expected_token`/`missing_identifier`，一旦 >0 立即中止并提示 `git diff`。
- 已实现的 handler：
  - `insert_bang`：成员访问/方法调用/迭代器/return 加 `!`（按 analyzer 分类）
  - `insert_arg_bang`：`argument_type`/`invalid_assignment` 在参数值末尾加 `!`（值必须是简单标识符链 `[\w.\[\]()!]`，含运算符/空格跳过）
  - `late` 实例字段、参数空安全、field-formal 字段空安全
  - `missing_default_value_for_parameter`、`@required`→`required` 已并入
- **已禁用的危险 handler**：基于正则的 `if(x)`→`if(x==true)`（曾破坏 89 文件）。条件修复改用 §3.3a 的 analyzer 驱动版。
- **已知边界**：多行表达式（接收者跨行）、含空格/逗号的函数调用参数、三元/比较表达式 → 跳过，需手动。

### `tools/badge_migrate.py` — 未验证，**已不需要**
badges 3.x 迁移已手动完成（`badgeStyle`/`badgeAnimation`），此脚本可忽略/删除。

---

## 5. 延后项

### `lib/utils/util_io.dart`（6 error，仅原生）
- 通过 `dart.library.html` 条件导入从 **Web 构建中排除**，不影响 `flutter build web`。
- 错误来自 `flutter_local_notifications` v17 API 变更：
  - `IOSInitializationSettings` / `IOSNotificationDetails` → v17 改名（`DarwinInitializationSettings` / `DarwinNotificationDetails`）
  - `onSelectNotification` 参数已移除
  - `extra_positional_arguments`（构造参数变化）
  - `body_might_complete_normally`（`Future<void>` 缺 return）
- 原生（Android/iOS）构建时才需要修。Web 不阻塞。

---

## 6. 关键上下文 / 决策（避免踩坑）

- **`late` 用于实例字段**：避免模型 `fromJson` 初始化列表 + State 类的级联错误。模型字段置为可空，调用点加 `!`。
- **脚本必须 analyzer 驱动**：盲目正则会损坏代码（如 `return? item;`、`this.?field`）。`nullfix.py` 用 analyzer 的列/列定位 + 腐败自检是关键。
- **badges 实际解析 3.2.0**（不是 2.0.3）：参数是 `badgeStyle:`/`badgeAnimation:`（不是 `badgeColor`/`animationType`/`animation`）。
- **pinput 实际 5.0.2**：类名 `Pinput`（非 `PinPut`），导入 `package:pinput/pinput.dart`，`length`/`onCompleted`/`defaultPinDecoration`。
- **youtube_player_iframe 5.2.2**：`YoutubePlayerController.fromVideoId(videoId:, startSeconds:, params:)`。
- **BreadCrumb.route 已改可空**：`String? route`，调用点 `BreadCrumb(X, null)` 合法。
- **e-Transfer**：Stripe 已全移除，付款走 `payment@wisetronic.com`，备注写 `R#<receipt.id>`。
- **搜索框**：自写轻量 `lib/widgets/general/search_bar.dart`（非第三方包）。
- **不升级全局 Flutter SDK**：用本地 Flutter 3.29.3 / Dart 3.7.2。

## 7. 提交历史（基线 f8a90ad 之后）

```
最新  phase3: mobile/shop clean ... 131->118   ← resume 起点
     phase3: checkout files clean ... 142->131
     phase3: fix attribute widgets ... ~30 fixed
     phase3: FIX arg-bang regex (missing \]) ... 235->183   ← 关键 bug 修复
     ...（共约 28 个 phase3 提交）
f8a90ad  基线
```

## 8. 最终验收

```bash
flutter build web        # 目标：无 error，生成 build/web
flutter run -d chrome    # 目标：能打开、首页/商品/购物车/结账流程跑通
```

---

## 9. 运行时问题（应用已能运行，以下为后续优化，非阻塞）

应用 `flutter run -d chrome` 已成功启动并加载首页/API 数据/数据库。控制台有以下运行时警告/异常：

### 9.1 Scrollbar 缺 ScrollPosition（真实，影响滚动条显示）
4 个 `Scrollbar(` 未配 `controller`，新版 Flutter 的 Scrollbar 找不到 ScrollPosition：
- `lib/pages/home.dart:101` — ✅ 已修：给 `SingleChildScrollView` 加 `primary: true`
- `lib/pages/blog.dart:24` — 子是 `DesktopBlog`（自定义组件）
- `lib/pages/download.dart:63` — 子是 `DesktopDownloadApps`（自定义组件）
- `lib/widgets/desktop/shop.dart:137` — 包裹 `NotificationListener`

后 3 处的子是自定义组件，需让内部 ListView/ScrollView 暴露 controller（设 `primary: true`）或 Scrollbar 改用 `NotificationListener` 关联。**仅影响滚动条渲染，不影响内容加载/显示。**

### 9.2 CustomersLogo setState-after-dispose（小 bug）
`lib/widgets/desktop/desktop_customers_logo.dart` 的 `Timer.periodic`（每 10s 刷新 logo）未在 `dispose()` 里 `cancel()`，组件卸载后 timer 仍触发 `setState`。建议在 `dispose` 里 `timer?.cancel()`。**热重载时易触发，干净启动偶发。**

### 9.3 `_business` LateInitializationError（热重载假象）
`_business` 是店铺页面组件（`shop_promote.dart`/`shop.dart` 等）的 `late Business` 字段。热重载重建 widget 树时字段状态丢失触发，**干净启动（浏览器刷新 Cmd+R）不会出现**。

> **验证方式**：在浏览器里 Cmd+R 刷新页面（而非热重载），9.2/9.3 类异常应消失，仅剩 9.1 的滚动条警告。
