forked from ChaiHao/BPOS-MVP
M6(1/n): 工作台任务要素 —— 原型 1:1 平移的第一屏
样式整文件搬(README 平移规则 1):app.css 2767 行 + proto.vendor.css + theme.css + console.css 原样搬,设计令牌与全部 jc-* / g-* 类名不动。 前端(按 §9 重排,渲染输出 1:1): - shared/:Ico / ScrollArea / useChatWidth 自原型搬,去掉 "use client" 等 Next 专有件; 失效的 react-hooks disable 指令按 §17 清掉(不改渲染) - shared/typed-fetch.ts:服务端交互唯一通道,路由契约来自 @contracts/api; 不引 Hono RPC 客户端(那要求前端 import 后端类型,违反 §5.1) - app/query.ts:TanStack Query;缓存失效由订阅层驱动,组件不轮询 - features/desk-task:任务列表(一条 = 一个活动的一次运行,不是一整条工作流)/ 标题区 + 工作区四页签 / 对话区(气泡二元制 + 卡族 + **composer 锁**) - @ui:ViewContainer 用**沙箱 iframe(sandbox 不含 allow-scripts)** —— 结构上无脚本执行权, 不依赖净化算法的完备性;ViewBlocks 渲染十一种视图块 feature 不得自行 dangerouslySetInnerHTML,eslint 有规则守着 后端:工作台路由 /api/desk/*(任务清单 / 任务详情 / 工单回传 / 自由输入 / 消息); 回传与后端校验**同 schema**;任务详情是服务端投影 —— 前端零过程状态,刷新即重建 已验证(真机):任务卡「A1 RFQ需求分析 · 序位 1/7 · awaiting-event」、 预算 2/40 步、1 张工单(LLM 步失败后转人兜底那张)、7 行带归侧标签的任务日志 358 个测试通过,门禁五道全绿;web 构建通过 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
co-authored by
Claude Opus 5
parent
3df8becf67
commit
3cfaa04098
+1957
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 工作台路由 —— 五要素是**同一份过程事实的五种投影**(编码规范 §3;语义出处 OS 2.1)。
|
||||
*
|
||||
* 「web 是纯呈现与输入面」:这里出的每一样都是服务端投影,前端零过程状态、刷新即重建(§9.2)。
|
||||
*/
|
||||
import type { Hono } from 'hono';
|
||||
import type { ApiError, ChatMessage, TaskCard, ViewBlock } from '@contracts';
|
||||
import {
|
||||
ATTRIBUTION_SIDE_LABEL,
|
||||
listMessagesRoute,
|
||||
listTasksRoute,
|
||||
replyWorkOrderRoute,
|
||||
sayRoute,
|
||||
taskDetailRoute,
|
||||
taskIdSchema,
|
||||
workOrderReplySchema,
|
||||
} from '@contracts';
|
||||
import { ReplyRejectedError } from '@modules/human-tool';
|
||||
import type { Container } from '../container.ts';
|
||||
import type { AppEnv } from '../http-env.ts';
|
||||
|
||||
export function registerDeskRoutes(app: Hono<AppEnv>, container: Container): void {
|
||||
/** 当前登录者;缺会话即 401 */
|
||||
async function me(account: string) {
|
||||
const staff = await container.org.findByAccount(account);
|
||||
return staff;
|
||||
}
|
||||
|
||||
/** 任务卡 —— 一条 = 一个活动的一次运行;序位取自工作流的节点实例 */
|
||||
async function toCard(task: Awaited<ReturnType<Container['task']['get']>>): Promise<TaskCard> {
|
||||
let position = 1;
|
||||
let total = 1;
|
||||
let subject = task.title;
|
||||
let workflowState: TaskCard['workflowState'] = 'running';
|
||||
|
||||
if (task.workflowId !== undefined) {
|
||||
const workflow = await container.process.getWorkflow(task.workflowId).catch(() => undefined);
|
||||
if (workflow !== undefined) {
|
||||
subject = workflow.subject;
|
||||
workflowState = workflow.state;
|
||||
const pkg = await container.process.getPackage(workflow.packageId, workflow.packageVersion);
|
||||
const activityNodes = (pkg?.nodes ?? []).filter((n) => n.kind === 'activity' || n.kind === 'approval');
|
||||
total = Math.max(activityNodes.length, 1);
|
||||
const index = activityNodes.findIndex((n) => n.id === task.nodeId);
|
||||
position = index < 0 ? 1 : index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: task.id,
|
||||
activity: task.title,
|
||||
title: task.title,
|
||||
runtime: task.runtime,
|
||||
...(task.terminal === undefined ? {} : { terminal: task.terminal }),
|
||||
position,
|
||||
total,
|
||||
workflowSubject: subject,
|
||||
workflowState,
|
||||
startedAt: task.startedAt,
|
||||
};
|
||||
}
|
||||
|
||||
app.get(listTasksRoute.path, async (c) => {
|
||||
const staff = await me(c.get('account'));
|
||||
if (staff === undefined) {
|
||||
const body: ApiError = { code: 'unauthenticated', message: '会话无效' };
|
||||
return c.json(body, 401);
|
||||
}
|
||||
// 任务要素永远只装「我负责的任务」—— 跨人卡进对方的消息要素,不占任务清单
|
||||
const tasks = await container.task.listByOwner(staff.id);
|
||||
return c.json({ tasks: await Promise.all(tasks.map(toCard)) });
|
||||
});
|
||||
|
||||
app.get(taskDetailRoute.path, async (c) => {
|
||||
const staff = await me(c.get('account'));
|
||||
if (staff === undefined) {
|
||||
const body: ApiError = { code: 'unauthenticated', message: '会话无效' };
|
||||
return c.json(body, 401);
|
||||
}
|
||||
const taskId = taskIdSchema.parse(c.req.param('taskId'));
|
||||
const task = await container.task.get(taskId);
|
||||
|
||||
const [workOrders, chat, lines] = await Promise.all([
|
||||
container.humanTool.openWorkOrders(taskId),
|
||||
container.interaction.timeline(taskId),
|
||||
container.memory.taskLog(taskId),
|
||||
]);
|
||||
const cards = await container.humanTool.chatCards(taskId);
|
||||
|
||||
// 对话流 = 时间线 + 对话卡族,按时间全序 —— 与任务记忆同源、不同筛选
|
||||
const merged: ChatMessage[] = [...chat, ...cards].sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : 0));
|
||||
|
||||
// 视图区:活动推送的结构化块(尚无推送时给一份进度概览,不留空屏)
|
||||
const view: ViewBlock[] = [
|
||||
{
|
||||
t: 'kv',
|
||||
title: '任务概览',
|
||||
items: [
|
||||
{ k: '活动', v: task.activityId },
|
||||
{ k: '负责人', v: staff.name },
|
||||
{ k: '状态', v: task.runtime },
|
||||
{ k: '已用步数', v: `${task.used.steps} / ${task.caps.steps}` },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return c.json({
|
||||
card: await toCard(task),
|
||||
goal: task.title,
|
||||
budget: {
|
||||
steps: `${task.used.steps}/${task.caps.steps}`,
|
||||
tokens: `${task.used.tokens}/${task.caps.tokens}`,
|
||||
activeMinutes: `${Math.round(task.used.activeMs / 60_000)}/${task.caps.activeMinutes}`,
|
||||
humanCalls: `${task.used.humanCalls}/${task.caps.humanCalls}`,
|
||||
},
|
||||
stages: [],
|
||||
deliverables: [],
|
||||
workOrders,
|
||||
chat: merged,
|
||||
view,
|
||||
log: lines.map((l) => ({
|
||||
id: l.id,
|
||||
at: l.at,
|
||||
side: l.side,
|
||||
sideLabel: ATTRIBUTION_SIDE_LABEL[l.side],
|
||||
title: l.title,
|
||||
...(l.summary === undefined ? {} : { summary: l.summary }),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
/** 回传:与后端回传校验**同 schema** —— 前端提交校验用的也是它 */
|
||||
app.post(replyWorkOrderRoute.path, async (c) => {
|
||||
const staff = await me(c.get('account'));
|
||||
if (staff === undefined) {
|
||||
const body: ApiError = { code: 'unauthenticated', message: '会话无效' };
|
||||
return c.json(body, 401);
|
||||
}
|
||||
const parsed = workOrderReplySchema.safeParse((await c.req.json())['reply']);
|
||||
if (!parsed.success) {
|
||||
const body: ApiError = { code: 'validation-failed', message: '回传不合协议', detail: parsed.error.issues };
|
||||
return c.json(body, 400);
|
||||
}
|
||||
try {
|
||||
const outcome = await container.humanTool.replyWorkOrder(parsed.data, staff.id);
|
||||
return c.json({ ok: true, producesCompletion: outcome.producesCompletion });
|
||||
} catch (error) {
|
||||
if (error instanceof ReplyRejectedError) {
|
||||
// 驳回必附意见 / 必填未回填 / 已答不得再答 —— 三规矩的回传面
|
||||
const body: ApiError = { code: 'validation-failed', message: error.message, detail: error.violations };
|
||||
return c.json(body, 400);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
/** 自由输入 —— 人事件的第二形态:写入任务记忆,loop 作为观察;不驱动 flow 步 */
|
||||
app.post(sayRoute.path, async (c) => {
|
||||
const staff = await me(c.get('account'));
|
||||
if (staff === undefined) {
|
||||
const body: ApiError = { code: 'unauthenticated', message: '会话无效' };
|
||||
return c.json(body, 401);
|
||||
}
|
||||
const taskId = taskIdSchema.parse(c.req.param('taskId'));
|
||||
const parsed = sayRoute.body.safeParse(await c.req.json());
|
||||
if (!parsed.success) {
|
||||
const body: ApiError = { code: 'validation-failed', message: '入参不合法' };
|
||||
return c.json(body, 400);
|
||||
}
|
||||
return c.json(await container.interaction.say(taskId, staff.id, parsed.data.text));
|
||||
});
|
||||
|
||||
app.get(listMessagesRoute.path, async (c) => {
|
||||
const staff = await me(c.get('account'));
|
||||
if (staff === undefined) {
|
||||
const body: ApiError = { code: 'unauthenticated', message: '会话无效' };
|
||||
return c.json(body, 401);
|
||||
}
|
||||
const messages = await container.message.inbox(staff.id);
|
||||
return c.json({
|
||||
messages: messages.map((m) => ({
|
||||
id: m.id,
|
||||
kind: m.kind,
|
||||
title: m.title,
|
||||
body: m.body,
|
||||
at: m.at,
|
||||
read: m.readAt !== undefined,
|
||||
})),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type { ApiError } from '@contracts';
|
||||
import { DEFAULT_LOGIN_ACCOUNT } from '@modules/org';
|
||||
import type { Container } from './container.ts';
|
||||
import type { AppEnv } from './http-env.ts';
|
||||
import { registerDeskRoutes } from './routes/desk.ts';
|
||||
import { registerGovernanceRoutes } from './routes/governance.ts';
|
||||
import { registerIngestRoutes } from './routes/ingest.ts';
|
||||
import { registerWorkbenchRoutes } from './routes/workbench.ts';
|
||||
@@ -40,6 +41,7 @@ export function createServer(container: Container): Hono<AppEnv> {
|
||||
|
||||
registerWorkbenchRoutes(app, container);
|
||||
registerGovernanceRoutes(app, container);
|
||||
registerDeskRoutes(app, container);
|
||||
registerIngestRoutes(app, container);
|
||||
|
||||
return app;
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.101.4",
|
||||
"@tanstack/react-router": "^1.170.18",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
|
||||
@@ -1,10 +1,196 @@
|
||||
/**
|
||||
* 应用壳 —— M0 骨架。工作台五要素 / 治理台九功能的 features 自 M6 起挂入。
|
||||
* 工作台外壳 —— 自原型 `(proto)/page.tsx` 平移(README《前端 UI · 原型 1:1 平移》)。
|
||||
*
|
||||
* 左轨 = 品牌 → **工作台五要素**(任务 / 审批 / 消息 / 纪要 / 汇报)→ 轨底身份;
|
||||
* 任务四区 = 左栏列表区 / 中上标题区 / 中下工作区 / 右栏对话区。
|
||||
*
|
||||
* **前端零过程状态**:数据全部来自服务端投影,刷新即从投影重建(§9.2)。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type { Session, TaskCard, TaskDetail } from '@contracts';
|
||||
import { listMessagesRoute, listTasksRoute, replyWorkOrderRoute, sayRoute, sessionRoute, taskDetailRoute } from '@contracts';
|
||||
import { TaskChat, TaskList, ViewRegion } from '@features/desk-task';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import { call } from '@web-shared/typed-fetch.ts';
|
||||
import { useChatWidth } from '@web-shared/useChatWidth.ts';
|
||||
import { queryKeys } from './query.ts';
|
||||
|
||||
type ModuleKey = 'tasks' | 'approvals' | 'messages' | 'minutes' | 'report';
|
||||
|
||||
const NAV_MODULES: ReadonlyArray<{ key: ModuleKey; label: string; icon: string }> = [
|
||||
{ key: 'tasks', label: '任务', icon: 'list-checks' },
|
||||
{ key: 'approvals', label: '审批', icon: 'file-check' },
|
||||
{ key: 'messages', label: '消息', icon: 'messages-square' },
|
||||
{ key: 'minutes', label: '纪要', icon: 'notebook-pen' },
|
||||
{ key: 'report', label: '汇报', icon: 'bar-chart' },
|
||||
];
|
||||
|
||||
export function AppShell() {
|
||||
const queryClient = useQueryClient();
|
||||
const [module, setModule] = useState<ModuleKey>('tasks');
|
||||
const [taskId, setTaskId] = useState('');
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const split = useChatWidth();
|
||||
|
||||
const session = useQuery({
|
||||
queryKey: queryKeys.session,
|
||||
queryFn: () => call<Session>(sessionRoute),
|
||||
});
|
||||
|
||||
const tasks = useQuery({
|
||||
queryKey: queryKeys.tasks,
|
||||
queryFn: () => call<{ tasks: TaskCard[] }>(listTasksRoute),
|
||||
});
|
||||
|
||||
const messages = useQuery({
|
||||
queryKey: queryKeys.messages,
|
||||
queryFn: () => call<{ messages: Array<{ id: string; title: string; body: string; at: string; read: boolean }> }>(
|
||||
listMessagesRoute,
|
||||
),
|
||||
enabled: module === 'messages',
|
||||
});
|
||||
|
||||
const list = tasks.data?.tasks ?? [];
|
||||
const active = list.filter((t) => t.terminal === undefined);
|
||||
const current = taskId !== '' ? taskId : (active[0]?.id ?? list[0]?.id ?? '');
|
||||
|
||||
const detail = useQuery({
|
||||
queryKey: queryKeys.task(current),
|
||||
queryFn: () => call<TaskDetail>(taskDetailRoute, { params: { taskId: current } }),
|
||||
enabled: current !== '',
|
||||
});
|
||||
|
||||
const reply = useMutation({
|
||||
mutationFn: (payload: Record<string, unknown>) =>
|
||||
call<{ ok: boolean }>(replyWorkOrderRoute, { body: { reply: payload } }),
|
||||
// 回传后按目标失效缓存 —— 组件不自行轮询
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.task(current) });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.tasks });
|
||||
},
|
||||
});
|
||||
|
||||
const say = useMutation({
|
||||
mutationFn: (text: string) => call(sayRoute, { params: { taskId: current }, body: { text } }),
|
||||
onSuccess: () => void queryClient.invalidateQueries({ queryKey: queryKeys.task(current) }),
|
||||
});
|
||||
|
||||
const me = session.data?.staff.name ?? '—';
|
||||
// composer 锁:有卡在等作答时锁住自由输入
|
||||
const locked = (detail.data?.workOrders ?? []).some((o) => o.done === undefined);
|
||||
|
||||
return (
|
||||
<div className="proto-root">
|
||||
<main className="app-shell">JCAI · 任务操作系统 —— M0 骨架</main>
|
||||
<div className="app-shell">
|
||||
<div className="app-body">
|
||||
<nav className="app-nav">
|
||||
<div className="app-nav-mark" title="JCAI 智能协同工作台">
|
||||
<Ico name="bot" s={20} />
|
||||
</div>
|
||||
<span className="jc-rail-brand">JCAI</span>
|
||||
{NAV_MODULES.map((m) => {
|
||||
const badge = m.key === 'tasks' ? active.length : undefined;
|
||||
return (
|
||||
<button
|
||||
key={m.key}
|
||||
className={module === m.key ? 'active' : ''}
|
||||
onClick={() => setModule(m.key)}
|
||||
aria-current={module === m.key ? 'page' : undefined}
|
||||
>
|
||||
<span className="app-nav-icon-wrap">
|
||||
<Ico name={m.icon} s={20} />
|
||||
{badge !== undefined && badge > 0 ? <span className="app-nav-badge">{badge}</span> : null}
|
||||
</span>
|
||||
<span>{m.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="jc-rail-foot">
|
||||
<div className="jc-nav-user" title={`${me}(本工作台所有者)`}>
|
||||
<span className="jc-nav-ava">{me.slice(0, 1)}</span>
|
||||
<span className="jc-nav-name">{me}</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{module === 'tasks' ? (
|
||||
<div className={'workspace' + (collapsed ? ' jc-side-collapsed' : '')} style={split.style}>
|
||||
{collapsed && (
|
||||
<button
|
||||
type="button"
|
||||
className="jc-wl-expand"
|
||||
title="展开列表区"
|
||||
aria-label="展开列表区"
|
||||
onClick={() => setCollapsed(false)}
|
||||
>
|
||||
<Ico name="chevron-right" s={12} />
|
||||
</button>
|
||||
)}
|
||||
<TaskList
|
||||
tasks={list}
|
||||
selectedId={current}
|
||||
collapsed={collapsed}
|
||||
onPick={setTaskId}
|
||||
onCollapse={() => setCollapsed(true)}
|
||||
/>
|
||||
{detail.data !== undefined ? (
|
||||
<ViewRegion key={current} detail={detail.data} onReply={(payload) => reply.mutate(payload)} />
|
||||
) : (
|
||||
<main className="execution-panel jc-panel-card">
|
||||
<div className="jc-list-empty">
|
||||
<Ico name="list-checks" s={18} />
|
||||
{tasks.isLoading ? '载入中…' : '还没有落到你工位的任务'}
|
||||
</div>
|
||||
</main>
|
||||
)}
|
||||
{detail.data !== undefined && (
|
||||
<i
|
||||
className="jc-vsplit"
|
||||
title="拖拽调整对话区宽度 · 双击复位"
|
||||
onPointerDown={split.onSplitDown}
|
||||
onDoubleClick={split.onSplitReset}
|
||||
/>
|
||||
)}
|
||||
{detail.data !== undefined && (
|
||||
<TaskChat
|
||||
key={current}
|
||||
chat={detail.data.chat}
|
||||
locked={locked}
|
||||
onSay={(text) => say.mutate(text)}
|
||||
onDecide={() => undefined}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<main className="execution-panel jc-panel-card">
|
||||
<header className="jc-vr-head">
|
||||
<div className="jc-vr-title">
|
||||
<h1>{NAV_MODULES.find((m) => m.key === module)?.label}</h1>
|
||||
<span className="jc-vr-sub">工作台五要素 —— 同一份过程事实的五种投影</span>
|
||||
</div>
|
||||
</header>
|
||||
{module === 'messages' ? (
|
||||
<ul className="jc-msg-list">
|
||||
{(messages.data?.messages ?? []).map((m) => (
|
||||
<li key={m.id} className={m.read ? 'read' : ''}>
|
||||
<b>{m.title}</b>
|
||||
<p>{m.body}</p>
|
||||
<time className="jc-mono">{m.at.slice(0, 19).replace('T', ' ')}</time>
|
||||
</li>
|
||||
))}
|
||||
{(messages.data?.messages ?? []).length === 0 && (
|
||||
<li className="jc-list-empty">暂无消息</li>
|
||||
)}
|
||||
</ul>
|
||||
) : (
|
||||
<div className="jc-list-empty">
|
||||
<Ico name="bar-chart" s={18} />
|
||||
该要素随 M6 后续接入
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
/**
|
||||
* apps/web 唯一组装层(编码规范 §9):路由、Provider、鉴权壳、SSE 连接。
|
||||
* M0:骨架 —— 两台原型的 1:1 平移自 M6 起(README《前端 UI · 原型 1:1 平移》)。
|
||||
* apps/web 唯一组装层(编码规范 §9):Provider、鉴权壳、SSE 连接。
|
||||
* 样式整文件搬自两台原型:设计令牌 + 全部 `jc-*` 组件样式 + vendor 皮肤。
|
||||
*/
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import './styles/proto.vendor.css';
|
||||
import './styles/app.css';
|
||||
import './styles/theme.css';
|
||||
import './styles/console.css';
|
||||
import { AppShell } from './app-shell.tsx';
|
||||
import { queryClient } from './query.ts';
|
||||
|
||||
const container = document.getElementById('root');
|
||||
if (!container) throw new Error('#root 缺失');
|
||||
|
||||
createRoot(container).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{/* .proto-root 是设计令牌的作用域(原型 app.css 的 `.proto-root .app-shell`) */}
|
||||
<div className="proto-root">
|
||||
<AppShell />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 服务端状态 —— TanStack Query(编码规范 §1 / §9.2)。
|
||||
* 「SSE 事件到达 → 按信封目标失效对应缓存,由订阅层统一做;组件禁止自行轮询或手写缓存。」
|
||||
*/
|
||||
import { QueryClient } from '@tanstack/react-query';
|
||||
|
||||
export const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// 缓存失效由 SSE 订阅层驱动,不靠组件轮询
|
||||
refetchOnWindowFocus: false,
|
||||
retry: 1,
|
||||
staleTime: 5_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const queryKeys = {
|
||||
session: ['session'] as const,
|
||||
tasks: ['desk', 'tasks'] as const,
|
||||
task: (id: string) => ['desk', 'task', id] as const,
|
||||
messages: ['desk', 'messages'] as const,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,460 @@
|
||||
/* ============================================================================
|
||||
JCAI 治理台 · 控制台设计层(2026-07-25 二次重做:分级页面)
|
||||
————————————————————————————————————————————————
|
||||
治理面 ≠ 工单面。工作台是「一条一条把活干完」;治理台是「看住总体、处置偏差、配置边界」。
|
||||
这一版把「一屏摞四种维度」拆开:
|
||||
|
||||
· 深色轨 + 深色顶栏(与工作台的浅色壳明确区分,一眼知道自己在管理面)
|
||||
· 区首页 = 面包屑 + 区头 + **页签**,一个页签 = 一个维度(主视图 / 次要清单 / 需处置 / 概览)
|
||||
· 深挖 = 整页下钻,层层有面包屑:区 → 对象 → 子维度 → 元素;编排画布独占整页
|
||||
· 动作 = **就地确认带**(影响预览三必答),不弹窗、不遮挡;抽屉与模态一律不用
|
||||
· 密度靠图形承载(分布条、环、迷你柱、泳道),不靠塞列
|
||||
========================================================================== */
|
||||
|
||||
.gov-root {
|
||||
--nav: #18222e;
|
||||
--nav-2: #202c3a;
|
||||
--nav-line: #2b3a4b;
|
||||
--nav-ink: #93a3b5;
|
||||
--nav-on: #ffffff;
|
||||
--top: #131c26;
|
||||
--face: #eceff4; /* 工作面 */
|
||||
--card: #ffffff;
|
||||
--cline: #e2e7ee;
|
||||
--cshadow: 0 1px 2px rgba(19, 28, 38, .05);
|
||||
--cshadow-2: 0 8px 24px -14px rgba(19, 28, 38, .5);
|
||||
}
|
||||
|
||||
/* ============ 外壳 ============ */
|
||||
.g-shell { height: 100vh; display: grid; grid-template-columns: 76px minmax(0, 1fr); background: var(--face); overflow: hidden; }
|
||||
|
||||
/* ---- 深色左轨 ---- */
|
||||
.g-nav { display: flex; flex-direction: column; background: var(--nav); border-right: 1px solid var(--nav-line); padding: 12px 8px 10px; gap: 3px; overflow-y: auto; scrollbar-width: none; }
|
||||
.g-nav::-webkit-scrollbar { display: none; }
|
||||
.g-nav-mark { flex: none; width: 40px; height: 40px; margin: 0 auto 6px; border-radius: 11px; display: grid; place-items: center; color: #fff; background: linear-gradient(140deg, #3370ff, #7f3bf5); box-shadow: 0 4px 14px -4px rgba(51, 112, 255, .8); }
|
||||
.g-nav-brand { text-align: center; font-size: 9px; font-weight: 800; letter-spacing: .18em; color: #5d6f83; margin-bottom: 10px; }
|
||||
.g-nav-b { position: relative; flex: 0 0 auto; display: grid; justify-items: center; align-content: center; gap: 5px; min-height: 50px; padding: 6px 2px; border: 0; border-radius: 10px; background: transparent; color: var(--nav-ink); font: inherit; font-size: 11px; font-weight: 600; cursor: pointer; transition: background var(--t-fast), color var(--t-fast); }
|
||||
.g-nav-b:hover { background: var(--nav-2); color: #dbe4ee; }
|
||||
.g-nav-b.on { background: var(--nav-2); color: var(--nav-on); font-weight: 700; }
|
||||
.g-nav-b.on::before { content: ""; position: absolute; left: -8px; top: 10px; bottom: 10px; width: 3px; border-radius: 0 3px 3px 0; background: #3370ff; }
|
||||
.g-nav-ico { position: relative; display: inline-grid; place-items: center; }
|
||||
.g-nav-badge { position: absolute; top: -7px; right: -12px; min-width: 17px; height: 17px; display: inline-flex; align-items: center; justify-content: center; padding: 0 5px; border-radius: 999px; background: #f54a45; color: #fff; font-size: 10px; font-weight: 800; line-height: 1; border: 2px solid var(--nav); }
|
||||
.g-nav-foot { margin-top: auto; display: grid; justify-items: center; gap: 5px; padding-top: 12px; border-top: 1px solid var(--nav-line); }
|
||||
.g-nav-ava { width: 30px; height: 30px; border-radius: 999px; display: grid; place-items: center; color: #fff; font-size: 12px; font-weight: 800; background: linear-gradient(140deg, #2bc7d4, #0e9fd8); }
|
||||
.g-nav-foot span { font-size: 10px; color: #5d6f83; }
|
||||
|
||||
/* ---- 深色顶栏:环境 + 全局检索 + 时间窗 + 告警 + 事件流 ---- */
|
||||
.g-body { min-width: 0; display: flex; flex-direction: column; min-height: 0; }
|
||||
.g-top { flex: none; height: 52px; display: flex; align-items: center; gap: 14px; padding: 0 18px; background: var(--top); border-bottom: 1px solid #0c141c; }
|
||||
.g-top-brand { display: flex; align-items: baseline; gap: 8px; color: #fff; font-size: 13.5px; font-weight: 800; letter-spacing: -.01em; }
|
||||
.g-top-brand em { font-style: normal; font-size: 10.5px; font-weight: 600; color: #6d8199; }
|
||||
.g-search { flex: 1; max-width: 460px; display: flex; align-items: center; gap: 8px; height: 32px; padding: 0 11px; border-radius: 9px; background: #1d2833; border: 1px solid #2b3a4b; color: #8fa1b4; transition: border-color var(--t-fast); }
|
||||
.g-search:focus-within { border-color: #3370ff; }
|
||||
.g-search input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; font: inherit; font-size: 12px; color: #e6edf5; }
|
||||
.g-search input::placeholder { color: #5d6f83; }
|
||||
.g-search kbd { font-family: var(--mono); font-size: 10px; color: #5d6f83; border: 1px solid #2b3a4b; border-radius: 5px; padding: 1px 5px; }
|
||||
.g-top-r { margin-left: auto; display: flex; align-items: center; gap: 8px; }
|
||||
.g-tbtn { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 11px; border-radius: 8px; border: 1px solid #2b3a4b; background: #1d2833; color: #b9c4d0; font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer; transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast); }
|
||||
.g-tbtn:hover { background: #253242; color: #fff; }
|
||||
.g-tbtn b { font-family: var(--mono); font-size: 11px; color: #fff; }
|
||||
.g-tbtn.alert { border-color: #5b2a2a; background: #2a1a1c; color: #ff9b96; }
|
||||
.g-tbtn.alert b { color: #ff6b64; }
|
||||
.g-tbtn.flash { border-color: #3370ff; background: #1b2c4d; color: #cddffd; }
|
||||
|
||||
/* ---- 工作面 ---- */
|
||||
.g-page { flex: 1; min-height: 0; overflow-y: auto; padding: 14px 22px 30px; display: flex; flex-direction: column; gap: 16px; scrollbar-width: thin; }
|
||||
.g-detail { display: flex; flex-direction: column; gap: 16px; animation: gRise .18s cubic-bezier(.3, 0, .2, 1) both; }
|
||||
@keyframes gRise { from { opacity: 0; transform: translateY(5px); } }
|
||||
|
||||
/* 面包屑:每一级都能回跳 */
|
||||
.g-crumb { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; min-height: 28px; }
|
||||
.g-back { flex: none; width: 28px; height: 28px; margin-right: 2px; border-radius: 8px; border: 1px solid var(--cline); background: #fff; color: var(--ink-3); display: grid; place-items: center; cursor: pointer; transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast); }
|
||||
.g-back:hover { background: var(--brand-wash); border-color: #c5d8ff; color: var(--brand-deep); }
|
||||
.g-crumb-i { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; }
|
||||
.g-crumb-i .sep { font-style: normal; color: #b6bfc9; }
|
||||
.g-crumb-i button { border: 0; background: none; padding: 0; font: inherit; font-size: 11.5px; color: var(--ink-3); cursor: pointer; }
|
||||
.g-crumb-i button:hover { color: var(--brand); text-decoration: underline; }
|
||||
.g-crumb-i b { font-weight: 700; color: var(--ink-2); }
|
||||
|
||||
/* 区头 */
|
||||
.g-head { display: flex; align-items: flex-start; gap: 13px; }
|
||||
.g-head-i { flex: none; width: 38px; height: 38px; border-radius: 11px; display: grid; place-items: center; color: #fff; }
|
||||
.g-head-t { flex: 1; min-width: 0; }
|
||||
.g-head-t h1 { margin: 0; font-size: 18px; font-weight: 800; letter-spacing: -.015em; color: var(--ink); }
|
||||
.g-head-t p { margin: 5px 0 0; font-size: 12.5px; line-height: 1.7; color: var(--ink-3); max-width: 108ch; }
|
||||
|
||||
/* 区内页签:一个页签 = 一个维度 */
|
||||
.g-tabs { display: flex; flex-wrap: wrap; gap: 3px; padding: 4px; border-radius: 12px; background: #e3e8ef; align-self: flex-start; }
|
||||
.g-tab { display: inline-flex; align-items: center; gap: 7px; height: 32px; padding: 0 14px; border: 0; border-radius: 9px; background: transparent; color: var(--ink-3); font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; transition: background var(--t-fast), color var(--t-fast); }
|
||||
.g-tab svg { color: var(--ink-4); }
|
||||
.g-tab:hover { color: var(--ink); background: rgba(255, 255, 255, .55); }
|
||||
.g-tab.on { background: #fff; color: var(--ink); font-weight: 700; box-shadow: var(--cshadow); }
|
||||
.g-tab.on svg { color: var(--brand); }
|
||||
.g-tab b { min-width: 17px; height: 17px; padding: 0 5px; border-radius: 999px; background: var(--danger-accent); color: #fff; font-family: var(--mono); font-size: 10px; font-weight: 800; display: inline-grid; place-items: center; }
|
||||
|
||||
/* 段标题 */
|
||||
.g-band { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 800; letter-spacing: .01em; color: var(--ink-2); }
|
||||
.g-band svg { color: var(--ink-4); }
|
||||
.g-band .x { margin-left: auto; font-size: 10.5px; font-weight: 600; color: var(--ink-4); }
|
||||
.g-sec { display: flex; flex-direction: column; gap: 10px; }
|
||||
|
||||
/* ============ 详情页头 + 就地确认带 ============ */
|
||||
.g-dhwrap { display: flex; flex-direction: column; gap: 10px; }
|
||||
.g-dh { display: flex; align-items: flex-start; gap: 14px; padding: 16px 18px; background: var(--card); border: 1px solid var(--cline); border-radius: 14px; box-shadow: var(--cshadow); }
|
||||
.g-dh-i { flex: none; width: 42px; height: 42px; border-radius: 12px; display: grid; place-items: center; color: var(--tone-ink, var(--brand-deep)); background: var(--tone-soft, var(--brand-wash)); }
|
||||
.g-dh-t { flex: 1; min-width: 0; }
|
||||
.g-dh-t em { display: block; font-style: normal; font-family: var(--mono); font-size: 10.5px; color: var(--ink-4); }
|
||||
.g-dh-t h2 { margin: 3px 0 0; font-size: 17px; font-weight: 800; letter-spacing: -.015em; color: var(--ink); }
|
||||
.g-dh-t .chips { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 9px; }
|
||||
.g-dh-t p { margin: 9px 0 0; font-size: 12px; line-height: 1.7; color: var(--ink-3); max-width: 104ch; }
|
||||
.g-dh-a { flex: none; display: flex; gap: 8px; padding-top: 2px; }
|
||||
|
||||
.g-acts { display: flex; flex-direction: column; gap: 10px; }
|
||||
.g-acts-r { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
/* 就地确认:影响预览三必答,展开在原地,不遮挡上下文 */
|
||||
.g-ask { display: flex; flex-direction: column; gap: 11px; padding: 14px 16px; border-radius: 13px; background: #fff; border: 1px solid var(--tone-line, var(--cline)); border-left: 3px solid var(--tone, var(--warn-accent)); box-shadow: var(--cshadow); animation: gDrop .16s cubic-bezier(.3, 0, .2, 1) both; }
|
||||
.g-ask-h { display: flex; align-items: center; gap: 8px; font-size: 13.5px; font-weight: 800; color: var(--ink); }
|
||||
.g-ask-h svg { color: var(--tone, var(--warn)); }
|
||||
.g-ask-a { display: flex; justify-content: flex-end; gap: 9px; }
|
||||
.g-impact { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--line-soft); border: 1px solid var(--line-soft); border-radius: 11px; overflow: hidden; }
|
||||
.g-impact > div { background: #fff; padding: 11px 13px; display: flex; flex-direction: column; gap: 4px; }
|
||||
.g-impact em { font-style: normal; font-size: 10.5px; color: var(--ink-4); }
|
||||
.g-impact b { font-size: 12px; font-weight: 600; line-height: 1.55; color: var(--ink); }
|
||||
@keyframes gDrop { from { opacity: 0; transform: translateY(-5px); } }
|
||||
|
||||
/* ============ 概览页签:KPI 块 ============ */
|
||||
.g-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(176px, 1fr)); gap: 12px; }
|
||||
.g-kpi { position: relative; display: flex; flex-direction: column; gap: 7px; padding: 14px 16px; background: var(--card); border: 1px solid var(--cline); border-radius: 13px; box-shadow: var(--cshadow); overflow: hidden; }
|
||||
.g-kpi::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--tone, #c9d2dd); }
|
||||
.g-kpi-l { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--ink-4); }
|
||||
.g-kpi-l svg { color: var(--tone-ink, var(--ink-4)); }
|
||||
.g-kpi-v { font-size: 26px; font-weight: 800; font-family: var(--mono); letter-spacing: -.03em; line-height: 1; color: var(--ink); }
|
||||
.g-kpi-v small { font-size: 12px; font-weight: 600; font-family: var(--sans); color: var(--ink-4); margin-left: 4px; }
|
||||
.g-kpi.t-ok .g-kpi-v { color: var(--ok); } .g-kpi.t-warn .g-kpi-v { color: var(--warn); }
|
||||
.g-kpi.t-danger .g-kpi-v { color: var(--danger); } .g-kpi.t-brand .g-kpi-v { color: var(--brand-deep); }
|
||||
.g-kpi-s { font-size: 11px; line-height: 1.5; color: var(--ink-4); }
|
||||
|
||||
/* 环形(比率型指标) */
|
||||
.g-dial { display: grid; place-items: center; }
|
||||
.g-dial svg { transform: rotate(-90deg); }
|
||||
.g-dial .trk { fill: none; stroke: #eaeef3; }
|
||||
.g-dial .val { fill: none; stroke-linecap: round; }
|
||||
|
||||
/* 小事实块(预算 / 要素统计 / 负载) */
|
||||
.g-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(158px, 1fr)); gap: 10px; }
|
||||
.g-fact { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; background: var(--card); border: 1px solid var(--cline); border-radius: 12px; box-shadow: var(--cshadow); }
|
||||
.g-fact em { font-style: normal; font-size: 10.5px; color: var(--ink-4); }
|
||||
.g-fact b { font-family: var(--mono); font-size: 15px; font-weight: 800; color: var(--ink); letter-spacing: -.01em; }
|
||||
.g-fact .t { height: 4px; border-radius: 999px; background: #eaeef3; overflow: hidden; }
|
||||
.g-fact .t i { display: block; height: 100%; border-radius: 999px; background: var(--tone, var(--brand)); }
|
||||
|
||||
/* ============ 需处置页签:偏差 + 就地处置 ============ */
|
||||
.g-todo { display: flex; flex-direction: column; gap: 10px; }
|
||||
/* 一行说清偏差 + 右侧就地处置;确认带展开时整行贯通 */
|
||||
.g-todo-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: 16px; row-gap: 11px; padding: 13px 15px; background: var(--card); border: 1px solid var(--cline); border-left: 3px solid var(--tone, var(--danger-accent)); border-radius: 12px; box-shadow: var(--cshadow); }
|
||||
.g-todo-card .g-acts { display: contents; }
|
||||
.g-todo-card .g-ask { grid-column: 1 / -1; }
|
||||
.g-todo-row { display: flex; align-items: flex-start; gap: 12px; min-width: 0; }
|
||||
.g-todo-i { flex: none; width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; color: var(--tone-ink, var(--danger)); background: var(--tone-soft, var(--danger-soft)); }
|
||||
.g-todo-t { flex: 1; min-width: 0; line-height: 1.55; }
|
||||
.g-todo-t b { display: block; font-size: 13px; font-weight: 700; color: var(--ink); }
|
||||
.g-todo-t span { display: block; margin-top: 2px; font-size: 11.5px; color: var(--ink-3); }
|
||||
.g-todo-card .g-acts-r { justify-content: flex-end; }
|
||||
.g-clear { display: flex; align-items: center; gap: 9px; padding: 13px 15px; border-radius: 11px; background: var(--ok-soft); border: 1px solid #bfedb4; color: var(--ok); font-size: 12.5px; font-weight: 600; }
|
||||
|
||||
/* ============ 运行看板:状态泳道 ============ */
|
||||
.g-board { display: grid; grid-template-columns: repeat(auto-fit, minmax(238px, 1fr)); gap: 12px; align-items: start; }
|
||||
.g-lane { display: flex; flex-direction: column; gap: 9px; padding: 12px; border-radius: 13px; background: #e6eaf0; border: 1px solid #dbe1e9; }
|
||||
.g-lane-h { display: flex; align-items: center; gap: 7px; font-size: 11.5px; font-weight: 800; color: var(--ink-2); }
|
||||
.g-lane-h i { width: 8px; height: 8px; border-radius: 999px; background: var(--tone, var(--brand)); }
|
||||
.g-lane-h b { margin-left: auto; font-family: var(--mono); font-size: 11px; color: var(--ink-4); }
|
||||
.g-lane-e { padding: 6px 2px; font-size: 11.5px; color: var(--ink-4); }
|
||||
.g-wf { display: flex; flex-direction: column; gap: 8px; padding: 11px 12px; background: var(--card); border: 1px solid var(--cline); border-radius: 11px; box-shadow: var(--cshadow); cursor: pointer; text-align: left; width: 100%; transition: box-shadow var(--t-fast), border-color var(--t-fast), transform var(--t-fast); }
|
||||
.g-wf:hover { border-color: #c9d6e6; box-shadow: 0 4px 12px -4px rgba(19, 28, 38, .22); transform: translateY(-1px); }
|
||||
.g-wf-top { display: flex; align-items: center; gap: 6px; font-size: 10px; font-family: var(--mono); color: var(--ink-4); }
|
||||
.g-wf-top .r { margin-left: auto; }
|
||||
.g-wf-t { font-size: 13px; font-weight: 700; line-height: 1.4; color: var(--ink); }
|
||||
.g-wf-m { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--ink-3); }
|
||||
.g-wf-m > svg { flex: none; color: var(--ink-4); }
|
||||
.g-wf-hold { padding-top: 7px; border-top: 1px dashed var(--line-soft); }
|
||||
.g-wf-hold em { font-style: normal; margin-left: auto; font-size: 10px; color: var(--ink-4); }
|
||||
.g-wf-track, .g-prog { height: 4px; border-radius: 999px; background: #eaeef3; overflow: hidden; display: flex; }
|
||||
.g-wf-track i, .g-prog i { display: block; height: 100%; background: var(--tone, var(--brand)); }
|
||||
.g-prog { height: 6px; }
|
||||
.g-prog i { background: var(--brand); }
|
||||
|
||||
/* ============ 可下钻清单:清单只导航,动作在详情页 ============ */
|
||||
.g-nlist { display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--cline); border-radius: 14px; box-shadow: var(--cshadow); overflow: hidden; }
|
||||
.g-nrow { display: flex; align-items: center; gap: 13px; width: 100%; padding: 13px 16px; border: 0; border-bottom: 1px solid var(--line-soft); background: transparent; font: inherit; text-align: left; cursor: pointer; transition: background var(--t-fast); }
|
||||
.g-nrow:last-child { border-bottom: 0; }
|
||||
.g-nrow:hover { background: #f7f9fc; }
|
||||
.g-nrow:hover .g-nrow-x { color: var(--brand); transform: translateX(2px); }
|
||||
.g-nrow-i { flex: none; width: 32px; height: 32px; border-radius: 10px; display: grid; place-items: center; color: var(--tone-ink, var(--ink-3)); background: var(--tone-soft, var(--soft)); }
|
||||
.g-nrow-t { flex: 1 1 auto; min-width: 0; line-height: 1.5; }
|
||||
.g-nrow-t b { display: block; font-size: 13px; font-weight: 700; color: var(--ink); }
|
||||
.g-nrow-t span { display: block; margin-top: 2px; font-family: var(--mono); font-size: 10.5px; color: var(--ink-4); overflow-wrap: anywhere; }
|
||||
.g-nrow-m { flex: none; display: flex; align-items: center; gap: 18px; }
|
||||
.g-nrow-c { display: flex; flex-direction: column; gap: 3px; min-width: 56px; }
|
||||
.g-nrow-c em { font-style: normal; font-size: 10px; color: var(--ink-4); }
|
||||
.g-nrow-c b { font-size: 12px; font-weight: 600; color: var(--ink-2); }
|
||||
.g-nrow-r { flex: none; min-width: 92px; display: flex; justify-content: flex-end; }
|
||||
.g-nrow-x { flex: none; color: var(--ink-4); display: grid; place-items: center; transition: color var(--t-fast), transform var(--t-fast); }
|
||||
|
||||
/* 下一级入口(详情页里指向子维度页) */
|
||||
.g-entries { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; }
|
||||
.g-entry { display: flex; align-items: center; gap: 12px; padding: 14px 16px; background: var(--card); border: 1px solid var(--cline); border-radius: 13px; box-shadow: var(--cshadow); font: inherit; text-align: left; cursor: pointer; transition: box-shadow var(--t-fast), border-color var(--t-fast), transform var(--t-fast); }
|
||||
.g-entry:hover { border-color: #c9d6e6; box-shadow: 0 5px 14px -5px rgba(19, 28, 38, .22); transform: translateY(-1px); }
|
||||
.g-entry:hover .g-entry-x { color: var(--brand); transform: translateX(2px); }
|
||||
.g-entry-i { flex: none; width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; color: var(--tone-ink, var(--brand-deep)); background: var(--tone-soft, var(--brand-wash)); }
|
||||
.g-entry-t { flex: 1; min-width: 0; line-height: 1.45; }
|
||||
.g-entry-t b { display: block; font-size: 13px; font-weight: 700; color: var(--ink); }
|
||||
.g-entry-t em { display: block; margin-top: 2px; font-style: normal; font-size: 11px; color: var(--ink-4); }
|
||||
.g-entry-m { flex: none; }
|
||||
.g-entry-x { flex: none; color: var(--ink-4); display: grid; place-items: center; transition: color var(--t-fast), transform var(--t-fast); }
|
||||
|
||||
/* 过滤芯片(日志归侧 / 事件流分区) */
|
||||
.g-filter { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||
.g-fchip { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 11px; border: 1px solid var(--cline); border-radius: 999px; background: #fff; color: var(--ink-3); font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer; transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast); }
|
||||
.g-fchip:hover { border-color: #c9d6e6; color: var(--ink); }
|
||||
.g-fchip.on { background: var(--brand); border-color: var(--brand); color: #fff; }
|
||||
.g-fchip b { font-family: var(--mono); font-size: 10.5px; opacity: .8; }
|
||||
|
||||
/* ============ 通用数据卡 ============ */
|
||||
.g-card { background: var(--card); border: 1px solid var(--cline); border-radius: 14px; box-shadow: var(--cshadow); overflow: hidden; }
|
||||
.g-card-h { display: flex; align-items: center; gap: 9px; padding: 12px 16px; border-bottom: 1px solid var(--line-soft); font-size: 12.5px; font-weight: 800; color: var(--ink-2); }
|
||||
.g-card-h svg { color: var(--ink-4); }
|
||||
.g-card-h .x { margin-left: auto; font-size: 10.5px; font-weight: 600; color: var(--ink-4); }
|
||||
.g-card-b { padding: 14px 16px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.g-code { display: block; padding: 11px 13px; border-radius: 10px; background: #f5f7fa; border: 1px solid var(--line-soft); font-family: var(--mono); font-size: 11.5px; line-height: 1.75; color: var(--ink-2); overflow-wrap: anywhere; }
|
||||
|
||||
/* ============ 矩阵表 ============ */
|
||||
.g-matrix { width: 100%; border-collapse: collapse; }
|
||||
.g-matrix th { text-align: left; font-size: 11px; font-weight: 700; color: var(--ink-4); background: #f7f9fc; padding: 9px 14px; border-bottom: 1px solid var(--line-soft); white-space: nowrap; }
|
||||
.g-matrix td { padding: 11px 14px; font-size: 12.5px; color: var(--ink-2); border-bottom: 1px solid var(--line-soft); vertical-align: middle; line-height: 1.6; }
|
||||
.g-matrix tr:last-child td { border-bottom: 0; }
|
||||
.g-matrix tbody tr:hover td { background: #fbfcfe; }
|
||||
.g-matrix td.k { font-weight: 700; color: var(--ink); }
|
||||
.g-matrix td.mono { font-family: var(--mono); font-size: 11.5px; color: var(--ink); white-space: nowrap; }
|
||||
.g-matrix td.num { font-family: var(--mono); font-size: 12.5px; font-weight: 700; text-align: right; }
|
||||
.g-matrix .cell-ok { color: var(--ok); } .g-matrix .cell-warn { color: var(--warn); } .g-matrix .cell-mute { color: var(--ink-4); }
|
||||
|
||||
/* 负载条 */
|
||||
.g-load { display: flex; align-items: center; gap: 8px; }
|
||||
.g-load-t { flex: 1; min-width: 46px; max-width: 96px; height: 6px; border-radius: 999px; background: #eaeef3; overflow: hidden; }
|
||||
.g-load-t i { display: block; height: 100%; border-radius: 999px; background: var(--tone, var(--brand)); }
|
||||
.g-load b { font-family: var(--mono); font-size: 11.5px; font-weight: 700; color: var(--ink-2); }
|
||||
|
||||
/* ============ 货架:卡片墙 ============ */
|
||||
.g-shelf { display: grid; grid-template-columns: repeat(auto-fill, minmax(264px, 1fr)); gap: 12px; }
|
||||
.g-tile { display: flex; flex-direction: column; gap: 10px; padding: 14px 15px; background: var(--card); border: 1px solid var(--cline); border-radius: 13px; box-shadow: var(--cshadow); cursor: pointer; text-align: left; font: inherit; transition: box-shadow var(--t-fast), border-color var(--t-fast), transform var(--t-fast); }
|
||||
.g-tile:hover { border-color: #c9d6e6; box-shadow: 0 5px 14px -5px rgba(19, 28, 38, .22); transform: translateY(-1px); }
|
||||
.g-tile-h { display: flex; align-items: center; gap: 9px; }
|
||||
.g-tile-i { flex: none; width: 32px; height: 32px; border-radius: 10px; display: grid; place-items: center; color: var(--tone-ink, var(--brand-deep)); background: var(--tone-soft, var(--brand-wash)); }
|
||||
.g-tile-t { flex: 1; min-width: 0; line-height: 1.35; }
|
||||
.g-tile-t b { display: block; font-size: 13.5px; font-weight: 700; color: var(--ink); }
|
||||
.g-tile-t em { display: block; font-style: normal; font-size: 10.5px; font-family: var(--mono); color: var(--ink-4); }
|
||||
.g-tile-m { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.g-tile-d { font-size: 11.5px; line-height: 1.6; color: var(--ink-3); }
|
||||
.g-tile-f { display: flex; align-items: center; gap: 8px; padding-top: 9px; border-top: 1px dashed var(--line-soft); font-size: 11px; color: var(--ink-3); }
|
||||
.g-tile-f .r { margin-left: auto; font-family: var(--mono); color: var(--ink-4); }
|
||||
|
||||
/* ============ 规则清单(触发器):整行可下钻 ============ */
|
||||
.g-rules { display: flex; flex-direction: column; }
|
||||
.g-rule { display: grid; grid-template-columns: 30px minmax(0, 1fr) 84px 108px 78px 18px; align-items: center; gap: 12px; width: 100%; padding: 13px 16px; border: 0; border-bottom: 1px solid var(--line-soft); background: transparent; font: inherit; text-align: left; cursor: pointer; transition: background var(--t-fast); }
|
||||
.g-rule:last-child { border-bottom: 0; }
|
||||
.g-rule:hover { background: #f7f9fc; }
|
||||
.g-rule:hover .g-nrow-x { color: var(--brand); transform: translateX(2px); }
|
||||
.g-rule-i { width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center; color: var(--tone-ink); background: var(--tone-soft); }
|
||||
.g-rule-t { min-width: 0; }
|
||||
.g-rule-t b { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; font-size: 13px; font-weight: 700; color: var(--ink); }
|
||||
.g-rule-t code { display: block; font-family: var(--mono); font-size: 10.5px; color: var(--ink-4); margin-top: 4px; overflow-wrap: anywhere; }
|
||||
.g-rule-n { font-size: 11.5px; color: var(--ink-3); line-height: 1.5; }
|
||||
.g-rule-n b { font-size: 13px; color: var(--ink); }
|
||||
.g-rule-n em { display: block; font-style: normal; font-size: 10.5px; color: var(--ink-4); }
|
||||
.g-bars { display: flex; align-items: flex-end; gap: 2px; height: 26px; }
|
||||
.g-bars i { flex: 1; min-width: 3px; border-radius: 2px 2px 0 0; background: var(--tone, var(--brand)); opacity: .55; }
|
||||
.g-bars i.hot { opacity: 1; }
|
||||
|
||||
/* ============ 分布条 ============ */
|
||||
.g-dist { display: flex; flex-direction: column; gap: 9px; }
|
||||
.g-dist-row { display: grid; grid-template-columns: minmax(84px, 132px) minmax(0, 1fr) 52px; align-items: center; gap: 11px; }
|
||||
.g-dist-l { font-size: 12px; color: var(--ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.g-dist-t { height: 8px; border-radius: 999px; background: #eaeef3; overflow: hidden; }
|
||||
.g-dist-t i { display: block; height: 100%; border-radius: 999px; background: var(--tone, var(--brand)); }
|
||||
.g-dist-v { font-family: var(--mono); font-size: 12px; font-weight: 700; text-align: right; color: var(--ink-2); }
|
||||
|
||||
/* ============ 标签 / 状态 / 说明 / 按钮 ============ */
|
||||
.g-chip { display: inline-flex; align-items: center; gap: 4px; font-size: 10.5px; font-weight: 700; border-radius: 6px; padding: 2px 8px; color: var(--tone-ink, var(--ink-3)); background: var(--tone-soft, var(--soft)); border: 1px solid var(--tone-line, var(--line-soft)); white-space: nowrap; }
|
||||
.g-st { display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 700; color: var(--tone-ink, var(--ink-3)); white-space: nowrap; }
|
||||
.g-st svg { color: var(--tone, var(--ink-4)); }
|
||||
.g-mono { font-family: var(--mono); }
|
||||
.g-note { display: flex; align-items: flex-start; gap: 9px; padding: 11px 14px; border-radius: 10px; font-size: 12px; line-height: 1.7; color: var(--ink-3); background: #f5f7fa; border: 1px solid var(--line-soft); }
|
||||
.g-note svg { flex: none; margin-top: 2px; color: var(--ink-4); }
|
||||
.g-note.warn { color: var(--warn); background: var(--warn-soft); border-color: #f0d5a8; }
|
||||
.g-note.warn svg { color: var(--warn); }
|
||||
.g-note.ok { color: var(--ok); background: var(--ok-soft); border-color: #bfedb4; }
|
||||
.g-note.ok svg { color: var(--ok); }
|
||||
.g-btn { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px; border: 1px solid var(--line); border-radius: 8px; background: #fff; color: var(--ink-2); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast); }
|
||||
.g-btn:hover { background: var(--soft); border-color: #cdd5df; }
|
||||
.g-btn.primary { background: var(--brand); border-color: var(--brand); color: #fff; }
|
||||
.g-btn.primary:hover { background: var(--brand-deep); border-color: var(--brand-deep); }
|
||||
.g-btn.danger { color: var(--danger); border-color: #f2c4c1; }
|
||||
.g-btn.danger:hover { background: var(--danger-soft); }
|
||||
.g-btn.armed { box-shadow: 0 0 0 2px rgba(51, 112, 255, .18); border-color: var(--brand); }
|
||||
.g-btn.danger.armed { box-shadow: 0 0 0 2px rgba(245, 74, 69, .18); border-color: var(--danger-accent); }
|
||||
.g-btn.xs { height: 26px; padding: 0 10px; font-size: 11.5px; }
|
||||
.g-btn:disabled { color: var(--ink-4); background: var(--soft); border-color: var(--line-soft); cursor: default; }
|
||||
.g-btn:disabled:hover { background: var(--soft); border-color: var(--line-soft); }
|
||||
.g-avatar { width: 21px; height: 21px; border-radius: 999px; display: inline-grid; place-items: center; color: #fff; font-size: 10px; font-weight: 800; background: linear-gradient(140deg, #2bc7d4, #0e9fd8); }
|
||||
|
||||
/* 标签值行 */
|
||||
.g-rows { display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--cline); border-radius: 12px; overflow: hidden; box-shadow: var(--cshadow); }
|
||||
.g-rows > div { display: grid; grid-template-columns: 116px minmax(0, 1fr); gap: 14px; padding: 11px 15px; border-bottom: 1px solid var(--line-soft); }
|
||||
.g-rows > div:last-child { border-bottom: 0; }
|
||||
.g-rows > div:nth-child(even) { background: #fbfcfe; }
|
||||
.g-rows em { font-style: normal; font-size: 11.5px; color: var(--ink-4); }
|
||||
.g-rows b { font-size: 12.5px; font-weight: 600; line-height: 1.65; color: var(--ink); overflow-wrap: anywhere; }
|
||||
.g-rows b.mono { font-family: var(--mono); font-size: 11.5px; }
|
||||
|
||||
/* 事件流条目 */
|
||||
.g-ev { display: flex; gap: 11px; padding: 12px 0; border-bottom: 1px solid var(--line-soft); }
|
||||
.g-ev:last-child { border-bottom: 0; }
|
||||
.g-ev-r { flex: none; width: 3px; border-radius: 3px; background: var(--tone, var(--brand)); }
|
||||
.g-ev-b { flex: 1; min-width: 0; }
|
||||
.g-ev-h { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; }
|
||||
.g-ev-h b { font-size: 12.5px; font-weight: 700; color: var(--ink); }
|
||||
.g-ev-h time { margin-left: auto; font-family: var(--mono); font-size: 10.5px; color: var(--ink-4); }
|
||||
.g-ev-b p { margin: 0; font-size: 12px; line-height: 1.65; color: var(--ink-3); }
|
||||
.g-ev-b span { display: block; margin-top: 4px; font-size: 10.5px; color: var(--ink-4); }
|
||||
|
||||
/* ============ 编排工作区:左 2/3 画布(整图不裁切)+ 右 1/3 属性栏 ============ */
|
||||
.g-orch { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 12px; align-items: start; }
|
||||
.g-canvas { border: 1px solid var(--cline); border-radius: 14px; overflow: hidden; background: #e9edf2; }
|
||||
/* 图自身按 aspect-ratio 缩放,整张一定看得全——纸面不再限高、不再内部滚动 */
|
||||
.g-canvas .process-desk { height: auto; max-height: none; overflow: visible; padding: 12px 10px 16px; background: transparent; }
|
||||
.g-canvas .process-doc { max-width: none; padding: 14px 18px 18px; border-radius: 10px; box-shadow: var(--cshadow); }
|
||||
/* 画布上的选中态(外挂类,_flow 复刻件未改):命中层描边 = 选中框 */
|
||||
.g-canvas .flow-node-sel .djs-hit { fill: rgba(51, 112, 255, .09) !important; stroke: #3370ff !important; stroke-width: 2.5px !important; stroke-opacity: 1 !important; rx: 7px; ry: 7px; }
|
||||
|
||||
/* 右栏:属性卡就地填写,不下钻 */
|
||||
.g-side { position: sticky; top: 2px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.g-side-h { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 800; color: var(--ink-2); }
|
||||
.g-side-h svg { color: var(--ink-4); }
|
||||
.g-side-h .x { margin-left: auto; font-size: 10.5px; font-weight: 600; color: var(--ink-4); }
|
||||
.g-side .g-props { grid-template-columns: minmax(0, 1fr); }
|
||||
.g-side .g-prop-b { padding: 12px 13px; gap: 11px; }
|
||||
.g-picker { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.g-pick { display: inline-flex; align-items: center; gap: 7px; height: 30px; padding: 0 11px; border: 1px solid var(--cline); border-radius: 9px; background: #fff; color: var(--ink-2); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; transition: border-color var(--t-fast), background var(--t-fast), color var(--t-fast); }
|
||||
.g-pick b { font-size: 10.5px; font-weight: 700; color: var(--tone-ink, var(--ink-4)); }
|
||||
.g-pick:hover { border-color: var(--tone-line, #c9d6e6); background: var(--tone-soft, var(--soft)); }
|
||||
.g-pick.on { border-color: var(--tone, var(--brand)); background: var(--tone-soft, var(--brand-wash)); color: var(--tone-ink, var(--brand-deep)); box-shadow: 0 0 0 2px color-mix(in srgb, var(--tone, #3370ff) 14%, transparent); }
|
||||
|
||||
.g-props { display: grid; grid-template-columns: repeat(auto-fit, minmax(292px, 1fr)); gap: 12px; align-items: start; }
|
||||
.g-prop { display: flex; flex-direction: column; background: var(--card); border: 1px solid var(--cline); border-radius: 13px; box-shadow: var(--cshadow); overflow: hidden; }
|
||||
.g-prop-h { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-bottom: 1px solid var(--line-soft); background: #fbfcfe; font-size: 12px; font-weight: 800; color: var(--ink-2); }
|
||||
.g-prop-h svg { color: var(--ink-4); }
|
||||
.g-prop-h .x { margin-left: auto; font-size: 10.5px; font-weight: 600; color: var(--ink-4); }
|
||||
.g-prop-b { padding: 13px 14px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.g-prop-ref { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
|
||||
.g-field { display: flex; flex-direction: column; gap: 6px; }
|
||||
.g-field > em { display: flex; align-items: baseline; gap: 7px; font-style: normal; font-size: 11px; font-weight: 600; color: var(--ink-3); }
|
||||
.g-field > em i { font-style: normal; font-weight: 400; font-size: 10.5px; color: var(--ink-4); }
|
||||
.g-input, .g-select, .g-ta { width: 100%; padding: 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: #fff; color: var(--ink); font: inherit; font-size: 12.5px; line-height: 1.6; transition: border-color var(--t-fast), box-shadow var(--t-fast); }
|
||||
.g-input:focus, .g-select:focus, .g-ta:focus { outline: 0; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(51, 112, 255, .12); }
|
||||
.g-input.mono { font-family: var(--mono); font-size: 11.5px; }
|
||||
.g-input[readonly] { background: var(--soft); color: var(--ink-3); }
|
||||
.g-ta { resize: vertical; min-height: 62px; }
|
||||
.g-select { appearance: none; padding-right: 26px; background-image: linear-gradient(45deg, transparent 50%, #8f959e 50%), linear-gradient(135deg, #8f959e 50%, transparent 50%); background-position: right 13px center, right 8px center; background-size: 5px 5px, 5px 5px; background-repeat: no-repeat; cursor: pointer; }
|
||||
|
||||
.g-seg { display: inline-flex; padding: 3px; gap: 2px; border-radius: 9px; background: #eef1f5; }
|
||||
.g-seg-b { flex: 1; height: 26px; padding: 0 10px; border: 0; border-radius: 7px; background: transparent; color: var(--ink-3); font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer; white-space: nowrap; transition: background var(--t-fast), color var(--t-fast); }
|
||||
.g-seg-b:hover { color: var(--ink); }
|
||||
.g-seg-b.on { background: #fff; color: var(--brand-deep); font-weight: 700; box-shadow: var(--cshadow); }
|
||||
|
||||
.g-listedit { display: flex; flex-direction: column; gap: 7px; }
|
||||
.g-le-row { display: flex; align-items: center; gap: 7px; }
|
||||
.g-le-x { flex: none; width: 30px; height: 30px; border: 1px solid var(--line); border-radius: 8px; background: #fff; color: var(--ink-4); display: grid; place-items: center; cursor: pointer; transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast); }
|
||||
.g-le-x:hover { color: var(--danger); border-color: #f2c4c1; background: var(--danger-soft); }
|
||||
.g-le-add { align-self: flex-start; display: inline-flex; align-items: center; gap: 5px; height: 26px; padding: 0 10px; border: 1px dashed var(--line); border-radius: 8px; background: transparent; color: var(--ink-3); font: inherit; font-size: 11.5px; font-weight: 600; cursor: pointer; transition: color var(--t-fast), border-color var(--t-fast); }
|
||||
.g-le-add:hover { color: var(--brand); border-color: var(--brand); }
|
||||
.g-le-e { font-size: 11.5px; color: var(--ink-4); }
|
||||
|
||||
/* 色调字典:一个 .t-* 带一整套语义色,组件只读 var(--tone*) */
|
||||
.gov-root .t-brand { --tone: #3370ff; --tone-ink: #245bdb; --tone-soft: #f0f4ff; --tone-line: #c5d8ff; }
|
||||
.gov-root .t-ok { --tone: #34c724; --tone-ink: #278717; --tone-soft: #e8f8e4; --tone-line: #bfedb4; }
|
||||
.gov-root .t-warn { --tone: #ff8800; --tone-ink: #8a5f1c; --tone-soft: #fff4e5; --tone-line: #ecd9ab; }
|
||||
.gov-root .t-danger { --tone: #f54a45; --tone-ink: #d83931; --tone-soft: #feeae9; --tone-line: #f6c6c3; }
|
||||
.gov-root .t-mute { --tone: #8f959e; --tone-ink: #46586a; --tone-soft: #eef2f6; --tone-line: #d8dee6; }
|
||||
.gov-root .t-auto { --tone: #4954e6; --tone-ink: #3a44c9; --tone-soft: #f0f1fd; --tone-line: #ccd0f7; }
|
||||
.gov-root .t-decision { --tone: #7f3bf5; --tone-ink: #5b21cf; --tone-soft: #f5f0fe; --tone-line: #ddccfb; }
|
||||
.gov-root .t-collab { --tone: #0aa294; --tone-ink: #0a6a64; --tone-soft: #e4f8f5; --tone-line: #b6e7e0; }
|
||||
.gov-root .t-wait { --tone: #6a7590; --tone-ink: #46586a; --tone-soft: #eef0f5; --tone-line: #d5dae4; }
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.g-orch { grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); }
|
||||
}
|
||||
@media (max-width: 1320px) {
|
||||
.g-nrow-m { gap: 12px; }
|
||||
.g-nrow-c { min-width: 48px; }
|
||||
}
|
||||
@media (max-width: 1150px) {
|
||||
/* 窄到放不下两栏才叠起来(body 最小宽 1120px,日常不会触发) */
|
||||
.g-orch { grid-template-columns: minmax(0, 1fr); }
|
||||
.g-side { position: static; }
|
||||
.g-side .g-props { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); }
|
||||
}
|
||||
@media (max-width: 1180px) {
|
||||
.g-shell { grid-template-columns: 64px minmax(0, 1fr); }
|
||||
.g-search { display: none; }
|
||||
.g-nrow-m { display: none; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.gov-root *, .gov-root *::before, .gov-root *::after { animation-duration: .01ms !important; transition-duration: .01ms !important; }
|
||||
}
|
||||
|
||||
/* ══ 数据区 · CRM/ERP 式库表管理(2026-07-26)══ */
|
||||
/* 工具栏:域筛选 chips + 检索 + 计数 */
|
||||
.g-dbbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
.g-dbchips { display: inline-flex; gap: 4px; padding: 3px; background: rgba(255,255,255,.6); border: 1px solid var(--line-soft); border-radius: 10px; }
|
||||
.g-dbchip { display: inline-flex; align-items: center; gap: 5px; height: 26px; padding: 0 11px; border: 0; border-radius: 8px; background: transparent; color: var(--ink-3); font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; transition: background var(--t-fast), color var(--t-fast); }
|
||||
.g-dbchip:hover:not(.on) { color: var(--ink); background: rgba(255,255,255,.8); }
|
||||
.g-dbchip.on { background: var(--brand); color: #fff; font-weight: 700; }
|
||||
.g-dbchip em { font-style: normal; font-size: 10px; font-family: var(--mono); opacity: .75; }
|
||||
.g-dbsearch { display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 11px; background: #fff; border: 1px solid var(--line-soft); border-radius: 9px; color: var(--ink-4); }
|
||||
.g-dbsearch input { border: 0; outline: none; background: none; font: inherit; font-size: 12.5px; color: var(--ink); width: 180px; }
|
||||
.g-dbcount { margin-left: auto; font-size: 11px; color: var(--ink-3); }
|
||||
/* 管理表:白卡包表,行可点 */
|
||||
.g-dbwrap { padding: 0; overflow: hidden; }
|
||||
.g-dbtable { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
||||
.g-dbtable thead th { text-align: left; padding: 9px 14px; background: #f5f7fa; color: var(--ink-3); font-size: 11px; font-weight: 700; letter-spacing: .03em; border-bottom: 1px solid var(--line-soft); white-space: nowrap; }
|
||||
.g-dbtable tbody td { padding: 10px 14px; border-bottom: 1px solid var(--line-soft); color: var(--ink); vertical-align: middle; }
|
||||
.g-dbtable tbody tr:last-child td { border-bottom: 0; }
|
||||
.g-dbtable tbody tr { transition: background var(--t-fast); }
|
||||
.g-dbtable tbody tr:hover { background: var(--brand-wash, #f0f4ff); }
|
||||
.g-dbtable tbody tr[onclick], .g-dbtable tbody tr { cursor: pointer; }
|
||||
.g-dbtable td.chev { width: 30px; color: var(--ink-4); }
|
||||
.g-dbtable tr.empty td { text-align: center; color: var(--ink-4); padding: 26px; cursor: default; }
|
||||
.g-db-ico { display: inline-grid; place-items: center; width: 26px; height: 26px; border-radius: 8px; margin-right: 9px; vertical-align: middle; }
|
||||
.g-db-ico.t-brand { background: var(--brand-soft, #e1eaff); color: var(--brand); }
|
||||
.g-db-ico.t-auto { background: #f0f1fd; color: #4954e6; }
|
||||
.g-db-name { display: inline-flex; flex-direction: column; vertical-align: middle; }
|
||||
.g-db-name b { font-weight: 700; }
|
||||
.g-db-name em { font-style: normal; font-size: 10.5px; color: var(--ink-4); }
|
||||
.g-db-dim { color: var(--ink-4); }
|
||||
/* Schema 表 / 预览表 */
|
||||
.g-schema td.fname { font-weight: 700; }
|
||||
.g-schema td.ftype { color: var(--ink-3); font-size: 11.5px; }
|
||||
.g-schema td.fsample { color: var(--ink-2); }
|
||||
.g-schema tbody tr, .g-preview tbody tr { cursor: default; }
|
||||
.g-preview td.masked { color: var(--ink-4); letter-spacing: .1em; }
|
||||
.g-preview thead th { font-family: var(--mono); text-transform: none; }
|
||||
.g-dbfoot { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 14px; background: #fafbfd; border-top: 1px solid var(--line-soft); font-size: 11px; color: var(--ink-3); }
|
||||
.g-dbfoot em { font-style: normal; color: var(--ink-4); }
|
||||
|
||||
/* 记录管理器:分页控件 */
|
||||
.g-dbpage { display: inline-flex; align-items: center; gap: 4px; }
|
||||
.g-dbpage button { display: inline-grid; place-items: center; width: 26px; height: 26px; border: 1px solid var(--line-soft); border-radius: 8px; background: #fff; color: var(--ink-3); cursor: pointer; transition: all var(--t-fast); }
|
||||
.g-dbpage button:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
|
||||
.g-dbpage button:disabled { opacity: .35; cursor: default; }
|
||||
.g-dbpage b { min-width: 36px; text-align: center; font-size: 11px; color: var(--ink-2); }
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,61 @@
|
||||
/* ============================================================================
|
||||
JCAI 治理台 · 令牌与基座
|
||||
—— 只放设计令牌 + 页面基座;治理台的外壳与全部组件在 console.css。
|
||||
令牌与工作台同源(飞书蓝阶 + 中性阶 + 功能色 + 类型强调色),但**壳不同**:
|
||||
工作台是浅色三栏工作面,治理台是深色轨 + 深色顶栏的控制台。
|
||||
========================================================================== */
|
||||
|
||||
:root {
|
||||
/* —— 飞书蓝阶 —— */
|
||||
--brand: #3370ff;
|
||||
--brand-deep: #245bdb;
|
||||
--brand-pressed: #1c4cba;
|
||||
--brand-soft: #e1eaff;
|
||||
--brand-wash: #f0f4ff;
|
||||
/* —— AI 在场渐变 —— */
|
||||
--ai-a: #3370ff;
|
||||
--ai-b: #7f3bf5;
|
||||
--ai-grad: linear-gradient(135deg, var(--ai-a), var(--ai-b));
|
||||
/* —— 中性阶 —— */
|
||||
--ink: #1f2329;
|
||||
--ink-2: #41464e;
|
||||
--ink-3: #646a73;
|
||||
--ink-4: #8f959e;
|
||||
--line: #dee0e3;
|
||||
--line-soft: #e8eaed;
|
||||
--canvas: #f0f2f5;
|
||||
--soft: #f8f9fb;
|
||||
--cardline: #e1e6ec;
|
||||
--card-shadow: 0 1px 2px rgba(31, 35, 41, .05);
|
||||
/* —— 功能色 —— */
|
||||
--ok: #278717; --ok-accent: #34c724; --ok-soft: #e8f8e4;
|
||||
--warn: #de7802; --warn-accent: #ff8800; --warn-soft: #fff4e5;
|
||||
--danger: #d83931; --danger-accent: #f54a45; --danger-soft: #feeae9;
|
||||
/* —— 活动 / 节点类型强调色 —— */
|
||||
--st-auto: #4954e6; --st-execute: #e07a00; --st-decision: #7f3bf5;
|
||||
--st-collab: #0aa294; --st-wait: #6a7590;
|
||||
/* —— 字体 / 动效 —— */
|
||||
--sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
--mono: ui-monospace, "SF Mono", SFMono-Regular, "JetBrains Mono", Menlo, Consolas, monospace;
|
||||
--t-fast: .16s cubic-bezier(.3, 0, .2, 1);
|
||||
--t-med: .26s cubic-bezier(.3, 0, .2, 1);
|
||||
}
|
||||
|
||||
/* ============================== 基座 ============================== */
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-width: 1120px;
|
||||
background: #eceff4;
|
||||
color: var(--ink);
|
||||
font-family: var(--sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
:root { scrollbar-width: thin; scrollbar-color: #c9ced6 transparent; }
|
||||
::selection { background: var(--brand-soft); }
|
||||
:where(button, input, textarea, select, a, [tabindex]):focus-visible {
|
||||
outline: 2px solid var(--brand);
|
||||
outline-offset: 2px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/** feature: desk-task —— 对外类型(编码规范 §9.1) */
|
||||
export type { TaskCard, TaskDetail } from '@contracts';
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* feature: desk-task —— 唯一出口(编码规范 §9.1)。
|
||||
* 任务要素四区:任务列表 / 标题区 / 工作区(工单两模式)/ 对话区(卡族 + composer 锁)。
|
||||
*/
|
||||
export type { TaskCard, TaskDetail } from './contract.ts';
|
||||
export { TaskList } from './internal/TaskList.tsx';
|
||||
export { ViewRegion } from './internal/ViewRegion.tsx';
|
||||
export { TaskChat } from './internal/TaskChat.tsx';
|
||||
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 对话区 —— 自原型 `_components/TaskChat.tsx` 平移。
|
||||
*
|
||||
* 「对话区**只承载非结构化消息 + 决策选择题**;事件由系统 Agent(灰头像)发进来。」
|
||||
* 对话流是**一切回环输入的唯一入口**(OS 4.1);输入侧不参与控制权转交(AM 07)。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import type { ChatMessage } from '@contracts';
|
||||
import { AI_TAG_LABEL, EVENT_TYPE_LABEL } from '@contracts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import ScrollArea from '@web-shared/ScrollArea.tsx';
|
||||
|
||||
export type TaskChatProps = {
|
||||
readonly chat: readonly ChatMessage[];
|
||||
readonly locked: boolean;
|
||||
onSay(text: string): void;
|
||||
onDecide(cardId: string, value: string, note?: string): void;
|
||||
};
|
||||
|
||||
export function TaskChat({ chat, locked, onSay, onDecide }: TaskChatProps) {
|
||||
const [draft, setDraft] = useState('');
|
||||
|
||||
const submit = () => {
|
||||
const text = draft.trim();
|
||||
if (text === '' || locked) return;
|
||||
onSay(text);
|
||||
setDraft('');
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="session-chat">
|
||||
<header className="jc-chat-head">
|
||||
<span className="jc-chat-ico">
|
||||
<Ico name="messages-square" s={16} />
|
||||
</span>
|
||||
<div>
|
||||
<h2>任务助理</h2>
|
||||
<span>对话流 · 唯一回环输入口</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<ScrollArea className="jc-sa--fill" vpClassName="jc-chat-body">
|
||||
{chat.map((msg) => (
|
||||
<Bubble key={msg.id} msg={msg} onDecide={onDecide} />
|
||||
))}
|
||||
{chat.length === 0 && (
|
||||
<div className="jc-list-empty">
|
||||
<Ico name="messages-square" s={18} />
|
||||
还没有对话
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
{/* composer 锁:有卡在等作答时锁住自由输入,逼回卡上作答 */}
|
||||
<footer className={'jc-composer' + (locked ? ' locked' : '')}>
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={(e) => setDraft(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
}
|
||||
}}
|
||||
placeholder={locked ? '有卡在等你作答 —— 请先在卡上回答' : '说点什么…(Enter 发送)'}
|
||||
disabled={locked}
|
||||
rows={2}
|
||||
/>
|
||||
<button className="jc-btn primary" onClick={submit} disabled={locked || draft.trim() === ''}>
|
||||
发送
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/** 气泡二元制:AI / 我;系统事件走灰头像 */
|
||||
function Bubble({ msg, onDecide }: { msg: ChatMessage; onDecide: TaskChatProps['onDecide'] }) {
|
||||
if (msg.k === 'ai') {
|
||||
return (
|
||||
<div className="jc-bubble side-ai">
|
||||
<span className="jc-ava ai">AI</span>
|
||||
<div className="jc-bubble-body">
|
||||
{msg.tag !== undefined && <b className={'jc-tag tag-' + msg.tag}>{AI_TAG_LABEL[msg.tag]}</b>}
|
||||
<p>{msg.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.k === 'human') {
|
||||
return (
|
||||
<div className="jc-bubble side-me">
|
||||
<div className="jc-bubble-body">
|
||||
<p>{msg.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.k === 'system') {
|
||||
return (
|
||||
<div className="jc-bubble side-sys">
|
||||
<span className="jc-ava sys">
|
||||
<Ico name="bot" s={14} />
|
||||
</span>
|
||||
<div className="jc-bubble-body">
|
||||
<b className="jc-tag tag-event">{EVENT_TYPE_LABEL[msg.type]}事件</b>
|
||||
<p>{msg.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.k === 'decision') {
|
||||
// 对话区只做选择题 —— 决策卡
|
||||
return (
|
||||
<div className="jc-bubble side-ai">
|
||||
<span className="jc-ava ai">AI</span>
|
||||
<div className="jc-bubble-body jc-card-decide">
|
||||
<p className="jc-card-ask">{msg.ask}</p>
|
||||
<dl className="jc-basis">
|
||||
{msg.basis.map((b, i) => (
|
||||
<div key={i}>
|
||||
<dt>{b.k}</dt>
|
||||
<dd>{b.v}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<div className="jc-options">
|
||||
{msg.options.map((o) => (
|
||||
<button
|
||||
key={o.value}
|
||||
className={'jc-opt' + (o.recommended === true ? ' recommended' : '') + (msg.chosen?.value === o.value ? ' chosen' : '')}
|
||||
disabled={msg.chosen !== undefined}
|
||||
onClick={() => onDecide(msg.id, o.value)}
|
||||
>
|
||||
<b>{o.label}</b>
|
||||
{o.detail !== undefined && <span>{o.detail}</span>}
|
||||
{o.consequence !== undefined && <em>{o.consequence}</em>}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.k === 'elicitation') {
|
||||
return (
|
||||
<div className="jc-bubble side-ai">
|
||||
<span className="jc-ava ai">AI</span>
|
||||
<div className="jc-bubble-body jc-card-fill">
|
||||
<p className="jc-card-ask">{msg.ask}</p>
|
||||
<ul>
|
||||
{msg.fields.map((f) => (
|
||||
<li key={f.key}>{f.label}</li>
|
||||
))}
|
||||
</ul>
|
||||
{msg.done !== undefined && <p className="jc-card-done">已作答</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="jc-bubble side-ai">
|
||||
<span className="jc-ava ai">AI</span>
|
||||
<div className="jc-bubble-body jc-card-rate">
|
||||
<p className="jc-card-ask">{msg.ask}</p>
|
||||
{msg.done !== undefined && <p className="jc-card-done">已评价 {msg.done.score} 分</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 任务列表区 —— 自 `~/coding/JCAI_PRD/workbench/(proto)/page.tsx` 平移。
|
||||
*
|
||||
* **DOM 结构与类名原样保留**(`jc-*` 前缀、vendor 皮肤一并搬);
|
||||
* 只把 mock 数据换成服务端投影,渲染输出保持 1:1(README 平移规则 1/2/4)。
|
||||
*
|
||||
* 「清单里一条 = 一个任务 = 一个活动的一次运行,不是一整条工作流。」
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import type { TaskCard } from '@contracts';
|
||||
import { TERMINAL_STATE_LABEL } from '@contracts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import ScrollArea from '@web-shared/ScrollArea.tsx';
|
||||
|
||||
const TASK_COLORS = ['#3370ff', '#ff8800', '#7f3bf5', '#0aa294', '#f14ba9'];
|
||||
|
||||
type Status = 'active' | 'done';
|
||||
|
||||
export type TaskListProps = {
|
||||
readonly tasks: readonly TaskCard[];
|
||||
readonly selectedId: string;
|
||||
readonly collapsed: boolean;
|
||||
onPick(id: string): void;
|
||||
onCollapse(): void;
|
||||
};
|
||||
|
||||
const statusOf = (task: TaskCard): Status => (task.terminal === undefined ? 'active' : 'done');
|
||||
|
||||
export function TaskList({ tasks, selectedId, collapsed, onPick, onCollapse }: TaskListProps) {
|
||||
const [filter, setFilter] = useState<Status>('active');
|
||||
const [query, setQuery] = useState('');
|
||||
|
||||
if (collapsed) return null;
|
||||
|
||||
const count = (s: Status) => tasks.filter((t) => statusOf(t) === s).length;
|
||||
const q = query.trim().toLowerCase();
|
||||
const visible = tasks.filter(
|
||||
(t) =>
|
||||
statusOf(t) === filter &&
|
||||
(q === '' || `${t.title} ${t.activity} ${t.workflowSubject}`.toLowerCase().includes(q)),
|
||||
);
|
||||
|
||||
return (
|
||||
<aside className="task-sidebar">
|
||||
<div className="sidebar-head jc-wl-head">
|
||||
<span className="jc-wl-ico">
|
||||
<Ico name="list-checks" s={16} />
|
||||
</span>
|
||||
<div>
|
||||
<h2>任务列表</h2>
|
||||
<span>我负责的任务 · 共 {tasks.length} 项</span>
|
||||
</div>
|
||||
<button type="button" className="icon-button jc-wl-collapse" title="收起任务栏" onClick={onCollapse}>
|
||||
<Ico name="panel-left-close" s={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="jc-flt-wrap" aria-label="任务清单筛选">
|
||||
<div className="jc-flt" role="tablist" aria-label="按状态">
|
||||
<button className={filter === 'active' ? 'active' : ''} onClick={() => setFilter('active')}>
|
||||
<span>进行中</span>
|
||||
<strong>{count('active')}</strong>
|
||||
</button>
|
||||
<button className={filter === 'done' ? 'active' : ''} onClick={() => setFilter('done')}>
|
||||
<span>已完成</span>
|
||||
<strong>{count('done')}</strong>
|
||||
</button>
|
||||
</div>
|
||||
<div className="jc-search">
|
||||
<Ico name="search" s={14} />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
placeholder="搜索任务 / 活动 / 工作流…"
|
||||
aria-label="检索任务"
|
||||
/>
|
||||
{query !== '' && (
|
||||
<button type="button" className="jc-search-x" onClick={() => setQuery('')} aria-label="清空">
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ScrollArea className="jc-sa--fill" vpClassName="task-list">
|
||||
{visible.length === 0 && (
|
||||
<div className="jc-list-empty">
|
||||
<Ico name="search" s={18} />
|
||||
无匹配任务
|
||||
</div>
|
||||
)}
|
||||
{visible.map((task, i) => (
|
||||
<TaskCardView
|
||||
key={task.id}
|
||||
task={task}
|
||||
index={i}
|
||||
selected={task.id === selectedId}
|
||||
onPick={() => onPick(task.id)}
|
||||
/>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 任务卡:定高信息卡 —— 图标 + 活动名 + 状态章 / 主题两行位 / 编号 + 流内序位 / 进度条。
|
||||
* 每行都有硬约束(单行省略或两行位),信息丰富但永不超限。
|
||||
*/
|
||||
function TaskCardView({
|
||||
task,
|
||||
index,
|
||||
selected,
|
||||
onPick,
|
||||
}: {
|
||||
task: TaskCard;
|
||||
index: number;
|
||||
selected: boolean;
|
||||
onPick: () => void;
|
||||
}) {
|
||||
const done = task.terminal !== undefined;
|
||||
const pct = done ? 100 : Math.round((task.position / task.total) * 100);
|
||||
return (
|
||||
<button
|
||||
className={'task-card jc-tc jc-tc3' + (selected ? ' selected' : '')}
|
||||
style={{ '--task-color': TASK_COLORS[index % TASK_COLORS.length] } as React.CSSProperties}
|
||||
onClick={onPick}
|
||||
>
|
||||
<span className="jc-tc3-head">
|
||||
<span className="jc-tc-ico">
|
||||
<Ico name="list-checks" s={14} />
|
||||
</span>
|
||||
<h3 className="jc-tc-title">{task.activity}</h3>
|
||||
{done ? (
|
||||
<span className="jc-tc-state st-done">
|
||||
<i />
|
||||
{TERMINAL_STATE_LABEL[task.terminal!]}
|
||||
</span>
|
||||
) : (
|
||||
<span className="jc-tc-state i-todo">
|
||||
<i />
|
||||
进行中
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="jc-tc2-sub jc-clamp2">{task.workflowSubject}</span>
|
||||
<span className="jc-tc3-meta">
|
||||
<code className="jc-mono">{task.id.slice(0, 8)}</code>
|
||||
<b className="jc-mono" title="它在工作流上的序位">
|
||||
{task.position}/{task.total}
|
||||
</b>
|
||||
</span>
|
||||
<span className="jc-tc-bar">
|
||||
<span style={{ width: `${pct}%` }} />
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* 标题区 + 工作区 —— 自原型 `_components/ViewRegion.tsx` 平移(四页签结构)。
|
||||
*
|
||||
* 「视图区 = 全部结构化交互:HTML 交互界面(表单读写 + 图形展示)+ 工作事项列表 + 过程成果」;
|
||||
* 容器渲染活动推送的内容,**无脚本执行权**(AM 07);渲染一律经 @ui 的 ViewContainer。
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import type { TaskDetail, ViewBlock, WorkOrder } from '@contracts';
|
||||
import { ACTION_EFFECT_LABEL, WORK_ORDER_MODE_LABEL } from '@contracts';
|
||||
import { Ico } from '@web-shared/Ico.tsx';
|
||||
import ScrollArea from '@web-shared/ScrollArea.tsx';
|
||||
import { ViewBlocks } from '@ui';
|
||||
|
||||
type Tab = 'todo' | 'result' | 'detail' | 'log';
|
||||
|
||||
const TABS: ReadonlyArray<{ key: Tab; label: string; icon: string }> = [
|
||||
{ key: 'todo', label: '待处理', icon: 'list-checks' },
|
||||
{ key: 'result', label: '工作成果', icon: 'file-check' },
|
||||
{ key: 'detail', label: '任务详情', icon: 'bar-chart' },
|
||||
{ key: 'log', label: '工作日志', icon: 'notebook-pen' },
|
||||
];
|
||||
|
||||
export type ViewRegionProps = {
|
||||
readonly detail: TaskDetail;
|
||||
onReply(reply: Record<string, unknown>): void;
|
||||
};
|
||||
|
||||
export function ViewRegion({ detail, onReply }: ViewRegionProps) {
|
||||
const [tab, setTab] = useState<Tab>('todo');
|
||||
const open = detail.workOrders.filter((o) => o.done === undefined);
|
||||
|
||||
return (
|
||||
<main className="execution-panel jc-panel-card">
|
||||
<header className="jc-vr-head">
|
||||
<div className="jc-vr-title">
|
||||
<h1>{detail.card.title}</h1>
|
||||
<span className="jc-vr-sub">{detail.card.workflowSubject}</span>
|
||||
</div>
|
||||
<div className="jc-vr-budget jc-mono" title="预算与熔断由基座在环外强制">
|
||||
<span>步 {detail.budget.steps}</span>
|
||||
<span>token {detail.budget.tokens}</span>
|
||||
<span>时长 {detail.budget.activeMinutes}</span>
|
||||
<span>调人 {detail.budget.humanCalls}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav className="jc-vr-tabs" role="tablist">
|
||||
{TABS.map((t) => (
|
||||
<button key={t.key} className={tab === t.key ? 'active' : ''} onClick={() => setTab(t.key)} role="tab">
|
||||
<Ico name={t.icon} s={14} />
|
||||
<span>{t.label}</span>
|
||||
{t.key === 'todo' && open.length > 0 ? <b className="jc-vr-tab-badge">{open.length}</b> : null}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<ScrollArea className="jc-sa--fill" vpClassName="jc-vr-body">
|
||||
{tab === 'todo' &&
|
||||
(open.length === 0 ? (
|
||||
<div className="jc-list-empty">
|
||||
<Ico name="list-checks" s={18} />
|
||||
当前没有等你的工作事项
|
||||
</div>
|
||||
) : (
|
||||
open.map((order) => <WorkOrderCard key={order.id} order={order} onReply={onReply} />)
|
||||
))}
|
||||
|
||||
{tab === 'result' && <ViewBlocks blocks={detail.view as ViewBlock[]} />}
|
||||
|
||||
{tab === 'detail' && (
|
||||
<div className="jc-vr-detail">
|
||||
<ViewBlocks blocks={detail.view as ViewBlock[]} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'log' && (
|
||||
<ol className="jc-log">
|
||||
{detail.log.map((line) => (
|
||||
<li key={line.id} className={'jc-log-item side-' + line.side}>
|
||||
<span className="jc-log-side">{line.sideLabel}</span>
|
||||
<span className="jc-log-title">{line.title}</span>
|
||||
<time className="jc-mono">{line.at.slice(11, 19)}</time>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
/** 工单两模式:执行 = 指导书 + 空表单;验收 = 可修订报告 + 判据核对 + 放行三选一 */
|
||||
function WorkOrderCard({ order, onReply }: { order: WorkOrder; onReply(reply: Record<string, unknown>): void }) {
|
||||
const [values, setValues] = useState<Record<string, string>>(() =>
|
||||
Object.fromEntries(
|
||||
(order.mode === 'execution' ? order.fields : order.fields).map((f) => [f.key, f.ai ?? '']),
|
||||
),
|
||||
);
|
||||
const [comment, setComment] = useState('');
|
||||
|
||||
const set = (key: string, value: string) => setValues((v) => ({ ...v, [key]: value }));
|
||||
|
||||
return (
|
||||
<article className="jc-wo jc-panel-card">
|
||||
<header className="jc-wo-head">
|
||||
<span className={'jc-wo-mode mode-' + order.mode}>{WORK_ORDER_MODE_LABEL[order.mode]}</span>
|
||||
<h3>{order.title}</h3>
|
||||
</header>
|
||||
|
||||
{/* 调用必附上下文 —— 依据快照 */}
|
||||
<dl className="jc-wo-basis">
|
||||
{order.basis.map((b, i) => (
|
||||
<div key={i}>
|
||||
<dt>{b.k}</dt>
|
||||
<dd>{b.v}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
|
||||
{order.mode === 'execution' ? (
|
||||
<>
|
||||
<p className="jc-wo-ask">{order.ask}</p>
|
||||
{order.standard !== undefined && <p className="jc-wo-standard">完成标准:{order.standard}</p>}
|
||||
{order.steps !== undefined && (
|
||||
<ol className="jc-wo-steps">
|
||||
{order.steps.map((s, i) => (
|
||||
<li key={i}>{s}</li>
|
||||
))}
|
||||
</ol>
|
||||
)}
|
||||
<div className="jc-wo-form">
|
||||
{order.fields.map((f) => (
|
||||
<label key={f.key} className={'jc-field' + (f.cols === 2 ? ' half' : '')}>
|
||||
<span>
|
||||
{f.label}
|
||||
{f.required === true ? <i className="jc-req">*</i> : null}
|
||||
</span>
|
||||
{f.kind === 'textarea' ? (
|
||||
<textarea value={values[f.key] ?? ''} onChange={(e) => set(f.key, e.target.value)} rows={3} />
|
||||
) : f.kind === 'radio' || f.kind === 'select' ? (
|
||||
<select value={values[f.key] ?? ''} onChange={(e) => set(f.key, e.target.value)}>
|
||||
<option value="">请选择</option>
|
||||
{(f.options ?? []).map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input value={values[f.key] ?? ''} onChange={(e) => set(f.key, e.target.value)} placeholder={f.placeholder} />
|
||||
)}
|
||||
{f.hint !== undefined && <em className="jc-hint">{f.hint}</em>}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<footer className="jc-wo-foot">
|
||||
<button
|
||||
className="jc-btn primary"
|
||||
onClick={() => onReply({ mode: 'execution', workOrderId: order.id, values })}
|
||||
>
|
||||
{order.submitLabel ?? '提交'}
|
||||
</button>
|
||||
</footer>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* 判据逐条核对 —— 成果清单的成色标记 */}
|
||||
<ul className="jc-criteria">
|
||||
{order.criteria.map((c, i) => (
|
||||
<li key={i} className={c.pass ? 'pass' : 'fail'}>
|
||||
<b className="jc-lv">{c.level}</b>
|
||||
<span>{c.label}</span>
|
||||
<i>{c.pass ? '✓' : '○'}</i>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{order.actions !== undefined && order.actions.length > 0 && (
|
||||
<ul className="jc-actions" title="预检全过才发卡:这里是已验证的动作清单">
|
||||
{order.actions.map((a, i) => (
|
||||
<li key={i}>
|
||||
<b className={'jc-eff eff-' + a.effect}>{ACTION_EFFECT_LABEL[a.effect]}</b>
|
||||
<span>{a.label}</span>
|
||||
<em>{a.detail}</em>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<label className="jc-field">
|
||||
<span>意见(驳回必填)</span>
|
||||
<textarea value={comment} onChange={(e) => setComment(e.target.value)} rows={2} />
|
||||
</label>
|
||||
<footer className="jc-wo-foot">
|
||||
{/* 三选一:放行通过 / 驳回重做 / 驳回迭代 —— 完成只出自放行通过 */}
|
||||
<button
|
||||
className="jc-btn primary"
|
||||
onClick={() =>
|
||||
onReply({ mode: 'acceptance', workOrderId: order.id, verdict: 'pass', values, selectedPorts: {} })
|
||||
}
|
||||
>
|
||||
{order.passLabel ?? '放行通过'}
|
||||
</button>
|
||||
<button
|
||||
className="jc-btn"
|
||||
onClick={() =>
|
||||
onReply({ mode: 'acceptance', workOrderId: order.id, verdict: 'revise', values, comment, selectedPorts: {} })
|
||||
}
|
||||
>
|
||||
驳回迭代
|
||||
</button>
|
||||
<button
|
||||
className="jc-btn danger"
|
||||
onClick={() =>
|
||||
onReply({ mode: 'acceptance', workOrderId: order.id, verdict: 'redo', values, comment, selectedPorts: {} })
|
||||
}
|
||||
>
|
||||
驳回重做
|
||||
</button>
|
||||
</footer>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// 共享内联 lucide 图标(会话面板 / 工作区驾驶舱 / 设置等共用)。
|
||||
export function Ico({ name, s = 18 }: { name: string; s?: number }) {
|
||||
const p: Record<string, React.ReactNode> = {
|
||||
"chevron-right": (<path d="m9 18 6-6-6-6" />),
|
||||
"chevron-down": (<path d="m6 9 6 6 6-6" />),
|
||||
check: (<path d="M20 6 9 17l-5-5" />),
|
||||
"circle-check": (<><circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" /></>),
|
||||
star: (<><path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z" /></>),
|
||||
sparkles: (<><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.582a.5.5 0 0 1 0 .962L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z" /><path d="M20 3v4" /><path d="M22 5h-4" /><path d="M4 17v2" /><path d="M5 18H3" /></>),
|
||||
terminal: (<><polyline points="4 17 10 11 4 5" /><line x1="12" x2="20" y1="19" y2="19" /></>),
|
||||
clock: (<><circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /></>),
|
||||
info: (<><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></>),
|
||||
"clipboard-check": (<><rect width="8" height="4" x="8" y="2" rx="1" ry="1" /><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" /><path d="m9 14 2 2 4-4" /></>),
|
||||
target: (<><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="6" /><circle cx="12" cy="12" r="2" /></>),
|
||||
"list-checks": (<><path d="m3 17 2 2 4-4" /><path d="m3 7 2 2 4-4" /><path d="M13 6h8" /><path d="M13 12h8" /><path d="M13 18h8" /></>),
|
||||
plus: (<><path d="M5 12h14" /><path d="M12 5v14" /></>),
|
||||
"panel-left-close": (<><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M9 3v18" /><path d="m16 15-3-3 3-3" /></>),
|
||||
"sliders-horizontal": (<><line x1="21" x2="14" y1="4" y2="4" /><line x1="10" x2="3" y1="4" y2="4" /><line x1="21" x2="12" y1="12" y2="12" /><line x1="8" x2="3" y1="12" y2="12" /><line x1="21" x2="16" y1="20" y2="20" /><line x1="12" x2="3" y1="20" y2="20" /><line x1="14" x2="14" y1="2" y2="6" /><line x1="8" x2="8" y1="10" y2="14" /><line x1="16" x2="16" y1="18" y2="22" /></>),
|
||||
filter: (<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />),
|
||||
"arrow-left": (<><path d="m12 19-7-7 7-7" /><path d="M19 12H5" /></>),
|
||||
"file-spreadsheet": (<><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /><path d="M8 13h2" /><path d="M14 13h2" /><path d="M8 17h2" /><path d="M14 17h2" /></>),
|
||||
"file-type": (<><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /><path d="M9 13v-1h6v1" /><path d="M11 18h2" /><path d="M12 12v6" /></>),
|
||||
wrench: (<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z" />),
|
||||
"git-branch": (<><line x1="6" x2="6" y1="3" y2="15" /><circle cx="18" cy="6" r="3" /><circle cx="6" cy="18" r="3" /><path d="M18 9a9 9 0 0 1-9 9" /></>),
|
||||
search: (<><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></>),
|
||||
bell: (<><path d="M10.268 21a2 2 0 0 0 3.464 0" /><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326" /></>),
|
||||
settings: (<><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></>),
|
||||
user: (<><circle cx="12" cy="12" r="10" /><circle cx="12" cy="10" r="3" /><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662" /></>),
|
||||
"user-round": (<><circle cx="12" cy="8" r="5" /><path d="M20 21a8 8 0 0 0-16 0" /></>),
|
||||
bot: (<><path d="M12 8V4H8" /><rect width="16" height="12" x="4" y="8" rx="2" /><path d="M2 14h2" /><path d="M20 14h2" /><path d="M15 13v2" /><path d="M9 13v2" /></>),
|
||||
dashboard: (<><rect width="7" height="9" x="3" y="3" rx="1" /><rect width="7" height="5" x="14" y="3" rx="1" /><rect width="7" height="9" x="14" y="12" rx="1" /><rect width="7" height="5" x="3" y="16" rx="1" /></>),
|
||||
mail: (<><rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /></>),
|
||||
"mail-open": (<><path d="M21.2 8.4c.5.38.8.97.8 1.6v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V10a2 2 0 0 1 .8-1.6l8-6a2 2 0 0 1 2.4 0z" /><path d="m22 10-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 10" /></>),
|
||||
inbox: (<><polyline points="22 12 16 12 14 15 10 15 8 12 2 12" /><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /></>),
|
||||
"shield-alert": (<><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /><path d="M12 8v4" /><path d="M12 16h.01" /></>),
|
||||
send: (<><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" /><path d="m21.854 2.147-10.94 10.939" /></>),
|
||||
archive: (<><rect width="20" height="5" x="2" y="3" rx="1" /><path d="M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8" /><path d="M10 12h4" /></>),
|
||||
"file-text": (<><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /><path d="M10 9H8" /><path d="M16 13H8" /><path d="M16 17H8" /></>),
|
||||
"pen-line": (<><path d="M12 20h9" /><path d="M16.376 3.622a1 1 0 0 1 3.002 3.002L7.368 18.635a2 2 0 0 1-.855.506l-2.872.838a.5.5 0 0 1-.62-.62l.838-2.872a2 2 0 0 1 .506-.854z" /></>),
|
||||
"messages-square": (<><path d="M14 9a2 2 0 0 1-2 2H6l-4 4V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2z" /><path d="M18 9h2a2 2 0 0 1 2 2v11l-4-4h-6a2 2 0 0 1-2-2v-1" /></>),
|
||||
"notebook-pen": (<><path d="M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4" /><path d="M2 6h4" /><path d="M2 10h4" /><path d="M2 14h4" /><path d="M2 18h4" /><path d="M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z" /></>),
|
||||
"calendar-days": (<><path d="M8 2v4" /><path d="M16 2v4" /><rect width="18" height="18" x="3" y="4" rx="2" /><path d="M3 10h18" /><path d="M8 14h.01" /><path d="M12 14h.01" /><path d="M16 14h.01" /><path d="M8 18h.01" /><path d="M12 18h.01" /><path d="M16 18h.01" /></>),
|
||||
hourglass: (<><path d="M5 22h14" /><path d="M5 2h14" /><path d="M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22" /><path d="M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2" /></>),
|
||||
play: (<polygon points="6 3 20 12 6 21 6 3" />),
|
||||
paperclip: (<path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" />),
|
||||
"building-2": (<><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z" /><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2" /><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2" /><path d="M10 6h4" /><path d="M10 10h4" /><path d="M10 14h4" /><path d="M10 18h4" /></>),
|
||||
"panel-left-open": (<><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M9 3v18" /><path d="m14 9 3 3-3 3" /></>),
|
||||
"trash-2": (<><path d="M3 6h18" /><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /><line x1="10" x2="10" y1="11" y2="17" /><line x1="14" x2="14" y1="11" y2="17" /></>),
|
||||
calculator: (<><rect width="16" height="20" x="4" y="2" rx="2" /><line x1="8" x2="16" y1="6" y2="6" /><line x1="16" x2="16" y1="14" y2="18" /><path d="M16 10h.01" /><path d="M12 10h.01" /><path d="M8 10h.01" /><path d="M12 14h.01" /><path d="M8 14h.01" /><path d="M12 18h.01" /><path d="M8 18h.01" /></>),
|
||||
/* —— 治理中心扩充图标 —— */
|
||||
gauge: (<><path d="m12 14 4-4" /><path d="M3.34 19a10 10 0 1 1 17.32 0" /></>),
|
||||
database: (<><ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M3 5v14a9 3 0 0 0 18 0V5" /><path d="M3 12a9 3 0 0 0 18 0" /></>),
|
||||
layers: (<><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z" /><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65" /><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65" /></>),
|
||||
users: (<><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><path d="M22 21v-2a4 4 0 0 0-3-3.87" /><path d="M16 3.13a4 4 0 0 1 0 7.75" /></>),
|
||||
"share-2": (<><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><line x1="8.59" x2="15.42" y1="13.51" y2="17.49" /><line x1="15.41" x2="8.59" y1="6.51" y2="10.49" /></>),
|
||||
cpu: (<><rect width="16" height="16" x="4" y="4" rx="2" /><rect width="6" height="6" x="9" y="9" rx="1" /><path d="M15 2v2" /><path d="M15 20v2" /><path d="M2 15h2" /><path d="M2 9h2" /><path d="M20 15h2" /><path d="M20 9h2" /><path d="M9 2v2" /><path d="M9 20v2" /></>),
|
||||
zap: (<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z" />),
|
||||
"git-merge": (<><circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" /><path d="M6 21V9a9 9 0 0 0 9 9" /></>),
|
||||
"refresh-cw": (<><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" /><path d="M21 3v5h-5" /><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" /><path d="M8 16H3v5" /></>),
|
||||
"rotate-ccw": (<><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" /><path d="M3 3v5h5" /></>),
|
||||
"alert-triangle": (<><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" /><path d="M12 9v4" /><path d="M12 17h.01" /></>),
|
||||
"shield-check": (<><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z" /><path d="m9 12 2 2 4-4" /></>),
|
||||
scan: (<><path d="M3 7V5a2 2 0 0 1 2-2h2" /><path d="M17 3h2a2 2 0 0 1 2 2v2" /><path d="M21 17v2a2 2 0 0 1-2 2h-2" /><path d="M7 21H5a2 2 0 0 1-2-2v-2" /><path d="M7 12h10" /></>),
|
||||
"file-check": (<><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /><path d="m9 15 2 2 4-4" /></>),
|
||||
"trending-up": (<><path d="M16 7h6v6" /><path d="m22 7-8.5 8.5-5-5L2 17" /></>),
|
||||
ban: (<><circle cx="12" cy="12" r="10" /><path d="m4.9 4.9 14.2 14.2" /></>),
|
||||
key: (<><path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4" /><path d="m21 2-9.6 9.6" /><circle cx="7.5" cy="15.5" r="5.5" /></>),
|
||||
workflow: (<><rect width="8" height="8" x="3" y="3" rx="2" /><path d="M7 11v4a2 2 0 0 0 2 2h4" /><rect width="8" height="8" x="13" y="13" rx="2" /></>),
|
||||
"arrow-right": (<><path d="M5 12h14" /><path d="m12 5 7 7-7 7" /></>),
|
||||
"circle-dot": (<><circle cx="12" cy="12" r="10" /><circle cx="12" cy="12" r="3" /></>),
|
||||
/* —— 治理中心补充图标(运行 / 连接 / 岗位 / 事件 / 度量)—— */
|
||||
activity: (<path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2" />),
|
||||
route: (<><circle cx="6" cy="19" r="3" /><path d="M9 19h8.5a3.5 3.5 0 0 0 0-7h-11a3.5 3.5 0 0 1 0-7H15" /><circle cx="18" cy="5" r="3" /></>),
|
||||
plug: (<><path d="M12 22v-5" /><path d="M9 8V2" /><path d="M15 8V2" /><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" /></>),
|
||||
radio: (<><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9" /><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5" /><circle cx="12" cy="12" r="2" /><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5" /><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19" /></>),
|
||||
"git-commit": (<><circle cx="12" cy="12" r="3" /><line x1="3" x2="9" y1="12" y2="12" /><line x1="15" x2="21" y1="12" y2="12" /></>),
|
||||
history: (<><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" /><path d="M3 3v5h5" /><path d="M12 7v5l4 2" /></>),
|
||||
"user-check": (<><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><polyline points="16 11 18 13 22 9" /></>),
|
||||
"user-plus": (<><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /><line x1="19" x2="19" y1="8" y2="14" /><line x1="22" x2="16" y1="11" y2="11" /></>),
|
||||
power: (<><path d="M12 2v10" /><path d="M18.4 6.6a9 9 0 1 1-12.77.04" /></>),
|
||||
"alert-circle": (<><circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" /></>),
|
||||
"bar-chart": (<><line x1="12" x2="12" y1="20" y2="10" /><line x1="18" x2="18" y1="20" y2="4" /><line x1="6" x2="6" y1="20" y2="16" /></>),
|
||||
shuffle: (<><path d="M2 18h1.4c1.3 0 2.5-.6 3.3-1.7l6.1-8.6c.7-1.1 2-1.7 3.3-1.7H22" /><path d="m18 2 4 4-4 4" /><path d="M2 6h1.9c1.5 0 2.9.9 3.6 2.2" /><path d="M22 18h-5.9c-1.3 0-2.6-.7-3.3-1.8l-.5-.8" /><path d="m18 14 4 4-4 4" /></>),
|
||||
pin: (<><path d="M12 17v5" /><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z" /></>),
|
||||
timer: (<><line x1="10" x2="14" y1="2" y2="2" /><line x1="12" x2="15" y1="14" y2="11" /><circle cx="12" cy="14" r="8" /></>),
|
||||
repeat: (<><path d="m17 2 4 4-4 4" /><path d="M3 11v-1a4 4 0 0 1 4-4h14" /><path d="m7 22-4-4 4-4" /><path d="M21 13v1a4 4 0 0 1-4 4H3" /></>),
|
||||
briefcase: (<><path d="M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16" /><rect width="20" height="14" x="2" y="6" rx="2" /></>),
|
||||
fingerprint: (<><path d="M12 10a2 2 0 0 0-2 2c0 1.02-.1 2.51-.26 4" /><path d="M14 13.12c0 2.38 0 6.38-1 8.88" /><path d="M17.29 21.02c.12-.6.43-2.3.5-3.02" /><path d="M2 12a10 10 0 0 1 18-6" /><path d="M2 16h.01" /><path d="M21.8 16c.2-2 .131-5.354 0-6" /><path d="M5 19.5C5.5 18 6 15 6 12a6 6 0 0 1 .34-2" /><path d="M8.65 22c.21-.66.45-1.32.57-2" /><path d="M9 6.8a6 6 0 0 1 9 5.2v2" /></>),
|
||||
server: (<><rect width="20" height="8" x="2" y="2" rx="2" /><rect width="20" height="8" x="2" y="14" rx="2" /><line x1="6" x2="6.01" y1="6" y2="6" /><line x1="6" x2="6.01" y1="18" y2="18" /></>),
|
||||
network: (<><rect x="16" y="16" width="6" height="6" rx="1" /><rect x="2" y="16" width="6" height="6" rx="1" /><rect x="9" y="2" width="6" height="6" rx="1" /><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3" /><path d="M12 12V8" /></>),
|
||||
eye: (<><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" /><circle cx="12" cy="12" r="3" /></>),
|
||||
flag: (<><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z" /><line x1="4" x2="4" y1="22" y2="15" /></>),
|
||||
lock: (<><rect width="18" height="11" x="3" y="11" rx="2" ry="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" /></>),
|
||||
percent: (<><line x1="19" x2="5" y1="5" y2="19" /><circle cx="6.5" cy="6.5" r="2.5" /><circle cx="17.5" cy="17.5" r="2.5" /></>),
|
||||
hash: (<><line x1="4" x2="20" y1="9" y2="9" /><line x1="4" x2="20" y1="15" y2="15" /><line x1="10" x2="8" y1="3" y2="21" /><line x1="16" x2="14" y1="3" y2="21" /></>),
|
||||
"external-link": (<><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></>),
|
||||
upload: (<><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="17 8 12 3 7 8" /><line x1="12" x2="12" y1="3" y2="15" /></>),
|
||||
puzzle: (<path d="M15.39 4.39a1 1 0 0 0 1.68-.474 2.5 2.5 0 1 1 3.014 3.015 1 1 0 0 0-.474 1.68l1.683 1.682a2.414 2.414 0 0 1 0 3.414L19.61 15.39a1 1 0 0 1-1.68-.474 2.5 2.5 0 1 0-3.014 3.015 1 1 0 0 1 .474 1.68l-1.683 1.682a2.414 2.414 0 0 1-3.414 0L8.61 19.61a1 1 0 0 0-1.68.474 2.5 2.5 0 1 1-3.014-3.015 1 1 0 0 0 .474-1.68l-1.683-1.682a2.414 2.414 0 0 1 0-3.414L4.39 8.61a1 1 0 0 1 1.68.474 2.5 2.5 0 1 0 3.014-3.015 1 1 0 0 1-.474-1.68l1.683-1.682a2.414 2.414 0 0 1 3.414 0z" />),
|
||||
"circle-x": (<><circle cx="12" cy="12" r="10" /><path d="m15 9-6 6" /><path d="m9 9 6 6" /></>),
|
||||
"chevron-left": (<path d="m15 18-6-6 6-6" />),
|
||||
"calendar-clock": (<><path d="M21 7.5V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3.5" /><path d="M16 2v4" /><path d="M8 2v4" /><path d="M3 10h5" /><circle cx="16" cy="16" r="6" /><path d="M16 14v2l1 1" /></>),
|
||||
"file-search": (<><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h5" /><path d="M14 2v4a2 2 0 0 0 2 2h4" /><circle cx="16" cy="17" r="3" /><path d="m21 22-1.5-1.5" /></>),
|
||||
boxes: (<><path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" /><path d="m7 16.5-4.74-2.85" /><path d="m7 16.5 5-3" /><path d="M7 16.5v5.17" /><path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" /><path d="m17 16.5-5-3" /><path d="m17 16.5 4.74-2.85" /><path d="M17 16.5v5.17" /><path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" /><path d="M12 8 7.26 5.15" /><path d="m12 8 4.74-2.85" /><path d="M12 13.5V8" /></>),
|
||||
"square-pen": (<><path d="M12 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /><path d="M18.375 2.625a1 1 0 0 1 3 3l-9.013 9.014a2 2 0 0 1-.853.505l-2.873.84a.5.5 0 0 1-.62-.62l.84-2.873a2 2 0 0 1 .506-.852z" /></>),
|
||||
"log-out": (<><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" /><polyline points="16 17 21 12 16 7" /><line x1="21" x2="9" y1="12" y2="12" /></>),
|
||||
};
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={s} height={s} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
{p[name]}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// 自绘滚动条容器:原生滚动条彻底隐藏(零占位——出现/消失永不挤版面,也绕开 Blink
|
||||
// 对滚动条伪元素动态切换不重绘的 quirk),自绘 thumb 悬浮在内衬/列间隔上。
|
||||
// 行为契约:鼠标移入滚动区立即现身;移出 / 停止滚动 800ms 后淡出;thumb 可拖拽。
|
||||
// 用法:外层 .jc-sa 接管原容器的布局角色(className 传 jc-sa--fill / jc-sa--full),
|
||||
// 原滚动容器的类名整体挪到 vpClassName(视口 = 真正滚动的元素,样式不变);
|
||||
// inset = thumb 距右缘 px(负值 = 伸出容器、悬进列间 gutter)。
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
const HIDE_MS = 800;
|
||||
|
||||
export default function ScrollArea({ className = "", vpClassName = "", inset = 3, scrollRef, children }: {
|
||||
className?: string;
|
||||
vpClassName?: string;
|
||||
inset?: number;
|
||||
scrollRef?: React.Ref<HTMLElement | null>; // 上层需要直接操作滚动位置时(如会话流自动滚底)
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const vpRef = useRef<HTMLDivElement | null>(null);
|
||||
const thRef = useRef<HTMLDivElement | null>(null);
|
||||
const hideT = useRef(0);
|
||||
const st = useRef({ inside: false, dragging: false, startY: 0, startTop: 0 });
|
||||
|
||||
// 按滚动几何同步 thumb 高度与位置;不可滚时整体隐藏
|
||||
const sync = useCallback(() => {
|
||||
const vp = vpRef.current, th = thRef.current;
|
||||
if (!vp || !th) return;
|
||||
const { scrollTop, scrollHeight, clientHeight } = vp;
|
||||
if (scrollHeight <= clientHeight + 1) { th.style.display = "none"; return; }
|
||||
th.style.display = "";
|
||||
const h = Math.max(28, Math.round((clientHeight / scrollHeight) * clientHeight));
|
||||
const y = Math.round((scrollTop / (scrollHeight - clientHeight)) * (clientHeight - h));
|
||||
th.style.height = `${h}px`;
|
||||
th.style.transform = `translateY(${y}px)`;
|
||||
}, []);
|
||||
|
||||
// 移出/停止滚动后延时淡出(悬停内 / 拖拽中不隐藏)
|
||||
const hideSoon = useCallback(() => {
|
||||
window.clearTimeout(hideT.current);
|
||||
hideT.current = window.setTimeout(() => {
|
||||
if (!st.current.inside && !st.current.dragging) thRef.current?.classList.remove("on");
|
||||
}, HIDE_MS);
|
||||
}, []);
|
||||
|
||||
const show = useCallback(() => {
|
||||
sync();
|
||||
thRef.current?.classList.add("on");
|
||||
hideSoon();
|
||||
}, [sync, hideSoon]);
|
||||
|
||||
useEffect(() => {
|
||||
const vp = vpRef.current;
|
||||
if (!vp) return;
|
||||
const ro = new ResizeObserver(sync); // 面板拖宽/视口变化时校准
|
||||
ro.observe(vp);
|
||||
sync();
|
||||
return () => { ro.disconnect(); window.clearTimeout(hideT.current); };
|
||||
}, [sync]);
|
||||
|
||||
// 标准 ref 转发(写入外部 ref.current 是 ref 的本职),compiler 规则误报
|
||||
const setVp = useCallback((el: HTMLDivElement | null) => {
|
||||
vpRef.current = el;
|
||||
if (typeof scrollRef === "function") scrollRef(el);
|
||||
else if (scrollRef) (scrollRef as React.MutableRefObject<HTMLElement | null>).current = el;
|
||||
}, [scrollRef]);
|
||||
|
||||
// thumb 拖拽滚动
|
||||
const onThumbDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const vp = vpRef.current;
|
||||
if (!vp) return;
|
||||
e.preventDefault();
|
||||
st.current = { ...st.current, dragging: true, startY: e.clientY, startTop: vp.scrollTop };
|
||||
thRef.current?.classList.add("on");
|
||||
const move = (ev: PointerEvent) => {
|
||||
const { scrollHeight, clientHeight } = vp;
|
||||
const h = thRef.current?.offsetHeight ?? 28;
|
||||
vp.scrollTop = st.current.startTop + (ev.clientY - st.current.startY) * ((scrollHeight - clientHeight) / Math.max(1, clientHeight - h));
|
||||
};
|
||||
const up = () => {
|
||||
st.current.dragging = false;
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", up);
|
||||
hideSoon();
|
||||
};
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", up);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={"jc-sa " + className}
|
||||
onPointerEnter={() => { st.current.inside = true; show(); }}
|
||||
onPointerLeave={() => { st.current.inside = false; hideSoon(); }}
|
||||
>
|
||||
<div ref={setVp} className={"jc-sa-vp " + vpClassName} onScroll={show}>
|
||||
{children}
|
||||
</div>
|
||||
<div ref={thRef} className="jc-sa-thumb" style={{ right: inset, display: "none" }} onPointerDown={onThumbDown} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 服务端交互的唯一通道(编码规范 §9.2):组件内**禁止裸 fetch**。
|
||||
* 路由契约来自 `@contracts/api`;**禁止 Hono RPC 客户端** —— 那要求前端 import 后端类型(§5.1)。
|
||||
*/
|
||||
import type { RouteContract } from '@contracts';
|
||||
|
||||
export class ApiCallError extends Error {
|
||||
constructor(
|
||||
readonly status: number,
|
||||
readonly code: string,
|
||||
message: string,
|
||||
readonly detail?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiCallError';
|
||||
}
|
||||
}
|
||||
|
||||
type CallOptions = {
|
||||
readonly params?: Readonly<Record<string, string>>;
|
||||
readonly body?: unknown;
|
||||
};
|
||||
|
||||
/** 路径参数按 `:name` 就地替换 */
|
||||
function resolvePath(path: string, params: Readonly<Record<string, string>> = {}): string {
|
||||
return path.replace(/:([A-Za-z_][\w]*)/g, (_, key: string) => encodeURIComponent(params[key] ?? ''));
|
||||
}
|
||||
|
||||
export async function call<T>(route: RouteContract, options: CallOptions = {}): Promise<T> {
|
||||
const response = await fetch(resolvePath(route.path, options.params), {
|
||||
method: route.method,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
|
||||
});
|
||||
|
||||
const payload: unknown = await response.json().catch(() => undefined);
|
||||
if (!response.ok) {
|
||||
const error = (payload ?? {}) as { code?: string; message?: string; detail?: unknown };
|
||||
throw new ApiCallError(
|
||||
response.status,
|
||||
error.code ?? 'internal',
|
||||
error.message ?? `${response.status} ${response.statusText}`,
|
||||
error.detail,
|
||||
);
|
||||
}
|
||||
return payload as T;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 对话区/视图区可调比例:workspace 上的分隔柄拖拽写入 --jc-chat-w(CSS 变量,
|
||||
// 各断点的对话区宽度均以它优先),localStorage 记忆,双击复位回默认配比。
|
||||
// 任务工作台与消息模块共用同一 storage key —— 两处对话区宽度保持一致。
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
const MIN_W = 340;
|
||||
// 上限随视口收敛:给 视图区(≥480) + 清单列 + 间距 留足空间,避免拖到中栏塌陷
|
||||
const maxW = () => Math.max(MIN_W, Math.min(720, window.innerWidth - 830));
|
||||
|
||||
export function useChatWidth(storageKey = "jc-chat-w") {
|
||||
const [w, setW] = useState<number | null>(null); // null = 未拖拽,走断点默认
|
||||
|
||||
useEffect(() => {
|
||||
const v = Number(localStorage.getItem(storageKey));
|
||||
if (v) setW(Math.min(maxW(), Math.max(MIN_W, v)));
|
||||
}, [storageKey]);
|
||||
|
||||
const onSplitDown = useCallback((e: React.PointerEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
const handle = e.currentTarget;
|
||||
const chatEl = handle.parentElement?.querySelector<HTMLElement>(".session-chat");
|
||||
const startX = e.clientX;
|
||||
const startW = chatEl?.getBoundingClientRect().width ?? 440;
|
||||
let cur = startW;
|
||||
handle.classList.add("dragging");
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
const move = (ev: PointerEvent) => {
|
||||
cur = Math.min(maxW(), Math.max(MIN_W, Math.round(startW + (startX - ev.clientX))));
|
||||
setW(cur);
|
||||
};
|
||||
const up = () => {
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", up);
|
||||
handle.classList.remove("dragging");
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
localStorage.setItem(storageKey, String(cur));
|
||||
};
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", up);
|
||||
}, [storageKey]);
|
||||
|
||||
const onSplitReset = useCallback(() => {
|
||||
setW(null);
|
||||
localStorage.removeItem(storageKey);
|
||||
}, [storageKey]);
|
||||
|
||||
const style = (w ? { "--jc-chat-w": `${w}px` } : undefined) as React.CSSProperties | undefined;
|
||||
return { style, onSplitDown, onSplitReset };
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export default defineConfig({
|
||||
'@utils': `${root}packages/utils/src/index.ts`,
|
||||
'@features': p('./src/features'),
|
||||
'@web-shared': p('./src/shared'),
|
||||
'@contracts/': `${root}packages/contracts/src/`,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
import { z } from 'zod';
|
||||
import type { RouteContract } from './http.ts';
|
||||
import { sessionSchema } from '../org.ts';
|
||||
import { chatMessageSchema, deliverableSchema, stageSchema, viewBlockSchema, workOrderSchema } from '../card.ts';
|
||||
import { attributionSideSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts';
|
||||
import { taskIdSchema } from '../id.ts';
|
||||
|
||||
export const sessionRoute = {
|
||||
method: 'GET',
|
||||
@@ -21,3 +24,97 @@ export const healthRoute = {
|
||||
db: z.enum(['up', 'down']),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── 任务要素:四区(任务列表 / 标题区 / 工作区 / 对话区) ─────────────── */
|
||||
|
||||
/** 任务卡 —— 清单里一条 = 一个活动的一次运行,不是一整条工作流 */
|
||||
export const taskCardSchema = z.object({
|
||||
id: taskIdSchema,
|
||||
activity: z.string(),
|
||||
title: z.string(),
|
||||
runtime: taskRuntimeStateSchema,
|
||||
terminal: terminalStateSchema.optional(),
|
||||
/** 它在工作流上的序位 */
|
||||
position: z.number().int().min(1),
|
||||
total: z.number().int().min(1),
|
||||
workflowSubject: z.string(),
|
||||
workflowState: workflowStateSchema,
|
||||
startedAt: z.string(),
|
||||
});
|
||||
export type TaskCard = z.infer<typeof taskCardSchema>;
|
||||
|
||||
export const listTasksRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/tasks',
|
||||
output: z.object({ tasks: z.array(taskCardSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 任务详情 —— 工作区与对话区的服务端投影(前端零过程状态,刷新即重建) */
|
||||
export const taskDetailSchema = z.object({
|
||||
card: taskCardSchema,
|
||||
goal: z.string(),
|
||||
budget: z.object({
|
||||
steps: z.string(),
|
||||
tokens: z.string(),
|
||||
activeMinutes: z.string(),
|
||||
humanCalls: z.string(),
|
||||
}),
|
||||
stages: z.array(stageSchema),
|
||||
deliverables: z.array(deliverableSchema),
|
||||
workOrders: z.array(workOrderSchema),
|
||||
chat: z.array(chatMessageSchema),
|
||||
view: z.array(viewBlockSchema),
|
||||
log: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
at: z.string(),
|
||||
side: attributionSideSchema,
|
||||
sideLabel: z.string(),
|
||||
title: z.string(),
|
||||
summary: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
export type TaskDetail = z.infer<typeof taskDetailSchema>;
|
||||
|
||||
export const taskDetailRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/tasks/:taskId',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
output: taskDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 回传工单 —— 与后端回传校验同 schema(§9.2) */
|
||||
export const replyWorkOrderRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/work-orders/reply',
|
||||
body: z.object({ reply: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({ ok: z.boolean(), producesCompletion: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 负责人的自由输入 —— 人事件的第二形态 */
|
||||
export const sayRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/tasks/:taskId/say',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
body: z.object({ text: z.string().min(1) }),
|
||||
output: chatMessageSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 消息要素 */
|
||||
export const listMessagesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/messages',
|
||||
output: z.object({
|
||||
messages: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
kind: z.string(),
|
||||
title: z.string(),
|
||||
body: z.string(),
|
||||
at: z.string(),
|
||||
read: z.boolean(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -3,5 +3,11 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": { ".": "./src/index.ts" }
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.17",
|
||||
"react": "^19.2.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 视图块渲染 —— 无业务语义的展示原语(编码规范 §2 packages/ui-kit)。
|
||||
* DOM 与类名自原型平移(`jc-*`),渲染输出保持 1:1。
|
||||
*/
|
||||
import type { ViewBlock } from '@contracts';
|
||||
|
||||
export function ViewBlocks({ blocks }: { readonly blocks: readonly ViewBlock[] }) {
|
||||
return (
|
||||
<>
|
||||
{blocks.map((block, i) => (
|
||||
<Block key={i} block={block} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Block({ block }: { block: ViewBlock }) {
|
||||
switch (block.t) {
|
||||
case 'callout':
|
||||
return <p className={'jc-callout tone-' + block.tone}>{block.text}</p>;
|
||||
|
||||
case 'kv':
|
||||
return (
|
||||
<section className="jc-kv">
|
||||
{block.title !== undefined && <h4>{block.title}</h4>}
|
||||
<dl>
|
||||
{block.items.map((item, i) => (
|
||||
<div key={i}>
|
||||
<dt>{item.k}</dt>
|
||||
<dd>{item.v}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
);
|
||||
|
||||
case 'badges':
|
||||
return (
|
||||
<section className="jc-badges">
|
||||
{block.title !== undefined && <h4>{block.title}</h4>}
|
||||
{block.items.map((item, i) => (
|
||||
<span key={i} className={item.on ? 'on' : ''}>
|
||||
{item.label}
|
||||
</span>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
|
||||
case 'table':
|
||||
return (
|
||||
<section className="jc-table-wrap">
|
||||
{block.title !== undefined && <h4>{block.title}</h4>}
|
||||
<table className="jc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
{block.cols.map((c, i) => (
|
||||
<th key={i}>{c}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{block.rows.map((row, i) => (
|
||||
<tr key={i}>
|
||||
{row.map((cell, j) => (
|
||||
<td key={j}>{cell}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
);
|
||||
|
||||
case 'bars':
|
||||
return (
|
||||
<section className="jc-bars">
|
||||
{block.title !== undefined && <h4>{block.title}</h4>}
|
||||
{block.items.map((item, i) => (
|
||||
<div key={i} className={'jc-bar tone-' + (item.tone ?? 'brand')}>
|
||||
<span className="jc-bar-label">{item.label}</span>
|
||||
<span className="jc-bar-track">
|
||||
<i style={{ width: `${Math.min((item.value / (block.max ?? 100)) * 100, 100)}%` }} />
|
||||
</span>
|
||||
<span className="jc-bar-value">{item.text ?? item.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
|
||||
case 'donut':
|
||||
return (
|
||||
<section className="jc-donut">
|
||||
{block.title !== undefined && <h4>{block.title}</h4>}
|
||||
<ul>
|
||||
{block.items.map((item, i) => (
|
||||
<li key={i} className={'tone-' + item.tone}>
|
||||
<b>{item.value}</b>
|
||||
<span>{item.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
|
||||
case 'cards':
|
||||
return (
|
||||
<section className="jc-cards">
|
||||
{block.title !== undefined && <h4>{block.title}</h4>}
|
||||
{block.items.map((item, i) => (
|
||||
<article key={i} className={'jc-mini-card tone-' + (item.tone ?? 'brand')}>
|
||||
<span className="jc-mini-tag">{item.tag}</span>
|
||||
<h5>{item.title}</h5>
|
||||
{item.swap !== undefined && (
|
||||
<p className="jc-swap">
|
||||
<code>{item.swap.from}</code> → <code>{item.swap.to}</code>
|
||||
</p>
|
||||
)}
|
||||
<b className="jc-mini-big">{item.big}</b>
|
||||
{item.bigLabel !== undefined && <em>{item.bigLabel}</em>}
|
||||
{item.note !== undefined && <p className="jc-mini-note">{item.note}</p>}
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
|
||||
case 'waiting':
|
||||
return (
|
||||
<p className="jc-waiting">
|
||||
{block.label}
|
||||
{block.hint !== undefined && <em>{block.hint}</em>}
|
||||
</p>
|
||||
);
|
||||
|
||||
case 'mail':
|
||||
return (
|
||||
<article className={'jc-mail dir-' + block.dir}>
|
||||
<header>
|
||||
<b>{block.subject}</b>
|
||||
<span>
|
||||
{block.from} → {block.to}
|
||||
</span>
|
||||
<time>{block.time}</time>
|
||||
</header>
|
||||
<pre className="jc-mail-body">{block.body}</pre>
|
||||
</article>
|
||||
);
|
||||
|
||||
case 'files':
|
||||
return (
|
||||
<section className="jc-files">
|
||||
{block.title !== undefined && <h4>{block.title}</h4>}
|
||||
{block.items.map((item, i) => (
|
||||
<div key={i} className="jc-file">
|
||||
<b>{item.label}</b>
|
||||
<span>{item.desc}</span>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 视图区容器 —— **净化渲染,无脚本执行权**(编码规范 §9.2;语义出处 AM 07)。
|
||||
*
|
||||
* 「视图区容器渲染活动推送的 HTML:沙箱 iframe(srcdoc + sandbox)或净化后注入,
|
||||
* 二选一由 `ui-kit` 的 `ViewContainer` 统一实现,**feature 不得自行 dangerouslySetInnerHTML**。」
|
||||
*
|
||||
* 这里选沙箱 iframe:结构上就没有脚本执行权,不依赖净化算法的完备性。
|
||||
*/
|
||||
export type ViewContainerProps = {
|
||||
readonly html: string;
|
||||
readonly title?: string;
|
||||
};
|
||||
|
||||
export function ViewContainer({ html, title = '活动视图' }: ViewContainerProps) {
|
||||
return (
|
||||
<iframe
|
||||
className="jc-view-frame"
|
||||
title={title}
|
||||
// sandbox 不含 allow-scripts —— 无脚本执行权
|
||||
sandbox=""
|
||||
srcDoc={html}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
/**
|
||||
* @ui —— 无业务语义展示组件(卡片原语 / 时间线 / 净化视图容器 ViewContainer)。
|
||||
* @ui —— 无业务语义展示组件(卡片原语 / 时间线 / **净化视图容器**)。
|
||||
*/
|
||||
export const UI_KIT_VERSION = '0.0.0-m0';
|
||||
export type { ViewContainerProps } from './ViewContainer.tsx';
|
||||
export { ViewContainer } from './ViewContainer.tsx';
|
||||
export { ViewBlocks } from './ViewBlocks.tsx';
|
||||
|
||||
export const UI_KIT_VERSION = '0.1.0-m6';
|
||||
|
||||
Generated
+126
-1
@@ -48,8 +48,18 @@ importers:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.7(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0)
|
||||
|
||||
activities/quote-alternative: {}
|
||||
|
||||
activities/quote-archive: {}
|
||||
|
||||
activities/quote-intake: {}
|
||||
|
||||
activities/quote-negotiate: {}
|
||||
|
||||
activities/quote-pricing: {}
|
||||
|
||||
activities/quote-sourcing: {}
|
||||
|
||||
apps/api:
|
||||
dependencies:
|
||||
'@ai-sdk/openai-compatible':
|
||||
@@ -125,6 +135,12 @@ importers:
|
||||
|
||||
apps/web:
|
||||
dependencies:
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.101.4
|
||||
version: 5.101.4(react@19.2.8)
|
||||
'@tanstack/react-router':
|
||||
specifier: ^1.170.18
|
||||
version: 1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
react:
|
||||
specifier: ^19.2.0
|
||||
version: 19.2.8
|
||||
@@ -155,7 +171,14 @@ importers:
|
||||
|
||||
packages/sfeel: {}
|
||||
|
||||
packages/ui-kit: {}
|
||||
packages/ui-kit:
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: ^19.2.17
|
||||
version: 19.2.17
|
||||
react:
|
||||
specifier: ^19.2.8
|
||||
version: 19.2.8
|
||||
|
||||
packages/utils: {}
|
||||
|
||||
@@ -1159,6 +1182,38 @@ packages:
|
||||
'@standard-schema/spec@1.1.0':
|
||||
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
|
||||
|
||||
'@tanstack/history@1.162.0':
|
||||
resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==}
|
||||
engines: {node: '>=20.19'}
|
||||
|
||||
'@tanstack/query-core@5.101.4':
|
||||
resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==}
|
||||
|
||||
'@tanstack/react-query@5.101.4':
|
||||
resolution: {integrity: sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
|
||||
'@tanstack/react-router@1.170.18':
|
||||
resolution: {integrity: sha512-wpbGYZEp/fmz1q4bn7BD8VZ+/VZ7GBqSJv5V969pU+chP8y7dquWDmKTFMohvUegb9lg12m1uPVvD6kB2wORvQ==}
|
||||
engines: {node: '>=20.19'}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0 || >=19.0.0'
|
||||
react-dom: '>=18.0.0 || >=19.0.0'
|
||||
|
||||
'@tanstack/react-store@0.9.3':
|
||||
resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@tanstack/router-core@1.171.15':
|
||||
resolution: {integrity: sha512-IILCDcLaItMZQ2jEmCABHY1Nhjjn5XUvwpQp3e4Nmu+vfg0BgYFuu/QASz2SwE2ZNbVMrvt8X/wxa+Gg5aErxA==}
|
||||
engines: {node: '>=20.19'}
|
||||
|
||||
'@tanstack/store@0.9.3':
|
||||
resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==}
|
||||
|
||||
'@testcontainers/postgresql@12.0.4':
|
||||
resolution: {integrity: sha512-a/pLU6j5lpKKAlUTPwqweqMGhOSjgTSb6HBX69TOrXn32ifU37nnQDmNFTj8ddOAw+BQL9oTRkeOxVbZkqhgZA==}
|
||||
|
||||
@@ -1581,6 +1636,9 @@ packages:
|
||||
convert-source-map@2.0.0:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
|
||||
cookie-es@3.1.1:
|
||||
resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==}
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
@@ -2028,6 +2086,10 @@ packages:
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
isbot@5.2.1:
|
||||
resolution: {integrity: sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
@@ -2473,6 +2535,16 @@ packages:
|
||||
resolution: {integrity: sha512-bBZaRwLH9PN5HbLCjPId4dP5bNGEtumcErgOX952IsvOhVPrm3/AeK1y0UHA/QaPG701eg0yEnOKsCOC6X/kaA==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
seroval-plugins@1.5.6:
|
||||
resolution: {integrity: sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
seroval: ^1.0
|
||||
|
||||
seroval@1.5.6:
|
||||
resolution: {integrity: sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2734,6 +2806,11 @@ packages:
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
@@ -3551,6 +3628,40 @@ snapshots:
|
||||
|
||||
'@standard-schema/spec@1.1.0': {}
|
||||
|
||||
'@tanstack/history@1.162.0': {}
|
||||
|
||||
'@tanstack/query-core@5.101.4': {}
|
||||
|
||||
'@tanstack/react-query@5.101.4(react@19.2.8)':
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.101.4
|
||||
react: 19.2.8
|
||||
|
||||
'@tanstack/react-router@1.170.18(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.162.0
|
||||
'@tanstack/react-store': 0.9.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)
|
||||
'@tanstack/router-core': 1.171.15
|
||||
isbot: 5.2.1
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
|
||||
'@tanstack/react-store@0.9.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)':
|
||||
dependencies:
|
||||
'@tanstack/store': 0.9.3
|
||||
react: 19.2.8
|
||||
react-dom: 19.2.8(react@19.2.8)
|
||||
use-sync-external-store: 1.6.0(react@19.2.8)
|
||||
|
||||
'@tanstack/router-core@1.171.15':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.162.0
|
||||
cookie-es: 3.1.1
|
||||
seroval: 1.5.6
|
||||
seroval-plugins: 1.5.6(seroval@1.5.6)
|
||||
|
||||
'@tanstack/store@0.9.3': {}
|
||||
|
||||
'@testcontainers/postgresql@12.0.4':
|
||||
dependencies:
|
||||
testcontainers: 12.0.4
|
||||
@@ -4033,6 +4144,8 @@ snapshots:
|
||||
|
||||
convert-source-map@2.0.0: {}
|
||||
|
||||
cookie-es@3.1.1: {}
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
cpu-features@0.0.10:
|
||||
@@ -4483,6 +4596,8 @@ snapshots:
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
isbot@5.2.1: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
@@ -4904,6 +5019,12 @@ snapshots:
|
||||
non-error: 0.1.0
|
||||
type-fest: 5.8.0
|
||||
|
||||
seroval-plugins@1.5.6(seroval@1.5.6):
|
||||
dependencies:
|
||||
seroval: 1.5.6
|
||||
|
||||
seroval@1.5.6: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
@@ -5212,6 +5333,10 @@ snapshots:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
use-sync-external-store@1.6.0(react@19.2.8):
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
vite-node@3.2.4(@types/node@22.20.1)(tsx@4.23.1)(yaml@2.9.0):
|
||||
|
||||
Reference in New Issue
Block a user