# TaskProgress · 可复制 React 组件

用于订单详情中的多任务进度区域。界面只消费标准模型，业务名称、状态文案、节点和备注均由外部传入；不绑定餐饮、酒店、ITS 或 Business_task 的原始结构。

## 1. 复制与运行

将整个 `task-progress` 文件夹复制到 React 项目，例如 `src/components/task-progress/`。运行时仅依赖 **React 18 的 hooks**，没有 UI 库、图标库、字体、图片、网络地址或项目别名依赖。宿主负责安装 React / React DOM，并用构建工具处理 JSX 和 CSS。

- TypeScript 项目选择 TS / TSX 版本；JavaScript 项目选择 JS / JSX 版本。JS 版从同一源码自动生成，行为和 CSS 一致。
- **生产页面需显式导入一次 `task-progress.css`**；组件本身不会偷偷注入全局样式。
- 文件内引用均为相对路径。不要只复制 `TaskProgress.tsx`，漏掉图标、文案、类型或 CSS。
- `examples/Demo` 使用本地数据，能直接运行。`examples/WithITS` 演示接真实服务，需要传入你自己的已鉴权查询函数。

最快试用（两个版本的导入写法相同）：

```tsx
import { Demo } from './components/task-progress/examples/Demo';

export default function App() {
  return <div style={{ maxWidth: 390, margin: '24px auto' }}><Demo /></div>;
}
```

Demo 自己导入 CSS，并用本地延迟模拟查询；刷新不会伪造业务状态推进。它不连接真实订单。

## 2. 接入已有请求状态

```tsx
import { TaskProgress } from './components/task-progress';
import './components/task-progress/task-progress.css';

// model / loading / error / reload 来自你的页面或请求库。
<TaskProgress
  model={model}
  loading={loading}
  error={error}
  reference={{ label: '凭证编号', value: orderReference }}
  onRefresh={reload}
/>;
```

`model` 和 `onRefresh` 可直接传入，也可通过 `componentProps` 整包传入。组件是受控展示层：点击刷新调用回调，旋转由 `loading` 驱动，不自行请求接口。使用已有请求库时，不必再调用 `useTaskProgress`。

| 参数 | 默认值 | 含义 |
| --- | --- | --- |
| `componentProps` | 无 | 整包属性对象，支持部分字段；同名字段优先于单项配置，未传入的字段沿用单项配置 |
| `model: TaskProgressModel \| null` | `null` | 标准任务数据；未取得数据时传 `null`，也可通过 `componentProps` 提供 |
| `onRefresh: () => void` | 无 | 宿主的刷新操作；异步函数也可直接传入，也可通过 `componentProps` 提供；未配置时点击刷新不触发查询 |
| `loading` | `false` | 刷新图标旋转、按钮禁用；旧任务可继续显示 |
| `error` | `false` | 无数据时显示错误，有数据时标记为旧结果并停止光晕 |
| `forbidden` | `false` | 访问被撤销，优先隐藏所有旧任务、编号和业务标题，显示访问提示 |
| `updatedAt: Date \| null` | `null` | 成功查询记录，用于辅助阅读更新播报；不显示更新时间 |
| `reference: { label, value }` | 不显示 | 独立订单凭证；顶部只显示 `value`，`label` 供辅助阅读 |
| `locale` | `'zh-CN'` | 内置中文；以 `en` 开头使用英文，其余回退中文 |
| `messages` | 内置文案 | 覆盖公共交互文案，类型为 `Partial<TaskProgressMessages>` |
| `initialVisible` | `2` | 首屏任务数，多余任务可展开；小数向下取整，至少 1，非有限值回退 2 |
| `motion` | `'auto'` | `'off'` 关闭光晕和刷新动画；始终尊重系统降动效偏好 |
| `className` / `style` | 无 | 宿主样式与 `--tp-*` 变量；不会覆盖组件的基础类名 |

无任务、无异常、无编号且不在加载或失败时，组件不占空间。已知编号在加载、空任务、刷新失败及任务完成时持续显示。**切换订单时，宿主必须同步更换或清除 `reference`**；不要拿订单 ID、任务 ID 代替凭证编号。切换到不同订单时不会沿用其他订单的展开状态；如需每次切换订单或数据源均重置局部 UI，使用 `key={requestKey}`。

### 在 lowcode 面板中绑定整体 Props

选中组件，在属性面板顶部找到「整体 Props」，切换到变量绑定，填写 `this.state.taskProgressProps`，或按页面实际变量填写表达式：

```js
({
  model: this.state.taskProgressModel,
  loading: this.state.loading,
  error: this.state.error,
  forbidden: this.state.forbidden,
  updatedAt: this.state.updatedAt,
  onRefresh: this.refreshTaskProgress,
  initialVisible: 2
})
```

包含 `onRefresh`、`messages.showAll` 等函数或 `updatedAt: Date` 时使用变量绑定，传入实际函数和 `Date` 对象；纯数据可直接使用 JSON 设置器。

React 中也支持仅通过整包对象传参：

```tsx
<TaskProgress componentProps={{ model, loading, error, onRefresh: reload }} />
```

