From e939dc4548ed7b7bad8995c408c9d7dd6273ad67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Sun, 2 Aug 2026 21:52:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E5=B2=97=E4=BD=8D=E4=BC=9A?= =?UTF-8?q?=E5=95=86=E9=A1=B5=E9=9D=A2=E5=B8=83=E5=B1=80=E4=B8=8E=E6=84=8F?= =?UTF-8?q?=E5=9B=BE=E7=90=86=E8=A7=A3=E4=BA=A4=E4=BA=92=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 完善 PositionRoundtable 样式与输入组件,补充 SVG 资源,并同步 Step1 意图相关逻辑。 Co-authored-by: Cursor --- .../assets/position-roundtable/add-task.svg | 1 + .../position-roundtable/agent-config-logo.svg | 1 + .../position-roundtable/ask-question.svg | 1 + .../position-roundtable/collapse-panel.svg | 1 + .../double-arrow-decoration.svg | 1 + .../assets/position-roundtable/more-task.svg | 1 + .../position-roundtable/search-task.svg | 1 + .../position-roundtable/send-message.svg | 1 + .../position-roundtable/upload-file.svg | 1 + .../src/assets/position-roundtable/时间.svg | 1 + .../src/assets/position-roundtable/标签1.svg | 1 + .../src/assets/position-roundtable/标签2.svg | 1 + .../src/assets/position-roundtable/标签3.svg | 1 + .../src/assets/position-roundtable/标签4.svg | 1 + .../src/assets/position-roundtable/标签5.svg | 1 + .../src/assets/position-roundtable/标签6.svg | 1 + .../assets/position-roundtable/责任岗位.svg | 1 + .../workspace/agents/agent-detail-panel.tsx | 162 +- .../components/PositionAgentInput.tsx | 113 +- .../components/PositionBuiltinAgentChat.tsx | 2 +- .../components/PositionIntentInput.tsx | 171 +- .../components/PositionNodeChat.tsx | 3 +- .../pages/PositionRoundtablePage.tsx | 862 ++++++--- .../styles/intent-understanding.css | 1575 ++++++++++++++++- .../src/roundtable-planning/api/intent.ts | 3 + .../components/Step1Panel.tsx | 60 +- .../hooks/useStep1Intent.ts | 43 +- frontend-web/src/styles/globals.css | 2 +- 28 files changed, 2653 insertions(+), 360 deletions(-) create mode 100644 frontend-web/src/assets/position-roundtable/add-task.svg create mode 100644 frontend-web/src/assets/position-roundtable/agent-config-logo.svg create mode 100644 frontend-web/src/assets/position-roundtable/ask-question.svg create mode 100644 frontend-web/src/assets/position-roundtable/collapse-panel.svg create mode 100644 frontend-web/src/assets/position-roundtable/double-arrow-decoration.svg create mode 100644 frontend-web/src/assets/position-roundtable/more-task.svg create mode 100644 frontend-web/src/assets/position-roundtable/search-task.svg create mode 100644 frontend-web/src/assets/position-roundtable/send-message.svg create mode 100644 frontend-web/src/assets/position-roundtable/upload-file.svg create mode 100644 frontend-web/src/assets/position-roundtable/时间.svg create mode 100644 frontend-web/src/assets/position-roundtable/标签1.svg create mode 100644 frontend-web/src/assets/position-roundtable/标签2.svg create mode 100644 frontend-web/src/assets/position-roundtable/标签3.svg create mode 100644 frontend-web/src/assets/position-roundtable/标签4.svg create mode 100644 frontend-web/src/assets/position-roundtable/标签5.svg create mode 100644 frontend-web/src/assets/position-roundtable/标签6.svg create mode 100644 frontend-web/src/assets/position-roundtable/责任岗位.svg diff --git a/frontend-web/src/assets/position-roundtable/add-task.svg b/frontend-web/src/assets/position-roundtable/add-task.svg new file mode 100644 index 00000000..e27cab8f --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/add-task.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/src/assets/position-roundtable/agent-config-logo.svg b/frontend-web/src/assets/position-roundtable/agent-config-logo.svg new file mode 100644 index 00000000..d51eb71d --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/agent-config-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/position-roundtable/ask-question.svg b/frontend-web/src/assets/position-roundtable/ask-question.svg new file mode 100644 index 00000000..af9052eb --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/ask-question.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/src/assets/position-roundtable/collapse-panel.svg b/frontend-web/src/assets/position-roundtable/collapse-panel.svg new file mode 100644 index 00000000..9f064680 --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/collapse-panel.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/src/assets/position-roundtable/double-arrow-decoration.svg b/frontend-web/src/assets/position-roundtable/double-arrow-decoration.svg new file mode 100644 index 00000000..bf85a192 --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/double-arrow-decoration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/position-roundtable/more-task.svg b/frontend-web/src/assets/position-roundtable/more-task.svg new file mode 100644 index 00000000..055a158d --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/more-task.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/src/assets/position-roundtable/search-task.svg b/frontend-web/src/assets/position-roundtable/search-task.svg new file mode 100644 index 00000000..fc8fc78c --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/search-task.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/src/assets/position-roundtable/send-message.svg b/frontend-web/src/assets/position-roundtable/send-message.svg new file mode 100644 index 00000000..3a96000e --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/send-message.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/src/assets/position-roundtable/upload-file.svg b/frontend-web/src/assets/position-roundtable/upload-file.svg new file mode 100644 index 00000000..9f47e22a --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/upload-file.svg @@ -0,0 +1 @@ + diff --git a/frontend-web/src/assets/position-roundtable/时间.svg b/frontend-web/src/assets/position-roundtable/时间.svg new file mode 100644 index 00000000..26888994 --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/时间.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/position-roundtable/标签1.svg b/frontend-web/src/assets/position-roundtable/标签1.svg new file mode 100644 index 00000000..780de1a5 --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/标签1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/position-roundtable/标签2.svg b/frontend-web/src/assets/position-roundtable/标签2.svg new file mode 100644 index 00000000..7443e13d --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/标签2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/position-roundtable/标签3.svg b/frontend-web/src/assets/position-roundtable/标签3.svg new file mode 100644 index 00000000..7d9257a5 --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/标签3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/position-roundtable/标签4.svg b/frontend-web/src/assets/position-roundtable/标签4.svg new file mode 100644 index 00000000..2be60358 --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/标签4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/position-roundtable/标签5.svg b/frontend-web/src/assets/position-roundtable/标签5.svg new file mode 100644 index 00000000..df814d72 --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/标签5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/position-roundtable/标签6.svg b/frontend-web/src/assets/position-roundtable/标签6.svg new file mode 100644 index 00000000..3df12c8a --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/标签6.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/assets/position-roundtable/责任岗位.svg b/frontend-web/src/assets/position-roundtable/责任岗位.svg new file mode 100644 index 00000000..66040aa7 --- /dev/null +++ b/frontend-web/src/assets/position-roundtable/责任岗位.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx index ca90f080..6e23d8db 100644 --- a/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx +++ b/frontend-web/src/components/workspace/agents/agent-detail-panel.tsx @@ -100,15 +100,17 @@ function Section({ icon, title, extra, + className, children, }: { icon?: ReactNode; title: string; extra?: ReactNode; + className?: string; children: ReactNode; }) { return ( -
+

{icon} @@ -139,6 +141,15 @@ export function AgentDetailPanel({ defaultCollapsed, showRecentChats, compactAppearanceActions, + avatarSrc, + positionName, + skillsIcon, + skillsTitle, + showCommonQuestions = false, + panelTitle, + embedded, + hideStatusBadges = false, + editIcon, }: { agent: Agent | null | undefined; agentName: string; @@ -150,6 +161,19 @@ export function AgentDetailPanel({ showRecentChats?: boolean; /** 简洁模式下挂在右上角收起/展开按钮左侧的外观控件。 */ compactAppearanceActions?: ReactNode; + avatarSrc?: string; + positionName?: string; + skillsIcon?: ReactNode; + skillsTitle?: string; + showCommonQuestions?: boolean; + /** Overrides the standard panel heading for an embedded consumer. */ + panelTitle?: string; + /** Renders one fixed panel instead of the responsive collapsible rail. */ + embedded?: boolean; + /** Keeps deployment/status badges out of an embedded product surface. */ + hideStatusBadges?: boolean; + /** Optional icon used by the compact embedded edit action. */ + editIcon?: ReactNode; }) { const router = useRouter(); const [collapsed, setCollapsed] = useState(defaultCollapsed ?? false); @@ -472,11 +496,25 @@ export function AgentDetailPanel({

) : (
- - {canTogglePublish && ( + {embedded ? ( + + + + ) : ( + + )} + {!embedded && canTogglePublish && ( - + {!embedded && ( + + + + )}
) ) : null; @@ -521,7 +561,11 @@ export function AgentDetailPanel({
- + {avatarSrc ? ( + + ) : ( + + )}
{editing && draft ? ( @@ -541,9 +585,16 @@ export function AgentDetailPanel({ {displayName}
)} -
- {statusBadges} -
+ {!editing && positionName && ( +
+ 岗位:{desensitize(positionName)} +
+ )} + {!hideStatusBadges && ( +
+ {statusBadges} +
+ )} {!editing && agent.owner_username && !agent.builtin && (
@@ -646,7 +697,7 @@ export function AgentDetailPanel({ {/* ── 常用问题 ───────────────────────────────────────────────────── */} - {(editing || questions.length > 0 || canEdit) && ( + {(showCommonQuestions || editing || questions.length > 0 || canEdit) && (
} title="常用问题" @@ -787,8 +838,9 @@ export function AgentDetailPanel({ {/* ── 技能管理 ───────────────────────────────────────────────────── */}
} - title={`已启用技能 ${skillNamesShown.length} 个`} + icon={skillsIcon ?? } + title={skillsTitle ?? `已启用技能 ${skillNamesShown.length} 个`} + className="position-roundtable-skills-section" extra={ editing ? ( - -
- - - - - 智能体信息 - {headerActions} - - -
{body}
-
-
+ {!embedded && ( + <> + {/* ── Narrow screens ( + + + +
+ + + + + 智能体信息 + {headerActions} + + +
{body}
+
+
+ + )} {/* ── Secondary surfaces ───────────────────────────────────────────── */} & { +type InputBoxProps = ComponentProps; +type PositionAgentInputProps = Pick< + InputBoxProps, + | "className" + | "assistantId" + | "status" + | "disabled" + | "context" + | "onContextChange" + | "onSubmit" + | "onStop" + | "isNewThread" + | "threadId" + | "hideAdminRecommendedQuestions" +> & { + commonQuestions?: (string | AgentQuestion)[] | null; /** Stable node/builtin key used to restore the matching SQL-backed draft. */ draftKey?: string; draftValue?: string; @@ -18,18 +38,36 @@ type PositionAgentInputProps = ComponentProps & { * 只改这个组件即可,不会影响普通对话、工作区或其他业务页面。 */ export function PositionAgentInput({ - className, + context, + disabled = false, + status = "ready", + onContextChange, + onSubmit, + onStop, + commonQuestions, draftKey = "", draftValue = "", onDraftChange, - ...props }: PositionAgentInputProps) { - const { textInput } = usePromptInputController(); + const { textInput, attachments } = usePromptInputController(); + const { models } = useModels(); const activeDraftKeyRef = useRef(""); const restoringValueRef = useRef(null); const textInputRef = useRef(textInput); textInputRef.current = textInput; + const modelOptions = useMemo( + () => models.map((model) => ({ label: model.display_name ?? model.name, value: model.name })), + [models], + ); + const attachedFiles = useMemo( + () => + attachments.files + .map((attachment) => attachment.file) + .filter((file): file is File => file instanceof File), + [attachments.files], + ); + useEffect(() => { activeDraftKeyRef.current = draftKey; if (textInputRef.current.value !== draftValue) { @@ -51,13 +89,60 @@ export function PositionAgentInput({ onDraftChange?.(textInput.value); }, [draftKey, onDraftChange, textInput.value]); - // Keep the page-specific class on the wrapper rather than adding a variant - // to InputBox. Node chat and built-in chat can evolve together while every - // other workspace keeps the global composer untouched. + const handleSend = () => { + if (status === "streaming") { + onStop?.(); + return; + } + const text = textInput.value.trim(); + if (!text && attachedFiles.length === 0) return; + const message: PromptInputMessage = { + text: textInput.value, + files: attachments.files, + }; + void onSubmit?.(message); + textInput.clear(); + attachments.clear(); + }; + + const handleModelChange = (modelName: string) => { + const model = models.find((candidate) => candidate.name === modelName); + if (!model) return; + onContextChange?.({ ...context, model_name: model.name }); + }; + const selectedModel = + typeof context.model_name === "string" + ? context.model_name + : modelOptions[0]?.value; + return ( - { + const attachment = attachments.files[index]; + if (attachment) attachments.remove(attachment.id); + }} + charactersLeft={textInput.value.length} + isSending={status === "streaming"} + canSend={!disabled && (Boolean(textInput.value.trim()) || attachedFiles.length > 0)} + pendingClarification={false} + onSend={handleSend} + onStop={() => onStop?.()} + onFollowUp={() => undefined} + onSkip={() => undefined} + onEnterRoundtable={() => undefined} + enterButtonLabel="" + intentReady={null} + hideEnterRoundtable + onManualSave={() => undefined} + onTriggerToast={() => undefined} + modelOptions={modelOptions} + selectedModel={selectedModel} + onSelectedModelChange={handleModelChange} + commonQuestions={commonQuestions} /> ); } diff --git a/frontend-web/src/position-roundtable/components/PositionBuiltinAgentChat.tsx b/frontend-web/src/position-roundtable/components/PositionBuiltinAgentChat.tsx index e12e461c..0cec3208 100644 --- a/frontend-web/src/position-roundtable/components/PositionBuiltinAgentChat.tsx +++ b/frontend-web/src/position-roundtable/components/PositionBuiltinAgentChat.tsx @@ -716,7 +716,7 @@ export function PositionBuiltinAgentChat({ )} -
+
void; + commonQuestions?: (string | AgentQuestion)[] | null; } /** Keep attachment metadata presentation local to this position-only composer. */ @@ -64,40 +66,25 @@ export function PositionIntentInput({ modelOptions = [], selectedModel, onSelectedModelChange, + commonQuestions, }: PositionIntentInputProps) { // Files are owned by Step1Panel. This ref only opens the native picker and // forwards its selection through onAddFiles, so replacement layouts do not // accidentally create a second attachment state. const fileInputRef = useRef(null); + const [showMoreQuestions, setShowMoreQuestions] = useState(false); + const normalizedCommonQuestions = normalizeAgentQuestions(commonQuestions); + const visibleCommonQuestions = normalizedCommonQuestions.slice(0, 2); + const extraCommonQuestions = normalizedCommonQuestions.slice(2); + + const pickCommonQuestion = (question: AgentQuestion) => { + onValueChange(question.prompt); + setShowMoreQuestions(false); + }; return (
- {attachedFiles.length > 0 && ( -
- {attachedFiles.map((file, index) => ( - - - - {file.name} - - {formatFileSize(file.size)} - - - ))} -
- )} - - + + 上传附件
{charactersLeft}/500 @@ -154,8 +142,79 @@ export function PositionIntentInput({ Parent pages can still hide optional actions by their stable ids without changing this component's layout or global input styles. */} -
-
+
+
+ {visibleCommonQuestions.length > 0 && ( +
+ {visibleCommonQuestions.map((question, index) => ( + + ))} + {extraCommonQuestions.length > 0 && ( +
+ + {showMoreQuestions && ( +
+ {extraCommonQuestions.map((question, index) => ( + + ))} +
+ )} +
+ )} +
+ )} +
+ {attachedFiles.length > 0 && ( +
+ {attachedFiles.map((file, index) => ( + + + + {file.name} + + {formatFileSize(file.size)} + + + ))} +
+ )} +
+
fileInputRef.current?.click()} - title="上传参考文件" - aria-label="上传参考文件" - className="position-roundtable-intent-upload-btn flex size-10 items-center justify-center rounded-[4px] border border-slate-200 bg-white text-slate-500 transition hover:border-primary/45 hover:text-primary dark:border-[#2b8ffe]/35 dark:bg-[#102f63] dark:text-[#b9d3f7] dark:hover:bg-[#1a417b] dark:hover:text-[#73c8ff]" + title="上传附件" + aria-label="上传附件" + className="position-roundtable-intent-upload-btn flex items-center justify-center rounded-[24px] border-0" > - + + 上传附件
-
{modelOptions.length > 0 && (
- + onSelectedModelChange?.(String(value))} - className="w-[150px] shrink-0 [&_.t-input]:!h-8 [&_.t-input]:!border-0 [&_.t-input]:!bg-transparent [&_.t-input]:!px-1 [&_.t-input]:!text-xs [&_.t-input]:!font-bold [&_.t-input]:!shadow-none dark:[&_.t-input]:!text-[#dceaff]" + className="w-[150px] shrink-0 [&_.t-input]:!h-7 [&_.t-input]:!rounded-[4px] [&_.t-input]:!border-0 [&_.t-input]:!bg-transparent [&_.t-input]:!px-1 [&_.t-input]:!text-xs [&_.t-input]:!font-bold [&_.t-input]:!text-[#dceaff] [&_.t-input]:!shadow-none" popupProps={{ overlayClassName: "z-[80]" }} />
@@ -198,26 +257,20 @@ export function PositionIntentInput({ - {isSending && ( - - )} - + +
+ } + rightSidebar={ + !sandboxOpen && ( + + ) + } + hideQuestionCount + emptyState={ + intent.isIntentInitializing ? ( +
+
+ ) : ( +
+ +

开始提问

+

输入问题,获取基于任务的智能回答

+
+ ) + } + interactiveMessages={visibleIntentMessages} intentThreadId={intent.intentThreadId} intentReady={chainStarted ? frozenIntent ?? intent.intentReady : intent.intentReady} isIntentSending={intent.isIntentSending} @@ -2390,6 +2598,7 @@ export default function PositionRoundtablePage({ modelOptions={intentModelOptions} selectedModel={selectedModel} onSelectedModelChange={setSelectedModel} + commonQuestions={selectedAgentForPanel?.common_questions} /> )} onManualSave={() => { @@ -2612,6 +2821,7 @@ export default function PositionRoundtablePage({ value={selectedChainId} onChange={setSelectedChainId} onConfigure={openPositionConfig} + onDeleteChain={handleDeleteChain} /> )} {(!activePosition.isIntentPosition || isIntelligenceDeliveryOverview) && ( @@ -2623,14 +2833,25 @@ export default function PositionRoundtablePage({ collapsed={false} onCollapsedChange={() => undefined} /> - + {showIntentDeliveryOverview ? ( + + ) : ( + + )} )} @@ -2649,8 +2870,16 @@ export default function PositionRoundtablePage({ )} -
- {!rightPanelOpen && +
+ {!activePosition.isIntentPosition && + !rightPanelOpen && !sandboxOpen && !(positionSession && activeNode && !activeBuiltinKind) && (
- {activePosition.isIntentPosition && rightPanelOpen && !sandboxOpen && ( - - )} {!activePosition.isIntentPosition && ( setSelectedAgentId(null)} - intentObjective={frozenIntent?.objective ?? intent.intentReady?.objective} + selectedAgent={selectedAgentForPanel} + selectedAgentId={selectedAgentId} + positionName={activePosition.name} + specialAgentConfig={activePosition.isIntentPosition} + onSelectAgent={handleSelectAgent} + intentObjective={frozenIntent?.objective ?? intent.intentReady?.objective} intentCompletedAt={intentCompletedAt} assignedAgentIds={configurationAgentIds} hasSelectedChain={showClosureAgents || Boolean(selectedChain)} @@ -3888,6 +4085,7 @@ function PositionRoundtableEntrySidebar({ value, onChange, onConfigure, + onDeleteChain, }: { task: PositionTask; editable: boolean; @@ -3897,11 +4095,47 @@ function PositionRoundtableEntrySidebar({ value: string; onChange: (value: string) => void; onConfigure: (chainId: string) => void; + onDeleteChain: (chain: BusinessChain) => void | Promise; }) { + const navigate = useNavigate(); + const [searchQuery, setSearchQuery] = useState(""); + const [menuOpenId, setMenuOpenId] = useState(null); + const [menuPosition, setMenuPosition] = useState<{ top: number; left: number } | null>(null); + useEffect(() => { + if (!menuOpenId) return; + const closeMenu = () => { + setMenuOpenId(null); + setMenuPosition(null); + }; + window.addEventListener("resize", closeMenu); + window.addEventListener("scroll", closeMenu, true); + return () => { + window.removeEventListener("resize", closeMenu); + window.removeEventListener("scroll", closeMenu, true); + }; + }, [menuOpenId]); + const filteredChains = useMemo(() => { + const query = searchQuery.trim().toLocaleLowerCase(); + if (!query) return chains; + return chains.filter((chain) => + `${desensitize(chain.title)} ${desensitize(chain.description ?? "")}` + .toLocaleLowerCase() + .includes(query), + ); + }, [chains, searchQuery]); + const openChainEditor = (chainId?: string) => { + const returnTo = `${window.location.pathname}${window.location.search}`; + const chainQuery = chainId ? `chainId=${encodeURIComponent(chainId)}&` : ""; + navigate( + `/page/workspace/roundtable/chains?${chainQuery}mode=position&returnTo=${encodeURIComponent(returnTo)}`, + ); + }; + const displayDescription = task.id === DEFAULT_MOCK_POSITION_TASK_ID ? "围绕暑期活动的游泳夏令营开展宣传,结合线上线下渠道吸引目标人群参与,持续提升活动影响力。" : desensitize(task.description); + const menuChain = menuOpenId ? chains.find((chain) => chain.id === menuOpenId) : null; return (
@@ -3926,31 +4160,129 @@ function PositionRoundtableEntrySidebar({

作业链条选择

{chains.length} +
+ + +
{loading ? (
正在加载作业链条…
) : chains.length === 0 ? (
暂无可用作业链条
+ ) : filteredChains.length === 0 ? ( +
未找到匹配的作业链条
) : ( - chains.map((chain) => { + filteredChains.map((chain) => { const selected = chain.id === value; + const description = chain.description?.trim(); + const unassignedSeatCount = chain.seats.filter( + (seat) => !seat.position_id?.trim(), + ).length; return ( -
+