docs/:系统梳理、架构优化方案、CR-02 三端功能设计修订(含 §15 三端一致性同步与平台统计分析)、公寓端/应用端/平台端分册、公寓统计分析与排名报表、公寓端小步迭代开发计划(IT-0~IT-8) dev/:RuoYi-Vue-master(后端)与 RuoYi-Vue2-master(前端)官方上游基线,未改动
394 lines
23 KiB
JavaScript
394 lines
23 KiB
JavaScript
/* ============================================================================
|
||
* JcXx.optimized.vue 验收脚本
|
||
* 1) 真实 SFC 编译:vue-template-compiler / @babel/parser / sass
|
||
* 2) 功能集合零变化:script 哈希 + 模板指令/列/按钮/弹窗集合比对
|
||
* 3) 优化前 / 优化后同一份静态 DOM + 两份真实 CSS,多视口截图与布局断言
|
||
* 用法:node verify_jcxx.cjs
|
||
* ==========================================================================*/
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const crypto = require('crypto');
|
||
|
||
const ROOT = path.resolve(__dirname, '..', '..', '..');
|
||
const VUE = path.join(ROOT, 'YktVue');
|
||
const SRC = path.join(VUE, 'src', 'views', 'JcXx.vue');
|
||
const OUT = path.join(VUE, 'src', 'views', 'JcXx.optimized.vue');
|
||
const UI = path.join(ROOT, 'docs', 'frontend', 'ui');
|
||
|
||
const req = p => require(path.join(VUE, 'node_modules', p));
|
||
const compiler = req('vue-template-compiler');
|
||
const babel = req('@babel/parser');
|
||
const sass = req('sass');
|
||
|
||
let chromium;
|
||
const PW = 'C:/Users/zfjia/AppData/Roaming/npm/node_modules/@playwright/cli/node_modules/playwright';
|
||
const EXE = 'C:/Users/zfjia/AppData/Local/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-win64/chrome-headless-shell.exe';
|
||
try { ({ chromium } = require(PW)); } catch (e) { ({ chromium } = require('playwright')); }
|
||
|
||
const beforeVue = fs.readFileSync(SRC, 'utf8');
|
||
const afterVue = fs.readFileSync(OUT, 'utf8');
|
||
|
||
function parts(s) {
|
||
// 注意:模板内部有大量 <template slot-scope>,必须取最后一个 </template>
|
||
const a = s.indexOf('<template>') + '<template>'.length;
|
||
const b = s.lastIndexOf('</template>');
|
||
const c = s.indexOf('<script>'), d = s.indexOf('</script>') + 9;
|
||
const e = s.indexOf('<style'), f = s.indexOf('</style>');
|
||
return {
|
||
template: s.slice(a, b),
|
||
script: s.slice(c, d),
|
||
scriptBody: s.slice(s.indexOf('>', c) + 1, d - 9),
|
||
style: s.slice(s.indexOf('>', e) + 1, f)
|
||
};
|
||
}
|
||
const B = parts(beforeVue), A = parts(afterVue);
|
||
const sha = s => crypto.createHash('sha256').update(s).digest('hex');
|
||
|
||
let pass = 0, fail = 0;
|
||
const results = [];
|
||
function T(name, cond, detail) {
|
||
if (cond) { pass++; results.push(' PASS ' + name); }
|
||
else { fail++; results.push(' FAIL ' + name + (detail ? ' ' + detail : '')); }
|
||
}
|
||
|
||
// ---------------------------------------------------------------- 1. 编译
|
||
const rcB = compiler.compile(B.template);
|
||
const rcA = compiler.compile(A.template);
|
||
T('SFC-01 优化前模板可由 vue-template-compiler 编译', rcB.errors.length === 0, JSON.stringify(rcB.errors));
|
||
T('SFC-02 优化后模板可由 vue-template-compiler 编译', rcA.errors.length === 0, JSON.stringify(rcA.errors));
|
||
let jsErr = '';
|
||
try {
|
||
babel.parse(A.scriptBody, {
|
||
sourceType: 'module',
|
||
plugins: ['objectRestSpread', 'dynamicImport', 'optionalChaining',
|
||
'nullishCoalescingOperator', 'classProperties']
|
||
});
|
||
} catch (e) { jsErr = e.message; }
|
||
T('SFC-03 优化后 <script> 通过 Babel 语法解析', !jsErr, jsErr);
|
||
|
||
const compileSass = scss => sass.renderSync({ data: scss, outputStyle: 'expanded' }).css.toString();
|
||
const cssB = compileSass(B.style);
|
||
const cssA = compileSass(A.style);
|
||
T('SFC-04 优化前 style 可通过 sass 编译', cssB.length > 10000);
|
||
T('SFC-05 优化后 style 可通过 sass 编译', cssA.length > cssB.length);
|
||
|
||
// ---------------------------------------------------------------- 2. 功能集合
|
||
T('FUN-01 <script> 段逐字节一致(功能/接口/定时器/状态机全部保留)',
|
||
B.script === A.script && sha(B.script) === sha(A.script));
|
||
|
||
function inventory(tpl) {
|
||
const pick = re => (tpl.match(re) || []).sort();
|
||
return {
|
||
events: pick(/@[a-zA-Z][\w.-]*(?:=|\s|>)/g).map(x => x.replace(/[=>\s]/g, '')).sort(),
|
||
models: pick(/v-model(?:\.\w+)?="[^"]+"/g),
|
||
binds: pick(/:[a-zA-Z][\w.-]*="[^"]+"/g),
|
||
refs: pick(/ref="[^"]+"/g),
|
||
slots: pick(/slot-scope="[^"]+"/g),
|
||
vif: pick(/v-(?:if|else-if|else|show|for|html|once)(?:="[^"]+")?/g),
|
||
cols: pick(/<el-table-column[^>]*?\b(?:prop|label)="[^"]+"/g)
|
||
.map(x => (x.match(/\bprop="([^"]+)"/) || [, ''])[1] + '|' +
|
||
(x.match(/\blabel="([^"]+)"/) || [, ''])[1]),
|
||
tags: ['el-dialog', 'el-table', 'el-table-column', 'el-button', 'el-input',
|
||
'el-select', 'el-form', 'el-form-item', 'el-tab-pane', 'el-checkbox',
|
||
'audio', 'el-result'].map(t => t + '=' + (tpl.match(new RegExp('<' + t + '\\b', 'g')) || []).length)
|
||
};
|
||
}
|
||
const ib = inventory(B.template), ia = inventory(A.template);
|
||
Object.keys(ib).forEach(k => {
|
||
if (k === 'events') {
|
||
// 唯一预期差异:tab_click -> tab-click(Element UI 实际派发 tab-click;处理函数体为空)
|
||
const nb = ib.events.map(x => x === '@tab_click' ? '@tab-click' : x).sort();
|
||
T('FUN-02 事件绑定集合一致(仅修正 tab_click 拼写)',
|
||
JSON.stringify(nb) === JSON.stringify(ia.events),
|
||
JSON.stringify({ before: ib.events.filter(x => !ia.events.includes(x)), after: ia.events.filter(x => !nb.includes(x)) }));
|
||
} else {
|
||
T('FUN-02 ' + k + ' 集合与数量一致',
|
||
JSON.stringify(ib[k]) === JSON.stringify(ia[k]),
|
||
JSON.stringify({ before: ib[k].length, after: ia[k].length }));
|
||
}
|
||
});
|
||
T('FUN-03 未新增/删除任何按钮、表格列、弹窗',
|
||
ib.tags.join('|') === ia.tags.join('|'), ib.tags.join('|'));
|
||
T('FUN-04 所有弹窗带统一 jc-dialog 作用域类名、宽弹窗带 jc-wide-dialog',
|
||
(A.template.match(/<el-dialog/g) || []).length === (A.template.match(/class="jc-dialog/g) || []).length &&
|
||
(A.template.match(/jc-wide-dialog/g) || []).length === 4);
|
||
T('FUN-05 原样式核心规则全部保留,优化层仅做追加与 4 处修正',
|
||
['.marquee-container', '.center-time-section', '.form-item-container',
|
||
'.bottom-duty-bar', '@keyframes marquee', '.history-jcxx-form-row',
|
||
'.sync-warning-content', '.person-photo-panel', '.jl-add-form']
|
||
.every(k => A.style.includes(k)) && cssA.length > cssB.length);
|
||
|
||
// ---------------------------------------------------------------- 3. 静态布局快照
|
||
const elementCss = fs.readFileSync(
|
||
path.join(VUE, 'node_modules', 'element-ui', 'lib', 'theme-chalk', 'index.css'), 'utf8');
|
||
|
||
const ORANGE = 'background: linear-gradient(135deg, #e6a23c, #f7ba2a); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(230,162,60,.3); color: white;';
|
||
const TEAL = 'background: linear-gradient(135deg, #0b5858, #0f6b6b); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(11,88,88,.3); color: white;';
|
||
const BLUE = 'background: linear-gradient(135deg, #409EFF, #66b3ff); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(64,158,255,.3); color: white;';
|
||
const GREY = 'background: linear-gradient(135deg, #909399, #b1b3b8); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(144,147,153,.3); color: white;';
|
||
const RED = 'background: linear-gradient(135deg, #f56c6c, #ff7979); border: none; border-radius: 6px; padding: 6px 16px; font-weight: 600; box-shadow: 0 3px 8px rgba(245,108,108,.3); color: white;';
|
||
const btn = (label, style, extra) =>
|
||
`<button class="el-button el-button--small" style="${style}${extra || ''}"><span class="buttonFont-small">${label}</span></button>`;
|
||
|
||
const COLS = ['序号', '状态', '到寓时间', '单位', '机车号', '姓名', '房间号', '出发车次',
|
||
'开车时间', '叫乘时间', '复叫时间', '叫乘日期', '应离寓时间', '接车方式', '操作'];
|
||
const ROWS = [
|
||
{ bg: '#F56C6C', st: 'jc-st-alarm', v: ['1', '叫乘报警', '11-08 21:40', '机务段', 'HXD3C-0131', '冯建国', '305', 'K671', '08:35', '06:35', '06:45', '11-09', '07:05', '站接'] },
|
||
{ bg: '#00FF00', st: 'jc-st-normal', v: ['2', '正在呼叫', '11-08 22:05', '机务段', 'HXD3C-0132', '蒋卫东', '517', 'T118', '09:10', '07:10', '07:20', '11-09', '07:40', '库出'] },
|
||
{ bg: '#FFF200', st: 'jc-st-normal', v: ['3', '待叫乘', '11-08 22:20', '客运段', 'HXD3C-0133', '李红军', '206', 'Z267', '10:05', '08:05', '08:15', '11-09', '08:35', '站接'] },
|
||
{ bg: '#B3E19D', st: 'jc-st-normal', v: ['4', '待离寓', '11-08 20:15', '机务段', 'HXD3C-0134', '王志强', '408', 'K42', '07:40', '05:40', '05:50', '11-09', '06:10', '库出'] },
|
||
{ bg: '', st: 'jc-st-muted', v: ['5', '已入寓', '11-09 01:10', '客运段', 'HXD3C-0135', '赵明亮', '612', 'K671', '14:20', '12:20', '12:30', '11-09', '12:50', '站接'] }
|
||
];
|
||
|
||
function makeBody() {
|
||
const search = ['机车号', '车次', '房间号', '姓名'].map((k, i) =>
|
||
`<div class="el-col el-col-6" style="padding-left:7.5px;padding-right:7.5px">
|
||
<div class="form-item-container"><label class="form-label">${k}</label>
|
||
<div class="el-input"><input class="el-input__inner" placeholder="请输入${k}" id="q${i}"></div></div></div>`).join('');
|
||
const topBtns = ['房间信息', '设备信息', '人员维护', '交路管理', '历史数据']
|
||
.map(x => btn(x, ORANGE, 'height:32px;')).join('');
|
||
const row2Btns = ['换乘', '出乘'].map(x => btn(x, TEAL, 'height:32px;width:84px;')).join('');
|
||
const row3Btns = ['语音临叫', '喊话临叫', '停止呼叫', '停止报警']
|
||
.map(x => btn(x, TEAL, 'height:32px;')).join('');
|
||
const ths = COLS.map(c => `<th class="el-table__cell"><div class="cell">${c}</div></th>`).join('');
|
||
const trs = ROWS.map((r, ri) => {
|
||
const tds = r.v.map((v, ci) => {
|
||
let inner = v;
|
||
if (ci === 1) inner = `<span class="jc-st ${r.st}">${v}</span>`;
|
||
return `<td class="el-table__cell"><div class="cell">${inner}</div></td>`;
|
||
});
|
||
tds.push(`<td class="el-table__cell"><div class="cell">${btn('暂停报警', BLUE, 'height:28px;')}</div></td>`);
|
||
return `<tr class="el-table__row${ri === 0 ? ' current-row' : ''}" style="background-color:${r.bg};font-size:20px;font-weight:600">${tds.join('')}</tr>`;
|
||
}).join('');
|
||
|
||
return `<div class="app_contianer jc-opt-root" id="app">
|
||
<div class="el-card"><div class="el-card__body">
|
||
<div class="el-row jc-header-row" style="height:150px;margin-left:-7.5px;margin-right:-7.5px">
|
||
<div class="el-col el-col-9 jc-header-search" style="height:100%;display:flex;flex-direction:column;justify-content:center;padding-left:7.5px;padding-right:7.5px">
|
||
<div class="el-row top-left-marquee-row" style="height:30%;margin-top:40px">
|
||
<div class="marquee-container" style="background-color:rgb(2,145,145);height:100%">
|
||
<div class="marquee-content"><span>305 房间叫乘时间已过,请值班员关注</span><span class="marquee-repeat-text">305 房间叫乘时间已过,请值班员关注</span></div>
|
||
</div>
|
||
</div>
|
||
<div class="top-left-search-area" style="flex:1;display:flex;flex-direction:column;justify-content:center;padding:10px 20px">
|
||
<div class="el-row top-left-input-row" style="margin-bottom:15px">${search}</div>
|
||
<div class="el-row top-left-action-row">
|
||
<div class="el-col el-col-24" style="display:flex;justify-content:space-between;align-items:center">
|
||
${btn('取消叫乘', ORANGE)}
|
||
<div style="display:flex;align-items:center;gap:12px">${btn('查询', BLUE)}${btn('重置', GREY)}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="el-col el-col-6 jc-header-time" style="height:100%;padding-left:7.5px;padding-right:7.5px">
|
||
<div class="el-row"><div class="center-time-section"><div class="date-time">
|
||
<p class="text-24 text-white">2026-11-09 星期一<br>09:03:21</p>
|
||
</div></div></div>
|
||
</div>
|
||
<div class="el-col el-col-9 jc-header-actions" style="height:100%;padding-left:7.5px;padding-right:7.5px">
|
||
<div class="el-row" style="height:50%;padding:8px 20px 8px 0px;display:flex;align-items:center">
|
||
<div class="el-col el-col-18"><div style="display:flex;gap:5px;justify-content:space-between;align-items:center">${topBtns}</div></div>
|
||
<div class="el-col el-col-1"></div>
|
||
<div class="el-col el-col-5"><div style="display:flex;align-items:center;gap:3px">
|
||
<span class="jc-user-label" style="font-size:12px;color:#666;width:45%">当前用户: 张值班</span>
|
||
${btn('退出', RED, 'height:32px;width:55%;')}
|
||
</div></div>
|
||
</div>
|
||
<div class="el-row" style="height:50%;padding:8px 20px 8px 0px;display:flex;align-items:center">
|
||
<div class="el-col el-col-7"><div style="display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-start">${row2Btns}</div></div>
|
||
<div class="el-col el-col-17"><div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-start;margin-left:14px">${row3Btns}</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div></div>
|
||
|
||
<div class="el-card" style="margin-top:10px"><div class="el-card__body">
|
||
<div class="el-tabs el-tabs--card"><div class="el-tabs__header is-top"><div class="el-tabs__nav-wrap"><div class="el-tabs__nav-scroll">
|
||
<div class="el-tabs__nav is-top"><div class="el-tabs__item is-top is-active">机务人员和客运人员</div><div class="el-tabs__item is-top">其他人员</div></div>
|
||
</div></div></div></div>
|
||
<div class="el-table main-jcxx-table el-table--border" style="width:100%;height:600px">
|
||
<div class="el-table__header-wrapper"><table class="el-table__header" style="width:100%"><thead><tr style="height:54px">${ths}</tr></thead></table></div>
|
||
<div class="el-table__body-wrapper" style="height:546px"><table class="el-table__body" style="width:100%"><tbody>${trs}</tbody></table></div>
|
||
</div>
|
||
</div></div>
|
||
|
||
<div class="bottom-duty-bar">
|
||
<div class="bottom-stay-summary"><span>在寓班次:14</span><span>在寓人次:30</span></div>
|
||
<div class="latest-sync-time" role="status">最新同步时间:09:03:18</div>
|
||
<div class="bottom-duty-info">
|
||
${btn('交接班', ORANGE, 'position:relative;right:20px;top:2px;')}
|
||
<span style="font-size:26px;margin-right:80px;position:relative;top:6px">值班员:张值班</span>
|
||
</div>
|
||
</div>
|
||
<div class="bottom-right-div" id="notice">有新的交路信息,请及时处理!</div>
|
||
|
||
<!-- 宽弹窗骨架:默认隐藏,验收时按需显示以测量视口约束 -->
|
||
<div class="el-dialog__wrapper jc-dialog jc-wide-dialog" id="wideWrap" style="display:none">
|
||
<div class="el-dialog" style="width:1800px"><div class="el-dialog__header"><span class="el-dialog__title">历史叫乘信息</span></div>
|
||
<div class="el-dialog__body"><div class="el-form history-jcxx-form"><div class="history-jcxx-form-row">
|
||
<div class="history-jcxx-field-box"><label class="history-jcxx-inline-label">机车号</label><div class="el-input"><input class="el-input__inner"></div></div>
|
||
<div class="history-jcxx-field-box"><label class="history-jcxx-inline-label">车次</label><div class="el-input"><input class="el-input__inner"></div></div>
|
||
</div></div></div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function makeHtml(css, label) {
|
||
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8">
|
||
<title>JcXx ${label}</title>
|
||
<style>${elementCss}</style>
|
||
<style>${css}</style>
|
||
<style>body{margin:0;background:#f3f6f5}#app{font-family:"Helvetica Neue",Helvetica,"PingFang SC","Microsoft YaHei",Arial,sans-serif}
|
||
/* 快照专用:模拟 el-table 运行时的内部横向滚动,避免 15 列被静态表格压扁 */
|
||
.main-jcxx-table .el-table__header-wrapper,
|
||
.main-jcxx-table .el-table__body-wrapper{overflow-x:auto}
|
||
.main-jcxx-table table{min-width:1500px}
|
||
</style>
|
||
</head><body>${makeBody()}</body></html>`;
|
||
}
|
||
const pb = path.join(__dirname, 'preview-before.html');
|
||
const pa = path.join(__dirname, 'preview-after.html');
|
||
fs.writeFileSync(pb, makeHtml(cssB, '优化前'), 'utf8');
|
||
fs.writeFileSync(pa, makeHtml(cssA, '优化后'), 'utf8');
|
||
const URLB = require('url').pathToFileURL(pb).href;
|
||
const URLA = require('url').pathToFileURL(pa).href;
|
||
|
||
// ---------------------------------------------------------------- 4. 多视口断言
|
||
const VIEWPORTS = [
|
||
{ w: 1920, h: 1080 }, { w: 1440, h: 900 }, { w: 1366, h: 768 },
|
||
{ w: 1280, h: 800 }, { w: 1024, h: 768 }
|
||
];
|
||
|
||
const probe = () => {
|
||
const R = s => {
|
||
const e = document.querySelector(s);
|
||
if (!e) return null;
|
||
const b = e.getBoundingClientRect();
|
||
return { t: Math.round(b.top), b: Math.round(b.bottom), l: Math.round(b.left), r: Math.round(b.right), w: Math.round(b.width), h: Math.round(b.height) };
|
||
};
|
||
const root = document.querySelector('#app');
|
||
const bar = R('.bottom-duty-bar');
|
||
const wrap = document.querySelector('#wideWrap');
|
||
wrap.style.display = 'block';
|
||
const dlg = R('.jc-wide-dialog .el-dialog');
|
||
wrap.style.display = 'none';
|
||
return {
|
||
vw: window.innerWidth, vh: window.innerHeight,
|
||
docW: document.documentElement.scrollWidth,
|
||
header: R('.jc-header-row'), search: R('.top-left-search-area'),
|
||
leftCol: R('.jc-header-search'), time: R('.center-time-section'),
|
||
actions: R('.jc-header-actions'), topBtns: R('.jc-header-actions .el-col-18 > div'),
|
||
bar, left: R('.bottom-stay-summary'), mid: R('.latest-sync-time'), right: R('.bottom-duty-info'),
|
||
notice: R('.bottom-right-div'), table: R('.main-jcxx-table'),
|
||
dlg,
|
||
paddingBottom: parseFloat(getComputedStyle(root).paddingBottom) || 0,
|
||
headerH: Math.round(document.querySelector('.jc-header-row').getBoundingClientRect().height)
|
||
};
|
||
};
|
||
|
||
(async () => {
|
||
const browser = await chromium.launch({ executablePath: EXE, headless: true });
|
||
const ctx = await browser.newContext({ viewport: { width: 1920, height: 1080 } });
|
||
const beforeIssues = {}, afterIssues = {};
|
||
|
||
for (const v of VIEWPORTS) {
|
||
const tag = `${v.w}x${v.h}`;
|
||
for (const [kind, url, bucket] of [['before', URLB, beforeIssues], ['after', URLA, afterIssues]]) {
|
||
const p = await ctx.newPage();
|
||
await p.setViewportSize({ width: v.w, height: v.h });
|
||
const errs = [];
|
||
p.on('pageerror', e => errs.push(e.message));
|
||
await p.goto(url);
|
||
await p.waitForTimeout(120);
|
||
const m = await p.evaluate(probe);
|
||
const issues = [];
|
||
if (m.docW > m.vw + 1) issues.push('横向溢出 ' + m.docW + '>' + m.vw);
|
||
if (m.search.b > m.leftCol.b + 2) issues.push('顶部查询区被裁切');
|
||
if (m.time.r > m.vw + 1) issues.push('时间卡越界');
|
||
if (m.topBtns.r > m.actions.r + 2) issues.push('右上按钮组越界');
|
||
if (m.left.r > m.mid.l + 2 || m.mid.r > m.right.l + 2) issues.push('吸底三段重叠');
|
||
if (m.notice.b > m.bar.t + 2) issues.push('右下提示被吸底条压住');
|
||
if (m.paddingBottom < 50) issues.push('底部无留白内容被遮挡');
|
||
if (m.dlg.r > m.vw + 1) issues.push('宽弹窗横向越界');
|
||
if (errs.length) issues.push('JS 错误 ' + errs.join('|'));
|
||
bucket[tag] = issues;
|
||
|
||
const shot = path.join(UI, `JcXx-${kind === 'before' ? '优化前' : '优化后'}-${tag}.png`);
|
||
await p.screenshot({ path: shot, fullPage: false });
|
||
await p.close();
|
||
}
|
||
T(`LAY-${tag} 优化后无横向溢出/裁切/重叠/遮挡/弹窗越界`,
|
||
afterIssues[tag].length === 0, JSON.stringify(afterIssues[tag]));
|
||
results.push(` └ 优化前同视口问题:${beforeIssues[tag].length ? beforeIssues[tag].join(';') : '无'}`);
|
||
}
|
||
|
||
// ------------------------------------------------------------ 交互与状态反馈
|
||
const p = await ctx.newPage();
|
||
await p.setViewportSize({ width: 1920, height: 1080 });
|
||
await p.goto(URLA);
|
||
await p.waitForTimeout(120);
|
||
await p.hover('.main-jcxx-table .el-table__body tr:nth-child(3) td');
|
||
const hoverAfter = await p.evaluate(() =>
|
||
getComputedStyle(document.querySelector('.main-jcxx-table .el-table__body tr:nth-child(3) td')).backgroundImage);
|
||
T('FB-01 表格行 hover 有可见反馈(保留行内状态底色)',
|
||
hoverAfter !== 'none' && hoverAfter.includes('gradient'), hoverAfter);
|
||
|
||
const current = await p.evaluate(() => {
|
||
const td = document.querySelector('.main-jcxx-table tr.current-row > td');
|
||
const cs = getComputedStyle(td);
|
||
return { bg: cs.backgroundColor, img: cs.backgroundImage, trBg: td.parentElement.style.backgroundColor };
|
||
});
|
||
T('FB-02 选中行不再用纯蓝底抹掉报警行状态色',
|
||
current.bg !== 'rgb(25, 144, 241)' && current.img.includes('gradient'),
|
||
JSON.stringify(current));
|
||
|
||
const grad = await p.evaluate(() => {
|
||
const b = document.querySelector('.top-left-action-row .el-button');
|
||
return getComputedStyle(b).backgroundImage;
|
||
});
|
||
await p.hover('.top-left-action-row .el-button');
|
||
const gradHover = await p.evaluate(() =>
|
||
getComputedStyle(document.querySelector('.top-left-action-row .el-button')).backgroundImage);
|
||
T('FB-03 橙色按钮 hover 不再被强制刷成蓝色渐变',
|
||
gradHover === grad && gradHover.includes('230, 162, 60'), gradHover);
|
||
|
||
const shadowBefore = await p.evaluate(() =>
|
||
getComputedStyle(document.querySelector('#q0')).boxShadow);
|
||
await p.focus('#q0');
|
||
await p.waitForTimeout(160); // headless 下 :focus 样式重算 + .3s 过渡起效
|
||
const focusFeedback = await p.evaluate(() => {
|
||
void document.body.offsetHeight; // 强制同步重排后再取计算样式
|
||
const c = getComputedStyle(document.querySelector('#q0'));
|
||
return { shadow: c.boxShadow, outlineStyle: c.outlineStyle };
|
||
});
|
||
T('FB-04 键盘焦点产生可见反馈(光晕变化 + 轮廓)',
|
||
focusFeedback.shadow !== shadowBefore && focusFeedback.outlineStyle !== 'none',
|
||
JSON.stringify({ before: shadowBefore, after: focusFeedback }));
|
||
|
||
const stateWeight = await p.evaluate(() =>
|
||
getComputedStyle(document.querySelector('.main-jcxx-table .jc-st-alarm')).fontWeight);
|
||
T('FB-05 报警状态文字权重高于普通单元格', Number(stateWeight) >= 700, stateWeight);
|
||
|
||
const noticeCursor = await p.evaluate(() =>
|
||
getComputedStyle(document.querySelector('#notice')).cursor);
|
||
T('FB-06 右下角提示有可点击指针反馈', noticeCursor === 'pointer', noticeCursor);
|
||
|
||
T('FB-07 优化层含 prefers-reduced-motion 降级', cssA.includes('prefers-reduced-motion'));
|
||
|
||
await p.close();
|
||
await browser.close();
|
||
|
||
console.log('\n================ JcXx 优化版验收 ================');
|
||
console.log(results.join('\n'));
|
||
console.log('--------------------------------------------------');
|
||
const bTotal = Object.values(beforeIssues).reduce((n, x) => n + x.length, 0);
|
||
const aTotal = Object.values(afterIssues).reduce((n, x) => n + x.length, 0);
|
||
console.log(`优化前 5 个视口累计布局问题:${bTotal} 项`);
|
||
console.log(`优化后 5 个视口累计布局问题:${aTotal} 项`);
|
||
console.log(`断言:${pass}/${pass + fail} 通过`);
|
||
console.log(`截图:${VIEWPORTS.length * 2} 张 -> ${UI}`);
|
||
console.log('==================================================');
|
||
if (fail) process.exit(1);
|
||
})().catch(e => { console.error(e); process.exit(1); });
|