forked from ChaiHao/BPOS-MVP
基线:鉴权会话+工作组+工作台三区修复+编排工作区+产品设计文档
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -192,6 +192,19 @@ export const listProcessPackagesRoute = {
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 画图即校验 —— 与前端画布同源的闭合校验 */
|
||||
/**
|
||||
* 保存流程包(编排工作区的落笔)。
|
||||
* **保存 ≠ 发布**:落的是这一版的定义(草稿可反复存);已发布版本不可回改 ——
|
||||
* 后端按 (id, version, state) 判:published 版拒存,提示另起新版本。
|
||||
* 返回连同闭合报告:画图即校验,每次落笔都知道离「可发布」还差几条。
|
||||
*/
|
||||
export const saveProcessRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/process',
|
||||
body: z.object({ pkg: processPackageSchema }),
|
||||
output: z.object({ pkg: processPackageSchema, report: closureReportSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const validateProcessRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/process/validate',
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
import type { z } from 'zod';
|
||||
|
||||
export const HTTP_METHODS = ['GET', 'POST', 'PATCH', 'DELETE'] as const;
|
||||
export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
|
||||
export type HttpMethod = (typeof HTTP_METHODS)[number];
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,16 +5,35 @@
|
||||
import { z } from 'zod';
|
||||
import type { RouteContract } from './http.ts';
|
||||
import { sessionSchema } from '../org.ts';
|
||||
import { chatMessageSchema, deliverableSchema, formFieldSchema, stageSchema, viewBlockSchema, workOrderSchema } from '../card.ts';
|
||||
import { chatMessageSchema, deliverableSchema, fileDocSchema, formFieldSchema, stageSchema, viewBlockSchema, workOrderSchema } from '../card.ts';
|
||||
import { attributionSideSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts';
|
||||
import { staffIdSchema, taskIdSchema } from '../id.ts';
|
||||
|
||||
/** 我是谁 —— 无会话即 401,前端据此落登录页(ADR-010) */
|
||||
export const sessionRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/session',
|
||||
output: sessionSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 登录 —— 单组织 cookie session(编码规范 §4 / ADR-010)。
|
||||
* 成功即种 `jcai_session`(HttpOnly);失败只回一句话,不区分账号不存在还是口令错。
|
||||
*/
|
||||
export const loginRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/login',
|
||||
body: z.object({ account: z.string().min(1), password: z.string().min(1) }),
|
||||
output: sessionSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 登出 —— 服务端会话立刻作废,不等 cookie 到期 */
|
||||
export const logoutRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/logout',
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const healthRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/health',
|
||||
@@ -49,10 +68,29 @@ export const listTasksRoute = {
|
||||
output: z.object({ tasks: z.array(taskCardSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 流程链上的一环 —— 「一条 = 一个活动的一次运行」之外,人还要看见自己在整条流程的哪儿。
|
||||
* 别人负责的节点也在链上(只读),但不进任务清单。
|
||||
*/
|
||||
export const chainNodeSchema = z.object({
|
||||
nodeId: z.string(),
|
||||
label: z.string(),
|
||||
kind: z.enum(['activity', 'approval']),
|
||||
/** 谁做:姓名 · 岗位;岗位未落到具体人时只给岗位名 */
|
||||
ownerName: z.string(),
|
||||
ownerRole: z.string(),
|
||||
state: z.enum(['done', 'current', 'todo']),
|
||||
});
|
||||
export type ChainNode = z.infer<typeof chainNodeSchema>;
|
||||
|
||||
/** 任务详情 —— 工作区与对话区的服务端投影(前端零过程状态,刷新即重建) */
|
||||
export const taskDetailSchema = z.object({
|
||||
card: taskCardSchema,
|
||||
goal: z.string(),
|
||||
/** 本单负责人 —— 标题区落款与报告署名 */
|
||||
owner: z.object({ name: z.string(), role: z.string() }),
|
||||
/** 整条流程的节点链(含他人节点,只读) */
|
||||
chain: z.array(chainNodeSchema),
|
||||
budget: z.object({
|
||||
steps: z.string(),
|
||||
tokens: z.string(),
|
||||
@@ -61,6 +99,18 @@ export const taskDetailSchema = z.object({
|
||||
}),
|
||||
stages: z.array(stageSchema),
|
||||
deliverables: z.array(deliverableSchema),
|
||||
/**
|
||||
* 工作成果 = 一组**文件**,大部分是 HTML 汇报报告。
|
||||
* 一行一份、点进去看;文件本体随投影一并下发,前端不另取。
|
||||
*/
|
||||
results: z.array(
|
||||
z.object({
|
||||
label: z.string(),
|
||||
desc: z.string(),
|
||||
at: z.string().optional(),
|
||||
file: fileDocSchema,
|
||||
}),
|
||||
),
|
||||
workOrders: z.array(workOrderSchema),
|
||||
chat: z.array(chatMessageSchema),
|
||||
view: z.array(viewBlockSchema),
|
||||
@@ -94,6 +144,17 @@ export const replyWorkOrderRoute = {
|
||||
output: z.object({ ok: z.boolean(), producesCompletion: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 对话卡回传(决策 / 补全 / 评价)—— 与后端回传校验**同 schema**。
|
||||
* 对话流是一切回环输入的唯一入口(OS 4.1):卡就长在对话流上,答也就地答。
|
||||
*/
|
||||
export const replyChatCardRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/chat-cards/reply',
|
||||
body: z.object({ reply: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 负责人的自由输入 —— 人事件的第二形态 */
|
||||
export const sayRoute = {
|
||||
method: 'POST',
|
||||
@@ -319,3 +380,41 @@ export const decideApprovalRoute = {
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export type ListApprovalsForMeOutput = z.infer<typeof listApprovalsForMeRoute.output>;
|
||||
|
||||
/* ── 登记触发:人工起单的统一形态(OS 3.1) ─────────────────────────── */
|
||||
|
||||
/**
|
||||
* 「登记触发 —— 人工起单的统一形态:登记人选择流程包、按调用接口 schema 填写输入
|
||||
* (表单由 schema 自动渲染)、提交即起单。」**登记人即来源**。
|
||||
*/
|
||||
export const listRegistrationTriggersRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/triggers/registration',
|
||||
output: z.object({
|
||||
triggers: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
processPackageId: z.string(),
|
||||
enabled: z.boolean(),
|
||||
/** 调用接口的入参 schema —— 登记表单据此渲染 */
|
||||
inputSchema: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const fireRegistrationRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/triggers/:triggerId/register',
|
||||
params: z.object({ triggerId: z.string() }),
|
||||
body: z.object({ data: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({
|
||||
ok: z.boolean(),
|
||||
workflowId: z.string().optional(),
|
||||
skipped: z.string().optional(),
|
||||
missing: z.array(z.string()).optional(),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export type ListRegistrationTriggersOutput = z.infer<typeof listRegistrationTriggersRoute.output>;
|
||||
|
||||
@@ -73,6 +73,11 @@ export type FormField = z.infer<typeof formFieldSchema>;
|
||||
/* ── 视图块:视图区的结构化呈现(活动推送,只读) ────────────────────── */
|
||||
|
||||
const fileDocSchema = z.discriminatedUnion('kind', [
|
||||
/**
|
||||
* HTML 汇报报告 —— 成果文件的**主要形态**:活动按当下局面组织的一屏。
|
||||
* 渲染一律经 `@ui` 的 `ViewContainer` 沙箱,无脚本执行权(AM 07)。
|
||||
*/
|
||||
z.object({ kind: z.literal('html'), name: z.string(), html: z.string() }),
|
||||
z.object({ kind: z.literal('markdown'), name: z.string(), md: z.string() }),
|
||||
z.object({
|
||||
kind: z.literal('excel'),
|
||||
@@ -243,11 +248,31 @@ export const WORK_ORDER_MODE_LABEL: Readonly<Record<WorkOrderMode, string>> = {
|
||||
acceptance: '验收',
|
||||
};
|
||||
|
||||
/**
|
||||
* 办结口径:执行件只有「已提交」,验收件是放行三选一 —— 与回传协议同一套标识符。
|
||||
* 界面文案由字典给出(与原型逐字一致),不在组件里散落中文。
|
||||
*/
|
||||
export const WORK_ORDER_RESULTS = ['submitted', 'pass', 'redo', 'revise'] as const;
|
||||
export const workOrderResultSchema = z.enum(WORK_ORDER_RESULTS);
|
||||
export type WorkOrderResult = z.infer<typeof workOrderResultSchema>;
|
||||
|
||||
export const WORK_ORDER_RESULT_LABEL: Readonly<Record<WorkOrderResult, string>> = {
|
||||
submitted: '已提交',
|
||||
pass: '验收通过',
|
||||
redo: '驳回重做',
|
||||
revise: '驳回迭代',
|
||||
};
|
||||
|
||||
/** 驳回两式 —— 退回即重来,判定处只此一处 */
|
||||
export function isRejection(result: WorkOrderResult): boolean {
|
||||
return result === 'redo' || result === 'revise';
|
||||
}
|
||||
|
||||
/** 完成回传:谁、何时、答了什么(回传即留痕) */
|
||||
export const workDoneSchema = z.object({
|
||||
at: z.iso.datetime(),
|
||||
by: staffIdSchema,
|
||||
result: z.string(),
|
||||
result: workOrderResultSchema,
|
||||
values: z.record(z.string(), z.string()).optional(),
|
||||
});
|
||||
export type WorkDone = z.infer<typeof workDoneSchema>;
|
||||
|
||||
@@ -107,3 +107,25 @@ export const sessionSchema = z.object({
|
||||
grants: z.array(grantSchema),
|
||||
});
|
||||
export type Session = z.infer<typeof sessionSchema>;
|
||||
|
||||
/* ── 工作组(ADR-011) ────────────────────────────────────────────────── */
|
||||
|
||||
/**
|
||||
* 工作组 —— 一组「岗位 → 员工」的绑定,起单时装载、随工作流走。
|
||||
*
|
||||
* **不是第五种负责人策略**(OS 03「策略封闭四选」不动):
|
||||
* 四策略回答的是「这个岗位该派给谁」,工作组直接给出了答案,策略于是无从发问。
|
||||
* 键是**岗位 code**(BD / PM / FAE …),与流程包 `owner.position` 同一口径。
|
||||
*/
|
||||
export const workGroupSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
/** 组归谁 —— 起单时按来源人取其组 */
|
||||
ownerId: staffIdSchema,
|
||||
members: z.record(z.string().min(1), staffIdSchema),
|
||||
});
|
||||
export type WorkGroup = z.infer<typeof workGroupSchema>;
|
||||
|
||||
/** 装进工作流的**快照**:改组不改已起的单(留痕不被改写) */
|
||||
export const workGroupBindingSchema = z.record(z.string().min(1), staffIdSchema);
|
||||
export type WorkGroupBinding = z.infer<typeof workGroupBindingSchema>;
|
||||
|
||||
@@ -71,6 +71,8 @@ export const edgeSchema = z.object({
|
||||
mapping: z.array(fieldMappingSchema).default([]),
|
||||
/** 边标签:画布上的出口名(如「TYPE-A」「通过」) */
|
||||
label: z.string().optional(),
|
||||
/** 边标签色调(呈现):ok = 绿(通过类) / bad = 红(驳回类) */
|
||||
tone: z.enum(['ok', 'bad']).optional(),
|
||||
/** 回边标记:人裁决后重开新任务实例(同节点、新任务 ID、留痕关联) */
|
||||
reopen: z.boolean().default(false),
|
||||
});
|
||||
@@ -117,8 +119,20 @@ export const GATEWAY_KIND_LABEL: Readonly<Record<GatewayKind, string>> = {
|
||||
const nodeBase = {
|
||||
id: nodeIdSchema,
|
||||
label: z.string().min(1),
|
||||
/** 画布坐标 —— 布局引擎的输入;不影响语义 */
|
||||
layout: z.object({ row: z.number().int(), col: z.number().int() }).optional(),
|
||||
/** 画布上的第二行小字(如「照谱执行 / 临场演绎」)—— 呈现注记,不影响语义 */
|
||||
sub: z.string().optional(),
|
||||
/**
|
||||
* 画布坐标 —— 布局引擎的输入;不影响语义。
|
||||
* row/col = 编译布局的行列;x/y = **手绘几何**(编辑器保存后,手绘成为呈现真相)。
|
||||
*/
|
||||
layout: z
|
||||
.object({
|
||||
row: z.number().int(),
|
||||
col: z.number().int(),
|
||||
x: z.number().optional(),
|
||||
y: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
};
|
||||
|
||||
export const processNodeSchema = z.discriminatedUnion('kind', [
|
||||
@@ -134,6 +148,13 @@ export const processNodeSchema = z.discriminatedUnion('kind', [
|
||||
kind: z.literal('activity'),
|
||||
/** 以声明入流:引用活动库中已安装的活动 */
|
||||
activityId: activityIdSchema,
|
||||
/**
|
||||
* 契约注记(编排定契约,AM 03):这个节点**声明**吃什么、吐什么。
|
||||
* 是「导出依赖文件」的骨架 —— 开发者对着声明在系统外开发活动;
|
||||
* 闭合校验的机检仍以边映射与活动契约面为准,注记是给人读的那一层。
|
||||
*/
|
||||
in: z.array(z.string()).optional(),
|
||||
out: z.array(z.string()).optional(),
|
||||
/** 版本约束:留空 = 装配时取最新生效版 */
|
||||
activityVersion: z.string().optional(),
|
||||
owner: ownerSpecSchema,
|
||||
@@ -160,6 +181,8 @@ export const processNodeSchema = z.discriminatedUnion('kind', [
|
||||
gateway: gatewayKindSchema,
|
||||
/** 分叉 / 汇合 */
|
||||
mode: z.enum(['fork', 'join']),
|
||||
/** 判据说明(人读):条件本体仍在出边的 S-FEEL 上,这里是「为什么这么分」 */
|
||||
basis: z.string().optional(),
|
||||
}),
|
||||
]);
|
||||
export type ProcessNode = z.infer<typeof processNodeSchema>;
|
||||
@@ -190,6 +213,12 @@ export const processPackageSchema = z.object({
|
||||
/** 流程变量声明:边映射的 workflow-var 来源 */
|
||||
variables: z.array(z.object({ name: z.string().min(1), schema: jsonSchemaSchema })).default([]),
|
||||
description: z.string().optional(),
|
||||
/**
|
||||
* 手绘图存证(可空):编辑器保存的 FlowGraph 原样落此 —— **手绘几何即呈现真相**。
|
||||
* 语义(节点/边/映射)永远以上面的 nodes/edges 为准,流转判断不读它;
|
||||
* 它只承载语义装不下的画布事实:卡点、行分组、锚定、折线点。
|
||||
*/
|
||||
diagram: z.record(z.string(), z.unknown()).optional(),
|
||||
});
|
||||
export type ProcessPackage = z.infer<typeof processPackageSchema>;
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// 共享内联 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" /></>),
|
||||
/* —— 手绘编辑器工具条 —— */
|
||||
"undo-2": (<><path d="M9 14 4 9l5-5" /><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" /></>),
|
||||
"redo-2": (<><path d="m15 14 5-5-5-5" /><path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13" /></>),
|
||||
"maximize-2": (<><polyline points="15 3 21 3 21 9" /><polyline points="9 21 3 21 3 15" /><line x1="21" x2="14" y1="3" y2="10" /><line x1="3" x2="10" y1="21" y2="14" /></>),
|
||||
x: (<><path d="M18 6 6 18" /><path d="m6 6 12 12" /></>),
|
||||
"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>
|
||||
);
|
||||
}
|
||||
+380
-116
@@ -1,161 +1,425 @@
|
||||
/**
|
||||
* 视图块渲染 —— 无业务语义的展示原语(编码规范 §2 packages/ui-kit)。
|
||||
* DOM 与类名自原型平移(`jc-*`),渲染输出保持 1:1。
|
||||
*
|
||||
* DOM 与类名逐字取自原型 `_components/ViewRegion.tsx` 的 `Block`(2026-07-26 定稿):
|
||||
* 皮肤(`proto.vendor.css` / `app.css`)是原型的原样,类名对不上就等于没样式 ——
|
||||
* 所以这里**不新造类名**,块的骨架、层级、元素种类一律照搬。
|
||||
*/
|
||||
import type { ViewBlock } from '@contracts';
|
||||
import { useState } from 'react';
|
||||
import type { FileDoc, Tone, ViewBlock } from '@contracts';
|
||||
import { Ico } from './Ico.tsx';
|
||||
|
||||
export function ViewBlocks({ blocks }: { readonly blocks: readonly ViewBlock[] }) {
|
||||
/** 语义色 → 皮肤色位(原型口径:danger 走 bad,muted 走 mute) */
|
||||
const TONE_CLASS: Readonly<Record<Tone, string>> = {
|
||||
ok: 'ok',
|
||||
warn: 'warn',
|
||||
danger: 'bad',
|
||||
brand: 'brand',
|
||||
muted: 'mute',
|
||||
};
|
||||
|
||||
export type ViewBlocksProps = {
|
||||
readonly blocks: readonly ViewBlock[];
|
||||
/** 报告级「修改」态:表 / 邮件就地进入编辑(存与弃由外层报告统一管) */
|
||||
readonly editing?: boolean | undefined;
|
||||
readonly onOpenFile?: ((file: FileDoc) => void) | undefined;
|
||||
};
|
||||
|
||||
export function ViewBlocks({ blocks, editing = false, onOpenFile }: ViewBlocksProps) {
|
||||
return (
|
||||
<>
|
||||
{blocks.map((block, i) => (
|
||||
<Block key={i} block={block} />
|
||||
<Block key={i} b={block} editing={editing} onOpenFile={onOpenFile} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Block({ block }: { block: ViewBlock }) {
|
||||
switch (block.t) {
|
||||
function Block({
|
||||
b,
|
||||
editing,
|
||||
onOpenFile,
|
||||
}: {
|
||||
b: ViewBlock;
|
||||
editing: boolean;
|
||||
onOpenFile?: ((file: FileDoc) => void) | undefined;
|
||||
}) {
|
||||
switch (b.t) {
|
||||
case 'callout':
|
||||
return <p className={'jc-callout tone-' + block.tone}>{block.text}</p>;
|
||||
return (
|
||||
<div className={'jc-callout t-' + TONE_CLASS[b.tone]}>
|
||||
<Ico name="info" s={15} />
|
||||
<p>{b.text}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
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>
|
||||
<Section title={b.title}>
|
||||
<div className="jc-kv">
|
||||
{b.items.map((it, i) => (
|
||||
<div key={i} className="jc-kv-i">
|
||||
<i>{it.k}</i>
|
||||
<b>{it.v}</b>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
</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>
|
||||
<Section title={b.title}>
|
||||
<div className="jc-badges">
|
||||
{b.items.map((it, i) => (
|
||||
<span key={i} className={'jc-badge' + (it.on ? ' on' : '')}>
|
||||
<Ico name={it.on ? 'circle-check' : 'circle-x'} s={14} />
|
||||
{it.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
return <TableBlock b={b} editing={editing} />;
|
||||
|
||||
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>
|
||||
<Section title={b.title}>
|
||||
<div className="jc-bars">
|
||||
{b.items.map((it, i) => {
|
||||
const max = b.max ?? Math.max(...b.items.map((x) => x.value));
|
||||
return (
|
||||
<div key={i} className="jc-bar-row">
|
||||
<span className="jc-bar-l">{it.label}</span>
|
||||
<div className="jc-bar-t">
|
||||
<i
|
||||
className={'t-' + TONE_CLASS[it.tone ?? 'brand']}
|
||||
style={{ width: Math.min(100, (it.value / max) * 100) + '%' }}
|
||||
/>
|
||||
{b.redline !== undefined && (
|
||||
<em className="jc-redline" style={{ left: Math.min(100, (b.redline / max) * 100) + '%' }} />
|
||||
)}
|
||||
</div>
|
||||
<span className="jc-bar-v jc-mono">{it.text ?? it.value}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</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>
|
||||
);
|
||||
return <Donut title={b.title} unit={b.unit} items={b.items} />;
|
||||
|
||||
case 'mail':
|
||||
return <MailBlock b={b} editing={editing} onOpenFile={onOpenFile} />;
|
||||
|
||||
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>
|
||||
<Section title={b.title}>
|
||||
<div className="jc-rcards">
|
||||
{b.items.map((it, i) => (
|
||||
<div key={i} className={'jc-rcard t-' + TONE_CLASS[it.tone ?? 'brand']}>
|
||||
<div className="jc-rcard-h">
|
||||
<span className="jc-rcard-tag">
|
||||
<Ico name={it.icon ?? 'circle-dot'} s={13} />
|
||||
{it.tag}
|
||||
</span>
|
||||
{it.code !== undefined && <code className="jc-mono">{it.code}</code>}
|
||||
</div>
|
||||
{it.swap === undefined ? (
|
||||
<b className="jc-rcard-t">{it.title}</b>
|
||||
) : (
|
||||
<div className="jc-swap">
|
||||
<s>{it.swap.from}</s>
|
||||
<Ico name="arrow-right" s={14} />
|
||||
<b>{it.swap.to}</b>
|
||||
{it.swap.hint !== undefined && <em>{it.swap.hint}</em>}
|
||||
</div>
|
||||
)}
|
||||
<div className="jc-rcard-big">
|
||||
<b>{it.big}</b>
|
||||
{it.bigLabel !== undefined && <span>{it.bigLabel}</span>}
|
||||
</div>
|
||||
{it.meta !== undefined && it.meta.length > 0 && (
|
||||
<div className="jc-rcard-meta">
|
||||
{it.meta.map((m, k) => (
|
||||
<span key={k}>
|
||||
<i>{m.k}</i>
|
||||
{m.v}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{it.note !== undefined && <p className="jc-rcard-note">{it.note}</p>}
|
||||
{(it.from !== undefined || it.time !== undefined) && (
|
||||
<div className="jc-rcard-sign">
|
||||
{it.from}
|
||||
{it.time !== undefined && <code className="jc-mono">{it.time}</code>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</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>
|
||||
<div className="jc-waitbar">
|
||||
<em className="jc-pulse-dot" />
|
||||
<div>
|
||||
<b>{b.label}</b>
|
||||
{b.hint !== undefined && <p>{b.hint}</p>}
|
||||
</div>
|
||||
<div className="jc-skel">
|
||||
<i />
|
||||
<i />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
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>
|
||||
<Section title={b.title}>
|
||||
<div className="jc-files">
|
||||
{b.items.map((it, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className="jc-file"
|
||||
onClick={onOpenFile === undefined ? undefined : () => onOpenFile(it.file)}
|
||||
>
|
||||
<Ico
|
||||
name={it.file.kind === 'excel' ? 'file-spreadsheet' : it.file.kind === 'pdf' ? 'file-type' : 'file-text'}
|
||||
s={18}
|
||||
/>
|
||||
<div>
|
||||
<b>{it.label}</b>
|
||||
<span>{it.desc}</span>
|
||||
</div>
|
||||
<Ico name="chevron-right" s={15} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function Section({ title, children }: { title?: string | undefined; children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="jc-sec">
|
||||
{title !== undefined && <h3>{title}</h3>}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
/** 可编辑表:报告「修改」态下就地编辑;数据源一换即复位(渲染期调整 state,不用 effect) */
|
||||
function TableBlock({ b, editing }: { b: Extract<ViewBlock, { t: 'table' }>; editing: boolean }) {
|
||||
const [rows, setRows] = useState<readonly (readonly string[])[]>(b.rows);
|
||||
const [srcRows, setSrcRows] = useState(b.rows);
|
||||
if (srcRows !== b.rows) {
|
||||
setSrcRows(b.rows);
|
||||
setRows(b.rows);
|
||||
}
|
||||
|
||||
const live = editing && b.editable === true;
|
||||
const canEditCol = (c: number) => b.editableCols === undefined || b.editableCols.includes(c);
|
||||
const donut = b.linkDonut;
|
||||
|
||||
return (
|
||||
<Section title={b.title}>
|
||||
{donut !== undefined && (
|
||||
<Donut
|
||||
title={donut.title}
|
||||
unit={donut.unit}
|
||||
live={live}
|
||||
items={donut.map.map((m) => ({
|
||||
label: m.label,
|
||||
tone: m.tone,
|
||||
value: rows.filter((r) => (r[donut.col] ?? '').includes(m.match)).length,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
<div className="jc-tbl-wrap">
|
||||
<table className="jc-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
{b.cols.map((c, i) => (
|
||||
<th key={i}>{c}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((r, ri) => (
|
||||
<tr key={ri}>
|
||||
{r.map((cell, ci) => (
|
||||
<td key={ci} className={ci === 0 ? 'jc-mono' : ''}>
|
||||
{live && canEditCol(ci) ? (
|
||||
<input
|
||||
value={cell}
|
||||
onChange={(e) =>
|
||||
setRows(rows.map((x, i) => (i === ri ? x.map((y, j) => (j === ci ? e.target.value : y)) : x)))
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
cell
|
||||
)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
/** 环图:圆心 = 合计,图例带数值;`live` 标注随编辑联动 */
|
||||
function Donut({
|
||||
title,
|
||||
unit,
|
||||
items,
|
||||
live = false,
|
||||
}: {
|
||||
title?: string | undefined;
|
||||
unit?: string | undefined;
|
||||
items: ReadonlyArray<{ label: string; value: number; tone: Tone }>;
|
||||
live?: boolean | undefined;
|
||||
}) {
|
||||
const total = items.reduce((s, x) => s + x.value, 0) || 1;
|
||||
const R = 52;
|
||||
const C = 2 * Math.PI * R;
|
||||
// 各段占比与累计偏移一次算完(纯函数式,不在渲染中改闭包变量)
|
||||
const segs = items.reduce<Array<{ tone: Tone; fr: number; off: number }>>((acc, x) => {
|
||||
const prev = acc[acc.length - 1];
|
||||
return [...acc, { tone: x.tone, fr: x.value / total, off: prev === undefined ? 0 : prev.off + prev.fr }];
|
||||
}, []);
|
||||
return (
|
||||
<Section title={title}>
|
||||
<div className="jc-donut">
|
||||
<svg viewBox="0 0 140 140" width="140" height="140">
|
||||
<circle cx="70" cy="70" r={R} className="jc-donut-bg" />
|
||||
{segs.map((sg, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
cx="70"
|
||||
cy="70"
|
||||
r={R}
|
||||
className={'jc-donut-seg t-' + TONE_CLASS[sg.tone]}
|
||||
strokeDasharray={`${C * sg.fr} ${C * (1 - sg.fr)}`}
|
||||
strokeDashoffset={-C * sg.off}
|
||||
/>
|
||||
))}
|
||||
<text x="70" y="66" className="jc-donut-n">
|
||||
{total}
|
||||
</text>
|
||||
<text x="70" y="84" className="jc-donut-u">
|
||||
{unit ?? ''}
|
||||
</text>
|
||||
</svg>
|
||||
<ul className="jc-donut-lg">
|
||||
{items.map((it, i) => (
|
||||
<li key={i}>
|
||||
<em className={'t-' + TONE_CLASS[it.tone]} />
|
||||
<span>{it.label}</span>
|
||||
<b className="jc-mono">{it.value}</b>
|
||||
</li>
|
||||
))}
|
||||
{live && (
|
||||
<li className="jc-donut-live">
|
||||
<em className="jc-pulse-dot" />
|
||||
<span>随编辑实时联动</span>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
function MailBlock({
|
||||
b,
|
||||
editing,
|
||||
onOpenFile,
|
||||
}: {
|
||||
b: Extract<ViewBlock, { t: 'mail' }>;
|
||||
editing: boolean;
|
||||
onOpenFile?: ((file: FileDoc) => void) | undefined;
|
||||
}) {
|
||||
const [d, setD] = useState({ subject: b.subject, to: b.to, cc: b.cc ?? '', body: b.body });
|
||||
const [src, setSrc] = useState(b);
|
||||
if (src !== b) {
|
||||
setSrc(b);
|
||||
setD({ subject: b.subject, to: b.to, cc: b.cc ?? '', body: b.body });
|
||||
}
|
||||
const edit = editing && b.editable === true;
|
||||
|
||||
return (
|
||||
<div className={'jc-letter ' + (b.dir === 'in' ? 'in' : 'out')}>
|
||||
<div className="jc-letter-h">
|
||||
<span className="jc-letter-dir">
|
||||
<Ico name={b.dir === 'in' ? 'mail-open' : 'send'} s={14} />
|
||||
{b.dir === 'in' ? '收件' : '发件'}
|
||||
</span>
|
||||
{b.statusLabel !== undefined && <span className="jc-chip">{b.statusLabel}</span>}
|
||||
<code className="jc-mono">{b.time}</code>
|
||||
</div>
|
||||
<div className="jc-letter-meta">
|
||||
<p>
|
||||
<i>发件人</i>
|
||||
{b.from}
|
||||
{b.fromAddr !== undefined && <code className="jc-mono">{b.fromAddr}</code>}
|
||||
</p>
|
||||
<p>
|
||||
<i>收件人</i>
|
||||
{edit ? <input value={d.to} onChange={(e) => setD({ ...d, to: e.target.value })} /> : d.to}
|
||||
</p>
|
||||
{(d.cc !== '' || edit) && (
|
||||
<p>
|
||||
<i>抄送</i>
|
||||
{edit ? <input value={d.cc} onChange={(e) => setD({ ...d, cc: e.target.value })} /> : d.cc}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{edit ? (
|
||||
<input className="jc-letter-sub" value={d.subject} onChange={(e) => setD({ ...d, subject: e.target.value })} />
|
||||
) : (
|
||||
<h4 className="jc-letter-sub">{d.subject}</h4>
|
||||
)}
|
||||
{edit ? (
|
||||
<textarea
|
||||
className="jc-letter-body"
|
||||
value={d.body}
|
||||
onChange={(e) => setD({ ...d, body: e.target.value })}
|
||||
rows={7}
|
||||
/>
|
||||
) : (
|
||||
<p className="jc-letter-body">{d.body}</p>
|
||||
)}
|
||||
{b.attachments !== undefined && b.attachments.length > 0 && (
|
||||
<div className="jc-atts">
|
||||
{b.attachments.map((a, i) => {
|
||||
const file = a.file;
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
className="jc-att"
|
||||
disabled={file === undefined}
|
||||
onClick={file === undefined || onOpenFile === undefined ? undefined : () => onOpenFile(file)}
|
||||
>
|
||||
<Ico name="paperclip" s={13} />
|
||||
<b>{a.name}</b>
|
||||
{a.meta !== undefined && <span>{a.meta}</span>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,23 +2,62 @@
|
||||
* 视图区容器 —— **净化渲染,无脚本执行权**(编码规范 §9.2;语义出处 AM 07)。
|
||||
*
|
||||
* 「视图区容器渲染活动推送的 HTML:沙箱 iframe(srcdoc + sandbox)或净化后注入,
|
||||
* 二选一由 `ui-kit` 的 `ViewContainer` 统一实现,**feature 不得自行 dangerouslySetInnerHTML**。」
|
||||
* 二选一由 `ui-kit` 的 `ViewContainer` 统一实现,**feature 不得自行 dangerouslySetInnerHTML`。」
|
||||
*
|
||||
* 这里选沙箱 iframe:结构上就没有脚本执行权,不依赖净化算法的完备性。
|
||||
*
|
||||
* **高度随内容长**:HTML 报告既是独立文件,也是交互视图的组成部分 ——
|
||||
* 它得像页面里的一段正文那样整篇铺开、跟着外层一起滚,而不是缩在一个定高小窗里自己滚。
|
||||
* 定高会把长报告拦腰截断,人得在两层滚动条之间来回找。
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
export type ViewContainerProps = {
|
||||
readonly html: string;
|
||||
readonly title?: string;
|
||||
};
|
||||
|
||||
/** 量不到高度时的兜底,避免塌成 0 高 */
|
||||
const FALLBACK_HEIGHT = 420;
|
||||
|
||||
export function ViewContainer({ html, title = '活动视图' }: ViewContainerProps) {
|
||||
const ref = useRef<HTMLIFrameElement>(null);
|
||||
const [height, setHeight] = useState(FALLBACK_HEIGHT);
|
||||
|
||||
const measure = useCallback(() => {
|
||||
const doc = ref.current?.contentDocument;
|
||||
if (doc?.documentElement == null) return;
|
||||
// 取 body 与 documentElement 的较大者:两者谁撑开高度随文档结构而异
|
||||
const next = Math.max(doc.documentElement.scrollHeight, doc.body?.scrollHeight ?? 0);
|
||||
if (next > 0) setHeight(next);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const frame = ref.current;
|
||||
const doc = frame?.contentDocument;
|
||||
if (doc?.body == null) return undefined;
|
||||
// 图片late-load、字体回流都会改高度 —— 盯着 body 尺寸变化重量一次
|
||||
const observer = new ResizeObserver(measure);
|
||||
observer.observe(doc.body);
|
||||
return () => observer.disconnect();
|
||||
}, [measure, html]);
|
||||
|
||||
return (
|
||||
<iframe
|
||||
ref={ref}
|
||||
className="jc-view-frame"
|
||||
title={title}
|
||||
// sandbox 不含 allow-scripts —— 无脚本执行权
|
||||
sandbox=""
|
||||
/*
|
||||
* `allow-same-origin` **只为让外层量到内容高度**(否则 iframe 是不透明源,
|
||||
* contentDocument 读不到,高度无从得知)。
|
||||
* 关键在于**始终不给 `allow-scripts`** —— 里面一行脚本也跑不了,
|
||||
* 「无脚本执行权」这条不变。两者同时给才是破沙箱,这里永远不会。
|
||||
*/
|
||||
sandbox="allow-same-origin"
|
||||
srcDoc={html}
|
||||
style={{ height }}
|
||||
onLoad={measure}
|
||||
scrolling="no"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
export type { ViewContainerProps } from './ViewContainer.tsx';
|
||||
export { ViewContainer } from './ViewContainer.tsx';
|
||||
export { ViewBlocks } from './ViewBlocks.tsx';
|
||||
export { Ico } from './Ico.tsx';
|
||||
|
||||
export const UI_KIT_VERSION = '0.1.0-m6';
|
||||
|
||||
Reference in New Issue
Block a user