@marsken/dsh-studio 0.1.18 → 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 +1 -1
- package/client.js +1 -1
- package/dist/client.js +1 -1
- package/docs/compatibility.md +1 -1
- package/package.json +1 -1
- package/src/host/discovery.js +25 -0
- package/src/host/domain.js +4 -4
- package/src/host/executor.js +2 -1
- 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/`) | ❌ 生成物 |
|
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/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
|
@@ -515,7 +515,8 @@ export async function saveRequirement(store, input) {
|
|
|
515
515
|
points: requirementPointsFingerprint(existing.points),
|
|
516
516
|
links: JSON.stringify(existing.links ?? {}),
|
|
517
517
|
}
|
|
518
|
-
|
|
518
|
+
// 更新时的字段白名单:漏一个就会「保存成功但字段没变」(discovery 就是这样丢过一次)
|
|
519
|
+
for (const key of ['title', 'description', 'project', 'iteration', 'owner', 'domain', 'priority', 'status', 'links', 'discovery']) {
|
|
519
520
|
existing[key] = draft[key]
|
|
520
521
|
}
|
|
521
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('这些功能点都已经有卡了')
|