Archived
Feat/activity l3 detail #1
@@ -27,6 +27,11 @@ describe('identityOf —— claims 归一成一个人', () => {
|
|||||||
expect('email' in id).toBe(false);
|
expect('email' in id).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('姓/名散件在 → 姓在前拼「姓 名」,不采用全名字段的名+姓序(2026-08-26 用户定)', () => {
|
||||||
|
const id = identityOf({ sub: 's', preferred_username: 'shan', name: '亲 删', given_name: '亲', family_name: '删' });
|
||||||
|
expect(id.name).toBe('删 亲');
|
||||||
|
});
|
||||||
|
|
||||||
it('realm_access 有但 roles 缺 → 空角色', () => {
|
it('realm_access 有但 roles 缺 → 空角色', () => {
|
||||||
expect(identityOf({ sub: 's', preferred_username: 'bob', realm_access: {} }).roles).toEqual([]);
|
expect(identityOf({ sub: 's', preferred_username: 'bob', realm_access: {} }).roles).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,13 +42,13 @@ function fakeKeycloak(): { fetch: typeof fetch; calls: string[] } {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('Keycloak Admin API 名册', () => {
|
describe('Keycloak Admin API 名册', () => {
|
||||||
it('分页拉全(100 + 5)、姓名拼 firstName lastName、缺名回退用户名、角色滤噪声、enabled 照抄', async () => {
|
it('分页拉全(100 + 5)、姓名姓在前拼 lastName firstName、缺名回退用户名、角色滤噪声、enabled 照抄', async () => {
|
||||||
const kc = fakeKeycloak();
|
const kc = fakeKeycloak();
|
||||||
const roster = createKeycloakRoster({ url: 'https://kc.test/', realm: 'corp', clientId: 'provisioning', secret: 's', fetch: kc.fetch });
|
const roster = createKeycloakRoster({ url: 'https://kc.test/', realm: 'corp', clientId: 'provisioning', secret: 's', fetch: kc.fetch });
|
||||||
const users = await roster.listUsers();
|
const users = await roster.listUsers();
|
||||||
expect(users).toHaveLength(105);
|
expect(users).toHaveLength(105);
|
||||||
expect(users[0]).toEqual({ sub: 'sub-0', account: 'u0', name: 'u0', email: 'u0@x.test', roles: ['user'], enabled: true });
|
expect(users[0]).toEqual({ sub: 'sub-0', account: 'u0', name: 'u0', email: 'u0@x.test', roles: ['user'], enabled: true });
|
||||||
expect(users[1]).toEqual({ sub: 'sub-1', account: 'u1', name: '名 1', email: 'u1@x.test', roles: ['admin', 'user'], enabled: true });
|
expect(users[1]).toEqual({ sub: 'sub-1', account: 'u1', name: '1 名', email: 'u1@x.test', roles: ['admin', 'user'], enabled: true });
|
||||||
expect(users[3]?.enabled).toBe(false);
|
expect(users[3]?.enabled).toBe(false);
|
||||||
expect(kc.calls.filter((c) => c.includes('/users?')).length).toBe(2);
|
expect(kc.calls.filter((c) => c.includes('/users?')).length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ export function createKeycloakRoster(options: KeycloakRosterOptions): RosterSour
|
|||||||
const out: RosterUser[] = [];
|
const out: RosterUser[] = [];
|
||||||
for (const u of users) {
|
for (const u of users) {
|
||||||
const roles = await adminGet<KcRole[]>(`/users/${encodeURIComponent(u.id)}/role-mappings/realm/composite`);
|
const roles = await adminGet<KcRole[]>(`/users/${encodeURIComponent(u.id)}/role-mappings/realm/composite`);
|
||||||
const name = [u.firstName, u.lastName]
|
/* 姓在前(2026-08-26 用户定)—— 与 token 那条路(claims.ts `identityOf`)同一把尺 */
|
||||||
|
const name = [u.lastName, u.firstName]
|
||||||
.filter((s): s is string => s !== undefined && s.trim() !== '')
|
.filter((s): s is string => s !== undefined && s.trim() !== '')
|
||||||
.join(' ')
|
.join(' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ const claimsSchema = z.object({
|
|||||||
sub: z.string().min(1),
|
sub: z.string().min(1),
|
||||||
preferred_username: z.string().min(1),
|
preferred_username: z.string().min(1),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
|
given_name: z.string().optional(),
|
||||||
|
family_name: z.string().optional(),
|
||||||
email: z.string().optional(),
|
email: z.string().optional(),
|
||||||
realm_access: z.object({ roles: z.array(z.string()).optional() }).optional(),
|
realm_access: z.object({ roles: z.array(z.string()).optional() }).optional(),
|
||||||
});
|
});
|
||||||
@@ -39,6 +41,8 @@ function toVerifiedClaims(claims: z.infer<typeof claimsSchema>): VerifiedClaims
|
|||||||
sub: claims.sub,
|
sub: claims.sub,
|
||||||
preferred_username: claims.preferred_username,
|
preferred_username: claims.preferred_username,
|
||||||
...(claims.name === undefined ? {} : { name: claims.name }),
|
...(claims.name === undefined ? {} : { name: claims.name }),
|
||||||
|
...(claims.given_name === undefined ? {} : { given_name: claims.given_name }),
|
||||||
|
...(claims.family_name === undefined ? {} : { family_name: claims.family_name }),
|
||||||
...(claims.email === undefined ? {} : { email: claims.email }),
|
...(claims.email === undefined ? {} : { email: claims.email }),
|
||||||
...(claims.realm_access === undefined
|
...(claims.realm_access === undefined
|
||||||
? {}
|
? {}
|
||||||
|
|||||||
@@ -21,7 +21,13 @@ export function normalizeRoles(roles: readonly string[]): string[] {
|
|||||||
|
|
||||||
export function identityOf(claims: VerifiedClaims): Identity {
|
export function identityOf(claims: VerifiedClaims): Identity {
|
||||||
const account = claims.preferred_username;
|
const account = claims.preferred_username;
|
||||||
const name = claims.name === undefined || claims.name.trim() === '' ? account : claims.name.trim();
|
/* 姓名展示姓在前(2026-08-26 用户定):有姓/名散件就按「姓 名」拼;
|
||||||
|
没有散件才用 Keycloak 的全名 `name`(它按「名 姓」拼,中文场景顺序是反的) */
|
||||||
|
const cjk = [claims.family_name, claims.given_name]
|
||||||
|
.filter((s): s is string => s !== undefined && s.trim() !== '')
|
||||||
|
.join(' ');
|
||||||
|
const fallback = claims.name === undefined || claims.name.trim() === '' ? account : claims.name.trim();
|
||||||
|
const name = cjk !== '' ? cjk : fallback;
|
||||||
const email = claims.email === undefined || claims.email.trim() === '' ? undefined : claims.email.trim();
|
const email = claims.email === undefined || claims.email.trim() === '' ? undefined : claims.email.trim();
|
||||||
return {
|
return {
|
||||||
sub: claims.sub,
|
sub: claims.sub,
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export type VerifiedClaims = {
|
|||||||
readonly sub: string;
|
readonly sub: string;
|
||||||
readonly preferred_username: string;
|
readonly preferred_username: string;
|
||||||
readonly name?: string;
|
readonly name?: string;
|
||||||
|
/* 名/姓分开收(2026-08-26 用户定:中文姓名姓在前)—— Keycloak 的 `name` 是全名(名+姓),
|
||||||
|
展示要「姓 名」得靠这两个散件拼;没带就回退 `name` 原样 */
|
||||||
|
readonly given_name?: string;
|
||||||
|
readonly family_name?: string;
|
||||||
readonly email?: string;
|
readonly email?: string;
|
||||||
readonly realm_access?: { readonly roles?: readonly string[] };
|
readonly realm_access?: { readonly roles?: readonly string[] };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,97 +3,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>活动视图 · 询价接收与结构化</title>
|
<title>活动视图</title>
|
||||||
<!--
|
<!--
|
||||||
活动视图占位页(2026-08-25,L2 沙箱 v1):
|
活动视图占位页(L2 沙箱):
|
||||||
模拟「服务器返回的 HTML」—— 由 iframe + sandbox="allow-scripts" 隔离渲染,
|
模拟「服务器返回的 HTML」—— 由 iframe + sandbox="allow-scripts" 隔离渲染,
|
||||||
与宿主页面零样式/脚本互通;正式版由接口按活动定义返回。
|
与宿主页面零样式/脚本互通;正式版由接口按活动定义返回。
|
||||||
|
2026-08-26 用户定:内容全部清空(数据与骨架都不留),只留可加载的空白页等接口的 HTML。
|
||||||
-->
|
-->
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
body { margin: 0; background: #fff; }
|
||||||
body {
|
|
||||||
font: 14px/1.7 -apple-system, "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
|
|
||||||
color: #161a22; background: #f6f7f9; padding: 24px;
|
|
||||||
}
|
|
||||||
.wrap { max-width: 880px; margin: 0 auto; display: flex; flex-direction: column; gap: 16px; }
|
|
||||||
.head { display: flex; align-items: center; gap: 12px; }
|
|
||||||
.head .logo { width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center;
|
|
||||||
background: linear-gradient(150deg, #eaf1ff, #fff); color: #3370ff; box-shadow: inset 0 0 0 1px rgba(51,112,255,.18); }
|
|
||||||
.head h1 { font-size: 18px; font-weight: 700; letter-spacing: .01em; }
|
|
||||||
.head p { font-size: 12px; color: #8b93a3; font-family: ui-monospace, monospace; }
|
|
||||||
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; }
|
|
||||||
.stat { background: #fff; border: 1px solid #dfe3ea; border-radius: 8px; padding: 12px 14px; }
|
|
||||||
.stat em { display: block; font-style: normal; font-size: 11px; color: #8b93a3; }
|
|
||||||
.stat b { display: block; margin-top: 2px; font: 600 20px/1.2 ui-monospace, monospace; color: #161a22; }
|
|
||||||
.stat.hot b { color: #3370ff; }
|
|
||||||
.card { background: #fff; border: 1px solid #dfe3ea; border-radius: 8px; overflow: hidden; }
|
|
||||||
.card h2 { padding: 12px 16px; font-size: 13px; font-weight: 700; color: #4a5160;
|
|
||||||
border-bottom: 1px solid #eef1f5; letter-spacing: .03em; }
|
|
||||||
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
||||||
thead th { text-align: left; padding: 8px 16px; background: #f5f7fa; color: #4a5160;
|
|
||||||
font-size: 11px; font-weight: 700; border-bottom: 1px solid #e2e7ee; }
|
|
||||||
tbody td { padding: 10px 16px; border-bottom: 1px solid #eef1f5; }
|
|
||||||
tbody tr:nth-child(even) { background: #f8fafc; }
|
|
||||||
tbody tr:last-child td { border-bottom: 0; }
|
|
||||||
.mono { font-family: ui-monospace, monospace; font-size: 12px; }
|
|
||||||
.ok { display: inline-block; padding: 1px 8px; border-radius: 999px; background: #e6f6ee; color: #0c8154; font-size: 11px; font-weight: 600; }
|
|
||||||
.wait { display: inline-block; padding: 1px 8px; border-radius: 999px; background: #fdf3e3; color: #b0822d; font-size: 11px; font-weight: 600; }
|
|
||||||
.form { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; padding: 16px; }
|
|
||||||
.field em { display: block; font-style: normal; font-size: 11px; color: #8b93a3; margin-bottom: 4px; }
|
|
||||||
.field .box { height: 34px; border: 1px solid #dfe3ea; border-radius: 6px; background: #fbfcfd;
|
|
||||||
display: flex; align-items: center; padding: 0 10px; font-size: 12.5px; color: #4a5160; }
|
|
||||||
.field.wide { grid-column: 1 / -1; }
|
|
||||||
.cta { grid-column: 1 / -1; display: flex; justify-content: flex-end; gap: 8px; }
|
|
||||||
.btn { height: 34px; padding: 0 16px; border: 1px solid #dfe3ea; border-radius: 6px; background: #fff;
|
|
||||||
font-size: 13px; color: #4a5160; }
|
|
||||||
.btn.primary { background: #3370ff; border-color: #3370ff; color: #fff; font-weight: 600; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="wrap">
|
|
||||||
<div class="head">
|
|
||||||
<span class="logo">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline><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"></path></svg>
|
|
||||||
</span>
|
|
||||||
<div>
|
|
||||||
<h1>询价接收与结构化</h1>
|
|
||||||
<p>quote-intake · 活动视图(服务器返回 HTML · 沙箱隔离渲染)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="stats">
|
|
||||||
<div class="stat"><em>今日接收</em><b>6</b></div>
|
|
||||||
<div class="stat hot"><em>待人工补全</em><b>1</b></div>
|
|
||||||
<div class="stat"><em>结构化成功</em><b>5</b></div>
|
|
||||||
<div class="stat"><em>平均耗时</em><b>42s</b></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>最近的询价原文</h2>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr><th>来源</th><th>物料数</th><th>接收时间</th><th>结构化结果</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr><td>Itelma 采购部 · 邮件</td><td class="mono">3 项</td><td class="mono">10:12</td><td><span class="wait">待补全</span></td></tr>
|
|
||||||
<tr><td>伏尔加集团 · 聊天记录</td><td class="mono">12 项</td><td class="mono">09:40</td><td><span class="ok">已结构化</span></td></tr>
|
|
||||||
<tr><td>GAZ 采购 · 邮件附件</td><td class="mono">7 项</td><td class="mono">昨天 17:03</td><td><span class="ok">已结构化</span></td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<h2>人工补全(待执行 · INS-2417)</h2>
|
|
||||||
<div class="form">
|
|
||||||
<div class="field"><em>物料编码</em><div class="box">MCU-STM32F407VET6</div></div>
|
|
||||||
<div class="field"><em>数量</em><div class="box mono">2,400 pcs</div></div>
|
|
||||||
<div class="field wide"><em>交期要求</em><div class="box">2026-09-30 前到莫斯科仓</div></div>
|
|
||||||
<div class="cta">
|
|
||||||
<button class="btn">暂存</button>
|
|
||||||
<button class="btn primary">确认提交</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* 工作台外壳 —— 自原型 `(proto)/page.tsx` 平移(README《前端 UI · 原型 1:1 平移》)。
|
* 工作台外壳 —— 自原型 `(proto)/page.tsx` 平移(README《前端 UI · 原型 1:1 平移》)。
|
||||||
*
|
*
|
||||||
* 左轨 = 品牌 → **工作台五要素**(任务 / 审批 / 消息 / 纪要 / 汇报);身份在顶栏右侧菜单;
|
* 左轨 = 品牌 → **工作台五要素**(任务 / 审批 / 消息 / 纪要 / 汇报)→ 轨底身份菜单;
|
||||||
* 任务四区 = 左栏列表区 / 中上标题区 / 中下工作区 / 右栏对话区。
|
* 任务四区 = 左栏列表区 / 中上标题区 / 中下工作区 / 右栏对话区。
|
||||||
*
|
*
|
||||||
* **前端零过程状态**:数据全部来自服务端投影,刷新即从投影重建(§9.2)。
|
* **前端零过程状态**:数据全部来自服务端投影,刷新即从投影重建(§9.2)。
|
||||||
@@ -167,38 +167,31 @@ export function AppShell() {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* 轨底不再报身份:头像与姓名挪到顶栏右侧的身份菜单里(2026-08-25 用户定) */}
|
{/* 身份菜单收在轨底(2026-08-26 用户定:顶栏撤掉,内容挪到左轨底部)——
|
||||||
|
轨宽 78px,头像上姓名下竖排;悬停/聚焦从轨右侧弹出,登出仍是全站唯一入口 */}
|
||||||
|
<div className="jc-usermenu jc-usermenu-rail">
|
||||||
|
<span className="jc-nav-ava">{me.slice(0, 1)}</span>
|
||||||
|
<span className="jc-topbar-name">{me}</span>
|
||||||
|
<div className="jc-usermenu-pop">
|
||||||
|
{/* 个人中心独立成页(/me 台面),整页导航过去(2026-08-25 用户定) */}
|
||||||
|
<a href="/me">
|
||||||
|
<Ico name="user" s={14} />
|
||||||
|
个人中心
|
||||||
|
</a>
|
||||||
|
{/* 设计中心直链数据域页,整页导航过去(2026-08-25 用户定) */}
|
||||||
|
<a href="/design#/design/data/domains">
|
||||||
|
<Ico name="pen-line" s={14} />
|
||||||
|
设计中心
|
||||||
|
</a>
|
||||||
|
<button type="button" className="danger" onClick={logout}>
|
||||||
|
<Ico name="log-out" s={14} />
|
||||||
|
退出登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="app-main">
|
<div className="app-main">
|
||||||
<header className="jc-topbar jc-topbar-app">
|
|
||||||
{/* 顶栏不挂模块名,只留右侧身份菜单(2026-08-25 用户定) */}
|
|
||||||
<div className="jc-topbar-user">
|
|
||||||
{/* 身份菜单:悬停头像/姓名出下拉 —— 个人中心(建设中提示)+ 退出登录;登出仍是全站唯一入口 */}
|
|
||||||
<div className="jc-usermenu">
|
|
||||||
<span className="jc-nav-ava">{me.slice(0, 1)}</span>
|
|
||||||
<span className="jc-topbar-name">{me}</span>
|
|
||||||
<Ico name="chevron-down" s={12} />
|
|
||||||
<div className="jc-usermenu-pop">
|
|
||||||
{/* 个人中心独立成页(/me 台面),整页导航过去(2026-08-25 用户定) */}
|
|
||||||
<a href="/me">
|
|
||||||
<Ico name="user" s={14} />
|
|
||||||
个人中心
|
|
||||||
</a>
|
|
||||||
{/* 设计中心直链数据域页,整页导航过去(2026-08-25 用户定) */}
|
|
||||||
<a href="/design#/design/data/domains">
|
|
||||||
<Ico name="pen-line" s={14} />
|
|
||||||
设计中心
|
|
||||||
</a>
|
|
||||||
<button type="button" className="danger" onClick={logout}>
|
|
||||||
<Ico name="log-out" s={14} />
|
|
||||||
退出登录
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className="app-main-body">
|
<div className="app-main-body">
|
||||||
{module === 'tasks' ? (
|
{module === 'tasks' ? (
|
||||||
<div className={'workspace' + (collapsed ? ' jc-side-collapsed' : '')} style={split.style}>
|
<div className={'workspace' + (collapsed ? ' jc-side-collapsed' : '')} style={split.style}>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { useGovObjects, useGovTasks } from '@web-shared/gov/gov-api.ts';
|
|||||||
import { domainShort, sortDomains } from '@web-shared/gov/domains.ts';
|
import { domainShort, sortDomains } from '@web-shared/gov/domains.ts';
|
||||||
import { queryKeys } from './query.ts';
|
import { queryKeys } from './query.ts';
|
||||||
import { BlueprintHome } from '@features/gov-blueprint';
|
import { BlueprintHome } from '@features/gov-blueprint';
|
||||||
import { TasksHome, ActvPage, TaskLogPage, TaskPage, WfPage } from '@features/gov-task';
|
import { TasksHome, ActiPage, ActvPage, TaskLogPage, TaskPage, WfPage } from '@features/gov-task';
|
||||||
import { RunsHome } from '@features/gov-runs';
|
import { RunsHome } from '@features/gov-runs';
|
||||||
import { EmpPage, PeopleHome, RolePage } from '@features/gov-staff';
|
import { EmpPage, PeopleHome, RolePage } from '@features/gov-staff';
|
||||||
import { PkgPage, WorkflowsHome } from '@features/gov-process';
|
import { PkgPage, WorkflowsHome } from '@features/gov-process';
|
||||||
@@ -330,7 +330,9 @@ function Detail({ route, nav }: { route: Route; nav: Nav }) {
|
|||||||
case 'act':
|
case 'act':
|
||||||
return <ActPage id={route.id} />;
|
return <ActPage id={route.id} />;
|
||||||
case 'actd':
|
case 'actd':
|
||||||
return <ActvPage id={route.id} />;
|
return <ActvPage id={route.id} nav={nav} />;
|
||||||
|
case 'acti':
|
||||||
|
return <ActiPage id={route.id} />;
|
||||||
case 'tool':
|
case 'tool':
|
||||||
return <ToolPage id={route.id} />;
|
return <ToolPage id={route.id} />;
|
||||||
case 'obj':
|
case 'obj':
|
||||||
|
|||||||
@@ -476,16 +476,14 @@
|
|||||||
.proto-root .jc-sup:focus { outline: 0; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(51,112,255,.12); }
|
.proto-root .jc-sup:focus { outline: 0; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(51,112,255,.12); }
|
||||||
.proto-root .jc-sup:disabled { background: #f3f4f6; color: #5c626b; }
|
.proto-root .jc-sup:disabled { background: #f3f4f6; color: #5c626b; }
|
||||||
|
|
||||||
/* ===== 左侧一体化导航轨:品牌 → 五大模块(身份已挪去顶栏右侧,见下)===== */
|
/* ===== 左侧一体化导航轨:品牌 → 五大模块 → 轨底身份区(2026-08-26 用户定:顶栏撤了,身份收回轨底)===== */
|
||||||
.proto-root .app-nav { padding: 14px 10px 12px; }
|
.proto-root .app-nav { padding: 14px 10px 12px; }
|
||||||
.proto-root .jc-rail-brand { text-align: center; font-size: 10px; font-weight: 800; letter-spacing: .14em; color: var(--ink-3); margin: -4px 0 6px; }
|
.proto-root .jc-rail-brand { text-align: center; font-size: 10px; font-weight: 800; letter-spacing: .14em; color: var(--ink-3); margin: -4px 0 6px; }
|
||||||
.proto-root .app-nav button { min-height: 58px; transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast); }
|
.proto-root .app-nav button { min-height: 58px; transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast); }
|
||||||
.proto-root .app-nav button:active { transform: scale(.97); }
|
.proto-root .app-nav button:active { transform: scale(.97); }
|
||||||
/* 轨底身份区已撤:头像与姓名挪去顶栏右侧身份菜单(2026-08-25 用户定);
|
|
||||||
.jc-nav-ava 保留 —— 顶栏身份菜单复用这枚渐变头像 */
|
|
||||||
.proto-root .jc-nav-ava { width: 32px; height: 32px; border-radius: 999px; display: grid; place-items: center; flex: 0 0 auto; line-height: 1; color: #fff; font-size: 13px; font-weight: 800; background: linear-gradient(140deg, #2bc7d4, #0e9fd8); box-shadow: 0 2px 6px rgba(14, 159, 216, .35); }
|
.proto-root .jc-nav-ava { width: 32px; height: 32px; border-radius: 999px; display: grid; place-items: center; flex: 0 0 auto; line-height: 1; color: #fff; font-size: 13px; font-weight: 800; background: linear-gradient(140deg, #2bc7d4, #0e9fd8); box-shadow: 0 2px 6px rgba(14, 159, 216, .35); }
|
||||||
|
|
||||||
/* ===== 顶栏:44px 通栏,只放右侧身份菜单 —— 与左轨互不重叠,登出只有这一处入口 ===== */
|
/* ===== 主区:顶栏已撤(2026-08-26 用户定),内容区直接铺满 ===== */
|
||||||
.proto-root .app-main { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
.proto-root .app-main { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
||||||
.proto-root .app-main-body { flex: 1 1 auto; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
.proto-root .app-main-body { flex: 1 1 auto; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
|
||||||
.proto-root .app-main-body > * { flex: 1 1 auto; min-width: 0; min-height: 0; }
|
.proto-root .app-main-body > * { flex: 1 1 auto; min-width: 0; min-height: 0; }
|
||||||
@@ -500,8 +498,8 @@
|
|||||||
/* 但右上角装饰光晕定位 right:-60px,裁切一放开它就探出视口右缘 60px,整页出现横向滚动
|
/* 但右上角装饰光晕定位 right:-60px,裁切一放开它就探出视口右缘 60px,整页出现横向滚动
|
||||||
(2026-08-25 用户报:页面能左右滑)—— 把它收回盒内,光晕照留 */
|
(2026-08-25 用户报:页面能左右滑)—— 把它收回盒内,光晕照留 */
|
||||||
.proto-root .jc-topbar.jc-topbar-app::after { right: 0; }
|
.proto-root .jc-topbar.jc-topbar-app::after { right: 0; }
|
||||||
/* 顶栏右侧身份菜单:头像 + 姓名,悬停出下拉(个人中心 / 退出登录)—— 纯 CSS 悬停无 JS;
|
/* 身份菜单:头像 + 姓名,悬停出下拉(个人中心 / 设计中心 / 退出登录)—— 纯 CSS 悬停无 JS;
|
||||||
登出入口从独立按钮收进下拉,仍是全站唯一入口(2026-08-25 用户定) */
|
登出入口收进下拉,是全站唯一入口(2026-08-25 用户定);2026-08-26 从顶栏挪进左轨底部(见下方 rail 变体) */
|
||||||
.proto-root .jc-usermenu { position: relative; display: flex; align-items: center; gap: 8px; padding: 4px 8px 4px 4px; border-radius: 10px; cursor: default; color: var(--ink-4); transition: background var(--t-fast); }
|
.proto-root .jc-usermenu { position: relative; display: flex; align-items: center; gap: 8px; padding: 4px 8px 4px 4px; border-radius: 10px; cursor: default; color: var(--ink-4); transition: background var(--t-fast); }
|
||||||
.proto-root .jc-usermenu:hover { background: var(--soft); }
|
.proto-root .jc-usermenu:hover { background: var(--soft); }
|
||||||
.proto-root .jc-usermenu .jc-nav-ava { width: 28px; height: 28px; font-size: 12px; }
|
.proto-root .jc-usermenu .jc-nav-ava { width: 28px; height: 28px; font-size: 12px; }
|
||||||
@@ -514,6 +512,14 @@
|
|||||||
.proto-root .jc-usermenu-pop button.danger:hover { background: var(--danger-soft); color: var(--danger); }
|
.proto-root .jc-usermenu-pop button.danger:hover { background: var(--danger-soft); color: var(--danger); }
|
||||||
.proto-root .jc-usermenu-pop button:disabled { opacity: .6; cursor: default; }
|
.proto-root .jc-usermenu-pop button:disabled { opacity: .6; cursor: default; }
|
||||||
|
|
||||||
|
/* 轨底变体(2026-08-26 用户定):轨宽 78px,头像上姓名下竖排,与模块钮之间一道细分隔;
|
||||||
|
下拉改从轨右侧弹出、底缘与触发区对齐,悬停桥跟着转到右侧 */
|
||||||
|
.proto-root .jc-usermenu-rail { margin-top: auto; align-self: stretch; flex-direction: column; justify-content: center; gap: 4px; padding: 10px 2px 4px; border-top: 1px solid var(--line-soft); border-radius: 0; }
|
||||||
|
.proto-root .jc-usermenu-rail:hover { background: none; }
|
||||||
|
.proto-root .jc-usermenu-rail .jc-topbar-name { max-width: 66px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; text-align: center; }
|
||||||
|
.proto-root .jc-usermenu-rail::after { top: 0; bottom: 0; left: 100%; right: auto; width: 10px; height: auto; }
|
||||||
|
.proto-root .jc-usermenu-rail .jc-usermenu-pop { top: auto; right: auto; bottom: 0; left: calc(100% + 10px); transform: translateX(-4px); }
|
||||||
|
|
||||||
/* 「建设中」提示弹窗:未开放模块(审批/消息/纪要/汇报)与个人中心的占位口径 —— 不跳页只弹提示 */
|
/* 「建设中」提示弹窗:未开放模块(审批/消息/纪要/汇报)与个人中心的占位口径 —— 不跳页只弹提示 */
|
||||||
.proto-root .jc-wip-overlay { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; background: rgba(31, 35, 41, .45); }
|
.proto-root .jc-wip-overlay { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; background: rgba(31, 35, 41, .45); }
|
||||||
.proto-root .jc-wip-modal { width: 360px; max-width: 92vw; padding: 24px 20px 18px; background: #fff; border-radius: 16px; box-shadow: 0 24px 60px rgba(31, 35, 41, .22); text-align: center; animation: rise var(--t-med) both; }
|
.proto-root .jc-wip-modal { width: 360px; max-width: 92vw; padding: 24px 20px 18px; background: #fff; border-radius: 16px; box-shadow: 0 24px 60px rgba(31, 35, 41, .22); text-align: center; animation: rise var(--t-med) both; }
|
||||||
|
|||||||
@@ -635,6 +635,106 @@
|
|||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.g-actvd > * { animation: none; }
|
.g-actvd > * { animation: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ══ 实例详情 L3(2026-08-26,`.g-actid` 作用域)══
|
||||||
|
主从 2:1(DESIGN-SPEC §9):左「动作过程记录」是主角,右栏输入/输出是辅助;
|
||||||
|
业务色相从 L1/L2 接过来(--ah 只染页头图标块与文件图标块,一处记忆点) */
|
||||||
|
.g-actid { display: flex; flex-direction: column; gap: 16px; --ah: #3370ff; --ah-soft: #eaf1ff; }
|
||||||
|
.g-actid.h-teal { --ah: #0aa294; --ah-soft: #e0f5f3; }
|
||||||
|
.g-actid.h-amber { --ah: #d9730d; --ah-soft: #fdf0e0; }
|
||||||
|
.g-actid.h-violet { --ah: #7f3bf5; --ah-soft: #f1e9fe; }
|
||||||
|
/* 入场与 L2 同一节奏:自下而上淡入,60ms 逐段 stagger(深入一层,从下进场) */
|
||||||
|
.g-actid > * { animation: actvd-in .4s ease-out both; }
|
||||||
|
.g-actid > *:nth-child(2) { animation-delay: 60ms; }
|
||||||
|
.g-actid > *:nth-child(3) { animation-delay: 120ms; }
|
||||||
|
.g-actid > *:nth-child(4) { animation-delay: 180ms; }
|
||||||
|
.g-actid .g-dh-i { color: var(--ah); background: var(--ah-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ah) 18%, transparent); }
|
||||||
|
/* 状态丸翻转(终止)时弹出一次高亮 —— 变化要看得见 */
|
||||||
|
.g-actid .g-dh .chips > * { animation: actvd-pop .28s ease-out; }
|
||||||
|
|
||||||
|
/* 主从栅格:宽屏 2:1,≤1024 收单列(§14 降级路径) */
|
||||||
|
.g-actid-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 16px; align-items: start; }
|
||||||
|
.g-actid-rail { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.g-actid-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* —— 动作过程记录:竖时间线 —— 轨(点+线)在左,内容在右;
|
||||||
|
状态色只进圆点与结果词(--tone 来自行上的 t-* 类),行面不上色 */
|
||||||
|
.g-actid .g-tl { list-style: none; margin: 0; padding: 0; background: #fff; border: 1px solid #dfe3ea; border-radius: 8px; overflow: hidden; }
|
||||||
|
.g-actid .g-tl-row { display: grid; grid-template-columns: 22px minmax(0, 1fr); gap: 12px; padding: 12px 14px; transition: background var(--t-fast); }
|
||||||
|
.g-actid .g-tl-row + .g-tl-row { border-top: 1px solid #eef1f5; }
|
||||||
|
.g-actid .g-tl-row:hover { background: #f7f9fc; }
|
||||||
|
.g-actid .g-tl-rail { display: flex; flex-direction: column; align-items: center; padding-top: 4px; }
|
||||||
|
.g-actid .g-tl-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--tone, #c7cdd6); flex: none; }
|
||||||
|
/* 连接线向下探出 25px(下行上 padding 12 + 边框 1 + 本行下 padding 12),把时间线串成一条不间断的竖轨 */
|
||||||
|
.g-actid .g-tl-line { width: 1px; flex: 1; margin-top: 3px; margin-bottom: -25px; background: #dde2ea; }
|
||||||
|
/* 未到达:空心点 + 灰字 —— 「还没走到」与「走完了」要一眼分开 */
|
||||||
|
.g-actid .g-tl-row.pending .g-tl-dot { background: #fff; box-shadow: inset 0 0 0 2px #d8dee6; }
|
||||||
|
/* 当前步:呼吸环(全页唯一一处持续动效,§13 克制;reduced-motion 关掉) */
|
||||||
|
.g-actid .g-tl-row.current .g-tl-dot { animation: actid-pulse 1.6s ease-out infinite; }
|
||||||
|
@keyframes actid-pulse {
|
||||||
|
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--tone) 35%, transparent); }
|
||||||
|
100% { box-shadow: 0 0 0 7px transparent; }
|
||||||
|
}
|
||||||
|
.g-actid .g-tl-c { min-width: 0; }
|
||||||
|
.g-actid .g-tl-h { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
||||||
|
.g-actid .g-tl-h b { font-size: 13px; font-weight: 600; color: var(--ink); }
|
||||||
|
.g-actid .g-tl-by { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--ink-3); }
|
||||||
|
.g-actid .g-tl-meta { margin-left: auto; font: 500 11px/1.6 var(--mono); color: var(--ink-4); }
|
||||||
|
.g-actid .g-tl-res { font-size: 11px; font-weight: 600; color: var(--tone-ink); }
|
||||||
|
.g-actid .g-tl-note { margin: 3px 0 0; font-size: 12px; line-height: 1.6; color: var(--ink-3); }
|
||||||
|
.g-actid .g-tl-row.pending .g-tl-h b { font-weight: 500; color: var(--ink-4); }
|
||||||
|
.g-actid .g-tl-row.pending .g-tl-res { font-weight: 400; color: var(--ink-4); }
|
||||||
|
.g-actid .g-tl-row.pending .g-tl-note { color: var(--ink-4); }
|
||||||
|
|
||||||
|
/* —— 辅栏:输入(键值行)/ 输出(文件行)—— 白卡 1px 描边,与 L2 列表同表面 */
|
||||||
|
.g-actid .g-kv { background: #fff; border: 1px solid #dfe3ea; border-radius: 8px; padding: 4px 14px; }
|
||||||
|
.g-actid .g-kv-row { display: flex; gap: 12px; padding: 8px 0; }
|
||||||
|
.g-actid .g-kv-row + .g-kv-row { border-top: 1px solid #eef1f5; }
|
||||||
|
.g-actid .g-kv-row em { flex: none; width: 44px; padding-top: 1px; font-style: normal; font-size: 12px; color: var(--ink-4); }
|
||||||
|
.g-actid .g-kv-row b { font-weight: 500; font-size: 12.5px; line-height: 1.6; color: var(--ink); word-break: break-all; }
|
||||||
|
.g-actid .g-kv-note { padding: 8px 0; border-top: 1px solid #eef1f5; font-size: 11.5px; color: var(--ink-4); }
|
||||||
|
.g-actid .g-file { display: flex; align-items: center; gap: 10px; background: #fff; border: 1px solid #dfe3ea; border-radius: 8px; padding: 10px 14px; }
|
||||||
|
.g-actid .g-file-i { flex: none; width: 30px; height: 30px; display: grid; place-items: center; border-radius: 6px; background: var(--ah-soft); color: var(--ah); }
|
||||||
|
.g-actid .g-file-n { min-width: 0; }
|
||||||
|
.g-actid .g-file-n b { display: block; font: 500 12px/1.4 var(--mono); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.g-actid .g-file-n em { font-style: normal; font-size: 11px; color: var(--ink-4); }
|
||||||
|
.g-actid .g-file .g-rowbtn { margin-left: auto; flex: none; }
|
||||||
|
.g-actid .g-file-empty { background: #fff; border: 1px dashed #d8dee6; border-radius: 8px; padding: 18px 14px; text-align: center; font-size: 12px; color: var(--ink-4); }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.g-actid > * { animation: none; }
|
||||||
|
.g-actid .g-tl-row.current .g-tl-dot { animation: none; box-shadow: 0 0 0 3px var(--tone-soft); }
|
||||||
|
.g-actid .g-dh .chips > * { animation: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══ 二次确认·轻气泡(2026-08-26 用户定:只问一句 + 确认/取消,不摆影响三必答)══
|
||||||
|
气泡挨在触发钮**左边**(垂直对到钮中线),小三角从右缘指回钮;点透明罩即取消;
|
||||||
|
阴影只给弹层(§10),确认钮 danger 非实心(§11),进场 180ms 从触发点展开(§13) */
|
||||||
|
/* 正 z-index 必须有:`.g-actvd/.g-actid > *` 的入场动画 forwards 填充会让每个区块长期保持独立层叠上下文,
|
||||||
|
没有它,气泡的 z-index 只在页头内部作数,盖不住后面的区块 */
|
||||||
|
.g-actvd .g-pahead, .g-actid .g-pahead { position: relative; z-index: 30; }
|
||||||
|
.g-actvd .g-popask-veil, .g-actid .g-popask-veil { position: fixed; inset: 0; z-index: 39; }
|
||||||
|
/* right 的取值 = 页头右 padding + 触发钮右边那几枚钮的宽度和 + 10px 间隙 —— 钮文案改了这里要跟着量 */
|
||||||
|
.g-actvd .g-popask, .g-actid .g-popask { position: absolute; top: 16px; z-index: 40;
|
||||||
|
width: 264px; padding: 13px 14px 12px; background: #fff;
|
||||||
|
border: 1px solid var(--tone-line, var(--cline)); border-radius: 12px;
|
||||||
|
box-shadow: 0 12px 32px rgba(22, 26, 34, .14), 0 2px 8px rgba(22, 26, 34, .08);
|
||||||
|
animation: actvd-ask .18s cubic-bezier(.3, 0, .2, 1) both; transform-origin: right center; }
|
||||||
|
/* L2:触发钮右边还有「启动活动」占位钮 → 让出两枚钮的宽度;L3:触发钮自己就是最右枚 */
|
||||||
|
.g-actvd .g-popask { right: 232px; }
|
||||||
|
.g-actid .g-popask { right: 130px; }
|
||||||
|
@keyframes actvd-ask { from { opacity: 0; transform: translateX(4px) scale(.97); } }
|
||||||
|
/* 小三角:右缘中点指向触发钮(上/右两条边旋转 45° 拼出) */
|
||||||
|
.g-actvd .g-popask::before, .g-actid .g-popask::before { content: ""; position: absolute; top: 50%; margin-top: -5px; right: -6px;
|
||||||
|
width: 10px; height: 10px; background: #fff; transform: rotate(45deg);
|
||||||
|
border-top: 1px solid var(--tone-line, var(--cline)); border-right: 1px solid var(--tone-line, var(--cline)); }
|
||||||
|
.g-actvd .g-popask p, .g-actid .g-popask p { margin: 0; font-size: 13px; font-weight: 600; line-height: 1.55; color: var(--ink); }
|
||||||
|
.g-actvd .g-popask-a, .g-actid .g-popask-a { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.g-actvd .g-popask, .g-actid .g-popask { animation: none; }
|
||||||
|
}
|
||||||
.g-fcard .mbtn { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--ink-4); cursor: pointer; }
|
.g-fcard .mbtn { flex: none; width: 26px; height: 26px; display: grid; place-items: center; border: 1px solid transparent; border-radius: 7px; background: transparent; color: var(--ink-4); cursor: pointer; }
|
||||||
.g-fcard .mbtn:hover, .g-fcard .mbtn.on { border-color: var(--cline); background: #f3f6fa; color: var(--ink); }
|
.g-fcard .mbtn:hover, .g-fcard .mbtn.on { border-color: var(--cline); background: #f3f6fa; color: var(--ink); }
|
||||||
.g-fcard .mbtn.bad:hover { border-color: #e6c4c4; background: #fdf2f2; color: var(--danger); }
|
.g-fcard .mbtn.bad:hover { border-color: #e6c4c4; background: #fdf2f2; color: var(--danger); }
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
* feature `gov-task` 唯一出口(编码规范 §9.1)。
|
* feature `gov-task` 唯一出口(编码规范 §9.1)。
|
||||||
* 任务管理:在途监控、控制事件(暂停/叫停/终止)、恢复、改派、任务日志查看
|
* 任务管理:在途监控、控制事件(暂停/叫停/终止)、恢复、改派、任务日志查看
|
||||||
*/
|
*/
|
||||||
export { TasksHome, ActvPage, WfPage, TaskPage, TaskLogPage, WorkflowBoard } from './internal/panel.tsx';
|
export { TasksHome, ActiPage, ActvPage, WfPage, TaskPage, TaskLogPage, WorkflowBoard } from './internal/panel.tsx';
|
||||||
|
|||||||
@@ -283,6 +283,36 @@ function inTimeRange(at: string, from: string, to: string): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 轻量二次确认的规格(2026-08-26 用户定:只问一句 + 确认/取消 —— 影响三必答(AskBand)是治理台重度动作的规格,禁用活动/终止实例是轻动作,不摆) */
|
||||||
|
type PopAskSpec = { title: string; okLabel: string; danger?: boolean; onOk: () => void };
|
||||||
|
|
||||||
|
/** 气泡式二次确认 —— 锚在页头右下的小泡泡:一句话问清,确认/取消两钮;点气泡外的透明罩即取消 */
|
||||||
|
function PopAsk({ spec, onCancel }: { spec: PopAskSpec; onCancel: () => void }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="g-popask-veil" onClick={onCancel} />
|
||||||
|
<div className={'g-popask' + (spec.danger === true ? ' t-danger' : '')} role="alertdialog" aria-label={spec.title}>
|
||||||
|
<p>{spec.title}</p>
|
||||||
|
<div className="g-popask-a">
|
||||||
|
<button type="button" className="g-rowbtn" onClick={onCancel}>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={'g-rowbtn' + (spec.danger === true ? ' danger' : '')}
|
||||||
|
onClick={() => {
|
||||||
|
spec.onOk();
|
||||||
|
onCancel();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{spec.okLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* L2 · 活动详情 —— 两个问题一屏答:「这个活动是什么」(定义,只读)
|
* L2 · 活动详情 —— 两个问题一屏答:「这个活动是什么」(定义,只读)
|
||||||
* 与「它跑得怎么样」(小统计 + 实例清单,点行进 L3)。
|
* 与「它跑得怎么样」(小统计 + 实例清单,点行进 L3)。
|
||||||
@@ -294,7 +324,7 @@ function inTimeRange(at: string, from: string, to: string): boolean {
|
|||||||
* 沙箱区:加载服务器返回的活动视图 HTML —— iframe + sandbox 隔离;
|
* 沙箱区:加载服务器返回的活动视图 HTML —— iframe + sandbox 隔离;
|
||||||
* v1 静态占位页,src 与内容高度通讯(postMessage)待接口一起定。
|
* v1 静态占位页,src 与内容高度通讯(postMessage)待接口一起定。
|
||||||
*/
|
*/
|
||||||
export function ActvPage({ id }: { id: string }) {
|
export function ActvPage({ id, nav }: { id: string; nav: Nav }) {
|
||||||
const a = DEMO_ACTIVITIES.find((x) => x.id === id);
|
const a = DEMO_ACTIVITIES.find((x) => x.id === id);
|
||||||
const [status, setStatus] = useState<'active' | 'disabled' | undefined>(undefined);
|
const [status, setStatus] = useState<'active' | 'disabled' | undefined>(undefined);
|
||||||
const [blocked, setBlocked] = useState(false);
|
const [blocked, setBlocked] = useState(false);
|
||||||
@@ -309,6 +339,8 @@ export function ActvPage({ id }: { id: string }) {
|
|||||||
const [fState, setFState] = useState<InstanceState | ''>('');
|
const [fState, setFState] = useState<InstanceState | ''>('');
|
||||||
const [fFrom, setFFrom] = useState('');
|
const [fFrom, setFFrom] = useState('');
|
||||||
const [fTo, setFTo] = useState('');
|
const [fTo, setFTo] = useState('');
|
||||||
|
/* 轻量二次确认(2026-08-26 用户定):一句话 + 确认/取消的气泡,不走 AskBand 三必答 */
|
||||||
|
const [ask, setAsk] = useState<PopAskSpec | null>(null);
|
||||||
if (a === undefined) return <Note tone="warn">活动不存在 —— 它可能已被调整,返回活动清单看看。</Note>;
|
if (a === undefined) return <Note tone="warn">活动不存在 —— 它可能已被调整,返回活动清单看看。</Note>;
|
||||||
const st = status ?? a.status;
|
const st = status ?? a.status;
|
||||||
const instances = rows ?? demoInstances(a.owner);
|
const instances = rows ?? demoInstances(a.owner);
|
||||||
@@ -354,71 +386,73 @@ export function ActvPage({ id }: { id: string }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'g-actvd' + (a.hue === 'blue' ? '' : ` h-${a.hue}`)}>
|
<div className={'g-actvd' + (a.hue === 'blue' ? '' : ` h-${a.hue}`)}>
|
||||||
<PageHead
|
<div className="g-pahead">
|
||||||
ico={a.ico}
|
<PageHead
|
||||||
kicker={`${a.biz} · ${a.id}`}
|
ico={a.ico}
|
||||||
title={a.name}
|
kicker={`${a.biz} · ${a.id}`}
|
||||||
chips={
|
title={a.name}
|
||||||
<>
|
chips={
|
||||||
<Chip tone={st === 'active' ? 'ok' : 'mute'}>{st === 'active' ? '激活中' : '已禁用'}</Chip>
|
<>
|
||||||
{a.version !== undefined && <Chip ico="tag">v{a.version}</Chip>}
|
<Chip tone={st === 'active' ? 'ok' : 'mute'}>{st === 'active' ? '激活中' : '已禁用'}</Chip>
|
||||||
{/* 绑定的 LLM 上头版(2026-08-25 用户定):每个活动一个专属 agent */}
|
{a.version !== undefined && <Chip ico="tag">v{a.version}</Chip>}
|
||||||
<Chip ico="cpu">{a.llm}</Chip>
|
{/* 绑定的 LLM 上头版(2026-08-25 用户定):每个活动一个专属 agent */}
|
||||||
<Chip ico="user">{a.owner}</Chip>
|
<Chip ico="cpu">{a.llm}</Chip>
|
||||||
<Chip ico="calendar-days">创建于 {a.createdAt}</Chip>
|
<Chip ico="user">{a.owner}</Chip>
|
||||||
</>
|
<Chip ico="calendar-days">创建于 {a.createdAt}</Chip>
|
||||||
}
|
</>
|
||||||
desc="活动定义只读 —— 输入与输出在创建时定死,演进走版本迭代;验收在工作台,这里只看状态。"
|
}
|
||||||
acts={[
|
desc="活动定义只读 —— 输入与输出在创建时定死,演进走版本迭代;验收在工作台,这里只看状态。"
|
||||||
st === 'active'
|
acts={[
|
||||||
? {
|
st === 'active'
|
||||||
label: '禁用活动',
|
? {
|
||||||
ico: 'power',
|
label: '禁用活动',
|
||||||
danger: true,
|
ico: 'power',
|
||||||
ask: {
|
danger: true,
|
||||||
title: '确认禁用这个活动?',
|
onClick: () =>
|
||||||
note: '禁用后不能再为它创建实例;已在跑的实例不受影响。',
|
setAsk({
|
||||||
impact: { who: '该活动及其后续实例创建', undo: '可恢复 —— 随时重新激活', when: '确认后立即生效(本期前端演示,接口对接后落库)' },
|
title: '确认禁用这个活动?',
|
||||||
okLabel: '确认禁用',
|
okLabel: '确认禁用',
|
||||||
onOk: () => {
|
danger: true,
|
||||||
/* 规则(2026-08-25 用户定):有实例时接口会提示无法禁用 —— 本期前端先演出这个拦截 */
|
onOk: () => {
|
||||||
if (a.instances > 0) {
|
/* 规则(2026-08-25 用户定):有实例时接口会提示无法禁用 —— 本期前端先演出这个拦截 */
|
||||||
setBlocked(true);
|
if (a.instances > 0) {
|
||||||
} else {
|
setBlocked(true);
|
||||||
setStatus('disabled');
|
} else {
|
||||||
setBlocked(false);
|
setStatus('disabled');
|
||||||
}
|
setBlocked(false);
|
||||||
},
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
label: '激活活动',
|
||||||
|
ico: 'zap',
|
||||||
|
primary: true,
|
||||||
|
onClick: () =>
|
||||||
|
setAsk({
|
||||||
|
title: '确认激活这个活动?',
|
||||||
|
okLabel: '确认激活',
|
||||||
|
onOk: () => {
|
||||||
|
setStatus('active');
|
||||||
|
setBlocked(false);
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}
|
{ label: '启动活动', ico: 'plus', disabled: true },
|
||||||
: {
|
/* 版本迭代入口先隐藏(2026-08-25 用户定):功能设计回头讨论,要做时把这行加回 acts
|
||||||
label: '激活活动',
|
{ label: '版本迭代', ico: 'git-branch', disabled: true },
|
||||||
ico: 'zap',
|
*/
|
||||||
primary: true,
|
]}
|
||||||
ask: {
|
/>
|
||||||
title: '确认激活这个活动?',
|
{ask !== null && <PopAsk spec={ask} onCancel={() => setAsk(null)} />}
|
||||||
note: '激活后可以为它创建实例。',
|
</div>
|
||||||
impact: { who: '该活动及其后续实例创建', undo: '可逆 —— 随时可以再次禁用', when: '确认后立即生效(本期前端演示,接口对接后落库)' },
|
|
||||||
okLabel: '确认激活',
|
|
||||||
onOk: () => {
|
|
||||||
setStatus('active');
|
|
||||||
setBlocked(false);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ label: '启动活动', ico: 'plus', disabled: true },
|
|
||||||
/* 版本迭代入口先隐藏(2026-08-25 用户定):功能设计回头讨论,要做时把这行加回 acts
|
|
||||||
{ label: '版本迭代', ico: 'git-branch', disabled: true },
|
|
||||||
*/
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{blocked && (
|
{blocked && (
|
||||||
<Note tone="warn">
|
<Note tone="warn">
|
||||||
无法禁用:该活动还有 {a.instances} 个实例 —— 这条提示将来由接口返回(有实例时接口拒绝禁用),本期为前端占位演示。
|
无法禁用:该活动还有 {a.instances} 个实例 —— 这条提示将来由接口返回(有实例时接口拒绝禁用),本期为前端占位演示。
|
||||||
</Note>
|
</Note>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Sec ico="repeat" title="历史活动" extra={`共 ${instances.length} 个 · 点行进 L3(即将开放)`}>
|
<Sec ico="repeat" title="历史活动" extra={`共 ${instances.length} 个 · 查看详情进 L3`}>
|
||||||
{/* 统计带 + 检索条同一行(2026-08-26 用户定:检索放右侧)——
|
{/* 统计带 + 检索条同一行(2026-08-26 用户定:检索放右侧)——
|
||||||
左:总数 + 五态灰字合计(状态色只进 6px 小圆点);右:条件下拉 + 随条件换控件 */}
|
左:总数 + 五态灰字合计(状态色只进 6px 小圆点);右:条件下拉 + 随条件换控件 */}
|
||||||
<div className="g-ihead">
|
<div className="g-ihead">
|
||||||
@@ -568,7 +602,8 @@ export function ActvPage({ id }: { id: string }) {
|
|||||||
终止活动
|
终止活动
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button type="button" className="g-rowbtn" title="L3 实例详情即将开放">
|
{/* L3 实例详情(2026-08-26 接):路由 id 带活动与实例两段,详情页按活动找回上下文 */}
|
||||||
|
<button type="button" className="g-rowbtn" onClick={() => nav.push({ k: 'acti', id: `${a.id}:${r.id}` })}>
|
||||||
查看详情
|
查看详情
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
@@ -635,6 +670,248 @@ export function ActvPage({ id }: { id: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==================== 实例详情(L3,2026-08-26) ==================== */
|
||||||
|
|
||||||
|
/** 动作过程的一步(占位形状,按将来接口铺):步骤 / 执行方 / 时间 / 用时 / 结果 / 一句说明 */
|
||||||
|
type DemoStep = {
|
||||||
|
name: string;
|
||||||
|
by: 'agent' | 'human';
|
||||||
|
/** `MM-DD HH:mm`,未到达的步骤为空串 */
|
||||||
|
at: string;
|
||||||
|
/** 完成步的用时,如 `6m`;未完成为空串 */
|
||||||
|
dur: string;
|
||||||
|
note: string;
|
||||||
|
res: 'done' | 'fail' | 'current' | 'pending';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 活动一次运行的节点流(占位)—— agent 跑规划与执行,验收这一步永远留给人
|
||||||
|
* (2026-08-25 用户定的人机分工:AI 不能承担责任,负责人验收规划与统计产出)。
|
||||||
|
*/
|
||||||
|
const STEP_FLOW: ReadonlyArray<{ name: string; by: 'agent' | 'human'; dur: string; note: string }> = [
|
||||||
|
{ name: '输入接收', by: 'agent', dur: '1m', note: '询价单已进入,要素识别完成,实例正式起步。' },
|
||||||
|
{ name: '要素结构化', by: 'agent', dur: '6m', note: '抽取客户 / 物料 / 交期等字段,落入结构化模型。' },
|
||||||
|
{ name: '定价测算', by: 'agent', dur: '12m', note: '按成本模型逐项给出建议价与毛利测算。' },
|
||||||
|
{ name: '负责人验收', by: 'human', dur: '5m', note: '确认测算结果 —— 规划类产出必须由人验收担责。' },
|
||||||
|
{ name: '输出归档', by: 'agent', dur: '1m', note: '生成报价文件并归档,产出回写台账。' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** 各步相对启动时间的偏移(分钟,占位)—— 真数据随接口时间戳一起换 */
|
||||||
|
const STEP_OFFSET = [0, 3, 9, 21, 26] as const;
|
||||||
|
|
||||||
|
/** 占位时间 `MM-DD HH:mm`(2026 年)的加分钟格式化 —— 与 instanceTime 配套 */
|
||||||
|
function fmtMinute(t: number): string {
|
||||||
|
const d = new Date(t);
|
||||||
|
const p = (n: number): string => String(n).padStart(2, '0');
|
||||||
|
return `${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 步工厂:以启动时间为零点铺某一步(占位)—— note 可覆盖默认说明(如「被手动终止」) */
|
||||||
|
function mkStep(t0: number, i: number, res: DemoStep['res'], note?: string): DemoStep {
|
||||||
|
return {
|
||||||
|
...STEP_FLOW[i]!,
|
||||||
|
...(note === undefined ? {} : { note }),
|
||||||
|
res,
|
||||||
|
at: res === 'pending' ? '' : fmtMinute(t0 + STEP_OFFSET[i]! * 60000),
|
||||||
|
dur: res === 'done' ? STEP_FLOW[i]!.dur : '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按实例状态铺五步的结果(占位演示):进行中走到测算,驳回倒在验收,失败倒在结构化,待执行一步未走 */
|
||||||
|
function demoSteps(state: InstanceState, startedAt: string): readonly DemoStep[] {
|
||||||
|
const t0 = instanceTime(startedAt);
|
||||||
|
const mk = (i: number, res: DemoStep['res']): DemoStep => mkStep(t0, i, res);
|
||||||
|
switch (state) {
|
||||||
|
case '已完成':
|
||||||
|
return [mk(0, 'done'), mk(1, 'done'), mk(2, 'done'), mk(3, 'done'), mk(4, 'done')];
|
||||||
|
case '进行中':
|
||||||
|
return [mk(0, 'done'), mk(1, 'done'), mk(2, 'current'), mk(3, 'pending'), mk(4, 'pending')];
|
||||||
|
case '待执行':
|
||||||
|
return [mk(0, 'pending'), mk(1, 'pending'), mk(2, 'pending'), mk(3, 'pending'), mk(4, 'pending')];
|
||||||
|
case '驳回':
|
||||||
|
return [mk(0, 'done'), mk(1, 'done'), mk(2, 'done'), mk(3, 'fail'), mk(4, 'pending')];
|
||||||
|
case '失败':
|
||||||
|
return [mk(0, 'done'), mk(1, 'fail'), mk(2, 'pending'), mk(3, 'pending'), mk(4, 'pending')];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手动终止的过程面(占位):终止发生在测算步 —— 走完的留痕,当前步记「已终止」,后面不再走 */
|
||||||
|
function terminatedSteps(startedAt: string): readonly DemoStep[] {
|
||||||
|
const t0 = instanceTime(startedAt);
|
||||||
|
return [
|
||||||
|
mkStep(t0, 0, 'done'),
|
||||||
|
mkStep(t0, 1, 'done'),
|
||||||
|
mkStep(t0, 2, 'fail', '定价测算进行中,被负责人手动终止 —— 已算出的中间结果留痕保留。'),
|
||||||
|
mkStep(t0, 3, 'pending'),
|
||||||
|
mkStep(t0, 4, 'pending'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 步结果的色调类 —— 复用全局 t-*(色即状态,色盲可读:结果词在点旁边) */
|
||||||
|
const STEP_RES_TONE: Readonly<Record<DemoStep['res'], string>> = {
|
||||||
|
done: 't-ok',
|
||||||
|
fail: 't-danger',
|
||||||
|
current: 't-brand',
|
||||||
|
pending: 't-mute',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* L3 · 实例详情 —— 一次运行的完整视图(2026-08-26 规划,用户确认后落地):
|
||||||
|
* 主从 2:1(DESIGN-SPEC §9)—— 左「动作过程记录」是主角(追溯与验收的落点),
|
||||||
|
* 右栏输入/输出是辅助。实例不可删除、不可编辑(2026-08-26 用户定)——
|
||||||
|
* 编辑/删除入口撤掉,唯一的动作是进行中的「终止活动」。
|
||||||
|
* 数据为静态占位,结构按将来接口的形状铺。
|
||||||
|
*/
|
||||||
|
export function ActiPage({ id }: { id: string }) {
|
||||||
|
const [aid, iid] = id.split(':');
|
||||||
|
const a = DEMO_ACTIVITIES.find((x) => x.id === aid);
|
||||||
|
const inst = a !== undefined ? demoInstances(a.owner).find((x) => x.id === iid) : undefined;
|
||||||
|
/* 状态就地翻转(本期前端演示):终止暂落「失败」(归态接口语义未定),对接后走 mutation 由服务端重投影;
|
||||||
|
term 记下终止时刻 —— 终止的过程面与「自己跑挂」不同(当前步记「已终止」,不是「附件缺失」那种失败原因) */
|
||||||
|
const [term, setTerm] = useState<string | undefined>(undefined);
|
||||||
|
/* 轻量二次确认(2026-08-26 用户定):一句话 + 确认/取消的气泡 */
|
||||||
|
const [ask, setAsk] = useState<PopAskSpec | null>(null);
|
||||||
|
if (a === undefined || inst === undefined) {
|
||||||
|
return <Note tone="warn">实例不存在 —— 它不在该活动的清单里,从面包屑返回活动详情看看。</Note>;
|
||||||
|
}
|
||||||
|
const state: InstanceState = term !== undefined ? '失败' : inst.state;
|
||||||
|
const steps = term !== undefined ? terminatedSteps(inst.startedAt) : demoSteps(state, inst.startedAt);
|
||||||
|
const doneN = steps.filter((s) => s.res === 'done').length;
|
||||||
|
/* 结束时间/耗时:占位按「第五步走完」推(27m),真数据随接口一起换 */
|
||||||
|
const endedAt = fmtMinute(instanceTime(inst.startedAt) + 27 * 60000);
|
||||||
|
const reason =
|
||||||
|
term !== undefined
|
||||||
|
? `已于 ${term} 由 ${inst.owner} 手动终止 —— 终止后归入哪个状态接口语义未定,本期前端落「失败」占位。`
|
||||||
|
: state === '驳回'
|
||||||
|
? `验收驳回:测算毛利低于阈值,退回重新测算 —— 负责人 ${inst.owner}。`
|
||||||
|
: state === '失败'
|
||||||
|
? '要素结构化失败:询价单附件缺失,已通知发起人补齐后重试。'
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'g-actid' + (a.hue === 'blue' ? '' : ` h-${a.hue}`)}>
|
||||||
|
<div className="g-pahead">
|
||||||
|
<PageHead
|
||||||
|
ico={a.ico}
|
||||||
|
tone={INSTANCE_ST[state]}
|
||||||
|
kicker={`${a.name} · 一次运行`}
|
||||||
|
title={inst.id}
|
||||||
|
chips={
|
||||||
|
<>
|
||||||
|
{/* key 带状态:终止后状态丸翻转时弹出一次高亮 —— 变化要看得见 */}
|
||||||
|
<Chip key={state} tone={INSTANCE_ST[state]} ico={state === '进行中' ? 'play' : state === '已完成' ? 'circle-check' : state === '待执行' ? 'hourglass' : state === '驳回' ? 'circle-x' : 'power'}>
|
||||||
|
{state}
|
||||||
|
</Chip>
|
||||||
|
<Chip ico="user">{inst.owner}</Chip>
|
||||||
|
<Chip ico="calendar-days">启动 {inst.startedAt}</Chip>
|
||||||
|
{state === '已完成' && <Chip ico="flag">结束 {endedAt} · 耗时 27m</Chip>}
|
||||||
|
{state === '进行中' && <Chip ico="timer">已跑 12m</Chip>}
|
||||||
|
<Chip ico="cpu">{a.llm}</Chip>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
desc="一次运行的完整视图:输入 / 过程 / 输出 / 状态 —— 实例不可编辑、不可删除,进行中的运行可终止。"
|
||||||
|
acts={[
|
||||||
|
/* 唯一动作(2026-08-26 用户定:实例不许删、不许编):进行中可终止,轻气泡二次确认 */
|
||||||
|
...(state === '进行中'
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
label: '终止活动',
|
||||||
|
ico: 'power',
|
||||||
|
danger: true,
|
||||||
|
onClick: () =>
|
||||||
|
setAsk({
|
||||||
|
title: `确认终止实例 ${inst.id}?`,
|
||||||
|
okLabel: '确认终止',
|
||||||
|
danger: true,
|
||||||
|
onOk: () => setTerm(fmtMinute(Date.now())),
|
||||||
|
}),
|
||||||
|
} satisfies ActSpec,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{ask !== null && <PopAsk spec={ask} onCancel={() => setAsk(null)} />}
|
||||||
|
</div>
|
||||||
|
{reason !== undefined && <Note tone="warn">{reason}</Note>}
|
||||||
|
{state === '待执行' && <Note>待执行:已排入队列,等待调度 —— 到点或由负责人手动起步。</Note>}
|
||||||
|
|
||||||
|
<div className="g-actid-grid">
|
||||||
|
{/* 主角:动作过程记录 —— 时间线竖排,当前步呼吸高亮,未到达灰显(2026-08-26 用户定:过程是实例页的重点) */}
|
||||||
|
<Sec ico="list-checks" title="动作过程记录" extra={`${doneN} / ${steps.length} 步`}>
|
||||||
|
<ol className="g-tl">
|
||||||
|
{steps.map((s, i) => (
|
||||||
|
<li key={s.name} className={`g-tl-row ${s.res} ${STEP_RES_TONE[s.res]}`}>
|
||||||
|
<span className="g-tl-rail" aria-hidden>
|
||||||
|
<i className="g-tl-dot" />
|
||||||
|
{i < steps.length - 1 && <i className="g-tl-line" />}
|
||||||
|
</span>
|
||||||
|
<div className="g-tl-c">
|
||||||
|
<div className="g-tl-h">
|
||||||
|
<b>
|
||||||
|
{i + 1} · {s.name}
|
||||||
|
</b>
|
||||||
|
<span className="g-tl-by">
|
||||||
|
<Ico name={s.by === 'agent' ? 'cpu' : 'user'} s={11} />
|
||||||
|
{s.by === 'agent' ? a.llm : `负责人 · ${inst.owner}`}
|
||||||
|
</span>
|
||||||
|
{/* 时间 meta 只摆事实(什么时候 / 用了多久),结果交给右侧结果词 —— 不重复说两遍 */}
|
||||||
|
<span className="g-tl-meta">
|
||||||
|
{s.res === 'pending' ? '' : s.res === 'current' ? `${s.at} 起` : `${s.at} · 用时 ${s.dur}`}
|
||||||
|
</span>
|
||||||
|
<span className="g-tl-res">
|
||||||
|
{s.res === 'done' ? '完成' : s.res === 'fail' ? (term !== undefined ? '已终止' : state === '驳回' ? '驳回' : '失败') : s.res === 'current' ? '进行中' : '未到达'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="g-tl-note">{s.note}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</Sec>
|
||||||
|
|
||||||
|
{/* 辅栏:输入 / 输出 —— 完整输入与最终产出(2026-08-26 规划);文件下载随接口开放(票据机制已备) */}
|
||||||
|
<div className="g-actid-rail">
|
||||||
|
<Sec ico="inbox" title="输入">
|
||||||
|
<div className="g-kv">
|
||||||
|
<div className="g-kv-row">
|
||||||
|
<em>内容</em>
|
||||||
|
<b>{inst.input}</b>
|
||||||
|
</div>
|
||||||
|
<div className="g-kv-row">
|
||||||
|
<em>来源</em>
|
||||||
|
<b>邮件 · quotes@bpos.demo</b>
|
||||||
|
</div>
|
||||||
|
<div className="g-kv-row">
|
||||||
|
<em>提交</em>
|
||||||
|
<b className="g-mono">{inst.startedAt}</b>
|
||||||
|
</div>
|
||||||
|
<div className="g-kv-note">文件类输入的原文下载随接口开放(本期占位)。</div>
|
||||||
|
</div>
|
||||||
|
</Sec>
|
||||||
|
<Sec ico="file-text" title="输出">
|
||||||
|
{inst.output === '—' ? (
|
||||||
|
<div className="g-file-empty">暂无产出 —— 走到「输出归档」后在这里出现。</div>
|
||||||
|
) : (
|
||||||
|
<div className="g-file">
|
||||||
|
<span className="g-file-i">
|
||||||
|
<Ico name="file-text" s={15} />
|
||||||
|
</span>
|
||||||
|
<span className="g-file-n">
|
||||||
|
<b>{inst.output}</b>
|
||||||
|
<em>报价文件 · 输出归档步生成</em>
|
||||||
|
</span>
|
||||||
|
<button type="button" className="g-rowbtn" disabled title="随接口开放下载(票据机制已备)">
|
||||||
|
<Ico name="download" s={12} /> 下载
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Sec>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- ② 员工管理 ---------------- */
|
/* ---------------- ② 员工管理 ---------------- */
|
||||||
|
|
||||||
/** 治理写动作统一入口:成功即失效相关查询,画面由服务端重投影 */
|
/** 治理写动作统一入口:成功即失效相关查询,画面由服务端重投影 */
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ export type Route =
|
|||||||
/* 活动定义详情(运营中心 · 活动模块 L2,2026-08-25 重构):id = 活动 id;
|
/* 活动定义详情(运营中心 · 活动模块 L2,2026-08-25 重构):id = 活动 id;
|
||||||
与设计中心的 `act`(活动库)是两回事 —— 那是技术安装面,这是业务运营面 */
|
与设计中心的 `act`(活动库)是两回事 —— 那是技术安装面,这是业务运营面 */
|
||||||
| { k: 'actd'; id: string }
|
| { k: 'actd'; id: string }
|
||||||
|
/* 实例详情(活动模块 L3,2026-08-26):id = `<活动 id>:<实例号>` ——
|
||||||
|
实例没有独立接口,详情页按活动 id 找回它的定义与清单,再取这一条实例 */
|
||||||
|
| { k: 'acti'; id: string }
|
||||||
| { k: 'llm'; id: string }
|
| { k: 'llm'; id: string }
|
||||||
| { k: 'chan'; id: string };
|
| { k: 'chan'; id: string };
|
||||||
|
|
||||||
@@ -243,6 +246,8 @@ export function routeLabel(r: Route): string {
|
|||||||
return '记录详情';
|
return '记录详情';
|
||||||
case 'actd':
|
case 'actd':
|
||||||
return '活动详情';
|
return '活动详情';
|
||||||
|
case 'acti':
|
||||||
|
return '实例详情';
|
||||||
case 'llm':
|
case 'llm':
|
||||||
return 'LLM 实例';
|
return 'LLM 实例';
|
||||||
case 'chan':
|
case 'chan':
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ L1(模块首页 · 活动清单)需要一个**标签页**切换,因为活
|
|||||||
- 2026-08-25 · **文案口径(用户定)**:创建实例 → **启动活动**;实例清单 → **历史活动**;停止 → **终止活动**。
|
- 2026-08-25 · **文案口径(用户定)**:创建实例 → **启动活动**;实例清单 → **历史活动**;停止 → **终止活动**。
|
||||||
- 2026-08-26 · **L2 历史活动改列表(用户定)**:卡片式清单撤掉,改表格列(实例 / 负责人 / 输入 / 输出 / 启动时间 / 状态 / 操作,操作栏带标题),隔行浅斑马 + 悬停亮行,行内操作(终止活动 / 查看详情)常驻;分页单页 8 条不变。
|
- 2026-08-26 · **L2 历史活动改列表(用户定)**:卡片式清单撤掉,改表格列(实例 / 负责人 / 输入 / 输出 / 启动时间 / 状态 / 操作,操作栏带标题),隔行浅斑马 + 悬停亮行,行内操作(终止活动 / 查看详情)常驻;分页单页 8 条不变。
|
||||||
- 2026-08-26 · **L2 历史活动加检索(用户定)**:列表上方检索条 —— 条件下拉四选(实例 / 负责人 / 状态 / 启动时间),控件随条件换:实例=按实例号模糊输入、负责人=人员下拉(候选取自现有实例,接接口后换人员接口)、状态=五态下拉、启动时间=日期段(含两端整日);筛中显示「筛出 X / 共 Y」并可一键清空;分页按筛出的集合走。
|
- 2026-08-26 · **L2 历史活动加检索(用户定)**:列表上方检索条 —— 条件下拉四选(实例 / 负责人 / 状态 / 启动时间),控件随条件换:实例=按实例号模糊输入、负责人=人员下拉(候选取自现有实例,接接口后换人员接口)、状态=五态下拉、启动时间=日期段(含两端整日);筛中显示「筛出 X / 共 Y」并可一键清空;分页按筛出的集合走。
|
||||||
|
- 2026-08-26 · **L3 实例详情已落地(静态占位版,用户确认规划后实施)**:路由 `acti`(id = `活动id:实例号`),L2 行内「查看详情」接入。布局 = 主从 2:1(DESIGN-SPEC §9):左侧「动作过程记录」竖时间线是主角(五步节点流:输入接收 → 要素结构化 → 定价测算 → **负责人验收** → 输出归档 —— 验收永远留给人,对应人机分工),当前步呼吸高亮、未到达空心灰显、连接线成不间断竖轨;右栏输入(键值)/ 输出(文件行,下载随接口开放)。头部 = 状态丸 + 负责人/启动/结束·耗时/专属 LLM;驳回/失败出原因说明块;终止后的过程面与「自己跑挂」区分:当前步记「已终止」而非失败原因。数据仍是静态假数据。
|
||||||
|
- 2026-08-26 · **实例不可删除、不可编辑(用户定,推翻 L3 原规划)**:L3 的编辑/删除入口撤掉,唯一动作是进行中的「终止活动」(二次确认);六功能里实例的「编辑/删除」两项就此划掉,待确认问题里的两条(编辑指什么 / 删除是否放开)同时关闭。
|
||||||
|
- 2026-08-26 · **二次确认的规格(用户定)**:活动模块的确认走**轻气泡**(锚在触发钮下方的小泡泡,只问一句 + 确认/取消,点罩即取消)—— 不摆影响三必答;三必答(AskBand)是治理台重度动作的规格,别处的确认带暂不动。
|
||||||
|
|
||||||
## 三、功能表(整理稿)
|
## 三、功能表(整理稿)
|
||||||
|
|
||||||
@@ -99,6 +102,6 @@ L1(模块首页 · 活动清单)需要一个**标签页**切换,因为活
|
|||||||
- ~~活动模块里的「设计」与设计中心「活动库」是什么关系?~~ **已答(2026-08-25)**:不再有「设计中心 vs 运营中心」的分工 —— 设计并入运营,活动模块自带设计能力;设计中心各区后续按同思路并入运营中心重做。
|
- ~~活动模块里的「设计」与设计中心「活动库」是什么关系?~~ **已答(2026-08-25)**:不再有「设计中心 vs 运营中心」的分工 —— 设计并入运营,活动模块自带设计能力;设计中心各区后续按同思路并入运营中心重做。
|
||||||
- ~~「设计」在活动模块里做到什么程度?~~ **已答(2026-08-25)**:六功能已界定 —— 活动(定义)的创建与展示 + 实例的创建/展示/编辑/删除;定义只许创建不许编删,创建要过审核。
|
- ~~「设计」在活动模块里做到什么程度?~~ **已答(2026-08-25)**:六功能已界定 —— 活动(定义)的创建与展示 + 实例的创建/展示/编辑/删除;定义只许创建不许编删,创建要过审核。
|
||||||
- 活动创建的**审核**:谁来审、在模块内审还是别处、审什么(输入输出契约的合理性?)?(待用户展开)
|
- 活动创建的**审核**:谁来审、在模块内审还是别处、审什么(输入输出契约的合理性?)?(待用户展开)
|
||||||
- 实例的**编辑**具体指编辑什么:实例的输入数据?进行中的干预(暂停/叫停)?还是产出修正?(待用户展开)
|
- 实例的**编辑**~~具体指编辑什么~~:**已答(2026-08-26):实例不可编辑**,入口不做。
|
||||||
- 实例删除是否真放开 —— 用户已标注存疑,先规划,后续按需求调。(悬着,不阻塞)
|
- 实例删除~~是否真放开~~:**已答(2026-08-26):实例不可删除**,入口不做;六功能只剩实例的创建/展示。
|
||||||
- 「运营」侧还要不要监控/验收类功能(此前提过负责人验收规划与统计产出)—— 六功能里未直接出现,是否归在「实例的展示/编辑」里,还是另有入口?(待用户展开)
|
- 「运营」侧还要不要监控/验收类功能(此前提过负责人验收规划与统计产出)—— 六功能里未直接出现,是否归在「实例的展示/编辑」里,还是另有入口?(待用户展开)
|
||||||
|
|||||||
Reference in New Issue
Block a user