feat: 同步全部系统改动与文档
216
work/system-manual/analyze-full-manual-pages.js
Normal file
@@ -0,0 +1,216 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { modules } = require('./capture-full-manual-pages')
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..')
|
||||
const outputPath = path.join(__dirname, 'full-page-catalog.json')
|
||||
|
||||
const purposes = {
|
||||
'设备信息维护': '维护设备名称、公司编码、生产用名、规格型号、设备类型、使用部门、数量、状态及维修记录,为工艺能力、工位绑定和设备数据查询提供设备主数据。',
|
||||
'设备数据': '按工位查看设备运行数据及当前采集状态,用于现场设备状态监控和生产运行判断。',
|
||||
'设备历史数据': '按设备、工位和时间范围追溯历史采集值,用于故障分析、运行复盘和数据导出。',
|
||||
'实时数据': '查看设备最新实时采集值和更新时间,快速判断设备连接及当前运行状态。',
|
||||
'设计任务': '建立、分派和维护设计任务,记录项目、物料、设计要求、计划时间、可见人员、批示、附件及完成状态。',
|
||||
'设计报工': '由设计人员对被授权的设计任务执行开始、结束和说明登记,形成设计工时与进度记录。',
|
||||
'设计工时校验': '由授权人员复核设计报工的开始时间、结束时间、计算工时和备注,形成已校验状态。',
|
||||
'设计工时报表': '按作业者或任务汇总设计工时,支持条件查询、树形展开和结果导出。',
|
||||
'工序管理': '维护工艺分类、工序名称、简称及工艺要求,为标准工艺和生产工艺编制提供基础工序字典。',
|
||||
'制定标准工艺': '按物料建立可复用的标准工艺路线,维护工序顺序、设备、工时和工艺要求。',
|
||||
'生产工艺': '根据生产订单和物料生成、编辑、复制并启用本次生产工艺,可将工艺任务下发到计划排产。',
|
||||
'工艺查询': '按物料、订单等条件查看已维护的工艺路线及工序明细,供生产、计划和质量人员核对。',
|
||||
'工艺任务': '查询待编制或待下发工艺的生产订单任务,进入对应工艺处理流程。',
|
||||
'图纸维护': '维护物料对应的零件图纸及版本,支持上传、查看、替换、下载和删除记录查询。',
|
||||
'工艺维护': '维护工艺相关图纸及名称,使工序和工艺路线能够关联正确的技术文件。',
|
||||
'查看图纸': '以只读方式按物料、图号等条件查找和预览零件图纸。',
|
||||
'采购图纸查询': '供采购场景按物料或图号查询所需图纸,支持只读预览和下载。',
|
||||
'机加件排产': '对待排产机加工序选择工位、指派数量并维护计划开始、完成和最晚开始时间,批量形成机加计划。',
|
||||
'装配件排产': '对装配任务选择工位、人员和计划时间,形成装配执行任务。',
|
||||
'机加件已排产': '查询已形成计划的机加任务,并按订单、物料、工序、工位和日期核对排产结果。',
|
||||
'机加件未排产': '汇总尚未完成排产的机加工序,定位缺少工位、数量或计划时间的任务。',
|
||||
'优先装配': '维护需要优先处理的装配订单或任务,为装配排产和现场执行提供优先级依据。',
|
||||
'计划用表': '以计划人员使用的综合表格查看订单、物料、工艺、计划时间和任务状态。',
|
||||
'装配未排产': '查询尚未形成装配计划的任务,支持按订单、物料和日期范围筛选。',
|
||||
'生产订单关闭': '查询可关闭或已关闭的生产订单,并执行订单关闭状态维护。',
|
||||
'工位任务汇总': '按工位汇总计划任务、数量、时间和状态,用于检查工位负荷及任务分布。',
|
||||
'装配已排产': '查询已排产装配任务及其工位、人员、数量和计划时间。',
|
||||
'系统类装配任务': '维护和查看系统类产品的装配任务、工位分配及计划执行状态。',
|
||||
'阀类装配任务': '维护和查看阀类产品的装配任务、工位分配及计划执行状态。',
|
||||
'及时齐套跟踪结果': '汇总物料齐套跟踪结果,展示任务是否按计划具备投产或装配条件。',
|
||||
'生产计划跟踪': '综合查看生产订单从计划、工艺、排产到执行的进度与状态。',
|
||||
'加工中心': '供机加现场查询本人或工位任务,并执行开工、暂停、报工、收料、自检、异常和图纸查看等生产操作。',
|
||||
'过程异常管控': '集中查询和处理生产过程中登记的异常,跟踪异常类型、责任、状态和处理结果。',
|
||||
'工时校验': '对生产报工形成的工时记录进行核对和校验,修正异常时长并保留校验状态。',
|
||||
'外协管理': '管理生产外协任务的发料、收料、数量和状态,连接内部生产订单与外协执行。',
|
||||
'人员绑定': '将人员与工位、设备或生产资源建立绑定关系,决定现场任务和报工的人员范围。',
|
||||
'装配中心': '供装配现场执行开始、结束、送检、收验、领料、自检、异常和图纸查看等操作。',
|
||||
'班次管理': '维护班次名称、班次类型及工作时间,为人员、设备和工时计算提供班次基础数据。',
|
||||
'工时报表': '按项目、人员、任务或设备汇总生产工时,并支持日期过滤和 Excel 导出。',
|
||||
'加急件管理': '登记和跟踪加急生产任务,维护加急数量、状态及相关订单信息。',
|
||||
'拣配任务': '查看和处理生产物料拣配任务,跟踪拣配数量和完成状态。',
|
||||
'采购外协管理': '跟踪采购外协任务的订单、发料、到货、收料和完成情况。',
|
||||
'工时查询': '按人员、订单、物料、工序和日期查询生产工时明细。',
|
||||
'机加正在进行中': '实时查看正在机加执行的人员、设备、任务及开始时间。',
|
||||
'装配状态': '实时查看装配任务的当前执行、送检、收验和完成状态。',
|
||||
'外协报表': '按订单、供应商、物料和日期汇总外协发料、收料及完成数据。',
|
||||
'工时修正': '对满足权限和状态条件的生产工时记录进行修正,并保留修改依据。',
|
||||
'物料机器人': '查询 AGV 物料搬运任务的起点、终点、状态和时间,并在授权情况下处理任务状态。',
|
||||
'手续确认报表': '汇总订单或任务相关手续的确认状态,供生产和发货前核查。',
|
||||
'发货通知列表': '查询发货通知及其订单、物料、数量、状态和关联文件,供生产与销售协同。',
|
||||
'结算工时': '按任务和人员结算可计入的生产工时,形成结算口径的工时结果。',
|
||||
'机加及时开工': '对比机加计划开始时间与实际开工状态,识别未按时开工任务。',
|
||||
'外协及时开工': '对比外协计划与实际执行状态,识别未按时开始的外协任务。',
|
||||
'加急任务列表': '集中展示加急任务、当前环节和完成状态,提示相关人员优先处理。',
|
||||
'装配及时送检': '对比装配计划和实际送检时间,识别未按时送检任务。',
|
||||
'终检': '处理产品最终检验任务,登记检验数量、合格数量、不合格数量和检验结果。',
|
||||
'采购入库检': '处理采购物料入库前检验,关联采购到货、物料和质量标准。',
|
||||
'销售退货检': '处理销售退货物料的质量检验并记录判定结果。',
|
||||
'原材料退库检': '处理原材料退库前检验,记录质量状态和处置依据。',
|
||||
'序检收检': '处理工序间送检和收检任务,连接生产报工与后续工序放行。',
|
||||
'质量检验标准': '查询和维护物料、工序对应的检验项目、标准和判定要求。',
|
||||
'其他入库检': '处理非采购等其他来源的入库检验任务。',
|
||||
'质检明细': '按订单、物料、检验类型和日期查询质量检验记录明细。',
|
||||
'不合格品处置': '登记不合格品的评审、返工、让步、报废等处置结论并跟踪状态。',
|
||||
'质检报工': '由检验人员执行开始、结束、送检和收验等质量任务操作并形成质检工时。',
|
||||
'测试装配任务': '处理测试类装配和检验任务,记录执行数量、状态及结果。',
|
||||
'及时检验报表': '统计检验任务是否按计划及时完成,支持责任和日期维度分析。',
|
||||
'产品合格率(工位)': '按工位汇总检验数量、合格数量和合格率,用于质量绩效分析。',
|
||||
'销售订单状态': '综合展示销售订单对应生产订单的计划、齐套、拣配、发料、装配、入库和发货状态。'
|
||||
}
|
||||
|
||||
const moduleRelations = {
|
||||
'设备管理': '上游使用系统工位、部门和设备主数据;设备能力和采集数据向工艺编制、计划排产、生产监控、异常分析提供设备依据。',
|
||||
'研发管理': '上游使用人员、角色、项目、物料和附件;设计任务依次形成设计报工、工时校验和工时报表,并可为工艺、生产和项目跟踪提供设计进度。',
|
||||
'工艺管理': '上游接收 SAP/MES 生产订单、物料、图纸和设备信息;输出标准工艺、生产工艺和工艺任务,供计划排产、生产执行与质量检验使用。',
|
||||
'计划排产': '上游接收生产订单、物料、工艺路线、工位和人员;输出机加及装配任务、计划时间和优先级,供生产中心、异常提醒和齐套跟踪使用。',
|
||||
'生产管理': '上游接收已排产任务、工艺、人员、工位和物料;执行结果形成报工、工时、异常、外协、送检和发货协同数据,向质量、报表和销售状态反馈。',
|
||||
'异常提醒': '读取计划时间、实际开工、外协执行、加急标识和送检时间进行差异判断;结果供计划与生产人员跟进,不单独改变原业务单据。',
|
||||
'质量管理': '上游接收采购到货、生产送检、退货、物料和质量标准;检验及处置结果反馈生产状态、库存入库、不合格品处理和质量统计。',
|
||||
'销售订单管理': '汇总 SAP/MES 销售订单、生产订单、计划、齐套、拣配、发料、装配、入库和发货数据,形成面向订单的全流程状态视图。'
|
||||
}
|
||||
|
||||
function normalizeText(value) {
|
||||
return String(value || '')
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/\{\{[^}]+\}\}/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function unique(values, limit = 60) {
|
||||
return [...new Set(values.map(normalizeText).filter(value => value && value.length <= 40))].slice(0, limit)
|
||||
}
|
||||
|
||||
function extractSearchFields(template) {
|
||||
const firstTable = template.search(/<el-table\b/i)
|
||||
const prefix = firstTable >= 0 ? template.slice(0, firstTable) : template.slice(0, 12000)
|
||||
const labels = []
|
||||
const labelPatterns = [
|
||||
/<el-form-item\b[^>]*\blabel=["']([^"']+)["'][^>]*>/gi,
|
||||
/<span\b[^>]*>([^<]{1,40})<\/span>/gi,
|
||||
/<label\b[^>]*>([^<]{1,40})<\/label>/gi
|
||||
]
|
||||
for (const pattern of labelPatterns) {
|
||||
let match
|
||||
while ((match = pattern.exec(prefix))) {
|
||||
const label = normalizeText(match[1]).replace(/[::]$/, '')
|
||||
if (label && !/^(至|表单|日程|首页)$/.test(label)) labels.push(label)
|
||||
}
|
||||
}
|
||||
const placeholders = []
|
||||
const placeholderPattern = /\bplaceholder=["']([^"']+)["']/gi
|
||||
let match
|
||||
while ((match = placeholderPattern.exec(prefix))) {
|
||||
const value = normalizeText(match[1]).replace(/^(请输入|请选择|输入|选择)/, '')
|
||||
if (value && value.length <= 30) placeholders.push(value)
|
||||
}
|
||||
return unique([...labels, ...placeholders], 30)
|
||||
}
|
||||
|
||||
function extractButtons(template) {
|
||||
const values = []
|
||||
const pattern = /<(?:el-button|button)\b[^>]*>([\s\S]*?)<\/(?:el-button|button)>/gi
|
||||
let match
|
||||
while ((match = pattern.exec(template))) {
|
||||
const value = normalizeText(match[1])
|
||||
if (value && value.length <= 24) values.push(value)
|
||||
}
|
||||
return unique(values, 30)
|
||||
}
|
||||
|
||||
function extractAttributes(template, tag, attribute, limit) {
|
||||
const values = []
|
||||
const pattern = new RegExp(`<${tag}\\b[^>]*\\b${attribute}=["']([^"']+)["'][^>]*>`, 'gi')
|
||||
let match
|
||||
while ((match = pattern.exec(template))) values.push(match[1])
|
||||
return unique(values, limit)
|
||||
}
|
||||
|
||||
function extractProcedures(source, importedSources) {
|
||||
const read = []
|
||||
const write = []
|
||||
const directPattern = /CreateData\s*\(\s*["'](11|12)["']\s*,\s*["'`]([^"'`$]+)["'`]/g
|
||||
let match
|
||||
while ((match = directPattern.exec(source))) {
|
||||
(match[1] === '11' ? read : write).push(match[2])
|
||||
}
|
||||
const apiPattern = /\bname\s*:\s*["'`]([^"'`$]+)["'`]/g
|
||||
for (const apiSource of importedSources) {
|
||||
while ((match = apiPattern.exec(apiSource))) {
|
||||
if (/查询|获取|列表|状态|select/i.test(match[1])) read.push(match[1])
|
||||
else write.push(match[1])
|
||||
}
|
||||
}
|
||||
return { read: unique(read, 20), write: unique(write, 20) }
|
||||
}
|
||||
|
||||
function readImportedApiSources(source) {
|
||||
const sources = []
|
||||
const pattern = /from\s+["']@\/api\/([^"']+)["']/g
|
||||
let match
|
||||
while ((match = pattern.exec(source))) {
|
||||
const apiPath = path.join(root, 'src', 'api', `${match[1]}.js`)
|
||||
if (fs.existsSync(apiPath)) sources.push(fs.readFileSync(apiPath, 'utf8'))
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
const catalog = []
|
||||
let sequence = 0
|
||||
for (let moduleIndex = 0; moduleIndex < modules.length; moduleIndex++) {
|
||||
const module = modules[moduleIndex]
|
||||
for (let pageIndex = 0; pageIndex < module.pages.length; pageIndex++) {
|
||||
sequence++
|
||||
const [title, route, relativeSource] = module.pages[pageIndex]
|
||||
const sourcePath = path.join(root, 'src', 'views', relativeSource)
|
||||
const source = fs.readFileSync(sourcePath, 'utf8')
|
||||
const templateStart = source.indexOf('<template>')
|
||||
const templateEnd = source.lastIndexOf('</template>')
|
||||
const template = templateStart >= 0 && templateEnd > templateStart
|
||||
? source.slice(templateStart + '<template>'.length, templateEnd)
|
||||
: source
|
||||
const procedures = extractProcedures(source, readImportedApiSources(source))
|
||||
catalog.push({
|
||||
sequence,
|
||||
moduleIndex: moduleIndex + 1,
|
||||
pageIndex: pageIndex + 1,
|
||||
module: module.title,
|
||||
title,
|
||||
route,
|
||||
source: relativeSource,
|
||||
screenshot: `${String(moduleIndex + 1).padStart(2, '0')}-${String(pageIndex + 1).padStart(2, '0')}-${module.key}.png`,
|
||||
purpose: purposes[title] || `用于处理${title}相关业务。`,
|
||||
moduleRelation: moduleRelations[module.title],
|
||||
searchFields: extractSearchFields(template),
|
||||
tabs: extractAttributes(template, 'el-tab-pane', 'label', 20),
|
||||
tableColumns: extractAttributes(template, 'el-table-column', 'label', 40),
|
||||
buttons: extractButtons(template),
|
||||
readProcedures: procedures.read,
|
||||
writeProcedures: procedures.write
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(catalog, null, 2), 'utf8')
|
||||
console.log(`Generated ${catalog.length} page records: ${outputPath}`)
|
||||
158
work/system-manual/audit-manual-actions.js
Normal file
@@ -0,0 +1,158 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { modules } = require('./capture-full-manual-pages')
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..')
|
||||
const outputPath = path.resolve(__dirname, 'manual-action-audit.json')
|
||||
|
||||
function unique(values) {
|
||||
return [...new Set(values.map(value => String(value || '').trim()).filter(Boolean))]
|
||||
}
|
||||
|
||||
function cleanText(value) {
|
||||
return String(value || '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\{\{[\s\S]*?\}\}/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function literalAttribute(attributes, name) {
|
||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const match = attributes.match(new RegExp(`(?:^|\\s)${escaped}\\s*=\\s*["']([^"']+)["']`))
|
||||
return match ? match[1].trim() : ''
|
||||
}
|
||||
|
||||
function handlerName(expression) {
|
||||
const match = String(expression || '').match(/(?:^|[;\s])([A-Za-z_$][\w$]*)\s*(?:\(|$)/)
|
||||
return match ? match[1] : ''
|
||||
}
|
||||
|
||||
function matchingTooltip(template, index) {
|
||||
const before = template.slice(0, index)
|
||||
const openingIndex = before.lastIndexOf('<el-tooltip')
|
||||
const closingIndex = before.lastIndexOf('</el-tooltip>')
|
||||
if (openingIndex < 0 || openingIndex < closingIndex) return ''
|
||||
const opening = before.slice(openingIndex, before.indexOf('>', openingIndex) + 1)
|
||||
return literalAttribute(opening, 'content')
|
||||
}
|
||||
|
||||
function extractBlock(script, method) {
|
||||
if (!method) return ''
|
||||
const escaped = method.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const pattern = new RegExp(`(?:async\\s+)?${escaped}\\s*\\([^)]*\\)\\s*\\{`, 'm')
|
||||
const match = pattern.exec(script)
|
||||
if (!match) return ''
|
||||
const start = match.index + match[0].lastIndexOf('{')
|
||||
let depth = 0
|
||||
let quote = ''
|
||||
let lineComment = false
|
||||
let blockComment = false
|
||||
for (let index = start; index < script.length; index++) {
|
||||
const char = script[index]
|
||||
const next = script[index + 1]
|
||||
if (lineComment) {
|
||||
if (char === '\n') lineComment = false
|
||||
continue
|
||||
}
|
||||
if (blockComment) {
|
||||
if (char === '*' && next === '/') {
|
||||
blockComment = false
|
||||
index++
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (quote) {
|
||||
if (char === '\\') {
|
||||
index++
|
||||
} else if (char === quote) {
|
||||
quote = ''
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (char === '/' && next === '/') {
|
||||
lineComment = true
|
||||
index++
|
||||
continue
|
||||
}
|
||||
if (char === '/' && next === '*') {
|
||||
blockComment = true
|
||||
index++
|
||||
continue
|
||||
}
|
||||
if (char === '"' || char === "'" || char === '`') {
|
||||
quote = char
|
||||
continue
|
||||
}
|
||||
if (char === '{') depth++
|
||||
if (char === '}') {
|
||||
depth--
|
||||
if (depth === 0) return script.slice(start + 1, index)
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function methodDetails(script, method) {
|
||||
const body = extractBlock(script, method)
|
||||
const procedures = unique([...body.matchAll(/CreateData\s*\([^,]+,\s*["']([^"']+)["']/g)].map(match => match[1]))
|
||||
const parameters = unique([...body.matchAll(/\[\s*["']([^"']+)["']\s*,/g)].map(match => match[1]))
|
||||
const calls = unique([...body.matchAll(/this\.([A-Za-z_$][\w$]*)\s*\(/g)].map(match => match[1])
|
||||
.filter(name => !['CreateData', 'ExecDatabase'].includes(name)))
|
||||
const closesDialog = /this\.[A-Za-z_$][\w$]*(?:Visible|dialog[A-Za-z_$]*)\s*=\s*false/.test(body)
|
||||
const refreshesList = calls.some(name => /^(?:search|get|query|load|refresh|init)/i.test(name))
|
||||
return { procedures, parameters, calls, closesDialog, refreshesList }
|
||||
}
|
||||
|
||||
function addAction(actions, template, index, attributes, body, fallbackLabel = '') {
|
||||
const expression = literalAttribute(attributes, '@click') ||
|
||||
literalAttribute(attributes, '@click.native') ||
|
||||
literalAttribute(attributes, '@row-click')
|
||||
if (!expression) return
|
||||
const label = cleanText(body) || matchingTooltip(template, index) || fallbackLabel
|
||||
if (!label) return
|
||||
actions.push({ label, expression, method: handlerName(expression) })
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const module of modules) {
|
||||
for (const [title, route, source] of module.pages) {
|
||||
const sourcePath = path.resolve(root, 'src', 'views', source)
|
||||
const content = fs.readFileSync(sourcePath, 'utf8')
|
||||
const scriptStart = content.indexOf('<script')
|
||||
const template = (scriptStart >= 0 ? content.slice(0, scriptStart) : content)
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
const script = scriptStart >= 0 ? content.slice(scriptStart) : ''
|
||||
const actions = []
|
||||
|
||||
for (const match of template.matchAll(/<el-button\b([^>]*?)>([\s\S]*?)<\/el-button>/g)) {
|
||||
addAction(actions, template, match.index, match[1], match[2])
|
||||
}
|
||||
for (const match of template.matchAll(/<el-button\b([^>]*?)\/>/g)) {
|
||||
addAction(actions, template, match.index, match[1], '', '图标操作')
|
||||
}
|
||||
for (const match of template.matchAll(/<el-dropdown-item\b([^>]*)>([\s\S]*?)<\/el-dropdown-item>/g)) {
|
||||
addAction(actions, template, match.index, match[1], match[2])
|
||||
}
|
||||
for (const match of template.matchAll(/<el-table\b([^>]*@row-click[^>]*)>/g)) {
|
||||
addAction(actions, template, match.index, match[1], '', '点击列表行')
|
||||
}
|
||||
|
||||
const deduplicated = []
|
||||
const keys = new Set()
|
||||
for (const action of actions) {
|
||||
const key = `${action.label}|${action.method}|${action.expression}`
|
||||
if (keys.has(key)) continue
|
||||
keys.add(key)
|
||||
deduplicated.push({ ...action, ...methodDetails(script, action.method) })
|
||||
}
|
||||
|
||||
results.push({ module: module.title, title, route, source, actions: deduplicated })
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2), 'utf8')
|
||||
console.log(`Audited ${results.length} pages`)
|
||||
console.log(`Actions: ${results.reduce((total, page) => total + page.actions.length, 0)}`)
|
||||
console.log(`Actions with database effects: ${results.reduce((total, page) => total + page.actions.filter(action => action.procedures.length).length, 0)}`)
|
||||
74
work/system-manual/audit-manual-dialogs.js
Normal file
@@ -0,0 +1,74 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { modules } = require('./capture-full-manual-pages')
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..')
|
||||
const outputPath = path.resolve(__dirname, 'manual-dialog-audit.json')
|
||||
|
||||
function unique(values) {
|
||||
return [...new Set(values.map(value => String(value || '').trim()).filter(Boolean))]
|
||||
}
|
||||
|
||||
function literalAttribute(attributes, name) {
|
||||
const match = attributes.match(new RegExp(`(?:^|\\s)${name}\\s*=\\s*["']([^"']+)["']`))
|
||||
return match ? match[1].trim() : ''
|
||||
}
|
||||
|
||||
function cleanText(value) {
|
||||
return String(value || '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\{\{[\s\S]*?\}\}/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
const results = []
|
||||
for (const module of modules) {
|
||||
for (const [title, route, source] of module.pages) {
|
||||
const sourcePath = path.resolve(root, 'src', 'views', source)
|
||||
const content = fs.readFileSync(sourcePath, 'utf8')
|
||||
const scriptStart = content.indexOf('<script')
|
||||
const template = (scriptStart >= 0 ? content.slice(0, scriptStart) : content)
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
|
||||
const dialogs = []
|
||||
const dialogPattern = /<el-dialog\b([^>]*)>([\s\S]*?)<\/el-dialog>/g
|
||||
let dialogMatch
|
||||
while ((dialogMatch = dialogPattern.exec(template))) {
|
||||
const attributes = dialogMatch[1]
|
||||
const body = dialogMatch[2]
|
||||
const dialogTitle = literalAttribute(attributes, 'title') || '业务操作弹窗'
|
||||
const fields = unique([...body.matchAll(/<el-form-item\b([^>]*)>/g)]
|
||||
.map(match => literalAttribute(match[1], 'label')))
|
||||
const columns = unique([...body.matchAll(/<el-table-column\b([^>]*)>/g)]
|
||||
.map(match => literalAttribute(match[1], 'label')))
|
||||
const commands = unique([...body.matchAll(/<el-button\b[^>]*>([\s\S]*?)<\/el-button>/g)]
|
||||
.map(match => cleanText(match[1])))
|
||||
dialogs.push({ title: dialogTitle, fields, columns, commands })
|
||||
}
|
||||
|
||||
const inlineEditableFields = []
|
||||
const columnPattern = /<el-table-column\b([^>]*)>([\s\S]*?)<\/el-table-column>/g
|
||||
let columnMatch
|
||||
while ((columnMatch = columnPattern.exec(template))) {
|
||||
const label = literalAttribute(columnMatch[1], 'label')
|
||||
if (label && /@change\s*=|@blur\s*=|@click\s*=|v-model\s*=/.test(columnMatch[2])) {
|
||||
inlineEditableFields.push(label)
|
||||
}
|
||||
}
|
||||
|
||||
results.push({
|
||||
module: module.title,
|
||||
title,
|
||||
route,
|
||||
source,
|
||||
dialogs,
|
||||
inlineEditableFields: unique(inlineEditableFields)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2), 'utf8')
|
||||
console.log(`Audited ${results.length} pages`)
|
||||
console.log(`Pages with dialogs: ${results.filter(item => item.dialogs.length).length}`)
|
||||
console.log(`Pages with inline editing: ${results.filter(item => item.inlineEditableFields.length).length}`)
|
||||
420
work/system-manual/build-full-manual.ps1
Normal file
@@ -0,0 +1,420 @@
|
||||
param([string]$ScriptRootOverride = '')
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$scriptRootPath = if ($ScriptRootOverride) { $ScriptRootOverride } else { $PSScriptRoot }
|
||||
$root = (Resolve-Path (Join-Path $scriptRootPath '..\..')).Path
|
||||
$catalogPath = Join-Path $scriptRootPath 'full-page-catalog.json'
|
||||
$screenshots = Join-Path $scriptRootPath 'full-screenshots'
|
||||
$docDir = Join-Path $root 'doc'
|
||||
$desktop = [Environment]::GetFolderPath('Desktop')
|
||||
$sourceDocx = Join-Path $docDir '大连元利流体技术有限公司MES系统操作手册_系统管理篇_样稿.docx'
|
||||
$fileName = '大连元利流体技术有限公司MES系统用户操作手册_V1.1_2026-07-23'
|
||||
$docxPath = Join-Path $docDir ($fileName + '.docx')
|
||||
$pdfPath = Join-Path $docDir ($fileName + '.pdf')
|
||||
$desktopDocx = Join-Path $desktop ($fileName + '.docx')
|
||||
$desktopPdf = Join-Path $desktop ($fileName + '.pdf')
|
||||
$progressPath = Join-Path ([IO.Path]::GetTempPath()) 'mes-full-manual-build.log'
|
||||
[IO.File]::WriteAllText($progressPath, "开始生成 $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')`r`n", [Text.Encoding]::UTF8)
|
||||
|
||||
function Write-BuildProgress {
|
||||
param([string]$Message)
|
||||
[IO.File]::AppendAllText($progressPath, "$(Get-Date -Format 'HH:mm:ss') $Message`r`n", [Text.Encoding]::UTF8)
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $sourceDocx)) { throw "系统管理篇源文档不存在:$sourceDocx" }
|
||||
if (-not (Test-Path -LiteralPath $catalogPath)) { throw "页面目录不存在:$catalogPath" }
|
||||
|
||||
$catalog = Get-Content -Raw -Encoding UTF8 $catalogPath | ConvertFrom-Json
|
||||
if ($catalog.Length -ne 69) { throw "页面目录数量应为69,实际为$($catalog.Length)" }
|
||||
|
||||
foreach ($page in $catalog) {
|
||||
$imagePath = Join-Path $screenshots $page.screenshot
|
||||
if (-not (Test-Path -LiteralPath $imagePath)) { throw "页面截图不存在:$imagePath" }
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $docDir | Out-Null
|
||||
|
||||
$moduleChapters = [ordered]@{
|
||||
'设备管理' = 7
|
||||
'研发管理' = 8
|
||||
'工艺管理' = 9
|
||||
'计划排产' = 10
|
||||
'生产管理' = 11
|
||||
'异常提醒' = 12
|
||||
'质量管理' = 13
|
||||
'销售订单管理' = 14
|
||||
}
|
||||
|
||||
function Join-Values {
|
||||
param([object]$Values, [int]$Limit = 12)
|
||||
if ($null -eq $Values -or $Values.Length -eq 0) { return '无' }
|
||||
$items = @($Values | Select-Object -First $Limit)
|
||||
$text = $items -join '、'
|
||||
if ($Values.Length -gt $Limit) { $text += "等$($Values.Length)项" }
|
||||
return $text
|
||||
}
|
||||
|
||||
function Get-FilterType {
|
||||
param([string]$Field)
|
||||
if ($Field -match '日期|时间|开始|结束|完工|入账') { return '日期/时间或范围选择' }
|
||||
if ($Field -match '状态|类型|类别|属性|性质|结果|部门|工位|工序|人员|指派|来源|班次|齐套|送检|入库|发货') { return '下拉、开关或选项选择' }
|
||||
if ($Field -match '编码|编号|订单|任务ID|计划号|合同号|图号|名称|操作人|物料|产品') { return '文本输入或远程选择' }
|
||||
return '输入或选择控件'
|
||||
}
|
||||
|
||||
function Get-FilterGuide {
|
||||
param([string]$Field)
|
||||
if ($Field -match '日期|时间|开始|结束|完工|入账') { return '选择起止时间时按区间过滤;清空表示不限制该时间条件。' }
|
||||
if ($Field -match '状态|类型|类别|属性|性质|结果|部门|工位|工序|人员|指派|来源|班次|齐套|送检|入库|发货') { return '从页面提供的选项中选择;“全部”或清空通常表示不限制该条件。' }
|
||||
if ($Field -match '编码|编号|订单|任务ID|计划号|合同号|图号|名称|操作人|物料|产品') { return '输入关键值或从远程候选项中选择,再与其他已填写条件共同查询。' }
|
||||
return '按页面控件输入或选择,点击【查询】后与其他条件共同生效。'
|
||||
}
|
||||
|
||||
function Get-ActionGuide {
|
||||
param([string]$Button)
|
||||
if ($Button -match '查询|搜索|刷新') { return '读取当前筛选条件并刷新列表,不写入业务数据。' }
|
||||
if ($Button -match '导出|Excel|下载') { return '将当前查询或当前页签结果导出/下载;导出前先确认筛选范围。' }
|
||||
if ($Button -match '查看|预览|明细|记录|图纸|附件') { return '打开当前记录的只读详情、历史、图纸或附件;关闭窗口返回列表。' }
|
||||
if ($Button -match '新增|新建|添加|设备$') { return '打开新增窗口,填写必填项并确认后写入;正式操作前检查编码和关联主数据。' }
|
||||
if ($Button -match '编辑|修改|修正') { return '选择目标记录后打开编辑窗口,核对影响范围并保存修改。' }
|
||||
if ($Button -match '删除|移除|作废|关闭订单') { return '执行不可逆或影响状态的操作;必须核对下游占用并在确认框中再次确认。' }
|
||||
if ($Button -match '取消|关闭$|返回') { return '关闭当前窗口或放弃本次未提交内容,通常不写入数据。' }
|
||||
if ($Button -match '保存|确定|提交|确认') { return '提交当前表单或选择结果;提交前复核必填项、数量、时间和目标记录。' }
|
||||
if ($Button -match '批量|排产|下发|派工|指派') { return '先选择目标行并完整填写工位、人员、数量和时间,再批量生成或下发任务。' }
|
||||
if ($Button -match '开始|开工|暂停|结束|完成|报工|送检|收验|收检|发料|收料|领料|退库') { return '改变任务或物料流转状态并形成操作记录;只对已核对的当前任务执行。' }
|
||||
if ($Button -match '上传') { return '选择文件并上传到当前业务记录;上传前确认文件版本、格式和归属。' }
|
||||
return '按当前选中记录执行对应业务动作;根据弹窗提示核对后再确认。'
|
||||
}
|
||||
|
||||
function Test-WritePage {
|
||||
param([object]$Page)
|
||||
if ($Page.writeProcedures.Length -gt 0) { return $true }
|
||||
return [bool](@($Page.buttons | Where-Object { $_ -match '新增|新建|编辑|修改|删除|保存|确定|提交|开始|结束|完成|报工|送检|收料|发料|排产|下发|上传' }).Length)
|
||||
}
|
||||
|
||||
$word = $null
|
||||
$doc = $null
|
||||
$buildError = $null
|
||||
|
||||
try {
|
||||
$word = New-Object -ComObject Word.Application
|
||||
$word.Visible = $false
|
||||
$word.DisplayAlerts = 0
|
||||
$doc = $word.Documents.Open($sourceDocx, $false, $false)
|
||||
$doc.SaveAs2($docxPath, 16)
|
||||
Write-BuildProgress '已打开系统管理篇并另存完整篇'
|
||||
$selection = $word.Selection
|
||||
$normal = $doc.Styles.Item(-1)
|
||||
|
||||
function Set-ParagraphText {
|
||||
param([object]$Paragraph, [string]$Text)
|
||||
$range = $Paragraph.Range.Duplicate
|
||||
if ($range.End -gt $range.Start) { $range.End = $range.End - 1 }
|
||||
$range.Text = $Text
|
||||
}
|
||||
|
||||
$headingMap = @{
|
||||
'7 典型业务链路' = '15 典型业务链路'
|
||||
'7.1 新增可登录人员' = '15.1 新增可登录人员'
|
||||
'7.2 下线一个角色' = '15.2 下线一个角色'
|
||||
'7.3 新增生产工位' = '15.3 新增生产工位'
|
||||
'7.4 追溯一次报工操作' = '15.4 追溯一次报工操作'
|
||||
'8 常见问题' = '16 常见问题'
|
||||
'9 正式环境操作红线' = '17 正式环境操作红线'
|
||||
}
|
||||
|
||||
$paragraphCount = $doc.Paragraphs.Count
|
||||
for ($paragraphIndex = 1; $paragraphIndex -le $paragraphCount; $paragraphIndex++) {
|
||||
$paragraph = $doc.Paragraphs.Item($paragraphIndex)
|
||||
$text = ($paragraph.Range.Text -replace '[\r\a]+$', '').Trim()
|
||||
if ($text -eq '系统管理篇(样稿)') {
|
||||
Set-ParagraphText $paragraph '完整操作手册(系统管理至销售订单)'
|
||||
} elseif ($text -eq '版本:V1.0') {
|
||||
Set-ParagraphText $paragraph '版本:V1.1'
|
||||
} elseif ($text -eq '编制日期:2026年7月23日') {
|
||||
Set-ParagraphText $paragraph '编制日期:2026年7月23日'
|
||||
} elseif ($text.StartsWith('当前正式菜单以数据库配置为准,包括人员管理')) {
|
||||
Set-ParagraphText $paragraph '当前正式菜单以数据库配置为准。本版在系统管理基础上继续覆盖设备管理、研发管理、工艺管理、计划排产、生产管理、异常提醒、质量管理和销售订单管理,共计74个正式功能页面。'
|
||||
} elseif ($headingMap.ContainsKey($text)) {
|
||||
Set-ParagraphText $paragraph $headingMap[$text]
|
||||
}
|
||||
}
|
||||
Write-BuildProgress '已更新封面、阅读说明和后续章节编号'
|
||||
|
||||
$revisionTable = $null
|
||||
for ($tableIndex = 1; $tableIndex -le $doc.Tables.Count; $tableIndex++) {
|
||||
$candidate = $doc.Tables.Item($tableIndex)
|
||||
if ($candidate.Columns.Count -ge 4) {
|
||||
$firstCell = ($candidate.Cell(1, 1).Range.Text -replace '[\r\a]+$', '').Trim()
|
||||
if ($firstCell -eq '版本') {
|
||||
$revisionTable = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($null -ne $revisionTable) {
|
||||
$newRow = $revisionTable.Rows.Add()
|
||||
$newRow.Cells.Item(1).Range.Text = 'V1.1'
|
||||
$newRow.Cells.Item(2).Range.Text = '2026-07-23'
|
||||
$newRow.Cells.Item(3).Range.Text = '增加设备至销售订单八个模块'
|
||||
$newRow.Cells.Item(4).Range.Text = '新增69个正式功能页、69张页面截图及完整数据关系说明'
|
||||
}
|
||||
Write-BuildProgress '已更新修订记录'
|
||||
|
||||
$insertAt = $null
|
||||
$paragraphCount = $doc.Paragraphs.Count
|
||||
for ($paragraphIndex = 1; $paragraphIndex -le $paragraphCount; $paragraphIndex++) {
|
||||
$paragraph = $doc.Paragraphs.Item($paragraphIndex)
|
||||
$text = ($paragraph.Range.Text -replace '[\r\a]+$', '').Trim()
|
||||
if ($text -eq '15 典型业务链路') {
|
||||
$insertAt = $paragraph.Range.Start
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($null -eq $insertAt) { throw '未找到“15 典型业务链路”插入位置' }
|
||||
$selection.SetRange($insertAt, $insertAt)
|
||||
Write-BuildProgress '已定位模块章节插入位置'
|
||||
|
||||
function Move-End {
|
||||
$selection.SetRange($selection.Range.End, $selection.Range.End)
|
||||
}
|
||||
|
||||
function Add-Text {
|
||||
param(
|
||||
[string]$Text,
|
||||
[double]$Size = 10.5,
|
||||
[bool]$Bold = $false,
|
||||
[int]$Align = 0,
|
||||
[int]$Color = 0,
|
||||
[double]$Before = 0,
|
||||
[double]$After = 6,
|
||||
[double]$FirstIndentCm = 0
|
||||
)
|
||||
$selection.Font.NameFarEast = '宋体'
|
||||
$selection.Font.Name = 'Arial'
|
||||
$selection.Font.Size = $Size
|
||||
$selection.Font.Bold = [int]$Bold
|
||||
$selection.Font.Color = $Color
|
||||
$selection.ParagraphFormat.Alignment = $Align
|
||||
$selection.ParagraphFormat.SpaceBefore = $Before
|
||||
$selection.ParagraphFormat.SpaceAfter = $After
|
||||
$selection.ParagraphFormat.FirstLineIndent = $word.CentimetersToPoints($FirstIndentCm)
|
||||
$selection.TypeText($Text)
|
||||
$selection.TypeParagraph()
|
||||
}
|
||||
|
||||
function Add-Heading {
|
||||
param([string]$Text, [int]$Level)
|
||||
$styleId = switch ($Level) { 1 { -2 } 2 { -3 } default { -4 } }
|
||||
$selection.Style = $doc.Styles.Item($styleId)
|
||||
$selection.Font.NameFarEast = '微软雅黑'
|
||||
$selection.Font.Name = 'Arial'
|
||||
$selection.Font.Color = 0x8F4A31
|
||||
$selection.Font.Bold = 1
|
||||
$selection.ParagraphFormat.KeepWithNext = -1
|
||||
$selection.TypeText($Text)
|
||||
$selection.TypeParagraph()
|
||||
$selection.Style = $normal
|
||||
}
|
||||
|
||||
function Add-Table {
|
||||
param([object[]]$Rows, [double[]]$WidthsCm = @())
|
||||
if ($Rows.Count -eq 0) { return }
|
||||
$range = $selection.Range
|
||||
$table = $doc.Tables.Add($range, $Rows.Count, $Rows[0].Count)
|
||||
$table.Borders.Enable = 1
|
||||
$table.Range.Font.NameFarEast = '宋体'
|
||||
$table.Range.Font.Name = 'Arial'
|
||||
$table.Range.Font.Size = 8.5
|
||||
$table.Range.ParagraphFormat.SpaceAfter = 0
|
||||
for ($r = 0; $r -lt $Rows.Count; $r++) {
|
||||
for ($c = 0; $c -lt $Rows[$r].Count; $c++) {
|
||||
$table.Cell($r + 1, $c + 1).Range.Text = [string]$Rows[$r][$c]
|
||||
$table.Cell($r + 1, $c + 1).VerticalAlignment = 1
|
||||
if ($WidthsCm.Count -gt $c) {
|
||||
$table.Cell($r + 1, $c + 1).Width = $word.CentimetersToPoints($WidthsCm[$c])
|
||||
}
|
||||
}
|
||||
}
|
||||
$table.Rows.Item(1).Range.Bold = 1
|
||||
$table.Rows.Item(1).Shading.BackgroundPatternColor = 0xE6D8D0
|
||||
$table.Rows.Item(1).Range.ParagraphFormat.Alignment = 1
|
||||
$table.AllowAutoFit = -1
|
||||
$selection.SetRange($table.Range.End, $table.Range.End)
|
||||
$selection.TypeParagraph()
|
||||
}
|
||||
|
||||
function Add-Note {
|
||||
param([string]$Title, [string]$Text, [string]$Kind = 'info')
|
||||
$range = $selection.Range
|
||||
$table = $doc.Tables.Add($range, 1, 1)
|
||||
$table.Borders.Enable = 1
|
||||
$cell = $table.Cell(1, 1)
|
||||
$cell.Range.Text = $Title + ':' + $Text
|
||||
$cell.Range.Font.NameFarEast = '微软雅黑'
|
||||
$cell.Range.Font.Name = 'Arial'
|
||||
$cell.Range.Font.Size = 9.5
|
||||
$cell.Range.ParagraphFormat.SpaceAfter = 0
|
||||
if ($Kind -eq 'warn') { $cell.Shading.BackgroundPatternColor = 0xD9F2FF }
|
||||
elseif ($Kind -eq 'danger') { $cell.Shading.BackgroundPatternColor = 0xD9E2F3 }
|
||||
else { $cell.Shading.BackgroundPatternColor = 0xF4F1EE }
|
||||
$selection.SetRange($table.Range.End, $table.Range.End)
|
||||
$selection.TypeParagraph()
|
||||
}
|
||||
|
||||
function Add-Figure {
|
||||
param([string]$Path, [string]$Caption, [double]$MaxWidthCm = 16.4)
|
||||
$selection.ParagraphFormat.Alignment = 1
|
||||
$shape = $selection.InlineShapes.AddPicture($Path, $false, $true, $selection.Range)
|
||||
$shape.LockAspectRatio = -1
|
||||
$maxWidth = $word.CentimetersToPoints($MaxWidthCm)
|
||||
if ($shape.Width -gt $maxWidth) { $shape.Width = $maxWidth }
|
||||
$selection.SetRange($shape.Range.End, $shape.Range.End)
|
||||
$selection.TypeParagraph()
|
||||
Add-Text $Caption 9 $false 1 0x777777 0 8 0
|
||||
}
|
||||
|
||||
function Add-PageBreak {
|
||||
$selection.InsertBreak(7)
|
||||
}
|
||||
|
||||
Add-PageBreak
|
||||
foreach ($moduleName in $moduleChapters.Keys) {
|
||||
$chapter = $moduleChapters[$moduleName]
|
||||
$modulePages = @($catalog | Where-Object { $_.module -eq $moduleName } | Sort-Object pageIndex)
|
||||
if ($modulePages.Count -eq 0) { continue }
|
||||
Write-BuildProgress "开始生成第$chapter章 $moduleName"
|
||||
|
||||
Add-Heading ("$chapter $moduleName") 1
|
||||
Add-Text $modulePages[0].moduleRelation 10.5 $false 0 0 0 6 0.74
|
||||
Add-Note '本章范围' "本章按正式菜单顺序说明$($modulePages.Count)个功能页。截图仅执行登录、页面导航和默认查询,没有点击任何业务写入按钮。" 'warn'
|
||||
|
||||
$moduleRows = [System.Collections.Generic.List[object]]::new()
|
||||
$moduleRows.Add(@('序号', '功能页面', '主要用途'))
|
||||
for ($i = 0; $i -lt $modulePages.Count; $i++) {
|
||||
$moduleRows.Add(@(($i + 1).ToString(), $modulePages[$i].title, $modulePages[$i].purpose))
|
||||
}
|
||||
Add-Table $moduleRows.ToArray() @(1.2, 3.4, 10.8)
|
||||
|
||||
for ($pageIndex = 0; $pageIndex -lt $modulePages.Count; $pageIndex++) {
|
||||
$page = $modulePages[$pageIndex]
|
||||
$section = "$chapter.$($pageIndex + 1)"
|
||||
Write-BuildProgress "生成 $section $($page.title)"
|
||||
Add-PageBreak
|
||||
Add-Heading ("$section $($page.title)") 2
|
||||
Add-Text $page.purpose 10.5 $false 0 0 0 6 0.74
|
||||
|
||||
$writePage = Test-WritePage $page
|
||||
Add-Table @(
|
||||
@('项目', '说明'),
|
||||
@('菜单路径', "$moduleName > $($page.title)"),
|
||||
@('页面路由', $page.route),
|
||||
@('数据性质', $(if ($writePage) { '查询为只读;业务按钮可能新增、修改或改变状态' } else { '以只读查询、汇总或导出为主' }))
|
||||
) @(3.2, 12.2)
|
||||
|
||||
Add-Figure (Join-Path $screenshots $page.screenshot) ("图 $section-1 $moduleName - $($page.title)正式页面")
|
||||
|
||||
Add-Heading ("$section.1 搜索与过滤") 3
|
||||
if ($page.searchFields.Length -gt 0) {
|
||||
$filterRows = [System.Collections.Generic.List[object]]::new()
|
||||
$filterRows.Add(@('条件', '控件形式', '使用说明'))
|
||||
foreach ($field in $page.searchFields) {
|
||||
$filterRows.Add(@($field, (Get-FilterType $field), (Get-FilterGuide $field)))
|
||||
}
|
||||
Add-Table $filterRows.ToArray() @(3.2, 4.2, 8.2)
|
||||
Add-Note '组合规则' '多个条件同时填写时,由页面一并传入后台查询,通常取条件交集。若查询不到记录,先逐项清空状态、日期和编号条件,再重新查询;精确或包含匹配以对应页面后台过程为准。' 'info'
|
||||
} else {
|
||||
Add-Text '本页面没有独立的顶部搜索区,进入页面后按默认条件加载数据,或通过页签、树节点、当前工位/人员上下文切换数据范围。' 10.5 $false 0 0 0 6 0.74
|
||||
}
|
||||
if ($page.tabs.Length -gt 0) {
|
||||
Add-Table @(
|
||||
@('页面页签/视图', '切换说明'),
|
||||
@((Join-Values $page.tabs 20), '切换页签会改变展示维度或数据范围;导出前确认当前页签。')
|
||||
) @(5.2, 10.4)
|
||||
}
|
||||
|
||||
Add-Heading ("$section.2 列表字段与页面结果") 3
|
||||
if ($page.tableColumns.Length -gt 0) {
|
||||
$columnRows = [System.Collections.Generic.List[object]]::new()
|
||||
$columnRows.Add(@('分组', '字段/列'))
|
||||
for ($columnIndex = 0; $columnIndex -lt $page.tableColumns.Length; $columnIndex += 8) {
|
||||
$endIndex = [Math]::Min($columnIndex + 7, $page.tableColumns.Length - 1)
|
||||
$group = @($page.tableColumns[$columnIndex..$endIndex])
|
||||
$columnRows.Add(@("第$($columnIndex + 1)-$($endIndex + 1)列", ($group -join '、')))
|
||||
}
|
||||
Add-Table $columnRows.ToArray() @(3.2, 12.4)
|
||||
Add-Text '列表用于展示当前筛选结果。存在横向滚动条时,应左右滚动核对全部状态、数量、时间和操作列;存在分页时,页码和每页条数只影响当前显示范围。' 10.5 $false 0 0 0 6 0.74
|
||||
} else {
|
||||
Add-Text '该页面主要使用自定义看板、卡片或复用组件展示结果,字段布局以截图及页面实时内容为准。' 10.5 $false 0 0 0 6 0.74
|
||||
}
|
||||
|
||||
Add-Heading ("$section.3 功能按钮与操作") 3
|
||||
if ($page.buttons.Length -gt 0) {
|
||||
$buttonRows = [System.Collections.Generic.List[object]]::new()
|
||||
$buttonRows.Add(@('按钮/命令', '操作说明'))
|
||||
foreach ($button in $page.buttons) {
|
||||
$buttonRows.Add(@($button, (Get-ActionGuide $button)))
|
||||
}
|
||||
Add-Table $buttonRows.ToArray() @(4.2, 11.4)
|
||||
} else {
|
||||
Add-Text '本页主要通过大尺寸现场操作按钮、图标按钮、表格行操作或复用组件执行命令。操作前必须先选择当前任务,并根据按钮文字和确认提示核对目标记录。' 10.5 $false 0 0 0 6 0.74
|
||||
}
|
||||
if ($writePage) {
|
||||
Add-Note '正式操作注意' '新增、编辑、删除、开始、结束、排产、报工、送检、收料、上传等操作会写入业务数据或改变状态。本文档编制未实际执行这些按钮;正式使用时应按岗位权限和业务单据逐条确认。' 'danger'
|
||||
}
|
||||
|
||||
Add-Heading ("$section.4 前后数据关系") 3
|
||||
Add-Text $page.moduleRelation 10.5 $false 0 0 0 6 0.74
|
||||
Add-Table @(
|
||||
@('关系层次', '内容'),
|
||||
@('页面输入', $(if ($page.searchFields.Length -gt 0) { Join-Values $page.searchFields 18 } else { '当前登录人员、角色、工位、默认状态或上游任务上下文' })),
|
||||
@('只读查询过程/接口', (Join-Values $page.readProcedures 10)),
|
||||
@('写入过程/接口', (Join-Values $page.writeProcedures 10)),
|
||||
@('页面输出', $(if ($page.tableColumns.Length -gt 0) { Join-Values $page.tableColumns 16 } else { '自定义看板、统计结果、状态卡片或复用组件结果' }))
|
||||
) @(4.2, 11.4)
|
||||
Add-Note '技术核对说明' '过程名称来自当前前端源码和已引用接口文件,用于说明数据流向;动态过程名、通用审批接口及服务端二次调用可能不会全部显示在本表中。' 'info'
|
||||
}
|
||||
Write-BuildProgress "完成第$chapter章 $moduleName"
|
||||
}
|
||||
|
||||
Write-BuildProgress '开始更新目录并导出PDF'
|
||||
if ($doc.TablesOfContents.Count -gt 0) {
|
||||
$doc.TablesOfContents.Item(1).Update()
|
||||
}
|
||||
$doc.Save()
|
||||
Write-BuildProgress 'DOCX正文及目录已保存'
|
||||
$doc.ExportAsFixedFormat($pdfPath, 17)
|
||||
Write-BuildProgress 'PDF已导出'
|
||||
|
||||
# WPS 兼容层在大型文档导出后读取统计信息可能长时间阻塞,统计校验改由生成后的 Open XML/PDF 文件完成。
|
||||
$doc.Close($false)
|
||||
[void][Runtime.InteropServices.Marshal]::ReleaseComObject($doc)
|
||||
$doc = $null
|
||||
$word.Quit()
|
||||
[void][Runtime.InteropServices.Marshal]::ReleaseComObject($word)
|
||||
$word = $null
|
||||
|
||||
Copy-Item -LiteralPath $docxPath -Destination $desktopDocx -Force
|
||||
Copy-Item -LiteralPath $pdfPath -Destination $desktopPdf -Force
|
||||
|
||||
Write-Output "DOCX=$docxPath"
|
||||
Write-Output "PDF=$pdfPath"
|
||||
Write-BuildProgress '完整操作手册生成完成'
|
||||
} catch {
|
||||
$buildError = $_
|
||||
Write-BuildProgress ("生成失败:" + $_.Exception.Message)
|
||||
} finally {
|
||||
if ($doc) {
|
||||
try { $doc.Close($false) } catch { }
|
||||
}
|
||||
if ($word) {
|
||||
try { $word.Quit() } catch { }
|
||||
}
|
||||
if ($doc) { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($doc) }
|
||||
if ($word) { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($word) }
|
||||
[GC]::Collect()
|
||||
[GC]::WaitForPendingFinalizers()
|
||||
}
|
||||
|
||||
if ($buildError) { throw $buildError }
|
||||
382
work/system-manual/build-system-issues.ps1
Normal file
@@ -0,0 +1,382 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
$prepared = Join-Path $PSScriptRoot 'prepared'
|
||||
$docDir = Join-Path $root 'doc'
|
||||
$desktop = [Environment]::GetFolderPath('Desktop')
|
||||
$fileName = '大连元利流体技术有限公司MES系统_系统管理问题清单_2026-07-23'
|
||||
$docxPath = Join-Path $docDir ($fileName + '.docx')
|
||||
$pdfPath = Join-Path $docDir ($fileName + '.pdf')
|
||||
$desktopDocx = Join-Path $desktop ($fileName + '.docx')
|
||||
$desktopPdf = Join-Path $desktop ($fileName + '.pdf')
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $docDir | Out-Null
|
||||
|
||||
$word = $null
|
||||
$doc = $null
|
||||
$buildError = $null
|
||||
|
||||
try {
|
||||
$word = New-Object -ComObject Word.Application
|
||||
$word.Visible = $false
|
||||
$word.DisplayAlerts = 0
|
||||
$doc = $word.Documents.Add()
|
||||
$selection = $word.Selection
|
||||
|
||||
$doc.PageSetup.PageWidth = $word.CentimetersToPoints(21)
|
||||
$doc.PageSetup.PageHeight = $word.CentimetersToPoints(29.7)
|
||||
$doc.PageSetup.TopMargin = $word.CentimetersToPoints(2.2)
|
||||
$doc.PageSetup.BottomMargin = $word.CentimetersToPoints(2.0)
|
||||
$doc.PageSetup.LeftMargin = $word.CentimetersToPoints(2.1)
|
||||
$doc.PageSetup.RightMargin = $word.CentimetersToPoints(2.1)
|
||||
|
||||
$normal = $doc.Styles.Item(-1)
|
||||
$normal.Font.NameFarEast = '宋体'
|
||||
$normal.Font.Name = 'Arial'
|
||||
$normal.Font.Size = 10.5
|
||||
$normal.ParagraphFormat.SpaceAfter = 6
|
||||
|
||||
function Move-End {
|
||||
$end = $doc.Content.End - 1
|
||||
$selection.SetRange($end, $end)
|
||||
}
|
||||
|
||||
function Add-Text {
|
||||
param(
|
||||
[string]$Text,
|
||||
[double]$Size = 10.5,
|
||||
[bool]$Bold = $false,
|
||||
[int]$Align = 0,
|
||||
[int]$Color = 0,
|
||||
[double]$Before = 0,
|
||||
[double]$After = 6,
|
||||
[double]$FirstIndentCm = 0
|
||||
)
|
||||
$selection.Style = $normal
|
||||
$selection.Font.NameFarEast = '宋体'
|
||||
$selection.Font.Name = 'Arial'
|
||||
$selection.Font.Size = $Size
|
||||
$selection.Font.Bold = [int]$Bold
|
||||
$selection.Font.Color = $Color
|
||||
$selection.ParagraphFormat.Alignment = $Align
|
||||
$selection.ParagraphFormat.SpaceBefore = $Before
|
||||
$selection.ParagraphFormat.SpaceAfter = $After
|
||||
$selection.ParagraphFormat.FirstLineIndent = $word.CentimetersToPoints($FirstIndentCm)
|
||||
$selection.TypeText($Text)
|
||||
$selection.TypeParagraph()
|
||||
}
|
||||
|
||||
function Add-Heading {
|
||||
param([string]$Text, [int]$Level)
|
||||
$styleId = switch ($Level) { 1 { -2 } 2 { -3 } default { -4 } }
|
||||
$selection.Style = $doc.Styles.Item($styleId)
|
||||
$selection.Font.NameFarEast = '微软雅黑'
|
||||
$selection.Font.Name = 'Arial'
|
||||
$selection.Font.Color = 0x8F4A31
|
||||
$selection.Font.Bold = 1
|
||||
$selection.ParagraphFormat.KeepWithNext = -1
|
||||
$selection.TypeText($Text)
|
||||
$selection.TypeParagraph()
|
||||
$selection.Style = $normal
|
||||
}
|
||||
|
||||
function Add-StepList {
|
||||
param([string[]]$Items)
|
||||
for ($i = 0; $i -lt $Items.Count; $i++) {
|
||||
Add-Text (('{0}. {1}' -f ($i + 1), $Items[$i])) 10.5 $false 0 0 0 4 0
|
||||
}
|
||||
}
|
||||
|
||||
function Add-ItemList {
|
||||
param([string[]]$Items)
|
||||
for ($i = 0; $i -lt $Items.Count; $i++) {
|
||||
Add-Text ('(' + ($i + 1) + ')' + $Items[$i]) 10.5 $false 0 0 0 4 0
|
||||
}
|
||||
}
|
||||
|
||||
function Add-Table {
|
||||
param([object[]]$Rows, [double[]]$WidthsCm = @())
|
||||
$table = $doc.Tables.Add($selection.Range, $Rows.Count, $Rows[0].Count)
|
||||
$table.Borders.Enable = 1
|
||||
$table.Range.Font.NameFarEast = '宋体'
|
||||
$table.Range.Font.Name = 'Arial'
|
||||
$table.Range.Font.Size = 8.8
|
||||
$table.Range.ParagraphFormat.SpaceAfter = 0
|
||||
for ($r = 0; $r -lt $Rows.Count; $r++) {
|
||||
for ($c = 0; $c -lt $Rows[$r].Count; $c++) {
|
||||
$table.Cell($r + 1, $c + 1).Range.Text = [string]$Rows[$r][$c]
|
||||
$table.Cell($r + 1, $c + 1).VerticalAlignment = 1
|
||||
if ($WidthsCm.Count -gt $c) {
|
||||
$table.Cell($r + 1, $c + 1).Width = $word.CentimetersToPoints($WidthsCm[$c])
|
||||
}
|
||||
}
|
||||
}
|
||||
$table.Rows.Item(1).Range.Bold = 1
|
||||
$table.Rows.Item(1).Shading.BackgroundPatternColor = 0xE6D8D0
|
||||
$table.Rows.Item(1).Range.ParagraphFormat.Alignment = 1
|
||||
$table.AllowAutoFit = -1
|
||||
Move-End
|
||||
$selection.TypeParagraph()
|
||||
}
|
||||
|
||||
function Add-Note {
|
||||
param([string]$Title, [string]$Text, [string]$Kind = 'info')
|
||||
$table = $doc.Tables.Add($selection.Range, 1, 1)
|
||||
$cell = $table.Cell(1, 1)
|
||||
$table.Borders.Enable = 1
|
||||
$cell.Range.Text = $Title + ':' + $Text
|
||||
$cell.Range.Font.NameFarEast = '微软雅黑'
|
||||
$cell.Range.Font.Size = 9.5
|
||||
$cell.Range.ParagraphFormat.SpaceAfter = 0
|
||||
if ($Kind -eq 'danger') {
|
||||
$cell.Shading.BackgroundPatternColor = 0xD9E2F3
|
||||
} elseif ($Kind -eq 'warn') {
|
||||
$cell.Shading.BackgroundPatternColor = 0xD9F2FF
|
||||
} else {
|
||||
$cell.Shading.BackgroundPatternColor = 0xF4F1EE
|
||||
}
|
||||
Move-End
|
||||
$selection.TypeParagraph()
|
||||
}
|
||||
|
||||
function Add-Figure {
|
||||
param([string]$Path, [string]$Caption, [double]$MaxWidthCm = 16.4)
|
||||
$selection.ParagraphFormat.Alignment = 1
|
||||
$shape = $selection.InlineShapes.AddPicture($Path, $false, $true, $selection.Range)
|
||||
$shape.LockAspectRatio = -1
|
||||
$maxWidth = $word.CentimetersToPoints($MaxWidthCm)
|
||||
if ($shape.Width -gt $maxWidth) { $shape.Width = $maxWidth }
|
||||
$selection.SetRange($shape.Range.End, $shape.Range.End)
|
||||
$selection.TypeParagraph()
|
||||
Add-Text $Caption 9 $false 1 0x777777 0 8 0
|
||||
}
|
||||
|
||||
function Add-Issue {
|
||||
param(
|
||||
[string]$Id,
|
||||
[string]$Title,
|
||||
[string]$Priority,
|
||||
[string]$Status,
|
||||
[string]$Description,
|
||||
[string]$Evidence,
|
||||
[string]$Impact,
|
||||
[string]$Recommendation,
|
||||
[string]$Verification
|
||||
)
|
||||
Add-Heading ($Id + ' ' + $Title) 2
|
||||
Add-Table @(
|
||||
@('优先级', '确认状态', '涉及模块'),
|
||||
@($Priority, $Status, $Id.Split('-')[0])
|
||||
) @(4, 5, 8)
|
||||
Add-Text ('问题描述:' + $Description) 10.5 $false 0 0 0 5 0
|
||||
Add-Text ('证据:' + $Evidence) 10 $false 0 0x555555 0 5 0
|
||||
Add-Text ('影响:' + $Impact) 10.5 $false 0 0 0 5 0
|
||||
Add-Text ('建议:' + $Recommendation) 10.5 $false 0 0 0 5 0
|
||||
Add-Text ('验证建议:' + $Verification) 10 $false 0 0x555555 0 9 0
|
||||
}
|
||||
|
||||
function Add-PageBreak { $selection.InsertBreak(7) }
|
||||
|
||||
Add-Text '' 12 $false 1 0 0 18 0
|
||||
Add-Text '大连元利流体技术有限公司' 22 $true 1 0x8F4A31 0 12 0
|
||||
Add-Text 'MES 系统管理问题清单' 30 $true 1 0x8F4A31 0 16 0
|
||||
Add-Text '操作手册编制发现项(独立文档)' 19 $true 1 0x4D4D4D 0 28 0
|
||||
Add-Text '版本:V1.0' 12 $false 1 0 0 8 0
|
||||
Add-Text '编制日期:2026年7月23日' 12 $false 1 0 0 8 0
|
||||
Add-Text '问题来源:前端代码、正式菜单、数据库对象定义及只读页面检查' 11 $false 1 0 0 24 0
|
||||
Add-Note '安全声明' '本清单通过只读查询、源代码检查和页面查看形成。没有在正式环境执行新增、修改、删除、密码重置、权限分配或其他写操作。涉及写操作的问题未在正式环境提交复现。' 'warn'
|
||||
Add-PageBreak
|
||||
|
||||
Add-Text '目 录' 20 $true 1 0x8F4A31 0 12 0
|
||||
$toc = $doc.TablesOfContents.Add($selection.Range, $true, 1, 3, $true, '', $true, $true, '', $true, $true, $true)
|
||||
Move-End
|
||||
Add-PageBreak
|
||||
|
||||
Add-Heading '1 文档说明' 1
|
||||
Add-Text '本文件只记录系统管理篇编制过程中发现的问题和风险,不与用户操作手册正文混排。问题按业务影响和修复紧迫程度分级,最终是否立项、修复方案和上线时间应由项目负责人、业务负责人及开发人员共同确认。' 10.5 $false 0 0 0 6 0.74
|
||||
Add-Table @(
|
||||
@('级别', '定义', '处理建议'),
|
||||
@('P1 高', '可能造成账号安全、跨系统数据不一致、主数据不可恢复或权限风险', '优先评审,修复前设置管理措施'),
|
||||
@('P2 中', '影响常用查询、维护准确性、审计完整性或容易导致误操作', '纳入近期版本,补充测试用例'),
|
||||
@('P3 低', '文档、可维护性或未启用代码问题,当前业务影响较小', '随版本整理或技术债处理')
|
||||
) @(2.5, 9, 5.5)
|
||||
Add-Note '确认状态说明' '“已确认”表示代码、页面或数据库定义可直接证明;“待测试环境复现”表示风险已从实现中识别,但为避免正式环境产生数据,尚未提交写操作验证。' 'info'
|
||||
|
||||
Add-Heading '2 问题汇总' 1
|
||||
Add-Table @(
|
||||
@('编号', '优先级', '问题摘要', '确认状态'),
|
||||
@('SEC-001', 'P1', '登录和人员接口存在明文密码暴露风险,初始密码固定', '已确认'),
|
||||
@('MENU-001', 'P1', '菜单管理前端发送原始SQL文本,接口授权边界风险', '已确认'),
|
||||
@('WS-001', 'P1', '工位MES与SAP B1写入非原子,失败无补偿', '待测试环境复现'),
|
||||
@('WS-002', 'P1', '工位删除无占用校验,且未同步删除B1资源', '已确认'),
|
||||
@('WS-003', 'P2', '工位名称查询框不生效', '已确认'),
|
||||
@('WS-004', 'P2', '工位名称/工位号校验不完整', '已确认'),
|
||||
@('PER-001', 'P2', '人员“永久删除”提示与实际处理语义不一致', '已确认'),
|
||||
@('LOG-001', 'P2', '报工记录精确匹配、最多100条且无分页', '已确认'),
|
||||
@('MENU-002', 'P2', '菜单结构维护和按钮级权限界面未开放', '已确认'),
|
||||
@('DOC-001', 'P3', '参考手册与当前正式菜单范围不一致', '已确认'),
|
||||
@('WS-005', 'P3', '工位分页回调引用不存在的方法', '已确认')
|
||||
) @(2.4, 2.2, 9, 3.4)
|
||||
|
||||
Add-PageBreak
|
||||
Add-Heading '3 P1 高优先级问题' 1
|
||||
|
||||
Add-Issue 'SEC-001' '密码处理与默认密码存在安全风险' 'P1 高' '已确认' `
|
||||
'人员查询响应包含密码字段;前端虽然显示为星号,仍把返回值保存到页面对象。登录页“记住账号”会把用户输入的密码写入浏览器Cookie并保留7天。新增和重置密码统一使用固定初始密码123456。' `
|
||||
'src/views/SystemMaintenance/PersonnelManagement/index.vue:279、383、460;src/views/login/index.vue:206、243-245;数据库重置过程直接写入固定密码。' `
|
||||
'拥有浏览器本机访问权限、调试权限或接口访问权限的人员可能取得密码;统一初始密码会扩大弱口令和批量撞库风险。' `
|
||||
'后端仅保存加盐哈希;人员查询接口不得返回密码;移除密码Cookie,记住登录应使用短期安全令牌;新账号生成随机一次性密码并强制首次登录修改;重置后立即失效其他会话。' `
|
||||
'在隔离测试环境完成登录、记住账号、创建账号、重置密码和首次改密的安全测试,并检查浏览器Cookie、网络响应和数据库字段。'
|
||||
Add-Figure (Join-Path $prepared 'personnel-list.png') '图3-1 人员页面虽然遮蔽密码,但接口对象仍接收密码字段'
|
||||
|
||||
Add-Issue 'MENU-001' '菜单管理由浏览器发送原始SQL文本' 'P1 高' '已确认' `
|
||||
'菜单管理的角色菜单查询、一级菜单初始化和二级菜单初始化由前端把SELECT语句放在请求name字段中发送给通用接口,其中角色编号直接插入SQL字符串。' `
|
||||
'src/api/BasicData/MenuManagement.js:20-59,getModule、initFisrtLevel、initSecondLevel均使用type=3和原始SQL文本。' `
|
||||
'即使正常页面只允许选择数字角色,攻击者仍可绕过页面直接构造请求。若后端缺少严格白名单和参数化限制,可能形成越权查询或SQL注入入口。' `
|
||||
'改为固定后端接口或存储过程,角色编号使用强类型参数;服务器按当前用户权限校验目标角色;关闭通用任意SQL执行能力;增加安全审计和异常请求告警。' `
|
||||
'由安全人员在测试环境检查通用接口是否允许修改SQL、跨角色查询和非SELECT语句,禁止在正式环境进行攻击性验证。'
|
||||
|
||||
Add-Issue 'WS-001' '工位MES与SAP B1写入缺少一致性保障' 'P1 高' '待测试环境复现' `
|
||||
'新增和编辑工位时先等待MES接口成功,然后调用postB1或patchB1,但B1调用没有await、失败处理、重试或回滚。页面在MES成功后即可提示成功并关闭弹窗。' `
|
||||
'src/views/SystemMaintenance/WorkstationManagement/index.vue:193-205、228-249。' `
|
||||
'B1超时、证书、登录态或业务校验失败时,MES已经成功而B1可能失败,形成同一工位号在两个系统中的名称或存在状态不一致。用户重复提交还可能产生重复资源。' `
|
||||
'将跨系统写入放到后端编排;记录同步状态和错误详情;B1成功后再向页面返回整体成功;失败时执行补偿或进入可重试队列;按工位号实现幂等。' `
|
||||
'测试环境模拟B1成功、超时、业务拒绝和重复编码四种情况,核对MES、B1和页面提示是否一致。'
|
||||
|
||||
Add-Issue 'WS-002' '工位删除无占用校验且未同步B1' 'P1 高' '已确认' `
|
||||
'工位删除存储过程中的菜单/人员占用校验代码已注释,当前直接删除MES_资源主数据;前端删除成功后没有调用SAP B1资源删除或停用接口。' `
|
||||
'src/views/SystemMaintenance/WorkstationManagement/index.vue:257-280;数据库过程“系统管理_工位管理_工位信息删除”直接DELETE,依赖检查为注释。' `
|
||||
'可能删除仍被排产、在制任务、人员绑定、设备或工时使用的工位;同时B1保留资源,造成主数据不一致和历史联查异常。' `
|
||||
'恢复并扩展占用校验;有历史记录时优先停用而不是物理删除;同步B1停用状态;增加删除审批、影响预览和可恢复机制。' `
|
||||
'只在测试环境建立完整的排产、人员绑定和工时引用场景,验证阻止删除、停用和跨系统同步。'
|
||||
|
||||
Add-PageBreak
|
||||
Add-Heading '4 P2 中优先级问题' 1
|
||||
|
||||
Add-Issue 'WS-003' '工位名称查询框不生效' 'P2 中' '已确认' `
|
||||
'页面绑定了工位名称输入框,但searchData构造的参数数组为空,没有把RoleFunctioning传入查询接口。输入任意名称点击查询仍刷新全部工位。' `
|
||||
'src/views/SystemMaintenance/WorkstationManagement/index.vue:4-10、122、163-169;只读页面检查结果与代码一致。' `
|
||||
'工位数量增多后无法快速定位,用户可能误以为没有匹配结果或误操作同名/相近工位。' `
|
||||
'前端把工位名称作为查询参数传入参数化存储过程;后端按业务要求采用包含匹配;增加空条件、中文关键字、无结果和特殊字符测试。' `
|
||||
'测试环境准备多个相近名称工位,验证空条件、部分名称和完整名称查询。'
|
||||
Add-Figure (Join-Path $prepared 'workstation-list.png') '图4-1 工位页面显示查询框,但当前请求未使用输入值' 15
|
||||
|
||||
Add-Issue 'WS-004' '工位校验规则不完整' 'P2 中' '已确认' `
|
||||
'新增只显式检查同名工位,没有在存储过程中预检查工位号重复。编辑的同名检查统计全部记录,包含当前工位本身,因此不修改名称直接保存时也会被判断为同名。' `
|
||||
'数据库过程“系统管理_工位管理_工位信息增加”和“系统管理_工位管理_工位信息修改”;编辑过程的同名条件未排除当前工位号。' `
|
||||
'重复工位号可能依赖数据库异常兜底并产生不友好提示;无变化保存可能失败,增加用户困惑和重复操作。' `
|
||||
'新增时同时校验工位号和名称唯一;编辑时使用“Name=@名称 AND VisCode<>@当前工位号”;后端返回明确错误码和字段级提示。' `
|
||||
'测试空值、重复名称、重复编号、原名称不变、名称仅大小写/空格变化等边界。'
|
||||
|
||||
Add-Issue 'PER-001' '人员删除提示与实际处理语义不一致' 'P2 中' '已确认' `
|
||||
'页面确认文字为“当前人员将永久删除”,成功提示为“人员离职成功”;数据库过程没有物理删除人员主记录,而是写Quit=0并删除人员角色关系。列表通过角色关系联查,因此人员从列表消失。' `
|
||||
'src/views/SystemMaintenance/PersonnelManagement/index.vue:345-366;数据库过程“人员管理_人员信息修改_删除”。' `
|
||||
'管理员难以判断人员是离职、停用还是永久删除;人员主记录仍在但无角色关系,后续重新启用和历史查询缺少明确流程。' `
|
||||
'统一业务语义为“停用/离职”;明确Quit字段状态定义;保留角色历史或增加有效期;页面展示在职/离职状态和恢复入口;禁止使用“永久删除”误导。' `
|
||||
'测试环境验证离职后登录、列表、历史工时、报工记录和重新启用行为。'
|
||||
|
||||
Add-Issue 'LOG-001' '报工操作记录查询能力不足' 'P2 中' '已确认' `
|
||||
'订单编号和操作人采用精确相等;查询最多返回按记录编号倒序的前100条;页面分页控件整体注释,日期和操作类别等已存在的数据字段没有查询入口。' `
|
||||
'src/views/SystemMaintenance/OperationLog/index.vue:63-75、87-93、108-125;数据库过程“系统管理_报工操作记录_查询”使用TOP 100和等值条件。' `
|
||||
'较早或同一订单超过100条的记录无法完整查看;姓名/订单输入不完整时无结果;审计人员无法按时间范围和操作类别定位。' `
|
||||
'增加服务端分页、总数、开始/结束日期和操作类别;订单支持精确优先并可选包含匹配;提供导出并限制最大范围;建立合适索引。' `
|
||||
'使用脱敏测试数据验证跨页、超过100条、时间边界、空条件和导出结果完整性。'
|
||||
Add-Figure (Join-Path $prepared 'operation-log.png') '图4-2 报工操作记录当前仅提供订单编号和操作人条件'
|
||||
|
||||
Add-Issue 'MENU-002' '菜单结构维护和按钮级权限界面未开放' 'P2 中' '已确认' `
|
||||
'页面存在“新增一级模块”弹窗和相关方法,但模板没有可见入口;查询、新增、编辑、删除按钮级权限复选框被注释。当前实际功能只是把既有一级、二级菜单分配给角色。' `
|
||||
'src/views/SystemMaintenance/MenuManagement/index.vue:82-89、152-170、367-393。' `
|
||||
'旧手册描述新增、编辑、删除菜单和权限分配,容易让管理员寻找不存在的功能;页面级授权过粗,不能限制同页写操作。' `
|
||||
'明确产品边界:如不开放结构维护,删除不可达代码并修订文档;如需按钮权限,完成前后端授权校验,不能只依赖前端隐藏按钮。' `
|
||||
'测试不同角色的查询、新增、编辑、删除接口权限,确认直接请求也会被服务器拒绝。'
|
||||
Add-Figure (Join-Path $prepared 'menu-permission.png') '图4-3 当前菜单页实际为角色菜单树分配'
|
||||
|
||||
Add-PageBreak
|
||||
Add-Heading '5 P3 低优先级问题' 1
|
||||
|
||||
Add-Issue 'DOC-001' '参考手册与当前正式菜单不一致' 'P3 低' '已确认' `
|
||||
'参考手册系统管理包含“节假日设定”,但正式数据库菜单没有该页面;正式菜单新增“报工操作记录”,参考手册没有对应章节。项目中还存在OrgManagement页面,但未配置为正式菜单。' `
|
||||
'参考文档系统管理章节;正式表“登录基础数据_二级菜单”;src/views/SystemMaintenance目录。' `
|
||||
'继续沿用旧手册会遗漏正式功能并培训不存在的入口,造成操作人员和实施人员理解不一致。' `
|
||||
'以正式菜单版本为基线建立文档版本号和发布日期;每次菜单上线同步更新手册;未上线页面标注“开发中/停用”而不是混入正式手册。' `
|
||||
'发布前由业务、实施和开发三方按角色逐项核对左侧菜单与手册目录。'
|
||||
|
||||
Add-Issue 'WS-005' '工位分页回调引用不存在的方法' 'P3 低' '已确认' `
|
||||
'工位页面当前没有显示分页控件,但handleSizeChange和handleCurrentChange均调用this.searchTable(),组件实际查询方法名为searchData()。' `
|
||||
'src/views/SystemMaintenance/WorkstationManagement/index.vue:163-175、288-297。' `
|
||||
'当前未触发;如果后续恢复分页控件,切换页码将产生运行时错误,查询无法执行。' `
|
||||
'把回调统一改为searchData,补充组件测试;删除未使用的分页状态或完整实现服务端分页。' `
|
||||
'在测试环境启用分页后验证切换每页条数、页码和末页行为。'
|
||||
|
||||
Add-Heading '6 建议处理顺序' 1
|
||||
Add-Table @(
|
||||
@('阶段', '建议事项', '目标'),
|
||||
@('立即管理措施', '禁用共享账号;不勾选记住密码;限制系统管理权限;禁止直接删除工位;工位变更人工核对MES/B1', '在代码修复前降低安全和数据风险'),
|
||||
@('第一批修复', 'SEC-001、MENU-001、WS-001、WS-002', '处理账号安全、接口授权和跨系统一致性'),
|
||||
@('第二批修复', 'WS-003、WS-004、PER-001、LOG-001、MENU-002', '改善维护准确性、审计完整性和权限粒度'),
|
||||
@('文档与技术债', 'DOC-001、WS-005', '统一正式菜单与文档,清理潜在故障代码')
|
||||
) @(3, 10, 4)
|
||||
|
||||
Add-Heading '7 验收要求' 1
|
||||
Add-ItemList @(
|
||||
'所有写操作问题必须在测试环境复现和回归,不得以正式业务数据作为测试数据。',
|
||||
'涉及账号安全的修改需完成接口响应、Cookie、日志、数据库存储和会话失效检查。',
|
||||
'涉及MES与SAP B1的修改需覆盖单边成功、单边失败、超时、重复请求和补偿重试。',
|
||||
'删除/停用类功能需验证下游占用、历史查询、恢复和审计记录。',
|
||||
'菜单权限必须同时验证页面可见性和后端接口授权,不能只检查按钮是否隐藏。',
|
||||
'修复上线后同步更新操作手册、问题状态、版本号和发布日期。'
|
||||
)
|
||||
Add-Note '结论' '本清单共记录11项:P1高优先级4项、P2中优先级5项、P3低优先级2项。问题清单与操作手册分开交付,便于技术整改和业务培训分别管理。' 'danger'
|
||||
|
||||
foreach ($section in $doc.Sections) {
|
||||
$header = $section.Headers.Item(1).Range
|
||||
$header.Text = '大连元利流体技术有限公司 MES系统 | 系统管理问题清单'
|
||||
$header.Font.NameFarEast = '微软雅黑'
|
||||
$header.Font.Size = 8.5
|
||||
$header.Font.Color = 0x777777
|
||||
$header.ParagraphFormat.Alignment = 2
|
||||
|
||||
$footer = $section.Footers.Item(1).Range
|
||||
$footer.Text = '内部整改使用 '
|
||||
$footer.Font.NameFarEast = '宋体'
|
||||
$footer.Font.Size = 8.5
|
||||
$footer.Font.Color = 0x777777
|
||||
$footer.ParagraphFormat.Alignment = 1
|
||||
$footer.Collapse(0)
|
||||
$footer.Fields.Add($footer, 33) | Out-Null
|
||||
}
|
||||
|
||||
foreach ($item in $doc.TablesOfContents) { $item.Update() }
|
||||
$doc.Fields.Update() | Out-Null
|
||||
$doc.Repaginate()
|
||||
|
||||
if (Test-Path $docxPath) { Remove-Item -LiteralPath $docxPath -Force }
|
||||
if (Test-Path $pdfPath) { Remove-Item -LiteralPath $pdfPath -Force }
|
||||
$doc.SaveAs($docxPath, 12)
|
||||
$doc.ExportAsFixedFormat($pdfPath, 17)
|
||||
$pageCount = $doc.ComputeStatistics(2)
|
||||
$tableCount = $doc.Tables.Count
|
||||
$imageCount = $doc.InlineShapes.Count
|
||||
$doc.Close(0)
|
||||
$doc = $null
|
||||
|
||||
Copy-Item -LiteralPath $docxPath -Destination $desktopDocx -Force
|
||||
Copy-Item -LiteralPath $pdfPath -Destination $desktopPdf -Force
|
||||
|
||||
Write-Output ('DOCX=' + $docxPath)
|
||||
Write-Output ('PDF=' + $pdfPath)
|
||||
Write-Output ('PAGES=' + $pageCount)
|
||||
Write-Output ('TABLES=' + $tableCount)
|
||||
Write-Output ('IMAGES=' + $imageCount)
|
||||
} catch {
|
||||
$buildError = $_
|
||||
Write-Output ('BUILD_ERROR=' + $_.Exception.Message)
|
||||
Write-Output ('BUILD_ERROR_LINE=' + $_.InvocationInfo.ScriptLineNumber)
|
||||
} finally {
|
||||
if ($doc -ne $null) { try { $doc.Close(0) } catch {} }
|
||||
if ($word -ne $null) { try { $word.Quit() } catch {} }
|
||||
[GC]::Collect()
|
||||
[GC]::WaitForPendingFinalizers()
|
||||
}
|
||||
|
||||
if ($buildError -ne $null) { throw $buildError }
|
||||
652
work/system-manual/build-system-manual.ps1
Normal file
@@ -0,0 +1,652 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
$shots = Join-Path $PSScriptRoot 'screenshots'
|
||||
$prepared = Join-Path $PSScriptRoot 'prepared'
|
||||
$docDir = Join-Path $root 'doc'
|
||||
$desktop = [Environment]::GetFolderPath('Desktop')
|
||||
$fileName = '大连元利流体技术有限公司MES系统操作手册_系统管理篇_样稿'
|
||||
$docxPath = Join-Path $docDir ($fileName + '.docx')
|
||||
$pdfPath = Join-Path $docDir ($fileName + '.pdf')
|
||||
$desktopDocx = Join-Path $desktop ($fileName + '.docx')
|
||||
$desktopPdf = Join-Path $desktop ($fileName + '.pdf')
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $prepared, $docDir | Out-Null
|
||||
|
||||
function New-Crop {
|
||||
param(
|
||||
[string]$Source,
|
||||
[string]$Target,
|
||||
[int]$X,
|
||||
[int]$Y,
|
||||
[int]$Width,
|
||||
[int]$Height
|
||||
)
|
||||
$sourceImage = [System.Drawing.Bitmap]::FromFile($Source)
|
||||
try {
|
||||
$cropWidth = [Math]::Min($Width, $sourceImage.Width - $X)
|
||||
$cropHeight = [Math]::Min($Height, $sourceImage.Height - $Y)
|
||||
$targetImage = New-Object System.Drawing.Bitmap($cropWidth, $cropHeight)
|
||||
try {
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($targetImage)
|
||||
try {
|
||||
$graphics.DrawImage(
|
||||
$sourceImage,
|
||||
(New-Object System.Drawing.Rectangle(0, 0, $cropWidth, $cropHeight)),
|
||||
(New-Object System.Drawing.Rectangle($X, $Y, $cropWidth, $cropHeight)),
|
||||
[System.Drawing.GraphicsUnit]::Pixel
|
||||
)
|
||||
} finally {
|
||||
$graphics.Dispose()
|
||||
}
|
||||
$targetImage.Save($Target, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
} finally {
|
||||
$targetImage.Dispose()
|
||||
}
|
||||
} finally {
|
||||
$sourceImage.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Copy-Item (Join-Path $shots '01-personnel-list.png') (Join-Path $prepared 'personnel-list.png') -Force
|
||||
Copy-Item (Join-Path $shots '03-menu-list.png') (Join-Path $prepared 'menu-list.png') -Force
|
||||
Copy-Item (Join-Path $shots '03-menu-permission-panel.png') (Join-Path $prepared 'menu-permission.png') -Force
|
||||
Copy-Item (Join-Path $shots '05-operation-log.png') (Join-Path $prepared 'operation-log.png') -Force
|
||||
New-Crop (Join-Path $shots '01-personnel-add-dialog.png') (Join-Path $prepared 'personnel-add.png') 560 105 800 470
|
||||
New-Crop (Join-Path $shots '02-role-list.png') (Join-Path $prepared 'role-list.png') 0 0 1020 940
|
||||
New-Crop (Join-Path $shots '02-role-add-dialog.png') (Join-Path $prepared 'role-add.png') 560 105 800 390
|
||||
New-Crop (Join-Path $shots '04-workstation-list.png') (Join-Path $prepared 'workstation-list.png') 0 0 1020 940
|
||||
New-Crop (Join-Path $shots '04-workstation-add-dialog.png') (Join-Path $prepared 'workstation-add.png') 560 105 800 450
|
||||
|
||||
$word = $null
|
||||
$doc = $null
|
||||
$buildError = $null
|
||||
|
||||
try {
|
||||
$word = New-Object -ComObject Word.Application
|
||||
$word.Visible = $false
|
||||
$word.DisplayAlerts = 0
|
||||
$doc = $word.Documents.Add()
|
||||
$selection = $word.Selection
|
||||
|
||||
$doc.PageSetup.PageWidth = $word.CentimetersToPoints(21)
|
||||
$doc.PageSetup.PageHeight = $word.CentimetersToPoints(29.7)
|
||||
$doc.PageSetup.TopMargin = $word.CentimetersToPoints(2.2)
|
||||
$doc.PageSetup.BottomMargin = $word.CentimetersToPoints(2.0)
|
||||
$doc.PageSetup.LeftMargin = $word.CentimetersToPoints(2.1)
|
||||
$doc.PageSetup.RightMargin = $word.CentimetersToPoints(2.1)
|
||||
|
||||
$normal = $doc.Styles.Item(-1)
|
||||
$normal.Font.NameFarEast = '宋体'
|
||||
$normal.Font.Name = 'Arial'
|
||||
$normal.Font.Size = 10.5
|
||||
$normal.ParagraphFormat.LineSpacingRule = 1
|
||||
$normal.ParagraphFormat.SpaceAfter = 6
|
||||
|
||||
function Move-End {
|
||||
$end = $doc.Content.End - 1
|
||||
$selection.SetRange($end, $end)
|
||||
}
|
||||
|
||||
function Add-Text {
|
||||
param(
|
||||
[string]$Text,
|
||||
[double]$Size = 10.5,
|
||||
[bool]$Bold = $false,
|
||||
[int]$Align = 0,
|
||||
[int]$Color = 0,
|
||||
[double]$Before = 0,
|
||||
[double]$After = 6,
|
||||
[double]$FirstIndentCm = 0
|
||||
)
|
||||
$selection.Font.NameFarEast = '宋体'
|
||||
$selection.Font.Name = 'Arial'
|
||||
$selection.Font.Size = $Size
|
||||
$selection.Font.Bold = [int]$Bold
|
||||
$selection.Font.Color = $Color
|
||||
$selection.ParagraphFormat.Alignment = $Align
|
||||
$selection.ParagraphFormat.SpaceBefore = $Before
|
||||
$selection.ParagraphFormat.SpaceAfter = $After
|
||||
$selection.ParagraphFormat.FirstLineIndent = $word.CentimetersToPoints($FirstIndentCm)
|
||||
$selection.TypeText($Text)
|
||||
$selection.TypeParagraph()
|
||||
}
|
||||
|
||||
function Add-Heading {
|
||||
param([string]$Text, [int]$Level)
|
||||
$styleId = switch ($Level) { 1 { -2 } 2 { -3 } default { -4 } }
|
||||
$selection.Style = $doc.Styles.Item($styleId)
|
||||
$selection.Font.NameFarEast = '微软雅黑'
|
||||
$selection.Font.Name = 'Arial'
|
||||
$selection.Font.Color = 0x8F4A31
|
||||
$selection.Font.Bold = 1
|
||||
$selection.ParagraphFormat.KeepWithNext = -1
|
||||
$selection.TypeText($Text)
|
||||
$selection.TypeParagraph()
|
||||
$selection.Style = $normal
|
||||
}
|
||||
|
||||
function Add-StepList {
|
||||
param([string[]]$Items)
|
||||
for ($i = 0; $i -lt $Items.Count; $i++) {
|
||||
Add-Text (('{0}. {1}' -f ($i + 1), $Items[$i])) 10.5 $false 0 0 0 4 0
|
||||
}
|
||||
}
|
||||
|
||||
function Add-ItemList {
|
||||
param([string[]]$Items)
|
||||
foreach ($item in $Items) {
|
||||
Add-Text ('(' + ([array]::IndexOf($Items, $item) + 1) + ')' + $item) 10.5 $false 0 0 0 4 0
|
||||
}
|
||||
}
|
||||
|
||||
function Add-Table {
|
||||
param([object[]]$Rows, [double[]]$WidthsCm = @())
|
||||
$range = $selection.Range
|
||||
$table = $doc.Tables.Add($range, $Rows.Count, $Rows[0].Count)
|
||||
$table.Borders.Enable = 1
|
||||
$table.Range.Font.NameFarEast = '宋体'
|
||||
$table.Range.Font.Name = 'Arial'
|
||||
$table.Range.Font.Size = 9
|
||||
$table.Range.ParagraphFormat.SpaceAfter = 0
|
||||
$table.Range.ParagraphFormat.LineSpacingRule = 0
|
||||
for ($r = 0; $r -lt $Rows.Count; $r++) {
|
||||
for ($c = 0; $c -lt $Rows[$r].Count; $c++) {
|
||||
$table.Cell($r + 1, $c + 1).Range.Text = [string]$Rows[$r][$c]
|
||||
$table.Cell($r + 1, $c + 1).VerticalAlignment = 1
|
||||
if ($WidthsCm.Count -gt $c) {
|
||||
$table.Cell($r + 1, $c + 1).Width = $word.CentimetersToPoints($WidthsCm[$c])
|
||||
}
|
||||
}
|
||||
}
|
||||
$table.Rows.Item(1).Range.Bold = 1
|
||||
$table.Rows.Item(1).Shading.BackgroundPatternColor = 0xE6D8D0
|
||||
$table.Rows.Item(1).Range.ParagraphFormat.Alignment = 1
|
||||
$table.AllowAutoFit = -1
|
||||
Move-End
|
||||
$selection.TypeParagraph()
|
||||
}
|
||||
|
||||
function Add-Note {
|
||||
param([string]$Title, [string]$Text, [string]$Kind = 'info')
|
||||
$range = $selection.Range
|
||||
$table = $doc.Tables.Add($range, 1, 1)
|
||||
$table.Borders.Enable = 1
|
||||
$cell = $table.Cell(1, 1)
|
||||
$cell.Range.Text = $Title + ':' + $Text
|
||||
$cell.Range.Font.NameFarEast = '微软雅黑'
|
||||
$cell.Range.Font.Name = 'Arial'
|
||||
$cell.Range.Font.Size = 9.5
|
||||
$cell.Range.Font.Bold = 0
|
||||
$cell.Range.ParagraphFormat.SpaceAfter = 0
|
||||
if ($Kind -eq 'warn') {
|
||||
$cell.Shading.BackgroundPatternColor = 0xD9F2FF
|
||||
} elseif ($Kind -eq 'danger') {
|
||||
$cell.Shading.BackgroundPatternColor = 0xD9E2F3
|
||||
} else {
|
||||
$cell.Shading.BackgroundPatternColor = 0xF4F1EE
|
||||
}
|
||||
Move-End
|
||||
$selection.TypeParagraph()
|
||||
}
|
||||
|
||||
function Add-Figure {
|
||||
param([string]$Path, [string]$Caption, [double]$MaxWidthCm = 16.4)
|
||||
$selection.ParagraphFormat.Alignment = 1
|
||||
$shape = $selection.InlineShapes.AddPicture($Path, $false, $true, $selection.Range)
|
||||
$shape.LockAspectRatio = -1
|
||||
$maxWidth = $word.CentimetersToPoints($MaxWidthCm)
|
||||
if ($shape.Width -gt $maxWidth) { $shape.Width = $maxWidth }
|
||||
$selection.SetRange($shape.Range.End, $shape.Range.End)
|
||||
$selection.TypeParagraph()
|
||||
Add-Text $Caption 9 $false 1 0x777777 0 8 0
|
||||
}
|
||||
|
||||
function Add-PageBreak {
|
||||
$selection.InsertBreak(7)
|
||||
}
|
||||
|
||||
# Cover
|
||||
Add-Text '' 12 $false 1 0 0 20 0
|
||||
Add-Text '大连元利流体技术有限公司' 22 $true 1 0x8F4A31 0 12 0
|
||||
Add-Text 'MES 系统用户操作手册' 30 $true 1 0x8F4A31 0 16 0
|
||||
Add-Text '系统管理篇(样稿)' 24 $true 1 0x4D4D4D 0 30 0
|
||||
Add-Text '版本:V1.0' 12 $false 1 0 0 8 0
|
||||
Add-Text '编制日期:2026年7月23日' 12 $false 1 0 0 8 0
|
||||
Add-Text '适用系统:大连元利 MES 正式环境' 12 $false 1 0 0 28 0
|
||||
Add-Note '编制说明' '本样稿依据现有项目操作手册、当前前端程序、正式菜单配置及数据库只读核对结果编写。截图过程仅登录、查询、打开并取消弹窗,未执行任何新增、修改、删除或保存操作。' 'warn'
|
||||
Add-PageBreak
|
||||
|
||||
Add-Heading '文档修订记录' 1
|
||||
Add-Table @(
|
||||
@('版本', '日期', '修订内容', '说明'),
|
||||
@('V1.0', '2026-07-23', '首次编制系统管理篇样稿', '覆盖当前正式菜单中的五个功能')
|
||||
) @(2, 3, 7, 4)
|
||||
|
||||
Add-Heading '阅读说明' 1
|
||||
Add-ItemList @(
|
||||
'本手册中的【按钮名称】表示页面可点击按钮,“字段名称”表示输入框、下拉框或表格列。',
|
||||
'截图中的业务数据来自正式环境,只用于说明界面;本次编制没有创建测试数据。',
|
||||
'涉及新增、编辑、删除、重置密码、菜单分配的步骤均为操作说明,编制时未实际提交。',
|
||||
'当前正式菜单以数据库配置为准,包括人员管理、菜单管理、角色管理、工位管理、报工操作记录。参考手册中的“节假日设定”不在当前正式菜单,本样稿不纳入。'
|
||||
)
|
||||
Add-PageBreak
|
||||
|
||||
Add-Text '目 录' 20 $true 1 0x8F4A31 0 12 0
|
||||
$tocRange = $selection.Range
|
||||
$toc = $doc.TablesOfContents.Add($tocRange, $true, 1, 3, $true, '', $true, $true, '', $true, $true, $true)
|
||||
Move-End
|
||||
Add-PageBreak
|
||||
|
||||
Add-Heading '1 系统管理概述' 1
|
||||
Add-Text '系统管理用于维护登录账号、角色、角色可见菜单、生产工位主数据,并查询生产报工操作轨迹。该模块中的主数据会被计划排产、生产执行、质量、设备、工时和审计功能共同使用,因此操作人员应具备系统管理员权限,并在修改前确认影响范围。' 10.5 $false 0 0 0 6 0.74
|
||||
|
||||
Add-Heading '1.1 功能范围' 2
|
||||
Add-Table @(
|
||||
@('序号', '功能', '主要用途', '是否会写入数据'),
|
||||
@('1', '人员管理', '维护账号、姓名、角色、班组及密码', '查询不写入;新增/编辑/重置/删除会写入'),
|
||||
@('2', '角色管理', '维护角色名称', '查询不写入;新增/编辑/删除会写入'),
|
||||
@('3', '菜单管理', '按角色分配或移除一级、二级菜单', '过滤不写入;分配/移除会写入'),
|
||||
@('4', '工位管理', '维护生产工位名称和工位号', '查询不写入;新增/编辑/删除会写入'),
|
||||
@('5', '报工操作记录', '按订单或操作人查询生产操作轨迹', '只读查询')
|
||||
) @(1.2, 3, 7.6, 4.2)
|
||||
|
||||
Add-Heading '1.2 数据关系总览' 2
|
||||
Add-Table @(
|
||||
@('上游/主数据', '本模块关系', '下游使用位置', '变更影响'),
|
||||
@('角色主数据', '角色管理创建角色;菜单管理为角色分配页面', '用户登录后的左侧菜单和页面访问范围', '角色改名会同步显示;菜单变更通常需重新登录后验证'),
|
||||
@('人员主数据 + 人员角色关系', '人员管理把账号关联到一个角色,并可维护班组', '登录、排产指派、人员绑定、班次、工时、报工操作人', '更换角色会改变菜单;删除角色关系后账号不再出现在人员列表'),
|
||||
@('MES资源主数据(工位)', '工位管理维护工位号和工位名称', '排产、加工中心、人员绑定、设备/工位查询、工时记录', '新增/改名还会调用 SAP B1 Resources;删除当前仅删除MES记录'),
|
||||
@('生产执行动作', '开始、结束、报工、辅助等动作形成操作记录', '报工操作记录页面用于追溯订单、人员、工位和工时', '本页面不反向修改生产数据')
|
||||
) @(3.5, 4.5, 5.2, 4.4)
|
||||
Add-Note '推荐顺序' '新增账号时,先确认角色,再配置角色菜单,最后新增人员并验证登录;新增工位时,先确认工位号和名称,再维护工位,随后到人员绑定或计划相关页面完成下游配置。' 'info'
|
||||
|
||||
Add-Heading '1.3 通用查询与安全原则' 2
|
||||
Add-ItemList @(
|
||||
'空白查询条件通常表示不限制该字段;多个条件同时填写时按“并且”组合。',
|
||||
'带“×”或清除图标的输入框可先清空,再点击【查询】恢复全量或默认结果。',
|
||||
'分页页面可切换每页10、20、30、40条;切换每页条数后从第1页重新查询。',
|
||||
'红色【删除】和所有确认弹窗均属于写操作。正式环境中应先确认下游占用和回退方案。',
|
||||
'本手册不要求使用示例数据验证写操作;如需验证,应在测试环境完成。'
|
||||
)
|
||||
|
||||
Add-PageBreak
|
||||
Add-Heading '2 人员管理' 1
|
||||
Add-Text '入口:左侧菜单【系统管理】→【人员管理】。该页面用于维护能够登录MES的人员账号,以及账号对应的角色和班组。人员角色关系直接决定登录后的可见菜单;人员姓名、工号和班组还会被排产、生产、人员绑定、班次、工时等功能引用。' 10.5 $false 0 0 0 6 0.74
|
||||
Add-Figure (Join-Path $prepared 'personnel-list.png') '图2-1 人员管理列表(正式环境只读截图)'
|
||||
|
||||
Add-Heading '2.1 页面字段' 2
|
||||
Add-Table @(
|
||||
@('区域', '字段/按钮', '说明'),
|
||||
@('查询区', '姓名', '按姓名关键字模糊匹配,可输入完整姓名或其中一部分'),
|
||||
@('查询区', '角色', '从现有角色中选择,按角色编号精确过滤'),
|
||||
@('列表', '姓名、工号、密码、角色、班组', '密码固定显示为“******”,页面不展示明文'),
|
||||
@('操作', '重置密码、编辑、删除', '分别重置初始密码、维护人员资料、解除人员登录角色关系'),
|
||||
@('分页', '总数、每页条数、页码、跳页', '默认每页10条,可选10/20/30/40条')
|
||||
) @(2.3, 4.2, 10.5)
|
||||
|
||||
Add-Heading '2.2 搜索与过滤' 2
|
||||
Add-StepList @(
|
||||
'在“姓名”中输入关键字;不填写表示不限制姓名。',
|
||||
'在“角色”下拉框中选择一个角色;不选择表示不限制角色。',
|
||||
'点击【查询】。姓名和角色同时填写时,系统只显示同时满足两个条件的人员。',
|
||||
'如无结果,先清空一个条件再查询,并确认当前页码;必要时返回第1页。'
|
||||
)
|
||||
Add-Table @(
|
||||
@('条件示例', '匹配方式', '结果说明'),
|
||||
@('姓名=“张”', '包含匹配', '姓名中包含“张”的人员'),
|
||||
@('角色=“计划员”', '精确匹配角色', '关联到计划员角色的人员'),
|
||||
@('姓名=“张” 且 角色=“计划员”', '两个条件取交集', '同时满足姓名关键字和角色的人员'),
|
||||
@('两个条件均为空', '不限制', '按人员内部编号倒序分页显示')
|
||||
) @(5, 4, 8)
|
||||
|
||||
Add-Heading '2.3 新增人员' 2
|
||||
Add-Figure (Join-Path $prepared 'personnel-add.png') '图2-2 新增人员弹窗'
|
||||
Add-StepList @(
|
||||
'点击【新增】打开“增加人员”弹窗。',
|
||||
'填写“姓名”和“工号”,并从“角色”下拉框选择角色。三项均为必填。',
|
||||
'核对工号未被其他账号使用后,点击【确定】。点击【取消】不会产生数据。',
|
||||
'新增成功后列表回到第1页,并按默认每页10条重新查询。'
|
||||
)
|
||||
Add-Note '初始密码' '新增人员的初始密码由系统固定为 123456,弹窗不显示密码输入框。应通知本人首次登录后按公司密码管理要求处理,避免长期使用初始密码。' 'warn'
|
||||
Add-Note '数据关系' '保存时同时创建人员基本记录和“人员—角色”关联。新增弹窗不能维护班组;如需班组,新增后使用【编辑】补充。工号已存在时新增失败。' 'info'
|
||||
|
||||
Add-Heading '2.4 编辑人员' 2
|
||||
Add-StepList @(
|
||||
'在目标人员行点击【编辑】。',
|
||||
'可修改姓名、班组和角色;工号为只读,不能在编辑弹窗中更换。',
|
||||
'班组可选装配、电气、机加、质检、技术、计划、库房、采购、销售、售后、工艺、其他,也可清空。',
|
||||
'点击【确定】写入人员资料并更新该人员的角色关联;点击【取消】放弃修改。'
|
||||
)
|
||||
Add-Note '影响范围' '修改角色后,该账号下一次登录加载的新菜单以目标角色的菜单配置为准;修改姓名或班组会影响后续页面中的人员显示和筛选,但历史业务记录通常保留原记录内容。' 'warn'
|
||||
|
||||
Add-Heading '2.5 重置密码' 2
|
||||
Add-StepList @(
|
||||
'在目标人员行点击【重置密码】。',
|
||||
'核对人员姓名和工号,在确认框点击【确定】。',
|
||||
'系统将密码重置为 123456,并提示“密码重置成功”。点击【取消】不做任何修改。'
|
||||
)
|
||||
|
||||
Add-Heading '2.6 删除人员' 2
|
||||
Add-StepList @(
|
||||
'先确认该人员不再需要登录,并完成在办任务、工位绑定、班次和责任事项交接。',
|
||||
'点击该行【删除】,在“当前人员将永久删除”确认框中再次核对。',
|
||||
'点击【确定】后,系统移除人员与角色的关联,列表提示“人员离职成功”;点击【取消】不做修改。'
|
||||
)
|
||||
Add-Note '重要说明' '当前实现不是物理删除全部人员历史,而是移除人员角色关联,使其不再出现在本页联查结果中。已有报工、工时、检验等历史业务记录不会随此操作级联删除。' 'danger'
|
||||
|
||||
Add-PageBreak
|
||||
Add-Heading '3 角色管理' 1
|
||||
Add-Text '入口:左侧菜单【系统管理】→【角色管理】。角色是人员和菜单之间的中间层:人员选择角色,菜单管理再为角色分配页面。角色名称用于界面展示,角色编号用于保持人员、菜单关联。' 10.5 $false 0 0 0 6 0.74
|
||||
Add-Figure (Join-Path $prepared 'role-list.png') '图3-1 角色管理列表(右侧空白为当前页面布局)' 15
|
||||
|
||||
Add-Heading '3.1 搜索与分页' 2
|
||||
Add-StepList @(
|
||||
'在“角色功能”输入角色名称关键字。',
|
||||
'点击【查询】;系统按包含关系模糊匹配角色名称。',
|
||||
'清空输入框后再次查询可恢复全部普通角色。',
|
||||
'列表默认每页10条,可选10/20/30/40条并支持跳页。'
|
||||
)
|
||||
Add-Note '系统保留角色' '超级管理员(角色编号1)被查询逻辑排除,不会显示在角色管理列表和人员页面的角色下拉框中,避免通过普通维护误改。' 'info'
|
||||
|
||||
Add-Heading '3.2 新增角色' 2
|
||||
Add-Figure (Join-Path $prepared 'role-add.png') '图3-2 新增角色弹窗'
|
||||
Add-StepList @(
|
||||
'点击【新增】。',
|
||||
'填写唯一的“角色功能”名称,如岗位或职责名称。该字段必填。',
|
||||
'点击【确定】创建角色;存在同名角色时系统提示失败。点击【取消】不产生数据。',
|
||||
'角色创建后还没有菜单权限,应进入【菜单管理】完成分配,再用于人员账号。'
|
||||
)
|
||||
|
||||
Add-Heading '3.3 编辑角色' 2
|
||||
Add-StepList @(
|
||||
'在目标角色行点击【编辑】。',
|
||||
'修改角色名称后点击【确定】。系统校验不能与其他角色重名。',
|
||||
'编辑只修改显示名称,角色编号不变,因此原有人员关联和菜单配置仍保留。'
|
||||
)
|
||||
|
||||
Add-Heading '3.4 删除角色' 2
|
||||
Add-StepList @(
|
||||
'先在人员管理中把该角色下的人员调整到其他角色。',
|
||||
'再到菜单管理逐项移除该角色下的二级菜单,最后移除一级菜单。',
|
||||
'返回角色管理点击【删除】,确认后完成删除。'
|
||||
)
|
||||
Add-Table @(
|
||||
@('系统提示', '原因', '处理方法'),
|
||||
@('当前角色已经分配了菜单', '角色仍有关联菜单记录', '到菜单管理先移除子菜单,再移除一级菜单'),
|
||||
@('当前角色已经分配了人员', '仍有人员使用该角色', '到人员管理把相关人员改为其他角色'),
|
||||
@('删除成功', '菜单和人员关联均已解除', '角色从角色主数据中删除')
|
||||
) @(5.2, 5.2, 6.6)
|
||||
|
||||
Add-PageBreak
|
||||
Add-Heading '4 菜单管理' 1
|
||||
Add-Text '入口:左侧菜单【系统管理】→【菜单管理】。当前正式页面用于“给角色分配菜单”,不是维护菜单名称、路径或页面代码。菜单数据在用户登录时转换为动态路由,因此分配结果决定该角色登录后左侧导航栏显示哪些模块。' 10.5 $false 0 0 0 6 0.74
|
||||
Add-Figure (Join-Path $prepared 'menu-list.png') '图4-1 菜单管理初始页面(尚未选择角色)'
|
||||
|
||||
Add-Heading '4.1 三栏布局与过滤' 2
|
||||
Add-Table @(
|
||||
@('位置', '内容', '过滤规则'),
|
||||
@('顶部角色下拉框', '选择待配置的普通角色', '支持在下拉框中输入关键字筛选角色'),
|
||||
@('左栏:一级模块', '系统管理、计划排产、生产管理等一级菜单模板', '输入关键字后按模块名称包含匹配'),
|
||||
@('中栏:已分配菜单树', '当前角色已拥有的一级和二级菜单', '输入关键字后按树节点名称包含匹配'),
|
||||
@('右栏:二级菜单', '全部页面菜单及其所属一级模块路径', '输入关键字后按二级模块名称包含匹配')
|
||||
) @(3.5, 7, 6.5)
|
||||
Add-Note '过滤性质' '三栏关键字过滤均在当前已加载列表中即时执行,不会修改权限,也不需要点击查询按钮。清空关键字即可恢复该栏全部内容。' 'info'
|
||||
|
||||
Add-Figure (Join-Path $prepared 'menu-permission.png') '图4-2 选择“管理员”后的已分配菜单树'
|
||||
|
||||
Add-Heading '4.2 分配一级菜单' 2
|
||||
Add-StepList @(
|
||||
'在顶部选择目标角色,中栏随即加载该角色现有菜单。',
|
||||
'在左栏勾选一个或多个一级模块。已分配的一级模块复选框不可重复选择。',
|
||||
'点击左栏与中栏之间的向右箭头。',
|
||||
'在确认框核对角色和选择项,点击【确定】完成分配;点击【取消】不写入。'
|
||||
)
|
||||
Add-Note '数据关系' '一级菜单以超级管理员的菜单模板为来源,分配时复制到目标角色的菜单配置中。一级菜单只是目录;要看到具体页面,还需继续分配二级菜单。' 'info'
|
||||
|
||||
Add-Heading '4.3 分配二级菜单' 2
|
||||
Add-StepList @(
|
||||
'在中栏仅勾选一个一级菜单,作为二级菜单的归属目录。',
|
||||
'在右栏勾选一个或多个二级菜单。已分配的二级菜单不可重复选择。',
|
||||
'点击中栏与右栏之间的向左箭头。',
|
||||
'确认后,所选页面加入目标角色的目标一级目录。'
|
||||
)
|
||||
Add-Note '校验规则' '必须且只能选择一个中栏一级菜单;选中二级节点或同时选多个节点时,箭头不可用或系统提示“只可以分配给一个一级菜单”。' 'warn'
|
||||
|
||||
Add-Heading '4.4 移除菜单' 2
|
||||
Add-StepList @(
|
||||
'在中栏找到要撤回的二级菜单,必要时使用顶部关键字过滤。',
|
||||
'点击该二级菜单右侧【移除】,在确认框点击【确定】。',
|
||||
'如需移除一级菜单,必须先移除其全部二级菜单;有子节点时一级菜单的【移除】不可用。',
|
||||
'全部子菜单移除后,再移除空的一级菜单。'
|
||||
)
|
||||
Add-Note '生效验证' '菜单变更后,应让目标账号退出并重新登录,核对左侧导航。移除菜单会影响页面入口,但不删除该页面产生的历史业务数据。' 'danger'
|
||||
|
||||
Add-Heading '4.5 当前页面边界' 2
|
||||
Add-ItemList @(
|
||||
'当前页面未提供可见的“新增菜单、编辑菜单名称、修改路径、删除菜单模板”入口。',
|
||||
'页面代码中的查询/新增/编辑/删除按钮级权限控件处于停用状态,当前只分配页面级菜单。',
|
||||
'因此正式操作中不要把本页理解为菜单结构设计器;新页面上线需由开发/实施人员维护模板和路由。'
|
||||
)
|
||||
|
||||
Add-PageBreak
|
||||
Add-Heading '5 工位管理' 1
|
||||
Add-Text '入口:左侧菜单【系统管理】→【工位管理】。工位是生产任务、设备、人员绑定和工时记录的共同基础数据。列表只显示有效的机加工资源,以及名称为“外协”的特殊资源。' 10.5 $false 0 0 0 6 0.74
|
||||
Add-Figure (Join-Path $prepared 'workstation-list.png') '图5-1 工位管理列表(右侧空白为当前页面布局)' 15
|
||||
|
||||
Add-Heading '5.1 列表与查询' 2
|
||||
Add-Table @(
|
||||
@('字段/按钮', '说明'),
|
||||
@('工位名称', '业务人员识别工位的显示名称'),
|
||||
@('工位号', '工位唯一业务编码,供MES与SAP B1资源关联'),
|
||||
@('编辑', '只允许修改工位名称,工位号保持不变'),
|
||||
@('删除', '从MES资源主数据删除该工位'),
|
||||
@('查询', '当前版本刷新全部工位列表')
|
||||
) @(5, 12)
|
||||
Add-Note '当前限制' '页面虽然显示“工位名称”输入框,但当前查询请求没有传递该字段。无论输入什么内容,点击【查询】都会重新加载全部工位,不能按名称过滤。' 'warn'
|
||||
|
||||
Add-Heading '5.2 新增工位' 2
|
||||
Add-Figure (Join-Path $prepared 'workstation-add.png') '图5-2 新增工位弹窗'
|
||||
Add-StepList @(
|
||||
'点击【新增】。',
|
||||
'填写“工位名称”和“工位号”,两项均为必填。工位号应沿用公司现有编码规则。',
|
||||
'确认名称未重复、工位号未占用后点击【确定】;点击【取消】不产生数据。',
|
||||
'MES新增成功后,前端同时向 SAP B1 Resources 创建类型为 rtMachine 的资源。'
|
||||
)
|
||||
Add-Note '保存前检查' '正式环境不得用临时名称或测试编码尝试保存。新增会同时影响MES和SAP B1,失败时应分别核对两端是否一致,避免重复创建。' 'danger'
|
||||
|
||||
Add-Heading '5.3 编辑工位' 2
|
||||
Add-StepList @(
|
||||
'在目标工位行点击【编辑】。',
|
||||
'修改工位名称。工位号作为关联键不在编辑弹窗中开放修改。',
|
||||
'点击【确定】后,MES更新资源名称,并调用 SAP B1 Resources 按同一工位号更新名称。',
|
||||
'如新名称已被其他工位使用,系统提示“存在同名工位”。'
|
||||
)
|
||||
|
||||
Add-Heading '5.4 删除工位' 2
|
||||
Add-StepList @(
|
||||
'先确认工位没有未完排产、在制任务、人员绑定、设备关联和进行中的工时记录。',
|
||||
'记录工位号和名称,确认已具备可恢复依据。',
|
||||
'点击【删除】,在确认框点击【确定】后从MES资源主数据删除。'
|
||||
)
|
||||
Add-Note '高风险操作' '当前删除过程没有启用菜单/人员等占用校验,也没有调用 SAP B1 删除资源。删除可能造成MES与SAP B1不一致,并影响历史关联查询。未经业务负责人和系统管理员确认,不应执行。' 'danger'
|
||||
|
||||
Add-Heading '5.5 下游数据关系' 2
|
||||
Add-Table @(
|
||||
@('下游功能', '使用方式', '变更影响'),
|
||||
@('计划排产/工位计划', '作为指派对象或工位维度', '改名后新查询显示新名称;删除会使后续选择缺失'),
|
||||
@('生产执行/加工中心', '定位任务执行工位并形成操作记录', '错误工位会影响报工归属和工时统计'),
|
||||
@('人员绑定/班次', '建立人员与工位的可执行关系', '删除前必须解除或确认绑定'),
|
||||
@('设备与工时', '关联设备状态、人员工时、设备工时', '删除可能导致历史或在制数据无法正常联查'),
|
||||
@('SAP B1 Resources', '新增和改名时以工位号同步资源', '两端应使用同一编码和名称')
|
||||
) @(4, 6, 7)
|
||||
|
||||
Add-PageBreak
|
||||
Add-Heading '6 报工操作记录' 1
|
||||
Add-Text '入口:左侧菜单【系统管理】→【报工操作记录】。该页面是只读审计页面,用于追溯生产执行产生的操作轨迹,不提供新增、编辑、删除或导出。进入页面后自动加载最新记录。' 10.5 $false 0 0 0 6 0.74
|
||||
Add-Figure (Join-Path $prepared 'operation-log.png') '图6-1 报工操作记录(正式环境只读截图)'
|
||||
|
||||
Add-Heading '6.1 搜索与过滤' 2
|
||||
Add-StepList @(
|
||||
'按订单追溯时,在“订单编号”输入完整订单编号。',
|
||||
'按人员追溯时,在“操作人”输入完整姓名。',
|
||||
'两个条件可同时填写,系统只返回订单编号和操作人均完全相同的记录。',
|
||||
'点击【查询】;清空两个条件后查询,恢复最新100条记录。'
|
||||
)
|
||||
Add-Table @(
|
||||
@('条件', '匹配方式', '注意事项'),
|
||||
@('订单编号', '精确相等', '不支持包含匹配;少字符、多空格或编号不完整均可能无结果'),
|
||||
@('操作人', '精确相等', '应使用系统记录中的完整姓名'),
|
||||
@('订单编号 + 操作人', '两个条件取交集', '适合核对某人在指定订单上的操作'),
|
||||
@('均为空', '最新记录', '按记录内部编号倒序,仅返回前100条')
|
||||
) @(4.5, 4, 8.5)
|
||||
Add-Note '当前限制' '页面分页控件已停用,后端每次最多返回最新100条。较早记录如不在最近100条内,必须填写准确订单编号或操作人缩小范围,但同一条件仍只返回该结果集最新100条。' 'warn'
|
||||
|
||||
Add-Heading '6.2 表格字段说明' 2
|
||||
Add-Table @(
|
||||
@('字段组', '字段', '业务含义'),
|
||||
@('业务定位', '订单编号、合同号、物料编号、物料描述', '定位生产订单及对应产品/零件'),
|
||||
@('工艺与资源', '工序名称、工位名称', '说明在哪一道工序、哪个工位发生操作'),
|
||||
@('人员与动作', '操作人、操作类别、操作数值、操作描述', '说明谁执行了什么动作及数量/补充说明'),
|
||||
@('时间与工时', '开始时间、结束时间、人员工时、设备工时', '用于核对动作区间及工时结果'),
|
||||
@('审计信息', '最后编辑人、最后编辑时间', '识别记录后续调整责任人和时间')
|
||||
) @(3, 6, 8)
|
||||
|
||||
Add-Heading '6.3 前后数据关系' 2
|
||||
Add-Table @(
|
||||
@('前序动作', '形成的记录信息', '本页用途'),
|
||||
@('开始加工/辅助开始', '操作类别、订单、工序、工位、操作人、开始时间', '核对任务是否启动、由谁启动'),
|
||||
@('报工/辅助结束/加工结束', '结束时间、操作数值、人员工时、设备工时', '核对报工数量和工时结果'),
|
||||
@('后续编辑', '最后编辑人、最后编辑时间、操作描述', '追溯调整人员和调整时间')
|
||||
) @(5, 7, 5)
|
||||
Add-Note '审计原则' '本页数据来自生产执行操作记录表,只用于查询展示。发现异常时应回到对应生产任务、工时或业务处理页面核对,不能在本页直接更改记录。' 'info'
|
||||
|
||||
Add-PageBreak
|
||||
Add-Heading '7 典型业务链路' 1
|
||||
Add-Heading '7.1 新增可登录人员' 2
|
||||
Add-StepList @(
|
||||
'在角色管理确认目标角色存在;没有时创建唯一角色名称。',
|
||||
'在菜单管理选择该角色,先分配一级菜单,再分配二级页面。',
|
||||
'在人员管理新增姓名、工号和角色;系统使用初始密码123456。',
|
||||
'如需班组,新增成功后编辑人员补充班组。',
|
||||
'让本人登录验证菜单范围,禁止使用正式业务写操作做权限测试。'
|
||||
)
|
||||
|
||||
Add-Heading '7.2 下线一个角色' 2
|
||||
Add-StepList @(
|
||||
'在人员管理按角色筛选,逐一把人员调整到其他有效角色。',
|
||||
'在菜单管理选择原角色,先移除所有二级菜单,再移除一级菜单。',
|
||||
'在角色管理删除角色。若仍提示占用,返回前两步继续核对。',
|
||||
'保留变更审批或操作记录,以便追溯权限调整。'
|
||||
)
|
||||
|
||||
Add-Heading '7.3 新增生产工位' 2
|
||||
Add-StepList @(
|
||||
'由生产、设备、计划和系统管理员共同确认工位号、工位名称及SAP B1资源规则。',
|
||||
'在工位管理一次性准确填写并保存,避免用测试编码试录。',
|
||||
'核对MES工位列表和SAP B1 Resources是否一致。',
|
||||
'再到人员绑定、设备或计划页面完成下游关系配置。',
|
||||
'使用只读查询验证可选项,不创建正式生产任务作为测试。'
|
||||
)
|
||||
|
||||
Add-Heading '7.4 追溯一次报工操作' 2
|
||||
Add-StepList @(
|
||||
'优先取得完整订单编号,在报工操作记录中精确查询。',
|
||||
'如需进一步缩小范围,再填写完整操作人姓名。',
|
||||
'横向滚动核对合同、物料、工序、工位、开始/结束时间和工时。',
|
||||
'结合最后编辑人、最后编辑时间判断是否发生后续调整。',
|
||||
'需要纠正时转到对应业务页面,并按审批流程处理。'
|
||||
)
|
||||
|
||||
Add-Heading '8 常见问题' 1
|
||||
Add-Table @(
|
||||
@('问题', '原因判断', '处理建议'),
|
||||
@('人员查询不到', '姓名/角色组合无匹配,或人员角色关联已移除', '清空条件并回到第1页;确认人员是否已执行删除/离职'),
|
||||
@('新账号登录后没有菜单', '角色尚未配置一级/二级菜单,或登录会话未刷新', '菜单管理补充分配后退出并重新登录'),
|
||||
@('角色删除失败', '角色仍分配菜单或人员', '先转移人员,再按“二级→一级”顺序移除菜单'),
|
||||
@('工位名称输入后仍显示全部', '当前版本未把名称条件传给查询接口', '按列表滚动查找;不要反复输入或保存测试数据'),
|
||||
@('工位编辑提示同名', '目标名称已被资源主数据占用', '核对现有工位名称,使用经审批的唯一名称'),
|
||||
@('报工记录查询不到', '页面使用精确匹配,且最多返回最新100条', '输入完整订单编号/操作人,检查空格和编号格式'),
|
||||
@('菜单无法移除一级模块', '一级模块仍有子菜单', '先逐个移除二级菜单,再移除一级模块')
|
||||
) @(5, 6, 6)
|
||||
|
||||
Add-Heading '9 正式环境操作红线' 1
|
||||
Add-ItemList @(
|
||||
'不得使用“测试、test、临时”等数据在正式环境验证新增功能。',
|
||||
'不得通过新增后删除的方式验证操作流程;删除不一定能恢复全部上下游关系。',
|
||||
'不得直接删除仍有关联任务、人员、菜单、设备或工时的角色和工位。',
|
||||
'重置密码、分配菜单、人员换角色均应取得授权,并通知受影响人员。',
|
||||
'涉及SAP B1同步的工位新增/改名,必须核对两端结果;异常时停止重复提交。',
|
||||
'所有截图、查询和核对优先使用只读操作;写操作应按审批、备份和回退方案执行。'
|
||||
)
|
||||
|
||||
Add-Heading '附录A 技术数据关系(供管理员核对)' 1
|
||||
Add-Table @(
|
||||
@('功能', '主要数据对象', '关系说明'),
|
||||
@('人员管理', '登录基础数据_人员信息;登录基础数据_人员权限角色', '人员主记录通过人员编号关联角色编号'),
|
||||
@('角色管理', '登录基础数据_角色信息', '角色编号被人员关系和菜单配置引用'),
|
||||
@('菜单管理', '登录基础数据_二级菜单', '同一菜单模板按角色编号形成角色菜单配置'),
|
||||
@('工位管理', 'MES_资源主数据;View_基础数据_工位;SAP B1 Resources', '工位号/VisCode为跨系统关联键,名称用于显示'),
|
||||
@('报工操作记录', 'YL_加工中心_操作记录表', '生产动作写入,本页按订单或操作人只读查询')
|
||||
) @(4, 7, 6)
|
||||
|
||||
Add-Text '—— 系统管理篇结束 ——' 11 $true 1 0x777777 18 0 0
|
||||
Write-Output 'PROGRESS=content-complete'
|
||||
|
||||
# Header and footer
|
||||
foreach ($section in $doc.Sections) {
|
||||
$header = $section.Headers.Item(1).Range
|
||||
$header.Text = '大连元利流体技术有限公司 MES系统操作手册 | 系统管理篇'
|
||||
$header.Font.NameFarEast = '微软雅黑'
|
||||
$header.Font.Size = 8.5
|
||||
$header.Font.Color = 0x777777
|
||||
$header.ParagraphFormat.Alignment = 2
|
||||
|
||||
$footer = $section.Footers.Item(1).Range
|
||||
$footer.Text = '内部使用 '
|
||||
$footer.Font.NameFarEast = '宋体'
|
||||
$footer.Font.Size = 8.5
|
||||
$footer.Font.Color = 0x777777
|
||||
$footer.ParagraphFormat.Alignment = 1
|
||||
$footer.Collapse(0)
|
||||
$footer.Fields.Add($footer, 33) | Out-Null
|
||||
}
|
||||
|
||||
Write-Output 'PROGRESS=headers-complete'
|
||||
foreach ($item in $doc.TablesOfContents) { $item.Update() }
|
||||
$doc.Fields.Update() | Out-Null
|
||||
$doc.Repaginate()
|
||||
Write-Output 'PROGRESS=fields-complete'
|
||||
|
||||
if (Test-Path $docxPath) { Remove-Item -LiteralPath $docxPath -Force }
|
||||
if (Test-Path $pdfPath) { Remove-Item -LiteralPath $pdfPath -Force }
|
||||
$doc.SaveAs($docxPath, 12)
|
||||
Write-Output 'PROGRESS=docx-saved'
|
||||
$doc.ExportAsFixedFormat($pdfPath, 17)
|
||||
Write-Output 'PROGRESS=pdf-saved'
|
||||
$pageCount = $doc.ComputeStatistics(2)
|
||||
$doc.Close(0)
|
||||
$doc = $null
|
||||
|
||||
Copy-Item -LiteralPath $docxPath -Destination $desktopDocx -Force
|
||||
Copy-Item -LiteralPath $pdfPath -Destination $desktopPdf -Force
|
||||
|
||||
Write-Output ('DOCX=' + $docxPath)
|
||||
Write-Output ('PDF=' + $pdfPath)
|
||||
Write-Output ('DESKTOP_DOCX=' + $desktopDocx)
|
||||
Write-Output ('DESKTOP_PDF=' + $desktopPdf)
|
||||
Write-Output ('PAGES=' + $pageCount)
|
||||
} catch {
|
||||
$buildError = $_
|
||||
Write-Output ('BUILD_ERROR=' + $_.Exception.Message)
|
||||
Write-Output ('BUILD_ERROR_LINE=' + $_.InvocationInfo.ScriptLineNumber)
|
||||
} finally {
|
||||
if ($doc -ne $null) { try { $doc.Close(0) } catch {} }
|
||||
if ($word -ne $null) { try { $word.Quit() } catch {} }
|
||||
[GC]::Collect()
|
||||
[GC]::WaitForPendingFinalizers()
|
||||
}
|
||||
|
||||
if ($buildError -ne $null) { throw $buildError }
|
||||
412
work/system-manual/capture-full-manual-pages.js
Normal file
@@ -0,0 +1,412 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const WebSocket = require('ws')
|
||||
|
||||
const outputDir = path.resolve(__dirname, 'full-screenshots')
|
||||
const pageFilter = new Set(String(process.env.MES_MANUAL_PAGE_FILTER || '').split('|').filter(Boolean))
|
||||
const manifestPath = path.join(outputDir, pageFilter.size ? 'manifest-recapture.json' : 'manifest.json')
|
||||
const baseUrl = process.env.MES_MANUAL_BASE_URL || 'https://127.0.0.1:1998'
|
||||
const debugPort = process.env.MES_MANUAL_DEBUG_PORT || '9223'
|
||||
const captureDelay = Number(process.env.MES_MANUAL_CAPTURE_DELAY || 5000)
|
||||
const loginUser = process.env.MES_MANUAL_USER
|
||||
const loginPassword = process.env.MES_MANUAL_PASSWORD
|
||||
const loginRole = process.env.MES_MANUAL_ROLE
|
||||
const loginPersonnelId = process.env.MES_MANUAL_PERSONNEL_ID || '1'
|
||||
const loginName = process.env.MES_MANUAL_PERSONNEL_NAME || '超级管理员'
|
||||
const loginAccount = process.env.MES_MANUAL_ACCOUNT || '0000'
|
||||
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
|
||||
const modules = [
|
||||
{
|
||||
key: 'device',
|
||||
title: '设备管理',
|
||||
pages: [
|
||||
['设备信息维护', '/DeviceManagement/DeviceInformation/index', 'DeviceManagement/DeviceInformation/index.vue'],
|
||||
['设备数据', '/DeviceManagement/Devicedata/index', 'DeviceManagement/Devicedata/index.vue'],
|
||||
['设备历史数据', '/DeviceManagement/DeviceDataHistory/index', 'DeviceManagement/DeviceDataHistory/index.vue'],
|
||||
['实时数据', '/DeviceManagement/DeviceRealData/index', 'DeviceManagement/DeviceRealData/index.vue']
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'research',
|
||||
title: '研发管理',
|
||||
pages: [
|
||||
['设计任务', '/ResearchManagement/DesignReportTask/Task/index', 'ProductionManagement/DesignReportTask/Task/index.vue'],
|
||||
['设计报工', '/ResearchManagement/DesignReportTask/Report/index', 'ProductionManagement/DesignReportTask/Report/index.vue'],
|
||||
['设计工时校验', '/ResearchManagement/DesignReportTask/Check/index', 'ProductionManagement/DesignReportTask/Check/index.vue'],
|
||||
['设计工时报表', '/ResearchManagement/DesignReportTask/HoursReport/index', 'ProductionManagement/DesignReportTask/HoursReport/index.vue']
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'process',
|
||||
title: '工艺管理',
|
||||
pages: [
|
||||
['工序管理', '/ProcessManagement/ProcedureManagement/index', 'ProcessManagement/ProcedureManagement/index.vue'],
|
||||
['制定标准工艺', '/ProcessManagement/ProcessesDevelop/index', 'ProcessManagement/ProcessesDevelop/index.vue'],
|
||||
['生产工艺', '/ProcessManagement/ProcessProduct/index', 'ProcessManagement/ProcessProduct/index.vue'],
|
||||
['工艺查询', '/ProcessManagement/ProcessInquiry/index', 'ProcessManagement/ProcessInquiry/index.vue'],
|
||||
['工艺任务', '/ProcessManagement/ProductionOrder/index', 'ProcessManagement/ProductionOrder/index.vue'],
|
||||
['图纸维护', '/ProcessManagement/PartDrawing/index', 'ProcessManagement/PartDrawing/index.vue'],
|
||||
['工艺维护', '/ProcessManagement/ProcessDrawing/index', 'ProcessManagement/ProcessDrawing/index.vue'],
|
||||
['查看图纸', '/ProcessManagement/PartDrawlook/index', 'ProcessManagement/PartDrawlook/index.vue'],
|
||||
['采购图纸查询', '/ProcessManagement/ProcurementDrawlook/index', 'ProcessManagement/ProcurementDrawlook/index.vue']
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'plan',
|
||||
title: '计划排产',
|
||||
pages: [
|
||||
['机加件排产', '/PlanManagement/SelfMakePlan/index', 'PlanManagement/SelfMakePlan/index.vue'],
|
||||
['装配件排产', '/PlanManagement/InstallMakePlan/index', 'PlanManagement/InstallMakePlan/index.vue'],
|
||||
['机加件已排产', '/PlanManagement/SelfMakePlandone/index', 'PlanManagement/SelfMakePlandone/index.vue'],
|
||||
['机加件未排产', '/PlanManagement/SelfMakePlando/index', 'PlanManagement/SelfMakePlando/index.vue'],
|
||||
['优先装配', '/PlanManagement/priorityinstall/index', 'PlanManagement/priorityinstall/index.vue'],
|
||||
['计划用表', '/PlanManagement/PlanShell/index', 'PlanManagement/PlanShell/index.vue'],
|
||||
['装配未排产', '/PlanManagement/InstallMakePlanDo/index', 'PlanManagement/InstallMakePlanDo/index.vue'],
|
||||
['生产订单关闭', '/PlanManagement/PlannOrderClose/index', 'PlanManagement/PlannOrderClose/index.vue'],
|
||||
['工位任务汇总', '/PlanManagement/WorkstationPlan/index', 'PlanManagement/WorkstationPlan/index.vue'],
|
||||
['装配已排产', '/PlanManagement/InstallMakePlanDone/index', 'PlanManagement/InstallMakePlanDone/index.vue'],
|
||||
['系统类装配任务', '/PlanManagement/SystemInstallTask/index', 'PlanManagement/SystemInstallTask/index.vue'],
|
||||
['阀类装配任务', '/PlanManagement/ValueInstallTask/index', 'PlanManagement/ValueInstallTask/index.vue'],
|
||||
['及时齐套跟踪结果', '/PlanManagement/TimelyKitTrackingResult/index', 'PlanManagement/TimelyKitTrackingResult/index.vue']
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'production',
|
||||
title: '生产管理',
|
||||
pages: [
|
||||
['生产计划跟踪', '/ProductionManagement/ProductionPlanTrack/index', 'ProductionManagement/ProductionPlanTrack/index.vue'],
|
||||
['加工中心', '/ProductionManagement/MachiningCenter/index', 'ProductionManagement/MachiningCenter/index.vue'],
|
||||
['过程异常管控', '/ProductionManagement/AbnormalControl/index', 'ProductionManagement/AbnormalControl/index.vue'],
|
||||
['工时校验', '/ProductionManagement/WorkHoursCheck/index', 'ProductionManagement/WorkHoursCheck/index.vue'],
|
||||
['外协管理', '/ProductionManagement/OutsourceMange/index', 'ProductionManagement/OutsourceMange/index.vue'],
|
||||
['人员绑定', '/ProductionManagement/PersonnelBinding/index', 'ProductionManagement/PersonnelBinding/index.vue'],
|
||||
['装配中心', '/ProductionManagement/AssemblyCenter/index', 'ProductionManagement/AssemblyCenter/index.vue'],
|
||||
['班次管理', '/ProductionManagement/ShiftManagement/index', 'ProductionManagement/ShiftManagement/index.vue'],
|
||||
['工时报表', '/ProductionManagement/Timesheet/index', 'ProductionManagement/Timesheet/index.vue'],
|
||||
['加急件管理', '/ProductionManagement/Urgent/index', 'ProductionManagement/Urgent/index.vue'],
|
||||
['拣配任务', '/ProductionManagement/PickingTask/index', 'ProductionManagement/PickingTask/index.vue'],
|
||||
['采购外协管理', '/ProductionManagement/SaleOutsourceMange/index', 'ProductionManagement/SaleOutsourceMange/index.vue'],
|
||||
['工时查询', '/ProductionManagement/Workhours/index', 'ProductionManagement/Workhours/index.vue'],
|
||||
['机加正在进行中', '/ProductionManagement/DeviceStatus/index', 'ProductionManagement/DeviceStatus/index.vue'],
|
||||
['装配状态', '/ProductionManagement/AssembleStatus/index', 'ProductionManagement/AssembleStatus/index.vue'],
|
||||
['外协报表', '/ProductionManagement/OutsourceSheet/index', 'ProductionManagement/OutsourceSheet/index.vue'],
|
||||
['工时修正', '/ProductionManagement/WorkhoursEdit/index', 'ProductionManagement/WorkhoursEdit/index.vue'],
|
||||
['物料机器人', '/ProductionManagement/AGVtask/index', 'ProductionManagement/AGVtask/index.vue'],
|
||||
['手续确认报表', '/ProductionManagement/FormalityConfirmReport/index', 'ProductionManagement/FormalityConfirmReport/index.vue'],
|
||||
['发货通知列表', '/ProductionManagement/DeliveryNoticeList/index', 'ProductionManagement/DeliveryNoticeList/index.vue'],
|
||||
['结算工时', '/ProductionManagement/SettlementWorkhours/index', 'ProductionManagement/SettlementWorkhours/index.vue']
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'anomaly',
|
||||
title: '异常提醒',
|
||||
pages: [
|
||||
['机加及时开工', '/AnomalousManagement/PunctualWork/index', 'AnomalousManagement/PunctualWork/index.vue'],
|
||||
['外协及时开工', '/AnomalousManagement/OutPunctualWork/index', 'AnomalousManagement/OutPunctualWork/index.vue'],
|
||||
['加急任务列表', '/AnomalousManagement/UrgentTask/index', 'AnomalousManagement/UrgentTask/index.vue'],
|
||||
['装配及时送检', '/AnomalousManagement/AssemblyPunctualInspection/index', 'AnomalousManagement/AssemblyPunctualInspection/index.vue']
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'quality',
|
||||
title: '质量管理',
|
||||
pages: [
|
||||
['终检', '/QualityManagement/CheckTask/final', 'QualityManagement/CheckTask/final.vue'],
|
||||
['采购入库检', '/QualityManagement/CheckTask/purchaseIncoming', 'QualityManagement/CheckTask/purchaseIncoming.vue'],
|
||||
['销售退货检', '/QualityManagement/CheckTask/salesReturn', 'QualityManagement/CheckTask/salesReturn.vue'],
|
||||
['原材料退库检', '/QualityManagement/CheckTask/materialReturn', 'QualityManagement/CheckTask/materialReturn.vue'],
|
||||
['序检收检', '/QualityManagement/CheckTask/processReceive', 'QualityManagement/CheckTask/processReceive.vue'],
|
||||
['质量检验标准', '/QualityManagement/QualityStandardQuery/index', 'QualityManagement/QualityStandardQuery/index.vue'],
|
||||
['其他入库检', '/QualityManagement/CheckTask/OtherInbound', 'QualityManagement/CheckTask/OtherInbound.vue'],
|
||||
['质检明细', '/QualityManagement/QualityDetails/index', 'QualityManagement/QualityDetails/index.vue'],
|
||||
['不合格品处置', '/QualityManagement/Unqualified/index', 'QualityManagement/Unqualified/index.vue'],
|
||||
['质检报工', '/QualityManagement/QualityCenter/index', 'QualityManagement/QualityCenter/index.vue'],
|
||||
['测试装配任务', '/QualityManagement/TestAssemblyTask/index', 'QualityManagement/TestAssemblyTask/index.vue'],
|
||||
['及时检验报表', '/QualityManagement/CheckTask/punctualInspection', 'QualityManagement/CheckTask/punctualInspection.vue'],
|
||||
['产品合格率(工位)', '/QualityManagement/ProductPassRateWorkstation/index', 'ProductionManagement/ProductPassRateWorkstation/index.vue']
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'sales',
|
||||
title: '销售订单管理',
|
||||
pages: [
|
||||
['销售订单状态', '/SaleManagement/saleorderstatus/index', 'SaleManagement/saleorderstatus/index.vue']
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
async function connect() {
|
||||
const targets = await fetch(`http://127.0.0.1:${debugPort}/json`).then(res => res.json())
|
||||
const target = targets.find(item => item.type === 'page')
|
||||
if (!target) throw new Error('No Chrome page target found')
|
||||
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl)
|
||||
let id = 0
|
||||
const pending = new Map()
|
||||
ws.on('message', raw => {
|
||||
const message = JSON.parse(raw.toString())
|
||||
if (message.id && pending.has(message.id)) {
|
||||
const request = pending.get(message.id)
|
||||
pending.delete(message.id)
|
||||
message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result)
|
||||
}
|
||||
})
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.once('open', resolve)
|
||||
ws.once('error', reject)
|
||||
})
|
||||
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const requestId = ++id
|
||||
pending.set(requestId, { resolve, reject })
|
||||
ws.send(JSON.stringify({ id: requestId, method, params }))
|
||||
})
|
||||
return { ws, send }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { ws, send } = await connect()
|
||||
await send('Page.enable')
|
||||
await send('Runtime.enable')
|
||||
await send('Network.enable')
|
||||
await send('Security.enable')
|
||||
await send('Security.setIgnoreCertificateErrors', { ignore: true })
|
||||
await send('Emulation.setDeviceMetricsOverride', {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
deviceScaleFactor: 1,
|
||||
mobile: false
|
||||
})
|
||||
|
||||
const evaluate = expression => send('Runtime.evaluate', {
|
||||
expression,
|
||||
awaitPromise: true,
|
||||
returnByValue: true
|
||||
}).then(result => result.result.value)
|
||||
|
||||
async function waitFor(expression, timeout = 45000) {
|
||||
const started = Date.now()
|
||||
while (Date.now() - started < timeout) {
|
||||
if (await evaluate(`Boolean(${expression})`)) return true
|
||||
await wait(300)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function clickByText(text, selector = 'button') {
|
||||
return evaluate(`(() => {
|
||||
const normalize = value => String(value || '').replace(/\\s+/g, '')
|
||||
const node = [...document.querySelectorAll(${JSON.stringify(selector)})]
|
||||
.find(item => item.offsetParent !== null && !item.disabled &&
|
||||
normalize(item.innerText).includes(${JSON.stringify(text.replace(/\s+/g, ''))}))
|
||||
if (!node) return false
|
||||
node.click()
|
||||
return true
|
||||
})()`)
|
||||
}
|
||||
|
||||
async function login() {
|
||||
if (loginRole) {
|
||||
for (const [name, value] of [
|
||||
['Admin-Token', loginRole],
|
||||
['id', loginPersonnelId],
|
||||
['name', loginName],
|
||||
['account', loginAccount]
|
||||
]) {
|
||||
await send('Network.setCookie', { name, value, url: baseUrl })
|
||||
}
|
||||
await send('Page.navigate', { url: `${baseUrl}/#/` })
|
||||
const ready = await waitFor("document.querySelector('.sidebar-container')", 60000)
|
||||
if (!ready) throw new Error('Cookie session did not reach the application shell')
|
||||
return
|
||||
}
|
||||
await send('Page.navigate', { url: `${baseUrl}/#/login` })
|
||||
await waitFor("document.querySelector('input[name=username]') || document.querySelector('.sidebar-container')")
|
||||
if (await evaluate("Boolean(document.querySelector('input[name=username]'))")) {
|
||||
if (!loginUser || !loginPassword) throw new Error('MES manual login credentials are not available')
|
||||
await evaluate(`(() => {
|
||||
const setValue = (element, value) => {
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set
|
||||
setter.call(element, value)
|
||||
element.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
element.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
setValue(document.querySelector('input[name=username]'), ${JSON.stringify(loginUser)})
|
||||
setValue(document.querySelector('input[name=password]'), ${JSON.stringify(loginPassword)})
|
||||
})()`)
|
||||
await clickByText('登录')
|
||||
await wait(1200)
|
||||
if (await evaluate("Boolean(document.querySelector('.el-dialog__wrapper:not([style*=\"display: none\"]) .el-table__row'))")) {
|
||||
await evaluate("document.querySelector('.el-dialog__wrapper:not([style*=\"display: none\"]) .el-table__row').click()")
|
||||
await clickByText('确定', '.el-dialog__wrapper:not([style*="display: none"]) button')
|
||||
}
|
||||
}
|
||||
const ready = await waitFor("document.querySelector('.sidebar-container')", 60000)
|
||||
if (!ready) throw new Error('Login did not reach the application shell')
|
||||
}
|
||||
|
||||
async function capture(page, fileName) {
|
||||
await send('Page.navigate', { url: `${baseUrl}/#${page.route}` })
|
||||
const ready = await waitFor("document.readyState === 'complete' && (document.querySelector('.app-main') || document.querySelector('.app-container'))")
|
||||
if (!ready) throw new Error(`Page shell timed out: ${page.route}`)
|
||||
await waitFor("!document.querySelector('.el-loading-mask:not([style*=\"display: none\"])')", 15000)
|
||||
if (page.title === '生产工艺') {
|
||||
await waitFor(`(() => {
|
||||
const app = document.querySelector('#app') && document.querySelector('#app').__vue__
|
||||
return Boolean(app && app.$route && app.$route.matched && app.$route.matched
|
||||
.some(record => Object.values(record.instances || {}).some(instance => instance && typeof instance.searchTable === 'function')))
|
||||
})()`, 15000)
|
||||
const filtered = await evaluate(`(() => {
|
||||
const app = document.querySelector('#app') && document.querySelector('#app').__vue__
|
||||
const records = app && app.$route && app.$route.matched ? [...app.$route.matched].reverse() : []
|
||||
for (const record of records) {
|
||||
const instances = Object.values(record.instances || {}).filter(Boolean)
|
||||
const vm = instances.find(instance => instance.taskstatus !== undefined && typeof instance.searchTable === 'function')
|
||||
if (!vm) continue
|
||||
vm.taskstatus = '2'
|
||||
vm.searchTable()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})()`)
|
||||
if (!filtered) throw new Error('生产工艺页面路由实例未就绪')
|
||||
} else {
|
||||
await clickByText('查询')
|
||||
}
|
||||
await waitFor("!document.querySelector('.el-loading-mask:not([style*=\"display: none\"])')", 30000)
|
||||
if (page.title === '机加件已排产') {
|
||||
await waitFor("document.querySelectorAll('.el-table__body-wrapper .el-table__row').length > 0", 60000)
|
||||
}
|
||||
await wait(captureDelay)
|
||||
await evaluate('window.scrollTo(0, 0)')
|
||||
const result = await send('Page.captureScreenshot', {
|
||||
format: 'png',
|
||||
captureBeyondViewport: false,
|
||||
fromSurface: true
|
||||
})
|
||||
fs.writeFileSync(path.join(outputDir, fileName), Buffer.from(result.data, 'base64'))
|
||||
return evaluate(`({
|
||||
title: document.title,
|
||||
bodyTextLength: document.body ? document.body.innerText.length : 0,
|
||||
hasError: Boolean(document.querySelector('.el-message--error')),
|
||||
route: location.hash
|
||||
})`)
|
||||
}
|
||||
|
||||
async function captureEditDialog(page) {
|
||||
const dialogConfig = {
|
||||
'机加件已排产': {
|
||||
button: '编辑',
|
||||
title: '编辑备注信息',
|
||||
fileName: '04-03-plan-edit-dialog.png'
|
||||
},
|
||||
'生产工艺': {
|
||||
button: '编辑工艺',
|
||||
title: '编辑生产工艺',
|
||||
fileName: '03-03-process-edit-dialog.png'
|
||||
}
|
||||
}[page.title]
|
||||
if (!dialogConfig) return null
|
||||
let opened = await evaluate(`(() => {
|
||||
const normalize = value => String(value || '').replace(/\\s+/g, '')
|
||||
const button = [...document.querySelectorAll('button')]
|
||||
.find(item => normalize(item.innerText) === '${dialogConfig.button}')
|
||||
if (!button) return false
|
||||
button.click()
|
||||
return true
|
||||
})()`)
|
||||
if (!opened) throw new Error(`${page.title}页面未找到可用的${dialogConfig.button}按钮`)
|
||||
const dialogReady = await waitFor(`[...document.querySelectorAll('.el-dialog__wrapper:not([style*="display: none"])')]
|
||||
.some(item => String(item.innerText || '').includes('${dialogConfig.title}'))`, 15000)
|
||||
if (!dialogReady) throw new Error(`${dialogConfig.title}弹窗未打开`)
|
||||
if (page.title === '生产工艺') {
|
||||
let populated = await waitFor("document.querySelectorAll('.el-dialog__wrapper:not([style*=\"display: none\"]) .el-table__body-wrapper .el-table__row').length > 0", 5000)
|
||||
const buttonCount = await evaluate(`(() => {
|
||||
const normalize = value => String(value || '').replace(/\\s+/g, '')
|
||||
return [...document.querySelectorAll('button')]
|
||||
.filter(item => item.offsetParent !== null && normalize(item.innerText) === '${dialogConfig.button}').length
|
||||
})()`)
|
||||
for (let index = 1; !populated && index < buttonCount; index += 1) {
|
||||
await evaluate(`(() => {
|
||||
const dialog = [...document.querySelectorAll('.el-dialog__wrapper:not([style*=\"display: none\"])')]
|
||||
.find(item => String(item.innerText || '').includes('${dialogConfig.title}'))
|
||||
const close = dialog && dialog.querySelector('.el-dialog__headerbtn')
|
||||
if (close) close.click()
|
||||
})()`)
|
||||
await waitFor(`![...document.querySelectorAll('.el-dialog__wrapper:not([style*=\"display: none\"])')]
|
||||
.some(item => String(item.innerText || '').includes('${dialogConfig.title}'))`, 5000)
|
||||
opened = await evaluate(`(() => {
|
||||
const normalize = value => String(value || '').replace(/\\s+/g, '')
|
||||
const buttons = [...document.querySelectorAll('button')]
|
||||
.filter(item => item.offsetParent !== null && normalize(item.innerText) === '${dialogConfig.button}')
|
||||
if (!buttons[${index}]) return false
|
||||
buttons[${index}].click()
|
||||
return true
|
||||
})()`)
|
||||
if (!opened) continue
|
||||
await waitFor(`[...document.querySelectorAll('.el-dialog__wrapper:not([style*=\"display: none\"])')]
|
||||
.some(item => String(item.innerText || '').includes('${dialogConfig.title}'))`, 5000)
|
||||
populated = await waitFor("document.querySelectorAll('.el-dialog__wrapper:not([style*=\"display: none\"]) .el-table__body-wrapper .el-table__row').length > 0", 5000)
|
||||
}
|
||||
}
|
||||
await wait(1000)
|
||||
const result = await send('Page.captureScreenshot', {
|
||||
format: 'png',
|
||||
captureBeyondViewport: false,
|
||||
fromSurface: true
|
||||
})
|
||||
fs.writeFileSync(path.join(outputDir, dialogConfig.fileName), Buffer.from(result.data, 'base64'))
|
||||
return { fileName: dialogConfig.fileName, title: dialogConfig.title }
|
||||
}
|
||||
|
||||
await login()
|
||||
const manifest = []
|
||||
const totalPages = modules.reduce((total, module) => {
|
||||
return total + module.pages.filter(page => pageFilter.size === 0 || pageFilter.has(page[0])).length
|
||||
}, 0)
|
||||
let sequence = 0
|
||||
for (let moduleIndex = 0; moduleIndex < modules.length; moduleIndex++) {
|
||||
const module = modules[moduleIndex]
|
||||
for (let pageIndex = 0; pageIndex < module.pages.length; pageIndex++) {
|
||||
sequence++
|
||||
const [title, route, source] = module.pages[pageIndex]
|
||||
if (pageFilter.size && !pageFilter.has(title)) {
|
||||
sequence--
|
||||
continue
|
||||
}
|
||||
const fileName = `${String(moduleIndex + 1).padStart(2, '0')}-${String(pageIndex + 1).padStart(2, '0')}-${module.key}.png`
|
||||
const item = { sequence, module: module.title, title, route, source, fileName, status: 'pending' }
|
||||
try {
|
||||
item.pageState = await capture(item, fileName)
|
||||
item.dialogCapture = await captureEditDialog(item)
|
||||
item.status = 'captured'
|
||||
console.log(`[${sequence}/${totalPages}] ${module.title} / ${title}`)
|
||||
} catch (error) {
|
||||
item.status = 'failed'
|
||||
item.error = error.message
|
||||
console.error(`[${sequence}/${totalPages}] FAILED ${module.title} / ${title}: ${error.message}`)
|
||||
}
|
||||
manifest.push(item)
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
|
||||
}
|
||||
}
|
||||
ws.close()
|
||||
|
||||
const failed = manifest.filter(item => item.status !== 'captured')
|
||||
console.log(`Captured ${manifest.length - failed.length}/${manifest.length} pages`)
|
||||
await send('Browser.close')
|
||||
if (failed.length) process.exitCode = 1
|
||||
}
|
||||
|
||||
module.exports = { modules }
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
console.error(error.stack || error.message)
|
||||
process.exitCode = 1
|
||||
})
|
||||
}
|
||||
168
work/system-manual/capture-system-pages.js
Normal file
@@ -0,0 +1,168 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const WebSocket = require('ws')
|
||||
|
||||
const outputDir = path.resolve(__dirname, 'screenshots')
|
||||
fs.mkdirSync(outputDir, { recursive: true })
|
||||
const loginUser = process.env.MES_MANUAL_USER
|
||||
const loginPassword = process.env.MES_MANUAL_PASSWORD
|
||||
|
||||
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||
|
||||
async function connect() {
|
||||
const targets = await fetch('http://127.0.0.1:9222/json').then(res => res.json())
|
||||
const target = targets.find(item => item.type === 'page')
|
||||
if (!target) throw new Error('No Chrome page target found')
|
||||
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl)
|
||||
let id = 0
|
||||
const pending = new Map()
|
||||
ws.on('message', raw => {
|
||||
const message = JSON.parse(raw.toString())
|
||||
if (message.id && pending.has(message.id)) {
|
||||
const { resolve, reject } = pending.get(message.id)
|
||||
pending.delete(message.id)
|
||||
message.error ? reject(new Error(message.error.message)) : resolve(message.result)
|
||||
}
|
||||
})
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.once('open', resolve)
|
||||
ws.once('error', reject)
|
||||
})
|
||||
|
||||
const send = (method, params = {}) => new Promise((resolve, reject) => {
|
||||
const requestId = ++id
|
||||
pending.set(requestId, { resolve, reject })
|
||||
ws.send(JSON.stringify({ id: requestId, method, params }))
|
||||
})
|
||||
return { ws, send }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { ws, send } = await connect()
|
||||
await send('Page.enable')
|
||||
await send('Runtime.enable')
|
||||
await send('Emulation.setDeviceMetricsOverride', {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
deviceScaleFactor: 1,
|
||||
mobile: false
|
||||
})
|
||||
|
||||
const evaluate = expression => send('Runtime.evaluate', {
|
||||
expression,
|
||||
awaitPromise: true,
|
||||
returnByValue: true
|
||||
}).then(result => result.result.value)
|
||||
|
||||
async function waitFor(expression, timeout = 30000) {
|
||||
const started = Date.now()
|
||||
while (Date.now() - started < timeout) {
|
||||
if (await evaluate(`Boolean(${expression})`)) return
|
||||
await wait(300)
|
||||
}
|
||||
throw new Error(`Timed out waiting for: ${expression}`)
|
||||
}
|
||||
|
||||
async function screenshot(name) {
|
||||
await wait(700)
|
||||
const result = await send('Page.captureScreenshot', {
|
||||
format: 'png',
|
||||
captureBeyondViewport: false,
|
||||
fromSurface: true
|
||||
})
|
||||
fs.writeFileSync(path.join(outputDir, `${name}.png`), Buffer.from(result.data, 'base64'))
|
||||
console.log(`Captured ${name}`)
|
||||
}
|
||||
|
||||
async function navigate(hashPath) {
|
||||
await send('Page.navigate', { url: `https://127.0.0.1:1997/#${hashPath}` })
|
||||
await waitFor("document.readyState === 'complete'")
|
||||
await waitFor("document.querySelector('.app-main') || document.querySelector('.el-card')", 40000)
|
||||
await wait(1800)
|
||||
}
|
||||
|
||||
async function clickByText(text, selector = 'button') {
|
||||
return evaluate(`(() => {
|
||||
const nodes = [...document.querySelectorAll(${JSON.stringify(selector)})]
|
||||
const node = nodes.find(item => item.innerText && item.innerText.replace(/\\s+/g, '').includes(${JSON.stringify(text.replace(/\s+/g, ''))}))
|
||||
if (!node) return false
|
||||
node.click()
|
||||
return true
|
||||
})()`)
|
||||
}
|
||||
|
||||
await send('Page.navigate', { url: 'https://127.0.0.1:1997/#/login' })
|
||||
await waitFor("document.querySelector('input[name=username]') || document.querySelector('.sidebar-container')", 40000)
|
||||
if (await evaluate("Boolean(document.querySelector('input[name=username]'))")) {
|
||||
if (!loginUser || !loginPassword) {
|
||||
throw new Error('Set MES_MANUAL_USER and MES_MANUAL_PASSWORD before capturing pages')
|
||||
}
|
||||
await evaluate(`(() => {
|
||||
const setValue = (el, value) => {
|
||||
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set
|
||||
setter.call(el, value)
|
||||
el.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
setValue(document.querySelector('input[name=username]'), ${JSON.stringify(loginUser)})
|
||||
setValue(document.querySelector('input[name=password]'), ${JSON.stringify(loginPassword)})
|
||||
})()`)
|
||||
await clickByText('登录')
|
||||
}
|
||||
await waitFor("document.querySelector('.sidebar-container')", 40000)
|
||||
await wait(1500)
|
||||
|
||||
const pages = [
|
||||
['01-personnel-list', '/SystemMaintenance/PersonnelManagement/index'],
|
||||
['02-role-list', '/SystemMaintenance/SystemRrolemaintenance/index'],
|
||||
['03-menu-list', '/SystemMaintenance/MenuManagement/index'],
|
||||
['04-workstation-list', '/SystemMaintenance/WorkstationManagement/index'],
|
||||
['05-operation-log', '/SystemMaintenance/OperationLog/index']
|
||||
]
|
||||
|
||||
for (const [name, route] of pages) {
|
||||
await navigate(route)
|
||||
await screenshot(name)
|
||||
|
||||
if (name === '01-personnel-list') {
|
||||
if (await clickByText('新增')) {
|
||||
await waitFor("document.querySelector('.el-dialog__wrapper:not([style*=\"display: none\"])')")
|
||||
await screenshot('01-personnel-add-dialog')
|
||||
await clickByText('取消', '.el-dialog__wrapper:not([style*=\"display: none\"]) button')
|
||||
}
|
||||
}
|
||||
if (name === '02-role-list') {
|
||||
if (await clickByText('新增')) {
|
||||
await waitFor("document.querySelector('.el-dialog__wrapper:not([style*=\"display: none\"])')")
|
||||
await screenshot('02-role-add-dialog')
|
||||
await clickByText('取消', '.el-dialog__wrapper:not([style*=\"display: none\"]) button')
|
||||
}
|
||||
}
|
||||
if (name === '03-menu-list') {
|
||||
await evaluate(`document.querySelector('#selectRoleName').click()`)
|
||||
await waitFor("[...document.querySelectorAll('.el-select-dropdown__item')].some(item => item.innerText.trim() === '管理员')")
|
||||
await evaluate(`(() => {
|
||||
const option = [...document.querySelectorAll('.el-select-dropdown__item')]
|
||||
.find(item => item.innerText.trim() === '管理员')
|
||||
option.click()
|
||||
})()`)
|
||||
await waitFor("document.querySelectorAll('.moduleTree .el-tree-node').length > 0")
|
||||
await screenshot('03-menu-permission-panel')
|
||||
}
|
||||
if (name === '04-workstation-list') {
|
||||
if (await clickByText('新增')) {
|
||||
await waitFor("document.querySelector('.el-dialog__wrapper:not([style*=\"display: none\"])')")
|
||||
await screenshot('04-workstation-add-dialog')
|
||||
await clickByText('取消', '.el-dialog__wrapper:not([style*=\"display: none\"]) button')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ws.close()
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
38
work/system-manual/export-revised-manual-pdf.ps1
Normal file
@@ -0,0 +1,38 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
$docDir = Join-Path $root 'doc'
|
||||
$source = Get-ChildItem -LiteralPath $docDir -Filter '*.docx' | Sort-Object Length -Descending | Select-Object -First 1
|
||||
if (-not $source) { throw 'doc目录中未找到待导出的DOCX文件' }
|
||||
$docxPath = $source.FullName
|
||||
$pdfPath = Join-Path $docDir ($source.BaseName + '.pdf')
|
||||
$tempDocxPath = Join-Path $PSScriptRoot 'revised-manual-for-pdf.docx'
|
||||
|
||||
Copy-Item -LiteralPath $docxPath -Destination $tempDocxPath -Force
|
||||
|
||||
$word = $null
|
||||
$doc = $null
|
||||
try {
|
||||
$word = New-Object -ComObject Word.Application
|
||||
$word.Visible = $false
|
||||
$word.DisplayAlerts = 0
|
||||
$doc = $word.Documents.Open($tempDocxPath, $false, $false)
|
||||
if ($doc.TablesOfContents.Count -gt 0) {
|
||||
for ($index = 1; $index -le $doc.TablesOfContents.Count; $index++) {
|
||||
try { $doc.TablesOfContents.Item($index).Update() } catch { }
|
||||
}
|
||||
}
|
||||
$doc.Save()
|
||||
$doc.ExportAsFixedFormat($pdfPath, 17)
|
||||
Write-Output "DOCX=$docxPath"
|
||||
Write-Output "PDF=$pdfPath"
|
||||
}
|
||||
finally {
|
||||
if ($doc) { try { $doc.Close($false) } catch { } }
|
||||
if ($word) { try { $word.Quit() } catch { } }
|
||||
if ($doc) { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($doc) }
|
||||
if ($word) { [void][Runtime.InteropServices.Marshal]::ReleaseComObject($word) }
|
||||
[GC]::Collect()
|
||||
[GC]::WaitForPendingFinalizers()
|
||||
if (Test-Path -LiteralPath $tempDocxPath) { Remove-Item -LiteralPath $tempDocxPath -Force -ErrorAction SilentlyContinue }
|
||||
}
|
||||
4424
work/system-manual/full-page-catalog.json
Normal file
BIN
work/system-manual/full-screenshots/01-01-device.png
Normal file
|
After Width: | Height: | Size: 149 KiB |
BIN
work/system-manual/full-screenshots/01-02-device.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
work/system-manual/full-screenshots/01-03-device.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
work/system-manual/full-screenshots/01-04-device.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
work/system-manual/full-screenshots/02-01-research.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
work/system-manual/full-screenshots/02-02-research.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
work/system-manual/full-screenshots/02-03-research.png
Normal file
|
After Width: | Height: | Size: 275 KiB |
BIN
work/system-manual/full-screenshots/02-04-research.png
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
work/system-manual/full-screenshots/03-01-process.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
work/system-manual/full-screenshots/03-02-process.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 219 KiB |
BIN
work/system-manual/full-screenshots/03-03-process.png
Normal file
|
After Width: | Height: | Size: 298 KiB |
BIN
work/system-manual/full-screenshots/03-04-process.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
work/system-manual/full-screenshots/03-05-process.png
Normal file
|
After Width: | Height: | Size: 127 KiB |
BIN
work/system-manual/full-screenshots/03-06-process.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
work/system-manual/full-screenshots/03-07-process.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
work/system-manual/full-screenshots/03-08-process.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
work/system-manual/full-screenshots/03-09-process.png
Normal file
|
After Width: | Height: | Size: 186 KiB |
BIN
work/system-manual/full-screenshots/04-01-plan.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
work/system-manual/full-screenshots/04-02-plan.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
work/system-manual/full-screenshots/04-03-plan-edit-dialog.png
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
work/system-manual/full-screenshots/04-03-plan.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
BIN
work/system-manual/full-screenshots/04-04-plan.png
Normal file
|
After Width: | Height: | Size: 307 KiB |
BIN
work/system-manual/full-screenshots/04-05-plan.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
work/system-manual/full-screenshots/04-06-plan.png
Normal file
|
After Width: | Height: | Size: 118 KiB |
BIN
work/system-manual/full-screenshots/04-07-plan.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
work/system-manual/full-screenshots/04-08-plan.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
work/system-manual/full-screenshots/04-09-plan.png
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
work/system-manual/full-screenshots/04-10-plan.png
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
work/system-manual/full-screenshots/04-11-plan.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
work/system-manual/full-screenshots/04-12-plan.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
work/system-manual/full-screenshots/04-13-plan.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
work/system-manual/full-screenshots/05-01-production.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
work/system-manual/full-screenshots/05-02-production.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
work/system-manual/full-screenshots/05-03-production.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
work/system-manual/full-screenshots/05-04-production.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
work/system-manual/full-screenshots/05-05-production.png
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
work/system-manual/full-screenshots/05-06-production.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
work/system-manual/full-screenshots/05-07-production.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
work/system-manual/full-screenshots/05-08-production.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
work/system-manual/full-screenshots/05-09-production.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
work/system-manual/full-screenshots/05-10-production.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
work/system-manual/full-screenshots/05-11-production.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
work/system-manual/full-screenshots/05-12-production.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
work/system-manual/full-screenshots/05-13-production.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
work/system-manual/full-screenshots/05-14-production.png
Normal file
|
After Width: | Height: | Size: 232 KiB |
BIN
work/system-manual/full-screenshots/05-15-production.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
work/system-manual/full-screenshots/05-16-production.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
work/system-manual/full-screenshots/05-17-production.png
Normal file
|
After Width: | Height: | Size: 287 KiB |
BIN
work/system-manual/full-screenshots/05-18-production.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
work/system-manual/full-screenshots/05-19-production.png
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
work/system-manual/full-screenshots/05-20-production.png
Normal file
|
After Width: | Height: | Size: 305 KiB |
BIN
work/system-manual/full-screenshots/05-21-production.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
work/system-manual/full-screenshots/06-01-anomaly.png
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
work/system-manual/full-screenshots/06-02-anomaly.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
work/system-manual/full-screenshots/06-03-anomaly.png
Normal file
|
After Width: | Height: | Size: 259 KiB |
BIN
work/system-manual/full-screenshots/06-04-anomaly.png
Normal file
|
After Width: | Height: | Size: 320 KiB |
BIN
work/system-manual/full-screenshots/07-01-quality.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
work/system-manual/full-screenshots/07-02-quality.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
work/system-manual/full-screenshots/07-03-quality.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
work/system-manual/full-screenshots/07-04-quality.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
BIN
work/system-manual/full-screenshots/07-05-quality.png
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
work/system-manual/full-screenshots/07-06-quality.png
Normal file
|
After Width: | Height: | Size: 104 KiB |
BIN
work/system-manual/full-screenshots/07-07-quality.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
work/system-manual/full-screenshots/07-08-quality.png
Normal file
|
After Width: | Height: | Size: 248 KiB |
BIN
work/system-manual/full-screenshots/07-09-quality.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
work/system-manual/full-screenshots/07-10-quality.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
work/system-manual/full-screenshots/07-11-quality.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
work/system-manual/full-screenshots/07-12-quality.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
work/system-manual/full-screenshots/07-13-quality.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
work/system-manual/full-screenshots/08-01-sales.png
Normal file
|
After Width: | Height: | Size: 295 KiB |
21
work/system-manual/full-screenshots/manifest-recapture.json
Normal file
@@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"sequence": 1,
|
||||
"module": "工艺管理",
|
||||
"title": "生产工艺",
|
||||
"route": "/ProcessManagement/ProcessProduct/index",
|
||||
"source": "ProcessManagement/ProcessProduct/index.vue",
|
||||
"fileName": "03-03-process.png",
|
||||
"status": "captured",
|
||||
"pageState": {
|
||||
"title": "数据分析处理平台",
|
||||
"bodyTextLength": 4843,
|
||||
"hasError": false,
|
||||
"route": "#/ProcessManagement/ProcessProduct/index"
|
||||
},
|
||||
"dialogCapture": {
|
||||
"fileName": "03-03-process-edit-dialog.png",
|
||||
"title": "编辑生产工艺"
|
||||
}
|
||||
}
|
||||
]
|
||||
1109
work/system-manual/full-screenshots/manifest.json
Normal file
30
work/system-manual/locate-pdf-pages.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const { DOMMatrix, ImageData, Path2D } = require('@napi-rs/canvas')
|
||||
|
||||
global.DOMMatrix = DOMMatrix
|
||||
global.ImageData = ImageData
|
||||
global.Path2D = Path2D
|
||||
if (!process.getBuiltinModule) process.getBuiltinModule = name => require(name)
|
||||
|
||||
async function main() {
|
||||
const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs')
|
||||
const root = path.resolve(__dirname, '..', '..')
|
||||
const pdfPath = path.resolve(root, 'doc', '大连元利流体技术有限公司MES系统用户操作手册_V1.1_2026-07-23.pdf')
|
||||
const terms = process.argv.slice(2)
|
||||
const pdf = await pdfjs.getDocument({ data: new Uint8Array(fs.readFileSync(pdfPath)), disableWorker: true }).promise
|
||||
console.log(`PDF pages: ${pdf.numPages}`)
|
||||
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
|
||||
const page = await pdf.getPage(pageNumber)
|
||||
const content = await page.getTextContent()
|
||||
const text = content.items.map(item => item.str).join(' ').replace(/\s+/g, ' ').trim()
|
||||
if (terms.length === 0 || terms.some(term => text.includes(term))) {
|
||||
console.log(`${pageNumber}\t${text.slice(0, 220)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error.stack || error.message)
|
||||
process.exitCode = 1
|
||||
})
|
||||
7915
work/system-manual/manual-action-audit.json
Normal file
2735
work/system-manual/manual-dialog-audit.json
Normal file
BIN
work/system-manual/pdf-page-1.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
work/system-manual/pdf-page-100.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
work/system-manual/pdf-page-101.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
work/system-manual/pdf-page-102.png
Normal file
|
After Width: | Height: | Size: 257 KiB |
BIN
work/system-manual/pdf-page-104.png
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
work/system-manual/pdf-page-105.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
work/system-manual/pdf-page-155.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
work/system-manual/pdf-page-156.png
Normal file
|
After Width: | Height: | Size: 412 KiB |
BIN
work/system-manual/pdf-page-157.png
Normal file
|
After Width: | Height: | Size: 384 KiB |
BIN
work/system-manual/pdf-page-248.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
work/system-manual/pdf-page-252.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
work/system-manual/pdf-page-256.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
work/system-manual/pdf-page-260.png
Normal file
|
After Width: | Height: | Size: 205 KiB |