feat(mpuniapp): 支持在移动端App维护产品相册与文章动态

这个提交包含在:
wikiboss
2026-09-21 14:59:34 +08:00
父节点 15ad5ef90c
当前提交 10b9a6b263
共有 13 个文件被更改,包括 4104 次插入 和 365 次删除

查看文件

@@ -0,0 +1,46 @@
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 articlesUtsPath = path.join(MP_ROOT, 'pages/owner/card/useCardArticles.uts');
test('1. useCardArticles.uts 文件必须存在且格式完整', () => {
assert.ok(fs.existsSync(articlesUtsPath), '必须存在 useCardArticles.uts');
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
assert.ok(content.includes('export function createArticlesState'), '必须导出 createArticlesState');
assert.ok(content.includes('export function extractArticleSummary'), '必须导出 extractArticleSummary');
assert.ok(content.includes('export function validateArticleForm'), '必须导出 validateArticleForm');
assert.ok(content.includes('export async function loadArticles'), '必须导出 loadArticles');
assert.ok(content.includes('export async function saveArticle'), '必须导出 saveArticle');
assert.ok(content.includes('export async function toggleArticlePublish'), '必须导出 toggleArticlePublish');
assert.ok(content.includes('export async function deleteArticle'), '必须导出 deleteArticle');
});
test('2. 摘要提取器必须支持剥离 Markdown 与 HTML 语法', () => {
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
assert.ok(content.includes('.replace(/<[^>]+>/g, \'\')'), '必须剥离 HTML 标签');
assert.ok(content.includes('.replace(/!\\[.*?\\]\\(.*?\\)/g, \'\')'), '必须剥离 Markdown 图片');
});
test('3. 文章表单校验逻辑实现规范', () => {
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
assert.ok(content.includes("title === ''"), '必须拦截空标题');
assert.ok(content.includes("content === ''"), '必须拦截空正文');
});
test('4. 必须通过 Cool-Admin 标准 CRUD 契约对接后端接口', () => {
const content = fs.readFileSync(articlesUtsPath, 'utf-8');
assert.ok(content.includes('fetchCardArticlesPage'), '必须调用 fetchCardArticlesPage');
assert.ok(content.includes('addCardArticle'), '必须调用 addCardArticle');
assert.ok(content.includes('updateCardArticle'), '必须调用 updateCardArticle');
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('文章与商务动态'), '必须支持中文标题');
});

查看文件

@@ -0,0 +1,49 @@
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 casesUtsPath = path.join(MP_ROOT, 'pages/owner/card/useCardCases.uts');
test('1. useCardCases.uts 文件必须存在且格式完整', () => {
assert.ok(fs.existsSync(casesUtsPath), '必须存在 useCardCases.uts');
const content = fs.readFileSync(casesUtsPath, 'utf-8');
assert.ok(content.includes('export function createCasesState'), '必须导出 createCasesState');
assert.ok(content.includes('export function validateCaseForm'), '必须导出 validateCaseForm');
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');
});
test('2. 预设指标标签完备且包含出海核心量化成效', () => {
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 细分类目');
});
test('3. 案例表单校验逻辑实现规范', () => {
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 契约对接后端接口', () => {
const content = fs.readFileSync(casesUtsPath, 'utf-8');
assert.ok(content.includes('fetchCardCasesPage'), '必须调用 fetchCardCasesPage');
assert.ok(content.includes('addCardCase'), '必须调用 addCardCase');
assert.ok(content.includes('updateCardCase'), '必须调用 updateCardCase');
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('产品相册与标杆案例'), '必须支持中文标题');
});

查看文件

