@marsken/dsh-studio 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -2
- package/client.js +1 -1
- package/dist/client.js +1 -1
- package/docs/compatibility.md +1 -1
- package/docs/publish.md +1 -0
- package/index.js +14 -3
- package/package.json +1 -1
- package/src/host/discovery.js +25 -0
- package/src/host/domain.js +4 -4
- package/src/host/executor.js +10 -6
- package/src/host/routes/requirements.js +3 -0
package/AGENTS.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| 路径 | 是什么 | 谁能改 |
|
|
8
8
|
| --- | --- | --- |
|
|
9
9
|
| `index.js` | 宿主半边:`apply(ctx)`、39 个 `studio_*` 工具、`/studio-api/*` 路由、SQLite 存储、派单引擎、门禁 | ✅ 直接改 |
|
|
10
|
-
| `src/host/*.js` | 宿主层模块:`cases.js`(跨卡用例聚合与回归汇总:`collectCases` / `groupCaseKeys` / `summarizeRun`,纯函数)、`team-memory.js`(专家团的团级记忆,存 side 键)、`quality.js`(AI 质量:返工识别与按专家汇总,纯函数)、`gitlab-facts.js`(执行记录 vs MR 事实的对账,纯函数)、`impact.js`(需求改版的影响面:受影响的卡与要重跑的用例,纯函数)、`design.js`(设计评审记录与进开发前的闸门,纯函数)、`shell-runner.js`(shell 用例执行器,默认关闭)、`shell-case.js`(shell 用例的数据形状,纯函数)、`runners.js`(用例执行器注册表:match + run,加新执行方式不用动 runOneCase)、`limits.js`(数量上限的默认值与清洗,可配置)、`db-runner.js`(SQLite 用例执行器与数据形状,默认关闭)、`usage.js`(会话用量解析与计价,纯函数)、`session-usage.js`(读 session.v4 日志并按卡/专家汇总)、`events.js`(进程内事件总线,同步扇出)、`webhooks.js`(事件 → 出站 webhook,失败不重试)、`domain.js`(板列/数据形状/`sanitize*`/默认配置,纯函数)、`dispatcher.js`(派单引擎)、`executor.js`(用例执行、代码事实核对、健康检查)、`store.js`(`StudioStore`/`SecretStore`:SQLite 台账 + 快照 + 登录会话)、`integrations.js`(GitLab / 飞书 / 平台健康)、`routes/*.js`(按前缀拆出的 HTTP 处理:`exports.js` 导出 CSV、`auth.js` 统一登录、`tasks.js` 卡片/专家/列、`requirements.js` 需求/文档/公开提单、`releases.js` 版本/灰度/部署、`insights.js` 指标与工作台、`skills.js` 技能库读写上传、`admin.js` 配置/权限成员/审计/快照、`connect.js` 市场/集成健康/GitLab/仓库/飞书、`agents.js` 派单运行态与起停、`coding.js` 执行记录/失败重试/worktree/项目配置、`tools.js` 39 个 `studio_*` 工具) | ✅ 直接改,新逻辑优先放这里 |
|
|
10
|
+
| `src/host/*.js` | 宿主层模块:`cases.js`(跨卡用例聚合与回归汇总:`collectCases` / `groupCaseKeys` / `summarizeRun`,纯函数)、`team-memory.js`(专家团的团级记忆,存 side 键)、`quality.js`(AI 质量:返工识别与按专家汇总,纯函数)、`gitlab-facts.js`(执行记录 vs MR 事实的对账,纯函数)、`impact.js`(需求改版的影响面:受影响的卡与要重跑的用例,纯函数)、`design.js`(设计评审记录与进开发前的闸门,纯函数)、`shell-runner.js`(shell 用例执行器,默认关闭)、`shell-case.js`(shell 用例的数据形状,纯函数)、`runners.js`(用例执行器注册表:match + run,加新执行方式不用动 runOneCase)、`limits.js`(数量上限的默认值与清洗,可配置)、`db-runner.js`(SQLite 用例执行器与数据形状,默认关闭)、`discovery.js`(需求澄清必答项与拆卡门禁,纯函数)、`usage.js`(会话用量解析与计价,纯函数)、`session-usage.js`(读 session.v4 日志并按卡/专家汇总)、`events.js`(进程内事件总线,同步扇出)、`webhooks.js`(事件 → 出站 webhook,失败不重试)、`domain.js`(板列/数据形状/`sanitize*`/默认配置,纯函数)、`dispatcher.js`(派单引擎)、`executor.js`(用例执行、代码事实核对、健康检查)、`store.js`(`StudioStore`/`SecretStore`:SQLite 台账 + 快照 + 登录会话)、`integrations.js`(GitLab / 飞书 / 平台健康)、`routes/*.js`(按前缀拆出的 HTTP 处理:`exports.js` 导出 CSV、`auth.js` 统一登录、`tasks.js` 卡片/专家/列、`requirements.js` 需求/文档/公开提单、`releases.js` 版本/灰度/部署、`insights.js` 指标与工作台、`skills.js` 技能库读写上传、`admin.js` 配置/权限成员/审计/快照、`connect.js` 市场/集成健康/GitLab/仓库/飞书、`agents.js` 派单运行态与起停、`coding.js` 执行记录/失败重试/worktree/项目配置、`tools.js` 39 个 `studio_*` 工具) | ✅ 直接改,新逻辑优先放这里 |
|
|
11
11
|
| `client.src.js` | 浏览器半边的手写源码(通过 `window.__ModuleLoader__` 注册面板) | ✅ 直接改(前端逻辑) |
|
|
12
12
|
| `web/src/**` | shadcn/Radix 组件、样式、`legacy.js` 桥;`web/src/studio/*.js` 是从 `client.src.js` 抬出来的纯模块(`markdown.js` 渲染器、`format.js` 日期格式、`labels.js` 标签表、`roster.js` 负责人与候选人、`fields.js` 表单适配层、`shell.js` 共享抽屉、`card.js` 看板卡片、`board.js` 看板本体、`mentions.js` @ 选择器、`cases.js` 多步用例解析、`drawer.js` 卡片详情抽屉、`editor.js` 通用编辑器、`palette.js` 命令面板(⌘K 全局搜索卡片/需求/版本/专家/专家团/项目/技能 + 动作项 + Ctrl/⌘+Alt+Shift+N/T/D 全局快捷键;带 Shift 是为了避开宿主的 Ctrl+Alt+N 新会话)、`charts.js` 进度条与燃尽图、`context.js` 共享上下文(`provideClient`/`client`)、`panels/*.js` 设置页 17 个面板、`pages/*.js` 六个页面组件(看板之外的工作台/需求/用例/版本/效能/自助提单);**`StudioPage` 是组合根,故意留在 `client.src.js`**,它负责顶栏、页签与「+ 新建」菜单并调用 `provideClient()`,别为了行数把它搬走),由 `make-legacy` 生成的 `legacy.js` 顶部统一 import | ✅ 直接改(样式与组件) |
|
|
13
13
|
| `dist/client.js` | `pnpm build` 的产物(单文件 IIFE,注意在仓库根 `dist/`,不是 `web/dist/`) | ❌ 生成物 |
|
|
@@ -80,7 +80,8 @@ STUDIO_TOKEN=$(sudo journalctl -u dsh-web -n 50 | grep -o 'token=[A-Za-z0-9_-]*'
|
|
|
80
80
|
8. 真库自检:`npm run smoke:store -- ~/.dsh/dsh-studio.db`(把 `.db/-wal/-shm` 拷到临时目录起一遍真实 `StudioStore`,专抓「空库能起、真库就炸」的导入问题)
|
|
81
81
|
9. 成本数字可疑时跑 `npm run verify:usage [taskId]`:ssh 取原始会话日志现算一遍 token,与 `/costs` 对账(两条路径不同:一条走 session-usage 的 zstd 缓存,一条走 usage.js 直接解析)。
|
|
82
82
|
10. 发版后跑 `npm run verify:release`:比对 package.json 版本 / 本地 tag / origin main 与 tag / npm registry,不一致直接退出码 1。
|
|
83
|
-
11.
|
|
83
|
+
11. 全闭环自检:`STUDIO_BASE=http://<host>:18080 node tools/e2e-loop.mjs`——真建一条需求和卡,跑完需求评审→拆卡→各列→代码事实→用例→验收→版本发布。**会写入真实数据**,跑完记得清。
|
|
84
|
+
12. CI 在 `.github/workflows/ci.yml`:语法 → 契约 → `web/` 里 `pnpm install --frozen-lockfile && pnpm build` → `cmp dist/client.js client.js` → `git diff --exit-code`(产物必须可复现,改了前端忘记构建会直接红)
|
|
84
85
|
|
|
85
86
|
## 5. 禁止事项
|
|
86
87
|
|
package/client.js
CHANGED
|
@@ -42,7 +42,7 @@ GET /api/points | 200 |`,onChange:m=>ie({...X,steps:m.target.value})}),e("div",{
|
|
|
42
42
|
- `,onChange:O=>q("content",O.target.value)})),a("div",{className:"ds-field"},a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:k.enabled!==!1,onChange:O=>q("enabled",O.target.checked)}),a("span",{},"启用(停用则不写入磁盘)")))))}function ew({state:e,onOpenTask:t,onOpenBoard:a,onOpenCases:n}){const{h:r,React:s,api:o,toast:i}=vt(),[l,c]=s.useState(null),[u,p]=s.useState(null),[f,v]=s.useState(null);if(s.useEffect(()=>{let S=!0;return(async()=>{try{const C=await o("/dashboard");S&&(c(C?.dashboard??null),p(C?.regression??null))}catch(C){if(S){const E=C instanceof Error?C.message:String(C);v(E),i(E,"error")}}})(),()=>{S=!1}},[e.revision]),l===null)return r("div",{className:"ds-body"},r("div",{className:"ds-hint"},f===null?"正在算今天该看什么…":`算不出来:${f}`));const w=u===null?null:(()=>{const S=(Number(u.failed)||0)+(Number(u.failedCards)||0)>0;return r("div",{className:"ds-reg-banner","data-bad":S?"1":"0"},r("span",null,`最近回归 · ${Ke(u.at)} · ${u.by||"人"}`),r("span",{className:"ds-hint"},`${u.cards} 张卡 / ${u.total} 条:通过 ${u.passed}${S?`,不通过 ${u.failed}`:""}${u.bugs>0?` · 自动开缺陷 ${u.bugs} 张`:""}`),n===void 0?null:r("button",{className:"ds-btn ds-btn-ghost",onClick:()=>n()},"看用例"))})(),y=l.risks??{},k=l.flow??{},g=l.ai??{},d=(S,C,E,_)=>r("div",{className:"ds-kpi","data-warn":_===!0?"1":"0"},r("div",{className:"ds-kpi-value"},String(C??0)),r("div",{className:"ds-kpi-label"},S),r("div",{className:"ds-kpi-hint"},E)),h=S=>r("div",{key:S.id,className:"ds-listrow"},r("div",{className:"ds-listrow-main"},r("div",null,`${S.code||S.id} · ${S.title}`),r("div",{className:"ds-hint"},`${S.statusLabel}${S.dueAt>0?` · 截止 ${Xa(S.dueAt)}`:""}${S.sla?.state==="overdue"?" · 缺陷超时":""}`)),r("div",{className:"ds-listrow-actions"},r("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(S.id)},"打开"))),b=(S,C,E,_)=>r("div",{className:"ds-panel"},r("div",{className:"ds-panel-head"},r("span",null,S),r("span",{className:"ds-hint"},_??"")),r("div",{className:"ds-panel-body"},C.length===0?r("div",{className:"ds-hint"},E):C));return r("div",{className:"ds-body"},w,r("div",{className:"ds-kpi-grid"},d("在办卡",l.scope.open,`共 ${l.scope.tasks} 张`),d("逾期",y.overdue,"过了期望完成时间还没完成",y.overdue>0),d("阻塞",y.blocked,"挂着阻塞标记等人决策",y.blocked>0),d("缺陷超时",y.slaOverdue,"按 P0-P3 时限算",y.slaOverdue>0),d("准出被拦",y.gateFailed,"待测试/待验收里过不了准出",y.gateFailed>0),d("流水线失败",y.pipelineFailed,"最近一次 CI 没通过",y.pipelineFailed>0),d("集成异常",y.integrationsBroken,"飞书/GitLab/云盘/隔离",y.integrationsBroken>0),d("AI 在跑",g.running,`今天派单 ${g.todayRuns} 次`)),r("div",{className:"ds-panel"},r("div",{className:"ds-panel-head"},r("span",null,"流程与产出"),r("span",{className:"ds-hint"},"需要人动手的地方")),r("div",{className:"ds-panel-body ds-grid4"},r("div",{className:"ds-field"},r("div",{className:"ds-label"},"待评审"),r("div",null,String(k.review??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"待测试"),r("div",null,String(k.test??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"待验收"),r("div",null,String(k.accept??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"今天完成"),r("div",null,String(k.doneToday??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"近 7 天完成"),r("div",null,String(k.doneWeek??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"需求卡住"),r("div",null,String(y.requirementBlocked??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"版本卡住"),r("div",null,String(y.releaseBlocked??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},""),r("button",{className:"ds-btn ds-btn-ghost",onClick:()=>a()},"去看板")))),b("逾期",(l.list?.overdue??[]).map(h),"没有逾期的卡。"),b("阻塞",(l.list?.blocked??[]).map(h),"没有卡在等人决策。"),b("缺陷超时",(l.list?.slaOverdue??[]).map(h),"缺陷都在时限内。"),b("准出被拦",(l.list?.gateFailed??[]).map(h),"没有卡被准出规则拦住。","清单没勾完 / 用例不通过 / 已签过字但被打回"),b("流水线失败",(l.list?.pipelineFailed??[]).map(h),"没有 CI 失败的卡。"),b("需求与版本卡住",[...(l.list?.requirementBlocked??[]).map(S=>r("div",{key:S.id,className:"ds-listrow"},r("div",{className:"ds-listrow-main"},r("div",null,`${S.code||S.id} · ${S.title}`),r("div",{className:"ds-hint"},`需求:${(S.blockers??[]).join(";")}`)))),...(l.list?.releaseBlocked??[]).map(S=>r("div",{key:S.id,className:"ds-listrow"},r("div",{className:"ds-listrow-main"},r("div",null,`${S.version||S.id} · ${S.name||""}`),r("div",{className:"ds-hint"},`版本:${(S.blockers??[]).join(";")}`))))],"需求与版本都还顺。"),r("div",{className:"ds-hint"},`统计时间 ${Ke(l.at)},数字随看板变化实时算,不落库。`))}function tw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,toast:s,confirmDialog:o,promptDialog:i,TextField:l,SelectField:c,ChipField:u,CheckField:p}=vt(),[f,v]=n.useState(null),[w,y]=n.useState(null),[k,g]=n.useState(""),[d,h]=n.useState("__all__"),[b,S]=n.useState("__all__"),[C,E]=n.useState(()=>new Set),[_,I]=n.useState(!1),[z,H]=n.useState([]),[M,$]=n.useState({}),[B,J]=n.useState({}),[q,L]=n.useState(null),[A,x]=n.useState(""),[P,K]=n.useState([]),[R,O]=n.useState(null),G=n.useCallback(async()=>{try{const U=await r("/cases",{});v(Array.isArray(U.cases)?U.cases:[]),K(Array.isArray(U.regressions)?U.regressions:[]),H(Array.isArray(U.suites)?U.suites:[]),$(Object.fromEntries((U.counts??[]).map(he=>[he.id,he.count]))),J(Object.fromEntries((U.counts??[]).map(he=>[he.id,he.keys??[]]))),y(null)}catch(U){y(U instanceof Error?U.message:String(U)),v([])}},[]),we=(e.tasks??[]).reduce((U,he)=>{const je=he.caseSummary??{total:0,pass:0,fail:0};return U+je.total*7+je.pass*13+je.fail*17},0);n.useEffect(()=>{G()},[G,we]);const fe=n.useMemo(()=>{const U=k.trim().toLowerCase(),he=q===null?null:new Set(B[q]??[]);return(f??[]).filter(je=>he!==null&&!he.has(je.key)||d!=="__all__"&&je.requirementId!==d||b!=="__all__"&&je.result!==b?!1:U===""?!0:`${je.id} ${je.title} ${je.taskTitle} ${je.requirementTitle} ${je.pointId}`.toLowerCase().includes(U))},[f,k,d,b,q,B]),ye=n.useMemo(()=>{const U=fe.filter(Ue=>Ue.result==="pass").length,he=fe.filter(Ue=>Ue.result==="fail").length,je=fe.filter(Ue=>Ue.result==="skip").length;return{pass:U,fail:he,skip:je,rest:fe.length-U-he-je}},[fe]),He=e.requirements??[],ae=fe.filter(U=>U.runnable===!0),F=ae.filter(U=>C.has(U.key)),te=U=>E(he=>{const je=new Set(he);return je.has(U)?je.delete(U):je.add(U),je}),xe=()=>E(new Set(ae.map(U=>U.key))),X=()=>E(new Set),ie=async U=>{if(U.length===0){x("先勾几条带接口定义的用例再跑(手工用例没有 http,跑不了)");return}I(!0),x("");try{const je=(await r("/cases/run",{keys:U.map(st=>st.key),by:"回归"}))?.results??[],Ue=st=>je.reduce((Ct,pt)=>Ct+(Number(pt[st])||0),0),_t=je.filter(st=>st.ok===!1),Gt=[`跑了 ${je.length} 张卡 / ${Ue("total")} 条:通过 ${Ue("passed")},不通过 ${Ue("failed")}`];Ue("bugs")>0&&Gt.push(`自动开缺陷 ${Ue("bugs")} 张`),_t.length>0&&Gt.push(`${_t.length} 张卡报错:${_t[0].error??""}`),x(Gt.join(",")),E(new Set),await G()}catch(he){x(he instanceof Error?he.message:String(he))}finally{I(!1)}},ge=U=>{const he=U?.results??[],je=Gt=>he.reduce((st,Ct)=>st+(Number(Ct[Gt])||0),0),Ue=he.filter(Gt=>Gt.ok===!1),_t=[`跑了 ${he.length} 张卡 / ${je("total")} 条:通过 ${je("passed")},不通过 ${je("failed")}`];return je("bugs")>0&&_t.push(`自动开缺陷 ${je("bugs")} 张`),Ue.length>0&&_t.push(`${Ue.length} 张卡报错:${Ue[0].error??""}`),U?.truncated!==void 0&&_t.push(`套件共 ${U.truncated.total} 条,本次只跑了前 ${U.truncated.ran} 条`),_t.join(",")},W=async U=>{I(!0),x("");try{const he=await r("/suite/run",{id:U.id});x(ge(he)),await G()}catch(he){x(he instanceof Error?he.message:String(he))}finally{I(!1)}},Ee=async()=>{const U=await i("套件名字(跑的就是当前筛选里的用例)",{title:"新建回归套件",defaultValue:"默认回归"});if(!(U===null||String(U).trim()===""))try{await r("/suite/save",{suite:{name:String(U).trim().slice(0,40),rules:{requirementIds:d==="__all__"?[]:[d],httpOnly:!0},keys:[],isDefault:z.length===0},by:"人"}),s("已保存套件"),await G()}catch(he){s(he instanceof Error?he.message:String(he),"error")}},ke=async U=>{try{await r("/suite/save",{suite:{...U,isDefault:!0},by:"人"}),s(`夜间回归改跑「${U.name}」`),await G()}catch(he){s(he instanceof Error?he.message:String(he),"error")}},Ae=async U=>{if(await o(`删除套件「${U.name}」?用例本身不受影响。`,{title:"删除套件",danger:!0,okText:"删除"}))try{await r("/suite/delete",{id:U.id}),await G()}catch(he){s(he instanceof Error?he.message:String(he),"error")}},Ve=fe.filter(U=>U.runnable===!0&&U.result==="fail"),Ya=()=>ie(F),Rn=()=>ie(Ve);return a("div",{className:"ds-body"},a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},a("span",null,"用例总览"),a("span",{className:"ds-hint"},"全公司的用例按卡片汇总:点一行跳回那张卡;用例本身仍在卡片的「测试」里维护"),a(u,{className:"ds-chip",style:{marginLeft:"auto"}},`${fe.length} 条 · ✅ ${ye.pass} · ❌ ${ye.fail}${ye.skip>0?` · ⏭️ ${ye.skip}`:""}${ye.rest>0?` · ⬜ ${ye.rest}`:""}`),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void G()},"刷新")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-row"},a(l,{className:"ds-input",style:{width:240},value:k,placeholder:"搜用例 / 卡片 / 需求 / 功能点",onChange:U=>g(U.target.value)}),a(c,{className:"ds-select",style:{width:220},value:d,onChange:U=>h(U.target.value)},a("option",{value:"__all__"},"全部需求"),He.map(U=>a("option",{key:U.id,value:U.id},`${U.code||U.id} ${U.title}`))),a(c,{className:"ds-select",style:{width:140},value:b,onChange:U=>S(U.target.value)},a("option",{value:"__all__"},"全部结果"),Zp.map(U=>a("option",{key:U,value:U},Ql[U])))),a("div",{className:"ds-row",style:{marginTop:8}},a("button",{className:"ds-btn ds-btn-ghost",disabled:_||ae.length===0,onClick:xe},`全选可跑的 ${ae.length} 条`),a("button",{className:"ds-btn ds-btn-ghost",disabled:_||C.size===0,onClick:X},"清空选择"),a("button",{className:"ds-btn ds-btn-primary",disabled:_||F.length===0,onClick:()=>void Ya()},_?"跑着…":`▶ 跑选中的 ${F.length} 条`),a("button",{className:"ds-btn",disabled:_||Ve.length===0,title:"只补跑上次不通过、且有接口定义的用例",onClick:()=>void Rn()},`↻ 只跑上次失败的 ${Ve.length} 条`),a("span",{className:"ds-hint"},"按卡串行跑,每张卡一轮;失败的照旧自动开缺陷(最多一次 60 条)")),a("div",{className:"ds-row",style:{marginTop:8,flexWrap:"wrap",gap:8}},a("span",{className:"ds-hint"},"回归套件:"),z.length===0?a("span",{className:"ds-hint"},"还没有套件。把当前筛选存成套件,夜间就自动跑它。"):z.map(U=>a("span",{key:U.id,className:"ds-suite","data-default":U.isDefault?"1":"0"},a("span",{style:{cursor:"pointer"},title:"点一下只看这个套件里的用例,再点一次取消",onClick:()=>L(he=>he===U.id?null:U.id)},`📦 ${U.name}${U.isDefault?" · 默认":""} · ${M[U.id]??0} 条${q===U.id?"(只看中)":""}`),a("button",{className:"ds-btn ds-btn-ghost",disabled:_||(M[U.id]??0)===0,onClick:()=>void W(U)},"▶ 跑"),U.isDefault?null:a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void ke(U)},"设为默认"),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Ae(U),"data-danger":"1"},"删除"))),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Ee()},"+ 存为套件")),A===""?null:a("div",{className:"ds-hint",style:{marginTop:6}},A),P.length===0?null:a("div",{className:"ds-reg-list"},a("span",{className:"ds-hint"},"最近回归:"),P.slice(0,4).map(U=>a("span",{key:String(U.at),className:"ds-reg-item","data-bad":(Number(U.failed)||0)+(Number(U.failedCards)||0)>0?"1":"0",title:`${new Date(U.at).toLocaleString("zh-CN")} · ${U.by||"人"}`},`${Ke(U.at)} ${U.by||""} · ${U.cards} 卡 / ${U.total} 条 · ✅${U.passed} ❌${U.failed}${U.bugs>0?` 🐞${U.bugs}`:""}`))),w!==null?a("div",{className:"ds-err"},w):null,f===null?a("div",{className:"ds-hint",style:{padding:"10px 2px"}},"载入用例…"):fe.length===0?a("div",{className:"ds-empty"},"没有符合条件的用例。用例登记在卡片详情的「测试」里。"):a("div",{className:"ds-case-lib"},fe.map(U=>a("div",{key:U.key,className:"ds-case-lib-wrap"},a("div",{className:"ds-case-lib-row","data-result":U.result,title:"打开所属卡片",onClick:()=>t(U.taskId)},a("span",{className:"ds-case-lib-pick",onClick:he=>he.stopPropagation()},a(p,{checked:C.has(U.key),disabled:_||U.runnable!==!0,onChange:()=>te(U.key)})),a("span",{className:"ds-case-lib-result"},Ql[U.result]??U.result),a("span",{className:"ds-case-lib-title"},U.title),a("span",{className:"ds-case-lib-meta"},`${U.taskCode||U.taskId} · ${U.requirementTitle||"未挂需求"}${U.pointId?" · "+U.pointId:""}`),a("span",{className:"ds-case-lib-meta"},U.http?"接口":U.shell?"shell":U.db?"数据库":"手工"),a("span",{className:"ds-case-lib-pri"},U.priority),a("span",{className:"ds-case-lib-at"},U.lastRunAt>0?Ke(U.lastRunAt):"没跑过"),a("button",{className:"ds-btn ds-btn-ghost ds-case-lib-more",title:"看最近一次执行的明细",onClick:he=>{he.stopPropagation(),O(R===U.key?null:U.key)}},R===U.key?"收起":"明细")),R===U.key?a("div",{className:"ds-case-lib-detail"},a("div",null,`期望:${U.expected||"—"}`),a("div",null,`最近一次:${U.note||(U.lastRunAt>0?"只有结果,没有记录":"还没跑过")}`),U.lastError===""?null:a("div",{className:"ds-err"},`错误:${U.lastError}`),U.lastPreview===""?null:a("pre",{className:"ds-case-lib-preview"},U.lastPreview),a("div",{className:"ds-hint"},`用例编号 ${U.id} · 所属卡片 ${U.taskCode||U.taskId}`)):null))))))}function aw({value:e,tone:t}){const{h:a,kit:n}=vt(),r=n("ProgressBar"),s=Math.max(0,Math.min(100,Number(e)||0));if(r!==null)return a(r,{value:s,tone:t});const o=t==="over"?"#e5484d":t==="warn"?"#d97706":void 0;return a("div",{className:"ds-an-track"},a("div",{className:"ds-an-fill",style:{width:`${s}%`,background:o}}))}function nw({series:e}){const{h:t,kit:a}=vt(),n=Array.isArray(e)?e:[];if(n.length===0)return t("div",{className:"ds-empty"},"还没有数据。");const r=n.map(u=>Math.max(0,Number(u.remaining)||0)),s=Math.max(1,...r),o=n.length===1?100:100/(n.length-1),i=n.map((u,p)=>({x:p*o,y:38-r[p]/s*32,remaining:r[p],day:String(u.day??"")})),l=i.map((u,p)=>`${p===0?"M":"L"}${u.x.toFixed(2)},${u.y.toFixed(2)}`).join(" "),c=`${l} L100,40 L0,40 Z`;return t("div",{className:"ds-burn"},t("svg",{viewBox:"0 0 100 40",preserveAspectRatio:"none",className:"ds-burn-svg",role:"img","aria-label":"燃尽趋势"},t("path",{d:c,className:"ds-burn-area"}),t("path",{d:l,className:"ds-burn-line",vectorEffect:"non-scaling-stroke"}),i.map(u=>t("circle",{key:u.day,cx:u.x,cy:u.y,r:1.1,className:"ds-burn-dot"},t("title",{},`${u.day}:还剩 ${u.remaining} 张`)))),t("div",{className:"ds-burn-axis"},t("span",{},i[0].day),t("span",{},i[i.length-1].day)))}const od=800;function uf(e){const t=String(e??"").replace(/\r\n?/g,`
|
|
43
43
|
`);if(t==="")return[];const a=t.split(`
|
|
44
44
|
`);return a.length>0&&a[a.length-1]===""&&a.pop(),a}function sw(e,t){const a=uf(e),n=uf(t);if(a.length>od||n.length>od)return[{kind:"del",text:`(原基线 ${a.length} 行,超过 ${od} 行的对比上限)`},{kind:"add",text:`(当前文档 ${n.length} 行)`}];const r=Array.from({length:a.length+1},()=>new Array(n.length+1).fill(0));for(let l=a.length-1;l>=0;l-=1)for(let c=n.length-1;c>=0;c-=1)r[l][c]=a[l]===n[c]?r[l+1][c+1]+1:Math.max(r[l+1][c],r[l][c+1]);const s=[];let o=0,i=0;for(;o<a.length&&i<n.length;){if(a[o]===n[i]){s.push({kind:"same",text:a[o]}),o+=1,i+=1;continue}if(r[o+1][i]>=r[o][i+1]){s.push({kind:"del",text:a[o]}),o+=1;continue}s.push({kind:"add",text:n[i]}),i+=1}for(;o<a.length;)s.push({kind:"del",text:a[o]}),o+=1;for(;i<n.length;)s.push({kind:"add",text:n[i]}),i+=1;return s}function rw(e){const t=Array.isArray(e)?e:[];return{added:t.filter(a=>a.kind==="add").length,removed:t.filter(a=>a.kind==="del").length,same:t.filter(a=>a.kind==="same").length}}function ow({state:e,onOpenTask:t,onChanged:a,createSignal:n,focusReq:r}){const{h:s,React:o,api:i,toast:l,currentUser:c,confirmDialog:u,promptDialog:p,renderMarkdown:f,TextField:v,TextAreaField:w,ChipField:y,CheckField:k,SelectField:g,MdField:d}=vt(),h=e.requirements??[],b=e.config.projects??[],[S,C]=o.useState(h.length>0?h[0].id:null),E=()=>{I({title:"",description:"",project:"",iteration:"",owner:"",priority:"normal",status:"draft",links:{}}),H(null)};o.useEffect(()=>{Number(n)>0&&E()},[n]),o.useEffect(()=>{r&&r.id&&C(r.id)},[r]);const[_,I]=o.useState(null),[z,H]=o.useState(null),[M,$]=o.useState(!1),[B,J]=o.useState(null),[q,L]=o.useState(null),[A,x]=o.useState(null),[P,K]=o.useState(null),[R,O]=o.useState(null),[G,we]=o.useState(!1),[fe,ye]=o.useState(null),[He,ae]=o.useState(!1),[F,te]=o.useState(null),[xe,X]=o.useState(null),[ie,ge]=o.useState(null),[W,Ee]=o.useState(""),[ke,Ae]=o.useState(null),[Ve,Ya]=o.useState("0"),[Rn,U]=o.useState([]),[he,je]=o.useState(null),[Ue,_t]=o.useState(null),[Gt,st]=o.useState(!1),Ct=async()=>{if(pt!==null){st(!0);try{const V=await i("/requirement/impact",{id:pt.id});_t({summary:V.summary,cards:V.cards??[],cases:V.cases??[]})}catch(V){je(V instanceof Error?V.message:String(V))}finally{st(!1)}}},pt=h.find(V=>V.id===S)??null;o.useEffect(()=>{Ya(String(pt?.order??0)),U(Array.isArray(pt?.dependsOn)?pt.dependsOn:[]),je(null)},[S,pt?.updatedAt]);const Ea=async()=>{if(pt!==null)try{await i("/requirement/save",{requirement:{id:pt.id,title:pt.title,description:pt.description??"",project:pt.project??"",iteration:pt.iteration??"",owner:pt.owner??"",priority:pt.priority??"normal",status:pt.status??"draft",links:pt.links??{},order:Number(Ve)||0,dependsOn:Rn}}),je("已保存:排序与依赖"),l("排序与依赖已保存"),a!==void 0&&a()}catch(V){je(V instanceof Error?V.message:String(V))}},hs=async V=>{if(Q===null)return;const ce=String(W??"").trim();if(V==="sign"&&ce===""){Ae("签字要写一句结论:交付了什么、遗留什么");return}try{await i("/requirement/signoff",{id:Q.id,action:V,note:ce,by:c()}),Ae(V==="sign"?"已签字":"已取消签字"),l(V==="sign"?"需求已签字":"已取消需求签字"),Ee(""),a!==void 0&&a()}catch(Le){Ae(Le.message)}},Y=o.useCallback(async V=>{if(V==null||V===""){te(null);return}try{const ce=await i("/requirement/matrix",{id:V});te(ce.matrix)}catch(ce){X(ce.message)}},[]),ne=async V=>{if(Q!==null)try{await i("/requirement/save",{requirement:{...Q,links:Q.links??{},points:V}}),await Y(Q.id),X("功能点已保存"),ge(null)}catch(ce){X(ce.message)}},ee=()=>({title:"",description:"",priority:"normal",status:"draft",acceptance:[]}),be=async()=>{if(ie===null||Q===null)return;if(String(ie.title??"").trim()===""){X("功能点要有标题");return}const V=String(ie.acceptanceText??"").split(`
|
|
45
|
-
`).map(At=>At.trim()).filter(At=>At!==""),ce={id:ie.id??`p${(Q.points??[]).length+1}`,title:ie.title,description:ie.description??"",priority:ie.priority??"normal",status:ie.status??"draft",acceptance:V.map((At,$a)=>({id:`a${$a+1}`,text:At,done:!1}))},Le=(Q.points??[]).filter(At=>At.id!==ce.id);await ne([...Le,ce])},se=async V=>{Q!==null&&await u("删除这个功能点?挂在上面的卡会变成「未归功能点」。",{title:"删除功能点",danger:!0,okText:"删除"})&&await ne((Q.points??[]).filter(ce=>ce.id!==V))},de=async(V,ce)=>{try{await i("/task/update",{id:V,pointId:ce}),Q!==null&&await Y(Q.id)}catch(Le){X(Le.message)}},Q=h.find(V=>V.id===S)??null,ve=V=>e.tasks.filter(ce=>ce.requirementId===V),Ce=V=>e.statuses.find(ce=>ce.id===V)?.label??V;o.useEffect(()=>{te(null),X(null),Q!==null&&Y(Q.id)},[Q===null?"":Q.id,Y]);const Se=async()=>{if(_!==null){if(String(_.title??"").trim()===""){H("需求标题不能为空");return}$(!0);try{const V=await i("/requirement/save",{requirement:{id:_.id,title:_.title,description:_.description,project:_.project,owner:_.owner,domain:_.domain??"",priority:_.priority,status:_.status,links:{prd:_.prd,proto:_.proto,acceptance:_.acceptance}}});C(V.requirement.id),I(null),H(null),l(V.created===!0?"需求已创建":"需求已保存")}catch(V){H(V.message)}finally{$(!1)}}},Pe=async V=>{if(Q!==null)try{await i("/requirement/save",{requirement:{...Q,links:Q.links??{},status:V}}),l("需求状态已更新")}catch(ce){H(ce.message)}},dt=async V=>{if(await u("删除这条需求?它下面的卡会保留,只是不再挂需求。",{title:"删除需求",danger:!0,okText:"删除"}))try{await i("/requirement/delete",{id:V}),C(null)}catch(ce){H(ce.message)}},rt={pending:"待评审",approved:"评审通过",changes_requested:"评审打回"},[Je,We]=o.useState(!1),[tt,Ge]=o.useState(null),Te=async(V,ce)=>{if(Q!==null){We(!0),Ge(null);try{const Le=await i("/requirement/review",{id:Q.id,decision:V,note:ce??"",by:c(),operator:c()});Ge(`已${V==="approve"?"通过":V==="reject"?"打回":"提交"}评审:${rt[Le.review?.status]??""}`),typeof a=="function"&&a()}catch(Le){Ge(Le.message)}finally{We(!1)}}},It=async()=>{const V=await p("打回要给意见(会记在评审记录里):",{title:"评审打回",danger:!0,okText:"打回",placeholder:"写清楚哪里不满足,之后要据此改"});if(V!==null){if(String(V).trim()===""){Ge("打回必须写意见");return}Te("reject",String(V).trim().slice(0,600))}},[Be,yt]=o.useState(!1),[Rt,Ut]=o.useState(null),le=async()=>{if(Q!==null){yt(!0),Ut(null);try{const ce=(await i("/requirement/split",{id:Q.id})).created??[];Ut(ce.length===0?"没有可拆的功能点:要么都覆盖了,要么还没加功能点。":`已建 ${ce.length} 张卡:${ce.map(Le=>Le.code||Le.id).join("、")}`),typeof a=="function"&&a()}catch(V){Ut(V.message)}finally{yt(!1)}}},Ne=()=>({kind:"prd",title:"",url:Q?.links?.prd??"",version:"v1.0",content:"",status:"draft"}),me=async V=>{if(L(null),V===null){J(Ne());return}try{const Le=((await i("/requirement/get",{id:Q.id})).requirement?.docs??[]).find(At=>At.id===V);J(Le===void 0?Ne():{...Le})}catch(ce){H(ce.message)}},Qa=async V=>{if(A!==null&&A.id===V){x(null);return}x({id:V,content:"",busy:!0});try{const Le=((await i("/requirement/get",{id:Q.id})).requirement?.docs??[]).find(At=>At.id===V);x({id:V,content:String(Le?.content??""),busy:!1})}catch(ce){H(ce.message),x(null)}},Et=async()=>{if(P!==null){K(null);return}try{const ce=(await i("/requirement/get",{id:Q.id})).requirement??{},Le=ce.baseline??null,$a=[...ce.docs??[]].sort((Dn,Fo)=>(Number(Fo.at)||0)-(Number(Dn.at)||0)),ra=$a.find(Dn=>Dn.kind==="prd")??$a[0]??null;if(Le===null){K(null),H("这条需求还没有基线,先挑一份文档「设为基线」");return}if(ra===null){K(null),H("这条需求还没有文档可以对比");return}const Ma=sw(Le.content??"",ra.content??"");K({rows:Ma,summary:rw(Ma),version:Le.version,docTitle:ra.title})}catch(V){K(null),H(V instanceof Error?V.message:String(V))}},fa=async()=>{if(R!==null){O(null);return}try{const V=await i("/requirement/get",{id:Q.id}),ce={};for(const Le of V.requirement?.docs??[])ce[Le.id]=String(Le.content??"");O(ce)}catch(V){H(V.message)}},An=async()=>{if(B!==null){if(String(B.title??"").trim()===""){L("文档要有标题");return}we(!0);try{const V=await i("/requirement/doc",{id:Q.id,action:B.id===void 0?"add":"update",docId:B.id,doc:{kind:B.kind,title:B.title,url:B.url,version:B.version,content:B.content,status:B.status}});J(null),L(null)}catch(V){L(V.message)}finally{we(!1)}}},La=async()=>{ae(!0),ye(null);try{const V=await i("/requirement/folder",{id:Q.id});ye(V.ok?`云盘文件夹已就绪:${V.folder?.url??""}`:`创建失败:${V.error||V.folder?.error||"未知原因"}`)}catch(V){ye(V.message)}finally{ae(!1)}},Lt=async()=>{ae(!0),ye(null);try{const ce=(await i("/requirement/sync",{id:Q.id})).report??{},Le=Number(ce.failed??0);ye(`归档完成:共 ${ce.total??0} 件,成功 ${ce.written??0} 件${Le>0?`,失败 ${Le} 件(${ce.errors?.[0]??""})`:"。"}`)}catch(V){ye(V.message)}finally{ae(!1)}},Mt=async V=>{try{await i("/requirement/doc",{id:Q.id,action:"baseline",docId:V,by:"人"}),L("这一版已生效,之后新建的卡都会记下它。")}catch(ce){L(ce.message)}},jn=async()=>{try{await i("/requirement/doc",{id:Q.id,action:"clear-baseline"}),L("基线已取消,需求回到「评审中」。")}catch(V){L(V.message)}},Z=async V=>{if(await u("删除这份文档?",{title:"删除文档",danger:!0,okText:"删除"}))try{await i("/requirement/doc",{id:Q.id,action:"remove",docId:V})}catch(ce){L(ce.message)}},_e=async()=>{we(!0),L(null);try{const V=await i("/requirement/pull",{url:B?.url??""});if(V.ok===!0){const ce=String(V.text??"");J(Le=>({...Le,title:String(Le?.title??"").trim()===""?String(V.title??""):Le.title,content:ce})),L(`已拉取 ${ce.length} 字正文${V.title?`,标题取自文档「${V.title}」`:""}。`)}else L(V.error??"拉取失败")}catch(V){L(V.message)}finally{we(!1)}},xt=_===null?null:s("div",{className:"ds-panel ds-form-panel"},s("div",{className:"ds-panel-head"},_.id===void 0?"新建需求":`编辑 ${Q?.code??""}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-field"},s("div",{className:"ds-label"},"需求标题"),s(v,{className:"ds-input",value:_.title,onChange:V=>I({..._,title:V.target.value})})),s("div",{className:"ds-row"},s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务项目"),s(g,{className:"ds-select",value:_.project,onChange:V=>I({..._,project:V.target.value})},s("option",{value:""},"不指定"),b.map(V=>s("option",{key:V.id,value:V.id},V.name)))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"状态"),s(g,{className:"ds-select",value:_.status,onChange:V=>I({..._,status:V.target.value})},ef.map(V=>s("option",{key:V,value:V},Ar[V])))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务负责人"),s(v,{className:"ds-input",value:_.owner,onChange:V=>I({..._,owner:V.target.value})})),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务领域"),s(v,{className:"ds-input",placeholder:"支付 / 风控 / 会员(决定派给哪位领域专家)",value:_.domain??"",onChange:V=>I({..._,domain:V.target.value})}))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"PRD 链接"),s(v,{className:"ds-input",placeholder:"https://…(飞书云文档)",value:_.prd,onChange:V=>I({..._,prd:V.target.value})})),s("div",{className:"ds-row"},s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"原型链接"),s(v,{className:"ds-input",value:_.proto,onChange:V=>I({..._,proto:V.target.value})})),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"验收标准链接"),s(v,{className:"ds-input",value:_.acceptance,onChange:V=>I({..._,acceptance:V.target.value})}))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"需求正文 / PRD 摘要"),s(d,{className:"ds-textarea",rows:10,style:{minHeight:180},placeholder:"支持 Markdown:`# 背景`、`- 列表`、`**重点**`、`` `代码` ``、`| 表格 |`;保存后在「需求明细」里按 Markdown 渲染,不再截断。",value:_.description,onChange:V=>I({..._,description:V.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{I(null),H(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:M,onClick:()=>void Se()},M?"保存中…":"保存需求")))),ht=Q===null?s("div",{className:"ds-empty"},"左边选一条需求,或先新建一条。"):(()=>{const V=ve(Q.id),ce=V.filter(j=>j.status==="done").length,Le=V.filter(j=>j.blocked!==void 0&&j.blocked!==null).length,At=V.filter(j=>j.type==="bug"&&j.status!=="done").length,$a=V.reduce((j,ue)=>{const Fe=ed(ue);return{done:j.done+Fe.done,total:j.total+Fe.total}},{done:0,total:0}),ra=V.reduce((j,ue)=>{const Fe=Jl(ue);return{pass:j.pass+Fe.pass,total:j.total+Fe.total}},{pass:0,total:0}),Ma=V.flatMap(j=>(j.artifacts??[]).map(ue=>({...ue,task:j}))),Dn=V.filter(j=>(j.gitlabMR?.iid??0)>0),Fo=ra.pass,On=Q.links??{},dn=Q.folder??{},Ln=(j,ue)=>ue===""?null:s("a",{className:"ds-link",href:ue,target:"_blank",rel:"noreferrer"},j);return s("div",{className:"ds-req-body"},s("div",{className:"ds-req-headline"},s("div",null,s("div",{className:"ds-req-code"},Q.code||Q.id),s("div",{className:"ds-req-name"},Q.title)),s("div",{className:"ds-req-actions"},s(g,{className:"ds-select",value:Q.status,onChange:j=>void Pe(j.target.value)},ef.map(j=>s("option",{key:j,value:j},Ar[j]))),s("button",{className:"ds-btn",onClick:()=>I({...Q,...On})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void dt(Q.id),"data-danger":"1"},"删除"))),(()=>{const j=Q.review??null,ue=j===null?"还没提交评审":rt[j.status]??j.status,Fe=j===null||j.status==="changes_requested",wt=j!==null&&j.status==="pending";return s("div",{className:"ds-panel","data-review":j===null?"none":j.status,style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求评审"),s(y,{className:"ds-chip","data-on":j?.status==="approved"?"1":"0",style:{marginLeft:8}},ue),j===null||j.at===0?null:s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ke(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},j!==null&&String(j.note??"")!==""?s("div",{className:"ds-hint",style:{marginBottom:8}},`评审意见:${j.note}`):null,s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},Fe?s("button",{className:"ds-btn ds-btn-primary",disabled:Je,onClick:()=>void Te("submit")},Je?"提交中…":"提交评审"):null,wt?s("button",{className:"ds-btn ds-btn-primary",disabled:Je,onClick:()=>void Te("approve")},"评审通过"):null,wt?s("button",{className:"ds-btn ds-btn-ghost",disabled:Je,onClick:It,"data-danger":"1"},"打回"):null,j?.status==="approved"?s("span",{className:"ds-hint"},"评审已通过,可以拆卡和派活了。"):null),tt===null?null:s("div",{className:"ds-hint",style:{marginTop:8}},tt)))})(),Ln("PRD",On.prd)||Ln("原型",On.proto)||Ln("验收标准",On.acceptance)?s("div",{className:"ds-req-links"},Ln("PRD 文档",On.prd),Ln("原型",On.proto),Ln("验收标准",On.acceptance)):s("div",{className:"ds-empty"},[s("span",null,"还没有登记 PRD / 原型 / 验收标准链接;PRD 与原型由产品在澄清后输出,可在这里补,或点右边「让产品补 PRD」。"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:async()=>{const j=await p("想请产品补什么?",{title:"申请产品补 PRD",defaultValue:"需要产品输出 PRD 与原型"});if(j!==null)try{await i("/requirement/request-prd",{id:Q.id,by:c(),note:j}),l("已申请产品补 PRD"),a()}catch(ue){l(ue.message??String(ue),"error")}}},"让产品补 PRD")]),Q.description?s("div",{className:"ds-panel ds-req-desc-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求明细"),Q.source==="business"?s("span",{className:"ds-hint","data-src":"business"},`业务提单 · ${Q.requester?.name||"未署名"}${Q.requester?.dept?"("+Q.requester.dept+")":""} · ${Ke(Q.requester?.at||Q.createdAt)}`):null,s("span",{className:"ds-hint",style:{marginLeft:"auto"}},"支持 Markdown")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-req-desc ds-md"},f(Q.description)))):s("div",{className:"ds-req-desc ds-hint"},"还没有写需求描述:产品背景、要解决的问题、验收口径都建议写在这里,点右侧「编辑」补上。"),(()=>{const j=(Q.history??[]).slice().reverse();if(j.length===0||(Q.change??null)!==null)return null;const ue={title:"标题",description:"需求明细",priority:"优先级",status:"状态",owner:"负责人",project:"项目",iteration:"迭代"};return s("div",{className:"ds-panel ds-history-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"变更记录"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`最近 ${j.length} 条`)),s("div",{className:"ds-panel-body"},s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:260,overflowY:"auto"}},j.map((Fe,wt)=>s("div",{key:`${Fe.at}-${wt}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint",style:{flex:"0 0 auto",fontVariantNumeric:"tabular-nums"}},Ke(Fe.at)),s("span",{style:{flex:"0 0 auto"}},Fe.by===""?"人":Fe.by),s("span",{className:"ds-chip",style:{flex:"0 0 auto"}},ue[Fe.field]??Fe.field),s("span",{className:"ds-hint",style:{minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},`${Fe.from===""?"(空)":Fe.from} → ${Fe.to===""?"(空)":Fe.to}`))))))})(),(()=>{const j=Q.pool??{reach:0,impact:0,confidence:0,effort:0,score:0},ue=async(wt,ma,et)=>{const De=await p(ma,{title:wt,defaultValue:String(et??0)});if(De===null)return null;const Ye=Number(String(De).trim());return Number.isFinite(Ye)&&Ye>=0?Ye:null},Fe=async()=>{const wt=await ue("影响面 Reach","影响多少用户或多少笔交易?估算依据写进正文",j.reach);if(wt===null)return;const ma=await ue("影响度 Impact","3 显著 / 2 高 / 1 中 / 0.5 低",j.impact);if(ma===null)return;const et=await ue("置信度 Confidence","100 有数据 / 80 有访谈 / 50 只有猜测(百分数)",j.confidence);if(et===null)return;const De=await ue("成本 Effort","人日(研发+测试+联调)",j.effort);if(De!==null)try{await i("/requirement/pool",{id:Q.id,pool:{reach:wt,impact:ma,confidence:et,effort:De}}),l("需求池评分已更新"),await a()}catch(Ye){l(Ye instanceof Error?Ye.message:String(Ye),"error")}};return s("div",{className:"ds-panel ds-pool-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求池评分(RICE)"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},j.score>0?`得分 ${j.score}`:"还没打分:不打分就没法和别的需求比优先级"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void Fe()},j.score>0?"重新打分":"打分")),j.score>0?s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},s("span",{className:"ds-chip"},`影响面 ${j.reach}`),s("span",{className:"ds-chip"},`影响度 ${j.impact}`),s("span",{className:"ds-chip"},`置信度 ${j.confidence}%`),s("span",{className:"ds-chip"},`成本 ${j.effort} 人日`),s("span",{className:"ds-chip","data-on":"1"},`得分 ${j.score}`))):null)})(),(()=>{const j=Q.questions??[],ue=j.filter(De=>De.status==="open").length,Fe=async(De,Ye)=>{try{await i("/requirement/questions",{id:Q.id,...De}),Ye&&l(Ye),await a()}catch(Mn){l(Mn instanceof Error?Mn.message:String(Mn),"error")}},wt=async()=>{const De=await p("还没确认清楚的问题是什么?",{title:"加一个待确认问题",placeholder:"例如:退款失败时是否允许人工补发?"});De===null||De.trim()===""||await Fe({action:"add",text:De},"已加入待确认清单")},ma=async De=>{const Ye=await p(De.text,{title:"回答这个问题",defaultValue:De.answer??"",placeholder:"写清结论与依据"});Ye===null||Ye.trim()===""||await Fe({action:"answer",questionId:De.id,answer:Ye},"已回答")},et=async De=>{await u("删掉这个问题?",{title:"删除待确认项",danger:!0,okText:"删除"})===!0&&await Fe({action:"delete",questionId:De.id},"已删除")};return s("div",{className:"ds-panel ds-question-panel","data-open":ue>0?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"待确认清单"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},ue>0?`${ue} 个没回答,回答完才能拆卡`:j.length===0?"澄清阶段把不确定的先记在这里":"全部已回答"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void wt()},"+ 加问题")),s("div",{className:"ds-panel-body"},j.length===0?s("div",{className:"ds-empty"},"还没有待确认的问题。产品澄清时把「需要业务拍板」的点写在这里,避免猜业务规则。"):s("div",{style:{display:"flex",flexDirection:"column",gap:8}},j.map(De=>s("div",{key:De.id,className:"ds-question-row","data-status":De.status,style:{display:"flex",gap:10,alignItems:"flex-start",padding:"8px 10px",border:"1px solid var(--border)",borderRadius:8,background:"var(--card)"}},s("span",{className:"ds-chip","data-on":De.status==="answered"?"1":"0"},De.status==="answered"?"已回答":"待回答"),s("div",{style:{flex:"1 1 auto",minWidth:0}},s("div",{style:{fontSize:13}},De.text),De.answer===""||De.answer===void 0?null:s("div",{className:"ds-hint",style:{marginTop:4}},`答:${De.answer}`)),s("div",{style:{display:"flex",gap:6,flex:"0 0 auto"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void ma(De)},De.status==="answered"?"改答案":"回答"),s("button",{className:"ds-btn ds-btn-ghost","data-danger":"1",onClick:()=>void et(De)},"删除")))))))})(),(()=>{const j=Q.change??null;if(j===null)return null;const ue=j.affected??[],Fe=j.affectedCases??[],wt=new Map(V.map(et=>[et.id,et])),ma=et=>e.statuses.find(De=>De.id===et)?.label??et;return s("div",{className:"ds-panel ds-change-panel","data-invalidated":j.invalidated?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,j.invalidated?"变更影响(签字已作废)":"变更影响"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ke(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},function(){const et=(Q.history??[]).slice().reverse();if(et.length===0)return null;const De={title:"标题",description:"需求明细",priority:"优先级",status:"状态",owner:"负责人",project:"项目",iteration:"迭代"};return s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:220,overflowY:"auto",marginBottom:8,paddingBottom:8,borderBottom:"1px dashed var(--border)"}},s("div",{className:"ds-hint",style:{fontWeight:600}},`变更记录 · 最近 ${et.length} 条`),et.map((Ye,Mn)=>s("div",{key:`${Ye.at}-${Mn}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint"},Ke(Ye.at)),s("span",null,Ye.by===""?"人":Ye.by),s("span",{className:"ds-chip"},De[Ye.field]??Ye.field),s("span",{className:"ds-hint"},`${Ye.from===""?"(空)":Ye.from} → ${Ye.to===""?"(空)":Ye.to}`))))}(),s("div",{className:"ds-hint"},`改了:${(j.parts??[]).join("、")}。${ue.length===0?"当时没有已产出的卡受影响,不用返工。":`${ue.length} 张卡已经产出过东西${Fe.length===0?"":`、${Fe.length} 条用例要重跑`},要按新版复核:`}`),ue.length===0?null:s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8,marginTop:8}},ue.map(et=>s("span",{key:et.id,className:"ds-chip","data-pt":"open",style:{cursor:"pointer"},title:et.title,onClick:()=>{const De=wt.get(et.id);De!==void 0&&typeof t=="function"&&t(De)}},`${et.code||et.id} ${ma(et.status)}`))),j.invalidated?s("div",{className:"ds-hint",style:{marginTop:8}},"验收签字已清空:这些卡是按旧版做的,复核完重新签一次。"):null))})(),(()=>{const j=Q.deploys??[];return j.length===0?null:s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"上线足迹"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},"来自版本的部署记录,只留痕不触发")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},j.map(ue=>s("span",{key:ue.id,className:"ds-chip","data-env":ue.kind,title:`${ue.releaseName??""} · ${Ke(ue.at)} · ${ue.by}`},`${ue.name} · ${Xa(ue.at)}`)))))})(),s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"依赖与排序")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row"},s("span",{className:"ds-hint"},"排序号"),s(v,{className:"ds-input",style:{width:90},value:Ve,onChange:j=>Ya(j.target.value)}),s("span",{className:"ds-hint"},"被依赖的需求没交付,这条需求的准出会被拦")),s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:10}},h.filter(j=>j.id!==Q.id).map(j=>s("label",{key:j.id,className:"ds-check-row"},s(k,{checked:Rn.includes(j.id),onChange:ue=>U(Fe=>ue.target.checked?[...Fe,j.id]:Fe.filter(wt=>wt!==j.id))}),s("span",{className:"ds-check-text"},`${j.code||j.id} ${j.title}(${Ar[j.status]??j.status})`))),h.length<=1?s("span",{className:"ds-hint"},"只有这一条需求,暂时没有可依赖的对象。"):null),s("div",{className:"ds-row"},s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void Ea()},"保存排序与依赖"),he===null?null:s("span",{className:"ds-hint"},he)))),(()=>{const j=Q.progress??null,ue=j?.gate??{ready:!1,blockers:["还没有加载出准出状态,稍后自动刷新"],warnings:[]},Fe=ue.stats??{},wt=Q.signoff??null,ma=j?.progress??(V.length===0?0:Math.round(ce/V.length*100)),et=Fe.coveredPoints??0,De=Fe.points??(Q.points??[]).length,Ye=Fe.pass??Fo,Mn=Fe.cases??ra.total;return s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求准出与验收签字"),s(y,{className:"ds-chip","data-pt":ue.ready?"covered":"open",style:{marginLeft:"auto"}},ue.ready?"可以交付":`还差 ${ue.blockers.length} 项`)),s("div",{className:"ds-panel-body"},s("div",{className:"ds-check-progress"},s("div",{className:"ds-check-bar","data-full":ma===100?"1":"0"},s("span",{style:{width:`${ma}%`}})),s("div",{className:"ds-hint"},`${ma}% · ${ce}/${V.length} 张卡完成 · 功能点覆盖 ${et}/${De} · 用例通过 ${Ye}/${Mn}`)),(ue.blockers??[]).map(zn=>s("div",{key:zn,className:"ds-gate-item","data-kind":"block"},`⛔ ${zn}`)),(ue.warnings??[]).map(zn=>s("div",{key:zn,className:"ds-gate-item","data-kind":"warn"},`⚠ ${zn}`)),ue.ready?s("div",{className:"ds-hint"},"门禁已通过:签完字这条需求就算交付。"):null,wt===null?s("div",{className:"ds-gate-sign"},s(v,{className:"ds-input",style:{flex:1,minWidth:220},placeholder:"验收结论:交付了什么、遗留什么",value:W,onChange:zn=>Ee(zn.target.value)}),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void hs("sign")},"需求验收签字")):s("div",{className:"ds-gate-sign"},s("div",{className:"ds-hint",style:{flex:1}},`✍ ${wt.by} 于 ${Ke(wt.at)} 签字:${wt.note}`),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void hs("clear")},"取消签字")),ke===null?null:s("div",{className:"ds-hint"},ke)))})(),s("div",{className:"ds-sec-head",style:{marginTop:16}},"交付物归档(飞书云盘)",s("div",{className:"ds-req-actions"},dn.url?s("a",{className:"ds-link",href:dn.url,target:"_blank",rel:"noreferrer"},"打开文件夹"):null,s("button",{className:"ds-btn ds-btn-ghost",disabled:He,onClick:()=>void La()},dn.url?"校验文件夹":"创建文件夹"),s("button",{className:"ds-btn",disabled:He||Ma.length===0,onClick:()=>void Lt()},He?"归档中…":`归档 ${Ma.length} 件交付物`))),s("div",{className:"ds-hint"},dn.url?`文件夹:${dn.name||dn.token} 已归档 ${Ma.filter(j=>j.doc?.url).length}/${Ma.length} 件交付物`:dn.error?`上次创建失败:${dn.error}`:"还没有云盘文件夹;建好后,这条需求下每件交付物都会写成文件夹里的一份 Markdown 文档。"),fe?s("div",{className:"ds-hint"},fe):null,s("div",{className:"ds-sec-head",style:{marginTop:16}},"文档与基线",s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void me(null)},"+ 添加文档")),Q.baseline?s("div",{className:"ds-base-box"},s("div",{className:"ds-base-main"},s("b",null,`当前基线 ${Q.baseline.version}`),s("span",null,`${Q.baseline.title} · 生效于 ${Ke(Q.baseline.at)} · ${Q.baseline.by||"人"} · 正文 ${Q.baseline.contentLength??0} 字`)),s("button",{className:"ds-btn ds-btn-ghost",title:"把当前文档和这份基线逐行比一遍(只读)",onClick:()=>void Et()},P===null?"对比基线":"收起对比"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void jn()},"取消基线")):s("div",{className:"ds-base-box","data-empty":"1"},s("div",{className:"ds-base-main"},s("b",null,"还没有基线"),s("span",null,"PRD 评审通过后点「设为基线」,之后建的卡都会记下用的是哪一版。"))),Q.baseline!==null&&P!==null?s("div",{className:"ds-diff"},s("div",{className:"ds-diff-head"},s("b",null,`基线 ${P.version} → 当前:${P.docTitle}`),s("span",{className:"ds-hint"},P.summary.added+P.summary.removed===0?"正文没有变化":`+${P.summary.added} 行 / −${P.summary.removed} 行`)),s("div",{className:"ds-diff-body"},P.rows.slice(0,400).map((j,ue)=>s("div",{key:ue,className:"ds-diff-line","data-kind":j.kind},`${j.kind==="add"?"+":j.kind==="del"?"−":" "} ${j.text}`)),P.rows.length>400?s("div",{className:"ds-hint"},`(还有 ${P.rows.length-400} 行未显示)`):null)):null,(Q.docs??[]).length===0?s("div",{className:"ds-hint",style:{marginTop:8}},"还没有登记文档。可以先加 PRD 链接,把正文粘进来,评审通过后设为基线。"):s("div",null,s("div",{className:"ds-row",style:{marginTop:8,gap:8,alignItems:"center"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void fa()},R===null?"全部展开":"全部收起"),s("span",{className:"ds-hint"},`共 ${(Q.docs??[]).length} 篇,展开后可直接读 Markdown 正文`)),s("div",{className:"ds-doc-list"},(Q.docs??[]).map(j=>s("div",{key:j.id,className:"ds-doc-wrap"},s("div",{className:"ds-doc-row"},s(y,{className:"ds-chip","data-doc":j.kind},tf[j.kind]??j.kind),s("div",{className:"ds-doc-main"},s("div",{className:"ds-doc-title"},j.url===""?j.title:s("a",{className:"ds-link",href:j.url,target:"_blank",rel:"noreferrer"},j.title),s("span",{className:"ds-chip-code"},j.version),s(y,{className:"ds-chip","data-docstatus":j.status},cx[j.status]??j.status)),s("div",{className:"ds-doc-meta"},`正文 ${j.contentLength??0} 字 · ${j.by||"—"} · ${j.at>0?Ke(j.at):"—"}`)),s("div",{className:"ds-listrow-actions"},j.status==="baseline"?null:s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Mt(j.id)},"设为基线"),s("button",{className:"ds-btn ds-btn-ghost",title:"直接读正文(Markdown 渲染)",onClick:()=>void Qa(j.id)},A!==null&&A.id===j.id?"收起":"阅读"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void me(j.id)},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Z(j.id),"data-danger":"1"},"删除"))),A!==null&&A.id===j.id?s("div",{className:"ds-md-preview ds-reader"},A.busy?s("span",{className:"ds-hint"},"读取中…"):A.content.trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(A.content)):R!==null&&R[j.id]!==void 0?s("div",{className:"ds-md-preview ds-reader"},R[j.id].trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(R[j.id])):null)))),q===null?null:s("div",{className:"ds-hint",style:{marginTop:6}},q),B===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},B.id===void 0?"添加文档":`编辑 ${B.title}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"类型"),s(g,{className:"ds-select",value:B.kind,onChange:j=>J({...B,kind:j.target.value})},dx.map(j=>s("option",{key:j,value:j},tf[j])))),s("div",{className:"ds-field",style:{flex:"1 1 220px"}},s("div",{className:"ds-label"},"标题"),s(v,{className:"ds-input",value:B.title,placeholder:"例如 会员积分抵扣 PRD",onChange:j=>J({...B,title:j.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 110px"}},s("div",{className:"ds-label"},"版本"),s(v,{className:"ds-input",value:B.version,placeholder:"v1.0",onChange:j=>J({...B,version:j.target.value})}))),s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"1 1 auto"}},s("div",{className:"ds-label"},"飞书文档链接"),s(v,{className:"ds-input",value:B.url,placeholder:"https://<租户>.feishu.cn/wiki/…",onChange:j=>J({...B,url:j.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 auto",justifyContent:"flex-end"}},s("div",{className:"ds-label"}," "),s("button",{className:"ds-btn",disabled:G,onClick:()=>void _e()},G?"拉取中…":"从链接拉正文"))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"文档正文(拉不到就自己粘,研发和测试都读这一份)"),s(d,{className:"ds-textarea",rows:8,value:B.content,onChange:j=>J({...B,content:j.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{J(null),L(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:G,onClick:()=>void An()},"保存文档")))),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"需求变更影响面"),s("div",{className:"ds-pane-desc"},"需求一改,下面这些卡和用例就要重看。点一下现算,不改任何数据。")),s("div",{className:"ds-row",style:{gap:8,alignItems:"center",marginBottom:6}},s("button",{className:"ds-btn",disabled:Gt||pt===null,onClick:()=>void Ct()},Gt?"计算中…":"算一下影响面"),Ue===null?s("span",{className:"ds-hint"},"还没算过"):s("span",{className:"ds-hint"},`卡 ${Ue.summary.cards} · 用例 ${Ue.summary.cases}(能自动跑 ${Ue.summary.runnable})· 失败 ${Ue.summary.failing}`)),Ue===null||Ue.cards.length===0?null:s("div",{className:"ds-sec-body",style:{maxHeight:160,overflow:"auto"}},Ue.cards.slice(0,12).map(j=>s("div",{key:j.id,className:"ds-art-row"},s("span",{className:"ds-art-title"},(j.code===""?"":j.code+" ")+j.title),s("span",{className:"ds-hint"},j.status))),Ue.cards.length>12?s("div",{className:"ds-hint"},`还有 ${Ue.cards.length-12} 张`):null),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"功能点与可追溯矩阵"),s("div",{className:"ds-pane-desc"},"功能点是需求的骨架:一条功能点对若干张卡、若干条用例。卡和用例上都挂 pointId,这里就能看出哪条没人做、哪条没被验证。")),F===null?s("div",{className:"ds-hint",style:{padding:"6px 0"}},"还没有功能点。点「+ 加功能点」把需求拆成可交付、可验证的条目。"):s("div",{className:"ds-matrix"},s("div",{className:"ds-matrix-sum"},`功能点 ${F.summary.points} · 已覆盖 ${F.summary.coveredPoints}${F.summary.pointsWithoutCases>0?`(${F.summary.pointsWithoutCases} 条还没有用例,覆盖率 ${F.summary.coverageRate}%)`:""} · 卡 ${F.summary.cards}(未归点 ${F.summary.looseCards})· 用例 ${F.summary.cases}(通过 ${F.summary.pass}${F.summary.fail>0?`,失败 ${F.summary.fail}`:""})· 通过率 ${F.summary.passRate}%`),(F.points??[]).map(j=>s("div",{key:j.id,className:"ds-pt-row","data-covered":j.covered?"1":"0"},s("div",{className:"ds-pt-main"},s("div",{className:"ds-pt-title"},s("span",{className:"ds-chip-code"},j.id),s("b",null,j.title),s(y,{className:"ds-chip","data-pt":j.covered?"covered":"open"},j.covered?"已覆盖":"未覆盖")),String(j.description??"")===""?null:s("div",{className:"ds-req-desc ds-md"},f(j.description)),s("div",{className:"ds-pt-meta"},`卡 ${j.cards.length}${j.cards.length===0?"(还没人做)":":"+j.cards.map(ue=>ue.code||ue.id).join("、")}`,` · 用例 ${j.cases.total}(通过 ${j.cases.pass}${j.cases.fail>0?`,失败 ${j.cases.fail}`:""}${j.cases.pending>0?`,没跑 ${j.cases.pending}`:""})`,j.acceptance.length===0?"":` · 验收条件 ${j.acceptance.filter(ue=>ue.covered).length}/${j.acceptance.length}`)),s("div",{className:"ds-listrow-actions"},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>ge({...j,acceptanceText:(j.acceptance??[]).map(ue=>ue.text).join(`
|
|
45
|
+
`).map(At=>At.trim()).filter(At=>At!==""),ce={id:ie.id??`p${(Q.points??[]).length+1}`,title:ie.title,description:ie.description??"",priority:ie.priority??"normal",status:ie.status??"draft",acceptance:V.map((At,$a)=>({id:`a${$a+1}`,text:At,done:!1}))},Le=(Q.points??[]).filter(At=>At.id!==ce.id);await ne([...Le,ce])},se=async V=>{Q!==null&&await u("删除这个功能点?挂在上面的卡会变成「未归功能点」。",{title:"删除功能点",danger:!0,okText:"删除"})&&await ne((Q.points??[]).filter(ce=>ce.id!==V))},de=async(V,ce)=>{try{await i("/task/update",{id:V,pointId:ce}),Q!==null&&await Y(Q.id)}catch(Le){X(Le.message)}},Q=h.find(V=>V.id===S)??null,ve=V=>e.tasks.filter(ce=>ce.requirementId===V),Ce=V=>e.statuses.find(ce=>ce.id===V)?.label??V;o.useEffect(()=>{te(null),X(null),Q!==null&&Y(Q.id)},[Q===null?"":Q.id,Y]);const Se=async()=>{if(_!==null){if(String(_.title??"").trim()===""){H("需求标题不能为空");return}$(!0);try{const V=await i("/requirement/save",{requirement:{id:_.id,title:_.title,description:_.description,project:_.project,owner:_.owner,domain:_.domain??"",priority:_.priority,status:_.status,links:{prd:_.prd,proto:_.proto,acceptance:_.acceptance},discovery:_.discovery??{server:"",auth:"",boundary:""}}});C(V.requirement.id),I(null),H(null),l(V.created===!0?"需求已创建":"需求已保存")}catch(V){H(V.message)}finally{$(!1)}}},Pe=async V=>{if(Q!==null)try{await i("/requirement/save",{requirement:{...Q,links:Q.links??{},status:V}}),l("需求状态已更新")}catch(ce){H(ce.message)}},dt=async V=>{if(await u("删除这条需求?它下面的卡会保留,只是不再挂需求。",{title:"删除需求",danger:!0,okText:"删除"}))try{await i("/requirement/delete",{id:V}),C(null)}catch(ce){H(ce.message)}},rt={pending:"待评审",approved:"评审通过",changes_requested:"评审打回"},[Je,We]=o.useState(!1),[tt,Ge]=o.useState(null),Te=async(V,ce)=>{if(Q!==null){We(!0),Ge(null);try{const Le=await i("/requirement/review",{id:Q.id,decision:V,note:ce??"",by:c(),operator:c()});Ge(`已${V==="approve"?"通过":V==="reject"?"打回":"提交"}评审:${rt[Le.review?.status]??""}`),typeof a=="function"&&a()}catch(Le){Ge(Le.message)}finally{We(!1)}}},It=async()=>{const V=await p("打回要给意见(会记在评审记录里):",{title:"评审打回",danger:!0,okText:"打回",placeholder:"写清楚哪里不满足,之后要据此改"});if(V!==null){if(String(V).trim()===""){Ge("打回必须写意见");return}Te("reject",String(V).trim().slice(0,600))}},[Be,yt]=o.useState(!1),[Rt,Ut]=o.useState(null),le=async()=>{if(Q!==null){yt(!0),Ut(null);try{const ce=(await i("/requirement/split",{id:Q.id})).created??[];Ut(ce.length===0?"没有可拆的功能点:要么都覆盖了,要么还没加功能点。":`已建 ${ce.length} 张卡:${ce.map(Le=>Le.code||Le.id).join("、")}`),typeof a=="function"&&a()}catch(V){Ut(V.message)}finally{yt(!1)}}},Ne=()=>({kind:"prd",title:"",url:Q?.links?.prd??"",version:"v1.0",content:"",status:"draft"}),me=async V=>{if(L(null),V===null){J(Ne());return}try{const Le=((await i("/requirement/get",{id:Q.id})).requirement?.docs??[]).find(At=>At.id===V);J(Le===void 0?Ne():{...Le})}catch(ce){H(ce.message)}},Qa=async V=>{if(A!==null&&A.id===V){x(null);return}x({id:V,content:"",busy:!0});try{const Le=((await i("/requirement/get",{id:Q.id})).requirement?.docs??[]).find(At=>At.id===V);x({id:V,content:String(Le?.content??""),busy:!1})}catch(ce){H(ce.message),x(null)}},Et=async()=>{if(P!==null){K(null);return}try{const ce=(await i("/requirement/get",{id:Q.id})).requirement??{},Le=ce.baseline??null,$a=[...ce.docs??[]].sort((Dn,Fo)=>(Number(Fo.at)||0)-(Number(Dn.at)||0)),ra=$a.find(Dn=>Dn.kind==="prd")??$a[0]??null;if(Le===null){K(null),H("这条需求还没有基线,先挑一份文档「设为基线」");return}if(ra===null){K(null),H("这条需求还没有文档可以对比");return}const Ma=sw(Le.content??"",ra.content??"");K({rows:Ma,summary:rw(Ma),version:Le.version,docTitle:ra.title})}catch(V){K(null),H(V instanceof Error?V.message:String(V))}},fa=async()=>{if(R!==null){O(null);return}try{const V=await i("/requirement/get",{id:Q.id}),ce={};for(const Le of V.requirement?.docs??[])ce[Le.id]=String(Le.content??"");O(ce)}catch(V){H(V.message)}},An=async()=>{if(B!==null){if(String(B.title??"").trim()===""){L("文档要有标题");return}we(!0);try{const V=await i("/requirement/doc",{id:Q.id,action:B.id===void 0?"add":"update",docId:B.id,doc:{kind:B.kind,title:B.title,url:B.url,version:B.version,content:B.content,status:B.status}});J(null),L(null)}catch(V){L(V.message)}finally{we(!1)}}},La=async()=>{ae(!0),ye(null);try{const V=await i("/requirement/folder",{id:Q.id});ye(V.ok?`云盘文件夹已就绪:${V.folder?.url??""}`:`创建失败:${V.error||V.folder?.error||"未知原因"}`)}catch(V){ye(V.message)}finally{ae(!1)}},Lt=async()=>{ae(!0),ye(null);try{const ce=(await i("/requirement/sync",{id:Q.id})).report??{},Le=Number(ce.failed??0);ye(`归档完成:共 ${ce.total??0} 件,成功 ${ce.written??0} 件${Le>0?`,失败 ${Le} 件(${ce.errors?.[0]??""})`:"。"}`)}catch(V){ye(V.message)}finally{ae(!1)}},Mt=async V=>{try{await i("/requirement/doc",{id:Q.id,action:"baseline",docId:V,by:"人"}),L("这一版已生效,之后新建的卡都会记下它。")}catch(ce){L(ce.message)}},jn=async()=>{try{await i("/requirement/doc",{id:Q.id,action:"clear-baseline"}),L("基线已取消,需求回到「评审中」。")}catch(V){L(V.message)}},Z=async V=>{if(await u("删除这份文档?",{title:"删除文档",danger:!0,okText:"删除"}))try{await i("/requirement/doc",{id:Q.id,action:"remove",docId:V})}catch(ce){L(ce.message)}},_e=async()=>{we(!0),L(null);try{const V=await i("/requirement/pull",{url:B?.url??""});if(V.ok===!0){const ce=String(V.text??"");J(Le=>({...Le,title:String(Le?.title??"").trim()===""?String(V.title??""):Le.title,content:ce})),L(`已拉取 ${ce.length} 字正文${V.title?`,标题取自文档「${V.title}」`:""}。`)}else L(V.error??"拉取失败")}catch(V){L(V.message)}finally{we(!1)}},xt=_===null?null:s("div",{className:"ds-panel ds-form-panel"},s("div",{className:"ds-panel-head"},_.id===void 0?"新建需求":`编辑 ${Q?.code??""}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-field"},s("div",{className:"ds-label"},"需求标题"),s(v,{className:"ds-input",value:_.title,onChange:V=>I({..._,title:V.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"有没有服务端?数据存在哪、刷新会不会丢"),s(v,{className:"ds-input",placeholder:"例:有,node:sqlite;或「纯前端演示,刷新即还原」",value:_.discovery?.server??"",onChange:V=>I({..._,discovery:{..._.discovery??{},server:V.target.value}})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"鉴权谁做:前端隐藏按钮,还是服务端拒绝"),s(v,{className:"ds-input",placeholder:"例:服务端校验角色,viewer 调写接口直接 403",value:_.discovery?.auth??"",onChange:V=>I({..._,discovery:{..._.discovery??{},auth:V.target.value}})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"交付边界:只交付界面,还是点开就能用"),s(v,{className:"ds-input",placeholder:"例:前后端都交付,含测试;或「只做 UI,接口后续接」",value:_.discovery?.boundary??"",onChange:V=>I({..._,discovery:{..._.discovery??{},boundary:V.target.value}})})),s("div",{className:"ds-row"},s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务项目"),s(g,{className:"ds-select",value:_.project,onChange:V=>I({..._,project:V.target.value})},s("option",{value:""},"不指定"),b.map(V=>s("option",{key:V.id,value:V.id},V.name)))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"状态"),s(g,{className:"ds-select",value:_.status,onChange:V=>I({..._,status:V.target.value})},ef.map(V=>s("option",{key:V,value:V},Ar[V])))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务负责人"),s(v,{className:"ds-input",value:_.owner,onChange:V=>I({..._,owner:V.target.value})})),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务领域"),s(v,{className:"ds-input",placeholder:"支付 / 风控 / 会员(决定派给哪位领域专家)",value:_.domain??"",onChange:V=>I({..._,domain:V.target.value})}))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"PRD 链接"),s(v,{className:"ds-input",placeholder:"https://…(飞书云文档)",value:_.prd,onChange:V=>I({..._,prd:V.target.value})})),s("div",{className:"ds-row"},s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"原型链接"),s(v,{className:"ds-input",value:_.proto,onChange:V=>I({..._,proto:V.target.value})})),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"验收标准链接"),s(v,{className:"ds-input",value:_.acceptance,onChange:V=>I({..._,acceptance:V.target.value})}))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"需求正文 / PRD 摘要"),s(d,{className:"ds-textarea",rows:10,style:{minHeight:180},placeholder:"支持 Markdown:`# 背景`、`- 列表`、`**重点**`、`` `代码` ``、`| 表格 |`;保存后在「需求明细」里按 Markdown 渲染,不再截断。",value:_.description,onChange:V=>I({..._,description:V.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{I(null),H(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:M,onClick:()=>void Se()},M?"保存中…":"保存需求")))),ht=Q===null?s("div",{className:"ds-empty"},"左边选一条需求,或先新建一条。"):(()=>{const V=ve(Q.id),ce=V.filter(j=>j.status==="done").length,Le=V.filter(j=>j.blocked!==void 0&&j.blocked!==null).length,At=V.filter(j=>j.type==="bug"&&j.status!=="done").length,$a=V.reduce((j,ue)=>{const Fe=ed(ue);return{done:j.done+Fe.done,total:j.total+Fe.total}},{done:0,total:0}),ra=V.reduce((j,ue)=>{const Fe=Jl(ue);return{pass:j.pass+Fe.pass,total:j.total+Fe.total}},{pass:0,total:0}),Ma=V.flatMap(j=>(j.artifacts??[]).map(ue=>({...ue,task:j}))),Dn=V.filter(j=>(j.gitlabMR?.iid??0)>0),Fo=ra.pass,On=Q.links??{},dn=Q.folder??{},Ln=(j,ue)=>ue===""?null:s("a",{className:"ds-link",href:ue,target:"_blank",rel:"noreferrer"},j);return s("div",{className:"ds-req-body"},s("div",{className:"ds-req-headline"},s("div",null,s("div",{className:"ds-req-code"},Q.code||Q.id),s("div",{className:"ds-req-name"},Q.title)),s("div",{className:"ds-req-actions"},s(g,{className:"ds-select",value:Q.status,onChange:j=>void Pe(j.target.value)},ef.map(j=>s("option",{key:j,value:j},Ar[j]))),s("button",{className:"ds-btn",onClick:()=>I({...Q,...On})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void dt(Q.id),"data-danger":"1"},"删除"))),(()=>{const j=Q.review??null,ue=j===null?"还没提交评审":rt[j.status]??j.status,Fe=j===null||j.status==="changes_requested",wt=j!==null&&j.status==="pending";return s("div",{className:"ds-panel","data-review":j===null?"none":j.status,style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求评审"),s(y,{className:"ds-chip","data-on":j?.status==="approved"?"1":"0",style:{marginLeft:8}},ue),j===null||j.at===0?null:s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ke(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},j!==null&&String(j.note??"")!==""?s("div",{className:"ds-hint",style:{marginBottom:8}},`评审意见:${j.note}`):null,s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},Fe?s("button",{className:"ds-btn ds-btn-primary",disabled:Je,onClick:()=>void Te("submit")},Je?"提交中…":"提交评审"):null,wt?s("button",{className:"ds-btn ds-btn-primary",disabled:Je,onClick:()=>void Te("approve")},"评审通过"):null,wt?s("button",{className:"ds-btn ds-btn-ghost",disabled:Je,onClick:It,"data-danger":"1"},"打回"):null,j?.status==="approved"?s("span",{className:"ds-hint"},"评审已通过,可以拆卡和派活了。"):null),tt===null?null:s("div",{className:"ds-hint",style:{marginTop:8}},tt)))})(),Ln("PRD",On.prd)||Ln("原型",On.proto)||Ln("验收标准",On.acceptance)?s("div",{className:"ds-req-links"},Ln("PRD 文档",On.prd),Ln("原型",On.proto),Ln("验收标准",On.acceptance)):s("div",{className:"ds-empty"},[s("span",null,"还没有登记 PRD / 原型 / 验收标准链接;PRD 与原型由产品在澄清后输出,可在这里补,或点右边「让产品补 PRD」。"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:async()=>{const j=await p("想请产品补什么?",{title:"申请产品补 PRD",defaultValue:"需要产品输出 PRD 与原型"});if(j!==null)try{await i("/requirement/request-prd",{id:Q.id,by:c(),note:j}),l("已申请产品补 PRD"),a()}catch(ue){l(ue.message??String(ue),"error")}}},"让产品补 PRD")]),Q.description?s("div",{className:"ds-panel ds-req-desc-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求明细"),Q.source==="business"?s("span",{className:"ds-hint","data-src":"business"},`业务提单 · ${Q.requester?.name||"未署名"}${Q.requester?.dept?"("+Q.requester.dept+")":""} · ${Ke(Q.requester?.at||Q.createdAt)}`):null,s("span",{className:"ds-hint",style:{marginLeft:"auto"}},"支持 Markdown")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-req-desc ds-md"},f(Q.description)))):s("div",{className:"ds-req-desc ds-hint"},"还没有写需求描述:产品背景、要解决的问题、验收口径都建议写在这里,点右侧「编辑」补上。"),(()=>{const j=(Q.history??[]).slice().reverse();if(j.length===0||(Q.change??null)!==null)return null;const ue={title:"标题",description:"需求明细",priority:"优先级",status:"状态",owner:"负责人",project:"项目",iteration:"迭代"};return s("div",{className:"ds-panel ds-history-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"变更记录"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`最近 ${j.length} 条`)),s("div",{className:"ds-panel-body"},s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:260,overflowY:"auto"}},j.map((Fe,wt)=>s("div",{key:`${Fe.at}-${wt}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint",style:{flex:"0 0 auto",fontVariantNumeric:"tabular-nums"}},Ke(Fe.at)),s("span",{style:{flex:"0 0 auto"}},Fe.by===""?"人":Fe.by),s("span",{className:"ds-chip",style:{flex:"0 0 auto"}},ue[Fe.field]??Fe.field),s("span",{className:"ds-hint",style:{minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},`${Fe.from===""?"(空)":Fe.from} → ${Fe.to===""?"(空)":Fe.to}`))))))})(),(()=>{const j=Q.pool??{reach:0,impact:0,confidence:0,effort:0,score:0},ue=async(wt,ma,et)=>{const De=await p(ma,{title:wt,defaultValue:String(et??0)});if(De===null)return null;const Ye=Number(String(De).trim());return Number.isFinite(Ye)&&Ye>=0?Ye:null},Fe=async()=>{const wt=await ue("影响面 Reach","影响多少用户或多少笔交易?估算依据写进正文",j.reach);if(wt===null)return;const ma=await ue("影响度 Impact","3 显著 / 2 高 / 1 中 / 0.5 低",j.impact);if(ma===null)return;const et=await ue("置信度 Confidence","100 有数据 / 80 有访谈 / 50 只有猜测(百分数)",j.confidence);if(et===null)return;const De=await ue("成本 Effort","人日(研发+测试+联调)",j.effort);if(De!==null)try{await i("/requirement/pool",{id:Q.id,pool:{reach:wt,impact:ma,confidence:et,effort:De}}),l("需求池评分已更新"),await a()}catch(Ye){l(Ye instanceof Error?Ye.message:String(Ye),"error")}};return s("div",{className:"ds-panel ds-pool-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求池评分(RICE)"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},j.score>0?`得分 ${j.score}`:"还没打分:不打分就没法和别的需求比优先级"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void Fe()},j.score>0?"重新打分":"打分")),j.score>0?s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},s("span",{className:"ds-chip"},`影响面 ${j.reach}`),s("span",{className:"ds-chip"},`影响度 ${j.impact}`),s("span",{className:"ds-chip"},`置信度 ${j.confidence}%`),s("span",{className:"ds-chip"},`成本 ${j.effort} 人日`),s("span",{className:"ds-chip","data-on":"1"},`得分 ${j.score}`))):null)})(),(()=>{const j=Q.questions??[],ue=j.filter(De=>De.status==="open").length,Fe=async(De,Ye)=>{try{await i("/requirement/questions",{id:Q.id,...De}),Ye&&l(Ye),await a()}catch(Mn){l(Mn instanceof Error?Mn.message:String(Mn),"error")}},wt=async()=>{const De=await p("还没确认清楚的问题是什么?",{title:"加一个待确认问题",placeholder:"例如:退款失败时是否允许人工补发?"});De===null||De.trim()===""||await Fe({action:"add",text:De},"已加入待确认清单")},ma=async De=>{const Ye=await p(De.text,{title:"回答这个问题",defaultValue:De.answer??"",placeholder:"写清结论与依据"});Ye===null||Ye.trim()===""||await Fe({action:"answer",questionId:De.id,answer:Ye},"已回答")},et=async De=>{await u("删掉这个问题?",{title:"删除待确认项",danger:!0,okText:"删除"})===!0&&await Fe({action:"delete",questionId:De.id},"已删除")};return s("div",{className:"ds-panel ds-question-panel","data-open":ue>0?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"待确认清单"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},ue>0?`${ue} 个没回答,回答完才能拆卡`:j.length===0?"澄清阶段把不确定的先记在这里":"全部已回答"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void wt()},"+ 加问题")),s("div",{className:"ds-panel-body"},j.length===0?s("div",{className:"ds-empty"},"还没有待确认的问题。产品澄清时把「需要业务拍板」的点写在这里,避免猜业务规则。"):s("div",{style:{display:"flex",flexDirection:"column",gap:8}},j.map(De=>s("div",{key:De.id,className:"ds-question-row","data-status":De.status,style:{display:"flex",gap:10,alignItems:"flex-start",padding:"8px 10px",border:"1px solid var(--border)",borderRadius:8,background:"var(--card)"}},s("span",{className:"ds-chip","data-on":De.status==="answered"?"1":"0"},De.status==="answered"?"已回答":"待回答"),s("div",{style:{flex:"1 1 auto",minWidth:0}},s("div",{style:{fontSize:13}},De.text),De.answer===""||De.answer===void 0?null:s("div",{className:"ds-hint",style:{marginTop:4}},`答:${De.answer}`)),s("div",{style:{display:"flex",gap:6,flex:"0 0 auto"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void ma(De)},De.status==="answered"?"改答案":"回答"),s("button",{className:"ds-btn ds-btn-ghost","data-danger":"1",onClick:()=>void et(De)},"删除")))))))})(),(()=>{const j=Q.change??null;if(j===null)return null;const ue=j.affected??[],Fe=j.affectedCases??[],wt=new Map(V.map(et=>[et.id,et])),ma=et=>e.statuses.find(De=>De.id===et)?.label??et;return s("div",{className:"ds-panel ds-change-panel","data-invalidated":j.invalidated?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,j.invalidated?"变更影响(签字已作废)":"变更影响"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ke(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},function(){const et=(Q.history??[]).slice().reverse();if(et.length===0)return null;const De={title:"标题",description:"需求明细",priority:"优先级",status:"状态",owner:"负责人",project:"项目",iteration:"迭代"};return s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:220,overflowY:"auto",marginBottom:8,paddingBottom:8,borderBottom:"1px dashed var(--border)"}},s("div",{className:"ds-hint",style:{fontWeight:600}},`变更记录 · 最近 ${et.length} 条`),et.map((Ye,Mn)=>s("div",{key:`${Ye.at}-${Mn}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint"},Ke(Ye.at)),s("span",null,Ye.by===""?"人":Ye.by),s("span",{className:"ds-chip"},De[Ye.field]??Ye.field),s("span",{className:"ds-hint"},`${Ye.from===""?"(空)":Ye.from} → ${Ye.to===""?"(空)":Ye.to}`))))}(),s("div",{className:"ds-hint"},`改了:${(j.parts??[]).join("、")}。${ue.length===0?"当时没有已产出的卡受影响,不用返工。":`${ue.length} 张卡已经产出过东西${Fe.length===0?"":`、${Fe.length} 条用例要重跑`},要按新版复核:`}`),ue.length===0?null:s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8,marginTop:8}},ue.map(et=>s("span",{key:et.id,className:"ds-chip","data-pt":"open",style:{cursor:"pointer"},title:et.title,onClick:()=>{const De=wt.get(et.id);De!==void 0&&typeof t=="function"&&t(De)}},`${et.code||et.id} ${ma(et.status)}`))),j.invalidated?s("div",{className:"ds-hint",style:{marginTop:8}},"验收签字已清空:这些卡是按旧版做的,复核完重新签一次。"):null))})(),(()=>{const j=Q.deploys??[];return j.length===0?null:s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"上线足迹"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},"来自版本的部署记录,只留痕不触发")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},j.map(ue=>s("span",{key:ue.id,className:"ds-chip","data-env":ue.kind,title:`${ue.releaseName??""} · ${Ke(ue.at)} · ${ue.by}`},`${ue.name} · ${Xa(ue.at)}`)))))})(),s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"依赖与排序")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row"},s("span",{className:"ds-hint"},"排序号"),s(v,{className:"ds-input",style:{width:90},value:Ve,onChange:j=>Ya(j.target.value)}),s("span",{className:"ds-hint"},"被依赖的需求没交付,这条需求的准出会被拦")),s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:10}},h.filter(j=>j.id!==Q.id).map(j=>s("label",{key:j.id,className:"ds-check-row"},s(k,{checked:Rn.includes(j.id),onChange:ue=>U(Fe=>ue.target.checked?[...Fe,j.id]:Fe.filter(wt=>wt!==j.id))}),s("span",{className:"ds-check-text"},`${j.code||j.id} ${j.title}(${Ar[j.status]??j.status})`))),h.length<=1?s("span",{className:"ds-hint"},"只有这一条需求,暂时没有可依赖的对象。"):null),s("div",{className:"ds-row"},s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void Ea()},"保存排序与依赖"),he===null?null:s("span",{className:"ds-hint"},he)))),(()=>{const j=Q.progress??null,ue=j?.gate??{ready:!1,blockers:["还没有加载出准出状态,稍后自动刷新"],warnings:[]},Fe=ue.stats??{},wt=Q.signoff??null,ma=j?.progress??(V.length===0?0:Math.round(ce/V.length*100)),et=Fe.coveredPoints??0,De=Fe.points??(Q.points??[]).length,Ye=Fe.pass??Fo,Mn=Fe.cases??ra.total;return s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求准出与验收签字"),s(y,{className:"ds-chip","data-pt":ue.ready?"covered":"open",style:{marginLeft:"auto"}},ue.ready?"可以交付":`还差 ${ue.blockers.length} 项`)),s("div",{className:"ds-panel-body"},s("div",{className:"ds-check-progress"},s("div",{className:"ds-check-bar","data-full":ma===100?"1":"0"},s("span",{style:{width:`${ma}%`}})),s("div",{className:"ds-hint"},`${ma}% · ${ce}/${V.length} 张卡完成 · 功能点覆盖 ${et}/${De} · 用例通过 ${Ye}/${Mn}`)),(ue.blockers??[]).map(zn=>s("div",{key:zn,className:"ds-gate-item","data-kind":"block"},`⛔ ${zn}`)),(ue.warnings??[]).map(zn=>s("div",{key:zn,className:"ds-gate-item","data-kind":"warn"},`⚠ ${zn}`)),ue.ready?s("div",{className:"ds-hint"},"门禁已通过:签完字这条需求就算交付。"):null,wt===null?s("div",{className:"ds-gate-sign"},s(v,{className:"ds-input",style:{flex:1,minWidth:220},placeholder:"验收结论:交付了什么、遗留什么",value:W,onChange:zn=>Ee(zn.target.value)}),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void hs("sign")},"需求验收签字")):s("div",{className:"ds-gate-sign"},s("div",{className:"ds-hint",style:{flex:1}},`✍ ${wt.by} 于 ${Ke(wt.at)} 签字:${wt.note}`),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void hs("clear")},"取消签字")),ke===null?null:s("div",{className:"ds-hint"},ke)))})(),s("div",{className:"ds-sec-head",style:{marginTop:16}},"交付物归档(飞书云盘)",s("div",{className:"ds-req-actions"},dn.url?s("a",{className:"ds-link",href:dn.url,target:"_blank",rel:"noreferrer"},"打开文件夹"):null,s("button",{className:"ds-btn ds-btn-ghost",disabled:He,onClick:()=>void La()},dn.url?"校验文件夹":"创建文件夹"),s("button",{className:"ds-btn",disabled:He||Ma.length===0,onClick:()=>void Lt()},He?"归档中…":`归档 ${Ma.length} 件交付物`))),s("div",{className:"ds-hint"},dn.url?`文件夹:${dn.name||dn.token} 已归档 ${Ma.filter(j=>j.doc?.url).length}/${Ma.length} 件交付物`:dn.error?`上次创建失败:${dn.error}`:"还没有云盘文件夹;建好后,这条需求下每件交付物都会写成文件夹里的一份 Markdown 文档。"),fe?s("div",{className:"ds-hint"},fe):null,s("div",{className:"ds-sec-head",style:{marginTop:16}},"文档与基线",s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void me(null)},"+ 添加文档")),Q.baseline?s("div",{className:"ds-base-box"},s("div",{className:"ds-base-main"},s("b",null,`当前基线 ${Q.baseline.version}`),s("span",null,`${Q.baseline.title} · 生效于 ${Ke(Q.baseline.at)} · ${Q.baseline.by||"人"} · 正文 ${Q.baseline.contentLength??0} 字`)),s("button",{className:"ds-btn ds-btn-ghost",title:"把当前文档和这份基线逐行比一遍(只读)",onClick:()=>void Et()},P===null?"对比基线":"收起对比"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void jn()},"取消基线")):s("div",{className:"ds-base-box","data-empty":"1"},s("div",{className:"ds-base-main"},s("b",null,"还没有基线"),s("span",null,"PRD 评审通过后点「设为基线」,之后建的卡都会记下用的是哪一版。"))),Q.baseline!==null&&P!==null?s("div",{className:"ds-diff"},s("div",{className:"ds-diff-head"},s("b",null,`基线 ${P.version} → 当前:${P.docTitle}`),s("span",{className:"ds-hint"},P.summary.added+P.summary.removed===0?"正文没有变化":`+${P.summary.added} 行 / −${P.summary.removed} 行`)),s("div",{className:"ds-diff-body"},P.rows.slice(0,400).map((j,ue)=>s("div",{key:ue,className:"ds-diff-line","data-kind":j.kind},`${j.kind==="add"?"+":j.kind==="del"?"−":" "} ${j.text}`)),P.rows.length>400?s("div",{className:"ds-hint"},`(还有 ${P.rows.length-400} 行未显示)`):null)):null,(Q.docs??[]).length===0?s("div",{className:"ds-hint",style:{marginTop:8}},"还没有登记文档。可以先加 PRD 链接,把正文粘进来,评审通过后设为基线。"):s("div",null,s("div",{className:"ds-row",style:{marginTop:8,gap:8,alignItems:"center"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void fa()},R===null?"全部展开":"全部收起"),s("span",{className:"ds-hint"},`共 ${(Q.docs??[]).length} 篇,展开后可直接读 Markdown 正文`)),s("div",{className:"ds-doc-list"},(Q.docs??[]).map(j=>s("div",{key:j.id,className:"ds-doc-wrap"},s("div",{className:"ds-doc-row"},s(y,{className:"ds-chip","data-doc":j.kind},tf[j.kind]??j.kind),s("div",{className:"ds-doc-main"},s("div",{className:"ds-doc-title"},j.url===""?j.title:s("a",{className:"ds-link",href:j.url,target:"_blank",rel:"noreferrer"},j.title),s("span",{className:"ds-chip-code"},j.version),s(y,{className:"ds-chip","data-docstatus":j.status},cx[j.status]??j.status)),s("div",{className:"ds-doc-meta"},`正文 ${j.contentLength??0} 字 · ${j.by||"—"} · ${j.at>0?Ke(j.at):"—"}`)),s("div",{className:"ds-listrow-actions"},j.status==="baseline"?null:s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Mt(j.id)},"设为基线"),s("button",{className:"ds-btn ds-btn-ghost",title:"直接读正文(Markdown 渲染)",onClick:()=>void Qa(j.id)},A!==null&&A.id===j.id?"收起":"阅读"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void me(j.id)},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Z(j.id),"data-danger":"1"},"删除"))),A!==null&&A.id===j.id?s("div",{className:"ds-md-preview ds-reader"},A.busy?s("span",{className:"ds-hint"},"读取中…"):A.content.trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(A.content)):R!==null&&R[j.id]!==void 0?s("div",{className:"ds-md-preview ds-reader"},R[j.id].trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(R[j.id])):null)))),q===null?null:s("div",{className:"ds-hint",style:{marginTop:6}},q),B===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},B.id===void 0?"添加文档":`编辑 ${B.title}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"类型"),s(g,{className:"ds-select",value:B.kind,onChange:j=>J({...B,kind:j.target.value})},dx.map(j=>s("option",{key:j,value:j},tf[j])))),s("div",{className:"ds-field",style:{flex:"1 1 220px"}},s("div",{className:"ds-label"},"标题"),s(v,{className:"ds-input",value:B.title,placeholder:"例如 会员积分抵扣 PRD",onChange:j=>J({...B,title:j.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 110px"}},s("div",{className:"ds-label"},"版本"),s(v,{className:"ds-input",value:B.version,placeholder:"v1.0",onChange:j=>J({...B,version:j.target.value})}))),s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"1 1 auto"}},s("div",{className:"ds-label"},"飞书文档链接"),s(v,{className:"ds-input",value:B.url,placeholder:"https://<租户>.feishu.cn/wiki/…",onChange:j=>J({...B,url:j.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 auto",justifyContent:"flex-end"}},s("div",{className:"ds-label"}," "),s("button",{className:"ds-btn",disabled:G,onClick:()=>void _e()},G?"拉取中…":"从链接拉正文"))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"文档正文(拉不到就自己粘,研发和测试都读这一份)"),s(d,{className:"ds-textarea",rows:8,value:B.content,onChange:j=>J({...B,content:j.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{J(null),L(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:G,onClick:()=>void An()},"保存文档")))),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"需求变更影响面"),s("div",{className:"ds-pane-desc"},"需求一改,下面这些卡和用例就要重看。点一下现算,不改任何数据。")),s("div",{className:"ds-row",style:{gap:8,alignItems:"center",marginBottom:6}},s("button",{className:"ds-btn",disabled:Gt||pt===null,onClick:()=>void Ct()},Gt?"计算中…":"算一下影响面"),Ue===null?s("span",{className:"ds-hint"},"还没算过"):s("span",{className:"ds-hint"},`卡 ${Ue.summary.cards} · 用例 ${Ue.summary.cases}(能自动跑 ${Ue.summary.runnable})· 失败 ${Ue.summary.failing}`)),Ue===null||Ue.cards.length===0?null:s("div",{className:"ds-sec-body",style:{maxHeight:160,overflow:"auto"}},Ue.cards.slice(0,12).map(j=>s("div",{key:j.id,className:"ds-art-row"},s("span",{className:"ds-art-title"},(j.code===""?"":j.code+" ")+j.title),s("span",{className:"ds-hint"},j.status))),Ue.cards.length>12?s("div",{className:"ds-hint"},`还有 ${Ue.cards.length-12} 张`):null),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"功能点与可追溯矩阵"),s("div",{className:"ds-pane-desc"},"功能点是需求的骨架:一条功能点对若干张卡、若干条用例。卡和用例上都挂 pointId,这里就能看出哪条没人做、哪条没被验证。")),F===null?s("div",{className:"ds-hint",style:{padding:"6px 0"}},"还没有功能点。点「+ 加功能点」把需求拆成可交付、可验证的条目。"):s("div",{className:"ds-matrix"},s("div",{className:"ds-matrix-sum"},`功能点 ${F.summary.points} · 已覆盖 ${F.summary.coveredPoints}${F.summary.pointsWithoutCases>0?`(${F.summary.pointsWithoutCases} 条还没有用例,覆盖率 ${F.summary.coverageRate}%)`:""} · 卡 ${F.summary.cards}(未归点 ${F.summary.looseCards})· 用例 ${F.summary.cases}(通过 ${F.summary.pass}${F.summary.fail>0?`,失败 ${F.summary.fail}`:""})· 通过率 ${F.summary.passRate}%`),(F.points??[]).map(j=>s("div",{key:j.id,className:"ds-pt-row","data-covered":j.covered?"1":"0"},s("div",{className:"ds-pt-main"},s("div",{className:"ds-pt-title"},s("span",{className:"ds-chip-code"},j.id),s("b",null,j.title),s(y,{className:"ds-chip","data-pt":j.covered?"covered":"open"},j.covered?"已覆盖":"未覆盖")),String(j.description??"")===""?null:s("div",{className:"ds-req-desc ds-md"},f(j.description)),s("div",{className:"ds-pt-meta"},`卡 ${j.cards.length}${j.cards.length===0?"(还没人做)":":"+j.cards.map(ue=>ue.code||ue.id).join("、")}`,` · 用例 ${j.cases.total}(通过 ${j.cases.pass}${j.cases.fail>0?`,失败 ${j.cases.fail}`:""}${j.cases.pending>0?`,没跑 ${j.cases.pending}`:""})`,j.acceptance.length===0?"":` · 验收条件 ${j.acceptance.filter(ue=>ue.covered).length}/${j.acceptance.length}`)),s("div",{className:"ds-listrow-actions"},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>ge({...j,acceptanceText:(j.acceptance??[]).map(ue=>ue.text).join(`
|
|
46
46
|
`)})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void se(j.id),"data-danger":"1"},"删除")))),(F.loose??[]).length===0?null:s("div",{className:"ds-pt-loose"},s("div",{className:"ds-hint",style:{marginBottom:6}},`还有 ${F.loose.length} 张卡没归到功能点,选一个挂上去,矩阵才算完整:`),F.loose.map(j=>s("div",{key:j.id,className:"ds-pt-loose-row"},s("span",{className:"ds-chip-code"},j.code||j.id),s("span",{className:"ds-pt-loose-title"},j.title),s(g,{className:"ds-select",style:{width:220},value:"",onChange:ue=>{ue.target.value!==""&&de(j.id,ue.target.value)}},s("option",{value:""},"挂到…"),(F.points??[]).map(ue=>s("option",{key:ue.id,value:ue.id},`${ue.id} ${ue.title}`))))))),s("div",{className:"ds-row",style:{gap:8,marginTop:10,alignItems:"center"}},s("button",{className:"ds-btn",onClick:()=>ge(ee())},"+ 加功能点"),s("button",{className:"ds-btn ds-btn-primary",disabled:Be,onClick:()=>void le()},Be?"拆卡中…":"按未覆盖的功能点拆卡"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>{Q!==null&&Y(Q.id)}},"刷新矩阵"),Rt===null?null:s("span",{className:"ds-hint"},Rt),xe===null?null:s("span",{className:"ds-hint"},xe)),ie===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},ie.id===void 0?"新增功能点":`编辑功能点 ${ie.id}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-field"},s("div",{className:"ds-label"},"功能点标题"),s(v,{className:"ds-input",value:ie.title,placeholder:"例如 结算页展示可用积分",onChange:j=>ge({...ie,title:j.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"说明"),s(d,{className:"ds-textarea",rows:3,value:ie.description??"",onChange:j=>ge({...ie,description:j.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"验收条件(一行一条,做成什么样算完成)"),s(w,{className:"ds-textarea",rows:4,value:ie.acceptanceText??"",onChange:j=>ge({...ie,acceptanceText:j.target.value})})),s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"优先级"),s(g,{className:"ds-select",value:ie.priority,onChange:j=>ge({...ie,priority:j.target.value})},s("option",{value:"urgent"},"紧急"),s("option",{value:"normal"},"普通"),s("option",{value:"relaxed"},"可缓"))),s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"状态"),s(g,{className:"ds-select",value:ie.status,onChange:j=>ge({...ie,status:j.target.value})},s("option",{value:"draft"},"草稿"),s("option",{value:"ready"},"就绪"),s("option",{value:"done"},"已完成")))),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>ge(null)},"取消"),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void be()},"保存功能点")))),s("div",{className:"ds-req-stats"},s("div",{className:"ds-stat-box"},s("b",null,`${ce}/${V.length}`),s("span",null,"卡已完成")),s("div",{className:"ds-stat-box","data-warn":Le>0?"1":"0"},s("b",null,String(Le)),s("span",null,"阻塞")),s("div",{className:"ds-stat-box","data-warn":At>0?"1":"0"},s("b",null,String(At)),s("span",null,"未结缺陷")),s("div",{className:"ds-stat-box"},s("b",null,`${$a.done}/${$a.total}`),s("span",null,"验收清单")),s("div",{className:"ds-stat-box"},s("b",null,`${ra.pass}/${ra.total}`),s("span",null,"用例通过")),s("div",{className:"ds-stat-box"},s("b",null,String(Ma.length)),s("span",null,"交付物")),s("div",{className:"ds-stat-box"},s("b",null,String(Dn.length)),s("span",null,"MR"))),s("div",{className:"ds-sec-head",style:{marginTop:14}},`这张需求下的 ${V.length} 张卡`),V.length===0?s("div",{className:"ds-empty"},"还没有挂上来的卡。建卡时选择这条需求,编号会自动分配。"):s("div",{className:"ds-req-tasks"},V.map(j=>s("div",{key:j.id,className:"ds-listrow ds-req-task",onClick:()=>t(j.id)},s("div",{className:"ds-listrow-main"},s("div",{className:"ds-req-task-title"},j.code?s(y,{className:"ds-chip ds-chip-code"},j.code):null,s("span",null,j.title)),s("div",{className:"ds-hint"},`${Ce(j.status)}${(()=>{const ue=j.expert===""?"":(e.config.experts??[]).find(Fe=>Fe.id===j.expert)?.name??j.expert;return ue===""?"":` · ${ue}`})()}${j.dueAt>0?` · 截止 ${Xa(j.dueAt)}`:""}`)),s("div",{className:"ds-listrow-actions"},j.blocked!==void 0&&j.blocked!==null?s(y,{className:"ds-chip ds-chip-blocked"},"⛔"):null,j.type==="bug"?s(y,{className:"ds-chip ds-chip-bug","data-sev":j.severity??"p2"},"BUG"):null)))),Dn.length>0?s("div",null,s("div",{className:"ds-sec-head",style:{marginTop:14}},"关联 MR"),Dn.map(j=>s("div",{key:j.id,className:"ds-hint"},`${j.code||j.id} · !${j.gitlabMR.iid} ${j.gitlabMR.title??""}(${j.gitlabMR.state||"open"})`))):null)})();return s("div",{className:"ds-body"},z!==null?s("div",{className:"ds-err",style:{marginBottom:10}},z):null,xt,s("div",{className:"ds-req-grid"},s("div",{className:"ds-intake-launch",style:{display:"flex",gap:8,padding:"0 0 8px"}},s("span",{className:"ds-hint"},"新建需求请用右上角「+ 新建」")),s("div",{className:"ds-req-list"},s("div",{className:"ds-req-list-head"},s("span",null,`需求 ${h.length}`),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},title:"按来源筛选:全部 / 业务提单 / 内部",onClick:()=>{const V=window.localStorage.getItem("dsh-studio:reqsrc")??"all";window.localStorage.setItem("dsh-studio:reqsrc",V==="all"?"business":V==="business"?"internal":"all"),window.dispatchEvent(new Event("resize"))}},(window.localStorage.getItem("dsh-studio:reqsrc")??"all")==="all"?"来源:全部":window.localStorage.getItem("dsh-studio:reqsrc")==="business"?"来源:业务提单":"来源:内部")),h.length===0?s("div",{className:"ds-empty"},"还没有需求。先建一条——后面所有的卡、用例、缺陷、MR 都会挂在它下面。"):((window.localStorage.getItem("dsh-studio:reqsort")??"score")==="score"?h.slice().sort((V,ce)=>(ce.pool?.score??0)-(V.pool?.score??0)):h).map(V=>{const ce=window.localStorage.getItem("dsh-studio:reqsrc")??"all";if(ce!=="all"&&(V.source==="business"?"business":"internal")!==ce)return null;const Le=ve(V.id),At=Le.filter(Ma=>Ma.status==="done").length,$a=V.progress?.progress??(Le.length===0?0:Math.round(At/Le.length*100)),ra=V.progress?.gate??null;return s("div",{key:V.id,className:"ds-req-item","data-on":V.id===S?"1":"0",onClick:()=>C(V.id)},s("div",{className:"ds-req-item-title"},V.title),s("div",{className:"ds-req-item-meta"},s(y,{className:"ds-chip ds-chip-code"},V.code||"未编号"),V.source==="business"?s(y,{className:"ds-chip","data-src":"business",title:[V.requester?.name,V.requester?.dept].filter(Boolean).join(" · ")||"业务提单"},"业务提单"):null,s(y,{className:"ds-chip","data-req":V.status},Ar[V.status]??V.status),s("span",{className:"ds-hint"},`${At}/${Le.length} 卡`),ra===null?null:s("span",{className:"ds-hint",style:{color:ra.ready?"#0f9d58":"#b26a00"}},ra.ready?"准出通过":`还差 ${ra.blockers.length} 项`)),s("div",{className:"ds-check-bar","data-full":$a===100?"1":"0",style:{marginTop:2}},s("span",{style:{width:`${$a}%`}})))})),s("div",{className:"ds-req-detail"},ht)))}function iw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,toast:s,confirmDialog:o,promptDialog:i,tipOf:l,TextField:c,ChipField:u,CheckField:p,SelectField:f,MdField:v}=vt(),[w,y]=n.useState([]),[k,g]=n.useState(null),[d,h]=n.useState(null),[b,S]=n.useState(null),[C,E]=n.useState(""),[_,I]=n.useState(null),[z,H]=n.useState(""),[M,$]=n.useState(!1),[B,J]=n.useState(""),[q,L]=n.useState(""),[A,x]=n.useState(""),[P,K]=n.useState("10"),[R,O]=n.useState(""),[G,we]=n.useState(""),fe=n.useCallback(async W=>{const ke=(await r("/releases")).releases??[];y(ke);const Ae=W??(ke.some(Ya=>Ya.id===k)?k:ke[0]?.id??null);if(g(Ae),Ae===null){h(null);return}const Ve=await r("/release/get",{id:Ae});h(Ve),S({id:Ve.release.id,version:Ve.release.version,name:Ve.release.name,project:Ve.release.project,owner:Ve.release.owner,status:Ve.release.status,plannedAt:Ve.release.plannedAt,tagName:Ve.release.tagName??"",commitSha:Ve.release.commitSha??""}),E(Ve.release.notes??"")},[k]);n.useEffect(()=>{let W=!0;return fe().then(()=>{W&&I(null)}).catch(Ee=>{W&&I(Ee instanceof Error?Ee.message:String(Ee))}),()=>{W=!1}},[e.revision,fe]);async function ye(W){g(W),$(!0);try{const Ee=await r("/release/get",{id:W});h(Ee),S({id:Ee.release.id,version:Ee.release.version,name:Ee.release.name,project:Ee.release.project,owner:Ee.release.owner,status:Ee.release.status,plannedAt:Ee.release.plannedAt,tagName:Ee.release.tagName??"",commitSha:Ee.release.commitSha??""}),E(Ee.release.notes??""),H("")}catch(Ee){I(Ee instanceof Error?Ee.message:String(Ee))}finally{$(!1)}}function He(){g(null),h(null),S({id:"",version:"",name:"",project:"",owner:"",status:"draft",plannedAt:0,tagName:"",commitSha:""}),E(""),H("")}async function ae(W,Ee,ke){$(!0),I(null);try{const Ae=await r(W,Ee);H(ke??"已保存"),s(ke??"已保存"),s(ke??"已保存"),await fe(Ae.release?.id??k)}catch(Ae){const Ve=Ae instanceof Error?Ae.message:String(Ae);I(Ve),s(Ve,"error")}finally{$(!1)}}if(_!==null&&w.length===0)return a("div",{className:"ds-body"},a("div",{className:"ds-err"},_));const F=d?.gate,te=d?.release,xe=d?.tasks??[],X=e.tasks.filter(W=>W.releaseId===""||W.releaseId===void 0),ie=a("div",{className:"ds-req-list"},a("div",{className:"ds-row",style:{justifyContent:"space-between",marginBottom:6}},a("span",{className:"ds-hint"},`${w.length} 个版本`),a("button",{className:"ds-btn",onClick:He},"+ 新建版本")),w.length===0?a("div",{className:"ds-empty"},"还没有版本。先建一个版本,再把卡挂上来。"):w.map(W=>a("div",{key:W.id,className:"ds-req-item","data-on":k===W.id?"1":"0",onClick:()=>void ye(W.id)},a("div",{className:"ds-req-item-title"},`${W.version||W.id} ${W.name}`.trim()),a("div",{className:"ds-req-item-meta"},a(u,{className:"ds-chip","data-kind":W.status},af[W.status]??W.status),a(u,{className:"ds-chip"},`${W.gate.stats.done}/${W.gate.stats.tasks} 卡`),W.gate.blockers.length===0?a(u,{className:"ds-chip",style:{color:"#0f9d58",borderColor:"#0f9d5855"}},"门禁通过"):a(u,{className:"ds-chip ds-chip-blocked"},`${W.gate.blockers.length} 项拦着`))))),ge=b===null?a("div",{className:"ds-hint"},"左边选一个版本,或者新建一个。"):a("div",{className:"ds-req-detail"},(te?.requirements??[]).length===0?null:a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginBottom:8}},a("span",{className:"ds-hint"},`这个版本涉及 ${te.requirements.length} 条需求:`),te.requirements.map(W=>a("span",{key:W.id,className:"ds-chip",title:W.title},`${W.code||W.id} ${W.title}(${Ar[W.derived]??W.derived})`))),a("div",{className:"ds-req-headline"},a("div",null,a("div",{className:"ds-req-name"},`${b.version||"未编号"} ${b.name}`.trim()),a("div",{className:"ds-hint"},te==null?"新版本:先填版本号与名字,保存后可以把卡挂进来":`创建于 ${Xa(te.createdAt)}${te.releasedAt>0?` · 发布于 ${Xa(te.releasedAt)}`:""}${te.approveBy!==""?` · 审批人 ${te.approveBy}`:""}`)),a("div",{className:"ds-req-actions"},a(f,{className:"ds-select",style:{width:108},value:b.status,onChange:W=>S({...b,status:W.target.value})},ux.map(W=>a("option",{key:W,value:W},af[W]))))),a("div",{className:"ds-panel",style:{marginTop:4}},a("div",{className:"ds-panel-head"},"版本信息"),a("div",{className:"ds-panel-body ds-grid2"},a("label",{className:"ds-field"},a("span",{},"版本号"),a(c,{className:"ds-input",value:b.version,placeholder:"v1.3.0",disabled:te?.status==="released",onChange:W=>S({...b,version:W.target.value})})),a("label",{className:"ds-field"},a("span",{},"版本名"),a(c,{className:"ds-input",value:b.name,placeholder:"积分抵扣首个版本",onChange:W=>S({...b,name:W.target.value})})),a("label",{className:"ds-field"},a("span",{},"业务项目"),a(f,{className:"ds-select",value:b.project,onChange:W=>S({...b,project:W.target.value})},a("option",{value:""},"不属于任何项目"),(e.config.projects??[]).map(W=>a("option",{key:W.id,value:W.id},W.name)))),a("label",{className:"ds-field"},a("span",{},"发布负责人"),a(c,{className:"ds-input",value:b.owner,onChange:W=>S({...b,owner:W.target.value})})),a("label",{className:"ds-field"},a("span",{},"计划发布时间"),a(c,{className:"ds-input",type:"date",value:nf(b.plannedAt),onChange:W=>S({...b,plannedAt:jr(W.target.value)})})),a("label",{className:"ds-field"},a("span",{},"发布标签(tag)"),a(c,{className:"ds-input",value:b.tagName??"",placeholder:"v1.0.0",onChange:W=>S({...b,tagName:W.target.value})})),a("label",{className:"ds-field"},a("span",{},"对应提交(sha,选填)"),a(c,{className:"ds-input",value:b.commitSha??"",placeholder:"a1b2c3d",onChange:W=>S({...b,commitSha:W.target.value})})),a("div",{className:"ds-row",style:{alignItems:"flex-end",justifyContent:"flex-end",gap:8}},a("button",{className:"ds-btn ds-btn-primary",disabled:M,onClick:()=>void ae("/release/save",{release:b},"版本已保存")},"保存"),b.id===""?null:a("button",{className:"ds-btn ds-btn-ghost",disabled:M,style:{color:"var(--destructive)"},onClick:async()=>{await o("删除这个版本?卡会保留,只是脱离版本。",{title:"删除版本",danger:!0,okText:"删除"})&&ae("/release/delete",{id:b.id},"版本已删除")}},"删除")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布门禁",a("span",{className:"ds-hint"},"卡全做完、没阻塞、缺陷关闭、用例没有失败,才算能发")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-req-stats"},a("div",{className:"ds-stat-box"},a("b",{},String(F?.stats.done??0)),a("span",{},"已完成卡")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(F?.stats.open??0)>0?"1":"0"},String(F?.stats.open??0)),a("span",{},"未完成")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(F?.stats.blocked??0)>0?"1":"0"},String(F?.stats.blocked??0)),a("span",{},"阻塞")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(F?.stats.bugs??0)>0?"1":"0"},String(F?.stats.bugs??0)),a("span",{},"未结缺陷")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(F?.stats.failing??0)>0?"1":"0"},String(F?.stats.failing??0)),a("span",{},"失败用例")),a("div",{className:"ds-stat-box"},a("b",{},String(F?.stats.artifacts??0)),a("span",{},"交付物"))),(F?.blockers??[]).map((W,Ee)=>a("div",{key:`b${Ee}`,className:"ds-error"},`⛔ ${W}`)),(F?.warnings??[]).map((W,Ee)=>a("div",{key:`w${Ee}`,className:"ds-hint"},`⚠ ${W}`)),a("div",{className:"ds-action-bar"},a("div",{className:"ds-action-bar-left"},a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/notes",{id:b.id},"发布说明已生成")},"生成发布说明"),a("button",{className:"ds-btn",disabled:M||!F?.ready,onClick:()=>void ae("/release/approve",{id:b.id,by:"人"},"已审批,可以发布了")},"审批通过")),a("div",{className:"ds-action-bar-right"},a("button",{className:"ds-btn ds-btn-primary",disabled:M||!F?.ready||te.status==="released",onClick:()=>void ae("/release/publish",{id:b.id,by:"人"},"已发布")},te.status==="released"?"已发布":"发布"),l("发布准入(预发部署 / 需求签字)没过时也要发:必须写原因,会记进部署日志",a("button",{className:"ds-btn ds-btn-ghost",disabled:M||!F?.ready||te.status==="released","data-force":"1",onClick:async()=>{const W=await i("强制发布的原因(会记进部署日志,便于复盘):",{title:"强制发布",okText:"强制发布",placeholder:"为什么不等预发部署 / 需求签字?"});if(W!==null){if(String(W).trim()===""){H("强制发布必须写原因");return}ae("/release/publish",{id:b.id,by:"人",force:!0,forceNote:String(W).trim()},"已强制发布(原因已记入部署日志)")}}},"强制发布")),te.status==="released"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:async()=>{const W=await i("回滚原因(要进复盘记录):",{title:"回滚这个版本",danger:!0,okText:"回滚"});W!==null&&W.trim()!==""&&ae("/release/rollback",{id:b.id,reason:W},"已回滚")}},"回滚"):null)),te.rollbackReason===""?null:a("div",{className:"ds-error"},`回滚原因:${te.rollbackReason}`))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布冒烟清单",a("span",{className:"ds-hint"},`${(te.smoke??[]).filter(W=>W.done).length}/${(te.smoke??[]).length} 项已过 · 发布前逐条点过,门禁才算过`)),a("div",{className:"ds-panel-body"},(te.smoke??[]).length===0?a("div",{className:"ds-empty"},"还没有冒烟项:把发布前必验的几条写在这里,比如「能登录」「下单支付成功」「首页不报错」。"):null,(te.smoke??[]).map(W=>a("div",{key:W.id,className:"ds-listrow"},a("label",{className:"ds-row",style:{gap:8,alignItems:"center",flex:1,cursor:"pointer"}},a(p,{checked:W.done===!0,onChange:()=>void ae("/release/smoke",{id:b.id,action:"toggle",itemId:W.id,done:W.done!==!0,by:"人"},W.done?"已取消":"冒烟项已过")}),a("span",{className:W.done?"ds-smoke-done":""},W.text)),W.done&&W.by!==""?a("span",{className:"ds-hint"},`${W.by} · ${Xa(W.at)}`):null,a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/smoke",{id:b.id,action:"remove",itemId:W.id},"已删除")},"×")))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(c,{className:"ds-input",style:{flex:1},value:q,placeholder:"发布前必须验证什么?",onChange:W=>L(W.target.value)}),a("button",{className:"ds-btn",disabled:M||q.trim()==="",onClick:()=>{const W=q.trim();W!==""&&(L(""),ae("/release/smoke",{id:b.id,action:"add",text:W},"已加冒烟项"))}},"+ 加一条")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"审批、灰度与部署记录",a("span",{className:"ds-hint"},`审批 ${(te.approvals??[]).length} 条 · 灰度 ${(te.grayBatches??[]).length} 批`)),a("div",{className:"ds-panel-body"},(te.approvals??[]).length===0?a("div",{className:"ds-empty"},"还没有审批记录:点上面的「审批通过」,系统会把谁批的、什么结论记在这里。"):(te.approvals??[]).map(W=>a("div",{key:W.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${W.by} 审批通过`),a("div",{className:"ds-hint"},W.note||"(没写结论)")),a("span",{className:"ds-hint"},Xa(W.at)))),a("div",{style:{borderTop:"1px solid var(--ds-line)",margin:"10px 0"}}),(te.grayBatches??[]).length===0?a("div",{className:"ds-hint"},"没有灰度批次:发布就是一次性全量。先加一批 10% 跑一轮再全量,出事好收。"):(te.grayBatches??[]).map(W=>a("div",{key:W.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${W.name} · ${W.percent}%`),a("div",{className:"ds-hint"},`${{pending:"待开始",running:"灰度中",passed:"已通过",rolled_back:"已回滚"}[W.status]??W.status}${W.note===""?"":` · ${W.note}`}`)),a("div",{className:"ds-listrow-actions"},W.status==="pending"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/gray",{id:b.id,action:"start",batchId:W.id,by:"人"},"已开始灰度")},"开始"):null,W.status==="running"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/gray",{id:b.id,action:"advance",batchId:W.id,by:"人"},"这一批已通过")},"通过"):null,W.status==="passed"?a(u,{className:"ds-chip","data-on":"1"},"已通过"):null,W.status==="rolled_back"?a(u,{className:"ds-chip","data-sla":"overdue"},"已回滚"):null))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(c,{className:"ds-input",style:{flex:2},value:A,placeholder:"批次名,例如 内测用户",onChange:W=>x(W.target.value)}),a(c,{className:"ds-input",style:{flex:"0 0 88px"},type:"number",min:1,max:100,value:P,onChange:W=>K(W.target.value)}),a("button",{className:"ds-btn",disabled:M||Number(P)<=0,onClick:async()=>{await ae("/release/gray",{id:b.id,action:"add",name:A,percent:Number(P),by:"人"},"已加一批灰度"),x(""),K("10")}},"+ 加一批")),a("div",{style:{borderTop:"1px solid var(--ds-line)",margin:"10px 0"}}),a("div",{className:"ds-hint"},"上过哪些环境(只登记,不触发部署)"),(te.deploys??[]).length===0?null:a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginTop:6}},te.deploys.map(W=>a("span",{key:W.id,className:"ds-chip","data-env":W.kind,"data-on":W.at===0?"0":"1",title:W.at===0?`还没登记部署到「${W.name}」`:`${W.name}:${Xa(W.at)} ${W.action} · ${W.by}`},W.at===0?`${W.name} —`:`${W.name} ✓`))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(f,{className:"ds-input",style:{flex:"0 0 148px"},value:R,onChange:W=>O(W.target.value)},a("option",{value:""},"选环境…"),(e.config?.environments??[]).map(W=>a("option",{key:W.id,value:W.id},W.name))),a(c,{className:"ds-input",style:{flex:1},value:G,placeholder:"备注:谁发的、发了什么(可留空)",onChange:W=>we(W.target.value)}),a("button",{className:"ds-btn",disabled:M||R==="",onClick:async()=>{await ae("/release/deploy",{id:b.id,env:R,note:G,by:"人"},"已登记部署(没有触发任何部署)"),we("")}},"登记部署")),a("div",{className:"ds-hint",style:{marginTop:10}},"部署与灰度记录(最近 8 条)"),(()=>{const W={pending:"待开始",running:"灰度中",passed:"已通过",rolled_back:"已回滚"},Ee=[...(te.deployLog??[]).map(ke=>({at:ke.at,kind:"deploy",by:ke.by,text:`${ke.action}${ke.env===""?"":` · ${(e.config?.environments??[]).find(Ae=>Ae.id===ke.env)?.name??ke.env}`}`,note:ke.note})),...(te.grayBatches??[]).map(ke=>({at:ke.at??ke.createdAt??0,kind:"gray",by:ke.by??"人",text:`灰度 ${ke.name} ${ke.percent}%`,note:W[ke.status]??ke.status}))].filter(ke=>ke.at).sort((ke,Ae)=>ke.at-Ae.at).slice(-8).reverse();return Ee.length===0?a("div",{className:"ds-empty"},"还没有部署或灰度记录。"):Ee.map(ke=>a("div",{key:`${ke.at}-${ke.kind}-${ke.text}`,className:"ds-hint"},a("span",{className:"ds-chip",style:{marginRight:6}},ke.kind==="gray"?"灰度":"部署"),`${Xa(ke.at)} · ${ke.text} · ${ke.by}${ke.note===""||ke.note===void 0?"":`:${ke.note}`}`))})())),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"这个版本里的卡",a("span",{className:"ds-hint"},`${xe.length} 张`)),a("div",{className:"ds-panel-body"},xe.length===0?a("div",{className:"ds-empty"},"还没有卡。从下面挑,或者在卡片详情里选「所属版本」。"):xe.map(W=>a("div",{key:W.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${W.code?`${W.code} `:""}${W.title}`),a("div",{className:"ds-hint"},`${(e.statuses??[]).find(Ee=>Ee.id===W.status)?.label??W.status}${W.blocked!==null&&W.blocked!==void 0?" · 阻塞":""}${W.type==="bug"?" · 缺陷":""}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(W.id)},"打开"),a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/link",{id:b.id,taskId:W.id,action:"remove"},"已从版本移除")},"移除")))),a("div",{className:"ds-row",style:{marginTop:8}},a(f,{className:"ds-select",style:{flex:1},value:B,onChange:W=>J(W.target.value)},a("option",{value:""},`挑一张卡挂进来(${X.length} 张可选)`),X.map(W=>a("option",{key:W.id,value:W.id},`${W.code?`${W.code} `:""}${W.title}`))),a("button",{className:"ds-btn",disabled:M||B==="",onClick:async()=>{await ae("/release/link",{id:b.id,taskId:B,action:"add"},"已挂到这个版本"),J("")}},"挂进来")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布说明",a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void navigator.clipboard?.writeText(C).then(()=>H("发布说明已复制"),()=>{})},"复制")),a("div",{className:"ds-panel-body"},a(v,{className:"ds-input",rows:12,value:C,placeholder:"点「生成发布说明」按类型自动汇总;之后可以在这里手动补充",onChange:W=>E(W.target.value),onBlur:()=>{C!==(te.notes??"")&&ae("/release/save",{release:{...b,notes:C}},"发布说明已保存")}}))),_===null?null:a("div",{className:"ds-error",style:{marginTop:8}},_),z===""?null:a("div",{className:"ds-hint",style:{marginTop:8}},z));return a("div",{className:"ds-body"},a("div",{className:"ds-req-grid"},ie,ge))}function lw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,SelectField:s}=vt(),[o,i]=n.useState(30),[l,c]=n.useState(""),[u,p]=n.useState(null),[f,v]=n.useState(null),[w,y]=n.useState(null),[k,g]=n.useState(null),[d,h]=n.useState(null),[b,S]=n.useState(null);if(n.useEffect(()=>{let x=!0;const P=`/analytics?days=${o}${l===""?"":`&project=${encodeURIComponent(l)}`}`;r(P).then(R=>{x&&(p(R.analytics),v(null))}).catch(R=>{x&&v(R instanceof Error?R.message:String(R))});const K=l===""?"":`&project=${encodeURIComponent(l)}`;return Promise.all([r("/workload"),r(`/burndown?days=14${K}`),r("/costs"),r("/quality")]).then(([R,O,G,we])=>{x&&(y(R.workload??null),g(O.burndown??null),h({...G.costs??{},budget:G.budget??null,usage:G.usage??null}),S(we.quality??null))}).catch(()=>{}),()=>{x=!1}},[o,l,e.revision]),f!==null)return a("div",{className:"ds-body"},a("div",{className:"ds-err"},f));if(u===null)return a("div",{className:"ds-loading"},"正在统计…");const C=x=>`${Math.round((x??0)*100)}%`,E=x=>{const P=Number(x)||0;return P<=0?"0":P*1440<60?`${Math.max(1,Math.round(P*1440))} 分钟`:P<1?`${(P*24).toFixed(1)} 小时`:`${P.toFixed(1)} 天`},_=(x,P,K,R)=>a("div",{className:"ds-kpi","data-warn":R===!0?"1":"0"},a("div",{className:"ds-kpi-value"},P),a("div",{className:"ds-kpi-label"},x),a("div",{className:"ds-kpi-hint"},K)),I=(u.cycle.buckets??[]).reduce((x,P)=>x+P.count,0),z=Math.max(1,...(u.throughput??[]).map(x=>x.count),...(u.cycle.buckets??[]).map(x=>x.count)),H=Math.max(.001,...(u.columns??[]).map(x=>x.avgDays)),M=(u.columns??[]).reduce((x,P)=>x===null||P.avgDays>x.avgDays?P:x,null),$=(u.throughput??[]).reduce((x,P)=>x+P.count,0),B=(w?.experts??[]).filter(x=>x.enabled),J=B.reduce((x,P)=>x+P.open,0),q=B.reduce((x,P)=>x+(P.overdue??0),0),L=k?.series??[],A=L.length===0?null:L[L.length-1];return a("div",{className:"ds-body"},a("div",{className:"ds-board-bar"},a("span",{className:"ds-hint"},`统计口径:最近 ${u.days} 天 · ${u.scope.tasks} 张卡(${u.scope.done} 张已完成)`),a(s,{className:"ds-select",value:l,style:{width:"auto",minWidth:130},onChange:x=>c(x.target.value)},a("option",{value:""},"全部项目"),(e.config.projects??[]).map(x=>a("option",{key:x.id,value:x.id},`${x.key?`[${x.key}] `:""}${x.name}`))),a(s,{className:"ds-select",value:String(o),style:{width:"auto",minWidth:96},onChange:x=>i(Number(x.target.value))},a("option",{value:"7"},"近 7 天"),a("option",{value:"30"},"近 30 天"),a("option",{value:"90"},"近 90 天"))),a("div",{className:"ds-an-sec"},"质量",a("span",{className:"ds-hint"},"先看这四个数:返工多不多、多久能做完、AI 交的东西有多少被人验收")),a("div",{className:"ds-kpi-grid"},_("一次通过率",C(u.rework.firstPassRate),"完成的卡里,从没被退回过的比例",u.rework.firstPassRate<.6),_("返工率",C(u.rework.rate),`${u.rework.reworked}/${u.rework.moved} 张卡倒退过`,u.rework.rate>.3),_("中位周期",E(u.cycle.medianDays),`平均 ${E(u.cycle.avgDays)} · P90 ${E(u.cycle.p90Days)}`),_("AI 交付物采纳率",C(u.ai.acceptedRate),`${u.ai.accepted}/${u.ai.artifactsAi} 件已被验收 · 占全部交付物 ${C(u.ai.aiShare)}`)),a("div",{className:"ds-an-sec"},"流程与产能",a("span",{className:"ds-hint"},"完成的卡各花了多久 · 时间都卡在哪一列 · 最近每天完成几张")),a("div",{className:"ds-an-grid"},a("div",{className:"ds-span-4",style:{display:"grid",gap:12,gridTemplateRows:"auto 1fr",alignContent:"stretch"}},a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"周期分布",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${I} 张已完成`)),a("div",{className:"ds-panel-body"},(u.cycle.buckets??[]).map(x=>a("div",{key:x.id,className:"ds-an-row"},a("span",{className:"ds-an-name"},x.label),a("div",{className:"ds-an-track","data-empty":x.count===0?"1":"0"},a("div",{className:"ds-an-fill",style:{width:`${Math.round(x.count/z*100)}%`}})),a("span",{className:"ds-an-num"},`${x.count}`))),I===0?a("div",{className:"ds-hint"},"这个区间里还没有完成的卡。"):null,a("div",{className:"ds-an-foot"},`平均 ${E(u.cycle.avgDays)} · 中位 ${E(u.cycle.medianDays)} · P90 ${E(u.cycle.p90Days)}`))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"近 14 天完成"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-spark"},(u.throughput??[]).map((x,P)=>a("div",{key:x.day,className:"ds-spark-col",title:`${x.day}:完成 ${x.count} 张`},a("div",{className:"ds-spark-bar",style:{height:`${Math.round(x.count/Math.max(1,Math.max(...u.throughput.map(K=>K.count)))*100)}%`}}),a("span",{className:"ds-spark-day"},P%2===0?x.day:"")))),a("div",{className:"ds-an-foot"},`近 14 天合计 ${$} 张 · 日均 ${($/14).toFixed(1)} 张 · 空着的日子是没卡完成,不是没数据`)))),a("div",{className:"ds-panel ds-span-8"},a("div",{className:"ds-panel-head"},"每列停留(平均)"),a("div",{className:"ds-panel-body"},(u.columns??[]).map(x=>a("div",{key:x.status,className:"ds-an-row"},a("span",{className:"ds-an-name"},x.label),a("div",{className:"ds-an-track","data-empty":x.avgDays===0?"1":"0"},a("div",{className:"ds-an-fill ds-an-fill-warn",style:{width:`${Math.round(x.avgDays/H*100)}%`}})),a("span",{className:"ds-an-num"},E(x.avgDays)))),a("div",{className:"ds-an-foot"},M===null?"还没有数据。":`最慢的是「${M.label}」:平均 ${E(M.avgDays)},时间主要压在这一列。`)))),a("div",{className:"ds-an-sec"},"交付、成本与返工",a("span",{className:"ds-hint"},"AI 交的东西被验收多少 · 谁手上压得最多 · 派单花了多少 · 哪些卡来回折腾")),a("div",{className:"ds-an-grid"},a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"按专家看质量",a("span",{className:"ds-hint"},"派单量来自运行日志(内存最近 300 条)")),a("div",{className:"ds-panel-body"},b===null?a("div",{className:"ds-hint"},"还没算出来"):(b.experts??[]).length===0?a("div",{className:"ds-hint"},"还没有派出过活"):a("div",null,(b.experts??[]).slice(0,8).map(x=>a("div",{className:"ds-an-kv",key:x.expert},a("span",{},`${x.label} · ${x.dispatched} 次`),a("b",{"data-warn":x.reworkRate!==null&&x.reworkRate>=40?"1":"0"},`${x.done}/${x.cards} 完成 · 一次过 ${x.firstPassRate===null?"—":x.firstPassRate+"%"}`))),a("div",{className:"ds-hint",style:{marginTop:6}},`合计:派单 ${b.totals.dispatched} 次 · 平均耗时 ${b.totals.avgMs>0?Math.round(b.totals.avgMs/1e3)+" 秒":"—"} · 返工率 ${b.totals.reworkRate===null?"—":b.totals.reworkRate+"%"} · AI 失败 ${b.totals.failures} 次 · 缺陷重开 ${b.totals.reopens} 次`),a("div",{className:"ds-hint"},b.cost?.note??"")))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"AI 产出质量"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-an-kv"},a("span",{},"交付物由专家登记"),a("b",{},`${u.ai.artifactsAi} / ${u.ai.artifactsTotal}`)),a("div",{className:"ds-an-kv"},a("span",{},"其中被人验收"),a("b",{},`${u.ai.accepted}(${C(u.ai.acceptedRate)})`)),a("div",{className:"ds-an-kv"},a("span",{},"缺正文(只有标题)"),a("b",{"data-warn":u.ai.emptyBody>0?"1":"0"},`${u.ai.emptyBody}`)),a("div",{className:"ds-an-kv"},a("span",{},"写码卡有执行记录"),a("b",{},`${u.ai.codingTasks}`)),a("div",{className:"ds-an-kv"},a("span",{},"评审通过 / 要求修改"),a("b",{},`${u.ai.codingApproved} / ${u.ai.codingChanges}`)),a("div",{className:"ds-an-kv"},a("span",{},"单测通过"),a("b",{},`${u.ai.unitPassed}/${u.ai.unitTests}(${C(u.ai.unitPassRate)})`)),a("div",{className:"ds-an-kv"},a("span",{},"已合并"),a("b",{},`${u.ai.merged}`)))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"团队负载",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${w?.busy??0} 位在跑`)),a("div",{className:"ds-panel-body"},(w?.experts??[]).filter(x=>x.enabled).length===0?a("div",{className:"ds-empty"},"还没有启用的专家。"):(w?.experts??[]).filter(x=>x.enabled).map(x=>a("div",{key:x.id,className:"ds-an-row"},a("span",{className:"ds-an-name"},`${x.emoji} ${x.name}`),a("div",{className:"ds-an-track"},a("div",{className:"ds-an-fill",style:{width:`${Math.round(x.open/Math.max(1,...(w.experts??[]).map(P=>P.open))*100)}%`}})),a("span",{className:"ds-an-num"},`在办 ${x.open}${x.running>0?` · 跑 ${x.running}`:""}${x.overdue>0?` · 逾期 ${x.overdue}`:""}`))),a("div",{className:"ds-an-foot"},`${B.length} 位在岗,手上合计 ${J} 张${q>0?`,其中 ${q} 张逾期`:",没有逾期的"}`))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"燃尽(近 14 天剩余)",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`现在还剩 ${k?.now?.remaining??0} 张`)),a("div",{className:"ds-panel-body"},a(nw,{series:k?.series??[]}),a("div",{className:"ds-an-foot"},A===null?"还没有数据。":`累计建 ${A.created} · 累计完成 ${A.done} · 现在还剩 ${A.remaining} 张`))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"AI 执行成本",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${d?.total??0} 次派单`)),a("div",{className:"ds-panel-body"},d?.usage?.totals===null||d?.usage?.totals===void 0?null:a("div",{className:"ds-an-kv"},a("span",{},"token 用量"),a("b",{},(d.usage.totals.total/1e3).toFixed(1)+"k"),a("span",{className:"ds-hint",style:{marginLeft:8}},"入 "+(d.usage.totals.input/1e3).toFixed(1)+"k · 出 "+(d.usage.totals.output/1e3).toFixed(1)+"k · 缓存 "+(d.usage.totals.cacheRead/1e3).toFixed(1)+"k")),d?.usage?.totals===null||d?.usage?.totals===void 0?null:a("div",{className:"ds-an-kv"},a("span",{},"预估花费"),a("b",{},d.usage.totals.cost===null?"未知(价目表没配)":"¥"+Number(d.usage.totals.cost).toFixed(2)),(d.usage.totals.unknownCalls??0)>0?a("span",{className:"ds-hint",style:{marginLeft:8}},d.usage.totals.unknownCalls+" 次无价目"):null),a("div",{className:"ds-an-kv"},a("span",{},"平均执行时长"),a("b",{},`${Math.round((d?.avgMs??0)/1e3)} 秒`)),(d?.byModel??[]).slice(0,3).map(x=>a("div",{key:`m-${x.key}`,className:"ds-an-kv"},a("span",{},`模型 ${x.key}`),a("b",{},`${x.runs} 次`))),(d?.byPromptVersion??[]).slice(0,3).map(x=>a("div",{key:`p-${x.key}`,className:"ds-an-kv"},a("span",{},`提示词 ${x.key}`),a("b",{},`${x.runs} 次`))),(d?.usage?.unpricedModels??[]).length===0?null:a("div",{className:"ds-hint"},"未配价目的模型:"+d.usage.unpricedModels.map(x=>x.model+" ×"+x.calls).join("、")),a("div",{className:"ds-hint"},d?.note??""),d?.budget===null||d?.budget===void 0?null:a("div",{style:{marginTop:8}},a("div",{className:"ds-an-kv"},a("span",{},"预算使用"),a("b",{style:{color:d.budget.level==="over"?"#e5484d":d.budget.level==="warn"?"#d97706":void 0}},`${Math.max(d.budget.runRate,d.budget.hourRate)}%`)),a(aw,{value:Math.min(100,Math.max(d.budget.runRate,d.budget.hourRate)),tone:d.budget.level}),a("div",{className:"ds-hint"},`${d.budget.runs}/${d.budget.budgetRuns} 次 · ${d.budget.hours}/${d.budget.budgetHours} 小时 · ${d.budget.hint}`)))),a("div",{className:"ds-panel ds-span-8"},a("div",{className:"ds-panel-head"},"返工最多的卡",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`评审打回 ${u.rework.reviewRework} 次 · 测试打回 ${u.rework.testRework} 次`)),a("div",{className:"ds-panel-body"},(u.rework.worst??[]).length===0?a("div",{className:"ds-hint"},"没有卡倒退过,挺好。"):u.rework.worst.map(x=>a("div",{key:x.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${x.code===""?"":`${x.code} `}${x.title}`),a("div",{className:"ds-hint"},`倒退 ${x.backward} 次 · 现在在「${(e.statuses.find(P=>P.id===x.status)??{}).label??x.status}」`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(x.id)},"打开"))))))))}function dw(e,t){const a="studio",n="/studio-api";let r=null;function s(Y){r=Y??null}function o(Y){return r===null?null:r[Y]??null}function i(Y,ne){if(ne==null)return ne;const ee=ne.props??{},be=ee.title!==void 0?ne:t(ne.type,{...ee,title:Y}),se=o("Tip");return se===null?be:t(se,{key:ne.key,label:Y},be)}const{TextField:l,TextAreaField:c,ChipField:u,CheckField:p,SelectField:f}=px({h:t,kit:o,React:e}),v=fx({h:t,kit:o}),w=Lx({h:t,React:e}),y=mx({h:t,ChipField:u}),k=gx({h:t,React:e,Card:y,TextField:l,SelectField:f,CheckField:p,toast:B});function g({rows:Y=6,value:ne,onChange:ee,placeholder:be,className:se,...de}){const[Q,ve]=e.useState(!1),Ce=String(ne??"");return t("div",{className:"ds-md-field"},t("div",{className:"ds-md-bar"},t("span",{className:"ds-hint"},"支持 Markdown:标题 / 列表 / 粗体 / 代码块 / 表格"),t("button",{className:"ds-btn ds-btn-ghost",type:"button",onClick:()=>ve(!Q)},Q?"回到编辑":"预览渲染")),Q?t("div",{className:"ds-md-preview ds-req-desc ds-md"},Ce.trim()===""?t("span",{className:"ds-hint"},"还没写内容。"):Ee(Ce)):t(c,{className:se??"ds-textarea",rows:Y,value:ne,onChange:ee,placeholder:be,...de}))}function d(Y,ne){const ee=t("div",{className:"ds-label",style:{cursor:"help"}},Y);return ne==null||ne===""?ee:i(ne,ee)}let h=null,b=null;function S(Y){h=Y,typeof b=="function"&&b(Y)}function C(Y,ne){const ee=ne??{};return o("DialogBox")===null?Promise.resolve(window.confirm(String(Y))):new Promise(se=>{S({kind:"confirm",title:ee.title??"确认操作",message:String(Y??""),danger:ee.danger===!0,okText:ee.okText??"确定",cancelText:ee.cancelText??"取消",resolve:se})})}function E(Y,ne){const ee=ne??{};return o("DialogBox")===null?Promise.resolve(window.prompt(String(Y),ee.defaultValue??"")):new Promise(se=>{S({kind:"prompt",title:ee.title??"需要填写",message:String(Y??""),value:String(ee.defaultValue??""),placeholder:ee.placeholder??"",okText:ee.okText??"确定",cancelText:"取消",resolve:se})})}function _(){const[Y,ne]=e.useState(null),[ee,be]=e.useState("");e.useEffect(()=>{const Q=ve=>{ne(ve),be(ve!==null&&ve.kind==="prompt"?ve.value:"")};return b=Q,h!==null&&Q(h),()=>{b===Q&&(b=null)}},[]);const se=o("DialogBox");if(Y===null||se===null)return null;const de=Q=>{const ve=Y.resolve;ne(null),h=null,ve(Q)};return t(se,{open:!0,title:Y.title,message:Y.message,danger:Y.danger===!0,okText:Y.okText,cancelText:Y.cancelText,input:Y.kind==="prompt",value:ee,placeholder:Y.placeholder,onChange:Q=>be(Q),onOk:()=>de(Y.kind==="prompt"?ee.trim():!0),onCancel:()=>de(Y.kind==="prompt"?null:!1)})}let I=0,z=[],H=[],M={text:"",at:0};function $(){const Y=z.slice();for(const ne of H.slice())ne(Y)}function B(Y,ne="success",ee=3200){const be=String(Y??"").trim();if(be==="")return null;const se=Date.now();if(M.text===be&&se-M.at<600)return null;M={text:be,at:se};const de=++I;return z=z.concat([{id:de,text:be,tone:ne}]),$(),window.setTimeout(()=>{J(de)},ee),de}function J(Y){const ne=z.filter(ee=>ee.id!==Y);ne.length!==z.length&&(z=ne,$())}const q={success:"✓",error:"!",info:"i"};function L(){const[Y,ne]=e.useState(()=>z.slice());return e.useEffect(()=>(H.push(ne),()=>{H=H.filter(ee=>ee!==ne)}),[]),Y.length===0?null:t("div",{className:"ds-toasts",role:"status","aria-live":"polite"},Y.map(ee=>t("div",{key:ee.id,className:"ds-toast","data-tone":ee.tone},t("span",{className:"ds-toast-icon","aria-hidden":"true"},q[ee.tone]??"i"),t("span",{className:"ds-toast-text"},ee.text),t("button",{type:"button",className:"ds-toast-close","aria-label":"关闭提示",onClick:()=>J(ee.id)},"×"))))}const A="dsh-studio:user";function x(){try{const Y=window.localStorage.getItem(A);if(typeof Y=="string"&&Y.trim()!=="")return Y.trim().slice(0,40)}catch{}return"人"}function P(Y){const ne=String(Y??"").trim().slice(0,40)||"人";try{window.localStorage.setItem(A,ne)}catch{}return ne}const K="dsh-studio:theme",R=[["light","☀ 明亮"],["dark","☾ 黑暗"],["system","跟随系统"]];function O(){return typeof window.matchMedia=="function"?window.matchMedia("(prefers-color-scheme: dark)"):null}function G(){try{const Y=window.getComputedStyle(document.body).backgroundColor,ne=/rgba?\(([^)]+)\)/.exec(String(Y??""));if(ne===null)return null;const ee=ne[1].split(",").map(ve=>Number(ve.trim())),[be,se,de,Q]=ee;return![be,se,de].every(ve=>Number.isFinite(ve))||Number.isFinite(Q)&&Q<.2?null:.2126*be+.7152*se+.0722*de<128?"dark":"light"}catch{return null}}function we(){try{const Y=window.localStorage.getItem(K);if(Y==="light"||Y==="dark"||Y==="system")return Y}catch{}return"system"}function fe(Y){const ne=Y==="light"||Y==="dark"?Y:"system";try{window.localStorage.setItem(K,ne)}catch{}return ne}const ye="dsh-studio:session",He="dsh-studio:auth-user";function ae(){try{const Y=window.localStorage.getItem(ye);if(typeof Y=="string"&&Y.trim()!=="")return Y.trim()}catch{}return""}function F(Y){try{Y===""||Y===null||Y===void 0?window.localStorage.removeItem(ye):window.localStorage.setItem(ye,String(Y))}catch{}}function te(){try{const Y=window.localStorage.getItem(He);if(typeof Y=="string"&&Y!==""){const ne=JSON.parse(Y);if(ne!==null&&typeof ne=="object")return ne}}catch{}return null}function xe(Y){try{Y==null?window.localStorage.removeItem(He):window.localStorage.setItem(He,JSON.stringify(Y))}catch{}return Y??null}async function X(Y,ne){const ee={"x-studio-user":encodeURIComponent(x())},be=ae();be!==""&&(ee["x-studio-session"]=be),ne!==void 0&&(ee["content-type"]="application/json");const se=ne===void 0?{method:"GET",headers:ee}:{method:"POST",headers:ee,body:JSON.stringify(ne)},de=await fetch(n+Y,se);let Q={};try{Q=await de.json()}catch{}if(!de.ok||Q.ok===!1)throw new Error(Q.error??`请求失败(HTTP ${de.status})`);return Q}const ie=`
|
|
47
47
|
html[data-dsh-theme="dark"] body { background: #0e1014 !important; color: #e6e8ee }
|
|
48
48
|
html[data-dsh-theme="dark"] [class*="frame"],
|
package/dist/client.js
CHANGED
|
@@ -42,7 +42,7 @@ GET /api/points | 200 |`,onChange:m=>ie({...X,steps:m.target.value})}),e("div",{
|
|
|
42
42
|
- `,onChange:O=>q("content",O.target.value)})),a("div",{className:"ds-field"},a("label",{className:"ds-row",style:{alignItems:"center",gap:8,cursor:"pointer"}},a(u,{checked:k.enabled!==!1,onChange:O=>q("enabled",O.target.checked)}),a("span",{},"启用(停用则不写入磁盘)")))))}function ew({state:e,onOpenTask:t,onOpenBoard:a,onOpenCases:n}){const{h:r,React:s,api:o,toast:i}=vt(),[l,c]=s.useState(null),[u,p]=s.useState(null),[f,v]=s.useState(null);if(s.useEffect(()=>{let S=!0;return(async()=>{try{const C=await o("/dashboard");S&&(c(C?.dashboard??null),p(C?.regression??null))}catch(C){if(S){const E=C instanceof Error?C.message:String(C);v(E),i(E,"error")}}})(),()=>{S=!1}},[e.revision]),l===null)return r("div",{className:"ds-body"},r("div",{className:"ds-hint"},f===null?"正在算今天该看什么…":`算不出来:${f}`));const w=u===null?null:(()=>{const S=(Number(u.failed)||0)+(Number(u.failedCards)||0)>0;return r("div",{className:"ds-reg-banner","data-bad":S?"1":"0"},r("span",null,`最近回归 · ${Ke(u.at)} · ${u.by||"人"}`),r("span",{className:"ds-hint"},`${u.cards} 张卡 / ${u.total} 条:通过 ${u.passed}${S?`,不通过 ${u.failed}`:""}${u.bugs>0?` · 自动开缺陷 ${u.bugs} 张`:""}`),n===void 0?null:r("button",{className:"ds-btn ds-btn-ghost",onClick:()=>n()},"看用例"))})(),y=l.risks??{},k=l.flow??{},g=l.ai??{},d=(S,C,E,_)=>r("div",{className:"ds-kpi","data-warn":_===!0?"1":"0"},r("div",{className:"ds-kpi-value"},String(C??0)),r("div",{className:"ds-kpi-label"},S),r("div",{className:"ds-kpi-hint"},E)),h=S=>r("div",{key:S.id,className:"ds-listrow"},r("div",{className:"ds-listrow-main"},r("div",null,`${S.code||S.id} · ${S.title}`),r("div",{className:"ds-hint"},`${S.statusLabel}${S.dueAt>0?` · 截止 ${Xa(S.dueAt)}`:""}${S.sla?.state==="overdue"?" · 缺陷超时":""}`)),r("div",{className:"ds-listrow-actions"},r("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(S.id)},"打开"))),b=(S,C,E,_)=>r("div",{className:"ds-panel"},r("div",{className:"ds-panel-head"},r("span",null,S),r("span",{className:"ds-hint"},_??"")),r("div",{className:"ds-panel-body"},C.length===0?r("div",{className:"ds-hint"},E):C));return r("div",{className:"ds-body"},w,r("div",{className:"ds-kpi-grid"},d("在办卡",l.scope.open,`共 ${l.scope.tasks} 张`),d("逾期",y.overdue,"过了期望完成时间还没完成",y.overdue>0),d("阻塞",y.blocked,"挂着阻塞标记等人决策",y.blocked>0),d("缺陷超时",y.slaOverdue,"按 P0-P3 时限算",y.slaOverdue>0),d("准出被拦",y.gateFailed,"待测试/待验收里过不了准出",y.gateFailed>0),d("流水线失败",y.pipelineFailed,"最近一次 CI 没通过",y.pipelineFailed>0),d("集成异常",y.integrationsBroken,"飞书/GitLab/云盘/隔离",y.integrationsBroken>0),d("AI 在跑",g.running,`今天派单 ${g.todayRuns} 次`)),r("div",{className:"ds-panel"},r("div",{className:"ds-panel-head"},r("span",null,"流程与产出"),r("span",{className:"ds-hint"},"需要人动手的地方")),r("div",{className:"ds-panel-body ds-grid4"},r("div",{className:"ds-field"},r("div",{className:"ds-label"},"待评审"),r("div",null,String(k.review??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"待测试"),r("div",null,String(k.test??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"待验收"),r("div",null,String(k.accept??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"今天完成"),r("div",null,String(k.doneToday??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"近 7 天完成"),r("div",null,String(k.doneWeek??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"需求卡住"),r("div",null,String(y.requirementBlocked??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},"版本卡住"),r("div",null,String(y.releaseBlocked??0))),r("div",{className:"ds-field"},r("div",{className:"ds-label"},""),r("button",{className:"ds-btn ds-btn-ghost",onClick:()=>a()},"去看板")))),b("逾期",(l.list?.overdue??[]).map(h),"没有逾期的卡。"),b("阻塞",(l.list?.blocked??[]).map(h),"没有卡在等人决策。"),b("缺陷超时",(l.list?.slaOverdue??[]).map(h),"缺陷都在时限内。"),b("准出被拦",(l.list?.gateFailed??[]).map(h),"没有卡被准出规则拦住。","清单没勾完 / 用例不通过 / 已签过字但被打回"),b("流水线失败",(l.list?.pipelineFailed??[]).map(h),"没有 CI 失败的卡。"),b("需求与版本卡住",[...(l.list?.requirementBlocked??[]).map(S=>r("div",{key:S.id,className:"ds-listrow"},r("div",{className:"ds-listrow-main"},r("div",null,`${S.code||S.id} · ${S.title}`),r("div",{className:"ds-hint"},`需求:${(S.blockers??[]).join(";")}`)))),...(l.list?.releaseBlocked??[]).map(S=>r("div",{key:S.id,className:"ds-listrow"},r("div",{className:"ds-listrow-main"},r("div",null,`${S.version||S.id} · ${S.name||""}`),r("div",{className:"ds-hint"},`版本:${(S.blockers??[]).join(";")}`))))],"需求与版本都还顺。"),r("div",{className:"ds-hint"},`统计时间 ${Ke(l.at)},数字随看板变化实时算,不落库。`))}function tw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,toast:s,confirmDialog:o,promptDialog:i,TextField:l,SelectField:c,ChipField:u,CheckField:p}=vt(),[f,v]=n.useState(null),[w,y]=n.useState(null),[k,g]=n.useState(""),[d,h]=n.useState("__all__"),[b,S]=n.useState("__all__"),[C,E]=n.useState(()=>new Set),[_,I]=n.useState(!1),[z,H]=n.useState([]),[M,$]=n.useState({}),[B,J]=n.useState({}),[q,L]=n.useState(null),[A,x]=n.useState(""),[P,K]=n.useState([]),[R,O]=n.useState(null),G=n.useCallback(async()=>{try{const U=await r("/cases",{});v(Array.isArray(U.cases)?U.cases:[]),K(Array.isArray(U.regressions)?U.regressions:[]),H(Array.isArray(U.suites)?U.suites:[]),$(Object.fromEntries((U.counts??[]).map(he=>[he.id,he.count]))),J(Object.fromEntries((U.counts??[]).map(he=>[he.id,he.keys??[]]))),y(null)}catch(U){y(U instanceof Error?U.message:String(U)),v([])}},[]),we=(e.tasks??[]).reduce((U,he)=>{const je=he.caseSummary??{total:0,pass:0,fail:0};return U+je.total*7+je.pass*13+je.fail*17},0);n.useEffect(()=>{G()},[G,we]);const fe=n.useMemo(()=>{const U=k.trim().toLowerCase(),he=q===null?null:new Set(B[q]??[]);return(f??[]).filter(je=>he!==null&&!he.has(je.key)||d!=="__all__"&&je.requirementId!==d||b!=="__all__"&&je.result!==b?!1:U===""?!0:`${je.id} ${je.title} ${je.taskTitle} ${je.requirementTitle} ${je.pointId}`.toLowerCase().includes(U))},[f,k,d,b,q,B]),ye=n.useMemo(()=>{const U=fe.filter(Ue=>Ue.result==="pass").length,he=fe.filter(Ue=>Ue.result==="fail").length,je=fe.filter(Ue=>Ue.result==="skip").length;return{pass:U,fail:he,skip:je,rest:fe.length-U-he-je}},[fe]),He=e.requirements??[],ae=fe.filter(U=>U.runnable===!0),F=ae.filter(U=>C.has(U.key)),te=U=>E(he=>{const je=new Set(he);return je.has(U)?je.delete(U):je.add(U),je}),xe=()=>E(new Set(ae.map(U=>U.key))),X=()=>E(new Set),ie=async U=>{if(U.length===0){x("先勾几条带接口定义的用例再跑(手工用例没有 http,跑不了)");return}I(!0),x("");try{const je=(await r("/cases/run",{keys:U.map(st=>st.key),by:"回归"}))?.results??[],Ue=st=>je.reduce((Ct,pt)=>Ct+(Number(pt[st])||0),0),_t=je.filter(st=>st.ok===!1),Gt=[`跑了 ${je.length} 张卡 / ${Ue("total")} 条:通过 ${Ue("passed")},不通过 ${Ue("failed")}`];Ue("bugs")>0&&Gt.push(`自动开缺陷 ${Ue("bugs")} 张`),_t.length>0&&Gt.push(`${_t.length} 张卡报错:${_t[0].error??""}`),x(Gt.join(",")),E(new Set),await G()}catch(he){x(he instanceof Error?he.message:String(he))}finally{I(!1)}},ge=U=>{const he=U?.results??[],je=Gt=>he.reduce((st,Ct)=>st+(Number(Ct[Gt])||0),0),Ue=he.filter(Gt=>Gt.ok===!1),_t=[`跑了 ${he.length} 张卡 / ${je("total")} 条:通过 ${je("passed")},不通过 ${je("failed")}`];return je("bugs")>0&&_t.push(`自动开缺陷 ${je("bugs")} 张`),Ue.length>0&&_t.push(`${Ue.length} 张卡报错:${Ue[0].error??""}`),U?.truncated!==void 0&&_t.push(`套件共 ${U.truncated.total} 条,本次只跑了前 ${U.truncated.ran} 条`),_t.join(",")},W=async U=>{I(!0),x("");try{const he=await r("/suite/run",{id:U.id});x(ge(he)),await G()}catch(he){x(he instanceof Error?he.message:String(he))}finally{I(!1)}},Ee=async()=>{const U=await i("套件名字(跑的就是当前筛选里的用例)",{title:"新建回归套件",defaultValue:"默认回归"});if(!(U===null||String(U).trim()===""))try{await r("/suite/save",{suite:{name:String(U).trim().slice(0,40),rules:{requirementIds:d==="__all__"?[]:[d],httpOnly:!0},keys:[],isDefault:z.length===0},by:"人"}),s("已保存套件"),await G()}catch(he){s(he instanceof Error?he.message:String(he),"error")}},ke=async U=>{try{await r("/suite/save",{suite:{...U,isDefault:!0},by:"人"}),s(`夜间回归改跑「${U.name}」`),await G()}catch(he){s(he instanceof Error?he.message:String(he),"error")}},Ae=async U=>{if(await o(`删除套件「${U.name}」?用例本身不受影响。`,{title:"删除套件",danger:!0,okText:"删除"}))try{await r("/suite/delete",{id:U.id}),await G()}catch(he){s(he instanceof Error?he.message:String(he),"error")}},Ve=fe.filter(U=>U.runnable===!0&&U.result==="fail"),Ya=()=>ie(F),Rn=()=>ie(Ve);return a("div",{className:"ds-body"},a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},a("span",null,"用例总览"),a("span",{className:"ds-hint"},"全公司的用例按卡片汇总:点一行跳回那张卡;用例本身仍在卡片的「测试」里维护"),a(u,{className:"ds-chip",style:{marginLeft:"auto"}},`${fe.length} 条 · ✅ ${ye.pass} · ❌ ${ye.fail}${ye.skip>0?` · ⏭️ ${ye.skip}`:""}${ye.rest>0?` · ⬜ ${ye.rest}`:""}`),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void G()},"刷新")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-row"},a(l,{className:"ds-input",style:{width:240},value:k,placeholder:"搜用例 / 卡片 / 需求 / 功能点",onChange:U=>g(U.target.value)}),a(c,{className:"ds-select",style:{width:220},value:d,onChange:U=>h(U.target.value)},a("option",{value:"__all__"},"全部需求"),He.map(U=>a("option",{key:U.id,value:U.id},`${U.code||U.id} ${U.title}`))),a(c,{className:"ds-select",style:{width:140},value:b,onChange:U=>S(U.target.value)},a("option",{value:"__all__"},"全部结果"),Zp.map(U=>a("option",{key:U,value:U},Ql[U])))),a("div",{className:"ds-row",style:{marginTop:8}},a("button",{className:"ds-btn ds-btn-ghost",disabled:_||ae.length===0,onClick:xe},`全选可跑的 ${ae.length} 条`),a("button",{className:"ds-btn ds-btn-ghost",disabled:_||C.size===0,onClick:X},"清空选择"),a("button",{className:"ds-btn ds-btn-primary",disabled:_||F.length===0,onClick:()=>void Ya()},_?"跑着…":`▶ 跑选中的 ${F.length} 条`),a("button",{className:"ds-btn",disabled:_||Ve.length===0,title:"只补跑上次不通过、且有接口定义的用例",onClick:()=>void Rn()},`↻ 只跑上次失败的 ${Ve.length} 条`),a("span",{className:"ds-hint"},"按卡串行跑,每张卡一轮;失败的照旧自动开缺陷(最多一次 60 条)")),a("div",{className:"ds-row",style:{marginTop:8,flexWrap:"wrap",gap:8}},a("span",{className:"ds-hint"},"回归套件:"),z.length===0?a("span",{className:"ds-hint"},"还没有套件。把当前筛选存成套件,夜间就自动跑它。"):z.map(U=>a("span",{key:U.id,className:"ds-suite","data-default":U.isDefault?"1":"0"},a("span",{style:{cursor:"pointer"},title:"点一下只看这个套件里的用例,再点一次取消",onClick:()=>L(he=>he===U.id?null:U.id)},`📦 ${U.name}${U.isDefault?" · 默认":""} · ${M[U.id]??0} 条${q===U.id?"(只看中)":""}`),a("button",{className:"ds-btn ds-btn-ghost",disabled:_||(M[U.id]??0)===0,onClick:()=>void W(U)},"▶ 跑"),U.isDefault?null:a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void ke(U)},"设为默认"),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Ae(U),"data-danger":"1"},"删除"))),a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Ee()},"+ 存为套件")),A===""?null:a("div",{className:"ds-hint",style:{marginTop:6}},A),P.length===0?null:a("div",{className:"ds-reg-list"},a("span",{className:"ds-hint"},"最近回归:"),P.slice(0,4).map(U=>a("span",{key:String(U.at),className:"ds-reg-item","data-bad":(Number(U.failed)||0)+(Number(U.failedCards)||0)>0?"1":"0",title:`${new Date(U.at).toLocaleString("zh-CN")} · ${U.by||"人"}`},`${Ke(U.at)} ${U.by||""} · ${U.cards} 卡 / ${U.total} 条 · ✅${U.passed} ❌${U.failed}${U.bugs>0?` 🐞${U.bugs}`:""}`))),w!==null?a("div",{className:"ds-err"},w):null,f===null?a("div",{className:"ds-hint",style:{padding:"10px 2px"}},"载入用例…"):fe.length===0?a("div",{className:"ds-empty"},"没有符合条件的用例。用例登记在卡片详情的「测试」里。"):a("div",{className:"ds-case-lib"},fe.map(U=>a("div",{key:U.key,className:"ds-case-lib-wrap"},a("div",{className:"ds-case-lib-row","data-result":U.result,title:"打开所属卡片",onClick:()=>t(U.taskId)},a("span",{className:"ds-case-lib-pick",onClick:he=>he.stopPropagation()},a(p,{checked:C.has(U.key),disabled:_||U.runnable!==!0,onChange:()=>te(U.key)})),a("span",{className:"ds-case-lib-result"},Ql[U.result]??U.result),a("span",{className:"ds-case-lib-title"},U.title),a("span",{className:"ds-case-lib-meta"},`${U.taskCode||U.taskId} · ${U.requirementTitle||"未挂需求"}${U.pointId?" · "+U.pointId:""}`),a("span",{className:"ds-case-lib-meta"},U.http?"接口":U.shell?"shell":U.db?"数据库":"手工"),a("span",{className:"ds-case-lib-pri"},U.priority),a("span",{className:"ds-case-lib-at"},U.lastRunAt>0?Ke(U.lastRunAt):"没跑过"),a("button",{className:"ds-btn ds-btn-ghost ds-case-lib-more",title:"看最近一次执行的明细",onClick:he=>{he.stopPropagation(),O(R===U.key?null:U.key)}},R===U.key?"收起":"明细")),R===U.key?a("div",{className:"ds-case-lib-detail"},a("div",null,`期望:${U.expected||"—"}`),a("div",null,`最近一次:${U.note||(U.lastRunAt>0?"只有结果,没有记录":"还没跑过")}`),U.lastError===""?null:a("div",{className:"ds-err"},`错误:${U.lastError}`),U.lastPreview===""?null:a("pre",{className:"ds-case-lib-preview"},U.lastPreview),a("div",{className:"ds-hint"},`用例编号 ${U.id} · 所属卡片 ${U.taskCode||U.taskId}`)):null))))))}function aw({value:e,tone:t}){const{h:a,kit:n}=vt(),r=n("ProgressBar"),s=Math.max(0,Math.min(100,Number(e)||0));if(r!==null)return a(r,{value:s,tone:t});const o=t==="over"?"#e5484d":t==="warn"?"#d97706":void 0;return a("div",{className:"ds-an-track"},a("div",{className:"ds-an-fill",style:{width:`${s}%`,background:o}}))}function nw({series:e}){const{h:t,kit:a}=vt(),n=Array.isArray(e)?e:[];if(n.length===0)return t("div",{className:"ds-empty"},"还没有数据。");const r=n.map(u=>Math.max(0,Number(u.remaining)||0)),s=Math.max(1,...r),o=n.length===1?100:100/(n.length-1),i=n.map((u,p)=>({x:p*o,y:38-r[p]/s*32,remaining:r[p],day:String(u.day??"")})),l=i.map((u,p)=>`${p===0?"M":"L"}${u.x.toFixed(2)},${u.y.toFixed(2)}`).join(" "),c=`${l} L100,40 L0,40 Z`;return t("div",{className:"ds-burn"},t("svg",{viewBox:"0 0 100 40",preserveAspectRatio:"none",className:"ds-burn-svg",role:"img","aria-label":"燃尽趋势"},t("path",{d:c,className:"ds-burn-area"}),t("path",{d:l,className:"ds-burn-line",vectorEffect:"non-scaling-stroke"}),i.map(u=>t("circle",{key:u.day,cx:u.x,cy:u.y,r:1.1,className:"ds-burn-dot"},t("title",{},`${u.day}:还剩 ${u.remaining} 张`)))),t("div",{className:"ds-burn-axis"},t("span",{},i[0].day),t("span",{},i[i.length-1].day)))}const od=800;function uf(e){const t=String(e??"").replace(/\r\n?/g,`
|
|
43
43
|
`);if(t==="")return[];const a=t.split(`
|
|
44
44
|
`);return a.length>0&&a[a.length-1]===""&&a.pop(),a}function sw(e,t){const a=uf(e),n=uf(t);if(a.length>od||n.length>od)return[{kind:"del",text:`(原基线 ${a.length} 行,超过 ${od} 行的对比上限)`},{kind:"add",text:`(当前文档 ${n.length} 行)`}];const r=Array.from({length:a.length+1},()=>new Array(n.length+1).fill(0));for(let l=a.length-1;l>=0;l-=1)for(let c=n.length-1;c>=0;c-=1)r[l][c]=a[l]===n[c]?r[l+1][c+1]+1:Math.max(r[l+1][c],r[l][c+1]);const s=[];let o=0,i=0;for(;o<a.length&&i<n.length;){if(a[o]===n[i]){s.push({kind:"same",text:a[o]}),o+=1,i+=1;continue}if(r[o+1][i]>=r[o][i+1]){s.push({kind:"del",text:a[o]}),o+=1;continue}s.push({kind:"add",text:n[i]}),i+=1}for(;o<a.length;)s.push({kind:"del",text:a[o]}),o+=1;for(;i<n.length;)s.push({kind:"add",text:n[i]}),i+=1;return s}function rw(e){const t=Array.isArray(e)?e:[];return{added:t.filter(a=>a.kind==="add").length,removed:t.filter(a=>a.kind==="del").length,same:t.filter(a=>a.kind==="same").length}}function ow({state:e,onOpenTask:t,onChanged:a,createSignal:n,focusReq:r}){const{h:s,React:o,api:i,toast:l,currentUser:c,confirmDialog:u,promptDialog:p,renderMarkdown:f,TextField:v,TextAreaField:w,ChipField:y,CheckField:k,SelectField:g,MdField:d}=vt(),h=e.requirements??[],b=e.config.projects??[],[S,C]=o.useState(h.length>0?h[0].id:null),E=()=>{I({title:"",description:"",project:"",iteration:"",owner:"",priority:"normal",status:"draft",links:{}}),H(null)};o.useEffect(()=>{Number(n)>0&&E()},[n]),o.useEffect(()=>{r&&r.id&&C(r.id)},[r]);const[_,I]=o.useState(null),[z,H]=o.useState(null),[M,$]=o.useState(!1),[B,J]=o.useState(null),[q,L]=o.useState(null),[A,x]=o.useState(null),[P,K]=o.useState(null),[R,O]=o.useState(null),[G,we]=o.useState(!1),[fe,ye]=o.useState(null),[He,ae]=o.useState(!1),[F,te]=o.useState(null),[xe,X]=o.useState(null),[ie,ge]=o.useState(null),[W,Ee]=o.useState(""),[ke,Ae]=o.useState(null),[Ve,Ya]=o.useState("0"),[Rn,U]=o.useState([]),[he,je]=o.useState(null),[Ue,_t]=o.useState(null),[Gt,st]=o.useState(!1),Ct=async()=>{if(pt!==null){st(!0);try{const V=await i("/requirement/impact",{id:pt.id});_t({summary:V.summary,cards:V.cards??[],cases:V.cases??[]})}catch(V){je(V instanceof Error?V.message:String(V))}finally{st(!1)}}},pt=h.find(V=>V.id===S)??null;o.useEffect(()=>{Ya(String(pt?.order??0)),U(Array.isArray(pt?.dependsOn)?pt.dependsOn:[]),je(null)},[S,pt?.updatedAt]);const Ea=async()=>{if(pt!==null)try{await i("/requirement/save",{requirement:{id:pt.id,title:pt.title,description:pt.description??"",project:pt.project??"",iteration:pt.iteration??"",owner:pt.owner??"",priority:pt.priority??"normal",status:pt.status??"draft",links:pt.links??{},order:Number(Ve)||0,dependsOn:Rn}}),je("已保存:排序与依赖"),l("排序与依赖已保存"),a!==void 0&&a()}catch(V){je(V instanceof Error?V.message:String(V))}},hs=async V=>{if(Q===null)return;const ce=String(W??"").trim();if(V==="sign"&&ce===""){Ae("签字要写一句结论:交付了什么、遗留什么");return}try{await i("/requirement/signoff",{id:Q.id,action:V,note:ce,by:c()}),Ae(V==="sign"?"已签字":"已取消签字"),l(V==="sign"?"需求已签字":"已取消需求签字"),Ee(""),a!==void 0&&a()}catch(Le){Ae(Le.message)}},Y=o.useCallback(async V=>{if(V==null||V===""){te(null);return}try{const ce=await i("/requirement/matrix",{id:V});te(ce.matrix)}catch(ce){X(ce.message)}},[]),ne=async V=>{if(Q!==null)try{await i("/requirement/save",{requirement:{...Q,links:Q.links??{},points:V}}),await Y(Q.id),X("功能点已保存"),ge(null)}catch(ce){X(ce.message)}},ee=()=>({title:"",description:"",priority:"normal",status:"draft",acceptance:[]}),be=async()=>{if(ie===null||Q===null)return;if(String(ie.title??"").trim()===""){X("功能点要有标题");return}const V=String(ie.acceptanceText??"").split(`
|
|
45
|
-
`).map(At=>At.trim()).filter(At=>At!==""),ce={id:ie.id??`p${(Q.points??[]).length+1}`,title:ie.title,description:ie.description??"",priority:ie.priority??"normal",status:ie.status??"draft",acceptance:V.map((At,$a)=>({id:`a${$a+1}`,text:At,done:!1}))},Le=(Q.points??[]).filter(At=>At.id!==ce.id);await ne([...Le,ce])},se=async V=>{Q!==null&&await u("删除这个功能点?挂在上面的卡会变成「未归功能点」。",{title:"删除功能点",danger:!0,okText:"删除"})&&await ne((Q.points??[]).filter(ce=>ce.id!==V))},de=async(V,ce)=>{try{await i("/task/update",{id:V,pointId:ce}),Q!==null&&await Y(Q.id)}catch(Le){X(Le.message)}},Q=h.find(V=>V.id===S)??null,ve=V=>e.tasks.filter(ce=>ce.requirementId===V),Ce=V=>e.statuses.find(ce=>ce.id===V)?.label??V;o.useEffect(()=>{te(null),X(null),Q!==null&&Y(Q.id)},[Q===null?"":Q.id,Y]);const Se=async()=>{if(_!==null){if(String(_.title??"").trim()===""){H("需求标题不能为空");return}$(!0);try{const V=await i("/requirement/save",{requirement:{id:_.id,title:_.title,description:_.description,project:_.project,owner:_.owner,domain:_.domain??"",priority:_.priority,status:_.status,links:{prd:_.prd,proto:_.proto,acceptance:_.acceptance}}});C(V.requirement.id),I(null),H(null),l(V.created===!0?"需求已创建":"需求已保存")}catch(V){H(V.message)}finally{$(!1)}}},Pe=async V=>{if(Q!==null)try{await i("/requirement/save",{requirement:{...Q,links:Q.links??{},status:V}}),l("需求状态已更新")}catch(ce){H(ce.message)}},dt=async V=>{if(await u("删除这条需求?它下面的卡会保留,只是不再挂需求。",{title:"删除需求",danger:!0,okText:"删除"}))try{await i("/requirement/delete",{id:V}),C(null)}catch(ce){H(ce.message)}},rt={pending:"待评审",approved:"评审通过",changes_requested:"评审打回"},[Je,We]=o.useState(!1),[tt,Ge]=o.useState(null),Te=async(V,ce)=>{if(Q!==null){We(!0),Ge(null);try{const Le=await i("/requirement/review",{id:Q.id,decision:V,note:ce??"",by:c(),operator:c()});Ge(`已${V==="approve"?"通过":V==="reject"?"打回":"提交"}评审:${rt[Le.review?.status]??""}`),typeof a=="function"&&a()}catch(Le){Ge(Le.message)}finally{We(!1)}}},It=async()=>{const V=await p("打回要给意见(会记在评审记录里):",{title:"评审打回",danger:!0,okText:"打回",placeholder:"写清楚哪里不满足,之后要据此改"});if(V!==null){if(String(V).trim()===""){Ge("打回必须写意见");return}Te("reject",String(V).trim().slice(0,600))}},[Be,yt]=o.useState(!1),[Rt,Ut]=o.useState(null),le=async()=>{if(Q!==null){yt(!0),Ut(null);try{const ce=(await i("/requirement/split",{id:Q.id})).created??[];Ut(ce.length===0?"没有可拆的功能点:要么都覆盖了,要么还没加功能点。":`已建 ${ce.length} 张卡:${ce.map(Le=>Le.code||Le.id).join("、")}`),typeof a=="function"&&a()}catch(V){Ut(V.message)}finally{yt(!1)}}},Ne=()=>({kind:"prd",title:"",url:Q?.links?.prd??"",version:"v1.0",content:"",status:"draft"}),me=async V=>{if(L(null),V===null){J(Ne());return}try{const Le=((await i("/requirement/get",{id:Q.id})).requirement?.docs??[]).find(At=>At.id===V);J(Le===void 0?Ne():{...Le})}catch(ce){H(ce.message)}},Qa=async V=>{if(A!==null&&A.id===V){x(null);return}x({id:V,content:"",busy:!0});try{const Le=((await i("/requirement/get",{id:Q.id})).requirement?.docs??[]).find(At=>At.id===V);x({id:V,content:String(Le?.content??""),busy:!1})}catch(ce){H(ce.message),x(null)}},Et=async()=>{if(P!==null){K(null);return}try{const ce=(await i("/requirement/get",{id:Q.id})).requirement??{},Le=ce.baseline??null,$a=[...ce.docs??[]].sort((Dn,Fo)=>(Number(Fo.at)||0)-(Number(Dn.at)||0)),ra=$a.find(Dn=>Dn.kind==="prd")??$a[0]??null;if(Le===null){K(null),H("这条需求还没有基线,先挑一份文档「设为基线」");return}if(ra===null){K(null),H("这条需求还没有文档可以对比");return}const Ma=sw(Le.content??"",ra.content??"");K({rows:Ma,summary:rw(Ma),version:Le.version,docTitle:ra.title})}catch(V){K(null),H(V instanceof Error?V.message:String(V))}},fa=async()=>{if(R!==null){O(null);return}try{const V=await i("/requirement/get",{id:Q.id}),ce={};for(const Le of V.requirement?.docs??[])ce[Le.id]=String(Le.content??"");O(ce)}catch(V){H(V.message)}},An=async()=>{if(B!==null){if(String(B.title??"").trim()===""){L("文档要有标题");return}we(!0);try{const V=await i("/requirement/doc",{id:Q.id,action:B.id===void 0?"add":"update",docId:B.id,doc:{kind:B.kind,title:B.title,url:B.url,version:B.version,content:B.content,status:B.status}});J(null),L(null)}catch(V){L(V.message)}finally{we(!1)}}},La=async()=>{ae(!0),ye(null);try{const V=await i("/requirement/folder",{id:Q.id});ye(V.ok?`云盘文件夹已就绪:${V.folder?.url??""}`:`创建失败:${V.error||V.folder?.error||"未知原因"}`)}catch(V){ye(V.message)}finally{ae(!1)}},Lt=async()=>{ae(!0),ye(null);try{const ce=(await i("/requirement/sync",{id:Q.id})).report??{},Le=Number(ce.failed??0);ye(`归档完成:共 ${ce.total??0} 件,成功 ${ce.written??0} 件${Le>0?`,失败 ${Le} 件(${ce.errors?.[0]??""})`:"。"}`)}catch(V){ye(V.message)}finally{ae(!1)}},Mt=async V=>{try{await i("/requirement/doc",{id:Q.id,action:"baseline",docId:V,by:"人"}),L("这一版已生效,之后新建的卡都会记下它。")}catch(ce){L(ce.message)}},jn=async()=>{try{await i("/requirement/doc",{id:Q.id,action:"clear-baseline"}),L("基线已取消,需求回到「评审中」。")}catch(V){L(V.message)}},Z=async V=>{if(await u("删除这份文档?",{title:"删除文档",danger:!0,okText:"删除"}))try{await i("/requirement/doc",{id:Q.id,action:"remove",docId:V})}catch(ce){L(ce.message)}},_e=async()=>{we(!0),L(null);try{const V=await i("/requirement/pull",{url:B?.url??""});if(V.ok===!0){const ce=String(V.text??"");J(Le=>({...Le,title:String(Le?.title??"").trim()===""?String(V.title??""):Le.title,content:ce})),L(`已拉取 ${ce.length} 字正文${V.title?`,标题取自文档「${V.title}」`:""}。`)}else L(V.error??"拉取失败")}catch(V){L(V.message)}finally{we(!1)}},xt=_===null?null:s("div",{className:"ds-panel ds-form-panel"},s("div",{className:"ds-panel-head"},_.id===void 0?"新建需求":`编辑 ${Q?.code??""}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-field"},s("div",{className:"ds-label"},"需求标题"),s(v,{className:"ds-input",value:_.title,onChange:V=>I({..._,title:V.target.value})})),s("div",{className:"ds-row"},s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务项目"),s(g,{className:"ds-select",value:_.project,onChange:V=>I({..._,project:V.target.value})},s("option",{value:""},"不指定"),b.map(V=>s("option",{key:V.id,value:V.id},V.name)))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"状态"),s(g,{className:"ds-select",value:_.status,onChange:V=>I({..._,status:V.target.value})},ef.map(V=>s("option",{key:V,value:V},Ar[V])))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务负责人"),s(v,{className:"ds-input",value:_.owner,onChange:V=>I({..._,owner:V.target.value})})),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务领域"),s(v,{className:"ds-input",placeholder:"支付 / 风控 / 会员(决定派给哪位领域专家)",value:_.domain??"",onChange:V=>I({..._,domain:V.target.value})}))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"PRD 链接"),s(v,{className:"ds-input",placeholder:"https://…(飞书云文档)",value:_.prd,onChange:V=>I({..._,prd:V.target.value})})),s("div",{className:"ds-row"},s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"原型链接"),s(v,{className:"ds-input",value:_.proto,onChange:V=>I({..._,proto:V.target.value})})),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"验收标准链接"),s(v,{className:"ds-input",value:_.acceptance,onChange:V=>I({..._,acceptance:V.target.value})}))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"需求正文 / PRD 摘要"),s(d,{className:"ds-textarea",rows:10,style:{minHeight:180},placeholder:"支持 Markdown:`# 背景`、`- 列表`、`**重点**`、`` `代码` ``、`| 表格 |`;保存后在「需求明细」里按 Markdown 渲染,不再截断。",value:_.description,onChange:V=>I({..._,description:V.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{I(null),H(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:M,onClick:()=>void Se()},M?"保存中…":"保存需求")))),ht=Q===null?s("div",{className:"ds-empty"},"左边选一条需求,或先新建一条。"):(()=>{const V=ve(Q.id),ce=V.filter(j=>j.status==="done").length,Le=V.filter(j=>j.blocked!==void 0&&j.blocked!==null).length,At=V.filter(j=>j.type==="bug"&&j.status!=="done").length,$a=V.reduce((j,ue)=>{const Fe=ed(ue);return{done:j.done+Fe.done,total:j.total+Fe.total}},{done:0,total:0}),ra=V.reduce((j,ue)=>{const Fe=Jl(ue);return{pass:j.pass+Fe.pass,total:j.total+Fe.total}},{pass:0,total:0}),Ma=V.flatMap(j=>(j.artifacts??[]).map(ue=>({...ue,task:j}))),Dn=V.filter(j=>(j.gitlabMR?.iid??0)>0),Fo=ra.pass,On=Q.links??{},dn=Q.folder??{},Ln=(j,ue)=>ue===""?null:s("a",{className:"ds-link",href:ue,target:"_blank",rel:"noreferrer"},j);return s("div",{className:"ds-req-body"},s("div",{className:"ds-req-headline"},s("div",null,s("div",{className:"ds-req-code"},Q.code||Q.id),s("div",{className:"ds-req-name"},Q.title)),s("div",{className:"ds-req-actions"},s(g,{className:"ds-select",value:Q.status,onChange:j=>void Pe(j.target.value)},ef.map(j=>s("option",{key:j,value:j},Ar[j]))),s("button",{className:"ds-btn",onClick:()=>I({...Q,...On})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void dt(Q.id),"data-danger":"1"},"删除"))),(()=>{const j=Q.review??null,ue=j===null?"还没提交评审":rt[j.status]??j.status,Fe=j===null||j.status==="changes_requested",wt=j!==null&&j.status==="pending";return s("div",{className:"ds-panel","data-review":j===null?"none":j.status,style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求评审"),s(y,{className:"ds-chip","data-on":j?.status==="approved"?"1":"0",style:{marginLeft:8}},ue),j===null||j.at===0?null:s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ke(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},j!==null&&String(j.note??"")!==""?s("div",{className:"ds-hint",style:{marginBottom:8}},`评审意见:${j.note}`):null,s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},Fe?s("button",{className:"ds-btn ds-btn-primary",disabled:Je,onClick:()=>void Te("submit")},Je?"提交中…":"提交评审"):null,wt?s("button",{className:"ds-btn ds-btn-primary",disabled:Je,onClick:()=>void Te("approve")},"评审通过"):null,wt?s("button",{className:"ds-btn ds-btn-ghost",disabled:Je,onClick:It,"data-danger":"1"},"打回"):null,j?.status==="approved"?s("span",{className:"ds-hint"},"评审已通过,可以拆卡和派活了。"):null),tt===null?null:s("div",{className:"ds-hint",style:{marginTop:8}},tt)))})(),Ln("PRD",On.prd)||Ln("原型",On.proto)||Ln("验收标准",On.acceptance)?s("div",{className:"ds-req-links"},Ln("PRD 文档",On.prd),Ln("原型",On.proto),Ln("验收标准",On.acceptance)):s("div",{className:"ds-empty"},[s("span",null,"还没有登记 PRD / 原型 / 验收标准链接;PRD 与原型由产品在澄清后输出,可在这里补,或点右边「让产品补 PRD」。"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:async()=>{const j=await p("想请产品补什么?",{title:"申请产品补 PRD",defaultValue:"需要产品输出 PRD 与原型"});if(j!==null)try{await i("/requirement/request-prd",{id:Q.id,by:c(),note:j}),l("已申请产品补 PRD"),a()}catch(ue){l(ue.message??String(ue),"error")}}},"让产品补 PRD")]),Q.description?s("div",{className:"ds-panel ds-req-desc-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求明细"),Q.source==="business"?s("span",{className:"ds-hint","data-src":"business"},`业务提单 · ${Q.requester?.name||"未署名"}${Q.requester?.dept?"("+Q.requester.dept+")":""} · ${Ke(Q.requester?.at||Q.createdAt)}`):null,s("span",{className:"ds-hint",style:{marginLeft:"auto"}},"支持 Markdown")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-req-desc ds-md"},f(Q.description)))):s("div",{className:"ds-req-desc ds-hint"},"还没有写需求描述:产品背景、要解决的问题、验收口径都建议写在这里,点右侧「编辑」补上。"),(()=>{const j=(Q.history??[]).slice().reverse();if(j.length===0||(Q.change??null)!==null)return null;const ue={title:"标题",description:"需求明细",priority:"优先级",status:"状态",owner:"负责人",project:"项目",iteration:"迭代"};return s("div",{className:"ds-panel ds-history-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"变更记录"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`最近 ${j.length} 条`)),s("div",{className:"ds-panel-body"},s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:260,overflowY:"auto"}},j.map((Fe,wt)=>s("div",{key:`${Fe.at}-${wt}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint",style:{flex:"0 0 auto",fontVariantNumeric:"tabular-nums"}},Ke(Fe.at)),s("span",{style:{flex:"0 0 auto"}},Fe.by===""?"人":Fe.by),s("span",{className:"ds-chip",style:{flex:"0 0 auto"}},ue[Fe.field]??Fe.field),s("span",{className:"ds-hint",style:{minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},`${Fe.from===""?"(空)":Fe.from} → ${Fe.to===""?"(空)":Fe.to}`))))))})(),(()=>{const j=Q.pool??{reach:0,impact:0,confidence:0,effort:0,score:0},ue=async(wt,ma,et)=>{const De=await p(ma,{title:wt,defaultValue:String(et??0)});if(De===null)return null;const Ye=Number(String(De).trim());return Number.isFinite(Ye)&&Ye>=0?Ye:null},Fe=async()=>{const wt=await ue("影响面 Reach","影响多少用户或多少笔交易?估算依据写进正文",j.reach);if(wt===null)return;const ma=await ue("影响度 Impact","3 显著 / 2 高 / 1 中 / 0.5 低",j.impact);if(ma===null)return;const et=await ue("置信度 Confidence","100 有数据 / 80 有访谈 / 50 只有猜测(百分数)",j.confidence);if(et===null)return;const De=await ue("成本 Effort","人日(研发+测试+联调)",j.effort);if(De!==null)try{await i("/requirement/pool",{id:Q.id,pool:{reach:wt,impact:ma,confidence:et,effort:De}}),l("需求池评分已更新"),await a()}catch(Ye){l(Ye instanceof Error?Ye.message:String(Ye),"error")}};return s("div",{className:"ds-panel ds-pool-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求池评分(RICE)"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},j.score>0?`得分 ${j.score}`:"还没打分:不打分就没法和别的需求比优先级"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void Fe()},j.score>0?"重新打分":"打分")),j.score>0?s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},s("span",{className:"ds-chip"},`影响面 ${j.reach}`),s("span",{className:"ds-chip"},`影响度 ${j.impact}`),s("span",{className:"ds-chip"},`置信度 ${j.confidence}%`),s("span",{className:"ds-chip"},`成本 ${j.effort} 人日`),s("span",{className:"ds-chip","data-on":"1"},`得分 ${j.score}`))):null)})(),(()=>{const j=Q.questions??[],ue=j.filter(De=>De.status==="open").length,Fe=async(De,Ye)=>{try{await i("/requirement/questions",{id:Q.id,...De}),Ye&&l(Ye),await a()}catch(Mn){l(Mn instanceof Error?Mn.message:String(Mn),"error")}},wt=async()=>{const De=await p("还没确认清楚的问题是什么?",{title:"加一个待确认问题",placeholder:"例如:退款失败时是否允许人工补发?"});De===null||De.trim()===""||await Fe({action:"add",text:De},"已加入待确认清单")},ma=async De=>{const Ye=await p(De.text,{title:"回答这个问题",defaultValue:De.answer??"",placeholder:"写清结论与依据"});Ye===null||Ye.trim()===""||await Fe({action:"answer",questionId:De.id,answer:Ye},"已回答")},et=async De=>{await u("删掉这个问题?",{title:"删除待确认项",danger:!0,okText:"删除"})===!0&&await Fe({action:"delete",questionId:De.id},"已删除")};return s("div",{className:"ds-panel ds-question-panel","data-open":ue>0?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"待确认清单"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},ue>0?`${ue} 个没回答,回答完才能拆卡`:j.length===0?"澄清阶段把不确定的先记在这里":"全部已回答"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void wt()},"+ 加问题")),s("div",{className:"ds-panel-body"},j.length===0?s("div",{className:"ds-empty"},"还没有待确认的问题。产品澄清时把「需要业务拍板」的点写在这里,避免猜业务规则。"):s("div",{style:{display:"flex",flexDirection:"column",gap:8}},j.map(De=>s("div",{key:De.id,className:"ds-question-row","data-status":De.status,style:{display:"flex",gap:10,alignItems:"flex-start",padding:"8px 10px",border:"1px solid var(--border)",borderRadius:8,background:"var(--card)"}},s("span",{className:"ds-chip","data-on":De.status==="answered"?"1":"0"},De.status==="answered"?"已回答":"待回答"),s("div",{style:{flex:"1 1 auto",minWidth:0}},s("div",{style:{fontSize:13}},De.text),De.answer===""||De.answer===void 0?null:s("div",{className:"ds-hint",style:{marginTop:4}},`答:${De.answer}`)),s("div",{style:{display:"flex",gap:6,flex:"0 0 auto"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void ma(De)},De.status==="answered"?"改答案":"回答"),s("button",{className:"ds-btn ds-btn-ghost","data-danger":"1",onClick:()=>void et(De)},"删除")))))))})(),(()=>{const j=Q.change??null;if(j===null)return null;const ue=j.affected??[],Fe=j.affectedCases??[],wt=new Map(V.map(et=>[et.id,et])),ma=et=>e.statuses.find(De=>De.id===et)?.label??et;return s("div",{className:"ds-panel ds-change-panel","data-invalidated":j.invalidated?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,j.invalidated?"变更影响(签字已作废)":"变更影响"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ke(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},function(){const et=(Q.history??[]).slice().reverse();if(et.length===0)return null;const De={title:"标题",description:"需求明细",priority:"优先级",status:"状态",owner:"负责人",project:"项目",iteration:"迭代"};return s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:220,overflowY:"auto",marginBottom:8,paddingBottom:8,borderBottom:"1px dashed var(--border)"}},s("div",{className:"ds-hint",style:{fontWeight:600}},`变更记录 · 最近 ${et.length} 条`),et.map((Ye,Mn)=>s("div",{key:`${Ye.at}-${Mn}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint"},Ke(Ye.at)),s("span",null,Ye.by===""?"人":Ye.by),s("span",{className:"ds-chip"},De[Ye.field]??Ye.field),s("span",{className:"ds-hint"},`${Ye.from===""?"(空)":Ye.from} → ${Ye.to===""?"(空)":Ye.to}`))))}(),s("div",{className:"ds-hint"},`改了:${(j.parts??[]).join("、")}。${ue.length===0?"当时没有已产出的卡受影响,不用返工。":`${ue.length} 张卡已经产出过东西${Fe.length===0?"":`、${Fe.length} 条用例要重跑`},要按新版复核:`}`),ue.length===0?null:s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8,marginTop:8}},ue.map(et=>s("span",{key:et.id,className:"ds-chip","data-pt":"open",style:{cursor:"pointer"},title:et.title,onClick:()=>{const De=wt.get(et.id);De!==void 0&&typeof t=="function"&&t(De)}},`${et.code||et.id} ${ma(et.status)}`))),j.invalidated?s("div",{className:"ds-hint",style:{marginTop:8}},"验收签字已清空:这些卡是按旧版做的,复核完重新签一次。"):null))})(),(()=>{const j=Q.deploys??[];return j.length===0?null:s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"上线足迹"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},"来自版本的部署记录,只留痕不触发")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},j.map(ue=>s("span",{key:ue.id,className:"ds-chip","data-env":ue.kind,title:`${ue.releaseName??""} · ${Ke(ue.at)} · ${ue.by}`},`${ue.name} · ${Xa(ue.at)}`)))))})(),s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"依赖与排序")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row"},s("span",{className:"ds-hint"},"排序号"),s(v,{className:"ds-input",style:{width:90},value:Ve,onChange:j=>Ya(j.target.value)}),s("span",{className:"ds-hint"},"被依赖的需求没交付,这条需求的准出会被拦")),s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:10}},h.filter(j=>j.id!==Q.id).map(j=>s("label",{key:j.id,className:"ds-check-row"},s(k,{checked:Rn.includes(j.id),onChange:ue=>U(Fe=>ue.target.checked?[...Fe,j.id]:Fe.filter(wt=>wt!==j.id))}),s("span",{className:"ds-check-text"},`${j.code||j.id} ${j.title}(${Ar[j.status]??j.status})`))),h.length<=1?s("span",{className:"ds-hint"},"只有这一条需求,暂时没有可依赖的对象。"):null),s("div",{className:"ds-row"},s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void Ea()},"保存排序与依赖"),he===null?null:s("span",{className:"ds-hint"},he)))),(()=>{const j=Q.progress??null,ue=j?.gate??{ready:!1,blockers:["还没有加载出准出状态,稍后自动刷新"],warnings:[]},Fe=ue.stats??{},wt=Q.signoff??null,ma=j?.progress??(V.length===0?0:Math.round(ce/V.length*100)),et=Fe.coveredPoints??0,De=Fe.points??(Q.points??[]).length,Ye=Fe.pass??Fo,Mn=Fe.cases??ra.total;return s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求准出与验收签字"),s(y,{className:"ds-chip","data-pt":ue.ready?"covered":"open",style:{marginLeft:"auto"}},ue.ready?"可以交付":`还差 ${ue.blockers.length} 项`)),s("div",{className:"ds-panel-body"},s("div",{className:"ds-check-progress"},s("div",{className:"ds-check-bar","data-full":ma===100?"1":"0"},s("span",{style:{width:`${ma}%`}})),s("div",{className:"ds-hint"},`${ma}% · ${ce}/${V.length} 张卡完成 · 功能点覆盖 ${et}/${De} · 用例通过 ${Ye}/${Mn}`)),(ue.blockers??[]).map(zn=>s("div",{key:zn,className:"ds-gate-item","data-kind":"block"},`⛔ ${zn}`)),(ue.warnings??[]).map(zn=>s("div",{key:zn,className:"ds-gate-item","data-kind":"warn"},`⚠ ${zn}`)),ue.ready?s("div",{className:"ds-hint"},"门禁已通过:签完字这条需求就算交付。"):null,wt===null?s("div",{className:"ds-gate-sign"},s(v,{className:"ds-input",style:{flex:1,minWidth:220},placeholder:"验收结论:交付了什么、遗留什么",value:W,onChange:zn=>Ee(zn.target.value)}),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void hs("sign")},"需求验收签字")):s("div",{className:"ds-gate-sign"},s("div",{className:"ds-hint",style:{flex:1}},`✍ ${wt.by} 于 ${Ke(wt.at)} 签字:${wt.note}`),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void hs("clear")},"取消签字")),ke===null?null:s("div",{className:"ds-hint"},ke)))})(),s("div",{className:"ds-sec-head",style:{marginTop:16}},"交付物归档(飞书云盘)",s("div",{className:"ds-req-actions"},dn.url?s("a",{className:"ds-link",href:dn.url,target:"_blank",rel:"noreferrer"},"打开文件夹"):null,s("button",{className:"ds-btn ds-btn-ghost",disabled:He,onClick:()=>void La()},dn.url?"校验文件夹":"创建文件夹"),s("button",{className:"ds-btn",disabled:He||Ma.length===0,onClick:()=>void Lt()},He?"归档中…":`归档 ${Ma.length} 件交付物`))),s("div",{className:"ds-hint"},dn.url?`文件夹:${dn.name||dn.token} 已归档 ${Ma.filter(j=>j.doc?.url).length}/${Ma.length} 件交付物`:dn.error?`上次创建失败:${dn.error}`:"还没有云盘文件夹;建好后,这条需求下每件交付物都会写成文件夹里的一份 Markdown 文档。"),fe?s("div",{className:"ds-hint"},fe):null,s("div",{className:"ds-sec-head",style:{marginTop:16}},"文档与基线",s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void me(null)},"+ 添加文档")),Q.baseline?s("div",{className:"ds-base-box"},s("div",{className:"ds-base-main"},s("b",null,`当前基线 ${Q.baseline.version}`),s("span",null,`${Q.baseline.title} · 生效于 ${Ke(Q.baseline.at)} · ${Q.baseline.by||"人"} · 正文 ${Q.baseline.contentLength??0} 字`)),s("button",{className:"ds-btn ds-btn-ghost",title:"把当前文档和这份基线逐行比一遍(只读)",onClick:()=>void Et()},P===null?"对比基线":"收起对比"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void jn()},"取消基线")):s("div",{className:"ds-base-box","data-empty":"1"},s("div",{className:"ds-base-main"},s("b",null,"还没有基线"),s("span",null,"PRD 评审通过后点「设为基线」,之后建的卡都会记下用的是哪一版。"))),Q.baseline!==null&&P!==null?s("div",{className:"ds-diff"},s("div",{className:"ds-diff-head"},s("b",null,`基线 ${P.version} → 当前:${P.docTitle}`),s("span",{className:"ds-hint"},P.summary.added+P.summary.removed===0?"正文没有变化":`+${P.summary.added} 行 / −${P.summary.removed} 行`)),s("div",{className:"ds-diff-body"},P.rows.slice(0,400).map((j,ue)=>s("div",{key:ue,className:"ds-diff-line","data-kind":j.kind},`${j.kind==="add"?"+":j.kind==="del"?"−":" "} ${j.text}`)),P.rows.length>400?s("div",{className:"ds-hint"},`(还有 ${P.rows.length-400} 行未显示)`):null)):null,(Q.docs??[]).length===0?s("div",{className:"ds-hint",style:{marginTop:8}},"还没有登记文档。可以先加 PRD 链接,把正文粘进来,评审通过后设为基线。"):s("div",null,s("div",{className:"ds-row",style:{marginTop:8,gap:8,alignItems:"center"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void fa()},R===null?"全部展开":"全部收起"),s("span",{className:"ds-hint"},`共 ${(Q.docs??[]).length} 篇,展开后可直接读 Markdown 正文`)),s("div",{className:"ds-doc-list"},(Q.docs??[]).map(j=>s("div",{key:j.id,className:"ds-doc-wrap"},s("div",{className:"ds-doc-row"},s(y,{className:"ds-chip","data-doc":j.kind},tf[j.kind]??j.kind),s("div",{className:"ds-doc-main"},s("div",{className:"ds-doc-title"},j.url===""?j.title:s("a",{className:"ds-link",href:j.url,target:"_blank",rel:"noreferrer"},j.title),s("span",{className:"ds-chip-code"},j.version),s(y,{className:"ds-chip","data-docstatus":j.status},cx[j.status]??j.status)),s("div",{className:"ds-doc-meta"},`正文 ${j.contentLength??0} 字 · ${j.by||"—"} · ${j.at>0?Ke(j.at):"—"}`)),s("div",{className:"ds-listrow-actions"},j.status==="baseline"?null:s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Mt(j.id)},"设为基线"),s("button",{className:"ds-btn ds-btn-ghost",title:"直接读正文(Markdown 渲染)",onClick:()=>void Qa(j.id)},A!==null&&A.id===j.id?"收起":"阅读"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void me(j.id)},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Z(j.id),"data-danger":"1"},"删除"))),A!==null&&A.id===j.id?s("div",{className:"ds-md-preview ds-reader"},A.busy?s("span",{className:"ds-hint"},"读取中…"):A.content.trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(A.content)):R!==null&&R[j.id]!==void 0?s("div",{className:"ds-md-preview ds-reader"},R[j.id].trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(R[j.id])):null)))),q===null?null:s("div",{className:"ds-hint",style:{marginTop:6}},q),B===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},B.id===void 0?"添加文档":`编辑 ${B.title}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"类型"),s(g,{className:"ds-select",value:B.kind,onChange:j=>J({...B,kind:j.target.value})},dx.map(j=>s("option",{key:j,value:j},tf[j])))),s("div",{className:"ds-field",style:{flex:"1 1 220px"}},s("div",{className:"ds-label"},"标题"),s(v,{className:"ds-input",value:B.title,placeholder:"例如 会员积分抵扣 PRD",onChange:j=>J({...B,title:j.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 110px"}},s("div",{className:"ds-label"},"版本"),s(v,{className:"ds-input",value:B.version,placeholder:"v1.0",onChange:j=>J({...B,version:j.target.value})}))),s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"1 1 auto"}},s("div",{className:"ds-label"},"飞书文档链接"),s(v,{className:"ds-input",value:B.url,placeholder:"https://<租户>.feishu.cn/wiki/…",onChange:j=>J({...B,url:j.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 auto",justifyContent:"flex-end"}},s("div",{className:"ds-label"}," "),s("button",{className:"ds-btn",disabled:G,onClick:()=>void _e()},G?"拉取中…":"从链接拉正文"))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"文档正文(拉不到就自己粘,研发和测试都读这一份)"),s(d,{className:"ds-textarea",rows:8,value:B.content,onChange:j=>J({...B,content:j.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{J(null),L(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:G,onClick:()=>void An()},"保存文档")))),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"需求变更影响面"),s("div",{className:"ds-pane-desc"},"需求一改,下面这些卡和用例就要重看。点一下现算,不改任何数据。")),s("div",{className:"ds-row",style:{gap:8,alignItems:"center",marginBottom:6}},s("button",{className:"ds-btn",disabled:Gt||pt===null,onClick:()=>void Ct()},Gt?"计算中…":"算一下影响面"),Ue===null?s("span",{className:"ds-hint"},"还没算过"):s("span",{className:"ds-hint"},`卡 ${Ue.summary.cards} · 用例 ${Ue.summary.cases}(能自动跑 ${Ue.summary.runnable})· 失败 ${Ue.summary.failing}`)),Ue===null||Ue.cards.length===0?null:s("div",{className:"ds-sec-body",style:{maxHeight:160,overflow:"auto"}},Ue.cards.slice(0,12).map(j=>s("div",{key:j.id,className:"ds-art-row"},s("span",{className:"ds-art-title"},(j.code===""?"":j.code+" ")+j.title),s("span",{className:"ds-hint"},j.status))),Ue.cards.length>12?s("div",{className:"ds-hint"},`还有 ${Ue.cards.length-12} 张`):null),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"功能点与可追溯矩阵"),s("div",{className:"ds-pane-desc"},"功能点是需求的骨架:一条功能点对若干张卡、若干条用例。卡和用例上都挂 pointId,这里就能看出哪条没人做、哪条没被验证。")),F===null?s("div",{className:"ds-hint",style:{padding:"6px 0"}},"还没有功能点。点「+ 加功能点」把需求拆成可交付、可验证的条目。"):s("div",{className:"ds-matrix"},s("div",{className:"ds-matrix-sum"},`功能点 ${F.summary.points} · 已覆盖 ${F.summary.coveredPoints}${F.summary.pointsWithoutCases>0?`(${F.summary.pointsWithoutCases} 条还没有用例,覆盖率 ${F.summary.coverageRate}%)`:""} · 卡 ${F.summary.cards}(未归点 ${F.summary.looseCards})· 用例 ${F.summary.cases}(通过 ${F.summary.pass}${F.summary.fail>0?`,失败 ${F.summary.fail}`:""})· 通过率 ${F.summary.passRate}%`),(F.points??[]).map(j=>s("div",{key:j.id,className:"ds-pt-row","data-covered":j.covered?"1":"0"},s("div",{className:"ds-pt-main"},s("div",{className:"ds-pt-title"},s("span",{className:"ds-chip-code"},j.id),s("b",null,j.title),s(y,{className:"ds-chip","data-pt":j.covered?"covered":"open"},j.covered?"已覆盖":"未覆盖")),String(j.description??"")===""?null:s("div",{className:"ds-req-desc ds-md"},f(j.description)),s("div",{className:"ds-pt-meta"},`卡 ${j.cards.length}${j.cards.length===0?"(还没人做)":":"+j.cards.map(ue=>ue.code||ue.id).join("、")}`,` · 用例 ${j.cases.total}(通过 ${j.cases.pass}${j.cases.fail>0?`,失败 ${j.cases.fail}`:""}${j.cases.pending>0?`,没跑 ${j.cases.pending}`:""})`,j.acceptance.length===0?"":` · 验收条件 ${j.acceptance.filter(ue=>ue.covered).length}/${j.acceptance.length}`)),s("div",{className:"ds-listrow-actions"},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>ge({...j,acceptanceText:(j.acceptance??[]).map(ue=>ue.text).join(`
|
|
45
|
+
`).map(At=>At.trim()).filter(At=>At!==""),ce={id:ie.id??`p${(Q.points??[]).length+1}`,title:ie.title,description:ie.description??"",priority:ie.priority??"normal",status:ie.status??"draft",acceptance:V.map((At,$a)=>({id:`a${$a+1}`,text:At,done:!1}))},Le=(Q.points??[]).filter(At=>At.id!==ce.id);await ne([...Le,ce])},se=async V=>{Q!==null&&await u("删除这个功能点?挂在上面的卡会变成「未归功能点」。",{title:"删除功能点",danger:!0,okText:"删除"})&&await ne((Q.points??[]).filter(ce=>ce.id!==V))},de=async(V,ce)=>{try{await i("/task/update",{id:V,pointId:ce}),Q!==null&&await Y(Q.id)}catch(Le){X(Le.message)}},Q=h.find(V=>V.id===S)??null,ve=V=>e.tasks.filter(ce=>ce.requirementId===V),Ce=V=>e.statuses.find(ce=>ce.id===V)?.label??V;o.useEffect(()=>{te(null),X(null),Q!==null&&Y(Q.id)},[Q===null?"":Q.id,Y]);const Se=async()=>{if(_!==null){if(String(_.title??"").trim()===""){H("需求标题不能为空");return}$(!0);try{const V=await i("/requirement/save",{requirement:{id:_.id,title:_.title,description:_.description,project:_.project,owner:_.owner,domain:_.domain??"",priority:_.priority,status:_.status,links:{prd:_.prd,proto:_.proto,acceptance:_.acceptance},discovery:_.discovery??{server:"",auth:"",boundary:""}}});C(V.requirement.id),I(null),H(null),l(V.created===!0?"需求已创建":"需求已保存")}catch(V){H(V.message)}finally{$(!1)}}},Pe=async V=>{if(Q!==null)try{await i("/requirement/save",{requirement:{...Q,links:Q.links??{},status:V}}),l("需求状态已更新")}catch(ce){H(ce.message)}},dt=async V=>{if(await u("删除这条需求?它下面的卡会保留,只是不再挂需求。",{title:"删除需求",danger:!0,okText:"删除"}))try{await i("/requirement/delete",{id:V}),C(null)}catch(ce){H(ce.message)}},rt={pending:"待评审",approved:"评审通过",changes_requested:"评审打回"},[Je,We]=o.useState(!1),[tt,Ge]=o.useState(null),Te=async(V,ce)=>{if(Q!==null){We(!0),Ge(null);try{const Le=await i("/requirement/review",{id:Q.id,decision:V,note:ce??"",by:c(),operator:c()});Ge(`已${V==="approve"?"通过":V==="reject"?"打回":"提交"}评审:${rt[Le.review?.status]??""}`),typeof a=="function"&&a()}catch(Le){Ge(Le.message)}finally{We(!1)}}},It=async()=>{const V=await p("打回要给意见(会记在评审记录里):",{title:"评审打回",danger:!0,okText:"打回",placeholder:"写清楚哪里不满足,之后要据此改"});if(V!==null){if(String(V).trim()===""){Ge("打回必须写意见");return}Te("reject",String(V).trim().slice(0,600))}},[Be,yt]=o.useState(!1),[Rt,Ut]=o.useState(null),le=async()=>{if(Q!==null){yt(!0),Ut(null);try{const ce=(await i("/requirement/split",{id:Q.id})).created??[];Ut(ce.length===0?"没有可拆的功能点:要么都覆盖了,要么还没加功能点。":`已建 ${ce.length} 张卡:${ce.map(Le=>Le.code||Le.id).join("、")}`),typeof a=="function"&&a()}catch(V){Ut(V.message)}finally{yt(!1)}}},Ne=()=>({kind:"prd",title:"",url:Q?.links?.prd??"",version:"v1.0",content:"",status:"draft"}),me=async V=>{if(L(null),V===null){J(Ne());return}try{const Le=((await i("/requirement/get",{id:Q.id})).requirement?.docs??[]).find(At=>At.id===V);J(Le===void 0?Ne():{...Le})}catch(ce){H(ce.message)}},Qa=async V=>{if(A!==null&&A.id===V){x(null);return}x({id:V,content:"",busy:!0});try{const Le=((await i("/requirement/get",{id:Q.id})).requirement?.docs??[]).find(At=>At.id===V);x({id:V,content:String(Le?.content??""),busy:!1})}catch(ce){H(ce.message),x(null)}},Et=async()=>{if(P!==null){K(null);return}try{const ce=(await i("/requirement/get",{id:Q.id})).requirement??{},Le=ce.baseline??null,$a=[...ce.docs??[]].sort((Dn,Fo)=>(Number(Fo.at)||0)-(Number(Dn.at)||0)),ra=$a.find(Dn=>Dn.kind==="prd")??$a[0]??null;if(Le===null){K(null),H("这条需求还没有基线,先挑一份文档「设为基线」");return}if(ra===null){K(null),H("这条需求还没有文档可以对比");return}const Ma=sw(Le.content??"",ra.content??"");K({rows:Ma,summary:rw(Ma),version:Le.version,docTitle:ra.title})}catch(V){K(null),H(V instanceof Error?V.message:String(V))}},fa=async()=>{if(R!==null){O(null);return}try{const V=await i("/requirement/get",{id:Q.id}),ce={};for(const Le of V.requirement?.docs??[])ce[Le.id]=String(Le.content??"");O(ce)}catch(V){H(V.message)}},An=async()=>{if(B!==null){if(String(B.title??"").trim()===""){L("文档要有标题");return}we(!0);try{const V=await i("/requirement/doc",{id:Q.id,action:B.id===void 0?"add":"update",docId:B.id,doc:{kind:B.kind,title:B.title,url:B.url,version:B.version,content:B.content,status:B.status}});J(null),L(null)}catch(V){L(V.message)}finally{we(!1)}}},La=async()=>{ae(!0),ye(null);try{const V=await i("/requirement/folder",{id:Q.id});ye(V.ok?`云盘文件夹已就绪:${V.folder?.url??""}`:`创建失败:${V.error||V.folder?.error||"未知原因"}`)}catch(V){ye(V.message)}finally{ae(!1)}},Lt=async()=>{ae(!0),ye(null);try{const ce=(await i("/requirement/sync",{id:Q.id})).report??{},Le=Number(ce.failed??0);ye(`归档完成:共 ${ce.total??0} 件,成功 ${ce.written??0} 件${Le>0?`,失败 ${Le} 件(${ce.errors?.[0]??""})`:"。"}`)}catch(V){ye(V.message)}finally{ae(!1)}},Mt=async V=>{try{await i("/requirement/doc",{id:Q.id,action:"baseline",docId:V,by:"人"}),L("这一版已生效,之后新建的卡都会记下它。")}catch(ce){L(ce.message)}},jn=async()=>{try{await i("/requirement/doc",{id:Q.id,action:"clear-baseline"}),L("基线已取消,需求回到「评审中」。")}catch(V){L(V.message)}},Z=async V=>{if(await u("删除这份文档?",{title:"删除文档",danger:!0,okText:"删除"}))try{await i("/requirement/doc",{id:Q.id,action:"remove",docId:V})}catch(ce){L(ce.message)}},_e=async()=>{we(!0),L(null);try{const V=await i("/requirement/pull",{url:B?.url??""});if(V.ok===!0){const ce=String(V.text??"");J(Le=>({...Le,title:String(Le?.title??"").trim()===""?String(V.title??""):Le.title,content:ce})),L(`已拉取 ${ce.length} 字正文${V.title?`,标题取自文档「${V.title}」`:""}。`)}else L(V.error??"拉取失败")}catch(V){L(V.message)}finally{we(!1)}},xt=_===null?null:s("div",{className:"ds-panel ds-form-panel"},s("div",{className:"ds-panel-head"},_.id===void 0?"新建需求":`编辑 ${Q?.code??""}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-field"},s("div",{className:"ds-label"},"需求标题"),s(v,{className:"ds-input",value:_.title,onChange:V=>I({..._,title:V.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"有没有服务端?数据存在哪、刷新会不会丢"),s(v,{className:"ds-input",placeholder:"例:有,node:sqlite;或「纯前端演示,刷新即还原」",value:_.discovery?.server??"",onChange:V=>I({..._,discovery:{..._.discovery??{},server:V.target.value}})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"鉴权谁做:前端隐藏按钮,还是服务端拒绝"),s(v,{className:"ds-input",placeholder:"例:服务端校验角色,viewer 调写接口直接 403",value:_.discovery?.auth??"",onChange:V=>I({..._,discovery:{..._.discovery??{},auth:V.target.value}})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"交付边界:只交付界面,还是点开就能用"),s(v,{className:"ds-input",placeholder:"例:前后端都交付,含测试;或「只做 UI,接口后续接」",value:_.discovery?.boundary??"",onChange:V=>I({..._,discovery:{..._.discovery??{},boundary:V.target.value}})})),s("div",{className:"ds-row"},s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务项目"),s(g,{className:"ds-select",value:_.project,onChange:V=>I({..._,project:V.target.value})},s("option",{value:""},"不指定"),b.map(V=>s("option",{key:V.id,value:V.id},V.name)))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"状态"),s(g,{className:"ds-select",value:_.status,onChange:V=>I({..._,status:V.target.value})},ef.map(V=>s("option",{key:V,value:V},Ar[V])))),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务负责人"),s(v,{className:"ds-input",value:_.owner,onChange:V=>I({..._,owner:V.target.value})})),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"业务领域"),s(v,{className:"ds-input",placeholder:"支付 / 风控 / 会员(决定派给哪位领域专家)",value:_.domain??"",onChange:V=>I({..._,domain:V.target.value})}))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"PRD 链接"),s(v,{className:"ds-input",placeholder:"https://…(飞书云文档)",value:_.prd,onChange:V=>I({..._,prd:V.target.value})})),s("div",{className:"ds-row"},s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"原型链接"),s(v,{className:"ds-input",value:_.proto,onChange:V=>I({..._,proto:V.target.value})})),s("div",{className:"ds-field",style:{flex:1}},s("div",{className:"ds-label"},"验收标准链接"),s(v,{className:"ds-input",value:_.acceptance,onChange:V=>I({..._,acceptance:V.target.value})}))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"需求正文 / PRD 摘要"),s(d,{className:"ds-textarea",rows:10,style:{minHeight:180},placeholder:"支持 Markdown:`# 背景`、`- 列表`、`**重点**`、`` `代码` ``、`| 表格 |`;保存后在「需求明细」里按 Markdown 渲染,不再截断。",value:_.description,onChange:V=>I({..._,description:V.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{I(null),H(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:M,onClick:()=>void Se()},M?"保存中…":"保存需求")))),ht=Q===null?s("div",{className:"ds-empty"},"左边选一条需求,或先新建一条。"):(()=>{const V=ve(Q.id),ce=V.filter(j=>j.status==="done").length,Le=V.filter(j=>j.blocked!==void 0&&j.blocked!==null).length,At=V.filter(j=>j.type==="bug"&&j.status!=="done").length,$a=V.reduce((j,ue)=>{const Fe=ed(ue);return{done:j.done+Fe.done,total:j.total+Fe.total}},{done:0,total:0}),ra=V.reduce((j,ue)=>{const Fe=Jl(ue);return{pass:j.pass+Fe.pass,total:j.total+Fe.total}},{pass:0,total:0}),Ma=V.flatMap(j=>(j.artifacts??[]).map(ue=>({...ue,task:j}))),Dn=V.filter(j=>(j.gitlabMR?.iid??0)>0),Fo=ra.pass,On=Q.links??{},dn=Q.folder??{},Ln=(j,ue)=>ue===""?null:s("a",{className:"ds-link",href:ue,target:"_blank",rel:"noreferrer"},j);return s("div",{className:"ds-req-body"},s("div",{className:"ds-req-headline"},s("div",null,s("div",{className:"ds-req-code"},Q.code||Q.id),s("div",{className:"ds-req-name"},Q.title)),s("div",{className:"ds-req-actions"},s(g,{className:"ds-select",value:Q.status,onChange:j=>void Pe(j.target.value)},ef.map(j=>s("option",{key:j,value:j},Ar[j]))),s("button",{className:"ds-btn",onClick:()=>I({...Q,...On})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void dt(Q.id),"data-danger":"1"},"删除"))),(()=>{const j=Q.review??null,ue=j===null?"还没提交评审":rt[j.status]??j.status,Fe=j===null||j.status==="changes_requested",wt=j!==null&&j.status==="pending";return s("div",{className:"ds-panel","data-review":j===null?"none":j.status,style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求评审"),s(y,{className:"ds-chip","data-on":j?.status==="approved"?"1":"0",style:{marginLeft:8}},ue),j===null||j.at===0?null:s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ke(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},j!==null&&String(j.note??"")!==""?s("div",{className:"ds-hint",style:{marginBottom:8}},`评审意见:${j.note}`):null,s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},Fe?s("button",{className:"ds-btn ds-btn-primary",disabled:Je,onClick:()=>void Te("submit")},Je?"提交中…":"提交评审"):null,wt?s("button",{className:"ds-btn ds-btn-primary",disabled:Je,onClick:()=>void Te("approve")},"评审通过"):null,wt?s("button",{className:"ds-btn ds-btn-ghost",disabled:Je,onClick:It,"data-danger":"1"},"打回"):null,j?.status==="approved"?s("span",{className:"ds-hint"},"评审已通过,可以拆卡和派活了。"):null),tt===null?null:s("div",{className:"ds-hint",style:{marginTop:8}},tt)))})(),Ln("PRD",On.prd)||Ln("原型",On.proto)||Ln("验收标准",On.acceptance)?s("div",{className:"ds-req-links"},Ln("PRD 文档",On.prd),Ln("原型",On.proto),Ln("验收标准",On.acceptance)):s("div",{className:"ds-empty"},[s("span",null,"还没有登记 PRD / 原型 / 验收标准链接;PRD 与原型由产品在澄清后输出,可在这里补,或点右边「让产品补 PRD」。"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:async()=>{const j=await p("想请产品补什么?",{title:"申请产品补 PRD",defaultValue:"需要产品输出 PRD 与原型"});if(j!==null)try{await i("/requirement/request-prd",{id:Q.id,by:c(),note:j}),l("已申请产品补 PRD"),a()}catch(ue){l(ue.message??String(ue),"error")}}},"让产品补 PRD")]),Q.description?s("div",{className:"ds-panel ds-req-desc-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求明细"),Q.source==="business"?s("span",{className:"ds-hint","data-src":"business"},`业务提单 · ${Q.requester?.name||"未署名"}${Q.requester?.dept?"("+Q.requester.dept+")":""} · ${Ke(Q.requester?.at||Q.createdAt)}`):null,s("span",{className:"ds-hint",style:{marginLeft:"auto"}},"支持 Markdown")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-req-desc ds-md"},f(Q.description)))):s("div",{className:"ds-req-desc ds-hint"},"还没有写需求描述:产品背景、要解决的问题、验收口径都建议写在这里,点右侧「编辑」补上。"),(()=>{const j=(Q.history??[]).slice().reverse();if(j.length===0||(Q.change??null)!==null)return null;const ue={title:"标题",description:"需求明细",priority:"优先级",status:"状态",owner:"负责人",project:"项目",iteration:"迭代"};return s("div",{className:"ds-panel ds-history-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"变更记录"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`最近 ${j.length} 条`)),s("div",{className:"ds-panel-body"},s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:260,overflowY:"auto"}},j.map((Fe,wt)=>s("div",{key:`${Fe.at}-${wt}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint",style:{flex:"0 0 auto",fontVariantNumeric:"tabular-nums"}},Ke(Fe.at)),s("span",{style:{flex:"0 0 auto"}},Fe.by===""?"人":Fe.by),s("span",{className:"ds-chip",style:{flex:"0 0 auto"}},ue[Fe.field]??Fe.field),s("span",{className:"ds-hint",style:{minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}},`${Fe.from===""?"(空)":Fe.from} → ${Fe.to===""?"(空)":Fe.to}`))))))})(),(()=>{const j=Q.pool??{reach:0,impact:0,confidence:0,effort:0,score:0},ue=async(wt,ma,et)=>{const De=await p(ma,{title:wt,defaultValue:String(et??0)});if(De===null)return null;const Ye=Number(String(De).trim());return Number.isFinite(Ye)&&Ye>=0?Ye:null},Fe=async()=>{const wt=await ue("影响面 Reach","影响多少用户或多少笔交易?估算依据写进正文",j.reach);if(wt===null)return;const ma=await ue("影响度 Impact","3 显著 / 2 高 / 1 中 / 0.5 低",j.impact);if(ma===null)return;const et=await ue("置信度 Confidence","100 有数据 / 80 有访谈 / 50 只有猜测(百分数)",j.confidence);if(et===null)return;const De=await ue("成本 Effort","人日(研发+测试+联调)",j.effort);if(De!==null)try{await i("/requirement/pool",{id:Q.id,pool:{reach:wt,impact:ma,confidence:et,effort:De}}),l("需求池评分已更新"),await a()}catch(Ye){l(Ye instanceof Error?Ye.message:String(Ye),"error")}};return s("div",{className:"ds-panel ds-pool-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求池评分(RICE)"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},j.score>0?`得分 ${j.score}`:"还没打分:不打分就没法和别的需求比优先级"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void Fe()},j.score>0?"重新打分":"打分")),j.score>0?s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},s("span",{className:"ds-chip"},`影响面 ${j.reach}`),s("span",{className:"ds-chip"},`影响度 ${j.impact}`),s("span",{className:"ds-chip"},`置信度 ${j.confidence}%`),s("span",{className:"ds-chip"},`成本 ${j.effort} 人日`),s("span",{className:"ds-chip","data-on":"1"},`得分 ${j.score}`))):null)})(),(()=>{const j=Q.questions??[],ue=j.filter(De=>De.status==="open").length,Fe=async(De,Ye)=>{try{await i("/requirement/questions",{id:Q.id,...De}),Ye&&l(Ye),await a()}catch(Mn){l(Mn instanceof Error?Mn.message:String(Mn),"error")}},wt=async()=>{const De=await p("还没确认清楚的问题是什么?",{title:"加一个待确认问题",placeholder:"例如:退款失败时是否允许人工补发?"});De===null||De.trim()===""||await Fe({action:"add",text:De},"已加入待确认清单")},ma=async De=>{const Ye=await p(De.text,{title:"回答这个问题",defaultValue:De.answer??"",placeholder:"写清结论与依据"});Ye===null||Ye.trim()===""||await Fe({action:"answer",questionId:De.id,answer:Ye},"已回答")},et=async De=>{await u("删掉这个问题?",{title:"删除待确认项",danger:!0,okText:"删除"})===!0&&await Fe({action:"delete",questionId:De.id},"已删除")};return s("div",{className:"ds-panel ds-question-panel","data-open":ue>0?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"待确认清单"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},ue>0?`${ue} 个没回答,回答完才能拆卡`:j.length===0?"澄清阶段把不确定的先记在这里":"全部已回答"),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},onClick:()=>void wt()},"+ 加问题")),s("div",{className:"ds-panel-body"},j.length===0?s("div",{className:"ds-empty"},"还没有待确认的问题。产品澄清时把「需要业务拍板」的点写在这里,避免猜业务规则。"):s("div",{style:{display:"flex",flexDirection:"column",gap:8}},j.map(De=>s("div",{key:De.id,className:"ds-question-row","data-status":De.status,style:{display:"flex",gap:10,alignItems:"flex-start",padding:"8px 10px",border:"1px solid var(--border)",borderRadius:8,background:"var(--card)"}},s("span",{className:"ds-chip","data-on":De.status==="answered"?"1":"0"},De.status==="answered"?"已回答":"待回答"),s("div",{style:{flex:"1 1 auto",minWidth:0}},s("div",{style:{fontSize:13}},De.text),De.answer===""||De.answer===void 0?null:s("div",{className:"ds-hint",style:{marginTop:4}},`答:${De.answer}`)),s("div",{style:{display:"flex",gap:6,flex:"0 0 auto"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void ma(De)},De.status==="answered"?"改答案":"回答"),s("button",{className:"ds-btn ds-btn-ghost","data-danger":"1",onClick:()=>void et(De)},"删除")))))))})(),(()=>{const j=Q.change??null;if(j===null)return null;const ue=j.affected??[],Fe=j.affectedCases??[],wt=new Map(V.map(et=>[et.id,et])),ma=et=>e.statuses.find(De=>De.id===et)?.label??et;return s("div",{className:"ds-panel ds-change-panel","data-invalidated":j.invalidated?"1":"0",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,j.invalidated?"变更影响(签字已作废)":"变更影响"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${Ke(j.at)}${j.by===""?"":` · ${j.by}`}`)),s("div",{className:"ds-panel-body"},function(){const et=(Q.history??[]).slice().reverse();if(et.length===0)return null;const De={title:"标题",description:"需求明细",priority:"优先级",status:"状态",owner:"负责人",project:"项目",iteration:"迭代"};return s("div",{style:{display:"flex",flexDirection:"column",gap:6,maxHeight:220,overflowY:"auto",marginBottom:8,paddingBottom:8,borderBottom:"1px dashed var(--border)"}},s("div",{className:"ds-hint",style:{fontWeight:600}},`变更记录 · 最近 ${et.length} 条`),et.map((Ye,Mn)=>s("div",{key:`${Ye.at}-${Mn}`,style:{display:"flex",gap:8,alignItems:"baseline",fontSize:12}},s("span",{className:"ds-hint"},Ke(Ye.at)),s("span",null,Ye.by===""?"人":Ye.by),s("span",{className:"ds-chip"},De[Ye.field]??Ye.field),s("span",{className:"ds-hint"},`${Ye.from===""?"(空)":Ye.from} → ${Ye.to===""?"(空)":Ye.to}`))))}(),s("div",{className:"ds-hint"},`改了:${(j.parts??[]).join("、")}。${ue.length===0?"当时没有已产出的卡受影响,不用返工。":`${ue.length} 张卡已经产出过东西${Fe.length===0?"":`、${Fe.length} 条用例要重跑`},要按新版复核:`}`),ue.length===0?null:s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8,marginTop:8}},ue.map(et=>s("span",{key:et.id,className:"ds-chip","data-pt":"open",style:{cursor:"pointer"},title:et.title,onClick:()=>{const De=wt.get(et.id);De!==void 0&&typeof t=="function"&&t(De)}},`${et.code||et.id} ${ma(et.status)}`))),j.invalidated?s("div",{className:"ds-hint",style:{marginTop:8}},"验收签字已清空:这些卡是按旧版做的,复核完重新签一次。"):null))})(),(()=>{const j=Q.deploys??[];return j.length===0?null:s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"上线足迹"),s("span",{className:"ds-hint",style:{marginLeft:"auto"}},"来自版本的部署记录,只留痕不触发")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:8}},j.map(ue=>s("span",{key:ue.id,className:"ds-chip","data-env":ue.kind,title:`${ue.releaseName??""} · ${Ke(ue.at)} · ${ue.by}`},`${ue.name} · ${Xa(ue.at)}`)))))})(),s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"依赖与排序")),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row"},s("span",{className:"ds-hint"},"排序号"),s(v,{className:"ds-input",style:{width:90},value:Ve,onChange:j=>Ya(j.target.value)}),s("span",{className:"ds-hint"},"被依赖的需求没交付,这条需求的准出会被拦")),s("div",{className:"ds-row",style:{flexWrap:"wrap",gap:10}},h.filter(j=>j.id!==Q.id).map(j=>s("label",{key:j.id,className:"ds-check-row"},s(k,{checked:Rn.includes(j.id),onChange:ue=>U(Fe=>ue.target.checked?[...Fe,j.id]:Fe.filter(wt=>wt!==j.id))}),s("span",{className:"ds-check-text"},`${j.code||j.id} ${j.title}(${Ar[j.status]??j.status})`))),h.length<=1?s("span",{className:"ds-hint"},"只有这一条需求,暂时没有可依赖的对象。"):null),s("div",{className:"ds-row"},s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void Ea()},"保存排序与依赖"),he===null?null:s("span",{className:"ds-hint"},he)))),(()=>{const j=Q.progress??null,ue=j?.gate??{ready:!1,blockers:["还没有加载出准出状态,稍后自动刷新"],warnings:[]},Fe=ue.stats??{},wt=Q.signoff??null,ma=j?.progress??(V.length===0?0:Math.round(ce/V.length*100)),et=Fe.coveredPoints??0,De=Fe.points??(Q.points??[]).length,Ye=Fe.pass??Fo,Mn=Fe.cases??ra.total;return s("div",{className:"ds-panel",style:{marginTop:14}},s("div",{className:"ds-panel-head"},s("span",null,"需求准出与验收签字"),s(y,{className:"ds-chip","data-pt":ue.ready?"covered":"open",style:{marginLeft:"auto"}},ue.ready?"可以交付":`还差 ${ue.blockers.length} 项`)),s("div",{className:"ds-panel-body"},s("div",{className:"ds-check-progress"},s("div",{className:"ds-check-bar","data-full":ma===100?"1":"0"},s("span",{style:{width:`${ma}%`}})),s("div",{className:"ds-hint"},`${ma}% · ${ce}/${V.length} 张卡完成 · 功能点覆盖 ${et}/${De} · 用例通过 ${Ye}/${Mn}`)),(ue.blockers??[]).map(zn=>s("div",{key:zn,className:"ds-gate-item","data-kind":"block"},`⛔ ${zn}`)),(ue.warnings??[]).map(zn=>s("div",{key:zn,className:"ds-gate-item","data-kind":"warn"},`⚠ ${zn}`)),ue.ready?s("div",{className:"ds-hint"},"门禁已通过:签完字这条需求就算交付。"):null,wt===null?s("div",{className:"ds-gate-sign"},s(v,{className:"ds-input",style:{flex:1,minWidth:220},placeholder:"验收结论:交付了什么、遗留什么",value:W,onChange:zn=>Ee(zn.target.value)}),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void hs("sign")},"需求验收签字")):s("div",{className:"ds-gate-sign"},s("div",{className:"ds-hint",style:{flex:1}},`✍ ${wt.by} 于 ${Ke(wt.at)} 签字:${wt.note}`),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void hs("clear")},"取消签字")),ke===null?null:s("div",{className:"ds-hint"},ke)))})(),s("div",{className:"ds-sec-head",style:{marginTop:16}},"交付物归档(飞书云盘)",s("div",{className:"ds-req-actions"},dn.url?s("a",{className:"ds-link",href:dn.url,target:"_blank",rel:"noreferrer"},"打开文件夹"):null,s("button",{className:"ds-btn ds-btn-ghost",disabled:He,onClick:()=>void La()},dn.url?"校验文件夹":"创建文件夹"),s("button",{className:"ds-btn",disabled:He||Ma.length===0,onClick:()=>void Lt()},He?"归档中…":`归档 ${Ma.length} 件交付物`))),s("div",{className:"ds-hint"},dn.url?`文件夹:${dn.name||dn.token} 已归档 ${Ma.filter(j=>j.doc?.url).length}/${Ma.length} 件交付物`:dn.error?`上次创建失败:${dn.error}`:"还没有云盘文件夹;建好后,这条需求下每件交付物都会写成文件夹里的一份 Markdown 文档。"),fe?s("div",{className:"ds-hint"},fe):null,s("div",{className:"ds-sec-head",style:{marginTop:16}},"文档与基线",s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void me(null)},"+ 添加文档")),Q.baseline?s("div",{className:"ds-base-box"},s("div",{className:"ds-base-main"},s("b",null,`当前基线 ${Q.baseline.version}`),s("span",null,`${Q.baseline.title} · 生效于 ${Ke(Q.baseline.at)} · ${Q.baseline.by||"人"} · 正文 ${Q.baseline.contentLength??0} 字`)),s("button",{className:"ds-btn ds-btn-ghost",title:"把当前文档和这份基线逐行比一遍(只读)",onClick:()=>void Et()},P===null?"对比基线":"收起对比"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void jn()},"取消基线")):s("div",{className:"ds-base-box","data-empty":"1"},s("div",{className:"ds-base-main"},s("b",null,"还没有基线"),s("span",null,"PRD 评审通过后点「设为基线」,之后建的卡都会记下用的是哪一版。"))),Q.baseline!==null&&P!==null?s("div",{className:"ds-diff"},s("div",{className:"ds-diff-head"},s("b",null,`基线 ${P.version} → 当前:${P.docTitle}`),s("span",{className:"ds-hint"},P.summary.added+P.summary.removed===0?"正文没有变化":`+${P.summary.added} 行 / −${P.summary.removed} 行`)),s("div",{className:"ds-diff-body"},P.rows.slice(0,400).map((j,ue)=>s("div",{key:ue,className:"ds-diff-line","data-kind":j.kind},`${j.kind==="add"?"+":j.kind==="del"?"−":" "} ${j.text}`)),P.rows.length>400?s("div",{className:"ds-hint"},`(还有 ${P.rows.length-400} 行未显示)`):null)):null,(Q.docs??[]).length===0?s("div",{className:"ds-hint",style:{marginTop:8}},"还没有登记文档。可以先加 PRD 链接,把正文粘进来,评审通过后设为基线。"):s("div",null,s("div",{className:"ds-row",style:{marginTop:8,gap:8,alignItems:"center"}},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void fa()},R===null?"全部展开":"全部收起"),s("span",{className:"ds-hint"},`共 ${(Q.docs??[]).length} 篇,展开后可直接读 Markdown 正文`)),s("div",{className:"ds-doc-list"},(Q.docs??[]).map(j=>s("div",{key:j.id,className:"ds-doc-wrap"},s("div",{className:"ds-doc-row"},s(y,{className:"ds-chip","data-doc":j.kind},tf[j.kind]??j.kind),s("div",{className:"ds-doc-main"},s("div",{className:"ds-doc-title"},j.url===""?j.title:s("a",{className:"ds-link",href:j.url,target:"_blank",rel:"noreferrer"},j.title),s("span",{className:"ds-chip-code"},j.version),s(y,{className:"ds-chip","data-docstatus":j.status},cx[j.status]??j.status)),s("div",{className:"ds-doc-meta"},`正文 ${j.contentLength??0} 字 · ${j.by||"—"} · ${j.at>0?Ke(j.at):"—"}`)),s("div",{className:"ds-listrow-actions"},j.status==="baseline"?null:s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Mt(j.id)},"设为基线"),s("button",{className:"ds-btn ds-btn-ghost",title:"直接读正文(Markdown 渲染)",onClick:()=>void Qa(j.id)},A!==null&&A.id===j.id?"收起":"阅读"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void me(j.id)},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void Z(j.id),"data-danger":"1"},"删除"))),A!==null&&A.id===j.id?s("div",{className:"ds-md-preview ds-reader"},A.busy?s("span",{className:"ds-hint"},"读取中…"):A.content.trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(A.content)):R!==null&&R[j.id]!==void 0?s("div",{className:"ds-md-preview ds-reader"},R[j.id].trim()===""?s("span",{className:"ds-hint"},"这条文档还没有正文。"):f(R[j.id])):null)))),q===null?null:s("div",{className:"ds-hint",style:{marginTop:6}},q),B===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},B.id===void 0?"添加文档":`编辑 ${B.title}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"类型"),s(g,{className:"ds-select",value:B.kind,onChange:j=>J({...B,kind:j.target.value})},dx.map(j=>s("option",{key:j,value:j},tf[j])))),s("div",{className:"ds-field",style:{flex:"1 1 220px"}},s("div",{className:"ds-label"},"标题"),s(v,{className:"ds-input",value:B.title,placeholder:"例如 会员积分抵扣 PRD",onChange:j=>J({...B,title:j.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 110px"}},s("div",{className:"ds-label"},"版本"),s(v,{className:"ds-input",value:B.version,placeholder:"v1.0",onChange:j=>J({...B,version:j.target.value})}))),s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"1 1 auto"}},s("div",{className:"ds-label"},"飞书文档链接"),s(v,{className:"ds-input",value:B.url,placeholder:"https://<租户>.feishu.cn/wiki/…",onChange:j=>J({...B,url:j.target.value})})),s("div",{className:"ds-field",style:{flex:"0 0 auto",justifyContent:"flex-end"}},s("div",{className:"ds-label"}," "),s("button",{className:"ds-btn",disabled:G,onClick:()=>void _e()},G?"拉取中…":"从链接拉正文"))),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"文档正文(拉不到就自己粘,研发和测试都读这一份)"),s(d,{className:"ds-textarea",rows:8,value:B.content,onChange:j=>J({...B,content:j.target.value})})),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>{J(null),L(null)}},"取消"),s("button",{className:"ds-btn ds-btn-primary",disabled:G,onClick:()=>void An()},"保存文档")))),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"需求变更影响面"),s("div",{className:"ds-pane-desc"},"需求一改,下面这些卡和用例就要重看。点一下现算,不改任何数据。")),s("div",{className:"ds-row",style:{gap:8,alignItems:"center",marginBottom:6}},s("button",{className:"ds-btn",disabled:Gt||pt===null,onClick:()=>void Ct()},Gt?"计算中…":"算一下影响面"),Ue===null?s("span",{className:"ds-hint"},"还没算过"):s("span",{className:"ds-hint"},`卡 ${Ue.summary.cards} · 用例 ${Ue.summary.cases}(能自动跑 ${Ue.summary.runnable})· 失败 ${Ue.summary.failing}`)),Ue===null||Ue.cards.length===0?null:s("div",{className:"ds-sec-body",style:{maxHeight:160,overflow:"auto"}},Ue.cards.slice(0,12).map(j=>s("div",{key:j.id,className:"ds-art-row"},s("span",{className:"ds-art-title"},(j.code===""?"":j.code+" ")+j.title),s("span",{className:"ds-hint"},j.status))),Ue.cards.length>12?s("div",{className:"ds-hint"},`还有 ${Ue.cards.length-12} 张`):null),s("div",{className:"ds-pane-head",style:{marginTop:16}},s("div",{className:"ds-pane-title"},"功能点与可追溯矩阵"),s("div",{className:"ds-pane-desc"},"功能点是需求的骨架:一条功能点对若干张卡、若干条用例。卡和用例上都挂 pointId,这里就能看出哪条没人做、哪条没被验证。")),F===null?s("div",{className:"ds-hint",style:{padding:"6px 0"}},"还没有功能点。点「+ 加功能点」把需求拆成可交付、可验证的条目。"):s("div",{className:"ds-matrix"},s("div",{className:"ds-matrix-sum"},`功能点 ${F.summary.points} · 已覆盖 ${F.summary.coveredPoints}${F.summary.pointsWithoutCases>0?`(${F.summary.pointsWithoutCases} 条还没有用例,覆盖率 ${F.summary.coverageRate}%)`:""} · 卡 ${F.summary.cards}(未归点 ${F.summary.looseCards})· 用例 ${F.summary.cases}(通过 ${F.summary.pass}${F.summary.fail>0?`,失败 ${F.summary.fail}`:""})· 通过率 ${F.summary.passRate}%`),(F.points??[]).map(j=>s("div",{key:j.id,className:"ds-pt-row","data-covered":j.covered?"1":"0"},s("div",{className:"ds-pt-main"},s("div",{className:"ds-pt-title"},s("span",{className:"ds-chip-code"},j.id),s("b",null,j.title),s(y,{className:"ds-chip","data-pt":j.covered?"covered":"open"},j.covered?"已覆盖":"未覆盖")),String(j.description??"")===""?null:s("div",{className:"ds-req-desc ds-md"},f(j.description)),s("div",{className:"ds-pt-meta"},`卡 ${j.cards.length}${j.cards.length===0?"(还没人做)":":"+j.cards.map(ue=>ue.code||ue.id).join("、")}`,` · 用例 ${j.cases.total}(通过 ${j.cases.pass}${j.cases.fail>0?`,失败 ${j.cases.fail}`:""}${j.cases.pending>0?`,没跑 ${j.cases.pending}`:""})`,j.acceptance.length===0?"":` · 验收条件 ${j.acceptance.filter(ue=>ue.covered).length}/${j.acceptance.length}`)),s("div",{className:"ds-listrow-actions"},s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>ge({...j,acceptanceText:(j.acceptance??[]).map(ue=>ue.text).join(`
|
|
46
46
|
`)})},"编辑"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>void se(j.id),"data-danger":"1"},"删除")))),(F.loose??[]).length===0?null:s("div",{className:"ds-pt-loose"},s("div",{className:"ds-hint",style:{marginBottom:6}},`还有 ${F.loose.length} 张卡没归到功能点,选一个挂上去,矩阵才算完整:`),F.loose.map(j=>s("div",{key:j.id,className:"ds-pt-loose-row"},s("span",{className:"ds-chip-code"},j.code||j.id),s("span",{className:"ds-pt-loose-title"},j.title),s(g,{className:"ds-select",style:{width:220},value:"",onChange:ue=>{ue.target.value!==""&&de(j.id,ue.target.value)}},s("option",{value:""},"挂到…"),(F.points??[]).map(ue=>s("option",{key:ue.id,value:ue.id},`${ue.id} ${ue.title}`))))))),s("div",{className:"ds-row",style:{gap:8,marginTop:10,alignItems:"center"}},s("button",{className:"ds-btn",onClick:()=>ge(ee())},"+ 加功能点"),s("button",{className:"ds-btn ds-btn-primary",disabled:Be,onClick:()=>void le()},Be?"拆卡中…":"按未覆盖的功能点拆卡"),s("button",{className:"ds-btn ds-btn-ghost",onClick:()=>{Q!==null&&Y(Q.id)}},"刷新矩阵"),Rt===null?null:s("span",{className:"ds-hint"},Rt),xe===null?null:s("span",{className:"ds-hint"},xe)),ie===null?null:s("div",{className:"ds-panel ds-form-panel",style:{marginTop:10}},s("div",{className:"ds-panel-head"},ie.id===void 0?"新增功能点":`编辑功能点 ${ie.id}`),s("div",{className:"ds-panel-body"},s("div",{className:"ds-field"},s("div",{className:"ds-label"},"功能点标题"),s(v,{className:"ds-input",value:ie.title,placeholder:"例如 结算页展示可用积分",onChange:j=>ge({...ie,title:j.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"说明"),s(d,{className:"ds-textarea",rows:3,value:ie.description??"",onChange:j=>ge({...ie,description:j.target.value})})),s("div",{className:"ds-field"},s("div",{className:"ds-label"},"验收条件(一行一条,做成什么样算完成)"),s(w,{className:"ds-textarea",rows:4,value:ie.acceptanceText??"",onChange:j=>ge({...ie,acceptanceText:j.target.value})})),s("div",{className:"ds-row",style:{gap:10}},s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"优先级"),s(g,{className:"ds-select",value:ie.priority,onChange:j=>ge({...ie,priority:j.target.value})},s("option",{value:"urgent"},"紧急"),s("option",{value:"normal"},"普通"),s("option",{value:"relaxed"},"可缓"))),s("div",{className:"ds-field",style:{flex:"0 0 150px"}},s("div",{className:"ds-label"},"状态"),s(g,{className:"ds-select",value:ie.status,onChange:j=>ge({...ie,status:j.target.value})},s("option",{value:"draft"},"草稿"),s("option",{value:"ready"},"就绪"),s("option",{value:"done"},"已完成")))),s("div",{className:"ds-row",style:{justifyContent:"flex-end",gap:8}},s("button",{className:"ds-btn",onClick:()=>ge(null)},"取消"),s("button",{className:"ds-btn ds-btn-primary",onClick:()=>void be()},"保存功能点")))),s("div",{className:"ds-req-stats"},s("div",{className:"ds-stat-box"},s("b",null,`${ce}/${V.length}`),s("span",null,"卡已完成")),s("div",{className:"ds-stat-box","data-warn":Le>0?"1":"0"},s("b",null,String(Le)),s("span",null,"阻塞")),s("div",{className:"ds-stat-box","data-warn":At>0?"1":"0"},s("b",null,String(At)),s("span",null,"未结缺陷")),s("div",{className:"ds-stat-box"},s("b",null,`${$a.done}/${$a.total}`),s("span",null,"验收清单")),s("div",{className:"ds-stat-box"},s("b",null,`${ra.pass}/${ra.total}`),s("span",null,"用例通过")),s("div",{className:"ds-stat-box"},s("b",null,String(Ma.length)),s("span",null,"交付物")),s("div",{className:"ds-stat-box"},s("b",null,String(Dn.length)),s("span",null,"MR"))),s("div",{className:"ds-sec-head",style:{marginTop:14}},`这张需求下的 ${V.length} 张卡`),V.length===0?s("div",{className:"ds-empty"},"还没有挂上来的卡。建卡时选择这条需求,编号会自动分配。"):s("div",{className:"ds-req-tasks"},V.map(j=>s("div",{key:j.id,className:"ds-listrow ds-req-task",onClick:()=>t(j.id)},s("div",{className:"ds-listrow-main"},s("div",{className:"ds-req-task-title"},j.code?s(y,{className:"ds-chip ds-chip-code"},j.code):null,s("span",null,j.title)),s("div",{className:"ds-hint"},`${Ce(j.status)}${(()=>{const ue=j.expert===""?"":(e.config.experts??[]).find(Fe=>Fe.id===j.expert)?.name??j.expert;return ue===""?"":` · ${ue}`})()}${j.dueAt>0?` · 截止 ${Xa(j.dueAt)}`:""}`)),s("div",{className:"ds-listrow-actions"},j.blocked!==void 0&&j.blocked!==null?s(y,{className:"ds-chip ds-chip-blocked"},"⛔"):null,j.type==="bug"?s(y,{className:"ds-chip ds-chip-bug","data-sev":j.severity??"p2"},"BUG"):null)))),Dn.length>0?s("div",null,s("div",{className:"ds-sec-head",style:{marginTop:14}},"关联 MR"),Dn.map(j=>s("div",{key:j.id,className:"ds-hint"},`${j.code||j.id} · !${j.gitlabMR.iid} ${j.gitlabMR.title??""}(${j.gitlabMR.state||"open"})`))):null)})();return s("div",{className:"ds-body"},z!==null?s("div",{className:"ds-err",style:{marginBottom:10}},z):null,xt,s("div",{className:"ds-req-grid"},s("div",{className:"ds-intake-launch",style:{display:"flex",gap:8,padding:"0 0 8px"}},s("span",{className:"ds-hint"},"新建需求请用右上角「+ 新建」")),s("div",{className:"ds-req-list"},s("div",{className:"ds-req-list-head"},s("span",null,`需求 ${h.length}`),s("button",{className:"ds-btn ds-btn-ghost",style:{marginLeft:8},title:"按来源筛选:全部 / 业务提单 / 内部",onClick:()=>{const V=window.localStorage.getItem("dsh-studio:reqsrc")??"all";window.localStorage.setItem("dsh-studio:reqsrc",V==="all"?"business":V==="business"?"internal":"all"),window.dispatchEvent(new Event("resize"))}},(window.localStorage.getItem("dsh-studio:reqsrc")??"all")==="all"?"来源:全部":window.localStorage.getItem("dsh-studio:reqsrc")==="business"?"来源:业务提单":"来源:内部")),h.length===0?s("div",{className:"ds-empty"},"还没有需求。先建一条——后面所有的卡、用例、缺陷、MR 都会挂在它下面。"):((window.localStorage.getItem("dsh-studio:reqsort")??"score")==="score"?h.slice().sort((V,ce)=>(ce.pool?.score??0)-(V.pool?.score??0)):h).map(V=>{const ce=window.localStorage.getItem("dsh-studio:reqsrc")??"all";if(ce!=="all"&&(V.source==="business"?"business":"internal")!==ce)return null;const Le=ve(V.id),At=Le.filter(Ma=>Ma.status==="done").length,$a=V.progress?.progress??(Le.length===0?0:Math.round(At/Le.length*100)),ra=V.progress?.gate??null;return s("div",{key:V.id,className:"ds-req-item","data-on":V.id===S?"1":"0",onClick:()=>C(V.id)},s("div",{className:"ds-req-item-title"},V.title),s("div",{className:"ds-req-item-meta"},s(y,{className:"ds-chip ds-chip-code"},V.code||"未编号"),V.source==="business"?s(y,{className:"ds-chip","data-src":"business",title:[V.requester?.name,V.requester?.dept].filter(Boolean).join(" · ")||"业务提单"},"业务提单"):null,s(y,{className:"ds-chip","data-req":V.status},Ar[V.status]??V.status),s("span",{className:"ds-hint"},`${At}/${Le.length} 卡`),ra===null?null:s("span",{className:"ds-hint",style:{color:ra.ready?"#0f9d58":"#b26a00"}},ra.ready?"准出通过":`还差 ${ra.blockers.length} 项`)),s("div",{className:"ds-check-bar","data-full":$a===100?"1":"0",style:{marginTop:2}},s("span",{style:{width:`${$a}%`}})))})),s("div",{className:"ds-req-detail"},ht)))}function iw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,toast:s,confirmDialog:o,promptDialog:i,tipOf:l,TextField:c,ChipField:u,CheckField:p,SelectField:f,MdField:v}=vt(),[w,y]=n.useState([]),[k,g]=n.useState(null),[d,h]=n.useState(null),[b,S]=n.useState(null),[C,E]=n.useState(""),[_,I]=n.useState(null),[z,H]=n.useState(""),[M,$]=n.useState(!1),[B,J]=n.useState(""),[q,L]=n.useState(""),[A,x]=n.useState(""),[P,K]=n.useState("10"),[R,O]=n.useState(""),[G,we]=n.useState(""),fe=n.useCallback(async W=>{const ke=(await r("/releases")).releases??[];y(ke);const Ae=W??(ke.some(Ya=>Ya.id===k)?k:ke[0]?.id??null);if(g(Ae),Ae===null){h(null);return}const Ve=await r("/release/get",{id:Ae});h(Ve),S({id:Ve.release.id,version:Ve.release.version,name:Ve.release.name,project:Ve.release.project,owner:Ve.release.owner,status:Ve.release.status,plannedAt:Ve.release.plannedAt,tagName:Ve.release.tagName??"",commitSha:Ve.release.commitSha??""}),E(Ve.release.notes??"")},[k]);n.useEffect(()=>{let W=!0;return fe().then(()=>{W&&I(null)}).catch(Ee=>{W&&I(Ee instanceof Error?Ee.message:String(Ee))}),()=>{W=!1}},[e.revision,fe]);async function ye(W){g(W),$(!0);try{const Ee=await r("/release/get",{id:W});h(Ee),S({id:Ee.release.id,version:Ee.release.version,name:Ee.release.name,project:Ee.release.project,owner:Ee.release.owner,status:Ee.release.status,plannedAt:Ee.release.plannedAt,tagName:Ee.release.tagName??"",commitSha:Ee.release.commitSha??""}),E(Ee.release.notes??""),H("")}catch(Ee){I(Ee instanceof Error?Ee.message:String(Ee))}finally{$(!1)}}function He(){g(null),h(null),S({id:"",version:"",name:"",project:"",owner:"",status:"draft",plannedAt:0,tagName:"",commitSha:""}),E(""),H("")}async function ae(W,Ee,ke){$(!0),I(null);try{const Ae=await r(W,Ee);H(ke??"已保存"),s(ke??"已保存"),s(ke??"已保存"),await fe(Ae.release?.id??k)}catch(Ae){const Ve=Ae instanceof Error?Ae.message:String(Ae);I(Ve),s(Ve,"error")}finally{$(!1)}}if(_!==null&&w.length===0)return a("div",{className:"ds-body"},a("div",{className:"ds-err"},_));const F=d?.gate,te=d?.release,xe=d?.tasks??[],X=e.tasks.filter(W=>W.releaseId===""||W.releaseId===void 0),ie=a("div",{className:"ds-req-list"},a("div",{className:"ds-row",style:{justifyContent:"space-between",marginBottom:6}},a("span",{className:"ds-hint"},`${w.length} 个版本`),a("button",{className:"ds-btn",onClick:He},"+ 新建版本")),w.length===0?a("div",{className:"ds-empty"},"还没有版本。先建一个版本,再把卡挂上来。"):w.map(W=>a("div",{key:W.id,className:"ds-req-item","data-on":k===W.id?"1":"0",onClick:()=>void ye(W.id)},a("div",{className:"ds-req-item-title"},`${W.version||W.id} ${W.name}`.trim()),a("div",{className:"ds-req-item-meta"},a(u,{className:"ds-chip","data-kind":W.status},af[W.status]??W.status),a(u,{className:"ds-chip"},`${W.gate.stats.done}/${W.gate.stats.tasks} 卡`),W.gate.blockers.length===0?a(u,{className:"ds-chip",style:{color:"#0f9d58",borderColor:"#0f9d5855"}},"门禁通过"):a(u,{className:"ds-chip ds-chip-blocked"},`${W.gate.blockers.length} 项拦着`))))),ge=b===null?a("div",{className:"ds-hint"},"左边选一个版本,或者新建一个。"):a("div",{className:"ds-req-detail"},(te?.requirements??[]).length===0?null:a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginBottom:8}},a("span",{className:"ds-hint"},`这个版本涉及 ${te.requirements.length} 条需求:`),te.requirements.map(W=>a("span",{key:W.id,className:"ds-chip",title:W.title},`${W.code||W.id} ${W.title}(${Ar[W.derived]??W.derived})`))),a("div",{className:"ds-req-headline"},a("div",null,a("div",{className:"ds-req-name"},`${b.version||"未编号"} ${b.name}`.trim()),a("div",{className:"ds-hint"},te==null?"新版本:先填版本号与名字,保存后可以把卡挂进来":`创建于 ${Xa(te.createdAt)}${te.releasedAt>0?` · 发布于 ${Xa(te.releasedAt)}`:""}${te.approveBy!==""?` · 审批人 ${te.approveBy}`:""}`)),a("div",{className:"ds-req-actions"},a(f,{className:"ds-select",style:{width:108},value:b.status,onChange:W=>S({...b,status:W.target.value})},ux.map(W=>a("option",{key:W,value:W},af[W]))))),a("div",{className:"ds-panel",style:{marginTop:4}},a("div",{className:"ds-panel-head"},"版本信息"),a("div",{className:"ds-panel-body ds-grid2"},a("label",{className:"ds-field"},a("span",{},"版本号"),a(c,{className:"ds-input",value:b.version,placeholder:"v1.3.0",disabled:te?.status==="released",onChange:W=>S({...b,version:W.target.value})})),a("label",{className:"ds-field"},a("span",{},"版本名"),a(c,{className:"ds-input",value:b.name,placeholder:"积分抵扣首个版本",onChange:W=>S({...b,name:W.target.value})})),a("label",{className:"ds-field"},a("span",{},"业务项目"),a(f,{className:"ds-select",value:b.project,onChange:W=>S({...b,project:W.target.value})},a("option",{value:""},"不属于任何项目"),(e.config.projects??[]).map(W=>a("option",{key:W.id,value:W.id},W.name)))),a("label",{className:"ds-field"},a("span",{},"发布负责人"),a(c,{className:"ds-input",value:b.owner,onChange:W=>S({...b,owner:W.target.value})})),a("label",{className:"ds-field"},a("span",{},"计划发布时间"),a(c,{className:"ds-input",type:"date",value:nf(b.plannedAt),onChange:W=>S({...b,plannedAt:jr(W.target.value)})})),a("label",{className:"ds-field"},a("span",{},"发布标签(tag)"),a(c,{className:"ds-input",value:b.tagName??"",placeholder:"v1.0.0",onChange:W=>S({...b,tagName:W.target.value})})),a("label",{className:"ds-field"},a("span",{},"对应提交(sha,选填)"),a(c,{className:"ds-input",value:b.commitSha??"",placeholder:"a1b2c3d",onChange:W=>S({...b,commitSha:W.target.value})})),a("div",{className:"ds-row",style:{alignItems:"flex-end",justifyContent:"flex-end",gap:8}},a("button",{className:"ds-btn ds-btn-primary",disabled:M,onClick:()=>void ae("/release/save",{release:b},"版本已保存")},"保存"),b.id===""?null:a("button",{className:"ds-btn ds-btn-ghost",disabled:M,style:{color:"var(--destructive)"},onClick:async()=>{await o("删除这个版本?卡会保留,只是脱离版本。",{title:"删除版本",danger:!0,okText:"删除"})&&ae("/release/delete",{id:b.id},"版本已删除")}},"删除")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布门禁",a("span",{className:"ds-hint"},"卡全做完、没阻塞、缺陷关闭、用例没有失败,才算能发")),a("div",{className:"ds-panel-body"},a("div",{className:"ds-req-stats"},a("div",{className:"ds-stat-box"},a("b",{},String(F?.stats.done??0)),a("span",{},"已完成卡")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(F?.stats.open??0)>0?"1":"0"},String(F?.stats.open??0)),a("span",{},"未完成")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(F?.stats.blocked??0)>0?"1":"0"},String(F?.stats.blocked??0)),a("span",{},"阻塞")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(F?.stats.bugs??0)>0?"1":"0"},String(F?.stats.bugs??0)),a("span",{},"未结缺陷")),a("div",{className:"ds-stat-box"},a("b",{"data-warn":(F?.stats.failing??0)>0?"1":"0"},String(F?.stats.failing??0)),a("span",{},"失败用例")),a("div",{className:"ds-stat-box"},a("b",{},String(F?.stats.artifacts??0)),a("span",{},"交付物"))),(F?.blockers??[]).map((W,Ee)=>a("div",{key:`b${Ee}`,className:"ds-error"},`⛔ ${W}`)),(F?.warnings??[]).map((W,Ee)=>a("div",{key:`w${Ee}`,className:"ds-hint"},`⚠ ${W}`)),a("div",{className:"ds-action-bar"},a("div",{className:"ds-action-bar-left"},a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/notes",{id:b.id},"发布说明已生成")},"生成发布说明"),a("button",{className:"ds-btn",disabled:M||!F?.ready,onClick:()=>void ae("/release/approve",{id:b.id,by:"人"},"已审批,可以发布了")},"审批通过")),a("div",{className:"ds-action-bar-right"},a("button",{className:"ds-btn ds-btn-primary",disabled:M||!F?.ready||te.status==="released",onClick:()=>void ae("/release/publish",{id:b.id,by:"人"},"已发布")},te.status==="released"?"已发布":"发布"),l("发布准入(预发部署 / 需求签字)没过时也要发:必须写原因,会记进部署日志",a("button",{className:"ds-btn ds-btn-ghost",disabled:M||!F?.ready||te.status==="released","data-force":"1",onClick:async()=>{const W=await i("强制发布的原因(会记进部署日志,便于复盘):",{title:"强制发布",okText:"强制发布",placeholder:"为什么不等预发部署 / 需求签字?"});if(W!==null){if(String(W).trim()===""){H("强制发布必须写原因");return}ae("/release/publish",{id:b.id,by:"人",force:!0,forceNote:String(W).trim()},"已强制发布(原因已记入部署日志)")}}},"强制发布")),te.status==="released"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:async()=>{const W=await i("回滚原因(要进复盘记录):",{title:"回滚这个版本",danger:!0,okText:"回滚"});W!==null&&W.trim()!==""&&ae("/release/rollback",{id:b.id,reason:W},"已回滚")}},"回滚"):null)),te.rollbackReason===""?null:a("div",{className:"ds-error"},`回滚原因:${te.rollbackReason}`))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布冒烟清单",a("span",{className:"ds-hint"},`${(te.smoke??[]).filter(W=>W.done).length}/${(te.smoke??[]).length} 项已过 · 发布前逐条点过,门禁才算过`)),a("div",{className:"ds-panel-body"},(te.smoke??[]).length===0?a("div",{className:"ds-empty"},"还没有冒烟项:把发布前必验的几条写在这里,比如「能登录」「下单支付成功」「首页不报错」。"):null,(te.smoke??[]).map(W=>a("div",{key:W.id,className:"ds-listrow"},a("label",{className:"ds-row",style:{gap:8,alignItems:"center",flex:1,cursor:"pointer"}},a(p,{checked:W.done===!0,onChange:()=>void ae("/release/smoke",{id:b.id,action:"toggle",itemId:W.id,done:W.done!==!0,by:"人"},W.done?"已取消":"冒烟项已过")}),a("span",{className:W.done?"ds-smoke-done":""},W.text)),W.done&&W.by!==""?a("span",{className:"ds-hint"},`${W.by} · ${Xa(W.at)}`):null,a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/smoke",{id:b.id,action:"remove",itemId:W.id},"已删除")},"×")))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(c,{className:"ds-input",style:{flex:1},value:q,placeholder:"发布前必须验证什么?",onChange:W=>L(W.target.value)}),a("button",{className:"ds-btn",disabled:M||q.trim()==="",onClick:()=>{const W=q.trim();W!==""&&(L(""),ae("/release/smoke",{id:b.id,action:"add",text:W},"已加冒烟项"))}},"+ 加一条")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"审批、灰度与部署记录",a("span",{className:"ds-hint"},`审批 ${(te.approvals??[]).length} 条 · 灰度 ${(te.grayBatches??[]).length} 批`)),a("div",{className:"ds-panel-body"},(te.approvals??[]).length===0?a("div",{className:"ds-empty"},"还没有审批记录:点上面的「审批通过」,系统会把谁批的、什么结论记在这里。"):(te.approvals??[]).map(W=>a("div",{key:W.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${W.by} 审批通过`),a("div",{className:"ds-hint"},W.note||"(没写结论)")),a("span",{className:"ds-hint"},Xa(W.at)))),a("div",{style:{borderTop:"1px solid var(--ds-line)",margin:"10px 0"}}),(te.grayBatches??[]).length===0?a("div",{className:"ds-hint"},"没有灰度批次:发布就是一次性全量。先加一批 10% 跑一轮再全量,出事好收。"):(te.grayBatches??[]).map(W=>a("div",{key:W.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${W.name} · ${W.percent}%`),a("div",{className:"ds-hint"},`${{pending:"待开始",running:"灰度中",passed:"已通过",rolled_back:"已回滚"}[W.status]??W.status}${W.note===""?"":` · ${W.note}`}`)),a("div",{className:"ds-listrow-actions"},W.status==="pending"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/gray",{id:b.id,action:"start",batchId:W.id,by:"人"},"已开始灰度")},"开始"):null,W.status==="running"?a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/gray",{id:b.id,action:"advance",batchId:W.id,by:"人"},"这一批已通过")},"通过"):null,W.status==="passed"?a(u,{className:"ds-chip","data-on":"1"},"已通过"):null,W.status==="rolled_back"?a(u,{className:"ds-chip","data-sla":"overdue"},"已回滚"):null))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(c,{className:"ds-input",style:{flex:2},value:A,placeholder:"批次名,例如 内测用户",onChange:W=>x(W.target.value)}),a(c,{className:"ds-input",style:{flex:"0 0 88px"},type:"number",min:1,max:100,value:P,onChange:W=>K(W.target.value)}),a("button",{className:"ds-btn",disabled:M||Number(P)<=0,onClick:async()=>{await ae("/release/gray",{id:b.id,action:"add",name:A,percent:Number(P),by:"人"},"已加一批灰度"),x(""),K("10")}},"+ 加一批")),a("div",{style:{borderTop:"1px solid var(--ds-line)",margin:"10px 0"}}),a("div",{className:"ds-hint"},"上过哪些环境(只登记,不触发部署)"),(te.deploys??[]).length===0?null:a("div",{className:"ds-row",style:{flexWrap:"wrap",gap:6,marginTop:6}},te.deploys.map(W=>a("span",{key:W.id,className:"ds-chip","data-env":W.kind,"data-on":W.at===0?"0":"1",title:W.at===0?`还没登记部署到「${W.name}」`:`${W.name}:${Xa(W.at)} ${W.action} · ${W.by}`},W.at===0?`${W.name} —`:`${W.name} ✓`))),a("div",{className:"ds-row",style:{gap:8,marginTop:8}},a(f,{className:"ds-input",style:{flex:"0 0 148px"},value:R,onChange:W=>O(W.target.value)},a("option",{value:""},"选环境…"),(e.config?.environments??[]).map(W=>a("option",{key:W.id,value:W.id},W.name))),a(c,{className:"ds-input",style:{flex:1},value:G,placeholder:"备注:谁发的、发了什么(可留空)",onChange:W=>we(W.target.value)}),a("button",{className:"ds-btn",disabled:M||R==="",onClick:async()=>{await ae("/release/deploy",{id:b.id,env:R,note:G,by:"人"},"已登记部署(没有触发任何部署)"),we("")}},"登记部署")),a("div",{className:"ds-hint",style:{marginTop:10}},"部署与灰度记录(最近 8 条)"),(()=>{const W={pending:"待开始",running:"灰度中",passed:"已通过",rolled_back:"已回滚"},Ee=[...(te.deployLog??[]).map(ke=>({at:ke.at,kind:"deploy",by:ke.by,text:`${ke.action}${ke.env===""?"":` · ${(e.config?.environments??[]).find(Ae=>Ae.id===ke.env)?.name??ke.env}`}`,note:ke.note})),...(te.grayBatches??[]).map(ke=>({at:ke.at??ke.createdAt??0,kind:"gray",by:ke.by??"人",text:`灰度 ${ke.name} ${ke.percent}%`,note:W[ke.status]??ke.status}))].filter(ke=>ke.at).sort((ke,Ae)=>ke.at-Ae.at).slice(-8).reverse();return Ee.length===0?a("div",{className:"ds-empty"},"还没有部署或灰度记录。"):Ee.map(ke=>a("div",{key:`${ke.at}-${ke.kind}-${ke.text}`,className:"ds-hint"},a("span",{className:"ds-chip",style:{marginRight:6}},ke.kind==="gray"?"灰度":"部署"),`${Xa(ke.at)} · ${ke.text} · ${ke.by}${ke.note===""||ke.note===void 0?"":`:${ke.note}`}`))})())),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"这个版本里的卡",a("span",{className:"ds-hint"},`${xe.length} 张`)),a("div",{className:"ds-panel-body"},xe.length===0?a("div",{className:"ds-empty"},"还没有卡。从下面挑,或者在卡片详情里选「所属版本」。"):xe.map(W=>a("div",{key:W.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${W.code?`${W.code} `:""}${W.title}`),a("div",{className:"ds-hint"},`${(e.statuses??[]).find(Ee=>Ee.id===W.status)?.label??W.status}${W.blocked!==null&&W.blocked!==void 0?" · 阻塞":""}${W.type==="bug"?" · 缺陷":""}`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(W.id)},"打开"),a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void ae("/release/link",{id:b.id,taskId:W.id,action:"remove"},"已从版本移除")},"移除")))),a("div",{className:"ds-row",style:{marginTop:8}},a(f,{className:"ds-select",style:{flex:1},value:B,onChange:W=>J(W.target.value)},a("option",{value:""},`挑一张卡挂进来(${X.length} 张可选)`),X.map(W=>a("option",{key:W.id,value:W.id},`${W.code?`${W.code} `:""}${W.title}`))),a("button",{className:"ds-btn",disabled:M||B==="",onClick:async()=>{await ae("/release/link",{id:b.id,taskId:B,action:"add"},"已挂到这个版本"),J("")}},"挂进来")))),te==null?null:a("div",{className:"ds-panel",style:{marginTop:12}},a("div",{className:"ds-panel-head"},"发布说明",a("button",{className:"ds-btn ds-btn-ghost",disabled:M,onClick:()=>void navigator.clipboard?.writeText(C).then(()=>H("发布说明已复制"),()=>{})},"复制")),a("div",{className:"ds-panel-body"},a(v,{className:"ds-input",rows:12,value:C,placeholder:"点「生成发布说明」按类型自动汇总;之后可以在这里手动补充",onChange:W=>E(W.target.value),onBlur:()=>{C!==(te.notes??"")&&ae("/release/save",{release:{...b,notes:C}},"发布说明已保存")}}))),_===null?null:a("div",{className:"ds-error",style:{marginTop:8}},_),z===""?null:a("div",{className:"ds-hint",style:{marginTop:8}},z));return a("div",{className:"ds-body"},a("div",{className:"ds-req-grid"},ie,ge))}function lw({state:e,onOpenTask:t}){const{h:a,React:n,api:r,SelectField:s}=vt(),[o,i]=n.useState(30),[l,c]=n.useState(""),[u,p]=n.useState(null),[f,v]=n.useState(null),[w,y]=n.useState(null),[k,g]=n.useState(null),[d,h]=n.useState(null),[b,S]=n.useState(null);if(n.useEffect(()=>{let x=!0;const P=`/analytics?days=${o}${l===""?"":`&project=${encodeURIComponent(l)}`}`;r(P).then(R=>{x&&(p(R.analytics),v(null))}).catch(R=>{x&&v(R instanceof Error?R.message:String(R))});const K=l===""?"":`&project=${encodeURIComponent(l)}`;return Promise.all([r("/workload"),r(`/burndown?days=14${K}`),r("/costs"),r("/quality")]).then(([R,O,G,we])=>{x&&(y(R.workload??null),g(O.burndown??null),h({...G.costs??{},budget:G.budget??null,usage:G.usage??null}),S(we.quality??null))}).catch(()=>{}),()=>{x=!1}},[o,l,e.revision]),f!==null)return a("div",{className:"ds-body"},a("div",{className:"ds-err"},f));if(u===null)return a("div",{className:"ds-loading"},"正在统计…");const C=x=>`${Math.round((x??0)*100)}%`,E=x=>{const P=Number(x)||0;return P<=0?"0":P*1440<60?`${Math.max(1,Math.round(P*1440))} 分钟`:P<1?`${(P*24).toFixed(1)} 小时`:`${P.toFixed(1)} 天`},_=(x,P,K,R)=>a("div",{className:"ds-kpi","data-warn":R===!0?"1":"0"},a("div",{className:"ds-kpi-value"},P),a("div",{className:"ds-kpi-label"},x),a("div",{className:"ds-kpi-hint"},K)),I=(u.cycle.buckets??[]).reduce((x,P)=>x+P.count,0),z=Math.max(1,...(u.throughput??[]).map(x=>x.count),...(u.cycle.buckets??[]).map(x=>x.count)),H=Math.max(.001,...(u.columns??[]).map(x=>x.avgDays)),M=(u.columns??[]).reduce((x,P)=>x===null||P.avgDays>x.avgDays?P:x,null),$=(u.throughput??[]).reduce((x,P)=>x+P.count,0),B=(w?.experts??[]).filter(x=>x.enabled),J=B.reduce((x,P)=>x+P.open,0),q=B.reduce((x,P)=>x+(P.overdue??0),0),L=k?.series??[],A=L.length===0?null:L[L.length-1];return a("div",{className:"ds-body"},a("div",{className:"ds-board-bar"},a("span",{className:"ds-hint"},`统计口径:最近 ${u.days} 天 · ${u.scope.tasks} 张卡(${u.scope.done} 张已完成)`),a(s,{className:"ds-select",value:l,style:{width:"auto",minWidth:130},onChange:x=>c(x.target.value)},a("option",{value:""},"全部项目"),(e.config.projects??[]).map(x=>a("option",{key:x.id,value:x.id},`${x.key?`[${x.key}] `:""}${x.name}`))),a(s,{className:"ds-select",value:String(o),style:{width:"auto",minWidth:96},onChange:x=>i(Number(x.target.value))},a("option",{value:"7"},"近 7 天"),a("option",{value:"30"},"近 30 天"),a("option",{value:"90"},"近 90 天"))),a("div",{className:"ds-an-sec"},"质量",a("span",{className:"ds-hint"},"先看这四个数:返工多不多、多久能做完、AI 交的东西有多少被人验收")),a("div",{className:"ds-kpi-grid"},_("一次通过率",C(u.rework.firstPassRate),"完成的卡里,从没被退回过的比例",u.rework.firstPassRate<.6),_("返工率",C(u.rework.rate),`${u.rework.reworked}/${u.rework.moved} 张卡倒退过`,u.rework.rate>.3),_("中位周期",E(u.cycle.medianDays),`平均 ${E(u.cycle.avgDays)} · P90 ${E(u.cycle.p90Days)}`),_("AI 交付物采纳率",C(u.ai.acceptedRate),`${u.ai.accepted}/${u.ai.artifactsAi} 件已被验收 · 占全部交付物 ${C(u.ai.aiShare)}`)),a("div",{className:"ds-an-sec"},"流程与产能",a("span",{className:"ds-hint"},"完成的卡各花了多久 · 时间都卡在哪一列 · 最近每天完成几张")),a("div",{className:"ds-an-grid"},a("div",{className:"ds-span-4",style:{display:"grid",gap:12,gridTemplateRows:"auto 1fr",alignContent:"stretch"}},a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"周期分布",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${I} 张已完成`)),a("div",{className:"ds-panel-body"},(u.cycle.buckets??[]).map(x=>a("div",{key:x.id,className:"ds-an-row"},a("span",{className:"ds-an-name"},x.label),a("div",{className:"ds-an-track","data-empty":x.count===0?"1":"0"},a("div",{className:"ds-an-fill",style:{width:`${Math.round(x.count/z*100)}%`}})),a("span",{className:"ds-an-num"},`${x.count}`))),I===0?a("div",{className:"ds-hint"},"这个区间里还没有完成的卡。"):null,a("div",{className:"ds-an-foot"},`平均 ${E(u.cycle.avgDays)} · 中位 ${E(u.cycle.medianDays)} · P90 ${E(u.cycle.p90Days)}`))),a("div",{className:"ds-panel"},a("div",{className:"ds-panel-head"},"近 14 天完成"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-spark"},(u.throughput??[]).map((x,P)=>a("div",{key:x.day,className:"ds-spark-col",title:`${x.day}:完成 ${x.count} 张`},a("div",{className:"ds-spark-bar",style:{height:`${Math.round(x.count/Math.max(1,Math.max(...u.throughput.map(K=>K.count)))*100)}%`}}),a("span",{className:"ds-spark-day"},P%2===0?x.day:"")))),a("div",{className:"ds-an-foot"},`近 14 天合计 ${$} 张 · 日均 ${($/14).toFixed(1)} 张 · 空着的日子是没卡完成,不是没数据`)))),a("div",{className:"ds-panel ds-span-8"},a("div",{className:"ds-panel-head"},"每列停留(平均)"),a("div",{className:"ds-panel-body"},(u.columns??[]).map(x=>a("div",{key:x.status,className:"ds-an-row"},a("span",{className:"ds-an-name"},x.label),a("div",{className:"ds-an-track","data-empty":x.avgDays===0?"1":"0"},a("div",{className:"ds-an-fill ds-an-fill-warn",style:{width:`${Math.round(x.avgDays/H*100)}%`}})),a("span",{className:"ds-an-num"},E(x.avgDays)))),a("div",{className:"ds-an-foot"},M===null?"还没有数据。":`最慢的是「${M.label}」:平均 ${E(M.avgDays)},时间主要压在这一列。`)))),a("div",{className:"ds-an-sec"},"交付、成本与返工",a("span",{className:"ds-hint"},"AI 交的东西被验收多少 · 谁手上压得最多 · 派单花了多少 · 哪些卡来回折腾")),a("div",{className:"ds-an-grid"},a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"按专家看质量",a("span",{className:"ds-hint"},"派单量来自运行日志(内存最近 300 条)")),a("div",{className:"ds-panel-body"},b===null?a("div",{className:"ds-hint"},"还没算出来"):(b.experts??[]).length===0?a("div",{className:"ds-hint"},"还没有派出过活"):a("div",null,(b.experts??[]).slice(0,8).map(x=>a("div",{className:"ds-an-kv",key:x.expert},a("span",{},`${x.label} · ${x.dispatched} 次`),a("b",{"data-warn":x.reworkRate!==null&&x.reworkRate>=40?"1":"0"},`${x.done}/${x.cards} 完成 · 一次过 ${x.firstPassRate===null?"—":x.firstPassRate+"%"}`))),a("div",{className:"ds-hint",style:{marginTop:6}},`合计:派单 ${b.totals.dispatched} 次 · 平均耗时 ${b.totals.avgMs>0?Math.round(b.totals.avgMs/1e3)+" 秒":"—"} · 返工率 ${b.totals.reworkRate===null?"—":b.totals.reworkRate+"%"} · AI 失败 ${b.totals.failures} 次 · 缺陷重开 ${b.totals.reopens} 次`),a("div",{className:"ds-hint"},b.cost?.note??"")))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"AI 产出质量"),a("div",{className:"ds-panel-body"},a("div",{className:"ds-an-kv"},a("span",{},"交付物由专家登记"),a("b",{},`${u.ai.artifactsAi} / ${u.ai.artifactsTotal}`)),a("div",{className:"ds-an-kv"},a("span",{},"其中被人验收"),a("b",{},`${u.ai.accepted}(${C(u.ai.acceptedRate)})`)),a("div",{className:"ds-an-kv"},a("span",{},"缺正文(只有标题)"),a("b",{"data-warn":u.ai.emptyBody>0?"1":"0"},`${u.ai.emptyBody}`)),a("div",{className:"ds-an-kv"},a("span",{},"写码卡有执行记录"),a("b",{},`${u.ai.codingTasks}`)),a("div",{className:"ds-an-kv"},a("span",{},"评审通过 / 要求修改"),a("b",{},`${u.ai.codingApproved} / ${u.ai.codingChanges}`)),a("div",{className:"ds-an-kv"},a("span",{},"单测通过"),a("b",{},`${u.ai.unitPassed}/${u.ai.unitTests}(${C(u.ai.unitPassRate)})`)),a("div",{className:"ds-an-kv"},a("span",{},"已合并"),a("b",{},`${u.ai.merged}`)))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"团队负载",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${w?.busy??0} 位在跑`)),a("div",{className:"ds-panel-body"},(w?.experts??[]).filter(x=>x.enabled).length===0?a("div",{className:"ds-empty"},"还没有启用的专家。"):(w?.experts??[]).filter(x=>x.enabled).map(x=>a("div",{key:x.id,className:"ds-an-row"},a("span",{className:"ds-an-name"},`${x.emoji} ${x.name}`),a("div",{className:"ds-an-track"},a("div",{className:"ds-an-fill",style:{width:`${Math.round(x.open/Math.max(1,...(w.experts??[]).map(P=>P.open))*100)}%`}})),a("span",{className:"ds-an-num"},`在办 ${x.open}${x.running>0?` · 跑 ${x.running}`:""}${x.overdue>0?` · 逾期 ${x.overdue}`:""}`))),a("div",{className:"ds-an-foot"},`${B.length} 位在岗,手上合计 ${J} 张${q>0?`,其中 ${q} 张逾期`:",没有逾期的"}`))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"燃尽(近 14 天剩余)",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`现在还剩 ${k?.now?.remaining??0} 张`)),a("div",{className:"ds-panel-body"},a(nw,{series:k?.series??[]}),a("div",{className:"ds-an-foot"},A===null?"还没有数据。":`累计建 ${A.created} · 累计完成 ${A.done} · 现在还剩 ${A.remaining} 张`))),a("div",{className:"ds-panel ds-span-4"},a("div",{className:"ds-panel-head"},"AI 执行成本",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`${d?.total??0} 次派单`)),a("div",{className:"ds-panel-body"},d?.usage?.totals===null||d?.usage?.totals===void 0?null:a("div",{className:"ds-an-kv"},a("span",{},"token 用量"),a("b",{},(d.usage.totals.total/1e3).toFixed(1)+"k"),a("span",{className:"ds-hint",style:{marginLeft:8}},"入 "+(d.usage.totals.input/1e3).toFixed(1)+"k · 出 "+(d.usage.totals.output/1e3).toFixed(1)+"k · 缓存 "+(d.usage.totals.cacheRead/1e3).toFixed(1)+"k")),d?.usage?.totals===null||d?.usage?.totals===void 0?null:a("div",{className:"ds-an-kv"},a("span",{},"预估花费"),a("b",{},d.usage.totals.cost===null?"未知(价目表没配)":"¥"+Number(d.usage.totals.cost).toFixed(2)),(d.usage.totals.unknownCalls??0)>0?a("span",{className:"ds-hint",style:{marginLeft:8}},d.usage.totals.unknownCalls+" 次无价目"):null),a("div",{className:"ds-an-kv"},a("span",{},"平均执行时长"),a("b",{},`${Math.round((d?.avgMs??0)/1e3)} 秒`)),(d?.byModel??[]).slice(0,3).map(x=>a("div",{key:`m-${x.key}`,className:"ds-an-kv"},a("span",{},`模型 ${x.key}`),a("b",{},`${x.runs} 次`))),(d?.byPromptVersion??[]).slice(0,3).map(x=>a("div",{key:`p-${x.key}`,className:"ds-an-kv"},a("span",{},`提示词 ${x.key}`),a("b",{},`${x.runs} 次`))),(d?.usage?.unpricedModels??[]).length===0?null:a("div",{className:"ds-hint"},"未配价目的模型:"+d.usage.unpricedModels.map(x=>x.model+" ×"+x.calls).join("、")),a("div",{className:"ds-hint"},d?.note??""),d?.budget===null||d?.budget===void 0?null:a("div",{style:{marginTop:8}},a("div",{className:"ds-an-kv"},a("span",{},"预算使用"),a("b",{style:{color:d.budget.level==="over"?"#e5484d":d.budget.level==="warn"?"#d97706":void 0}},`${Math.max(d.budget.runRate,d.budget.hourRate)}%`)),a(aw,{value:Math.min(100,Math.max(d.budget.runRate,d.budget.hourRate)),tone:d.budget.level}),a("div",{className:"ds-hint"},`${d.budget.runs}/${d.budget.budgetRuns} 次 · ${d.budget.hours}/${d.budget.budgetHours} 小时 · ${d.budget.hint}`)))),a("div",{className:"ds-panel ds-span-8"},a("div",{className:"ds-panel-head"},"返工最多的卡",a("span",{className:"ds-hint",style:{marginLeft:"auto"}},`评审打回 ${u.rework.reviewRework} 次 · 测试打回 ${u.rework.testRework} 次`)),a("div",{className:"ds-panel-body"},(u.rework.worst??[]).length===0?a("div",{className:"ds-hint"},"没有卡倒退过,挺好。"):u.rework.worst.map(x=>a("div",{key:x.id,className:"ds-listrow"},a("div",{className:"ds-listrow-main"},a("div",null,`${x.code===""?"":`${x.code} `}${x.title}`),a("div",{className:"ds-hint"},`倒退 ${x.backward} 次 · 现在在「${(e.statuses.find(P=>P.id===x.status)??{}).label??x.status}」`)),a("div",{className:"ds-listrow-actions"},a("button",{className:"ds-btn ds-btn-ghost",onClick:()=>t(x.id)},"打开"))))))))}function dw(e,t){const a="studio",n="/studio-api";let r=null;function s(Y){r=Y??null}function o(Y){return r===null?null:r[Y]??null}function i(Y,ne){if(ne==null)return ne;const ee=ne.props??{},be=ee.title!==void 0?ne:t(ne.type,{...ee,title:Y}),se=o("Tip");return se===null?be:t(se,{key:ne.key,label:Y},be)}const{TextField:l,TextAreaField:c,ChipField:u,CheckField:p,SelectField:f}=px({h:t,kit:o,React:e}),v=fx({h:t,kit:o}),w=Lx({h:t,React:e}),y=mx({h:t,ChipField:u}),k=gx({h:t,React:e,Card:y,TextField:l,SelectField:f,CheckField:p,toast:B});function g({rows:Y=6,value:ne,onChange:ee,placeholder:be,className:se,...de}){const[Q,ve]=e.useState(!1),Ce=String(ne??"");return t("div",{className:"ds-md-field"},t("div",{className:"ds-md-bar"},t("span",{className:"ds-hint"},"支持 Markdown:标题 / 列表 / 粗体 / 代码块 / 表格"),t("button",{className:"ds-btn ds-btn-ghost",type:"button",onClick:()=>ve(!Q)},Q?"回到编辑":"预览渲染")),Q?t("div",{className:"ds-md-preview ds-req-desc ds-md"},Ce.trim()===""?t("span",{className:"ds-hint"},"还没写内容。"):Ee(Ce)):t(c,{className:se??"ds-textarea",rows:Y,value:ne,onChange:ee,placeholder:be,...de}))}function d(Y,ne){const ee=t("div",{className:"ds-label",style:{cursor:"help"}},Y);return ne==null||ne===""?ee:i(ne,ee)}let h=null,b=null;function S(Y){h=Y,typeof b=="function"&&b(Y)}function C(Y,ne){const ee=ne??{};return o("DialogBox")===null?Promise.resolve(window.confirm(String(Y))):new Promise(se=>{S({kind:"confirm",title:ee.title??"确认操作",message:String(Y??""),danger:ee.danger===!0,okText:ee.okText??"确定",cancelText:ee.cancelText??"取消",resolve:se})})}function E(Y,ne){const ee=ne??{};return o("DialogBox")===null?Promise.resolve(window.prompt(String(Y),ee.defaultValue??"")):new Promise(se=>{S({kind:"prompt",title:ee.title??"需要填写",message:String(Y??""),value:String(ee.defaultValue??""),placeholder:ee.placeholder??"",okText:ee.okText??"确定",cancelText:"取消",resolve:se})})}function _(){const[Y,ne]=e.useState(null),[ee,be]=e.useState("");e.useEffect(()=>{const Q=ve=>{ne(ve),be(ve!==null&&ve.kind==="prompt"?ve.value:"")};return b=Q,h!==null&&Q(h),()=>{b===Q&&(b=null)}},[]);const se=o("DialogBox");if(Y===null||se===null)return null;const de=Q=>{const ve=Y.resolve;ne(null),h=null,ve(Q)};return t(se,{open:!0,title:Y.title,message:Y.message,danger:Y.danger===!0,okText:Y.okText,cancelText:Y.cancelText,input:Y.kind==="prompt",value:ee,placeholder:Y.placeholder,onChange:Q=>be(Q),onOk:()=>de(Y.kind==="prompt"?ee.trim():!0),onCancel:()=>de(Y.kind==="prompt"?null:!1)})}let I=0,z=[],H=[],M={text:"",at:0};function $(){const Y=z.slice();for(const ne of H.slice())ne(Y)}function B(Y,ne="success",ee=3200){const be=String(Y??"").trim();if(be==="")return null;const se=Date.now();if(M.text===be&&se-M.at<600)return null;M={text:be,at:se};const de=++I;return z=z.concat([{id:de,text:be,tone:ne}]),$(),window.setTimeout(()=>{J(de)},ee),de}function J(Y){const ne=z.filter(ee=>ee.id!==Y);ne.length!==z.length&&(z=ne,$())}const q={success:"✓",error:"!",info:"i"};function L(){const[Y,ne]=e.useState(()=>z.slice());return e.useEffect(()=>(H.push(ne),()=>{H=H.filter(ee=>ee!==ne)}),[]),Y.length===0?null:t("div",{className:"ds-toasts",role:"status","aria-live":"polite"},Y.map(ee=>t("div",{key:ee.id,className:"ds-toast","data-tone":ee.tone},t("span",{className:"ds-toast-icon","aria-hidden":"true"},q[ee.tone]??"i"),t("span",{className:"ds-toast-text"},ee.text),t("button",{type:"button",className:"ds-toast-close","aria-label":"关闭提示",onClick:()=>J(ee.id)},"×"))))}const A="dsh-studio:user";function x(){try{const Y=window.localStorage.getItem(A);if(typeof Y=="string"&&Y.trim()!=="")return Y.trim().slice(0,40)}catch{}return"人"}function P(Y){const ne=String(Y??"").trim().slice(0,40)||"人";try{window.localStorage.setItem(A,ne)}catch{}return ne}const K="dsh-studio:theme",R=[["light","☀ 明亮"],["dark","☾ 黑暗"],["system","跟随系统"]];function O(){return typeof window.matchMedia=="function"?window.matchMedia("(prefers-color-scheme: dark)"):null}function G(){try{const Y=window.getComputedStyle(document.body).backgroundColor,ne=/rgba?\(([^)]+)\)/.exec(String(Y??""));if(ne===null)return null;const ee=ne[1].split(",").map(ve=>Number(ve.trim())),[be,se,de,Q]=ee;return![be,se,de].every(ve=>Number.isFinite(ve))||Number.isFinite(Q)&&Q<.2?null:.2126*be+.7152*se+.0722*de<128?"dark":"light"}catch{return null}}function we(){try{const Y=window.localStorage.getItem(K);if(Y==="light"||Y==="dark"||Y==="system")return Y}catch{}return"system"}function fe(Y){const ne=Y==="light"||Y==="dark"?Y:"system";try{window.localStorage.setItem(K,ne)}catch{}return ne}const ye="dsh-studio:session",He="dsh-studio:auth-user";function ae(){try{const Y=window.localStorage.getItem(ye);if(typeof Y=="string"&&Y.trim()!=="")return Y.trim()}catch{}return""}function F(Y){try{Y===""||Y===null||Y===void 0?window.localStorage.removeItem(ye):window.localStorage.setItem(ye,String(Y))}catch{}}function te(){try{const Y=window.localStorage.getItem(He);if(typeof Y=="string"&&Y!==""){const ne=JSON.parse(Y);if(ne!==null&&typeof ne=="object")return ne}}catch{}return null}function xe(Y){try{Y==null?window.localStorage.removeItem(He):window.localStorage.setItem(He,JSON.stringify(Y))}catch{}return Y??null}async function X(Y,ne){const ee={"x-studio-user":encodeURIComponent(x())},be=ae();be!==""&&(ee["x-studio-session"]=be),ne!==void 0&&(ee["content-type"]="application/json");const se=ne===void 0?{method:"GET",headers:ee}:{method:"POST",headers:ee,body:JSON.stringify(ne)},de=await fetch(n+Y,se);let Q={};try{Q=await de.json()}catch{}if(!de.ok||Q.ok===!1)throw new Error(Q.error??`请求失败(HTTP ${de.status})`);return Q}const ie=`
|
|
47
47
|
html[data-dsh-theme="dark"] body { background: #0e1014 !important; color: #e6e8ee }
|
|
48
48
|
html[data-dsh-theme="dark"] [class*="frame"],
|
package/docs/compatibility.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
| 项 | 值 |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| dsh-studio | `package.json` `version: 0.1.
|
|
9
|
+
| dsh-studio | `package.json` `version: 0.1.19` |
|
|
10
10
|
| DeepSeek Harness | `0.2.2-rc.1`(`dsh --version`) |
|
|
11
11
|
| Node | `v24.22.2`(Linux 目标机);本机 Windows `v24.19.0` |
|
|
12
12
|
| 目标系统 | Ubuntu 24.04,`dsh web` 监听 `18080`,nginx `:80` 反代并注入 `dsh-auth-*` cookie |
|
package/docs/publish.md
CHANGED
|
@@ -151,6 +151,7 @@ token 只写进临时 npmrc(放在仓库外或 `tmp/` 且不提交),发完
|
|
|
151
151
|
3. **发版一致性用命令查,不靠记性**:`npm run verify:release`(= `node tools/verify-release.mjs`)比对五处——
|
|
152
152
|
package.json 版本、本地 tag、tag 里的 package.json、origin/main 与 origin 上的 tag、npm registry 的版本与 latest。
|
|
153
153
|
全绿输出「发版一致」,有不一致退出码 1。
|
|
154
|
+
**刚 publish 完那几分钟 registry 还看不到新版本**,这时用 `npm run verify:release -- --wait 180` 让它自己轮询等待,别把「还没同步」当成「没发」。
|
|
154
155
|
这条是因为 0.1.15/0.1.16 真的发到 npm 了、git 却没推上去:`git tag -f vX.Y.Z && git push ... | tail -1` 会把 push 的失败吞掉。
|
|
155
156
|
npm latest 滞后(刚发几分钟)只算 warn,不算失败。
|
|
156
157
|
4. 部署脚本退出码 0 **不等于服务活着**——它打印的 `state=` 要自己看一眼,再单独 `curl -o /dev/null -w '%{http_code}' <host>/studio-api/state` 确认 200(2026-11-13 有过一次 `state=404` 被当成成功,线上挂了十分钟)。
|
package/index.js
CHANGED
|
@@ -1555,7 +1555,10 @@ function serviceConflicts(state, task) {
|
|
|
1555
1555
|
|
|
1556
1556
|
/** 一卡一分支名:优先用卡号(DEV-001),没编号就用 id。 */
|
|
1557
1557
|
function worktreeBranch(task) {
|
|
1558
|
-
|
|
1558
|
+
// 卡号只在一条需求内唯一(两条需求都会拆出 REQ-001):带上 id 后半段,否则 git 不让同一分支挂两个 worktree
|
|
1559
|
+
const code = String(task?.code ?? '').trim(), id = String(task?.id ?? '').trim()
|
|
1560
|
+
const tail = id.replace(/[^a-z0-9]/gi, '').slice(-8)
|
|
1561
|
+
const label = code === '' ? id : `${code}-${tail}`
|
|
1559
1562
|
return `task/${label}`.slice(0, 120)
|
|
1560
1563
|
}
|
|
1561
1564
|
|
|
@@ -1582,9 +1585,17 @@ async function ensureWorktree(store, task, config) {
|
|
|
1582
1585
|
if (alive.ok && String(alive.stdout).includes(`worktree ${existing.path}`)) return existing
|
|
1583
1586
|
}
|
|
1584
1587
|
const branch = worktreeBranch(task)
|
|
1585
|
-
const path = join(worktreeRoot(config),
|
|
1588
|
+
const path = join(worktreeRoot(config), branch.replace(/^[a-z]+\//, ''))
|
|
1586
1589
|
await mkdir(dirname(path), { recursive: true })
|
|
1587
|
-
|
|
1590
|
+
// 基准分支没有显式配就用仓库当前检出的分支:默认写死 main 会在用 master 的仓库上直接失败
|
|
1591
|
+
// 配置里默认写的是 main(sanitizeProject 的默认值),但很多仓库还在用 master:
|
|
1592
|
+
// 先确认这个分支真的存在,不存在就退回仓库当前检出的分支。
|
|
1593
|
+
let base = String(service?.baseBranch ?? project.baseBranch ?? '').trim()
|
|
1594
|
+
const branchOk = async (name) => name !== '' && (await runCommand('git', ['-C', repo, 'rev-parse', '--verify', 'refs/heads/' + name], 20000)).ok
|
|
1595
|
+
if (!(await branchOk(base))) {
|
|
1596
|
+
const current = await runCommand('git', ['-C', repo, 'rev-parse', '--abbrev-ref', 'HEAD'], 20000)
|
|
1597
|
+
base = (current.ok ? String(current.stdout).trim() : '') || base || 'main'
|
|
1598
|
+
}
|
|
1588
1599
|
const branches = await runCommand('git', ['-C', repo, 'branch', '--list', branch], 20000)
|
|
1589
1600
|
const exists = branches.ok && String(branches.stdout).trim().length > 0
|
|
1590
1601
|
const added = exists
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marsken/dsh-studio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DSH Studio: role-based multi-agent workspace (product / architect / developer / QA) with a live work board, GitLab + Feishu connection settings, all configurable from the web GUI.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// 需求澄清的必答项:不问清楚就会出现「管理后台只交付了前端」这类返工。
|
|
2
|
+
// 纯函数,拆卡门禁和界面提示共用。
|
|
3
|
+
|
|
4
|
+
export const DISCOVERY_FIELDS = [
|
|
5
|
+
['server', '有没有服务端?数据存在哪、刷新会不会丢'],
|
|
6
|
+
['auth', '鉴权谁做:前端隐藏按钮,还是服务端拒绝'],
|
|
7
|
+
['boundary', '交付边界:只交付界面,还是点开就能用'],
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export function sanitizeDiscovery(raw) {
|
|
11
|
+
const input = raw !== null && typeof raw === 'object' ? raw : {}
|
|
12
|
+
return {
|
|
13
|
+
server: String(input.server ?? '').trim().slice(0, 300),
|
|
14
|
+
auth: String(input.auth ?? '').trim().slice(0, 300),
|
|
15
|
+
boundary: String(input.boundary ?? '').trim().slice(0, 300),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 还缺哪几项澄清;空了才允许拆卡。 */
|
|
20
|
+
export function discoveryGaps(requirement) {
|
|
21
|
+
const discovery = requirement?.discovery ?? {}
|
|
22
|
+
return DISCOVERY_FIELDS
|
|
23
|
+
.filter(([key]) => String(discovery[key] ?? '').trim() === '')
|
|
24
|
+
.map(([, label]) => label)
|
|
25
|
+
}
|
package/src/host/domain.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** dsh-studio 的领域层:板列、卡片/需求/发布的数据形状与清洗、默认配置、编号生成。纯函数,可单测。 */
|
|
2
2
|
import { createHash, randomBytes } from 'node:crypto'
|
|
3
3
|
import { sanitizeDbCase } from './db-runner.js'
|
|
4
|
+
import { sanitizeDiscovery } from './discovery.js'
|
|
4
5
|
import { sanitizePrices } from './usage.js'
|
|
5
6
|
import { sanitizeWebhooks } from './webhooks.js'
|
|
6
7
|
import { sanitizeCaseShell } from './shell-case.js'
|
|
@@ -979,8 +980,7 @@ export function sanitizeRequirement(raw) {
|
|
|
979
980
|
const title = String(raw.title ?? '').trim().slice(0, 200)
|
|
980
981
|
if (title.length === 0) return undefined
|
|
981
982
|
const now = Date.now()
|
|
982
|
-
const links = raw.links !== null && typeof raw.links === 'object' ? raw.links : {}
|
|
983
|
-
const folderRaw = raw.folder !== null && typeof raw.folder === 'object' ? raw.folder : {}
|
|
983
|
+
const links = raw.links !== null && typeof raw.links === 'object' ? raw.links : {}, folderRaw = raw.folder !== null && typeof raw.folder === 'object' ? raw.folder : {}
|
|
984
984
|
return {
|
|
985
985
|
id: cleanId(raw.id ?? '', 'req'),
|
|
986
986
|
code: String(raw.code ?? '').trim().toUpperCase().slice(0, 40),
|
|
@@ -992,6 +992,7 @@ export function sanitizeRequirement(raw) {
|
|
|
992
992
|
/** 业务领域:卡没写领域时继承它,用来把卡路由给同领域专家。 */
|
|
993
993
|
domain: String(raw.domain ?? '').trim().slice(0, 40),
|
|
994
994
|
priority: PRIORITIES.includes(String(raw.priority)) ? String(raw.priority) : 'normal',
|
|
995
|
+
discovery: sanitizeDiscovery(raw.discovery), // 澄清三问,空的会在拆卡时被拦
|
|
995
996
|
status: REQ_STATUSES.includes(String(raw.status)) ? String(raw.status) : 'draft',
|
|
996
997
|
links: {
|
|
997
998
|
prd: String(links.prd ?? '').trim().slice(0, 500),
|
|
@@ -1854,8 +1855,7 @@ export function sanitizeReleases(input) {
|
|
|
1854
1855
|
|
|
1855
1856
|
export function sanitizeRelease(raw, index = 0) {
|
|
1856
1857
|
if (raw === null || typeof raw !== 'object') return null
|
|
1857
|
-
const version = String(raw.version ?? '').trim().slice(0, 40)
|
|
1858
|
-
const name = String(raw.name ?? '').trim().slice(0, 120)
|
|
1858
|
+
const version = String(raw.version ?? '').trim().slice(0, 40), name = String(raw.name ?? '').trim().slice(0, 120)
|
|
1859
1859
|
if (version === '' && name === '') return null
|
|
1860
1860
|
return {
|
|
1861
1861
|
id: cleanId(raw.id ?? (version === '' ? name : version), `rel-${index}`),
|
package/src/host/executor.js
CHANGED
|
@@ -342,7 +342,6 @@ export async function runTaskCases(store, taskId, options = {}) {
|
|
|
342
342
|
if (task === undefined) throw new Error('任务不存在')
|
|
343
343
|
const config = state.config
|
|
344
344
|
const envs = config.testing.envs
|
|
345
|
-
if (envs.length === 0) throw new Error('还没有配置测试环境(设置 → 测试执行)')
|
|
346
345
|
const envId = String(options.env ?? config.testing.defaultEnv ?? '')
|
|
347
346
|
const env = envs.find(item => item.id === envId) ?? envs[0]
|
|
348
347
|
const wanted = Array.isArray(options.caseIds) && options.caseIds.length > 0
|
|
@@ -350,11 +349,15 @@ export async function runTaskCases(store, taskId, options = {}) {
|
|
|
350
349
|
: null
|
|
351
350
|
const targets = (Array.isArray(task.cases) ? task.cases : [])
|
|
352
351
|
.filter(item => (wanted === null ? item.http != null || item.shell != null : wanted.has(item.id)))
|
|
353
|
-
if (targets.length === 0) throw new Error('
|
|
352
|
+
if (targets.length === 0) throw new Error('没有可执行的用例:接口用例要填 method + url(相对地址拼环境 baseUrl),shell / 数据库用例要填 command / sql')
|
|
354
353
|
|
|
355
|
-
|
|
356
|
-
if (
|
|
357
|
-
|
|
354
|
+
// 只有接口用例才需要测试环境;纯 shell / 数据库用例不该被「环境不可达」挡住。
|
|
355
|
+
if (targets.some(item => item.http != null)) {
|
|
356
|
+
if (env === undefined) throw new Error('还没有配置测试环境(设置 → 测试执行)')
|
|
357
|
+
const probe = await probeEnv(env, config)
|
|
358
|
+
if (probe.ok !== true) {
|
|
359
|
+
throw new Error(`测试环境「${env.name || env.id}」不可达(${probe.error}):一条用例都没跑,先把 ${probe.baseUrl || 'baseUrl'} 起来`)
|
|
360
|
+
}
|
|
358
361
|
}
|
|
359
362
|
|
|
360
363
|
const by = String(options.by ?? '系统').slice(0, 40)
|
|
@@ -512,7 +515,8 @@ export async function saveRequirement(store, input) {
|
|
|
512
515
|
points: requirementPointsFingerprint(existing.points),
|
|
513
516
|
links: JSON.stringify(existing.links ?? {}),
|
|
514
517
|
}
|
|
515
|
-
|
|
518
|
+
// 更新时的字段白名单:漏一个就会「保存成功但字段没变」(discovery 就是这样丢过一次)
|
|
519
|
+
for (const key of ['title', 'description', 'project', 'iteration', 'owner', 'domain', 'priority', 'status', 'links', 'discovery']) {
|
|
516
520
|
existing[key] = draft[key]
|
|
517
521
|
}
|
|
518
522
|
// 排序与依赖单独处理:显式给了才覆盖,避免详情页保存别的字段时把它们清空。
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
sanitizeRequirementPool,
|
|
8
8
|
text,
|
|
9
9
|
} from '../domain.js'
|
|
10
|
+
import { discoveryGaps } from '../discovery.js'
|
|
10
11
|
import { casesOfTasks, impactSummary } from '../impact.js'
|
|
11
12
|
import {
|
|
12
13
|
ensureReqFolder,
|
|
@@ -198,6 +199,8 @@ export async function handleRequirementRoute({ method, route, req, res, store, u
|
|
|
198
199
|
const approved = (requirement.review ?? null)?.status === 'approved'
|
|
199
200
|
const started = ['active', 'delivered', 'closed'].includes(requirement.status)
|
|
200
201
|
if (!approved && !started) throw new Error('需求还没评审通过,先过评审再拆卡')
|
|
202
|
+
const gaps = discoveryGaps(requirement)
|
|
203
|
+
if (gaps.length > 0) throw new Error('拆卡前先把澄清答完:' + gaps.join(';'))
|
|
201
204
|
const points = (requirement.points ?? []).filter(point => requested.length === 0 || requested.includes(point.id))
|
|
202
205
|
const uncovered = points.filter(point => !before.tasks.some(task => task.requirementId === id && task.pointId === point.id))
|
|
203
206
|
if (uncovered.length === 0) throw new Error('这些功能点都已经有卡了')
|