feat(ui): refine gem-blue acrylic palette and focus borders
Derive the gem-blue palette with OKLCH tonal and complementary-color rules while preserving acrylic opacity, blur and surface hierarchy. Darker focus tokens exposed stacked field rings, fallback outlines and persistent basemap selection rings. Keep fallback focus styles in the base layer and use one themed field boundary or inset card stroke. Preserve keyboard focus, invalid states and native input semantics. Add browser regression coverage for pointer and keyboard focus, all basemap presentations, narrow layouts and rendered contrast. Validation in an isolated checkout: pnpm lint, pnpm typecheck, pnpm test (393 tests), focused browser suites and pnpm build.
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { AgentExecutionPlan } from "@/features/agent";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Checkbox } from "@/shared/ui/checkbox";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
import { Disclosure } from "@/shared/ui/disclosure";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
@@ -122,8 +123,8 @@ export function AgentExecutionPlanPanel({
|
||||
|
||||
return (
|
||||
<section aria-label="工况任务处理" className="min-w-0 space-y-2.5">
|
||||
<section className="surface-reading overflow-hidden rounded-xl border border-slate-200/70">
|
||||
<div className="space-y-2 border-b border-slate-200/70 p-3">
|
||||
<section className="surface-reading overflow-hidden rounded-xl border border-[var(--surface-divider)]/70">
|
||||
<div className="space-y-2 border-b border-[var(--surface-divider)]/70 p-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h3 className="flex items-center gap-1.5 text-xs font-semibold text-slate-800">
|
||||
<Route size={13} aria-hidden="true" />
|
||||
@@ -136,7 +137,7 @@ export function AgentExecutionPlanPanel({
|
||||
<p className="text-xs leading-5 text-slate-600" role="status">
|
||||
{planSummary}
|
||||
</p>
|
||||
<p className="text-[11px] leading-5 text-slate-500">
|
||||
<p className="text-[11px] leading-5 text-[var(--ink-muted)]">
|
||||
{taskTypeLabel(task.task_type)} · 更新于 {formatDateTime(task.updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -145,7 +146,7 @@ export function AgentExecutionPlanPanel({
|
||||
) : plan ? (
|
||||
<ExecutionPlanSteps plan={plan} nodeActions={nodeActions} />
|
||||
) : (
|
||||
<p className="p-3 text-xs leading-5 text-slate-500">尚无执行节点,页面不会按时间推算或虚构完成进度。</p>
|
||||
<p className="p-3 text-xs leading-5 text-[var(--ink-muted)]">尚无执行节点,页面不会按时间推算或虚构完成进度。</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -281,13 +282,13 @@ function SemanticPath({ nodeId, stopId, plan, activeEdges, nodeById, nodeActions
|
||||
const branchEnd = join?.id ?? stopId;
|
||||
return <div className="space-y-2">
|
||||
<ExecutionNodeCard node={node} action={nodeActions[node.id]} />
|
||||
{node.kind === "decision" ? <div className="rounded-lg bg-blue-50/70 px-3 py-2 text-xs leading-5 text-slate-700">
|
||||
{node.kind === "decision" ? <div className="rounded-lg bg-[var(--action-soft)]/70 px-3 py-2 text-xs leading-5 text-slate-700">
|
||||
<span className="font-semibold">路线选择:</span>{allRoutes.find((edge) => edge.id === node.selected_edge_id)?.condition ?? allRoutes.find((edge) => edge.id === node.selected_edge_id)?.label ?? "等待判断"}
|
||||
<div className="mt-1 text-slate-500">{allRoutes.map((edge) => `${edge.condition ?? edge.label ?? "未命名路线"}${edge.id === node.selected_edge_id ? "(已选择)" : ""}`).join(" · ")}</div>
|
||||
<div className="mt-1 text-[var(--ink-muted)]">{allRoutes.map((edge) => `${edge.condition ?? edge.label ?? "未命名路线"}${edge.id === node.selected_edge_id ? "(已选择)" : ""}`).join(" · ")}</div>
|
||||
</div> : null}
|
||||
{node.kind === "parallel_split" ? <section aria-label={`并行处理 ${outgoing.length} 路`} className="min-w-0 rounded-xl border border-blue-100 bg-blue-50/40 p-2">
|
||||
<p className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-blue-800"><GitFork size={13} aria-hidden="true" />并行处理 · {outgoing.length} 路</p>
|
||||
<div className="grid gap-2 sm:grid-cols-2">{outgoing.map((edge, index) => <div key={edge.id ?? edge.to} className="min-w-0 rounded-lg border border-slate-200/70 bg-white p-2">
|
||||
{node.kind === "parallel_split" ? <section aria-label={`并行处理 ${outgoing.length} 路`} className="min-w-0 rounded-xl border border-[var(--surface-border)] bg-[var(--action-soft)]/40 p-2">
|
||||
<p className="mb-2 flex items-center gap-1.5 text-xs font-semibold text-[var(--action-blue-pressed)]"><GitFork size={13} aria-hidden="true" />并行处理 · {outgoing.length} 路</p>
|
||||
<div className="grid gap-2 sm:grid-cols-2">{outgoing.map((edge, index) => <div key={edge.id ?? edge.to} className="min-w-0 rounded-lg border border-[var(--surface-divider)]/70 bg-[var(--surface-reading)] p-2">
|
||||
<p className="mb-1.5 text-[11px] font-semibold text-slate-600">分支 {index + 1} · {edge.condition ?? edge.label ?? nodeById.get(edge.to)?.title}</p>
|
||||
<SemanticPath nodeId={edge.to} stopId={branchEnd} plan={plan} activeEdges={activeEdges} nodeById={nodeById} nodeActions={nodeActions} seen={nextSeen} />
|
||||
</div>)}</div>
|
||||
@@ -308,21 +309,21 @@ function findSemanticJoin(outgoing: ExecutionEdge[], activeEdges: ExecutionEdge[
|
||||
|
||||
function ExactDependencyList({ plan, nodeActions }: { plan: ExecutionPlanView; nodeActions: Record<string, ReactNode> }) {
|
||||
const titleById = new Map(plan.nodes.map((node) => [node.id, node.title]));
|
||||
return <div><p className="mb-2 text-[11px] leading-5 text-slate-500">历史流程按原始依赖展示;未标注路线或并行类型。</p><ol className="grid gap-2">{topologicalNodes(plan).map((node) => {
|
||||
return <div><p className="mb-2 text-[11px] leading-5 text-[var(--ink-muted)]">历史流程按原始依赖展示;未标注路线或并行类型。</p><ol className="grid gap-2">{topologicalNodes(plan).map((node) => {
|
||||
const prerequisites = plan.edges.filter((edge) => edge.to === node.id);
|
||||
return <li key={node.id}><ExecutionNodeCard node={node} action={nodeActions[node.id]} compact />{prerequisites.length > 0 ? <p className="px-2 pt-1 text-[11px] leading-5 text-slate-500">前置:{prerequisites.map((edge) => `${titleById.get(edge.from) ?? edge.from}${edge.condition || edge.label ? `(${edge.condition ?? edge.label})` : ""}`).join("、")}</p> : null}</li>;
|
||||
return <li key={node.id}><ExecutionNodeCard node={node} action={nodeActions[node.id]} compact />{prerequisites.length > 0 ? <p className="px-2 pt-1 text-[11px] leading-5 text-[var(--ink-muted)]">前置:{prerequisites.map((edge) => `${titleById.get(edge.from) ?? edge.from}${edge.condition || edge.label ? `(${edge.condition ?? edge.label})` : ""}`).join("、")}</p> : null}</li>;
|
||||
})}</ol></div>;
|
||||
}
|
||||
|
||||
function ExecutionNodeCard({ node, action, compact = false }: { node: ExecutionNode; action?: ReactNode; compact?: boolean }) {
|
||||
const { tone, Icon, label } = executionNodePresentation(node);
|
||||
return (
|
||||
<article data-execution-node={node.id} className={cn("surface-reading min-w-0 rounded-xl border border-slate-200/70", compact ? "px-2.5 py-2" : "px-3 py-2.5")}>
|
||||
<article data-execution-node={node.id} className={cn("surface-reading min-w-0 rounded-xl border border-[var(--surface-divider)]/70", compact ? "px-2.5 py-2" : "px-3 py-2.5")}>
|
||||
<div className="flex min-w-0 items-start gap-2">
|
||||
<StatusIcon tone={tone} activity={node.status === "running" ? "loading" : "static"} shape="circle" size="md" className="mt-0.5 shrink-0"><Icon size={14} /></StatusIcon>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center justify-between gap-2"><span className="text-xs font-semibold leading-5 text-slate-900">{node.title}</span><StatusBadge tone={tone}>{label}</StatusBadge></div>
|
||||
{node.kind && node.kind !== "activity" ? <p className="mt-0.5 text-[11px] font-medium text-blue-700">{{ decision: "条件判断", parallel_split: "并行分叉", parallel_join: "并行汇合", gate: "等待审核" }[node.kind]}</p> : null}
|
||||
{node.kind && node.kind !== "activity" ? <p className="mt-0.5 text-[11px] font-medium text-[var(--action-blue-hover)]">{{ decision: "条件判断", parallel_split: "并行分叉", parallel_join: "并行汇合", gate: "等待审核" }[node.kind]}</p> : null}
|
||||
<p className="mt-1 break-words text-xs leading-5 text-slate-700">{node.detail || (node.owner === "human" ? "等待人工补充执行事实。" : "等待 Agent 更新执行事实。")}</p>
|
||||
{action ? <div className="mt-2">{action}</div> : null}
|
||||
</div>
|
||||
@@ -338,11 +339,11 @@ function ExternalProgress({ evidence }: { evidence: OperationalTaskEvidence[] })
|
||||
{ordered.map((item, index) => (
|
||||
<li key={item.evidence_id} className="grid grid-cols-[28px_1fr] gap-2">
|
||||
<span className="relative flex justify-center">
|
||||
{index < ordered.length - 1 ? <span className="absolute bottom-[-0.65rem] top-7 w-px bg-slate-200" aria-hidden="true" /> : null}
|
||||
{index < ordered.length - 1 ? <span className="absolute bottom-[-0.65rem] top-7 w-px bg-[var(--surface-divider)]" aria-hidden="true" /> : null}
|
||||
<StatusIcon tone="info" shape="circle" size="md" className="relative z-10 ring-4 ring-white"><Clock3 size={14} /></StatusIcon>
|
||||
</span>
|
||||
<article className="surface-reading rounded-xl border border-slate-200/70 px-3 py-2.5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2"><span className="text-xs font-semibold text-slate-900">{item.source_label}</span><span className="font-mono text-[11px] text-slate-500">{formatDateTime(item.observed_at)}</span></div>
|
||||
<article className="surface-reading rounded-xl border border-[var(--surface-divider)]/70 px-3 py-2.5">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2"><span className="text-xs font-semibold text-slate-900">{item.source_label}</span><span className="font-mono text-[11px] text-[var(--ink-muted)]">{formatDateTime(item.observed_at)}</span></div>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-700">{item.summary}</p>
|
||||
</article>
|
||||
</li>
|
||||
@@ -357,9 +358,9 @@ function EvidenceSection({ evidence, referencedIds, onLoadSnapshot }: { evidence
|
||||
return [...evidence].sort((left, right) => Number(referenced.has(right.evidence_id)) - Number(referenced.has(left.evidence_id)));
|
||||
}, [evidence, referencedIds]);
|
||||
return (
|
||||
<section aria-label="关键证据" className="surface-reading rounded-xl border border-slate-200/70 p-3">
|
||||
<div className="flex items-center justify-between gap-2"><h3 className="flex items-center gap-1.5 text-xs font-semibold text-slate-800"><ShieldCheck size={13} />关键证据</h3><span className="text-[11px] text-slate-500">{ordered.length} 条</span></div>
|
||||
{ordered.length > 0 ? <div className="mt-2 grid gap-1.5">{ordered.map((item) => <EvidenceDisclosure key={item.evidence_id} item={item} onLoadSnapshot={onLoadSnapshot} />)}</div> : <p className="mt-2 text-xs leading-5 text-slate-500">尚无可核验的支撑证据。</p>}
|
||||
<section aria-label="关键证据" className="surface-reading rounded-xl border border-[var(--surface-divider)]/70 p-3">
|
||||
<div className="flex items-center justify-between gap-2"><h3 className="flex items-center gap-1.5 text-xs font-semibold text-slate-800"><ShieldCheck size={13} />关键证据</h3><span className="text-[11px] text-[var(--ink-muted)]">{ordered.length} 条</span></div>
|
||||
{ordered.length > 0 ? <div className="mt-2 grid gap-1.5">{ordered.map((item) => <EvidenceDisclosure key={item.evidence_id} item={item} onLoadSnapshot={onLoadSnapshot} />)}</div> : <p className="mt-2 text-xs leading-5 text-[var(--ink-muted)]">尚无可核验的支撑证据。</p>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -374,8 +375,8 @@ function EvidenceDisclosure({ item, onLoadSnapshot }: { item: OperationalTaskEvi
|
||||
catch (nextError) { setError(nextError instanceof Error ? nextError.message : "数据快照读取失败。"); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
return <Disclosure label={<span className="line-clamp-2 text-left text-xs leading-5 text-slate-700">{item.summary}</span>} className="surface-control rounded-lg border border-slate-200/70 px-2.5">
|
||||
<div className="border-t border-slate-200/70 py-2">
|
||||
return <Disclosure label={<span className="line-clamp-2 text-left text-xs leading-5 text-slate-700">{item.summary}</span>} className="surface-control rounded-lg border border-[var(--surface-divider)]/70 px-2.5">
|
||||
<div className="border-t border-[var(--surface-divider)]/70 py-2">
|
||||
<dl className="grid gap-1 text-[11px] leading-5"><EvidenceMeta label="来源" value={item.source_label} /><EvidenceMeta label="时间" value={formatDateTime(item.observed_at)} /><EvidenceMeta label="类型" value={evidenceKindLabel(item.kind)} />{item.source_ref ? <EvidenceMeta label="引用" value={item.source_ref} mono /> : null}</dl>
|
||||
{item.has_snapshot && !snapshot ? <Button className="mt-2" variant="outline" size="sm" disabled={loading} onClick={() => void load()}>{loading ? <Loader2 className="animate-spin" /> : null}查看数据快照</Button> : null}
|
||||
{error ? <p role="alert" className="mt-2 text-xs text-red-700">{error}</p> : null}
|
||||
@@ -386,7 +387,7 @@ function EvidenceDisclosure({ item, onLoadSnapshot }: { item: OperationalTaskEvi
|
||||
|
||||
function SnapshotSummary({ snapshot }: { snapshot: OperationalTaskDataSnapshot }) {
|
||||
const rows = snapshotRows(snapshot.data).slice(0, 12);
|
||||
return <section aria-label="数据快照" className="mt-2 rounded-lg bg-white px-2.5 py-2 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.3)]"><p className="text-[11px] font-semibold text-slate-700">{snapshot.source_ref} · 查询于 {formatDateTime(snapshot.queried_at)}</p><dl className="mt-1 grid gap-1 text-[11px] leading-5">{rows.map(([key, value]) => <EvidenceMeta key={key} label={key} value={value} />)}</dl>{snapshot.truncated ? <p className="mt-1 text-[11px] text-amber-700">数据已按有界查询范围截断。</p> : null}</section>;
|
||||
return <section aria-label="数据快照" className="mt-2 rounded-lg bg-[var(--surface-reading)] px-2.5 py-2 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.3)]"><p className="text-[11px] font-semibold text-slate-700">{snapshot.source_ref} · 查询于 {formatDateTime(snapshot.queried_at)}</p><dl className="mt-1 grid gap-1 text-[11px] leading-5">{rows.map(([key, value]) => <EvidenceMeta key={key} label={key} value={value} />)}</dl>{snapshot.truncated ? <p className="mt-1 text-[11px] text-[var(--status-warning-foreground)]">数据已按有界查询范围截断。</p> : null}</section>;
|
||||
}
|
||||
|
||||
function snapshotRows(value: unknown, prefix = ""): [string, string][] {
|
||||
@@ -396,7 +397,7 @@ function snapshotRows(value: unknown, prefix = ""): [string, string][] {
|
||||
}
|
||||
|
||||
function EvidenceMeta({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) {
|
||||
return <div className="grid grid-cols-[48px_1fr] gap-2"><dt className="font-semibold text-slate-500">{label}</dt><dd className={cn("break-words text-slate-700", mono && "font-mono")}>{value}</dd></div>;
|
||||
return <div className="grid grid-cols-[48px_1fr] gap-2"><dt className="font-semibold text-[var(--ink-muted)]">{label}</dt><dd className={cn("break-words text-slate-700", mono && "font-mono")}>{value}</dd></div>;
|
||||
}
|
||||
|
||||
export function AccessRequestCard({ request, nodeTitle, canDecide, onDecision, onRefresh }: {
|
||||
@@ -419,14 +420,14 @@ export function AccessRequestCard({ request, nodeTitle, canDecide, onDecision, o
|
||||
if (/409|版本|conflict/i.test(String(nextError))) onRefresh();
|
||||
} finally { setBusy(false); }
|
||||
};
|
||||
return <article className="surface-reading rounded-xl border border-amber-200/80 p-3">
|
||||
return <article className="surface-reading rounded-xl border border-[var(--status-warning-border)]/80 p-3">
|
||||
<div className="flex items-center justify-between gap-2"><h3 className="text-xs font-semibold text-slate-900">数据访问申请</h3><StatusBadge tone={request.status === "approved" ? "success" : request.status === "rejected" ? "danger" : "warning"}>{request.status === "approved" ? "已批准" : request.status === "rejected" ? "已拒绝" : "待审核"}</StatusBadge></div>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-700">{request.reason}</p>
|
||||
<p className="mt-1 text-[11px] text-slate-500">关联步骤 {nodeTitle ?? request.node_id} · 申请于 {formatDateTime(request.requested_at)}</p>
|
||||
<div className="mt-2 grid gap-1">{request.scopes.map((scope) => <label key={scope} className="flex min-h-10 items-center gap-2 text-xs text-slate-700"><input type="checkbox" checked={selected.includes(scope)} disabled={request.status !== "pending" || !canDecide || busy} onChange={(event) => setSelected((current) => event.target.checked ? [...current, scope] : current.filter((item) => item !== scope))} />{dataScopeLabel(scope)}</label>)}</div>
|
||||
<p className="mt-1 text-[11px] text-[var(--ink-muted)]">关联步骤 {nodeTitle ?? request.node_id} · 申请于 {formatDateTime(request.requested_at)}</p>
|
||||
<div className="mt-2 grid gap-1">{request.scopes.map((scope) => <label key={scope} className="flex min-h-10 items-center gap-2 text-xs text-slate-700"><Checkbox checked={selected.includes(scope)} disabled={request.status !== "pending" || !canDecide || busy} onChange={(event) => setSelected((current) => event.target.checked ? [...current, scope] : current.filter((item) => item !== scope))} />{dataScopeLabel(scope)}</label>)}</div>
|
||||
{request.status !== "pending" ? <p className="mt-1 text-xs text-slate-600">{request.status === "approved" ? `已授权:${request.approved_scopes.map(dataScopeLabel).join("、")}` : "未授予数据访问权限"}</p> : null}
|
||||
{request.status === "pending" && canDecide && onDecision ? <div className="mt-2 flex justify-end gap-2"><Button variant="outline" size="sm" disabled={busy} onClick={() => void decide("rejected")}>拒绝</Button><Button size="sm" disabled={busy || selected.length === 0} onClick={() => void decide("approved")}>批准所选范围</Button></div> : null}
|
||||
{request.status === "pending" && !canDecide ? <p className="mt-2 text-xs text-slate-500">等待具有该数据源读取权限的项目成员审核。</p> : null}
|
||||
{request.status === "pending" && !canDecide ? <p className="mt-2 text-xs text-[var(--ink-muted)]">等待具有该数据源读取权限的项目成员审核。</p> : null}
|
||||
{error ? <p role="alert" className="mt-2 text-xs text-red-700">{error}</p> : null}
|
||||
</article>;
|
||||
}
|
||||
@@ -478,25 +479,25 @@ export function HumanActionCard({ task, action, onDecision, onExecute, onDraftUp
|
||||
} finally { setSubmitting(null); }
|
||||
};
|
||||
return (
|
||||
<article className="surface-reading rounded-xl border border-amber-200/80 p-3" data-action-id={action.action_id}>
|
||||
<article className="surface-reading rounded-xl border border-[var(--status-warning-border)]/80 p-3" data-action-id={action.action_id}>
|
||||
<div className="flex items-start gap-2.5">
|
||||
<StatusIcon tone="warning" shape="circle" size="md" className="mt-0.5"><UserCheck size={14} /></StatusIcon>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2"><h3 className="text-xs font-semibold text-slate-900">{command ? "命令下发确认" : "工单创建确认"}</h3><div className="flex flex-wrap gap-1.5"><StatusBadge tone={approvalTone(action.approval_status)}>{approvalLabel(action.approval_status)}</StatusBadge><StatusBadge tone={executionTone(action.execution_status)}>{executionLabel(action.execution_status)}</StatusBadge></div></div>
|
||||
<p className="mt-2 text-xs leading-5 text-slate-700"><span className="font-semibold text-slate-500">原因:</span>{action.reason}</p>
|
||||
<p className="mt-2 text-xs leading-5 text-slate-700"><span className="font-semibold text-[var(--ink-muted)]">原因:</span>{action.reason}</p>
|
||||
<ActionPayloadSummary payload={action.payload} />
|
||||
{editing && !command ? <div className="mt-2 grid gap-2 rounded-lg border border-slate-200 bg-slate-50 p-2.5">
|
||||
{editing && !command ? <div className="mt-2 grid gap-2 rounded-lg border border-[var(--surface-divider)] bg-[var(--surface-reading)] p-2.5">
|
||||
<Input aria-label="工单标题" value={String(draft.title ?? "")} onChange={(event) => setDraft({ ...draft, title: event.target.value })} />
|
||||
<Input aria-label="工单说明" value={String(draft.description ?? "")} onChange={(event) => setDraft({ ...draft, description: event.target.value })} />
|
||||
<div className="grid grid-cols-2 gap-2"><select aria-label="工单类型" className="min-h-10 rounded-lg border border-slate-200 bg-white px-2 text-xs" value={String(draft.work_order_type ?? "general")} onChange={(event) => setDraft({ ...draft, work_order_type: event.target.value })}><option value="general">一般工单</option><option value="water_quality_complaint">水质投诉</option><option value="burst_report">爆管上报</option></select><select aria-label="工单优先级" className="min-h-10 rounded-lg border border-slate-200 bg-white px-2 text-xs" value={String(draft.priority ?? "normal")} onChange={(event) => setDraft({ ...draft, priority: event.target.value })}><option value="normal">普通</option><option value="urgent">紧急</option></select></div>
|
||||
<div className="grid grid-cols-2 gap-2"><select aria-label="工单类型" className="form-field min-h-10 rounded-lg border border-[var(--surface-divider)] bg-[var(--surface-reading)] px-2 text-xs" value={String(draft.work_order_type ?? "general")} onChange={(event) => setDraft({ ...draft, work_order_type: event.target.value })}><option value="general">一般工单</option><option value="water_quality_complaint">水质投诉</option><option value="burst_report">爆管上报</option></select><select aria-label="工单优先级" className="form-field min-h-10 rounded-lg border border-[var(--surface-divider)] bg-[var(--surface-reading)] px-2 text-xs" value={String(draft.priority ?? "normal")} onChange={(event) => setDraft({ ...draft, priority: event.target.value })}><option value="normal">普通</option><option value="urgent">紧急</option></select></div>
|
||||
<Input aria-label="工单位置" value={String(draft.location ?? "")} onChange={(event) => setDraft({ ...draft, location: event.target.value || null })} />
|
||||
<Input aria-label="处理班组" value={String(draft.assignee ?? "")} onChange={(event) => setDraft({ ...draft, assignee: event.target.value || null })} />
|
||||
<Input aria-label="预约时间" type="datetime-local" value={toLocalDateTime(draft.scheduled_at)} onChange={(event) => setDraft({ ...draft, scheduled_at: event.target.value ? new Date(event.target.value).toISOString() : null })} />
|
||||
<div className="flex justify-end gap-2"><Button variant="outline" size="sm" onClick={() => { setDraft(action.payload); setEditing(false); }}>取消编辑</Button><Button size="sm" disabled={submitting !== null || !String(draft.title ?? "").trim() || !String(draft.description ?? "").trim()} onClick={() => void run("save")}>保存草稿</Button></div>
|
||||
</div> : null}
|
||||
{action.evidence_refs.length > 0 ? (
|
||||
<div className="mt-2 rounded-lg bg-blue-50/70 px-2.5 py-2 text-xs leading-5 text-slate-700">
|
||||
<p className="font-semibold text-blue-700">确认依据</p>
|
||||
<div className="mt-2 rounded-lg bg-[var(--action-soft)]/70 px-2.5 py-2 text-xs leading-5 text-slate-700">
|
||||
<p className="font-semibold text-[var(--action-blue-hover)]">确认依据</p>
|
||||
<ul className="mt-1 grid gap-1">
|
||||
{action.evidence_refs.map((evidenceId) => {
|
||||
const evidence = task.evidence.find((item) => item.evidence_id === evidenceId);
|
||||
@@ -506,12 +507,12 @@ export function HumanActionCard({ task, action, onDecision, onExecute, onDraftUp
|
||||
</div>
|
||||
) : null}
|
||||
{action.receipt ? <ActionReceipt receipt={action.receipt} /> : null}
|
||||
{action.execution_detail && !action.receipt ? <p className="mt-2 rounded-lg bg-slate-50 px-2.5 py-2 text-xs leading-5 text-slate-700">{action.execution_detail}</p> : null}
|
||||
<Disclosure label="查看原始参数" className="surface-control mt-2 rounded-lg border border-slate-200/70 px-2.5"><pre className="max-h-52 overflow-auto whitespace-pre-wrap break-all border-t border-slate-200/70 py-2 font-mono text-[11px] leading-5 text-slate-700">{JSON.stringify(action.payload, null, 2)}</pre></Disclosure>
|
||||
<p className="mt-2 text-[11px] leading-5 text-slate-500">确认版本 v{action.version}。内容或版本变化后必须重新确认。</p>
|
||||
{action.execution_detail && !action.receipt ? <p className="mt-2 rounded-lg bg-[var(--surface-reading)] px-2.5 py-2 text-xs leading-5 text-slate-700">{action.execution_detail}</p> : null}
|
||||
<Disclosure label="查看原始参数" className="surface-control mt-2 rounded-lg border border-[var(--surface-divider)]/70 px-2.5"><pre className="max-h-52 overflow-auto whitespace-pre-wrap break-all border-t border-[var(--surface-divider)]/70 py-2 font-mono text-[11px] leading-5 text-slate-700">{JSON.stringify(action.payload, null, 2)}</pre></Disclosure>
|
||||
<p className="mt-2 text-[11px] leading-5 text-[var(--ink-muted)]">确认版本 v{action.version}。内容或版本变化后必须重新确认。</p>
|
||||
{error ? <p role="alert" className="mt-2 text-xs leading-5 text-red-700">{error} 当前状态未按成功处理。</p> : null}
|
||||
{stale ? <Button variant="outline" size="sm" className="mt-2" onClick={() => { setStale(false); onRefresh(); }}><RefreshCw size={14} />刷新后重新确认</Button> : null}
|
||||
{!canDecide && action.approval_status === "pending" ? <p className="mt-2 rounded-lg bg-slate-50 px-2.5 py-2 text-xs leading-5 text-slate-600">当前账号仅可查看,需要具备此动作审批权限的人员确认。</p> : null}
|
||||
{!canDecide && action.approval_status === "pending" ? <p className="mt-2 rounded-lg bg-[var(--surface-reading)] px-2.5 py-2 text-xs leading-5 text-slate-600">当前账号仅可查看,需要具备此动作审批权限的人员确认。</p> : null}
|
||||
{action.approval_status === "pending" && canDecide ? <Input className="mt-2" aria-label="退回理由" placeholder="退回理由,拒绝动作时必填" value={rejectComment} onChange={(event) => setRejectComment(event.target.value)} /> : null}
|
||||
<div className="mt-3 flex flex-wrap justify-end gap-2">
|
||||
{action.approval_status === "pending" && canDecide ? <><Button variant="outline" size="sm" disabled={submitting !== null} onClick={() => void run("reject")}>拒绝动作</Button>{!command && onDraftUpdate ? <Button variant="outline" size="sm" disabled={submitting !== null || editing} onClick={() => { setDraft(action.payload); setEditing(true); }}>编辑草稿</Button> : null}<Button size="sm" disabled={submitting !== null || editing || savedVersion === action.version || (!command && !!onConfirmDispatch && !canExecute)} onClick={() => void run(!command && onConfirmDispatch ? "confirm" : "approve")}>{submitting === "approve" || submitting === "confirm" ? <Loader2 className="animate-spin" /> : null}{command ? "批准下发命令" : onConfirmDispatch ? "确认并下发工单" : "批准创建工单"}</Button></> : null}
|
||||
@@ -544,7 +545,7 @@ function ActionPayloadSummary({ payload }: { payload: Record<string, unknown> })
|
||||
}
|
||||
return [{ key, value }];
|
||||
});
|
||||
return <dl className="surface-control mt-2 grid gap-1 rounded-lg border border-slate-200/70 px-2.5 py-2 text-xs leading-5">{rows.map((row) => <div key={row.key} className="grid grid-cols-[76px_1fr] gap-2"><dt className="font-semibold text-slate-500">{payloadLabel(row.key)}</dt><dd className="break-words text-slate-800">{payloadValue(row.key, row.value)}</dd></div>)}</dl>;
|
||||
return <dl className="surface-control mt-2 grid gap-1 rounded-lg border border-[var(--surface-divider)]/70 px-2.5 py-2 text-xs leading-5">{rows.map((row) => <div key={row.key} className="grid grid-cols-[76px_1fr] gap-2"><dt className="font-semibold text-[var(--ink-muted)]">{payloadLabel(row.key)}</dt><dd className="break-words text-slate-800">{payloadValue(row.key, row.value)}</dd></div>)}</dl>;
|
||||
}
|
||||
|
||||
function payloadLabel(key: string) {
|
||||
|
||||
@@ -307,7 +307,7 @@ export function AgentTaskTicker({
|
||||
key={runningCondition.id}
|
||||
className={cn(
|
||||
"block h-1.5 w-1.5 rounded-sm transition-colors duration-150",
|
||||
index === activeIndicatorIndex ? "bg-blue-600" : "bg-slate-300"
|
||||
index === activeIndicatorIndex ? "bg-[var(--action-blue)]" : "bg-slate-300"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
@@ -391,7 +391,7 @@ function TickerTaskCardContent({ view }: { view: TickerStackCard["view"] }) {
|
||||
<div className="grid h-full grid-cols-[14px_minmax(0,1fr)] items-center gap-3 pr-4">
|
||||
<TickerStatusDot progressValue={view.progressValue} />
|
||||
<div className="min-w-0">
|
||||
<span className="block truncate text-[13px] font-semibold leading-5 text-slate-500">
|
||||
<span className="block truncate text-[13px] font-semibold leading-5 text-[var(--ink-muted)]">
|
||||
{view.title}
|
||||
</span>
|
||||
{view.progressValue !== undefined ? (
|
||||
@@ -407,7 +407,7 @@ function TickerTaskCardContent({ view }: { view: TickerStackCard["view"] }) {
|
||||
<TickerStatusDot progressValue={view.progressValue} />
|
||||
<div className="flex min-w-0 flex-col justify-center">
|
||||
<div className="min-w-0 pr-1">
|
||||
<span className="block truncate text-[13px] font-semibold leading-4 text-slate-500">
|
||||
<span className="block truncate text-[13px] font-semibold leading-4 text-[var(--ink-muted)]">
|
||||
{view.title}
|
||||
</span>
|
||||
</div>
|
||||
@@ -417,7 +417,7 @@ function TickerTaskCardContent({ view }: { view: TickerStackCard["view"] }) {
|
||||
{view.currentStep.title}
|
||||
</span>
|
||||
<span className="h-1 w-1 rounded-full bg-slate-300" aria-hidden="true" />
|
||||
<span className="min-w-0 truncate text-[13px] font-medium leading-5 text-slate-500">
|
||||
<span className="min-w-0 truncate text-[13px] font-medium leading-5 text-[var(--ink-muted)]">
|
||||
{view.currentStep.detail}
|
||||
</span>
|
||||
</div>
|
||||
@@ -441,7 +441,7 @@ function TickerProgressBar({
|
||||
<div
|
||||
className={cn(
|
||||
"mt-0.5 h-0.5 overflow-hidden rounded-full",
|
||||
muted ? "bg-slate-200/60" : "bg-slate-200/80"
|
||||
muted ? "bg-[var(--surface-divider)]/60" : "bg-[var(--surface-divider)]/80"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
|
||||
@@ -101,7 +101,7 @@ export function AnalysisHistorySection({
|
||||
icon={<Archive size={15} />}
|
||||
tone="info"
|
||||
meta={loadedRef.current ? (
|
||||
<span className="rounded-md bg-slate-100 px-2 py-0.5 text-[11px] font-medium tabular-nums text-slate-600">
|
||||
<span className="rounded-md bg-[var(--surface-control)] px-2 py-0.5 text-[11px] font-medium tabular-nums text-slate-600">
|
||||
已加载 {items.length} 项
|
||||
</span>
|
||||
) : null}
|
||||
@@ -112,10 +112,10 @@ export function AnalysisHistorySection({
|
||||
if (event.currentTarget.open && !loadedRef.current && !loading) void load();
|
||||
}}
|
||||
>
|
||||
<div className="mt-1 border-t border-slate-200 pt-3">
|
||||
<div className="mt-1 border-t border-[var(--surface-divider)] pt-3">
|
||||
{error ? <WorkbenchNotice role="alert" tone="danger" className="mb-3">{error}</WorkbenchNotice> : null}
|
||||
{loading && !items.length ? (
|
||||
<div role="status" className="flex min-h-20 items-center justify-center gap-2 text-xs text-slate-500">
|
||||
<div role="status" className="flex min-h-20 items-center justify-center gap-2 text-xs text-[var(--ink-muted)]">
|
||||
<LoaderCircle size={15} className="animate-spin" />正在读取分析成果历史
|
||||
</div>
|
||||
) : null}
|
||||
@@ -135,9 +135,9 @@ export function AnalysisHistorySection({
|
||||
aria-label={`打开分析成果:${item.name}`}
|
||||
disabled={Boolean(openingRunId)}
|
||||
onClick={() => void openArtifact(item)}
|
||||
className="group flex min-h-[68px] w-full items-center gap-3 rounded-xl border border-slate-200 bg-white px-3 py-2.5 text-left shadow-xs transition-[border-color,background-color,scale] duration-150 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)] [@media(hover:hover)]:hover:border-blue-200 [@media(hover:hover)]:hover:bg-blue-50/55 active:scale-[0.99] motion-reduce:transition-none motion-reduce:active:scale-100 disabled:cursor-wait disabled:opacity-60"
|
||||
className="group flex min-h-[68px] w-full items-center gap-3 rounded-xl border border-[var(--surface-divider)] bg-[var(--surface-reading)] px-3 py-2.5 text-left shadow-xs transition-[border-color,background-color,scale] duration-150 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)] [@media(hover:hover)]:hover:border-[var(--surface-border)] [@media(hover:hover)]:hover:bg-[var(--action-soft)]/55 active:scale-[0.99] motion-reduce:transition-none motion-reduce:active:scale-100 disabled:cursor-wait disabled:opacity-60"
|
||||
>
|
||||
<span className="grid size-8 shrink-0 place-items-center rounded-lg bg-blue-50 text-blue-700">
|
||||
<span className="grid size-8 shrink-0 place-items-center rounded-lg bg-[var(--action-soft)] text-[var(--action-blue-hover)]">
|
||||
{openingRunId === item.runId
|
||||
? <LoaderCircle size={15} className="animate-spin" />
|
||||
: <BarChart3 size={15} />}
|
||||
@@ -147,14 +147,14 @@ export function AnalysisHistorySection({
|
||||
<span className="truncate text-xs font-semibold text-slate-900">{item.name}</span>
|
||||
<StatusBadge tone={statusTone(item.status)}>{item.statusLabel}</StatusBadge>
|
||||
</span>
|
||||
<span className="mt-1 block truncate text-[11px] leading-4 text-slate-500">
|
||||
<span className="mt-1 block truncate text-[11px] leading-4 text-[var(--ink-muted)]">
|
||||
{item.runTypeLabel} · {formatHistoryTime(item.createdAt)} · {item.createdBy}
|
||||
</span>
|
||||
<span className="mt-0.5 block text-[11px] leading-4 text-slate-400">
|
||||
<span className="mt-0.5 block text-[11px] leading-4 text-[var(--ink-muted)]">
|
||||
{item.hasResults ? "已有持久化结果" : "仅有运行记录"}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight size={14} className="shrink-0 text-slate-400 transition-colors group-hover:text-blue-600" />
|
||||
<ChevronRight size={14} className="shrink-0 text-[var(--ink-muted)] transition-colors group-hover:text-[var(--action-blue-hover)]" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -22,13 +22,13 @@ export function AnalysisResultsPanel({
|
||||
aria-label="项目分析成果"
|
||||
className="acrylic-panel absolute bottom-3 right-3 top-16 z-[60] flex w-[min(560px,calc(100%-1.5rem))] flex-col overflow-hidden rounded-2xl border text-slate-900 shadow-[0_16px_44px_rgba(15,23,42,0.18)]"
|
||||
>
|
||||
<header className="flex h-14 shrink-0 items-center gap-3 border-b border-slate-200/80 px-4">
|
||||
<span className="grid h-8 w-8 place-items-center rounded-lg bg-blue-600 text-white">
|
||||
<header className="flex h-14 shrink-0 items-center gap-3 border-b border-[var(--surface-divider)]/80 px-4">
|
||||
<span className="grid h-8 w-8 place-items-center rounded-lg bg-[var(--action-blue)] text-white">
|
||||
<BarChart3 size={17} aria-hidden="true" />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="text-sm font-semibold">项目分析成果</h2>
|
||||
<p className="truncate text-xs text-slate-500">查看已写入当前项目 BizDB 的分析结果</p>
|
||||
<p className="truncate text-xs text-[var(--ink-muted)]">查看已写入当前项目 BizDB 的分析结果</p>
|
||||
</div>
|
||||
<Button size="icon" variant="ghost" aria-label="关闭分析成果面板" onClick={onClose}>
|
||||
<X size={17} />
|
||||
|
||||
@@ -31,10 +31,10 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
|
||||
|
||||
return (
|
||||
<aside className="acrylic-panel pointer-events-auto flex h-full min-h-0 flex-col rounded border">
|
||||
<div className="border-b border-slate-200/80 p-4">
|
||||
<div className="border-b border-[var(--surface-divider)]/80 p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-xs font-medium tracking-[0.14em] text-slate-500">要素详情</p>
|
||||
<p className="text-xs font-medium tracking-[0.14em] text-[var(--ink-muted)]">要素详情</p>
|
||||
<h2 className="mt-1 text-lg font-semibold text-slate-950">
|
||||
{feature ? feature.title : "选择一个管网对象"}
|
||||
</h2>
|
||||
@@ -44,7 +44,7 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
|
||||
aria-label="关闭详情"
|
||||
title="关闭详情"
|
||||
onClick={onClose}
|
||||
className="surface-control grid h-9 w-9 place-items-center rounded border text-slate-500 hover:text-slate-900"
|
||||
className="surface-control grid h-9 w-9 place-items-center rounded border text-[var(--ink-muted)] hover:text-slate-900"
|
||||
>
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
@@ -60,14 +60,14 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{metrics.map(({ label, value }) => (
|
||||
<div key={label} className="surface-reading rounded border p-3">
|
||||
<p className="text-xs text-slate-500">{label}</p>
|
||||
<p className="text-xs text-[var(--ink-muted)]">{label}</p>
|
||||
<p className="mt-1 truncate text-sm font-semibold text-slate-900 tabular-nums">{value}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="material-tone-agent mt-4 rounded border border-violet-100 p-3">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-violet-800">
|
||||
<section className="material-tone-agent mt-4 rounded border border-[var(--surface-border)] p-3">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-[var(--action-blue-pressed)]">
|
||||
<TriangleAlert size={16} aria-hidden="true" />
|
||||
Agent 解释
|
||||
</div>
|
||||
@@ -85,11 +85,11 @@ export function FeatureInsightPanel({ feature, onClose }: FeatureInsightPanelPro
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full flex-col items-center justify-center text-center">
|
||||
<div className="surface-reading grid h-12 w-12 place-items-center rounded border text-slate-500">
|
||||
<div className="surface-reading grid h-12 w-12 place-items-center rounded border text-[var(--ink-muted)]">
|
||||
<Target size={22} aria-hidden="true" />
|
||||
</div>
|
||||
<p className="mt-3 text-sm font-medium text-slate-800">等待地图选择</p>
|
||||
<p className="mt-1 max-w-[260px] text-sm leading-6 text-slate-500">
|
||||
<p className="mt-1 max-w-[260px] text-sm leading-6 text-[var(--ink-muted)]">
|
||||
选择要素后,可查看基础属性、模拟入口与 Agent 会话联动结果。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -51,10 +51,10 @@ const FEATURE_LAYER_META: Partial<Record<
|
||||
}
|
||||
>> = {
|
||||
pipes: { icon: PipeFeatureIcon, label: "管线", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-teal-700 text-white ring-teal-900/10" },
|
||||
junctions: { icon: JunctionFeatureIcon, label: "节点", headerClassName: "bg-blue-50/70", labelClassName: "text-blue-800", iconClassName: "bg-sky-700 text-white ring-sky-900/10" },
|
||||
valves: { icon: ValveFeatureIcon, label: "阀门", headerClassName: "bg-violet-50/70", labelClassName: "text-violet-800", iconClassName: "bg-violet-600 text-white ring-violet-900/10" },
|
||||
junctions: { icon: JunctionFeatureIcon, label: "节点", headerClassName: "bg-[var(--action-soft)]/70", labelClassName: "text-[var(--action-blue-pressed)]", iconClassName: "bg-sky-700 text-white ring-sky-900/10" },
|
||||
valves: { icon: ValveFeatureIcon, label: "阀门", headerClassName: "bg-[var(--action-soft)]/70", labelClassName: "text-[var(--action-blue-pressed)]", iconClassName: "bg-[var(--action-blue)] text-white ring-[var(--action-blue)]/10" },
|
||||
reservoirs: { icon: ReservoirFeatureIcon, label: "水库", headerClassName: "bg-sky-50/70", labelClassName: "text-sky-900", iconClassName: "bg-sky-800 text-white ring-sky-900/10" },
|
||||
scada: { icon: ScadaFeatureIcon, label: "SCADA", headerClassName: "bg-slate-50/80", labelClassName: "text-slate-800", iconClassName: "bg-slate-700 text-white ring-slate-900/10" },
|
||||
scada: { icon: ScadaFeatureIcon, label: "SCADA", headerClassName: "bg-[var(--surface-reading)]/80", labelClassName: "text-slate-800", iconClassName: "bg-slate-700 text-white ring-slate-900/10" },
|
||||
pumps: { icon: PumpFeatureIcon, label: "泵", headerClassName: "bg-cyan-50/70", labelClassName: "text-cyan-800", iconClassName: "bg-cyan-800 text-white ring-cyan-900/10" },
|
||||
tanks: { icon: ReservoirFeatureIcon, label: "水箱", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-teal-700 text-white ring-teal-900/10" },
|
||||
water_quality_monitoring_points: { icon: ScadaFeatureIcon, label: "水质监测点", headerClassName: "bg-teal-50/70", labelClassName: "text-teal-800", iconClassName: "bg-teal-700 text-white ring-teal-900/10" }
|
||||
@@ -63,7 +63,7 @@ const FEATURE_LAYER_META: Partial<Record<
|
||||
const SOURCE_GIS_LAYER_META = {
|
||||
icon: JunctionFeatureIcon,
|
||||
label: "GIS 交付要素",
|
||||
headerClassName: "bg-slate-50/80",
|
||||
headerClassName: "bg-[var(--surface-reading)]/80",
|
||||
labelClassName: "text-slate-700",
|
||||
iconClassName: "bg-slate-600 text-white ring-slate-900/10"
|
||||
};
|
||||
@@ -164,12 +164,12 @@ export function FeaturePopover({
|
||||
|
||||
<div className="flex items-center gap-0.5">
|
||||
{canCopy ? (
|
||||
<button type="button" aria-label={copied ? "编号已复制" : "复制编号"} title={copied ? "编号已复制" : "复制编号"} onClick={() => void handleCopyId()} className="relative grid h-10 w-10 shrink-0 place-items-center rounded-xl text-slate-500 [@media(hover:hover)]:hover:bg-white/70 [@media(hover:hover)]:hover:text-slate-950">
|
||||
<button type="button" aria-label={copied ? "编号已复制" : "复制编号"} title={copied ? "编号已复制" : "复制编号"} onClick={() => void handleCopyId()} className="relative grid h-10 w-10 shrink-0 place-items-center rounded-xl text-[var(--ink-muted)] [@media(hover:hover)]:hover:bg-[var(--surface-reading)]/70 [@media(hover:hover)]:hover:text-slate-950">
|
||||
<Copy size={15} aria-hidden="true" className={cn("absolute transition-[opacity,transform] duration-150", copied ? "scale-90 opacity-0" : "scale-100 opacity-100")} />
|
||||
<Check size={16} aria-hidden="true" className={cn("status-tone-success absolute text-[var(--status-foreground)] transition-[opacity,transform] duration-150", copied ? "scale-100 opacity-100" : "scale-90 opacity-0")} />
|
||||
</button>
|
||||
) : null}
|
||||
<button type="button" aria-label="关闭要素信息" title="关闭要素信息" onClick={onClose} className="grid h-10 w-10 shrink-0 place-items-center rounded-xl text-slate-500 [@media(hover:hover)]:hover:bg-white/70 [@media(hover:hover)]:hover:text-slate-950">
|
||||
<button type="button" aria-label="关闭要素信息" title="关闭要素信息" onClick={onClose} className="grid h-10 w-10 shrink-0 place-items-center rounded-xl text-[var(--ink-muted)] [@media(hover:hover)]:hover:bg-[var(--surface-reading)]/70 [@media(hover:hover)]:hover:text-slate-950">
|
||||
<X size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -179,20 +179,20 @@ export function FeaturePopover({
|
||||
<div className="min-h-0 overflow-y-auto overscroll-contain px-4 py-3">
|
||||
{panelModel.attributes.length > 0 ? (
|
||||
<>
|
||||
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-slate-500">
|
||||
<div className="mb-2 flex items-center gap-2 text-xs font-semibold text-[var(--ink-muted)]">
|
||||
<Database size={14} aria-hidden="true" />
|
||||
设施属性
|
||||
</div>
|
||||
<FeaturePropertyList entries={panelModel.attributes} variant="popover" />
|
||||
</>
|
||||
) : (
|
||||
<p className="py-3 text-center text-sm text-slate-500">暂无可展示属性</p>
|
||||
<p className="py-3 text-center text-sm text-[var(--ink-muted)]">暂无可展示属性</p>
|
||||
)}
|
||||
|
||||
{gisProperties.groups.length > 0 || gisProperties.status === "error" ? (
|
||||
<Disclosure
|
||||
key={featureKey}
|
||||
className="mt-2 border-t border-slate-100 pt-1"
|
||||
className="mt-2 border-t border-[var(--surface-divider)] pt-1"
|
||||
onToggle={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
setExpandedGisFeatureKey(event.currentTarget.open ? featureKey : null);
|
||||
@@ -201,7 +201,7 @@ export function FeaturePopover({
|
||||
label={(
|
||||
<span className="flex min-w-0 flex-1 items-center justify-between gap-3">
|
||||
<span>GIS 台账属性</span>
|
||||
<span className="shrink-0 text-[11px] font-normal text-slate-400">
|
||||
<span className="shrink-0 text-[11px] font-normal text-[var(--ink-muted)]">
|
||||
{gisProperties.status === "error"
|
||||
? "读取失败"
|
||||
: `${gisProperties.groups.length} 份`}
|
||||
@@ -217,30 +217,30 @@ export function FeaturePopover({
|
||||
return (
|
||||
<Disclosure
|
||||
key={`${group.layer_key}:${group.source_fid}`}
|
||||
className="border-b border-slate-100 last:border-b-0"
|
||||
className="border-b border-[var(--surface-divider)] last:border-b-0"
|
||||
label={(
|
||||
<span className="min-w-0">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate text-slate-700">{group.layer_name}</span>
|
||||
<span className="shrink-0 rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-500">
|
||||
<span className="shrink-0 rounded-full bg-[var(--surface-control)] px-1.5 py-0.5 text-[10px] font-medium text-[var(--ink-muted)]">
|
||||
{presentation.datasetRole}
|
||||
</span>
|
||||
</span>
|
||||
<span className="block text-[11px] font-normal text-slate-400 tabular-nums">
|
||||
<span className="block text-[11px] font-normal text-[var(--ink-muted)] tabular-nums">
|
||||
{group.source_date} · {presentation.usefulCount} 个有效字段
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
>
|
||||
<div className="space-y-2 pb-3 pl-6">
|
||||
<p className="text-[11px] leading-5 text-slate-500">
|
||||
<p className="text-[11px] leading-5 text-[var(--ink-muted)]">
|
||||
{presentation.description}
|
||||
</p>
|
||||
{presentation.summary.length > 0 ? (
|
||||
<dl className="grid grid-cols-2 gap-1.5 sm:grid-cols-3">
|
||||
{presentation.summary.map((entry) => (
|
||||
<div key={entry.key} className="min-w-0 rounded-lg border border-slate-100 bg-white/70 px-2.5 py-2">
|
||||
<dt className="truncate text-[10px] font-medium text-slate-400">{entry.label}</dt>
|
||||
<div key={entry.key} className="min-w-0 rounded-lg border border-[var(--surface-divider)] bg-[var(--surface-reading)]/70 px-2.5 py-2">
|
||||
<dt className="truncate text-[10px] font-medium text-[var(--ink-muted)]">{entry.label}</dt>
|
||||
<dd className="mt-0.5 truncate text-xs font-semibold text-slate-800" title={entry.value}>{entry.value}</dd>
|
||||
</div>
|
||||
))}
|
||||
@@ -249,7 +249,7 @@ export function FeaturePopover({
|
||||
{presentation.sections.map((section) => (
|
||||
<Disclosure
|
||||
key={section.id}
|
||||
className="border-t border-slate-100"
|
||||
className="border-t border-[var(--surface-divider)]"
|
||||
label={`${section.label} · ${section.entries.length} 项`}
|
||||
>
|
||||
<div className="pb-2 pl-6">
|
||||
@@ -258,11 +258,11 @@ export function FeaturePopover({
|
||||
</Disclosure>
|
||||
))}
|
||||
<Disclosure
|
||||
className="border-t border-slate-100"
|
||||
className="border-t border-[var(--surface-divider)]"
|
||||
label={`全部原始字段 · ${presentation.rawCount} 项`}
|
||||
>
|
||||
<div className="pb-2 pl-6">
|
||||
<p className="pb-2 text-[11px] leading-5 text-slate-400">
|
||||
<p className="pb-2 text-[11px] leading-5 text-[var(--ink-muted)]">
|
||||
包含源系统编码、空值和兼容字段,仅用于核对原始数据。
|
||||
</p>
|
||||
<FeaturePropertyList entries={presentation.rawEntries} variant="popover" />
|
||||
|
||||
@@ -11,12 +11,12 @@ export function FeaturePropertyList({
|
||||
variant = "insight"
|
||||
}: FeaturePropertyListProps) {
|
||||
if (entries.length === 0) {
|
||||
return <p className="py-3 text-center text-sm text-slate-500">暂无可展示属性</p>;
|
||||
return <p className="py-3 text-center text-sm text-[var(--ink-muted)]">暂无可展示属性</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<dl className={cn(
|
||||
"surface-reading divide-y divide-slate-100 border",
|
||||
"surface-reading divide-y divide-[var(--surface-divider)] border",
|
||||
variant === "popover" ? "rounded-xl px-3" : "rounded"
|
||||
)}>
|
||||
{entries.map((entry) => (
|
||||
@@ -30,7 +30,7 @@ export function FeaturePropertyList({
|
||||
)}
|
||||
>
|
||||
<dt className={cn(
|
||||
"text-slate-500",
|
||||
"text-[var(--ink-muted)]",
|
||||
variant === "popover" ? "text-xs font-medium leading-5" : "truncate"
|
||||
)}>
|
||||
{entry.label}
|
||||
@@ -40,7 +40,7 @@ export function FeaturePropertyList({
|
||||
variant === "popover"
|
||||
? "break-words text-right text-sm font-semibold leading-5 tabular-nums"
|
||||
: "truncate",
|
||||
entry.value === "暂无" && "font-medium text-slate-400"
|
||||
entry.value === "暂无" && "font-medium text-[var(--ink-muted)]"
|
||||
)}>
|
||||
{entry.value}
|
||||
</dd>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function IconButton({ label, children, onClick, active = false }: IconBut
|
||||
className={`grid h-10 w-10 place-items-center rounded border transition ${
|
||||
active
|
||||
? "border-slate-800 bg-slate-900 text-white"
|
||||
: "surface-control border-transparent text-slate-700 hover:bg-white"
|
||||
: "surface-control border-transparent text-slate-700 hover:bg-[var(--surface-reading)]"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -234,10 +234,10 @@ export function MapDevPanel({
|
||||
className="acrylic-panel pointer-events-auto absolute right-3 top-[4.5rem] z-40 hidden max-h-[calc(100dvh-5.5rem)] w-[400px] flex-col overflow-hidden rounded-2xl border lg:flex"
|
||||
data-testid="map-dev-panel"
|
||||
>
|
||||
<div className="flex h-12 shrink-0 items-center justify-between border-b border-slate-200/70 px-4">
|
||||
<div className="flex h-12 shrink-0 items-center justify-between border-b border-[var(--surface-divider)]/70 px-4">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-slate-950">地图能力 Dev Panel</h2>
|
||||
<p className="text-[11px] text-slate-500">仅本次页面会话生效</p>
|
||||
<p className="text-[11px] text-[var(--ink-muted)]">仅本次页面会话生效</p>
|
||||
</div>
|
||||
<Button
|
||||
size="icon"
|
||||
@@ -519,11 +519,11 @@ export function MapDevPanel({
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
<div className="space-y-2.5 rounded-xl bg-slate-100/75 p-3 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.18)]">
|
||||
<div className="space-y-2.5 rounded-xl bg-[var(--surface-control)]/75 p-3 shadow-[inset_0_0_0_1px_rgba(148,163,184,0.18)]">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h4 className="text-[11px] font-semibold text-slate-800">整层标注</h4>
|
||||
<p className="mt-0.5 text-[10px] leading-4 text-slate-500">
|
||||
<p className="mt-0.5 text-[10px] leading-4 text-[var(--ink-muted)]">
|
||||
作用于当前图层所有可见 Feature,自动避让重叠。
|
||||
</p>
|
||||
</div>
|
||||
@@ -623,7 +623,7 @@ export function MapDevPanel({
|
||||
</PanelSection>
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 border-t border-slate-200/70 p-3">
|
||||
<div className="shrink-0 border-t border-[var(--surface-divider)]/70 p-3">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => commands.resetAll()}
|
||||
@@ -648,10 +648,10 @@ function PanelSection({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="space-y-2.5 border-b border-slate-200/70 px-1 pb-4 last:border-b-0 last:pb-1">
|
||||
<section className="space-y-2.5 border-b border-[var(--surface-divider)]/70 px-1 pb-4 last:border-b-0 last:pb-1">
|
||||
<div>
|
||||
<h3 className="text-xs font-semibold text-slate-900">{title}</h3>
|
||||
<p className="mt-0.5 text-[11px] leading-4 text-slate-500">{description}</p>
|
||||
<p className="mt-0.5 text-[11px] leading-4 text-[var(--ink-muted)]">{description}</p>
|
||||
</div>
|
||||
{children}
|
||||
</section>
|
||||
@@ -677,7 +677,7 @@ function ToggleField({
|
||||
onChange: (checked: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<label className="flex h-9 cursor-pointer items-center gap-2 rounded-lg bg-white/80 px-2.5 text-[11px] font-medium text-slate-700 shadow-[inset_0_0_0_1px_rgba(203,213,225,0.9)]">
|
||||
<label className="flex h-9 cursor-pointer items-center gap-2 rounded-lg bg-[var(--surface-reading)]/80 px-2.5 text-[11px] font-medium text-slate-700 shadow-[inset_0_0_0_1px_rgba(203,213,225,0.9)]">
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={(event) => onChange(event.target.checked)}
|
||||
@@ -699,7 +699,7 @@ function ColorField({
|
||||
}) {
|
||||
return (
|
||||
<Field label={label}>
|
||||
<div className="flex h-9 items-center gap-2 rounded-lg border border-slate-200 bg-white/80 px-2">
|
||||
<div className="flex h-9 items-center gap-2 rounded-lg border border-[var(--surface-divider)] bg-[var(--surface-reading)]/80 px-2">
|
||||
<input
|
||||
type="color"
|
||||
value={value}
|
||||
@@ -737,7 +737,7 @@ function RangeField({
|
||||
step={step}
|
||||
value={value}
|
||||
onChange={(event) => onChange(Number(event.target.value))}
|
||||
className="h-6 w-full accent-blue-600"
|
||||
className="h-6 w-full accent-[var(--action-blue)]"
|
||||
/>
|
||||
</Field>
|
||||
);
|
||||
@@ -755,4 +755,4 @@ function StatusText({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
const inputClassName =
|
||||
"h-10 w-full rounded-lg border border-slate-200 bg-white/80 px-2.5 text-xs text-slate-800 outline-hidden transition focus:border-blue-400 focus:ring-2 focus:ring-blue-500/15";
|
||||
"h-10 w-full rounded-lg border border-[var(--surface-divider)] bg-[var(--surface-reading)]/80 px-2.5 text-xs text-slate-800 transition-colors";
|
||||
|
||||
@@ -160,7 +160,7 @@ export function MobileWorkbenchSheet({
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
>
|
||||
<span className="h-1 w-10 rounded-full bg-slate-400/70 transition-[width,background-color] group-focus-visible:w-12 group-focus-visible:bg-blue-600" />
|
||||
<span className="h-1 w-10 rounded-full bg-slate-400/70 transition-[width,background-color] group-focus-visible:w-12 group-focus-visible:bg-[var(--action-blue)]" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 overscroll-contain">{children}</div>
|
||||
|
||||
@@ -59,9 +59,9 @@ import {
|
||||
import { getConditionReportRiskLevel } from "./condition-report-risk";
|
||||
|
||||
const RUNNING_WORKFLOW_TICK_MS = 1_000;
|
||||
const NEUTRAL_SURFACE_CLASS_NAME = "surface-reading rounded-xl border border-slate-200/70";
|
||||
const INSET_SURFACE_CLASS_NAME = "surface-control rounded-xl border border-slate-200/70";
|
||||
const REPORT_META_TILE_CLASS_NAME = "surface-control rounded-lg border border-slate-200/70 px-2 py-1.5";
|
||||
const NEUTRAL_SURFACE_CLASS_NAME = "surface-reading rounded-xl border border-[var(--surface-divider)]/70";
|
||||
const INSET_SURFACE_CLASS_NAME = "surface-control rounded-xl border border-[var(--surface-divider)]/70";
|
||||
const REPORT_META_TILE_CLASS_NAME = "surface-control rounded-lg border border-[var(--surface-divider)]/70 px-2 py-1.5";
|
||||
|
||||
const riskTones = {
|
||||
normal: "success",
|
||||
@@ -283,7 +283,7 @@ function ConditionRecordDetailPanel({
|
||||
) : (
|
||||
<DetailSectionCard>
|
||||
<SectionTitle icon={ClipboardList} title="工单报告" />
|
||||
<p className="mt-2 text-xs leading-5 text-slate-500">尚未同步到工单详情,请刷新工况任务后查看。</p>
|
||||
<p className="mt-2 text-xs leading-5 text-[var(--ink-muted)]">尚未同步到工单详情,请刷新工况任务后查看。</p>
|
||||
</DetailSectionCard>
|
||||
)
|
||||
) : condition.operationalTask ? (
|
||||
@@ -486,7 +486,7 @@ function WorkOrderDetailPanel({ condition }: { condition: ScheduledWorkOrderItem
|
||||
<DetailHeader
|
||||
condition={condition}
|
||||
icon={ClipboardList}
|
||||
iconClassName="bg-blue-50 text-blue-700"
|
||||
iconClassName="bg-[var(--action-soft)] text-[var(--action-blue-hover)]"
|
||||
hideDetail
|
||||
/>
|
||||
<DetailDecisionBody>
|
||||
@@ -538,7 +538,7 @@ function getConditionDetailHeader(condition: ScheduledConditionRecord) {
|
||||
if (condition.status === "running") {
|
||||
return {
|
||||
icon: Loader2,
|
||||
iconClassName: "bg-blue-600 text-white",
|
||||
iconClassName: "bg-[var(--action-blue)] text-white",
|
||||
iconSpin: true
|
||||
};
|
||||
}
|
||||
@@ -601,7 +601,7 @@ function DetailHeader({
|
||||
<div className="flex min-w-0 items-start gap-2.5">
|
||||
<span
|
||||
className={cn(
|
||||
"grid h-9 w-9 shrink-0 place-items-center bg-slate-100 text-slate-600",
|
||||
"grid h-9 w-9 shrink-0 place-items-center bg-[var(--surface-control)] text-slate-600",
|
||||
"rounded-lg",
|
||||
iconClassName
|
||||
)}
|
||||
@@ -610,7 +610,7 @@ function DetailHeader({
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-mono text-xs font-semibold text-slate-500">{formatTime(condition.scheduledAt)}</span>
|
||||
<span className="font-mono text-xs font-semibold text-[var(--ink-muted)]">{formatTime(condition.scheduledAt)}</span>
|
||||
{condition.kind === "work_order" || condition.status === "running" || condition.status === "pending" ? (
|
||||
<StatusPill condition={condition} />
|
||||
) : null}
|
||||
@@ -671,26 +671,26 @@ function SolutionOptionsList({
|
||||
return (
|
||||
<section className={cn(NEUTRAL_SURFACE_CLASS_NAME, "px-3 py-3")}>
|
||||
<SectionEyebrow icon={Route} label="演示处置方案" />
|
||||
<div className={cn(INSET_SURFACE_CLASS_NAME, "mt-2 divide-y divide-slate-200/70 overflow-hidden")}>
|
||||
<div className={cn(INSET_SURFACE_CLASS_NAME, "mt-2 divide-y divide-[var(--surface-divider)]/70 overflow-hidden")}>
|
||||
{options.map((option, index) => (
|
||||
<article key={option.id} className="px-2.5 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="surface-reading grid h-5 w-5 shrink-0 place-items-center rounded-lg border border-slate-200/70 text-xs font-semibold text-slate-600">
|
||||
<span className="surface-reading grid h-5 w-5 shrink-0 place-items-center rounded-lg border border-[var(--surface-divider)]/70 text-xs font-semibold text-slate-600">
|
||||
{index + 1}
|
||||
</span>
|
||||
<h4 className="min-w-0 truncate text-xs font-semibold leading-4 text-slate-900">{option.title}</h4>
|
||||
</div>
|
||||
<dl className="mt-2 grid gap-1.5 text-xs leading-5">
|
||||
<div className="grid grid-cols-[56px_1fr] gap-2">
|
||||
<dt className="font-semibold text-slate-400">适用</dt>
|
||||
<dt className="font-semibold text-[var(--ink-muted)]">适用</dt>
|
||||
<dd className="text-slate-700">{option.scenario}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[56px_1fr] gap-2">
|
||||
<dt className="font-semibold text-slate-400">动作</dt>
|
||||
<dt className="font-semibold text-[var(--ink-muted)]">动作</dt>
|
||||
<dd className="text-slate-700">{option.action}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[56px_1fr] gap-2">
|
||||
<dt className="font-semibold text-slate-400">代价</dt>
|
||||
<dt className="font-semibold text-[var(--ink-muted)]">代价</dt>
|
||||
<dd className="text-slate-700">{option.tradeoff}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
@@ -755,7 +755,7 @@ function IncidentWorkOrderPanel({
|
||||
"rounded-xl",
|
||||
submitted
|
||||
? "status-tone-success cursor-not-allowed bg-[var(--status-soft)] text-[var(--status-foreground)]"
|
||||
: "bg-blue-600 text-white hover:bg-blue-700"
|
||||
: "bg-[var(--action-blue)] text-white hover:bg-[var(--action-blue-hover)]"
|
||||
)}
|
||||
>
|
||||
{submitted ? <CheckCircle2 size={14} aria-hidden="true" /> : <Send size={14} aria-hidden="true" />}
|
||||
@@ -773,7 +773,7 @@ function IncidentOperationList({
|
||||
submitted: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn(INSET_SURFACE_CLASS_NAME, "divide-y divide-slate-200/70 overflow-hidden")}>
|
||||
<div className={cn(INSET_SURFACE_CLASS_NAME, "divide-y divide-[var(--surface-divider)]/70 overflow-hidden")}>
|
||||
{operations.map((operation) => (
|
||||
<article key={`${operation.target}-${operation.action}`} className="px-2.5 py-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
@@ -784,15 +784,15 @@ function IncidentOperationList({
|
||||
</div>
|
||||
<dl className="mt-1.5 grid gap-1 text-xs leading-5">
|
||||
<div className="grid grid-cols-[56px_1fr] gap-2">
|
||||
<dt className="font-semibold text-slate-400">对象</dt>
|
||||
<dt className="font-semibold text-[var(--ink-muted)]">对象</dt>
|
||||
<dd className="text-slate-700">{operation.target}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[56px_1fr] gap-2">
|
||||
<dt className="font-semibold text-slate-400">指令</dt>
|
||||
<dt className="font-semibold text-[var(--ink-muted)]">指令</dt>
|
||||
<dd className="text-slate-700">{operation.command}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[56px_1fr] gap-2">
|
||||
<dt className="font-semibold text-slate-400">复核</dt>
|
||||
<dt className="font-semibold text-[var(--ink-muted)]">复核</dt>
|
||||
<dd className="text-slate-700">{operation.verification}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
@@ -806,10 +806,10 @@ function EscalationCriteriaList({ items }: { items: string[] }) {
|
||||
return (
|
||||
<section className={cn(NEUTRAL_SURFACE_CLASS_NAME, "px-3 py-3")}>
|
||||
<SectionEyebrow icon={AlertTriangle} label="升级条件" />
|
||||
<ol className={cn(INSET_SURFACE_CLASS_NAME, "mt-2 divide-y divide-slate-200/70 overflow-hidden")}>
|
||||
<ol className={cn(INSET_SURFACE_CLASS_NAME, "mt-2 divide-y divide-[var(--surface-divider)]/70 overflow-hidden")}>
|
||||
{items.map((item, index) => (
|
||||
<li key={item} className="grid grid-cols-[18px_1fr] gap-2 px-2 py-1.5 text-xs leading-5 text-slate-700">
|
||||
<span className="surface-reading mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg border border-slate-200/70 text-xs font-semibold text-slate-600">
|
||||
<span className="surface-reading mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg border border-[var(--surface-divider)]/70 text-xs font-semibold text-slate-600">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span>{item}</span>
|
||||
@@ -837,7 +837,7 @@ function ConditionStatusReport({
|
||||
|
||||
return (
|
||||
<section className={cn(NEUTRAL_SURFACE_CLASS_NAME, "overflow-hidden")}>
|
||||
<div className="border-b border-slate-200/70 px-3 py-3">
|
||||
<div className="border-b border-[var(--surface-divider)]/70 px-3 py-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<SectionTitle icon={FileText} title="完成状态报告" />
|
||||
<LifecyclePill />
|
||||
@@ -849,7 +849,7 @@ function ConditionStatusReport({
|
||||
<RiskBadge riskLevel={riskLevel} />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs leading-5 text-slate-500">
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs leading-5 text-[var(--ink-muted)]">
|
||||
<Clock3 size={13} aria-hidden="true" />
|
||||
<span>报告时段</span>
|
||||
<span className="font-mono font-semibold tabular-nums text-slate-700">{statusTime}</span>
|
||||
@@ -868,7 +868,7 @@ function ConditionStatusReport({
|
||||
) : null}
|
||||
|
||||
{report?.sections?.length ? (
|
||||
<div className="grid gap-2 border-t border-slate-200/70 px-3 py-3">
|
||||
<div className="grid gap-2 border-t border-[var(--surface-divider)]/70 px-3 py-3">
|
||||
{report.sections.map((section) => (
|
||||
<ReportSection key={section.title} title={section.title} items={section.items} />
|
||||
))}
|
||||
@@ -904,7 +904,7 @@ function RiskBadge({ riskLevel }: { riskLevel: ScheduledConditionRiskLevel }) {
|
||||
function ReportMetaTile({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className={REPORT_META_TILE_CLASS_NAME}>
|
||||
<div className="text-xs font-semibold text-slate-400">{label}</div>
|
||||
<div className="text-xs font-semibold text-[var(--ink-muted)]">{label}</div>
|
||||
<div className="mt-0.5 truncate text-xs font-semibold text-slate-700" title={value}>
|
||||
{value}
|
||||
</div>
|
||||
@@ -921,7 +921,7 @@ function ReportSection({
|
||||
}) {
|
||||
return (
|
||||
<section className={cn(INSET_SURFACE_CLASS_NAME, "px-2.5 py-2")}>
|
||||
<div className="text-xs font-semibold text-slate-500">{title}</div>
|
||||
<div className="text-xs font-semibold text-[var(--ink-muted)]">{title}</div>
|
||||
<ul className="mt-1.5 grid gap-1">
|
||||
{items.map((item) => (
|
||||
<li key={item} className="grid grid-cols-[7px_1fr] gap-2 text-xs leading-5 text-slate-700">
|
||||
@@ -936,15 +936,15 @@ function ReportSection({
|
||||
|
||||
function KpiTile({ kpi }: { kpi: ScheduledConditionKpi }) {
|
||||
return (
|
||||
<div className="surface-reading min-w-0 rounded-xl border border-slate-200/70 px-2.5 py-2">
|
||||
<div className="surface-reading min-w-0 rounded-xl border border-[var(--surface-divider)]/70 px-2.5 py-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-xs font-semibold text-slate-500" title={kpi.label}>
|
||||
<div className="truncate text-xs font-semibold text-[var(--ink-muted)]" title={kpi.label}>
|
||||
{kpi.label}
|
||||
</div>
|
||||
<div className="mt-1 flex items-baseline gap-1">
|
||||
<span className="font-mono text-lg font-semibold leading-5 text-slate-900">{kpi.value}</span>
|
||||
{kpi.unit ? <span className="text-xs font-semibold text-slate-500">{kpi.unit}</span> : null}
|
||||
{kpi.unit ? <span className="text-xs font-semibold text-[var(--ink-muted)]">{kpi.unit}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge tone={riskTones[kpi.status]}>
|
||||
@@ -953,7 +953,7 @@ function KpiTile({ kpi }: { kpi: ScheduledConditionKpi }) {
|
||||
</div>
|
||||
<p className="mt-1.5 text-xs leading-4 text-slate-600">{kpi.description}</p>
|
||||
{kpi.threshold || kpi.baseline ? (
|
||||
<div className="mt-1.5 space-y-0.5 text-xs leading-4 text-slate-500">
|
||||
<div className="mt-1.5 space-y-0.5 text-xs leading-4 text-[var(--ink-muted)]">
|
||||
{kpi.threshold ? <div>阈值:{kpi.threshold}</div> : null}
|
||||
{kpi.baseline ? <div>基线:{kpi.baseline}</div> : null}
|
||||
</div>
|
||||
@@ -995,8 +995,8 @@ function DecisionActionStrip({
|
||||
"flex min-h-9 shrink-0 items-center justify-center gap-1.5 px-3 text-xs font-semibold leading-4 transition-colors min-[1500px]:w-[104px]",
|
||||
"rounded-xl",
|
||||
actionDisabled
|
||||
? "surface-control cursor-not-allowed border border-slate-200/70 text-slate-500 opacity-80"
|
||||
: "bg-blue-600 text-white hover:bg-blue-700"
|
||||
? "surface-control cursor-not-allowed border border-[var(--surface-divider)]/70 text-[var(--ink-muted)] opacity-80"
|
||||
: "bg-[var(--action-blue)] text-white hover:bg-[var(--action-blue-hover)]"
|
||||
)}
|
||||
>
|
||||
<MessageSquare size={14} aria-hidden="true" />
|
||||
@@ -1014,7 +1014,7 @@ function SectionEyebrow({
|
||||
label: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-500">
|
||||
<div className="flex items-center gap-1.5 text-xs font-semibold text-[var(--ink-muted)]">
|
||||
{Icon ? <Icon size={13} aria-hidden="true" /> : null}
|
||||
<span>{label}</span>
|
||||
</div>
|
||||
@@ -1173,7 +1173,7 @@ function DetailSectionCard({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("surface-reading rounded-xl border border-slate-200/70 px-2.5 py-2.5 text-slate-500", className)}>
|
||||
<div className={cn("surface-reading rounded-xl border border-[var(--surface-divider)]/70 px-2.5 py-2.5 text-[var(--ink-muted)]", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -1200,7 +1200,7 @@ function TaskExecutionPanel({
|
||||
}) {
|
||||
return (
|
||||
<section className={cn(NEUTRAL_SURFACE_CLASS_NAME, "overflow-hidden")}>
|
||||
<div className="border-b border-slate-200/70 px-3 py-3">
|
||||
<div className="border-b border-[var(--surface-divider)]/70 px-3 py-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
{Icon ? (
|
||||
<SectionTitle icon={Icon} title={title} />
|
||||
@@ -1233,7 +1233,7 @@ function TaskExecutionPanel({
|
||||
/>
|
||||
))}
|
||||
</ol>
|
||||
{children ? <div className="grid gap-2 border-t border-slate-200/70 px-3 py-3">{children}</div> : null}
|
||||
{children ? <div className="grid gap-2 border-t border-[var(--surface-divider)]/70 px-3 py-3">{children}</div> : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1249,7 +1249,7 @@ function TaskExecutionStepRow({
|
||||
<li className="grid grid-cols-[28px_1fr] gap-2">
|
||||
<span className="relative flex justify-center">
|
||||
{!isLast ? (
|
||||
<span className="absolute bottom-[-0.65rem] top-7 w-px bg-slate-200" aria-hidden="true" />
|
||||
<span className="absolute bottom-[-0.65rem] top-7 w-px bg-[var(--surface-divider)]" aria-hidden="true" />
|
||||
) : null}
|
||||
<SemanticStatusIcon
|
||||
tone={getTaskExecutionStepTone(step.status)}
|
||||
@@ -1270,7 +1270,7 @@ function TaskExecutionStepRow({
|
||||
</SemanticStatusIcon>
|
||||
</span>
|
||||
<span
|
||||
className="surface-reading min-w-0 rounded-xl border border-slate-200/70 px-2.5 py-2"
|
||||
className="surface-reading min-w-0 rounded-xl border border-[var(--surface-divider)]/70 px-2.5 py-2"
|
||||
>
|
||||
<span className="flex min-w-0 flex-wrap items-center justify-between gap-2">
|
||||
<span className="text-xs font-semibold text-slate-900">{step.title}</span>
|
||||
@@ -1279,10 +1279,10 @@ function TaskExecutionStepRow({
|
||||
</StatusBadge>
|
||||
</span>
|
||||
{step.meta?.length ? (
|
||||
<span className="mt-1 grid gap-1 text-xs leading-4 text-slate-500 sm:grid-cols-[68px_1fr]">
|
||||
<span className="mt-1 grid gap-1 text-xs leading-4 text-[var(--ink-muted)] sm:grid-cols-[68px_1fr]">
|
||||
{step.meta.map((item) => (
|
||||
<span key={`${item.label}-${item.value}`} className="contents">
|
||||
<span className="font-semibold text-slate-400">{item.label}</span>
|
||||
<span className="font-semibold text-[var(--ink-muted)]">{item.label}</span>
|
||||
<span className={item.label === "时间" ? "tabular-nums text-slate-700" : "text-slate-700"}>{item.value}</span>
|
||||
</span>
|
||||
))}
|
||||
@@ -1304,18 +1304,18 @@ export function WorkOrderReportPanel({ condition }: { condition: ScheduledWorkOr
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<DetailSectionCard className="overflow-hidden p-0">
|
||||
<div className="border-b border-slate-200/70 px-3 py-3">
|
||||
<div className="border-b border-[var(--surface-divider)]/70 px-3 py-3">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
<p className="text-[10px] font-semibold text-slate-400">工单报告</p>
|
||||
<p className="text-[10px] font-semibold text-[var(--ink-muted)]">工单报告</p>
|
||||
<SectionTitle icon={ClipboardList} title="任务执行" />
|
||||
</div>
|
||||
<StatusBadge tone={getStatusTone(condition.status)}>{progress.label}</StatusBadge>
|
||||
</div>
|
||||
<p className="mt-2 whitespace-pre-wrap break-words text-xs leading-5 text-slate-700">{condition.summary}</p>
|
||||
<p className="mt-1 break-all text-[11px] text-slate-500">工单编号:{condition.code}</p>
|
||||
<p className="mt-1 break-all text-[11px] text-[var(--ink-muted)]">工单编号:{condition.code}</p>
|
||||
<section aria-label="派单信息" className="mt-3">
|
||||
<h4 className="mb-1.5 text-[11px] font-semibold text-slate-500">派单信息</h4>
|
||||
<h4 className="mb-1.5 text-[11px] font-semibold text-[var(--ink-muted)]">派单信息</h4>
|
||||
<dl className="grid grid-cols-2 gap-1.5 sm:grid-cols-3">
|
||||
<WorkOrderField label="来源" value={condition.source} />
|
||||
<WorkOrderField label="位置" value={condition.location ?? "未填写"} />
|
||||
@@ -1341,20 +1341,20 @@ export function WorkOrderReportPanel({ condition }: { condition: ScheduledWorkOr
|
||||
<ol className="mt-2 grid gap-1.5">
|
||||
{condition.feedbackRequirements.map((item, index) => (
|
||||
<li key={`${index}-${item}`} className="flex gap-2 text-xs leading-5 text-slate-700">
|
||||
<span className="grid size-5 shrink-0 place-items-center rounded-full border border-blue-100 bg-white text-[11px] font-semibold text-blue-700">{index + 1}</span>
|
||||
<span className="grid size-5 shrink-0 place-items-center rounded-full border border-[var(--surface-border)] bg-[var(--surface-reading)] text-[11px] font-semibold text-[var(--action-blue-hover)]">{index + 1}</span>
|
||||
<span className="min-w-0 break-words">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : <p className="mt-2 text-xs text-slate-500">未填写回执要求</p>}
|
||||
<div className="mt-3 border-t border-slate-200/70 pt-3">
|
||||
<p className="text-[11px] font-semibold text-slate-500">现场反馈</p>
|
||||
) : <p className="mt-2 text-xs text-[var(--ink-muted)]">未填写回执要求</p>}
|
||||
<div className="mt-3 border-t border-[var(--surface-divider)]/70 pt-3">
|
||||
<p className="text-[11px] font-semibold text-[var(--ink-muted)]">现场反馈</p>
|
||||
{latestFeedback ? (
|
||||
<div className="mt-1 rounded-lg bg-slate-50 px-3 py-2 text-xs leading-5 text-slate-700">
|
||||
<div className="mt-1 rounded-lg bg-[var(--surface-reading)] px-3 py-2 text-xs leading-5 text-slate-700">
|
||||
<p className="whitespace-pre-wrap break-words">{latestFeedback.detail}</p>
|
||||
<p className="mt-1 text-[11px] text-slate-500">{latestFeedback.actor} · {formatWorkOrderDateTime(latestFeedback.occurredAt)}</p>
|
||||
<p className="mt-1 text-[11px] text-[var(--ink-muted)]">{latestFeedback.actor} · {formatWorkOrderDateTime(latestFeedback.occurredAt)}</p>
|
||||
</div>
|
||||
) : <p className="mt-1 text-xs text-slate-500">暂无现场反馈</p>}
|
||||
) : <p className="mt-1 text-xs text-[var(--ink-muted)]">暂无现场反馈</p>}
|
||||
</div>
|
||||
</section>
|
||||
</DetailSectionCard>
|
||||
@@ -1368,7 +1368,7 @@ export function WorkOrderReportPanel({ condition }: { condition: ScheduledWorkOr
|
||||
}
|
||||
|
||||
function WorkOrderField({ label, value }: { label: string; value: string }) {
|
||||
return <div className={cn(REPORT_META_TILE_CLASS_NAME, "min-w-0")}><dt className="text-[11px] font-semibold text-slate-500">{label}</dt><dd className="mt-0.5 break-words text-xs font-medium leading-5 text-slate-800">{value}</dd></div>;
|
||||
return <div className={cn(REPORT_META_TILE_CLASS_NAME, "min-w-0")}><dt className="text-[11px] font-semibold text-[var(--ink-muted)]">{label}</dt><dd className="mt-0.5 break-words text-xs font-medium leading-5 text-slate-800">{value}</dd></div>;
|
||||
}
|
||||
|
||||
function getWorkOrderExecutionSteps(condition: ScheduledWorkOrderItem, hasFeedback: boolean): ExecutionStep[] {
|
||||
@@ -1478,7 +1478,7 @@ function RunningEvidencePreview({ items }: { items: string[] }) {
|
||||
{items.length > 0 ? (
|
||||
items.map((item, index) => (
|
||||
<li key={item} className="grid grid-cols-[18px_1fr_auto] items-start gap-2 text-xs leading-5 text-slate-700">
|
||||
<span className="surface-reading mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg border border-blue-100 text-xs font-semibold text-blue-700">
|
||||
<span className="surface-reading mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg border border-[var(--surface-border)] text-xs font-semibold text-[var(--action-blue-hover)]">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="line-clamp-1">{item}</span>
|
||||
@@ -1488,7 +1488,7 @@ function RunningEvidencePreview({ items }: { items: string[] }) {
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li className="text-xs leading-5 text-slate-500">正在等待首批证据返回。</li>
|
||||
<li className="text-xs leading-5 text-[var(--ink-muted)]">正在等待首批证据返回。</li>
|
||||
)}
|
||||
</ol>
|
||||
</DetailSectionCard>
|
||||
@@ -1503,14 +1503,14 @@ function EvidenceList({ items }: { items: string[] }) {
|
||||
{items.length > 0 ? (
|
||||
items.map((item, index) => (
|
||||
<li key={item} className="grid grid-cols-[18px_1fr] gap-2 text-xs leading-5 text-slate-700">
|
||||
<span className="surface-reading mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg border border-blue-100 text-xs font-semibold text-blue-700">
|
||||
<span className="surface-reading mt-0.5 grid h-[18px] w-[18px] place-items-center rounded-lg border border-[var(--surface-border)] text-xs font-semibold text-[var(--action-blue-hover)]">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span>{item}</span>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li className="text-xs leading-5 text-slate-500">暂无关键证据记录。</li>
|
||||
<li className="text-xs leading-5 text-[var(--ink-muted)]">暂无关键证据记录。</li>
|
||||
)}
|
||||
</ol>
|
||||
</DetailSectionCard>
|
||||
|
||||
@@ -53,9 +53,9 @@ import {
|
||||
} from "./scheduled-condition-feed-utils";
|
||||
|
||||
const CONDITION_CONTENT_SURFACE_CLASS_NAME =
|
||||
"surface-reading rounded-xl border border-slate-200/70";
|
||||
"surface-reading rounded-xl border border-[var(--surface-divider)]/70";
|
||||
const CONDITION_CONTROL_SURFACE_CLASS_NAME =
|
||||
"surface-control rounded-xl border border-slate-200/70";
|
||||
"surface-control rounded-xl border border-[var(--surface-divider)]/70";
|
||||
const MINIMUM_REFRESH_FEEDBACK_MS = 3_000;
|
||||
|
||||
export type ScheduledConditionFeedPresentation =
|
||||
@@ -416,7 +416,7 @@ export function ScheduledConditionFeed({
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"grid h-8 w-8 shrink-0 place-items-center bg-blue-50 text-blue-700",
|
||||
"grid h-8 w-8 shrink-0 place-items-center bg-[var(--action-soft)] text-[var(--action-blue-hover)]",
|
||||
"rounded-lg"
|
||||
)}
|
||||
>
|
||||
@@ -424,7 +424,7 @@ export function ScheduledConditionFeed({
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 className="truncate text-sm font-semibold leading-tight text-slate-950">工况任务</h2>
|
||||
<p className="mt-0.5 truncate text-xs text-slate-500">{headerSummary}</p>
|
||||
<p className="mt-0.5 truncate text-xs text-[var(--ink-muted)]">{headerSummary}</p>
|
||||
</div>
|
||||
{onRefresh ? (
|
||||
<button
|
||||
@@ -433,7 +433,7 @@ export function ScheduledConditionFeed({
|
||||
title={refreshing ? "正在刷新" : "刷新工况任务"}
|
||||
disabled={refreshing}
|
||||
onClick={() => void handleRefresh()}
|
||||
className="grid h-10 w-10 shrink-0 place-items-center rounded-lg text-slate-500 transition-colors active:scale-95 disabled:cursor-wait disabled:text-blue-600 [@media(hover:hover)]:hover:bg-blue-50 [@media(hover:hover)]:hover:text-blue-700"
|
||||
className="grid h-10 w-10 shrink-0 place-items-center rounded-lg text-[var(--ink-muted)] transition-colors active:scale-95 disabled:cursor-wait disabled:text-[var(--action-blue-hover)] [@media(hover:hover)]:hover:bg-[var(--action-soft)] [@media(hover:hover)]:hover:text-[var(--action-blue-hover)]"
|
||||
>
|
||||
<RefreshCw
|
||||
size={17}
|
||||
@@ -447,7 +447,7 @@ export function ScheduledConditionFeed({
|
||||
aria-expanded={expanded}
|
||||
onClick={handleToggleExpanded}
|
||||
className={cn(
|
||||
"grid h-8 w-8 shrink-0 place-items-center text-slate-500 transition hover:bg-blue-50 hover:text-blue-700",
|
||||
"grid h-8 w-8 shrink-0 place-items-center text-[var(--ink-muted)] transition hover:bg-[var(--action-soft)] hover:text-[var(--action-blue-hover)]",
|
||||
"rounded-lg"
|
||||
)}
|
||||
>
|
||||
@@ -594,17 +594,17 @@ function TaskCategorySwitch({
|
||||
aria-current={active ? "page" : undefined}
|
||||
onClick={() => onChange(category.id)}
|
||||
className={cn(
|
||||
"min-h-12 min-w-0 rounded-lg px-1.5 py-1.5 text-left transition-[background-color,color,box-shadow] duration-150 motion-reduce:transition-none! focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-200",
|
||||
"min-h-12 min-w-0 rounded-lg px-1.5 py-1.5 text-left transition-[background-color,color,box-shadow] duration-150 motion-reduce:transition-none! focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)]",
|
||||
active
|
||||
? "bg-white text-slate-900 shadow-[0_1px_3px_rgba(15,23,42,0.1)]"
|
||||
: "text-slate-500 [@media(hover:hover)]:hover:bg-white/70 [@media(hover:hover)]:hover:text-slate-800"
|
||||
? "bg-[var(--surface-reading)] text-slate-900 shadow-[0_1px_3px_rgba(15,23,42,0.1)]"
|
||||
: "text-[var(--ink-muted)] [@media(hover:hover)]:hover:bg-[var(--surface-reading)]/70 [@media(hover:hover)]:hover:text-slate-800"
|
||||
)}
|
||||
>
|
||||
<span className="flex min-w-0 items-center justify-between gap-1">
|
||||
<span className="truncate text-xs font-semibold">{category.label}</span>
|
||||
<span className="shrink-0 text-xs tabular-nums text-slate-400">{counts[category.id]}</span>
|
||||
<span className="shrink-0 text-xs tabular-nums text-[var(--ink-muted)]">{counts[category.id]}</span>
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-[10px] leading-3 text-slate-400">
|
||||
<span className="mt-0.5 block truncate text-[10px] leading-3 text-[var(--ink-muted)]">
|
||||
{category.sourceLabel}
|
||||
</span>
|
||||
</button>
|
||||
@@ -772,7 +772,7 @@ function ConditionFilterBar({
|
||||
<div className={cn(CONDITION_CONTROL_SURFACE_CLASS_NAME, "mb-2 p-2")}>
|
||||
<div className="mb-2 flex items-center justify-between gap-2 px-1">
|
||||
<span className="text-xs font-semibold text-slate-700">筛选</span>
|
||||
<span className="shrink-0 text-xs font-semibold text-slate-400">
|
||||
<span className="shrink-0 text-xs font-semibold text-[var(--ink-muted)]">
|
||||
{filteredConditionCount}/{totalConditionCount}
|
||||
</span>
|
||||
</div>
|
||||
@@ -800,7 +800,7 @@ function ConditionFilterBar({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onResetFilters}
|
||||
className="surface-control relative mt-2 inline-flex h-8 w-full items-center justify-center gap-1.5 rounded-lg border border-slate-200/70 px-2 text-xs font-semibold text-blue-700 after:absolute after:-inset-1 after:content-[''] hover:bg-blue-50"
|
||||
className="surface-control relative mt-2 inline-flex h-8 w-full items-center justify-center gap-1.5 rounded-lg border border-[var(--surface-divider)]/70 px-2 text-xs font-semibold text-[var(--action-blue-hover)] after:absolute after:-inset-1 after:content-[''] hover:bg-[var(--action-soft)]"
|
||||
>
|
||||
<XCircle size={12} aria-hidden="true" />
|
||||
重置筛选
|
||||
@@ -831,13 +831,13 @@ function ConditionFilterDropdown<TValue extends string>({
|
||||
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<span className="mb-1 block text-xs font-semibold text-slate-400">{label}</span>
|
||||
<span className="mb-1 block text-xs font-semibold text-[var(--ink-muted)]">{label}</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"surface-reading group flex h-9 w-full min-w-0 items-center gap-2 border border-slate-200/70 px-2 text-left text-xs outline-hidden transition-colors hover:bg-blue-50 focus-visible:border-blue-200 focus-visible:ring-2 focus-visible:ring-blue-100 data-[state=open]:border-blue-200 data-[state=open]:bg-blue-50 data-[state=open]:ring-2 data-[state=open]:ring-blue-100",
|
||||
"surface-reading group flex h-9 w-full min-w-0 items-center gap-2 border border-[var(--surface-divider)]/70 px-2 text-left text-xs outline-hidden transition-colors hover:bg-[var(--action-soft)] focus-visible:border-[var(--surface-border)] focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)] data-[state=open]:border-[var(--surface-border)] data-[state=open]:bg-[var(--action-soft)] data-[state=open]:ring-2 data-[state=open]:ring-[var(--surface-border)]",
|
||||
"rounded-xl"
|
||||
)}
|
||||
>
|
||||
@@ -848,21 +848,21 @@ function ConditionFilterDropdown<TValue extends string>({
|
||||
>
|
||||
{selectedOption?.label ?? "全部"}
|
||||
</span>
|
||||
<span className="block text-xs leading-3 text-slate-400">
|
||||
<span className="block text-xs leading-3 text-[var(--ink-muted)]">
|
||||
{selectedOption?.count ?? 0} 条
|
||||
</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
size={13}
|
||||
aria-hidden="true"
|
||||
className="shrink-0 text-slate-400 transition group-hover:text-blue-600 group-data-[state=open]:rotate-180 group-data-[state=open]:text-blue-600"
|
||||
className="shrink-0 text-[var(--ink-muted)] transition group-hover:text-[var(--action-blue-hover)] group-data-[state=open]:rotate-180 group-data-[state=open]:text-[var(--action-blue-hover)]"
|
||||
/>
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="start"
|
||||
sideOffset={6}
|
||||
className="surface-reading scheduled-feed-scroll z-50 max-h-[280px] w-[var(--radix-dropdown-menu-trigger-width)] overflow-y-auto rounded-xl border border-slate-200/70 p-1.5 text-slate-700 shadow-md shadow-slate-900/10"
|
||||
className="surface-reading scheduled-feed-scroll z-50 max-h-[280px] w-[var(--radix-dropdown-menu-trigger-width)] overflow-y-auto rounded-xl border border-[var(--surface-divider)]/70 p-1.5 text-slate-700 shadow-md shadow-slate-900/10"
|
||||
>
|
||||
<DropdownMenuRadioGroup
|
||||
value={value}
|
||||
@@ -873,12 +873,12 @@ function ConditionFilterDropdown<TValue extends string>({
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
className={cn(
|
||||
"my-0.5 min-h-9 gap-2 border border-transparent bg-transparent py-1.5 pl-7 pr-2 text-xs transition focus:bg-blue-50 focus:text-blue-800 data-[state=checked]:border-blue-100 data-[state=checked]:bg-blue-50 data-[state=checked]:text-blue-800",
|
||||
"my-0.5 min-h-9 gap-2 border border-transparent bg-transparent py-1.5 pl-7 pr-2 text-xs transition focus:bg-[var(--action-soft)] focus:text-[var(--action-blue-pressed)] data-[state=checked]:border-[var(--surface-border)] data-[state=checked]:bg-[var(--action-soft)] data-[state=checked]:text-[var(--action-blue-pressed)]",
|
||||
"rounded-xl"
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 truncate font-semibold">{option.label}</span>
|
||||
<span className="surface-control shrink-0 rounded-full border px-1.5 py-0.5 text-xs font-semibold leading-4 text-slate-500">
|
||||
<span className="surface-control shrink-0 rounded-full border px-1.5 py-0.5 text-xs font-semibold leading-4 text-[var(--ink-muted)]">
|
||||
{option.count}
|
||||
</span>
|
||||
</DropdownMenuRadioItem>
|
||||
@@ -892,12 +892,12 @@ function ConditionFilterDropdown<TValue extends string>({
|
||||
|
||||
function ConditionEmptyState({ title, description }: { title: string; description: string }) {
|
||||
return (
|
||||
<div className="surface-reading flex min-h-full flex-col items-center justify-center rounded-xl border border-dashed border-slate-200/80 px-4 py-5 text-center">
|
||||
<span className="grid h-8 w-8 place-items-center rounded-lg bg-slate-100 text-slate-400">
|
||||
<div className="surface-reading flex min-h-full flex-col items-center justify-center rounded-xl border border-dashed border-[var(--surface-divider)]/80 px-4 py-5 text-center">
|
||||
<span className="grid h-8 w-8 place-items-center rounded-lg bg-[var(--surface-control)] text-[var(--ink-muted)]">
|
||||
<ChevronRight size={15} aria-hidden="true" />
|
||||
</span>
|
||||
<p className="mt-2 text-xs font-semibold text-slate-700">{title}</p>
|
||||
<p className="mt-1 text-xs leading-4 text-slate-500">{description}</p>
|
||||
<p className="mt-1 text-xs leading-4 text-[var(--ink-muted)]">{description}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -910,23 +910,23 @@ function ConditionTimelineSkeleton({ rows }: { rows: number }) {
|
||||
key={index}
|
||||
className="grid min-h-[60px] grid-cols-[42px_24px_minmax(0,1fr)] gap-2 rounded-xl px-1 py-1"
|
||||
>
|
||||
<span className="mt-2 h-4 w-9 animate-pulse rounded-lg bg-slate-200/80" />
|
||||
<span className="mt-2 h-4 w-9 animate-pulse rounded-lg bg-[var(--surface-divider)]/80" />
|
||||
<span className="relative flex min-h-12 justify-center pt-1.5">
|
||||
{index < rows - 1 ? (
|
||||
<span
|
||||
className="pointer-events-none absolute bottom-[-0.5rem] top-8 w-px bg-slate-200"
|
||||
className="pointer-events-none absolute bottom-[-0.5rem] top-8 w-px bg-[var(--surface-divider)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<span className="relative z-10 h-6 w-6 animate-pulse rounded-full bg-slate-200 ring-4 ring-white" />
|
||||
<span className="relative z-10 h-6 w-6 animate-pulse rounded-full bg-[var(--surface-divider)] ring-4 ring-white" />
|
||||
</span>
|
||||
<span className="surface-reading min-w-0 rounded-xl border border-slate-200/70 px-2 py-1.5">
|
||||
<span className="surface-reading min-w-0 rounded-xl border border-[var(--surface-divider)]/70 px-2 py-1.5">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="min-w-0 flex-1 space-y-1.5">
|
||||
<span className="block h-3.5 w-24 animate-pulse rounded-full bg-slate-200/90" />
|
||||
<span className="block h-3 w-full max-w-[260px] animate-pulse rounded-full bg-slate-100" />
|
||||
<span className="block h-3.5 w-24 animate-pulse rounded-full bg-[var(--surface-divider)]/90" />
|
||||
<span className="block h-3 w-full max-w-[260px] animate-pulse rounded-full bg-[var(--surface-control)]" />
|
||||
</span>
|
||||
<span className="h-5 w-12 shrink-0 animate-pulse rounded-full bg-slate-100" />
|
||||
<span className="h-5 w-12 shrink-0 animate-pulse rounded-full bg-[var(--surface-control)]" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -944,26 +944,26 @@ function ConditionDetailSkeleton() {
|
||||
aria-label="工况详情加载中"
|
||||
>
|
||||
<div className="flex gap-2.5">
|
||||
<span className={cn("h-9 w-9 shrink-0 animate-pulse bg-slate-100", "rounded-lg")} />
|
||||
<span className={cn("h-9 w-9 shrink-0 animate-pulse bg-[var(--surface-control)]", "rounded-lg")} />
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<span className="h-4 w-12 animate-pulse rounded-full bg-slate-200" />
|
||||
<span className="h-5 w-14 animate-pulse rounded-full bg-blue-100" />
|
||||
<span className="h-4 w-12 animate-pulse rounded-full bg-[var(--surface-divider)]" />
|
||||
<span className="h-5 w-14 animate-pulse rounded-full bg-[var(--action-selection-soft)]" />
|
||||
</div>
|
||||
<span className="block h-4 w-44 animate-pulse rounded-full bg-slate-200" />
|
||||
<span className="block h-3 w-full animate-pulse rounded-full bg-slate-100" />
|
||||
<span className="block h-3 w-4/5 animate-pulse rounded-full bg-slate-100" />
|
||||
<span className="block h-4 w-44 animate-pulse rounded-full bg-[var(--surface-divider)]" />
|
||||
<span className="block h-3 w-full animate-pulse rounded-full bg-[var(--surface-control)]" />
|
||||
<span className="block h-3 w-4/5 animate-pulse rounded-full bg-[var(--surface-control)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-2">
|
||||
{Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="surface-reading rounded-xl border border-slate-200/70 px-3 py-3"
|
||||
className="surface-reading rounded-xl border border-[var(--surface-divider)]/70 px-3 py-3"
|
||||
>
|
||||
<span className="block h-3.5 w-28 animate-pulse rounded-full bg-slate-200" />
|
||||
<span className="mt-2 block h-3 w-full animate-pulse rounded-full bg-slate-100" />
|
||||
<span className="mt-1.5 block h-3 w-3/4 animate-pulse rounded-full bg-slate-100" />
|
||||
<span className="block h-3.5 w-28 animate-pulse rounded-full bg-[var(--surface-divider)]" />
|
||||
<span className="mt-2 block h-3 w-full animate-pulse rounded-full bg-[var(--surface-control)]" />
|
||||
<span className="mt-1.5 block h-3 w-3/4 animate-pulse rounded-full bg-[var(--surface-control)]" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -1006,7 +1006,7 @@ function ConditionGroupHeader({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-between px-1 py-1 text-xs font-semibold text-slate-500",
|
||||
"flex items-center justify-between px-1 py-1 text-xs font-semibold text-[var(--ink-muted)]",
|
||||
compact ? "mb-1" : "mb-2"
|
||||
)}
|
||||
>
|
||||
@@ -1064,19 +1064,19 @@ function ConditionTimelineRow({
|
||||
aria-pressed={selected}
|
||||
onClick={onSelect}
|
||||
className={cn(
|
||||
"group grid min-h-[60px] w-full grid-cols-[42px_24px_minmax(0,1fr)] gap-2 rounded-xl border px-1 py-1 text-left outline-hidden transition-colors focus-visible:bg-blue-50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-200",
|
||||
"group grid min-h-[60px] w-full grid-cols-[42px_24px_minmax(0,1fr)] gap-2 rounded-xl border px-1 py-1 text-left outline-hidden transition-colors focus-visible:bg-[var(--action-soft)] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--action-focus-ring)]",
|
||||
selected
|
||||
? "surface-reading border-blue-100 text-blue-900"
|
||||
: "border-transparent hover:bg-slate-50"
|
||||
? "surface-reading border-[var(--surface-border)] text-[var(--action-blue-pressed)]"
|
||||
: "border-transparent hover:bg-[var(--surface-reading)]"
|
||||
)}
|
||||
>
|
||||
<span className="pt-2 font-mono text-xs font-semibold leading-4 text-slate-500">
|
||||
<span className="pt-2 font-mono text-xs font-semibold leading-4 text-[var(--ink-muted)]">
|
||||
{formatTime(condition.scheduledAt)}
|
||||
</span>
|
||||
<span className="relative flex min-h-12 justify-center pt-1.5">
|
||||
{!isLast ? (
|
||||
<span
|
||||
className="pointer-events-none absolute bottom-[-0.5rem] top-8 w-px bg-slate-200 transition-colors group-hover:bg-blue-200 group-focus-visible:bg-blue-300"
|
||||
className="pointer-events-none absolute bottom-[-0.5rem] top-8 w-px bg-[var(--surface-divider)] transition-colors group-hover:bg-[var(--action-selection-soft)] group-focus-visible:bg-[var(--action-blue)]"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
@@ -1087,7 +1087,7 @@ function ConditionTimelineRow({
|
||||
data-selected={selected ? "true" : undefined}
|
||||
className={cn(
|
||||
"relative z-10 ring-4 ring-white",
|
||||
selected && "group-focus-visible:ring-blue-100"
|
||||
selected && "group-focus-visible:ring-[var(--action-focus-ring)]"
|
||||
)}
|
||||
>
|
||||
<Icon size={12} />
|
||||
@@ -1096,7 +1096,7 @@ function ConditionTimelineRow({
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-0 px-2 py-1.5 transition-colors",
|
||||
selected ? "text-blue-900" : "text-slate-700"
|
||||
selected ? "text-[var(--action-blue-pressed)]" : "text-slate-700"
|
||||
)}
|
||||
>
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
@@ -1104,7 +1104,7 @@ function ConditionTimelineRow({
|
||||
<span className="block truncate text-xs font-semibold leading-4">
|
||||
{condition.title}
|
||||
</span>
|
||||
<span className="block truncate text-xs leading-4 text-slate-500">
|
||||
<span className="block truncate text-xs leading-4 text-[var(--ink-muted)]">
|
||||
{condition.summary}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -19,7 +19,7 @@ export function TaskConversationReview({ task, onAccessDecision, onActionDecisio
|
||||
const pendingActions = task.actions.filter((action) => action.approval_status === "pending"
|
||||
|| action.approval_status === "approved" && ["not_submitted", "submitted", "unknown"].includes(action.execution_status));
|
||||
if (pendingAccess.length === 0 && pendingActions.length === 0) return null;
|
||||
return <section aria-label="当前任务审核" className="surface-reading space-y-2 rounded-xl border border-blue-200/80 p-3">
|
||||
return <section aria-label="当前任务审核" className="surface-reading space-y-2 rounded-xl border border-[var(--surface-border)]/80 p-3">
|
||||
<div className="flex items-center gap-1.5 text-xs font-semibold text-slate-800"><ShieldCheck size={14} aria-hidden="true" />当前任务审核 · {task.title}</div>
|
||||
{pendingAccess.map((request) => <AccessRequestCard key={request.access_request_id} request={request}
|
||||
nodeTitle={task.latest_plan?.plan.nodes.find((node) => node.id === request.node_id)?.title}
|
||||
|
||||
@@ -450,7 +450,7 @@ function LegendPanel({ legendItems }: { legendItems: MapLegendItem[] }) {
|
||||
<MapLegendList items={legendItems} />
|
||||
<div role="note" className="surface-control rounded-xl border px-3 py-2.5">
|
||||
<p className="text-[11px] font-semibold text-slate-700">图例随图层显隐同步</p>
|
||||
<p className="mt-1 text-[10px] leading-relaxed text-slate-500">
|
||||
<p className="mt-1 text-[10px] leading-relaxed text-[var(--ink-muted)]">
|
||||
管线颜色和线宽表示口径,设施颜色表示类型,风险色仅用于分析覆盖。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -61,8 +61,8 @@ export function WorkbenchAgentPanels({
|
||||
onPointerDown={handlePointerDown}
|
||||
className="group pointer-events-auto absolute inset-y-0 -right-1.5 z-10 w-3 cursor-col-resize touch-none"
|
||||
>
|
||||
<span className="pointer-events-none absolute inset-y-0 left-[5px] w-px bg-transparent transition-colors duration-150 group-hover:bg-blue-300/70 group-data-[resizing=true]:bg-blue-400/80 motion-reduce:transition-none" />
|
||||
<span className="agent-resize-grip pointer-events-none absolute left-1/2 top-1/2 h-11 w-[3px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-400 opacity-0 transition-opacity duration-150 group-hover:opacity-100 group-data-[resizing=true]:bg-blue-500 group-data-[resizing=true]:opacity-100 motion-reduce:transition-none" />
|
||||
<span className="pointer-events-none absolute inset-y-0 left-[5px] w-px bg-transparent transition-colors duration-150 group-hover:bg-[var(--action-blue-hover)]/70 group-data-[resizing=true]:bg-[var(--action-blue)]/80 motion-reduce:transition-none" />
|
||||
<span className="agent-resize-grip pointer-events-none absolute left-1/2 top-1/2 h-11 w-[3px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--action-blue)] opacity-0 transition-opacity duration-150 group-hover:opacity-100 group-data-[resizing=true]:bg-[var(--action-blue)] group-data-[resizing=true]:opacity-100 motion-reduce:transition-none" />
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
||||
@@ -6,19 +6,19 @@ import { cn } from "@/shared/ui/cn";
|
||||
export type WorkbenchTone = "neutral" | "info" | "success" | "warning" | "danger" | "teal";
|
||||
|
||||
const sectionTone: Record<WorkbenchTone, string> = {
|
||||
neutral: "bg-slate-100 text-slate-600",
|
||||
info: "bg-blue-50 text-blue-700",
|
||||
neutral: "bg-[var(--surface-control)] text-slate-600",
|
||||
info: "bg-[var(--action-soft)] text-[var(--action-blue-hover)]",
|
||||
success: "bg-emerald-50 text-emerald-700",
|
||||
warning: "bg-amber-50 text-amber-700",
|
||||
warning: "bg-[var(--status-warning-soft)] text-[var(--status-warning-foreground)]",
|
||||
danger: "bg-rose-50 text-rose-700",
|
||||
teal: "bg-cyan-50 text-cyan-800"
|
||||
};
|
||||
|
||||
const noticeTone: Record<Exclude<WorkbenchTone, "teal">, string> = {
|
||||
neutral: "border-slate-300 bg-slate-50 text-slate-700",
|
||||
info: "border-blue-500 bg-blue-50/80 text-blue-950",
|
||||
neutral: "border-[var(--surface-border)] bg-[var(--surface-reading)] text-slate-700",
|
||||
info: "border-[var(--action-blue)] bg-[var(--action-soft)]/80 text-[var(--action-blue-pressed)]",
|
||||
success: "border-emerald-500 bg-emerald-50/80 text-emerald-950",
|
||||
warning: "border-amber-500 bg-amber-50/90 text-amber-950",
|
||||
warning: "border-[var(--status-warning-mark)] bg-[var(--status-warning-soft)]/90 text-[var(--status-warning-foreground)]",
|
||||
danger: "border-rose-500 bg-rose-50/90 text-rose-950"
|
||||
};
|
||||
|
||||
@@ -53,12 +53,12 @@ export function WorkbenchSection({
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
"overflow-hidden rounded-[14px] border border-slate-200/90 bg-[var(--surface-reading)] shadow-[0_1px_2px_rgba(15,23,42,0.06)]",
|
||||
"overflow-hidden rounded-[14px] border border-[var(--surface-divider)]/90 bg-[var(--surface-reading)] shadow-[0_1px_2px_rgba(15,23,42,0.06)]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex min-h-14 items-start gap-3 border-b border-slate-200/75 px-3.5 py-3">
|
||||
<div className="flex min-h-14 items-start gap-3 border-b border-[var(--surface-divider)]/75 bg-[var(--surface-control)] px-3.5 py-3">
|
||||
{icon ? (
|
||||
<span className={cn("mt-0.5 grid size-7 shrink-0 place-items-center rounded-lg", sectionTone[tone])}>
|
||||
{icon}
|
||||
@@ -69,7 +69,7 @@ export function WorkbenchSection({
|
||||
<h3 className="text-[13px] font-semibold leading-5 text-slate-900">{title}</h3>
|
||||
{meta}
|
||||
</div>
|
||||
{description ? <p className="mt-0.5 text-xs leading-5 text-slate-500">{description}</p> : null}
|
||||
{description ? <p className="mt-0.5 text-xs leading-5 text-[var(--ink-muted)]">{description}</p> : null}
|
||||
</div>
|
||||
{actions ? <div className="flex shrink-0 items-center gap-1">{actions}</div> : null}
|
||||
</div>
|
||||
@@ -125,10 +125,10 @@ export function WorkbenchEmptyState({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("rounded-xl border border-dashed border-slate-300 bg-slate-50/70 px-4 py-5 text-center", className)}>
|
||||
{icon ? <span className="mx-auto grid size-8 place-items-center rounded-full bg-white text-slate-500 shadow-xs">{icon}</span> : null}
|
||||
<div className={cn("rounded-xl border border-dashed border-[var(--surface-border)] bg-[var(--surface-reading)]/70 px-4 py-5 text-center", className)}>
|
||||
{icon ? <span className="mx-auto grid size-8 place-items-center rounded-full bg-[var(--surface-reading)] text-[var(--ink-muted)] shadow-xs">{icon}</span> : null}
|
||||
<p className={cn("text-xs font-semibold text-slate-700", icon && "mt-2")}>{title}</p>
|
||||
{description ? <p className="mx-auto mt-1 max-w-72 text-[11px] leading-5 text-slate-500">{description}</p> : null}
|
||||
{description ? <p className="mx-auto mt-1 max-w-72 text-[11px] leading-5 text-[var(--ink-muted)]">{description}</p> : null}
|
||||
{action ? <div className="mt-3 flex justify-center">{action}</div> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -52,7 +52,7 @@ const menuSurfaceClassName = cn(
|
||||
"acrylic-panel border p-2 text-slate-900"
|
||||
);
|
||||
const menuReadableRowClassName =
|
||||
"surface-well focus:bg-blue-50 focus:text-slate-950";
|
||||
"surface-well focus:bg-[var(--action-soft)] focus:text-slate-950";
|
||||
export function AnalysisViewMenu({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -92,7 +92,7 @@ export function AnalysisViewMenu({
|
||||
<span className="hidden max-w-[160px] truncate text-sm font-semibold xl:inline 2xl:max-w-[200px]">
|
||||
分析视图 · {title}
|
||||
</span>
|
||||
<ChevronDown size={14} className="hidden shrink-0 text-slate-400 transition group-hover:text-blue-600 group-data-[state=open]:rotate-180 group-data-[state=open]:text-blue-600 2xl:block" aria-hidden="true" />
|
||||
<ChevronDown size={14} className="hidden shrink-0 text-[var(--ink-muted)] transition group-hover:text-[var(--action-blue-hover)] group-data-[state=open]:rotate-180 group-data-[state=open]:text-[var(--action-blue-hover)] 2xl:block" aria-hidden="true" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className={cn("w-[340px]", menuSurfaceClassName)}>
|
||||
@@ -213,28 +213,28 @@ export function AlertMenu({
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<StatusDot tone={tone} size="md" />
|
||||
<span className="min-w-0 flex-1 truncate text-sm font-semibold leading-5 text-slate-950">{alert.title}</span>
|
||||
<span className="shrink-0 font-mono text-xs font-semibold leading-4 text-slate-400 tabular-nums">{alert.time}</span>
|
||||
<span className="shrink-0 font-mono text-xs font-semibold leading-4 text-[var(--ink-muted)] tabular-nums">{alert.time}</span>
|
||||
</span>
|
||||
<span className="mt-1 block line-clamp-2 text-xs leading-5 text-slate-500">{alert.description}</span>
|
||||
<span className="mt-1 block line-clamp-2 text-xs leading-5 text-[var(--ink-muted)]">{alert.description}</span>
|
||||
</span>
|
||||
<span className={cn(toneClassName, "grid h-7 w-7 shrink-0 place-items-center bg-slate-50 text-slate-400 transition-colors group-focus:text-[var(--status-foreground)]", MAP_ICON_CELL_RADIUS_CLASS_NAME)} aria-hidden="true">
|
||||
<span className={cn(toneClassName, "grid h-7 w-7 shrink-0 place-items-center bg-[var(--surface-reading)] text-[var(--ink-muted)] transition-colors group-focus:text-[var(--status-foreground)]", MAP_ICON_CELL_RADIUS_CLASS_NAME)} aria-hidden="true">
|
||||
<ChevronRight size={14} />
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
}) : (
|
||||
<div className={cn("surface-reading my-1 grid min-h-[76px] place-items-center border border-dashed border-slate-200 px-3 py-3 text-center", MAP_COMPACT_RADIUS_CLASS_NAME)}>
|
||||
<div className={cn("surface-reading my-1 grid min-h-[76px] place-items-center border border-dashed border-[var(--surface-divider)] px-3 py-3 text-center", MAP_COMPACT_RADIUS_CLASS_NAME)}>
|
||||
<div className="min-w-0">
|
||||
<SemanticStatusIcon tone="success" size="md" className="mx-auto">
|
||||
<CheckCircle2 size={16} />
|
||||
</SemanticStatusIcon>
|
||||
<p className="mt-1 text-xs font-semibold text-slate-700">当前无待复核工况</p>
|
||||
<p className="mt-0.5 text-xs leading-4 text-slate-500">新的工况提醒会进入这里。</p>
|
||||
<p className="mt-0.5 text-xs leading-4 text-[var(--ink-muted)]">新的工况提醒会进入这里。</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-[minmax(0,1fr)_auto] gap-2 border-t border-slate-200/70 pt-2">
|
||||
<div className="mt-2 grid grid-cols-[minmax(0,1fr)_auto] gap-2 border-t border-[var(--surface-divider)]/70 pt-2">
|
||||
<MenuPlainButton
|
||||
label={conditionFeedVisible ? "工况面板已打开" : "打开工况面板"}
|
||||
description="查看时间线与处置详情"
|
||||
@@ -294,7 +294,7 @@ export function UserMenu({
|
||||
<UserRound size={14} aria-hidden="true" />
|
||||
</span>
|
||||
<span className={cn(compactHeaderTextClassName, "text-sm font-semibold")}>{user.name}</span>
|
||||
<ChevronDown size={14} className="hidden shrink-0 text-slate-400 transition group-hover:text-blue-600 group-data-[state=open]:rotate-180 group-data-[state=open]:text-blue-600 2xl:block" aria-hidden="true" />
|
||||
<ChevronDown size={14} className="hidden shrink-0 text-[var(--ink-muted)] transition group-hover:text-[var(--action-blue-hover)] group-data-[state=open]:rotate-180 group-data-[state=open]:text-[var(--action-blue-hover)] 2xl:block" aria-hidden="true" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className={cn("w-72", menuSurfaceClassName)}>
|
||||
@@ -310,7 +310,7 @@ export function UserMenu({
|
||||
onSelect={onLogout ? () => void onLogout() : undefined}
|
||||
className={cn(
|
||||
"px-2 py-2",
|
||||
!onLogout && "text-slate-400",
|
||||
!onLogout && "text-[var(--ink-muted)]",
|
||||
MAP_COMPACT_RADIUS_CLASS_NAME
|
||||
)}
|
||||
>
|
||||
@@ -350,17 +350,17 @@ function AlertQueueSummary({
|
||||
</SemanticStatusIcon>
|
||||
<div className="min-w-0">
|
||||
<h2 className="truncate text-sm font-semibold leading-5 text-slate-950">异常处置</h2>
|
||||
<p className="mt-0.5 text-xs leading-5 text-slate-500">
|
||||
<p className="mt-0.5 text-xs leading-5 text-[var(--ink-muted)]">
|
||||
{hasAlerts ? `${count} 条需复核${latestAlertTime ? ` · 最近 ${latestAlertTime}` : ""}` : "当前无待复核工况"}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs leading-4 text-slate-400">来源:{sourceLabel}</p>
|
||||
<p className="mt-0.5 text-xs leading-4 text-[var(--ink-muted)]">来源:{sourceLabel}</p>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge tone={tone}>
|
||||
{hasAlerts ? "需复核" : "正常"}
|
||||
</StatusBadge>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs leading-4 text-slate-500">
|
||||
<div className="mt-2 flex items-center gap-1.5 text-xs leading-4 text-[var(--ink-muted)]">
|
||||
<StatusDot tone={conditionFeedVisible ? "info" : "neutral"} />
|
||||
<span className="truncate">{conditionFeedVisible ? "工况面板已显示,可查看详情。" : "点击条目可打开工况任务。"}</span>
|
||||
</div>
|
||||
@@ -388,12 +388,12 @@ function MenuPlainButton({
|
||||
"flex h-11 min-w-0 flex-col justify-center border px-3 text-left transition-colors disabled:cursor-default disabled:opacity-70",
|
||||
MAP_COMPACT_RADIUS_CLASS_NAME,
|
||||
disabled
|
||||
? "border-slate-200 bg-slate-50 text-slate-500"
|
||||
: "border-slate-200 bg-white text-slate-800 hover:border-blue-100 hover:bg-blue-50/55 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-100"
|
||||
? "border-[var(--surface-divider)] bg-[var(--surface-reading)] text-[var(--ink-muted)]"
|
||||
: "border-[var(--surface-divider)] bg-[var(--surface-reading)] text-slate-800 hover:border-[var(--surface-border)] hover:bg-[var(--action-soft)]/55 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)]"
|
||||
)}
|
||||
>
|
||||
<span className="truncate text-xs font-semibold leading-4">{label}</span>
|
||||
<span className="truncate text-xs font-medium leading-4 text-slate-500">{description}</span>
|
||||
<span className="truncate text-xs font-medium leading-4 text-[var(--ink-muted)]">{description}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -418,8 +418,8 @@ function MenuPrimaryButton({
|
||||
"inline-flex h-11 shrink-0 items-center justify-center gap-2 border px-3 text-xs font-semibold transition-colors disabled:cursor-not-allowed",
|
||||
MAP_COMPACT_RADIUS_CLASS_NAME,
|
||||
disabled
|
||||
? "border-slate-200 bg-slate-50 text-slate-400"
|
||||
: "border-blue-200 bg-blue-600 text-white shadow-lg shadow-blue-600/20 hover:bg-blue-700 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-200"
|
||||
? "border-[var(--surface-divider)] bg-[var(--surface-reading)] text-[var(--ink-muted)]"
|
||||
: "border-[var(--surface-border)] bg-[var(--action-blue)] text-white shadow-lg shadow-[color:var(--action-blue)]/20 hover:bg-[var(--action-blue-hover)] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)]"
|
||||
)}
|
||||
>
|
||||
<Icon size={14} aria-hidden="true" />
|
||||
@@ -443,10 +443,10 @@ function MenuHeader({
|
||||
<div className={cn("surface-control flex items-center justify-between gap-3 border px-2 py-2", MAP_READABLE_RADIUS_CLASS_NAME)}>
|
||||
<div className="min-w-0">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
{Icon ? <Icon size={13} className="shrink-0 text-slate-500" aria-hidden="true" /> : null}
|
||||
{Icon ? <Icon size={13} className="shrink-0 text-[var(--ink-muted)]" aria-hidden="true" /> : null}
|
||||
<span className="truncate text-sm font-semibold text-slate-950">{title}</span>
|
||||
</div>
|
||||
<p className={cn("mt-0.5 truncate text-xs text-slate-500", tone === "warning" && "status-tone-danger text-[var(--status-foreground)]")}>{meta}</p>
|
||||
<p className={cn("mt-0.5 truncate text-xs text-[var(--ink-muted)]", tone === "warning" && "status-tone-danger text-[var(--status-foreground)]")}>{meta}</p>
|
||||
</div>
|
||||
{tone === "warning" ? <StatusDot tone="danger" size="md" /> : null}
|
||||
</div>
|
||||
@@ -454,11 +454,11 @@ function MenuHeader({
|
||||
}
|
||||
|
||||
function MenuSectionLabel({ children }: { children: ReactNode }) {
|
||||
return <DropdownMenuLabel className="px-2 pb-1 pt-2.5 text-xs font-semibold uppercase text-slate-500">{children}</DropdownMenuLabel>;
|
||||
return <DropdownMenuLabel className="px-2 pb-1 pt-2.5 text-xs font-semibold uppercase text-[var(--ink-muted)]">{children}</DropdownMenuLabel>;
|
||||
}
|
||||
|
||||
function MenuSeparator() {
|
||||
return <DropdownMenuSeparator className="my-2 bg-slate-200/70" />;
|
||||
return <DropdownMenuSeparator className="my-2 bg-[var(--surface-divider)]/70" />;
|
||||
}
|
||||
|
||||
function MenuAction({
|
||||
@@ -487,16 +487,16 @@ function MenuAction({
|
||||
className={cn(
|
||||
"grid h-7 w-7 shrink-0 place-items-center border",
|
||||
MAP_ICON_CELL_RADIUS_CLASS_NAME,
|
||||
tone === "primary" && "border-blue-100 bg-blue-50 text-blue-700",
|
||||
tone === "primary" && "border-[var(--surface-border)] bg-[var(--action-soft)] text-[var(--action-blue-hover)]",
|
||||
tone === "warning" && "status-icon status-tone-warning",
|
||||
tone === "default" && "border-slate-100 bg-slate-50 text-slate-600"
|
||||
tone === "default" && "border-[var(--surface-divider)] bg-[var(--surface-reading)] text-slate-600"
|
||||
)}
|
||||
>
|
||||
<Icon size={15} aria-hidden="true" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold leading-5 text-slate-900">{label}</span>
|
||||
<span className="mt-0.5 block truncate text-xs text-slate-500">{description}</span>
|
||||
<span className="mt-0.5 block truncate text-xs text-[var(--ink-muted)]">{description}</span>
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,6 @@ export const compactHeaderControlButtonClassName =
|
||||
"h-10 w-10 justify-center px-0";
|
||||
|
||||
export const headerControlIconClassName =
|
||||
"header-control-icon grid h-5 w-5 shrink-0 place-items-center rounded-full bg-slate-100/80 text-slate-500 transition-colors group-hover:bg-blue-50 group-hover:text-blue-700 group-data-[state=open]:bg-blue-50 group-data-[state=open]:text-blue-700";
|
||||
"header-control-icon grid h-5 w-5 shrink-0 place-items-center rounded-full bg-[var(--surface-control)]/80 text-[var(--ink-muted)] transition-colors group-hover:bg-[var(--action-soft)] group-hover:text-[var(--action-blue-hover)] group-data-[state=open]:bg-[var(--action-soft)] group-data-[state=open]:text-[var(--action-blue-hover)]";
|
||||
|
||||
export const compactHeaderTextClassName = "hidden min-[1800px]:inline";
|
||||
|
||||
@@ -108,7 +108,7 @@ export function WorkbenchTopBar({
|
||||
<div className="flex min-w-0 items-center gap-2 sm:gap-2.5 lg:shrink-0">
|
||||
<div
|
||||
className={cn(
|
||||
"grid h-8 w-8 shrink-0 place-items-center bg-blue-600 text-white shadow-lg shadow-blue-600/20",
|
||||
"grid h-8 w-8 shrink-0 place-items-center bg-[var(--palette-brand)] text-white shadow-lg shadow-[color:var(--palette-brand)]/20",
|
||||
MAP_ICON_CELL_RADIUS_CLASS_NAME
|
||||
)}
|
||||
>
|
||||
@@ -119,7 +119,7 @@ export function WorkbenchTopBar({
|
||||
<span className="sm:hidden">供水</span>
|
||||
<span className="hidden sm:inline">供水管网智能调度系统</span>
|
||||
</h1>
|
||||
<p className="hidden truncate text-xs leading-4 text-slate-500 sm:block">调度工作台</p>
|
||||
<p className="hidden truncate text-xs leading-4 text-[var(--ink-muted)] sm:block">调度工作台</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -363,7 +363,7 @@ function HeaderReadout({
|
||||
}) {
|
||||
return (
|
||||
<div className="flex shrink-0 items-center gap-1.5 whitespace-nowrap">
|
||||
{Icon ? <Icon size={14} className="shrink-0 text-blue-700" aria-hidden="true" /> : null}
|
||||
{Icon ? <Icon size={14} className="shrink-0 text-[var(--action-blue-hover)]" aria-hidden="true" /> : null}
|
||||
<span className="sr-only">{label}</span>
|
||||
<span className="max-w-40 shrink-0 truncate text-sm font-semibold text-slate-800">
|
||||
{value}
|
||||
@@ -373,5 +373,5 @@ function HeaderReadout({
|
||||
}
|
||||
|
||||
function HeaderDivider() {
|
||||
return <div className="h-5 w-px shrink-0 bg-slate-200/80" />;
|
||||
return <div className="h-5 w-px shrink-0 bg-[var(--surface-divider)]/80" />;
|
||||
}
|
||||
|
||||
@@ -289,7 +289,7 @@ function TimelineMarker({
|
||||
aria-label={`${formatClock(event.cursorTime)} ${event.title}`}
|
||||
onClick={() => onSelectEvent(event)}
|
||||
className={cn(
|
||||
"absolute z-20 grid -translate-x-1/2 -translate-y-1/2 place-items-center outline-hidden transition-[transform,box-shadow] hover:scale-125 focus-visible:ring-2 focus-visible:ring-blue-600",
|
||||
"absolute z-20 grid -translate-x-1/2 -translate-y-1/2 place-items-center outline-hidden transition-[transform,box-shadow] hover:scale-125 focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)]",
|
||||
compact ? "h-2 w-2" : "h-5 w-5",
|
||||
event.lane === "plan" &&
|
||||
"rounded-md border border-blue-400 bg-blue-50 text-blue-700 shadow-[0_2px_7px_rgba(37,99,235,0.28)]",
|
||||
@@ -481,7 +481,7 @@ function TimelineLegend({
|
||||
})
|
||||
}
|
||||
className={cn(
|
||||
"grid h-7 w-7 place-items-center rounded-lg outline-hidden transition-[transform,filter,opacity] hover:scale-110 focus-visible:ring-2 focus-visible:ring-blue-500 active:scale-95",
|
||||
"grid h-7 w-7 place-items-center rounded-lg outline-hidden transition-[transform,filter,opacity] hover:scale-110 focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)] active:scale-95",
|
||||
visible ? "opacity-100" : "grayscale opacity-25"
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -219,7 +219,7 @@ function WorkOrders({ orders, canWrite, busy, mutate, client, onAgentTrigger }:
|
||||
});
|
||||
|
||||
const canSave = canWrite && !busy && title.trim() && summary.trim();
|
||||
const editorInputClassName = "mt-1 h-9 rounded-lg border-slate-200 bg-white focus-visible:outline-0! focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)]";
|
||||
const editorInputClassName = "mt-1 h-9 rounded-lg border-[var(--surface-divider)] bg-[var(--surface-reading)]";
|
||||
|
||||
const orderEditor = editorOpen ? (
|
||||
<section
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AccessTokenProvider } from "@/shared/auth/keycloak-auth";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { cn } from "@/shared/ui/cn";
|
||||
import { Disclosure } from "@/shared/ui/disclosure";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { ManagedTestResources } from "./managed-test-resources";
|
||||
import { WorkbenchNotice } from "../components/workbench-panel-ui";
|
||||
import {
|
||||
@@ -100,12 +101,12 @@ export function PeripheralSamplePanel({ open, onClose, getAccessToken, onAgentTr
|
||||
结果未知时只查询原请求,不会重复创建或更新工单。
|
||||
</p>
|
||||
<div className="flex gap-2 pb-3">
|
||||
<input
|
||||
<Input
|
||||
aria-label="提交请求号"
|
||||
value={receiptRequestId}
|
||||
onChange={(event) => setReceiptRequestId(event.target.value.trim())}
|
||||
placeholder="request_id"
|
||||
className="h-10 min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-3 font-mono text-xs focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[var(--action-focus-ring)]"
|
||||
className="h-10 min-w-0 flex-1 rounded-lg border-[var(--surface-divider)] bg-[var(--surface-reading)] font-mono text-xs md:text-xs"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -194,7 +194,7 @@ function AnalysisBlockView({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-h-14 w-full items-start gap-3 px-4 py-3 text-left transition-colors hover:bg-blue-50/40 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500/60"
|
||||
className="flex min-h-14 w-full items-start gap-3 px-4 py-3 text-left transition-colors hover:bg-blue-50/40 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--action-focus-ring)]"
|
||||
aria-label={`查看${block.title}详情`}
|
||||
onClick={onSelect}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user