This commit is contained in:
2026-08-21 09:20:26 +08:00
parent c9c6d5a0f9
commit 49465315e2
89 changed files with 16413 additions and 1776 deletions
@@ -0,0 +1,122 @@
#!/usr/bin/env node
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const root = path.resolve(__dirname, '..');
const shell = fs.readFileSync(path.join(root, 'prototype-src', 'shell.html'), 'utf8');
const tail = fs.readFileSync(path.join(root, 'prototype-src', 'tail.html'), 'utf8');
const bolStart = tail.indexOf('<div class="modal-overlay" id="modal-bol">');
const bolEnd = tail.indexOf('<!-- ==================== 装柜清单', bolStart);
const bolModal = tail.slice(bolStart, bolEnd);
assert.ok(bolStart >= 0 && bolEnd > bolStart, '缺少完整转派送弹框');
function matchingDivEnd(source, start) {
const tagPattern = /<div\b[^>]*>|<\/div>/g;
tagPattern.lastIndex = start;
let depth = 0;
let match;
while ((match = tagPattern.exec(source))) {
depth += match[0] === '</div>' ? -1 : 1;
if (depth === 0) return tagPattern.lastIndex;
}
return -1;
}
function matchingBraceEnd(source, openingBrace) {
let depth = 0;
for (let index = openingBrace; index < source.length; index += 1) {
if (source[index] === '{') depth += 1;
else if (source[index] === '}') {
depth -= 1;
if (depth === 0) return index;
}
}
return -1;
}
function ruleBlock(source, selector) {
const selectorPattern = selector
.split('>')
.map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
.join('\\s*>\\s*');
const match = new RegExp(`${selectorPattern}\\s*\\{([^}]*)\\}`).exec(source);
return match?.[1] || '';
}
function hasDeclaration(block, property, value) {
const normalized = block.replace(/\s+/g, ' ').trim();
return new RegExp(`${property}\\s*:\\s*${value}\\s*(?:;|$)`).test(normalized);
}
const overlayEnd = matchingDivEnd(bolModal, bolStart - bolStart);
const modalStart = bolModal.indexOf('<div class="modal outbound-vehicle-modal outbound-vehicle-delivery-modal"');
const modalEnd = matchingDivEnd(bolModal, modalStart);
const bodyStart = bolModal.indexOf('<div class="modal-body outbound-vehicle-modal-body">', modalStart);
const bodyEnd = matchingDivEnd(bolModal, bodyStart);
const footerStart = bolModal.indexOf('<div class="modal-footer">', bodyEnd);
const footerEnd = matchingDivEnd(bolModal, footerStart);
assert.ok(modalStart >= 0 && modalEnd > modalStart, '转派送弹框缺少主体容器');
assert.ok(bodyStart >= 0 && bodyEnd > bodyStart, '转派送弹框缺少内容区');
assert.ok(footerStart >= bodyEnd && footerEnd > footerStart, '转派送底部操作栏必须位于内容区之后');
assert.equal(
bolModal.slice(bodyEnd, footerStart).trim(),
'',
'转派送底部操作栏必须在内容区关闭后直接作为弹框主体子节点出现',
);
assert.equal(
bolModal.slice(footerEnd, modalEnd - '</div>'.length).trim(),
'',
'转派送底部操作栏必须是弹框主体的最后一个子节点',
);
assert.equal(bolModal.slice(overlayEnd).trim(), '', '转派送弹框主体必须位于遮罩层内');
const modalRule = ruleBlock(shell, '.outbound-vehicle-modal');
const bodyRule = ruleBlock(shell, '.outbound-vehicle-modal-body');
const footerRule = ruleBlock(shell, '.outbound-vehicle-modal>.modal-footer');
const mediaPattern = /@media\s*\(\s*max-width\s*:\s*760px\s*\)/g;
let mobileRule = '';
let mediaMatch;
while ((mediaMatch = mediaPattern.exec(shell))) {
const mobileOpeningBrace = shell.indexOf('{', mediaMatch.index);
const mobileEnd = matchingBraceEnd(shell, mobileOpeningBrace);
if (mobileOpeningBrace < 0 || mobileEnd <= mobileOpeningBrace) continue;
const candidateRule = ruleBlock(
shell.slice(mobileOpeningBrace + 1, mobileEnd),
'.outbound-vehicle-modal',
);
if (candidateRule) {
mobileRule = candidateRule;
break;
}
mediaPattern.lastIndex = mobileEnd + 1;
}
assert.ok(
hasDeclaration(modalRule, 'display', 'flex')
&& hasDeclaration(modalRule, 'flex-direction', 'column')
&& hasDeclaration(modalRule, 'overflow', 'hidden'),
'转派送弹框应使用纵向 flex 布局',
);
assert.ok(
hasDeclaration(bodyRule, 'flex', '1(?:\\s+1\\s+auto)?')
&& hasDeclaration(bodyRule, 'min-height', '0')
&& hasDeclaration(bodyRule, 'overflow-y', 'auto'),
'转派送内容区应独立纵向滚动',
);
assert.ok(
hasDeclaration(footerRule, 'flex', '0\\s+0\\s+auto')
&& hasDeclaration(footerRule, 'border-top', '1px\\s+solid\\s+#e5e7eb'),
'转派送底栏应固定在弹框内并使用中性分隔线',
);
assert.ok(
hasDeclaration(mobileRule, 'width', 'calc\\(100vw\\s+-\\s+16px\\)'),
'移动端弹框应保持窄视口宽度约束',
);
console.log('OUTBOUND_VEHICLE_MODAL_LAYOUT_CHECK_PASS');