合并顺序为 `{ ...单项属性, ...componentProps }`，然后应用组件默认值。`componentProps` 为 `null` 或未设置时沿用单项属性。对象中的 `false`、`model: null` 等显式值会覆盖单项配置；未包含的字段沿用单项配置。`style`、`messages`、`reference` 和 `model` 按整个字段替换，不做深层合并。

## 3. 标准模型

```ts
const model: TaskProgressModel = {
  orderId: 'order-001',
  title: '任务进度', // 辅助阅读名称，顶部不显示此标题
  issues: [],
  tasks: [{
    id: 'task-001',
    title: '服务项目',
    subtitle: '标准规格',
    quantity: '1',
    remark: { label: '备注', value: '按约定方式处理' },
    validity: 'active',
    status: { label: '处理中', tone: 'active' },
    nodes: [
      { id: 'queued', label: '待处理', state: 'done' },
      { id: 'processing', label: '处理中', state: 'current' },
      { id: 'ready', label: '待确认', state: 'upcoming' },
      { id: 'completed', label: '已完成', state: 'upcoming' },
    ],
  }],
};
```

JS 版去掉 `: TaskProgressModel` 类型声明即可。完整类型从 `index.ts` 导出。

| 字段 | 约定 |
| --- | --- |
| `orderId`、`task.id`、`node.id` | 使用字符串；任务 ID 在订单内唯一，节点 ID 在任务内唯一。不要用数组位置作为业务身份 |
| `tasks` / `issues` | 必须是数组；没有内容传 `[]`。模型应由宿主校验或转换后提供 |
| `status.label` | 客户可见文案，组件不通过文案判断样式 |
| `status.tone` | `waiting` 黄色、`active` 强调色、`ready` 绿色、`complete` 灰色、`neutral` 中性、`warning` 异常 |
| `validity` | `active` 表示记录有效，**不是正在处理**；`inactive` / `unknown` 保留摘要，不画正常流程 |
| `nodes` | 可省略；没有可靠历史时只展示状态摘要。最多四个横排，更多节点纵排 |
| `node.state` | `done`、`current`、`upcoming`、`skipped`、`unknown`；外部明确给出，不按数组位置补造历史 |
| `quantity` | 可选字符串，支持整数／小数显示 |
| `remark` | 可选；空白不占空间，保留换行。仅传入已确认可对客展示的备注 |
| `fields` | 可选 `Array<{label, value, emphasis?: 'primary'}>`；只传入必要的业务字段 |
| `notice` | 可选简短业务提示；失效任务和旧数据状态下隐藏 |
| `issues` | 转换异常列表 `{code, recordId?}`；组件只显示通用降级提示，不直接暴露内部错误码 |

节点和状态由模型控制。组件不推进任务，不按时间、打印结果或数值大小猜进度。完成终点需同时满足：任务有效、`tone='complete'`、节点全部完成或跳过；突出最后一个完成节点，但其语义仍是 `done`。一个任务完成不代表整单已可领取。

## 4. 可选请求 Hook

```tsx
import { TaskProgress, useTaskProgress, ProgressAccessError } from './components/task-progress';
import './components/task-progress/task-progress.css';

// 在已取得当前订单上下文的组件中使用。
const requestKey = JSON.stringify([shopId, orderId, source, configVersion, locale]);
const progress = useTaskProgress(requestKey, async (signal) => {
  // queryTasks 是宿主服务函数，应传递 signal，并返回标准模型。
  // 已撤销权限时抛出 new ProgressAccessError()；普通失败抛 Error。
  return queryTasks({ shopId, orderId, source, signal });
});

<TaskProgress key={requestKey} {...progress} reference={orderReference}
  locale={locale} onRefresh={progress.refresh} />;
```

- 首次挂载和 `requestKey` 变化时查询；`refresh()` 手动重查。没有轮询、自动推进或内置 URL。
- 同一请求进行中去重；切单／切源时立即屏蔽旧状态，取消旧请求并拒绝迟到响应覆盖。卸载时取消请求。
- `loader` 函数身份变化不会自动重查，避免内联回调造成循环；改变查询含义时要改变 `requestKey`。它应包含租户、订单、源、配置版本和影响结果的语言。
- 普通刷新失败保留最后一次成功的数据并标记过期；重试进行中仍保持过期状态，只有成功才清除。`ProgressAccessError` 则清空数据。
- 身份、授权和 HTTP 返回格式由宿主负责。请先取得有效订单再挂载；不要把缺失订单拼成一个请求键去查询。

## 5. ITS 与后续数据源

`fromITS(rows, context)` 接受**已鉴权接口返回的记录数组**，不接受 SQL 字符串或接口外层 envelope。见可复制的 `examples/WithITS.tsx`。

