Files
yl/tools/rewrite_user_manual_pages.js
2026-05-26 17:59:27 +08:00

574 lines
30 KiB
JavaScript
Raw Permalink 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.
const fs = require('fs');
const path = require('path');
const repoRoot = process.cwd();
const projectRoot = path.join(repoRoot, 'MES_Manage_View_V20');
const viewsRoot = path.join(projectRoot, 'src', 'views');
const manualPath = path.join(projectRoot, '元利项目详细用户使用说明书.md');
function readText(file) {
const buf = fs.readFileSync(file);
if (buf.length >= 3 && buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) {
return buf.slice(3).toString('utf8');
}
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xfe) {
return buf.slice(2).toString('utf16le');
}
return buf.toString('utf8');
}
function writeText(file, text) {
fs.writeFileSync(file, text, 'utf8');
}
function walk(dir, out = []) {
if (!fs.existsSync(dir)) return out;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full, out);
else if (/\.(vue|js)$/i.test(entry.name)) out.push(full);
}
return out;
}
function rel(file) {
return path.relative(projectRoot, file).replace(/\\/g, '/');
}
function clean(value) {
return (value || '')
.replace(/<!--[\s\S]*?-->/g, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/\{\{[\s\S]*?\}\}/g, ' ')
.replace(/&nbsp;/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function uniq(values) {
const seen = new Set();
const out = [];
for (const value of values) {
const v = String(value || '').trim();
if (!v || seen.has(v)) continue;
seen.add(v);
out.push(v);
}
return out;
}
function stripNoise(values) {
return uniq(values)
.filter(x => x.length <= 42)
.filter(x => !/[{};]/.test(x))
.filter(x => !/^(scope|row|item|index|true|false|null|undefined)$/i.test(x))
.filter(x => !/^\d+$/.test(x));
}
function extractAll(re, text, map) {
const out = [];
let match;
while ((match = re.exec(text))) out.push(map(match));
return out;
}
function attr(attrs, name) {
const m = new RegExp(`${name}="([^"]*)"`).exec(attrs);
return m ? clean(m[1]) : '';
}
function extractTemplate(text) {
const start = text.search(/<template\b[^>]*>/i);
if (start < 0) return text;
const open = /<template\b[^>]*>/i.exec(text.slice(start));
if (!open) return text;
const contentStart = start + open.index + open[0].length;
const scriptStart = text.search(/<script\b/i);
const searchEnd = scriptStart >= 0 ? scriptStart : text.length;
const close = text.lastIndexOf('</template>', searchEnd);
if (close > contentStart) return text.slice(contentStart, close);
return text.slice(contentStart, searchEnd);
}
function extractScript(text) {
const m = /<script\b[^>]*>([\s\S]*?)<\/script>/i.exec(text);
return m ? m[1] : text;
}
function extractPage(file) {
const text = readText(file);
const template = extractTemplate(text);
const script = extractScript(text);
const relative = rel(file);
const parts = relative.split('/');
const module = parts[2] || '根页面';
const name = path.basename(file) === 'index.vue'
? parts[parts.length - 2]
: path.basename(file);
const labels = extractAll(/\blabel="([^"]+)"/g, template, m => clean(m[1]));
const placeholders = extractAll(/\bplaceholder="([^"]+)"/g, template, m => clean(m[1]));
const titles = extractAll(/\btitle="([^"]+)"/g, template, m => clean(m[1]));
const formLabels = extractAll(/<el-form-item\b([^>]*)>/g, template, m => attr(m[1], 'label'));
const tabLabels = extractAll(/<el-tab-pane\b([^>]*)>/g, template, m => attr(m[1], 'label'));
const options = extractAll(/<el-option\b([^>]*)>/g, template, m => attr(m[1], 'label'));
const buttons = extractAll(/<el-button\b([^>]*)>([\s\S]*?)<\/el-button>/g, template, m => {
const textValue = clean(m[2]) || attr(m[1], 'title') || attr(m[1], 'aria-label');
const click = attr(m[1], '@click') || attr(m[1], 'v-on:click');
return textValue ? { text: textValue, click } : null;
})
.filter(Boolean)
.filter(b => b.text.length <= 42)
.filter(b => !/[{};]/.test(b.text));
const tableColumns = extractAll(/<el-table-column\b([^>]*)>/g, template, m => {
const label = attr(m[1], 'label');
const prop = attr(m[1], 'prop');
const type = attr(m[1], 'type');
return label || prop || type ? { label, prop, type } : null;
}).filter(Boolean);
const vModels = extractAll(/\bv-model(?:\.[\w-]+)?="([^"]+)"/g, template, m => clean(m[1]));
const params = extractAll(/\.(?:push|unshift)\s*\(\s*\[\s*['"]([^'"]+)['"]\s*,\s*([^\]\n]+)\]/g, script, m => ({
name: clean(m[1]),
expr: clean(m[2])
}));
const procs = extractAll(/CreateData\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/g, script, m => ({
type: m[1],
name: clean(m[2])
}));
const sapCalls = extractAll(/\b(getB1|postB1|patchB1|posttm)\s*\(\s*([`'"])([\s\S]*?)\2/g, script, m => ({
method: m[1],
url: clean(m[3]).slice(0, 90)
}));
const dialogs = extractAll(/<el-dialog\b([\s\S]*?)>/g, template, m => {
const title = attr(m[1], 'title') || clean((/:title="([^"]+)"/.exec(m[1]) || [])[1]);
return title;
});
const methods = extractAll(/^\s*([A-Za-z_$\u4e00-\u9fa5][\w$\u4e00-\u9fa5]*)\s*\([^)]*\)\s*\{/gm, script, m => m[1]);
const hasCalendar = /FullCalendar|@fullcalendar|eventDrop|eventResize/.test(text);
const hasPagination = /el-pagination|pageSize|pageCurrent/.test(text);
const hasUpload = /el-upload|上传|upload/i.test(text);
const hasTree = /el-tree|treeData|Tree/.test(text);
return {
file,
relative,
module,
name,
text,
labels: stripNoise(labels),
placeholders: stripNoise(placeholders),
titles: stripNoise(titles),
formLabels: stripNoise(formLabels),
tabLabels: stripNoise(tabLabels),
options: stripNoise(options),
buttons,
tableColumns,
vModels: stripNoise(vModels),
params,
procs,
sapCalls,
dialogs: stripNoise(dialogs),
methods: stripNoise(methods),
hasCalendar,
hasPagination,
hasUpload,
hasTree
};
}
function roleFor(page) {
if (/SystemMaintenance|layout|login|dashboard|404/.test(page.relative)) return '系统管理员/相关用户';
if (/QualityManagement/.test(page.relative)) return '质检员/质量主管';
if (/PlanManagement/.test(page.relative)) return '计划员/生产主管';
if (/ProductionManagement/.test(page.relative)) return '操作工/班组长/生产主管';
if (/ProcessManagement|CraftManagement/.test(page.relative)) return '工艺员/技术人员';
if (/DeviceManagement/.test(page.relative)) return '设备管理员/生产主管';
if (/SaleManagement/.test(page.relative)) return '销售/计划/生产跟单人员';
if (/AnomalousManagement/.test(page.relative)) return '班组长/生产主管/计划员';
return '相关业务人员';
}
function moduleName(page) {
const map = {
AnomalousManagement: '异常管理',
CraftManagement: '产品结构维护',
DeviceManagement: '设备管理',
PlanManagement: '计划排产',
ProcessManagement: '工艺与图纸管理',
ProductionManagement: '生产执行管理',
QualityManagement: '质量管理',
SaleManagement: '销售与交付查询',
SystemMaintenance: '系统维护',
dashboard: '首页与看板',
layout: '布局与导航',
login: '登录',
};
if (page.relative === 'src/views/404.vue') return '错误页';
return map[page.module] || page.module;
}
function purpose(page) {
const p = page.relative;
const rules = [
[/404\.vue$/, '该页面用于路由不存在或无效地址访问时展示 404 提示,帮助用户返回首页或重新选择菜单。'],
[/login/, '该页面是系统登录入口,用户输入工号、密码并选择角色后进入 MES同时提供修改密码功能。'],
[/dashboard/, '该页面是系统首页/看板入口,用户登录后查看系统概览或进入各业务模块。'],
[/PunctualWork/, '该页面用于查看未及时开工、待执行、可执行、已开工和已完成任务,帮助班组长或计划员催办生产开工。'],
[/CraftMaintain/, '该页面用于维护产品结构树支持新增、编辑、删除产品结构节点为工艺、BOM 和生产任务提供层级基础资料。'],
[/DeviceDataHistory/, '该页面用于查询设备历史采集数据,按工位、设备、参数和时间追溯设备运行记录。'],
[/DeviceInformation/, '该页面用于维护设备台账、设备属性、维修/工时/点检相关信息。'],
[/DeviceRealData|Devicedata/, '该页面用于查看设备实时数据或采集缓存数据,判断设备当前运行、报警和参数状态。'],
[/SelfMakePlandone[\\/]/, '该页面用于机加/自制件已排产任务查看和调整,支持计划时间、指派对象、指派数量、最晚开始、二次派工和备注维护。'],
[/SelfMakePlando[\\/]/, '该页面用于机加/自制件待排产任务处理,支持表单排产、日程拖拽调整、部分派工、批量排产、二次派工、原料状态和图纸查看。'],
[/SelfMakePlan[\\/]/, '该页面用于机加/自制件排产前准备,查询生产订单工序任务并维护计划、工位、数量和说明后确认排产。'],
[/InstallMakePlanDo|InstallMakePlan/, '该页面用于装配件排产,维护装配工位、人员、计划开始/完成、优先级、说明,并在排产完成时同步 SAP 或生成相关请求。'],
[/PlanShell/, '该页面用于计划外壳和计划跟踪,集中查看订单计划、齐套、缺料、发货或关闭相关信息。'],
[/PlannOrderClose/, '该页面用于关闭生产订单,用户查询待关闭订单后执行 MES/SAP 关闭动作。'],
[/priorityinstall/, '该页面用于装配计划优先级和日程调整,支持表单和日程方式查看重点任务。'],
[/ProductionOrder/, '该页面用于查询和同步生产订单,把 SAP/ERP 生产订单转成 MES 生产计划与工序任务。'],
[/PartDrawing/, '该页面用于维护零件图纸,支持按物料查询、查看、上传/验证和删除图纸。'],
[/PartDrawlook|ProcurementDrawlook/, '该页面用于按物料查看零件图纸,供生产、采购、外协或质检快速查阅技术文件。'],
[/ProcessDrawing/, '该页面用于维护工艺图纸,支持查询、查看、上传/验证和删除工艺图纸。'],
[/ProcedureManagement/, '该页面用于维护工序/路线阶段主数据,并与 SAP B1 RouteStages 保持一致。'],
[/ProcessInquiry/, '该页面用于查询标准工艺或生产工艺,供排产、生产、质检核对工序路线。'],
[/ProcessProduct|ProcessesDevelop/, '该页面用于维护产品工艺资料、工序路线、参数和图纸关系。'],
[/MachiningCenter/, '该页面是机加生产执行主界面,操作工按工位执行收料、开工、暂停、报工、检验、退库、发料和图纸查看。'],
[/AssemblyCenter/, '该页面是装配生产执行主界面,装配人员按工位执行开工、报工、送检、退库、缺料查询和图纸查看。'],
[/ProductionTaskManage|AssembleTaskManage/, '该页面用于生产/装配任务管理,查询任务、查看工时、维护派工和任务说明。'],
[/ProductionTasksSum|AssembleTasksSum/, '该页面用于生产/装配任务汇总,按订单、人员、工位、状态和时间统计任务执行情况。'],
[/ProductionPlanTrack|AssemblePlanTrack/, '该页面用于跟踪生产/装配计划执行进度,查看计划、完成、延期和异常情况。'],
[/AssembleStatus|DeviceStatus/, '该页面用于查看现场状态,关注工位、设备、任务是否开工、进行中或完成。'],
[/OutsourceMange|SaleOutsourceMange/, '该页面用于外协任务管理,处理外协派工、供应商、发料、收料、质检记录和 SAP 采购请求。'],
[/OutsourceSheet/, '该页面用于外协报表查询和导出,跟踪外协订单、供应商、数量、质检和结算状态。'],
[/PickingTask/, '该页面用于生产领料/拣配任务查询与处理,核对订单物料和库存任务。'],
[/PersonnelBinding/, '该页面用于维护人员与工位绑定关系,控制或提示人员可操作的工位。'],
[/ShiftManagement/, '该页面用于维护班次、班次人员和排班基础数据。'],
[/Timesheet|Workhours$/, '该页面用于查询工时记录,按人员、设备、任务、订单和日期统计工时。'],
[/WorkhoursEdit/, '该页面用于修正或补录工时/报工记录,处理异常工时。'],
[/WorkHoursCheck/, '该页面用于工时校验和 SAP 提交,用户核对报工记录后批量或单条校验并上传相关单据。'],
[/Urgent/, '该页面用于维护加急任务,跟踪加急订单、加急数量和完成情况。'],
[/AbnormalControl/, '该页面用于生产异常管理,查询异常、处理异常、转办、查看进度和关闭异常。'],
[/QualityCenter/, '该页面是质量综合处理中心,集中处理待检、收检、检测明细、不合格和 SAP/WMS 联动。'],
[/QualityDetails/, '该页面用于查询质检明细,按订单、物料、检验类型和时间追溯检测记录。'],
[/QualityStandardQuery/, '该页面用于查询或维护质量检验标准、检测项目、上下限和启用状态。'],
[/Unqualified/, '该页面用于不合格品处理,记录处置方式、责任、原因和处理结果。'],
[/CheckTask[\\/].*purchaseIncoming/, '该页面用于采购/来料检验,质检人员录入来料检验数量、合格数、不合格数和检测明细。'],
[/CheckTask[\\/].*processReceive/, '该页面用于工序收检/序检收检,质检人员接收工序检验任务并录入检验结果。'],
[/CheckTask[\\/].*firstSpecial/, '该页面用于首检或特殊首件检验,录入首件检测结果。'],
[/CheckTask[\\/].*final/, '该页面用于终检任务处理,录入最终检验结果。'],
[/CheckTask[\\/].*materialReturn/, '该页面用于材料退库检验,核对退库物料并录入检验结论。'],
[/CheckTask[\\/].*salesReturn/, '该页面用于销售退货检验,录入退货质检结果。'],
[/CheckTask[\\/].*OtherInbound/, '该页面用于其他入库检验,录入其他入库质检结果。'],
[/CheckTask[\\/].*processSpecial/, '该页面用于过程特殊检验,录入特殊检验项目和结果。'],
[/saleorderstatus/, '该页面用于查询销售订单状态,跟踪销售订单在生产、交付和关闭过程中的状态。'],
[/MenuManagement/, '该页面用于维护系统菜单、按钮权限和路由配置。'],
[/OperationLog/, '该页面用于查询操作日志,追溯用户操作、报工或业务变更记录。'],
[/OrgManagement/, '该页面用于维护组织、部门或组织树基础资料。'],
[/PersonnelManagement/, '该页面用于维护人员账号、部门、角色和启用状态。'],
[/SystemRrolemaintenance/, '该页面用于维护系统角色和角色权限。'],
[/WorkstationManagement/, '该页面用于维护工位/资源主数据,并同步 SAP B1 Resources。'],
[/layout|Sidebar|Navbar|TagsView|AppMain|ResizeHandler/, '该文件属于系统布局和导航组件,用户通过它切换菜单、标签页、折叠菜单或退出登录,不维护具体业务单据。'],
];
const found = rules.find(([re]) => re.test(p));
if (found) return found[1];
return '该页面为业务页面,用户按查询条件筛选数据,在列表或弹窗中查看明细,并按页面按钮完成维护、提交或状态更新。';
}
function actionVerb(button) {
const text = button.text;
if (/查询|搜索|全部|待执行|可执行|已开工|已完成|未完成/.test(text)) return '查询/筛选';
if (/新增|添加|新建|创建/.test(text)) return '新增';
if (/编辑|修改|保存|确定|确 定/.test(text)) return '编辑/保存';
if (/删除|移除/.test(text)) return '删除';
if (/导出|打印/.test(text)) return '导出/打印';
if (/查看|详情|图纸|明细|进度/.test(text)) return '查看';
if (/排产|派工|开工|报工|送检|收料|发料|退库|关闭|提交|处理|转办|完结|校验|上传/.test(text)) return '业务提交';
if (/取消|关闭|返回/.test(text)) return '取消/关闭';
return '操作';
}
function fieldName(value) {
return value
.replace(/^.*\./, '')
.replace(/\[[^\]]+\]/g, '')
.replace(/Form$/, '')
.replace(/Time$/, '时间')
.replace(/No$/, '编号');
}
function queryItems(page) {
const fromParams = page.params
.filter(p => !/任务列表|操作人|最后编辑人|editstr|personnel|TaskAID/.test(p.name))
.map(p => p.name);
const fromModels = page.vModels
.filter(v => !/^scope\.row|^item\.|^row\.|multipleSelection|loading|dialog|Visible|activeTab/.test(v))
.map(fieldName);
const fromPlaceholders = page.placeholders
.map(x => x.replace(/^请输入/, '').replace(/^请选择/, '').replace(/起始|结束/g, '').trim())
.filter(Boolean);
return stripNoise([...fromParams, ...fromModels, ...fromPlaceholders]).slice(0, 22);
}
function tableItems(page) {
return stripNoise(page.tableColumns.map(c => c.label || c.prop || c.type)).slice(0, 34);
}
function buttonItems(page) {
return stripNoise(page.buttons.map(b => b.text)).slice(0, 30);
}
function procItems(page) {
return uniq(page.procs.map(p => p.name)).slice(0, 20);
}
function sapItems(page) {
return uniq(page.sapCalls.map(s => `${s.method} ${s.url}`)).slice(0, 12);
}
function lineList(values, fallback) {
if (!values.length) return `- ${fallback}`;
return values.map(v => `- \`${v}\``).join('\n');
}
function detailedSteps(page) {
const steps = [];
const queries = queryItems(page);
const tabs = page.tabLabels;
const columns = tableItems(page);
const buttons = page.buttons.slice(0, 24);
const dialogs = page.dialogs;
const forms = page.formLabels;
const procs = procItems(page);
const pathText = page.relative;
steps.push(`从左侧菜单进入该页,确认页面标题、当前账号和业务角色正确。`);
if (tabs.length) {
steps.push(`先查看页签:${tabs.map(x => `\`${x}\``).join('、')};需要表格处理时进入表单/列表页签,需要看时间排布时进入日程/看板页签。`);
}
if (queries.length) {
steps.push(`在查询区按需要填写筛选条件:${queries.slice(0, 12).map(x => `\`${x}\``).join('、')}`);
if (queries.length > 12) steps.push(`如果仍查不到数据,继续补充条件:${queries.slice(12, 22).map(x => `\`${x}\``).join('、')},或清空过窄条件后重新查询。`);
} else {
steps.push(`该页没有识别到固定查询区,按页面提示直接查看信息或使用菜单/按钮进入下一步。`);
}
const queryButtons = buttons.filter(b => actionVerb(b) === '查询/筛选');
if (queryButtons.length) {
steps.push(`点击 ${stripNoise(queryButtons.map(b => b.text)).slice(0, 8).map(x => `\`${x}\``).join('、')} 刷新列表;状态按钮会切换任务范围。`);
} else if (buttons.length) {
steps.push(`点击页面上的查询或刷新类按钮获取最新数据;如果页面自动加载,则先核对默认列表是否符合当前业务范围。`);
}
if (page.hasPagination) {
steps.push(`列表较多时使用分页区切换页码或调整每页条数,避免只检查第一页。`);
}
if (columns.length) {
steps.push(`查询后先核对关键列:${columns.slice(0, 12).map(x => `\`${x}\``).join('、')}`);
if (columns.length > 12) steps.push(`继续横向滚动或展开表格,核对:${columns.slice(12, 26).map(x => `\`${x}\``).join('、')}`);
}
if (page.hasTree) {
steps.push(`如果页面左侧或上方有树形结构,先选中组织、物料、菜单或产品节点,再查看右侧列表;新增子节点前确认父节点选中正确。`);
}
const nonQueryButtons = buttons.filter(b => !['查询/筛选', '取消/关闭'].includes(actionVerb(b)));
for (const button of nonQueryButtons.slice(0, 10)) {
const verb = actionVerb(button);
if (verb === '新增') steps.push(`需要新增时点击 \`${button.text}\`,在弹窗中逐项填写必填字段,确认编码、名称、状态等基础信息后保存。`);
else if (verb === '编辑/保存') steps.push(`需要维护已有记录时先选中目标行,再点击或触发表格内 \`${button.text}\`,修改字段后保存并等待成功提示。`);
else if (verb === '删除') steps.push(`需要删除时先确认该记录没有正在使用的业务数据,再点击 \`${button.text}\`,按确认提示执行。`);
else if (verb === '导出/打印') steps.push(`需要留存或流转时点击 \`${button.text}\`,导出/打印前确认当前查询条件和选中记录正确。`);
else if (verb === '查看') steps.push(`需要查看明细时点击 \`${button.text}\`,在弹窗或新区域核对图纸、明细、进度、记录或关联单据。`);
else if (verb === '业务提交') steps.push(`需要推进业务时先选中目标行并核对数量、时间、人员、工位、物料和备注,再点击 \`${button.text}\` 提交。`);
else steps.push(`按业务需要点击 \`${button.text}\`,操作前先确认选中行和输入内容。`);
}
if (dialogs.length) {
steps.push(`出现弹窗时按弹窗标题处理:${dialogs.slice(0, 8).map(x => `\`${x}\``).join('、')};弹窗关闭前确认必填项已保存。`);
}
if (forms.length) {
steps.push(`弹窗或表单内重点填写:${forms.slice(0, 14).map(x => `\`${x}\``).join('、')},提交前逐项核对。`);
}
if (page.hasUpload) {
steps.push(`涉及上传时先选择正确文件,再点击上传/保存;上传后重新查询,确认文件名、版本、上传时间或预览内容已显示。`);
}
if (page.hasCalendar) {
steps.push(`进入日程页后,单击任务查看详情;拖拽任务可调整计划开始时间,拉伸任务可调整计划完成时间,系统提示确认后才会保存。`);
steps.push(`日程保存失败或取消时任务会恢复原日期;保存成功后回到表单页重新查询,核对计划开始和计划完成是否同步。`);
}
if (procs.length) {
steps.push(`保存或提交成功后重新点击查询,确认页面数据已通过 ${procs.slice(0, 5).map(x => `\`${x}\``).join('、')} 等后台过程更新。`);
}
if (page.sapCalls.length) {
steps.push(`涉及 SAP/WMS 接口时,必须查看页面成功或失败提示;失败时先记录错误信息,不要连续重复提交同一单据。`);
}
if (/SelfMakePlando[\\/]/.test(pathText)) {
steps.push(`\`指派数量\` 中输入小于计划数的数量时,表示本次只派出部分数量,剩余数量需要继续跟踪和后续派工。`);
steps.push(`点击 \`批量排产\` 前应选择同一生产订单的多道工序,系统会按订单行号排序并继承上一道工序的部分计划信息。`);
steps.push(`点击 \`排产完成\` 后,系统校验指派对象、指派数量、计划开始、计划完成,并同步生产订单最早开始和最晚完成。`);
}
if (/SelfMakePlandone[\\/]/.test(pathText)) {
steps.push(`已排产列表中仍可调整指派对象、指派数量、计划开始、计划完成、外协分组、最晚开始和二次派工;调整后要重新查询确认。`);
}
if (/MachiningCenter|AssemblyCenter/.test(pathText)) {
steps.push(`现场执行类操作必须按实际顺序处理:先确认任务和工位,再收料/开工,完成加工或装配后再报工/送检。`);
}
if (/QualityManagement/.test(pathText)) {
steps.push(`录入检验结果时先核对检验类型、订单、物料、批次/工序,再填写合格数、不合格数和检测明细,避免检验记录挂错任务。`);
}
if (/WorkHoursCheck/.test(pathText)) {
steps.push(`工时校验前先比对 MES 报工、设备工时和 SAP 工序资源;只对核对无误的记录执行提交。`);
}
if (/WorkstationManagement/.test(pathText)) {
steps.push(`工位新增或修改会影响 SAP 资源、派工、报工和看板,保存后要用排产或生产执行页面验证工位是否可选。`);
}
if (/MenuManagement|SystemRrolemaintenance|PersonnelManagement/.test(pathText)) {
steps.push(`权限类修改保存后,需要让相关用户重新登录或刷新权限,再验证菜单、按钮和数据范围是否生效。`);
}
return steps.map((s, i) => `${i + 1}. ${s}`).join('\n');
}
function notes(page) {
const values = [];
if (/PlanManagement/.test(page.relative)) values.push('排产、派工和计划时间会影响生产看板、及时开工统计和 SAP 生产订单日期,提交前必须核对。');
if (/ProductionManagement/.test(page.relative)) values.push('生产执行页面的数据会直接影响报工、工时、质检和库存任务,现场操作必须与实际加工进度一致。');
if (/QualityManagement/.test(page.relative)) values.push('质检结果保存后会影响合格入库、不合格处理和后续工序,提交前必须核对数量。');
if (/ProcessManagement|CraftManagement/.test(page.relative)) values.push('工艺、图纸和结构资料会被排产、生产、质检引用,版本错误会直接影响现场执行。');
if (/DeviceManagement/.test(page.relative)) values.push('设备和采集数据用于看板、OEE 和报工分析,发现异常时应先确认设备编码和采集点位。');
if (/SystemMaintenance/.test(page.relative)) values.push('系统维护类数据会影响登录、权限、菜单和业务可选项,修改后需要验证。');
if (page.sapCalls.length) values.push('该页存在 SAP/WMS 调用,失败时应记录返回提示并确认是否已经生成外部单据。');
if (page.hasCalendar) values.push('日程拖拽属于直接改计划时间的操作,拖拽前应确认任务未被现场执行锁定。');
if (!values.length) values.push('操作后建议重新查询页面,确认列表、状态和明细已经刷新。');
return values.map(v => `- ${v}`).join('\n');
}
function section(page, index) {
const query = queryItems(page);
const columns = tableItems(page);
const buttons = buttonItems(page);
const procs = procItems(page);
const sap = sapItems(page);
return `### 21.${index} ${page.name}
页面路径:\`${page.relative}\`
所属模块:${moduleName(page)}
建议使用人员:${roleFor(page)}
页面用途:${purpose(page)}
**具体操作步骤**
${detailedSteps(page)}
**查询条件/输入项**
${lineList(query, '未识别到固定输入项,按现场页面提示操作。')}
**结果表重点列**
${lineList(columns, '未识别到固定表格列,以页面实际显示为准。')}
**页面按钮/状态/页签**
${lineList([...buttons, ...page.tabLabels].slice(0, 36), '未识别到明确按钮,以页面实际显示为准。')}
**弹窗/表单重点项**
${lineList([...page.dialogs, ...page.formLabels].slice(0, 30), '未识别到固定弹窗或表单项。')}
**后台处理/外部接口**
${lineList([...procs.map(x => `存储过程:${x}`), ...sap.map(x => `接口:${x}`)].slice(0, 30), '未发现页面级后台提交动作。')}
**操作注意事项**
${notes(page)}
`;
}
function moduleOrder(page) {
const order = [
'404.vue',
'AnomalousManagement',
'CraftManagement',
'DeviceManagement',
'PlanManagement',
'ProcessManagement',
'ProductionManagement',
'QualityManagement',
'SaleManagement',
'SystemMaintenance',
'dashboard',
'layout',
'login'
];
const key = page.relative === 'src/views/404.vue' ? '404.vue' : page.module;
const idx = order.indexOf(key);
return idx >= 0 ? idx : 99;
}
function moduleHeading(page) {
const key = page.relative === 'src/views/404.vue' ? '404.vue' : page.module;
const headings = {
'404.vue': '错误页',
AnomalousManagement: '异常管理',
CraftManagement: '产品结构维护',
DeviceManagement: '设备管理',
PlanManagement: '计划排产',
ProcessManagement: '工艺与图纸管理',
ProductionManagement: '生产执行管理',
QualityManagement: '质量管理',
SaleManagement: '销售与交付查询',
SystemMaintenance: '系统维护',
dashboard: '首页与看板',
layout: '布局与导航',
login: '登录'
};
return headings[key] || key;
}
function buildChapter(pages) {
const sorted = pages.sort((a, b) => {
const m = moduleOrder(a) - moduleOrder(b);
if (m !== 0) return m;
return a.relative.localeCompare(b.relative, 'zh-Hans-CN');
});
const lines = [];
lines.push('## 二十一、逐页详细操作说明(按 src/views 页面)');
lines.push('');
lines.push('本章按源码页面逐页编写。每个页面都从实际 `v-model`、表格列、按钮、弹窗、页签、存储过程和 SAP/WMS 调用中提取操作线索,转换成用户可执行的步骤。');
lines.push('');
lines.push('通用阅读方法:');
lines.push('- `查询条件/输入项` 是用户进入页面后可以填写、选择或用于筛选的字段。');
lines.push('- `结果表重点列` 是查询后需要核对的表格列,表格可横向滚动时应继续向右查看。');
lines.push('- `页面按钮/状态/页签` 是页面上能触发查询、查看、编辑、提交、导出、状态切换或页面切换的入口。');
lines.push('- `后台处理/外部接口` 用于说明保存后可能调用的存储过程、SAP 或 WMS 接口,业务人员主要关注成功/失败提示。');
lines.push('- 涉及排产、报工、质检、外协、SAP/WMS 的操作,提交前必须核对订单、物料、数量、人员、工位和日期。');
lines.push('');
let current = '';
let moduleIndex = 0;
sorted.forEach((page, i) => {
const heading = moduleHeading(page);
if (heading !== current) {
moduleIndex += 1;
current = heading;
lines.push(`## 21-${moduleIndex} ${heading}`);
lines.push('');
}
lines.push(section(page, i + 1).trimEnd());
lines.push('');
});
return lines.join('\n').trimEnd() + '\n';
}
function main() {
const pages = walk(viewsRoot)
.filter(file => !/node_modules/.test(file))
.map(extractPage);
const chapter = buildChapter(pages);
const manual = readText(manualPath);
const marker = '## 二十一、逐页详细操作说明(按 src/views 页面)';
const idx = manual.indexOf(marker);
if (idx < 0) throw new Error(`找不到章节标记:${marker}`);
const nextChapter = manual.slice(idx + marker.length).search(/\n## 二十二、/);
const head = manual.slice(0, idx).trimEnd() + '\n\n';
const tail = nextChapter >= 0 ? manual.slice(idx + marker.length + nextChapter) : '';
writeText(manualPath, head + chapter + (tail ? '\n' + tail.trimStart() : ''));
console.log(`rewrote chapter 21 for ${pages.length} pages`);
}
main();