## 目标与最终效果

这一章完成 **资料统计自动化任务**：接收文本、分批处理、显示实际进度、支持取消，完成后显示结果。它展示一个任务组件的最小闭环，后续可替换为自己的业务阶段。

示例不调用模型或外部进程，不持久化项目。取消通过运行身份停止后续批次；接入实际进程后，必须额外连接该进程真实的停止机制。

[下载完整任务源码](/official-design-v20261028u/example-task.zip)

## 目录和模块顺序

```text
my-example-task/
  manifest.json
  main.js
  modules/state.js
  modules/actions.js
  modules/view.js
  styles/workspace.css
  .tests/actions.test.cjs
  README.txt
```

先加载状态、再加载操作、再加载视图，最后注册入口。按[环境准备](environment.html)放进开发版的任务扩展目录，重新扫描、启用并打开。

## 第一步：声明任务类别


```json
{
  "id": "my-example-task",
  "name": "资料统计示例",
  "version": "1.0.0",
  "apiVersion": 1,
  "minAppVersion": "2.7.5",
  "type": "thirdparty",
  "category": "task",
  "slot": "standalone",
  "description": "分批统计文本材料，演示进度、取消与清理",
  "main": "main.js",
  "scripts": [
    "modules/state.js",
    "modules/actions.js",
    "modules/view.js"
  ],
  "styles": [
    "styles/workspace.css"
  ],
  "permissions": [],
  "button": {
    "label": "资料统计",
    "order": 100
  }
}
```


这里明确使用 `category: task`。不要复制一个内置组件 ID，也不要添加未经宿主支持的 `backend` 字段。

## 第二步：把运行状态独立出来


```javascript
(function () {
  window.MyExampleTask = window.MyExampleTask || {};
  MyExampleTask.createState = function () {
    return { runId: 0, status: 'idle', stage: '等待输入', processed: 0,
      total: 0, result: null, error: '', disposed: false };
  };
})();
```


`runId` 用于识别一次运行；`status` 描述任务状态；`stage` 面向用户解释当前步骤。`disposed` 用于阻止关闭后的回调继续更新界面。

## 第三步：实现分批执行与取消


```javascript
(function () {
  var task = window.MyExampleTask;
  var pause = function () { return new Promise(function (r) { setTimeout(r, 0); }); };
  task.cancel = function (state, render) {
    if (state.status !== 'running') return;
    state.runId += 1;
    state.status = 'cancelled';
    state.stage = '已取消，已有输入保持不变';
    render();
  };
  task.run = async function (state, input, render) {
    if (state.disposed || state.status === 'running') return;
    var text = input.trim();
    if (!text) {
      state.status = 'failed'; state.error = '请先粘贴需要统计的材料';
      state.stage = '检查输入'; render(); return;
    }
    var id = ++state.runId;
    state.status = 'running'; state.stage = '分批统计材料';
    state.error = ''; state.result = null; state.processed = 0;
    state.total = text.length;
    var lines = 1, characters = 0;
    var current = function () { return !state.disposed && state.runId === id; };
    try {
      for (var offset = 0; offset < text.length; offset += 4096) {
        if (!current()) return;
        var chunk = text.slice(offset, offset + 4096);
        for (var i = 0; i < chunk.length; i++) {
          if (chunk[i] === '\n') lines++;
          if (!/\s/.test(chunk[i])) characters++;
        }
        state.processed = Math.min(text.length, offset + 4096);
        render();
        await pause(); // Give the renderer a chance to paint and receive cancel.
      }
      if (!current()) return;
      state.result = { lines: lines, characters: characters };
      state.status = 'completed'; state.stage = '统计完成';
    } catch (error) {
      if (!current()) return;
      state.status = 'failed';
      state.error = state.stage + '失败：' + String(error.message || error);
    } finally {
      if (current()) render();
    }
  };
})();
```


每批最多处理 4096 个 UTF-16 单元，然后把执行权交回事件循环，使界面能重绘和响应取消。统计的 `characters` 是非空白 UTF-16 单元数，不是词数或字形数；文本首尾空白先被移除。

