@@ -1,113 +1,100 @@
import { useRef , useState } from 'react'
import { useEffect , useRef , useState } from 'react'
import { Link , useSearchParams } from 'react-router-dom'
import { ArrowRight } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Card } from '@/components/ui/card'
import { Textarea } from '@/components/ui/textarea'
import { Sheet , SheetContent , SheetHeader , SheetTitle , SheetDescription , SheetFooter } from '@/components/ui/sheet'
import { Sheet , SheetContent , SheetHeader , SheetTitle , SheetDescription } from '@/components/ui/sheet'
import { PageHeading , Panel , Empty } from '@/components/shared'
import DecisionChat from '@/components/decision-chat'
import { useWorkbench } from '@/lib/context'
import { ApiError , errorText , messageKey , request } from '@/lib/api'
import { date , type Decision } from '@/lib/types'
import { decisionStatus , decisionLabels , writable , submissionBody , confirmDraft , type DecisionDraft , type ChatDraft } from './decision-ui'
const statuses = { open : '等待决定' , decided : '已决定' , pending_verification : '待核实' }
const priorities = { normal : '普通' , high : '重要' , urgent : '紧急' }
type Draft = { optionId : string | null ; comment : string }
const emptyDraft : Draft = { optionId : null , comment : '' }
function Status ( { decision } : { decision : Decision } ) {
return < Badge variant = "outline" className = { decision . status === 'decided' ? 'status-success' : 'status-neutral' } > { statuses [ decision . status ] } < / Badge >
type HistoryEntry = { id? : string | number ; kind? : string ; created? : number ; revision? : number ; data? : Record < string , unknown > }
function Status ( { decision } : { decision : Decision } ) { const status = decisionStatus ( decision ) ; return < Badge variant = "outline" className = { status === 'completed' ? 'status-success' : status === 'needs_reconsideration' ? 'status-danger' : 'status-neutral' } > { decisionLabels [ status ] } < / Badge > }
function Text ( { label , text } : { label : string ; text? : string } ) { return text ? < section > < h3 className = "font-medium" > { label } < / h3 > < p className = "mt-2 whitespace-pre-wrap break-words text-sm leading-6" > { text } < / p > < / section > : null }
const historyLabels : Record < string , string > = { version : '决策版本' , submission : '提交选择' , review : '主控检查' , completion : '完成交接' , binding : '绑定咨询会话' , archive : '归档确认' , linked : '关联决策' , created : '建立决策' , updated : '内容更新' , revised : '版本更新' , submitted : '提交选择' , responded : '提交选择' , reviewed : '检查结论' , completed : '完成交接' , bound : '绑定咨询会话' , session_bound : '绑定咨询会话' , archived : '归档确认' , fact_requested : '请求核实' , fact_answered : '核实回复' }
function HistoryRecord ( { entry } : { entry : HistoryEntry } ) {
const data = entry . data ? ? { }
const snapshot = data . snapshot && typeof data . snapshot === 'object' ? data . snapshot as Record < string , unknown > : data . content && typeof data . content === 'object' ? data . content as Record < string , unknown > : data
const options = Array . isArray ( snapshot . options ) ? snapshot . options . filter ( ( item ) : item is { id : string ; label : string ; description? : string } = > Boolean ( item && typeof item === 'object' && typeof item . id === 'string' && typeof item . label === 'string' ) ) : [ ]
return < li className = "space-y-2 rounded-lg border p-3 text-sm" > < p className = "font-medium" > { historyLabels [ entry . kind ? ? '' ] || '处理记录' } { entry . revision ? ` · 版本 ${ entry . revision } ` : '' } · { date ( entry . created ) } < / p >
{ typeof data . option_id === 'string' && < p > 当 时 选 择 : { options . find ( option = > option . id === data . option_id ) ? . label || data . option_id } < / p > }
{ Object . entries ( { reason : '理由' , comment : '提交意见' , conclusion : '完成结论' , handoff : '交接' , impact : '影响' , execution_plan : '执行计划' , question : '问题' , answer : '核实回复' } ) . map ( ( [ key , label ] ) = > typeof data [ key ] === 'string' ? < Text key = { key } label = { label } text = { data [ key ] as string } / > : null ) }
{ snapshot !== data && typeof snapshot . question === 'string' && < Text label = "当时的问题" text = { snapshot . question } / > }
{ options . length > 0 && < details > < summary className = "cursor-pointer" > 查 看 当 时 全 部 方 案 < / summary > < ul className = "mt-2 space-y-2" > { options . map ( option = > < li key = { option . id } > < p className = "font-medium" > { option . label } < / p > < p className = "whitespace-pre-wrap" > { option . description } < / p > < / li > ) } < / ul > < / details > }
< / li >
}
function Detail ( { id , initial , drafts , chats } : { id : string ; initial? : Decision ; drafts : Map < string , DecisionDraft > ; chats : Map < string , ChatDraft > } ) {
const { token , refresh , state } = useWorkbench ( )
const [ decision , setDecision ] = useState ( initial ) , [ history , setHistory ] = useState < HistoryEntry [ ] > ( [ ] ) , [ error , setError ] = useState ( '' ) , [ notice , setNotice ] = useState ( '' ) , [ , redraw ] = useState ( 0 )
const reload = useRef < ( ) = > Promise < void > > ( async ( ) = > { } ) , generation = useRef ( 0 )
useEffect ( ( ) = > {
const controller = new AbortController ( ) ; let pending = false
const poll = async ( ) = > {
if ( pending || controller . signal . aborted ) return
pending = true ; const seq = ++ generation . current
try { const current = await request < Decision > ( token , ` /api/decisions/ ${ encodeURIComponent ( id ) } ` , undefined , controller . signal ) ; if ( ! controller . signal . aborted && seq === generation . current ) { setDecision ( current ) ; setError ( '' ) }
const events = await request < { history : HistoryEntry [ ] } > ( token , ` /api/decisions/ ${ encodeURIComponent ( id ) } /history ` , undefined , controller . signal ) ; if ( ! controller . signal . aborted && seq === generation . current ) setHistory ( events . history )
} catch ( cause ) { if ( ! controller . signal . aborted && seq === generation . current ) setError ( errorText ( cause ) ) } finally { pending = false }
}
reload . current = poll ; void poll ( ) ; const timer = setInterval ( ( ) = > void poll ( ) , 3000 )
return ( ) = > { controller . abort ( ) ; clearInterval ( timer ) }
} , [ id , token ] )
if ( decision && ! drafts . has ( id ) ) drafts . set ( id , { option : null , comment : '' , revision : decision.revision ? ? 1 , conflict : false } )
const draft = drafts . get ( id )
const revision = decision ? . revision ? ? 1
const changed = Boolean ( draft && ( draft . revision !== revision || draft . conflict ) )
const editable = Boolean ( decision && writable ( decision ) && ! draft ? . sending )
async function submit() {
if ( ! decision || ! draft || ! editable || changed || ( ! draft . option && ! draft . comment . trim ( ) ) ) return
const body = submissionBody ( decision , draft )
if ( ! body ) return
const signature = JSON . stringify ( body )
if ( ! draft . attempt || draft . attempt . signature !== signature ) draft . attempt = { signature , key : messageKey ( ) }
draft . sending = true ; redraw ( n = > n + 1 ) ; setNotice ( '' )
try {
const result = await request < Decision > ( token , ` /api/decisions/ ${ encodeURIComponent ( id ) } /respond ` , { . . . body , idempotency_key : draft.attempt.key } )
++ generation . current ; setDecision ( result ) ; setNotice ( '' ) ; draft . option = null ; draft . comment = '' ; draft . attempt = undefined
await refresh ( ) . catch ( ( ) = > { } ) ; await reload . current ( )
} catch ( cause ) {
if ( cause instanceof ApiError && cause . status === 409 ) { draft . conflict = true ; setNotice ( '内容或状态已变化,草稿已保留。请阅读最新内容并重新确认,未自动重交。' ) ; await reload . current ( ) }
else setNotice ( ` ${ errorText ( cause ) } 草稿已保留,相同内容重试复用原提交记录。 ` )
} finally { draft . sending = false ; redraw ( n = > n + 1 ) }
}
return < > < SheetHeader className = "pr-12" > < SheetTitle > { decision ? . title ? ? '读取决策…' } < / SheetTitle > < SheetDescription > { decision ? . summary || decision ? . question || '查看方案、检查结论与本决策咨询。' } < / SheetDescription > < / SheetHeader >
< div className = "min-h-0 flex-1 overflow-y-auto px-4 pb-5" > { error && < p role = "status" className = "mb-4 text-sm text-destructive" > { error } 保 留 已 读 取 内 容 。 < Button variant = "link" onClick = { ( ) = > void reload . current ( ) } > 重 试 < / Button > < / p > }
{ decision && draft ? < div className = "grid min-w-0 gap-6 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)]" > < div className = "min-w-0 space-y-5" >
< div className = "flex flex-wrap gap-2" > < Status decision = { decision } / > < Badge variant = "outline" > 版 本 { revision } < / Badge > { decision . task_id && < Link to = { ` /tasks?task= ${ encodeURIComponent ( decision . task_id ) } ` } className = "text-sm text-primary underline" > { state . tasks . find ( task = > task . id === decision . task_id ) ? . name || decision . task_id } < / Link > } < / div >
< Text label = "需要决定的问题" text = { decision . question } / > < Text label = "背景与依据" text = { decision . details } / > < Text label = "影响" text = { decision . impact } / > < Text label = "推荐与理由(不会替你选择)" text = { decision . recommendation } / >
{ decision . deadline && Number . isFinite ( Date . parse ( decision . deadline ) ) && < p className = "text-sm" > 截 止 时 间 : { date ( decision . deadline ) } < / p > }
< p className = "text-xs text-muted-foreground" > 选 择 提 交 后 需 检 查 ; 接 受 方 案 不 代 表 权 限 授 权 , 也 不 代 表 已 经 完 成 。 < / p >
{ decision . options . length > 0 && < fieldset disabled = { ! editable || changed } className = "space-y-2" > < legend className = "mb-2 font-medium" > 全 部 方 案 < / legend > { decision . options . map ( option = > < label key = { option . id } className = "flex gap-3 rounded-lg border p-3" > < input type = "radio" name = { ` decision- ${ id } ` } checked = { draft . option === option . id } onChange = { ( ) = > { draft . option = option . id ; redraw ( n = > n + 1 ) } } className = "mt-1 size-4 shrink-0 accent-primary" / > < span > < span className = "font-medium" > { option . label } < / span > < span className = "mt-1 block whitespace-pre-wrap text-sm text-muted-foreground" > { option . description } < / span > < / span > < / label > ) } < / fieldset > }
{ writable ( decision ) && < div className = "space-y-3" > < Button variant = "ghost" size = "sm" disabled = { ! editable || changed || ! draft . option } onClick = { ( ) = > { draft . option = null ; redraw ( n = > n + 1 ) } } > 清 除 选 项 , 仅 填 写 意 见 < / Button > < label className = "block text-sm font-medium" htmlFor = { ` opinion- ${ id } ` } > 你 的 意 见 ( 可 不 选 方 案 , 直 接 填 写 ) < / label > < Textarea id = { ` opinion- ${ id } ` } disabled = { ! editable } value = { draft . comment } onChange = { event = > { draft . comment = event . target . value ; redraw ( n = > n + 1 ) } } / >
{ changed && < div className = "space-y-2 rounded-lg border p-3" > < p className = "text-sm" > 请 重 新 阅 读 当 前 版 本 ; 之 前 的 选 择 不 会 自 动 作 为 本 次 确 认 。 < / p > < Button variant = "outline" disabled = { draft . sending || Boolean ( error ) } onClick = { ( ) = > { confirmDraft ( decision , draft ) ; setNotice ( '已确认当前版本,请重新选择方案或检查保留的意见后提交。' ) ; redraw ( n = > n + 1 ) } } > 我 已 阅 读 版 本 { revision } , 重 新 确 认 < / Button > < / div > }
< Button disabled = { ! editable || changed || ( ! draft . option && ! draft . comment . trim ( ) ) } onClick = { ( ) = > void submit ( ) } > { draft . sending ? '正在提交…' : '提交本次选择或意见' } < / Button >
< / div > }
{ notice && < p role = "status" className = "text-sm" > { notice } < / p > }
{ decision . response && < section className = "space-y-2 rounded-lg border p-3" > < h3 className = "font-medium" > 最 近 提 交 < / h3 > < p className = "text-sm" > 提 交 版 本 : { decision . response . revision ? ? '旧记录' } · { date ( decision . response . created ) } < / p > < p className = "text-sm" > 选 项 : { decision . response . snapshot ? . options ? . find ( option = > option . id === decision . response ? . option_id ) ? . label ? ? decision . response . option_id ? ? '仅填写意见' } < / p > < p className = "whitespace-pre-wrap text-sm" > { decision . response . comment } < / p > < / section > }
{ decision . review && < section className = "space-y-3 rounded-lg border p-3" > < h3 className = "font-medium" > 检 查 结 论 : { decision . review . outcome === 'accept' ? '接受,待落实' : '需要重新考虑' } < / h3 > < Text label = "理由" text = { decision . review . reason } / > < Text label = "影响" text = { decision . review . impact } / > < Text label = "执行计划" text = { decision . review . execution_plan } / > < / section > }
{ decision . completion && < section className = "space-y-3 rounded-lg border p-3" > < Text label = "完成结论" text = { decision . completion . conclusion } / > < Text label = "交接说明" text = { decision . completion . handoff } / > { decision . completion . evidence ? . map ( ( text , index ) = > < p key = { index } className = "break-words text-sm" > { text } < / p > ) } < / section > }
{ decision . status === 'completed' && < p className = "text-sm" > { decision . archive_status === 'archived' ? '咨询会话已确认归档,历史只读。' : '决策已完成,咨询会话待归档确认。' } < / p > }
{ Boolean ( decision . supersedes_id || decision . related_decision_ids ? . length ) && < section > < h3 className = "mb-2 font-medium" > 关 联 决 策 < / h3 > { [ . . . new Set ( [ . . . ( decision . supersedes_id ? [ decision . supersedes_id ] : [ ] ) , . . . ( decision . related_decision_ids ? ? [ ] ) ] ) ] . map ( other = > < Link key = { other } className = "mr-3 text-sm text-primary underline" to = { ` /decisions?decision= ${ encodeURIComponent ( other ) } ` } > { other } { other === decision . supersedes_id ? '(被此决策替代)' : '' } < / Link > ) } < / section > }
< section > < h3 className = "mb-2 font-medium" > 版 本 与 处 理 历 史 < / h3 > { history . length ? < ol className = "space-y-3" > { history . map ( ( entry , index ) = > < HistoryRecord key = { entry . id ? ? index } entry = { entry } / > ) } < / ol > : < p className = "text-sm text-muted-foreground" > 暂 无 已 读 取 的 历 史 记 录 。 < / p > } < / section >
< / div > < div className = "min-w-0" > < DecisionChat key = { id } decision = { decision } token = { token } drafts = { chats } / > < / div > < / div > : ! error && < Empty > 正 在 读 取 当 前 决 策 。 < / Empty > }
< / div > < / >
}
export default function Decisions() {
const { state , token , refresh } = useWorkbench ( )
const [ params , setParams ] = useSearchParams ( )
const [ receipts , setReceipts ] = useState < Record < string , Decision > > ( { } )
const decisions = ( state . decisions ? ? [ ] ) . map ( decision = > decision . status === 'open' ? receipts [ decision . id ] ? ? decision : decision )
const selectedId = params . get ( 'decision' )
const selected = decisions . find ( d = > d . id === selectedId )
const [ drafts , setDrafts ] = useState < Record < string , Draft > > ( { } )
const [ notices , setNotices ] = useState < Record < string , string > > ( { } )
const [ busy , setBusy ] = useState < Record < string , boolean > > ( { } )
const [ submitted , setSubmitted ] = useState < Record < string , boolean > > ( { } )
const inFlight = useRef ( new Set < string > ( ) )
const attempts = useRef ( new Map < string , { payload : string ; key : string } > ( ) )
const acknowledged = useRef ( new Set < string > ( ) )
const draft = selected ? drafts [ selected . id ] ? ? emptyDraft : emptyDraft
const editable = selected ? . status === 'open' && ! busy [ selected . id ] && ! submitted [ selected . id ]
function open ( id : string | null ) {
setParams ( previous = > { const next = new URLSearchParams ( previous ) ; if ( id ) next . set ( 'decision' , id ) ; else next . delete ( 'decision' ) ; return next } )
}
function updateDraft ( value : Partial < Draft > ) {
if ( ! selected || ! editable ) return
setDrafts ( previous = > ( { . . . previous , [ selected . id ] : { . . . ( previous [ selected . id ] ? ? emptyDraft ) , . . . value } } ) )
}
async function sync ( id : string ) {
try { await refresh ( ) ; setNotices ( previous = > ( { . . . previous , [ id ] : '' } ) ) }
catch ( error ) { setNotices ( previous = > ( { . . . previous , [ id ] : ` 状态同步未完成: ${ errorText ( error ) } 请刷新状态。 ` } ) ) }
}
async function submit ( decision : Decision ) {
const id = decision . id
const current = drafts [ id ] ? ? emptyDraft
if ( decision . status !== 'open' || inFlight . current . has ( id ) || acknowledged . current . has ( id ) ) return
if ( decision . options . length && ! decision . options . some ( option = > option . id === current . optionId ) ) return
const body = { option_id : current.optionId , comment : current.comment }
const payload = JSON . stringify ( body )
let attempt = attempts . current . get ( id )
if ( ! attempt || attempt . payload !== payload ) { attempt = { payload , key : messageKey ( ) } ; attempts . current . set ( id , attempt ) }
inFlight . current . add ( id )
setBusy ( previous = > ( { . . . previous , [ id ] : true } ) )
setNotices ( previous = > ( { . . . previous , [ id ] : '' } ) )
try {
const result = await request < Decision > ( token , ` /api/decisions/ ${ encodeURIComponent ( id ) } /respond ` , { . . . body , idempotency_key : attempt.key } )
setReceipts ( previous = > ( { . . . previous , [ id ] : result } ) )
acknowledged . current . add ( id )
setSubmitted ( previous = > ( { . . . previous , [ id ] : true } ) )
setNotices ( previous = > ( { . . . previous , [ id ] : '' } ) )
await sync ( id )
} catch ( error ) {
setNotices ( previous = > ( { . . . previous , [ id ] : ` ${ errorText ( error ) } 草稿已保留;重试相同内容不会重复提交。 ` } ) )
if ( error instanceof ApiError && error . status === 409 ) await sync ( id )
} finally {
inFlight . current . delete ( id )
setBusy ( previous = > ( { . . . previous , [ id ] : false } ) )
}
}
const legacy = state . tasks . filter ( task = > task . decision )
return < div className = "space-y-6" >
< PageHeading title = "方案与决策" description = "查看影响与方案,再明确提交你的选择。" action = { < Button asChild variant = "outline" > < Link to = "/communication" > 讨 论 方 案 < ArrowRight / > < / Link > < / Button > } / >
{ decisions . length ? < div className = "grid gap-4 md:grid-cols-2 xl:grid-cols-3" > { decisions . map ( decision = > < Card key = { decision . id } className = "gap-0 p-0" >
< button type = "button" onClick = { ( ) = > open ( decision . id ) } className = "flex h-full w-full flex-col gap-4 rounded-xl p-6 text-left transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" aria-label = { ` 查看决策: ${ decision . title } ` } >
< div className = "flex flex-wrap gap-2" > < Badge variant = "outline" className = { decision . priority === 'urgent' ? 'status-danger' : 'status-neutral' } > { priorities [ decision . priority ] } < / Badge > < Status decision = { decision } / > < / div >
< h2 className = "font-semibold break-words" > { decision . title } < / h2 >
< p className = "whitespace-pre-wrap break-words text-sm leading-6" > { decision . question } < / p >
< p className = "whitespace-pre-wrap break-words text-sm text-muted-foreground" > 影 响 : { decision . impact || '尚未补充' } < / p >
< span className = "mt-auto flex items-center gap-2 text-sm text-primary" > 查 看 方 案 与 详 情 < ArrowRight className = "size-4" / > < / span >
< / button >
< / Card > ) } < / div > : < Panel title = "决策展板" > < Empty > 当 前 没 有 结 构 化 决 策 事 项 。 可 在 沟 通 中 请 总 控 补 充 需 要 决 定 的 问 题 与 方 案 。 < / Empty > < / Panel > }
{ legacy . length > 0 && < Panel title = "待完善条目(只读)" description = "以下是工作包原有的文字记录,尚不能在此提交选择。" > < div className = "space-y-4" > { legacy . map ( task = > < article key = { task . id } className = "rounded-lg border p-4" > < h3 className = "font-medium" > { task . id } · { task . name } < / h3 > < p className = "mt-2 whitespace-pre-wrap break-words text-sm leading-6" > { task . decision } < / p > < p className = "mt-2 text-xs text-muted-foreground" > 更 新 于 { date ( task . updated ) } < / p > < Button asChild variant = "link" className = "mt-2 h-auto p-0" > < Link to = "/communication" > 请 总 控 补 充 结 构 化 选 项 < ArrowRight / > < / Link > < / Button > < / article > ) } < / div > < / Panel > }
< Sheet open = { selectedId !== null } onOpenChange = { value = > { if ( ! value ) open ( null ) } } >
< SheetContent className = "w-full sm:max-w-xl" >
< SheetHeader className = "pr-12" > < SheetTitle > { selected ? . title ? ? '决策暂不可用' } < / SheetTitle > < SheetDescription > { selected ? . question ? ? '该条目尚未同步或已移除,请刷新状态后重试。' } < / SheetDescription > < / SheetHeader >
{ selected ? < >
< div className = "min-h-0 flex-1 space-y-6 overflow-y-auto px-4 pb-4 break-words" >
< div className = "flex flex-wrap gap-2" > < Badge variant = "outline" > { priorities [ selected . priority ] } < / Badge > < Status decision = { selected } / > < / div >
< section > < h3 className = "font-medium" > 影 响 < / h3 > < p className = "mt-2 whitespace-pre-wrap text-sm leading-6" > { selected . impact || '尚未补充' } < / p > < / section >
< section > < h3 className = "font-medium" > 背 景 与 依 据 < / h3 > < p className = "mt-2 whitespace-pre-wrap text-sm leading-6" > { selected . details || '尚未补充' } < / p > < / section >
{ selected . recommendation && < section className = "rounded-lg border bg-muted/50 p-4" > < h3 className = "font-medium" > 推 荐 与 理 由 < / h3 > < p className = "mt-2 whitespace-pre-wrap text-sm leading-6" > { selected . recommendation } < / p > < p className = "mt-2 text-xs text-muted-foreground" > 推 荐 不 会 替 你 选 择 。 < / p > < / section > }
{ selected . deadline && Number . isFinite ( Date . parse ( selected . deadline ) ) && < p className = "text-sm" > 截 止 时 间 : { date ( selected . deadline ) } < / p > }
{ selected . requires_authorization && < p className = "rounded-lg border p-4 text-sm" > 此 事 项 涉 及 权 限 。 提 交 选 择 后 仍 需 核 实 , 不 代 表 已 授 权 。 < / p > }
{ selected . options . length > 0 && < fieldset disabled = { ! editable } className = "space-y-3" > < legend className = "mb-3 font-medium" > 方 案 与 取 舍 { selected . status === 'open' ? '(请选择一项)' : '' } < / legend > { selected . options . map ( option = > < label key = { option . id } className = { ` flex gap-3 rounded-lg border p-4 ${ editable ? 'cursor-pointer hover:bg-muted/50' : '' } ` } > < input type = "radio" name = { ` decision- ${ selected . id } ` } value = { option . id } checked = { selected . status === 'open' ? draft . optionId === option.id : selected.response?.option_id === option . id } onChange = { ( ) = > updateDraft ( { optionId : option.id } ) } className = "mt-1 size-4 shrink-0 accent-primary" / > < span > < span className = "font-medium" > { option . label } < / span > < span className = "mt-1 block whitespace-pre-wrap text-sm leading-6 text-muted-foreground" > { option . description } < / span > < / span > < / label > ) } < / fieldset > }
{ selected . status === 'open' && < div className = "space-y-2" > < label htmlFor = "decision-comment" className = "text-sm font-medium" > 补 充 备 注 < / label > < Textarea id = "decision-comment" value = { draft . comment } onChange = { event = > updateDraft ( { comment : event.target.value } ) } disabled = { ! editable } placeholder = "补充你的考虑或要求" / > < / div > }
{ selected . response && < section className = "space-y-2 rounded-lg border p-4" > < h3 className = "font-medium" > 已 记 录 的 回 复 < / h3 > < p className = "text-sm" > 选 择 : { selected . options . find ( option = > option . id === selected . response ? . option_id ) ? . label ? ? selected . response . option_id ? ? '仅备注' } < / p > < p className = "whitespace-pre-wrap text-sm" > { selected . response . comment || '无补充备注' } < / p > < p className = "text-xs text-muted-foreground" > { date ( selected . response . created ) } < / p > < / section > }
{ selected . status === 'pending_verification' && < p role = "status" className = "text-sm" > 选 择 已 记 录 , 等 待 核 实 ; 尚 未 授 权 。 < / p > }
{ notices [ selected . id ] && < p role = "status" className = "text-sm" > { notices [ selected . id ] } < / p > }
< / div >
< SheetFooter className = "border-t" >
{ selected . status === 'open' && ! submitted [ selected . id ] && < Button disabled = { ! editable || ( selected . options . length > 0 && ! selected . options . some ( option = > option . id === draft . optionId ) ) } onClick = { ( ) = > void submit ( selected ) } > { busy [ selected . id ] ? '正在提交…' : '提交选择' } < / Button > }
< Button variant = "outline" disabled = { busy [ selected . id ] } onClick = { ( ) = > void sync ( selected . id ) } > 刷 新 状 态 < / Button >
< / SheetFooter >
< / > : < SheetFooter > < Button variant = "outline" onClick = { ( ) = > void sync ( selectedId ? ? '' ) } > 刷 新 状 态 < / Button > < / SheetFooter > }
< / SheetContent >
< / Sheet >
const { state } = useWorkbench ( ) , [ params , setParams ] = useSearchParams ( )
const id = params . get ( 'decision' ) , drafts = useRef ( new Map < string , DecisionDraft > ( ) ) , chats = useRef ( new Map < string , ChatDraft > ( ) )
const decisions = state . decisions ? ? [ ]
return < div className = "space-y-6" > < PageHeading title = "方案与决策" description = "查看方案、咨询依据并明确提交,跟踪检查与落实结果。" / >
{ decisions . length ? < div className = "grid gap-4 md:grid-cols-2 xl:grid-cols-3" > { decisions . map ( decision = > < Card key = { decision . id } className = "p-0" > < button type = "button" onClick = { ( ) = > setParams ( previous = > { const next = new URLSearchParams ( previous ) ; next . set ( 'decision' , decision . id ) ; return next } ) } className = "flex h-full w-full flex-col gap-3 rounded-xl p-6 text-left hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-ring" > < div className = "flex flex-wrap gap-2" > < Status decision = { decision } / > < Badge variant = "outline" > { decision . task_id ? state . tasks . find ( task = > task . id === decision . task_id ) ? . name || decision . task_id : '未关联任务' } < / Badge > < / div > < h2 className = "font-semibold" > { decision . title } < / h2 > < p className = "whitespace-pre-wrap break-words text-sm text-muted-foreground" > { decision . summary || decision . question } < / p > < span className = "mt-auto text-sm text-primary" > 查 看 方 案 与 咨 询 < / span > < / button > < / Card > ) } < / div > : < Panel title = "决策展板" > < Empty > 当 前 没 有 登 记 决 策 事 项 。 < / Empty > < / Panel > }
{ state . tasks . some ( task = > task . decision ) && < Panel title = "旧版文字条目(只读)" > { state . tasks . filter ( task = > task . decision ) . map ( task = > < p key = { task . id } className = "mb-3 whitespace-pre-wrap text-sm" > { task . name } : { task . decision } < / p > ) } < / Panel > }
< Sheet open = { id !== null } onOpenChange = { value = > { if ( ! value ) setParams ( previous = > { const next = new URLSearchParams ( previous ) ; next . delete ( 'decision' ) ; return next } ) } } > < SheetContent className = "w-full sm:max-w-6xl" > { id && < Detail key = { id } id = { id } initial = { decisions . find ( decision = > decision . id === id ) } drafts = { drafts . current } chats = { chats . current } / > } < / SheetContent > < / Sheet >
< / div >
}