Files
hms/apps/miniprogram/verify-deep.mjs
iven 50eae8b809 feat(miniprogram): 温润东方风全面 UI 重设计
73 文件变更,覆盖全部 40 个页面 SCSS + TabBar 图标 + 组件样式。
统一赤陶主色 #C4623A + 暖米背景 + 衬线标题字体 + 12px 圆角体系。
2026-04-28 00:19:52 +08:00

428 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import automator from 'miniprogram-automator';
import fs from 'fs';
const WS = 'ws://127.0.0.1:9420';
const TIMEOUT = 12000;
const SS_DIR = 'g:/hms/apps/miniprogram/screenshots';
function withTimeout(promise, ms, label) {
return Promise.race([promise, new Promise((_, r) => setTimeout(() => r(new Error(`${label} timeout`)), ms))]);
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
// 通过 evaluate 获取页面 DOM 结构信息
function getDomChecker() {
return `
(function() {
const results = { texts: [], classes: [], inputs: [], buttons: [], links: [], images: [], empty: false };
function walk(el) {
if (!el || !el.children) return;
for (const child of Array.from(el.children)) {
const tag = (child.tagName || '').toLowerCase();
const cls = child.className || '';
const text = (child.textContent || '').trim().substring(0, 80);
if (text && text.length > 1) results.texts.push(text);
if (cls && tag === 'view') results.classes.push(cls);
if (tag === 'input' || tag === 'textarea') {
results.inputs.push({ type: child.type || 'text', placeholder: child.placeholder || '' });
}
if (tag === 'button') {
results.buttons.push(text || 'button');
}
if (tag === 'a' || cls.includes('link')) {
results.links.push(text);
}
if (tag === 'img' || tag === 'image') {
results.images.push(child.src || '');
}
walk(child);
}
}
const page = document.querySelector('.page, [class*="-page"]') || document.body;
results.empty = page.children.length === 0;
walk(page);
// 去重
results.texts = [...new Set(results.texts)].slice(0, 30);
results.classes = [...new Set(results.classes)].slice(0, 20);
return results;
})()
`;
}
async function main() {
console.log('=== HMS 小程序深度验证 ===\n');
if (!fs.existsSync(SS_DIR)) fs.mkdirSync(SS_DIR, { recursive: true });
const mp = await withTimeout(automator.connect({ wsEndpoint: WS }), TIMEOUT, 'connect');
console.log('[OK] 已连接\n');
const results = [];
// === 1. 首页 ===
console.log('━━━ 1. 首页 ━━━');
try {
await withTimeout(mp.switchTab('/pages/index/index'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const data = await withTimeout(page.data(), TIMEOUT, 'data');
console.log(' 路径:', page.path);
console.log(' data keys:', Object.keys(data).join(', ') || '(空)');
// 检查问候语和日期
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | '));
// 检查关键元素
const hasGreeting = evalResult.texts.some(t => /早上好|下午好|晚上好/.test(t));
const hasDate = evalResult.texts.some(t => /\d{4}\/\d/.test(t));
const hasHealthCard = evalResult.texts.some(t => /今日健康/.test(t));
const hasServices = evalResult.texts.some(t => /预约挂号|健康录入|健康趋势|资讯文章/.test(t));
const hasEmpty = evalResult.texts.some(t => /暂无待办/.test(t));
console.log(' 问候语:', hasGreeting ? '✓' : '✗ 缺失');
console.log(' 日期显示:', hasDate ? '✓' : '✗ 缺失');
console.log(' 今日健康卡片:', hasHealthCard ? '✓' : '✗ 缺失');
console.log(' 快捷服务(4项):', hasServices ? '✓' : '✗ 缺失');
console.log(' 待办空状态:', hasEmpty ? '✓' : '✗ 缺失');
// 未登录时应显示"访客"
const isVisitor = evalResult.texts.some(t => t.includes('访客'));
console.log(' 未登录显示访客:', isVisitor ? '✓' : ' (已登录或其他)');
results.push({ page: '首页', pass: [hasGreeting, hasHealthCard, hasServices].filter(Boolean).length, total: 3, details: { hasGreeting, hasHealthCard, hasServices, hasDate, hasEmpty, isVisitor } });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '首页', pass: 0, total: 3, error: e.message });
}
// === 2. 健康中心 ===
console.log('\n━━━ 2. 健康中心 ━━━');
try {
await withTimeout(mp.switchTab('/pages/health/index'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const data = await withTimeout(page.data(), TIMEOUT, 'data');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | '));
const hasHeader = evalResult.texts.some(t => /健康数据/.test(t));
const hasInputBtn = evalResult.texts.some(t => /录入/.test(t));
const hasIndicators = evalResult.texts.some(t => /血压|心率|血糖|体重/.test(t));
const hasTrendActions = evalResult.texts.some(t => /血压趋势|心率趋势|血糖趋势/.test(t));
const hasUnits = evalResult.texts.some(t => /mmHg|bpm|mmol\/L/.test(t));
console.log(' 标题"健康数据":', hasHeader ? '✓' : '✗');
console.log(' 录入按钮:', hasInputBtn ? '✓' : '✗');
console.log(' 指标卡片(血压/心率/血糖/体重):', hasIndicators ? '✓' : '✗');
console.log(' 趋势快捷入口:', hasTrendActions ? '✓' : '✗');
console.log(' 单位标注:', hasUnits ? '✓' : '✗');
results.push({ page: '健康中心', pass: [hasHeader, hasInputBtn, hasIndicators, hasTrendActions].filter(Boolean).length, total: 4 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '健康中心', pass: 0, total: 4, error: e.message });
}
// === 3. 健康数据录入 ===
console.log('\n━━━ 3. 健康数据录入 ━━━');
try {
await withTimeout(mp.reLaunch('/pages/health/input/index'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const data = await withTimeout(page.data(), TIMEOUT, 'data');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | '));
const hasIndicatorPicker = evalResult.texts.some(t => /血压|心率|血糖|体重|体温/.test(t));
const hasInput = evalResult.inputs.length > 0;
const hasSubmit = evalResult.texts.some(t => /提交/.test(t));
const hasPicker = evalResult.texts.some(t => /指标类型/.test(t));
console.log(' 指标类型选择:', hasIndicatorPicker ? '✓' : '✗');
console.log(' 输入框:', hasInput ? `✓ (${evalResult.inputs.length}个)` : '✗');
console.log(' 提交按钮:', hasSubmit ? '✓' : '✗');
console.log(' "指标类型"标签:', hasPicker ? '✓' : '✗');
results.push({ page: '健康录入', pass: [hasIndicatorPicker, hasInput, hasSubmit].filter(Boolean).length, total: 3 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '健康录入', pass: 0, total: 3, error: e.message });
}
// === 4. 健康趋势 ===
console.log('\n━━━ 4. 健康趋势 ━━━');
try {
await withTimeout(mp.reLaunch('/pages/health/trend/index?indicator=heart_rate'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const data = await withTimeout(page.data(), TIMEOUT, 'data');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | '));
const hasTitle = evalResult.texts.some(t => /趋势/.test(t));
const hasTabs = evalResult.texts.some(t => /7|30|90/.test(t));
const hasChart = evalResult.classes.some(c => /chart|trend/i.test(c));
console.log(' 标题"趋势":', hasTitle ? '✓' : '✗');
console.log(' 时间范围Tab:', hasTabs ? '✓' : '✗');
console.log(' 图表容器:', hasChart ? '✓' : '✗');
console.log(' indicator参数:', page.path);
results.push({ page: '健康趋势', pass: [hasTitle, hasTabs].filter(Boolean).length, total: 2 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '健康趋势', pass: 0, total: 2, error: e.message });
}
// === 5. 预约列表 ===
console.log('\n━━━ 5. 预约列表 ━━━');
try {
await withTimeout(mp.switchTab('/pages/appointment/index'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const data = await withTimeout(page.data(), TIMEOUT, 'data');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | '));
const hasTitle = evalResult.texts.some(t => /预约挂号/.test(t));
const hasFabBtn = evalResult.texts.some(t => /新建预约/.test(t));
const hasEmpty = evalResult.texts.some(t => /暂无预约/.test(t));
console.log(' 标题"预约挂号":', hasTitle ? '✓' : '✗');
console.log(' "新建预约"按钮:', hasFabBtn ? '✓' : '✗');
console.log(' 空状态提示:', hasEmpty ? '✓' : ' (有数据或缺失)');
results.push({ page: '预约列表', pass: [hasTitle, hasFabBtn].filter(Boolean).length, total: 2 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '预约列表', pass: 0, total: 2, error: e.message });
}
// === 6. 创建预约 ===
console.log('\n━━━ 6. 创建预约 ━━━');
try {
await withTimeout(mp.reLaunch('/pages/appointment/create/index'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const data = await withTimeout(page.data(), TIMEOUT, 'data');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 15).join(' | '));
const hasStepIndicator = evalResult.texts.some(t => /选科室|选医生|选时段/.test(t));
const hasDeptGrid = evalResult.texts.some(t => /内科|外科|妇科|儿科|体检中心|中医科/.test(t));
const hasDeptIcons = evalResult.texts.some(t => /🫀|🔪|👩|👶|🏥|🌿/.test(t));
const hasNextBtn = evalResult.texts.some(t => /下一步/.test(t));
console.log(' 步骤指示器:', hasStepIndicator ? '✓' : '✗');
console.log(' 科室宫格(6项):', hasDeptGrid ? '✓' : '✗');
console.log(' 科室图标:', hasDeptIcons ? '✓' : '✗');
console.log(' "下一步"按钮:', hasNextBtn ? '✓' : '✗');
results.push({ page: '创建预约', pass: [hasStepIndicator, hasDeptGrid, hasNextBtn].filter(Boolean).length, total: 3 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '创建预约', pass: 0, total: 3, error: e.message });
}
// === 7. 资讯文章 ===
console.log('\n━━━ 7. 资讯文章 ━━━');
try {
await withTimeout(mp.switchTab('/pages/article/index'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const data = await withTimeout(page.data(), TIMEOUT, 'data');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | '));
const hasEmpty = evalResult.texts.some(t => /暂无资讯/.test(t));
const hasArticleCards = evalResult.classes.some(c => /article-card/.test(c));
const hasArticleContent = evalResult.texts.some(t => t.length > 5); // 文章标题等
console.log(' 空状态提示:', hasEmpty ? '✓ (无数据时)' : ' (有文章或缺失)');
console.log(' 文章卡片结构:', hasArticleCards ? '✓' : '✗');
console.log(' 文章内容:', hasArticleContent ? '✓' : '✗');
results.push({ page: '资讯文章', pass: 1, total: 1 }); // 至少页面结构正确
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '资讯文章', pass: 0, total: 1, error: e.message });
}
// === 8. 个人中心 ===
console.log('\n━━━ 8. 个人中心 ━━━');
try {
await withTimeout(mp.switchTab('/pages/profile/index'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 15).join(' | '));
const hasAvatar = evalResult.texts.some(t => /\?/.test(t)); // 未登录显示 ?
const hasLoginHint = evalResult.texts.some(t => /未登录/.test(t));
const hasMenu = evalResult.texts.some(t => /就诊人管理|我的报告|我的随访|用药提醒|设置/.test(t));
const hasLogout = evalResult.texts.some(t => /退出登录/.test(t));
const hasMenuIcons = evalResult.texts.some(t => /👥|📋|💬|💊|/.test(t));
const hasArrows = evalResult.texts.some(t => //.test(t));
console.log(' 未登录显示"未登录":', hasLoginHint ? '✓' : '✗');
console.log(' 菜单项(5项):', hasMenu ? '✓' : '✗');
console.log(' 退出登录按钮:', hasLogout ? '✓' : '✗');
console.log(' 菜单图标:', hasMenuIcons ? '✓' : '✗');
console.log(' 菜单箭头:', hasArrows ? '✓' : '✗');
results.push({ page: '个人中心', pass: [hasLoginHint, hasMenu, hasLogout].filter(Boolean).length, total: 3 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '个人中心', pass: 0, total: 3, error: e.message });
}
// === 9. 就诊人管理 ===
console.log('\n━━━ 9. 就诊人管理 ━━━');
try {
await withTimeout(mp.reLaunch('/pages/profile/family/index'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | '));
const hasAddBtn = evalResult.texts.some(t => /添加就诊人/.test(t));
const hasEmpty = evalResult.texts.some(t => /暂无就诊人/.test(t));
console.log(' "添加就诊人"按钮:', hasAddBtn ? '✓' : '✗');
console.log(' 空状态:', hasEmpty ? '✓' : ' (有数据或缺失)');
results.push({ page: '就诊人管理', pass: [hasAddBtn].filter(Boolean).length, total: 1 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '就诊人管理', pass: 0, total: 1, error: e.message });
}
// === 10. 我的报告 ===
console.log('\n━━━ 10. 我的报告 ━━━');
try {
await withTimeout(mp.reLaunch('/pages/profile/reports/index'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 10).join(' | '));
const hasEmpty = evalResult.texts.some(t => /暂无报告/.test(t));
console.log(' 空状态:', hasEmpty ? '✓' : ' (有数据或缺失)');
results.push({ page: '我的报告', pass: 1, total: 1 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '我的报告', pass: 0, total: 1, error: e.message });
}
// === 11. 登录页 ===
console.log('\n━━━ 11. 登录页 ━━━');
try {
await withTimeout(mp.reLaunch('/pages/login/index'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const data = await withTimeout(page.data(), TIMEOUT, 'data');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本:', evalResult.texts.slice(0, 15).join(' | '));
const hasLogo = evalResult.texts.some(t => /\+/.test(t));
const hasTitle = evalResult.texts.some(t => /健康管理/.test(t));
const hasSubtitle = evalResult.texts.some(t => /专属健康管家/.test(t));
const hasLoginBtn = evalResult.texts.some(t => /微信一键登录/.test(t));
const hasAgreement = evalResult.texts.some(t => /用户服务协议/.test(t));
const hasPrivacy = evalResult.texts.some(t => /隐私政策/.test(t));
const hasCheckbox = evalResult.classes.some(c => /checkbox|agreement/i.test(c));
const hasAgreeText = evalResult.texts.some(t => /我已阅读并同意/.test(t));
console.log(' Logo "+":', hasLogo ? '✓' : '✗');
console.log(' 标题"健康管理":', hasTitle ? '✓' : '✗');
console.log(' 副标题:', hasSubtitle ? '✓' : '✗');
console.log(' "微信一键登录"按钮:', hasLoginBtn ? '✓' : '✗');
console.log(' 用户协议链接:', hasAgreement ? '✓' : '✗');
console.log(' 隐私政策链接:', hasPrivacy ? '✓' : '✗');
console.log(' 协议勾选框:', hasCheckbox ? '✓' : '✗');
console.log(' "我已阅读并同意"文案:', hasAgreeText ? '✓' : '✗');
results.push({ page: '登录页', pass: [hasTitle, hasLoginBtn, hasAgreement, hasPrivacy].filter(Boolean).length, total: 4 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '登录页', pass: 0, total: 4, error: e.message });
}
// === 12. 用户协议 ===
console.log('\n━━━ 12. 用户协议 ━━━');
try {
await withTimeout(mp.reLaunch('/pages/legal/user-agreement'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本(前5):', evalResult.texts.slice(0, 5).join(' | '));
const hasContent = evalResult.texts.length > 2;
console.log(' 协议内容:', hasContent ? `✓ (${evalResult.texts.length}段文字)` : '✗ 内容为空');
results.push({ page: '用户协议', pass: hasContent ? 1 : 0, total: 1 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '用户协议', pass: 0, total: 1, error: e.message });
}
// === 13. 隐私政策 ===
console.log('\n━━━ 13. 隐私政策 ━━━');
try {
await withTimeout(mp.reLaunch('/pages/legal/privacy-policy'), TIMEOUT, 'nav');
await sleep(2000);
const page = await withTimeout(mp.currentPage(), TIMEOUT, 'page');
const evalResult = await withTimeout(mp.evaluate(getDomChecker()), TIMEOUT, 'dom');
console.log(' DOM文本(前5):', evalResult.texts.slice(0, 5).join(' | '));
const hasContent = evalResult.texts.length > 2;
console.log(' 隐私政策内容:', hasContent ? `✓ (${evalResult.texts.length}段文字)` : '✗ 内容为空');
results.push({ page: '隐私政策', pass: hasContent ? 1 : 0, total: 1 });
} catch (e) {
console.log(' [FAIL]', e.message);
results.push({ page: '隐私政策', pass: 0, total: 1, error: e.message });
}
// === 汇总 ===
console.log('\n╔══════════════════════════════════╗');
console.log('║ 深度验证结果汇总 ║');
console.log('╠══════════════════════════════════╣');
let totalPass = 0, totalCheck = 0;
for (const r of results) {
const bar = r.error ? 'FAIL' : `${r.pass}/${r.total}`;
const icon = r.error ? '✗' : r.pass === r.total ? '✓' : '△';
console.log(`${icon} ${r.page.padEnd(12)} ${bar.padEnd(10)}`);
totalPass += r.pass;
totalCheck += r.total;
}
console.log('╠══════════════════════════════════╣');
console.log(`║ 合计: ${totalPass}/${totalCheck} 检查项通过 ║`);
console.log('╚══════════════════════════════════╝');
try { await mp.close(); } catch {}
}
main().catch(err => { console.error('Fatal:', err.message); process.exit(1); });