不要把这里的分批文本处理当作通用后台线程。更重的计算应使用目标宿主支持的后台执行路径，而不是持续占用渲染线程。

## 第四步：让界面只反映当前状态


```javascript
(function () {
  var task = window.MyExampleTask;
  var cleanup = null;
  task.dispose = function () { if (cleanup) cleanup(); cleanup = null; };
  task.mount = function (host) {
    task.dispose();
    if (!host) throw new Error('任务窗口容器不可用');
    var state = task.createState();
    var root = document.createElement('section'); root.className = 'my-example-task';
    var title = document.createElement('h2'); title.textContent = '资料统计';
    var input = document.createElement('textarea');
    input.setAttribute('aria-label', '待统计材料'); input.placeholder = '粘贴文本材料';
    var start = document.createElement('button'); start.textContent = '开始统计';
    var stop = document.createElement('button'); stop.textContent = '取消';
    start.type = stop.type = 'button';
    var status = document.createElement('p'); status.setAttribute('role', 'status');
    var progress = document.createElement('progress'); progress.max = 1;
    var output = document.createElement('pre');
    function render() {
      if (state.disposed) return;
      var running = state.status === 'running';
      start.disabled = running; stop.disabled = !running; input.disabled = running;
      status.textContent = state.error || state.stage;
      progress.value = state.total ? state.processed / state.total : 0;
      output.textContent = state.result ? JSON.stringify(state.result, null, 2) : '';
    }
    function run() { task.run(state, input.value, render); }
    function cancel() { task.cancel(state, render); }
    start.addEventListener('click', run); stop.addEventListener('click', cancel);
    root.append(title, input, start, stop, status, progress, output);
    host.append(root); render();
    cleanup = function () {
      state.disposed = true; state.runId++;
      start.removeEventListener('click', run); stop.removeEventListener('click', cancel);
      root.remove();
    };
  };
})();
```


开始、取消、进度和结果都由同一份状态驱动。旧运行失效后不能把新任务改成完成；清理时移除事件和根节点，不碰其他组件的监听。

## 第五步：注册与样式


```javascript
(function () {
  Mrite.plugins.register({
    id: 'my-example-task',
    activate: function (api, hostElement) { MyExampleTask.mount(hostElement); },
    deactivate: function () { MyExampleTask.dispose(); }
  });
})();
```



```css
.my-example-task { padding:24px; color:#152139; font:14px/1.7 system-ui; }
.my-example-task textarea { display:block; box-sizing:border-box; width:100%; min-height:180px; padding:12px; font:inherit; }
.my-example-task button { margin:12px 8px 0 0; padding:8px 18px; border:0; border-radius:8px; background:#245dba; color:white; }
.my-example-task button:disabled { opacity:.5; }
.my-example-task progress { width:100%; }
.my-example-task pre { white-space:pre-wrap; overflow-wrap:anywhere; }
```


## 第六步：按操作验收

| 操作 | 预期结果 |
| --- | --- |
| 输入“甲乙”，换行“丙” | `lines: 2`，`characters: 3` |
| 空输入直接开始 | 提示需要材料，可以继续编辑 |
| 长文本运行时取消 | 显示已取消，输入恢复可编辑 |
| 取消后换新文本运行 | 只显示新运行的结果 |
| 运行时关闭或停用 | 不再回写旧界面，不留下重复监听 |

在示例目录运行：

```powershell
node --test .tests/actions.test.cjs
```

## 扩展为真实任务

将 `actions.js` 中的本地统计替换为实际步骤时，继续保持运行身份、可解释阶段和清理链。输入文件先登记引用，确实需要时再读取；输出写入成功后才登记成果；外部请求、进程和任务取消分别接入自己的真实机制。

继续阅读[阶段、进度与取消](task-state.html)、[工作区与恢复](workspace.html)和[桌面验收](desktop-testing.html)。不要在尚未完成保存和结果核对时把状态改为 `completed`。