@@ -0,0 +1,93 @@
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 casesPath = path.join(MP_ROOT, 'pages/owner/card/cases.uvue');
const articlesPath = path.join(MP_ROOT, 'pages/owner/card/articles.uvue');
test('1. cases.uvue 和 articles.uvue 文件必须存在', () => {
assert.ok(fs.existsSync(casesPath), '必须存在 cases.uvue');
assert.ok(fs.existsSync(articlesPath), '必须存在 articles.uvue');
});
test('2. 零 Emoji 违规审查 (严禁使用 Emoji 作为功能图标或排版内容)', () => {
const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
const casesContent = fs.readFileSync(casesPath, 'utf-8');
const articlesContent = fs.readFileSync(articlesPath, 'utf-8');
assert.equal(emojiRegex.test(casesContent), false, 'cases.uvue 严禁含有 Emoji');
assert.equal(emojiRegex.test(articlesContent), false, 'articles.uvue 严禁含有 Emoji');
});
test('3. 底部悬浮主 CTA 栏适配移动端视口安全区 (env(safe-area-inset-bottom))', () => {
const casesContent = fs.readFileSync(casesPath, 'utf-8');
const articlesContent = fs.readFileSync(articlesPath, 'utf-8');
assert.ok(
casesContent.includes('env(safe-area-inset-bottom)'),
'cases.uvue 必须适配 safe-area-inset-bottom'
);
assert.ok(
articlesContent.includes('env(safe-area-inset-bottom)'),
'articles.uvue 必须适配 safe-area-inset-bottom'
);
});
test('4. 表单输入框与多行文本框符合移动端 16px 防缩放规范', () => {
const casesContent = fs.readFileSync(casesPath, 'utf-8');
const articlesContent = fs.readFileSync(articlesPath, 'utf-8');
assert.ok(casesContent.includes('font-size: 16px;'), 'cases.uvue 输入框必须使用 16px');
assert.ok(articlesContent.includes('font-size: 16px;'), 'articles.uvue 输入框必须使用 16px');
});
test('5. 核心操作按钮触控热区保证 (min-height >= 44px 或 height >= 44px)', () => {
const casesContent = fs.readFileSync(casesPath, 'utf-8');
const articlesContent = fs.readFileSync(articlesPath, 'utf-8');
assert.ok(casesContent.includes('min-height: 44px;'), 'cases.uvue 按钮必须保证触控高度 >= 44px');
assert.ok(articlesContent.includes('min-height: 44px;'), 'articles.uvue 按钮必须保证触控高度 >= 44px');
});
test('6. 必须复用 components/image-uploader/image-uploader.uvue', () => {
const casesContent = fs.readFileSync(casesPath, 'utf-8');
const articlesContent = fs.readFileSync(articlesPath, 'utf-8');
assert.ok(
casesContent.includes("import ImageUploader from '../../../components/image-uploader/image-uploader.uvue'"),
'cases.uvue 必须复用 image-uploader 组件'
);
assert.ok(
articlesContent.includes("import ImageUploader from '../../../components/image-uploader/image-uploader.uvue'"),
'articles.uvue 必须复用 image-uploader 组件'
);
});
test('7. pages.json 中必须注册 cases 与 articles 路由', () => {
const pagesJsonPath = path.join(MP_ROOT, 'pages.json');
const pagesJson = JSON.parse(fs.readFileSync(pagesJsonPath, 'utf-8'));
const routes = pagesJson.pages.map((p: any) => p.path);
assert.ok(routes.includes('pages/owner/card/cases'), 'pages.json 必须注册 cases 页面');
assert.ok(routes.includes('pages/owner/card/articles'), 'pages.json 必须注册 articles 页面');
});
test('8. edit.uvue 中必须包含 cases 与 articles 的 Bento 导航入口', () => {
const editPath = path.join(MP_ROOT, 'pages/owner/card/edit.uvue');
const editContent = fs.readFileSync(editPath, 'utf-8');
assert.ok(editContent.includes('handleGoCases'), 'edit.uvue 必须提供 handleGoCases');
assert.ok(editContent.includes('handleGoArticles'), 'edit.uvue 必须提供 handleGoArticles');
assert.ok(editContent.includes('pages/owner/card/cases'), 'edit.uvue 必须跳转至 cases 页面');
assert.ok(editContent.includes('pages/owner/card/articles'), 'edit.uvue 必须跳转至 articles 页面');
});
test('9. index.uvue 中必须提供快捷管理入口', () => {
const indexPath = path.join(MP_ROOT, 'pages/owner/card/index.uvue');
const indexContent = fs.readFileSync(indexPath, 'utf-8');
assert.ok(indexContent.includes('handleOpenCases'), 'index.uvue 必须提供 handleOpenCases');
assert.ok(indexContent.includes('handleOpenArticles'), 'index.uvue 必须提供 handleOpenArticles');
});

查看文件

@@ -17,6 +17,8 @@ const ALL_PAGES = [
'pages/owner/card/index.uvue',
'pages/owner/card/edit.uvue',
'pages/owner/card/qrcode.uvue',
'pages/owner/card/cases.uvue',
'pages/owner/card/articles.uvue',
'pages/owner/radar.uvue',
'pages/owner/leads.uvue',
'pages/owner/leads-detail.uvue',
@@ -47,6 +49,8 @@ const FORM_PAGES = [
'pages/portal/exchange.uvue',
'pages/common/login.uvue',
'pages/owner/card/edit.uvue',
'pages/owner/card/cases.uvue',
'pages/owner/card/articles.uvue',
'pages/owner/leads-detail.uvue',
'pages/owner/nfc/scan.uvue',
'pages/owner/nfc/mode.uvue',
@@ -61,6 +65,8 @@ const BOTTOM_CTA_PAGES = [
'pages/common/login.uvue',
'pages/owner/card/edit.uvue',
'pages/owner/card/qrcode.uvue',
'pages/owner/card/cases.uvue',
'pages/owner/card/articles.uvue',
'pages/owner/leads-detail.uvue',
'pages/owner/nfc/scan.uvue',
'pages/owner/nfc/mode.uvue',