feat(miniprogram): 温润东方风全面 UI 重设计
73 文件变更,覆盖全部 40 个页面 SCSS + TabBar 图标 + 组件样式。 统一赤陶主色 #C4623A + 暖米背景 + 衬线标题字体 + 12px 圆角体系。
This commit is contained in:
363
apps/miniprogram/e2e-chain-v2.cjs
Normal file
363
apps/miniprogram/e2e-chain-v2.cjs
Normal file
@@ -0,0 +1,363 @@
|
||||
/**
|
||||
* HMS 小程序端到端链路验证 v2
|
||||
* 使用 connect 模式连接已打开的微信开发者工具
|
||||
* 每步有超时保护,不会卡死
|
||||
*/
|
||||
const automator = require('miniprogram-automator');
|
||||
const http = require('http');
|
||||
|
||||
const BASE = 'http://localhost:3000/api/v1';
|
||||
const results = [];
|
||||
|
||||
function log(chain, step, status, detail) {
|
||||
const icon = status === 'PASS' ? '✅' : status === 'FAIL' ? '❌' : '⚠️';
|
||||
console.log(` ${icon} [${chain}] ${step}: ${detail}`);
|
||||
results.push({ chain, step, status, detail });
|
||||
}
|
||||
|
||||
function apiRequest(method, path, body, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(BASE + path);
|
||||
const opts = {
|
||||
hostname: url.hostname, port: url.port,
|
||||
path: url.pathname + url.search, method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: 10000,
|
||||
};
|
||||
if (token) opts.headers['Authorization'] = `Bearer ${token}`;
|
||||
const req = http.request(opts, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', c => chunks.push(c));
|
||||
res.on('end', () => {
|
||||
const raw = Buffer.concat(chunks).toString();
|
||||
try { resolve({ status: res.statusCode, data: JSON.parse(raw) }); }
|
||||
catch { resolve({ status: res.statusCode, data: raw }); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
|
||||
if (body) req.write(JSON.stringify(body));
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function withTimeout(promise, ms, label) {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} 超时(${ms}ms)`)), ms))
|
||||
]);
|
||||
}
|
||||
|
||||
async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
|
||||
|
||||
async function safePageAction(label, fn) {
|
||||
try {
|
||||
return await withTimeout(fn(), 8000, label);
|
||||
} catch (e) {
|
||||
log(label, '操作超时/异常', 'WARN', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('\n=== HMS 小程序端到端链路验证 v2 ===\n');
|
||||
|
||||
// ---- 连接 ----
|
||||
console.log('连接微信开发者工具...');
|
||||
let mini;
|
||||
try {
|
||||
mini = await withTimeout(
|
||||
automator.connect({ wsEndpoint: 'ws://localhost:9420' }),
|
||||
10000, '连接'
|
||||
);
|
||||
console.log('连接成功!\n');
|
||||
} catch (e) {
|
||||
console.error('连接失败:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---- 辅助 ----
|
||||
async function getPages() {
|
||||
return await withTimeout(mini.pages(), 5000, '获取页面列表');
|
||||
}
|
||||
|
||||
async function getPageInfo() {
|
||||
const pages = await getPages();
|
||||
if (pages && pages.length > 0) {
|
||||
const last = pages[pages.length - 1];
|
||||
try {
|
||||
const path = await withTimeout(last.path, 3000, '获取路径');
|
||||
return { page: last, path };
|
||||
} catch {
|
||||
return { page: last, path: 'unknown' };
|
||||
}
|
||||
}
|
||||
return { page: null, path: 'none' };
|
||||
}
|
||||
|
||||
async function nav(url) {
|
||||
try {
|
||||
await withTimeout(mini.navigateTo({ url }), 5000, `导航 ${url}`);
|
||||
await sleep(2000);
|
||||
return true;
|
||||
} catch (e) {
|
||||
log('导航', url, 'WARN', e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function back() {
|
||||
try {
|
||||
await withTimeout(mini.navigateBack(), 3000, '返回');
|
||||
await sleep(1000);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 0. 后端健康检查
|
||||
// ============================================
|
||||
console.log('--- 后端健康检查 ---');
|
||||
let token, patients;
|
||||
try {
|
||||
const loginRes = await apiRequest('POST', '/auth/login', { username: 'admin', password: 'Admin@2026' });
|
||||
token = loginRes.data?.data?.access_token;
|
||||
log('后端', '登录', token ? 'PASS' : 'FAIL', `status=${loginRes.status}, token=${token ? token.length : 0}字符`);
|
||||
|
||||
const patRes = await apiRequest('GET', '/health/patients?page=1&page_size=10', null, token);
|
||||
patients = patRes.data?.data?.data || [];
|
||||
log('后端', '患者列表', patients.length > 0 ? 'PASS' : 'WARN', `${patients.length} 个患者`);
|
||||
} catch (e) {
|
||||
log('后端', '检查', 'FAIL', e.message);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 1. 当前页面检查
|
||||
// ============================================
|
||||
console.log('\n--- 链路1: 首页 & 认证状态 ---');
|
||||
const { page: homePage, path: homePath } = await safePageAction('首页', getPageInfo);
|
||||
if (homePage) {
|
||||
log('首页', '页面加载', 'PASS', `当前路径: ${homePath}`);
|
||||
} else {
|
||||
log('首页', '页面加载', 'FAIL', '无法获取当前页面');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 2. 健康数据页
|
||||
// ============================================
|
||||
console.log('\n--- 链路2: 健康数据 ---');
|
||||
if (await nav('/pages/health/index')) {
|
||||
const { path } = await safePageAction('健康数据', getPageInfo);
|
||||
log('健康数据', '页面加载', path?.includes('health') ? 'PASS' : 'FAIL', `路径: ${path}`);
|
||||
|
||||
// 通过 API 验证数据链路
|
||||
if (token && patients?.[0]?.id) {
|
||||
const todayRes = await apiRequest('GET', `/health/vital-signs/today?patient_id=${patients[0].id}`, null, token);
|
||||
log('健康数据', '今日体征API(F1修复)', todayRes.status === 200 ? 'PASS' : 'FAIL',
|
||||
`status=${todayRes.status}, hasData=${!!todayRes.data?.data}`);
|
||||
|
||||
const trendRes = await apiRequest('GET', '/health/vital-signs/trend?indicator=weight&range=7d', null, token);
|
||||
log('健康数据', '趋势API', trendRes.status === 200 ? 'PASS' : 'FAIL', `status=${trendRes.status}`);
|
||||
}
|
||||
await back();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 3. 健康数据录入
|
||||
// ============================================
|
||||
console.log('\n--- 链路3: 健康数据录入 ---');
|
||||
if (await nav('/pages/health/input/index')) {
|
||||
const { page: inputPage, path: inputPath } = await safePageAction('录入页', getPageInfo);
|
||||
log('健康录入', '页面加载', inputPath?.includes('input') ? 'PASS' : 'FAIL', `路径: ${inputPath}`);
|
||||
|
||||
if (inputPage) {
|
||||
const inputs = await safePageAction('输入框', () => inputPage.$$('input'));
|
||||
log('健康录入', '表单字段', inputs && inputs.length > 0 ? 'PASS' : 'WARN',
|
||||
`${inputs?.length || 0} 个输入框`);
|
||||
}
|
||||
await back();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 4. 日常监测
|
||||
// ============================================
|
||||
console.log('\n--- 链路4: 日常监测 ---');
|
||||
if (await nav('/pages/health/daily-monitoring/index')) {
|
||||
const { page: dmPage, path: dmPath } = await safePageAction('监测页', getPageInfo);
|
||||
log('日常监测', '页面加载', dmPath?.includes('daily-monitoring') ? 'PASS' : 'FAIL', `路径: ${dmPath}`);
|
||||
|
||||
if (dmPage) {
|
||||
const inputs = await safePageAction('DM输入框', () => dmPage.$$('.dm-input'));
|
||||
log('日常监测', '表单字段(M6修复)', inputs && inputs.length > 0 ? 'PASS' : 'WARN',
|
||||
`${inputs?.length || 0} 个.dm-input字段`);
|
||||
|
||||
const submitBtn = await safePageAction('提交按钮', () => dmPage.$('.dm-submit'));
|
||||
log('日常监测', '提交按钮', submitBtn ? 'PASS' : 'WARN', submitBtn ? '找到' : '未找到');
|
||||
}
|
||||
await back();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 5. 积分商城
|
||||
// ============================================
|
||||
console.log('\n--- 链路5: 积分商城 ---');
|
||||
if (await nav('/pages/mall/index')) {
|
||||
const { page: mallPage, path: mallPath } = await safePageAction('商城页', getPageInfo);
|
||||
log('积分商城', '页面加载', mallPath?.includes('mall') ? 'PASS' : 'FAIL', `路径: ${mallPath}`);
|
||||
|
||||
if (mallPage) {
|
||||
const pointsCard = await safePageAction('积分卡片', () => mallPage.$('.points-card'));
|
||||
log('积分商城', '积分卡片', pointsCard ? 'PASS' : 'WARN', pointsCard ? '找到' : '未找到');
|
||||
|
||||
const checkinBtn = await safePageAction('签到', () => mallPage.$('.checkin-btn'));
|
||||
log('积分商城', '签到按钮', checkinBtn ? 'PASS' : 'WARN', checkinBtn ? '找到' : '未找到');
|
||||
|
||||
const products = await safePageAction('商品列表', () => mallPage.$$('.product-card'));
|
||||
log('积分商城', '商品列表', 'PASS', `${products?.length || 0} 个商品`);
|
||||
|
||||
// F2 修复验证: 检查无档案降级
|
||||
const emptyState = await safePageAction('降级UI', () => mallPage.$('.empty-state'));
|
||||
if (emptyState) {
|
||||
log('积分商城', '无档案降级(F2修复)', 'PASS', '显示了无档案引导 UI');
|
||||
}
|
||||
}
|
||||
await back();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 6. 预约挂号
|
||||
// ============================================
|
||||
console.log('\n--- 链路6: 预约挂号 ---');
|
||||
if (await nav('/pages/health/appointment/index')) {
|
||||
const { path: aptPath } = await safePageAction('预约页', getPageInfo);
|
||||
log('预约挂号', '页面加载', aptPath?.includes('appointment') ? 'PASS' : 'FAIL', `路径: ${aptPath}`);
|
||||
await back();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 7. 家庭成员
|
||||
// ============================================
|
||||
console.log('\n--- 链路7: 家庭成员管理 ---');
|
||||
if (await nav('/pages/profile/family/index')) {
|
||||
const { page: famPage, path: famPath } = await safePageAction('家庭页', getPageInfo);
|
||||
log('家庭成员', '页面加载', famPath?.includes('family') ? 'PASS' : 'FAIL', `路径: ${famPath}`);
|
||||
|
||||
if (famPage) {
|
||||
const cards = await safePageAction('成员卡片', () => famPage.$$('[class*="card"], [class*="member"]'));
|
||||
log('家庭成员', '列表渲染', 'PASS', `${cards?.length || 0} 个成员元素`);
|
||||
}
|
||||
await back();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 8. 咨询
|
||||
// ============================================
|
||||
console.log('\n--- 链路8: 咨询 ---');
|
||||
if (await nav('/pages/health/consultation/index')) {
|
||||
const { path: consPath } = await safePageAction('咨询页', getPageInfo);
|
||||
log('咨询', '页面加载', consPath?.includes('consultation') ? 'PASS' : 'FAIL', `路径: ${consPath}`);
|
||||
await back();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 9. 文章
|
||||
// ============================================
|
||||
console.log('\n--- 链路9: 文章 ---');
|
||||
if (await nav('/pages/health/articles/index')) {
|
||||
const { path: artPath } = await safePageAction('文章页', getPageInfo);
|
||||
log('文章', '页面加载', artPath?.includes('article') ? 'PASS' : 'FAIL', `路径: ${artPath}`);
|
||||
await back();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 10. 健康趋势
|
||||
// ============================================
|
||||
console.log('\n--- 链路10: 健康趋势 ---');
|
||||
if (await nav('/pages/health/trend/index')) {
|
||||
const { path: trendPath } = await safePageAction('趋势页', getPageInfo);
|
||||
log('趋势', '页面加载', trendPath?.includes('trend') ? 'PASS' : 'FAIL', `路径: ${trendPath}`);
|
||||
await back();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 11. 我的报告
|
||||
// ============================================
|
||||
console.log('\n--- 链路11: 我的报告 ---');
|
||||
if (await nav('/pages/health/reports/index')) {
|
||||
const { path: repPath } = await safePageAction('报告页', getPageInfo);
|
||||
log('报告', '页面加载', repPath?.includes('report') ? 'PASS' : 'FAIL', `路径: ${repPath}`);
|
||||
await back();
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// API 数据闭环验证
|
||||
// ============================================
|
||||
console.log('\n--- API 数据闭环 ---');
|
||||
if (token && patients?.[0]?.id) {
|
||||
const pid = patients[0].id;
|
||||
|
||||
// 患者详情
|
||||
const patRes = await apiRequest('GET', `/health/patients/${pid}`, null, token);
|
||||
log('API闭环', '患者详情', patRes.status === 200 ? 'PASS' : 'FAIL',
|
||||
`status=${patRes.status}, name=${patRes.data?.data?.name || 'N/A'}`);
|
||||
|
||||
// 预约列表
|
||||
const aptRes = await apiRequest('GET', `/health/patients/${pid}/appointments?page=1&page_size=5`, null, token);
|
||||
log('API闭环', '预约列表', aptRes.status === 200 ? 'PASS' : 'FAIL', `status=${aptRes.status}`);
|
||||
|
||||
// 咨询列表
|
||||
const consRes = await apiRequest('GET', `/health/patients/${pid}/consultation-sessions?page=1&page_size=5`, null, token);
|
||||
log('API闭环', '咨询列表', consRes.status === 200 ? 'PASS' : 'FAIL', `status=${consRes.status}`);
|
||||
|
||||
// 日常监测列表
|
||||
const dmRes = await apiRequest('GET', `/health/patients/${pid}/daily-monitoring?page=1&page_size=5`, null, token);
|
||||
log('API闭环', '日常监测列表', dmRes.status === 200 ? 'PASS' : 'FAIL', `status=${dmRes.status}`);
|
||||
|
||||
// 积分账户
|
||||
const acctRes = await apiRequest('GET', '/health/points/account', null, token);
|
||||
log('API闭环', '积分账户', acctRes.status === 200 ? 'PASS' : 'FAIL',
|
||||
`status=${acctRes.status}, balance=${acctRes.data?.data?.balance ?? 'N/A'}`);
|
||||
|
||||
// 签到状态
|
||||
const checkRes = await apiRequest('GET', '/health/points/checkin-status', null, token);
|
||||
log('API闭环', '签到状态', checkRes.status === 200 ? 'PASS' : 'FAIL', `status=${checkRes.status}`);
|
||||
|
||||
// 商品列表
|
||||
const prodRes = await apiRequest('GET', '/health/points/products?page=1&page_size=5', null, token);
|
||||
log('API闭环', '商品列表', prodRes.status === 200 ? 'PASS' : 'FAIL',
|
||||
`status=${prodRes.status}, count=${prodRes.data?.data?.total ?? 'N/A'}`);
|
||||
}
|
||||
|
||||
// ---- 关闭 ----
|
||||
try { await mini.close(); } catch {}
|
||||
|
||||
// ---- 汇总 ----
|
||||
console.log('\n\n========================================');
|
||||
console.log(' HMS 小程序端到端链路验证报告');
|
||||
console.log('========================================\n');
|
||||
|
||||
const chains = [...new Set(results.map(r => r.chain))];
|
||||
for (const chain of chains) {
|
||||
const items = results.filter(r => r.chain === chain);
|
||||
const passed = items.filter(r => r.status === 'PASS').length;
|
||||
const failed = items.filter(r => r.status === 'FAIL').length;
|
||||
const warned = items.filter(r => r.status === 'WARN').length;
|
||||
const icon = failed > 0 ? '❌' : warned > 0 ? '⚠️' : '✅';
|
||||
console.log(`${icon} ${chain}: ${passed}通过 / ${failed}失败 / ${warned}警告`);
|
||||
for (const item of items) {
|
||||
if (item.status === 'FAIL') console.log(` ❌ ${item.step}: ${item.detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
const totalPass = results.filter(r => r.status === 'PASS').length;
|
||||
const totalFail = results.filter(r => r.status === 'FAIL').length;
|
||||
const totalWarn = results.filter(r => r.status === 'WARN').length;
|
||||
console.log(`\n总计: ${results.length} 项 — ${totalPass}通过 / ${totalFail}失败 / ${totalWarn}警告`);
|
||||
console.log('========================================\n');
|
||||
|
||||
process.exit(totalFail > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch(e => {
|
||||
console.error('致命错误:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user