feat: 同步管理端、用户端与后端业务,并接通线上 Web 接口
补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
@@ -5,6 +5,20 @@ import path from 'node:path';
|
||||
|
||||
const MP_ROOT = path.resolve('m:/2026项目/CRMmp/mpuniapp');
|
||||
const articlesUtsPath = path.join(MP_ROOT, 'pages/owner/card/useCardArticles.uts');
|
||||
const articlesPagePath = path.join(MP_ROOT, 'pages/owner/card/articles.uvue');
|
||||
|
||||
/** 禁止预填的示例文章 / 假标题 / 假封面 */
|
||||
const SAMPLE_BAN_PATTERNS: RegExp[] = [
|
||||
/2026\s*全球智能硬件合规白皮书/,
|
||||
/全球智能硬件合规白皮书/,
|
||||
/DEMO_ARTICLES/,
|
||||
/SAMPLE_ARTICLES/,
|
||||
/MOCK_ARTICLES/,
|
||||
/placeholder\.png/i,
|
||||
/fake[-_]?cover/i,
|
||||
/unsplash\.com/i,
|
||||
/picsum\.photos/i,
|
||||
];
|
||||
|
||||
test('1. useCardArticles.uts 文件必须存在且格式完整', () => {
|
||||
assert.ok(fs.existsSync(articlesUtsPath), '必须存在 useCardArticles.uts');
|
||||
@@ -24,13 +38,50 @@ test('2. 摘要提取器必须支持剥离 Markdown 与 HTML 语法', () => {
|
||||
assert.ok(content.includes('.replace(/!\\[.*?\\]\\(.*?\\)/g, \'\')'), '必须剥离 Markdown 图片');
|
||||
});
|
||||
|
||||
test('3. 文章表单校验逻辑实现规范', () => {
|
||||
test('3. R41: 空列表必须 empty=true,禁止注入示例文章数组', () => {
|
||||
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
|
||||
|
||||
assert.ok(
|
||||
content.includes('state.empty = items.length === 0'),
|
||||
'loadArticles 必须按真实列表长度设置 empty'
|
||||
);
|
||||
assert.ok(content.includes('state.list = items'), '列表必须使用接口返回数据');
|
||||
|
||||
assert.ok(
|
||||
!/state\.list\s*=\s*\[[\s\S]*?title\s*:/.test(content),
|
||||
'禁止用内联示例文章数组填充 list'
|
||||
);
|
||||
assert.ok(!content.includes('DEMO_ARTICLES'), '禁止 DEMO_ARTICLES');
|
||||
assert.ok(!content.includes('SAMPLE_ARTICLES'), '禁止 SAMPLE_ARTICLES');
|
||||
assert.ok(!content.includes('MOCK_ARTICLES'), '禁止 MOCK_ARTICLES');
|
||||
});
|
||||
|
||||
test('4. R41: 禁止示例标题 / 假封面,新建表单字段为空', () => {
|
||||
const uts = fs.readFileSync(articlesUtsPath, 'utf-8');
|
||||
const page = fs.readFileSync(articlesPagePath, 'utf-8');
|
||||
|
||||
for (const pattern of SAMPLE_BAN_PATTERNS) {
|
||||
assert.ok(!pattern.test(uts), `useCardArticles 禁止示例数据: ${pattern}`);
|
||||
assert.ok(!pattern.test(page), `articles.uvue 禁止示例数据: ${pattern}`);
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
page.includes("editForm.title = ''") && page.includes("editForm.content = ''"),
|
||||
'新建时 title/content 必须置为空字符串'
|
||||
);
|
||||
assert.ok(
|
||||
page.includes("editForm.coverImage = ''") || page.includes("uploadedCover.value = []"),
|
||||
'新建时封面必须为空,禁止假封面'
|
||||
);
|
||||
});
|
||||
|
||||
test('5. 文章表单校验逻辑实现规范', () => {
|
||||
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
|
||||
assert.ok(content.includes("title === ''"), '必须拦截空标题');
|
||||
assert.ok(content.includes("content === ''"), '必须拦截空正文');
|
||||
});
|
||||
|
||||
test('4. 必须通过 Cool-Admin 标准 CRUD 契约对接后端接口', () => {
|
||||
test('6. 必须通过 Cool-Admin 标准 CRUD 契约对接后端接口', () => {
|
||||
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
|
||||
assert.ok(content.includes('fetchCardArticlesPage'), '必须调用 fetchCardArticlesPage');
|
||||
assert.ok(content.includes('addCardArticle'), '必须调用 addCardArticle');
|
||||
@@ -38,9 +89,47 @@ test('4. 必须通过 Cool-Admin 标准 CRUD 契约对接后端接口', () => {
|
||||
assert.ok(content.includes('deleteCardArticle'), '必须调用 deleteCardArticle');
|
||||
});
|
||||
|
||||
test('5. 国际化与文案键完整覆盖', () => {
|
||||
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
|
||||
assert.ok(content.includes('resolveArticlesText'), '必须实现文案解析');
|
||||
assert.ok(content.includes('Insights & Articles'), '必须支持英文标题');
|
||||
assert.ok(content.includes('文章与商务动态'), '必须支持中文标题');
|
||||
test('7. 国际化与文案键完整覆盖,模板禁止硬编码中文', () => {
|
||||
const uts = fs.readFileSync(articlesUtsPath, 'utf-8');
|
||||
const page = fs.readFileSync(articlesPagePath, 'utf-8');
|
||||
|
||||
assert.ok(uts.includes('resolveArticlesText'), '必须实现文案解析');
|
||||
assert.ok(uts.includes('Insights & Articles'), '必须支持英文标题');
|
||||
assert.ok(uts.includes('文章与商务动态'), '必须支持中文标题');
|
||||
assert.ok(uts.includes('emptyTitle'), '必须含空态标题词条');
|
||||
assert.ok(uts.includes('emptyDesc'), '必须含空态说明词条');
|
||||
|
||||
const templateMatch = page.match(/<template>[\s\S]*?<\/template>/);
|
||||
assert.ok(templateMatch, '必须有 template 块');
|
||||
const template = templateMatch![0];
|
||||
assert.ok(
|
||||
!/placeholder="[^"]*[\u4e00-\u9fff]/.test(template),
|
||||
'模板 placeholder 禁止硬编码中文'
|
||||
);
|
||||
assert.ok(template.includes("t('emptyTitle')"), '空态标题必须走 i18n');
|
||||
assert.ok(template.includes("t('emptyDesc')"), '空态说明必须走 i18n');
|
||||
assert.ok(template.includes("t('publishBtn')"), '主 CTA 文案必须走 i18n');
|
||||
});
|
||||
|
||||
test('8. R41: 空列表展示空态,一屏仅 1 个主 CTA', () => {
|
||||
const page = fs.readFileSync(articlesPagePath, 'utf-8');
|
||||
|
||||
assert.ok(page.includes('state.empty'), '必须根据 state.empty 分支');
|
||||
assert.ok(page.includes('articles-empty-card'), '必须有空态卡片');
|
||||
assert.ok(page.includes("t('emptyTitle')"), '空态必须显示 emptyTitle');
|
||||
assert.ok(page.includes("t('emptyDesc')"), '空态必须显示 emptyDesc');
|
||||
|
||||
const templateMatch = page.match(/<template>[\s\S]*?<\/template>/);
|
||||
assert.ok(templateMatch, '必须有 template 块');
|
||||
const template = templateMatch![0];
|
||||
|
||||
const bottomBarMatch = template.match(
|
||||
/articles-bottom-bar[\s\S]*?<\/view>\s*(?:<!--[\s\S]*?-->\s*)*<view v-if="state\.drawerVisible"/
|
||||
);
|
||||
assert.ok(bottomBarMatch, '必须存在底部主 CTA 栏');
|
||||
const bottomBar = bottomBarMatch![0];
|
||||
const ctaInBar = (bottomBar.match(/class="primary-cta-btn"/g) || []).length;
|
||||
assert.equal(ctaInBar, 1, '底部栏一屏仅允许 1 个 primary-cta-btn');
|
||||
assert.ok(bottomBar.includes('handleOpenAdd'), '主 CTA 必须是发布动态');
|
||||
assert.ok(bottomBar.includes("t('publishBtn')"), '主 CTA 文案必须是 publishBtn');
|
||||
});
|
||||
|
||||
@@ -5,6 +5,20 @@ import path from 'node:path';
|
||||
|
||||
const MP_ROOT = path.resolve('m:/2026项目/CRMmp/mpuniapp');
|
||||
const casesUtsPath = path.join(MP_ROOT, 'pages/owner/card/useCardCases.uts');
|
||||
const casesPagePath = path.join(MP_ROOT, 'pages/owner/card/cases.uvue');
|
||||
|
||||
/** 禁止预填的示例案例 / 客户 / 假 KPI */
|
||||
const SAMPLE_BAN_PATTERNS: RegExp[] = [
|
||||
/全球智能硬件数字化交付方案/,
|
||||
/阿联酋国际商业联合会/,
|
||||
/触达\s*10,000\+?\s*买家/,
|
||||
/\+320%\s*ROI/i,
|
||||
/\+150%\s*留资/,
|
||||
/45k\+?\s*国际线索/i,
|
||||
/2M\+?\s*全球曝光/i,
|
||||
/Top\s*1\s*细分类目/i,
|
||||
/VoltRide/i,
|
||||
];
|
||||
|
||||
test('1. useCardCases.uts 文件必须存在且格式完整', () => {
|
||||
assert.ok(fs.existsSync(casesUtsPath), '必须存在 useCardCases.uts');
|
||||
@@ -14,26 +28,55 @@ test('1. useCardCases.uts 文件必须存在且格式完整', () => {
|
||||
assert.ok(content.includes('export async function loadCases'), '必须导出 loadCases');
|
||||
assert.ok(content.includes('export async function saveCase'), '必须导出 saveCase');
|
||||
assert.ok(content.includes('export async function deleteCase'), '必须导出 deleteCase');
|
||||
assert.ok(content.includes('export const PRESET_CASE_METRICS'), '必须导出 PRESET_CASE_METRICS');
|
||||
assert.ok(content.includes('export function resolveCasesText'), '必须导出 resolveCasesText');
|
||||
});
|
||||
|
||||
test('2. 预设指标标签完备且包含国际业务核心量化成效', () => {
|
||||
test('2. R40: 空列表必须 empty=true,禁止注入示例案例数组', () => {
|
||||
const content = fs.readFileSync(casesUtsPath, 'utf-8');
|
||||
assert.ok(content.includes('+320% ROI'), '必须包含 +320% ROI');
|
||||
assert.ok(content.includes('+150% 留资率'), '必须包含 +150% 留资率');
|
||||
assert.ok(content.includes('45k+ 国际线索'), '必须包含 45k+ 国际线索');
|
||||
assert.ok(content.includes('2M+ 全球曝光'), '必须包含 2M+ 全球曝光');
|
||||
assert.ok(content.includes('Top 1 细分类目'), '必须包含 Top 1 细分类目');
|
||||
|
||||
assert.ok(
|
||||
content.includes('state.empty = items.length === 0'),
|
||||
'loadCases 必须按真实列表长度设置 empty'
|
||||
);
|
||||
assert.ok(content.includes('state.list = items'), '列表必须使用接口返回数据');
|
||||
|
||||
// 禁止空列表时回退到本地示例数组
|
||||
assert.ok(
|
||||
!/state\.list\s*=\s*\[[\s\S]*?title\s*:/.test(content),
|
||||
'禁止用内联示例案例数组填充 list'
|
||||
);
|
||||
assert.ok(!content.includes('DEMO_CASES'), '禁止 DEMO_CASES');
|
||||
assert.ok(!content.includes('SAMPLE_CASES'), '禁止 SAMPLE_CASES');
|
||||
assert.ok(!content.includes('MOCK_CASES'), '禁止 MOCK_CASES');
|
||||
});
|
||||
|
||||
test('3. 案例表单校验逻辑实现规范', () => {
|
||||
test('3. R40: 禁止预设假 KPI / 示例客户,metrics 新建为空', () => {
|
||||
const uts = fs.readFileSync(casesUtsPath, 'utf-8');
|
||||
const page = fs.readFileSync(casesPagePath, 'utf-8');
|
||||
|
||||
assert.ok(!uts.includes('PRESET_CASE_METRICS'), '禁止 PRESET_CASE_METRICS 假 KPI 常量');
|
||||
assert.ok(!page.includes('PRESET_CASE_METRICS'), '页面禁止引用预设 KPI');
|
||||
assert.ok(!page.includes('preset-metric-pill'), '禁止预设 KPI 胶囊点击注入');
|
||||
assert.ok(!page.includes('handleSelectMetric'), '禁止点击注入预设 KPI');
|
||||
|
||||
for (const pattern of SAMPLE_BAN_PATTERNS) {
|
||||
assert.ok(!pattern.test(uts), `useCardCases 禁止示例数据: ${pattern}`);
|
||||
assert.ok(!pattern.test(page), `cases.uvue 禁止示例数据: ${pattern}`);
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
/metrics:\s*['"]{2}/.test(page) || page.includes("editForm.metrics = ''"),
|
||||
'新建时 metrics 必须置为空字符串'
|
||||
);
|
||||
});
|
||||
|
||||
test('4. 案例表单校验逻辑实现规范', () => {
|
||||
const content = fs.readFileSync(casesUtsPath, 'utf-8');
|
||||
// 必须对标题非空和超长进行拦截
|
||||
assert.ok(content.includes("title === ''"), '必须拦截空标题');
|
||||
assert.ok(content.includes('title.length > 150'), '必须拦截超过 150 字符的标题');
|
||||
});
|
||||
|
||||
test('4. 必须通过 Cool-Admin 标准 CRUD 契约对接后端接口', () => {
|
||||
test('5. 必须通过 Cool-Admin 标准 CRUD 契约对接后端接口', () => {
|
||||
const content = fs.readFileSync(casesUtsPath, 'utf-8');
|
||||
assert.ok(content.includes('fetchCardCasesPage'), '必须调用 fetchCardCasesPage');
|
||||
assert.ok(content.includes('addCardCase'), '必须调用 addCardCase');
|
||||
@@ -41,9 +84,48 @@ test('4. 必须通过 Cool-Admin 标准 CRUD 契约对接后端接口', () => {
|
||||
assert.ok(content.includes('deleteCardCase'), '必须调用 deleteCardCase');
|
||||
});
|
||||
|
||||
test('5. 国际化与文案键完整覆盖', () => {
|
||||
const content = fs.readFileSync(casesUtsPath, 'utf-8');
|
||||
assert.ok(content.includes('resolveCasesText'), '必须实现文案解析');
|
||||
assert.ok(content.includes('Portfolio & Case Studies'), '必须支持英文标题');
|
||||
assert.ok(content.includes('产品相册与标杆案例'), '必须支持中文标题');
|
||||
test('6. 国际化与文案键完整覆盖,模板禁止硬编码中文', () => {
|
||||
const uts = fs.readFileSync(casesUtsPath, 'utf-8');
|
||||
const page = fs.readFileSync(casesPagePath, 'utf-8');
|
||||
|
||||
assert.ok(uts.includes('resolveCasesText'), '必须实现文案解析');
|
||||
assert.ok(uts.includes('Portfolio & Case Studies'), '必须支持英文标题');
|
||||
assert.ok(uts.includes('产品与案例') || uts.includes('产品相册与标杆案例'), '必须支持中文标题');
|
||||
assert.ok(uts.includes('emptyTitle'), '必须含空态标题词条');
|
||||
assert.ok(uts.includes('emptyDesc'), '必须含空态说明词条');
|
||||
|
||||
// 模板可见文案走 t() / resolveCasesText,禁止 placeholder 硬编码中文示例
|
||||
const templateMatch = page.match(/<template>[\s\S]*?<\/template>/);
|
||||
assert.ok(templateMatch, '必须有 template 块');
|
||||
const template = templateMatch![0];
|
||||
assert.ok(
|
||||
!/placeholder="[^"]*[\u4e00-\u9fff]/.test(template),
|
||||
'模板 placeholder 禁止硬编码中文'
|
||||
);
|
||||
assert.ok(template.includes("t('emptyTitle')"), '空态标题必须走 i18n');
|
||||
assert.ok(template.includes("t('emptyDesc')"), '空态说明必须走 i18n');
|
||||
assert.ok(template.includes("t('addBtn')"), '主 CTA 文案必须走 i18n');
|
||||
});
|
||||
|
||||
test('7. R40: 空列表展示空态,一屏仅 1 个主 CTA', () => {
|
||||
const page = fs.readFileSync(casesPagePath, 'utf-8');
|
||||
|
||||
assert.ok(page.includes('state.empty'), '必须根据 state.empty 分支');
|
||||
assert.ok(page.includes('cases-empty-card'), '必须有空态卡片');
|
||||
assert.ok(page.includes("t('emptyTitle')"), '空态必须显示 emptyTitle');
|
||||
assert.ok(page.includes("t('emptyDesc')"), '空态必须显示 emptyDesc');
|
||||
|
||||
const templateMatch = page.match(/<template>[\s\S]*?<\/template>/);
|
||||
assert.ok(templateMatch, '必须有 template 块');
|
||||
const template = templateMatch![0];
|
||||
|
||||
// 主屏底部仅一处 primary-cta-btn(抽屉内保存可另计,但主列表区底部栏只能 1 个)
|
||||
const bottomBarMatch = template.match(
|
||||
/cases-bottom-bar[\s\S]*?<\/view>\s*(?:<!--[\s\S]*?-->\s*)*<view v-if="state\.drawerVisible"/
|
||||
);
|
||||
assert.ok(bottomBarMatch, '必须存在底部主 CTA 栏');
|
||||
const bottomBar = bottomBarMatch![0];
|
||||
const ctaInBar = (bottomBar.match(/class="primary-cta-btn"/g) || []).length;
|
||||
assert.equal(ctaInBar, 1, '底部栏一屏仅允许 1 个 primary-cta-btn');
|
||||
assert.ok(bottomBar.includes('handleOpenAdd'), '主 CTA 必须是新增案例');
|
||||
});
|
||||
|
||||
@@ -57,6 +57,8 @@ test('3. 工作台状态机 useWorkplace.uts 逻辑完整性审查', () => {
|
||||
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、安全区避让、沉浸式且无遮挡悬浮按钮)', () => {
|
||||
@@ -81,3 +83,82 @@ test('4. 工作台视图 index.uvue 结构与规范审查 (Bento Grid 模组、
|
||||
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 栏');
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户