补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
165 行
9.2 KiB
TypeScript
165 行
9.2 KiB
TypeScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
|
||
const MP_ROOT = path.resolve('m:/2026项目/CRMmp/mpuniapp');
|
||
const pagesJsonPath = path.join(MP_ROOT, 'pages.json');
|
||
const workplaceUvuePath = path.join(MP_ROOT, 'pages/owner/workplace/index.uvue');
|
||
const workplaceUtsPath = path.join(MP_ROOT, 'pages/owner/workplace/useWorkplace.uts');
|
||
const apiUtsPath = path.join(MP_ROOT, 'common/api.uts');
|
||
const typesUtsPath = path.join(MP_ROOT, 'common/types.uts');
|
||
|
||
test('1. pages.json 必须规范配置 5 栏纯正 TabBar (工作台、名片、雷达、潜客、我的)', () => {
|
||
assert.ok(fs.existsSync(pagesJsonPath), 'pages.json 必须存在');
|
||
const content = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||
const config = JSON.parse(content);
|
||
|
||
const tabBarList = config.tabBar?.list || [];
|
||
assert.equal(tabBarList.length, 5, 'TabBar 应当精确包含 5 个核心工作台 Tab');
|
||
|
||
// 验证 5 个 Tab 顺序与路由
|
||
assert.equal(tabBarList[0].pagePath, 'pages/owner/workplace/index', 'TabBar 第 1 项必须是工作台');
|
||
assert.equal(tabBarList[0].text, '工作台');
|
||
|
||
assert.equal(tabBarList[1].pagePath, 'pages/owner/card/index', 'TabBar 第 2 项必须是名片');
|
||
assert.equal(tabBarList[1].text, '名片');
|
||
|
||
assert.equal(tabBarList[2].pagePath, 'pages/owner/radar', 'TabBar 第 3 项必须是雷达');
|
||
assert.equal(tabBarList[2].text, '雷达');
|
||
|
||
assert.equal(tabBarList[3].pagePath, 'pages/owner/leads', 'TabBar 第 4 项必须是潜客');
|
||
assert.equal(tabBarList[3].text, '潜客');
|
||
|
||
assert.equal(tabBarList[4].pagePath, 'pages/owner/profile/index', 'TabBar 第 5 项必须是我的');
|
||
assert.equal(tabBarList[4].text, '我的');
|
||
|
||
// 验证 pages[0] 首屏页面为工作台首页且为沉浸式导航
|
||
assert.equal(config.pages[0].path, 'pages/owner/workplace/index', '应用首屏 pages[0] 应设为工作台');
|
||
assert.equal(config.pages[0].style?.navigationStyle, 'custom', '工作台页面应采用沉浸式 custom 导航,消除原生白底标题栏');
|
||
});
|
||
|
||
test('2. 核心契约与接口复用审查 (fetchWorkplaceOverview 与 WorkplaceOverviewResult)', () => {
|
||
const typesContent = fs.readFileSync(typesUtsPath, 'utf-8');
|
||
assert.ok(typesContent.includes('WorkplaceOverviewResult'), 'types.uts 必须导出 WorkplaceOverviewResult');
|
||
assert.ok(typesContent.includes('WorkplaceKpi'), 'types.uts 必须导出 WorkplaceKpi');
|
||
assert.ok(typesContent.includes('WorkplaceRadarItem'), 'types.uts 必须导出 WorkplaceRadarItem');
|
||
|
||
const apiContent = fs.readFileSync(apiUtsPath, 'utf-8');
|
||
assert.ok(apiContent.includes('fetchWorkplaceOverview'), 'api.uts 必须封装 fetchWorkplaceOverview');
|
||
assert.ok(apiContent.includes('/admin/crm/radar/workplace'), '接口必须对齐后端 /admin/crm/radar/workplace');
|
||
});
|
||
|
||
test('3. 工作台状态机 useWorkplace.uts 逻辑完整性审查', () => {
|
||
assert.ok(fs.existsSync(workplaceUtsPath), '必须存在 useWorkplace.uts 业务逻辑文件');
|
||
const content = fs.readFileSync(workplaceUtsPath, 'utf-8');
|
||
|
||
assert.ok(content.includes('createWorkplaceState'), '必须提供 createWorkplaceState');
|
||
assert.ok(content.includes('loadWorkplaceData'), '必须提供 loadWorkplaceData 聚合加载逻辑');
|
||
assert.ok(content.includes('resolveWorkplaceText'), '必须提供国际化文案解析 resolveWorkplaceText');
|
||
assert.ok(content.includes('fetchCardQrcodeSvg'), 'loadWorkplaceData 必须复用已有 fetchCardQrcodeSvg');
|
||
assert.ok(content.includes('activeQrSvg'), '状态机必须持有 activeQrSvg');
|
||
});
|
||
|
||
test('4. 工作台视图 index.uvue 结构与规范审查 (Bento Grid 模组、零 Emoji、安全区避让、沉浸式且无遮挡悬浮按钮)', () => {
|
||
assert.ok(fs.existsSync(workplaceUvuePath), '必须存在 workplace/index.uvue 视图文件');
|
||
const content = fs.readFileSync(workplaceUvuePath, 'utf-8');
|
||
|
||
// 严禁装饰性 Emoji
|
||
const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
|
||
assert.equal(emojiRegex.test(content), false, 'workplace/index.uvue 严禁含有 Emoji');
|
||
|
||
// 必须包含核心 Bento 模组与沉浸式顶栏
|
||
assert.ok(content.includes('workplace-topbar'), '必须包含沉浸式工作台顶栏');
|
||
assert.ok(content.includes('quick-showcase-section'), '必须包含闪电出示微展台模组');
|
||
assert.ok(content.includes('kpi-grid-section'), '必须包含 2x2 Bento 战情报表模组');
|
||
assert.ok(content.includes('radar-pulse-section'), '必须包含实时雷达高光模组');
|
||
assert.ok(content.includes('leads-todo-section'), '必须包含今日待办商机模组');
|
||
|
||
// 移除底部遮挡内容的悬浮主 CTA 栏 (一屏单主操作由微展台承载,名片Tab独立)
|
||
assert.equal(content.includes('workplace-cta-bar'), false, '严禁包含遮挡内容的常驻底部悬浮主 CTA 栏');
|
||
|
||
// 必须避让安全区与 window-bottom
|
||
assert.ok(content.includes('safe-area-inset-bottom'), '必须适配 env(safe-area-inset-bottom)');
|
||
assert.ok(content.includes('var(--window-bottom'), 'TabBar 页面必须避让 var(--window-bottom)');
|
||
});
|
||
|
||
test('5. 有真实 svg 时渲染二维码;无码/失败仅空态文案,禁止伪码占位', () => {
|
||
assert.ok(fs.existsSync(workplaceUvuePath), '必须存在 workplace/index.uvue');
|
||
assert.ok(fs.existsSync(workplaceUtsPath), '必须存在 useWorkplace.uts');
|
||
|
||
const pageSource = fs.readFileSync(workplaceUvuePath, 'utf-8');
|
||
const utsSource = fs.readFileSync(workplaceUtsPath, 'utf-8');
|
||
const template = pageSource.split('<script')[0];
|
||
const styleBlock = pageSource.includes('<style')
|
||
? pageSource.split('<style')[1]
|
||
: '';
|
||
|
||
// 有码:rich-text 绑定真实 svg
|
||
assert.ok(template.includes('<rich-text'), '有 svg 时必须用 rich-text 渲染');
|
||
assert.ok(
|
||
template.includes('state.activeQrSvg') && template.includes(':nodes='),
|
||
'rich-text 必须绑定 activeQrSvg'
|
||
);
|
||
assert.ok(
|
||
/v-if="state\.activeQrSvg\s*!==\s*''"/.test(template) ||
|
||
/v-if="state\.activeQrSvg\s*!=\s*''"/.test(template),
|
||
'仅在 activeQrSvg 非空时渲染码图'
|
||
);
|
||
|
||
// 无码:仅空态文案,走已有 i18n
|
||
assert.ok(template.includes('showcase-qr-empty'), '无 svg 时必须显示空态文案节点');
|
||
assert.ok(template.includes('<text') && template.includes('showcase-qr-empty'), '空态必须是 text 文案,不是占位块');
|
||
assert.ok(
|
||
pageSource.includes("t('workplace.qr_empty')") ||
|
||
pageSource.includes('workplace.qr_empty') ||
|
||
(utsSource.includes("'common.empty'") && pageSource.includes('getOmniText')),
|
||
'空态文案必须走已有 i18n,禁止硬编码'
|
||
);
|
||
assert.ok(
|
||
utsSource.includes("'workplace.qr_empty'") || utsSource.includes("'common.empty'"),
|
||
'useWorkplace 必须提供空态 i18n 词条'
|
||
);
|
||
|
||
// 禁止假方块 / 占位块冒充二维码(模板 + 样式)
|
||
assert.equal(template.includes('showcase-qr-placeholder'), false, '严禁 showcase-qr-placeholder 伪码占位');
|
||
assert.equal(template.includes('qr-pulse-block'), false, '严禁 qr-pulse-block 假方块');
|
||
assert.equal(styleBlock.includes('qr-pulse-block'), false, '样式中严禁残留伪码方块');
|
||
assert.equal(styleBlock.includes('showcase-qr-placeholder'), false, '样式中严禁残留伪码占位');
|
||
assert.equal(pageSource.includes('fake-qr'), false, '严禁 fake-qr');
|
||
assert.equal(pageSource.includes('decorative-qr'), false, '严禁 decorative-qr');
|
||
assert.equal(pageSource.includes('placeholder-qr'), false, '严禁 placeholder-qr');
|
||
// 虚线框 + 灰块会像一枚未生成的码,空态容器不得用 dashed 冒充码框
|
||
const wrapStyleMatch = styleBlock.match(/\.showcase-qr-wrap\s*\{[^}]*\}/);
|
||
if (wrapStyleMatch != null) {
|
||
assert.equal(
|
||
/border\s*:[^;]*dashed/i.test(wrapStyleMatch[0]),
|
||
false,
|
||
'空态码区不得用 dashed 边框冒充二维码框'
|
||
);
|
||
}
|
||
|
||
// 展台区不得用 URL 文字冒充二维码
|
||
const qrWrapMatch = template.match(/class="showcase-qr-wrap"[\s\S]*?<\/view>\s*<view class="showcase-meta"/);
|
||
assert.ok(qrWrapMatch != null, '必须存在 showcase-qr-wrap 展台码区');
|
||
const qrWrapBlock = qrWrapMatch![0];
|
||
assert.equal(qrWrapBlock.includes('activeCard!.url'), false, '二维码区不得渲染 URL 冒充码');
|
||
assert.equal(qrWrapBlock.includes('currentDisplayLink'), false, '二维码区不得渲染直链冒充码');
|
||
assert.equal(/\{\{\s*[^}]*url[^}]*\}\}/i.test(qrWrapBlock), false, '二维码区不得插值 URL 文本');
|
||
|
||
// 必须拉取已有 fetchCardQrcodeSvg;失败清空,不造假 svg
|
||
assert.ok(utsSource.includes('fetchCardQrcodeSvg'), '必须调用 fetchCardQrcodeSvg');
|
||
assert.ok(utsSource.includes('catch'), '二维码请求必须有失败分支');
|
||
assert.ok(
|
||
/activeQrSvg\s*=\s*['"]['"]/.test(utsSource),
|
||
'请求失败或无 svg 时必须清空 activeQrSvg'
|
||
);
|
||
assert.equal(utsSource.includes('<svg'), false, '严禁手写造假 svg');
|
||
assert.equal(/activeQrSvg\s*=\s*['"][^'"]*<svg/.test(utsSource), false, '严禁赋值假 svg 字符串');
|
||
|
||
// 仍只允许一个主 CTA(主色实心按钮)
|
||
const primaryCtaMatches = template.match(/pulse-action-btn/g) || [];
|
||
assert.equal(primaryCtaMatches.length, 1, '主色实心 CTA 只能有一处 pulse-action-btn');
|
||
assert.equal(template.includes('workplace-cta-bar'), false, '严禁额外底部主 CTA 栏');
|
||
});
|