Archived
三面拆分⑫:UI 打磨 —— 补 10 枚缺失图标,蓝图改因果链,台账收成一个模块
图标注册表缺 arrow-left-right/columns/table/tag 等 10 名,用到即渲染成空 SVG —— 补齐; 蓝图四库改真箭头相连的因果链节点卡(卡即入口,删重复的入库段); 驾驶舱终态分布改五色堆叠条;运营数据收成左轨一枚「台账」,域下沉为 2-3 字短名页签 (受理→寻源→定价→议价→替代→共享,按流程序),进模块自动落首域。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
@@ -21,6 +21,7 @@ import type { GovZone, Nav, Route } from '@web-shared/gov/gov-route.ts';
|
|||||||
import { SURFACE_META, SURFACE_ZONES, canSeeSurface } from '@web-shared/gov/surfaces.ts';
|
import { SURFACE_META, SURFACE_ZONES, canSeeSurface } from '@web-shared/gov/surfaces.ts';
|
||||||
import type { Surface } from '@web-shared/gov/surfaces.ts';
|
import type { Surface } from '@web-shared/gov/surfaces.ts';
|
||||||
import { useGovObjects, useGovTasks } from '@web-shared/gov/gov-api.ts';
|
import { useGovObjects, useGovTasks } from '@web-shared/gov/gov-api.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, TaskLogPage, TaskPage, WfPage } from '@features/gov-task';
|
import { TasksHome, TaskLogPage, TaskPage, WfPage } from '@features/gov-task';
|
||||||
@@ -126,17 +127,19 @@ export function GovShell({ surface }: { surface: Surface }) {
|
|||||||
(tasks.data?.workflows ?? []).filter((w) => w.state === 'abnormal').length +
|
(tasks.data?.workflows ?? []).filter((w) => w.state === 'abnormal').length +
|
||||||
(tasks.data?.tasks ?? []).filter((t) => t.terminal !== undefined && t.terminal !== 'released').length;
|
(tasks.data?.tasks ?? []).filter((t) => t.terminal !== undefined && t.terminal !== 'released').length;
|
||||||
|
|
||||||
/** 台账的导航项按**已有数据域**长出来 —— 设计侧一发布新域,运营侧自动多一位 */
|
/** 台账的域页签按**已有域**长出来 —— 设计侧一发布新域,台账里自动多一个页签 */
|
||||||
const domains = [...new Set((objects.data?.objects ?? []).map((o) => o.domain))].sort();
|
const domains = sortDomains([...new Set((objects.data?.objects ?? []).map((o) => o.domain))]);
|
||||||
|
// 进台账没带域(左轨点进来)时落到首域 —— 页签与内容同一判定
|
||||||
|
const effectiveTab = zone === 'ledgers' && tab === '' ? (domains[0] ?? '') : tab;
|
||||||
|
|
||||||
/* 面包屑:台面 › 区 › 页签 › 下钻各级 */
|
/* 面包屑:台面 › 区 › 页签 › 下钻各级 */
|
||||||
const crumbs: Array<{ label: string; go?: () => void }> = [
|
const crumbs: Array<{ label: string; go?: () => void }> = [
|
||||||
{ label: SURFACE_META[surface].brand, go: () => goZone(zone) },
|
{ label: SURFACE_META[surface].brand, go: () => goZone(zone) },
|
||||||
];
|
];
|
||||||
crumbs.push({ label: meta.title, go: () => go({ zone, tab, stack: [] }) });
|
crumbs.push({ label: meta.title, go: () => go({ zone, tab, stack: [] }) });
|
||||||
// 台账区的页签名就是域名 —— 静态表里查不到,直接用它本身
|
// 台账区的页签名就是域键 —— 静态表里查不到,呈现走人读短名
|
||||||
crumbs.push({
|
crumbs.push({
|
||||||
label: TABS[zone].find((t) => t.key === tab)?.label ?? tab,
|
label: TABS[zone].find((t) => t.key === tab)?.label ?? (zone === 'ledgers' ? domainShort(effectiveTab) : tab),
|
||||||
go: () => go({ zone, tab, stack: [] }),
|
go: () => go({ zone, tab, stack: [] }),
|
||||||
});
|
});
|
||||||
stack.forEach((r, i) =>
|
stack.forEach((r, i) =>
|
||||||
@@ -154,24 +157,14 @@ export function GovShell({ surface }: { surface: Surface }) {
|
|||||||
<Ico name={surface === 'design' ? 'route' : 'gauge'} s={20} />
|
<Ico name={surface === 'design' ? 'route' : 'gauge'} s={20} />
|
||||||
</div>
|
</div>
|
||||||
<div className="g-nav-brand">{SURFACE_META[surface].brand}</div>
|
<div className="g-nav-brand">{SURFACE_META[surface].brand}</div>
|
||||||
{ZONES.filter((z) => SURFACE_ZONES[surface].includes(z.key)).map((z) =>
|
{ZONES.filter((z) => SURFACE_ZONES[surface].includes(z.key)).map((z) => (
|
||||||
// 台账不是一个固定位,是一段**随数据域生长**的动态位 —— 每域一枚
|
<button key={z.key} className={'g-nav-b' + (zone === z.key ? ' on' : '')} onClick={() => goZone(z.key)}>
|
||||||
z.key === 'ledgers' ? (
|
<span className="g-nav-ico">
|
||||||
<LedgerRail
|
<Ico name={z.icon} s={18} />
|
||||||
key={z.key}
|
</span>
|
||||||
domains={domains}
|
<span>{z.label}</span>
|
||||||
active={zone === 'ledgers' ? tab : undefined}
|
</button>
|
||||||
onPick={(domain) => go({ zone: 'ledgers', tab: domain, stack: [] })}
|
))}
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<button key={z.key} className={'g-nav-b' + (zone === z.key ? ' on' : '')} onClick={() => goZone(z.key)}>
|
|
||||||
<span className="g-nav-ico">
|
|
||||||
<Ico name={z.icon} s={18} />
|
|
||||||
</span>
|
|
||||||
<span>{z.label}</span>
|
|
||||||
</button>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
<div className="g-nav-foot">
|
<div className="g-nav-foot">
|
||||||
<span className="g-nav-ava">{(me?.name ?? '—').slice(0, 1)}</span>
|
<span className="g-nav-ava">{(me?.name ?? '—').slice(0, 1)}</span>
|
||||||
<span>{me?.name ?? '未登录'}</span>
|
<span>{me?.name ?? '未登录'}</span>
|
||||||
@@ -242,18 +235,31 @@ export function GovShell({ surface }: { surface: Surface }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="g-tabs">
|
<div className="g-tabs">
|
||||||
{TABS[zone].map((t) => (
|
{zone === 'ledgers'
|
||||||
<button
|
? // 台账的页签是**数据域**(短名 2-3 字),随发布生长 —— 设计产生运营的形状,落在页签上
|
||||||
key={t.key}
|
domains.map((d) => (
|
||||||
className={'g-tab' + (tab === t.key ? ' on' : '')}
|
<button
|
||||||
onClick={() => go({ zone, tab: t.key, stack: [] })}
|
key={d}
|
||||||
>
|
className={'g-tab' + (effectiveTab === d ? ' on' : '')}
|
||||||
<Ico name={t.ico} s={13} />
|
title={`数据域 · ${d}`}
|
||||||
{t.label}
|
onClick={() => go({ zone, tab: d, stack: [] })}
|
||||||
</button>
|
>
|
||||||
))}
|
<Ico name="table" s={13} />
|
||||||
|
{domainShort(d)}
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
: TABS[zone].map((t) => (
|
||||||
|
<button
|
||||||
|
key={t.key}
|
||||||
|
className={'g-tab' + (tab === t.key ? ' on' : '')}
|
||||||
|
onClick={() => go({ zone, tab: t.key, stack: [] })}
|
||||||
|
>
|
||||||
|
<Ico name={t.ico} s={13} />
|
||||||
|
{t.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<ZoneHome zone={zone} tab={tab} nav={nav} />
|
<ZoneHome zone={zone} tab={effectiveTab} nav={nav} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="g-detail" key={`${stack.length}:${top.k}:${top.id}`}>
|
<div className="g-detail" key={`${stack.length}:${top.k}:${top.id}`}>
|
||||||
@@ -266,57 +272,6 @@ export function GovShell({ surface }: { surface: Surface }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 台账段 —— 左轨里唯一的**动态位**:一域一枚,随数据域发布而生长。
|
|
||||||
*
|
|
||||||
* 这是「设计产生运营的形状」最直观的一处:导航本身就是被设计出来的。
|
|
||||||
*/
|
|
||||||
function LedgerRail({
|
|
||||||
domains,
|
|
||||||
active,
|
|
||||||
onPick,
|
|
||||||
}: {
|
|
||||||
domains: readonly string[];
|
|
||||||
active: string | undefined;
|
|
||||||
onPick: (domain: string) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="g-nav-brand" style={{ marginTop: 8, marginBottom: 4 }}>
|
|
||||||
台账
|
|
||||||
</div>
|
|
||||||
{domains.length === 0 ? (
|
|
||||||
<div className="g-nav-brand" style={{ fontWeight: 600, letterSpacing: 0, lineHeight: 1.5 }}>
|
|
||||||
随数据域
|
|
||||||
<br />
|
|
||||||
发布生长
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
domains.map((d) => (
|
|
||||||
<button
|
|
||||||
key={d}
|
|
||||||
className={'g-nav-b' + (active === d ? ' on' : '')}
|
|
||||||
onClick={() => onPick(d)}
|
|
||||||
title={`台账 · ${d}`}
|
|
||||||
>
|
|
||||||
<span className="g-nav-ico">
|
|
||||||
<Ico name="database" s={18} />
|
|
||||||
</span>
|
|
||||||
{/* 域名是 `活动_环节` 这种复合键,60px 的轨放不下一行 —— 按下划线断行,窄字号铺开 */}
|
|
||||||
<span style={{ fontSize: 10, lineHeight: 1.25, wordBreak: 'break-word' }}>
|
|
||||||
{d.split('_').map((seg, i) => (
|
|
||||||
<span key={seg} style={{ display: 'block' }}>
|
|
||||||
{i === 0 && d.includes('_') ? `${seg}_` : seg}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ZoneHome({ zone, tab, nav }: { zone: GovZone; tab: string; nav: Nav }) {
|
function ZoneHome({ zone, tab, nav }: { zone: GovZone; tab: string; nav: Nav }) {
|
||||||
const P = {
|
const P = {
|
||||||
// 设计中心
|
// 设计中心
|
||||||
|
|||||||
@@ -458,3 +458,35 @@
|
|||||||
.g-dbpage button:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
|
.g-dbpage button:hover:not(:disabled) { border-color: var(--brand); color: var(--brand); }
|
||||||
.g-dbpage button:disabled { opacity: .35; cursor: default; }
|
.g-dbpage button:disabled { opacity: .35; cursor: default; }
|
||||||
.g-dbpage b { min-width: 36px; text-align: center; font-size: 11px; color: var(--ink-2); }
|
.g-dbpage b { min-width: 36px; text-align: center; font-size: 11px; color: var(--ink-2); }
|
||||||
|
|
||||||
|
/* ═══════════ 三面补充(非原型)—— 蓝图因果链 / 终态堆叠条 / 数据域紧凑轨钮 ═══════════ */
|
||||||
|
|
||||||
|
/* 蓝图因果链:四库节点卡以真箭头相连 —— 箭头编码真实依赖(流程包→活动→工具→数据),节点即入口 */
|
||||||
|
.g-chain { display: flex; align-items: stretch; gap: 0; margin-bottom: 14px; }
|
||||||
|
.g-chain-node { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; padding: 14px 16px; background: var(--card); border: 1px solid var(--cline); border-radius: 13px; box-shadow: var(--cshadow); text-align: left; font: inherit; color: var(--ink); cursor: pointer; transition: border-color var(--t-fast), transform var(--t-fast), box-shadow var(--t-fast); }
|
||||||
|
.g-chain-node:hover { border-color: var(--brand); transform: translateY(-1px); box-shadow: 0 4px 14px -6px rgba(51, 112, 255, .35); }
|
||||||
|
.g-chain-node:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
|
||||||
|
.g-chain-k { display: flex; align-items: center; gap: 7px; font-size: 11px; font-weight: 600; color: var(--ink-3); }
|
||||||
|
.g-chain-k .ic { width: 22px; height: 22px; border-radius: 7px; display: grid; place-items: center; flex: none; background: var(--fill-2, #eef1f5); color: var(--ink-3); }
|
||||||
|
.g-chain-node.t-brand .ic { background: var(--brand-soft); color: var(--brand-deep); }
|
||||||
|
.g-chain-node.t-auto .ic { background: #e6f7f4; color: #0a6a64; }
|
||||||
|
.g-chain-node.t-collab .ic { background: #f0e9ff; color: #5b21cf; }
|
||||||
|
.g-chain-node.t-ok .ic { background: var(--ok-soft); color: var(--ok); }
|
||||||
|
.g-chain-v { font-size: 24px; font-weight: 800; font-family: var(--mono); letter-spacing: -.03em; line-height: 1; }
|
||||||
|
.g-chain-node.t-brand .g-chain-v { color: var(--brand-deep); }
|
||||||
|
.g-chain-node.t-auto .g-chain-v { color: #0a6a64; }
|
||||||
|
.g-chain-node.t-collab .g-chain-v { color: #5b21cf; }
|
||||||
|
.g-chain-node.t-ok .g-chain-v { color: var(--ok); }
|
||||||
|
.g-chain-h { font-size: 11px; line-height: 1.5; color: var(--ink-4); }
|
||||||
|
.g-chain-arr { flex: none; width: 26px; display: grid; place-items: center; color: var(--ink-4); }
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.g-chain { flex-direction: column; }
|
||||||
|
.g-chain-arr { transform: rotate(90deg); width: auto; padding: 2px 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 终态堆叠条:五终态五色一条,比例即信息 —— 装在 .g-kpi 卡里 */
|
||||||
|
.g-mixbar { display: flex; height: 8px; border-radius: 4px; overflow: hidden; background: var(--fill-2, #eef1f5); }
|
||||||
|
.g-mixbar i { display: block; min-width: 2px; }
|
||||||
|
.g-mixbar-legend { display: flex; flex-wrap: wrap; gap: 3px 10px; font-size: 10px; color: var(--ink-3); }
|
||||||
|
.g-mixbar-legend b { font-family: var(--mono); font-weight: 700; color: var(--ink-2); }
|
||||||
|
.g-mixbar-legend i { display: inline-block; width: 7px; height: 7px; border-radius: 2px; margin-right: 4px; }
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
* 这里**不出现任何运行态数字**(在跑几单、30 天起单数、死信数)。
|
* 这里**不出现任何运行态数字**(在跑几单、30 天起单数、死信数)。
|
||||||
* 设计者看的是定义完不完整,不是今天忙不忙 —— 混进运行数字,这页就退化成第二个驾驶舱。
|
* 设计者看的是定义完不完整,不是今天忙不忙 —— 混进运行数字,这页就退化成第二个驾驶舱。
|
||||||
*/
|
*/
|
||||||
import { Clear, Entry, Kpi, Sec, Todo } from '@web-shared/gov/primitives.tsx';
|
import { Ico } from '@web-shared/Ico.tsx';
|
||||||
|
import { Clear, Sec, Todo } from '@web-shared/gov/primitives.tsx';
|
||||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||||
import {
|
import {
|
||||||
useGovActivities,
|
useGovActivities,
|
||||||
@@ -58,38 +59,67 @@ export function BlueprintHome({ nav }: HomeProps) {
|
|||||||
|
|
||||||
const gaps = draftBad.length + missingActivities.length + uncalled.length;
|
const gaps = draftBad.length + missingActivities.length + uncalled.length;
|
||||||
|
|
||||||
// 因果链那句话写在区说明里(ZM.blueprint.desc),此处不重复 —— 直接上四库读数
|
// 因果链是这一屏的主角:四库以真箭头相连 —— 箭头编码的是真实依赖
|
||||||
|
// (流程包引用活动、活动调工具、活动物化数据),节点卡即入口,点卡即入库。
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="g-kpis">
|
<div className="g-chain">
|
||||||
<Kpi
|
<button className="g-chain-node t-brand" onClick={() => nav.jump('workflows', 'pkgs')}>
|
||||||
ico="workflow"
|
<span className="g-chain-k">
|
||||||
tone="brand"
|
<span className="ic">
|
||||||
label="流程包 · 战略"
|
<Ico name="workflow" s={13} />
|
||||||
value={packages.length}
|
</span>
|
||||||
sub={`草稿 ${drafts.length} · 未闭合 ${draftBad.length}`}
|
战略 · 流程包
|
||||||
/>
|
</span>
|
||||||
<Kpi
|
<span className="g-chain-v">{packages.length}</span>
|
||||||
ico="boxes"
|
<span className="g-chain-h">
|
||||||
tone="auto"
|
企业以哪些流转运转 —— 草稿 {drafts.length} · 未闭合 {draftBad.length}
|
||||||
label="活动 · 方法"
|
</span>
|
||||||
value={activities.length}
|
</button>
|
||||||
sub="一个活动 = 一个任务的定义"
|
<span className="g-chain-arr">
|
||||||
/>
|
<Ico name="chevron-right" s={15} />
|
||||||
<Kpi
|
</span>
|
||||||
ico="wrench"
|
<button className="g-chain-node t-auto" onClick={() => nav.jump('library', 'shelf')}>
|
||||||
tone="collab"
|
<span className="g-chain-k">
|
||||||
label="工具 · 能力"
|
<span className="ic">
|
||||||
value={toolRows.length}
|
<Ico name="boxes" s={13} />
|
||||||
sub={`读类 ${toolRows.filter((t) => t.effect === 'read').length} · 写类 ${toolRows.filter((t) => t.effect === 'write').length}`}
|
</span>
|
||||||
/>
|
方法 · 活动库
|
||||||
<Kpi
|
</span>
|
||||||
ico="database"
|
<span className="g-chain-v">{activities.length}</span>
|
||||||
tone="ok"
|
<span className="g-chain-h">每件事怎么干 —— 一个活动 = 一个任务的定义</span>
|
||||||
label="数据域 · 对象"
|
</button>
|
||||||
value={domains.length}
|
<span className="g-chain-arr">
|
||||||
sub={`${objectRows.length} 个对象 · 发布即在运营中心长出台账位`}
|
<Ico name="chevron-right" s={15} />
|
||||||
/>
|
</span>
|
||||||
|
<button className="g-chain-node t-collab" onClick={() => nav.jump('tools', 'catalog')}>
|
||||||
|
<span className="g-chain-k">
|
||||||
|
<span className="ic">
|
||||||
|
<Ico name="wrench" s={13} />
|
||||||
|
</span>
|
||||||
|
能力 · 工具箱
|
||||||
|
</span>
|
||||||
|
<span className="g-chain-v">{toolRows.length}</span>
|
||||||
|
<span className="g-chain-h">
|
||||||
|
系统能对世界做什么 —— 读类 {toolRows.filter((t) => t.effect === 'read').length} · 写类{' '}
|
||||||
|
{toolRows.filter((t) => t.effect === 'write').length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<span className="g-chain-arr">
|
||||||
|
<Ico name="chevron-right" s={15} />
|
||||||
|
</span>
|
||||||
|
<button className="g-chain-node t-ok" onClick={() => nav.jump('data', 'domains')}>
|
||||||
|
<span className="g-chain-k">
|
||||||
|
<span className="ic">
|
||||||
|
<Ico name="database" s={13} />
|
||||||
|
</span>
|
||||||
|
对象 · 数据域
|
||||||
|
</span>
|
||||||
|
<span className="g-chain-v">{domains.length}</span>
|
||||||
|
<span className="g-chain-h">
|
||||||
|
企业运营什么 —— {objectRows.length} 个对象,发布即入运营台账
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Sec ico="flag" title="待补清单" extra="定义上的缺口聚在这里,点进即改 —— 不散落在四库">
|
<Sec ico="flag" title="待补清单" extra="定义上的缺口聚在这里,点进即改 —— 不散落在四库">
|
||||||
@@ -144,34 +174,6 @@ export function BlueprintHome({ nav }: HomeProps) {
|
|||||||
)}
|
)}
|
||||||
</Sec>
|
</Sec>
|
||||||
|
|
||||||
<Sec ico="route" title="从这里进四库" extra="每库一页:定义在库里改,缺口在上面看">
|
|
||||||
<div className="g-entries">
|
|
||||||
<Entry
|
|
||||||
ico="workflow"
|
|
||||||
title="流程包 · 企业以哪些流转运转"
|
|
||||||
desc="五要素编排、版本谱系、边映射校验;调起规则也在本区"
|
|
||||||
onClick={() => nav.jump('workflows', 'pkgs')}
|
|
||||||
/>
|
|
||||||
<Entry
|
|
||||||
ico="boxes"
|
|
||||||
title="活动库 · 每件事怎么干"
|
|
||||||
desc="已安装活动的资产货架:契约面、范式、两条版本序列"
|
|
||||||
onClick={() => nav.jump('library', 'shelf')}
|
|
||||||
/>
|
|
||||||
<Entry
|
|
||||||
ico="wrench"
|
|
||||||
title="工具箱 · 系统能对世界做什么"
|
|
||||||
desc="读类过程中自由调用,写类只登记动作意向、放行后落地"
|
|
||||||
onClick={() => nav.jump('tools', 'catalog')}
|
|
||||||
/>
|
|
||||||
<Entry
|
|
||||||
ico="database"
|
|
||||||
title="数据域 · 企业运营什么"
|
|
||||||
desc="域与对象的结构定义;行数据在运营中心的台账看"
|
|
||||||
onClick={() => nav.jump('data', 'domains')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Sec>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* feature `gov-data` 唯一出口(编码规范 §9.1)。
|
* feature `gov-data` 唯一出口(编码规范 §9.1)。
|
||||||
* 数据的两副面孔:结构定义在设计中心(数据域),行数据在运营中心(台账 · 某域)。
|
* 数据的两副面孔:结构定义在设计中心(数据域),行数据在运营中心(台账 · 某域页签)。
|
||||||
*/
|
*/
|
||||||
export { DataHome, LedgersHome, ObjPage } from './internal/panel.tsx';
|
export { DataHome, LedgersHome, ObjPage } from './internal/panel.tsx';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* feature `gov-data` —— 数据的两副面孔,各归其台:
|
* feature `gov-data` —— 数据的两副面孔,各归其台:
|
||||||
*
|
*
|
||||||
* `DataHome`(设计中心 · 数据域):**定义面** —— 域 × 对象的结构:有哪些列、主键是什么、谁声明了它。
|
* `DataHome`(设计中心 · 数据域):**定义面** —— 域 × 对象的结构:有哪些列、主键是什么、谁声明了它。
|
||||||
* `LedgersHome`(运营中心 · 台账 · 某域):**运营面** —— 这张表现在有多少行、里面是什么。
|
* `LedgersHome`(运营中心 · 台账 · 某域页签):**运营面** —— 这张表现在有多少行、里面是什么。
|
||||||
*
|
*
|
||||||
* 分开的理由:改 schema 和查行数据是两种心智。
|
* 分开的理由:改 schema 和查行数据是两种心智。
|
||||||
* 设计者问「这个对象定义全不全」,运营者问「今天这张表有几条」——
|
* 设计者问「这个对象定义全不全」,运营者问「今天这张表有几条」——
|
||||||
@@ -15,6 +15,7 @@ import { Ico } from '@web-shared/Ico.tsx';
|
|||||||
import { Card, Chip, Clear, NList, NRow, Note, PageHead, QueryGate, Sec } from '@web-shared/gov/primitives.tsx';
|
import { Card, Chip, Clear, NList, NRow, Note, PageHead, QueryGate, Sec } from '@web-shared/gov/primitives.tsx';
|
||||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||||
import { useGovLedger, useGovObjects } from '@web-shared/gov/gov-api.ts';
|
import { useGovLedger, useGovObjects } from '@web-shared/gov/gov-api.ts';
|
||||||
|
import { domainShort, sortDomains } from '@web-shared/gov/domains.ts';
|
||||||
|
|
||||||
export type HomeProps = { tab: string; nav: Nav };
|
export type HomeProps = { tab: string; nav: Nav };
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ export type HomeProps = { tab: string; nav: Nav };
|
|||||||
export function DataHome({ nav }: HomeProps) {
|
export function DataHome({ nav }: HomeProps) {
|
||||||
const q = useGovObjects();
|
const q = useGovObjects();
|
||||||
const objects = q.data?.objects ?? [];
|
const objects = q.data?.objects ?? [];
|
||||||
const domains = [...new Set(objects.map((o) => o.domain))].sort();
|
const domains = sortDomains([...new Set(objects.map((o) => o.domain))]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryGate query={q}>
|
<QueryGate query={q}>
|
||||||
@@ -40,8 +41,11 @@ export function DataHome({ nav }: HomeProps) {
|
|||||||
<div key={d} className="g-sec">
|
<div key={d} className="g-sec">
|
||||||
<div className="g-band">
|
<div className="g-band">
|
||||||
<Ico name="database" s={13} />
|
<Ico name="database" s={13} />
|
||||||
{d}
|
{domainShort(d)}
|
||||||
<span className="x">{objects.filter((o) => o.domain === d).length} 个对象 · 发布即在运营中心长出一位</span>
|
<span className="g-mono" style={{ fontWeight: 500 }}>
|
||||||
|
{d}
|
||||||
|
</span>
|
||||||
|
<span className="x">{objects.filter((o) => o.domain === d).length} 个对象 · 发布即入运营台账</span>
|
||||||
</div>
|
</div>
|
||||||
<NList>
|
<NList>
|
||||||
{objects
|
{objects
|
||||||
@@ -80,10 +84,10 @@ function ObjectSchemaRow({ domain, object, nav }: { domain: string; object: stri
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 运营侧:台账(某一个域) ==================== */
|
/* ==================== 运营侧:台账(某一个域页签) ==================== */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 台账的一个域 —— 壳层把左轨选中的域名当页签传进来。
|
* 台账的一个域 —— 壳层把选中的域页签(键)传进来;没带域时壳层已落到首域。
|
||||||
* 这里看的是运营事实:这张表现在多少行,点进去逐行看。
|
* 这里看的是运营事实:这张表现在多少行,点进去逐行看。
|
||||||
*/
|
*/
|
||||||
export function LedgersHome({ tab, nav }: HomeProps) {
|
export function LedgersHome({ tab, nav }: HomeProps) {
|
||||||
@@ -95,8 +99,8 @@ export function LedgersHome({ tab, nav }: HomeProps) {
|
|||||||
<QueryGate query={q}>
|
<QueryGate query={q}>
|
||||||
<>
|
<>
|
||||||
<Note>
|
<Note>
|
||||||
<b>{domain}</b> 域的台账:行数据、主键与列结构。结构要改去设计中心 · 数据域;
|
<b>{domainShort(domain)}</b>域(<span className="g-mono">{domain}</span>)的行数据、主键与列结构。
|
||||||
这里做的是运营动作 —— 查行、看版本、追引用。
|
结构要改去设计中心 · 数据域;这里做的是运营动作 —— 查行、看版本、追引用。
|
||||||
</Note>
|
</Note>
|
||||||
{inDomain.length === 0 ? (
|
{inDomain.length === 0 ? (
|
||||||
<Clear>这个域还没有对象 —— 对象随活动首次写入而物化,建好就出现在这里。</Clear>
|
<Clear>这个域还没有对象 —— 对象随活动首次写入而物化,建好就出现在这里。</Clear>
|
||||||
@@ -112,7 +116,7 @@ export function LedgersHome({ tab, nav }: HomeProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 一个台账对象一行:行数是运营侧最想先看到的那个数 */
|
/** 一个数据对象一行:行数是运营侧最想先看到的那个数 */
|
||||||
function LedgerObjectRow({ domain, object, nav }: { domain: string; object: string; nav: Nav }) {
|
function LedgerObjectRow({ domain, object, nav }: { domain: string; object: string; nav: Nav }) {
|
||||||
const page = useGovLedger(domain, object);
|
const page = useGovLedger(domain, object);
|
||||||
const total = page.data?.total;
|
const total = page.data?.total;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TERMINAL_STATE_LABEL } from '@contracts';
|
import { TERMINAL_STATE_LABEL } from '@contracts';
|
||||||
import type { TerminalState } from '@contracts';
|
import type { TerminalState } from '@contracts';
|
||||||
|
import { Ico } from '@web-shared/Ico.tsx';
|
||||||
import { Chip, Clear, Kpi, NList, NRow, Note, PageHead, QueryGate, Todo } from '@web-shared/gov/primitives.tsx';
|
import { Chip, Clear, Kpi, NList, NRow, Note, PageHead, QueryGate, Todo } from '@web-shared/gov/primitives.tsx';
|
||||||
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
import type { Nav } from '@web-shared/gov/gov-route.ts';
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +26,15 @@ export type HomeProps = { tab: string; nav: Nav };
|
|||||||
|
|
||||||
const pct = (v: number | undefined) => Math.round((v ?? 0) * 100);
|
const pct = (v: number | undefined) => Math.round((v ?? 0) * 100);
|
||||||
|
|
||||||
|
/** 终态五名的呈现色 —— 放行绿、驳回琥珀、停滞灰、耗尽红、终止墨,与状态章同族 */
|
||||||
|
const TERMINAL_TONE: Readonly<Record<string, string>> = {
|
||||||
|
released: 'var(--ok-accent)',
|
||||||
|
blocked: 'var(--warn-accent)',
|
||||||
|
stalled: '#8f959e',
|
||||||
|
exhausted: 'var(--danger-accent)',
|
||||||
|
terminated: '#41464e',
|
||||||
|
};
|
||||||
|
|
||||||
/* ---------------- 经营驾驶舱(运营首页) ---------------- */
|
/* ---------------- 经营驾驶舱(运营首页) ---------------- */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,18 +129,35 @@ export function CockpitHome({ nav }: HomeProps) {
|
|||||||
unit="tok"
|
unit="tok"
|
||||||
sub={`每放行一单:${(m?.unitCost.minutes ?? 0).toFixed(1)} 分钟 · 调人 ${(m?.unitCost.humanCalls ?? 0).toFixed(1)} 次`}
|
sub={`每放行一单:${(m?.unitCost.minutes ?? 0).toFixed(1)} 分钟 · 调人 ${(m?.unitCost.humanCalls ?? 0).toFixed(1)} 次`}
|
||||||
/>
|
/>
|
||||||
<Kpi
|
{/* 终态分布:比例即信息 —— 五终态五色一条,不把五个百分比塞进一行小字 */}
|
||||||
ico="flag"
|
<div className="g-kpi">
|
||||||
label="终态分布"
|
<span className="g-kpi-l">
|
||||||
value={mixTop.length === 0 ? '—' : `${pct(mixTop[0]![1])}%`}
|
<Ico name="flag" s={12} />
|
||||||
sub={
|
终态分布
|
||||||
mixTop.length === 0
|
</span>
|
||||||
? '还没有任务落终态'
|
{mixTop.length === 0 ? (
|
||||||
: mixTop
|
<>
|
||||||
.map(([k, v]) => `${TERMINAL_STATE_LABEL[k as TerminalState] ?? k} ${pct(v)}%`)
|
<span className="g-kpi-v">—</span>
|
||||||
.join(' · ')
|
<span className="g-kpi-s">还没有任务落终态</span>
|
||||||
}
|
</>
|
||||||
/>
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="g-mixbar">
|
||||||
|
{mixTop.map(([k, v]) => (
|
||||||
|
<i key={k} style={{ width: `${pct(v)}%`, background: TERMINAL_TONE[k] ?? '#c9d2dd' }} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<span className="g-mixbar-legend">
|
||||||
|
{mixTop.map(([k, v]) => (
|
||||||
|
<span key={k}>
|
||||||
|
<i style={{ background: TERMINAL_TONE[k] ?? '#c9d2dd' }} />
|
||||||
|
{TERMINAL_STATE_LABEL[k as TerminalState] ?? k} <b>{pct(v)}%</b>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Note>
|
<Note>
|
||||||
自主率与纠正率<b>成对读</b>,复合为有效自主率——单看自主率会奖励「猛冲后返工」。度量只是仪表,不载权力。
|
自主率与纠正率<b>成对读</b>,复合为有效自主率——单看自主率会奖励「猛冲后返工」。度量只是仪表,不载权力。
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ function TriggerForm({
|
|||||||
)}
|
)}
|
||||||
{draft.spec.kind === 'periodic-data' && (
|
{draft.spec.kind === 'periodic-data' && (
|
||||||
<>
|
<>
|
||||||
<Field k="台账域 · 对象" hint="按这张表的日期字段逐行起单">
|
<Field k="数据域 · 对象" hint="按这张表的日期字段逐行起单">
|
||||||
<select
|
<select
|
||||||
className="g-input"
|
className="g-input"
|
||||||
value={`${draft.spec.domain}.${draft.spec.object}`}
|
value={`${draft.spec.domain}.${draft.spec.object}`}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 数据域短名 —— 字典命中给人读名,字典外回落键尾段:轨上永远有个能认的名字。
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { domainShort, sortDomains } from '../domains.ts';
|
||||||
|
|
||||||
|
describe('domainShort', () => {
|
||||||
|
it('已知域给 2-3 字人读名', () => {
|
||||||
|
expect(domainShort('quote_intake')).toBe('受理');
|
||||||
|
expect(domainShort('quote_sourcing')).toBe('寻源');
|
||||||
|
expect(domainShort('shared')).toBe('共享');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('未知域回落到键的最后一段,超长截 4 字符', () => {
|
||||||
|
expect(domainShort('warehouse_ops')).toBe('ops');
|
||||||
|
expect(domainShort('some_verylongtail')).toBe('very');
|
||||||
|
expect(domainShort('solo')).toBe('solo');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sortDomains —— 左轨按流程走向排', () => {
|
||||||
|
it('已知域按业务序,未知域殿后', () => {
|
||||||
|
expect(sortDomains(['shared', 'quote_pricing', 'zzz_custom', 'quote_intake'])).toEqual([
|
||||||
|
'quote_intake',
|
||||||
|
'quote_pricing',
|
||||||
|
'shared',
|
||||||
|
'zzz_custom',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 数据域的人读短名 —— 左轨一枚钮只放得下 2-3 个字。
|
||||||
|
*
|
||||||
|
* 域键是活动的复合键(`quote_intake`),给人看要翻译;
|
||||||
|
* 字典之外的域回落到键的最后一段(截 4 字符)—— 新装的活动没配短名时,轨上先有个能认的名字。
|
||||||
|
*/
|
||||||
|
export const DOMAIN_SHORT: Readonly<Record<string, string>> = {
|
||||||
|
quote_intake: '受理',
|
||||||
|
quote_sourcing: '寻源',
|
||||||
|
quote_pricing: '定价',
|
||||||
|
quote_negotiate: '议价',
|
||||||
|
quote_alternative: '替代',
|
||||||
|
shared: '共享',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 业务序:受理 → 寻源 → 定价 → 议价 → 替代 → 共享 —— 左轨按流程走向排,不按字母 */
|
||||||
|
const DOMAIN_ORDER = ['quote_intake', 'quote_sourcing', 'quote_pricing', 'quote_negotiate', 'quote_alternative', 'shared'];
|
||||||
|
|
||||||
|
export function sortDomains(domains: readonly string[]): string[] {
|
||||||
|
return [...domains].sort((a, b) => {
|
||||||
|
const ia = DOMAIN_ORDER.indexOf(a);
|
||||||
|
const ib = DOMAIN_ORDER.indexOf(b);
|
||||||
|
// 字典外的域排在已知域之后,彼此按键序稳定
|
||||||
|
if (ia === -1 && ib === -1) return a.localeCompare(b);
|
||||||
|
if (ia === -1) return 1;
|
||||||
|
if (ib === -1) return -1;
|
||||||
|
return ia - ib;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function domainShort(domain: string): string {
|
||||||
|
const hit = DOMAIN_SHORT[domain];
|
||||||
|
if (hit !== undefined) return hit;
|
||||||
|
const tail = domain.split('_').at(-1) ?? domain;
|
||||||
|
return tail.length <= 4 ? tail : tail.slice(0, 4);
|
||||||
|
}
|
||||||
@@ -54,7 +54,7 @@ export const ZONES: ReadonlyArray<{ key: GovZone; label: string; icon: string }>
|
|||||||
{ key: 'cockpit', label: '驾驶舱', icon: 'gauge' },
|
{ key: 'cockpit', label: '驾驶舱', icon: 'gauge' },
|
||||||
{ key: 'tasks', label: '任务', icon: 'activity' },
|
{ key: 'tasks', label: '任务', icon: 'activity' },
|
||||||
{ key: 'people', label: '员工', icon: 'users' },
|
{ key: 'people', label: '员工', icon: 'users' },
|
||||||
{ key: 'ledgers', label: '台账', icon: 'database' },
|
{ key: 'ledgers', label: '台账', icon: 'table' },
|
||||||
{ key: 'knowledge', label: '知识库', icon: 'layers' },
|
{ key: 'knowledge', label: '知识库', icon: 'layers' },
|
||||||
{ key: 'system', label: '系统', icon: 'settings' },
|
{ key: 'system', label: '系统', icon: 'settings' },
|
||||||
];
|
];
|
||||||
@@ -75,9 +75,9 @@ export const ZM: Readonly<Record<GovZone, { title: string; desc: string; accent:
|
|||||||
},
|
},
|
||||||
ledgers: {
|
ledgers: {
|
||||||
title: '台账管理',
|
title: '台账管理',
|
||||||
icon: 'database',
|
icon: 'table',
|
||||||
accent: 'linear-gradient(140deg,#245bdb,#16307a)',
|
accent: 'linear-gradient(140deg,#245bdb,#16307a)',
|
||||||
desc: '行数据 · 版本 · 引用——域的 schema 在设计中心改,这里做运营动作。导航按数据域动态生成:设计侧一发布新域,运营侧自动长出一位。',
|
desc: '行数据 · 版本 · 引用——域的结构在设计中心改,这里做运营动作。域即页签,随数据域发布生长:设计侧一发布新域,台账里自动多一个域页签。',
|
||||||
},
|
},
|
||||||
knowledge: {
|
knowledge: {
|
||||||
title: '知识库',
|
title: '知识库',
|
||||||
@@ -157,7 +157,7 @@ export const TABS: Readonly<Record<GovZone, ReadonlyArray<{ key: string; label:
|
|||||||
{ key: 'roles', label: '岗位授权', ico: 'key' },
|
{ key: 'roles', label: '岗位授权', ico: 'key' },
|
||||||
{ key: 'groups', label: '工作组', ico: 'users' },
|
{ key: 'groups', label: '工作组', ico: 'users' },
|
||||||
],
|
],
|
||||||
ledgers: [],
|
ledgers: [], // 动态:页签即数据域(短名呈现),由壳层按已发布域生成
|
||||||
knowledge: [{ key: 'layers', label: '活动记忆', ico: 'layers' }],
|
knowledge: [{ key: 'layers', label: '活动记忆', ico: 'layers' }],
|
||||||
system: [
|
system: [
|
||||||
{ key: 'llm', label: 'LLM 实例', ico: 'cpu' },
|
{ key: 'llm', label: 'LLM 实例', ico: 'cpu' },
|
||||||
|
|||||||
@@ -30,6 +30,16 @@ export function Ico({ name, s = 18 }: { name: string; s?: number }) {
|
|||||||
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" /></>),
|
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" /></>),
|
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" /></>),
|
||||||
hand: (<><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2" /><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2" /><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8" /><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" /></>),
|
hand: (<><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2" /><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2" /><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8" /><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" /></>),
|
||||||
|
"arrow-left-right": (<><path d="M8 3 4 7l4 4" /><path d="M4 7h16" /><path d="m16 21 4-4-4-4" /><path d="M20 17H4" /></>),
|
||||||
|
"at-sign": (<><circle cx="12" cy="12" r="4" /><path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-4 8" /></>),
|
||||||
|
columns: (<><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M12 3v18" /></>),
|
||||||
|
"help-circle": (<><circle cx="12" cy="12" r="10" /><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" /><path d="M12 17h.01" /></>),
|
||||||
|
link: (<><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" /><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" /></>),
|
||||||
|
package: (<><path d="M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z" /><path d="M12 22V12" /><path d="m3.3 7 7.7 4.7a2 2 0 0 0 2 0L20.7 7" /><path d="m7.5 4.27 9 5.15" /></>),
|
||||||
|
pause: (<><rect x="14" y="4" width="4" height="16" rx="1" /><rect x="6" y="4" width="4" height="16" rx="1" /></>),
|
||||||
|
"scroll-text": (<><path d="M15 12h-5" /><path d="M15 8h-5" /><path d="M19 17V5a2 2 0 0 0-2-2H4" /><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3" /></>),
|
||||||
|
table: (<><path d="M12 3v18" /><rect width="18" height="18" x="3" y="3" rx="2" /><path d="M3 9h18" /><path d="M3 15h18" /></>),
|
||||||
|
tag: (<><path d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z" /><circle cx="7.5" cy="7.5" r=".5" fill="currentColor" /></>),
|
||||||
unplug: (<><path d="m19 5 3-3" /><path d="m2 22 3-3" /><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z" /><path d="M7.5 13.5 10 11" /><path d="M10.5 16.5 13 14" /><path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z" /></>),
|
unplug: (<><path d="m19 5 3-3" /><path d="m2 22 3-3" /><path d="M6.3 20.3a2.4 2.4 0 0 0 3.4 0L12 18l-6-6-2.3 2.3a2.4 2.4 0 0 0 0 3.4Z" /><path d="M7.5 13.5 10 11" /><path d="M10.5 16.5 13 14" /><path d="m12 6 6 6 2.3-2.3a2.4 2.4 0 0 0 0-3.4l-2.6-2.6a2.4 2.4 0 0 0-3.4 0Z" /></>),
|
||||||
coins: (<><circle cx="8" cy="8" r="6" /><path d="M18.09 10.37A6 6 0 1 1 10.34 18" /><path d="M7 6h1v4" /><path d="m16.71 13.88.7.71-2.82 2.82" /></>),
|
coins: (<><circle cx="8" cy="8" r="6" /><path d="M18.09 10.37A6 6 0 1 1 10.34 18" /><path d="M7 6h1v4" /><path d="m16.71 13.88.7.71-2.82 2.82" /></>),
|
||||||
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: (<><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" /></>),
|
||||||
|
|||||||
Reference in New Issue
Block a user