```ts
const model = fromITS(rows, {
  shopId: '8660',
  orderId: '7333305',
  locale: 'zh-CN',
  detailNames: {
    '17434473': {
      title: '已核对的订单项目',
      subtitle: '标准规格',
      remark: { label: '备注', value: '对客备注' },
    },
  },
  statuses: {
    0: { label: '未备', tone: 'neutral' },
    1: { label: '已备', tone: 'active' },
    2: { label: '售罄', tone: 'warning' },
    3: { label: '已叫号', tone: 'active' },
    4: { label: '已划', tone: 'neutral' },
    99999: { label: '已移除', tone: 'warning' },
  },
});
```

此示例仅展示源状态，不是“待制作 → 制作中 → 待取餐 → 已完成”的推断映射；正式对客文案需由业务确认。转换器的边界：

- 按 `resource_type='order'`、`resource_id`、`shop_id` 过滤；仅做数据投影，不替代服务端鉴权。
- `extras` / `metadata` 接受对象、JSON 字符串、空数组；坏 JSON 记录 issue，保留其他可用进度。
- 0 是有效状态；未知／空值不默认归零。售罄和移除始终为无效任务，不能通过配置伪装为正常完成。
- 默认排除软删除；重复记录报告异常。ID 大整数应由接口返回字符串，避免 JS 数值精度丢失。
- 名称优先匹配当前授权订单中的 `detail_id`；其次使用有关联的 bundle 快照名称。不要只按 `product_id` 匹配项目。
- 备注仅来自宿主明确投影的 `detailNames[detail_id].remark`；不读取厨房内部备注来猜测。
- 当前样例缺少可信节点历史，所以转换器不会生成节点。不读取打印次数、`is_closed`、时间或资源状态来推进流程。
- 非中英文项目可通过 `fallbackLabels: {unnamed, unknown}` 配置源数据降级文案。

后续 Business_task 只需新增自己的转换器并输出 `TaskProgressModel`，展示组件无需修改。当前没有伪造 Business_task 的原始接口结构。

## 6. 文案、样式与当前效果

```tsx
<TaskProgress model={model} onRefresh={reload}
  locale="en"
  messages={{ refresh: 'Reload', title: 'Service progress', showAll: n => `Show ${n} more` }}
  className="my-order-tasks"
  style={{ '--tp-sticky-top': '56px', '--tp-accent': '#365f9c',
    '--tp-active-text': '#274c82', '--tp-active-bg': '#edf3fb' }}
/>
```

`messages` 只覆盖公共交互文字；任务／状态／节点／字段／备注文字来自模型。内置 zh/en 以外的语言请完整覆盖需要使用的公共文案。所有公开类型（包括 `TaskProgressMessages`、`TaskProgressStyle`、`ITSContext`）均可从入口导入。

主要 CSS 变量：`--tp-surface`、`--tp-text`、`--tp-muted`、`--tp-border`、`--tp-accent`、`--tp-active-text`、`--tp-active-bg`、`--tp-waiting` / `--tp-waiting-bg`、`--tp-ready` / `--tp-ready-bg`、`--tp-warning` / `--tp-warning-bg`、`--tp-neutral-bg`、`--tp-sticky-top`。颜色和对应底色应成对调整并检查对比度。基础颜色保留对既有 `--customer-*` 变量的回退兼容，但不要求宿主定义它们。

当前效果保留：只显示编号值、纯图标刷新、静态状态图标、无背景的节点文字、四种状态一致的柔和光晕呼吸、备注与紧凑任务行。光晕只作用于当前节点或确认完成的终点，历史节点和后续节点保持静态。刷新期间光晕暂停、刷新图标旋转；失败时停止光晕。

CSS 只使用 `.task-progress` / `.tp-*` 命名，不修改 `body`、全局字体或全局按钮。不要复用 `.tp-*` 作为宿主业务类名。顶部只在组件范围内吸顶，固定导航的偏移用 `--tp-sticky-top`；最终滚动容器由宿主决定。

## 7. 文件职责与验证范围

| 文件 | 职责 |
| --- | --- |
| `TaskProgress.tsx` / `.jsx` | 纯受控展示，唯一的产品组件 |
| `task-progress.css` | 独立样式；无外部资源 |
| `icons.tsx` / `.jsx` | 静态 SVG；内部实现，不作为公共组件 API |
| `types.ts` | 标准模型和访问错误类型；JS 版 `types.js` 只保留运行时错误类 |
| `texts.ts` / `.js` | 默认公共文案 |
| `useTaskProgress.ts` / `.js` | 可选请求生命周期 |
| `adapters/its.ts` / `.js` | 可选 ITS 投影 |
| `index.ts` / `.js` | 公开入口 |
| `examples/` | 本地可运行示例和真实服务接入示例，不被组件自动引用 |

面向 React Web 和现代浏览器，使用 CSS 变量、Grid 与容器查询。SSR 初次渲染不访问 window 或 document；实际请求在 effect 内运行。使用 React Server Components 的框架时，应从带 `'use client'` 的宿主组件导入；CSS 按框架要求放在全局入口／布局导入。

本次检查针对复制源码的 React 18.3.1 独立项目、TS/JS 两种版本及经典／自动 JSX 编译方式；真实业务 API、正式 Business_task 转换、实体手机与特定 Next.js 版本仍由接入项目验收。不要把本地 Demo 当作生产订单服务。
