tech-bid-manage20260423A/清标工具.js
2026-04-23 17:10:38 +08:00

311 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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.

// darkBidChecker.js
// 逻辑已迁移至项目内 Pythonmodules/dark_bid_format_check.py本文件保留作参考
// 依赖: jsdom (npm install jsdom)
const { JSDOM } = require('jsdom');
/**
* 暗标格式检查器
* @param {string} htmlContent - 技术暗标的HTML内容由Word/PDF导出的完整HTML
* @param {Object} options - 可选配置
* @returns {Object} 符合格式的JSON检查报告
*/
function checkTechnicalBid(htmlContent, options = {}) {
const dom = new JSDOM(htmlContent);
const document = dom.window.document;
const styleSheets = Array.from(document.styleSheets);
// 辅助函数获取元素实际渲染样式jsdom支持有限但可获取内联和style标签定义
function getStyle(element, property) {
return dom.window.getComputedStyle(element).getPropertyValue(property);
}
// 结果收集器
const results = {
overall: true,
details: [],
violations: []
};
// 通用添加结果方法
function addResult(ruleName, passed, message, elements = []) {
results.details.push({ rule: ruleName, passed, message });
if (!passed) {
results.overall = false;
results.violations.push({ rule: ruleName, message, elements: elements.map(el => el.outerHTML.slice(0, 200)) });
}
}
// ========== 1. 检查是否存在投标人身份信息 ==========
function checkIdentityInfo() {
const bodyText = document.body.innerText;
// 公司名称模式(可扩展)
const companyPattern = /(?:我公司|本公司|[(]?[A-Za-z\u4e00-\u9fa5]+(?:集团|股份|有限|责任|公司)[)]?)/g;
// 地址模式(省市区路号等)
const addrPattern = /(?:省|市|区|县|镇|路|街|大道|号|大厦|楼|层)[\u4e00-\u9fa50-9]+/g;
// 总监/专监真实姓名模式(除甲、乙等代称外)
const namePattern = /(?:总监理工程师|专业监理工程师|技术负责人|项目经理)[:]\s*[^甲乙丙丁戊己庚辛壬癸\s]{2,4}(?=[,。;\s]|$)/g;
let foundCompany = false;
let foundAddr = false;
let foundRealName = false;
if (companyPattern.test(bodyText)) foundCompany = true;
if (addrPattern.test(bodyText)) foundAddr = true;
if (namePattern.test(bodyText)) foundRealName = true;
// 检查图片alt或title是否包含公司标识
const images = document.querySelectorAll('img');
let hasLogo = false;
images.forEach(img => {
const alt = img.alt || '';
const src = img.src || '';
if (/logo|商标|微标|公司|品牌/i.test(alt) || /logo/i.test(src)) hasLogo = true;
});
const passed = !(foundCompany || foundAddr || foundRealName || hasLogo);
addResult('身份信息隐藏', passed,
passed ? '未发现投标人身份信息' : '发现投标人身份信息(公司名/地址/真实姓名/商标)');
}
// ========== 2. 标题格式检查(三号黑体,非斜体,无下划线等) ==========
function checkHeadings() {
// 标题选择器h1-h6 或任何 role="heading" 或具有大纲级别样式的元素
const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6, [role="heading"], .heading, .title');
let allValid = true;
const invalidHeadings = [];
headings.forEach(heading => {
const fontSize = getStyle(heading, 'font-size');
const fontFamily = getStyle(heading, 'font-family').toLowerCase();
const fontStyle = getStyle(heading, 'font-style');
const textDecoration = getStyle(heading, 'text-decoration');
const color = getStyle(heading, 'color');
const fontWeight = getStyle(heading, 'font-weight');
// 三号 ≈ 16pt (21.33px) 允许误差 ±2px
const sizeOk = Math.abs(parseFloat(fontSize) - 21.33) <= 3;
const fontOk = fontFamily.includes('黑体') || fontFamily.includes('simhei') || fontFamily.includes('microsoft yahei');
const styleOk = fontStyle !== 'italic';
const decorOk = !textDecoration.includes('underline');
const colorOk = color === 'rgb(0, 0, 0)' || color === '#000000';
const weightOk = fontWeight !== '400' && fontWeight !== 'normal'; // 黑体通常加粗
if (!(sizeOk && fontOk && styleOk && decorOk && colorOk && weightOk)) {
allValid = false;
invalidHeadings.push(heading);
}
});
addResult('标题格式', allValid,
allValid ? '所有标题符合三号黑体要求' : '部分标题字号/字体/颜色/下划线不符合要求', invalidHeadings);
}
// ========== 3. 正文格式检查四号宋体首行缩进2字符行距固定值26磅无着色等 ==========
function checkBodyText() {
const bodyElements = document.querySelectorAll('p, div, span, li, td, th');
let allValid = true;
const invalidElements = [];
// 排除标题、页眉页脚、目录等
const excludeSelectors = 'h1, h2, h3, h4, h5, h6, .header, .footer, .toc, .目录';
bodyElements.forEach(el => {
if (el.matches(excludeSelectors)) return;
const text = el.innerText.trim();
if (text.length === 0) return;
const fontSize = getStyle(el, 'font-size');
const fontFamily = getStyle(el, 'font-family').toLowerCase();
const color = getStyle(el, 'color');
const textIndent = getStyle(el, 'text-indent');
const lineHeight = getStyle(el, 'line-height');
const textDecoration = getStyle(el, 'text-decoration');
const fontWeight = getStyle(el, 'font-weight');
const fontStyle = getStyle(el, 'font-style');
// 四号 ≈ 14pt (18.67px)
const sizeOk = Math.abs(parseFloat(fontSize) - 18.67) <= 2;
const fontOk = fontFamily.includes('宋体') || fontFamily.includes('simsun') || fontFamily.includes('serif');
const colorOk = color === 'rgb(0, 0, 0)' || color === '#000000';
// 首行缩进2字符以em为单位2em代表2个汉字
const indentOk = parseFloat(textIndent) >= 1.8 && parseFloat(textIndent) <= 2.2;
// 行距固定值26磅 (34.67px)
const lineHeightOk = Math.abs(parseFloat(lineHeight) - 34.67) <= 2;
const decorOk = !textDecoration.includes('underline');
const weightOk = fontWeight === '400' || fontWeight === 'normal';
const styleOk = fontStyle !== 'italic';
if (!(sizeOk && fontOk && colorOk && indentOk && lineHeightOk && decorOk && weightOk && styleOk)) {
allValid = false;
invalidElements.push(el);
}
});
addResult('正文格式', allValid,
allValid ? '所有正文符合四号宋体/缩进/行距/颜色要求' : '部分正文段落格式不符合要求', invalidElements);
}
// ========== 4. 目录检查(无页码,无页眉页脚) ==========
function checkTOC() {
const tocElements = document.querySelectorAll('.toc, .table-of-contents, .目录, [role="directory"]');
let noPageNumbers = true;
let noHeaderFooter = true;
tocElements.forEach(toc => {
const text = toc.innerText;
// 检查是否存在页码(数字独立在行尾或制表符后)
if (/\d+$/.test(text.trim()) || /\.{2,}\s*\d+/.test(text)) {
noPageNumbers = false;
}
// 检查内部是否有页眉页脚元素
if (toc.querySelector('.header, .footer, .page-header, .page-footer')) {
noHeaderFooter = false;
}
});
// 若没有目录元素,按规则应存在目录但不得有页码,这里假设必须存在目录(招标要求通常有目录)
if (tocElements.length === 0) {
addResult('目录要求', false, '未检测到目录,请确保包含目录且目录无页码无页眉页脚');
} else {
const passed = noPageNumbers && noHeaderFooter;
addResult('目录要求', passed,
passed ? '目录符合无页码、无页眉页脚要求' : '目录中存在页码或页眉页脚');
}
}
// ========== 5. 图表位置及图表内文字格式 ==========
function checkChartsAndTables() {
// 定位附件/附表章节
const appendix = document.querySelector('#appendix, .appendix, .attachment, 附件, 附表');
const isInAppendix = (el) => appendix && appendix.contains(el);
const allTables = document.querySelectorAll('table');
const allImages = document.querySelectorAll('img');
const allFigures = document.querySelectorAll('figure, .chart');
let illegalCharts = [];
// 正文中不允许有图表,除非在附件内
[...allTables, ...allImages, ...allFigures].forEach(chart => {
if (!isInAppendix(chart)) {
illegalCharts.push(chart);
}
});
let chartTextValid = true;
// 附件内图表文字需五号宋体
if (appendix) {
const chartTexts = appendix.querySelectorAll('table, td, th, figcaption, .chart-text');
chartTexts.forEach(el => {
const fontSize = getStyle(el, 'font-size');
const fontFamily = getStyle(el, 'font-family').toLowerCase();
const color = getStyle(el, 'color');
const sizeOk = Math.abs(parseFloat(fontSize) - 10.5) <= 1.5; // 五号=10.5pt
const fontOk = fontFamily.includes('宋体') || fontFamily.includes('simsun');
const colorOk = color === 'rgb(0, 0, 0)' || color === '#000000';
if (!(sizeOk && fontOk && colorOk)) chartTextValid = false;
});
}
const chartsPassed = illegalCharts.length === 0 && chartTextValid;
addResult('图表规范', chartsPassed,
chartsPassed ? '图表仅出现在附件/附表内,且图表文字符合五号宋体' :
`正文中发现${illegalCharts.length}个图表或附件内图表文字格式错误`, illegalCharts);
}
// ========== 6. 全文字体颜色检查(无彩色,无着重号,无下划线) ==========
function checkColorsAndDecorations() {
const allElements = document.querySelectorAll('*');
let colorViolations = [];
let decorationViolations = [];
allElements.forEach(el => {
const color = getStyle(el, 'color');
if (color !== 'rgb(0, 0, 0)' && color !== '#000000' && color !== 'black') {
if (el.innerText.trim().length > 0) colorViolations.push(el);
}
const textDecor = getStyle(el, 'text-decoration');
if (textDecor.includes('underline')) decorationViolations.push(el);
// 着重号检测一般使用伪元素或border-bottom简单检测样式
const borderBottom = getStyle(el, 'border-bottom-style');
if (borderBottom === 'solid' || borderBottom === 'dotted') {
decorationViolations.push(el);
}
});
const passed = colorViolations.length === 0 && decorationViolations.length === 0;
addResult('颜色与装饰', passed,
passed ? '无彩色文字、无下划线、无着重号' :
`发现${colorViolations.length}处彩色文字,${decorationViolations.length}处下划线/着重号`,
[...colorViolations, ...decorationViolations]);
}
// ========== 7. 页面设置检查A4纵向页边距 ==========
function checkPageSetup() {
// 检查@page规则或根元素margin
let pageValid = true;
let marginTop, marginBottom, marginLeft, marginRight;
// 尝试从styleSheets中获取@page
let pageRule = null;
for (let sheet of styleSheets) {
try {
const rules = sheet.cssRules || sheet.rules;
for (let rule of rules) {
if (rule.type === CSSRule.PAGE_RULE) {
pageRule = rule.style;
break;
}
}
} catch(e) { /* 跨域限制忽略 */ }
}
if (pageRule) {
marginTop = pageRule.marginTop;
marginBottom = pageRule.marginBottom;
marginLeft = pageRule.marginLeft;
marginRight = pageRule.marginRight;
const size = pageRule.size;
if (size && size.toLowerCase() !== 'a4') pageValid = false;
} else {
// 检查body或根容器的margin
const bodyStyle = getStyle(document.body, 'margin');
if (bodyStyle) {
const margins = bodyStyle.split(' ');
// 简单近似
marginTop = margins[0];
marginBottom = margins[2] || margins[0];
marginLeft = margins[3] || margins[1];
marginRight = margins[1];
} else {
pageValid = false;
}
}
const topOk = marginTop === '2.54cm' || parseFloat(marginTop) === 2.54;
const bottomOk = marginBottom === '2.54cm' || parseFloat(marginBottom) === 2.54;
const leftOk = marginLeft === '3.18cm' || parseFloat(marginLeft) === 3.18;
const rightOk = marginRight === '3.18cm' || parseFloat(marginRight) === 3.18;
const pageOrientation = document.documentElement.style.width === 'auto' ? '纵向' : '横向'; // 近似
const passed = topOk && bottomOk && leftOk && rightOk && pageOrientation !== '横向';
addResult('页面设置', passed,
passed ? '页面设置符合A4纵向/边距要求' : '页面边距或纸张方向不符合要求');
}
// 执行所有检查
checkIdentityInfo();
checkHeadings();
checkBodyText();
checkTOC();
checkChartsAndTables();
checkColorsAndDecorations();
checkPageSetup();
return results;
}
// 导出函数供外部使用
module.exports = { checkTechnicalBid };
// ==================== 使用示例 ====================
// 假设已有htmlContent技术暗标HTML字符串
// const report = checkTechnicalBid(htmlContent);
// console.log(JSON.stringify(report, null, 2));