diff --git a/admin/src/pages/dashboard/workplace/index.tsx b/admin/src/pages/dashboard/workplace/index.tsx index f14f4b9..f642bcd 100644 --- a/admin/src/pages/dashboard/workplace/index.tsx +++ b/admin/src/pages/dashboard/workplace/index.tsx @@ -281,6 +281,44 @@ function Workplace(): React.ReactElement { }; }, [overview.radarFeed]); + const interactCount = useMemo( + () => overview.radarFeed.filter((f) => f.stayDurationMs > 10000).length, + [overview.radarFeed] + ); + + const funnelMetrics = useMemo(() => { + const tap = kpis.tapCount; + const view = kpis.viewCount; + const interact = interactCount; + const lead = kpis.newLeadCount; + + const interactRatio = view > 0 ? Math.min(100, Math.round((interact / view) * 100)) : 0; + const leadRatio = view > 0 ? Math.min(100, Math.round((lead / view) * 100)) : 0; + + return { + tap: { + value: tap, + ratioText: '100%', + barWidth: '100%', + }, + view: { + value: view, + ratioText: tap > 0 ? `${Math.min(100, Math.round((view / tap) * 100))}%` : '100%', + barWidth: `${Math.min(100, Math.max(15, tap > 0 ? (view / tap) * 100 : 80))}%`, + }, + interact: { + value: interact, + ratioText: `${interactRatio}%`, + barWidth: `${Math.min(100, Math.max(12, interactRatio))}%`, + }, + lead: { + value: lead, + ratioText: `${leadRatio}%`, + barWidth: `${Math.min(100, Math.max(10, leadRatio))}%`, + }, + }; + }, [kpis.tapCount, kpis.viewCount, kpis.newLeadCount, interactCount]); + // 雷达客商列表(根据过滤器筛选) const filteredFeed = useMemo(() => { if (feedFilter === 'high_intent') { @@ -394,71 +432,62 @@ function Workplace(): React.ReactElement { >
-
{t['workplace.funnel.tap']}
-
{kpis.tapCount}
+
+ {t['workplace.funnel.tap']} + {funnelMetrics.tap.ratioText} +
+
{funnelMetrics.tap.value}
-
+
+
+
+ Base
-
{t['workplace.funnel.view']}
-
{kpis.viewCount}
+
+ {t['workplace.funnel.view']} + {funnelMetrics.view.ratioText} +
+
{funnelMetrics.view.value}
-
0 ? (kpis.viewCount / kpis.tapCount) * 100 : 80) - )}%`, - }} - /> +
+
+
+ Taps → Views
-
{t['workplace.funnel.interact']}
-
- {overview.radarFeed.filter((f) => f.stayDurationMs > 10000).length} +
+ {t['workplace.funnel.interact']} + {funnelMetrics.interact.ratioText}
+
{funnelMetrics.interact.value}
-
0 - ? (overview.radarFeed.filter((f) => f.stayDurationMs > 10000).length / - kpis.viewCount) * - 100 - : 50 - ) - )}%`, - }} - /> +
+
+
+ Views → Engaged
-
{t['workplace.funnel.lead']}
-
{kpis.newLeadCount}
+
+ {t['workplace.funnel.lead']} + {funnelMetrics.lead.ratioText} +
+
{funnelMetrics.lead.value}
0 ? (kpis.newLeadCount / kpis.viewCount) * 100 : 25 - ) - )}%`, - }} + style={{ width: funnelMetrics.lead.barWidth }} />
+
+ Engaged → Leads +
diff --git a/admin/src/pages/dashboard/workplace/style/index.module.less b/admin/src/pages/dashboard/workplace/style/index.module.less index 109bd35..0402f76 100644 --- a/admin/src/pages/dashboard/workplace/style/index.module.less +++ b/admin/src/pages/dashboard/workplace/style/index.module.less @@ -1,8 +1,7 @@ .page { min-height: 100%; width: 100%; - max-width: 1200px; - margin: 0 auto; + margin: 0; font-family: var(--omni-font-family); box-sizing: border-box; } @@ -191,32 +190,46 @@ .funnelStep { display: flex; flex-direction: column; + position: relative; +} + +.funnelHeader { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: var(--omni-space-4); } .funnelLabel { font-size: 13px; + font-weight: 500; color: var(--omni-color-text-2); - margin-bottom: var(--omni-space-4); +} + +.funnelRate { + font-size: 12px; + color: var(--omni-color-primary); + font-weight: 600; } .funnelValue { - font-size: 20px; + font-size: 22px; font-weight: 600; color: var(--omni-color-text); margin-bottom: var(--omni-space-8); } .funnelBarWrapper { - height: 4px; - background: var(--omni-color-surface); - border-radius: 2px; + height: 6px; + background: rgba(11, 92, 255, 0.08); + border-radius: 3px; overflow: hidden; } .funnelBar { height: 100%; background: var(--omni-color-primary); - border-radius: 2px; + border-radius: 3px; transition: width 300ms ease; } @@ -224,6 +237,15 @@ background: var(--omni-color-success); } +.funnelMeta { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: var(--omni-space-4); + font-size: 11px; + color: var(--omni-color-text-2); +} + .radarCard { margin-top: var(--omni-space-16); } diff --git a/admin/tests/workplace-layout-alignment.test.ts b/admin/tests/workplace-layout-alignment.test.ts new file mode 100644 index 0000000..d092b8c --- /dev/null +++ b/admin/tests/workplace-layout-alignment.test.ts @@ -0,0 +1,79 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'path'; +import { fileURLToPath } from 'node:url'; + +const adminRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +function read(relPath: string): string { + return fs.readFileSync(path.join(adminRoot, relPath), 'utf-8'); +} + +const EMOJI_RE = + /[\u{1F300}-\u{1F5FF}\u{1F600}-\u{1F64F}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; + +test('1. Workplace 根容器必须全宽自然左对齐,严禁 margin: 0 auto 产生脱节白区', () => { + const styles = read('src/pages/dashboard/workplace/style/index.module.less'); + + // 必须全宽 + assert.ok(styles.includes('width: 100%'), 'Workplace 根元素必须 width: 100%'); + + // 严禁在 .page 根容器使用 margin: 0 auto,防止在大屏产生 200px+ 的左侧留白与错位 + const pageRuleMatch = styles.match(/\.page\s*\{([^}]+)\}/); + assert.ok(pageRuleMatch, '必须包含 .page 规则'); + const pageRule = pageRuleMatch[1]; + assert.ok( + !pageRule.includes('margin: 0 auto') && !pageRule.includes('margin:0 auto'), + 'Workplace .page 严禁使用 margin: 0 auto 导致与面包屑脱节错位' + ); + assert.ok( + !pageRule.includes('max-width: 1200px'), + 'Workplace 作为复杂工作台仪表盘不应在宽屏下硬锁 1200px 造成两侧悬空' + ); +}); + +test('2. 转化漏斗 (Conversion Funnel) 必须具有高对比度轨道底色、增厚条与流转连贯设计', () => { + const styles = read('src/pages/dashboard/workplace/style/index.module.less'); + const tsx = read('src/pages/dashboard/workplace/index.tsx'); + + // 轨道背景不可仅为近乎隐形的 omni-color-surface,必须有明显底色(如 rgba(11, 92, 255, 0.08) 或边框) + assert.ok( + styles.includes('rgba(11, 92, 255') || styles.includes('--omni-color-primary-soft'), + '漏斗条轨道槽必须具备清晰可见的对比度底色' + ); + + // 漏斗条高度提升至至少 6px + assert.ok( + styles.includes('height: 6px') || styles.includes('height: 8px'), + '漏斗条高度必须至少 6px 提升视觉分量' + ); + + // 必须展示漏斗转化比率或流转指标 + assert.ok( + tsx.includes('funnelRate') || tsx.includes('funnelRatio') || tsx.includes('funnelMeta'), + '漏斗各步骤必须展示转化比率或流转说明' + ); +}); + +test('3. 快捷导航卡片副标题必须具备单行文本防截断样式', () => { + const styles = read('src/pages/dashboard/workplace/style/index.module.less'); + + assert.ok( + styles.includes('text-overflow: ellipsis') || styles.includes('overflow: hidden'), + '快捷卡片副标题必须具备溢出隐藏保护' + ); + assert.ok( + styles.includes('white-space: nowrap'), + '快捷卡片副标题必须保持单行不发生难看的断字折行' + ); +}); + +test('4. 零 any,零 emoji,符合出海国际化商务规范', () => { + const tsx = read('src/pages/dashboard/workplace/index.tsx'); + const styles = read('src/pages/dashboard/workplace/style/index.module.less'); + + assert.ok(!/\bany\b/.test(tsx), 'Workplace 代码严禁使用 any'); + assert.ok(!EMOJI_RE.test(tsx), 'Workplace 代码严禁使用 emoji'); + assert.ok(!EMOJI_RE.test(styles), 'Workplace 样式严禁使用 emoji'); +});