This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-TMP-REPO/apps/web/src/features/desk-task/internal/TaskList.tsx
T
ChaiHaoandClaude Opus 5 259192f736 人手改的那一手真正落得下去;界面不撒谎那几处补齐
手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的:

件上就地改
· 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名,
  于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 ——
  而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。
· **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。
  删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)——
  「没回传就是删了」会把一次不完整的回传变成一次删表。
· **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。
· 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。

判据与门
· 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来,
  人填好的目标价它照样判「客户未提供」的毛病由此消掉。
· 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。
· 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开,
  于是一单没打标就进了放行门。三个数全由基座现数。
· 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。
  **指向两扇打不开的门,比不指还糟。**

界面不撒谎
· 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。
· 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、
  往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。
· 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。
· 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。
· 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。

A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。
五道门禁全绿(971 passed)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
2026-08-02 13:41:44 +08:00

186 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 任务列表区 —— 自 `~/coding/JCAI_PRD/workbench/(proto)/page.tsx` 平移。
*
* **DOM 结构与类名原样保留**(`jc-*` 前缀、vendor 皮肤一并搬);
* 只把 mock 数据换成服务端投影,渲染输出保持 1:1(README 平移规则 1/2/4)。
*
* 「清单里一条 = 一个任务 = 一个活动的一次运行,不是一整条工作流。」
*/
import { useState } from 'react';
import type { TaskCard } from '@contracts';
import { TERMINAL_STATE_LABEL } from '@contracts';
import { Ico } from '@web-shared/Ico.tsx';
import { ListEmpty } from '@web-shared/desk/module-kit.tsx';
import ScrollArea from '@web-shared/ScrollArea.tsx';
const TASK_COLORS = ['#3370ff', '#ff8800', '#7f3bf5', '#0aa294', '#f14ba9'];
type Status = 'active' | 'done';
export type TaskListProps = {
readonly tasks: readonly TaskCard[];
readonly selectedId: string;
readonly collapsed: boolean;
onPick(id: string): void;
onCollapse(): void;
};
const statusOf = (task: TaskCard): Status => (task.terminal === undefined ? 'active' : 'done');
export function TaskList({ tasks, selectedId, collapsed, onPick, onCollapse }: TaskListProps) {
const [filter, setFilter] = useState<Status>('active');
const [query, setQuery] = useState('');
if (collapsed) return null;
const count = (s: Status) => tasks.filter((t) => statusOf(t) === s).length;
const q = query.trim().toLowerCase();
const visible = tasks.filter(
(t) =>
statusOf(t) === filter &&
(q === '' || `${t.title} ${t.activity} ${t.workflowSubject}`.toLowerCase().includes(q)),
);
return (
<aside className="task-sidebar">
<div className="sidebar-head jc-wl-head">
<span className="jc-wl-ico">
<Ico name="list-checks" s={16} />
</span>
<div>
<h2>任务列表</h2>
<span>我负责的任务 · 共 {tasks.length} 项</span>
</div>
<button type="button" className="icon-button jc-wl-collapse" title="收起任务栏" onClick={onCollapse}>
<Ico name="panel-left-close" s={16} />
</button>
</div>
<div className="jc-flt-wrap" aria-label="任务清单筛选">
<div className="jc-flt" role="tablist" aria-label="按状态">
<button className={filter === 'active' ? 'active' : ''} onClick={() => setFilter('active')}>
<span>进行中</span>
<strong>{count('active')}</strong>
</button>
<button className={filter === 'done' ? 'active' : ''} onClick={() => setFilter('done')}>
<span>已完成</span>
<strong>{count('done')}</strong>
</button>
</div>
<div className="jc-search">
<Ico name="search" s={14} />
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索任务 / 活动 / 工作流…"
aria-label="检索任务"
/>
{query !== '' && (
<button type="button" className="jc-search-x" onClick={() => setQuery('')} aria-label="清空">
×
</button>
)}
</div>
</div>
<ScrollArea className="jc-sa--fill" vpClassName="task-list">
{visible.length === 0 &&
(query !== '' ? (
<ListEmpty icon="search" title="没找到">
换个关键词试试 —— 可搜任务名、活动名、工作流主题。
</ListEmpty>
) : filter === 'done' ? (
<ListEmpty icon="circle-check" title="还没有完成的任务">
办结的任务会归到这里,随时回看它的成果与日志。
</ListEmpty>
) : (
<ListEmpty icon="list-checks" title="手上没有任务">
分派给你的任务会出现在这里 —— 到了会有角标提醒。
</ListEmpty>
))}
{visible.map((task, i) => (
<TaskCardView
key={task.id}
task={task}
index={i}
selected={task.id === selectedId}
onPick={() => onPick(task.id)}
/>
))}
</ScrollArea>
</aside>
);
}
/**
* 任务卡:定高信息卡 —— 图标 + 活动名 + 状态章 / 主题两行位 / 编号 + 流内序位 / 进度条。
* 每行都有硬约束(单行省略或两行位),信息丰富但永不超限。
*/
function TaskCardView({
task,
index,
selected,
onPick,
}: {
task: TaskCard;
index: number;
selected: boolean;
onPick: () => void;
}) {
const done = task.terminal !== undefined;
/*
* 进度条读**活动自己的阶段**,不读工作流序位。
*
* 序位答的是「这单在整条流的第几个活动」——A1 在七节点流里恒 1/7,
* 从起单到收口一动不动,画成进度条就是一条永远 14% 的线;
* 单活动试跑更荒唐:没有工作流,序位恒 1/1,起单那一刻就满格。
* 活动没声明阶段就**不画** —— 编一个假的比不画更坏。
*/
const phase = task.phase;
const pct = done ? 100 : phase === undefined ? undefined : Math.round((phase.index / phase.total) * 100);
return (
<button
className={'task-card jc-tc jc-tc3' + (selected ? ' selected' : '')}
style={{ '--task-color': TASK_COLORS[index % TASK_COLORS.length] } as React.CSSProperties}
onClick={onPick}
>
<span className="jc-tc3-head">
<span className="jc-tc-ico">
<Ico name="list-checks" s={14} />
</span>
<h3 className="jc-tc-title">{task.activity}</h3>
{done ? (
<span className="jc-tc-state st-done">
<i />
{TERMINAL_STATE_LABEL[task.terminal!]}
</span>
) : (
<span className="jc-tc-state i-todo">
<i />
进行中
</span>
)}
</span>
<span className="jc-tc2-sub jc-clamp2">{task.workflowSubject}</span>
<span className="jc-tc3-meta">
<code className="jc-mono">{task.id.slice(0, 8)}</code>
{/* 阶段名 + x/N:有阶段就报阶段(人问的是「办到哪了」),没有就退回工作流序位 */}
{phase === undefined ? (
<b className="jc-mono" title="它在工作流上的序位">
{task.position}/{task.total}
</b>
) : (
<b className="jc-mono" title={`阶段:${phase.name}`}>
{phase.name} {phase.index}/{phase.total}
</b>
)}
</span>
{pct !== undefined && (
<span className="jc-tc-bar">
<span style={{ width: `${pct}%` }} />
</span>
)}
</button>
);
}