feat(mes): 添加质检指标条目类型支持并实现报告生成功能
- 新增 MES 质检指标条目类型枚举 (ENTRY/GROUP)
- 在质检指标表单中添加条目类型、所属分组、公式等字段
- 实现质检指标分组项的层级结构和依赖关系控制
- 为来料、过程、出货、退货检验页面添加报告生成功能入口
- 新增报告生成弹窗组件和相关API接口
- 添加部署脚本文件 CHDL.bat 和 KYZN.bat
- 引入 grapesjs 依赖用于报告模板设计
- 更新字典枚举配置以支持报告相关状态类型
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * 对æèæ¬ï¼AI è稿 â ç»å¸è£
é
å¨ï¼assembleQualityCanvasï¼çå¾è¿ä¸è´æ§ã |
| | | * |
| | | * æ ¸å¿æè¨æ¯ãè£
é
å¨ä¸ collectSchema äºä¸ºéè¿ç®ãï¼ |
| | | * è£
é
åè¿ç»å¸ç屿§ï¼ç» attributesToProps è¿ååï¼å¿
é¡»ä¸ resolveQualityProps çç»æå®å
¨ç¸çã |
| | | * ä¸ç¸çå°±æå³çãAI çæ â ä¿å â åæå¼ã伿屿§ææ¹åç±»åã |
| | | * |
| | | * ç¨æ³ï¼npx tsx .qc-conformance/assemble-ts.ts |
| | | */ |
| | | import type { |
| | | QualityComponentDefinition, |
| | | QualityProps, |
| | | } from '../src/components/quality/core/types'; |
| | | |
| | | import { |
| | | assembleQualityCanvas, |
| | | attributesToProps, |
| | | buildQualityComponentCatalog, |
| | | getQualityComponent, |
| | | listQualityComponents, |
| | | registerAllQualityComponents, |
| | | resolveQualityProps, |
| | | } from '../src/components/quality'; |
| | | import { QUALITY_TYPE_ATTR } from '../src/components/quality/core/attrs'; |
| | | |
| | | // 注åè¡¨è¦æ¾å¼æ³¨åææå
容ï¼è®¾è®¡å¨å¨ init æ¶åï¼ãè¿éè¡¥ä¸ï¼æ¨¡æè®¾è®¡å¨å·²åå§åçç¶æã |
| | | registerAllQualityComponents(); |
| | | |
| | | let failures = 0; |
| | | |
| | | function check(label: string, condition: boolean, detail?: string): void { |
| | | if (condition) { |
| | | // eslint-disable-next-line no-console |
| | | console.log(` â ${label}`); |
| | | return; |
| | | } |
| | | failures += 1; |
| | | // eslint-disable-next-line no-console |
| | | console.log(` â ${label}${detail ? `\n ${detail}` : ''}`); |
| | | } |
| | | |
| | | /** åªæ¯è¾å£°æè¿çåæ®µï¼undefined è§å缺å¸ï¼propsToAttributes ä¼è·³è¿å®ï¼ */ |
| | | function comparable( |
| | | definition: QualityComponentDefinition, |
| | | props: QualityProps, |
| | | ): string { |
| | | return definition.propertySchema |
| | | .filter((field) => props[field.key] !== undefined) |
| | | .map((field) => `${field.key}=${JSON.stringify(props[field.key])}`) |
| | | .sort() |
| | | .join('|'); |
| | | } |
| | | |
| | | interface FakeResult { |
| | | grapes: Record<string, unknown>; |
| | | } |
| | | |
| | | function nodesOf(result: FakeResult): Record<string, unknown>[] { |
| | | const pages = result.grapes.pages as { |
| | | component: { components: Record<string, unknown>[] }; |
| | | }[]; |
| | | return pages[0]!.component.components; |
| | | } |
| | | |
| | | function attributesOf(node: Record<string, unknown>): Record<string, string> { |
| | | return node.attributes as Record<string, string>; |
| | | } |
| | | |
| | | // ---------------------------------------------------------------- 1. å
¨ç»ä»¶è£
é
|
| | | |
| | | const definitions = listQualityComponents(); |
| | | |
| | | /** ç» columns å¡«çåæ³åå®ä¹ï¼headerSpans çè·¨åæ°å计è¦ä¸å®çåæ°å¯¹é½ */ |
| | | const FILLER_COLUMNS = '项ç®={{item.itemName}}|å¤å®={{item.resultText}}'; |
| | | |
| | | /** æ¯ä¸ªå段é½å¡ä¸ä¸ªãä¸é»è®¤å¼ä¸åãçåæ³å¼ï¼ç¡®ä¿æµçæ¯è½¬æ¢è䏿¯é»è®¤å¼ */ |
| | | const drafts = definitions.map((definition, index) => { |
| | | const props: Record<string, unknown> = {}; |
| | | definition.propertySchema.forEach((field) => { |
| | | if (field.type === 'boolean') { |
| | | props[field.key] = false; |
| | | } else if (field.type === 'number') { |
| | | props[field.key] = field.key === 'level' ? 3 : 1; |
| | | } else if (field.type === 'enum') { |
| | | const option = field.options?.[field.options.length - 1]; |
| | | if (option) { |
| | | props[field.key] = option.value; |
| | | } |
| | | } else if (field.key === 'columns') { |
| | | // åå®ä¹æ¯æè¯æ³çææ¬å±æ§ï¼åæ é¢=ç»å®è¡¨è¾¾å¼ï¼ï¼å¡ `å¼N-columns` è¿ç§èªç±ææ¬ |
| | | // åªä¼è®©æ ¡éªæ¥ãç¼ºå° =ãï¼æãæ é®é¢é¡¹ãè¿æ¡æè¨å红 |
| | | props[field.key] = FILLER_COLUMNS; |
| | | } else if (field.key === 'fields') { |
| | | // åä¸ï¼è¯æ³æ¯ åæ®µå=ç»å®è¡¨è¾¾å¼ |
| | | props[field.key] = 'æ ·åç¼å·={{report.sampleNo}}'; |
| | | } else if (field.key === 'headerSpans') { |
| | | // 段尾 ^ åçæ°åä¹åå¿
é¡»çäºä¸é¢ columns çåæ°ï¼éæå¡«ä¼è¢«æ ¡éªæ¡ä¸ |
| | | props[field.key] = `å计^${FILLER_COLUMNS.split('|').length}`; |
| | | } else { |
| | | props[field.key] = `å¼${index}-${field.key}`; |
| | | } |
| | | }); |
| | | return { type: definition.type, props }; |
| | | }); |
| | | |
| | | const all = assembleQualityCanvas(drafts); |
| | | const nodes = nodesOf(all); |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('1. å
¨ç»ä»¶è£
é
'); |
| | | check( |
| | | `å
¨é¨å
ç½®ç»ä»¶è£
é
æåï¼è稿 ${drafts.length} / èç¹ ${nodes.length}ï¼`, |
| | | nodes.length === definitions.length && definitions.length === 13, |
| | | ); |
| | | check(`æ è·³è¿é¡¹`, all.skipped.length === 0, JSON.stringify(all.skipped)); |
| | | check(`æ é®é¢é¡¹`, all.problems.length === 0, JSON.stringify(all.problems)); |
| | | |
| | | // ---------------------------------------------------------------- 2. collectSchema çå½ä¸æ¡ä»¶ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('2. collectSchema çå½ä¸æ¡ä»¶'); |
| | | nodes.forEach((node, index) => { |
| | | const attributes = attributesOf(node); |
| | | const label = `第 ${index + 1} 个èç¹ï¼${definitions[index]!.type}ï¼`; |
| | | check(`${label} 带 ${QUALITY_TYPE_ATTR}`, Boolean(attributes[QUALITY_TYPE_ATTR])); |
| | | // data-qc-repeat / repeat-row ç± buildContent çæå¨åµå¥ç tbody/tr ä¸ï¼ |
| | | // 䏿¦è½å°é¡¶å±èç¹ï¼å±æ§é¢æ¿ä¸æ¸²ææçéå¤å±å¼é½ä¼ä¸²å³ |
| | | check( |
| | | `${label} é¡¶å±ä¸å« repeat 屿§`, |
| | | !('data-qc-repeat' in attributes) && !('data-qc-repeat-row' in attributes), |
| | | ); |
| | | }); |
| | | |
| | | // ---------------------------------------------------------------- 3. å¾è¿ä¸è´ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('3. å¾è¿ä¸è´ï¼attributesToProps â assemble == resolveQualityPropsï¼'); |
| | | nodes.forEach((node, index) => { |
| | | const definition = definitions[index]!; |
| | | const attributes = attributesOf(node); |
| | | const fromCanvas = comparable(definition, attributesToProps(definition, attributes)); |
| | | const intended = comparable( |
| | | definition, |
| | | resolveQualityProps(definition, drafts[index]!.props as QualityProps), |
| | | ); |
| | | check(`${definition.type} å¾è¿æ æ`, fromCanvas === intended, `ç»å¸ä¾§[${fromCanvas}] â ææ[${intended}]`); |
| | | }); |
| | | |
| | | // ---------------------------------------------------------------- 4. 屿§ç¡®å®åè¿äºç»å¸ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('4. 屿§ç¡®å®åè¿äºç»å¸ï¼é²æ¢ç¬¬ 3 æ¥é ä¸¤è¾¹åæ¶åé»è®¤å¼èå绿ï¼'); |
| | | drafts.forEach((draft, index) => { |
| | | const definition = definitions[index]!; |
| | | const attributes = attributesOf(nodes[index]!); |
| | | const keys = Object.keys(draft.props); |
| | | const missing = keys.filter((key) => !(`data-qc-${key}` in attributes)); |
| | | check(`${definition.type}ï¼${keys.length} ä¸ªåæ®µï¼`, missing.length === 0, `æªåå
¥ï¼${missing.join(', ')}`); |
| | | }); |
| | | |
| | | // ---------------------------------------------------------------- 5. ç±»å稳å®å¾è¿ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('5. boolean / number / enum çå符串å¾è¿'); |
| | | const boolDefinition = getQualityComponent('QualityTable')!; |
| | | const boolResult = assembleQualityCanvas([ |
| | | { type: 'QualityTable', props: { showIndex: false, showUnit: true, title: 'æ£éªé¡¹ç®' } }, |
| | | ]); |
| | | { |
| | | const attributes = attributesOf(nodesOf(boolResult)[0]!); |
| | | check('boolean false â å符串 "false"', attributes['data-qc-showIndex'] === 'false', attributes['data-qc-showIndex']); |
| | | check('boolean true â å符串 "true"', attributes['data-qc-showUnit'] === 'true'); |
| | | const back = attributesToProps(boolDefinition, attributes); |
| | | check('boolean false è¿å为 false', back.showIndex === false, String(back.showIndex)); |
| | | check('boolean true è¿å为 true', back.showUnit === true, String(back.showUnit)); |
| | | } |
| | | |
| | | const headingDefinition = getQualityComponent('Heading')!; |
| | | const numResult = assembleQualityCanvas([ |
| | | { type: 'Heading', props: { level: 3, align: 'left', text: 'æ¥åæ é¢' } }, |
| | | ]); |
| | | { |
| | | const attributes = attributesOf(nodesOf(numResult)[0]!); |
| | | const back = attributesToProps(headingDefinition, attributes); |
| | | check('enumï¼å符串å¼ï¼åæ ·å¾è¿', back.align === 'left', String(back.align)); |
| | | // level æ¯ãåå¼ä¸ºæ°åçæä¸¾ãï¼ä¸æ¯ number åæ®µï¼å¾è¿åªè½ä¿è¯åé¢éä¸è´ï¼ |
| | | // ç±»åä¸å¾æ¶ææåç¬¦ä¸²ï¼æ ¡éªä¸ buildContent 齿 String()/Number() æ¯è¾ï¼ä¸å½±å渲æ |
| | | check('enumï¼æ°åå¼ï¼å¾è¿ä¸ºçä»·åé¢é', String(back.level) === '3', `${typeof back.level} ${back.level}`); |
| | | } |
| | | |
| | | const dividerDefinition = getQualityComponent('Divider')!; |
| | | const numberResult = assembleQualityCanvas([{ type: 'Divider', props: { thickness: 3 } }]); |
| | | { |
| | | const attributes = attributesOf(nodesOf(numberResult)[0]!); |
| | | check('number 3 â å符串 "3"', attributes['data-qc-thickness'] === '3', attributes['data-qc-thickness']); |
| | | const back = attributesToProps(dividerDefinition, attributes); |
| | | check( |
| | | 'number 3 è¿å为æ°å 3', |
| | | back.thickness === 3 && typeof back.thickness === 'number', |
| | | `${typeof back.thickness} ${back.thickness}`, |
| | | ); |
| | | } |
| | | |
| | | // ---------------------------------------------------------------- 6. åæ°æ®é离 |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('6. æªç¥ç±»åä¸ç»æå屿§'); |
| | | const messy = assembleQualityCanvas([ |
| | | { type: 'QualityTable', props: { title: 'æ£éªé¡¹ç®' } }, |
| | | { type: 'CustomHtml', props: { html: '<script>alert(1)</script>' } }, |
| | | { type: 'Heading', props: { text: { nested: true }, level: [1, 2] } }, |
| | | { type: '', props: {} }, |
| | | ]); |
| | | { |
| | | const messyNodes = nodesOf(messy); |
| | | check('åå
ç´ ä¸å½±åå
¶ä½ï¼4 项ä¸è£
è¿ 2 项', messyNodes.length === 2, `å®é
${messyNodes.length}`); |
| | | check( |
| | | 'æªç¥ç±»åä¸ç©ºç±»åè¿ skipped', |
| | | messy.skipped.length === 2, |
| | | JSON.stringify(messy.skipped.map((item) => item.type)), |
| | | ); |
| | | check( |
| | | 'æªæ³¨åç±»åæ²¡æäº§åºä»»ä½èç¹', |
| | | !messyNodes.some((node) => attributesOf(node)[QUALITY_TYPE_ATTR] === 'CustomHtml'), |
| | | ); |
| | | check('script å
å®¹æ²¡ææ¸è¿ç»å¸æ°æ®', !JSON.stringify(messy.grapes).includes('alert(1)')); |
| | | check('ç»æå屿§åäº§ä¸æ¡ problem', messy.problems.length === 2, JSON.stringify(messy.problems)); |
| | | const headingAttrs = attributesOf(messyNodes[1]!); |
| | | check('å¯¹è±¡å±æ§åè½å°é»è®¤å¼', headingAttrs['data-qc-text'] === 'æ¥åæ é¢', headingAttrs['data-qc-text']); |
| | | check('æ°ç»å±æ§åè½å°é»è®¤å¼', headingAttrs['data-qc-level'] === '2', headingAttrs['data-qc-level']); |
| | | check( |
| | | 'problem ææ¡ææç»ä»¶ä¸å段', |
| | | messy.problems.every((message) => message.includes('æ é¢') && message.includes('ç»æåæ°æ®')), |
| | | JSON.stringify(messy.problems), |
| | | ); |
| | | } |
| | | |
| | | // ---------------------------------------------------------------- 7. å¿
填缺失å¯è¢«æç¤º |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('7. å¿
填缺失'); |
| | | // itemsPath æ¯ QualityTable å¯ä¸çå¿
å¡«åæ®µï¼title 䏿¯ï¼ï¼ç¨å®éªè¯æ ¡éªç¡®å®è¢«è§¦å |
| | | const missing = assembleQualityCanvas([{ type: 'QualityTable', props: { itemsPath: '' } }]); |
| | | check('å¿
填缺失产 problem', missing.problems.length > 0, JSON.stringify(missing.problems)); |
| | | check( |
| | | 'problem ææ¡ææç»ä»¶ä¸å段', |
| | | missing.problems.some((message) => message.includes('æ£éª') && message.includes('æ°æ®æº')), |
| | | JSON.stringify(missing.problems), |
| | | ); |
| | | check('ç»ä»¶æ¬èº«ä»è£
è¿ç»å¸', nodesOf(missing).length === 1); |
| | | const nonRequired = assembleQualityCanvas([{ type: 'QualityTable', props: { title: '' } }]); |
| | | check('éå¿
å¡«ç空ä¸äº§ problem', nonRequired.problems.length === 0, JSON.stringify(nonRequired.problems)); |
| | | |
| | | // ---------------------------------------------------------------- 8. 空è¾å
¥ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('8. 空è¾å
¥'); |
| | | const empty = assembleQualityCanvas([]); |
| | | check('空è¾å
¥ä»è¿åé空 pages', (empty.grapes.pages as unknown[]).length === 1); |
| | | check('空è¾å
¥èç¹ä¸ºç©º', nodesOf(empty).length === 0); |
| | | check('undefined è¾å
¥ä¸æé', nodesOf(assembleQualityCanvas()).length === 0); |
| | | |
| | | // ---------------------------------------------------------------- 9. ç§¯æ¨æ¸
å |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('9. ç§¯æ¨æ¸
å'); |
| | | const catalog = buildQualityComponentCatalog(); |
| | | check('æ¸
åå«å
¨é¨ 13 个ç»ä»¶', catalog.length === 13, `å®é
${catalog.length}`); |
| | | check( |
| | | 'æ¸
å项åªå« category/fields/hint/label/type', |
| | | catalog.every((item) => Object.keys(item).sort().join(',') === 'category,fields,hint,label,type'), |
| | | JSON.stringify(Object.keys(catalog[0]!)), |
| | | ); |
| | | // éåæå¼æ¯ AI 导å
¥è´¨éçå°åºï¼å°å䏿¡ï¼æ¨¡åå°±å¨é£ä¸ªç»ä»¶ä¸éåçç |
| | | const missingHint = listQualityComponents() |
| | | .filter((definition) => !definition.aiHint?.trim()) |
| | | .map((definition) => definition.type); |
| | | check( |
| | | 'æ¯ä¸ªç»ä»¶é½åäºé空 aiHint', |
| | | missingHint.length === 0, |
| | | `缺 hint çç»ä»¶ï¼${missingHint.join(',') || '(æ )'}`, |
| | | ); |
| | | const serialized = JSON.stringify(catalog); |
| | | check( |
| | | 'æ buildContent / icon / validate / defaults æ³æ¼', |
| | | !serialized.includes('buildContent') && |
| | | !serialized.includes('"icon"') && |
| | | !serialized.includes('validate') && |
| | | !serialized.includes('defaults'), |
| | | ); |
| | | const tableEntry = catalog.find((item) => item.type === 'QualityTable')!; |
| | | const itemsPathField = tableEntry.fields.find((field) => field.key === 'itemsPath')!; |
| | | check( |
| | | 'itemsPath æ 记为å¿
å¡«ä¸å¯ç»å®', |
| | | itemsPathField.required === true && itemsPathField.bindable === true, |
| | | ); |
| | | check('éæä¸¾å段ä¸å¸¦ enumOptions', itemsPathField.enumOptions === undefined); |
| | | const alignField = catalog |
| | | .find((item) => item.type === 'Heading')! |
| | | .fields.find((field) => field.key === 'align')!; |
| | | check( |
| | | 'æä¸¾é¡¹åæ¶ç»åº label ä¸ value', |
| | | Boolean(alignField.enumOptions?.every((option) => option.label && option.value !== undefined)), |
| | | JSON.stringify(alignField.enumOptions), |
| | | ); |
| | | // æ¸
åæ¯æ´¾çè§å¾ï¼æ¡æ°ä¸æ³¨å表ä¸è´å³å¯ï¼ç±»åå¿
é¡»é½è½åæ¥å注å表 |
| | | check( |
| | | 'æ¸
åéæ¯ä¸ª type é½è½åæ¥å注å表', |
| | | catalog.every((item) => Boolean(getQualityComponent(item.type))), |
| | | ); |
| | | |
| | | // ---------------------------------------------------------------- 10. å¤è¡è½æ¬¾ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('10. å¤è¡è½æ¬¾ä¸è¢«ææä¸è¡'); |
| | | // AI 伿å件页èä¸è½æ¬¾ç¾ç½²è¡åå¹¶æä¸æ®µå¤è¡ææ¬ãåªè½¬ä¹ä¸æè¡çè¯ï¼ |
| | | // æ¸²ææ¶æ¢è¡ç¬¦ä¼è¢« HTML çç©ºç½æå åæï¼é¡µèä¸å¶é åä¿¡æ¯ç³æä¸çã |
| | | const MULTILINE = 'æ£éªåï¼__________\nå¶é åï¼æ°ç大ç½ç´ 马éè¯å¶åæéå
¬å¸'; |
| | | [ |
| | | { type: 'ReportFooter', props: { text: MULTILINE } }, |
| | | { type: 'Text', props: { text: MULTILINE } }, |
| | | ].forEach((draft) => { |
| | | const node = nodesOf(assembleQualityCanvas([draft]))[0]!; |
| | | const attributes = attributesOf(node); |
| | | const content = String(node.content ?? ''); |
| | | check( |
| | | `${draft.type} ç content ææ¢è¡ææ <br>`, |
| | | content.includes('<br>') && !content.includes('<br>'), |
| | | content, |
| | | ); |
| | | // 屿§éä¿çåå§æ¢è¡ï¼å±æ§é¢æ¿ä¸ç»å®é½æçº¯ææ¬å¤çï¼æè¡åªåç卿¸²æäº§ç©è¿ä¸ä¾§ |
| | | check(`${draft.type} ç屿§ä¿çåå§æ¢è¡`, attributes['data-qc-text'] === MULTILINE, attributes['data-qc-text']); |
| | | }); |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log(failures === 0 ? '\nå
¨é¨éè¿' : `\n${failures} 项失败`); |
| | | process.exit(failures === 0 ? 0 : 1); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * 临æ¶èæ¬ï¼æçå®çç§¯æ¨æ¸
åï¼å« aiHintï¼å¯¼åºæ JSON æä»¶ã |
| | | * |
| | | * åå¨ççç±æ¯ãèè°æ¶åç»åç«¯çæ¸
åå¿
é¡»æ¯çè´§ãï¼ææ²ä¸ä»½æ¸
å廿µï¼æµçæ¯ææ²çé£ä»½ï¼ |
| | | * è䏿¯åç«¯çæ£ä¼ååºå»çé£ä»½ââhint ææ²¡æçç被 catalog å¸¦åºæ¥ï¼åªæè¿æ¡è·¯å¾è½è¯æã |
| | | * ç¨æ³ï¼npx tsx .qc-conformance/dump-catalog.ts <è¾åºè·¯å¾> |
| | | */ |
| | | import { writeFileSync } from 'node:fs'; |
| | | |
| | | import { |
| | | buildQualityComponentCatalog, |
| | | registerAllQualityComponents, |
| | | } from '../src/components/quality'; |
| | | |
| | | registerAllQualityComponents(); |
| | | |
| | | const target = process.argv[2]; |
| | | if (!target) { |
| | | throw new Error('ç¨æ³ï¼npx tsx .qc-conformance/dump-catalog.ts <è¾åºè·¯å¾>'); |
| | | } |
| | | |
| | | const catalog = buildQualityComponentCatalog(); |
| | | writeFileSync(target, JSON.stringify(catalog), 'utf8'); |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log(`ç»ä»¶ ${catalog.length} 项ï¼å¸¦ hint ç ${catalog.filter((c) => c.hint).length} 项 â ${target}`); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!doctype html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="utf-8" /> |
| | | <title>æ¥ææ£éªæ¥å</title> |
| | | <style> |
| | | @page { size: 210mm 297mm; margin: 10mm 10mm 10mm 10mm; } |
| | | * { box-sizing: border-box; } |
| | | body { margin: 0; color: #000; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; font-size: 12px; line-height: 1.5; } |
| | | table { border-collapse: collapse; width: 100%; } |
| | | img { max-width: 100%; } |
| | | tr, td, th { page-break-inside: avoid; } |
| | | |
| | | </style> |
| | | </head> |
| | | <body><div><table><thead><tr><td>æ£éªé¡¹ç®</td><td>æ£æµè¦æ±</td><td>æ åå¼</td><td>宿µå¼</td><td>åä½</td><td>å¤å®</td><td>夿³¨</td></tr></thead><tbody><tbody><tr><td rowspan="5">æ°´å</td><td rowspan="5">w = (m1 - m0) / m à 100%</td></tr><tbody><tr><td>è¯æ ·è´¨é m</td><td></td><td>50.2</td><td>g</td><td></td><td></td></tr><tr><td>è¯æ ·+ç§°éç¶ m1</td><td></td><td>78.9</td><td>g</td><td></td><td></td></tr><tr><td>ç§°éç¶ m0</td><td></td><td>28.7</td><td>g</td><td></td><td></td></tr><tr><td>æ°´åç»æ w</td><td>â¤0.5</td><td>0.32</td><td>%</td><td></td><td></td></tr></tbody></tbody><tbody><tr><td rowspan="2">æ£ä¸éå«é</td><td rowspan="2">纯度 = 100% - æ°´å - é
¸å«é - å
¶å®æè´¨</td></tr><tbody><tr><td>æ£ä¸éå«é /%</td><td>â¥99.5</td><td>99.7</td><td>%</td><td></td><td></td></tr></tbody></tbody></tbody></table></div></body> |
| | | </html> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!doctype html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="utf-8" /> |
| | | <title>æ¥ææ£éªæ¥å</title> |
| | | <style> |
| | | @page { size: 210mm 297mm; margin: 10mm 10mm 10mm 10mm; } |
| | | * { box-sizing: border-box; } |
| | | body { margin: 0; color: #000; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; font-size: 12px; line-height: 1.5; } |
| | | table { border-collapse: collapse; width: 100%; } |
| | | img { max-width: 100%; } |
| | | tr, td, th { page-break-inside: avoid; } |
| | | |
| | | </style> |
| | | </head> |
| | | <body><div><table><thead><tr><td>æ£éªé¡¹ç®</td><td>æ£æµè¦æ±</td><td>å项</td><td>æ åå¼</td><td>宿µå¼</td><td>åä½</td><td>å¤å®</td></tr></thead><tbody><tr><td rowspan="4">æ°´å</td><td rowspan="4">w = (m1 - m0) / m à 100%</td><td>è¯æ ·è´¨é m</td><td></td><td>50.2</td><td>g</td><td>æ å¤å®è§å</td></tr><tr><td rowspan="1" style="display:none"></td><td rowspan="1" style="display:none"></td><td>è¯æ ·+ç§°éç¶ m1</td><td></td><td>78.9</td><td>g</td><td>æ å¤å®è§å</td></tr><tr><td rowspan="1" style="display:none"></td><td rowspan="1" style="display:none"></td><td>ç§°éç¶ m0</td><td></td><td>28.7</td><td>g</td><td>æ å¤å®è§å</td></tr><tr><td rowspan="1" style="display:none"></td><td rowspan="1" style="display:none"></td><td>æ°´åç»æ w</td><td>â¤0.5</td><td>0.32</td><td>%</td><td>æ å¤å®è§å</td></tr><tr><td rowspan="1">æ£ä¸éå«é</td><td rowspan="1">纯度 = 100% - æ°´å - é
¸å«é - å
¶å®æè´¨</td><td>æ£ä¸éå«é /%</td><td>â¥99.5</td><td>99.7</td><td>%</td><td>æ å¤å®è§å</td></tr></tbody></table></div></body> |
| | | </html> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!doctype html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="utf-8" /> |
| | | <title>æ¥ææ£éªæ¥å</title> |
| | | <style> |
| | | @page { size: 210mm 297mm; margin: 10mm 10mm 10mm 10mm; } |
| | | * { box-sizing: border-box; } |
| | | body { margin: 0; color: #000; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; font-size: 12px; line-height: 1.5; } |
| | | table { border-collapse: collapse; width: 100%; } |
| | | img { max-width: 100%; } |
| | | tr, td, th { page-break-inside: avoid; } |
| | | |
| | | </style> |
| | | </head> |
| | | <body><table><thead><tr><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">åºå·</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">æ£éªé¡¹ç®</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">å项</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">æ£æµè¦æ±</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">宿µå¼</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">åä½</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">å¤å®</th></tr></thead><tbody><tr><td style="border:1px solid #d9d9d9;padding:6px 8px">1</td><td style="border:1px solid #d9d9d9;padding:6px 8px">å¤è§</td><td style="border:1px solid #d9d9d9;padding:6px 8px"></td><td style="border:1px solid #d9d9d9;padding:6px 8px">æ åçãæ å¼ç©</td><td style="border:1px solid #d9d9d9;padding:6px 8px">æ åç</td><td style="border:1px solid #d9d9d9;padding:6px 8px"></td><td style="border:1px solid #d9d9d9;padding:6px 8px">æ å¤å®è§å</td></tr><tr><td style="border:1px solid #d9d9d9;padding:6px 8px">2</td><td style="border:1px solid #d9d9d9;padding:6px 8px">é¿åº¦</td><td style="border:1px solid #d9d9d9;padding:6px 8px"></td><td style="border:1px solid #d9d9d9;padding:6px 8px">10±0.5</td><td style="border:1px solid #d9d9d9;padding:6px 8px">10.2</td><td style="border:1px solid #d9d9d9;padding:6px 8px"></td><td style="border:1px solid #d9d9d9;padding:6px 8px">åæ ¼</td></tr></tbody></table></body> |
| | | </html> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <!doctype html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="utf-8" /> |
| | | <title>æ¥ææ£éªæ¥å</title> |
| | | <style> |
| | | @page { size: 210mm 297mm; margin: 10mm 10mm 10mm 10mm; } |
| | | * { box-sizing: border-box; } |
| | | body { margin: 0; color: #000; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; font-size: 12px; line-height: 1.5; } |
| | | table { border-collapse: collapse; width: 100%; } |
| | | img { max-width: 100%; } |
| | | tr, td, th { page-break-inside: avoid; } |
| | | |
| | | </style> |
| | | </head> |
| | | <body><table><thead><tr><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">åºå·</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">æ£éªé¡¹ç®</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">å项</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">æ£æµè¦æ±</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">宿µå¼</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">åä½</th><th style="border:1px solid #d9d9d9;padding:6px 8px;background:#fafafa;font-weight:600">å¤å®</th></tr></thead><tbody><tr><td style="border:1px solid #d9d9d9;padding:6px 8px">1</td><td rowspan="3" style="border:1px solid #d9d9d9;padding:6px 8px">æ°´å</td><td style="border:1px solid #d9d9d9;padding:6px 8px"></td><td style="border:1px solid #d9d9d9;padding:6px 8px">w = (m1 - m0) / m à 100%</td><td style="border:1px solid #d9d9d9;padding:6px 8px"></td><td style="border:1px solid #d9d9d9;padding:6px 8px"></td><td style="border:1px solid #d9d9d9;padding:6px 8px">æ å¤å®è§å</td></tr><tr><td style="border:1px solid #d9d9d9;padding:6px 8px">2</td><td rowspan="1" hidden="hidden" style="border:1px solid #d9d9d9;padding:6px 8px">è¯æ ·è´¨é m</td><td style="border:1px solid #d9d9d9;padding:6px 8px">è¯æ ·è´¨é m</td><td style="border:1px solid #d9d9d9;padding:6px 8px"></td><td style="border:1px solid #d9d9d9;padding:6px 8px">50.2</td><td style="border:1px solid #d9d9d9;padding:6px 8px">g</td><td style="border:1px solid #d9d9d9;padding:6px 8px">æ å¤å®è§å</td></tr><tr><td style="border:1px solid #d9d9d9;padding:6px 8px">3</td><td rowspan="1" hidden="hidden" style="border:1px solid #d9d9d9;padding:6px 8px">æ°´åç»æ w</td><td style="border:1px solid #d9d9d9;padding:6px 8px">æ°´åç»æ w</td><td style="border:1px solid #d9d9d9;padding:6px 8px">â¤0.5</td><td style="border:1px solid #d9d9d9;padding:6px 8px">0.32</td><td style="border:1px solid #d9d9d9;padding:6px 8px">%</td><td style="border:1px solid #d9d9d9;padding:6px 8px">åæ ¼</td></tr><tr><td style="border:1px solid #d9d9d9;padding:6px 8px">4</td><td rowspan="1" style="border:1px solid #d9d9d9;padding:6px 8px">å¤è§</td><td style="border:1px solid #d9d9d9;padding:6px 8px"></td><td style="border:1px solid #d9d9d9;padding:6px 8px">æ åçãæ å¼ç©</td><td style="border:1px solid #d9d9d9;padding:6px 8px">æ åç</td><td style="border:1px solid #d9d9d9;padding:6px 8px"></td><td style="border:1px solid #d9d9d9;padding:6px 8px">æ å¤å®è§å</td></tr></tbody></table></body> |
| | | </html> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * 临æ¶å¯¹æèæ¬ï¼ç¨å端渲æ/è§å弿è·åä¸ä»½ fixtureï¼äº§åºææäº§ç©ï¼ |
| | | * ä¾å端 Java 弿çä¸è´æ§æµè¯é忝坹ãç¨å®å³å ï¼ä¸å
¥åºã |
| | | * |
| | | * ç¨æ³ï¼npx tsx .qc-conformance/render-ts.ts <fixtureDir> <outputDir> |
| | | */ |
| | | import { readFileSync, writeFileSync } from 'node:fs'; |
| | | |
| | | import { createContext } from '../src/components/quality/engine/context'; |
| | | import { evaluateReport } from '../src/components/quality/engine/report-evaluator'; |
| | | import { renderReport } from '../src/components/quality/engine/render'; |
| | | import { evaluateRule } from '../src/components/quality/engine/rule-engine'; |
| | | |
| | | interface ExpressionCase { |
| | | expression: string; |
| | | scope: 'item2' | 'item3' | 'report'; |
| | | } |
| | | |
| | | const EXPRESSIONS: ExpressionCase[] = [ |
| | | { expression: 'report.reportNo', scope: 'report' }, |
| | | { expression: "report.inspectType IN ('IQC', 'IPQC')", scope: 'report' }, |
| | | { expression: "report.inspectType NOT IN ('OQC')", scope: 'report' }, |
| | | { expression: 'PASS_RATE(inspectionItems.result)', scope: 'report' }, |
| | | { expression: 'FAIL_RATE(inspectionItems.result)', scope: 'report' }, |
| | | { expression: 'COUNT(inspectionItems)', scope: 'report' }, |
| | | { expression: 'AVG(inspectionItems.actualValue)', scope: 'report' }, |
| | | { expression: 'ROUND(AVG(inspectionItems.actualValue), 1)', scope: 'report' }, |
| | | { expression: 'STDDEV(inspectionItems.actualValue)', scope: 'report' }, |
| | | { expression: 'SUM(inspectionItems.actualValue)', scope: 'report' }, |
| | | { expression: 'MAX(inspectionItems.actualValue)', scope: 'report' }, |
| | | { expression: 'MIN(inspectionItems.actualValue)', scope: 'report' }, |
| | | { expression: 'ABS(-3.5)', scope: 'report' }, |
| | | { expression: 'CP(inspectionItems.actualValue, 70, 30)', scope: 'report' }, |
| | | { expression: 'CPK(inspectionItems.actualValue, 70, 30)', scope: 'report' }, |
| | | { expression: 'ROUND(10 / 3, 4)', scope: 'report' }, |
| | | { expression: '3 * 2 - 1', scope: 'report' }, |
| | | { expression: "report.reportNo + '-' + report.spec", scope: 'report' }, |
| | | { expression: 'report.total = 3 AND report.passCount = 1', scope: 'report' }, |
| | | { expression: "report.passRate = '33.33%'", scope: 'report' }, |
| | | { expression: 'report.supplierCode = NULL', scope: 'report' }, |
| | | { expression: 'TRUE AND NOT FALSE', scope: 'report' }, |
| | | { expression: '!report.total', scope: 'report' }, |
| | | { expression: '1 / 0', scope: 'report' }, |
| | | { expression: 'FOO(1)', scope: 'report' }, |
| | | { expression: 'report.reportNo >= ', scope: 'report' }, |
| | | { expression: "AVG('abc')", scope: 'report' }, |
| | | { expression: 'item.actualValue >= item.lowerLimit AND item.actualValue <= item.upperLimit', scope: 'item2' }, |
| | | { expression: "item.itemName IN ('é¿åº¦', '硬度')", scope: 'item2' }, |
| | | { expression: "item.itemName NOT IN ('å¤è§')", scope: 'item2' }, |
| | | { expression: '-item.index', scope: 'item2' }, |
| | | { expression: 'item.actualValue = 10.2', scope: 'item2' }, |
| | | { expression: 'item.result', scope: 'item2' }, |
| | | { expression: 'item.actualValue BETWEEN item.lowerLimit AND item.upperLimit', scope: 'item3' }, |
| | | { expression: 'item.actualValue NOT BETWEEN item.lowerLimit AND item.upperLimit', scope: 'item3' }, |
| | | // çå¼å£å¾ï¼è½å½æ°åççä¸å¾æ !== 0 å¤çï¼å符串 "0" æ¯åå¼ï¼ä¸æ¯ JS Boolean() çå£å¾ï¼ |
| | | { expression: '!0', scope: 'report' }, |
| | | { expression: '!1', scope: 'report' }, |
| | | { expression: '!"0"', scope: 'report' }, |
| | | { expression: '!"abc"', scope: 'report' }, |
| | | { expression: '!""', scope: 'report' }, |
| | | { expression: '-0', scope: 'report' }, |
| | | ]; |
| | | |
| | | const fixtureDir = process.argv[2]!; |
| | | const outputDir = process.argv[3]!; |
| | | |
| | | const schema = JSON.parse(readFileSync(`${fixtureDir}/schema.json`, 'utf8')); |
| | | const rawContext = JSON.parse(readFileSync(`${fixtureDir}/context.json`, 'utf8')); |
| | | const rules = schema.rules ?? []; |
| | | const context = createContext(rawContext); |
| | | |
| | | const outcome = renderReport({ context, rules, schema }); |
| | | writeFileSync(`${outputDir}/expected-html.html`, outcome.html, 'utf8'); |
| | | |
| | | const evaluated = evaluateReport(context, rules).context; |
| | | const scopes: Record<string, unknown> = { |
| | | report: { ...evaluated, index: 0 }, |
| | | item2: { ...evaluated, item: evaluated.inspectionItems[1], index: 2 }, |
| | | item3: { ...evaluated, item: evaluated.inspectionItems[2], index: 3 }, |
| | | }; |
| | | |
| | | const results = EXPRESSIONS.map(({ expression, scope }) => { |
| | | try { |
| | | const value = evaluateRule(expression, scopes[scope]); |
| | | const kind = |
| | | value === null || value === undefined ? 'nil' : typeof value === 'object' ? 'object' : typeof value; |
| | | return { expression, scope, kind, text: String(value) }; |
| | | } catch (error) { |
| | | return { expression, scope, error: (error as Error).message }; |
| | | } |
| | | }); |
| | | |
| | | writeFileSync( |
| | | `${outputDir}/expected-rules.json`, |
| | | `${JSON.stringify({ errors: outcome.errors, results }, null, 2)}\n`, |
| | | 'utf8', |
| | | ); |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log( |
| | | `å¤å®åçæ¥ååæ®µï¼${JSON.stringify({ |
| | | result: evaluated.report.result, |
| | | resultText: evaluated.report.resultText, |
| | | total: evaluated.report.total, |
| | | passCount: evaluated.report.passCount, |
| | | failCount: evaluated.report.failCount, |
| | | passRate: evaluated.report.passRate, |
| | | })}`, |
| | | ); |
| | | // eslint-disable-next-line no-console |
| | | console.log(`å项å¤å®ï¼${evaluated.inspectionItems.map((item) => `${item.itemName}=${item.result}`).join(', ')}`); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æ¢éï¼å¯èªå®åçæ£éªè¡¨ï¼GroupedQualityTable.columns / headerSpansï¼ä¸å¯é
åæ®µçæ ·åä¿¡æ¯ |
| | | * ï¼SampleInfo.fieldsï¼ã |
| | | * |
| | | * åªè¯»ï¼ä¸æ¹ä»£ç ãä¸ç¢°æ°æ®åºãç¨æ³ï¼npx tsx .qc-conformance/verify-columns.ts |
| | | * |
| | | * å®ä¸ä»¶äºï¼ |
| | | * 1. **屿§ç¼ºå¸æ¶äº§ç©éåä¸å**ââè¿æ¯æ¬è½®ç第ä¸ä¼å
级ï¼åéåå·²å叿¨¡æ¿ä¸ä¸ªåèé½ä¸è¯¥å¨ã |
| | | * è¿éæ¯å¯¹çæ¯æ´æ®µäº§ç©å符串ç sha256ï¼è䏿¯ãåæ°è¿æ¯ 7ãè¿ç§å¼±æè¨ï¼ |
| | | * åæ°ä¸åä½ style 顺åºåäºã屿§æ¬å®¶äºï¼ä¸æ ·ä¼è®©å·²å叿¥åéåºæ¶åæ ·ã |
| | | * 2. 屿§å¨åºæ¶åæ°ãååºãå并屿§è½ç¹ã两级表头跨åé½å¯¹å¾ä¸ï¼ |
| | | * 3. åéæ¶ validate æ¥å¾åºã䏿¥çæ¯è½æ¹çé£ä¸å¤ï¼ä¸æ¯ç¬¼ç»ä¸å¥ãæ ¼å¼é误ãï¼ã |
| | | * |
| | | * ç´æ¥æå¨çå® buildContent / validate ç产ç©ä¸ï¼ç»æä¸æ¹è¿é就红ï¼ä¸å¿
çæµè§å¨ã |
| | | */ |
| | | import type { QualityProps } from '../src/components/quality/core/types'; |
| | | |
| | | import { createHash } from 'node:crypto'; |
| | | |
| | | import { resolveQualityProps, validateQualityProps } from '../src/components/quality/core/validator'; |
| | | import { buildQualityComponentCatalog } from '../src/components/quality/core/catalog'; |
| | | import { parseColumnSpec } from '../src/components/quality/core/column-spec'; |
| | | import { sampleInfoDefinition } from '../src/components/quality/header/sample-info'; |
| | | import { groupedQualityTableDefinition } from '../src/components/quality/inspection/grouped-quality-table'; |
| | | import { registerAllQualityComponents } from '../src/components/quality'; |
| | | |
| | | const failures: string[] = []; |
| | | |
| | | function expect(label: string, actual: unknown, wanted: unknown): void { |
| | | const ok = actual === wanted; |
| | | if (!ok) { |
| | | failures.push(`${label}ï¼ææ ${JSON.stringify(wanted)}ï¼å®é
${JSON.stringify(actual)}`); |
| | | } |
| | | // eslint-disable-next-line no-console |
| | | console.log(`${ok ? ' ok ' : ' FAIL '} ${label} = ${JSON.stringify(actual)}`); |
| | | } |
| | | |
| | | function expectTrue(label: string, actual: boolean, detail?: string): void { |
| | | if (!actual) { |
| | | failures.push(`${label}${detail ? `ï¼${detail}` : ''}`); |
| | | } |
| | | // eslint-disable-next-line no-console |
| | | console.log(`${actual ? ' ok ' : ' FAIL '} ${label}`); |
| | | } |
| | | |
| | | function sha(text: string): string { |
| | | return createHash('sha256').update(text, 'utf8').digest('hex').slice(0, 16); |
| | | } |
| | | |
| | | /** çå® buildContent ç产ç©ï¼ç»ä»¶äº§åºçæ¯ HTML åç¬¦ä¸²ï¼ */ |
| | | function build( |
| | | definition: typeof groupedQualityTableDefinition | typeof sampleInfoDefinition, |
| | | props?: QualityProps, |
| | | ): string { |
| | | const resolved = resolveQualityProps(definition, props); |
| | | return String(definition.buildContent(resolved).components); |
| | | } |
| | | |
| | | function validate( |
| | | definition: typeof groupedQualityTableDefinition | typeof sampleInfoDefinition, |
| | | props?: QualityProps, |
| | | ): string[] { |
| | | return validateQualityProps(definition, props); |
| | | } |
| | | |
| | | /** è¡¨å¤´æ ¼æ°ï¼`<th` 伿 `<thead` ä¹ç®è¿å»ï¼æä»¥æ°éåæ ç¾ */ |
| | | function headCellCount(html: string): number { |
| | | return (html.match(/<\/th>/g) ?? []).length; |
| | | } |
| | | |
| | | function theadRowCount(html: string): number { |
| | | const thead = html.slice(html.indexOf('<thead'), html.indexOf('</thead>')); |
| | | return (thead.match(/<tr>/g) ?? []).length; |
| | | } |
| | | |
| | | const MERGE_ATTRS = ' rowspan="{{item.group.span}}" hidden="{{item.group.hidden}}"'; |
| | | |
| | | // ------------------------------------------------- 1. ç¼ºå¸ = éåä¸åï¼ç¬¬ä¸ä¼å
çº§ï¼ |
| | | |
| | | // æ¹è¿è½®ä¹åçäº§ç©æçº¹ãåæ°ãstyle 顺åºãå并屿§è½ç¹ï¼ä»»ä½ä¸å¤å¨äºè¿ä¸ª hash é½ä¼åã |
| | | // 乿以å»ç» hash è䏿¯å»ç»æ´æ®µ HTMLï¼æ´æ®µå¤ªé¿ï¼diff æ¶çä¸åºæ¹çæ¯åªä¸æ ¼ã |
| | | // |
| | | // è¿ä¸¤ä¸ªå¼ä¸æ¯ãç
§çç°å¨çå®ç°æä¸éãï¼èæ¯ææ¹åçå®ç°ï¼ä»å·¥ä½åºéè¿ååºæ¥çæ§çæ¬ï¼ |
| | | // 䏿¹åçå®ç°è·å䏿¹å±æ§ãé忝坹ä¹åå䏿¥çï¼12 ç»å±æ§ç»åï¼é»è®¤ / title 空 / |
| | | // åæ¾ç¤ºå¼å
³åç¬ä¸ç»åå
³é / æ¢æ°æ®æºè·¯å¾ / æ¢æ é¢ / æ¯è¡ 1ã3 ç» / å
³è¾¹æ¡ï¼å
¨é¨ sameã |
| | | const GROUPED_TABLE_BASELINE = '8e803b882a62ba70'; |
| | | const SAMPLE_INFO_BASELINE = 'a07da23e176df130'; |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 1. 两个æ°å±æ§ç¼ºå¸ï¼äº§ç©å¿
须䏿¹è¿è½®ä¹åéåä¸è´ ====='); |
| | | { |
| | | // é»è®¤ propsï¼defaults é廿䏿¾ columns / headerSpans / fieldsï¼ |
| | | const table = build(groupedQualityTableDefinition); |
| | | const sample = build(sampleInfoDefinition); |
| | | expect('åç»è¡¨ï¼defaults éæ²¡æ columns', 'columns' in groupedQualityTableDefinition.defaults, false); |
| | | expect('åç»è¡¨ï¼defaults éæ²¡æ headerSpans', 'headerSpans' in groupedQualityTableDefinition.defaults, false); |
| | | expect('æ ·åä¿¡æ¯ï¼defaults éæ²¡æ fields', 'fields' in sampleInfoDefinition.defaults, false); |
| | | expect('åç»è¡¨é»è®¤äº§ç©æçº¹', sha(table), GROUPED_TABLE_BASELINE); |
| | | expect('æ ·åä¿¡æ¯é»è®¤äº§ç©æçº¹', sha(sample), SAMPLE_INFO_BASELINE); |
| | | |
| | | // 屿§å³ä½¿æ¾å¼ä¼ æç©ºä¸²ä¹å¿
é¡»åè½ï¼å±æ§é¢æ¿æ¸
ç©ºä¸æ ¼æ¯å¸¸äºï¼ |
| | | // 空串è¥è¢«å½æã0 åã就伿¸²æåºä¸å¼ 没æåçæ®è¡¨ |
| | | expect('åç»è¡¨ï¼columns ä¼ ç©ºä¸²ä»åºé»è®¤ 7 å', headCellCount(build(groupedQualityTableDefinition, { columns: '' })), 7); |
| | | expect('åç»è¡¨ï¼headerSpans ä¼ ç©ºä¸²ä»åºåå±è¡¨å¤´', theadRowCount(build(groupedQualityTableDefinition, { headerSpans: '' })), 1); |
| | | expect('æ ·åä¿¡æ¯ï¼fields ä¼ ç©ºä¸²ä»åºé»è®¤ 6 项', (build(sampleInfoDefinition, { fields: '' }).match(/<td/g) ?? []).length, 12); |
| | | |
| | | // é»è®¤åçå并屿§ä»åªæå¨ãæ£éªé¡¹ç®ãé£ä¸æ ¼ä¸ |
| | | const cells = table.split('<td'); |
| | | expectTrue('é»è®¤åå«ãå项ãå', table.includes('>å项</th>')); |
| | | expectTrue( |
| | | 'é»è®¤åï¼å¸¦ rowspan çæ ¼åªæ 1 个', |
| | | cells.filter((cell) => cell.includes('rowspan=')).length === 1, |
| | | ); |
| | | expectTrue('é»è®¤åï¼å并屿§å¨ãæ£éªé¡¹ç®ãæ ¼ä¸', Boolean(cells.find((cell) => cell.includes('{{item.itemName}}'))?.includes(MERGE_ATTRS))); |
| | | } |
| | | |
| | | // ------------------------------------------------- 2. columns å¨åºï¼åæ°ãååºãåå¹¶è½ç¹ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 2. columns å¨åºï¼æ´è¡¨æå®æ¥ï¼æ¾ç¤ºå¼å
³ä¸åçæ ====='); |
| | | { |
| | | const custom = |
| | | 'åºå·={{index}}|#æ£éªé¡¹ç®={{item.itemName}}|å项={{item.group.childName}}|' + |
| | | 'æ£æµæ¹æ³={{item.checkMethod}}|æ£æµè¦æ±={{item.requirement}}|宿µå¼={{item.actualValue}}|' + |
| | | 'åä½={{item.unit}}|å¤å®={{item.resultText}}'; |
| | | const html = build(groupedQualityTableDefinition, { columns: custom }); |
| | | |
| | | expect('表头 8 å', headCellCount(html), 8); |
| | | expect('è¡æ¨¡æ¿ 8 æ ¼', (html.match(/<td/g) ?? []).length, 8); |
| | | expectTrue('æ°å¢çãæ£æµæ¹æ³ãå表头å¨åº', html.includes('>æ£æµæ¹æ³</th>')); |
| | | expectTrue('ãæ£æµæ¹æ³ãåå item.checkMethod', html.includes('{{item.checkMethod}}')); |
| | | |
| | | // ååº = 串éç顺åºï¼æ£æµæ¹æ³å¿
须夹å¨ãå项ãä¸ãæ£æµè¦æ±ãä¹é´ |
| | | const heads = [...html.matchAll(/<th[^>]*>([^<]*)<\/th>/g)].map((match) => match[1]); |
| | | expect('表头顺åº', heads.join(','), 'åºå·,æ£éªé¡¹ç®,å项,æ£æµæ¹æ³,æ£æµè¦æ±,宿µå¼,åä½,å¤å®'); |
| | | |
| | | // å并屿§è·ç `#` èµ°ï¼å äºä¸åä¹åå®ä»å¿
é¡»è½å¨ãæ£éªé¡¹ç®ãé£ä¸æ ¼ï¼ä¸åªæä¸æ ¼ |
| | | const cells = html.split('<td'); |
| | | expectTrue('å并屿§è½å¨ # æ è®°çé£ä¸å', Boolean(cells.find((cell) => cell.includes('{{item.itemName}}'))?.includes(MERGE_ATTRS))); |
| | | expect( |
| | | '带å并屿§çæ ¼æ°', |
| | | cells.filter((cell) => cell.includes('rowspan=')).length, |
| | | 1, |
| | | ); |
| | | |
| | | // æ¾ç¤ºå¼å
³è¢« columns è¦çï¼æåºå·å
³æãåèªå·±åååºå·åï¼å°±å¾åºåºå·ï¼ä¸¤é¢ææ¶æ¶ä»¥ columns 为åï¼ |
| | | const withToggleOff = build(groupedQualityTableDefinition, { columns: custom, showIndex: false }); |
| | | expect('å
³æãæ¾ç¤ºåºå·ãä¹ä¸å½±åèªå®ä¹å', headCellCount(withToggleOff), 8); |
| | | |
| | | // é»è®¤å 7 åãè¿é 8 åï¼è¥è§£æè¢«å¿½ç¥ï¼ä¸å®æ¯ 7 |
| | | expectTrue('ç¡®å®æ²¡æåè½å°é»è®¤å', headCellCount(html) !== 7); |
| | | } |
| | | |
| | | // ------------------------------------------------- 3. headerSpans å¨åºï¼ä¸¤çº§è¡¨å¤´ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 3. headerSpans å¨åºï¼ä¸å±è¡¨å¤´ + ä¸å±åå ====='); |
| | | { |
| | | const html = build(groupedQualityTableDefinition, { |
| | | columns: 'åºå·={{index}}|æ£éªé¡¹ç®={{item.itemName}}|å项={{item.group.childName}}|æ£æµè¦æ±={{item.requirement}}|宿µå¼={{item.actualValue}}|åä½={{item.unit}}|å¤å®={{item.resultText}}', |
| | | headerSpans: 'æ£éªç»æ^5|ç»è®º^2', |
| | | }); |
| | | |
| | | expect('thead åæä¸¤è¡', theadRowCount(html), 2); |
| | | expectTrue('ä¸å±ç¬¬ä¸æ ¼è·¨ 5 å', html.includes('colspan="5"')); |
| | | expectTrue('ä¸å±ç¬¬äºæ ¼è·¨ 2 å', html.includes('colspan="2"')); |
| | | expectTrue('ä¸å±è¡¨å¤´æåå¨åº', html.includes('>æ£éªç»æ<') && html.includes('>ç»è®º<')); |
| | | |
| | | // ä¸å± 2 æ ¼ãä¸å± 7 æ ¼ï¼ä¸å±åªå è·¨åæ é¢ï¼ç»ä¸è½æä¸å±çååé¡¶æ |
| | | const theadRows = html |
| | | .slice(html.indexOf('<thead'), html.indexOf('</thead>')) |
| | | .split('<tr>') |
| | | .slice(1); |
| | | expect('ä¸å±è¡¨å¤´ 2 æ ¼', (theadRows[0]?.match(/<\/th>/g) ?? []).length, 2); |
| | | expect('ä¸å±è¡¨å¤´ 7 æ ¼', (theadRows[1]?.match(/<\/th>/g) ?? []).length, 7); |
| | | |
| | | // åªæä¸å±æ¶ä¸è¦å空å¤åºä¸è¡ |
| | | expect('没填 headerSpans 仿¯åå±', theadRowCount(build(groupedQualityTableDefinition)), 1); |
| | | |
| | | // è·¨åæ°åä¸åº ^N æ¶æ 1 åç®ï¼é æ»æ°å
åºï¼parser çæ¢æçº¦å®ï¼ |
| | | const implicit = build(groupedQualityTableDefinition, { |
| | | columns: 'åºå·={{index}}|æ£éªé¡¹ç®={{item.itemName}}|å项={{item.group.childName}}|æ£æµè¦æ±={{item.requirement}}|宿µå¼={{item.actualValue}}|åä½={{item.unit}}|å¤å®={{item.resultText}}', |
| | | headerSpans: 'æ£éªç»æ|ç»è®º', |
| | | }); |
| | | expect('䏿®µåæ 1 åç®ï¼å计 2 â 7 â æ´å±ä¸¢å¼éååå±', theadRowCount(implicit), 1); |
| | | } |
| | | |
| | | // --------------------------------- 3b. 纵ååå¹¶æ ¼ï¼ä¸åèªå·±æ¢æ¯åå忝䏿 ¼ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 3b. 纵ååå¹¶æ ¼ï¼ç»è®ºè¿ç±»åï¼åæè·¨ä¸¤è¡ç䏿 ¼ ====='); |
| | | { |
| | | // ç¾äºåä»¶éæå³ä¸åãç»è®ºãæ¯èªå·±å 满ä¸ä¸ä¸¤å±è¡¨å¤´ç䏿 ¼ï¼æåææ¬éä¸å±é£æ ¼åçãâåä¸ãï¼ã |
| | | // çæ¨¡åä¸ç´ç
§å `ç»è®º^1`ï¼è¥ä¸è®¤è¿ä¸ªå½¢æï¼ãç»è®ºãäºåä¼å¨ä¸ä¸ä¸¤å±åå°ä¸æ¬¡ã |
| | | const vertical = build(groupedQualityTableDefinition, { |
| | | columns: |
| | | 'æ£éªé¡¹ç®={{item.itemName}}|å项={{item.group.childName}}|æ£æµæ¹æ³={{item.checkMethod}}|æ åè¦æ±={{item.standardValue}}|ç»æ={{item.actualValue}}|ç»è®º={{item.resultText}}', |
| | | headerSpans: 'æ£éªç»æ^5|ç»è®º^1', |
| | | }); |
| | | expectTrue('纵ååå¹¶æ ¼åº rowspan="2"', vertical.includes('rowspan="2"')); |
| | | expectTrue('ä¸ååºä¸æ ¼è·¨ä¸åç䏿 ¼', !vertical.includes('colspan="1"')); |
| | | expect('ãç»è®ºãå
¨æåªå°ä¸æ¬¡', (vertical.match(/ç»è®º/g) ?? []).length, 1); |
| | | expect('ä¸å± 2 æ ¼ + ä¸å± 5 æ ¼ = 7', headCellCount(vertical), 7); |
| | | expect('ä»ç¶æ¯ä¸¤è¡è¡¨å¤´', theadRowCount(vertical), 2); |
| | | |
| | | // 夿®æ¯ãæ é¢ä¸è¯¥åååä¸åä¸å·®ãï¼ä¸æ¯ãæå¨æåãï¼ä¸ååå°±èèå®å®æåç»è·¨åèµ° |
| | | const notVertical = build(groupedQualityTableDefinition, { |
| | | columns: 'æ£éªé¡¹ç®={{item.itemName}}|å¤å®={{item.resultText}}', |
| | | headerSpans: 'æ£éªç»æ^1|ç»è®º^1', |
| | | }); |
| | | expectTrue('䏿 ¼ä¸ååä¸åå â ä¸åº rowspan', !notVertical.includes('rowspan="2"')); |
| | | expect('䏿 ¼ä¸ååä¸åå â 两å±å 2 æ ¼', headCellCount(notVertical), 4); |
| | | } |
| | | |
| | | // ------------------------------------------------- 3c. èªå®åä¸è®¸æè¡¨æ ¼æåºçº¸å¼ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 3c. èªå®åçåå
æ ¼å
许é¿ä¸²æè¡ï¼å¦åæ´è¡¨æº¢åºçº¸å¼ ï¼====='); |
| | | { |
| | | // èªå®åæ¿ä¸å°ä»»ä½å®½åº¦å£°æï¼å宽åªè½ç±æå°å
å®¹å®½åº¦åæ¨ï¼è弿¯ {{item.standardValue}} |
| | | // è¿ç±»ä¸å«ç©ºæ ¼çé¿ä¸²ï¼æå°å
å®¹å®½åº¦å°±æ¯æ´ä¸²é¿åº¦ã宿µ 6 åæ¶æ´è¡¨ 810pxï¼ |
| | | // è A4 æ£æå®½åªæ 672pxï¼è¡¨æ ¼æº¢åº 138pxï¼æå³ä¸åè¢«æ¤æ 29px çç«æ¡ |
| | | // ï¼è®¾è®¡å¨éå°±æ¯ãç»è®ºã两个åç«çæå¨çº¸å¼ å¤é¢ï¼ãå overflow-wrap:anywhere åæ´è¡¨ 672pxã |
| | | const custom = build(groupedQualityTableDefinition, { |
| | | columns: |
| | | '#项ç®={{item.itemName}}|å项={{item.group.childName}}|æ£æµæ¹æ³={{item.checkMethod}}|' + |
| | | 'æ åè¦æ±={{item.standardValue}}|ç»æ={{item.actualValue}}|ç»è®º={{item.resultText}}', |
| | | headerSpans: 'æ£éªç»æ^5|ç»è®º^1', |
| | | }); |
| | | const customCells = custom.split('<td').slice(1); |
| | | expect('èªå®ååº 6 个åå
æ ¼', customCells.length, 6); |
| | | expect( |
| | | 'èªå®åçæ ¼é½å¸¦ overflow-wrap:anywhere', |
| | | customCells.filter((cell) => cell.includes('overflow-wrap:anywhere')).length, |
| | | 6, |
| | | ); |
| | | |
| | | // é»è®¤ååè¿æ¥ï¼å®èªå¸¦æå·¥è°è¿ç宽度ï¼åå 䏿¡æè¡ä¼è®©æ²¡å宽度çãæ£éªé¡¹ç®ããå项ã |
| | | // 被æ¤å° 30 ä½åç´ ãè¡é«ä» 59px æ¶¨å° 283pxââ宿µè¿ï¼ä¸å æ¯å 好 |
| | | const fallback = build(groupedQualityTableDefinition); |
| | | expectTrue('é»è®¤åçæ ¼ä¸å¸¦ overflow-wrap', !fallback.includes('overflow-wrap')); |
| | | } |
| | | |
| | | // ------------------------------------------------- 4. åéæ¶æ¥å¾åºãæ¥å¾å |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 4. åéæ¶ validate æ¥çæ¯è½æ¹çé£ä¸å¤ ====='); |
| | | { |
| | | const badSum = validate(groupedQualityTableDefinition, { |
| | | columns: 'åºå·={{index}}|æ£éªé¡¹ç®={{item.itemName}}|å项={{item.group.childName}}|æ£æµè¦æ±={{item.requirement}}|宿µå¼={{item.actualValue}}', |
| | | headerSpans: 'æ£éªç»æ^5|ç»è®º^2', |
| | | }); |
| | | expect('è·¨åæ°ä¸å¹é
æ¥ 1 æ¡', badSum.length, 1); |
| | | expectTrue( |
| | | 'ææ¡åæ¶ç»åºå计ä¸å®é
åæ°', |
| | | badSum[0]?.includes('å计 7') && badSum[0]?.includes('å®é
åæ°ä¸º 5'), |
| | | badSum[0], |
| | | ); |
| | | expectTrue('ææ¡ç»åºå¯æ§è¡å¨ä½', Boolean(badSum[0]?.includes('^')), badSum[0]); |
| | | |
| | | const missingEqual = validate(groupedQualityTableDefinition, { |
| | | columns: 'åºå·={{index}}|æ£éªé¡¹ç®', |
| | | }); |
| | | expect('缺 = æ¥ 1 æ¡', missingEqual.length, 1); |
| | | expectTrue('ææ¡ææ®µå·å®ä½', Boolean(missingEqual[0]?.includes('第 2 段')), missingEqual[0]); |
| | | |
| | | const emptyExpr = validate(groupedQualityTableDefinition, { columns: 'åºå·={{index}}|æ£æµæ¹æ³=' }); |
| | | expectTrue('空çç»å®è¡¨è¾¾å¼è¢«æ¦ä¸', emptyExpr.some((line) => line.includes('第 2 段')), JSON.stringify(emptyExpr)); |
| | | |
| | | // åå®ä¹æéæ¶ä¸åå å è·¨åæ°æ ¡éªï¼é£æ¶åæ°ä¸å¯ä¿¡ï¼æ¥äºåªä¼æäººå¾éçæ¹å带 |
| | | const both = validate(groupedQualityTableDefinition, { |
| | | columns: 'åºå·={{index}}|æ£éªé¡¹ç®', |
| | | headerSpans: 'æ£éªç»æ^5', |
| | | }); |
| | | expect('åå®ä¹é + 表头跨åä¹é â åªæ¥åå®ä¹', both.length, 1); |
| | | expectTrue('æ¥çæ¯åå®ä¹é£æ¡', Boolean(both[0]?.includes('第 2 段')), both[0]); |
| | | |
| | | // ç¨é»è®¤å + 两å±è¡¨å¤´ï¼è·¨åæ°æé»è®¤ç 7 åæ ¡éª |
| | | const defaultOk = validate(groupedQualityTableDefinition, { headerSpans: 'æ£éªç»æ^5|ç»è®º^2' }); |
| | | expect('é»è®¤ 7 å + æ£éªç»æ^5|ç»è®º^2 éè¿', defaultOk.length, 0); |
| | | |
| | | // éèäºæ£æµè¦æ±å´ççå¤å®ï¼åªæå¨ç¨é»è®¤åæ¶æç®æ°ï¼å¡«äº columns æ´è¡¨æ columns æ¥ï¼ |
| | | const toggleClash = validate(groupedQualityTableDefinition, { showRequirement: false }); |
| | | expectTrue('é»è®¤å䏿æ¥ãéèæ£æµè¦æ±å´ä¿çå¤å®ã', toggleClash.length === 1, JSON.stringify(toggleClash)); |
| | | const toggleClashCustom = validate(groupedQualityTableDefinition, { |
| | | showRequirement: false, |
| | | columns: 'åºå·={{index}}|å¤å®={{item.resultText}}', |
| | | }); |
| | | expect('å¡«äº columns å°±ä¸åæ¥é£æ¡è¯¯æ¥', toggleClashCustom.length, 0); |
| | | } |
| | | |
| | | // ------------------------------------------------- 5. æ ·åä¿¡æ¯å段å¯é
|
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 5. SampleInfo.fields ====='); |
| | | { |
| | | const html = build(sampleInfoDefinition, { |
| | | fields: |
| | | 'æ ·åç¼å·={{report.sampleNo}}|产ååç§°={{report.productName}}|è§æ ¼={{report.spec}}|' + |
| | | 'çäº§æ¥æ={{report.produceDate}}|æ¹å·={{report.batchNo}}|åè±åç§={{report.potatoVariety}}|æææ¥æ={{report.expireDate}}', |
| | | pairsPerRow: 2, |
| | | }); |
| | | expect('7 项 à 2 æ ¼ = 14 个 td', (html.match(/<td/g) ?? []).length, 14); |
| | | expect('7 项æ¯è¡ 2 ç» â 4 è¡', (html.match(/<tr>/g) ?? []).length, 4); |
| | | expectTrue('èªå®ä¹å段åå¨åº', html.includes('>åè±åç§<')); |
| | | expectTrue('èªå®ä¹ç»å®å¨åº', html.includes('{{report.potatoVariety}}')); |
| | | |
| | | // æ¯è¡æ¾å ç»ä»ç± pairsPerRow æ§å¶ |
| | | const oneRow = build(sampleInfoDefinition, { fields: 'A={{report.a}}|B={{report.b}}|C={{report.c}}', pairsPerRow: 3 }); |
| | | expect('pairsPerRow=3 â 1 è¡', (oneRow.match(/<tr>/g) ?? []).length, 1); |
| | | expect('pairsPerRow=3 â 6 æ ¼', (oneRow.match(/<td/g) ?? []).length, 6); |
| | | |
| | | // æ«è¡å䏿»¡ä¸ç»æ¶ï¼æå䏿 ¼è¦æ¨ªååæä½ä¸çåã |
| | | // ä¸è¡¥çè¯å³ä¾§ç§åºå ä¸ªåªæè¾¹æ¡çç©ºæ ¼åââåæ®µæ°ä¸æ¯ãæ¯è¡ç»æ°ãæ´æ°åæ¶å¿
ç¶åºç°ï¼ |
| | | // è AI 导å
¥æåä»¶åæ®µå¡« fieldsï¼å¥å¶æ ¹æ¬ä¸ç±æä»¬å®ï¼åä»¶ 5 é¡¹ãæ¯è¡ 2 ç»æ¯æå
¸åçä¸ç§ï¼ã |
| | | const ragged = build(sampleInfoDefinition, { |
| | | fields: |
| | | '产ååç§°={{report.productName}}|è§æ ¼={{report.spec}}|æ¹å·={{report.batchNo}}|' + |
| | | 'æ£éªå={{report.inspector}}|æ£éªæ¥æ={{report.inspectDate}}', |
| | | pairsPerRow: 2, |
| | | }); |
| | | expect('5 项 ÷ 2 â 3 è¡', (ragged.match(/<tr>/g) ?? []).length, 3); |
| | | expect('è¡¥é½ä¸å¢åæ ¼æ°ï¼ä»æ¯ 5 对 = 10 æ ¼ï¼', (ragged.match(/<td/g) ?? []).length, 10); |
| | | expect('åªå¤åº 1 个 colspan', (ragged.match(/colspan=/g) ?? []).length, 1); |
| | | expectTrue('è¡¥é½è½å¨æ«è¡æå䏿 ¼ï¼è·¨ 3 åï¼2 åç©ºä½ + èªå·±ï¼', ragged.includes('colspan="3"'), ragged.slice(-300)); |
| | | const raggedTail = ragged.split('<tr>').pop() ?? ''; |
| | | const tailCells = raggedTail.split('<td').slice(1); |
| | | expect('æ«è¡è¿æ¯ 2 æ ¼ï¼å段å + å¼ï¼', tailCells.length, 2); |
| | | expectTrue( |
| | | 'è¡¥çæ¯ãå¼ãæ ¼è䏿¯ãåæ®µåãæ ¼ï¼è¡¥å°å段åä¸è¿ä¸åç宽度就è·çåï¼åè¡å·¦è¾¹ç¼ä¼éå¼', |
| | | tailCells[0]?.includes('width:90px') && !tailCells[0]?.includes('colspan'), |
| | | tailCells[0], |
| | | ); |
| | | expectTrue( |
| | | 'æ«è¡ä»æ¯ãæ£éªæ¥æ + å®çå¼ãè¿ä¸å¯¹', |
| | | raggedTail.includes('>æ£éªæ¥æ<') && raggedTail.includes('{{report.inspectDate}}'), |
| | | raggedTail, |
| | | ); |
| | | |
| | | // æ´è¡å满æ¶ä¸æ ¼é½ä¸è®¸è¡¥ï¼é»è®¤åæ®µæ£æ¯ 6 项ï¼Ã·2 æ´è¡ï¼ï¼å¤è¡¥ä¸æ ¼å°±ä¼å¨å°ä¸é¢å»ç»çéåæçº¹ |
| | | expectTrue('é»è®¤ 6 项 ÷ 2 å
¨æ¯æ´è¡ï¼æ colspan', !build(sampleInfoDefinition).includes('colspan=')); |
| | | expectTrue( |
| | | 'pairsPerRow=1 æ¶æ¯è¡èªæä¸ç»ï¼æ°¸è¿ä¸è¡¥', |
| | | !build(sampleInfoDefinition, { pairsPerRow: 1 }).includes('colspan='), |
| | | ); |
| | | const raggedThree = build(sampleInfoDefinition, { |
| | | fields: 'A={{report.a}}|B={{report.b}}|C={{report.c}}|D={{report.d}}', |
| | | pairsPerRow: 3, |
| | | }); |
| | | expect('4 项 ÷ 3 â æ«è¡ 1 项ï¼è·¨ 5 åè¡¥é½ï¼3 åç©ºä½ + èªå·±ï¼', (raggedThree.match(/colspan="\d+"/g) ?? []).join(','), 'colspan="5"'); |
| | | |
| | | // åå¹¶æ è®°å¨æ ·åä¿¡æ¯é没ææä¹ï¼å¿
é¡»æç¡®æç»è䏿¯éé»ä¸¢æ |
| | | const mergeRejected = validate(sampleInfoDefinition, { fields: '#æ ·åç¼å·={{report.sampleNo}}' }); |
| | | expect('åæ®µå带 # æ¥ 1 æ¡', mergeRejected.length, 1); |
| | | expectTrue('ææ¡è¯´æ # æ¯åç»è¡¨çç¨æ³', Boolean(mergeRejected[0]?.includes('åç»æ£éªé¡¹è¡¨')), mergeRejected[0]); |
| | | expect('没æ # åéè¿', validate(sampleInfoDefinition, { fields: 'æ ·åç¼å·={{report.sampleNo}}' }).length, 0); |
| | | } |
| | | |
| | | // ------------------------------------------------- 6. 注å
¥ï¼åæ é¢/表达å¼å
转ä¹åè¿ HTML |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 6. æåçåæ é¢ä¸å段åä¸è½æä¸ºæ³¨å
¥ç¹ ====='); |
| | | { |
| | | // æ³¨ææ é¢éä¸è½å¸¦ `=`ï¼`=` æ¯ãæ é¢=表达å¼ãçåé符ï¼å¸¦ `=` çæ é¢å¨è¯æ³ä¸å°±æ¯éæ³ç |
| | | // ï¼è§ parseColumnSpec ç约å®ï¼ï¼æä»¥è¿éç¨ä¸å« `=` çæ ç¾æ¥éªè½¬ä¹ |
| | | const table = build(groupedQualityTableDefinition, { |
| | | columns: 'æ£éªé¡¹ç®={{item.itemName}}|"><script>alert(1)</script>={{item.actualValue}}', |
| | | }); |
| | | expectTrue( |
| | | 'åæ é¢éçæ ç¾è¢«è½¬ä¹', |
| | | table.includes('"><script>alert(1)</script>'), |
| | | table.slice(0, 400), |
| | | ); |
| | | expectTrue('产ç©éæ²¡ææ´»ç script', !table.includes('<script'), table.slice(0, 400)); |
| | | |
| | | const sample = build(sampleInfoDefinition, { fields: '"><script>alert(1)</script>={{report.sampleNo}}' }); |
| | | expectTrue('åæ®µåéçå¼å·ä¸æ ç¾è¢«è½¬ä¹', sample.includes('"><script>'), sample.slice(0, 400)); |
| | | expectTrue('没æéåºå±æ§å¤ç script', !sample.includes('<script>'), sample.slice(0, 400)); |
| | | } |
| | | |
| | | // ------------------------------------------------- 7. è§£æå¨è¾¹çï¼ä¸¤ä¸ªç»ä»¶å
±ç¨çé£ä¸ä»½ï¼ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 7. å
±ç¨è§£æå¨ï¼ç©ºæ®µè·³è¿ã= åªå第ä¸ä¸ªã^ 仿åå ====='); |
| | | { |
| | | expect('é¦å°¾å¤å | 䏿¥é', parseColumnSpec('|A={{a}}|B={{b}}|').errors.length, 0); |
| | | expect('é¦å°¾å¤å | åº 2 å', parseColumnSpec('|A={{a}}|B={{b}}|').items.length, 2); |
| | | expect('ç©ºä¸²åº 0 å', parseColumnSpec(' ').items.length, 0); |
| | | expect('undefined åº 0 å', parseColumnSpec(undefined).items.length, 0); |
| | | // 表达å¼éååºç° = æ¶ä¸è½è¢«åæç¬¬ä¸æ®µ |
| | | expect('= åªå第ä¸ä¸ª', parseColumnSpec('A=1=2').items[0]?.expr, '1=2'); |
| | | expect('# è¢«å¥æå¹¶è®°ä¸ºåå¹¶å', parseColumnSpec('#æ£éªé¡¹ç®={{item.itemName}}').items[0]?.label, 'æ£éªé¡¹ç®'); |
| | | expect('没å # 就䏿¯åå¹¶å', parseColumnSpec('æ£éªé¡¹ç®={{item.itemName}}').items[0]?.merge, false); |
| | | } |
| | | |
| | | // ------------------------------------------------- 8. aiHint / ç§¯æ¨æ¸
åï¼æ¨¡åå¾ç¥éè½å¡«è¿ä¸¤ä¸ªå±æ§ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 8. aiHint è¦è¯´ãåä¸ä¸æ ·å°±ç¨ columnsãï¼è䏿¯ãæ¢ç»ä»¶ã ====='); |
| | | { |
| | | const hint = groupedQualityTableDefinition.aiHint ?? ''; |
| | | expectTrue('hint ç¹åäº columns', hint.includes('columns'), hint); |
| | | expectTrue('hint ç¹åäº headerSpans', hint.includes('headerSpans'), hint); |
| | | expectTrue( |
| | | 'hint 说æ¸
ãæ¢æåå忝䏿 ¼ãçåæä¹æ¥', |
| | | hint.includes('âåä¸') && hint.includes('跨两è¡'), |
| | | hint, |
| | | ); |
| | | // è¿ä¸æ¡æ¯æ¬è½®æè¦ç´§çæªè¾ï¼ä¸ä¸è½®éåä¸ç¨³çæ ¹å å°±æ¯æç¤ºè¯éå¤äºä¸å¤æåå«å¤çæè¿°ï¼ |
| | | // hint è¥æç¤ºãåä¸ä¸è´ â æ¢ä¸ªç»ä»¶ãï¼çäºæå修好çç¨³å®æ§åæ¨åå» |
| | | expectTrue('hint æç¡®ãä¸è¦æ¹ç¨å«çç»ä»¶ã', hint.includes('ä¸è¦æ¹ç¨å«çç»ä»¶'), hint); |
| | | expectTrue('hint ä»ä¿çãå¹³éºä¸å±æ¶ç¨ QualityTableãè¿æ¡è¾¹ç', hint.includes('QualityTable'), hint); |
| | | |
| | | // æ¸
忝 aiHint 䏿°å±æ§è¿æç¤ºè¯çå¯ä¸åºå£ï¼ä¸å¨è¿éåºç°ï¼æ¨¡åå°±ä¸å¯è½å¡«åºæ¥ |
| | | registerAllQualityComponents(); |
| | | const entry = buildQualityComponentCatalog().find((item) => item.type === 'GroupedQualityTable'); |
| | | const keys = (entry?.fields ?? []).map((field) => field.key).join(','); |
| | | expectTrue('columns é propertySchema èªå¨è¿æ¸
å', keys.includes('columns'), keys); |
| | | expectTrue('headerSpans é propertySchema èªå¨è¿æ¸
å', keys.includes('headerSpans'), keys); |
| | | expectTrue('columns 声æä¸º textï¼å±æ§é¢æ¿åºå¤è¡æ¡ï¼', |
| | | (entry?.fields ?? []).find((field) => field.key === 'columns')?.type === 'text', keys); |
| | | expectTrue('columns ä¸å¯ç»å®ï¼æåæ´ä¸²ï¼ä¸èµ°éæ°æ®å段ï¼', |
| | | !(entry?.fields ?? []).find((field) => field.key === 'columns')?.bindable, keys); |
| | | expectTrue('æ¸
å带ä¸äº hint', Boolean(entry?.hint?.includes('columns'))); |
| | | |
| | | const sample = buildQualityComponentCatalog().find((item) => item.type === 'SampleInfo'); |
| | | expectTrue('SampleInfo ç fields è¿äºæ¸
å', |
| | | (sample?.fields ?? []).some((field) => field.key === 'fields')); |
| | | } |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log( |
| | | failures.length === 0 |
| | | ? '\nå
¨é¨éè¿ï¼ç¼ºå¸æ¶éåä¸åï¼å¨åºæ¶åæ°/ååº/åå¹¶/è·¨åé½å¯¹ï¼åéè½å®ä½å°æ®µï¼æåææ¬å·²è½¬ä¹ï¼æ¸
åé带å¾å°æ°å±æ§ã' |
| | | : `\næ ${failures.length} å¤ä¸ç¬¦ï¼\n${failures.map((line) => ` - ${line}`).join('\n')}`, |
| | | ); |
| | | process.exitCode = failures.length === 0 ? 0 : 1; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æ¢éï¼åç»æ£éªé¡¹ï¼ç¶é¡¹ + å项ï¼å¨ç°ææ¸²æå¼æä¸è½è¡¨è¾¾æä»ä¹æ ·ã |
| | | * |
| | | * ç®çåªæä¸ä¸ªï¼æãrowspan åå¹¶åå
æ ¼è¦ä¸è¦å¨å¼æãè¿ä»¶äºä»æ¨æåæå®æµã |
| | | * åªè¯»ï¼ä¸æ¹ä»£ç ãä¸ç¢°æ°æ®åºã |
| | | * |
| | | * å
³é®åæï¼è¯» report-evaluator.ts:237 å¾å°ï¼ï¼å¤å®é¶æ®µä¼æä¸ä¸æé建æ |
| | | * `{ report, inspectionItems }` 两个é®ï¼ä»»ä½èªå®ä¹ç顶屿°ç»é½è¿ä¸äºæ¸²æä½ç¨åã |
| | | * æä»¥æ¬æ¢éåæ¶éªè¯ã顶屿°ç»ä¸å¯ç¨ãä¸ãæå¨æ¯ä¸ªæ£éªé¡¹ä¸çåæ®µå¯ç¨ã两æ¡ã |
| | | * |
| | | * ç¨æ³ï¼npx tsx .qc-conformance/verify-group-repeat.ts |
| | | */ |
| | | import type { RenderNode, RenderSchema } from '../src/components/quality/engine/render'; |
| | | |
| | | import { writeFileSync } from 'node:fs'; |
| | | |
| | | import { createContext } from '../src/components/quality/engine/context'; |
| | | import { renderReport } from '../src/components/quality/engine/render'; |
| | | |
| | | const REPEAT = 'data-qc-repeat'; |
| | | const REPEAT_ROW = 'data-qc-repeat-row'; |
| | | |
| | | const HEADERS = ['æ£éªé¡¹ç®', 'æ£æµè¦æ±', 'æ åå¼', '宿µå¼', 'åä½', 'å¤å®']; |
| | | |
| | | /** |
| | | * æãç» â å项ãæå¹³æä¸è¡ä¸é¡¹çæ£éªé¡¹æ°ç»ï¼ç»ä¿¡æ¯åªè½å¨ç»å¤´è¡ä¸ã |
| | | * è¿æ¯å端è¦äº§åºçå½¢ç¶ï¼ç»å¤´è¡æèªå·±ç itemNameï¼ç»åï¼ï¼æç»è¡ groupName 为空ã |
| | | */ |
| | | const items = [ |
| | | { itemName: 'æ°´å', standardValue: 'w = (m1 - m0) / m à 100%', actualValue: '', unit: '', groupSpan: 5 }, |
| | | { itemName: 'è¯æ ·è´¨é m', standardValue: '', actualValue: '50.2', unit: 'g', groupSpan: 1 }, |
| | | { itemName: 'è¯æ ·+ç§°éç¶ m1', standardValue: '', actualValue: '78.9', unit: 'g', groupSpan: 1 }, |
| | | { itemName: 'ç§°éç¶ m0', standardValue: '', actualValue: '28.7', unit: 'g', groupSpan: 1 }, |
| | | { itemName: 'æ°´åç»æ w', standardValue: 'â¤0.5', actualValue: '0.32', unit: '%', groupSpan: 1 }, |
| | | { itemName: 'æ£ä¸éå«é', standardValue: '纯度 = 100% - æ°´å - é
¸å«é - å
¶å®æè´¨', actualValue: '', unit: '', groupSpan: 2 }, |
| | | { itemName: 'æ£ä¸éå«é /%', standardValue: 'â¥99.5', actualValue: '99.7', unit: '%', groupSpan: 1 }, |
| | | ]; |
| | | |
| | | const BASE_CONTEXT = { |
| | | report: { reportNo: 'QC-2026-0001', reportName: 'æ¥ææ£éªæ¥å', spec: 'M8Ã20', inspectType: 'IQC' }, |
| | | inspectionItems: items, |
| | | }; |
| | | |
| | | const cell = (text: string, attributes?: Record<string, string>): RenderNode => ({ |
| | | type: 'cell', |
| | | attributes, |
| | | content: text, |
| | | }); |
| | | |
| | | const row = (cells: RenderNode[], attributes?: Record<string, string>): RenderNode => ({ |
| | | type: 'row', |
| | | attributes, |
| | | components: cells, |
| | | }); |
| | | |
| | | const tbody = (rows: RenderNode[], attributes?: Record<string, string>): RenderNode => ({ |
| | | type: 'tbody', |
| | | attributes, |
| | | components: rows, |
| | | }); |
| | | |
| | | const table = (headers: string[], bodies: RenderNode[]): RenderNode => ({ |
| | | type: 'table', |
| | | components: [{ type: 'thead', components: [row(headers.map((text) => cell(text)))] }, ...bodies], |
| | | }); |
| | | |
| | | function run( |
| | | label: string, |
| | | component: RenderNode, |
| | | context: Record<string, unknown> = BASE_CONTEXT, |
| | | ): { body: string; html: string; errors: string[] } { |
| | | const schema = { |
| | | grapes: { pages: [{ frames: [{ component: { components: [component] } }] }] }, |
| | | } as unknown as RenderSchema; |
| | | const outcome = renderReport({ context: createContext(context), rules: [], schema }); |
| | | // eslint-disable-next-line no-console |
| | | console.log(`\n========== ${label} ==========`); |
| | | // eslint-disable-next-line no-console |
| | | console.log(outcome.body.replace(/></g, '>\n<')); |
| | | // eslint-disable-next-line no-console |
| | | console.log(`ââ æªè§£æç»å®ï¼${JSON.stringify(outcome.errors)}`); |
| | | return { body: outcome.body, html: outcome.html, errors: outcome.errors }; |
| | | } |
| | | |
| | | // ----------------------------- P1 æå¹³ï¼repeat over inspectionItemsï¼ç»å¤´ä¿¡æ¯æå¨è¡ä¸ |
| | | |
| | | const p1 = run( |
| | | 'P1 æå¹³ï¼repeat inspectionItemsï¼ç»å¤´è¡çãæ£éªé¡¹ç®/æ£æµè¦æ±ãæ¾ç»åä¸å
¬å¼', |
| | | table(HEADERS, [ |
| | | tbody( |
| | | [ |
| | | row( |
| | | [ |
| | | cell('{{item.itemName}}'), |
| | | cell('{{item.standardValue}}'), |
| | | cell('{{item.actualValue}}'), |
| | | cell('{{item.unit}}'), |
| | | cell('{{item.resultText}}'), |
| | | cell('{{item.index}}'), |
| | | ], |
| | | { [REPEAT_ROW]: 'item' }, |
| | | ), |
| | | ], |
| | | { [REPEAT]: 'inspectionItems' }, |
| | | ), |
| | | ]), |
| | | ); |
| | | |
| | | // ----------------------------- P2 åä¸ï¼ä½ç»é¦åå rowspan ç»å® |
| | | |
| | | const p2 = run( |
| | | 'P2 rowspan ç»å®ï¼é¦å rowspan="{{item.groupSpan}}"ï¼ç屿§éçç»å®è½å¦è§£æ', |
| | | table(HEADERS, [ |
| | | tbody( |
| | | [ |
| | | row( |
| | | [ |
| | | cell('{{item.itemName}}', { rowspan: '{{item.groupSpan}}' }), |
| | | cell('{{item.standardValue}}'), |
| | | cell('{{item.actualValue}}'), |
| | | cell('{{item.unit}}'), |
| | | cell('{{item.resultText}}'), |
| | | cell('{{item.index}}'), |
| | | ], |
| | | { [REPEAT_ROW]: 'item' }, |
| | | ), |
| | | ], |
| | | { [REPEAT]: 'inspectionItems' }, |
| | | ), |
| | | ]), |
| | | ); |
| | | |
| | | // ----------------------------- P3 é¡¶å±èªå®ä¹æ°ç»ï¼é¢æä¸ºç©ºï¼è¯æä¸ä¸æè¢«æ¶å£ï¼ |
| | | |
| | | const p3 = run( |
| | | 'P3 é¡¶å±èªå®ä¹æ°ç»ï¼repeat inspectionGroupsï¼é¢ææ¸²æä¸åºä»»ä½è¡ï¼', |
| | | table(HEADERS, [ |
| | | tbody( |
| | | [row([cell('{{g.groupName}}'), cell('{{g.formula}}')], { [REPEAT_ROW]: 'g' })], |
| | | { [REPEAT]: 'inspectionGroups' }, |
| | | ), |
| | | ]), |
| | | ); |
| | | |
| | | // ----------------------------- P4 åµå¥ï¼ç»ä½ä¸ºä¸é¡¹ãå项æå¨ item.children ä¸ |
| | | |
| | | /** æ¯ä¸ªãé¡¹ãæ¯ä¸ä¸ªç»ï¼å项æå¨ children ä¸ï¼ä»èµ° inspectionItems è¿ä¸ä¸ªé¡¶å±æ°ç»ï¼ */ |
| | | const groupedItems = [ |
| | | { |
| | | itemName: 'æ°´å', |
| | | standardValue: 'w = (m1 - m0) / m à 100%', |
| | | span: 5, |
| | | children: items.slice(1, 5), |
| | | }, |
| | | { |
| | | itemName: 'æ£ä¸éå«é', |
| | | standardValue: '纯度 = 100% - æ°´å - é
¸å«é - å
¶å®æè´¨', |
| | | span: 2, |
| | | children: items.slice(6), |
| | | }, |
| | | ]; |
| | | |
| | | const p4 = run( |
| | | 'P4 åµå¥ repeatï¼å¤å±æç»ãç»å
tbody åæ item.children å±å¼ï¼ç»å/å
¬å¼å¸¦ rowspan', |
| | | table([...HEADERS, '夿³¨'], [ |
| | | tbody( |
| | | [ |
| | | { |
| | | type: 'tbody', |
| | | attributes: { [REPEAT_ROW]: 'g' }, |
| | | components: [ |
| | | row([ |
| | | cell('{{g.itemName}}', { rowspan: '{{g.span}}' }), |
| | | cell('{{g.standardValue}}', { rowspan: '{{g.span}}' }), |
| | | ]), |
| | | tbody( |
| | | [ |
| | | row( |
| | | [ |
| | | cell('{{c.itemName}}'), |
| | | cell('{{c.standardValue}}'), |
| | | cell('{{c.actualValue}}'), |
| | | cell('{{c.unit}}'), |
| | | cell('{{c.resultText}}'), |
| | | cell(''), |
| | | ], |
| | | { [REPEAT_ROW]: 'c' }, |
| | | ), |
| | | ], |
| | | { [REPEAT]: 'g.children' }, |
| | | ), |
| | | ], |
| | | }, |
| | | ], |
| | | { [REPEAT]: 'inspectionItems' }, |
| | | ), |
| | | ]), |
| | | { ...BASE_CONTEXT, inspectionItems: groupedItems }, |
| | | ); |
| | | |
| | | // --------- P5 æå¹³ + rowspan + éèéå¤åå
æ ¼ï¼æ°ç»éåªæç宿£éªé¡¹ï¼ç»å¤´è¡é ç»å®æ ·å¼"让ä½" |
| | | |
| | | /** ç»ä¿¡æ¯æå¨æ¯ä¸ªç宿£éªé¡¹ä¸ï¼ç»å¤´åå
æ ¼åªå¨ç»å
第ä¸è¡å¯è§å¹¶åä¸åå¹¶ */ |
| | | const groupedByChild = [ |
| | | { itemName: 'è¯æ ·è´¨é m', actualValue: '50.2', unit: 'g', group: { name: 'æ°´å', formula: 'w = (m1 - m0) / m à 100%', span: 4, cellStyle: '' } }, |
| | | { itemName: 'è¯æ ·+ç§°éç¶ m1', actualValue: '78.9', unit: 'g', group: { name: '', formula: '', span: 1, cellStyle: 'display:none' } }, |
| | | { itemName: 'ç§°éç¶ m0', actualValue: '28.7', unit: 'g', group: { name: '', formula: '', span: 1, cellStyle: 'display:none' } }, |
| | | { itemName: 'æ°´åç»æ w', standardValue: 'â¤0.5', actualValue: '0.32', unit: '%', group: { name: '', formula: '', span: 1, cellStyle: 'display:none' } }, |
| | | { itemName: 'æ£ä¸éå«é /%', standardValue: 'â¥99.5', actualValue: '99.7', unit: '%', group: { name: 'æ£ä¸éå«é', formula: '纯度 = 100% - æ°´å - é
¸å«é - å
¶å®æè´¨', span: 1, cellStyle: '' } }, |
| | | ]; |
| | | |
| | | const p5 = run( |
| | | 'P5 æå¹³ + rowspan + éèåå
æ ¼ï¼æ°ç»åªè£
ç宿£éªé¡¹ï¼ç»å¤´ä¸¤åç±ç»å®æ ·å¼æ§å¶æ¾é', |
| | | table(['æ£éªé¡¹ç®', 'æ£æµè¦æ±', 'å项', 'æ åå¼', '宿µå¼', 'åä½', 'å¤å®'], [ |
| | | tbody( |
| | | [ |
| | | row( |
| | | [ |
| | | { |
| | | type: 'cell', |
| | | content: '{{item.group.name}}', |
| | | attributes: { rowspan: '{{item.group.span}}', style: '{{item.group.cellStyle}}' }, |
| | | }, |
| | | { |
| | | type: 'cell', |
| | | content: '{{item.group.formula}}', |
| | | attributes: { rowspan: '{{item.group.span}}', style: '{{item.group.cellStyle}}' }, |
| | | }, |
| | | cell('{{item.itemName}}'), |
| | | cell('{{item.standardValue}}'), |
| | | cell('{{item.actualValue}}'), |
| | | cell('{{item.unit}}'), |
| | | cell('{{item.resultText}}'), |
| | | ], |
| | | { [REPEAT_ROW]: 'item' }, |
| | | ), |
| | | ], |
| | | { [REPEAT]: 'inspectionItems' }, |
| | | ), |
| | | ]), |
| | | { ...BASE_CONTEXT, inspectionItems: groupedByChild }, |
| | | ); |
| | | |
| | | // ææ¢é产ç©åå°ç£çï¼ä¾æµè§å¨æ ¡éª rowspan ææ²¡æççåå¹¶ãéèåå
æ ¼æ¯å¦è¢«è¡¨æ ¼å¸å±å¿½ç¥ |
| | | writeFileSync('.qc-conformance/group-probe-p4.html', p4.html, 'utf8'); |
| | | writeFileSync('.qc-conformance/group-probe-p5.html', p5.html, 'utf8'); |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n========== ç»è®ºéè§ =========='); |
| | | ( |
| | | [ |
| | | ['P1 æå¹³ repeat inspectionItems', p1], |
| | | ['P2 rowspan ç»å®', p2], |
| | | ['P3 é¡¶å±èªå®ä¹æ°ç»', p3], |
| | | ['P4 åµå¥ repeatï¼ç»å
åæ children å±å¼ï¼', p4], |
| | | ['P5 æå¹³+rowspan+éèåå
æ ¼', p5], |
| | | ] as Array<[string, { body: string; errors: string[] }]> |
| | | ).forEach(([label, result]) => { |
| | | // eslint-disable-next-line no-console |
| | | console.log( |
| | | `${label}: tr æ°=${(result.body.match(/<tr/g) ?? []).length} `< + |
| | | `td æ°=${(result.body.match(/<td/g) ?? []).length} ` + |
| | | `æªè§£æç»å®=${result.errors.length} rowspan åºç°=${result.body.includes('rowspan=')}`, |
| | | ); |
| | | }); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æ¢éï¼åç»æ£éªé¡¹è¡¨ï¼GroupedQualityTableï¼çç宿¸²æäº§ç©éªè¯ã |
| | | * |
| | | * åªè¯»ï¼ä¸æ¹ä»£ç ãä¸ç¢°æ°æ®åºãç¨æ³ï¼npx tsx .qc-conformance/verify-grouped-table.ts |
| | | * |
| | | * å®ä¸ä»¶äºï¼ |
| | | * 1. ç»åæ ¼ãåå¹¶å è¡ãä¸ãå项è¡è®©ä½ãé½ç±æ°æ®ä¸åï¼rowspan / hiddenï¼ââ |
| | | * 渲ææç®ä¸åºæ¥ï¼è·¯å¾å¨æ°ç»ä¸ç»§ç»å屿§ä¼æå
ç´ pluckï¼`group.children.length` åä¸å°å¼ï¼ |
| | | * 2. 让ä½åçè¡ä»è¦å 满 7 åãä¸ä¸²ä½ãè¿æ¯ãéè䏿 ¼ä¼ä¸ä¼ææ´è¡å©ä½æ ¼å左移ä¸åãççé®é¢ï¼ |
| | | * HTML ç»æå±é¢åªè½éªå°ãtd æ°ãï¼çæ£çè¡¨æ ¼å¸å±ç±æµè§å¨å®ï¼æä»¥èæ¬ææ¸²æäº§ç©åå° |
| | | * .qc-conformance/group-table-render.htmlï¼äº¤ç» Playwright 宿µæ¯ä¸æ ¼çåä½ç½®ï¼ |
| | | * 3. ç»å¤´éç¹è¡ä¸è¿ç¨åæ°ï¼æ å¤å®è§åï¼ä¸è½ææ´ä»½æ¥åçç»è®ºåä¸å»ã |
| | | * |
| | | * ç»ä»¶ç»ææè¨ç´æ¥æå¨çå® buildContent ç产ç©ä¸ï¼ç»æä¸æ¹ï¼åæ°ãç»å®è·¯å¾ãå并屿§æ¬å®¶ï¼ï¼ |
| | | * è¿é就红ï¼ä¸å¿
çæµè§å¨ã |
| | | */ |
| | | import type { ReportContextItem } from '../src/components/quality/engine/context'; |
| | | import type { RenderNode, RenderSchema } from '../src/components/quality/engine/render'; |
| | | |
| | | import { writeFileSync } from 'node:fs'; |
| | | |
| | | import { registerAllQualityComponents } from '../src/components/quality'; |
| | | import { assembleQualityCanvas } from '../src/components/quality/core/assembler'; |
| | | import { resolveQualityProps } from '../src/components/quality/core/validator'; |
| | | import { createContext } from '../src/components/quality/engine/context'; |
| | | import { renderReport } from '../src/components/quality/engine/render'; |
| | | import { groupedQualityTableDefinition } from '../src/components/quality/inspection/grouped-quality-table'; |
| | | |
| | | const failures: string[] = []; |
| | | |
| | | // 第 4 èæèç¨¿ç±»åæ¥æ³¨å表è£
é
ï¼æ³¨åè¡¨è¦æ¾å¼æ³¨åææå
容ï¼è®¾è®¡å¨å¨ init æ¶åï¼ |
| | | registerAllQualityComponents(); |
| | | |
| | | function expect(label: string, actual: unknown, wanted: unknown): void { |
| | | const ok = actual === wanted; |
| | | if (!ok) { |
| | | failures.push(`${label}ï¼ææ ${JSON.stringify(wanted)}ï¼å®é
${JSON.stringify(actual)}`); |
| | | } |
| | | // eslint-disable-next-line no-console |
| | | console.log(`${ok ? ' ok ' : ' FAIL '} ${label} = ${JSON.stringify(actual)}`); |
| | | } |
| | | |
| | | function expectTrue(label: string, actual: boolean, detail?: string): void { |
| | | if (!actual) { |
| | | failures.push(`${label}${detail ? `ï¼${detail}` : ''}`); |
| | | } |
| | | // eslint-disable-next-line no-console |
| | | console.log(`${actual ? ' ok ' : ' FAIL '} ${label}`); |
| | | } |
| | | |
| | | /** ç»å®è·¯å¾ä¸å并屿§é½å¿
é¡»è½å¨ä¸èµ·ï¼åæ£å°å«çåå°±æ¯éç */ |
| | | const GROUP_CELL_ATTRS = ' rowspan="{{item.group.span}}" hidden="{{item.group.hidden}}"'; |
| | | |
| | | /** ä¸ç»ä»¶éç CELL_BORDER åå¼ï¼è¿éåªæ±æµè§å¨éåºæ¥çæçä¸å®é
产ç©åå½¢ */ |
| | | const CELL_STYLE = { border: '1px solid #d9d9d9', padding: '6px 8px' }; |
| | | |
| | | const HEAD_CELL_STYLE = { ...CELL_STYLE, background: '#fafafa', 'font-weight': 600 }; |
| | | |
| | | // ---------------------------------------------------------------- 1. ç»ä»¶ç»æï¼çå® buildContentï¼ |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 1. ç»ä»¶ç»æï¼7 åã第 2 æ ¼å¸¦å并屿§ãé夿 è®°è½å¨ tbody/tr ä¸ ====='); |
| | | { |
| | | const content = groupedQualityTableDefinition.buildContent( |
| | | resolveQualityProps(groupedQualityTableDefinition, {}), |
| | | ); |
| | | const html = String(content.components); |
| | | // æ°éåæ ç¾ï¼`<th` 伿 `<thead` ä¹ç®è¿å» |
| | | const headCells = html.match(/<\/th>/g) ?? []; |
| | | const rowCells = html.match(/<td/g) ?? []; |
| | | |
| | | expect('表头 7 å', headCells.length, 7); |
| | | expect('è¡æ¨¡æ¿ 7 å', rowCells.length, 7); |
| | | expectTrue('表头å«ãå项ãå', html.includes('>å项</th>')); |
| | | expectTrue('表头å«ãæ£æµè¦æ±ãå', html.includes('>æ£æµè¦æ±</th>')); |
| | | |
| | | // å并屿§åªè½æå¨ãæ£éªé¡¹ç®ãè¿ä¸æ ¼ä¸ï¼æéæ ¼ = ç»åè·¨éå |
| | | const cells = html.split('<td'); |
| | | const itemNameCell = cells.find((cell) => cell.includes('{{item.itemName}}')); |
| | | expectTrue('ãæ£éªé¡¹ç®ãæ ¼å¸¦ rowspan ä¸ hidden', Boolean(itemNameCell?.includes(GROUP_CELL_ATTRS)), |
| | | itemNameCell?.slice(0, 200)); |
| | | expectTrue( |
| | | 'å并屿§æ²¡æè·å°å«çæ ¼ä¸', |
| | | cells.filter((cell) => cell.includes('rowspan=')).length === 1, |
| | | `带 rowspan çæ ¼æ°=${cells.filter((cell) => cell.includes('rowspan=')).length}`, |
| | | ); |
| | | expectTrue('ãåé¡¹ãæ ¼å item.group.childName', html.includes('{{item.group.childName}}')); |
| | | expectTrue('ãæ£æµè¦æ±ãæ ¼å item.requirement', html.includes('{{item.requirement}}')); |
| | | expectTrue( |
| | | 'é夿 è®°è½å¨ tbody/tr ä¸', |
| | | html.includes('data-qc-repeat="inspectionItems"') && html.includes('data-qc-repeat-row="item"'), |
| | | ); |
| | | } |
| | | |
| | | // ---------------------------------------------------------------- 2. çå®å¼ææ¸²æ |
| | | |
| | | const cell = (text: string, attributes?: Record<string, string>): RenderNode => ({ |
| | | type: 'cell', |
| | | attributes, |
| | | style: CELL_STYLE, |
| | | content: text, |
| | | }); |
| | | |
| | | /** è¡¨å¤´æ ¼ï¼ç»ä»¶äº§åºçæ¯ <th>ï¼GrapesJS è§£ææå¸¦ tagName ç cell */ |
| | | const headCell = (text: string): RenderNode => ({ |
| | | type: 'cell', |
| | | tagName: 'th', |
| | | style: HEAD_CELL_STYLE, |
| | | content: text, |
| | | }); |
| | | |
| | | const row = (cells: RenderNode[], attributes?: Record<string, string>): RenderNode => ({ |
| | | type: 'row', |
| | | attributes, |
| | | components: cells, |
| | | }); |
| | | |
| | | /** ä¸ buildContent 产ç©éæ ¼åæçè¡æ¨¡æ¿ï¼GrapesJS æé£æ®µ HTML è§£ææçæ å°±æ¯è¿ä¸ªå½¢ç¶ï¼ */ |
| | | function tableSchema(): RenderSchema { |
| | | const headers = ['åºå·', 'æ£éªé¡¹ç®', 'å项', 'æ£æµè¦æ±', '宿µå¼', 'åä½', 'å¤å®']; |
| | | return { |
| | | grapes: { |
| | | pages: [ |
| | | { |
| | | frames: [ |
| | | { |
| | | component: { |
| | | type: 'table', |
| | | components: [ |
| | | { |
| | | type: 'thead', |
| | | components: [row(headers.map((text) => headCell(text)))], |
| | | }, |
| | | { |
| | | type: 'tbody', |
| | | attributes: { 'data-qc-repeat': 'inspectionItems' }, |
| | | components: [ |
| | | row( |
| | | [ |
| | | cell('{{index}}'), |
| | | cell('{{item.itemName}}', { |
| | | rowspan: '{{item.group.span}}', |
| | | hidden: '{{item.group.hidden}}', |
| | | }), |
| | | cell('{{item.group.childName}}'), |
| | | cell('{{item.requirement}}'), |
| | | cell('{{item.actualValue}}'), |
| | | cell('{{item.unit}}'), |
| | | cell('{{item.resultText}}'), |
| | | ], |
| | | { 'data-qc-repeat-row': 'item' }, |
| | | ), |
| | | ], |
| | | }, |
| | | ], |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | ], |
| | | }, |
| | | } as unknown as RenderSchema; |
| | | } |
| | | |
| | | type Item = Partial<ReportContextItem> & { itemName: string }; |
| | | |
| | | /** ç»å¤´éç¹è¡ï¼æå®ä¹æ²¡æå®æµå¼ï¼æ£æµè¦æ±æ¾ç»å
¬å¼ */ |
| | | const anchor = (): Item => ({ |
| | | itemName: 'æ°´å', |
| | | requirement: 'w = (m1 - m0) / m à 100%', |
| | | actualValue: '', |
| | | // æ¯ä¸ªæ£éªé¡¹çåæ®µé½è¦é½ï¼åªææ¯ç©ºä¸²ï¼ï¼åæ®µç¼ºå¸æ¶ç»å®ç屿§ä¼è¢«è·³è¿ï¼åå¹¶æ ¼åèé²åºæ¥ |
| | | unit: '', |
| | | group: { childName: '', span: 3, hidden: '' }, |
| | | }); |
| | | |
| | | /** ç»å
å项è¡ï¼èªå·±çååè¿ãå项ãåï¼åå¹¶æ ¼è®©ç»ç»å¤´é£ä¸è¡ */ |
| | | const child = (name: string, actual: string, unit: string): Item => ({ |
| | | itemName: name, |
| | | requirement: '', |
| | | actualValue: actual, |
| | | unit, |
| | | group: { childName: name, span: 1, hidden: 'hidden' }, |
| | | }); |
| | | |
| | | /** ç»å
å项éçæ£å¸¦è§æ ¼ãè½å¤å®ç䏿¡ */ |
| | | const judgedChild = (name: string, actual: string, upper: number, lower: number): Item => ({ |
| | | ...child(name, actual, '%'), |
| | | requirement: `â¤${upper}`, |
| | | upperLimit: upper, |
| | | lowerLimit: lower, |
| | | }); |
| | | |
| | | /** ç¬ç«é¡¹ï¼æ²¡æåç»ï¼ä¸æ ¼é½ä¸åå¹¶ */ |
| | | const standalone = (): Item => ({ |
| | | itemName: 'å¤è§', |
| | | requirement: 'æ åçãæ å¼ç©', |
| | | actualValue: 'æ åç', |
| | | unit: '', |
| | | group: { childName: '', span: 1, hidden: '' }, |
| | | }); |
| | | |
| | | function render(items: Item[], label: string) { |
| | | const result = renderReport({ |
| | | context: createContext({ |
| | | report: { reportNo: 'QC-2026-0001', reportName: 'æ¥ææ£éªæ¥å' }, |
| | | inspectionItems: items as ReportContextItem[], |
| | | }), |
| | | rules: [], |
| | | schema: tableSchema(), |
| | | }); |
| | | writeFileSync(`.qc-conformance/group-table-${label}.html`, result.html, 'utf8'); |
| | | |
| | | const tbody = result.body.slice(result.body.indexOf('<tbody')); |
| | | const rows = tbody.split('<tr').slice(1); |
| | | return { result, rows, tds: rows.map((html) => html.split('<td').slice(1)) }; |
| | | } |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 2. ç»å¤´ + å项 + ç¬ç«é¡¹æ··æï¼åå¹¶æ ¼ãè®©ä½æ ¼ãåæ° ====='); |
| | | { |
| | | // ç»å¤´ 3 è¡ï¼éç¹ + 2 å项ï¼ãç¬ç«é¡¹ 1 è¡ |
| | | const { result, rows, tds } = render( |
| | | [anchor(), child('è¯æ ·è´¨é m', '50.2', 'g'), judgedChild('æ°´åç»æ w', '0.32', 0.5, 0), standalone()], |
| | | 'render', |
| | | ); |
| | | |
| | | expect('å±å¼ 4 è¡', rows.length, 4); |
| | | expectTrue('æ¯è¡é½æ¯ 7 æ ¼', tds.every((cells) => cells.length === 7), |
| | | tds.map((cells) => cells.length).join(',')); |
| | | // é®é¢æ¸
åéåªå
许åºç°ãæ å¤å®è§åãè¿ç±»å¦å®è¯´æï¼åºç°ãç»å®â¦åä¸å°å¼ã就说ææç»å®æ²¡åä¸ |
| | | expectTrue('没æåä¸å°å¼çç»å®', result.errors.every((message) => !message.includes('ç»å®')), |
| | | JSON.stringify(result.errors)); |
| | | expectTrue('æ å¤å®è§åç
§å®ä¸æ¥ï¼ä¸éé»ï¼', |
| | | result.errors.filter((message) => message.includes('没æè§æ ¼ä¸ä¸é乿²¡æå¤å®è§å')).length === 3, |
| | | JSON.stringify(result.errors)); |
| | | |
| | | // ç»å¤´è¡ï¼åå¹¶ 3 è¡ãèªå·±å¯è§ãå
¬å¼å¨èªå·±çãæ£æµè¦æ±ãæ ¼ |
| | | expectTrue('ç»å¤´è¡ rowspan=3', tds[0]![1]!.includes('rowspan="3"'), tds[0]![1]); |
| | | expectTrue('ç»å¤´è¡ä¸éèåå¹¶æ ¼', !tds[0]![1]!.includes('hidden='), tds[0]![1]); |
| | | expectTrue('ç»å¤´ãæ£æµè¦æ±ãæ¾å
¬å¼', tds[0]![3]!.includes('w = (m1 - m0) / m à 100%'), tds[0]![3]); |
| | | expectTrue('ç»å¤´ãåé¡¹ãæ ¼ä¸ºç©º', tds[0]![2]!.startsWith(' style='), tds[0]![2]); |
| | | expectTrue('ç»å¤´åºå·ä¸º 1', tds[0]![0]!.includes('>1<'), tds[0]![0]); |
| | | |
| | | // å项è¡ï¼è®©åºåå¹¶æ ¼ï¼hiddenï¼ï¼åå¹¶è¡æ°åè½ 1 |
| | | tds.slice(1, 3).forEach((cells, offset) => { |
| | | expectTrue(`å项第 ${offset + 1} è¡ rowspan=1`, cells[1]!.includes('rowspan="1"'), cells[1]); |
| | | expectTrue(`å项第 ${offset + 1} è¡è®©åºåå¹¶æ ¼`, cells[1]!.includes('hidden="hidden"'), cells[1]); |
| | | }); |
| | | expectTrue('å项èªå·±çååè¿ãå项ãå', tds[1]![2]!.includes('è¯æ ·è´¨é m'), tds[1]![2]); |
| | | // åé¡¹çæ£æµè¦æ±éè¡ç»ï¼ç»å
¬å¼åªå ç»å¤´é£ä¸æ ¼ï¼å项åæåçè§æ ¼ |
| | | expectTrue('åé¡¹ãæ£æµè¦æ±ã仿¯èªå·±çå¼', tds[2]![3]!.includes('â¤0.5'), tds[2]![3]); |
| | | |
| | | // ç¬ç«é¡¹ï¼å¯è§ãä¸åå¹¶ããå项ãå空 |
| | | expectTrue('ç¬ç«é¡¹ä¸éè', !tds[3]![1]!.includes('hidden='), tds[3]![1]); |
| | | expectTrue('ç¬ç«é¡¹ãåé¡¹ãæ ¼ä¸ºç©º', !tds[3]![2]!.includes('æ ·å') && tds[3]![2]!.includes('</td>'), tds[3]![2]); |
| | | expectTrue('ç¬ç«é¡¹èªå·±çæ£æµè¦æ±å¨åº', tds[3]![3]!.includes('æ åçãæ å¼ç©'), tds[3]![3]); |
| | | |
| | | // å¤å®åï¼ãæ å¤å®è§åãç
§æ ·æ¸²æåºæ¥ï¼ä¸è¢«åç»ç»æåæ |
| | | expectTrue('ç»å¤´éç¹è¡å¤ä¸ºãæ å¤å®è§åã', tds[0]![6]!.includes('æ å¤å®è§å'), tds[0]![6]); |
| | | expectTrue('è¿ç¨åæ°å¤ä¸ºãæ å¤å®è§åã', tds[1]![6]!.includes('æ å¤å®è§å'), tds[1]![6]); |
| | | expectTrue('å¸¦è§æ ¼çå项å¤ä¸ºãåæ ¼ã', tds[2]![6]!.includes('åæ ¼'), tds[2]![6]); |
| | | |
| | | // ç»è®ºä¸è¢«éç¹è¡ä¸è¿ç¨åæ°åä¸å»ï¼å®ä»¬ä¸è®¡å
¥åæ¯ï¼ |
| | | const report = result.context.report; |
| | | expect('report.total', report.total, 4); |
| | | expect('report.passCount', report.passCount, 1); |
| | | expect('report.passRateï¼åæ¯åªç®å¯å¤å®é¡¹ï¼', report.passRate, '100.00%'); |
| | | expect('report.conclusion', report.conclusion, 'åæ ¼'); |
| | | } |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 3. éçº§ï¼æ´ä»½æ°æ®é½æ²¡æåç»ä¿¡æ¯ï¼éåææ®é表è䏿¯å¡æ ====='); |
| | | { |
| | | // çå®å端ä¸ä¼äº§åºè¿ç§å½¢ç¶ï¼æåç»æ¶æ¯ä¸é¡¹é½å¸¦ groupï¼ï¼ä½æ¨¡æ¿å¯è½è¢«å«å¤åæ°æ®ï¼ |
| | | // åæ®µç¼ºå¸æ¶ç»å®ç屿§ä¼è¢«è·³è¿ â åå¹¶æ ¼ä¸è®©ä½æ ¼é½ä¸åºç°ï¼è¡ç
§å¸¸å 满 7 åãå
容ç
§å¸¸æ¸²æ |
| | | const { result, rows, tds } = render( |
| | | [ |
| | | { itemName: 'å¤è§', requirement: 'æ åçãæ å¼ç©', actualValue: 'æ åç', unit: '' }, |
| | | { |
| | | itemName: 'é¿åº¦', |
| | | requirement: '10±0.5', |
| | | actualValue: '10.2', |
| | | unit: '', |
| | | upperLimit: 10.5, |
| | | lowerLimit: 9.5, |
| | | }, |
| | | ], |
| | | 'degrade', |
| | | ); |
| | | |
| | | expect('å±å¼ 2 è¡', rows.length, 2); |
| | | expectTrue('æ¯è¡ä»æ¯ 7 æ ¼', tds.every((cells) => cells.length === 7), |
| | | tds.map((cells) => cells.length).join(',')); |
| | | expectTrue('å并屿§æ´ä¸ªä¸åºç°', rows.every((html) => !html.includes('rowspan=') && !html.includes('hidden=')), |
| | | rows.join('\n')); |
| | | expectTrue('è¡å
容ç
§å¸¸æ¸²æ', tds[1]![1]!.includes('é¿åº¦') && tds[1]![3]!.includes('10±0.5'), tds[1]!.join('|')); |
| | | expectTrue('å¤å®ç
§å¸¸ç®åº', tds[1]![6]!.includes('åæ ¼'), tds[1]![6]); |
| | | expectTrue( |
| | | '缺å£ä»¥é®é¢æ¸
åæ´é²ï¼ä¸éé»ï¼', |
| | | result.errors.some((message) => message.includes('item.group.span')), |
| | | JSON.stringify(result.errors), |
| | | ); |
| | | } |
| | | |
| | | // ------------------------------------------------- 4. AI 导å
¥èµ°çèªå®å |
| | | |
| | | // ------------------------------------------------- 4. AI 导å
¥èµ°çèªå®å |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== 4. AI 导å
¥èµ°èªå®åï¼columns é空ï¼ï¼é¡¹ç®åä»è¦æç»çºµååå¹¶ ====='); |
| | | { |
| | | // 第 1~3 èæçæ¯**é»è®¤å**忝ï¼tableSchema æå·¥åæï¼ï¼è AI 导å
¥ä¸å¾å¡« columnsï¼ |
| | | // èµ°çæ¯ effectiveColumns çå¦ä¸æ¡åæ¯ââ两æ¡åæ¯åèªæ¼ rowï¼å并屿§ææ²¡æè·çèµ°ï¼ |
| | | // é»è®¤å绿ä¸ä»£è¡¨èªå®å绿ãè¿éç¨æ¨¡åçå®äº§åºï¼æ¨¡æ¿ 33 é£ä»½è稿çæåä¸è½®ï¼éåå¤ç°ã |
| | | // |
| | | // åªéªå°ç»æä¸ºæ¢ï¼è£
é
产ç©äº¤ç» GrapesJS è§£æååç屿§æ¯ä¸åä¸å·®çï¼èãspan=N + hidden='' |
| | | // 伿¸²æå¯è§ç rowspan=Nãspan=1 + hidden='hidden' ä¼è®©ä½ãè¿å±è¯ä¹ç±ç¬¬ 2 èè´è´£ââ |
| | | // é£å¥å±æ§å¥çº¦ä¸å®è½å¨åªä¸åæ å
³ï¼ä¸å¿
å¨èªå®åä¸éè·ä¸é渲æã |
| | | const AI_PROPS = { |
| | | title: 'æ£ éª ç» æ', |
| | | itemsPath: 'inspectionItems', |
| | | showRequirement: true, |
| | | showActual: true, |
| | | showUnit: false, |
| | | showResult: true, |
| | | columns: |
| | | '#项ç®={{item.itemName}}|å项={{item.group.childName}}|æ£æµæ¹æ³={{item.checkMethod}}|' + |
| | | 'æ åè¦æ±={{item.standardValue}}|ç»æ={{item.actualValue}}|ç»è®º={{item.resultText}}', |
| | | headerSpans: 'æ£éªç»æ^5|ç»è®º^1', |
| | | }; |
| | | |
| | | /** è£
é
产ç©ç content å¨ node 䏿¯æªè§£æç HTML 串ï¼GrapesJS ææå®ææèç¹æ ï¼ï¼ç´æ¥æä¸²æè¨ */ |
| | | function assembleToHtml(props: Record<string, unknown>): { |
| | | html: string; |
| | | skipped: unknown[]; |
| | | problems: string[]; |
| | | } { |
| | | const assembled = assembleQualityCanvas([{ type: 'GroupedQualityTable', props }]); |
| | | const page = (assembled.grapes.pages as Array<{ component: { components: unknown[] } }>)[0]!; |
| | | const node = page.component.components[0] as { components: string }; |
| | | return { html: node.components, skipped: assembled.skipped, problems: assembled.problems }; |
| | | } |
| | | |
| | | const { html, skipped, problems } = assembleToHtml(AI_PROPS); |
| | | |
| | | expectTrue('è稿è£
é
æ è·³è¿', skipped.length === 0, JSON.stringify(skipped)); |
| | | expectTrue('è稿è£
é
æ é®é¢é¡¹', problems.length === 0, JSON.stringify(problems)); |
| | | expectTrue('é夿 è®°è½å¨ tbody/tr ä¸', html.includes('<tbody data-qc-repeat="inspectionItems">') && |
| | | html.includes('<tr data-qc-repeat-row="item">'), html.slice(0, 200)); |
| | | |
| | | // è¡æ¨¡æ¿æ columns åº 6 æ ¼ï¼ä¸ç¬¬ä¸æ ¼å°±æ¯ã项ç®ãå |
| | | const cells = html.split('<td').slice(1); |
| | | expect('è¡æ¨¡æ¿ 6 æ ¼ï¼ç
§ columns æ¥ï¼', cells.length, 6); |
| | | expectTrue('第 1 æ ¼ç» item.itemName', cells[0]!.includes('{{item.itemName}}'), cells[0]); |
| | | |
| | | // ã#ãé£ä¸æ¡ææ¯åå¹¶åï¼ç»åæ ¼èªå¸¦ span ä¸ hidden 两个ç»å®ï¼ç»å
å
¶ä½è¡é å®è®©ä½ã |
| | | // è¿æ£æ¯ãåºä»¶æ¶å±çº§åå¹¶ãçå
¨é¨æºå¶ââå°äºè¿è¡å±æ§ï¼é¡¹ç®å就伿¯è¡é夿ä¸éç»å |
| | | expectTrue( |
| | | 'ã#项ç®ãæ ¼å¸¦ rowspan ä¸ hidden ç»å®', |
| | | cells[0]!.includes('rowspan="{{item.group.span}}"') && cells[0]!.includes('hidden="{{item.group.hidden}}"'), |
| | | cells[0], |
| | | ); |
| | | expect( |
| | | 'å并屿§åªè½å¨è¿ä¸æ ¼', |
| | | cells.filter((cell) => cell.includes('rowspan=')).length, |
| | | 1, |
| | | ); |
| | | |
| | | // 表头ï¼ä¸å±ãæ£éªç»æã横跨 5 åï¼ãç»è®ºãèªå·±è·¨ä½ä¸ä¸ä¸¤å± â ä¸å±ä¸åéå¤å°ä¸é |
| | | expectTrue('ä¸å±ãæ£éªç»æãcolspan=5', html.includes('colspan="5"') && html.includes('>æ£éªç»æ</th>'), html.slice(0, 400)); |
| | | expectTrue('ãç»è®ºã纵å跨两å±', /<th rowspan="2"[^>]*>ç»è®º<\/th>/.test(html), html.slice(0, 400)); |
| | | const lowerHeads = html.split('<tr>')[2]?.split('<th').length ?? 0; |
| | | expect('ä¸å±è¡¨å¤´ 5 åï¼ç»è®ºå·²è¢«ä¸å±å¹¶èµ°ï¼', lowerHeads - 1, 5); |
| | | |
| | | // 廿 # 就该ä¸åå¹¶ ââ 项ç®åæ¯è¡é½é夿ä¸éç»åï¼æ£æ¯ãæç¹ä¸ãçé£ç§çå¼ã |
| | | // æè¨å®ç¡®å®åäºï¼é²æ¢ # è¢«å½æè£
é¥°ãææ é½ä¸å½±åç»æ |
| | | const plain = assembleToHtml({ ...AI_PROPS, columns: AI_PROPS.columns.replace('#项ç®=', '项ç®=') }); |
| | | expectTrue('廿 # åä¸åæ rowspan / hidden ç»å®', |
| | | !plain.html.includes('rowspan="{{') && !plain.html.includes('hidden="{{'), plain.html.slice(0, 400)); |
| | | expectTrue('廿 # ååæ°ä¸ç»å®é½ä¸åï¼åªå°äºåå¹¶ï¼', |
| | | plain.html.split('<td').length === 7 && plain.html.includes('{{item.itemName}}'), plain.html.slice(0, 200)); |
| | | } |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log( |
| | | failures.length === 0 |
| | | ? '\nå
¨é¨éè¿ï¼ç»ååå¹¶ä¸å项让ä½é½ç±æ°æ®ä¸åï¼åæ°ç¨³å®å¨ 7ï¼æ å¤å®è§åä¸å½±åç»è®ºã' |
| | | : `\næ ${failures.length} å¤ä¸ç¬¦ï¼\n${failures.map((line) => ` - ${line}`).join('\n')}`, |
| | | ); |
| | | process.exitCode = failures.length === 0 ? 0 : 1; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æ¢éï¼ç¡®è®¤å端å¤å®å¨æãæ å¤å®è§åãåãå¾
å¤å®ãåå¼ï¼ä¸åè
ä¸è®¡å
¥åæ ¼ç䏿¥åç»è®ºã |
| | | * |
| | | * èæ¯ï¼æ¥åé常æãè¯æ ·è´¨é mããç§°éç¶ m0ãè¿ç±»åªä¾å
¬å¼åæ°çè¿ç¨åæ°ï¼å®ä»¬æ²¡æè§æ ¼ã |
| | | * 乿²¡æè§åï¼å±äºãæ ä»å¤å®ããè¥ç
§å¸¸ç®è¿åæ¯ï¼ä¸ä»½æ¬æ¥å
¨åæ ¼çæ¥åä¼è¢«è¿äºè¡åæãå¾
å¤å®ãã |
| | | * |
| | | * åªè¯»ï¼ä¸æ¹ä»£ç ãä¸ç¢°æ°æ®åºãç¨æ³ï¼npx tsx .qc-conformance/verify-undecidable.ts |
| | | */ |
| | | import type { ReportContextItem } from '../src/components/quality/engine/context'; |
| | | |
| | | import { createContext } from '../src/components/quality/engine/context'; |
| | | import { evaluateReport } from '../src/components/quality/engine/report-evaluator'; |
| | | |
| | | type Item = Partial<ReportContextItem> & { itemName: string }; |
| | | |
| | | const base = { reportNo: 'QC-2026-0001', reportName: 'æ¥ææ£éªæ¥å' }; |
| | | |
| | | function evaluate(items: Item[], rules: unknown[] = []) { |
| | | const context = createContext({ report: base, inspectionItems: items as ReportContextItem[] }); |
| | | return evaluateReport(context, rules as never); |
| | | } |
| | | |
| | | /** 䏿¡å¸¦è§æ ¼ä¸ä¸éç项ï¼ç¨æ¥å½ãæ£å¸¸å¯å¤å®ãç对ç
§ç» */ |
| | | const withLimits = (name: string, actual: string, upper: number, lower: number): Item => ({ |
| | | itemName: name, |
| | | actualValue: actual, |
| | | upperLimit: upper, |
| | | lowerLimit: lower, |
| | | }); |
| | | |
| | | /** 䏿¡åªä¾å
¬å¼åæ°çè¿ç¨åæ°ï¼æ¢æ²¡æä¸ä¸éã乿²¡æèªå¸¦ç»æ */ |
| | | const processParam = (name: string, actual: string): Item => ({ itemName: name, actualValue: actual }); |
| | | |
| | | const failures: string[] = []; |
| | | |
| | | function expect(label: string, actual: unknown, wanted: unknown) { |
| | | const ok = actual === wanted; |
| | | if (!ok) { |
| | | failures.push(`${label}ï¼ææ ${JSON.stringify(wanted)}ï¼å®é
${JSON.stringify(actual)}`); |
| | | } |
| | | // eslint-disable-next-line no-console |
| | | console.log(`${ok ? ' ok ' : ' FAIL '} ${label} = ${JSON.stringify(actual)}`); |
| | | } |
| | | |
| | | // --------- åºæ¯ 1ï¼è¿ç¨åæ° + å¯å¤å®é¡¹æ··æï¼æ¥åä»ç¶åºå½ç»åºãåæ ¼ã |
| | | { |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== åºæ¯ 1ï¼è¯æ ·è´¨é mï¼è¿ç¨åæ°ï¼ä¸è¯¥æå
¨åæ ¼çæ¥ååæå¾
å¤å® ====='); |
| | | const outcome = evaluate([withLimits('é¿åº¦', '10.2', 10.5, 9.5), processParam('è¯æ ·è´¨é m', '50.2')]); |
| | | const report = outcome.context.report; |
| | | const items = outcome.context.inspectionItems; |
| | | |
| | | expect('é¿åº¦.resultText', items[0].resultText, 'åæ ¼'); |
| | | expect('è¯æ ·è´¨é m.resultText', items[1].resultText, 'æ å¤å®è§å'); |
| | | expect('è¯æ ·è´¨é m.result', items[1].result, ''); |
| | | expect('report.totalï¼çå®é¡¹æ°ï¼', report.total, 2); |
| | | expect('report.passCount', report.passCount, 1); |
| | | expect('report.failCount', report.failCount, 0); |
| | | expect('report.passRateï¼åæ¯åªç®å¯å¤å®é¡¹ï¼', report.passRate, '100.00%'); |
| | | expect('report.result', report.result, 'PASS'); |
| | | expect('report.conclusion', report.conclusion, 'åæ ¼'); |
| | | // eslint-disable-next-line no-console |
| | | console.log(` é®é¢æ¸
åï¼${JSON.stringify(outcome.errors)}`); |
| | | } |
| | | |
| | | // --------- åºæ¯ 2ï¼ä¸é¡¹é½å¤ä¸äºæ¶ä¸ç¡¬ç»ç»è®º |
| | | { |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== åºæ¯ 2ï¼å
¨é¨æ¯æ å¤å®è§å项ï¼åæ ¼çç空ãä¸ç»ç»è®º ====='); |
| | | const outcome = evaluate([processParam('è¯æ ·è´¨é m', '50.2'), processParam('ç§°éç¶ m0', '28.7')]); |
| | | const report = outcome.context.report; |
| | | |
| | | expect('report.total', report.total, 2); |
| | | expect('report.passRate', report.passRate, ''); |
| | | expect('report.result', report.result, ''); |
| | | expect('report.conclusion', report.conclusion, 'å¾
å¤å®'); |
| | | } |
| | | |
| | | // --------- åºæ¯ 3ï¼å¾
å¤å®ï¼è§åè·æï¼ä»çå¨åæ¯éï¼ä»åä½åæ ¼ç»è®º |
| | | { |
| | | // eslint-disable-next-line no-console |
| | | console.log('\n===== åºæ¯ 3ï¼å¾
å¤å®ï¼è§åè·æï¼ä»è®¡å
¥åæ¯ãä»ä¸ç»åæ ¼ç»è®º ====='); |
| | | const outcome = evaluate( |
| | | [withLimits('é¿åº¦', '10.2', 10.5, 9.5), withLimits('宽度', '4.9', 5.0, 4.0), processParam('å¤è§', 'æ åç')], |
| | | // ãæ åçã䏿¯æ°åï¼ABS å¨å®èº«ä¸ç®ä¸åºæ¥ â 该项å¾
å¤å® |
| | | [{ id: 'r1', name: '宿µå¼å¯ç®', scope: 'item', expression: 'ABS(item.actualValue) >= 0' }], |
| | | ); |
| | | const report = outcome.context.report; |
| | | const items = outcome.context.inspectionItems; |
| | | |
| | | expect('é¿åº¦.resultText', items[0].resultText, 'åæ ¼'); |
| | | expect('å¤è§.resultText', items[2].resultText, 'å¾
å¤å®'); |
| | | expect('report.total', report.total, 3); |
| | | expect('report.passCount', report.passCount, 2); |
| | | expect('report.passRateï¼å¾
å¤å®é¡¹çå¨åæ¯éï¼', report.passRate, '66.67%'); |
| | | expect('report.resultï¼æ²¡éªè¿ä¸ç»åæ ¼ï¼', report.result, ''); |
| | | expect('report.conclusion', report.conclusion, 'å¾
å¤å®'); |
| | | } |
| | | |
| | | // eslint-disable-next-line no-console |
| | | console.log( |
| | | failures.length === 0 |
| | | ? '\nå
¨é¨éè¿ï¼æ å¤å®è§åä¸è®¡å
¥åæ¯ä¸ç»è®ºï¼å¾
å¤å®ä»è®¡å
¥åæ¯å¹¶åä½åæ ¼ç»è®ºã' |
| | | : `\næ ${failures.length} å¤ä¸ç¬¦ï¼\n${failures.map((line) => ` - ${line}`).join('\n')}`, |
| | | ); |
| | | process.exitCode = failures.length === 0 ? 0 : 1; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=CHDL" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=36.213.150.237" |
| | | set "REMOTE_PORT=65022" |
| | | |
| | | set "PASSWORD=Sh@1008600" |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/data/vue/mom/chdl/dist" |
| | | |
| | | set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3" |
| | | |
| | | set "RETRY_COUNT=3" |
| | | |
| | | set "SSHPASS_DIR=%~dp0bin" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Start |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo Vue Dist Auto Deploy |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_USER%@%REMOTE_HOST% |
| | | echo Port : %REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check sshpass |
| | | REM ============================================================ |
| | | |
| | | echo [1/7] Checking sshpass... |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | ) |
| | | |
| | | where sshpass >nul 2>&1 |
| | | |
| | | if not errorlevel 1 ( |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [WARN] sshpass not found. |
| | | |
| | | if not exist "%SSHPASS_DIR%" ( |
| | | mkdir "%SSHPASS_DIR%" >nul 2>&1 |
| | | ) |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [INFO] Downloading sshpass... |
| | | |
| | | powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to download sshpass. |
| | | echo [ERROR] Please install sshpass manually. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | |
| | | echo [OK] sshpass installed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check pnpm |
| | | REM ============================================================ |
| | | |
| | | :CHECK_PNPM |
| | | |
| | | echo. |
| | | echo [2/7] Checking pnpm... |
| | | |
| | | where pnpm >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm not found. |
| | | echo [HINT] npm install -g pnpm |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call pnpm --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [3/7] Checking Node.js... |
| | | |
| | | where node >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js not found. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call node --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node_modules |
| | | REM ============================================================ |
| | | |
| | | if not exist "node_modules" ( |
| | | echo. |
| | | echo [INFO] node_modules not found. |
| | | echo [INFO] Running pnpm install... |
| | | |
| | | call pnpm install |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm install failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] pnpm install completed. |
| | | ) else ( |
| | | echo [OK] node_modules exists. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Build |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [4/7] Building project... |
| | | |
| | | if exist "%ZIP_FILE%" ( |
| | | echo. |
| | | echo [WARN] %ZIP_FILE% already exists. |
| | | set "REBUILD=" |
| | | set /p "REBUILD=Rebuild? Y/N (default Y): " |
| | | |
| | | if /I "!REBUILD!"=="N" ( |
| | | echo [INFO] Skip build. |
| | | goto CHECK_ZIP |
| | | ) |
| | | ) |
| | | |
| | | echo [INFO] Company: %COMPANY% |
| | | echo [INFO] Command: pnpm run build |
| | | echo. |
| | | |
| | | set "VITE_COMPANY=%COMPANY%" |
| | | |
| | | call pnpm run build |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Build failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Build completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check ZIP |
| | | REM ============================================================ |
| | | |
| | | :CHECK_ZIP |
| | | |
| | | echo. |
| | | echo [5/7] Checking package... |
| | | |
| | | if not exist "%ZIP_FILE%" ( |
| | | echo [ERROR] %ZIP_FILE% not found. |
| | | echo [HINT] Please check whether your build generates dist.zip. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF" |
| | | |
| | | set /a ZIP_MB=ZIP_BYTES/1024/1024 |
| | | |
| | | echo [OK] Package found. |
| | | echo [INFO] File size: !ZIP_BYTES! bytes |
| | | echo [INFO] File size: !ZIP_MB! MB |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Set sshpass password |
| | | REM ============================================================ |
| | | |
| | | set "SSHPASS=%PASSWORD%" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test SSH |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [6/7] Testing SSH connection... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] SSH connection failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] SSH connection successful. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Create remote directory |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Creating remote directory... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to create remote directory. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote directory ready. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Upload temporary file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Uploading package... |
| | | |
| | | set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading" |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'" |
| | | |
| | | sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Upload failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Upload completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check remote file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking remote package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Remote package does not exist. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote package exists. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Compare file size |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking package size... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'" |
| | | |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Rename temporary file |
| | | REM ============================================================ |
| | | |
| | | echo [INFO] Activating package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to activate package. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Package activated. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check unzip |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking unzip... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] unzip is not installed on server. |
| | | echo. |
| | | echo CentOS/RHEL: |
| | | echo yum install -y unzip |
| | | echo. |
| | | echo Ubuntu/Debian: |
| | | echo apt install -y unzip |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] unzip found. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Testing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] ZIP test failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] ZIP test passed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Extract |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Extracting package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Remote unzip failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Extract completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Remove ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Removing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [WARN] Failed to remove remote ZIP. |
| | | ) else ( |
| | | echo [OK] Remote ZIP removed. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Success |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY SUCCESS |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_HOST%:%REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo Size : !ZIP_MB! MB |
| | | echo. |
| | | echo Deployment completed successfully. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Error |
| | | REM ============================================================ |
| | | |
| | | :ERROR_EXIT |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY FAILED |
| | | echo ============================================================ |
| | | echo. |
| | | echo Please check the error message above. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM End |
| | | REM ============================================================ |
| | | |
| | | :END |
| | | |
| | | pause |
| | | endlocal |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | @echo off |
| | | setlocal EnableExtensions EnableDelayedExpansion |
| | | |
| | | REM ============================================================ |
| | | REM Vue Frontend Deploy Script |
| | | REM ============================================================ |
| | | |
| | | REM ========================= |
| | | REM Configuration |
| | | REM ========================= |
| | | |
| | | set "COMPANY=KYZN" |
| | | |
| | | set "REMOTE_USER=root" |
| | | set "REMOTE_HOST=36.213.150.237" |
| | | set "REMOTE_PORT=65022" |
| | | |
| | | set "PASSWORD=Sh@1008600" |
| | | |
| | | set "ZIP_FILE=dist.zip" |
| | | set "REMOTE_DIR=/data/vue/mom/kyzn/dist" |
| | | |
| | | set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3" |
| | | |
| | | set "RETRY_COUNT=3" |
| | | |
| | | set "SSHPASS_DIR=%~dp0bin" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Start |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo Vue Dist Auto Deploy |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_USER%@%REMOTE_HOST% |
| | | echo Port : %REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check sshpass |
| | | REM ============================================================ |
| | | |
| | | echo [1/7] Checking sshpass... |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | ) |
| | | |
| | | where sshpass >nul 2>&1 |
| | | |
| | | if not errorlevel 1 ( |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [WARN] sshpass not found. |
| | | |
| | | if not exist "%SSHPASS_DIR%" ( |
| | | mkdir "%SSHPASS_DIR%" >nul 2>&1 |
| | | ) |
| | | |
| | | if exist "%SSHPASS_DIR%\sshpass.exe" ( |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | echo [OK] sshpass found. |
| | | goto CHECK_PNPM |
| | | ) |
| | | |
| | | echo [INFO] Downloading sshpass... |
| | | |
| | | powershell -NoProfile -ExecutionPolicy Bypass -Command "$url='https://github.com/xhcoding/sshpass-win32/releases/download/v1.0.3/sshpass.exe'; $out='%SSHPASS_DIR%\sshpass.exe'; Invoke-WebRequest -Uri $url -OutFile $out" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to download sshpass. |
| | | echo [ERROR] Please install sshpass manually. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | set "PATH=%SSHPASS_DIR%;%PATH%" |
| | | |
| | | echo [OK] sshpass installed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check pnpm |
| | | REM ============================================================ |
| | | |
| | | :CHECK_PNPM |
| | | |
| | | echo. |
| | | echo [2/7] Checking pnpm... |
| | | |
| | | where pnpm >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm not found. |
| | | echo [HINT] npm install -g pnpm |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call pnpm --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [3/7] Checking Node.js... |
| | | |
| | | where node >nul 2>&1 |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js not found. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | call node --version |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Node.js execution failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check node_modules |
| | | REM ============================================================ |
| | | |
| | | if not exist "node_modules" ( |
| | | echo. |
| | | echo [INFO] node_modules not found. |
| | | echo [INFO] Running pnpm install... |
| | | |
| | | call pnpm install |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] pnpm install failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] pnpm install completed. |
| | | ) else ( |
| | | echo [OK] node_modules exists. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Build |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [4/7] Building project... |
| | | |
| | | if exist "%ZIP_FILE%" ( |
| | | echo. |
| | | echo [WARN] %ZIP_FILE% already exists. |
| | | set "REBUILD=" |
| | | set /p "REBUILD=Rebuild? Y/N (default Y): " |
| | | |
| | | if /I "!REBUILD!"=="N" ( |
| | | echo [INFO] Skip build. |
| | | goto CHECK_ZIP |
| | | ) |
| | | ) |
| | | |
| | | echo [INFO] Company: %COMPANY% |
| | | echo [INFO] Command: pnpm run build |
| | | echo. |
| | | |
| | | set "VITE_COMPANY=%COMPANY%" |
| | | |
| | | call pnpm run build |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Build failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Build completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check ZIP |
| | | REM ============================================================ |
| | | |
| | | :CHECK_ZIP |
| | | |
| | | echo. |
| | | echo [5/7] Checking package... |
| | | |
| | | if not exist "%ZIP_FILE%" ( |
| | | echo [ERROR] %ZIP_FILE% not found. |
| | | echo [HINT] Please check whether your build generates dist.zip. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | for %%F in ("%ZIP_FILE%") do set "ZIP_BYTES=%%~zF" |
| | | |
| | | set /a ZIP_MB=ZIP_BYTES/1024/1024 |
| | | |
| | | echo [OK] Package found. |
| | | echo [INFO] File size: !ZIP_BYTES! bytes |
| | | echo [INFO] File size: !ZIP_MB! MB |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Set sshpass password |
| | | REM ============================================================ |
| | | |
| | | set "SSHPASS=%PASSWORD%" |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test SSH |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [6/7] Testing SSH connection... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "echo SSH_OK" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] SSH connection failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] SSH connection successful. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Create remote directory |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Creating remote directory... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mkdir -p '%REMOTE_DIR%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to create remote directory. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote directory ready. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Upload temporary file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Uploading package... |
| | | |
| | | set "REMOTE_TMP=%REMOTE_DIR%/.dist.zip.uploading" |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_TMP%'" |
| | | |
| | | sshpass -e scp -P %REMOTE_PORT% %SSH_OPTS% "%ZIP_FILE%" %REMOTE_USER%@%REMOTE_HOST%:"%REMOTE_TMP%" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Upload failed. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Upload completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check remote file |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking remote package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "test -f '%REMOTE_TMP%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Remote package does not exist. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Remote package exists. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Compare file size |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking package size... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "stat -c %%s '%REMOTE_TMP%'" |
| | | |
| | | echo. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Rename temporary file |
| | | REM ============================================================ |
| | | |
| | | echo [INFO] Activating package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "mv -f '%REMOTE_TMP%' '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] Failed to activate package. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] Package activated. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Check unzip |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Checking unzip... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "command -v unzip" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [ERROR] unzip is not installed on server. |
| | | echo. |
| | | echo CentOS/RHEL: |
| | | echo yum install -y unzip |
| | | echo. |
| | | echo Ubuntu/Debian: |
| | | echo apt install -y unzip |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] unzip found. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Test ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Testing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -t '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] ZIP test failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo [OK] ZIP test passed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Extract |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Extracting package... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "cd '%REMOTE_DIR%' && unzip -o '%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo. |
| | | echo [ERROR] Remote unzip failed. |
| | | echo [INFO] Remote ZIP has been kept. |
| | | goto ERROR_EXIT |
| | | ) |
| | | |
| | | echo. |
| | | echo [OK] Extract completed. |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Remove ZIP |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo [INFO] Removing remote ZIP... |
| | | |
| | | sshpass -e ssh -p %REMOTE_PORT% %SSH_OPTS% %REMOTE_USER%@%REMOTE_HOST% "rm -f '%REMOTE_DIR%/%ZIP_FILE%'" |
| | | |
| | | if errorlevel 1 ( |
| | | echo [WARN] Failed to remove remote ZIP. |
| | | ) else ( |
| | | echo [OK] Remote ZIP removed. |
| | | ) |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Success |
| | | REM ============================================================ |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY SUCCESS |
| | | echo ============================================================ |
| | | echo. |
| | | echo Company : %COMPANY% |
| | | echo Server : %REMOTE_HOST%:%REMOTE_PORT% |
| | | echo Remote Dir : %REMOTE_DIR% |
| | | echo Package : %ZIP_FILE% |
| | | echo Size : !ZIP_MB! MB |
| | | echo. |
| | | echo Deployment completed successfully. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM Error |
| | | REM ============================================================ |
| | | |
| | | :ERROR_EXIT |
| | | |
| | | echo. |
| | | echo ============================================================ |
| | | echo DEPLOY FAILED |
| | | echo ============================================================ |
| | | echo. |
| | | echo Please check the error message above. |
| | | echo. |
| | | |
| | | goto END |
| | | |
| | | |
| | | REM ============================================================ |
| | | REM End |
| | | REM ============================================================ |
| | | |
| | | :END |
| | | |
| | | pause |
| | | endlocal |
| | |
| | | "dhtmlx-gantt": "catalog:", |
| | | "diagram-js": "catalog:", |
| | | "fast-xml-parser": "catalog:", |
| | | "grapesjs": "catalog:", |
| | | "highlight.js": "catalog:", |
| | | "livekit-client": "^2.20.0", |
| | | "pinia": "catalog:", |
| | |
| | | globals: |
| | | specifier: ^17.7.0 |
| | | version: 17.7.0 |
| | | grapesjs: |
| | | specifier: ^0.23.6 |
| | | version: 0.23.6 |
| | | highlight.js: |
| | | specifier: ^11.11.1 |
| | | version: 11.11.1 |
| | |
| | | fast-xml-parser: |
| | | specifier: 'catalog:' |
| | | version: 4.5.6 |
| | | grapesjs: |
| | | specifier: 'catalog:' |
| | | version: 0.23.6 |
| | | highlight.js: |
| | | specifier: 'catalog:' |
| | | version: 11.11.1 |
| | |
| | | '@jridgewell/sourcemap-codec@1.5.5': |
| | | resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} |
| | | |
| | | '@jridgewell/sourcemap-codec@1.6.0': |
| | | resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} |
| | | |
| | | '@jridgewell/trace-mapping@0.3.31': |
| | | resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} |
| | | |
| | |
| | | '@types/archiver@7.0.0': |
| | | resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==} |
| | | |
| | | '@types/backbone@1.4.15': |
| | | resolution: {integrity: sha512-WWeKtYlsIMtDyLbbhkb96taJMEbfQBnuz7yw1u0pkphCOtksemoWhIXhK74VRCY9hbjnsH3rsJu2uUiFtnsEYg==} |
| | | |
| | | '@types/codemirror@5.60.17': |
| | | resolution: {integrity: sha512-AZq2FIsUHVMlp7VSe2hTfl5w4pcUkoFkM3zVsRKsn1ca8CXRDYvnin04+HP2REkwsxemuHqvDofdlhUWNpbwfw==} |
| | | |
| | |
| | | |
| | | '@types/html-minifier-terser@7.0.2': |
| | | resolution: {integrity: sha512-mm2HqV22l8lFQh4r2oSsOEVea+m0qqxEmwpc9kC1p/XzmjLWrReR9D/GRs8Pex2NX/imyEH9c5IU/7tMBQCHOA==} |
| | | |
| | | '@types/jquery@4.0.1': |
| | | resolution: {integrity: sha512-9a59A/tycXgYuPABcp6/3spSShn0NT2UOM4EfHvMumjYi4lJWTsK5SZWjhx3yRm9IHGCeWXdV2YfNsrWrft/CA==} |
| | | |
| | | '@types/jsesc@2.5.1': |
| | | resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} |
| | |
| | | |
| | | '@types/trusted-types@2.0.7': |
| | | resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} |
| | | |
| | | '@types/underscore@1.13.0': |
| | | resolution: {integrity: sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==} |
| | | |
| | | '@types/video.js@7.3.58': |
| | | resolution: {integrity: sha512-1CQjuSrgbv1/dhmcfQ83eVyYbvGyqhTvb2Opxr0QCV+iJ4J6/J+XWQ3Om59WiwCd1MN3rDUHasx5XRrpUtewYQ==} |
| | |
| | | peerDependencies: |
| | | '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 |
| | | |
| | | backbone-undo@0.2.6: |
| | | resolution: {integrity: sha512-AsfpNiljLXlk7TcffDUu3EAUq7CxWbyTNwARWrql5XTzN4vh6WzEEBZYaKK4kTTz+iW1tSzqUooaGRIwO83kWA==} |
| | | deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. |
| | | |
| | | backbone@1.4.1: |
| | | resolution: {integrity: sha512-ADy1ztN074YkWbHi8ojJVFe3vAanO/lrzMGZWUClIP7oDD/Pjy2vrASraUP+2EVCfIiTtCW4FChVow01XneivA==} |
| | | |
| | | balanced-match@1.0.0: |
| | | resolution: {integrity: sha512-9Y0g0Q8rmSt+H33DfKv7FOc3v+iRI+o1lbzt8jGcIosYW37IIW/2XVYq5NPdmaD5NQ59Nk26Kl/vZbwW9Fr8vg==} |
| | | |
| | |
| | | cluster-key-slot@1.1.1: |
| | | resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} |
| | | engines: {node: '>=0.10.0'} |
| | | |
| | | codemirror-formatting@1.0.0: |
| | | resolution: {integrity: sha512-br9yM6eJI3pJHekEnoyHaBEb1B7XxxDjju+vRyBe8QGLp5saTIXXkZ+eFCTqXSAtI8QEZDFVEX2/SOjH2sVWRQ==} |
| | | |
| | | codemirror@5.63.0: |
| | | resolution: {integrity: sha512-KlLWRPggDg2rBD1Mx7/EqEhaBdy+ybBCVh/efgjBDsPpMeEu6MbTAJzIT4TuCzvmbTEgvKOGzVT6wdBTNusqrg==} |
| | | |
| | | codemirror@5.65.21: |
| | | resolution: {integrity: sha512-6teYk0bA0nR3QP0ihGMoxuKzpl5W80FpnHpBJpgy66NK3cZv5b/d/HY8PnRvfSsCG1MTfr92u2WUl+wT0E40mQ==} |
| | |
| | | graceful-fs@4.2.11: |
| | | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} |
| | | |
| | | grapesjs@0.23.6: |
| | | resolution: {integrity: sha512-I8M/sPWjHm6MLIdUtRFNOKsQ3PtDgIDsc2S8ANkJsfHj3ZoH98oNMMWr64Kx22XgjHCQ4xpUx7nvXyQSKV1mrQ==} |
| | | |
| | | gzip-size@7.0.0: |
| | | resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} |
| | | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} |
| | |
| | | |
| | | htm@3.1.1: |
| | | resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} |
| | | |
| | | html-entities@1.4.0: |
| | | resolution: {integrity: sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==} |
| | | |
| | | html-minifier-terser@7.2.0: |
| | | resolution: {integrity: sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==} |
| | |
| | | resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} |
| | | engines: {node: '>= 0.6.0'} |
| | | |
| | | promise-polyfill@8.3.0: |
| | | resolution: {integrity: sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==} |
| | | |
| | | prosemirror-changeset@2.4.1: |
| | | resolution: {integrity: sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==} |
| | | |
| | |
| | | resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} |
| | | engines: {node: '>=20'} |
| | | |
| | | string.prototype.matchall@4.0.12: |
| | | resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} |
| | | string.prototype.matchall@4.1.0: |
| | | resolution: {integrity: sha512-tHNHTxInrYLCga9O9YGxWA3G9/nnzQw8UGAyqGx3Ar1pSTTzIuM4woFSq4SowkXCjJIwq5sIiQvEfRI9tCH1qQ==} |
| | | engines: {node: '>= 0.4'} |
| | | |
| | | string.prototype.trim@1.2.11: |
| | |
| | | |
| | | unctx@2.5.0: |
| | | resolution: {integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==} |
| | | |
| | | underscore@1.13.8: |
| | | resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} |
| | | |
| | | undici-types@7.24.6: |
| | | resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} |
| | |
| | | zeebe-bpmn-moddle@1.15.0: |
| | | resolution: {integrity: sha512-C40qyX4q79lECM6JyEVGjBqDhgPgGf19T3waOPUtlW+ZuWXl/tvuAsrBwHhJssWbAFJAGYdJ9urPF80EijUmLg==} |
| | | |
| | | zimmerframe@1.1.4: |
| | | resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} |
| | | zimmerframe@1.1.5: |
| | | resolution: {integrity: sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==} |
| | | |
| | | zip-stream@6.0.1: |
| | | resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} |
| | |
| | | |
| | | '@jridgewell/sourcemap-codec@1.5.5': {} |
| | | |
| | | '@jridgewell/sourcemap-codec@1.6.0': {} |
| | | |
| | | '@jridgewell/trace-mapping@0.3.31': |
| | | dependencies: |
| | | '@jridgewell/resolve-uri': 3.1.2 |
| | |
| | | ejs: 3.1.10 |
| | | json5: 2.2.3 |
| | | magic-string: 0.30.21 |
| | | string.prototype.matchall: 4.0.12 |
| | | string.prototype.matchall: 4.1.0 |
| | | |
| | | '@tybys/wasm-util@0.10.2': |
| | | dependencies: |
| | |
| | | '@types/archiver@7.0.0': |
| | | dependencies: |
| | | '@types/readdir-glob': 1.1.5 |
| | | |
| | | '@types/backbone@1.4.15': |
| | | dependencies: |
| | | '@types/jquery': 4.0.1 |
| | | '@types/underscore': 1.13.0 |
| | | |
| | | '@types/codemirror@5.60.17': |
| | | dependencies: |
| | |
| | | '@types/estree@1.0.9': {} |
| | | |
| | | '@types/html-minifier-terser@7.0.2': {} |
| | | |
| | | '@types/jquery@4.0.1': {} |
| | | |
| | | '@types/jsesc@2.5.1': {} |
| | | |
| | |
| | | '@types/estree': 1.0.9 |
| | | |
| | | '@types/trusted-types@2.0.7': {} |
| | | |
| | | '@types/underscore@1.13.0': {} |
| | | |
| | | '@types/video.js@7.3.58': {} |
| | | |
| | |
| | | transitivePeerDependencies: |
| | | - supports-color |
| | | |
| | | backbone-undo@0.2.6: |
| | | dependencies: |
| | | backbone: 1.4.1 |
| | | underscore: 1.13.8 |
| | | |
| | | backbone@1.4.1: |
| | | dependencies: |
| | | underscore: 1.13.8 |
| | | |
| | | balanced-match@1.0.0: {} |
| | | |
| | | balanced-match@4.0.4: {} |
| | |
| | | clsx@2.1.1: {} |
| | | |
| | | cluster-key-slot@1.1.1: {} |
| | | |
| | | codemirror-formatting@1.0.0: {} |
| | | |
| | | codemirror@5.63.0: {} |
| | | |
| | | codemirror@5.65.21: {} |
| | | |
| | |
| | | |
| | | esrap@2.2.12(@typescript-eslint/types@8.62.0): |
| | | dependencies: |
| | | '@jridgewell/sourcemap-codec': 1.5.0 |
| | | '@jridgewell/sourcemap-codec': 1.6.0 |
| | | optionalDependencies: |
| | | '@typescript-eslint/types': 8.62.0 |
| | | |
| | |
| | | |
| | | graceful-fs@4.2.11: {} |
| | | |
| | | grapesjs@0.23.6: |
| | | dependencies: |
| | | '@types/backbone': 1.4.15 |
| | | backbone: 1.4.1 |
| | | backbone-undo: 0.2.6 |
| | | codemirror: 5.63.0 |
| | | codemirror-formatting: 1.0.0 |
| | | html-entities: 1.4.0 |
| | | promise-polyfill: 8.3.0 |
| | | underscore: 1.13.8 |
| | | |
| | | gzip-size@7.0.0: |
| | | dependencies: |
| | | duplexer: 0.1.2 |
| | |
| | | lru-cache: 7.5.1 |
| | | |
| | | htm@3.1.1: {} |
| | | |
| | | html-entities@1.4.0: {} |
| | | |
| | | html-minifier-terser@7.2.0: |
| | | dependencies: |
| | |
| | | |
| | | process@0.11.10: {} |
| | | |
| | | promise-polyfill@8.3.0: {} |
| | | |
| | | prosemirror-changeset@2.4.1: |
| | | dependencies: |
| | | prosemirror-transform: 1.12.0 |
| | |
| | | get-east-asian-width: 1.6.0 |
| | | strip-ansi: 7.2.0 |
| | | |
| | | string.prototype.matchall@4.0.12: |
| | | string.prototype.matchall@4.1.0: |
| | | dependencies: |
| | | call-bind: 1.0.9 |
| | | call-bound: 1.0.4 |
| | |
| | | svelte@5.56.3(@typescript-eslint/types@8.62.0): |
| | | dependencies: |
| | | '@jridgewell/remapping': 2.3.5 |
| | | '@jridgewell/sourcemap-codec': 1.5.0 |
| | | '@jridgewell/sourcemap-codec': 1.6.0 |
| | | '@sveltejs/acorn-typescript': 1.0.10(acorn@8.17.0) |
| | | '@types/estree': 1.0.9 |
| | | '@types/trusted-types': 2.0.7 |
| | |
| | | is-reference: 3.0.3 |
| | | locate-character: 3.0.0 |
| | | magic-string: 0.30.21 |
| | | zimmerframe: 1.1.4 |
| | | zimmerframe: 1.1.5 |
| | | transitivePeerDependencies: |
| | | - '@typescript-eslint/types' |
| | | |
| | |
| | | estree-walker: 3.0.3 |
| | | magic-string: 0.30.21 |
| | | unplugin: 2.3.11 |
| | | |
| | | underscore@1.13.8: {} |
| | | |
| | | undici-types@7.24.6: {} |
| | | |
| | |
| | | |
| | | zeebe-bpmn-moddle@1.15.0: {} |
| | | |
| | | zimmerframe@1.1.4: {} |
| | | zimmerframe@1.1.5: {} |
| | | |
| | | zip-stream@6.0.1: |
| | | dependencies: |
| | |
| | | find-up: ^8.0.0 |
| | | get-port: ^7.2.0 |
| | | globals: ^17.7.0 |
| | | grapesjs: ^0.23.6 |
| | | highlight.js: ^11.11.1 |
| | | html-minifier-terser: ^7.2.0 |
| | | jsbarcode: ^3.12.3 |
| | |
| | | name?: string; // æ£æµé¡¹åç§° |
| | | type?: number; // æ£æµé¡¹ç±»å |
| | | tool?: string; // æ£æµå·¥å
· |
| | | itemType?: number; // æ¡ç®ç±»åï¼1=å½å
¥é¡¹ï¼2=åç»é¡¹ |
| | | parentId?: number; // ç¶ææ ç¼å·ï¼0=é¡¶çº§ï¼ |
| | | parentName?: string; // ç¶ææ åç§°ï¼å³æå±åç»åï¼é¡¶çº§æ¶ä¸ºç©ºï¼ |
| | | formulaText?: string; // å
¬å¼å±ç¤ºææ¬ï¼åç»é¡¹çãæ£æµè¦æ±ãï¼ |
| | | sortOrder?: number; // å级æåºå· |
| | | resultType?: number; // ç»æå¼ç±»å |
| | | resultSpecification?: string; // ç»æå¼å±æ§ |
| | | remark?: string; // 夿³¨ |
| | |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢åç»é¡¹ç²¾ç®å表ï¼ãæå±åç»ã䏿éé¡¹ï¼ */ |
| | | export function getIndicatorGroupList() { |
| | | return requestClient.get<MesQcIndicatorApi.Indicator[]>( |
| | | '/mes/qc/indicator/group-list', |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢è´¨æ£ææ */ |
| | | export function createIndicator(data: MesQcIndicatorApi.Indicator) { |
| | | return requestClient.post('/mes/qc/indicator/create', data); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentCatalog } from '#/components/quality'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace QcReportAiApi { |
| | | /** ä¸ä¸ªç»ä»¶è稿ï¼ç»æå®å
¨æå¹³ï¼æ²¡æ children */ |
| | | export interface DraftComponent { |
| | | type?: string; |
| | | /** ç»ä»¶å±æ§ï¼å端已æç§¯æ¨æ¸
åè¿æ»¤ææªå£°æç key */ |
| | | props?: Record<string, unknown>; |
| | | } |
| | | |
| | | /** AI çåºççº¸å¼ é
ç½®ï¼å¯è½ä¸ºç©ºï¼ä¸ºç©ºè¡¨ç¤ºæ²¿ç¨å½å模æ¿ççº¸å¼ ï¼ */ |
| | | export interface DraftPage { |
| | | size?: string; |
| | | orientation?: string; |
| | | margin?: { top?: number; right?: number; bottom?: number; left?: number }; |
| | | } |
| | | |
| | | /** |
| | | * è¯å«ç»æï¼ä¸ä»½æ¨¡æ¿è稿ï¼**å°æªè½åº**ã |
| | | * <p> |
| | | * é人工å°è®¾è®¡å¨ç¡®è®¤å¹¶ç¹ãä¿åãï¼æä¼åææ¨¡æ¿çæ¬ã |
| | | */ |
| | | export interface DraftResp { |
| | | components?: DraftComponent[]; |
| | | page?: DraftPage; |
| | | /** AI 对è¿ä»½ææ¡£çä¸å¥è¯è¯´æ */ |
| | | summary?: string; |
| | | /** 软æç¤ºï¼è¢«è·³è¿çé¡µãæ æ³è¡¨è¾¾çåºåçï¼å¿
é¡»å±ç¤ºç»ç¨æ· */ |
| | | warnings?: string[]; |
| | | durationMs?: number; |
| | | } |
| | | |
| | | export interface DraftReq { |
| | | /** |
| | | * ä¸ä¼ æä»¶å¯¹åºç blobIdï¼**顺åºå³ææ¡£é¡µåº**ã |
| | | * <p> |
| | | * æä»¶å
ç» /system/storage-blob/upload ä¸ä¼ åå¾ blobIdï¼ä¸å¡æ¨¡åä¸èªå»ºä¸ä¼ ééã |
| | | */ |
| | | blobIds: number[]; |
| | | /** ç®æ 模æ¿ç¼å·ï¼ç¨äºæ¥å¿ççä¸éä»¶å½å± */ |
| | | templateId: number; |
| | | /** å¿
é¡»ä¸è®¾è®¡å¨ä¸è´ï¼ä¸¤ç«¯ä¸å¹é
æ¶åç«¯ç´æ¥æ¥é */ |
| | | schemaVersion: string; |
| | | /** ç§¯æ¨æ¸
åï¼ç± buildQualityComponentCatalog() 仿´»æ³¨åè¡¨çæ */ |
| | | catalog: QualityComponentCatalog[]; |
| | | /** ç¨æ·è¡¥å
说æï¼æ¼è¿æç¤ºè¯å¸®å©æ¨¡åçè§£ææ¡£ */ |
| | | hint?: string; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * è¯å«æä»¶çææ¨¡æ¿è稿ã |
| | | * <p> |
| | | * 忥è¿åä¸ä¸è½åºï¼æä»¥åç¬æ¾å®½è¶
æ¶ãè¿ä¸ªå¼å¿
é¡»æ¯å端çèæ¶é¢ç®æ´å®½ï¼ |
| | | * å端 `yudao.qcreport.ai-import.max-duration-seconds`ï¼é»è®¤ 180 ç§ï¼åªå¨ä¸¤æ¬¡æ¨¡åè°ç¨ä¹é´æ£æ¥ï¼ |
| | | * å ä¸å次è°ç¨è¶
æ¶ `yudao.ai.timeout`ï¼é»è®¤ 60 ç§ï¼ï¼æå 240 ç§ã |
| | | * æå¾æ¯å®æ©ï¼ç¨æ·çå°çæ¯ã请æ±è¶
æ¶ãè¿ç§æ²¡ææåçæ¥éï¼ |
| | | * èåç«¯æ¬æ¥è½ç»åºãåªä¸æ¥ã为ä»ä¹ãæä¹åãçæç¡®æç¤ºã |
| | | */ |
| | | export function generateAiDraft(data: QcReportAiApi.DraftReq) { |
| | | return requestClient.post<QcReportAiApi.DraftResp>( |
| | | '/qc-report/ai-import/draft', |
| | | data, |
| | | { timeout: 300_000 }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace QcReportInstanceApi { |
| | | /** æºè½è´¨æ£æ¥åå®ä¾ */ |
| | | export interface Instance { |
| | | id?: number; // å®ä¾ç¼å· |
| | | reportNo?: string; // æ¥åç¼å·ï¼ä¸å¡å¯ä¸ï¼ |
| | | templateId?: number; // 模æ¿ç¼å· |
| | | templateVersion?: string; // çææ¶ä½¿ç¨ç模æ¿çæ¬å· |
| | | businessId?: string; // ä¸å¡åæ®ç¼å· |
| | | businessType?: string; // ä¸å¡åæ®ç±»å |
| | | status?: number; // æ¥åç¶æï¼0çæä¸ 1çææå 2çæå¤±è´¥ |
| | | creator?: string; // å建人 |
| | | createTime?: Date; // å建æ¶é´ |
| | | updateTime?: Date; // æ´æ°æ¶é´ |
| | | } |
| | | |
| | | /** |
| | | * å®ä¾è¯¦æ
ã |
| | | * <p> |
| | | * æ¯å表å¤ä¸ä¸ª {@link renderHtml}ââ飿¯åºä»¶æ¶å»ç»ä¸æ¥ç产ç©ï¼ç¨ iframe ç´æ¥æ¸²æå³ä¸ºãé¢è§ãï¼ |
| | | * ä¸éè¦éæ°åºä»¶ãæ°æ®å¿«ç
§ä¸å¨è¿ä¸ªæ¥å£éï¼è¦çå¾åç¬è° {@link QcReportInstanceApi.Instance} ç snapshotã |
| | | */ |
| | | export interface InstanceDetail extends Instance { |
| | | renderHtml?: string; // 渲æäº§ç© HTML |
| | | } |
| | | |
| | | /** PDF åºä»¶ç»æ */ |
| | | export interface PdfResult { |
| | | id: number; // å®ä¾ç¼å· |
| | | reportNo: string; // æ¥åç¼å· |
| | | fileName: string; // PDF æä»¶å |
| | | byteSize: number; // PDF åèæ° |
| | | blobId?: number; // æä»¶ç¼å· |
| | | attachmentId?: number; // éä»¶å
³èç¼å· |
| | | previewURL?: string; // é¢è§å°åï¼ä¸´æ¶ç¾åï¼ä¼è¿æï¼ |
| | | downloadURL?: string; // ä¸è½½å°åï¼ä¸´æ¶ç¾åï¼ä¼è¿æï¼ |
| | | durationMs?: number; // æ¬æ¬¡åºä»¶æ»èæ¶ï¼æ¯«ç§ï¼ |
| | | pdfDurationMs?: number; // æå°èæ¶ï¼æ¯«ç§ï¼ |
| | | browserStatus?: string; // æµè§å¨ç¶æï¼READY / RESTARTED |
| | | } |
| | | |
| | | /** |
| | | * ç±è´¨æ£ååºä»¶çå
¥åã |
| | | * <p> |
| | | * è¿éå»ææ²¡æ contextââæ°æ®å
¨é¨ç±åç«¯ä» MES è´¨æ£ååï¼å端åªè´è´£éãåªå¼ åæ® + åªå¼ 模æ¿ãã |
| | | */ |
| | | export interface GenerateFromQcReq { |
| | | qcType: number; // è´¨æ£ç±»åï¼1 æ¥æ 2 è¿ç¨ 3 åºè´§ 4 éè´§ |
| | | qcId: number; // è´¨æ£åç¼å·ï¼åå¼ è¡¨åèªç主é®ï¼ |
| | | templateId: number; // æ¥å模æ¿ç¼å·ï¼å
¶ reportType å¿
é¡»ä¸ qcType ä¸è´ |
| | | version?: string; // 模æ¿çæ¬å·ï¼ä¸ä¼ 忍¡æ¿ currentVersion |
| | | reportNo?: string; // æ¥åç¼å·ï¼ä¸ä¼ ç±å端æ QR+æ¥æ+æµæ°´ çæ |
| | | } |
| | | |
| | | /** ç±è´¨æ£ååºä»¶çç»æ */ |
| | | export interface GenerateFromQcResult { |
| | | id: number; // æ¥åå®ä¾ç¼å·ï¼æ¿å»è·³è¯¦æ
/忥 |
| | | reportNo: string; // æç»çæçæ¥åç¼å· |
| | | templateVersion: string; // æ¬æ¬¡ä½¿ç¨ç模æ¿çæ¬å· |
| | | itemCount: number; // åå
¥æ¥åçæ£éªé¡¹æ¡æ°ï¼ä¸çäºè´¨æ£åè¡æ°ï¼ |
| | | undecidableCount: number; // å¾
å¤å®é¡¹æ°ï¼å¤§äº 0 å¿
é¡»æç¤ºç¨æ· |
| | | warnings?: string[]; // æ°æ®åèç软æç¤ºï¼åç»æ é¢è¢«è·³è¿ / ææ å·²å / æªå½å®æµå¼ / æ ·åæ°è¿å¤ |
| | | errors?: string[]; // æ¸²æææ°æ®ç¼ºå£ï¼æ¨¡æ¿ç»å®äºä½æ²¡æ°æ®çåæ®µï¼ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥åå®ä¾å页 */ |
| | | export function getInstancePage(params: PageParam) { |
| | | return requestClient.get<PageResult<QcReportInstanceApi.Instance>>( |
| | | '/qc-report/instance/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥åå®ä¾è¯¦æ
ï¼å«æ¸²æäº§ç© HTMLï¼ä¸å«æ°æ®å¿«ç
§ï¼ */ |
| | | export function getInstance(id: number) { |
| | | return requestClient.get<QcReportInstanceApi.InstanceDetail>( |
| | | `/qc-report/instance/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥åå®ä¾å»ç»çæ°æ®å¿«ç
§ */ |
| | | export function getInstanceSnapshot(id: number) { |
| | | return requestClient.get<Record<string, unknown>>( |
| | | `/qc-report/instance/snapshot?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** å¯¼åº PDFï¼æå°å®ä¾å·²åçæ¸²æäº§ç©å¹¶å½æ¡£å°éä»¶åºï¼å¯éå¤è°ç¨ï¼ */ |
| | | export function exportInstancePdf(id: number) { |
| | | return requestClient.post<QcReportInstanceApi.PdfResult>( |
| | | `/qc-report/instance/pdf?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** å 餿¥åå®ä¾ */ |
| | | export function deleteInstance(id: number) { |
| | | return requestClient.delete(`/qc-report/instance/delete?id=${id}`); |
| | | } |
| | | |
| | | /** |
| | | * ç±è´¨æ£åï¼IQC/IPQC/OQC/RQCï¼ç´æ¥çææ¥åã |
| | | * <p> |
| | | * æ°æ®ç±åç«¯ç» MesQcReportApi ä» MES åï¼åç«¯ä¸æ¼ contextã |
| | | * è°ç¨åäºéæ ¡éªæä¸å¡ä¼å
çº§åªæ¥ç¬¬ä¸å¤ï¼å¤±è´¥é½æ¯ãæ°æ®æéæ©ä¸å¯¹ãï¼éè¯æ ç¨ï¼ææ¥ææ¹æ°æ®ææ¢æ¨¡æ¿ã |
| | | */ |
| | | export function generateInstanceFromQc( |
| | | data: QcReportInstanceApi.GenerateFromQcReq, |
| | | ) { |
| | | return requestClient.post<QcReportInstanceApi.GenerateFromQcResult>( |
| | | '/qc-report/instance/generate-from-qc', |
| | | data, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace QcReportTemplateApi { |
| | | /** æºè½è´¨æ£æ¥åæ¨¡æ¿ */ |
| | | export interface Template { |
| | | id?: number; // 模æ¿ç¼å· |
| | | templateCode?: string; // 模æ¿ç¼ç |
| | | templateName?: string; // 模æ¿åç§° |
| | | industry?: string; // éç¨è¡ä¸ |
| | | reportType?: string; // æ¥åç±»åï¼å¤ç¨ mes_qc_typeï¼ |
| | | pageSize?: string; // çº¸å¼ å°ºå¯¸ |
| | | orientation?: string; // çº¸å¼ æ¹å |
| | | status?: number; // 模æ¿ç¶æ |
| | | currentVersion?: string; // å½åçæçæ¬å· |
| | | description?: string; // 模æ¿è¯´æ |
| | | creator?: string; // å建人 |
| | | createTime?: Date; // å建æ¶é´ |
| | | updateTime?: Date; // æ´æ°æ¶é´ |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥å模æ¿å页 */ |
| | | export function getTemplatePage(params: PageParam) { |
| | | return requestClient.get<PageResult<QcReportTemplateApi.Template>>( |
| | | '/qc-report/template/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** |
| | | * æ¥è¯¢å¯ç¨äºåºä»¶ç模æ¿å表ã |
| | | * <p> |
| | | * å端已æãæ¥åç±»åä¸è´ + 模æ¿å¯ç¨ + å½åçæ¬å·²åå¸ãè¿æ»¤å®æ¯ï¼ |
| | | * è°ç¨æ¹ä¸è¦åèªå·±æ¨å¯¼è¿å¥è§åï¼currentVersion é空并ä¸ä»£è¡¨é£ä¸ªçæ¬è¿è½ç¨ |
| | | * ââåç¨ä¸ä¸ªçæ¬ä¸ä¼æ¸
空模æ¿ç currentVersionã |
| | | */ |
| | | export function getSelectableTemplates(reportType: string) { |
| | | return requestClient.get<QcReportTemplateApi.Template[]>( |
| | | '/qc-report/template/selectable', |
| | | { params: { reportType } }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¥å模æ¿è¯¦æ
*/ |
| | | export function getTemplate(id: number) { |
| | | return requestClient.get<QcReportTemplateApi.Template>( |
| | | `/qc-report/template/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ°å¢æ¥åæ¨¡æ¿ */ |
| | | export function createTemplate(data: QcReportTemplateApi.Template) { |
| | | return requestClient.post<number>('/qc-report/template/create', data); |
| | | } |
| | | |
| | | /** ä¿®æ¹æ¥åæ¨¡æ¿ */ |
| | | export function updateTemplate(data: QcReportTemplateApi.Template) { |
| | | return requestClient.put('/qc-report/template/update', data); |
| | | } |
| | | |
| | | /** å 餿¥åæ¨¡æ¿ */ |
| | | export function deleteTemplate(id: number) { |
| | | return requestClient.delete(`/qc-report/template/delete?id=${id}`); |
| | | } |
| | | |
| | | /** å¤å¶æ¥å模æ¿ï¼è¿åæ°æ¨¡æ¿ç¼å· */ |
| | | export function copyTemplate(id: number) { |
| | | return requestClient.post<number>(`/qc-report/template/copy?id=${id}`); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace QcReportVersionApi { |
| | | /** çº¸å¼ é¡µè¾¹è·ï¼åä½ mm */ |
| | | export interface SchemaMargin { |
| | | top?: number; |
| | | right?: number; |
| | | bottom?: number; |
| | | left?: number; |
| | | } |
| | | |
| | | /** çº¸å¼ é
ç½® */ |
| | | export interface SchemaPage { |
| | | size?: string; // A3 / A4 / A5 / Letter |
| | | orientation?: string; // portrait / landscape |
| | | margin?: SchemaMargin; |
| | | } |
| | | |
| | | /** |
| | | * ç»å¸ä¸çä¸ä¸ªè´¨éç»ä»¶èç¹ï¼åªè®°ãæ¯è°ãå¨åªãä»ä¹å±æ§ã |
| | | * åµå¥ç»æä»¥ grapes 为åï¼è¿éä¸ååä¸ä»½æ ï¼é¿å
两个çç¸æ¥æºå¯¹ä¸ä¸ |
| | | */ |
| | | export interface QualityComponentNode { |
| | | /** ç»å¸å
ç»ä»¶ id */ |
| | | id?: string; |
| | | /** ç»ä»¶å¨ææ¡£ä¸ç顺åºï¼ä¾ç»å®ä¸å®ä½ä½¿ç¨ */ |
| | | index?: number; |
| | | /** ç»ä»¶ç±»åï¼å³ç»å¸èç¹ä¸ç data-quality-type */ |
| | | qualityType?: string; |
| | | /** ç»ä»¶çä¸å¡å±æ§ï¼å弿¯ data-qc-* / data-quality-type è¿ç±»å±æ§ */ |
| | | attributes?: Record<string, unknown>; |
| | | } |
| | | |
| | | /** å¤å®è§åï¼scope 为 item æ¶é项å¤å®ï¼ä¸º report æ¶å³å®æ´ä»½æ¥åçç»è®º */ |
| | | export interface SchemaRule { |
| | | id?: string; |
| | | name?: string; |
| | | scope?: 'item' | 'report'; |
| | | expression?: string; |
| | | enabled?: boolean; |
| | | } |
| | | |
| | | /** |
| | | * æ¨¡æ¿ Schema ââ 设计å¨äº§ç©ï¼ä¹æ¯æ¥ååºä»¶çå¯ä¸è¾å
¥ |
| | | * |
| | | * åæ®µåå·¥ï¼grapes æ¯ GrapesJS åå§é¡¹ç®æ°æ®ï¼è®¾è®¡å¨è¿åç»å¸ç¨ï¼æ¸²æå¼æä¹è¯»å®ï¼ |
| | | * æ£æç»æå¨è¿éï¼ï¼components æ¯è´¨éç»ä»¶ä¸å¡è¯ä¹æ¸
åï¼æ¸²æä¸è¯»ï¼ï¼rules æ¯å¤å®è§åã |
| | | * |
| | | * å¥çº¦çæ¬ 1.1ï¼å é¤äº 1.0 æ¶æç dataSources / bindings / styles ââ å®ä»¬å§ç»æ¯ç©ºæ°ç»ï¼ |
| | | * æ¢æ 人åå
¥ä¹æ 人读åãå端 ReportTemplateSchema 已忥å é¤ï¼ä¸¤ç«¯å¿
é¡»ä¸èµ·æ¹ã |
| | | */ |
| | | export interface ReportTemplateSchema { |
| | | schemaVersion?: string; |
| | | page?: SchemaPage; |
| | | grapes?: Record<string, unknown>; |
| | | components?: QualityComponentNode[]; |
| | | rules?: SchemaRule[]; |
| | | } |
| | | |
| | | /** 模æ¿çæ¬ */ |
| | | export interface Version { |
| | | id?: number; |
| | | templateId?: number; |
| | | version?: string; |
| | | schema?: ReportTemplateSchema; |
| | | status?: number; // 0è稿 1å·²åå¸ 2å·²åç¨ |
| | | description?: string; |
| | | creator?: string; |
| | | createTime?: Date; |
| | | updateTime?: Date; |
| | | } |
| | | |
| | | /** åå»ºçæ¬å
¥åï¼version ç空ç±åç«¯æææ°çæ¬èªå¢ */ |
| | | export interface VersionCreateReq { |
| | | templateId: number; |
| | | version?: string; |
| | | schema?: ReportTemplateSchema; |
| | | description?: string; |
| | | } |
| | | |
| | | /** æ´æ°çæ¬å
¥åï¼ä»
å
许æ¹è®¾è®¡å
容ä¸è¯´æ */ |
| | | export interface VersionUpdateReq { |
| | | id: number; |
| | | schema?: ReportTemplateSchema; |
| | | description?: string; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¨¡æ¿çæ¬å页 */ |
| | | export function getVersionPage(params: PageParam) { |
| | | return requestClient.get<PageResult<QcReportVersionApi.Version>>( |
| | | '/qc-report/template-version/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¨¡æ¿çæ¬è¯¦æ
*/ |
| | | export function getVersion(id: number) { |
| | | return requestClient.get<QcReportVersionApi.Version>( |
| | | `/qc-report/template-version/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ¨¡æ¿çææçæ¬ */ |
| | | export function getVersionListByTemplate(templateId: number) { |
| | | return requestClient.get<QcReportVersionApi.Version[]>( |
| | | `/qc-report/template-version/list-by-template?templateId=${templateId}`, |
| | | ); |
| | | } |
| | | |
| | | /** å建模æ¿çæ¬ï¼è¿åçæ¬ç¼å· */ |
| | | export function createVersion(data: QcReportVersionApi.VersionCreateReq) { |
| | | return requestClient.post<number>('/qc-report/template-version/create', data); |
| | | } |
| | | |
| | | /** æ´æ°æ¨¡æ¿çæ¬ */ |
| | | export function updateVersion(data: QcReportVersionApi.VersionUpdateReq) { |
| | | return requestClient.put('/qc-report/template-version/update', data); |
| | | } |
| | | |
| | | /** å 餿¨¡æ¿çæ¬ï¼ä»
èç¨¿ï¼ */ |
| | | export function deleteVersion(id: number) { |
| | | return requestClient.delete(`/qc-report/template-version/delete?id=${id}`); |
| | | } |
| | | |
| | | /** å叿¨¡æ¿çæ¬ */ |
| | | export function publishVersion(id: number) { |
| | | return requestClient.post(`/qc-report/template-version/publish?id=${id}`); |
| | | } |
| | | |
| | | /** 忻尿å®åå²çæ¬ */ |
| | | export function rollbackVersion(id: number) { |
| | | return requestClient.post(`/qc-report/template-version/rollback?id=${id}`); |
| | | } |
| | | |
| | | /** åç¨æ¨¡æ¿çæ¬ */ |
| | | export function disableVersion(id: number) { |
| | | return requestClient.post(`/qc-report/template-version/disable?id=${id}`); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | export const dividerDefinition: QualityComponentDefinition = { |
| | | type: 'Divider', |
| | | name: 'divider', |
| | | label: 'åé线', |
| | | category: QUALITY_CATEGORY.BASIC, |
| | | icon: QUALITY_ICONS.divider, |
| | | aiHint: 'å
容ä¹é´ç横ååé线ãä¸è¦ç¨å®ä»£æ¿è¡¨æ ¼è¾¹æ¡ã', |
| | | defaults: { color: '#d9d9d9', thickness: 1, spacing: 8 }, |
| | | propertySchema: [ |
| | | { key: 'color', label: '线æ¡é¢è²', type: 'string', defaultValue: '#d9d9d9' }, |
| | | { key: 'thickness', label: '线æ¡ç²ç»(px)', type: 'number', defaultValue: 1 }, |
| | | { key: 'spacing', label: 'ä¸ä¸é´è·(px)', type: 'number', defaultValue: 8 }, |
| | | ], |
| | | dataSchema: [], |
| | | buildContent: (properties) => ({ |
| | | tagName: 'hr', |
| | | style: { |
| | | margin: `${Number(properties.spacing ?? 8)}px 0`, |
| | | border: 'none', |
| | | 'border-top': `${Number(properties.thickness ?? 1)}px solid ${String(properties.color ?? '#d9d9d9')}`, |
| | | }, |
| | | }), |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { escapeHtml } from '../core/html'; |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | /** çº§å« â åå·ï¼å级å«åªç»ä¸ä¸ªå
å¶çè§è§å±æ¬¡ */ |
| | | const LEVEL_FONT_SIZE: Record<number, string> = { |
| | | 1: '24px', |
| | | 2: '20px', |
| | | 3: '16px', |
| | | 4: '14px', |
| | | }; |
| | | |
| | | export const headingDefinition: QualityComponentDefinition = { |
| | | type: 'Heading', |
| | | name: 'heading', |
| | | label: 'æ é¢', |
| | | category: QUALITY_CATEGORY.BASIC, |
| | | icon: QUALITY_ICONS.heading, |
| | | aiHint: 'æ£æéçåèå°æ é¢ï¼å¦ãæ£éªç»æããè´¨éè¯å®ãï¼ãææ¡£æé¡¶é¨çæ¥åæ é¢ä¸è¦ç¨æï¼ç¨ ReportHeaderã', |
| | | contentKey: 'text', |
| | | defaults: { text: 'æ¥åæ é¢', level: 2, align: 'center' }, |
| | | propertySchema: [ |
| | | { |
| | | key: 'text', |
| | | label: 'æ 颿å', |
| | | type: 'string', |
| | | defaultValue: 'æ¥åæ é¢', |
| | | required: true, |
| | | bindable: true, |
| | | }, |
| | | { |
| | | key: 'level', |
| | | label: 'æ é¢çº§å«', |
| | | type: 'enum', |
| | | defaultValue: 2, |
| | | options: [ |
| | | { label: 'ä¸çº§ï¼æå¤§ï¼', value: 1 }, |
| | | { label: 'äºçº§', value: 2 }, |
| | | { label: 'ä¸çº§', value: 3 }, |
| | | { label: 'åçº§ï¼æå°ï¼', value: 4 }, |
| | | ], |
| | | }, |
| | | { |
| | | key: 'align', |
| | | label: '坹齿¹å¼', |
| | | type: 'enum', |
| | | defaultValue: 'center', |
| | | options: [ |
| | | { label: '左对é½', value: 'left' }, |
| | | { label: 'å±
ä¸', value: 'center' }, |
| | | { label: 'å³å¯¹é½', value: 'right' }, |
| | | ], |
| | | }, |
| | | ], |
| | | dataSchema: [ |
| | | { key: 'text', label: 'æ 颿å', type: 'string', bindable: true }, |
| | | ], |
| | | buildContent: (properties) => { |
| | | const level = Number(properties.level ?? 2); |
| | | return { |
| | | tagName: `h${level}`, |
| | | content: escapeHtml(properties.text), |
| | | style: { |
| | | margin: '0 0 8px', |
| | | 'font-size': LEVEL_FONT_SIZE[level] ?? LEVEL_FONT_SIZE[2]!, |
| | | 'font-weight': '600', |
| | | 'text-align': String(properties.align ?? 'center'), |
| | | }, |
| | | }; |
| | | }, |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | /** æªè®¾ç½®å¾çæ¶çå ä½å¾ï¼é¿å
ç»å¸ä¸åºç°ç ´å¾ */ |
| | | const PLACEHOLDER_SRC = |
| | | 'data:image/svg+xml;charset=utf-8,%3Csvg xmlns="http://www.w3.org/2000/svg" width="160" height="90"%3E%3Crect width="160" height="90" fill="%23f0f0f0"/%3E%3Ctext x="80" y="50" font-size="12" fill="%23999" text-anchor="middle"%3Eå¾ç%3C/text%3E%3C/svg%3E'; |
| | | |
| | | export const imageDefinition: QualityComponentDefinition = { |
| | | type: 'Image', |
| | | name: 'image', |
| | | label: 'å¾ç', |
| | | category: QUALITY_CATEGORY.BASIC, |
| | | icon: QUALITY_ICONS.image, |
| | | aiHint: 'æ¥åéçæå¾ãå°ç« ãç¾åå¾çãè¯å«é¶æ®µåªæ¾å ä½å¾ï¼ä¸åçå®å¾çå°åã', |
| | | defaults: { src: PLACEHOLDER_SRC, alt: 'å¾ç', width: 60 }, |
| | | propertySchema: [ |
| | | { |
| | | key: 'src', |
| | | label: 'å¾çå°å', |
| | | type: 'string', |
| | | defaultValue: PLACEHOLDER_SRC, |
| | | bindable: true, |
| | | tip: 'å¯ç´æ¥å¡«å¾çå°åï¼æç»å®æ°æ®å段', |
| | | }, |
| | | { key: 'alt', label: 'æ¿ä»£æå', type: 'string', defaultValue: 'å¾ç' }, |
| | | { |
| | | key: 'width', |
| | | label: '宽度(%)', |
| | | type: 'number', |
| | | defaultValue: 60, |
| | | }, |
| | | ], |
| | | dataSchema: [{ key: 'src', label: 'å¾çå°å', type: 'string', bindable: true }], |
| | | buildContent: (properties) => ({ |
| | | tagName: 'img', |
| | | attributes: { |
| | | src: String(properties.src ?? PLACEHOLDER_SRC), |
| | | alt: String(properties.alt ?? ''), |
| | | }, |
| | | style: { |
| | | display: 'block', |
| | | 'max-width': '100%', |
| | | width: `${Number(properties.width ?? 60)}%`, |
| | | }, |
| | | }), |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | export const spacerDefinition: QualityComponentDefinition = { |
| | | type: 'Spacer', |
| | | name: 'spacer', |
| | | label: 'é´è·', |
| | | category: QUALITY_CATEGORY.BASIC, |
| | | icon: QUALITY_ICONS.spacer, |
| | | aiHint: '纯空ç½é´è·ï¼åªç¨æ¥æå¼çé¢ï¼ä¸æ¿è½½ä»»ä½å
容ã', |
| | | defaults: { height: 16 }, |
| | | propertySchema: [{ key: 'height', label: 'é«åº¦(px)', type: 'number', defaultValue: 16 }], |
| | | dataSchema: [], |
| | | buildContent: (properties) => ({ |
| | | tagName: 'div', |
| | | style: { height: `${Number(properties.height ?? 16)}px` }, |
| | | }), |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { cssStyle, escapeHtml } from '../core/html'; |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | /** åå
æ ¼ä¸è¡¨å¤´å
±ç¨çè¾¹æ¡æ ·å¼ */ |
| | | const CELL_BORDER = '1px solid #d9d9d9'; |
| | | |
| | | function buildCells(headers: string[], rows: number): string { |
| | | const head = headers |
| | | .map( |
| | | (text) => |
| | | `<th style="${cssStyle({ |
| | | border: CELL_BORDER, |
| | | padding: '6px 8px', |
| | | background: '#fafafa', |
| | | 'font-weight': 600, |
| | | })}">${escapeHtml(text)}</th>`, |
| | | ) |
| | | .join(''); |
| | | const body = Array.from({ length: Math.max(rows, 1) }) |
| | | .map(() => |
| | | headers |
| | | .map(() => `<td style="${cssStyle({ border: CELL_BORDER, padding: '6px 8px' })}"> </td>`) |
| | | .join(''), |
| | | ) |
| | | .map((cells) => `<tr>${cells}</tr>`) |
| | | .join(''); |
| | | return `<table style="width:100%;border-collapse:collapse;font-size:12px"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`; |
| | | } |
| | | |
| | | /** è§£æéå·åéç表头æå */ |
| | | function parseHeaders(raw: unknown): string[] { |
| | | const headers = String(raw ?? '') |
| | | .split(',') |
| | | .map((item) => item.trim()) |
| | | .filter(Boolean); |
| | | return headers.length > 0 ? headers : ['项ç®', 'æ åå¼', '宿µå¼', 'å¤å®']; |
| | | } |
| | | |
| | | export const tableDefinition: QualityComponentDefinition = { |
| | | type: 'Table', |
| | | name: 'table', |
| | | label: 'åºç¡è¡¨æ ¼', |
| | | category: QUALITY_CATEGORY.BASIC, |
| | | icon: QUALITY_ICONS.table, |
| | | aiHint: |
| | | 'éç¨äºç»´è¡¨æ ¼ï¼å¦æ ·ååºç¡ä¿¡æ¯ç两åé®å¼è¡¨ãè®¾å¤æ¸
åãå¤è¡æ£éªæ°æ®ä¸è¦ç¨æï¼ç¨ QualityTableã', |
| | | defaults: { headers: '项ç®,æ åå¼,宿µå¼,å¤å®', rows: 2 }, |
| | | propertySchema: [ |
| | | { |
| | | key: 'headers', |
| | | label: '表头ï¼è±æéå·åéï¼', |
| | | type: 'string', |
| | | defaultValue: '项ç®,æ åå¼,宿µå¼,å¤å®', |
| | | }, |
| | | { |
| | | key: 'rows', |
| | | label: 'æ°æ®è¡æ°', |
| | | type: 'number', |
| | | defaultValue: 2, |
| | | tip: 'ä»
ç¨äºç»å¸æçåèï¼æ¸²ææ¶å¯ç¨æ£éªé¡¹ç®æ°æ¿æ¢', |
| | | }, |
| | | ], |
| | | dataSchema: [], |
| | | buildContent: (properties) => ({ |
| | | tagName: 'div', |
| | | components: buildCells(parseHeaders(properties.headers), Number(properties.rows ?? 2)), |
| | | style: { margin: '0 0 8px' }, |
| | | }), |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { escapeHtmlMultiline } from '../core/html'; |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | export const textDefinition: QualityComponentDefinition = { |
| | | type: 'Text', |
| | | name: 'text', |
| | | label: 'ææ¬', |
| | | category: QUALITY_CATEGORY.BASIC, |
| | | icon: QUALITY_ICONS.text, |
| | | aiHint: |
| | | 'å
åºç纯æå段è½ãåªæå¨æ²¡æä»»ä½ä¸é¨ç»ä»¶åéæ¶æç¨æââ页çã页èãæ ·åä¿¡æ¯ãå¤å®ç»è®ºé½åæä¸é¨ç»ä»¶ï¼ä¸è¦å¾çäºé½ç¨æã', |
| | | contentKey: 'text', |
| | | defaults: { text: '卿¤è¾å
¥ææ¬å
容', align: 'left' }, |
| | | propertySchema: [ |
| | | { |
| | | key: 'text', |
| | | label: 'ææ¬å
容', |
| | | type: 'text', |
| | | defaultValue: '卿¤è¾å
¥ææ¬å
容', |
| | | bindable: true, |
| | | }, |
| | | { |
| | | key: 'align', |
| | | label: '坹齿¹å¼', |
| | | type: 'enum', |
| | | defaultValue: 'left', |
| | | options: [ |
| | | { label: '左对é½', value: 'left' }, |
| | | { label: 'å±
ä¸', value: 'center' }, |
| | | { label: 'å³å¯¹é½', value: 'right' }, |
| | | ], |
| | | }, |
| | | ], |
| | | dataSchema: [{ key: 'text', label: 'ææ¬å
容', type: 'string', bindable: true }], |
| | | buildContent: (properties) => ({ |
| | | tagName: 'p', |
| | | content: escapeHtmlMultiline(properties.text), |
| | | style: { |
| | | margin: '0 0 8px', |
| | | 'font-size': '12px', |
| | | 'line-height': '1.6', |
| | | 'text-align': String(properties.align ?? 'left'), |
| | | }, |
| | | }), |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * AI ç»ä»¶è稿 â ç»å¸æ°æ®ï¼è¯ä¹å±çéè¿ç®ï¼ |
| | | * <p> |
| | | * ä¸ use-designer ç collectSchema ææå¾è¿ï¼collectSchema ä»ç»å¸æ¶é {type, props}ï¼ |
| | | * æ¬æ¨¡åæ {type, props} è£
åç»å¸ã两è
å¿
é¡»é项对é½ï¼å¦åãAI çæ â ä¿å â åæå¼ã |
| | | * 伿ç»ä»¶æéä½å±æ§ã |
| | | * <p> |
| | | * 廿æ¾å¨ core/ è䏿¯è®¾è®¡å¨éï¼å®ä¸éè¦ editor å®ä¾ï¼æ¯çº¯å½æ°ï¼å¯åæµï¼ |
| | | * ä¹é¿å
components/ ååä¾èµ #/apiã |
| | | */ |
| | | import type { QualityComponentDefinition, QualityProps } from './types'; |
| | | |
| | | import { buildQualityContent } from './factory'; |
| | | import { getQualityComponent } from './registry'; |
| | | import { validateQualityProps } from './validator'; |
| | | |
| | | /** |
| | | * AI è稿éçä¸ä¸ªç»ä»¶ã |
| | | * <p> |
| | | * ç»æå®å
¨æå¹³ï¼æ²¡æ childrenãprops ç key ç±åç«¯ç§¯æ¨æ¸
åå®ä¹ï¼ |
| | | * æä»¥è¿éæ¯å®½æ¾ç Record è䏿¯å¼ºç±»åââåæ³ key çæå¨å¨æ³¨å表ã |
| | | */ |
| | | export interface QualityDraftComponent { |
| | | type?: string; |
| | | props?: Record<string, unknown>; |
| | | } |
| | | |
| | | /** 被跳è¿çè稿ç»ä»¶ */ |
| | | export interface QualityAssemblySkip { |
| | | type: string; |
| | | /** å±ç¤ºåï¼æªæ³¨åç type 没æå±ç¤ºåï¼åé为 type æ¬èº« */ |
| | | label: string; |
| | | reason: string; |
| | | } |
| | | |
| | | export interface QualityAssemblyResult { |
| | | /** å¯ç´æ¥äº¤ç» loadProjectData ç GrapesJS é¡¹ç®æ°æ® */ |
| | | grapes: Record<string, unknown>; |
| | | /** è¯å«å°äºä½è£
ä¸è¿ç»å¸çç»ä»¶ */ |
| | | skipped: QualityAssemblySkip[]; |
| | | /** è£
è¿å»äºãä½å±æ§ä¸å®æ´æä¸åæ³çç»ä»¶ï¼æç¤ºç¨ */ |
| | | problems: string[]; |
| | | } |
| | | |
| | | /** |
| | | * æè稿ç»ä»¶è£
é
æç»å¸æ°æ®ã |
| | | * <p> |
| | | * å个ç»ä»¶åæä¸å½±åå
¶ä½ï¼æªç¥ç±»åè¿ skippedï¼å±æ§æé®é¢çè¿ problemsï¼ |
| | | * é½ä¸ä¼è®©æ´æ¹å¤±è´¥ââAI çäº§ç©æ¬æ¥å°±ä¸å¯ä¿¡ï¼å
¨ææå
¨æ åªä¼è®©ç¨æ·ç½ç䏿¬¡è¯å«ã |
| | | */ |
| | | export function assembleQualityCanvas( |
| | | components?: QualityDraftComponent[], |
| | | ): QualityAssemblyResult { |
| | | const skipped: QualityAssemblySkip[] = []; |
| | | const problems: string[] = []; |
| | | const nodes: Record<string, unknown>[] = []; |
| | | |
| | | (components ?? []).forEach((draft, index) => { |
| | | const position = `第 ${index + 1} 个ç»ä»¶`; |
| | | const type = draft?.type ?? ''; |
| | | const definition = getQualityComponent(type); |
| | | if (!definition) { |
| | | skipped.push({ |
| | | type, |
| | | label: type || 'æªå½åç»ä»¶', |
| | | reason: `${position}ï¼ç±»åã${type || '空'}ãä¸å¨å½å设计å¨çç»ä»¶æ¸
åå
ï¼å·²è·³è¿`, |
| | | }); |
| | | return; |
| | | } |
| | | |
| | | const normalized = normalizeDraftProps(definition, draft?.props); |
| | | normalized.problems.forEach((message) => problems.push(`${position}ï¼${message}`)); |
| | | |
| | | // buildQualityContent åçæ£æ¯å¾è¿æéçåä»¶äºï¼defaults æåºãbuildContent çæç»æã |
| | | // propsToAttributes åªè¾åºå£°æè¿ç key å¹¶å符串åãåå
¥ data-quality-typeã |
| | | const content = buildQualityContent(definition, normalized.props) as Record<string, unknown>; |
| | | // traits ä¸è¿ç»å¸æ°æ®ï¼è½½å
¥åç± restoreQualityTraits æ data-quality-type éå»ºï¼ |
| | | // ä¸ãæå¼ä¸ä»½å·²ä¿åççæ¬ãèµ°çæ¯å䏿¡è·¯ï¼traits æ¬æ¥ä¹ä¸åä¸é¡¹ç®æ°æ®åºååï¼ã |
| | | delete content.traits; |
| | | nodes.push(content); |
| | | |
| | | validateQualityProps(definition, normalized.props).forEach((message) => |
| | | problems.push(`${position}ï¼${message}`), |
| | | ); |
| | | }); |
| | | |
| | | return { |
| | | // pages å¿
é¡»é空ï¼å¦å loadSchema ä¼èµ°ç©ºç½é¡µå
åºåæ¯ï¼è£
é
好çç»ä»¶å
¨é¨ä¸¢æ |
| | | grapes: { |
| | | pages: [{ component: { style: { padding: '0' }, components: nodes } }], |
| | | }, |
| | | skipped, |
| | | problems, |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * æ AI ç»ç屿§æ¶ææè¯ä¹å±è½æ¿è½½çåå§å¼ã |
| | | * <p> |
| | | * åªç®¡ä¸ä»¶äºï¼å¼å¿
é¡»æ¯ boolean / number / stringãæ¨¡åå¶å°ä¼æå段åæåµå¥å¯¹è±¡ææ°ç» |
| | | * ï¼æ¯å¦ææ£éªé¡¹æ°æ®è¡æ´æ®µå¡è¿ itemsPathï¼ï¼è¿ç±»å¼ç» coerceFieldValue ç String() ä¼åæ |
| | | * "[object Object]" åè¿ç»å¸ï¼å±æ§é¢æ¿éæ¾ç¤ºä¸ä¸²ä¹±ç ï¼è¿ä¼éé»çæé»è®¤å¼ââæä»¥å¨è¿éæ¦æå¹¶è¯´æã |
| | | * <p> |
| | | * 䏿£æ¥ key æ¯å¦å£°æè¿ï¼æªå£°æç key ç± propsToAttributes 丢å¼ï¼æ¬æ¥ä¹è¿ä¸äºç»å¸ã |
| | | */ |
| | | function normalizeDraftProps( |
| | | definition: QualityComponentDefinition, |
| | | props?: Record<string, unknown>, |
| | | ): { props: QualityProps; problems: string[] } { |
| | | const normalized: QualityProps = {}; |
| | | const problems: string[] = []; |
| | | if (!props) { |
| | | return { props: normalized, problems }; |
| | | } |
| | | definition.propertySchema.forEach((field) => { |
| | | const value = props[field.key]; |
| | | // null ä¸ undefined åä¹ï¼æ¨¡å没ç»å¼ï¼äº¤ç» defaults å
åº |
| | | if (value === undefined || value === null) { |
| | | return; |
| | | } |
| | | if ( |
| | | typeof value === 'boolean' || |
| | | typeof value === 'number' || |
| | | typeof value === 'string' |
| | | ) { |
| | | normalized[field.key] = value; |
| | | return; |
| | | } |
| | | problems.push( |
| | | `${definition.label}çã${field.label}ãæ¶å°çæ¯ç»æåæ°æ®ï¼è¯ä¹å±åªæ¿è½½å个å¼ï¼å·²æé»è®¤å¼å¤ç`, |
| | | ); |
| | | }); |
| | | return { props: normalized, problems }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * è´¨éç»ä»¶å¨ç»å¸ä¸çæ 记约å®ã |
| | | * <p> |
| | | * ç»å¸èç¹é 屿§èªæè¿°ï¼data-quality-type 声æå®æ¯åªä¸ªç»ä»¶ï¼ |
| | | * data-qc-<key> æ¿è½½ä¸å¡å±æ§å¼ï¼ä¸¤è
ä¸èµ·è®©ç»å¸ï¼grapes åå§æ°æ®ï¼æ¬èº«å°±è½è¿åä¸å¡è¯ä¹ã |
| | | */ |
| | | |
| | | /** æ è®°ç»ä»¶ç±»åç屿§å */ |
| | | export const QUALITY_TYPE_ATTR = 'data-quality-type'; |
| | | |
| | | /** ä¸å¡å±æ§åå
¥ç»å¸æ¶ç屿§åç¼ */ |
| | | export const QUALITY_PROP_PREFIX = 'data-qc-'; |
| | | |
| | | /** é夿¸²æå®¹å¨ï¼å¼ä¸ºæ°ç»å¨ä¸ä¸æä¸çè·¯å¾ï¼æ¸²ææææ°ç»é¿åº¦å¤å¶åè¡ */ |
| | | export const QUALITY_REPEAT_ATTR = `${QUALITY_PROP_PREFIX}repeat`; |
| | | |
| | | /** é夿¸²æçè¡æ¨¡æ¿ï¼å¼ä¸ºå¾ªç¯åéåï¼è¡å
{{åé.åæ®µ}} ç±è¯¥åéçå
ç´ è§£æ */ |
| | | export const QUALITY_REPEAT_ROW_ATTR = `${QUALITY_PROP_PREFIX}repeat-row`; |
| | | |
| | | /** |
| | | * 渲æäº§ç©ä¸æ¿è½½ç»ä»¶æ è¯ç屿§ã |
| | | * <p> |
| | | * GrapesJS ææ ·å¼æå¨ #<ç»ä»¶id> ä¸ï¼è id å¨éå¤è¡éå¿
ç¶æè½¦ï¼ |
| | | * æ¸²ææ¶ç»ä¸æ¹åæè¿ä¸ªå±æ§ï¼å¹¶ææ ·å¼éæ©å¨ä¸èµ·æ¹åï¼éå¤è¡æè½åèªå¸¦ä¸æ ·å¼ã |
| | | */ |
| | | export const QUALITY_ID_ATTR = `${QUALITY_PROP_PREFIX}id`; |
| | | |
| | | /** ä¸ä¸å¡å±æ§å
±ç¨åç¼ä½ä¸åä¸å±æ§è¯»åçä¿çå */ |
| | | const RESERVED_KEYS = new Set(['repeat', 'repeat-row', 'id']); |
| | | |
| | | /** ä¸å¡å±æ§ key 对åºçç»å¸å±æ§å */ |
| | | export function propAttrName(key: string): string { |
| | | return `${QUALITY_PROP_PREFIX}${key}`; |
| | | } |
| | | |
| | | /** 夿æä¸ªç»å¸å±æ§æ¯å¦å±äºä¸å¡å±æ§ */ |
| | | export function isQualityPropAttr(name: string): boolean { |
| | | return ( |
| | | name.startsWith(QUALITY_PROP_PREFIX) && |
| | | !RESERVED_KEYS.has(name.slice(QUALITY_PROP_PREFIX.length)) |
| | | ); |
| | | } |
| | | |
| | | /** ç»å¸å±æ§åè¿å为ä¸å¡å±æ§ key */ |
| | | export function propKeyFromAttr(name: string): string { |
| | | return name.slice(QUALITY_PROP_PREFIX.length); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * ç»å¸æåä¸ä¸å¡å±æ§çåå忥 |
| | | * <p> |
| | | * ç¨æ·å¨ç»å¸å
ç´æ¥æ¹æåï¼æ¹çæ¯ç»ä»¶ç contentï¼å¨å±æ§é¢æ¿æ¹å¼ï¼æ¹çæ¯ data-qc-* 屿§ã |
| | | * ä¿åæ¶ä»¥å±æ§ä¸ºåï¼ä¸æéçè¯ç»å¸éçä¿®æ¹ä¼å¨ä¸ä¸æ¬¡ä¿åæ¶è¢«éé»ä¸¢å¼ã |
| | | */ |
| | | import type { Component, Editor } from 'grapesjs'; |
| | | |
| | | import { QUALITY_TYPE_ATTR, propAttrName } from './attrs'; |
| | | import { buildQualityTraits } from './factory'; |
| | | import { definitionOf } from './lookup'; |
| | | |
| | | /** |
| | | * è½½å
¥æ¨¡æ¿åéå»ºå±æ§é¢æ¿å段ã |
| | | * <p> |
| | | * traits ä¸åä¸ GrapesJS é¡¹ç®æ°æ®çåºååï¼ä¿ååæå¼æ¶ç»ä»¶åªå©æ¡æ¶èªå¸¦ç id/title ä¸¤ä¸ªåæ®µï¼ |
| | | * è¿éæ data-quality-type 仿³¨å表é建ã屿§å¼ä»ä»¥ç»ä»¶ä¸ç data-qc-* 为å |
| | | * ï¼trait åå§åä¼å
读ç»ä»¶å±æ§ï¼è¯»ä¸å°æç¨é»è®¤å¼ï¼ï¼æä»¥ä¸ä¼æç¨æ·æ¹è¿çå¼å²æã |
| | | */ |
| | | export function restoreQualityTraits(editor: Editor): void { |
| | | editor |
| | | .getWrapper() |
| | | ?.find(`[${QUALITY_TYPE_ATTR}]`) |
| | | .forEach((component) => { |
| | | const definition = definitionOf(component); |
| | | if (definition) { |
| | | component.setTraits(buildQualityTraits(definition)); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | export function bindQualityCanvasSync(editor: Editor): void { |
| | | // ç»å¸å
æ¹æå â ååä¸å¡å±æ§ |
| | | editor.on('component:update:content', (component: Component) => { |
| | | const contentKey = definitionOf(component)?.contentKey; |
| | | if (!contentKey) { |
| | | return; |
| | | } |
| | | const text = component.get('content'); |
| | | if (typeof text !== 'string') { |
| | | return; |
| | | } |
| | | const attrName = propAttrName(contentKey); |
| | | if (component.getAttributes()[attrName] === text) { |
| | | return; |
| | | } |
| | | component.addAttributes({ [attrName]: text }); |
| | | }); |
| | | |
| | | // 屿§é¢æ¿æ¹å¼ â 忥ç»å¸å¯è§æå |
| | | editor.on('trait:value', ({ component, trait, value }) => { |
| | | const contentKey = definitionOf(component)?.contentKey; |
| | | if (!contentKey || typeof value !== 'string') { |
| | | return; |
| | | } |
| | | if (trait?.getName() !== propAttrName(contentKey)) { |
| | | return; |
| | | } |
| | | if (component.get('content') === value) { |
| | | return; |
| | | } |
| | | component.set('content', value); |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * è´¨éç»ä»¶æ³¨å表 â AI ç§¯æ¨æ¸
å |
| | | * <p> |
| | | * æ¸
åæ¯æ³¨å表ç**æ´¾çè§å¾**ï¼å¥æ icon / buildContent / validate è¿äºä¸å¯åºååçæåï¼ |
| | | * åªç䏿¨¡åéè¦ç¥éçãæåªäºç»ä»¶ãæ¯ä¸ªç»ä»¶è½å¡«åªäºå段ãã |
| | | * <p> |
| | | * è¿ä»½æ¸
åé请æ±ä¼ ç»å端æ¼è¿æç¤ºè¯ï¼ä½å®<b>䏿¯</b>åæ³æ§ç夿®ï¼çæ£æå
³çæ¯è£
é
å¨ |
| | | * å¯¹çæ´»æ³¨åè¡¨æ¥ getQualityComponentãæ¨¡åå³ä¾¿ç¼åºæ¸
åå¤ç typeï¼ä¹åªä¼è¢«è·³è¿ï¼ |
| | | * æ°¸è¿å䏿任æ HTMLï¼æ¸
åéä¸å«ä»»ä½ HTML è½åçæè¿°ï¼ã |
| | | */ |
| | | import type { QualityFieldSchema } from './types'; |
| | | |
| | | import { listQualityComponents } from './registry'; |
| | | |
| | | /** æ¸
åéçä¸ä¸ªå¯å¡«å段ï¼ä¸å端 QcReportComponentSpecVO.Field ä¸ä¸å¯¹åº */ |
| | | export interface QualityCatalogField { |
| | | key: string; |
| | | label: string; |
| | | type: QualityFieldSchema['type']; |
| | | required?: boolean; |
| | | bindable?: boolean; |
| | | /** |
| | | * æä¸¾åé项ã |
| | | * <p> |
| | | * ç¨ {label, value} æå¯¹ç»åºï¼åªç» label 模åä¼ç
§çè¾åºä¸ææ ç¾ï¼ |
| | | * åªç» value åä¸ç¥éå«ä¹ãæå¯¹ç»åºï¼å®æç¥é该è¾åºåªä¸ªåé¢éã |
| | | */ |
| | | enumOptions?: { label: string; value: number | string }[]; |
| | | } |
| | | |
| | | /** ä¸ä¸ªç»ä»¶ç§¯æ¨ */ |
| | | export interface QualityComponentCatalog { |
| | | type: string; |
| | | label: string; |
| | | /** ç»ä»¶åç±»ç䏿æ¾ç¤ºåï¼å¸®æ¨¡åçè§£ç»ä»¶ç¨é */ |
| | | category: string; |
| | | /** |
| | | * ç»ä»¶çéå说æï¼æ¥èªæ³¨å表ç aiHintã |
| | | * <p> |
| | | * åªæ label æ¶ï¼æ¨¡åé¢å¯¹ãæ¥åæ¬å¤´ãè¿ç±»å
容ä¼å¨ Heading / Text / ReportHeader ä¹é´ççï¼ |
| | | * 䏿»ä¼æå段æç®åçåºç¡ç»ä»¶ãè¿æ®µæåå°±æ¯ç»å®åè¾¹ççï¼ä¸æ¯ç»äººççã |
| | | */ |
| | | hint?: string; |
| | | fields: QualityCatalogField[]; |
| | | } |
| | | |
| | | /** 仿´»æ³¨å表çæç§¯æ¨æ¸
åï¼é¡ºåºä¸è®¾è®¡å¨å·¦ä¾§é¢æ¿ä¸è´ */ |
| | | export function buildQualityComponentCatalog(): QualityComponentCatalog[] { |
| | | const definitions = listQualityComponents(); |
| | | // 注åè¡¨è¦æ¾å¼è°ç¨ registerAllQualityComponents() ææå
容ï¼è®¾è®¡å¨å¨ init æ¶åï¼ã |
| | | // 空æ¸
åä¼è¢«å端å¤ä¸ºå
¥åéæ³ï¼æ¥åºæ¥çéåçå®åå éäºå¥½å å±ï¼æä»¥å¨è¿é就说æ¸
æ¥ã |
| | | if (definitions.length === 0) { |
| | | throw new Error('è´¨éç»ä»¶æ³¨å表为空ï¼è¯·å
è°ç¨ registerAllQualityComponents()'); |
| | | } |
| | | return definitions.map((definition) => { |
| | | const item: QualityComponentCatalog = { |
| | | type: definition.type, |
| | | label: definition.label, |
| | | category: definition.category, |
| | | fields: definition.propertySchema.map(toCatalogField), |
| | | }; |
| | | // 缺å¸å³çç¥ï¼ä¸ toCatalogField åä¸é£æ ¼ï¼çæçåæ®µè½ææ¾ç¼©å°æç¤ºè¯ |
| | | if (definition.aiHint) { |
| | | item.hint = definition.aiHint; |
| | | } |
| | | return item; |
| | | }); |
| | | } |
| | | |
| | | function toCatalogField(field: QualityFieldSchema): QualityCatalogField { |
| | | const item: QualityCatalogField = { |
| | | key: field.key, |
| | | label: field.label, |
| | | type: field.type, |
| | | }; |
| | | // 缺å¸å³ãå¦ãï¼çæçåæ®µè½ææ¾ç¼©å°æç¤ºè¯ |
| | | if (field.required) { |
| | | item.required = true; |
| | | } |
| | | if (field.bindable) { |
| | | item.bindable = true; |
| | | } |
| | | if (field.options?.length) { |
| | | item.enumOptions = field.options.map((option) => ({ |
| | | label: option.label, |
| | | value: option.value, |
| | | })); |
| | | } |
| | | return item; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * ç»ä»¶åå®ä¹ / 表头跨åçå符串解æã |
| | | * <p> |
| | | * ãåãè¿ä»¶äºå¿
é¡»è½è¢«ç¼ç æ**ä¸ä¸ªæ éå符串**ï¼ç»ä»¶ä¸å¡å±æ§çç±»ååªæ |
| | | * boolean / enum / number / string / textï¼è§ {@link QualityFieldSchema}ï¼ï¼ |
| | | * æç»è¿ä¼è½å°ç»å¸èç¹ç attributes ä¸ï¼èé£é乿¯å符串æ å°ã |
| | | * ç¨ JSON ç¼ç åæ°ç»ä¼è¦æ±æ¨¡åè¾åºè½¬ä¹åçåµå¥ JSONï¼æ¯å®åºéçéç¾åºï¼ |
| | | * ä¸è¡ `æ é¢=ç»å®|æ é¢=ç»å®` ä¿¡æ¯éç¸åï¼æ¨¡åã人é½å¥½åã |
| | | * <p> |
| | | * å¿
é¡»åè¡ï¼è¿ä¸²ä¸è¥¿æç»æ¯ HTML 屿§å¼ï¼æ¢è¡å¨å±æ§éåæ³ä½å®¹æè¢« |
| | | * ç¼è¾å¨/åºååç¯èæ¹åï¼ç»ä¸æåè¡å¤çã |
| | | * <p> |
| | | * è§£ææ¾å¨è¿éè䏿¯åç»ä»¶å
é¨ï¼æ¯å ä¸ºæ ¡éªåä¸è¡¨å¤´è·¨åé½ä¾èµåä¸å¥è¯æ³ï¼ |
| | | * 两份å®ç°è¿æ©æ¼ç§»ï¼èæ¼ç§»çè¡¨ç°æ¯ã忰坹ä¸ä¸ãè¿ç§å¾é¾æ¥ç渲æäºæ
ã |
| | | */ |
| | | |
| | | /** ä¸åï¼è¡¨å¤´æå + è¡å
åå¼è¡¨è¾¾å¼ */ |
| | | export interface QualityColumnSpec { |
| | | /** 表头æåï¼å·²å¥æåå¹¶æ è®°åç¼ */ |
| | | label: string; |
| | | /** è¡å
åå¼è¡¨è¾¾å¼ï¼å¦ {{item.itemName}} */ |
| | | expr: string; |
| | | /** æ¯å¦çºµååå¹¶è·¨ä½æ´ç»ï¼æ é¢ååäº #ï¼ */ |
| | | merge: boolean; |
| | | } |
| | | |
| | | /** ä¸å±è¡¨å¤´çä¸ä¸ªåå
æ ¼ */ |
| | | export interface QualityHeaderSpan { |
| | | label: string; |
| | | span: number; |
| | | } |
| | | |
| | | /** è§£æç»æï¼è½è§£æåºå¤å°ç®å¤å°ï¼éè¯¯éæ¡åæ¥ï¼ç±è°ç¨æ¹å³å®æ¯å¦æ¦ä¸ä¿å */ |
| | | export interface QualitySpecParseResult<T> { |
| | | items: T[]; |
| | | errors: string[]; |
| | | } |
| | | |
| | | /** åä¸åä¹é´çåé符 */ |
| | | const SEGMENT_SEPARATOR = '|'; |
| | | |
| | | /** åå¹¶åçæ è®°åç¼ */ |
| | | const MERGE_MARK = '#'; |
| | | |
| | | /** 表头跨åçè®¡æ°æ è®° */ |
| | | const SPAN_MARK = '^'; |
| | | |
| | | /** |
| | | * è§£æåå®ä¹ä¸²ã |
| | | * |
| | | * @param spec å½¢å¦ `åºå·={{index}}|#æ£éªé¡¹ç®={{item.itemName}}|å项={{item.group.childName}}` |
| | | * @returns åå®ä¹ä¸é误æ¸
åï¼ç©ºä¸²è¿å空æ°ç»ï¼ç±è°ç¨æ¹åè½å°ç»ä»¶é»è®¤å |
| | | */ |
| | | export function parseColumnSpec(spec: string | undefined): QualitySpecParseResult<QualityColumnSpec> { |
| | | const errors: string[] = []; |
| | | const items: QualityColumnSpec[] = []; |
| | | const segments = splitSegments(spec); |
| | | segments.forEach((segment, index) => { |
| | | const position = `第 ${index + 1} 段ã${segment}ã`; |
| | | const separatorAt = segment.indexOf('='); |
| | | if (separatorAt < 0) { |
| | | errors.push(`${position}缺å°ã=ãï¼æ¯ä¸åé½è¦åæãåæ é¢=ç»å®è¡¨è¾¾å¼ã`); |
| | | return; |
| | | } |
| | | const rawLabel = segment.slice(0, separatorAt).trim(); |
| | | const expr = segment.slice(separatorAt + 1).trim(); |
| | | const merge = rawLabel.startsWith(MERGE_MARK); |
| | | const label = merge ? rawLabel.slice(MERGE_MARK.length).trim() : rawLabel; |
| | | if (!label) { |
| | | errors.push(`${position}çåæ é¢æ¯ç©ºçï¼è¯·è¡¥ä¸æå°å¨è¡¨å¤´ä¸çæå`); |
| | | return; |
| | | } |
| | | if (!expr) { |
| | | errors.push(`${position}çç»å®è¡¨è¾¾å¼æ¯ç©ºçï¼è¯·è¡¥ä¸ã=ãåé¢çå
容ï¼å¦ {{item.itemName}}`); |
| | | return; |
| | | } |
| | | items.push({ label, expr, merge }); |
| | | }); |
| | | return { items, errors }; |
| | | } |
| | | |
| | | /** |
| | | * è§£æä¸å±è¡¨å¤´ä¸²ã |
| | | * |
| | | * @param spec å½¢å¦ `æ£éªç»æ^5|ç»è®º^1`ï¼åä¸åº `^N` çæ 1 åç®ï¼é æ»æ°æ ¡éªå
åº |
| | | * @param columnCount ä¸å±å®é
åæ°ï¼ç¨æ¥æ ¡éªåæ®µè·¨åæ°ä¹å |
| | | */ |
| | | export function parseHeaderSpans( |
| | | spec: string | undefined, |
| | | columnCount: number, |
| | | ): QualitySpecParseResult<QualityHeaderSpan> { |
| | | const errors: string[] = []; |
| | | const items: QualityHeaderSpan[] = []; |
| | | const segments = splitSegments(spec); |
| | | // 没填就æ¯ãä¸è¦ä¸å±è¡¨å¤´ãï¼ä¸æ¯ãè·¨åæ°å计为 0ãââä¸è½æ¿å®å»ææ»æ°æ ¡éª |
| | | if (segments.length === 0) { |
| | | return { items, errors }; |
| | | } |
| | | segments.forEach((segment, index) => { |
| | | const position = `第 ${index + 1} 段ã${segment}ã`; |
| | | const markAt = segment.lastIndexOf(SPAN_MARK); |
| | | const rawLabel = (markAt < 0 ? segment : segment.slice(0, markAt)).trim(); |
| | | const rawSpan = markAt < 0 ? '' : segment.slice(markAt + 1).trim(); |
| | | if (!rawLabel) { |
| | | errors.push(`${position}ç表头æåæ¯ç©ºçï¼è¯·è¡¥ä¸è·¨è¶è¿å åçåç»æ é¢`); |
| | | return; |
| | | } |
| | | const span = rawSpan ? Number(rawSpan) : 1; |
| | | if (!Number.isInteger(span) || span < 1) { |
| | | errors.push(`${position}çè·¨åæ°ã${rawSpan}ã䏿¯æ£æ´æ°ï¼è¯·åæå¦ æ£éªç»æ^5 çå½¢å¼`); |
| | | return; |
| | | } |
| | | items.push({ label: rawLabel, span }); |
| | | }); |
| | | if (errors.length > 0) { |
| | | return { items, errors }; |
| | | } |
| | | const total = items.reduce((sum, item) => sum + item.span, 0); |
| | | if (total !== columnCount) { |
| | | errors.push( |
| | | `è¡¨å¤´è·¨åæ°å计 ${total}ï¼æ£éªè¡¨çå®é
åæ°ä¸º ${columnCount}ï¼ä¸¤è
å¿
é¡»ç¸çã` + |
| | | `è¯·æ£æ¥ã表头跨åãéåæ®µæ«å°¾ ^ åé¢çæ°å`, |
| | | ); |
| | | } |
| | | return { items, errors }; |
| | | } |
| | | |
| | | /** æ `|` åæ®µï¼ç©ºæ®µï¼é¦å°¾å¤åäºåé符ï¼ç´æ¥è·³è¿ï¼ä¸å½ä½é误ï¼é¿å
ææ»å°±æ¥é */ |
| | | function splitSegments(spec: string | undefined): string[] { |
| | | if (!spec) { |
| | | return []; |
| | | } |
| | | return spec |
| | | .split(SEGMENT_SEPARATOR) |
| | | .map((segment) => segment.trim()) |
| | | .filter((segment) => segment.length > 0); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * è´¨éç»ä»¶ â GrapesJS äº§ç© |
| | | * <p> |
| | | * åè®®ï¼QualityComponentDefinitionï¼ä¸ GrapesJS çæ¡¥æ¥åªå¨è¿ä¸å±ï¼ |
| | | * ç»ä»¶åãç»å¸å
容ã屿§é¢æ¿ï¼traitsï¼é½ç± definition æ¨å¯¼ï¼ç»ä»¶å®ç°éä¸åºç° GrapesJS æ¦å¿µã |
| | | */ |
| | | import type { BlockProperties, ComponentAdd, TraitProperties } from 'grapesjs'; |
| | | |
| | | import type { |
| | | QualityComponentContent, |
| | | QualityComponentDefinition, |
| | | QualityFieldSchema, |
| | | QualityProps, |
| | | } from './types'; |
| | | |
| | | import { QUALITY_TYPE_ATTR, propAttrName } from './attrs'; |
| | | import { listQualityComponents } from './registry'; |
| | | import { propsToAttributes } from './serializer'; |
| | | import { BINDING_TRAIT_TYPE } from './trait-binding'; |
| | | import { resolveQualityProps } from './validator'; |
| | | |
| | | /** è½å°ç»å¸ä¸çç»ä»¶å®ä¹ï¼åè®®å
容 + 屿§é¢æ¿å段 */ |
| | | export interface QualityCanvasComponent extends QualityComponentContent { |
| | | traits?: TraitProperties[]; |
| | | } |
| | | |
| | | /** definition + 屿§ â ç»å¸å
容ï¼attributes é带ä¸ç±»åæ è®°ä¸å
¨é¨ä¸å¡å±æ§ */ |
| | | export function buildQualityContent( |
| | | definition: QualityComponentDefinition, |
| | | props?: QualityProps, |
| | | ): QualityCanvasComponent { |
| | | const resolved = resolveQualityProps(definition, props); |
| | | const content = definition.buildContent(resolved); |
| | | return { |
| | | ...content, |
| | | attributes: { |
| | | ...content.attributes, |
| | | [QUALITY_TYPE_ATTR]: definition.type, |
| | | ...propsToAttributes(definition, resolved), |
| | | }, |
| | | traits: buildQualityTraits(definition), |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * 屿§é¢æ¿å段ã |
| | | * <p> |
| | | * é»è®¤ changeProp 为 falseï¼trait å¼ç´æ¥ååç»ä»¶ç data-qc-<key> 屿§ï¼ |
| | | * ä¸ serializer ç读åè·¯å¾ä¿æä¸è´ã |
| | | */ |
| | | export function buildQualityTraits( |
| | | definition: QualityComponentDefinition, |
| | | ): TraitProperties[] { |
| | | return definition.propertySchema.map((field) => traitFromField(definition, field)); |
| | | } |
| | | |
| | | function traitFromField( |
| | | definition: QualityComponentDefinition, |
| | | field: QualityFieldSchema, |
| | | ): TraitProperties { |
| | | const base: TraitProperties = { |
| | | name: propAttrName(field.key), |
| | | label: field.label, |
| | | placeholder: field.placeholder, |
| | | }; |
| | | // å¯ç»å®å段ç¨èªå®ä¹æ§ä»¶ï¼æ¢è½æå·¥è¾å
¥ï¼ä¹è½ä»æ°æ®å段éæï¼æä¸çåæ {{è·¯å¾}} |
| | | if (field.bindable && (field.type === 'string' || field.type === 'text')) { |
| | | return { ...base, type: BINDING_TRAIT_TYPE }; |
| | | } |
| | | switch (field.type) { |
| | | case 'boolean': { |
| | | return { |
| | | ...base, |
| | | type: 'checkbox', |
| | | // æ¾å¼å£°æç/ååé¢éï¼DOM 屿§åªè½æ¯å符串ï¼é 宿è½ç¨³å®å¾è¿ |
| | | valueTrue: 'true', |
| | | valueFalse: 'false', |
| | | default: String(field.defaultValue ?? false), |
| | | }; |
| | | } |
| | | case 'enum': { |
| | | return { |
| | | ...base, |
| | | type: 'select', |
| | | options: (field.options ?? []).map((option) => ({ |
| | | id: String(option.value), |
| | | name: option.label, |
| | | })), |
| | | default: field.defaultValue === undefined ? undefined : String(field.defaultValue), |
| | | }; |
| | | } |
| | | case 'number': { |
| | | return { |
| | | ...base, |
| | | type: 'number', |
| | | default: field.defaultValue === undefined ? undefined : String(field.defaultValue), |
| | | }; |
| | | } |
| | | case 'text': { |
| | | return { ...base, type: 'textarea' }; |
| | | } |
| | | default: { |
| | | return { ...base, type: 'text' }; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** definition â 左侧颿¿çä¸ä¸ªç»ä»¶å */ |
| | | export function buildQualityBlock(definition: QualityComponentDefinition): BlockProperties { |
| | | return { |
| | | id: `quality-${definition.type}`, |
| | | label: definition.label, |
| | | category: definition.category, |
| | | media: definition.icon, |
| | | content: buildQualityContent(definition) as unknown as BlockProperties['content'], |
| | | // ç¹å»ç»ä»¶å追å å°çº¸å¼ æ«å°¾ãGrapesJS é»è®¤è¿½å è¿ãå½åéä¸çç»ä»¶ãå
é¨ï¼ |
| | | // 卿¥åè¿ç§æå¹³ç»æé伿æ°ç»ä»¶å¡è¿æ é¢ãè¡¨æ ¼åå
æ ¼çå
ç´ ï¼è¿ä¼é¡¶æå®ä»¬åæçææ¬ã |
| | | // éè¦åµå¥æ¶ä»å¯ç¨ææ½ç²¾ç¡®è½ç¹ã |
| | | onClick: (block, editor) => { |
| | | const content = block.get('content') as ComponentAdd; |
| | | editor.getWrapper()?.append(content); |
| | | }, |
| | | }; |
| | | } |
| | | |
| | | /** 注å表éçå
¨é¨ç»ä»¶ â GrapesJS ç»ä»¶åå表 */ |
| | | export function buildQualityBlocks(): BlockProperties[] { |
| | | return listQualityComponents().map(buildQualityBlock); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * ç»å¸å
容䏿¸²æäº§ç©é½èµ° HTML å符串ï¼ä»»ä½æ¥èªç¨æ·ï¼è®¾è®¡å¨å±æ§é¢æ¿ï¼çææ¬ |
| | | * é½å¿
é¡»å
转ä¹åæ¼è¿æ ç¾ï¼å¦å卿¸²ææä¼åææ³¨å
¥ç¹ã |
| | | */ |
| | | export function escapeHtml(value: unknown): string { |
| | | return String(value ?? '') |
| | | .replaceAll('&', '&') |
| | | .replaceAll('<', '<') |
| | | .replaceAll('>', '>') |
| | | .replaceAll('"', '"') |
| | | .replaceAll("'", '''); |
| | | } |
| | | |
| | | /** |
| | | * 转ä¹ä¹ååææ¢è¡ææ <br>ã |
| | | * <p> |
| | | * 页èè½æ¬¾ãå¤è¡ææ¬å¸¸å¨ä¸ä¸ªå±æ§éå¡å¤è¡ï¼AI 导å
¥ä¼æå件页èä¸ç¾ç½²è¡åå¹¶æä¸æ®µï¼ï¼ |
| | | * åªè½¬ä¹çè¯æ¢è¡ç¬¦ä¼è¢« HTML çç©ºç½æå åæï¼å è¡å
å®¹è¿æä¸æ´çã |
| | | */ |
| | | export function escapeHtmlMultiline(value: unknown): string { |
| | | return escapeHtml(value).replace(/\r\n?|\n/g, '<br>'); |
| | | } |
| | | |
| | | /** æ¼ä¸æ®µå
èæ ·å¼ï¼è¿æ»¤æç©ºå¼ */ |
| | | export function cssStyle(style: Record<string, number | string | undefined>): string { |
| | | return Object.entries(style) |
| | | .filter(([, value]) => value !== undefined && value !== '') |
| | | .map(([key, value]) => `${key}:${value}`) |
| | | .join(';'); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** 左侧ç»ä»¶é¢æ¿å¾æ ï¼å
è SVGï¼è·éå½åæåé¢è² */ |
| | | export function qualityIcon(paths: string): string { |
| | | return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" |
| | | stroke-linecap="round" stroke-linejoin="round" width="22" height="22">${paths}</svg>`; |
| | | } |
| | | |
| | | export const QUALITY_ICONS = { |
| | | heading: qualityIcon('<path d="M6 4v16M18 4v16M6 12h12"/>'), |
| | | text: qualityIcon('<path d="M4 6h16M4 12h16M4 18h10"/>'), |
| | | image: qualityIcon( |
| | | '<rect x="3" y="4" width="18" height="16" rx="2"/><circle cx="8.5" cy="9.5" r="1.5"/><path d="m21 16-5-5-6 6-3-3-4 4"/>', |
| | | ), |
| | | divider: qualityIcon('<path d="M3 12h18"/>'), |
| | | spacer: qualityIcon('<path d="M12 4v16M8 4h8M8 20h8"/>'), |
| | | table: qualityIcon( |
| | | '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 10h18M3 15h18M9 10v10M15 10v10"/>', |
| | | ), |
| | | header: qualityIcon('<rect x="3" y="4" width="18" height="6" rx="1"/><path d="M6 7h6"/>'), |
| | | footer: qualityIcon( |
| | | '<rect x="3" y="14" width="18" height="6" rx="1"/><path d="M6 17h6"/>', |
| | | ), |
| | | sample: qualityIcon( |
| | | '<rect x="3" y="5" width="18" height="14" rx="2"/><path d="M7 9h4M7 13h6M15 9h2"/>', |
| | | ), |
| | | inspection: qualityIcon( |
| | | '<path d="M9 4h6v3H9z"/><rect x="4" y="7" width="16" height="14" rx="2"/><path d="m8 14 2.5 2.5L16 11"/>', |
| | | ), |
| | | qualityTable: qualityIcon( |
| | | '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M8 9v11M13 9v11M3 14h18"/>', |
| | | ), |
| | | groupedTable: qualityIcon( |
| | | '<rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 9h18M8 9v11M8 14h13"/>', |
| | | ), |
| | | result: qualityIcon( |
| | | '<circle cx="12" cy="12" r="8"/><path d="m8.5 12.5 2.5 2.5 4.5-5"/>', |
| | | ), |
| | | } as const; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * ç»å¸èç¹ â è´¨éç»ä»¶å®ä¹ç忥ã |
| | | * <p> |
| | | * åç¬ææä»¶ï¼canvas-sync ä¸ trait-binding é½è¦ç¨å®ï¼ |
| | | * æ¾å¨ä»»ä¸æ¹é½ä¼è®©ã屿§é¢æ¿æ§ä»¶ãåãç»å¸åæ¥ãäºç¸ä¾èµã |
| | | */ |
| | | import type { Component } from 'grapesjs'; |
| | | |
| | | import type { QualityComponentDefinition } from './types'; |
| | | |
| | | import { QUALITY_TYPE_ATTR } from './attrs'; |
| | | import { getQualityComponent } from './registry'; |
| | | |
| | | /** æèç¹ä¸ç data-quality-type æ¥å®ä¹ï¼ä¸æ¯è´¨éç»ä»¶æ¶è¿å undefined */ |
| | | export function definitionOf(component?: Component): QualityComponentDefinition | undefined { |
| | | const attributes = component?.getAttributes() ?? {}; |
| | | return getQualityComponent(String(attributes[QUALITY_TYPE_ATTR] ?? '')); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * è´¨éç»ä»¶æ³¨å表 |
| | | * <p> |
| | | * å
¨å¹³å°å¯ä¸çç»ä»¶æ¥æºï¼è®¾è®¡å¨å·¦ä¾§é¢æ¿ã屿§é¢æ¿ã渲æå¼æãæ ¡éªé½ä»è¿éåå®ä¹ã |
| | | * æ°å¢ç»ä»¶åªéè¦å¨ index.ts é夿³¨åä¸ä¸ª definitionï¼ä¸æ¹å¨ GrapesJS éæä»£ç ã |
| | | */ |
| | | import type { |
| | | QualityCategory, |
| | | QualityComponentDefinition, |
| | | } from './types'; |
| | | |
| | | import { QUALITY_CATEGORY_ORDER } from './types'; |
| | | |
| | | const definitions = new Map<string, QualityComponentDefinition>(); |
| | | |
| | | /** 注åå个ç»ä»¶ï¼å type é夿³¨åæè¦çå¤çï¼ä¾¿äºå¼åæçæ´æ° */ |
| | | export function registerQualityComponent(definition: QualityComponentDefinition): void { |
| | | if (!definition.type) { |
| | | throw new Error('è´¨éç»ä»¶å®ä¹ç¼ºå° type'); |
| | | } |
| | | definitions.set(definition.type, definition); |
| | | } |
| | | |
| | | /** æ¹é注å */ |
| | | export function registerQualityComponents(list: QualityComponentDefinition[]): void { |
| | | list.forEach(registerQualityComponent); |
| | | } |
| | | |
| | | /** æç±»ååå®ä¹ï¼æªæ³¨åè¿å undefined */ |
| | | export function getQualityComponent(type?: string): QualityComponentDefinition | undefined { |
| | | return type ? definitions.get(type) : undefined; |
| | | } |
| | | |
| | | /** å
¨é¨ç»ä»¶ï¼æåç±»é¡ºåºæåï¼ååç±»å
ææ³¨å顺åºï¼ */ |
| | | export function listQualityComponents(): QualityComponentDefinition[] { |
| | | return [...definitions.values()].sort( |
| | | (a, b) => categoryIndex(a.category) - categoryIndex(b.category), |
| | | ); |
| | | } |
| | | |
| | | /** æåç±»åç»ï¼ç©ºåç±»ä¸è¿å */ |
| | | export function listQualityComponentsByCategory(): { |
| | | category: QualityCategory; |
| | | components: QualityComponentDefinition[]; |
| | | }[] { |
| | | const grouped = new Map<QualityCategory, QualityComponentDefinition[]>(); |
| | | listQualityComponents().forEach((definition) => { |
| | | const list = grouped.get(definition.category) ?? []; |
| | | list.push(definition); |
| | | grouped.set(definition.category, list); |
| | | }); |
| | | return QUALITY_CATEGORY_ORDER.filter((category) => grouped.has(category)).map( |
| | | (category) => ({ category, components: grouped.get(category)! }), |
| | | ); |
| | | } |
| | | |
| | | function categoryIndex(category: QualityCategory): number { |
| | | const index = QUALITY_CATEGORY_ORDER.indexOf(category); |
| | | return index === -1 ? QUALITY_CATEGORY_ORDER.length : index; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * è´¨é屿§ â ç»å¸ attributes çååè½¬æ¢ |
| | | * <p> |
| | | * ä¸å¡å±æ§ç»ä¸è½å¨ç»å¸èç¹ç data-qc-<key> ä¸ï¼ç»å¸æ°æ®ï¼grapesï¼æ¬èº«å³èªæè¿°ï¼ |
| | | * ä¿åãåæ¾ãæ¸²æé½ä¸éè¦åé¢å¤ç»´æ¤ä¸ä»½æ å°è¡¨ã |
| | | */ |
| | | import type { |
| | | QualityComponentDefinition, |
| | | QualityProps, |
| | | } from './types'; |
| | | |
| | | import { isQualityPropAttr, propAttrName, propKeyFromAttr } from './attrs'; |
| | | import { coerceFieldValue } from './validator'; |
| | | |
| | | /** ä¸å¡å±æ§ â ç»å¸ attributesï¼å
¨é¨å符串åï¼DOM 屿§åªè½æ¯åç¬¦ä¸²ï¼ */ |
| | | export function propsToAttributes( |
| | | definition: QualityComponentDefinition, |
| | | props: QualityProps, |
| | | ): Record<string, string> { |
| | | const attributes: Record<string, string> = {}; |
| | | definition.propertySchema.forEach((field) => { |
| | | const value = props[field.key]; |
| | | if (value === undefined) { |
| | | return; |
| | | } |
| | | attributes[propAttrName(field.key)] = String(value); |
| | | }); |
| | | return attributes; |
| | | } |
| | | |
| | | /** ç»å¸ attributes â ä¸å¡å±æ§ï¼æå段声æè¿åç±»åï¼ */ |
| | | export function attributesToProps( |
| | | definition: QualityComponentDefinition, |
| | | attributes: Record<string, string>, |
| | | ): QualityProps { |
| | | const props: QualityProps = {}; |
| | | definition.propertySchema.forEach((field) => { |
| | | const raw = attributes[propAttrName(field.key)]; |
| | | if (raw === undefined) { |
| | | return; |
| | | } |
| | | props[field.key] = coerceFieldValue(field, raw); |
| | | }); |
| | | return props; |
| | | } |
| | | |
| | | /** ååºèç¹ä¸ææä¸å¡å±æ§ï¼ä¸è®¤è¯å®ä¹æ¶ä¹è½è¯»ï¼ç¨äºè¯æä¸è¿ç§»ï¼ */ |
| | | export function extractRawProps(attributes: Record<string, string>): QualityProps { |
| | | const props: QualityProps = {}; |
| | | Object.entries(attributes).forEach(([name, value]) => { |
| | | if (isQualityPropAttr(name)) { |
| | | props[propKeyFromAttr(name)] = value; |
| | | } |
| | | }); |
| | | return props; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * ãç»å®æ°æ®ã屿§æ§ä»¶ |
| | | * <p> |
| | | * 声æäº bindable çåæ®µï¼å±æ§é¢æ¿é¤äºè½æå·¥è¾å
¥ï¼è¿å¯ä»¥ä»ç»ä»¶ dataSchema éæä¸ä¸ªæ°æ®å段ã |
| | | * æä¸ååè¿å±æ§å¼ç仿¯æ®éææ¬ï¼æ¸²æå¼ææç»å®å议解æï¼ |
| | | * æä»¥ç»å®ä¸æå·¥è¾å
¥èµ°çæ¯å䏿¡è·¯å¾ï¼ä¸åå¨ä¸¤å¥åå¼é»è¾ã |
| | | */ |
| | | import type { Component, Editor, Trait } from 'grapesjs'; |
| | | |
| | | import type { QualityFieldSchema } from './types'; |
| | | |
| | | import { propAttrName } from './attrs'; |
| | | import { definitionOf } from './lookup'; |
| | | |
| | | /** ç»å®æ§ä»¶å¨ GrapesJS éæ³¨åçç±»åå */ |
| | | export const BINDING_TRAIT_TYPE = 'qc-binding'; |
| | | |
| | | /** ä¸ç»å®æ¶ç空é项 */ |
| | | const NO_BINDING_LABEL = 'ä¸ç»å®ï¼æå·¥è¾å
¥ï¼'; |
| | | |
| | | const SELECT_CLASS = 'qc-binding-select'; |
| | | const INPUT_CLASS = 'qc-binding-input'; |
| | | const HINT_CLASS = 'qc-binding-hint'; |
| | | |
| | | /** ç»å®å¼åå屿§çå½¢å¼ï¼ä¸ QualityFieldSchema.bindAs å¯¹åº */ |
| | | type BindMode = 'path' | 'text'; |
| | | |
| | | /** ç»å®è·¯å¾ â 屿§å¼ï¼text å
æ {{path}} 交渲ææè§£æï¼path å¿
é¡»ä¿æè£¸è·¯å¾ */ |
| | | function toBindingValue(path: string, mode: BindMode): string { |
| | | if (!path) { |
| | | return ''; |
| | | } |
| | | return mode === 'path' ? path : `{{${path}}}`; |
| | | } |
| | | |
| | | /** 屿§å¼ â ç»å®è·¯å¾ï¼è¯å«ä¸åºå°±è¿å空串ï¼è¡¨ç¤ºè¿æ¯æå·¥è¾å
¥èéç»å® */ |
| | | function fromBindingValue(value: string, mode: BindMode): string { |
| | | const trimmed = value.trim(); |
| | | if (mode === 'path') { |
| | | return trimmed; |
| | | } |
| | | const match = /^\{\{\s*([^{}]+?)\s*\}\}$/.exec(trimmed); |
| | | return match?.[1]?.trim() ?? ''; |
| | | } |
| | | |
| | | interface BindingInputParts { |
| | | input: HTMLInputElement; |
| | | select: HTMLSelectElement; |
| | | } |
| | | |
| | | /** |
| | | * èªå®ä¹ trait åè°çå
¥åã |
| | | * <p> |
| | | * TraitManager.addType çæ³åä¸ä¼æåæ°ç±»åæ¨å¯¼å°åè°ä¸ï¼ä¸æ¾å¼æ æ³¨å°±ä¼æ¯éå¼ anyã |
| | | * å½¢ç¶ä¸ TraitView å
é¨ getClbOpts() çè¿åå¼ä¸è´ã |
| | | */ |
| | | interface TraitViewOpts { |
| | | component: Component; |
| | | elInput: HTMLInputElement; |
| | | trait: Trait; |
| | | } |
| | | |
| | | /** æ¾å°å½å trait 对åºç屿§å段ï¼åå®çç»å®å½¢å¼ä¸æç¤º */ |
| | | function fieldOf(component: Component | undefined, trait: Trait): QualityFieldSchema | undefined { |
| | | const name = trait.getName(); |
| | | return definitionOf(component)?.propertySchema.find((field) => propAttrName(field.key) === name); |
| | | } |
| | | |
| | | function bindModeOf(component: Component | undefined, trait: Trait): BindMode { |
| | | return fieldOf(component, trait)?.bindAs === 'path' ? 'path' : 'text'; |
| | | } |
| | | |
| | | /** å¨å±æ§é¢æ¿é建åºã䏿鿩 + æå·¥è¾å
¥ + æç¤ºãä¸ä»¶å¥ */ |
| | | function createBindingInput(trait: Trait, component?: Component): HTMLElement { |
| | | const definition = definitionOf(component); |
| | | const wrap = document.createElement('div'); |
| | | wrap.className = 'qc-binding'; |
| | | |
| | | const select = document.createElement('select'); |
| | | select.className = SELECT_CLASS; |
| | | select.append(new Option(NO_BINDING_LABEL, '')); |
| | | (definition?.dataSchema ?? []).forEach((field) => { |
| | | // é项å¼å°±æ¯ç»å®è·¯å¾ï¼æ ç¾ç»ç¨æ·çæç¨é |
| | | select.append(new Option(field.label, field.key)); |
| | | }); |
| | | |
| | | const input = document.createElement('input'); |
| | | input.type = 'text'; |
| | | input.className = INPUT_CLASS; |
| | | input.placeholder = trait.get('placeholder') ?? '{{report.batchNo}}'; |
| | | |
| | | wrap.append(select, input); |
| | | |
| | | const tip = fieldOf(component, trait)?.tip; |
| | | if (tip) { |
| | | const hint = document.createElement('div'); |
| | | hint.className = HINT_CLASS; |
| | | hint.textContent = tip; |
| | | wrap.append(hint); |
| | | } |
| | | return wrap; |
| | | } |
| | | |
| | | function partsOf(elInput: HTMLElement): BindingInputParts | undefined { |
| | | const select = elInput.querySelector<HTMLSelectElement>(`.${SELECT_CLASS}`); |
| | | const input = elInput.querySelector<HTMLInputElement>(`.${INPUT_CLASS}`); |
| | | if (!select || !input) { |
| | | return undefined; |
| | | } |
| | | return { input, select }; |
| | | } |
| | | |
| | | /** æå½å屿§å¼åå¡«å°ä¸¤ä¸ªæ§ä»¶ä¸ï¼è½è¯å«æç»å®çå¾ä¸æï¼å¦ååªå¡«è¾å
¥æ¡ */ |
| | | function syncFromTrait(elInput: HTMLElement, trait: Trait, component?: Component): void { |
| | | const parts = partsOf(elInput); |
| | | if (!parts) { |
| | | return; |
| | | } |
| | | const value = String(trait.getValue() ?? ''); |
| | | const path = fromBindingValue(value, bindModeOf(component, trait)); |
| | | // è®¾ç½®äº value å´ä¸å¨åé项éï¼å¦ç»ä»¶æ¢è¿ãè·¯å¾å·²åºå¼ï¼æ¶è¡¥ä¸ä¸ªä¸´æ¶é项ï¼é¿å
䏿æ¾ç¤ºç©ºç½ |
| | | if (path && ![...parts.select.options].some((option) => option.value === path)) { |
| | | parts.select.append(new Option(path, path)); |
| | | } |
| | | parts.select.value = path; |
| | | parts.input.value = value; |
| | | } |
| | | |
| | | /** 注åç»å®æ§ä»¶ç±»åï¼é夿³¨åç´æ¥è¦çï¼é
åçæ´æ°ä¸æ¥é */ |
| | | export function registerBindingTraitType(editor: Editor): void { |
| | | editor.TraitManager.addType(BINDING_TRAIT_TYPE, { |
| | | createInput({ trait, component }) { |
| | | return createBindingInput(trait, component); |
| | | }, |
| | | onRender({ elInput, trait, component }: TraitViewOpts) { |
| | | syncFromTrait(elInput, trait, component); |
| | | }, |
| | | onUpdate({ elInput, trait, component }: TraitViewOpts) { |
| | | syncFromTrait(elInput, trait, component); |
| | | }, |
| | | onEvent({ elInput, trait, component, event }: TraitViewOpts & { event: Event }) { |
| | | const target = event.target; |
| | | if (!(target instanceof HTMLElement)) { |
| | | return; |
| | | } |
| | | const parts = partsOf(elInput); |
| | | if (!parts) { |
| | | return; |
| | | } |
| | | if (target.classList.contains(SELECT_CLASS)) { |
| | | const value = toBindingValue(parts.select.value, bindModeOf(component, trait)); |
| | | parts.input.value = value; |
| | | trait.setValue(value); |
| | | return; |
| | | } |
| | | if (target.classList.contains(INPUT_CLASS)) { |
| | | trait.setValue(parts.input.value); |
| | | } |
| | | }, |
| | | }); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * è´¨éç»ä»¶åè®®ï¼Quality Component Protocolï¼ |
| | | * <p> |
| | | * 设计å¨åªè®¤è¿å¥åè®®ï¼ä¸è®¤å
·ä½ä¸å¡ç»ä»¶ï¼æ°å¢è´¨éç»ä»¶ï¼æ³¨åä¸ä¸ªæ° definitionï¼ |
| | | * ä¸éè¦æ¹å¨ GrapesJS éæä»£ç ï¼ä¹ä¸éè¦æ¹å¨è®¾è®¡å¨é¡µé¢ï¼æ¹æ¡è®¾è®¡ §9ï¼ã |
| | | */ |
| | | |
| | | /** ç»ä»¶åç±»ï¼é¡ºåºå³è®¾è®¡å¨å·¦ä¾§é¢æ¿çåç»é¡ºåº */ |
| | | export const QUALITY_CATEGORY = { |
| | | BASIC: 'åºç¡ç»ä»¶', |
| | | HEADER: 'æ¥åç»ä»¶', |
| | | INSPECTION: 'æ£éªç»ä»¶', |
| | | RESULT: 'ç»æç»ä»¶', |
| | | } as const; |
| | | |
| | | export type QualityCategory = (typeof QUALITY_CATEGORY)[keyof typeof QUALITY_CATEGORY]; |
| | | |
| | | /** åç±»çå±ç¤ºé¡ºåº */ |
| | | export const QUALITY_CATEGORY_ORDER: QualityCategory[] = [ |
| | | QUALITY_CATEGORY.BASIC, |
| | | QUALITY_CATEGORY.HEADER, |
| | | QUALITY_CATEGORY.INSPECTION, |
| | | QUALITY_CATEGORY.RESULT, |
| | | ]; |
| | | |
| | | /** 屿§å段çå¼ç±»åï¼å³å®å±æ§é¢æ¿ç¨ä»ä¹æ§ä»¶ */ |
| | | export type QualityFieldType = |
| | | | 'boolean' |
| | | | 'enum' |
| | | | 'number' |
| | | | 'string' |
| | | | 'text'; |
| | | |
| | | /** 屿§å段çå¯é项 */ |
| | | export interface QualityFieldOption { |
| | | label: string; |
| | | value: number | string; |
| | | } |
| | | |
| | | /** |
| | | * ä¸ä¸ªä¸å¡å±æ§ / å¯ç»å®æ°æ®å段çæè¿°ã |
| | | * <p> |
| | | * propertySchema æè¿°ã设计æç¨æ·è½æ¹ä»ä¹ãï¼dataSchema æè¿°ã渲ææè½ç»ä»ä¹ãï¼ |
| | | * 两è
å
±ç¨ä¸ä¸ªç»æï¼åºå«åªå¨ç¨éã |
| | | */ |
| | | export interface QualityFieldSchema { |
| | | /** åæ®µé®ï¼åæ¶æ¯åå
¥ç»å¸ attributes ç data-qc-<key> å */ |
| | | key: string; |
| | | label: string; |
| | | type: QualityFieldType; |
| | | /** é»è®¤å¼ï¼å¿
é¡»ä¸ type å¯¹åº */ |
| | | defaultValue?: boolean | number | string; |
| | | /** type 为 enum æ¶çåé项 */ |
| | | options?: QualityFieldOption[]; |
| | | placeholder?: string; |
| | | /** è¾å
¥æç¤ºï¼å±ç¤ºå¨å±æ§é¢æ¿åæ®µä¸æ¹ */ |
| | | tip?: string; |
| | | /** æ¯å¦å¿
å¡«ï¼æ ¡éªç¨ï¼ */ |
| | | required?: boolean; |
| | | /** |
| | | * æ¯å¦å
许ç»å®æ°æ®æºè¡¨è¾¾å¼ï¼{{path}}ï¼ã |
| | | * 为 false æ¶å±æ§é¢æ¿ä¸æä¾ãç»å®æ°æ®ãå
¥å£ã |
| | | */ |
| | | bindable?: boolean; |
| | | /** |
| | | * bindable 为 true æ¶ï¼ä»æ°æ®å段éæä¸ååå屿§å¼çå½¢å¼ã |
| | | * <p> |
| | | * - textï¼é»è®¤ï¼ï¼åæ {{path}}ï¼æ¸²æææç»å®åè®®æ¿æ¢ææ°æ®å¼ï¼éç¨äºæ é¢ãææ¬ãç»è®ºçï¼ |
| | | * - pathï¼ç´æ¥å裸路å¾ï¼æ¸²æææå®å½ä½ç»æåè·¯å¾ç¨ï¼å¦ææ°ç»éè¡çæçæ£éªé¡¹æ°æ®æºï¼ï¼ |
| | | * å
ä¸ {{}} åèä¼è®©æ¸²æææ¾ä¸å°æ°ç»ã |
| | | */ |
| | | bindAs?: 'path' | 'text'; |
| | | } |
| | | |
| | | /** ä¸å¡å±æ§çè¿è¡æ¶åå¼ */ |
| | | export type QualityProps = Record<string, boolean | number | string | undefined>; |
| | | |
| | | /** GrapesJS ç»ä»¶å
容ï¼çä»·äº BlockProperties['content'] ç常ç¨åé */ |
| | | export interface QualityComponentContent { |
| | | tagName?: string; |
| | | /** åç»ä»¶ï¼HTML å符串æç»ä»¶æè¿°å¯¹è±¡æ°ç» */ |
| | | components?: QualityComponentContent[] | string; |
| | | content?: string; |
| | | attributes?: Record<string, string>; |
| | | style?: Record<string, string>; |
| | | } |
| | | |
| | | /** |
| | | * è´¨éç»ä»¶å®ä¹ï¼æ³¨åè¿ registry çå¯ä¸åè®®ã |
| | | */ |
| | | export interface QualityComponentDefinition { |
| | | /** å¯ä¸ç±»åæ è¯ï¼åå
¥ç»å¸ç data-quality-typeï¼ä¹æ¯ Schema.components[].qualityType */ |
| | | type: string; |
| | | /** è±æåï¼ç¨äºä»£ç å
å¼ç¨ */ |
| | | name: string; |
| | | /** 设计å¨å·¦ä¾§é¢æ¿ä¸å±æ§é¢æ¿çæ¾ç¤ºå */ |
| | | label: string; |
| | | category: QualityCategory; |
| | | /** 左侧颿¿ç徿 ï¼å
è SVG åç¬¦ä¸²ï¼ */ |
| | | icon: string; |
| | | /** æ°ç»ä»¶è½å°ç»å¸æ¶çåå§ä¸å¡å±æ§ */ |
| | | defaults: QualityProps; |
| | | /** 设计æå¯ç¼è¾çä¸å¡å±æ§ */ |
| | | propertySchema: QualityFieldSchema[]; |
| | | /** 渲ææå¯ç»å®çæ°æ®å段 */ |
| | | dataSchema: QualityFieldSchema[]; |
| | | /** |
| | | * ç»ä»¶çå¯è§ææ¬å¯¹åºç屿§ keyã |
| | | * <p> |
| | | * 声æåï¼ç¨æ·å¨ç»å¸å
ç´æ¥æ¹æåä¼åæ¥åè¯¥å±æ§ï¼å±æ§é¢æ¿æ¹å¼ä¹ä¼åæ¥åç»å¸ï¼ |
| | | * å
容ç±å¤ä¸ªå
ç´ æ¼æçç»ä»¶ï¼é¡µçãè¡¨æ ¼ï¼ä¸å£°æï¼é¿å
æ´å HTML è¢«å½æææ¬ã |
| | | */ |
| | | contentKey?: string; |
| | | /** |
| | | * ç» AI 导å
¥ç¨çéå说æï¼è¿ä¸ªç»ä»¶ç¨æ¥æ¾ä»ä¹ãä¸è¦ç¨æ¥æ¾ä»ä¹ã |
| | | * <p> |
| | | * ä¸è¿è®¾è®¡å¨çé¢ï¼åªéç§¯æ¨æ¸
åè¿æç¤ºè¯ãå宿¯å 为æ¸
åéåªæ label æ¶ï¼ |
| | | * 模åé¢å¯¹ãæ¥åæ¬å¤´ãè¿ç±»å
容ä¼å¨ Heading / Text / ReportHeader ä¹é´ççï¼ |
| | | * èåªä¼çå°ä¸ææ ç¾ç宿²¡æä»»ä½çç±ä¸ææéç¨çé£ä¸ªã |
| | | * <p> |
| | | * å¿
é¡»å
å«ãä¸è¦ç¨æ¥æ¾ä»ä¹ãï¼åªè®²ç¨éæ¡ä¸ä½è¯¯ç¨ï¼è®²æ¸
è¾¹çææç¨ã |
| | | */ |
| | | aiHint?: string; |
| | | /** |
| | | * çæç»å¸å
容ãproperties å·²ç»æ¶æè¿é»è®¤å¼ï¼å®ç°éå¯ä»¥ç´æ¥ç¨ã |
| | | */ |
| | | buildContent: (properties: QualityProps) => QualityComponentContent; |
| | | /** |
| | | * æ ¡éªä¸å¡å±æ§ï¼è¿åéè¯¯ä¿¡æ¯æ°ç»ï¼ç©ºæ°ç»è¡¨ç¤ºéè¿ã |
| | | * ä»
å¨ä¿å/åå¸åè°ç¨ï¼æ£å¸¸è®¾è®¡è¿ç¨ä¸åºè§¦åã |
| | | */ |
| | | validate?: (properties: QualityProps) => string[]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * è´¨éç»ä»¶å±æ§æ ¡éªä¸é»è®¤å¼æ¶æ |
| | | * <p> |
| | | * åªå¨ä¿å / åå¸åè°ç¨ï¼è®¾è®¡è¿ç¨ä¸çä¸é´æï¼æ¯å¦åæå°ç»å¸ãè¿æ²¡å¡«å¼ï¼ä¸è§¦åæ ¡éªã |
| | | */ |
| | | import type { |
| | | QualityComponentDefinition, |
| | | QualityFieldSchema, |
| | | QualityProps, |
| | | } from './types'; |
| | | |
| | | /** æä»»ææ¥æºçåå§å¼æ¶ææå段声æçç±»å */ |
| | | export function coerceFieldValue( |
| | | field: QualityFieldSchema, |
| | | raw: boolean | number | string, |
| | | ): boolean | number | string { |
| | | switch (field.type) { |
| | | case 'boolean': { |
| | | if (typeof raw === 'boolean') { |
| | | return raw; |
| | | } |
| | | return raw === 'true' || raw === '1'; |
| | | } |
| | | case 'number': { |
| | | const parsed = Number(raw); |
| | | if (Number.isFinite(parsed)) { |
| | | return parsed; |
| | | } |
| | | return typeof field.defaultValue === 'number' ? field.defaultValue : 0; |
| | | } |
| | | default: { |
| | | return String(raw); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 以 defaults æåºï¼ç¨ä¼ å
¥å¼è¦çå
¶ä¸ç忳项ï¼å¾å°å®æ´å¯ç¨ç屿§é */ |
| | | export function resolveQualityProps( |
| | | definition: QualityComponentDefinition, |
| | | props?: QualityProps, |
| | | ): QualityProps { |
| | | const resolved: QualityProps = { ...definition.defaults }; |
| | | if (!props) { |
| | | return resolved; |
| | | } |
| | | definition.propertySchema.forEach((field) => { |
| | | const raw = props[field.key]; |
| | | if (raw === undefined) { |
| | | return; |
| | | } |
| | | resolved[field.key] = coerceFieldValue(field, raw); |
| | | }); |
| | | return resolved; |
| | | } |
| | | |
| | | /** |
| | | * æ ¡éªä¸å¡å±æ§ï¼è¿åéè¯¯ä¿¡æ¯æ°ç»ï¼ç©ºæ°ç»è¡¨ç¤ºéè¿ã |
| | | * <p> |
| | | * æç¤ºè¦è½è®©ç¨æ·ç´æ¥å®ä½ï¼æä»¥å¸¦ä¸å段æ¾ç¤ºåä¸å®é
å¼ã |
| | | */ |
| | | export function validateQualityProps( |
| | | definition: QualityComponentDefinition, |
| | | props?: QualityProps, |
| | | ): string[] { |
| | | const errors: string[] = []; |
| | | const resolved = resolveQualityProps(definition, props); |
| | | definition.propertySchema.forEach((field) => { |
| | | const value = resolved[field.key]; |
| | | const isEmpty = value === undefined || value === ''; |
| | | if (isEmpty) { |
| | | if (field.required) { |
| | | errors.push(`${definition.label}çã${field.label}ãä¸è½ä¸ºç©º`); |
| | | } |
| | | return; |
| | | } |
| | | if (field.type === 'enum' && field.options) { |
| | | const matched = field.options.some((option) => String(option.value) === String(value)); |
| | | if (!matched) { |
| | | errors.push( |
| | | `${definition.label}çã${field.label}ãå¼ã${value}ãä¸å¨å¯é项å
ï¼è¯·éæ°éæ©`, |
| | | ); |
| | | } |
| | | return; |
| | | } |
| | | if (field.type === 'number' && typeof value === 'number' && !Number.isFinite(value)) { |
| | | errors.push(`${definition.label}çã${field.label}ãå¿
é¡»æ¯æ°åï¼å½å为ã${value}ã`); |
| | | } |
| | | }); |
| | | // éä¸ªåæ®µçè§åä¸é¢å·²ç»æ¥å®ï¼å©ä¸çæ¯ãåæ®µé½å¨ãä½ç»åèµ·æ¥ä¸æç«ãçè§å |
| | | // ï¼åå®ä¹ä¸²çè¯æ³ãè¡¨å¤´è·¨åæ°å计çï¼ï¼åªæç»ä»¶èªå·±ç¥éï¼å¿
é¡»å¨è¿éæ¥ä¸ã |
| | | // 䏿¥çè¯ definition.validate å°±æ¯ä¸ªæ²¡äººè°ç¨çæ»æ¹æ³ï¼åäºä¹ä¸ä¼æ¦ä½ä»»ä½äººã |
| | | errors.push(...(definition.validate?.(resolved) ?? [])); |
| | | return errors; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æ°æ®ç»å®ï¼æ¨¡æ¿ææ¬éç {{path}} |
| | | * <p> |
| | | * åªååå¼ä¸æ¿æ¢ï¼ä¸åä»»ä½è¡¨è¾¾å¼æ±å¼ï¼éè¦è®¡ç®çåºæ¯äº¤ç»è§å弿ã |
| | | * åä¸å°çè·¯å¾æ¸²æä¸ºç©ºå符串ââæ¥åä¸ä¸è¯¥åºç°ç¨æ·ç䏿ç {{}}ã |
| | | */ |
| | | import { formatValue, readPath } from './path'; |
| | | |
| | | /** ç»å®è¡¨è¾¾å¼ï¼{{ report.reportNo }} */ |
| | | const BINDING_PATTERN = /\{\{\s*([^{}]+?)\s*\}\}/g; |
| | | |
| | | /** æåææ¬éåºç°çææç»å®è·¯å¾ï¼å»éï¼ï¼ç¨äºæ¸²æåçæ°æ®ä¾èµæ£æ¥ */ |
| | | export function listBindingPaths(template: string): string[] { |
| | | const paths = new Set<string>(); |
| | | for (const match of template.matchAll(BINDING_PATTERN)) { |
| | | const path = match[1]?.trim(); |
| | | if (path) { |
| | | paths.add(path); |
| | | } |
| | | } |
| | | return [...paths]; |
| | | } |
| | | |
| | | /** è§£æå个ç»å®è·¯å¾ï¼åä¸å°è¿å undefined */ |
| | | export function resolveBinding(path: string, context: unknown): unknown { |
| | | return readPath(context, path.trim()); |
| | | } |
| | | |
| | | /** |
| | | * æ¿æ¢ææ¬éçå
¨é¨ç»å®è¡¨è¾¾å¼ï¼å¹¶åæ¥åä¸å°å¼çè·¯å¾ã |
| | | * <p> |
| | | * æ¸²ææ¶ç»å®ä¸ä¸é常æå³çæ°æ®ç¼ºå£ï¼é黿¸²ææç©ºä¼è®©æ¥åçèµ·æ¥"æ£å¸¸ä½ç¼ºå
容"ï¼ |
| | | * æä»¥æç¼ºå£äº¤ç»è°ç¨æ¹ï¼ç±æ¸²æå¼ææ±æ»æå¯è§çé®é¢æ¸
åã |
| | | */ |
| | | export function resolveTextWithReport( |
| | | template: string, |
| | | context: unknown, |
| | | onMissing: (path: string) => void, |
| | | ): string { |
| | | return template.replaceAll(BINDING_PATTERN, (_, rawPath: string) => { |
| | | const path = rawPath.trim(); |
| | | const value = readPath(context, path); |
| | | if (value === undefined || value === null) { |
| | | onMissing(path); |
| | | } |
| | | return formatValue(value); |
| | | }); |
| | | } |
| | | |
| | | /** æ¿æ¢ææ¬éçå
¨é¨ç»å®è¡¨è¾¾å¼ï¼æªç»å®çè·¯å¾æ¸²æä¸ºç©º */ |
| | | export function resolveText(template: string, context: unknown): string { |
| | | return resolveTextWithReport(template, context, () => {}); |
| | | } |
| | | |
| | | /** 夿䏿®µææ¬æ¯å¦æ¯çº¯ç»å®è¡¨è¾¾å¼ï¼{{path}} åå 䏿®µï¼ï¼æ¯åè¿åè·¯å¾ */ |
| | | export function asSingleBinding(template: string): string | undefined { |
| | | const trimmed = template.trim(); |
| | | const match = /^\{\{\s*([^{}]+?)\s*\}\}$/.exec(trimmed); |
| | | return match?.[1]?.trim(); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æ¥åä¸ä¸æï¼Contextï¼ |
| | | * <p> |
| | | * 模æ¿éç {{path}} å
¨é¨ç¸å¯¹è¿ä¸ªç»æè§£æï¼report æ¯æ¥åçº§åæ®µï¼inspectionItems æ¯æ£éªé¡¹æ°ç»ã |
| | | * ä¸å¡åæ® â ä¸ä¸æçæ å°ç±ä¸ä¸ææå»ºå¨è´è´£ï¼æ¨¡æ¿æ¬èº«åªè®¤è¿ä¸ªæ åç»æï¼ |
| | | * æ¢è¡ä¸ãæ¢åæ®æ¥æºé½ä¸éè¦æ¹æ¨¡æ¿ä¸ç»ä»¶ã |
| | | */ |
| | | |
| | | /** å¤å®ç»æåå¼ï¼ç»ä»¶ä¸è§å弿å
±ç¨ */ |
| | | export const QUALITY_RESULT = { |
| | | PASS: 'PASS', |
| | | FAIL: 'FAIL', |
| | | } as const; |
| | | |
| | | export type QualityResult = (typeof QUALITY_RESULT)[keyof typeof QUALITY_RESULT]; |
| | | |
| | | /** å¤å®ç»æå¯¹åºçå±ç¤ºæå */ |
| | | export const QUALITY_RESULT_TEXT: Record<QualityResult, string> = { |
| | | PASS: 'åæ ¼', |
| | | FAIL: 'ä¸åæ ¼', |
| | | }; |
| | | |
| | | /** æ¥åçº§åæ®µ */ |
| | | export interface ReportContextReport { |
| | | reportNo: string; |
| | | reportName: string; |
| | | sampleNo: string; |
| | | productCode: string; |
| | | productName: string; |
| | | spec: string; |
| | | batchNo: string; |
| | | workOrderNo: string; |
| | | inspectType: string; |
| | | inspector: string; |
| | | inspectDate: string; |
| | | department: string; |
| | | customerName: string; |
| | | supplierName: string; |
| | | /** PASS / FAIL */ |
| | | result: string; |
| | | /** åæ ¼ / ä¸åæ ¼ / å¾
å¤å® */ |
| | | resultText: string; |
| | | conclusion: string; |
| | | passRate: string; |
| | | total: number; |
| | | passCount: number; |
| | | failCount: number; |
| | | /** |
| | | * è´¨æ£åä¸çåå¤å®ï¼æ°åå符串 "1"~"4"ï¼æ¥èª MES åæ®ï¼ã |
| | | * <p> |
| | | * ä¸å¼æç®åºç {@link result} å¹¶åãäºä¸è¦çï¼åæ®å¤å®æ¯äººå·¥æçæ¿ |
| | | * ï¼åæ ¼ / ç¹é / ä¸åæ ¼éè´§ / ä¸åæ ¼æ¥åºï¼åæï¼ï¼èå¼æåªæ PASS/FAIL/å¾
å¤å® 䏿è£
ä¸ä¸å®ã |
| | | * ä»
ãç±è´¨æ£ååºä»¶ãæ¶æå¼ï¼éç¨åºä»¶çä¸ä¸æéæä¸ºç©ºä¸²ã |
| | | */ |
| | | qcResult: string; |
| | | /** {@link qcResult} ç䏿ï¼å¦ãåæ ¼ããç¹éããä¸åæ ¼éè´§ããä¸åæ ¼æ¥åºã */ |
| | | qcResultText: string; |
| | | } |
| | | |
| | | /** |
| | | * æ£éªé¡¹çåç»ä¿¡æ¯ã |
| | | * <p> |
| | | * æ£éªåéçç¶åå±çº§ï¼åç»å¤´ + ç»å
å项ï¼åªæä¸¤é¡¹æ°æ®è½è¡¨è¾¾ï¼ç»ååå¹¶å è¡ã |
| | | * å项è¡è¦ä¸è¦è®©åºåå¹¶æ ¼ãæ¸²æææ æ³èªå·±ç®åºè¿ä¸¤ä»¶äº ââ ç»å®çè·¯å¾å¨æ°ç»ä¸ç»§ç»å屿§ |
| | | * 伿å
ç´ pluckï¼`group.children.length` åä¸å°å¼ ââ æä»¥å¿
é¡»ç±ä¸ä¸ææå»ºå¨é项ä¸åã |
| | | * <p> |
| | | * è¿å 项å¨**æ¯ä¸ä¸ª**æ£éªé¡¹ä¸é½å¿
é¡»æå¼ï¼åªææ¯ç©ºä¸²ï¼ï¼ç»å®çåæ®µç¼ºå¸æ¶å±æ§ä¼è¢«è·³è¿ï¼ |
| | | * åå¹¶æ ¼åèä¼é²åºæ¥ã没æåç»æ¶æ´ä¸ª {@link ReportContextItemGroup} å¯ä»¥ä¸ç»ã |
| | | */ |
| | | export interface ReportContextItemGroup { |
| | | /** ãå项ãåæåï¼ç»å
å项 = èªå·±çååï¼ç»å¤´è¡ä¸ç¬ç«é¡¹ = 空串 */ |
| | | childName: string; |
| | | /** ç»åæ ¼ç纵ååå¹¶è¡æ°ï¼ç»å¤´è¡ = 1 + ç»å
å项æ°ï¼ç»å
å项è¡ä¸ç¬ç«é¡¹ = 1 */ |
| | | span: number; |
| | | /** |
| | | * ç»åæ ¼çéèå¼ï¼ç»å
åé¡¹è¡ = 'hidden'ï¼è¯¥æ ¼è®©ä½ç»ä¸æ¹åå¹¶æ ¼ï¼ï¼ç»å¤´è¡ä¸ç¬ç«é¡¹ = 空串ã |
| | | * <p> |
| | | * å符串è䏿¯å¸å°ï¼`hidden` 屿§é ãå¨ä¸å¨ãèµ·ä½ç¨ï¼`hidden="false"` ç
§æ ·éèï¼ï¼ |
| | | * åªæç©ºä¸²è½è¡¨è¾¾ãä¸è¾åºè¯¥å±æ§ = è¿ä¸æ ¼å¯è§ãã |
| | | */ |
| | | hidden: string; |
| | | } |
| | | |
| | | /** å个æ£éªé¡¹ */ |
| | | export interface ReportContextItem { |
| | | index: number; |
| | | itemCode: string; |
| | | itemName: string; |
| | | standardValue: string; |
| | | actualValue: string; |
| | | unit: string; |
| | | /** |
| | | * ãæ£æµè¦æ±ãææ¬ï¼åç»ç¶é¡¹ = ææ ä¸ç计ç®å
¬å¼ï¼å
¶ä½ = åæ®ä¸çæ åè¦æ±ã |
| | | * <p> |
| | | * ä¸ {@link standardValue} å¹¶åèä¸åå¹¶ï¼æ¯å 为两è
çæ¥æºä¸åï¼æ åè¦æ±æ¯æ£éªåè¡ä¸çå¼ï¼ |
| | | * å
¬å¼æ¯æ£éªé¡¹ä¸»æ°æ®ä¸çå¼ï¼åç»ç¶åé¡¹åæ ¹æ²¡æè§æ ¼ï¼åªæå
¬å¼ã |
| | | */ |
| | | requirement?: string; |
| | | /** |
| | | * ãæ£æµæ¹æ³ãææ¬ï¼æ¥èªåæ®è¡ï¼å¦ GB 5009.3-2016ï¼ã |
| | | * <p> |
| | | * ä¸ {@link requirement} å¹¶åèä¸åå¹¶ï¼æ£æµè¦æ±è¯´çæ¯ãè¿ä¸é¡¹è¦è¾¾å°ä»ä¹ãï¼ |
| | | * æ£æµæ¹æ³è¯´çæ¯ãç¨ä»ä¹åæ³æµçãï¼åä»¶é常åå ä¸åã |
| | | */ |
| | | checkMethod?: string; |
| | | /** åç»ä¿¡æ¯ï¼ä»
卿£éªååå¨åç»ç»ææ¶é项ä¸å */ |
| | | group?: ReportContextItemGroup; |
| | | /** è§æ ¼ä¸éï¼è§å弿ååºé´å¤å®æ¶ä½¿ç¨ */ |
| | | upperLimit?: number; |
| | | /** è§æ ¼ä¸é */ |
| | | lowerLimit?: number; |
| | | /** PASS / FAIL */ |
| | | result: string; |
| | | /** |
| | | * åæ ¼ / ä¸åæ ¼ / å¾
å¤å® / æ å¤å®è§åã |
| | | * <p> |
| | | * å两è
é½ä¸æ¯ç»è®ºï¼å¾
å¤å®æ¯è§åè·æäºï¼ä»è®¡å
¥åæ ¼ç忝ï¼ï¼æ å¤å®è§åæ¯è¿é¡¹æ²¡æå¤å®ä¾æ®ï¼ä¸è®¡å
¥ï¼ã |
| | | */ |
| | | resultText: string; |
| | | remark: string; |
| | | } |
| | | |
| | | /** æ¥åä¸ä¸æ */ |
| | | export interface ReportContext { |
| | | report: ReportContextReport; |
| | | inspectionItems: ReportContextItem[]; |
| | | } |
| | | |
| | | function emptyReport(): ReportContextReport { |
| | | return { |
| | | reportNo: '', |
| | | reportName: '', |
| | | sampleNo: '', |
| | | productCode: '', |
| | | productName: '', |
| | | spec: '', |
| | | batchNo: '', |
| | | workOrderNo: '', |
| | | inspectType: '', |
| | | inspector: '', |
| | | inspectDate: '', |
| | | department: '', |
| | | customerName: '', |
| | | supplierName: '', |
| | | result: '', |
| | | resultText: '', |
| | | conclusion: '', |
| | | passRate: '', |
| | | total: 0, |
| | | passCount: 0, |
| | | failCount: 0, |
| | | qcResult: '', |
| | | qcResultText: '', |
| | | }; |
| | | } |
| | | |
| | | /** 空ç½ä¸ä¸æï¼å段é½å
¨ä½æ²¡æå¼ï¼ä¾¿äºé项填å
*/ |
| | | export function createEmptyContext(): ReportContext { |
| | | return { report: emptyReport(), inspectionItems: [] }; |
| | | } |
| | | |
| | | /** ç¨ä¼ å
¥å¼è¦ç空ç½ä¸ä¸æï¼æªä¼ çåæ®µä¿çç©ºå¼ */ |
| | | export function createContext(input?: Partial<ReportContext>): ReportContext { |
| | | const base = createEmptyContext(); |
| | | return { |
| | | report: { ...base.report, ...input?.report }, |
| | | inspectionItems: input?.inspectionItems ?? base.inspectionItems, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * çº¸å¼ å°ºå¯¸ |
| | | * <p> |
| | | * ä¸å端 QcReportEnums.PageSizeEnum åå¼ä¸ä¸å¯¹åºã |
| | | * è®¾è®¡å¨æ¢ç®ç»å¸åç´ ãæ¸²æå¼ææ¢ç® @page é½ç¨è¿ä¸ä»½ï¼é¿å
两å¤å°ºå¯¸ååä¸éå对ä¸ä¸ã |
| | | */ |
| | | |
| | | /** çº¸å¼ å°ºå¯¸ï¼æ¯«ç±³ï¼ï¼æªååºç尺寸åéå°é»è®¤çº¸å¼ */ |
| | | export const PAGE_SIZE_MM: Record<string, { height: number; width: number }> = { |
| | | A3: { width: 297, height: 420 }, |
| | | A4: { width: 210, height: 297 }, |
| | | A5: { width: 148, height: 210 }, |
| | | Letter: { width: 215.9, height: 279.4 }, |
| | | }; |
| | | |
| | | /** é»è®¤çº¸å¼ */ |
| | | export const DEFAULT_PAGE_SIZE = 'A4'; |
| | | |
| | | /** é»è®¤é¡µè¾¹è·ï¼æ¯«ç±³ï¼ */ |
| | | export const DEFAULT_MARGIN_MM = 10; |
| | | |
| | | /** çº¸å¼ é
ç½®ï¼ç»æçä»·äº Schema.pageï¼ä¸ä¾èµå
·ä½æ¥å£ç±»åå®ä¹ */ |
| | | export interface PageSetting { |
| | | size?: string; |
| | | orientation?: string; |
| | | margin?: { bottom?: number; left?: number; right?: number; top?: number }; |
| | | } |
| | | |
| | | export interface ResolvedPage { |
| | | heightMm: number; |
| | | marginMm: { bottom: number; left: number; right: number; top: number }; |
| | | widthMm: number; |
| | | } |
| | | |
| | | /** è§£æçº¸å¼ 尺寸ä¸é¡µè¾¹è·ï¼æ¯«ç±³ï¼ï¼æ¨ªåæ¶ç¿»è½¬å®½é« */ |
| | | export function resolvePageMm(page?: PageSetting): ResolvedPage { |
| | | const base = PAGE_SIZE_MM[page?.size ?? DEFAULT_PAGE_SIZE] ?? PAGE_SIZE_MM[DEFAULT_PAGE_SIZE]!; |
| | | const isLandscape = (page?.orientation ?? 'portrait') === 'landscape'; |
| | | const margin = page?.margin; |
| | | return { |
| | | widthMm: isLandscape ? base.height : base.width, |
| | | heightMm: isLandscape ? base.width : base.height, |
| | | marginMm: { |
| | | top: margin?.top ?? DEFAULT_MARGIN_MM, |
| | | right: margin?.right ?? DEFAULT_MARGIN_MM, |
| | | bottom: margin?.bottom ?? DEFAULT_MARGIN_MM, |
| | | left: margin?.left ?? DEFAULT_MARGIN_MM, |
| | | }, |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * ä¸ä¸æåå¼ |
| | | * <p> |
| | | * åªåç½ååå¼ç屿§å¯¼èªï¼ä¸è§£æã䏿§è¡ä»»ä½ä»£ç ï¼ |
| | | * æ¯æ a.b.c ä¸ a.b[0].c 两ç§åæ³ï¼è·¯å¾å¨æ°ç»ä¸ç»§ç»å屿§æ¶æå
ç´ é个åå¼ï¼pluckï¼ã |
| | | */ |
| | | |
| | | /** è·¯å¾çæ®µï¼æ®é屿§åææ°ç»ä¸æ */ |
| | | const TOKEN_PATTERN = /[^.[\]]+/g; |
| | | |
| | | /** ç¦æ¢è®¿é®ç屿§åï¼é¿å
顺çååé¾è¯»å°æé å¨ */ |
| | | const BLOCKED_KEYS = new Set(['__proto__', 'constructor', 'prototype']); |
| | | |
| | | /** æè·¯å¾ææç段ï¼inspectionItems[0].actualValue â ['inspectionItems','0','actualValue'] */ |
| | | export function parsePath(path: string): string[] { |
| | | return path.match(TOKEN_PATTERN) ?? []; |
| | | } |
| | | |
| | | function toIndex(token: string): number | undefined { |
| | | if (!/^\d+$/.test(token)) { |
| | | return undefined; |
| | | } |
| | | return Number(token); |
| | | } |
| | | |
| | | function readTokens(source: unknown, tokens: string[], start: number): unknown { |
| | | let current = source; |
| | | for (let index = start; index < tokens.length; index += 1) { |
| | | const token = tokens[index]!; |
| | | if (current === null || current === undefined) { |
| | | return undefined; |
| | | } |
| | | if (Array.isArray(current)) { |
| | | const arrayIndex = toIndex(token); |
| | | if (arrayIndex === undefined) { |
| | | // æ°ç»ä¸ç»§ç»å屿§ï¼éå
ç´ åå¼ï¼å¾å°åé¿åº¦çæ°ç» |
| | | return current.map((item) => readTokens(item, tokens, index)); |
| | | } |
| | | current = current[arrayIndex]; |
| | | continue; |
| | | } |
| | | if (typeof current === 'object') { |
| | | if (BLOCKED_KEYS.has(token)) { |
| | | return undefined; |
| | | } |
| | | current = (current as Record<string, unknown>)[token]; |
| | | continue; |
| | | } |
| | | // æ éåé¢è¿æè·¯å¾ç段ï¼è¯´æè·¯å¾åéäº |
| | | return undefined; |
| | | } |
| | | return current; |
| | | } |
| | | |
| | | /** æè·¯å¾åå¼ï¼åä¸å°è¿å undefined */ |
| | | export function readPath(source: unknown, path: string): unknown { |
| | | return readTokens(source, parsePath(path), 0); |
| | | } |
| | | |
| | | /** å±ç¤ºç¨æ ¼å¼åï¼ç©ºå¼æ¸²æä¸ºç©ºå符串ï¼å¸å°å¼è½¬ä¸æï¼æ¥æåªä¿çå°ç§ */ |
| | | export function formatValue(value: unknown): string { |
| | | if (value === null || value === undefined) { |
| | | return ''; |
| | | } |
| | | if (typeof value === 'boolean') { |
| | | return value ? 'æ¯' : 'å¦'; |
| | | } |
| | | if (value instanceof Date) { |
| | | const pad = (input: number) => String(input).padStart(2, '0'); |
| | | return ( |
| | | `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())} ` + |
| | | `${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}` |
| | | ); |
| | | } |
| | | if (Array.isArray(value)) { |
| | | return value.map(formatValue).join('ã'); |
| | | } |
| | | return String(value); |
| | | } |
| | | |
| | | /** æ°å¼åï¼éæ°å¼è¿å NaNï¼ç±è°ç¨æ¹å³å®å¦ä½å¤ç */ |
| | | export function toNumber(value: unknown): number { |
| | | if (typeof value === 'number') { |
| | | return value; |
| | | } |
| | | if (typeof value === 'boolean') { |
| | | return value ? 1 : 0; |
| | | } |
| | | if (typeof value === 'string' && value.trim() !== '') { |
| | | return Number(value); |
| | | } |
| | | return Number.NaN; |
| | | } |
| | | |
| | | /** æä»»æå弿¶æææ°å¼æ°ç»ï¼ä¾ç»è®¡å½æ°ä½¿ç¨ */ |
| | | export function toNumberArray(value: unknown): number[] { |
| | | if (Array.isArray(value)) { |
| | | return value |
| | | .flat(Number.POSITIVE_INFINITY) |
| | | .map((item) => toNumber(item)) |
| | | .filter((item) => Number.isFinite(item)); |
| | | } |
| | | const single = toNumber(value); |
| | | return Number.isFinite(single) ? [single] : []; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æ¥å渲æå¼æ |
| | | * <p> |
| | | * è¾å
¥ãæ¨¡æ¿ Schema + æ¥åä¸ä¸æãï¼è¾åºå¯ç´æ¥æå°ç HTML ææ¡£ã |
| | | * 渲æé¡ºåºåºå®ä¸ºãå
å¤å®ã忏²æãï¼PASS/FAIL ç± report-evaluator ç®å¥½åè¿ä¸ä¸æï¼ |
| | | * 模æ¿éåªååå¼ï¼å æ¤åä¸ä»½æ°æ®å¨ä»»ä½æ¨¡æ¿ä¸å¤å®ä¸è´ï¼æ¨¡æ¿ä¹æ æ³å·¦å³å¤å®ç»æã |
| | | * <p> |
| | | * è¿éä¸ä¾èµ GrapesJSï¼åªå GrapesJS 导åºçé¡¹ç®æ°æ®ï¼ç»ä»¶æ + æ ·å¼ï¼ï¼ |
| | | * åç»æ¢ææå¡ç«¯æ¸²ææ¶è¿å¥æå½±é»è¾å¯ä»¥ç
§æ¬ã |
| | | */ |
| | | import type { ReportContext } from './context'; |
| | | import type { QualityRuleDefinition } from './report-evaluator'; |
| | | |
| | | import { |
| | | QUALITY_ID_ATTR, |
| | | QUALITY_PROP_PREFIX, |
| | | QUALITY_REPEAT_ATTR, |
| | | QUALITY_REPEAT_ROW_ATTR, |
| | | QUALITY_TYPE_ATTR, |
| | | } from '../core/attrs'; |
| | | import { cssStyle, escapeHtml } from '../core/html'; |
| | | |
| | | import { resolveTextWithReport } from './binding'; |
| | | import type { PageSetting } from './page'; |
| | | import { readPath } from './path'; |
| | | import { evaluateReport } from './report-evaluator'; |
| | | import { resolvePageMm } from './page'; |
| | | |
| | | /** 渲æç¨å°ç GrapesJS é¡¹ç®æ°æ®èç¹ï¼åªå£°ææ¸²æéè¦çåæ®µï¼ */ |
| | | export interface RenderNode { |
| | | attributes?: Record<string, string>; |
| | | components?: RenderNode[] | string; |
| | | content?: string; |
| | | style?: Record<string, string>; |
| | | tagName?: string; |
| | | type?: string; |
| | | } |
| | | |
| | | /** GrapesJS 导åºç䏿¡æ ·å¼è§å */ |
| | | export interface RenderStyleRule { |
| | | selectors?: string[]; |
| | | style?: Record<string, string>; |
| | | /** @media çæ¥è¯¢æ¡ä»¶ï¼çè®ºä¸æ¨¡æ¿ä¸å«åªä½æ¥è¯¢ï¼ç»å¸è®¾å¤å·²ç½®ç©º widthMediaï¼ */ |
| | | mediaText?: string; |
| | | } |
| | | |
| | | /** 渲æç¨å°ç Schema ç»æï¼ç»æçä»·äº ReportTemplateSchemaï¼ä¸ä¾èµå
·ä½æ¥å£ç±»åå®ä¹ */ |
| | | export interface RenderSchema { |
| | | page?: PageSetting; |
| | | grapes?: { |
| | | pages?: Array<{ frames?: Array<{ component?: RenderNode }> }>; |
| | | /** æ°æ°æ®æ¯è§åæ°ç»ï¼å岿°æ®å¯è½æ¯ CSS ææ¬ */ |
| | | styles?: RenderStyleRule[] | string; |
| | | }; |
| | | } |
| | | |
| | | export interface RenderOptions { |
| | | schema?: RenderSchema; |
| | | context: ReportContext; |
| | | rules?: QualityRuleDefinition[]; |
| | | } |
| | | |
| | | export interface RenderOutcome { |
| | | /** 宿´ HTML ææ¡£ï¼æå°/PDF ç´æ¥ä½¿ç¨ */ |
| | | html: string; |
| | | /** æ£æçæ®µï¼ä¾¿äºåµå
¥é¡µé¢é¢è§ */ |
| | | body: string; |
| | | /** å¤å®åçä¸ä¸æï¼å« PASS/FAIL å¤å®ä¸åæ ¼çï¼ */ |
| | | context: ReportContext; |
| | | /** è§åä¸ç»å®çé®é¢æ¸
åï¼è°ç¨æ¹å¿
é¡»æ¾å¼æ´é²ï¼ä¸è½ææåæ */ |
| | | errors: string[]; |
| | | } |
| | | |
| | | /** 渲æä½ç¨åï¼å¤å®åçæ¥åä¸ä¸æï¼å¤å 循ç¯åéï¼å¦ itemï¼ä¸åºå· index */ |
| | | type RenderScope = Record<string, unknown>; |
| | | |
| | | /** 䏿¬¡æ¸²æçè¡èµ°ç¶æï¼ä½ç¨å + å½åä½ç½® + é®é¢æ¸
åï¼æå¼ç¨å¨éå½éå
񄧮 */ |
| | | interface RenderState { |
| | | scope: RenderScope; |
| | | /** å½å渲æä½ç½®çæè¿°ï¼ç¨äºæé®é¢å®ä½å°å
·ä½è¡ */ |
| | | where: string; |
| | | errors: string[]; |
| | | /** 已䏿¥ç缺å£ï¼åä¸å¤åªæç¤ºä¸æ¬¡ */ |
| | | reported: Set<string>; |
| | | } |
| | | |
| | | /** èªéåæ ç¾ï¼ä¸è½çæç»ææ ç¾ */ |
| | | const VOID_TAGS = new Set([ |
| | | 'br', |
| | | 'col', |
| | | 'hr', |
| | | 'img', |
| | | 'input', |
| | | 'link', |
| | | 'meta', |
| | | 'source', |
| | | 'track', |
| | | 'wbr', |
| | | ]); |
| | | |
| | | /** |
| | | * GrapesJS ç»ä»¶ç±»å â HTML æ ç¾ã |
| | | * <p> |
| | | * é¡¹ç®æ°æ®åªå¨æ ç¾ä¸ç±»åé»è®¤å¼ä¸åæ¶æå tagNameï¼ä¾å¦ thãspanãh2ï¼ï¼ |
| | | * è¡¨æ ¼ç»æï¼table/thead/tbody/row/cellï¼å¨æ°æ®éé½åªæ typeï¼ |
| | | * æä»¥æ¼æè¿å¼ è¡¨å°±ä¼ææ´å¼ è¡¨æ ¼æ¸²ææä¸å divãæªæ¶å½çç±»ååéå° divï¼ |
| | | * ä¸ GrapesJS åºç¡ç»ä»¶çé»è®¤æ ç¾ä¸è´ã |
| | | */ |
| | | const TYPE_TAGS: Record<string, string> = { |
| | | cell: 'td', |
| | | row: 'tr', |
| | | table: 'table', |
| | | tbody: 'tbody', |
| | | tfoot: 'tfoot', |
| | | thead: 'thead', |
| | | text: 'div', |
| | | wrapper: 'div', |
| | | }; |
| | | |
| | | /** 渲æäº§ç©çåºç¡æçãæµè§å¨é»è®¤çè¡¨æ ¼ä¸é¡µè¾¹è·ä¼è®©æ¥åèµ°æ ·ï¼è¿éæ¶æææå°å好çåºçº¿ */ |
| | | const BASE_CSS = [ |
| | | '* { box-sizing: border-box; }', |
| | | 'body { margin: 0; color: #000; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; font-size: 12px; line-height: 1.5; }', |
| | | 'table { border-collapse: collapse; width: 100%; }', |
| | | 'img { max-width: 100%; }', |
| | | 'tr, td, th { page-break-inside: avoid; }', |
| | | ].join('\n'); |
| | | |
| | | /** æ¸²ææ¥åï¼å¤å® â çææ£æ â æ¼æ ·å¼ â ç»è£
ææ¡£ */ |
| | | export function renderReport(options: RenderOptions): RenderOutcome { |
| | | const { context, errors } = evaluateReport(options.context, options.rules ?? []); |
| | | const state: RenderState = { |
| | | scope: { ...context }, |
| | | where: '模æ¿', |
| | | errors, |
| | | reported: new Set<string>(), |
| | | }; |
| | | |
| | | const root = options.schema?.grapes?.pages?.[0]?.frames?.[0]?.component; |
| | | const body = root ? renderNode(root, state) : ''; |
| | | const css = buildCss(options.schema?.grapes?.styles); |
| | | const html = buildDocument(options.schema?.page, body, css, context); |
| | | return { html, body, context, errors }; |
| | | } |
| | | |
| | | /** 渲æå个èç¹ï¼ææ¬èç¹ä¸éå¤å®¹å¨åç¬å¤çï¼å
¶ä½æãæ ç¾ + 屿§ + åèç¹ãå±å¼ */ |
| | | function renderNode(node: RenderNode, state: RenderState): string { |
| | | // ææ¬èç¹åªè´¡ç®ææ¬ï¼èªèº«ä¸äº§çæ ç¾ |
| | | if (node.type === 'textnode') { |
| | | return renderText(node.content ?? '', state); |
| | | } |
| | | |
| | | const repeatPath = node.attributes?.[QUALITY_REPEAT_ATTR]; |
| | | if (repeatPath) { |
| | | return renderRepeat(node, repeatPath, state); |
| | | } |
| | | |
| | | const tag = resolveTag(node); |
| | | const attributes = renderAttributes(node, state); |
| | | const style = renderStyle(node); |
| | | if (VOID_TAGS.has(tag)) { |
| | | return `<${tag}${attributes}${style}>`; |
| | | } |
| | | return `<${tag}${attributes}${style}>${renderChildren(node, state)}</${tag}>`; |
| | | } |
| | | |
| | | /** æ ç¾ä¼å
åæ°æ®éç tagNameï¼å
¶æ¬¡æç»ä»¶ç±»åæ¨æ */ |
| | | function resolveTag(node: RenderNode): string { |
| | | return node.tagName ?? TYPE_TAGS[node.type ?? ''] ?? 'div'; |
| | | } |
| | | |
| | | /** |
| | | * åèç¹æå£°æé¡ºåºå±å¼ã |
| | | * <p> |
| | | * æåç»ä»¶æ¶å¿½ç¥ contentââä¸ GrapesJS ä¸è´ï¼å¾ä¸ä¸ªå¸¦ææ¬çç»ä»¶éåæå
¥ç»ä»¶åï¼ |
| | | * ææ¬å°±è®©ä½ç»åç»ä»¶ï¼è¿éå¿
é¡»åè§åï¼å¦å渲æäº§ç©ä¼å设计å¨çå°çä¸ä¸æ ·ã |
| | | */ |
| | | function renderChildren(node: RenderNode, state: RenderState): string { |
| | | if (Array.isArray(node.components) && node.components.length > 0) { |
| | | return node.components.map((child) => renderNode(child, state)).join(''); |
| | | } |
| | | return typeof node.content === 'string' ? renderText(node.content, state) : ''; |
| | | } |
| | | |
| | | /** |
| | | * éå¤å®¹å¨ï¼ææ°ç»è·¯å¾å±å¼è¡æ¨¡æ¿ï¼å
¶ä½åèç¹å¨åä½ç½®æ¸²æä¸æ¬¡ã |
| | | * <p> |
| | | * è¡æ¨¡æ¿ä¸ç data-qc-repeat-row 声æäºè¡å
ç循ç¯åéåï¼å¦ itemï¼ï¼ |
| | | * è¡å
å®¹ç¨ {{item.xxx}} åå¼ï¼ä¸è®¾è®¡æå¨ç»ä»¶é约å®çç»å®è·¯å¾ä¸è´ã |
| | | */ |
| | | function renderRepeat(node: RenderNode, path: string, state: RenderState): string { |
| | | const list = toRowList(readPath(state.scope, path)); |
| | | const children = Array.isArray(node.components) ? node.components : []; |
| | | const inner = children |
| | | .map((child) => { |
| | | const rowVar = child.attributes?.[QUALITY_REPEAT_ROW_ATTR]; |
| | | if (!rowVar) { |
| | | return renderNode(child, state); |
| | | } |
| | | return list |
| | | .map((element, index) => { |
| | | const rowState: RenderState = { |
| | | ...state, |
| | | scope: { ...state.scope, [rowVar]: element, index: index + 1 }, |
| | | where: `第 ${index + 1} è¡`, |
| | | }; |
| | | return renderNode(child, rowState); |
| | | }) |
| | | .join(''); |
| | | }) |
| | | .join(''); |
| | | |
| | | const tag = resolveTag(node); |
| | | return `<${tag}${renderAttributes(node, state)}${renderStyle(node)}>${inner}</${tag}>`; |
| | | } |
| | | |
| | | /** éæ°ç»æåè¡å¤çï¼ç©ºå¼å¾å°ç©ºè¡¨ï¼æ¯æéæ´è´´å"æ°æ®æ²¡å¡«å
¨"çæ¥ååºæ¯ */ |
| | | function toRowList(value: unknown): unknown[] { |
| | | if (Array.isArray(value)) { |
| | | return value; |
| | | } |
| | | if (value === undefined || value === null) { |
| | | return []; |
| | | } |
| | | return [value]; |
| | | } |
| | | |
| | | /** |
| | | * 渲æèç¹å±æ§ã |
| | | * <p> |
| | | * è®¾è®¡å¨æ è®°ï¼data-quality-type / data-qc-*ï¼ä¸è¿å
¥äº§ç©ï¼ |
| | | * 屿§å¼éç {{path}} ä¸å¹¶è§£æï¼å¾çå°åã龿¥çä¹è½ç»å®æ°æ®ã |
| | | */ |
| | | function renderAttributes(node: RenderNode, state: RenderState): string { |
| | | const attributes = node.attributes ?? {}; |
| | | const parts: string[] = []; |
| | | Object.entries(attributes).forEach(([name, value]) => { |
| | | if (name === QUALITY_TYPE_ATTR || name.startsWith(QUALITY_PROP_PREFIX)) { |
| | | return; |
| | | } |
| | | const text = renderText(String(value ?? ''), state); |
| | | if (text === '') { |
| | | return; |
| | | } |
| | | // GrapesJS çç»ä»¶ id æ¢æ data 屿§ï¼éå¤è¡é id ä¼éåï¼data 屿§ä¸ä¼ |
| | | parts.push(` ${name === 'id' ? QUALITY_ID_ATTR : name}="${escapeHtml(text)}"`); |
| | | }); |
| | | return parts.join(''); |
| | | } |
| | | |
| | | /** èç¹èªèº«çè¡å
æ ·å¼ */ |
| | | function renderStyle(node: RenderNode): string { |
| | | const style = cssStyle(node.style ?? {}); |
| | | return style ? ` style="${escapeHtml(style)}"` : ''; |
| | | } |
| | | |
| | | /** åææ¬éçç»å®å¼ï¼åä¸å°çè·¯å¾è®°è¿é®é¢æ¸
åï¼æªè§£æç {{}} ä¸ä¼çå¨äº§ç©éï¼ */ |
| | | function renderText(template: string, state: RenderState): string { |
| | | return resolveTextWithReport(template, state.scope, (path) => { |
| | | const key = `${state.where}|${path}`; |
| | | if (state.reported.has(key)) { |
| | | return; |
| | | } |
| | | state.reported.add(key); |
| | | state.errors.push(`${state.where}ç»å®ã{{${path}}}ãå¨å½åæ°æ®ä¸åä¸å°å¼ï¼å·²æ¸²æä¸ºç©º`); |
| | | }); |
| | | } |
| | | |
| | | /** æ ·å¼è§åæ°ç» â CSS ææ¬ï¼GrapesJS ç¨ #ç»ä»¶id éæ©å¨ï¼è¿éæ¹åæ data 屿§éæ©å¨ */ |
| | | function buildCss(styles: RenderStyleRule[] | string | undefined): string { |
| | | if (typeof styles === 'string') { |
| | | return styles; |
| | | } |
| | | if (!Array.isArray(styles)) { |
| | | return ''; |
| | | } |
| | | const blocks: string[] = []; |
| | | const mediaBlocks = new Map<string, string[]>(); |
| | | |
| | | styles.forEach((rule) => { |
| | | const selector = (rule.selectors ?? []).map(toSafeSelector).filter(Boolean).join(', '); |
| | | const body = cssStyle(rule.style ?? {}); |
| | | if (!selector || !body) { |
| | | return; |
| | | } |
| | | const line = `${selector} { ${body} }`; |
| | | const mediaText = rule.mediaText?.trim(); |
| | | if (mediaText) { |
| | | mediaBlocks.set(mediaText, [...(mediaBlocks.get(mediaText) ?? []), line]); |
| | | return; |
| | | } |
| | | blocks.push(line); |
| | | }); |
| | | |
| | | mediaBlocks.forEach((lines, mediaText) => { |
| | | const atRule = mediaText.startsWith('@') ? mediaText : `@media ${mediaText}`; |
| | | blocks.push(`${atRule} { ${lines.join(' ')} }`); |
| | | }); |
| | | return blocks.join('\n'); |
| | | } |
| | | |
| | | /** #ç»ä»¶id â [data-qc-id="ç»ä»¶id"]ï¼éå¤è¡å¤å¶åæ ·å¼ä»ç¶å½ä¸ */ |
| | | function toSafeSelector(selector: string): string { |
| | | return selector.replaceAll(/#([\w-]+)/g, `[${QUALITY_ID_ATTR}="$1"]`); |
| | | } |
| | | |
| | | /** ç»è£
æç»ææ¡£ï¼çº¸å¼ ä¸é¡µè¾¹è·äº¤ç» @pageï¼æ£æéä¸åéå¤çç½ï¼æè½ä¿è¯æ¯é¡µé½æè¾¹è· */ |
| | | function buildDocument( |
| | | page: PageSetting | undefined, |
| | | body: string, |
| | | css: string, |
| | | context: ReportContext, |
| | | ): string { |
| | | const { heightMm, marginMm, widthMm } = resolvePageMm(page); |
| | | const size = `${widthMm}mm ${heightMm}mm`; |
| | | const margin = `${marginMm.top}mm ${marginMm.right}mm ${marginMm.bottom}mm ${marginMm.left}mm`; |
| | | const title = context.report.reportName || context.report.reportNo || 'è´¨æ£æ¥å'; |
| | | return [ |
| | | '<!doctype html>', |
| | | '<html lang="zh-CN">', |
| | | '<head>', |
| | | '<meta charset="utf-8" />', |
| | | `<title>${escapeHtml(title)}</title>`, |
| | | '<style>', |
| | | `@page { size: ${size}; margin: ${margin}; }`, |
| | | BASE_CSS, |
| | | css, |
| | | '</style>', |
| | | '</head>', |
| | | `<body>${body}</body>`, |
| | | '</html>', |
| | | ].join('\n'); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * æ¥åå¤å® |
| | | * <p> |
| | | * 卿¸²æåææ£éªé¡¹ç®æ PASS/FAILï¼å¹¶æ±æ»åºæ¥åç»è®ºä¸åæ ¼çã |
| | | * å¤å®åªåçå¨è¿éï¼ç»ä»¶ä¸æ¨¡æ¿é½ä¸å计ç®ï¼ä¿è¯åä¸ä»½æ°æ®å¨ä»»ä½æ¨¡æ¿ä¸å¤å®ä¸è´ã |
| | | */ |
| | | import type { QualityResult, ReportContext, ReportContextItem } from './context'; |
| | | import type { RuleNode } from './rule-engine'; |
| | | |
| | | import { QUALITY_RESULT, QUALITY_RESULT_TEXT } from './context'; |
| | | import { toNumber } from './path'; |
| | | import { |
| | | evaluateConditionNode, |
| | | findUnknownFunction, |
| | | listAllFunctionNames, |
| | | parseRule, |
| | | RuleSyntaxError, |
| | | } from './rule-engine'; |
| | | |
| | | /** è§åä½ç¨åï¼item é项å¤å®ï¼report æ¥åçº§æ±æ» */ |
| | | export type QualityRuleScope = 'item' | 'report'; |
| | | |
| | | export interface QualityRuleDefinition { |
| | | /** è§åæ è¯ï¼ç¨äºåºéæ¶å®ä½ */ |
| | | id?: string; |
| | | /** è§ååç§° */ |
| | | name?: string; |
| | | scope: QualityRuleScope; |
| | | /** å¤å®è¡¨è¾¾å¼ï¼æ±å¼ä¸ºçè§ä¸ºåæ ¼ */ |
| | | expression: string; |
| | | /** 该è§åæ¯å¦åä¸å¤å®ï¼é»è®¤ true */ |
| | | enabled?: boolean; |
| | | } |
| | | |
| | | /** å¾
å¤å®æ è®°ï¼è§åæ¬èº«åºéæ¶ç¨ï¼é¿å
æä¸ç¡®å®å½æåæ ¼ */ |
| | | const PENDING_TEXT = 'å¾
å¤å®'; |
| | | |
| | | /** |
| | | * æ å¤å®è§åæ è®°ï¼æ¢æ²¡æè§åã乿²¡æè§æ ¼ä¸ä¸éã |
| | | * <p> |
| | | * åãå¾
å¤å®ã廿åå¼ï¼å¾
å¤å®æ¯ãæ¬è¯¥å¤ãä½è§åè·æäºãï¼ä»çå¨åæ ¼ç忝éï¼ |
| | | * æ å¤å®è§åæ¯ãè¿é¡¹æ ¹æ¬æ²¡æå¤å®ä¾æ®ãï¼ä¸è®¡å
¥åæ ¼ç忝ãä¹ä¸å䏿¥åç»è®ºã |
| | | */ |
| | | const UNDECIDABLE_TEXT = 'æ å¤å®è§å'; |
| | | |
| | | /** |
| | | * æ ¡éªå
¥åï¼åªç¨å°è¿ä¸ä¸ªå段ï¼ä¸é½å
许缺çã |
| | | * <p> |
| | | * ä¸å¤ç¨ {@link QualityRuleDefinition}ââé£éç expression æ¯å¿
å¡«çï¼ |
| | | * è设计å¨éæ£å¨ç¼è¾çè§åã以åå岿°æ®éçè§å项é½å¯è½æ¯ç©ºçï¼ |
| | | * æ ¡éªå½æ°æ°æ°è¦è½æ¥ä½è¿ç§ãè¿æ²¡åå®ãçå
¥åã |
| | | */ |
| | | export interface RuleValidationInput { |
| | | id?: string; |
| | | name?: string; |
| | | expression?: string; |
| | | } |
| | | |
| | | /** |
| | | * ä¿ååæ ¡éªè§åææ¬ï¼è¯æ³ä¸å½æ°ç½ååã |
| | | * <p> |
| | | * 两åé½è¦æ¦ãåªæ¦è¯æ³çè¯ï¼{@code FOO(1)} è¿ç§è°ç¨ä¼ä¸è·¯åè¿åºéï¼ |
| | | * ä¹åæ¯å¼ æ¥åé½å¸¦ä¸æ¡ãæ æ³æ§è¡ãï¼èç¨æ·å¨è®¾è®¡å¨éçä¸åºåªéä¸å¯¹ã |
| | | * è¿åçé®é¢æ¸
åä¸å端 {@code QualityReportEngine.validateRules} åå£å¾ã |
| | | */ |
| | | export function validateRule(rule: RuleValidationInput): string[] { |
| | | const errors: string[] = []; |
| | | const label = rule.name?.trim() || rule.id || 'æªå½åè§å'; |
| | | if (!rule.expression?.trim()) { |
| | | errors.push(`è§åã${label}ãç表达å¼ä¸ºç©º`); |
| | | return errors; |
| | | } |
| | | |
| | | let node: RuleNode; |
| | | try { |
| | | node = parseRule(rule.expression); |
| | | } catch (error) { |
| | | if (error instanceof RuleSyntaxError) { |
| | | errors.push(`è§åã${label}ãè¯æ³é误ï¼${error.message}ï¼ç¬¬ ${error.position + 1} 个å符ï¼`); |
| | | } else { |
| | | errors.push(`è§åã${label}ãæ æ³è§£æï¼${(error as Error).message}`); |
| | | } |
| | | return errors; |
| | | } |
| | | |
| | | const unknown = findUnknownFunction(node); |
| | | if (unknown) { |
| | | errors.push( |
| | | `è§åã${label}ãè°ç¨äºä¸æ¯æç彿° ${unknown}ï¼å¯ç¨å½æ°ï¼${listAllFunctionNames().join('ã')}`, |
| | | ); |
| | | } |
| | | return errors; |
| | | } |
| | | |
| | | /** ç¨è§æ ¼ä¸ä¸éå¤å®å项ï¼å®æµå¼ä¸å¨åºé´å
å³ä¸åæ ¼ */ |
| | | function evaluateByLimit(item: ReportContextItem): boolean | undefined { |
| | | const actual = toNumber(item.actualValue); |
| | | if (!Number.isFinite(actual)) { |
| | | return undefined; |
| | | } |
| | | const hasUpper = typeof item.upperLimit === 'number' && Number.isFinite(item.upperLimit); |
| | | const hasLower = typeof item.lowerLimit === 'number' && Number.isFinite(item.lowerLimit); |
| | | if (!hasUpper && !hasLower) { |
| | | return undefined; |
| | | } |
| | | if (hasUpper && actual > item.upperLimit!) { |
| | | return false; |
| | | } |
| | | if (hasLower && actual < item.lowerLimit!) { |
| | | return false; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | function normalizeExisting(result: string): QualityResult | undefined { |
| | | const text = result?.trim().toUpperCase(); |
| | | if (text === QUALITY_RESULT.PASS) { |
| | | return QUALITY_RESULT.PASS; |
| | | } |
| | | if (text === QUALITY_RESULT.FAIL) { |
| | | return QUALITY_RESULT.FAIL; |
| | | } |
| | | return undefined; |
| | | } |
| | | |
| | | function applyResult(item: ReportContextItem, result: QualityResult | undefined): void { |
| | | if (!result) { |
| | | item.result = ''; |
| | | item.resultText = PENDING_TEXT; |
| | | return; |
| | | } |
| | | item.result = result; |
| | | item.resultText = QUALITY_RESULT_TEXT[result]; |
| | | } |
| | | |
| | | /** |
| | | * 没æå¤å®ä¾æ®ï¼ç½®ä¸ºãæ å¤å®è§åãã |
| | | * <p> |
| | | * æ¥åé常æãè¯æ ·è´¨é mããç§°éç¶ m0ãè¿ç±»åªä¾å
¬å¼åæ°çè¿ç¨åæ°ï¼å®ä»¬æ²¡æè§æ ¼ä¹å°±æ ä»å¤å®ï¼ |
| | | * è¥ç
§å¸¸ç®è¿åæ¯ï¼ä¸ä»½æ¬æ¥å
¨åæ ¼çæ¥åä¼å 为å 个è¿ç¨åæ°è¢«åæãå¾
å¤å®ãã |
| | | */ |
| | | function applyUndecidable(item: ReportContextItem): void { |
| | | item.result = ''; |
| | | item.resultText = UNDECIDABLE_TEXT; |
| | | } |
| | | |
| | | function formatPassRate(passCount: number, total: number): string { |
| | | if (total === 0) { |
| | | return ''; |
| | | } |
| | | return `${((passCount / total) * 100).toFixed(2)}%`; |
| | | } |
| | | |
| | | export interface EvaluationOutcome { |
| | | context: ReportContext; |
| | | /** è§åçé®é¢æ¸
åï¼æ¸²ææ¶åºå¨æ¥å䏿¾å¼æ´é²ï¼ä¸è½ææåæ */ |
| | | errors: string[]; |
| | | } |
| | | |
| | | /** |
| | | * è®¡ç®æ£éªé¡¹å¤å®ä¸æ¥åæ±æ»ã |
| | | * <p> |
| | | * å个æ£éªé¡¹çå¤å®ä¼å
级ï¼item ä½ç¨åè§å â è§æ ¼ä¸ä¸é â æ°æ®èªå¸¦ç»æã |
| | | * æ¥å级å¤å®é»è®¤åãå
¨é¨æ£éªé¡¹åæ ¼ãï¼æ report ä½ç¨åè§åæ¶ä»¥è§å为åã |
| | | */ |
| | | export function evaluateReport( |
| | | context: ReportContext, |
| | | rules: QualityRuleDefinition[] = [], |
| | | ): EvaluationOutcome { |
| | | const errors: string[] = []; |
| | | const itemRules = rules.filter((rule) => rule.scope === 'item' && rule.enabled !== false); |
| | | const reportRules = rules.filter((rule) => rule.scope === 'report' && rule.enabled !== false); |
| | | |
| | | const items = context.inspectionItems.map((item) => ({ ...item })); |
| | | /** æ å¤å®ä¾æ®ç项æ°ï¼åªç¨æ¥æè¿ç±»é¡¹ä»åæ ¼ç忝䏿¥åç»è®ºéæåºå» */ |
| | | let undecidableCount = 0; |
| | | |
| | | items.forEach((item, index) => { |
| | | if (itemRules.length > 0) { |
| | | let passed = true; |
| | | let failed = false; |
| | | itemRules.forEach((rule) => { |
| | | // é项è§åå¨ãå项ä¸ä¸æãéæ±å¼ï¼item æåå½åè¡ |
| | | const scoped = { ...context, report: context.report, item, index: index + 1 }; |
| | | try { |
| | | if (!evaluateConditionNode(compiledRule(rule.expression), scoped)) { |
| | | passed = false; |
| | | } |
| | | } catch (error) { |
| | | failed = true; |
| | | errors.push( |
| | | `第 ${index + 1} 项ã${item.itemName}ãçè§åã${rule.name ?? rule.id ?? ''}ãæ æ³æ§è¡ï¼${(error as Error).message}`, |
| | | ); |
| | | } |
| | | }); |
| | | applyResult(item, failed ? undefined : passed ? QUALITY_RESULT.PASS : QUALITY_RESULT.FAIL); |
| | | return; |
| | | } |
| | | |
| | | const byLimit = evaluateByLimit(item); |
| | | if (byLimit !== undefined) { |
| | | applyResult(item, byLimit ? QUALITY_RESULT.PASS : QUALITY_RESULT.FAIL); |
| | | return; |
| | | } |
| | | |
| | | const existing = normalizeExisting(item.result); |
| | | if (!existing) { |
| | | // æ¢æ²¡æè§åã乿²¡æå¤å®ä¾æ®ï¼å¦å®æ åºæ¥ï¼ä¸è¦é»è®¤åæ ¼ |
| | | applyUndecidable(item); |
| | | undecidableCount++; |
| | | errors.push( |
| | | `第 ${index + 1} 项ã${item.itemName || 'æªå½å'}ãæ²¡æè§æ ¼ä¸ä¸é乿²¡æå¤å®è§åï¼æ æ³å¤å®`, |
| | | ); |
| | | return; |
| | | } |
| | | applyResult(item, existing); |
| | | }); |
| | | |
| | | const passCount = items.filter((item) => item.result === QUALITY_RESULT.PASS).length; |
| | | const failCount = items.filter((item) => item.result === QUALITY_RESULT.FAIL).length; |
| | | const total = items.length; |
| | | /** åæ ¼çç忝åªç®ãè½å¤å®ãçé¡¹ï¼æ å¤å®è§åç项æ¢ä¸å åä¹ä¸åå */ |
| | | const decidableCount = total - undecidableCount; |
| | | |
| | | let reportResult: QualityResult | undefined; |
| | | if (reportRules.length > 0) { |
| | | let passed = true; |
| | | let failed = false; |
| | | reportRules.forEach((rule) => { |
| | | const scoped = { ...context, inspectionItems: items, index: 0 }; |
| | | try { |
| | | if (!evaluateConditionNode(compiledRule(rule.expression), scoped)) { |
| | | passed = false; |
| | | } |
| | | } catch (error) { |
| | | failed = true; |
| | | errors.push(`æ¥å级è§åã${rule.name ?? rule.id ?? ''}ãæ æ³æ§è¡ï¼${(error as Error).message}`); |
| | | } |
| | | }); |
| | | reportResult = failed ? undefined : passed ? QUALITY_RESULT.PASS : QUALITY_RESULT.FAIL; |
| | | } else if (decidableCount > 0) { |
| | | // åªæãå
¨é¨å¯å¤å®é¡¹é½åæ ¼ãæç»åæ ¼ç»è®ºãæ å¤å®è§åç项ä¸åä¸ï¼å®ä»¬æ²¡æå¯¹éå¯è¨ã |
| | | // åå¨å¾
å¤å®é¡¹ï¼è§åè·æï¼æ¶ä¸ç»ç»è®ºï¼æ²¡éªè¿ä¸è½è¯´åæ ¼ï¼æ²¡è¯æ®ä¹ä¸è½è¯´ä¸åæ ¼ï¼ |
| | | // å¦åä¼åºç°ãç»è®ºåæ ¼ãåæ ¼ç 0%ãè¿ç§èªç¸çç¾çæ¥åã |
| | | if (failCount > 0) { |
| | | reportResult = QUALITY_RESULT.FAIL; |
| | | } else if (passCount === decidableCount) { |
| | | reportResult = QUALITY_RESULT.PASS; |
| | | } |
| | | } |
| | | |
| | | const report = { |
| | | ...context.report, |
| | | total, |
| | | passCount, |
| | | failCount, |
| | | passRate: formatPassRate(passCount, decidableCount), |
| | | result: reportResult ?? '', |
| | | resultText: reportResult ? QUALITY_RESULT_TEXT[reportResult] : PENDING_TEXT, |
| | | conclusion: reportResult ? QUALITY_RESULT_TEXT[reportResult] : PENDING_TEXT, |
| | | }; |
| | | |
| | | return { context: { report, inspectionItems: items }, errors }; |
| | | } |
| | | |
| | | /** æ ¡éªè¿çè§ååªè§£æä¸æ¬¡ï¼å¤è¡å¤ç¨æ¶ç´æ¥å¯¹è¯æ³æ æ±å¼ */ |
| | | const ruleCache = new Map<string, RuleNode>(); |
| | | |
| | | function compiledRule(expression: string): RuleNode { |
| | | const cached = ruleCache.get(expression); |
| | | if (cached) { |
| | | return cached; |
| | | } |
| | | const node = parseRule(expression); |
| | | ruleCache.set(expression, node); |
| | | return node; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * å¤å®è§å弿 |
| | | * <p> |
| | | * éå½ä¸éè§£ææ AST åèªå·±æ±å¼ï¼å
¨ç¨ä¸ç¢° eval / new Functionï¼ |
| | | * è§åææ¬æ¥èªç¨æ·ï¼ä¸æ¦è½è¢«å½ä½ä»£ç æ§è¡å°±æ¯æ³¨å
¥å£åã |
| | | * æ¯æçè¯æ³è§ docs/æºè½è´¨æ£æ¥åå¹³å°-æ¹æ¡è®¾è®¡.md §13ã |
| | | */ |
| | | import { parsePath, readPath, toNumber, toNumberArray } from './path'; |
| | | |
| | | /** è§åææ¬åå¨è¯æ³é误 */ |
| | | export class RuleSyntaxError extends Error { |
| | | constructor( |
| | | message: string, |
| | | readonly position: number, |
| | | ) { |
| | | super(message); |
| | | this.name = 'RuleSyntaxError'; |
| | | } |
| | | } |
| | | |
| | | /** è§åè¿è¡æé误ï¼å½æ°ä¸åå¨ãåæ°ç±»åä¸å¯¹çï¼ */ |
| | | export class RuleRuntimeError extends Error { |
| | | constructor(message: string) { |
| | | super(message); |
| | | this.name = 'RuleRuntimeError'; |
| | | } |
| | | } |
| | | |
| | | type TokenType = 'comma' | 'eof' | 'lparen' | 'number' | 'operator' | 'rparen' | 'string' | 'word'; |
| | | |
| | | interface Token { |
| | | type: TokenType; |
| | | value: string; |
| | | position: number; |
| | | } |
| | | |
| | | export type RuleNode = |
| | | | { type: 'literal'; value: boolean | null | number | string } |
| | | | { type: 'path'; path: string } |
| | | | { type: 'call'; name: string; args: RuleNode[] } |
| | | | { type: 'unary'; operator: string; operand: RuleNode } |
| | | | { type: 'binary'; operator: string; left: RuleNode; right: RuleNode } |
| | | | { type: 'between'; value: RuleNode; lower: RuleNode; upper: RuleNode; negated: boolean } |
| | | | { type: 'in'; value: RuleNode; items: RuleNode[]; negated: boolean }; |
| | | |
| | | const KEYWORDS = new Set([ |
| | | 'AND', |
| | | 'BETWEEN', |
| | | 'FALSE', |
| | | 'IN', |
| | | 'NOT', |
| | | 'NOT_IN', |
| | | 'NULL', |
| | | 'OR', |
| | | 'TRUE', |
| | | ]); |
| | | |
| | | const IDENT_START = /[A-Za-z_$]/; |
| | | const IDENT_PART = /[\w$]/; |
| | | const TWO_CHAR_OPERATORS = new Set(['<=', '>=', '!=', '<>', '==']); |
| | | |
| | | /* ------------------------------ è¯æ³åæ ------------------------------ */ |
| | | |
| | | function tokenize(source: string): Token[] { |
| | | const tokens: Token[] = []; |
| | | const length = source.length; |
| | | let index = 0; |
| | | |
| | | while (index < length) { |
| | | const char = source[index]!; |
| | | |
| | | if (/\s/.test(char)) { |
| | | index += 1; |
| | | continue; |
| | | } |
| | | |
| | | if (char === "'" || char === '"') { |
| | | const quote = char; |
| | | let value = ''; |
| | | let cursor = index + 1; |
| | | while (cursor < length && source[cursor] !== quote) { |
| | | if (source[cursor] === '\\' && cursor + 1 < length) { |
| | | value += source[cursor + 1]; |
| | | cursor += 2; |
| | | continue; |
| | | } |
| | | value += source[cursor]; |
| | | cursor += 1; |
| | | } |
| | | if (cursor >= length) { |
| | | throw new RuleSyntaxError('å符串缺å°ç»å°¾å¼å·', index); |
| | | } |
| | | tokens.push({ type: 'string', value, position: index }); |
| | | index = cursor + 1; |
| | | continue; |
| | | } |
| | | |
| | | if (/[0-9]/.test(char)) { |
| | | let cursor = index; |
| | | while (cursor < length && /[0-9.]/.test(source[cursor]!)) { |
| | | cursor += 1; |
| | | } |
| | | const text = source.slice(index, cursor); |
| | | if (!/^\d+(?:\.\d+)?$/.test(text)) { |
| | | throw new RuleSyntaxError(`æ°åæ ¼å¼ä¸æ£ç¡®ï¼${text}`, index); |
| | | } |
| | | tokens.push({ type: 'number', value: text, position: index }); |
| | | index = cursor; |
| | | continue; |
| | | } |
| | | |
| | | if (IDENT_START.test(char)) { |
| | | let cursor = index; |
| | | while (cursor < length && IDENT_PART.test(source[cursor]!)) { |
| | | cursor += 1; |
| | | } |
| | | let value = source.slice(index, cursor); |
| | | // è·¯å¾å°¾å·´ï¼.屿§ ä¸ [䏿 ] |
| | | for (;;) { |
| | | const next = source[cursor]; |
| | | if (next === '.' && IDENT_START.test(source[cursor + 1] ?? '')) { |
| | | let end = cursor + 1; |
| | | while (end < length && IDENT_PART.test(source[end]!)) { |
| | | end += 1; |
| | | } |
| | | value += source.slice(cursor, end); |
| | | cursor = end; |
| | | continue; |
| | | } |
| | | if (next === '[') { |
| | | const close = source.indexOf(']', cursor); |
| | | if (close === -1) { |
| | | throw new RuleSyntaxError('æ°ç»ä¸æ ç¼ºå° ]', cursor); |
| | | } |
| | | const inner = source.slice(cursor + 1, close).trim(); |
| | | if (!/^\d+$/.test(inner)) { |
| | | throw new RuleSyntaxError(`æ°ç»ä¸æ åªè½æ¯æ°åï¼${inner}`, cursor); |
| | | } |
| | | value += `[${inner}]`; |
| | | cursor = close + 1; |
| | | continue; |
| | | } |
| | | break; |
| | | } |
| | | tokens.push({ type: 'word', value, position: index }); |
| | | index = cursor; |
| | | continue; |
| | | } |
| | | |
| | | const two = source.slice(index, index + 2); |
| | | if (TWO_CHAR_OPERATORS.has(two)) { |
| | | tokens.push({ type: 'operator', value: two, position: index }); |
| | | index += 2; |
| | | continue; |
| | | } |
| | | |
| | | if (char === '(' || char === ')') { |
| | | tokens.push({ type: char === '(' ? 'lparen' : 'rparen', value: char, position: index }); |
| | | index += 1; |
| | | continue; |
| | | } |
| | | if (char === ',') { |
| | | tokens.push({ type: 'comma', value: char, position: index }); |
| | | index += 1; |
| | | continue; |
| | | } |
| | | if ('><=+-*/!'.includes(char)) { |
| | | tokens.push({ type: 'operator', value: char, position: index }); |
| | | index += 1; |
| | | continue; |
| | | } |
| | | |
| | | throw new RuleSyntaxError(`æ æ³è¯å«çå符ã${char}ã`, index); |
| | | } |
| | | |
| | | tokens.push({ type: 'eof', value: '', position: length }); |
| | | return tokens; |
| | | } |
| | | |
| | | /* ------------------------------ è¯æ³åæ ------------------------------ */ |
| | | |
| | | class Parser { |
| | | private cursor = 0; |
| | | |
| | | constructor(private readonly tokens: Token[]) {} |
| | | |
| | | parse(): RuleNode { |
| | | const node = this.parseOr(); |
| | | const token = this.peek(); |
| | | if (token.type !== 'eof') { |
| | | throw new RuleSyntaxError(`å¤ä½çå
容ï¼${token.value}`, token.position); |
| | | } |
| | | return node; |
| | | } |
| | | |
| | | private parseOr(): RuleNode { |
| | | let left = this.parseAnd(); |
| | | while (this.matchWord('OR')) { |
| | | left = { type: 'binary', operator: 'OR', left, right: this.parseAnd() }; |
| | | } |
| | | return left; |
| | | } |
| | | |
| | | private parseAnd(): RuleNode { |
| | | let left = this.parseComparison(); |
| | | while (this.matchWord('AND')) { |
| | | left = { type: 'binary', operator: 'AND', left, right: this.parseComparison() }; |
| | | } |
| | | return left; |
| | | } |
| | | |
| | | private parseComparison(): RuleNode { |
| | | const left = this.parseAdditive(); |
| | | |
| | | if (this.matchWord('BETWEEN')) { |
| | | const lower = this.parseAdditive(); |
| | | if (!this.matchWord('AND')) { |
| | | throw new RuleSyntaxError('BETWEEN ç¼ºå° AND ä¸ä¸ç', this.peek().position); |
| | | } |
| | | const upper = this.parseAdditive(); |
| | | return { type: 'between', value: left, lower, upper, negated: false }; |
| | | } |
| | | |
| | | if (this.matchWord('NOT')) { |
| | | if (this.matchWord('IN') || this.matchWord('NOT_IN')) { |
| | | return { type: 'in', value: left, items: this.parseList(), negated: true }; |
| | | } |
| | | throw new RuleSyntaxError('NOT åªè½ç¨äº NOT IN', this.peek().position); |
| | | } |
| | | |
| | | if (this.matchWord('IN')) { |
| | | return { type: 'in', value: left, items: this.parseList(), negated: false }; |
| | | } |
| | | |
| | | const token = this.peek(); |
| | | if (token.type === 'operator' && ['<', '<=', '=', '==', '!=', '<>', '>', '>='].includes(token.value)) { |
| | | this.cursor += 1; |
| | | const operator = token.value === '==' ? '=' : token.value === '<>' ? '!=' : token.value; |
| | | return { type: 'binary', operator, left, right: this.parseAdditive() }; |
| | | } |
| | | |
| | | return left; |
| | | } |
| | | |
| | | private parseList(): RuleNode[] { |
| | | this.expect('lparen'); |
| | | const items: RuleNode[] = []; |
| | | if (this.peek().type !== 'rparen') { |
| | | items.push(this.parseOr()); |
| | | while (this.peek().type === 'comma') { |
| | | this.cursor += 1; |
| | | items.push(this.parseOr()); |
| | | } |
| | | } |
| | | this.expect('rparen'); |
| | | return items; |
| | | } |
| | | |
| | | private parseAdditive(): RuleNode { |
| | | let left = this.parseMultiplicative(); |
| | | for (;;) { |
| | | const token = this.peek(); |
| | | if (token.type !== 'operator' || (token.value !== '+' && token.value !== '-')) { |
| | | return left; |
| | | } |
| | | this.cursor += 1; |
| | | left = { type: 'binary', operator: token.value, left, right: this.parseMultiplicative() }; |
| | | } |
| | | } |
| | | |
| | | private parseMultiplicative(): RuleNode { |
| | | let left = this.parseUnary(); |
| | | for (;;) { |
| | | const token = this.peek(); |
| | | if (token.type !== 'operator' || (token.value !== '*' && token.value !== '/')) { |
| | | return left; |
| | | } |
| | | this.cursor += 1; |
| | | left = { type: 'binary', operator: token.value, left, right: this.parseUnary() }; |
| | | } |
| | | } |
| | | |
| | | private parseUnary(): RuleNode { |
| | | const token = this.peek(); |
| | | if (token.type === 'operator' && (token.value === '-' || token.value === '!')) { |
| | | this.cursor += 1; |
| | | return { type: 'unary', operator: token.value, operand: this.parseUnary() }; |
| | | } |
| | | return this.parsePrimary(); |
| | | } |
| | | |
| | | private parsePrimary(): RuleNode { |
| | | const token = this.peek(); |
| | | |
| | | if (token.type === 'number') { |
| | | this.cursor += 1; |
| | | return { type: 'literal', value: Number(token.value) }; |
| | | } |
| | | if (token.type === 'string') { |
| | | this.cursor += 1; |
| | | return { type: 'literal', value: token.value }; |
| | | } |
| | | if (token.type === 'lparen') { |
| | | this.cursor += 1; |
| | | const node = this.parseOr(); |
| | | this.expect('rparen'); |
| | | return node; |
| | | } |
| | | if (token.type === 'word') { |
| | | this.cursor += 1; |
| | | const upper = token.value.toUpperCase(); |
| | | if (upper === 'TRUE') { |
| | | return { type: 'literal', value: true }; |
| | | } |
| | | if (upper === 'FALSE') { |
| | | return { type: 'literal', value: false }; |
| | | } |
| | | if (upper === 'NULL') { |
| | | return { type: 'literal', value: null }; |
| | | } |
| | | if (this.peek().type === 'lparen') { |
| | | return { type: 'call', name: upper, args: this.parseList() }; |
| | | } |
| | | if (KEYWORDS.has(upper)) { |
| | | throw new RuleSyntaxError(`å
³é®å ${upper} çä½ç½®ä¸æ£ç¡®`, token.position); |
| | | } |
| | | return { type: 'path', path: token.value }; |
| | | } |
| | | |
| | | throw new RuleSyntaxError( |
| | | token.type === 'eof' ? '表达å¼ä¸å®æ´' : `æ æ³è§£æçå
容ï¼${token.value}`, |
| | | token.position, |
| | | ); |
| | | } |
| | | |
| | | private peek(): Token { |
| | | return this.tokens[this.cursor]!; |
| | | } |
| | | |
| | | private matchWord(word: string): boolean { |
| | | const token = this.peek(); |
| | | if (token.type === 'word' && token.value.toUpperCase() === word) { |
| | | this.cursor += 1; |
| | | return true; |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private expect(type: TokenType): void { |
| | | const token = this.peek(); |
| | | if (token.type !== type) { |
| | | throw new RuleSyntaxError(`åºè¯¥æ¯ ${type}ï¼å®é
æ¯ã${token.value}ã`, token.position); |
| | | } |
| | | this.cursor += 1; |
| | | } |
| | | } |
| | | |
| | | /** è§£æè§åææ¬ï¼è¯æ³éè¯¯ä¼æ RuleSyntaxError */ |
| | | export function parseRule(expression: string): RuleNode { |
| | | const trimmed = expression.trim(); |
| | | if (!trimmed) { |
| | | throw new RuleSyntaxError('è§å表达å¼ä¸ºç©º', 0); |
| | | } |
| | | return new Parser(tokenize(trimmed)).parse(); |
| | | } |
| | | |
| | | /* ------------------------------ æ±å¼ ------------------------------ */ |
| | | |
| | | function isBlank(value: unknown): boolean { |
| | | return value === null || value === undefined || value === ''; |
| | | } |
| | | |
| | | function truthy(value: unknown): boolean { |
| | | if (Array.isArray(value)) { |
| | | return value.length > 0; |
| | | } |
| | | if (isBlank(value)) { |
| | | return false; |
| | | } |
| | | if (typeof value === 'boolean') { |
| | | return value; |
| | | } |
| | | const numeric = toNumber(value); |
| | | if (Number.isFinite(numeric)) { |
| | | return numeric !== 0; |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** 两侧é½è½å½æ°åæ¶ææ°å¼æ¯è¾ï¼å¦åæå符串æ¯è¾ */ |
| | | function compare(left: unknown, right: unknown): number { |
| | | const leftNumber = toNumber(left); |
| | | const rightNumber = toNumber(right); |
| | | if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) { |
| | | return leftNumber === rightNumber ? 0 : leftNumber > rightNumber ? 1 : -1; |
| | | } |
| | | const leftText = left === null || left === undefined ? '' : String(left); |
| | | const rightText = right === null || right === undefined ? '' : String(right); |
| | | return leftText === rightText ? 0 : leftText > rightText ? 1 : -1; |
| | | } |
| | | |
| | | /** å¤å®ç»æç±»åå¼ç»ä¸æ PASS / FAILï¼å
¼å®¹ä¸æãå¸å°ä¸æ°å 1/0 */ |
| | | const PASS_TOKENS = new Set(['PASS', 'TRUE', 'åæ ¼', 'OK', '1', 'æ¯']); |
| | | const FAIL_TOKENS = new Set(['FAIL', 'FALSE', 'ä¸åæ ¼', 'NG', '0', 'å¦']); |
| | | |
| | | function normalizeResultToken(value: unknown): string { |
| | | if (typeof value === 'boolean') { |
| | | return value ? 'PASS' : 'FAIL'; |
| | | } |
| | | const text = value === null || value === undefined ? '' : String(value).trim().toUpperCase(); |
| | | if (PASS_TOKENS.has(text)) { |
| | | return 'PASS'; |
| | | } |
| | | if (FAIL_TOKENS.has(text)) { |
| | | return 'FAIL'; |
| | | } |
| | | return ''; |
| | | } |
| | | |
| | | function mean(values: number[]): number { |
| | | return values.reduce((sum, item) => sum + item, 0) / values.length; |
| | | } |
| | | |
| | | /** æ ·æ¬æ åå·®ï¼n-1ï¼ï¼SPC è®¡ç® CP/CPK ç¨è¿ä¸ªå£å¾ */ |
| | | function standardDeviation(values: number[]): number { |
| | | if (values.length < 2) { |
| | | return Number.NaN; |
| | | } |
| | | const average = mean(values); |
| | | const variance = |
| | | values.reduce((sum, item) => sum + (item - average) ** 2, 0) / (values.length - 1); |
| | | return Math.sqrt(variance); |
| | | } |
| | | |
| | | function requireNumbers(name: string, value: unknown): number[] { |
| | | const values = toNumberArray(value); |
| | | if (values.length === 0) { |
| | | throw new RuleRuntimeError(`彿° ${name} éè¦æ°å¼ååæ°ï¼å®é
åå°ã${String(value)}ã`); |
| | | } |
| | | return values; |
| | | } |
| | | |
| | | function requireNumber(name: string, value: unknown): number { |
| | | const numeric = toNumber(value); |
| | | if (!Number.isFinite(numeric)) { |
| | | throw new RuleRuntimeError(`彿° ${name} éè¦æ°ååæ°ï¼å®é
åå°ã${String(value)}ã`); |
| | | } |
| | | return numeric; |
| | | } |
| | | |
| | | /** ç½åå彿°è¡¨ï¼è§åææ¬åªè½è°ç¨è¿éé¢ç彿° */ |
| | | const FUNCTIONS: Record<string, (args: unknown[]) => unknown> = { |
| | | ABS: (args) => Math.abs(requireNumber('ABS', args[0])), |
| | | AVG: (args) => mean(requireNumbers('AVG', args[0])), |
| | | COUNT: (args) => (Array.isArray(args[0]) ? args[0].length : toNumberArray(args[0]).length), |
| | | /** è¿ç¨è½åææ° CP=(USL-LSL)/(6Ï) */ |
| | | CP: (args) => { |
| | | const values = requireNumbers('CP', args[0]); |
| | | const deviation = standardDeviation(values); |
| | | if (!Number.isFinite(deviation) || deviation === 0) { |
| | | return Number.NaN; |
| | | } |
| | | return (requireNumber('CP', args[1]) - requireNumber('CP', args[2])) / (6 * deviation); |
| | | }, |
| | | /** è¿ç¨è½åææ° CPK=min(USL-μ, μ-LSL)/(3Ï) */ |
| | | CPK: (args) => { |
| | | const values = requireNumbers('CPK', args[0]); |
| | | const deviation = standardDeviation(values); |
| | | if (!Number.isFinite(deviation) || deviation === 0) { |
| | | return Number.NaN; |
| | | } |
| | | const upper = requireNumber('CPK', args[1]); |
| | | const lower = requireNumber('CPK', args[2]); |
| | | const average = mean(values); |
| | | return Math.min(upper - average, average - lower) / (3 * deviation); |
| | | }, |
| | | /** ä¸åæ ¼çï¼ç¾åæ¯ï¼ï¼å£å¾ä¸ PASS_RATE ä¸è´ */ |
| | | FAIL_RATE: (args) => { |
| | | const values = Array.isArray(args[0]) ? args[0].flat(Number.POSITIVE_INFINITY) : [args[0]]; |
| | | if (values.length === 0) { |
| | | return Number.NaN; |
| | | } |
| | | const failCount = values.filter((item) => normalizeResultToken(item) === 'FAIL').length; |
| | | return Number(((failCount / values.length) * 100).toFixed(2)); |
| | | }, |
| | | MAX: (args) => Math.max(...requireNumbers('MAX', args[0])), |
| | | MIN: (args) => Math.min(...requireNumbers('MIN', args[0])), |
| | | /** åæ ¼çï¼ç¾åæ¯ï¼ä¿ç两ä½ï¼ */ |
| | | PASS_RATE: (args) => { |
| | | const values = Array.isArray(args[0]) ? args[0].flat(Number.POSITIVE_INFINITY) : [args[0]]; |
| | | if (values.length === 0) { |
| | | return Number.NaN; |
| | | } |
| | | const passCount = values.filter((item) => normalizeResultToken(item) === 'PASS').length; |
| | | return Number(((passCount / values.length) * 100).toFixed(2)); |
| | | }, |
| | | ROUND: (args) => { |
| | | const digits = args.length > 1 ? requireNumber('ROUND', args[1]) : 0; |
| | | const factor = 10 ** digits; |
| | | return Math.round(requireNumber('ROUND', args[0]) * factor) / factor; |
| | | }, |
| | | STDDEV: (args) => standardDeviation(requireNumbers('STDDEV', args[0])), |
| | | SUM: (args) => requireNumbers('SUM', args[0]).reduce((sum, item) => sum + item, 0), |
| | | }; |
| | | |
| | | /** |
| | | * ç½ååéçå
¨é¨å½æ°åï¼ä¾ä¿ååæ ¡éªä¸çé¢æç¤ºä½¿ç¨ã |
| | | * <p> |
| | | * ä¸è¦å¦æä¸ä»½å° UI ä¸ï¼ç½å忝弿çä¸é¨åï¼æä¸ä»½å°±ä¼å¨å 彿°æ¶æ¼æ¹ï¼ |
| | | * ç¨æ·çå°çå¯ç¨å½æ°å表ä¸å¼æå®é
æ¯æçææå¯¹ä¸ä¸ã |
| | | */ |
| | | export function listAllFunctionNames(): string[] { |
| | | return Object.keys(FUNCTIONS); |
| | | } |
| | | |
| | | /** éææ£æ¥è¡¨è¾¾å¼éè°ç¨ç彿°æ¯å¦å¨ç½ååå
ï¼ç¨äºä¿ååæ ¡éª */ |
| | | export function listFunctionNames(node: RuleNode): string[] { |
| | | if (node.type === 'call') { |
| | | return [node.name, ...node.args.flatMap(listFunctionNames)]; |
| | | } |
| | | if (node.type === 'unary') { |
| | | return listFunctionNames(node.operand); |
| | | } |
| | | if (node.type === 'binary') { |
| | | return [...listFunctionNames(node.left), ...listFunctionNames(node.right)]; |
| | | } |
| | | if (node.type === 'between') { |
| | | return [ |
| | | ...listFunctionNames(node.value), |
| | | ...listFunctionNames(node.lower), |
| | | ...listFunctionNames(node.upper), |
| | | ]; |
| | | } |
| | | if (node.type === 'in') { |
| | | return [...listFunctionNames(node.value), ...node.items.flatMap(listFunctionNames)]; |
| | | } |
| | | return []; |
| | | } |
| | | |
| | | /** æªç¥å½æ°åè¿å undefinedï¼ä¾è°ç¨æ¹ç»åºå¯è¯»æ¥é */ |
| | | export function findUnknownFunction(node: RuleNode): string | undefined { |
| | | return listFunctionNames(node).find((name) => !FUNCTIONS[name]); |
| | | } |
| | | |
| | | function evaluateNode(node: RuleNode, context: unknown): unknown { |
| | | switch (node.type) { |
| | | case 'literal': { |
| | | return node.value; |
| | | } |
| | | case 'path': { |
| | | return readPath(context, node.path); |
| | | } |
| | | case 'call': { |
| | | const handler = FUNCTIONS[node.name]; |
| | | if (!handler) { |
| | | throw new RuleRuntimeError( |
| | | `䏿¯æå½æ° ${node.name}ï¼å¯ç¨å½æ°ï¼${Object.keys(FUNCTIONS).join('ã')}`, |
| | | ); |
| | | } |
| | | return handler(node.args.map((arg) => evaluateNode(arg, context))); |
| | | } |
| | | case 'unary': { |
| | | const value = evaluateNode(node.operand, context); |
| | | return node.operator === '!' ? !truthy(value) : -toNumber(value); |
| | | } |
| | | case 'between': { |
| | | const value = evaluateNode(node.value, context); |
| | | const hit = |
| | | compare(value, evaluateNode(node.lower, context)) >= 0 && |
| | | compare(value, evaluateNode(node.upper, context)) <= 0; |
| | | return node.negated ? !hit : hit; |
| | | } |
| | | case 'in': { |
| | | const value = evaluateNode(node.value, context); |
| | | const hit = node.items.some( |
| | | (item) => compare(value, evaluateNode(item, context)) === 0, |
| | | ); |
| | | return node.negated ? !hit : hit; |
| | | } |
| | | case 'binary': { |
| | | return evaluateBinary(node, context); |
| | | } |
| | | default: { |
| | | throw new RuleRuntimeError('è§åèç¹ç±»å䏿¯æ'); |
| | | } |
| | | } |
| | | } |
| | | |
| | | function evaluateBinary( |
| | | node: Extract<RuleNode, { type: 'binary' }>, |
| | | context: unknown, |
| | | ): unknown { |
| | | if (node.operator === 'AND') { |
| | | return truthy(evaluateNode(node.left, context)) && truthy(evaluateNode(node.right, context)); |
| | | } |
| | | if (node.operator === 'OR') { |
| | | return truthy(evaluateNode(node.left, context)) || truthy(evaluateNode(node.right, context)); |
| | | } |
| | | |
| | | const left = evaluateNode(node.left, context); |
| | | const right = evaluateNode(node.right, context); |
| | | |
| | | switch (node.operator) { |
| | | case '=': { |
| | | return compare(left, right) === 0; |
| | | } |
| | | case '!=': { |
| | | return compare(left, right) !== 0; |
| | | } |
| | | case '>': { |
| | | return compare(left, right) > 0; |
| | | } |
| | | case '>=': { |
| | | return compare(left, right) >= 0; |
| | | } |
| | | case '<': { |
| | | return compare(left, right) < 0; |
| | | } |
| | | case '<=': { |
| | | return compare(left, right) <= 0; |
| | | } |
| | | case '+': { |
| | | const leftNumber = toNumber(left); |
| | | const rightNumber = toNumber(right); |
| | | // 䏤侧齿¯æ°å¼æåå æ³ï¼å¦åæææ¬æ¼æ¥ï¼ç¼å·ç±»å段常ç¨ï¼ |
| | | if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber)) { |
| | | return leftNumber + rightNumber; |
| | | } |
| | | return `${left ?? ''}${right ?? ''}`; |
| | | } |
| | | case '-': { |
| | | return toNumber(left) - toNumber(right); |
| | | } |
| | | case '*': { |
| | | return toNumber(left) * toNumber(right); |
| | | } |
| | | case '/': { |
| | | const divisor = toNumber(right); |
| | | if (divisor === 0) { |
| | | throw new RuleRuntimeError('è§åéåºç°äºé¤ä»¥ 0'); |
| | | } |
| | | return toNumber(left) / divisor; |
| | | } |
| | | default: { |
| | | throw new RuleRuntimeError(`䏿¯æçè¿ç®ç¬¦ ${node.operator}`); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** æ±å¼ï¼è¿ååå§ç»æï¼å¯è½æ¯æ°åãå符串ãå¸å°ï¼ */ |
| | | export function evaluateRule(expression: string, context: unknown): unknown { |
| | | return evaluateNode(parseRule(expression), context); |
| | | } |
| | | |
| | | /** æ±å¼å¹¶è½¬æå¸å°å¤å®ï¼ç¨äºãæ¯å¦åæ ¼ãè¿ç±»æ¡ä»¶ */ |
| | | export function evaluateCondition(expression: string, context: unknown): boolean { |
| | | return truthy(evaluateRule(expression, context)); |
| | | } |
| | | |
| | | /** 对已解æçè¯æ³æ æ±å¼ï¼é¿å
å䏿¡è§åå¨å¤è¡ä¸éå¤è§£æ */ |
| | | export function evaluateRuleNode(node: RuleNode, context: unknown): unknown { |
| | | return evaluateNode(node, context); |
| | | } |
| | | |
| | | /** 对已解æçè¯æ³æ åå¸å°å¤å® */ |
| | | export function evaluateConditionNode(node: RuleNode, context: unknown): boolean { |
| | | return truthy(evaluateRuleNode(node, context)); |
| | | } |
| | | |
| | | /** è§£æåçè·¯å¾ä¾èµï¼æ¸²æå坿®æ¤æ£æ¥æ°æ®æ¯å¦é½å¤ */ |
| | | export function listRulePaths(node: RuleNode): string[] { |
| | | const paths: string[] = []; |
| | | const visit = (current: RuleNode): void => { |
| | | switch (current.type) { |
| | | case 'path': { |
| | | paths.push(current.path); |
| | | return; |
| | | } |
| | | case 'call': { |
| | | current.args.forEach(visit); |
| | | return; |
| | | } |
| | | case 'unary': { |
| | | visit(current.operand); |
| | | return; |
| | | } |
| | | case 'binary': { |
| | | visit(current.left); |
| | | visit(current.right); |
| | | return; |
| | | } |
| | | case 'between': { |
| | | visit(current.value); |
| | | visit(current.lower); |
| | | visit(current.upper); |
| | | return; |
| | | } |
| | | case 'in': { |
| | | visit(current.value); |
| | | current.items.forEach(visit); |
| | | return; |
| | | } |
| | | default: { |
| | | return; |
| | | } |
| | | } |
| | | }; |
| | | visit(node); |
| | | // ç»è®¡å½æ°ä½ç¨å¨æ°ç»è·¯å¾ä¸ï¼è¿é廿æ«ç«¯å段åªä¿çæ°ç»æ¬èº« |
| | | return [...new Set(paths.map((path) => parsePath(path).join('.')))]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { escapeHtmlMultiline } from '../core/html'; |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | export const reportFooterDefinition: QualityComponentDefinition = { |
| | | type: 'ReportFooter', |
| | | name: 'reportFooter', |
| | | label: 'æ¥å页è', |
| | | category: QUALITY_CATEGORY.HEADER, |
| | | icon: QUALITY_ICONS.footer, |
| | | aiHint: |
| | | 'ææ¡£æåºé¨çè½æ¬¾åï¼æ£éªå/å®¡æ ¸äºº/æ¥æç¾ç½²è¡ãååä¸èç³»çµè¯ãæ´ä»½æ¥åæå¤ä¸ä¸ªï¼æ¾å¨æåã' + |
| | | 'ç¾ç½²è¡å³ä½¿æå¨æ£éªè¡¨æ ¼çæåä¸è¡ï¼ä¹å±äºè½æ¬¾è䏿¯æ£éªæ°æ®ï¼è¦ååºæ¥æ¾è¿æè¿éï¼' + |
| | | 'å件页èä¸ç¾ç½²è¡åæä¸å¤ãç¾ç½²è¡éç人å䏿¥æç空ï¼ä¸è¦åæ»æä¸ä»½æ¥åçå¼ã', |
| | | contentKey: 'text', |
| | | defaults: { text: 'æ£éªåï¼__________ããå®¡æ ¸ï¼__________ããæ¥æï¼__________' }, |
| | | propertySchema: [ |
| | | { |
| | | key: 'text', |
| | | label: '页èæå', |
| | | type: 'string', |
| | | defaultValue: 'æ£éªåï¼__________ããå®¡æ ¸ï¼__________ããæ¥æï¼__________', |
| | | bindable: true, |
| | | }, |
| | | ], |
| | | dataSchema: [{ key: 'text', label: '页èæå', type: 'string', bindable: true }], |
| | | buildContent: (properties) => ({ |
| | | tagName: 'div', |
| | | content: escapeHtmlMultiline(properties.text), |
| | | style: { |
| | | 'font-size': '11px', |
| | | color: '#8c8c8c', |
| | | 'text-align': 'center', |
| | | padding: '8px 0', |
| | | 'border-top': '1px solid #d9d9d9', |
| | | margin: '12px 0 0', |
| | | }, |
| | | }), |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { cssStyle, escapeHtml } from '../core/html'; |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | /** æ¥åç¼å·è¡ï¼æªç»å®æåæ¶æ¸²æä¸ºç©ºï¼ä¸ä¼å¨æ¥åä¸çä¸è±æ¬å· */ |
| | | const REPORT_NO_EXPR = '{{report.reportNo}}'; |
| | | |
| | | export const reportHeaderDefinition: QualityComponentDefinition = { |
| | | type: 'ReportHeader', |
| | | name: 'reportHeader', |
| | | label: 'æ¥å页ç', |
| | | category: QUALITY_CATEGORY.HEADER, |
| | | icon: QUALITY_ICONS.header, |
| | | aiHint: |
| | | 'ææ¡£æé¡¶é¨çæ¬å¤´åï¼å
¬å¸åç§°ãæ¥åæ é¢ãæ¥åç¼å·ãæ´ä»½æ¥åæå¤ä¸ä¸ªï¼ä¸å¿
é¡»æ¯æå¤å±ç¬¬ä¸ä¸ªç»ä»¶ã', |
| | | defaults: { |
| | | title: 'æºè½è´¨æ£æ¥å', |
| | | align: 'center', |
| | | showReportNo: true, |
| | | reportNo: REPORT_NO_EXPR, |
| | | }, |
| | | propertySchema: [ |
| | | { |
| | | key: 'title', |
| | | label: 'æ¥åæ é¢', |
| | | type: 'string', |
| | | defaultValue: 'æºè½è´¨æ£æ¥å', |
| | | required: true, |
| | | bindable: true, |
| | | }, |
| | | { |
| | | key: 'align', |
| | | label: '坹齿¹å¼', |
| | | type: 'enum', |
| | | defaultValue: 'center', |
| | | options: [ |
| | | { label: '左对é½', value: 'left' }, |
| | | { label: 'å±
ä¸', value: 'center' }, |
| | | ], |
| | | }, |
| | | { key: 'showReportNo', label: 'æ¾ç¤ºæ¥åç¼å·', type: 'boolean', defaultValue: true }, |
| | | { |
| | | key: 'reportNo', |
| | | label: 'æ¥åç¼å·', |
| | | type: 'string', |
| | | defaultValue: REPORT_NO_EXPR, |
| | | bindable: true, |
| | | }, |
| | | ], |
| | | dataSchema: [ |
| | | { key: 'title', label: 'æ¥åæ é¢', type: 'string', bindable: true }, |
| | | { key: 'reportNo', label: 'æ¥åç¼å·', type: 'string', bindable: true }, |
| | | ], |
| | | buildContent: (properties) => { |
| | | const align = String(properties.align ?? 'center'); |
| | | const title = `<div style="${cssStyle({ |
| | | 'font-size': '18px', |
| | | 'font-weight': 600, |
| | | 'text-align': align, |
| | | padding: '8px 0', |
| | | border: 'none', |
| | | 'border-bottom': '2px solid #1677ff', |
| | | })}">${escapeHtml(properties.title)}</div>`; |
| | | const reportNo = properties.showReportNo |
| | | ? `<div style="${cssStyle({ |
| | | 'font-size': '11px', |
| | | color: '#8c8c8c', |
| | | 'text-align': align, |
| | | padding: '4px 0 0', |
| | | })}">æ¥åç¼å·ï¼${escapeHtml(properties.reportNo)}</div>` |
| | | : ''; |
| | | return { |
| | | tagName: 'div', |
| | | components: title + reportNo, |
| | | style: { margin: '0 0 12px' }, |
| | | }; |
| | | }, |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition, QualityProps } from '../core/types'; |
| | | |
| | | import { parseColumnSpec } from '../core/column-spec'; |
| | | import { cssStyle, escapeHtml } from '../core/html'; |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | /** æ ·åä¿¡æ¯é»è®¤å±ç¤ºçåæ®µï¼å¼å
¨é¨æ¥èªæ¥åä¸ä¸æ */ |
| | | const SAMPLE_FIELDS: { expr: string; label: string }[] = [ |
| | | { label: 'æ ·åç¼å·', expr: '{{report.sampleNo}}' }, |
| | | { label: '产ååç§°', expr: '{{report.productName}}' }, |
| | | { label: 'è§æ ¼åå·', expr: '{{report.spec}}' }, |
| | | { label: 'æ¹æ¬¡å·', expr: '{{report.batchNo}}' }, |
| | | { label: 'æ£éªæ¥æ', expr: '{{report.inspectDate}}' }, |
| | | { label: 'æ£éªå', expr: '{{report.inspector}}' }, |
| | | ]; |
| | | |
| | | const CELL_BORDER = '1px solid #d9d9d9'; |
| | | |
| | | function textOf(value: unknown): string { |
| | | return String(value ?? '').trim(); |
| | | } |
| | | |
| | | /** |
| | | * åºè¡¨ç¨çåæ®µï¼å¡«äº fields å°±æ´åç
§å®æ¥ï¼å¦åç¨é»è®¤å段ã |
| | | * <p> |
| | | * ä¸åç»æ£éªé¡¹è¡¨å
±ç¨åä¸å¥ `åæ®µå=ç»å®è¡¨è¾¾å¼` è¯æ³ï¼è§ {@link parseColumnSpec}ï¼ï¼ |
| | | * å
å¾åä¸ä¸ªãé®å¼å¯¹å表ãé¿åºä¸¤ç§åæ³ãåå¹¶æ è®° `#` å¨è¿é没ææä¹ï¼è§£æåºæ¥ä¹åªæ¯è¢«ä¸¢æï¼ |
| | | * æä»¥ validate éæç¡®æ¦ä¸ï¼ä¸è®©ç¨æ·ä»¥ä¸ºèªå·±å对äºã |
| | | */ |
| | | function effectiveFields(properties: QualityProps): { expr: string; label: string }[] { |
| | | const custom = parseColumnSpec(textOf(properties.fields)); |
| | | return custom.items.length > 0 |
| | | ? custom.items.map((field) => ({ label: field.label, expr: field.expr })) |
| | | : SAMPLE_FIELDS; |
| | | } |
| | | |
| | | function buildRows( |
| | | pairsPerRow: number, |
| | | showBorder: boolean, |
| | | fields: { expr: string; label: string }[], |
| | | ): string { |
| | | const perRow = Math.max(pairsPerRow, 1); |
| | | const border = showBorder ? CELL_BORDER : 'none'; |
| | | const rows: string[] = []; |
| | | for (let index = 0; index < fields.length; index += perRow) { |
| | | const rowFields = fields.slice(index, index + perRow); |
| | | // æ«è¡å䏿»¡ä¸ç»æ¶ï¼è®©æåä¸ä¸ªå¼æ ¼æ¨ªååæä½ä¸çåãç
§åæ ·éºæ»¡æ ¼åçè¯ï¼ |
| | | // å³ä¾§ä¼ç§åºå ä¸ªåªæè¾¹æ¡çç©ºæ ¼ï¼å段æ°ä¸æ¯ãæ¯è¡ç»æ°ãæ´æ°åæ¶å¿
ç¶åºç°ï¼ã |
| | | const tailColspan = rowFields.length < perRow ? (perRow - rowFields.length) * 2 + 1 : 1; |
| | | const cells = rowFields |
| | | .map((field, position) => { |
| | | const span = position === rowFields.length - 1 && tailColspan > 1 ? ` colspan="${tailColspan}"` : ''; |
| | | return ( |
| | | `<td style="${cssStyle({ |
| | | border, |
| | | padding: '5px 8px', |
| | | background: '#fafafa', |
| | | 'font-weight': 600, |
| | | width: '90px', |
| | | })}">${escapeHtml(field.label)}</td>` + |
| | | `<td${span} style="${cssStyle({ border, padding: '5px 8px' })}">${escapeHtml(field.expr)}</td>` |
| | | ); |
| | | }) |
| | | .join(''); |
| | | rows.push(`<tr>${cells}</tr>`); |
| | | } |
| | | return rows.join(''); |
| | | } |
| | | |
| | | export const sampleInfoDefinition: QualityComponentDefinition = { |
| | | type: 'SampleInfo', |
| | | name: 'sampleInfo', |
| | | label: 'æ ·åä¿¡æ¯', |
| | | category: QUALITY_CATEGORY.HEADER, |
| | | icon: QUALITY_ICONS.sample, |
| | | aiHint: 'æ ·åä¸ç©æçåºæ¬ä¿¡æ¯åï¼äº§ååç§°ãè§æ ¼ãæ¹å·ãæ°éãçäº§æ¥æãæææ¥æçé®å¼å¯¹ã', |
| | | defaults: { title: 'æ ·åä¿¡æ¯', pairsPerRow: 2, showBorder: true }, |
| | | propertySchema: [ |
| | | { key: 'title', label: 'åºåæ é¢', type: 'string', defaultValue: 'æ ·åä¿¡æ¯' }, |
| | | { |
| | | key: 'pairsPerRow', |
| | | label: 'æ¯è¡å段æ°', |
| | | type: 'number', |
| | | defaultValue: 2, |
| | | tip: 'ä¸è¡æ¾å ç»ãåæ®µå + å¼ã', |
| | | }, |
| | | { key: 'showBorder', label: 'æ¾ç¤ºè¾¹æ¡', type: 'boolean', defaultValue: true }, |
| | | { |
| | | key: 'fields', |
| | | label: 'åæ®µå®ä¹ï¼è¦çé»è®¤å段ï¼', |
| | | type: 'text', |
| | | tip: |
| | | 'ç空åç¨é»è®¤çå
é¡¹ãæ ¼å¼ï¼å段å=ç»å®è¡¨è¾¾å¼ï¼å¤é¡¹ç¨ | åéï¼' + |
| | | 'å¦ æ ·åç¼å·={{report.sampleNo}}|产ååç§°={{report.productName}}|çäº§æ¥æ={{report.produceDate}}ã' + |
| | | 'æ¯è¡æ¾å ç»ä»ç±ä¸é¢çãæ¯è¡å段æ°ãæ§å¶ï¼è¿é䏿¯æ # åå¹¶æ è®°', |
| | | }, |
| | | ], |
| | | dataSchema: SAMPLE_FIELDS.map((field) => ({ |
| | | // 表达å¼å½¢å¦ {{report.sampleNo}}ï¼å»æä¸¤å±è±æ¬å·å³ç»å®è·¯å¾ |
| | | key: field.expr.slice(2, -2), |
| | | label: field.label, |
| | | type: 'string' as const, |
| | | bindable: true, |
| | | })), |
| | | buildContent: (properties) => { |
| | | const rawTitle = String(properties.title ?? '').trim(); |
| | | const heading = rawTitle |
| | | ? `<div style="${cssStyle({ |
| | | 'font-size': '13px', |
| | | 'font-weight': 600, |
| | | margin: '0 0 4px', |
| | | })}">${escapeHtml(rawTitle)}</div>` |
| | | : ''; |
| | | const table = `<table style="${cssStyle({ |
| | | width: '100%', |
| | | 'border-collapse': 'collapse', |
| | | 'font-size': '12px', |
| | | })}">${buildRows( |
| | | Number(properties.pairsPerRow ?? 2), |
| | | properties.showBorder !== false, |
| | | effectiveFields(properties), |
| | | )}</table>`; |
| | | return { |
| | | tagName: 'div', |
| | | components: heading + table, |
| | | style: { margin: '0 0 10px' }, |
| | | }; |
| | | }, |
| | | validate: (properties) => { |
| | | const parsed = parseColumnSpec(textOf(properties.fields)); |
| | | const errors = [...parsed.errors]; |
| | | if (parsed.items.some((field) => field.merge)) { |
| | | errors.push( |
| | | 'æ ·åä¿¡æ¯çãåæ®µå®ä¹ã䏿¯æ # åå¹¶æ è®°ï¼é£æ¯åç»æ£éªé¡¹è¡¨æ ç»åç¨çï¼ï¼è¯·å»æå段ååç #', |
| | | ); |
| | | } |
| | | return errors; |
| | | }, |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | /** |
| | | * è´¨éç»ä»¶ç»ä¸æ³¨åå
¥å£ |
| | | * <p> |
| | | * 设计å¨ã渲æå¼æé½ä»è¿éåç»ä»¶ï¼æ°å¢ä¸ä¸ªè´¨éç»ä»¶ï¼å¨ä¸é¢çæ¸
åéå ä¸ä¸ª definitionï¼ |
| | | * 䏿¹å¨ GrapesJS éæä»£ç ï¼ä¹ä¸æ¹å¨è®¾è®¡å¨é¡µé¢ã |
| | | */ |
| | | import { headingDefinition } from './base/heading'; |
| | | import { dividerDefinition } from './base/divider'; |
| | | import { imageDefinition } from './base/image'; |
| | | import { spacerDefinition } from './base/spacer'; |
| | | import { tableDefinition } from './base/table'; |
| | | import { textDefinition } from './base/text'; |
| | | import { registerQualityComponents } from './core/registry'; |
| | | import { reportFooterDefinition } from './header/report-footer'; |
| | | import { reportHeaderDefinition } from './header/report-header'; |
| | | import { sampleInfoDefinition } from './header/sample-info'; |
| | | import { groupedQualityTableDefinition } from './inspection/grouped-quality-table'; |
| | | import { inspectionItemDefinition } from './inspection/inspection-item'; |
| | | import { qualityTableDefinition } from './inspection/quality-table'; |
| | | import { reportResultDefinition } from './result/report-result'; |
| | | |
| | | /** å
ç½®ç»ä»¶æ¸
åï¼é¡ºåºå³å·¦ä¾§é¢æ¿åä¸åç±»å
çå±ç¤ºé¡ºåº */ |
| | | const BUILT_IN_DEFINITIONS = [ |
| | | headingDefinition, |
| | | textDefinition, |
| | | imageDefinition, |
| | | dividerDefinition, |
| | | spacerDefinition, |
| | | tableDefinition, |
| | | reportHeaderDefinition, |
| | | reportFooterDefinition, |
| | | sampleInfoDefinition, |
| | | qualityTableDefinition, |
| | | groupedQualityTableDefinition, |
| | | inspectionItemDefinition, |
| | | reportResultDefinition, |
| | | ]; |
| | | |
| | | /** å¹çï¼éå¤è°ç¨åªä¼ç¨ææ°å®ä¹è¦ç注å表 */ |
| | | export function registerAllQualityComponents(): void { |
| | | registerQualityComponents(BUILT_IN_DEFINITIONS); |
| | | } |
| | | |
| | | export { extractRawProps, attributesToProps, propsToAttributes } from './core/serializer'; |
| | | export { renderReport } from './engine/render'; |
| | | export { createContext, createEmptyContext, QUALITY_RESULT, QUALITY_RESULT_TEXT } from './engine/context'; |
| | | export { evaluateReport, validateRule } from './engine/report-evaluator'; |
| | | export { listAllFunctionNames } from './engine/rule-engine'; |
| | | export type { |
| | | QualityRuleDefinition, |
| | | QualityRuleScope, |
| | | } from './engine/report-evaluator'; |
| | | export type { |
| | | RenderNode, |
| | | RenderOptions, |
| | | RenderOutcome, |
| | | RenderSchema, |
| | | RenderStyleRule, |
| | | } from './engine/render'; |
| | | export type { |
| | | ReportContext, |
| | | ReportContextItem, |
| | | ReportContextItemGroup, |
| | | ReportContextReport, |
| | | QualityResult, |
| | | } from './engine/context'; |
| | | export { resolvePageMm, PAGE_SIZE_MM } from './engine/page'; |
| | | export { bindQualityCanvasSync, restoreQualityTraits } from './core/canvas-sync'; |
| | | export { BINDING_TRAIT_TYPE, registerBindingTraitType } from './core/trait-binding'; |
| | | export { |
| | | getQualityComponent, |
| | | listQualityComponents, |
| | | listQualityComponentsByCategory, |
| | | registerQualityComponent, |
| | | registerQualityComponents, |
| | | } from './core/registry'; |
| | | export { buildQualityBlock, buildQualityBlocks, buildQualityContent } from './core/factory'; |
| | | export { resolveQualityProps, validateQualityProps } from './core/validator'; |
| | | export { buildQualityComponentCatalog } from './core/catalog'; |
| | | export type { |
| | | QualityCatalogField, |
| | | QualityComponentCatalog, |
| | | } from './core/catalog'; |
| | | export { assembleQualityCanvas } from './core/assembler'; |
| | | export type { |
| | | QualityAssemblyResult, |
| | | QualityAssemblySkip, |
| | | QualityDraftComponent, |
| | | } from './core/assembler'; |
| | | export { QUALITY_TYPE_ATTR, propAttrName } from './core/attrs'; |
| | | export type { |
| | | QualityCategory, |
| | | QualityComponentDefinition, |
| | | QualityFieldSchema, |
| | | QualityProps, |
| | | } from './core/types'; |
| | | export { QUALITY_CATEGORY } from './core/types'; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition, QualityProps } from '../core/types'; |
| | | |
| | | import { QUALITY_REPEAT_ATTR, QUALITY_REPEAT_ROW_ATTR } from '../core/attrs'; |
| | | import { parseColumnSpec, parseHeaderSpans } from '../core/column-spec'; |
| | | import { cssStyle, escapeHtml } from '../core/html'; |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | /** æ£éªé¡¹æ°ç»å¨æ¥åä¸ä¸æä¸çé»è®¤è·¯å¾ */ |
| | | const DEFAULT_ITEMS_PATH = 'inspectionItems'; |
| | | |
| | | /** 循ç¯åéåï¼è¡å
å ä½ç¬¦åä½ {{item.xxx}} */ |
| | | const LOOP_VAR = 'item'; |
| | | |
| | | /** 渲ææç±éå¤å®¹å¨æ³¨å
¥çè¡å·åéï¼åä½ {{index}}ï¼ä» 1 å¼å§ */ |
| | | const INDEX_VAR = 'index'; |
| | | |
| | | const CELL_BORDER = '1px solid #d9d9d9'; |
| | | |
| | | /** åå
æ ¼éææ ·å¼ï¼åå¹¶æ ¼ä¸æ®éæ ¼ä¸è´ï¼å·®å«åªå¨å±æ§ï¼ */ |
| | | const CELL_STYLE = cssStyle({ border: CELL_BORDER, padding: '6px 8px' }); |
| | | |
| | | /** |
| | | * èªå®åæ¶çåå
æ ¼æ ·å¼ï¼æ¯é»è®¤åå¤ä¸æ¡å
许é¿ä¸²ä¸éæè¡ã |
| | | * <p> |
| | | * é»è®¤å带æå·¥è°è¿ç宽度ï¼åºå· 48pxãæ£æµè¦æ± 30%ã宿µå¼ 26%â¦â¦ï¼ï¼å®½åº¦ç±è¿äºå£°æè¯´äºç®ï¼ |
| | | * èªå®åä¸ä¸ªå®½åº¦é½æ²¡æï¼æ´å¼ 表çå宽就åªè½ç±åèªç**æå°å
容宽度**忍ãèæ¥åéç弿¯ |
| | | * `{{item.standardValue}}` è¿ç§ä¸å«ç©ºæ ¼çé¿ä¸²ï¼æå°å
å®¹å®½åº¦å°±æ¯æ´ä¸²çé¿åº¦ââå åå èµ·æ¥ |
| | | * è½»æè¶
è¿ A4 æ£æå®½åº¦ï¼æµè§å¨åªè½æè¡¨æ ¼æåºçº¸å¼ ï¼ææ«ä¸åè¢«æ¤æåå 个åç´ çç«æ¡ã |
| | | * <p> |
| | | * `anywhere` ä¼å䏿å°å
容宽度ç计ç®ï¼`break-word` ä¸ä¼ï¼ï¼åå®½å æ¤ä¸å被é¿ä¸²ç»æ¶ï¼ |
| | | * å¼å¨**åºä»¶æ¶**æ¬æ¥å°±çï¼ãçç®±å¹²ç¥æ³ãã13.2ãï¼ï¼æè¡æå°è§¦åï¼äº§ç©ä¸è®¾è®¡å¨æè§ä¸è´ã |
| | | * <p> |
| | | * åªå¨èªå®åæ¶å ï¼é»è®¤åç宽度声æå·²ç»å®ä½äºçé¢ï¼åå 䏿¡æè¡åèè®©ãæ£éªé¡¹ç®ããå项ã |
| | | * è¿ç±»æ²¡å宽度çå被æ¤å° 30 ä½åç´ ãè¡é«ç¿»å åââ宿µè¿ï¼ä¸å æ¯å 好ã |
| | | */ |
| | | const CUSTOM_CELL_STYLE = cssStyle({ |
| | | border: CELL_BORDER, |
| | | padding: '6px 8px', |
| | | 'overflow-wrap': 'anywhere', |
| | | }); |
| | | |
| | | /** |
| | | * ç»åæ ¼çå并屿§ã |
| | | * <p> |
| | | * åªè½åæã屿§ãèä¸è½åè¿è¡å
styleï¼è¡å
æ ·å¼ä¼è¢« GrapesJS è§£ææèç¹ style å¯¹è±¡ï¼ |
| | | * èæ¸²ææå¯¹ style 对象ä¸è·ç»å®ï¼renderStyleï¼ï¼ç»å®ä¸²ä¼è¢«æ´æ¡ä¸¢æï¼ |
| | | * 屿§ååæ ·ä¿çï¼ä¸¤ä¾§æ¸²æå¨é½ä¼æ {{}} è§£æå¼ã |
| | | * <p> |
| | | * hidden ç¨å符串èéå¸å°ï¼è¿ä¸ªå±æ§é ãå¨ä¸å¨ãèµ·ä½ç¨ï¼hidden="false" ç
§æ ·éèï¼ï¼ |
| | | * æä»¥åªæã空串 = ä¸è¾åºè¯¥å±æ§ = å¯è§ãè¿ä¸ä¸ªå¯å¤å®çå½¢æã |
| | | * 两侧渲æå¨é½è·³è¿æ±å¼å为空串ç屿§ï¼render.ts / HtmlRenderer.javaï¼ï¼è¡ä¸ºéåä¸è´ã |
| | | */ |
| | | const GROUP_CELL_ATTRS = |
| | | ` rowspan="{{${LOOP_VAR}.group.span}}" hidden="{{${LOOP_VAR}.group.hidden}}"`; |
| | | |
| | | interface ColumnSpec { |
| | | /** 表头æå */ |
| | | label: string; |
| | | /** è¡å
åå¼è¡¨è¾¾å¼ */ |
| | | expr: string; |
| | | /** åå¨ <td> ä¸çé¢å¤å±æ§ï¼ç»å®ï¼ */ |
| | | attrs?: string; |
| | | /** 对åºçæ¾ç¤ºå¼å
³å±æ§åï¼ç©ºä¸²è¡¨ç¤ºä¸å¯éè */ |
| | | toggle: string; |
| | | width?: string; |
| | | } |
| | | |
| | | const COLUMNS: ColumnSpec[] = [ |
| | | { label: 'åºå·', expr: `{{${INDEX_VAR}}}`, toggle: 'showIndex', width: '48px' }, |
| | | { label: 'æ£éªé¡¹ç®', expr: `{{${LOOP_VAR}.itemName}}`, attrs: GROUP_CELL_ATTRS, toggle: '' }, |
| | | { label: 'å项', expr: `{{${LOOP_VAR}.group.childName}}`, toggle: '' }, |
| | | { |
| | | label: 'æ£æµè¦æ±', |
| | | expr: `{{${LOOP_VAR}.requirement}}`, |
| | | toggle: 'showRequirement', |
| | | width: '30%', |
| | | }, |
| | | { |
| | | label: '宿µå¼', |
| | | expr: `{{${LOOP_VAR}.actualValue}}`, |
| | | toggle: 'showActual', |
| | | width: '26%', |
| | | }, |
| | | { label: 'åä½', expr: `{{${LOOP_VAR}.unit}}`, toggle: 'showUnit', width: '60px' }, |
| | | { label: 'å¤å®', expr: `{{${LOOP_VAR}.resultText}}`, toggle: 'showResult', width: '72px' }, |
| | | ]; |
| | | |
| | | /** çæ£åä¸åºè¡¨çä¸åï¼é»è®¤åä¸èªå®ä¹å齿¶ææè¿ä¸ªå½¢ç¶åæ¼ HTML */ |
| | | interface RenderColumn { |
| | | label: string; |
| | | expr: string; |
| | | /** æ¯ä¸æ¯çºµååå¹¶è·¨ä½æ´ç»çç»åå */ |
| | | merge: boolean; |
| | | width?: string; |
| | | } |
| | | |
| | | /** ææ¾ç¤ºå¼å
³æåºé»è®¤å */ |
| | | function defaultColumns(properties: QualityProps): RenderColumn[] { |
| | | return COLUMNS.filter( |
| | | (column) => column.toggle === '' || properties[column.toggle] !== false, |
| | | ).map((column) => ({ |
| | | label: column.label, |
| | | expr: column.expr, |
| | | merge: Boolean(column.attrs), |
| | | width: column.width, |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * åºè¡¨ç¨çåï¼å¡«äº columns å°±æ´è¡¨ç
§å®æ¥ï¼å¦åç¨é»è®¤åã |
| | | * <p> |
| | | * èªå®ä¹åæ¶**ä¸åçåæ¾ç¤ºå¼å
³**ï¼columns å·²ç»æåä¸é¡ºåºè¯´å
¨äºï¼ |
| | | * åå ä¸å±ãåºå·è¢«éèäºä½ç¨æ·åå忥äºãçå¼å
³å¤æï¼åªä¼è®©ä¸¤è¾¹ææ¶ã |
| | | * |
| | | * @return custom 为ç表示è¿å¼ 表ç
§ columns æ¥ï¼æ¿ä¸å°ä»»ä½å®½åº¦å£°æï¼åå
æ ¼è¦å
许æè¡ï¼ |
| | | */ |
| | | function effectiveColumns(properties: QualityProps): { |
| | | columns: RenderColumn[]; |
| | | custom: boolean; |
| | | } { |
| | | const custom = parseColumnSpec(textOf(properties.columns)); |
| | | return custom.items.length > 0 |
| | | ? { columns: custom.items, custom: true } |
| | | : { columns: defaultColumns(properties), custom: false }; |
| | | } |
| | | |
| | | function textOf(value: unknown): string { |
| | | return String(value ?? '').trim(); |
| | | } |
| | | |
| | | /** ä¸å±çåºè¡¨æ¹æ¡ï¼ä¸å±é£ä¸è¡çåå
æ ¼ï¼ä»¥åä¸å±è¦è·³è¿åªäºå */ |
| | | interface HeaderPlan { |
| | | cells: string; |
| | | covered: Set<number>; |
| | | } |
| | | |
| | | /** |
| | | * è§åä¸å±è¡¨å¤´ã |
| | | * <p> |
| | | * 表头éçæ ¼åæä¸¤ç§å½¢æï¼é½ä»åä¸ä¸² `æ é¢^è·¨åæ°` éè®¤åºæ¥ï¼ |
| | | * <ul> |
| | | * <li>åç»æ é¢ï¼æ¨ªåè·¨ä½ä¸é¢è¥å¹²åï¼å¦ `æ£éªç»æ^5`ï¼</li> |
| | | * <li>纵ååå¹¶æ ¼ï¼æä¸åèªå·±æ¢æ¯åå忝䏿 ¼ï¼å件鿝䏿 ¼è·¨ä½ä¸ä¸ä¸¤å± |
| | | * ï¼æåææ¬é表ç°ä¸ºä¸å±è¡¨å¤´é£ä¸æ ¼åçãâåä¸ãï¼ï¼å¦ `ç»è®º^1`ã |
| | | * 认åºå®é ã该段æ é¢ä¸å®æ£ä¸æ¹é£ä¸åçåæ é¢ä¸åä¸å·®ãä¸åªè·¨ 1 åãã</li> |
| | | * </ul> |
| | | * 纵ååå¹¶æ ¼å¿
é¡»ç¨ rowspan åºãå¹¶ä¸ä¸å±ä¸åéå¤åºè¿ä¸æ ¼ï¼å¦åãç»è®ºãäºåä¼å¨ä¸ä¸ä¸¤å±åå°ä¸æ¬¡ã |
| | | */ |
| | | function planHeader(properties: QualityProps, columns: RenderColumn[]): HeaderPlan { |
| | | const parsed = parseHeaderSpans(textOf(properties.headerSpans), columns.length); |
| | | if (parsed.items.length === 0 || parsed.errors.length > 0) { |
| | | // 没填æä¸åæ³ï¼ä¸åºä¸å±ï¼è¡¨å¤´éååå± |
| | | return { cells: '', covered: new Set() }; |
| | | } |
| | | const covered = new Set<number>(); |
| | | let lowerIndex = 0; |
| | | const cells = parsed.items |
| | | .map((span) => { |
| | | const start = lowerIndex; |
| | | lowerIndex += span.span; |
| | | const vertical = span.span === 1 && columns[start]?.label === span.label; |
| | | if (vertical) { |
| | | covered.add(start); |
| | | } |
| | | const size = vertical ? 'rowspan="2"' : `colspan="${span.span}"`; |
| | | return `<th ${size} style="${cssStyle({ |
| | | border: CELL_BORDER, |
| | | padding: '6px 8px', |
| | | background: '#fafafa', |
| | | 'font-weight': 600, |
| | | })}">${escapeHtml(span.label)}</th>`; |
| | | }) |
| | | .join(''); |
| | | return { cells, covered }; |
| | | } |
| | | |
| | | export const groupedQualityTableDefinition: QualityComponentDefinition = { |
| | | type: 'GroupedQualityTable', |
| | | name: 'groupedQualityTable', |
| | | label: 'åç»æ£éªé¡¹è¡¨', |
| | | category: QUALITY_CATEGORY.INSPECTION, |
| | | icon: QUALITY_ICONS.groupedTable, |
| | | aiHint: |
| | | 'æåç»å±çº§çæ£éªé¡¹è¡¨æ ¼ï¼ç»å纵ååå¹¶ãç»å
å项éè¡å±å¼ï¼åºå·/æ£éªé¡¹ç®/å项/æ£æµè¦æ±/宿µå¼/åä½/å¤å®ï¼ã' + |
| | | 'æ£éªåéåå¨ãä¸ä¸ªæ£éªé¡¹ç®ä¸æè¥å¹²å项ãè¿ç§ä¸¤å±ç»ææ¶ç¨å®ï¼æ£éªé¡¹æ¯å¹³éºä¸å±ã没æç¶åå
³ç³»æ¶ç¨ QualityTableã' + |
| | | 'åä»¶è¿å¼ 表ç**å**æ¯ä¸é¢ååºçå¤ï¼å¦è¿æãæ£æµæ¹æ³ããæ åå¼ããç»è®ºãã夿³¨ãï¼æ¶ä¸è¦æ¹ç¨å«çç»ä»¶ï¼' + |
| | | 'æå
¨é¨ååè¿æç columns 屿§ï¼è¡¨å¤´åæä¸ä¸ä¸¤å±æ¶ï¼æä¸é¢é£ä¸å±åè¿ headerSpansã' + |
| | | 'æä¸åå¨å件鿝ã䏿 ¼çºµååå¹¶è·¨ä½ä¸ä¸ä¸¤å±è¡¨å¤´ãï¼è¡¨ç°ä¸ºä¸å±è¡¨å¤´é£ä¸æ ¼åçãâåä¸ãã' + |
| | | 'æè¯¥ååä¸ä¸ååºç°ä¸æ¬¡ï¼æ¶ï¼å®ä¸ç®ä»»ä½åç»æ é¢çä¸å±åï¼åæ ·åè¿ headerSpans 使 é¢ä¸åæ é¢ä¿æä¸è´ï¼' + |
| | | 'æä¼æå®åæè·¨ä¸¤è¡ç䏿 ¼ã', |
| | | defaults: { |
| | | title: 'æ£éªé¡¹ç®', |
| | | itemsPath: DEFAULT_ITEMS_PATH, |
| | | showIndex: true, |
| | | showRequirement: true, |
| | | showActual: true, |
| | | showUnit: true, |
| | | showResult: true, |
| | | }, |
| | | propertySchema: [ |
| | | { key: 'title', label: 'è¡¨æ ¼æ é¢', type: 'string', defaultValue: 'æ£éªé¡¹ç®' }, |
| | | { |
| | | key: 'itemsPath', |
| | | label: 'æ£éªé¡¹æ°æ®æº', |
| | | type: 'string', |
| | | defaultValue: DEFAULT_ITEMS_PATH, |
| | | required: true, |
| | | bindable: true, |
| | | // æ¸²æææè¿ä¸ªè·¯å¾åæ°ç»éè¡çæï¼å¿
é¡»æ¯è£¸è·¯å¾ï¼ä¸è½åæ {{...}} |
| | | bindAs: 'path', |
| | | tip: 'æ¥åä¸ä¸æä¸æ£éªé¡¹ç®æ°ç»çè·¯å¾ï¼æ¸²ææ¶æå®éè¡çæãåç»ä¿¡æ¯éæ¯ä¸é¡¹çä¸åï¼ä¸éè¦å¦å¤æå®', |
| | | }, |
| | | { key: 'showIndex', label: 'æ¾ç¤ºåºå·', type: 'boolean', defaultValue: true }, |
| | | { key: 'showRequirement', label: 'æ¾ç¤ºæ£æµè¦æ±', type: 'boolean', defaultValue: true }, |
| | | { key: 'showActual', label: 'æ¾ç¤ºå®æµå¼', type: 'boolean', defaultValue: true }, |
| | | { key: 'showUnit', label: 'æ¾ç¤ºåä½', type: 'boolean', defaultValue: true }, |
| | | { key: 'showResult', label: 'æ¾ç¤ºå¤å®', type: 'boolean', defaultValue: true }, |
| | | { |
| | | key: 'columns', |
| | | label: 'åå®ä¹ï¼è¦çé»è®¤åï¼', |
| | | type: 'text', |
| | | tip: |
| | | 'ç空åç¨ä¸é¢çé»è®¤åãæ ¼å¼ï¼åæ é¢=ç»å®è¡¨è¾¾å¼ï¼å¤åç¨ | åéï¼' + |
| | | 'å¦ åºå·={{index}}|æ£éªé¡¹ç®={{item.itemName}}|æ£æµæ¹æ³={{item.checkMethod}}|宿µå¼={{item.actualValue}}ã' + |
| | | 'æ é¢åå # 表示该å纵ååå¹¶è·¨ä½æ´ç»ï¼æ¾ç»åçé£ä¸åï¼ã' + |
| | | 'å¡«äºå®æ´è¡¨å°±æå®æ¥ï¼ä¸é¢çæ¾ç¤ºå¼å
³ä¸åçæ', |
| | | }, |
| | | { |
| | | key: 'headerSpans', |
| | | label: '表头跨åï¼ä¸¤çº§è¡¨å¤´æå¡«ï¼', |
| | | type: 'text', |
| | | tip: |
| | | 'åªæè¡¨å¤´åä¸ä¸ä¸¤å±æ¶æå¡«ï¼åçæ¯ä¸é¢é£ä¸å±ï¼æ¯ä¸ªåå
æ ¼åä½ æ é¢^è·¨è¶åæ°ï¼å¤æ®µç¨ | åéï¼' + |
| | | 'å¦ æ£éªç»æ^5|ç»è®º^1ãä¸é¢é£ä¸å±ç±åæ é¢èªå¨æ¼åºï¼ä¸è¦éå¤åã' + |
| | | 'åæ®µè·¨åæ°ä¹åå¿
é¡»çäºåæ°ã' + |
| | | 'æä¸åèªå·±æ¢æ¯åå忝䏿 ¼ï¼å件鿝䏿 ¼çºµååå¹¶è·¨ä½ä¸ä¸ä¸¤å±ï¼æ¶ï¼ä»ç
§å䏿®µãè·¨åæ°å 1ï¼' + |
| | | 'ä½è¯¥æ®µæ é¢è¦ä¸è¿ä¸åçåæ é¢åæå®å
¨ä¸æ ·çåï¼æ¥åä¼ååºä¸æ ¼è·¨ä¸¤è¡ï¼ä¸ä¼ä¸ä¸åå°ä¸é', |
| | | }, |
| | | ], |
| | | dataSchema: [ |
| | | { key: DEFAULT_ITEMS_PATH, label: 'æ£éªé¡¹ç®å表', type: 'string', bindable: true }, |
| | | { key: INDEX_VAR, label: 'è¡åºå·', type: 'number', bindable: true }, |
| | | { key: `${LOOP_VAR}.itemName`, label: 'æ£éªé¡¹ç®åç§°', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.group.childName`, label: 'å项åç§°', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.group.span`, label: 'ç»ååå¹¶è¡æ°', type: 'number', bindable: true }, |
| | | { key: `${LOOP_VAR}.group.hidden`, label: 'å项è¡éèåå¹¶æ ¼', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.requirement`, label: 'æ£æµè¦æ± / ç»å
¬å¼', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.checkMethod`, label: 'æ£æµæ¹æ³', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.actualValue`, label: '宿µå¼', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.unit`, label: 'åä½', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.resultText`, label: 'å¤å®ç»æ', type: 'string', bindable: true }, |
| | | ], |
| | | buildContent: (properties) => { |
| | | const { columns, custom } = effectiveColumns(properties); |
| | | const cellStyle = custom ? CUSTOM_CELL_STYLE : CELL_STYLE; |
| | | const header = planHeader(properties, columns); |
| | | const head = columns |
| | | .map((column, index) => |
| | | header.covered.has(index) |
| | | ? '' |
| | | : `<th style="${cssStyle({ |
| | | border: CELL_BORDER, |
| | | padding: '6px 8px', |
| | | background: '#fafafa', |
| | | 'font-weight': 600, |
| | | width: column.width, |
| | | })}">${escapeHtml(column.label)}</th>`, |
| | | ) |
| | | .join(''); |
| | | const row = columns |
| | | .map( |
| | | (column) => |
| | | `<td${column.merge ? GROUP_CELL_ATTRS : ''} style="${cellStyle}">${escapeHtml(column.expr)}</td>`, |
| | | ) |
| | | .join(''); |
| | | const rawTitle = String(properties.title ?? '').trim(); |
| | | const heading = rawTitle |
| | | ? `<div style="${cssStyle({ 'font-size': '13px', 'font-weight': 600, margin: '0 0 4px' })}">${escapeHtml(rawTitle)}</div>` |
| | | : ''; |
| | | const itemsPath = String(properties.itemsPath ?? DEFAULT_ITEMS_PATH).trim() || DEFAULT_ITEMS_PATH; |
| | | const table = |
| | | `<table style="${cssStyle({ width: '100%', 'border-collapse': 'collapse', 'font-size': '12px' })}">` + |
| | | `<thead>${header.cells ? `<tr>${header.cells}</tr>` : ''}<tr>${head}</tr></thead>` + |
| | | // æ¸²æææ data-qc-repeat æåçæ°ç»å¤å¶ data-qc-repeat-row æå¨çè¡ï¼ |
| | | // ç»å¤´è¡ä¸ç»å
å项è¡å¨è¿å¥ç»æé齿¯æ°ç»éçä¸é¡¹ï¼åå¹¶é ç»åæ ¼èªå¸¦ç rowspan |
| | | `<tbody ${QUALITY_REPEAT_ATTR}="${escapeHtml(itemsPath)}">` + |
| | | `<tr ${QUALITY_REPEAT_ROW_ATTR}="${LOOP_VAR}">${row}</tr>` + |
| | | `</tbody></table>`; |
| | | return { |
| | | tagName: 'div', |
| | | components: heading + table, |
| | | style: { margin: '0 0 10px' }, |
| | | }; |
| | | }, |
| | | validate: (properties) => { |
| | | const errors: string[] = []; |
| | | const path = textOf(properties.itemsPath); |
| | | if (!path) { |
| | | errors.push('åç»æ£éªé¡¹è¡¨çãæ£éªé¡¹æ°æ®æºãä¸è½ä¸ºç©ºï¼æ¸²ææ¶æ æ³ç¡®å®çæåªäºè¡'); |
| | | } |
| | | |
| | | const parsed = parseColumnSpec(textOf(properties.columns)); |
| | | errors.push(...parsed.errors); |
| | | const customColumns = parsed.items.length > 0; |
| | | |
| | | // æ¾ç¤ºå¼å
³åªå¨ç¨é»è®¤åæ¶è¯´äºç®ï¼å¡«äº columns æ´è¡¨å°±æ columns æ¥ï¼ |
| | | // ãéèäºæ£æµè¦æ±å´ä¿çå¤å®ãå¨è¿éä¸åæç«ï¼æ¥äºå°±æ¯è¯¯æ¥ |
| | | if (!customColumns && properties.showRequirement === false && properties.showResult === true) { |
| | | errors.push( |
| | | 'åç»æ£éªé¡¹è¡¨éèäºãæ£æµè¦æ±ãå´ä¿çãå¤å®ãåï¼å¤å®ä¾æ®å°ä¸å¯è§ï¼è¯·ä¸å¹¶éèå¤å®ææ¢å¤æ£æµè¦æ±', |
| | | ); |
| | | } |
| | | |
| | | const headerSpec = textOf(properties.headerSpans); |
| | | // è·¨åæ°çåè®¡æ ¡éªè¦å
ç¥éå®é
åæ°ï¼èåå®ä¹æ¬èº«æéæ¶åæ°ä¸å¯ä¿¡ï¼ |
| | | // æ¤æ¶æ¥ãå计 Nï¼å®é
åæ° Mãä¼æç¨æ·å¾éè¯¯çæ¹å带ï¼å
åªæ¥åå®ä¹çé |
| | | if (headerSpec && parsed.errors.length === 0) { |
| | | const columnCount = (customColumns ? parsed.items : defaultColumns(properties)).length; |
| | | errors.push(...parseHeaderSpans(headerSpec, columnCount).errors); |
| | | } |
| | | return errors; |
| | | }, |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { cssStyle, escapeHtml } from '../core/html'; |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | /** |
| | | * å个æ£éªé¡¹çæç»åã |
| | | * <p> |
| | | * åå¼å
¨é¨æ¥èªå¾ªç¯åé itemï¼æ¾å¨æ£éªé¡¹è¡¨æ ¼çéå¤è¡éãæç¬ç«çéå¤å®¹å¨é使ç¨ï¼ |
| | | * åç¬æå°ç»å¸ä¸æ¶æ¸²æææ²¡æ item ä¸ä¸æï¼ä¼æ¸²ææç©ºå¼ï¼è¿æ¯é¢æè¡ä¸ºã |
| | | */ |
| | | const LOOP_VAR = 'item'; |
| | | |
| | | interface FieldSpec { |
| | | label: string; |
| | | /** åå¼è¡¨è¾¾å¼ï¼'' è¡¨ç¤ºè¯¥åæ®µåªæä¸ä¸ªå¼ãä¸å¸¦æ ç¾ */ |
| | | expr: string; |
| | | toggle: string; |
| | | } |
| | | |
| | | const FIELDS: FieldSpec[] = [ |
| | | { label: '', expr: `{{${LOOP_VAR}.itemName}}`, toggle: '' }, |
| | | { label: 'æ åå¼ï¼', expr: `{{${LOOP_VAR}.standardValue}}`, toggle: 'showStandard' }, |
| | | { label: '宿µå¼ï¼', expr: `{{${LOOP_VAR}.actualValue}}`, toggle: 'showActual' }, |
| | | { label: 'åä½ï¼', expr: `{{${LOOP_VAR}.unit}}`, toggle: 'showUnit' }, |
| | | { label: 'å¤å®ï¼', expr: `{{${LOOP_VAR}.resultText}}`, toggle: 'showResult' }, |
| | | ]; |
| | | |
| | | export const inspectionItemDefinition: QualityComponentDefinition = { |
| | | type: 'InspectionItem', |
| | | name: 'inspectionItem', |
| | | label: 'æ£éªé¡¹æç»', |
| | | category: QUALITY_CATEGORY.INSPECTION, |
| | | icon: QUALITY_ICONS.inspection, |
| | | aiHint: |
| | | 'åæ¡æ£éªé¡¹çç«ææç»å¡çï¼ç¨äºæ£éªé¡¹å¾å°ãä¸éåææè¡¨æ ¼çåºåãå¤è¡æ°æ®ç¨ QualityTableã', |
| | | defaults: { |
| | | showStandard: true, |
| | | showActual: true, |
| | | showUnit: true, |
| | | showResult: true, |
| | | showBorder: true, |
| | | }, |
| | | propertySchema: [ |
| | | { key: 'showStandard', label: 'æ¾ç¤ºæ åå¼', type: 'boolean', defaultValue: true }, |
| | | { key: 'showActual', label: 'æ¾ç¤ºå®æµå¼', type: 'boolean', defaultValue: true }, |
| | | { key: 'showUnit', label: 'æ¾ç¤ºåä½', type: 'boolean', defaultValue: true }, |
| | | { key: 'showResult', label: 'æ¾ç¤ºå¤å®', type: 'boolean', defaultValue: true }, |
| | | { key: 'showBorder', label: 'æ¾ç¤ºè¾¹æ¡', type: 'boolean', defaultValue: true }, |
| | | ], |
| | | dataSchema: [ |
| | | { key: `${LOOP_VAR}.itemName`, label: 'æ£éªé¡¹ç®åç§°', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.standardValue`, label: 'æ åå¼', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.actualValue`, label: '宿µå¼', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.unit`, label: 'åä½', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.resultText`, label: 'å¤å®ç»æ', type: 'string', bindable: true }, |
| | | ], |
| | | buildContent: (properties) => { |
| | | const visible = FIELDS.filter( |
| | | (field) => field.toggle === '' || properties[field.toggle] !== false, |
| | | ); |
| | | const [first, ...rest] = visible; |
| | | const name = first |
| | | ? `<div style="${cssStyle({ 'font-weight': 600, 'font-size': '13px' })}">${first.expr}</div>` |
| | | : ''; |
| | | const details = rest |
| | | .map( |
| | | (field) => |
| | | `<div style="${cssStyle({ 'font-size': '12px', 'line-height': '1.8' })}">${escapeHtml(field.label)}<span>${field.expr}</span></div>`, |
| | | ) |
| | | .join(''); |
| | | return { |
| | | tagName: 'div', |
| | | components: name + details, |
| | | style: { |
| | | margin: '0 0 6px', |
| | | padding: '6px 8px', |
| | | border: properties.showBorder === false ? 'none' : '1px solid #d9d9d9', |
| | | }, |
| | | }; |
| | | }, |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { QUALITY_REPEAT_ATTR, QUALITY_REPEAT_ROW_ATTR } from '../core/attrs'; |
| | | import { cssStyle, escapeHtml } from '../core/html'; |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | /** æ£éªé¡¹æ°ç»å¨æ¥åä¸ä¸æä¸çé»è®¤è·¯å¾ */ |
| | | const DEFAULT_ITEMS_PATH = 'inspectionItems'; |
| | | |
| | | /** 循ç¯åéåï¼è¡å
å ä½ç¬¦åä½ {{item.xxx}} */ |
| | | const LOOP_VAR = 'item'; |
| | | |
| | | /** 渲ææç±éå¤å®¹å¨æ³¨å
¥çè¡å·åéï¼åä½ {{index}}ï¼ä» 1 å¼å§ */ |
| | | const INDEX_VAR = 'index'; |
| | | |
| | | const CELL_BORDER = '1px solid #d9d9d9'; |
| | | |
| | | interface ColumnSpec { |
| | | /** 表头æå */ |
| | | label: string; |
| | | /** è¡å
åå¼è¡¨è¾¾å¼ */ |
| | | expr: string; |
| | | /** 对åºçæ¾ç¤ºå¼å
³å±æ§å */ |
| | | toggle: string; |
| | | width?: string; |
| | | } |
| | | |
| | | const COLUMNS: ColumnSpec[] = [ |
| | | { label: 'åºå·', expr: `{{${INDEX_VAR}}}`, toggle: 'showIndex', width: '48px' }, |
| | | { label: 'æ£éªé¡¹ç®', expr: `{{${LOOP_VAR}.itemName}}`, toggle: '' }, |
| | | { label: 'æ åå¼', expr: `{{${LOOP_VAR}.standardValue}}`, toggle: 'showStandard' }, |
| | | { label: '宿µå¼', expr: `{{${LOOP_VAR}.actualValue}}`, toggle: 'showActual', width: '40%' }, |
| | | { label: 'åä½', expr: `{{${LOOP_VAR}.unit}}`, toggle: 'showUnit', width: '60px' }, |
| | | { label: 'å¤å®', expr: `{{${LOOP_VAR}.resultText}}`, toggle: 'showResult', width: '72px' }, |
| | | ]; |
| | | |
| | | export const qualityTableDefinition: QualityComponentDefinition = { |
| | | type: 'QualityTable', |
| | | name: 'qualityTable', |
| | | label: 'æ£éªé¡¹ç®è¡¨æ ¼', |
| | | category: QUALITY_CATEGORY.INSPECTION, |
| | | icon: QUALITY_ICONS.qualityTable, |
| | | aiHint: |
| | | 'å¤è¡æ£éªæ°æ®çè¡¨æ ¼ï¼æ£éªé¡¹å¹³éºä¸å±ã没æç¶åå
³ç³»ï¼åºå·/æ£éªé¡¹ç®/æ åå¼/宿µå¼/åä½/å¤å®ï¼ã' + |
| | | 'ä¸ä¸ªæ£éªé¡¹ç®ä¸æè¥å¹²å项ï¼åä»¶é表ç°ä¸ºè¯¥å纵ååå¹¶ãåç»è¡åçãâåä¸ãï¼æ¶ä¸è¦ç¨æï¼' + |
| | | 'æ¹ç¨åç»æ£éªé¡¹è¡¨ã' + |
| | | 'æ¥åæ«å°¾çè½æ¬¾ç¾ç½²è¡ï¼æ£éªå/å®¡æ ¸äºº/æ¥æï¼ä¸è¦æ¾è¿æè¿éï¼é£å±äº ReportFooterã', |
| | | defaults: { |
| | | title: 'æ£éªé¡¹ç®', |
| | | itemsPath: DEFAULT_ITEMS_PATH, |
| | | showIndex: true, |
| | | showStandard: true, |
| | | showActual: true, |
| | | showUnit: true, |
| | | showResult: true, |
| | | }, |
| | | propertySchema: [ |
| | | { key: 'title', label: 'è¡¨æ ¼æ é¢', type: 'string', defaultValue: 'æ£éªé¡¹ç®' }, |
| | | { |
| | | key: 'itemsPath', |
| | | label: 'æ£éªé¡¹æ°æ®æº', |
| | | type: 'string', |
| | | defaultValue: DEFAULT_ITEMS_PATH, |
| | | required: true, |
| | | bindable: true, |
| | | // æ¸²æææè¿ä¸ªè·¯å¾åæ°ç»éè¡çæï¼å¿
é¡»æ¯è£¸è·¯å¾ï¼ä¸è½åæ {{...}} |
| | | bindAs: 'path', |
| | | tip: 'æ¥åä¸ä¸æä¸æ£éªé¡¹ç®æ°ç»çè·¯å¾ï¼æ¸²ææ¶æå®éè¡çæ', |
| | | }, |
| | | { key: 'showIndex', label: 'æ¾ç¤ºåºå·', type: 'boolean', defaultValue: true }, |
| | | { key: 'showStandard', label: 'æ¾ç¤ºæ åå¼', type: 'boolean', defaultValue: true }, |
| | | { key: 'showActual', label: 'æ¾ç¤ºå®æµå¼', type: 'boolean', defaultValue: true }, |
| | | { key: 'showUnit', label: 'æ¾ç¤ºåä½', type: 'boolean', defaultValue: true }, |
| | | { key: 'showResult', label: 'æ¾ç¤ºå¤å®', type: 'boolean', defaultValue: true }, |
| | | ], |
| | | dataSchema: [ |
| | | { key: DEFAULT_ITEMS_PATH, label: 'æ£éªé¡¹ç®å表', type: 'string', bindable: true }, |
| | | { key: INDEX_VAR, label: 'è¡åºå·', type: 'number', bindable: true }, |
| | | { key: `${LOOP_VAR}.itemName`, label: 'æ£éªé¡¹ç®åç§°', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.standardValue`, label: 'æ åå¼', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.actualValue`, label: '宿µå¼', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.unit`, label: 'åä½', type: 'string', bindable: true }, |
| | | { key: `${LOOP_VAR}.resultText`, label: 'å¤å®ç»æ', type: 'string', bindable: true }, |
| | | ], |
| | | buildContent: (properties) => { |
| | | const visible = COLUMNS.filter( |
| | | (column) => column.toggle === '' || properties[column.toggle] !== false, |
| | | ); |
| | | const head = visible |
| | | .map( |
| | | (column) => |
| | | `<th style="${cssStyle({ |
| | | border: CELL_BORDER, |
| | | padding: '6px 8px', |
| | | background: '#fafafa', |
| | | 'font-weight': 600, |
| | | width: column.width, |
| | | })}">${column.label}</th>`, |
| | | ) |
| | | .join(''); |
| | | const row = visible |
| | | .map( |
| | | (column) => |
| | | `<td style="${cssStyle({ border: CELL_BORDER, padding: '6px 8px' })}">${column.expr}</td>`, |
| | | ) |
| | | .join(''); |
| | | const rawTitle = String(properties.title ?? '').trim(); |
| | | const heading = rawTitle |
| | | ? `<div style="${cssStyle({ 'font-size': '13px', 'font-weight': 600, margin: '0 0 4px' })}">${escapeHtml(rawTitle)}</div>` |
| | | : ''; |
| | | const itemsPath = String(properties.itemsPath ?? DEFAULT_ITEMS_PATH).trim() || DEFAULT_ITEMS_PATH; |
| | | const table = |
| | | `<table style="${cssStyle({ width: '100%', 'border-collapse': 'collapse', 'font-size': '12px' })}">` + |
| | | `<thead><tr>${head}</tr></thead>` + |
| | | // æ¸²æææ data-qc-repeat æåçæ°ç»å¤å¶ data-qc-repeat-row æå¨çè¡ |
| | | `<tbody ${QUALITY_REPEAT_ATTR}="${escapeHtml(itemsPath)}">` + |
| | | `<tr ${QUALITY_REPEAT_ROW_ATTR}="${LOOP_VAR}">${row}</tr>` + |
| | | `</tbody></table>`; |
| | | return { |
| | | tagName: 'div', |
| | | components: heading + table, |
| | | style: { margin: '0 0 10px' }, |
| | | }; |
| | | }, |
| | | validate: (properties) => { |
| | | const path = String(properties.itemsPath ?? '').trim(); |
| | | const errors: string[] = []; |
| | | if (!path) { |
| | | errors.push('æ£éªé¡¹ç®è¡¨æ ¼çãæ£éªé¡¹æ°æ®æºãä¸è½ä¸ºç©ºï¼æ¸²ææ¶æ æ³ç¡®å®çæåªäºè¡'); |
| | | } |
| | | if (properties.showStandard === false && properties.showResult === true) { |
| | | errors.push( |
| | | 'æ£éªé¡¹ç®è¡¨æ ¼éèäºãæ åå¼ãå´ä¿çãå¤å®ãåï¼å¤å®ä¾æ®å°ä¸å¯è§ï¼è¯·ä¸å¹¶éèå¤å®ææ¢å¤æ åå¼', |
| | | ); |
| | | } |
| | | return errors; |
| | | }, |
| | | }; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QualityComponentDefinition } from '../core/types'; |
| | | |
| | | import { cssStyle, escapeHtml } from '../core/html'; |
| | | import { QUALITY_ICONS } from '../core/icons'; |
| | | import { QUALITY_CATEGORY } from '../core/types'; |
| | | |
| | | /** |
| | | * å¤å®ç»æåã |
| | | * <p> |
| | | * ç»è®ºä¸åæ ¼çæ¥èªæ¥åä¸ä¸æï¼ç±è§å弿卿¸²æåç®å¥½ï¼ |
| | | * è¿éåªè´è´£å±ç¤ºï¼ä¸å¨ç»ä»¶éåä»»ä½å¤å®è®¡ç®ã |
| | | */ |
| | | const CONCLUSION_EXPR = '{{report.conclusion}}'; |
| | | const PASS_RATE_EXPR = '{{report.passRate}}'; |
| | | |
| | | export const reportResultDefinition: QualityComponentDefinition = { |
| | | type: 'Result', |
| | | name: 'reportResult', |
| | | label: 'å¤å®ç»æ', |
| | | category: QUALITY_CATEGORY.RESULT, |
| | | icon: QUALITY_ICONS.result, |
| | | aiHint: 'æ´ä»½æ¥åçå¤å®ç»è®ºä¸å¤å®ä¾æ®ï¼æ¾å¨æ¥åæ«å°¾ã', |
| | | defaults: { |
| | | label: 'å¤å®ç»è®º', |
| | | conclusion: CONCLUSION_EXPR, |
| | | showPassRate: true, |
| | | passRateLabel: 'åæ ¼ç', |
| | | }, |
| | | propertySchema: [ |
| | | { key: 'label', label: 'ç»è®ºæ ç¾', type: 'string', defaultValue: 'å¤å®ç»è®º' }, |
| | | { |
| | | key: 'conclusion', |
| | | label: 'ç»è®ºåå¼', |
| | | type: 'string', |
| | | defaultValue: CONCLUSION_EXPR, |
| | | required: true, |
| | | bindable: true, |
| | | }, |
| | | { key: 'showPassRate', label: 'æ¾ç¤ºåæ ¼ç', type: 'boolean', defaultValue: true }, |
| | | { key: 'passRateLabel', label: 'åæ ¼çæ ç¾', type: 'string', defaultValue: 'åæ ¼ç' }, |
| | | ], |
| | | dataSchema: [ |
| | | { key: 'report.conclusion', label: 'å¤å®ç»è®º', type: 'string', bindable: true }, |
| | | { key: 'report.passRate', label: 'åæ ¼ç', type: 'string', bindable: true }, |
| | | ], |
| | | buildContent: (properties) => { |
| | | const label = escapeHtml(String(properties.label ?? '').trim()); |
| | | const conclusion = escapeHtml(String(properties.conclusion ?? '').trim()); |
| | | const conclusionLine = |
| | | `<div style="${cssStyle({ 'font-size': '13px', 'line-height': '1.9' })}">` + |
| | | `${label ? `${label}ï¼<span style="font-weight:600">${conclusion}</span>` : `<span style="font-weight:600">${conclusion}</span>`}` + |
| | | `</div>`; |
| | | const passRateLine = properties.showPassRate |
| | | ? `<div style="${cssStyle({ 'font-size': '12px', color: '#595959', 'line-height': '1.9' })}">` + |
| | | `${escapeHtml(String(properties.passRateLabel ?? '').trim())}ï¼<span>${PASS_RATE_EXPR}</span>` + |
| | | `</div>` |
| | | : ''; |
| | | return { |
| | | tagName: 'div', |
| | | components: conclusionLine + passRateLine, |
| | | style: { |
| | | margin: '0 0 10px', |
| | | padding: '8px 10px', |
| | | border: '1px solid #b7eb8f', |
| | | background: '#f6ffed', |
| | | }, |
| | | }; |
| | | }, |
| | | }; |
| | |
| | | FILE: 5, |
| | | } as const; |
| | | |
| | | /** MES è´¨æ£ææ æ¡ç®ç±»åæä¸¾ï¼å¯¹é½å端 MesQcIndicatorItemTypeEnumï¼ */ |
| | | export const MesQcIndicatorItemType = { |
| | | ENTRY: 1, |
| | | GROUP: 2, |
| | | } as const; |
| | | |
| | | /** MES ä¸å¡ç±»å常é */ |
| | | export const MesBizTypeEnum = { |
| | | // WM ä»åºæ¨¡å [100, 200) |
| | |
| | | MES_NCR_DISPOSITION: 'mes_ncr_disposition', // MES ä¸åæ ¼åå¤ç½®æ¹å¼ |
| | | } as const; |
| | | |
| | | /** ========== QCReport - æºè½è´¨æ£æ¥åè®¾è®¡å¹³å° ========== */ |
| | | const QC_REPORT_DICT = { |
| | | QC_REPORT_TEMPLATE_STATUS: 'qc_report_template_status', // æ¥å模æ¿ç¶æ |
| | | QC_REPORT_VERSION_STATUS: 'qc_report_version_status', // æ¥å模æ¿çæ¬ç¶æ |
| | | QC_REPORT_INSTANCE_STATUS: 'qc_report_instance_status', // æ¥åå®ä¾ç¶æ |
| | | QC_REPORT_INDUSTRY: 'qc_report_industry', // æ¥åæå±è¡ä¸ |
| | | QC_REPORT_PAGE_SIZE: 'qc_report_page_size', // æ¥åçº¸å¼ å°ºå¯¸ |
| | | QC_REPORT_ORIENTATION: 'qc_report_orientation', // æ¥åçº¸å¼ æ¹å |
| | | } as const; |
| | | |
| | | /** ========== WMS - ä»å¨ç®¡ç模å ========== */ |
| | | const WMS_DICT = { |
| | | WMS_MERCHANT_TYPE: 'merchant_type', // WMS 徿¥ä¼ä¸ç±»å |
| | |
| | | ...MP_DICT, |
| | | ...PAY_DICT, |
| | | ...MALL_DICT, |
| | | ...QC_REPORT_DICT, |
| | | ...SYSTEM_DICT, |
| | | ...COMMON_DICT, |
| | | } as const; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { RouteRecordRaw } from 'vue-router'; |
| | | |
| | | /** |
| | | * æºè½è´¨æ£æ¥å设计平å°çè·¯ç± |
| | | * <p> |
| | | * å表页ç±å端èå驱å¨ï¼/qc/report-templateï¼ï¼è®¾è®¡å¨ä¸å®ä¾é¢è§çé¡µé¢ |
| | | * ä¸è¿å
¥èåï¼ç±å表页带 id è·³è½¬è¿æ¥ã |
| | | */ |
| | | const routes: RouteRecordRaw[] = [ |
| | | { |
| | | path: '/qc/report-template/designer', |
| | | name: 'QcReportTemplateDesigner', |
| | | component: () => import('#/views/mes/qc/report/template/designer/index.vue'), |
| | | meta: { |
| | | title: 'æ¥å模æ¿è®¾è®¡å¨', |
| | | activePath: '/qc/report-template', |
| | | hideInMenu: true, |
| | | keepAlive: false, |
| | | }, |
| | | props: (route) => { |
| | | return { |
| | | templateId: route.query.id, |
| | | }; |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | | export default routes; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { QcReportTemplateApi } from '#/api/mes/qc/report/template'; |
| | | |
| | | import { ref } from 'vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Alert, Button, Empty, Radio, Spin, Tag } from 'ant-design-vue'; |
| | | |
| | | import { generateInstanceFromQc } from '#/api/mes/qc/report/instance'; |
| | | import { getSelectableTemplates } from '#/api/mes/qc/report/template'; |
| | | |
| | | /** |
| | | * è´¨æ£ç±»å â æ¥åè½åºç¨ç businessTypeï¼å°±æ¯ MES 表åï¼ã |
| | | * <p> |
| | | * ä¸ mes_qc_type åå
¸çæ°å¼ä¸æ¯ä¸åäºï¼åæ¥æ¥åæ¶ç¨çå°±æ¯è¿å个å符串ã |
| | | */ |
| | | const BUSINESS_TYPE_BY_QC_TYPE: Record<number, string> = { |
| | | 1: 'mes_qc_iqc', |
| | | 2: 'mes_qc_ipqc', |
| | | 3: 'mes_qc_oqc', |
| | | 4: 'mes_qc_rqc', |
| | | }; |
| | | |
| | | /** æå¼å¼¹çªæ¶ç±è´¨æ£ååè¡¨é¡µå¸¦è¿æ¥ */ |
| | | interface ReportModalData { |
| | | /** è´¨æ£ç±»åï¼1 æ¥æ 2 è¿ç¨ 3 åºè´§ 4 éè´§ */ |
| | | qcType: number; |
| | | /** è´¨æ£åç¼å· */ |
| | | qcId: number; |
| | | /** åæ®ç¼å·ï¼ä»
ç¨äºæ é¢å±ç¤º */ |
| | | qcNo?: string; |
| | | } |
| | | |
| | | /** åºä»¶ç»æï¼åªçå¼¹çªè¦å±ç¤ºçåæ®µ */ |
| | | interface ReportResult { |
| | | id: number; |
| | | reportNo: string; |
| | | templateVersion: string; |
| | | itemCount: number; |
| | | undecidableCount: number; |
| | | warnings: string[]; |
| | | errors: string[]; |
| | | } |
| | | |
| | | defineOptions({ name: 'QcReportGenerateModal' }); |
| | | |
| | | const router = useRouter(); |
| | | |
| | | const modalData = ref<ReportModalData>(); |
| | | const templates = ref<QcReportTemplateApi.Template[]>([]); |
| | | const templateId = ref<number>(); |
| | | const loadingTemplates = ref(false); |
| | | const generating = ref(false); |
| | | const result = ref<ReportResult>(); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | footer: false, |
| | | onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | modalData.value = undefined; |
| | | templates.value = []; |
| | | templateId.value = undefined; |
| | | result.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<ReportModalData>(); |
| | | modalData.value = data; |
| | | result.value = undefined; |
| | | templateId.value = undefined; |
| | | templates.value = []; |
| | | if (data?.qcType) { |
| | | loadTemplates(data.qcType); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** |
| | | * åªååç±»åãå·²å¯ç¨ãä¸å½åçæ¬å·²åå¸ç模æ¿ã |
| | | * <p> |
| | | * è¿ä¸éè¿æ»¤å
¨å¨å端åï¼/qc-report/template/selectableï¼ï¼å端ä¸åèªå·±æ¨å¯¼ï¼ |
| | | * report_type åçæ¯ mes_qc_type çæ°å¼å符串ï¼"1"~"4"ï¼ï¼æ¿ãIQCãè¿ç±»è±æå廿¯ä¼ä¸ä¸ªé½æ¥ä¸å°ï¼ |
| | | * ä¸ currentVersion é空 â è½ç¨ââåç¨çæ¬ä¸ä¼æ¸
空模æ¿ç currentVersionï¼ |
| | | * è¿ç§æ¨¡æ¿éä¸ååºä»¶å¿
æ¥ã模æ¿çæ¬æªåå¸ãã |
| | | */ |
| | | async function loadTemplates(qcType: number) { |
| | | loadingTemplates.value = true; |
| | | try { |
| | | templates.value = await getSelectableTemplates(String(qcType)); |
| | | } catch { |
| | | templates.value = []; |
| | | } finally { |
| | | loadingTemplates.value = false; |
| | | } |
| | | } |
| | | |
| | | /** çææ¥åï¼æ°æ®å
¨é¨ç±å端ä»è´¨æ£ååï¼è¿éåªå³å®ãåªå¼ åæ® + åªå¼ 模æ¿ã */ |
| | | async function handleGenerate() { |
| | | const data = modalData.value; |
| | | const chosen = templateId.value; |
| | | if (!data || !chosen || generating.value) { |
| | | return; |
| | | } |
| | | generating.value = true; |
| | | try { |
| | | const resp = await generateInstanceFromQc({ |
| | | qcType: data.qcType, |
| | | qcId: data.qcId, |
| | | templateId: chosen, |
| | | }); |
| | | result.value = { |
| | | id: resp.id, |
| | | reportNo: resp.reportNo, |
| | | templateVersion: resp.templateVersion, |
| | | itemCount: resp.itemCount, |
| | | undecidableCount: resp.undecidableCount, |
| | | warnings: resp.warnings ?? [], |
| | | errors: resp.errors ?? [], |
| | | }; |
| | | } catch { |
| | | // å¤±è´¥æ¥æå·²ç±è¯·æ±æ¦æªå¨å¼¹åºï¼äºéæ ¡éªæä¸å¡ä¼å
çº§åªæ¥ç¬¬ä¸å¤ï¼ï¼è¿éä¸éå¤æç¤ºã |
| | | // å¤±è´¥é½æ¯ãæ°æ®æéæ©ä¸å¯¹ãï¼éè¯æ ç¨ï¼ææ¥ææç¤ºå»æ¹æ°æ®ææ¢æ¨¡æ¿ã |
| | | } finally { |
| | | generating.value = false; |
| | | } |
| | | } |
| | | |
| | | /** çæå®è·³å°æ¥åå®ä¾å表ï¼å¹¶æåæ®è¿æ»¤æ¡ä»¶å¸¦è¿å»ï¼ç´æ¥çå°è¿ä»½æ¥å */ |
| | | function handleGoReportList() { |
| | | const data = modalData.value; |
| | | modalApi.close(); |
| | | if (!data) { |
| | | return; |
| | | } |
| | | router.push({ |
| | | path: '/qc/report-instance', |
| | | query: { |
| | | businessType: BUSINESS_TYPE_BY_QC_TYPE[data.qcType], |
| | | businessId: String(data.qcId), |
| | | }, |
| | | }); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | :title="`åºæ¥å - ${modalData?.qcNo ?? ''}`" |
| | | class="w-3/5" |
| | | :mask-closable="false" |
| | | > |
| | | <!-- 鿍¡æ¿é¶æ®µ --> |
| | | <template v-if="!result"> |
| | | <Spin :spinning="loadingTemplates"> |
| | | <div class="px-1 py-2"> |
| | | <div class="mb-3 text-sm text-gray-600"> |
| | | æ¥åæ°æ®ç±å端ä»è¯¥è´¨æ£å读åï¼åªééä¸å¼ 模æ¿ï¼æ¨¡æ¿ç±»åå¿
é¡»ä¸åæ®ç±»åä¸è´ã |
| | | </div> |
| | | <div v-if="templates.length === 0 && !loadingTemplates"> |
| | | <Empty description="没æå¯ç¨çåç±»åæ¨¡æ¿ï¼è¯·å
å°ãæ¥å模æ¿è®¾è®¡ãéé
好并åå¸ä¸ä¸ªçæ¬" /> |
| | | </div> |
| | | <Radio.Group v-else v-model:value="templateId" class="w-full"> |
| | | <div class="flex flex-col gap-2"> |
| | | <Radio v-for="item in templates" :key="item.id" :value="item.id"> |
| | | <span>{{ item.templateName }}</span> |
| | | <Tag class="ml-2" color="green">{{ item.currentVersion }}</Tag> |
| | | <span v-if="item.industry" class="ml-2 text-xs text-gray-400"> |
| | | {{ item.industry }} |
| | | </span> |
| | | </Radio> |
| | | </div> |
| | | </Radio.Group> |
| | | </div> |
| | | </Spin> |
| | | <div class="mt-4 flex justify-end gap-2"> |
| | | <Button @click="modalApi.close()">åæ¶</Button> |
| | | <Button |
| | | type="primary" |
| | | :loading="generating" |
| | | :disabled="!templateId" |
| | | @click="handleGenerate" |
| | | > |
| | | çææ¥å |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | |
| | | <!-- ç»æé¶æ®µï¼åè¦å¿
é¡»å±ç¤ºï¼ä¸è¦éé»åæ --> |
| | | <template v-else> |
| | | <div class="px-1 py-2"> |
| | | <Alert type="success" show-icon> |
| | | <template #message> |
| | | æ¥åå·²çæï¼<b>{{ result.reportNo }}</b> |
| | | </template> |
| | | <template #description> |
| | | <div> |
| | | 模æ¿çæ¬ {{ result.templateVersion }}ï¼åå
¥æ£éªé¡¹ |
| | | {{ result.itemCount }} æ¡ |
| | | </div> |
| | | </template> |
| | | </Alert> |
| | | |
| | | <Alert |
| | | v-if="result.undecidableCount > 0" |
| | | class="mt-3" |
| | | type="warning" |
| | | show-icon |
| | | :message="`æ¬æ¬¡æ ${result.undecidableCount} 项没æå¤å®ç»è®º`" |
| | | description="ç¼ºè§æ ¼ä¸ä¸éåæ²¡è§åç项æ ä¸ºãæ å¤å®è§åãï¼ä¸è®¡å
¥åæ ¼çä¹ä¸å䏿¥åç»è®ºï¼è§åæ§è¡å¤±è´¥ç项æ 为ãå¾
å¤å®ãï¼ä»è®¡å
¥åæ ¼çã两项é½è®¡å
¥æ»æ°ãå¦éå¤å®ï¼è¯·å
å¨è´¨æ£åä¸è¡¥è§æ ¼ä¸ä¸éï¼æä¿®å¥½æ¨¡æ¿éçè§ååéæ°åºä»¶ã" |
| | | /> |
| | | <Alert |
| | | v-else |
| | | class="mt-3" |
| | | type="info" |
| | | show-icon |
| | | message="å
¨é¨æ£éªé¡¹åå·²å¤å®ï¼æ²¡ææ æ³å¤å®ç项" |
| | | /> |
| | | |
| | | <div v-if="result.warnings.length > 0" class="mt-3"> |
| | | <div class="mb-1 text-sm font-medium text-gray-700"> |
| | | æ°æ®åèæç¤ºï¼{{ result.warnings.length }}ï¼ |
| | | </div> |
| | | <Alert type="warning"> |
| | | <ul class="m-0 list-disc pl-4"> |
| | | <li v-for="(w, i) in result.warnings" :key="i">{{ w }}</li> |
| | | </ul> |
| | | </Alert> |
| | | </div> |
| | | |
| | | <div v-if="result.errors.length > 0" class="mt-3"> |
| | | <div class="mb-1 text-sm font-medium text-gray-700"> |
| | | 模æ¿å段缺å£ï¼{{ result.errors.length }}ï¼ |
| | | </div> |
| | | <Alert type="error"> |
| | | <ul class="m-0 list-disc pl-4"> |
| | | <li v-for="(e, i) in result.errors" :key="i">{{ e }}</li> |
| | | </ul> |
| | | </Alert> |
| | | </div> |
| | | |
| | | <div class="mt-3 text-xs text-gray-400"> |
| | | ä»¥ä¸æç¤ºåªå¨æ¬æ¬¡ååºéåºç°ï¼ä¸è½åºï¼äºååçå岿¥åæ¶çä¸å°ï¼éè¦çç请èªè¡è®°å½ã |
| | | </div> |
| | | </div> |
| | | <div class="mt-4 flex justify-end gap-2"> |
| | | <Button @click="modalApi.close()">å
³é</Button> |
| | | <Button type="primary" @click="handleGoReportList">æ¥çæ¥åå表</Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesQcIndicatorItemType, |
| | | MesQcResultValueType, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { z } from '#/adapter/form'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { getIndicatorGroupList } from '#/api/mes/qc/indicator'; |
| | | |
| | | import { QcIndicatorResultSpecificationInput } from './components'; |
| | | |
| | |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'itemType', |
| | | label: 'æ¡ç®ç±»å', |
| | | component: 'RadioGroup', |
| | | componentProps: { |
| | | buttonStyle: 'solid', |
| | | optionType: 'button', |
| | | options: [ |
| | | { label: 'å½å
¥é¡¹', value: MesQcIndicatorItemType.ENTRY }, |
| | | { label: 'åç»é¡¹', value: MesQcIndicatorItemType.GROUP }, |
| | | ], |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['itemType'], |
| | | // åæåç»é¡¹æ¶æ¸
æåªå±äºå½å
¥é¡¹çåæ®µï¼åç»é¡¹å¿
é¡»é¡¶çº§ï¼æ®çç parentId ä¼è¢«å端å±çº§æ ¡éªé¡¶åæ¥ |
| | | trigger: (values) => { |
| | | if (values.itemType === MesQcIndicatorItemType.GROUP) { |
| | | formApi?.setFieldValue('parentId', 0); |
| | | formApi?.setFieldValue('resultType', undefined); |
| | | formApi?.setFieldValue('resultSpecification', undefined); |
| | | } |
| | | }, |
| | | }, |
| | | rules: z.number().default(MesQcIndicatorItemType.ENTRY), |
| | | }, |
| | | { |
| | | fieldName: 'parentId', |
| | | label: 'æå±åç»', |
| | | component: 'ApiTreeSelect', |
| | | componentProps: { |
| | | allowClear: true, |
| | | // ç¶çº§åéåªæåç»é¡¹ï¼åç»é¡¹åªè½æ¯é¡¶çº§ï¼æä»¥è¿éæ¯ä¸å±å¹³éºï¼ä¸éè¦ handleTree |
| | | api: async () => { |
| | | const data = await getIndicatorGroupList(); |
| | | return [ |
| | | { |
| | | id: 0, |
| | | name: 'é¡¶çº§ææ ', |
| | | children: data, |
| | | }, |
| | | ]; |
| | | }, |
| | | childrenField: 'children', |
| | | labelField: 'name', |
| | | placeholder: 'è¯·éæ©æå±åç»ï¼ä¸éåä¸ºé¡¶çº§ææ ', |
| | | treeDefaultExpandAll: true, |
| | | treeNodeFilterProp: 'name', |
| | | valueField: 'id', |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['itemType'], |
| | | if: (values) => values.itemType !== MesQcIndicatorItemType.GROUP, |
| | | }, |
| | | defaultValue: 0, |
| | | }, |
| | | { |
| | | fieldName: 'type', |
| | | label: 'æ£æµé¡¹ç±»å', |
| | | component: 'Select', |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'formulaText', |
| | | label: 'å
¬å¼', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | maxlength: 500, |
| | | placeholder: '请è¾å
¥å
¬å¼ï¼æ¥åéä½ä¸ºè¯¥åç»çæ£æµè¦æ±å±ç¤º', |
| | | rows: 2, |
| | | }, |
| | | dependencies: { |
| | | triggerFields: ['itemType'], |
| | | if: (values) => values.itemType === MesQcIndicatorItemType.GROUP, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'sortOrder', |
| | | label: 'æåºå·', |
| | | component: 'InputNumber', |
| | | componentProps: { |
| | | min: 0, |
| | | placeholder: 'ç空èªå¨æå°å级æ«å°¾', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'resultType', |
| | | label: 'ç»æå¼ç±»å', |
| | | component: 'Select', |
| | |
| | | formApi?.setFieldValue('resultSpecification', undefined), |
| | | options: getDictOptions(DICT_TYPE.MES_QC_RESULT_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©ç»æå¼ç±»å', |
| | | }, |
| | | // åç»é¡¹æ²¡æç»æå¼ï¼ä¸å±ç¤ºä¹ä¸æ ¡éªï¼è½åºæ¶åç«¯åæ ·ä¼æ¸
ç©ºï¼ |
| | | dependencies: { |
| | | triggerFields: ['itemType'], |
| | | if: (values) => values.itemType !== MesQcIndicatorItemType.GROUP, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'itemType', |
| | | label: 'æ¡ç®ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: [ |
| | | { label: 'å½å
¥é¡¹', value: MesQcIndicatorItemType.ENTRY }, |
| | | { label: 'åç»é¡¹', value: MesQcIndicatorItemType.GROUP }, |
| | | ], |
| | | placeholder: 'è¯·éæ©æ¡ç®ç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'type', |
| | | label: 'æ£æµé¡¹ç±»å', |
| | | component: 'Select', |
| | |
| | | minWidth: 160, |
| | | }, |
| | | { |
| | | field: 'itemType', |
| | | title: 'æ¡ç®ç±»å', |
| | | width: 100, |
| | | slots: { default: 'itemType' }, |
| | | }, |
| | | { |
| | | field: 'parentName', |
| | | title: 'æå±åç»', |
| | | width: 140, |
| | | slots: { default: 'parentName' }, |
| | | }, |
| | | { |
| | | field: 'type', |
| | | title: 'æ£æµé¡¹ç±»å', |
| | | width: 120, |
| | |
| | | exportIndicator, |
| | | getIndicatorPage, |
| | | } from '#/api/mes/qc/indicator'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { DICT_TYPE, MesQcIndicatorItemType } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { $t } from '#/locales'; |
| | | |
| | |
| | | function getTypeLabel(type: number): string { |
| | | const item = typeDictOptions.find((d) => d.value === type); |
| | | return item?.label || String(type); |
| | | } |
| | | |
| | | function getItemTypeColor(itemType: number): string { |
| | | return itemType === MesQcIndicatorItemType.GROUP ? 'purple' : 'blue'; |
| | | } |
| | | |
| | | function getItemTypeLabel(itemType: number): string { |
| | | return itemType === MesQcIndicatorItemType.GROUP ? 'åç»é¡¹' : 'å½å
¥é¡¹'; |
| | | } |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #itemType="{ row }"> |
| | | <Tag v-if="row.itemType" :color="getItemTypeColor(row.itemType)"> |
| | | {{ getItemTypeLabel(row.itemType) }} |
| | | </Tag> |
| | | </template> |
| | | <template #parentName="{ row }"> |
| | | {{ row.parentName || '-' }} |
| | | </template> |
| | | <template #type="{ row }"> |
| | | <Tag v-if="row.type !== undefined && row.type !== null" :color="getTypeColor(row.type)"> |
| | | {{ getTypeLabel(row.type) }} |
| | |
| | | import { deleteIpqc, exportIpqc, getIpqcPage } from '#/api/mes/qc/ipqc'; |
| | | import { $t } from '#/locales'; |
| | | import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue'; |
| | | import QcReportGenerateModal from '#/views/mes/qc/components/report-generate-modal.vue'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | /** æ¬é¡µå¯¹åº mes_qc_type çå¼ï¼2 è¿ç¨æ£éª */ |
| | | const QC_TYPE = 2; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | |
| | | const [ExportRangeModal, exportRangeModalApi] = useVbenModal({ |
| | | connectedComponent: QcExportRangeModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ReportModal, reportModalApi] = useVbenModal({ |
| | | connectedComponent: QcReportGenerateModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | .open(); |
| | | } |
| | | |
| | | /** åºæ¥åï¼æ°æ®ç±å端ä»è¯¥è´¨æ£ååï¼å端åªéæ¨¡æ¿ */ |
| | | function handleReport(row: MesQcIpqcApi.Ipqc) { |
| | | reportModalApi |
| | | .setData({ qcType: QC_TYPE, qcId: row.id, qcNo: row.code }) |
| | | .open(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ExportRangeModal /> |
| | | <ReportModal /> |
| | | <Grid table-title="è¿ç¨æ£éªåå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'åºæ¥å', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['qc-report:instance:generate'], |
| | | ifShow: row.status === MesQcStatusEnum.FINISHED, |
| | | onClick: handleReport.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | |
| | | import { deleteIqc, exportIqc, getIqcPage } from '#/api/mes/qc/iqc'; |
| | | import { $t } from '#/locales'; |
| | | import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue'; |
| | | import QcReportGenerateModal from '#/views/mes/qc/components/report-generate-modal.vue'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'MesQcIqc' }); |
| | | |
| | | /** æ¬é¡µå¯¹åº mes_qc_type çå¼ï¼1 æ¥ææ£éª */ |
| | | const QC_TYPE = 1; |
| | | |
| | | const route = useRoute(); |
| | | |
| | |
| | | |
| | | const [ExportRangeModal, exportRangeModalApi] = useVbenModal({ |
| | | connectedComponent: QcExportRangeModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ReportModal, reportModalApi] = useVbenModal({ |
| | | connectedComponent: QcReportGenerateModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | .open(); |
| | | } |
| | | |
| | | /** åºæ¥åï¼æ°æ®ç±å端ä»è¯¥è´¨æ£ååï¼å端åªéæ¨¡æ¿ */ |
| | | function handleReport(row: MesQcIqcApi.Iqc) { |
| | | reportModalApi |
| | | .setData({ qcType: QC_TYPE, qcId: row.id, qcNo: row.code }) |
| | | .open(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ExportRangeModal /> |
| | | <ReportModal /> |
| | | <Grid table-title="æ¥ææ£éªåå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'åºæ¥å', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['qc-report:instance:generate'], |
| | | ifShow: row.status === MesQcStatusEnum.FINISHED, |
| | | onClick: handleReport.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | |
| | | import { deleteOqc, exportOqc, getOqcPage } from '#/api/mes/qc/oqc'; |
| | | import { $t } from '#/locales'; |
| | | import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue'; |
| | | import QcReportGenerateModal from '#/views/mes/qc/components/report-generate-modal.vue'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | /** æ¬é¡µå¯¹åº mes_qc_type çå¼ï¼3 åºè´§æ£éª */ |
| | | const QC_TYPE = 3; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | |
| | | const [ExportRangeModal, exportRangeModalApi] = useVbenModal({ |
| | | connectedComponent: QcExportRangeModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ReportModal, reportModalApi] = useVbenModal({ |
| | | connectedComponent: QcReportGenerateModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | .open(); |
| | | } |
| | | |
| | | /** åºæ¥åï¼æ°æ®ç±å端ä»è¯¥è´¨æ£ååï¼å端åªéæ¨¡æ¿ */ |
| | | function handleReport(row: MesQcOqcApi.Oqc) { |
| | | reportModalApi |
| | | .setData({ qcType: QC_TYPE, qcId: row.id, qcNo: row.code }) |
| | | .open(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ExportRangeModal /> |
| | | <ReportModal /> |
| | | <Grid table-title="åºè´§æ£éªåå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'åºæ¥å', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['qc-report:instance:generate'], |
| | | ifShow: row.status === MesQcStatusEnum.FINISHED, |
| | | onClick: handleReport.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | |
| | | checkQuantity: row.quantity, |
| | | inspectDate: row.recordTime, |
| | | itemId: row.itemId, |
| | | name: `${row.sourceDocCode} éææ£éªå`, |
| | | name: `${row.sourceDocCode} éè´§æ£éªå`, |
| | | sourceDocCode: row.sourceDocCode, |
| | | sourceDocId: row.sourceDocId, |
| | | sourceDocType: row.sourceDocType, |
| | |
| | | onClick: handleCreateOqc.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'éææ£éª', |
| | | label: 'éè´§æ£éª', |
| | | type: 'link', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:qc-rqc:create'], |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { QcReportInstanceApi } from '#/api/mes/qc/report/instance'; |
| | | import type { QcReportTemplateApi } from '#/api/mes/qc/report/template'; |
| | | import type { SystemUserApi } from '#/api/system/user'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { getTemplatePage } from '#/api/mes/qc/report/template'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getRangePickerDefaultProps } from '#/utils'; |
| | | |
| | | /** |
| | | * ä¸å¡åæ®ç±»å â å±ç¤ºåã |
| | | * <p> |
| | | * å端æ businessType å®ä¹æèªç±åç¬¦ä¸²ï¼æ²¡ææä¸¾çº¦æï¼ï¼è¿éåªæå·²ç¥çåç§è´¨æ£åç¿»æä¸æï¼ |
| | | * 认ä¸åºçä¸å¾åæ ·æ¾ç¤ºââæ¯çéæå¦ä¸ç§åæ®è¦è¯å®ã |
| | | */ |
| | | const BUSINESS_TYPE_LABEL: Record<string, string> = { |
| | | mes_qc_iqc: 'æ¥ææ£éª', |
| | | mes_qc_ipqc: 'è¿ç¨æ£éª', |
| | | mes_qc_oqc: 'åºè´§æ£éª', |
| | | mes_qc_rqc: 'éè´§æ£éª', |
| | | }; |
| | | |
| | | export function businessTypeLabel(value?: string): string { |
| | | if (!value) { |
| | | return ''; |
| | | } |
| | | return BUSINESS_TYPE_LABEL[value] ?? value; |
| | | } |
| | | |
| | | /** å
³èæ°æ®ï¼å®ä¾è¡¨åªåäºæ¨¡æ¿ç¼å·ä¸å建人ç¼å·ï¼åç§°é½è¦å¦æ¥ä¸æ¬¡ */ |
| | | let templateList: QcReportTemplateApi.Template[] = []; |
| | | getTemplatePage({ pageNo: 1, pageSize: 100 }).then( |
| | | (data) => (templateList = data.list ?? []), |
| | | ); |
| | | |
| | | let userList: SystemUserApi.User[] = []; |
| | | getSimpleUserList().then((data) => (userList = data)); |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'reportNo', |
| | | label: 'æ¥åç¼å·', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥æ¥åç¼å·', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'businessType', |
| | | label: 'ä¸å¡ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: Object.entries(BUSINESS_TYPE_LABEL).map(([value, label]) => ({ |
| | | label, |
| | | value, |
| | | })), |
| | | placeholder: 'è¯·éæ©ä¸å¡ç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'businessId', |
| | | label: 'ä¸å¡åæ®', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥ä¸å¡åæ®ç¼å·ï¼ç²¾ç¡®å¹é
ï¼', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'æ¥åç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.QC_REPORT_INSTANCE_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©æ¥åç¶æ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'createTime', |
| | | label: 'çææ¶é´', |
| | | component: 'RangePicker', |
| | | componentProps: { |
| | | ...getRangePickerDefaultProps(), |
| | | allowClear: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<QcReportInstanceApi.Instance>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'reportNo', |
| | | title: 'æ¥åç¼å·', |
| | | width: 180, |
| | | }, |
| | | { |
| | | field: 'templateId', |
| | | title: 'æ¥å模æ¿', |
| | | minWidth: 200, |
| | | formatter: ({ row }) => |
| | | templateList.find((item) => item.id === row.templateId)?.templateName ?? |
| | | `æ¨¡æ¿ #${row.templateId}`, |
| | | }, |
| | | { |
| | | field: 'templateVersion', |
| | | title: 'çæ¬', |
| | | width: 100, |
| | | slots: { default: 'templateVersion' }, |
| | | }, |
| | | { |
| | | field: 'businessType', |
| | | title: 'ä¸å¡ç±»å', |
| | | width: 120, |
| | | formatter: ({ row }) => businessTypeLabel(row.businessType), |
| | | }, |
| | | { |
| | | field: 'businessId', |
| | | title: 'ä¸å¡åæ®', |
| | | width: 180, |
| | | formatter: ({ row }) => row.businessId || 'â', |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 110, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.QC_REPORT_INSTANCE_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'creator', |
| | | title: 'çæäºº', |
| | | width: 120, |
| | | formatter: ({ row }) => |
| | | userList.find((user) => String(user.id) === String(row.creator)) |
| | | ?.nickname ?? |
| | | row.creator ?? |
| | | 'â', |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'çææ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 200, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { QcReportInstanceApi } from '#/api/mes/qc/report/instance'; |
| | | |
| | | import { onMounted } from 'vue'; |
| | | import { useRoute } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deleteInstance, |
| | | getInstancePage, |
| | | } from '#/api/mes/qc/report/instance'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Detail from './modules/detail.vue'; |
| | | |
| | | defineOptions({ name: 'QcReportInstance' }); |
| | | |
| | | const route = useRoute(); |
| | | |
| | | const [DetailModal, detailModalApi] = useVbenModal({ |
| | | connectedComponent: Detail, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** æ¥çæ¥å详æ
ä¸é¢è§ */ |
| | | function handleDetail(row: QcReportInstanceApi.Instance) { |
| | | detailModalApi.setData(row).open(); |
| | | } |
| | | |
| | | /** å 餿¥åå®ä¾ */ |
| | | async function handleDelete(row: QcReportInstanceApi.Instance) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.reportNo]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteInstance(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.reportNo])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getInstancePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<QcReportInstanceApi.Instance>, |
| | | }); |
| | | |
| | | /** |
| | | * ä»è´¨æ£åãåºæ¥åãè·³è¿æ¥æ¶ä¼å¸¦ businessType + businessIdï¼é¢å¡«å°æç´¢æ¡ä»¶éå¹¶ç´æ¥æ¥ä¸æ¬¡ï¼ |
| | | * ç¨æ·è¿æ¥å°±çå°ååºçé£ä»½æ¥åï¼ä¸ç¨èªå·±åçä¸éã |
| | | * <p> |
| | | * å¿
é¡»è·çè° submitFormï¼è¡¨æ ¼åæç´¢æ¡ä»¶æ¯è¯» formApi.getLatestSubmissionValues()ï¼ |
| | | * åª setFieldValue çè¯æ¡ä»¶ä¼å¡«è¿è¡¨å使¥è¯¢ä»ç¨æ§å¼ï¼åè¡¨è¿æ¯å
¨éââçäºæ²¡çã |
| | | */ |
| | | onMounted(async () => { |
| | | const { businessType, businessId } = route.query; |
| | | if (!businessType && !businessId) { |
| | | return; |
| | | } |
| | | const values: Record<string, string> = {}; |
| | | if (businessType) { |
| | | values.businessType = String(businessType); |
| | | } |
| | | if (businessId) { |
| | | values.businessId = String(businessId); |
| | | } |
| | | await gridApi.formApi.setValues(values); |
| | | await gridApi.formApi.submitForm(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <DetailModal /> |
| | | |
| | | <Grid table-title="æ¥åå®ä¾å表"> |
| | | <template #templateVersion="{ row }"> |
| | | <Tag v-if="row.templateVersion" color="green"> |
| | | {{ row.templateVersion }} |
| | | </Tag> |
| | | <span v-else class="text-gray-400">â</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'é¢è§', |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | auth: ['qc-report:instance:query'], |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['qc-report:instance:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.reportNo]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { QcReportInstanceApi } from '#/api/mes/qc/report/instance'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { formatFileSize, openSafeUrl } from '@vben/utils'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | |
| | | import { Alert, Button, Descriptions, message, Spin, Tabs, Tag } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | exportInstancePdf, |
| | | getInstance, |
| | | getInstanceSnapshot, |
| | | } from '#/api/mes/qc/report/instance'; |
| | | import { DictTag } from '#/components/dict-tag'; |
| | | |
| | | import { businessTypeLabel } from '../data'; |
| | | |
| | | defineOptions({ name: 'QcReportInstanceDetail' }); |
| | | |
| | | const detail = ref<QcReportInstanceApi.InstanceDetail>(); |
| | | const snapshot = ref<Record<string, unknown>>(); |
| | | const activeTab = ref('preview'); |
| | | const loading = ref(false); |
| | | const exporting = ref(false); |
| | | const pdf = ref<QcReportInstanceApi.PdfResult>(); |
| | | |
| | | /** äº§ç© HTML æ¯åºä»¶æ¶å»ç»ä¸æ¥çé£ä¸ä»½ï¼ç´æ¥æ¸²æå³ä¸ºé¢è§ï¼ä¸å¿
éæ°åºä»¶ */ |
| | | const renderHtml = computed(() => detail.value?.renderHtml ?? ''); |
| | | |
| | | const snapshotText = computed(() => |
| | | snapshot.value ? JSON.stringify(snapshot.value, null, 2) : '', |
| | | ); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | detail.value = undefined; |
| | | snapshot.value = undefined; |
| | | pdf.value = undefined; |
| | | activeTab.value = 'preview'; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<QcReportInstanceApi.Instance>(); |
| | | if (!data?.id) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | loading.value = true; |
| | | try { |
| | | // 两个æ¥å£åå¼åï¼è¯¦æ
éæå MB çäº§ç© HTMLï¼å¿«ç
§ä¹ä¸å°ï¼åæä¸ä¸ªååºä¼è®©è¿ä»½æ°æ®æ¯æ¬¡é½è¢«æ´å
ä¼ è¾ |
| | | const [loadedDetail, loadedSnapshot] = await Promise.all([ |
| | | getInstance(data.id), |
| | | getInstanceSnapshot(data.id), |
| | | ]); |
| | | detail.value = loadedDetail; |
| | | snapshot.value = loadedSnapshot; |
| | | } finally { |
| | | loading.value = false; |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** å¯¼åº PDFï¼å端ä¼å
å æ§ç彿¡£éä»¶åç»å®æ°çï¼æä»¥éå¤ç¹ä¸ä¼å¨ç£çä¸å æä»¶ */ |
| | | async function handleExportPdf() { |
| | | if (!detail.value?.id) { |
| | | return; |
| | | } |
| | | exporting.value = true; |
| | | try { |
| | | pdf.value = await exportInstancePdf(detail.value.id); |
| | | message.success( |
| | | `å·²çæ ${pdf.value.fileName}ï¼${formatFileSize(pdf.value.byteSize)}ï¼èæ¶ ${pdf.value.durationMs} 毫ç§ï¼`, |
| | | ); |
| | | } finally { |
| | | exporting.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleOpenPreview() { |
| | | openSafeUrl(pdf.value?.previewURL); |
| | | } |
| | | |
| | | function handleDownload() { |
| | | openSafeUrl(pdf.value?.downloadURL); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="`æ¥å详æ
${detail?.reportNo ? ` - ${detail.reportNo}` : ''}`" class="w-4/5"> |
| | | <Spin :spinning="loading"> |
| | | <div class="flex flex-col gap-3"> |
| | | <Descriptions bordered size="small" :column="3"> |
| | | <Descriptions.Item label="æ¥åç¼å·"> |
| | | {{ detail?.reportNo || 'â' }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="模æ¿çæ¬"> |
| | | <Tag v-if="detail?.templateVersion" color="green"> |
| | | {{ detail.templateVersion }} |
| | | </Tag> |
| | | <span v-else>â</span> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="æ¥åç¶æ"> |
| | | <DictTag |
| | | v-if="detail?.status !== undefined && detail?.status !== null" |
| | | :type="DICT_TYPE.QC_REPORT_INSTANCE_STATUS" |
| | | :value="detail.status" |
| | | /> |
| | | <span v-else>â</span> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="ä¸å¡ç±»å"> |
| | | {{ businessTypeLabel(detail?.businessType) || 'â' }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="ä¸å¡åæ®"> |
| | | {{ detail?.businessId || 'â' }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="çææ¶é´"> |
| | | {{ detail?.createTime || 'â' }} |
| | | </Descriptions.Item> |
| | | </Descriptions> |
| | | |
| | | <Tabs v-model:active-key="activeTab" type="card"> |
| | | <Tabs.TabPane key="preview" tab="æ¥åé¢è§" force-render> |
| | | <Alert |
| | | v-if="!renderHtml" |
| | | type="warning" |
| | | show-icon |
| | | message="è¿ä»½æ¥å没æå¯é¢è§ç产ç©" |
| | | description="å®ä¾ç渲æäº§ç©ä¸ºç©ºï¼é常æå³çåºä»¶å½æ¶æªæåã请åå°å表确认æ¥åç¶æï¼æéæ°åºä»¶ã" |
| | | /> |
| | | <!-- |
| | | sandbox ç空 = å½»åºç¦ç¨èæ¬ã表åä¸åæºï¼åªè®© CSS çæï¼ |
| | | é¢è§çæ¯æ¸²æäº§ç©ï¼ä¸éè¦ä¹ä¸åºè¯¥è®©å®æ§è¡ä»»ä½ä¸è¥¿ã |
| | | --> |
| | | <iframe |
| | | v-else |
| | | :srcdoc="renderHtml" |
| | | sandbox="" |
| | | class="h-[60vh] w-full rounded border border-border bg-white" |
| | | ></iframe> |
| | | </Tabs.TabPane> |
| | | |
| | | <Tabs.TabPane key="snapshot" tab="æ°æ®å¿«ç
§"> |
| | | <Alert |
| | | class="mb-2" |
| | | type="info" |
| | | show-icon |
| | | message="è¿æ¯åºä»¶æ¶å»ç»çä¸å¡æ°æ®ï¼ä¸ä¼éåç»åæ®ä¿®æ¹èåå" |
| | | description="æ¥çãè¿ä»½å岿¥å彿¶ç¨çæ¯åªäºæ°æ®ã请以è¿é为åã" |
| | | /> |
| | | <pre |
| | | class="max-h-[55vh] overflow-auto rounded border border-border bg-gray-50 p-3 text-xs" |
| | | >{{ snapshotText || 'ï¼æ å¿«ç
§æ°æ®ï¼' }}</pre |
| | | > |
| | | </Tabs.TabPane> |
| | | </Tabs> |
| | | |
| | | <Alert v-if="pdf" type="success" show-icon> |
| | | <template #message> |
| | | <div class="text-xs"> |
| | | <div> |
| | | å·²çæ {{ pdf.fileName }}ï¼{{ formatFileSize(pdf.byteSize) }}ï¼æå°èæ¶ |
| | | {{ pdf.pdfDurationMs }} 毫ç§ï¼ |
| | | </div> |
| | | <div class="mt-1 flex gap-2"> |
| | | <Button size="small" @click="handleOpenPreview">å¨çº¿é¢è§</Button> |
| | | <Button size="small" type="primary" @click="handleDownload"> |
| | | ä¸è½½ PDF |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </Alert> |
| | | </div> |
| | | </Spin> |
| | | |
| | | <template #footer> |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="modalApi.close()">å
³é</Button> |
| | | <Button |
| | | type="primary" |
| | | :loading="exporting" |
| | | :disabled="!detail?.id" |
| | | @click="handleExportPdf" |
| | | > |
| | | <IconifyIcon icon="lucide:file-down" class="mr-1" /> |
| | | å¯¼åº PDF |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { QcReportTemplateApi } from '#/api/mes/qc/report/template'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { DEFAULT_PAGE } from './designer/constants'; |
| | | |
| | | /** æ°å¢/ä¿®æ¹ç表å */ |
| | | export function useFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'templateCode', |
| | | label: '模æ¿ç¼ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¨¡æ¿ç¼ç ï¼åæ¯/æ°å/ä¸å线/ä¸å线ï¼', |
| | | maxlength: 64, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'templateName', |
| | | label: '模æ¿åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¨¡æ¿åç§°', |
| | | maxlength: 128, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'industry', |
| | | label: 'éç¨è¡ä¸', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.QC_REPORT_INDUSTRY, 'string'), |
| | | placeholder: 'è¯·éæ©éç¨è¡ä¸', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'reportType', |
| | | label: 'æ¥åç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_QC_TYPE, 'string'), |
| | | placeholder: 'è¯·éæ©æ¥åç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'pageSize', |
| | | label: 'çº¸å¼ å°ºå¯¸', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.QC_REPORT_PAGE_SIZE, 'string'), |
| | | placeholder: 'è¯·éæ©çº¸å¼ å°ºå¯¸', |
| | | }, |
| | | // ç»å¤§å¤æ°è´¨æ£æ¥å齿¯ A4 纵åï¼é»è®¤å¸¦åºï¼ç徿¯æ¬¡é |
| | | defaultValue: DEFAULT_PAGE.size, |
| | | }, |
| | | { |
| | | fieldName: 'orientation', |
| | | label: 'çº¸å¼ æ¹å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.QC_REPORT_ORIENTATION, 'string'), |
| | | placeholder: 'è¯·éæ©çº¸å¼ æ¹å', |
| | | }, |
| | | defaultValue: DEFAULT_PAGE.orientation, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: '模æ¿ç¶æ', |
| | | component: 'RadioGroup', |
| | | componentProps: { |
| | | buttonStyle: 'solid', |
| | | optionType: 'button', |
| | | options: getDictOptions(DICT_TYPE.QC_REPORT_TEMPLATE_STATUS, 'number'), |
| | | }, |
| | | defaultValue: 0, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'description', |
| | | label: '模æ¿è¯´æ', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥æ¨¡æ¿è¯´æ', |
| | | rows: 3, |
| | | maxlength: 255, |
| | | showCount: true, |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'templateCode', |
| | | label: '模æ¿ç¼ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥æ¨¡æ¿ç¼ç ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'templateName', |
| | | label: '模æ¿åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | allowClear: true, |
| | | placeholder: '请è¾å
¥æ¨¡æ¿åç§°', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'industry', |
| | | label: 'éç¨è¡ä¸', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.QC_REPORT_INDUSTRY, 'string'), |
| | | placeholder: 'è¯·éæ©éç¨è¡ä¸', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'reportType', |
| | | label: 'æ¥åç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_QC_TYPE, 'string'), |
| | | placeholder: 'è¯·éæ©æ¥åç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: '模æ¿ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.QC_REPORT_TEMPLATE_STATUS, 'number'), |
| | | placeholder: 'è¯·éæ©æ¨¡æ¿ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çåæ®µ */ |
| | | export function useGridColumns(): VxeTableGridOptions<QcReportTemplateApi.Template>['columns'] { |
| | | return [ |
| | | { |
| | | field: 'templateCode', |
| | | title: '模æ¿ç¼ç ', |
| | | width: 160, |
| | | }, |
| | | { |
| | | field: 'templateName', |
| | | title: '模æ¿åç§°', |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | | field: 'industry', |
| | | title: 'éç¨è¡ä¸', |
| | | width: 120, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.QC_REPORT_INDUSTRY }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'reportType', |
| | | title: 'æ¥åç±»å', |
| | | width: 140, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_QC_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'pageSize', |
| | | title: 'çº¸å¼ ', |
| | | width: 100, |
| | | slots: { default: 'pageSetting' }, |
| | | }, |
| | | { |
| | | field: 'currentVersion', |
| | | title: 'å½åçæ¬', |
| | | width: 110, |
| | | slots: { default: 'currentVersion' }, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.QC_REPORT_TEMPLATE_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 180, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 280, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { QcReportVersionApi } from '#/api/mes/qc/report/version'; |
| | | |
| | | import { resolvePageMm } from '#/components/quality/engine/page'; |
| | | |
| | | /** |
| | | * å½å Schema çæ¬å·ï¼ä¸å端 ReportTemplateSchema.SCHEMA_VERSION å¿
é¡»ä¸è´ |
| | | * 1.1 å é¤äºä»æªä½¿ç¨ç dataSources / bindings / stylesï¼è¯¦è§ version/index.ts çå¥çº¦è¯´æ |
| | | */ |
| | | export const SCHEMA_VERSION = '1.1'; |
| | | |
| | | /** CSS åç´ ä¸æ¯«ç±³çæ¢ç®ï¼1mm = 96/25.4 px */ |
| | | export const MM_TO_PX = 96 / 25.4; |
| | | |
| | | /** ç»å¸ä¸çº¸å¼ çé¢è²ï¼ä»
ä½è§è§åè */ |
| | | export const PAPER_COLOR = '#ffffff'; |
| | | |
| | | /** é»è®¤çº¸å¼ é
ç½® */ |
| | | export const DEFAULT_PAGE: QcReportVersionApi.SchemaPage = { |
| | | size: 'A4', |
| | | orientation: 'portrait', |
| | | margin: { top: 10, right: 10, bottom: 10, left: 10 }, |
| | | }; |
| | | |
| | | /** 计ç®ç»å¸å°ºå¯¸ï¼pxï¼ï¼æçº¸å¼ æ¹åèªå¨ç¿»è½¬å®½é« */ |
| | | export function resolveCanvasSize(page?: QcReportVersionApi.SchemaPage): { |
| | | height: number; |
| | | marginPx: Required<QcReportVersionApi.SchemaMargin>; |
| | | width: number; |
| | | } { |
| | | const { heightMm, marginMm, widthMm } = resolvePageMm(page); |
| | | return { |
| | | width: Math.round(widthMm * MM_TO_PX), |
| | | height: Math.round(heightMm * MM_TO_PX), |
| | | marginPx: { |
| | | top: marginMm.top * MM_TO_PX, |
| | | right: marginMm.right * MM_TO_PX, |
| | | bottom: marginMm.bottom * MM_TO_PX, |
| | | left: marginMm.left * MM_TO_PX, |
| | | }, |
| | | }; |
| | | } |
| | | |
| | | /** ç±æ¨¡æ¿å
æ°æ®æ¨å¯¼åå§ Schemaï¼æ°å»ºçæ¬æ¶ä½¿ç¨ï¼ */ |
| | | export function createEmptySchema( |
| | | page?: QcReportVersionApi.SchemaPage, |
| | | ): QcReportVersionApi.ReportTemplateSchema { |
| | | return { |
| | | schemaVersion: SCHEMA_VERSION, |
| | | page: { ...DEFAULT_PAGE, ...page }, |
| | | grapes: {}, |
| | | components: [], |
| | | rules: [], |
| | | }; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { QcReportAiApi } from '#/api/mes/qc/report/ai'; |
| | | import type { QcReportVersionApi } from '#/api/mes/qc/report/version'; |
| | | import type { QualityAssemblySkip } from '#/components/quality'; |
| | | |
| | | import { computed, h, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'; |
| | | |
| | | import { useAccess } from '@vben/access'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { useTabs } from '@vben/hooks'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Button, |
| | | Empty, |
| | | InputNumber, |
| | | message, |
| | | Modal, |
| | | Select, |
| | | Spin, |
| | | Tag, |
| | | Tooltip, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { |
| | | createVersion, |
| | | getVersion, |
| | | getVersionListByTemplate, |
| | | publishVersion, |
| | | updateVersion, |
| | | } from '#/api/mes/qc/report/version'; |
| | | import { getTemplate, updateTemplate } from '#/api/mes/qc/report/template'; |
| | | import { validateRule } from '#/components/quality'; |
| | | import { router } from '#/router'; |
| | | |
| | | import { DEFAULT_PAGE } from './constants'; |
| | | import AiImportModal from './modules/ai-import-modal.vue'; |
| | | import RuleEditor from './modules/rule-editor.vue'; |
| | | import { useDesigner } from './use-designer'; |
| | | |
| | | defineOptions({ name: 'QcReportTemplateDesigner' }); |
| | | |
| | | const props = defineProps<{ |
| | | templateId?: number | string; |
| | | }>(); |
| | | |
| | | const tabs = useTabs(); |
| | | const { hasAccessByCodes } = useAccess(); |
| | | |
| | | /** AI 导å
¥æ¯ç¬ç«æéç ï¼è½æ¹æ¨¡æ¿ä¸çäºè½ç¨å¤§æ¨¡åè¯å«æä»¶ï¼æ¯æ¬¡è°ç¨é½è¦è±é±ï¼ */ |
| | | const canAiImport = computed(() => |
| | | hasAccessByCodes(['qc-report:template:ai-import']), |
| | | ); |
| | | |
| | | const loading = ref(true); |
| | | const saving = ref(false); |
| | | const publishing = ref(false); |
| | | const ruleDrawerOpen = ref(false); |
| | | const aiImportOpen = ref(false); |
| | | |
| | | const template = ref<Awaited<ReturnType<typeof getTemplate>>>(); |
| | | const versions = ref<QcReportVersionApi.Version[]>([]); |
| | | const activeVersionId = ref<number>(); |
| | | const pageSetting = ref<QcReportVersionApi.SchemaPage>({ ...DEFAULT_PAGE }); |
| | | |
| | | const blockPanelRef = ref<HTMLElement>(); |
| | | const canvasRef = ref<HTMLElement>(); |
| | | const traitPanelRef = ref<HTMLElement>(); |
| | | const stylePanelRef = ref<HTMLElement>(); |
| | | |
| | | const { |
| | | canRedo, |
| | | canUndo, |
| | | editor, |
| | | rules, |
| | | selectedLabel, |
| | | zoom, |
| | | applyPageSize, |
| | | collectSchema, |
| | | destroy, |
| | | init, |
| | | loadCanvasFromDraft, |
| | | loadSchema, |
| | | redo, |
| | | setZoom, |
| | | undo, |
| | | } = useDesigner(); |
| | | |
| | | const activeVersion = computed(() => |
| | | versions.value.find((item) => item.id === activeVersionId.value), |
| | | ); |
| | | const isDraft = computed(() => activeVersion.value?.status === 0); |
| | | const templateName = computed(() => template.value?.templateName ?? ''); |
| | | |
| | | /** åä¸éçè§åæ¡æ°ãè§åååäºè¦å°åºä»¶æ¶æç¸ï¼é¡¶æ å¾å
ç»åºæç¤º */ |
| | | const ruleErrorCount = computed( |
| | | () => rules.value.filter((rule) => validateRule(rule).length > 0).length, |
| | | ); |
| | | |
| | | /** |
| | | * ä¿ååå
å¡ä¸éã |
| | | * <p> |
| | | * å端ä¹ä¼æ¦ï¼ä½æ¥éå¼¹å°çé¢ä¸æ¶ç¨æ·å·²ç»ç¦»å¼æ½å±äºï¼è¿å¾èªå·±ç¿»æ¯åªæ¡è§åååçï¼ |
| | | * è¿éç´æ¥ææ½å±æå¼ï¼äººä¸è¿æ¥å°±çå°çº¢åæ å¨åªä¸æ¡ä¸ã |
| | | */ |
| | | function blockOnInvalidRules(): boolean { |
| | | if (ruleErrorCount.value === 0) { |
| | | return false; |
| | | } |
| | | ruleDrawerOpen.value = true; |
| | | message.warning(`æ ${ruleErrorCount.value} æ¡å¤å®è§åè¿æ²¡å宿´ï¼è¯·å
ä¿®æ£åä¿å`); |
| | | return true; |
| | | } |
| | | |
| | | const ruleButtonText = computed(() => |
| | | rules.value.length > 0 ? `å¤å®è§åï¼${rules.value.length}ï¼` : 'å¤å®è§å', |
| | | ); |
| | | |
| | | /** æ½å±éæ¹å®çè§ååå设计å¨ç¶æï¼ä¿åæ¶ç± collectSchema ä¸å¹¶äº§åº */ |
| | | function handleRulesChange(next: QcReportVersionApi.SchemaRule[]) { |
| | | rules.value = next; |
| | | } |
| | | |
| | | /** ç»å¸ä¸å·²æçé¡¶å±ç»ä»¶æ°ï¼ç¨æ¥å¤æ AI 导å
¥ä¼ä¸ä¼è¦çæç¨æ·æ£å¨åçå
容 */ |
| | | const canvasNodeCount = computed(() => editor.value?.getComponents().length ?? 0); |
| | | |
| | | /** |
| | | * AI è稿è½å°ã |
| | | * <p> |
| | | * è稿çåºççº¸å¼ è¦å¹¶è¿ pageSettingï¼ä¿åæ¶åçæ¯ pageSetting.valueï¼ |
| | | * ä¸åæ¥çè¯ AI è®¤åºæ¥ççº¸å¼ ä¼å¨ä¸ä¸æ¬¡ä¿åæ¶è¢«æ§å¼çåå»ã |
| | | */ |
| | | function handleApplyDraft(payload: { |
| | | components?: QcReportAiApi.DraftComponent[]; |
| | | page?: QcReportAiApi.DraftPage; |
| | | }) { |
| | | const components = payload.components ?? []; |
| | | if (components.length === 0) { |
| | | message.warning('èç¨¿éæ²¡æå¯ç¨çç»ä»¶ï¼è¯·éæ°è¯å«'); |
| | | return; |
| | | } |
| | | const mergedPage: QcReportVersionApi.SchemaPage = { |
| | | size: payload.page?.size ?? pageSetting.value.size, |
| | | orientation: payload.page?.orientation ?? pageSetting.value.orientation, |
| | | margin: { ...pageSetting.value.margin, ...payload.page?.margin }, |
| | | }; |
| | | |
| | | const applyDraft = () => { |
| | | pageSetting.value = mergedPage; |
| | | const { nodeCount, problems, skipped } = loadCanvasFromDraft( |
| | | components, |
| | | mergedPage, |
| | | ); |
| | | reportDraftIssues(nodeCount, problems, skipped); |
| | | }; |
| | | |
| | | if (canvasNodeCount.value > 0) { |
| | | Modal.confirm({ |
| | | title: 'æ¿æ¢ç»å¸å
容ï¼', |
| | | content: `ç»å¸ä¸å·²æ ${canvasNodeCount.value} 个ç»ä»¶ï¼å°è¢« AI è¯å«ç»ææ´ä½æ¿æ¢ï¼ä¸æ æ³ç¨æ¤éæ¢å¤ã建议å
æå½åå
容ä¿å为æ°çæ¬ã`, |
| | | onOk: applyDraft, |
| | | }); |
| | | return; |
| | | } |
| | | applyDraft(); |
| | | } |
| | | |
| | | /** è£
é
ç»æéçè·³è¿é¡¹ä¸é®é¢é¡¹ç»ä¸æ±æ¥ï¼éæ¡å¼¹ message ä¼å·æ»¡å±å¹ï¼ææä¸ä¸ªå¼¹çªæ´å¥½è¯» */ |
| | | function reportDraftIssues( |
| | | nodeCount: number, |
| | | problems: string[], |
| | | skipped: QualityAssemblySkip[], |
| | | ) { |
| | | if (nodeCount === 0) { |
| | | message.error('AI è¯å«åºçç»ä»¶è®¾è®¡å¨é½ä¸è®¤è¯ï¼ç»å¸æªæ¹å¨'); |
| | | return; |
| | | } |
| | | message.success(`å·²çæ ${nodeCount} 个ç»ä»¶å°ç»å¸ï¼ç¡®è®¤æ 误åç¹ãä¿åã`); |
| | | if (problems.length === 0 && skipped.length === 0) { |
| | | return; |
| | | } |
| | | Modal.warning({ |
| | | title: `æ ${skipped.length + problems.length} å¤éè¦çæ`, |
| | | width: 560, |
| | | content: h( |
| | | 'div', |
| | | { style: 'max-height: 40vh; overflow: auto; font-size: 12px;' }, |
| | | [ |
| | | ...skipped.map((item) => h('div', `· ${item.reason}`)), |
| | | ...problems.map((text) => h('div', `· ${text}`)), |
| | | ], |
| | | ), |
| | | }); |
| | | } |
| | | |
| | | const VERSION_STATUS_LABEL: Record<number, string> = { |
| | | 0: 'è稿', |
| | | 1: 'å·²åå¸', |
| | | 2: 'å·²åç¨', |
| | | }; |
| | | |
| | | const ZOOM_OPTIONS = [50, 75, 100, 125, 150].map((value) => ({ |
| | | label: `${value}%`, |
| | | value, |
| | | })); |
| | | |
| | | /** åºç¨çº¸å¼ 设置ï¼åªéæç»å¸å°ºå¯¸ä¸é¡µè¾¹è·åè线ï¼ä¸è¦çç»å¸ä¸æªä¿åçå
容 */ |
| | | function applyPageSetting() { |
| | | applyPageSize(pageSetting.value); |
| | | } |
| | | |
| | | /** å 载模æ¿ä¸å
¶çæ¬å表 */ |
| | | async function loadTemplate() { |
| | | const id = Number(props.templateId); |
| | | if (!Number.isFinite(id) || id <= 0) { |
| | | message.error('ç¼ºå°æ¨¡æ¿ç¼å·ï¼æ æ³è¿å
¥è®¾è®¡å¨'); |
| | | loading.value = false; |
| | | return; |
| | | } |
| | | loading.value = true; |
| | | try { |
| | | const detail = await getTemplate(id); |
| | | // 模æ¿å¯è½å·²è¢«ä»äººå é¤ï¼æé¾æ¥æ¯è¿æ/伪é çï¼è¿éå¿
é¡»å
ä½ç©ºååº |
| | | if (!detail) { |
| | | message.error('该æ¥å模æ¿ä¸åå¨æå·²è¢«å é¤ï¼æ æ³è¿å
¥è®¾è®¡å¨'); |
| | | handleBack(); |
| | | return; |
| | | } |
| | | template.value = detail; |
| | | pageSetting.value = { |
| | | size: detail.pageSize ?? DEFAULT_PAGE.size, |
| | | orientation: detail.orientation ?? DEFAULT_PAGE.orientation, |
| | | margin: { ...DEFAULT_PAGE.margin }, |
| | | }; |
| | | versions.value = await getVersionListByTemplate(id); |
| | | // é»è®¤å卿æ°çæ¬ï¼æ²¡æçæ¬æ¶ç空ï¼ç±ç¨æ·ç¹å»âä¿åâçæ v1.0 |
| | | const latest = versions.value[0]; |
| | | activeVersionId.value = latest?.id; |
| | | const schema = latest ? await getVersion(latest.id!) : undefined; |
| | | if (schema?.schema?.page) { |
| | | pageSetting.value = schema.schema.page; |
| | | } |
| | | await nextTick(); |
| | | loadSchema(schema?.schema); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** åæ¢çæ¬ï¼AntDV ç Select åè°å¼ç±»åæ¯ number 宽ï¼è¿éç»ä¸æ¶æï¼ */ |
| | | async function handleVersionChange(value: unknown) { |
| | | const id = Number(value); |
| | | if (!Number.isFinite(id)) { |
| | | return; |
| | | } |
| | | const detail = await getVersion(id); |
| | | if (!detail) { |
| | | message.error('è¯¥çæ¬ä¸åå¨æå·²è¢«å é¤ï¼è¯·éæ°éæ©çæ¬'); |
| | | await refreshVersions(); |
| | | return; |
| | | } |
| | | activeVersionId.value = id; |
| | | if (detail.schema?.page) { |
| | | pageSetting.value = detail.schema.page; |
| | | } |
| | | // è稿å¯è½è¿æ²¡åå
¥è¿å
容ï¼schema 为空ï¼ï¼æç©ºç½ç»å¸è½½å
¥å³å¯ï¼ä¸ç®é误 |
| | | loadSchema(detail.schema ?? undefined); |
| | | } |
| | | |
| | | /** ä¿åï¼èç¨¿ç´æ¥è¦çï¼å·²åå¸çæ¬å¦å为æ°çæ¬ */ |
| | | async function handleSave() { |
| | | const id = Number(props.templateId); |
| | | if (!Number.isFinite(id) || id <= 0) { |
| | | return; |
| | | } |
| | | if (blockOnInvalidRules()) { |
| | | return; |
| | | } |
| | | saving.value = true; |
| | | try { |
| | | const baseSchema = collectSchema(pageSetting.value); |
| | | const current = activeVersion.value; |
| | | if (current?.id && current.status === 0) { |
| | | await updateVersion({ id: current.id, schema: baseSchema }); |
| | | message.success(`å·²ä¿åå° ${current.version}`); |
| | | } else { |
| | | const newId = await createVersion({ |
| | | templateId: id, |
| | | schema: baseSchema, |
| | | description: current ? `åºäº ${current.version} å建` : undefined, |
| | | }); |
| | | message.success('å·²ä¿å为æ°çæ¬è稿'); |
| | | await refreshVersions(); |
| | | activeVersionId.value = newId; |
| | | } |
| | | /* |
| | | * çº¸å¼ è®¾ç½®å±äºæ¨¡æ¿çº§å±æ§ï¼åæ¥å模æ¿ï¼åè¡¨é¡µæ®æ¤å±ç¤ºã |
| | | * è¿ä¸ªæ¥å£æ¯æ´ä½æ´æ°ï¼æ¨¡æ¿ç¼ç /åç§°çå¿
å¡«åæ®µä¸å¸¦ä¸ä¼è¢«æ ¡éªæåæ¥ |
| | | * ï¼ã模æ¿ç¼ç ä¸è½ä¸ºç©ºãï¼ï¼æä»¥è¦ææ¨¡æ¿åæå段ä¸å¹¶å¸¦è¿å»ï¼åªè¦ççº¸å¼ ä¸¤é¡¹ã |
| | | */ |
| | | await updateTemplate({ |
| | | ...template.value, |
| | | id, |
| | | pageSize: pageSetting.value.size, |
| | | orientation: pageSetting.value.orientation, |
| | | }); |
| | | template.value = await getTemplate(id); |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | | } |
| | | |
| | | async function refreshVersions() { |
| | | const id = Number(props.templateId); |
| | | versions.value = await getVersionListByTemplate(id); |
| | | } |
| | | |
| | | /** åå¸å½åçæ¬ */ |
| | | function handlePublish() { |
| | | const current = activeVersion.value; |
| | | if (!current?.id) { |
| | | message.warning('请å
ä¿åè稿ï¼ååå¸çæ¬'); |
| | | return; |
| | | } |
| | | if (current.status !== 0) { |
| | | message.warning(`å½åçæ¬å·²æ¯ã${VERSION_STATUS_LABEL[current.status ?? 0]}ãï¼æ ééå¤åå¸`); |
| | | return; |
| | | } |
| | | // åå¸çäºæå½åå
容åºåæä¸å¯æ¹ççæ¬ï¼è§åååäºè¿ä¼å¿æ¦ä¸æ¥æçäº |
| | | if (blockOnInvalidRules()) { |
| | | return; |
| | | } |
| | | Modal.confirm({ |
| | | title: '确认åå¸è¯¥çæ¬ï¼', |
| | | content: `åå¸å ${current.version} çå
容å°ä¸å¯åä¿®æ¹ï¼å¦éè°æ´è¯·å¦åæ°çæ¬ã`, |
| | | async onOk() { |
| | | publishing.value = true; |
| | | try { |
| | | // åå¸åå
æç»å¸ææ°å
容è½åºï¼é¿å
åå¸çæ¯æ§å
容 |
| | | await updateVersion({ id: current.id!, schema: collectSchema(pageSetting.value) }); |
| | | await publishVersion(current.id!); |
| | | await refreshVersions(); |
| | | message.success(`çæ¬ ${current.version} å·²åå¸`); |
| | | } finally { |
| | | publishing.value = false; |
| | | } |
| | | }, |
| | | }); |
| | | } |
| | | |
| | | /** ç»å¸ç¼©æ¾ï¼Select åè°å¼ç±»åæ¯ number 宽ï¼è¿éç»ä¸æ¶æï¼ */ |
| | | function handleZoomChange(value: unknown) { |
| | | const next = Number(value); |
| | | if (Number.isFinite(next)) { |
| | | setZoom(next); |
| | | } |
| | | } |
| | | |
| | | /** è¿åå表页 */ |
| | | function handleBack() { |
| | | tabs.closeCurrentTab(); |
| | | router.push('/qc/report-template'); |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await nextTick(); |
| | | if (!canvasRef.value || !blockPanelRef.value || !traitPanelRef.value || !stylePanelRef.value) { |
| | | return; |
| | | } |
| | | init({ |
| | | canvasContainer: canvasRef.value, |
| | | blockContainer: blockPanelRef.value, |
| | | traitContainer: traitPanelRef.value, |
| | | styleContainer: stylePanelRef.value, |
| | | }); |
| | | await loadTemplate(); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | destroy(); |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="qc-report-designer flex h-full flex-col"> |
| | | <!-- é¡¶æ --> |
| | | <div class="flex items-center justify-between border-b border-border bg-card px-3 py-2"> |
| | | <div class="flex items-center gap-2"> |
| | | <Button size="small" @click="handleBack"> |
| | | <IconifyIcon icon="lucide:arrow-left" class="mr-1" /> |
| | | è¿å |
| | | </Button> |
| | | <span class="font-medium">{{ templateName || 'æ¥å模æ¿è®¾è®¡å¨' }}</span> |
| | | <Select |
| | | v-if="versions.length > 0" |
| | | :value="activeVersionId" |
| | | :options=" |
| | | versions.map((item) => ({ |
| | | label: `${item.version}ï¼${VERSION_STATUS_LABEL[item.status ?? 0]}ï¼`, |
| | | value: item.id, |
| | | })) |
| | | " |
| | | class="w-44" |
| | | size="small" |
| | | @change="handleVersionChange" |
| | | /> |
| | | <Tag v-else color="default">å°æ çæ¬</Tag> |
| | | <Tag v-if="activeVersion" :color="isDraft ? 'blue' : 'green'"> |
| | | {{ isDraft ? 'å¯ç¼è¾' : 'åªè¯»' }} |
| | | </Tag> |
| | | <Button v-if="canAiImport" size="small" @click="aiImportOpen = true"> |
| | | <IconifyIcon icon="lucide:sparkles" class="mr-1" /> |
| | | AI 导å
¥ |
| | | </Button> |
| | | <Button size="small" @click="ruleDrawerOpen = true"> |
| | | <IconifyIcon icon="lucide:list-checks" class="mr-1" /> |
| | | {{ ruleButtonText }} |
| | | </Button> |
| | | <Tag v-if="ruleErrorCount > 0" color="red"> |
| | | {{ ruleErrorCount }} æ¡è§åå¾
ä¿®æ£ |
| | | </Tag> |
| | | </div> |
| | | |
| | | <div class="flex items-center gap-2"> |
| | | <Tooltip title="æ¤é"> |
| | | <Button size="small" :disabled="!canUndo" @click="undo"> |
| | | <IconifyIcon icon="lucide:undo-2" /> |
| | | </Button> |
| | | </Tooltip> |
| | | <Tooltip title="éå"> |
| | | <Button size="small" :disabled="!canRedo" @click="redo"> |
| | | <IconifyIcon icon="lucide:redo-2" /> |
| | | </Button> |
| | | </Tooltip> |
| | | <Select |
| | | :value="zoom" |
| | | :options="ZOOM_OPTIONS" |
| | | size="small" |
| | | class="w-24" |
| | | @change="handleZoomChange" |
| | | /> |
| | | <span v-if="selectedLabel" class="text-xs text-gray-500"> |
| | | å·²éä¸ï¼{{ selectedLabel }} |
| | | </span> |
| | | <Button size="small" type="primary" :loading="saving" @click="handleSave"> |
| | | <IconifyIcon icon="lucide:save" class="mr-1" /> |
| | | ä¿å |
| | | </Button> |
| | | <Button |
| | | size="small" |
| | | :loading="publishing" |
| | | :disabled="!isDraft" |
| | | @click="handlePublish" |
| | | > |
| | | <IconifyIcon icon="lucide:send" class="mr-1" /> |
| | | åå¸ |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- çº¸å¼ è®¾ç½® --> |
| | | <div class="flex items-center gap-3 border-b border-border bg-card px-3 py-1.5 text-xs"> |
| | | <span class="text-gray-500">çº¸å¼ </span> |
| | | <Select |
| | | v-model:value="pageSetting.size" |
| | | :options="[ |
| | | { label: 'A3', value: 'A3' }, |
| | | { label: 'A4', value: 'A4' }, |
| | | { label: 'A5', value: 'A5' }, |
| | | { label: 'Letter', value: 'Letter' }, |
| | | ]" |
| | | size="small" |
| | | class="w-24" |
| | | @change="applyPageSetting" |
| | | /> |
| | | <Select |
| | | v-model:value="pageSetting.orientation" |
| | | :options="[ |
| | | { label: '纵å', value: 'portrait' }, |
| | | { label: '横å', value: 'landscape' }, |
| | | ]" |
| | | size="small" |
| | | class="w-24" |
| | | @change="applyPageSetting" |
| | | /> |
| | | <span class="text-gray-500">页边è·(mm)</span> |
| | | <InputNumber |
| | | v-model:value="pageSetting.margin!.top" |
| | | size="small" |
| | | :min="0" |
| | | :max="50" |
| | | class="w-20" |
| | | addon-before="ä¸" |
| | | /> |
| | | <InputNumber |
| | | v-model:value="pageSetting.margin!.right" |
| | | size="small" |
| | | :min="0" |
| | | :max="50" |
| | | class="w-20" |
| | | addon-before="å³" |
| | | /> |
| | | <InputNumber |
| | | v-model:value="pageSetting.margin!.bottom" |
| | | size="small" |
| | | :min="0" |
| | | :max="50" |
| | | class="w-20" |
| | | addon-before="ä¸" |
| | | /> |
| | | <InputNumber |
| | | v-model:value="pageSetting.margin!.left" |
| | | size="small" |
| | | :min="0" |
| | | :max="50" |
| | | class="w-20" |
| | | addon-before="å·¦" |
| | | /> |
| | | <Button size="small" type="link" @click="applyPageSetting">åºç¨å°ç»å¸</Button> |
| | | </div> |
| | | |
| | | <!-- è®¾è®¡åº --> |
| | | <Spin :spinning="loading" wrapper-class-name="flex-1 overflow-hidden"> |
| | | <div class="flex h-full"> |
| | | <div class="w-56 shrink-0 overflow-auto border-r border-border bg-card p-2"> |
| | | <div class="mb-2 text-xs font-medium text-gray-500">ç»ä»¶</div> |
| | | <div ref="blockPanelRef" class="qc-report-blocks"></div> |
| | | </div> |
| | | <div class="flex-1 overflow-hidden bg-gray-100"> |
| | | <div ref="canvasRef" class="h-full"></div> |
| | | </div> |
| | | <div class="w-72 shrink-0 overflow-auto border-l border-border bg-card p-2"> |
| | | <div class="mb-2 text-xs font-medium text-gray-500">屿§</div> |
| | | <div ref="traitPanelRef"></div> |
| | | <div class="mt-3 mb-2 text-xs font-medium text-gray-500">æ ·å¼</div> |
| | | <div ref="stylePanelRef"></div> |
| | | <Empty |
| | | v-if="!selectedLabel" |
| | | :image="Empty.PRESENTED_IMAGE_SIMPLE" |
| | | description="éä¸ç»å¸ä¸çç»ä»¶ä»¥ç¼è¾å±æ§" |
| | | class="mt-2" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </Spin> |
| | | |
| | | <RuleEditor |
| | | v-model:open="ruleDrawerOpen" |
| | | :rules="rules" |
| | | @update:rules="handleRulesChange" |
| | | /> |
| | | |
| | | <AiImportModal |
| | | v-model:open="aiImportOpen" |
| | | :template-id="Number(props.templateId)" |
| | | @apply="handleApplyDraft" |
| | | /> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | /* |
| | | * GrapesJS èªå¸¦çæ¯æ·±è²ä¸»é¢ï¼è¿éæå®å¯¹é½ Ant Design çæµ
è²ä¸»é¢ã |
| | | * åéæå¨é¡µé¢æ ¹èç¹ä¸ï¼ç»ä»¶é¢æ¿ / 屿§é¢æ¿æå¨ç¼è¾å¨å®¹å¨ä¹å¤ï¼åªæä»å
±åç¥å
ç»§æ¿æçæã |
| | | */ |
| | | .qc-report-designer { |
| | | --gjs-main-color: #f5f5f5; |
| | | --gjs-primary-color: #ffffff; |
| | | --gjs-secondary-color: #333333; |
| | | --gjs-tertiary-color: #1677ff; |
| | | --gjs-quaternary-color: #1677ff; |
| | | --gjs-font-color: #333333; |
| | | --gjs-font-color-active: #1677ff; |
| | | --gjs-main-dark-color: rgba(0, 0, 0, 6%); |
| | | --gjs-secondary-dark-color: rgba(0, 0, 0, 4%); |
| | | --gjs-main-light-color: rgba(0, 0, 0, 6%); |
| | | --gjs-secondary-light-color: rgba(0, 0, 0, 55%); |
| | | --gjs-soft-light-color: rgba(0, 0, 0, 2%); |
| | | --gjs-light-border: rgba(0, 0, 0, 8%); |
| | | --gjs-arrow-color: rgba(0, 0, 0, 55%); |
| | | --gjs-dark-text-shadow: rgba(0, 0, 0, 10%); |
| | | /* é¡¶æ ä¸å·¦ä¾§ç»ä»¶é¢æ¿é½ç±æ¬é¡µé¢èªç»ï¼ç»å¸å 满ç¼è¾å¨å®¹å¨ */ |
| | | --gjs-canvas-top: 0px; |
| | | --gjs-left-width: 0%; |
| | | } |
| | | |
| | | /* |
| | | * Ant ç Spin å¨ä¸é´å¥äºä¸å± .ant-spin-containerï¼å®é»è®¤æ¯å
容é«åº¦èéææ»¡ç¶çº§ï¼ |
| | | * 伿左å³ä¸¤æ æå¾æ¯å¯è§åºé«ï¼å¯¼è´ä¸¤æ ç overflow:auto æ°¸è¿ä¸è§¦åãåºé¨è¢«è£æã |
| | | * è¿éæå®æ¥åç¶çº§ç flex é«åº¦ï¼å·¦å³æ æè½æ£å¸¸æ»å¨ã |
| | | */ |
| | | :deep(.ant-spin-container) { |
| | | height: 100%; |
| | | } |
| | | |
| | | /* 让 GrapesJS é»è®¤é¢æ¿è´´åå³ä¾§æ 宽度 */ |
| | | :deep(.gjs-trt-traits), |
| | | :deep(.gjs-sm-sectors) { |
| | | font-size: 12px; |
| | | } |
| | | |
| | | :deep(.gjs-cv-canvas) { |
| | | overflow: auto; |
| | | background-color: #f0f0f0; |
| | | } |
| | | |
| | | /* |
| | | * GrapesJS çç»å¸é»è®¤æ¯ä¸å overflow:hidden çç»å¯¹å®ä½èå°ï¼çº¸å¼ æ¯å¯è§åºå¤§æ¶ä¼è¢«è£æã |
| | | * è¿éæç»å¸æ¹ææ®éæ»å¨å®¹å¨ï¼å¸§å®¹å¨åå½ææ¡£æµï¼çº¸å¼ ç¨ margin å±
ä¸ï¼è¶
åºå³å¯æ»å¨ã |
| | | */ |
| | | :deep(.gjs-cv-canvas__frames), |
| | | :deep(.gjs-frames) { |
| | | position: static; |
| | | width: auto; |
| | | height: auto; |
| | | } |
| | | |
| | | :deep(.gjs-frame-wrapper) { |
| | | position: relative; |
| | | top: auto !important; |
| | | left: auto !important; |
| | | margin: 24px auto; |
| | | } |
| | | |
| | | :deep(.gjs-block) { |
| | | width: calc(50% - 6px); |
| | | min-height: auto; |
| | | padding: 6px 4px; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | :deep(.gjs-block-label) { |
| | | font-size: 12px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | /** |
| | | * AI 导å
¥å¼¹çªï¼ä¸ä¼ ä¸ä»½å·²æçæ£éªæ¥å â è¯å«ææ¨¡æ¿è稿 â 交ç»è®¾è®¡å¨ã |
| | | * <p> |
| | | * å¼¹çªåªè´è´£ãæ¿è稿ãè¿ä¸æ®µï¼ä¸ä¼ ãç»éä»¶ã请æ±è¯å«ãç»ç¨æ·çç»æã |
| | | * è£
å°ç»å¸ä¸æ¯è®¾è®¡å¨çäºï¼ç±ç¶ç»ä»¶å¨ç¨æ·ç¡®è®¤åæ¥åââ |
| | | * ç»å¸é空æ¶è¦å
å¼¹æ¿æ¢ç¡®è®¤ï¼é£ä»½å¤æå±äºè®¾è®¡å¨è䏿¯è¿éã |
| | | */ |
| | | import type { QcReportAiApi } from '#/api/mes/qc/report/ai'; |
| | | import type { UploadFile, UploadProps } from 'ant-design-vue'; |
| | | |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Alert, Button, Input, message, Modal, Spin, Tag, Upload } from 'ant-design-vue'; |
| | | |
| | | import { generateAiDraft } from '#/api/mes/qc/report/ai'; |
| | | import { |
| | | bindAttachments, |
| | | deleteAttachments, |
| | | listAttachments, |
| | | uploadFile, |
| | | } from '#/api/system/storage'; |
| | | import { |
| | | buildQualityComponentCatalog, |
| | | getQualityComponent, |
| | | registerAllQualityComponents, |
| | | } from '#/components/quality'; |
| | | |
| | | import { SCHEMA_VERSION } from '../constants'; |
| | | |
| | | defineOptions({ name: 'QcReportAiImportModal' }); |
| | | |
| | | const props = defineProps<{ |
| | | templateId?: number; |
| | | }>(); |
| | | |
| | | const open = defineModel<boolean>('open', { default: false }); |
| | | |
| | | const emit = defineEmits<{ |
| | | /** ç¨æ·ç¡®è®¤æè稿è£
å°ç»å¸ä¸ */ |
| | | apply: [ |
| | | payload: { |
| | | components?: QcReportAiApi.DraftComponent[]; |
| | | page?: QcReportAiApi.DraftPage; |
| | | }, |
| | | ]; |
| | | }>(); |
| | | |
| | | /** ä¸å端 yudao.qcreport.ai-import.max-files ä¸è´ï¼è¿éåªåçé¢ä¸ç峿¶çº¦æï¼æå¨æ ¡éªå¨å端 */ |
| | | const MAX_FILES = 3; |
| | | |
| | | const ACCEPT = |
| | | '.pdf,.docx,.xlsx,.png,.jpg,.jpeg,.bmp,.gif,.webp,.txt,.csv'; |
| | | |
| | | const fileList = ref<UploadFile[]>([]); |
| | | const hint = ref(''); |
| | | const recognizing = ref(false); |
| | | const draft = ref<QcReportAiApi.DraftResp>(); |
| | | |
| | | /** Upload å
鍿æçæä»¶å¯¹è±¡ç±»åï¼RcFileï¼ç»§æ¿èªæµè§å¨ç Fileï¼ */ |
| | | type UploadedFile = NonNullable<UploadFile['originFileObj']>; |
| | | |
| | | /** å·²éæä»¶æ°ï¼AntDV ç fileList éå¯è½æ··çä¸ä¼ 失败项ï¼è¿éåªè®¤æå®ä½æä»¶ç */ |
| | | const selectedFiles = computed(() => |
| | | fileList.value |
| | | .map((item) => item.originFileObj) |
| | | .filter((file): file is UploadedFile => Boolean(file)), |
| | | ); |
| | | |
| | | const canRecognize = computed( |
| | | () => !recognizing.value && selectedFiles.value.length > 0, |
| | | ); |
| | | |
| | | /** ç»ä»¶è稿çå±ç¤ºè¡ï¼ç±»åå±ç¤ºå + 认å¾åºæ¥ç屿§ */ |
| | | const draftRows = computed(() => |
| | | (draft.value?.components ?? []).map((component, index) => { |
| | | const definition = getQualityComponent(component.type); |
| | | const propsText = Object.entries(component.props ?? {}) |
| | | .filter(([, value]) => value !== undefined && value !== '') |
| | | .map(([key, value]) => `${fieldLabel(component.type, key)}=${String(value)}`) |
| | | .join('ï¼'); |
| | | return { |
| | | index: index + 1, |
| | | label: definition?.label ?? component.type ?? '(æªç¥ç»ä»¶)', |
| | | type: component.type ?? '', |
| | | registered: Boolean(definition), |
| | | propsText, |
| | | }; |
| | | }), |
| | | ); |
| | | |
| | | const unknownCount = computed( |
| | | () => draftRows.value.filter((row) => !row.registered).length, |
| | | ); |
| | | |
| | | /** 软æç¤ºï¼è¢«è·³è¿çé¡µãæ æ³è¡¨è¾¾çåºåç */ |
| | | const warnings = computed(() => draft.value?.warnings ?? []); |
| | | |
| | | const durationText = computed(() => |
| | | draft.value?.durationMs ? `${(draft.value.durationMs / 1000).toFixed(1)} ç§` : '', |
| | | ); |
| | | |
| | | /** 屿§ key â åæ®µæ¾ç¤ºåï¼è®¤ä¸åºå®ä¹æ¶éå key æ¬èº« */ |
| | | function fieldLabel(type: string | undefined, key: string): string { |
| | | const definition = getQualityComponent(type); |
| | | return ( |
| | | definition?.propertySchema.find((field) => field.key === key)?.label ?? key |
| | | ); |
| | | } |
| | | |
| | | /** äº¤ç» Upload èªå·±æ¶éæä»¶ï¼ä¸èµ°å®å
ç½®çä¸ä¼ ééï¼ä¸ä¼ æ¶æºç±ç¨æ·ç¹æé®å³å®ï¼ */ |
| | | const beforeUpload: UploadProps['beforeUpload'] = () => false; |
| | | |
| | | function reset() { |
| | | fileList.value = []; |
| | | hint.value = ''; |
| | | draft.value = undefined; |
| | | } |
| | | |
| | | watch(open, (value) => { |
| | | if (value) { |
| | | reset(); |
| | | } |
| | | }); |
| | | |
| | | /** |
| | | * æ¸
ææ¨¡æ¿ä¸å·²æççæºæä»¶ã |
| | | * <p> |
| | | * è¯ä¹æ¯**æ¿æ¢**è䏿¯ç´¯ç§¯ï¼ä¸æ¬¡å¯¼å
¥å¯¹åºä¸ä»½æºæä»¶ï¼æ°çè¿æ¥ãæ§çåºå»ãè¿äºæä»¶åªæ¯è¯å«ç¨çè¾å
¥ï¼ |
| | | * 模æ¿çæ£çäº§ç©æ¯çæ¬ Schemaï¼æ²¡æå½æ¡£å®ä»¬çå¿
è¦ã |
| | | * <p> |
| | | * å¿
é¡»å¨è¿ä¸æ¥å ï¼ä¸è½ææç»å®æ¶é¡ºææ¸
æ ââ ç»å®çå®ç°æ¯ãåªå æ§çéä»¶å
³èè®°å½ãä¿ç blob è®°å½ |
| | | * ï¼æä»¶æ¬èº«ä¸å é¤ï¼ãï¼æ§æä»¶å ä¸äºè¡ä¹å ä¸äºçï¼éå¤å¯¼å
¥å 次ç£çä¸å°±å¤å 份忲¡äººå¼ç¨çæä»¶ã |
| | | */ |
| | | async function removeExistingSourceFiles(templateId: number) { |
| | | const existing = await listAttachments({ |
| | | application: 'file', |
| | | recordType: 'qc_report_template', |
| | | recordId: templateId, |
| | | }); |
| | | const attachmentIds = (existing ?? []) |
| | | .map((blob) => blob.storageAttachmentId) |
| | | .filter((id): id is number => typeof id === 'number'); |
| | | if (attachmentIds.length > 0) { |
| | | await deleteAttachments(attachmentIds); |
| | | } |
| | | } |
| | | |
| | | /** ä¸ä¼ â æ¸
æ§æºæä»¶ â ç»éä»¶ â è¯å«ã忥é½è¦èµ°å®ï¼å¤±è´¥å³ä¸æå¹¶å¦å®æ¥é */ |
| | | async function handleRecognize() { |
| | | const templateId = Number(props.templateId); |
| | | if (!Number.isFinite(templateId) || templateId <= 0) { |
| | | message.error('ç¼ºå°æ¨¡æ¿ç¼å·ï¼æ æ³å
³èä¸ä¼ æä»¶'); |
| | | return; |
| | | } |
| | | const files = selectedFiles.value; |
| | | if (files.length === 0) { |
| | | return; |
| | | } |
| | | |
| | | recognizing.value = true; |
| | | try { |
| | | // ä¸ä¼ è¿åç顺åºå³ pages 顺åºï¼ç´æ¥æè¿ä¸ªé¡ºåºæ blobId 交ç»å端 |
| | | const blobs = await uploadFile(files); |
| | | const blobIds = blobs.map((blob) => blob.id); |
| | | if (blobIds.length === 0) { |
| | | message.error('æä»¶ä¸ä¼ 失败ï¼è¯·éæ°éæ©æä»¶'); |
| | | return; |
| | | } |
| | | |
| | | // æ¾å¨ä¸ä¼ ä¹åï¼ä¸ä¼ 失败就ä»ä¹é½ä¸å¨ï¼ä¸ä¸ä»½æºæä»¶è¿å¨ |
| | | await removeExistingSourceFiles(templateId); |
| | | |
| | | // å
ç»å®åè¯å«ï¼è¯å«å¤±è´¥æä»¶ä¹å·²å½å±å°æ¨¡æ¿ï¼ä¸ä¼çæå¤å¿éä»¶ |
| | | await bindAttachments({ |
| | | application: 'file', |
| | | recordType: 'qc_report_template', |
| | | recordId: templateId, |
| | | blobItems: blobIds.map((blobId) => ({ blobId })), |
| | | }); |
| | | |
| | | // 注åè¡¨è¦æ¾å¼æ³¨åææå
容ï¼è®¾è®¡å¨ init æ¶åè¿ä¸æ¬¡ï¼è¿éæ¯å¹ççå
åºï¼ |
| | | registerAllQualityComponents(); |
| | | draft.value = await generateAiDraft({ |
| | | blobIds, |
| | | templateId, |
| | | schemaVersion: SCHEMA_VERSION, |
| | | catalog: buildQualityComponentCatalog(), |
| | | hint: hint.value.trim() || undefined, |
| | | }); |
| | | if ((draft.value?.components ?? []).length === 0) { |
| | | message.warning('没æè¯å«åºä»»ä½ç»ä»¶ï¼è¯·æ¢ä¸ä»½æ´æ¸
æ°çæä»¶éè¯'); |
| | | } |
| | | } finally { |
| | | // 失败æ¶ä¸å¨è¿ä¸å±åæé误ï¼å端已æã维度 + å®é
å¼ + 坿§è¡å¨ä½ãç»äºç²¾ç¡®ææ¡ï¼ |
| | | // ç±è¯·æ±å±ç»ä¸å¼¹åºå³å¯ï¼è¿éå
ä¸å±åèä¼çä½å® |
| | | recognizing.value = false; |
| | | } |
| | | } |
| | | |
| | | function handleApply() { |
| | | emit('apply', { |
| | | components: draft.value?.components, |
| | | page: draft.value?.page, |
| | | }); |
| | | open.value = false; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | v-model:open="open" |
| | | title="AI 导å
¥æ¨¡æ¿" |
| | | :width="720" |
| | | :footer="null" |
| | | :mask-closable="!recognizing" |
| | | :closable="!recognizing" |
| | | > |
| | | <div class="flex flex-col gap-3"> |
| | | <Alert |
| | | type="info" |
| | | show-icon |
| | | message="è¯å«ç»æåªæ¯è稿" |
| | | description="AI è¯å«åºçå
容ä¸ä¼èªå¨ä¿åï¼éè¦ä½ å°è®¾è®¡å¨é确认ãè°æ´åç¹ãä¿åãæä¼æä¸ºæ¨¡æ¿çæ¬ã" |
| | | /> |
| | | |
| | | <template v-if="!draft"> |
| | | <Upload.Dragger |
| | | v-model:file-list="fileList" |
| | | :multiple="true" |
| | | :max-count="MAX_FILES" |
| | | :accept="ACCEPT" |
| | | :before-upload="beforeUpload" |
| | | :disabled="recognizing" |
| | | > |
| | | <p class="ant-upload-drag-icon"> |
| | | <IconifyIcon icon="lucide:upload-cloud" class="text-2xl" /> |
| | | </p> |
| | | <p class="ant-upload-text">ç¹å»æææ½æä»¶å°æ¤å¤</p> |
| | | <p class="ant-upload-hint text-xs"> |
| | | æ¯ææ«æä»¶/ç
§çï¼pngãjpgãjpegãbmpãgifãwebpï¼ãPDFãWord(.docx)ãExcel(.xlsx)ãææ¬(txtãcsv)ï¼ |
| | | 䏿¬¡æå¤ {{ MAX_FILES }} 个æä»¶ï¼å¤é¡µæä»¶æé¡µåºè¯å«ã |
| | | </p> |
| | | <p class="ant-upload-hint text-xs"> |
| | | .doc / .xls æ§æ ¼å¼ä¸æ¯æï¼è¯·å
ç¨ Office å¦å为 .docx / .xlsxã |
| | | </p> |
| | | </Upload.Dragger> |
| | | |
| | | <Input |
| | | v-model:value="hint" |
| | | placeholder="è¡¥å
说æï¼éå¡«ï¼ï¼ä¾å¦ï¼è¿æ¯ IQC æ¥ææ£éªæ¥å" |
| | | :disabled="recognizing" |
| | | /> |
| | | |
| | | <Spin v-if="recognizing" size="small"> |
| | | <div class="py-2 text-xs text-gray-500"> |
| | | æ£å¨è¯å«ï¼é¢è®¡ 10â60 ç§ãæ«æä»¶æé¡µé页è¯å«ï¼æ¯é¡µåç¬ä¸æ¬¡è°ç¨ï¼é¡µæ°å¤æ¶ä¼ææ¾æ´ä¹
ï¼ |
| | | æé¿çå¾
约 5 åéï¼è¶
æ¶å请åå°æä»¶æ°éæé¡µæ°åè¯ã请å¿å
³é页é¢ã |
| | | </div> |
| | | </Spin> |
| | | |
| | | <div class="flex justify-end gap-2"> |
| | | <Button :disabled="recognizing" @click="open = false">åæ¶</Button> |
| | | <Button |
| | | type="primary" |
| | | :loading="recognizing" |
| | | :disabled="!canRecognize" |
| | | @click="handleRecognize" |
| | | > |
| | | <IconifyIcon icon="lucide:sparkles" class="mr-1" /> |
| | | å¼å§è¯å« |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | |
| | | <template v-else> |
| | | <div class="flex items-center gap-2 text-xs text-gray-500"> |
| | | <span>è¯å«åº {{ draftRows.length }} 个ç»ä»¶</span> |
| | | <span v-if="durationText">èæ¶ {{ durationText }}</span> |
| | | <Tag v-if="unknownCount > 0" color="orange"> |
| | | {{ unknownCount }} 个ç»ä»¶è®¾è®¡å¨ä¸è®¤è¯ |
| | | </Tag> |
| | | </div> |
| | | |
| | | <Alert |
| | | v-if="draft.summary" |
| | | type="success" |
| | | show-icon |
| | | :message="draft.summary" |
| | | /> |
| | | |
| | | <Alert v-if="warnings.length > 0" type="warning" show-icon> |
| | | <template #message> |
| | | <div class="text-xs"> |
| | | <div v-for="(warning, index) in warnings" :key="index"> |
| | | · {{ warning }} |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </Alert> |
| | | |
| | | <div class="max-h-72 overflow-auto rounded border border-border"> |
| | | <div |
| | | v-for="row in draftRows" |
| | | :key="row.index" |
| | | class="flex gap-2 border-b border-border px-3 py-2 text-xs last:border-b-0" |
| | | > |
| | | <span class="w-6 shrink-0 text-gray-400">{{ row.index }}</span> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="flex items-center gap-2"> |
| | | <span class="font-medium">{{ row.label }}</span> |
| | | <span class="text-gray-400">{{ row.type }}</span> |
| | | <Tag v-if="!row.registered" color="orange">æªæ³¨åï¼å°è¢«è·³è¿</Tag> |
| | | </div> |
| | | <div v-if="row.propsText" class="mt-0.5 truncate text-gray-500"> |
| | | {{ row.propsText }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="text-xs text-gray-500"> |
| | | è¡¨æ ¼åªè¯å«ç»æï¼æä»¶éçå
·ä½æ°æ®è¡ä¸ä¼åè¿æ¨¡æ¿ï¼æ°æ®å¨åºä»¶æ¶ç±ä¸å¡åæ®æä¾ã |
| | | </div> |
| | | |
| | | <div class="flex justify-end gap-2"> |
| | | <Button @click="reset"> |
| | | <IconifyIcon icon="lucide:refresh-cw" class="mr-1" /> |
| | | éæ°ä¸ä¼ |
| | | </Button> |
| | | <Button type="primary" :disabled="draftRows.length === 0" @click="handleApply"> |
| | | <IconifyIcon icon="lucide:check" class="mr-1" /> |
| | | çæå°ç»å¸ |
| | | </Button> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | /** |
| | | * å¤å®è§åæ½å±ã |
| | | * <p> |
| | | * è§åå表ç±è®¾è®¡å¨ææï¼use-designer ç rulesï¼ï¼è¿éç´æ¥æ¹é£ä»½æ°æ®ï¼ |
| | | * ä¿åæ¶ collectSchema 产åºçæ¯æ´ä»½ Schemaï¼æ½å±ä¸ç»å¸å¿
é¡»å
±ç¨åä¸ä»½è§åï¼ |
| | | * ååä¸ä»½å°±è¿æ©ä¼åºç°ãç»å¸ä¸çå°çè§åãåãä¿åä¸å»çè§åãä¸ä¸è´ã |
| | | */ |
| | | import type { QcReportVersionApi } from '#/api/mes/qc/report/version'; |
| | | |
| | | import { computed } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { |
| | | Button, |
| | | Drawer, |
| | | Empty, |
| | | Input, |
| | | Popconfirm, |
| | | Select, |
| | | Switch, |
| | | } from 'ant-design-vue'; |
| | | |
| | | import { listAllFunctionNames, validateRule } from '#/components/quality'; |
| | | |
| | | defineOptions({ name: 'QcReportRuleEditor' }); |
| | | |
| | | const rules = defineModel<QcReportVersionApi.SchemaRule[]>('rules', { required: true }); |
| | | const open = defineModel<boolean>('open', { default: false }); |
| | | |
| | | const SCOPE_OPTIONS = [ |
| | | { label: 'é项å¤å®', value: 'item' }, |
| | | { label: 'æ¥å级', value: 'report' }, |
| | | ]; |
| | | |
| | | /** 空ç½è§åçåæ³ç¤ºä¾ï¼æä½ç¨åç»ä¸åçæ ·ä¾ï¼çå¾ç¨æ·å¯¹çä¸ç空ç½çè¯æ³ */ |
| | | const EXPRESSION_PLACEHOLDER: Record<string, string> = { |
| | | item: 'ä¾ï¼item.actualValue >= item.lowerLimit AND item.actualValue <= item.upperLimit', |
| | | report: 'ä¾ï¼PASS_RATE(inspectionItems.result) >= 60', |
| | | }; |
| | | |
| | | /** |
| | | * å¯ç¨å½æ°çæ¸
åç´æ¥åèªå¼æç½ååï¼ä¸å¨çé¢ä¸å¦æä¸ä»½ã |
| | | * æä¸ä»½å°±ä¼å¨å¼æå 彿°æ¶æ¼æ¹ï¼ç¨æ·ç
§æç¤ºåç表达å¼åèè¢«å¼æææã |
| | | */ |
| | | const FUNCTION_HINT = listAllFunctionNames().join('ã'); |
| | | |
| | | /** æ¯æ¡è§åçé®é¢ï¼ç©ºä¸²è¡¨ç¤ºæ²¡é®é¢ãä¸å端ä¿åæ¶çæ ¡éªåå£å¾ */ |
| | | const ruleErrors = computed(() => rules.value.map((rule) => validateRule(rule)[0] ?? '')); |
| | | |
| | | const errorCount = computed(() => ruleErrors.value.filter(Boolean).length); |
| | | |
| | | let sequence = 0; |
| | | |
| | | function handleAdd() { |
| | | sequence += 1; |
| | | rules.value = [ |
| | | ...rules.value, |
| | | { |
| | | // id åªç¨äºåºéæ¶å®ä½ï¼æ¶é´æ³å åºå·è¶³å¤ä¸ä¸ä¾èµéæºæ° |
| | | id: `rule-${Date.now()}-${sequence}`, |
| | | name: `è§å ${rules.value.length + 1}`, |
| | | scope: 'item', |
| | | expression: '', |
| | | enabled: true, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | function handleRemove(index: number) { |
| | | rules.value = rules.value.filter((_, current) => current !== index); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Drawer v-model:open="open" title="å¤å®è§å" placement="right" :width="640"> |
| | | <div class="flex h-full flex-col gap-3"> |
| | | <div class="rounded border border-border bg-gray-50 p-2 text-xs leading-5 text-gray-600"> |
| | | <div> |
| | | <span class="font-medium">é项å¤å®</span |
| | | >ï¼å¯¹æ¯æ¡æ£éªé¡¹å嫿±å¼ï¼è¡¨è¾¾å¼ä¸ºçå³å¤è¯¥é¡¹åæ ¼ãæ²¡æä»»ä½é项è§åæ¶ï¼éåæè§æ ¼ä¸ä¸éå¤å®ã |
| | | </div> |
| | | <div> |
| | | <span class="font-medium">æ¥å级</span |
| | | >ï¼å³å®æ´ä»½æ¥åçç»è®ºã没æä»»ä½æ¥å级è§åæ¶ï¼åãå
¨é¨æ£éªé¡¹é½åæ ¼ãã |
| | | </div> |
| | | <div class="mt-1"> |
| | | å¯ç¨å段ï¼<code>item.actualValue</code>ã<code>item.lowerLimit</code>ã<code |
| | | >item.upperLimit</code |
| | | >ã<code>item.itemName</code>ã<code>item.result</code>ã<code>report.total</code>ã<code |
| | | >report.passCount</code |
| | | >ã<code>report.failCount</code>ã<code>inspectionItems.result</code> |
| | | </div> |
| | | <div> |
| | | å¯ç¨å½æ°ï¼{{ FUNCTION_HINT }} |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="errorCount > 0" class="text-xs text-red-500"> |
| | | æ {{ errorCount }} æ¡è§åè¿æ²¡å宿´ï¼ä¿ååéè¦å
æ¹å¥½ã |
| | | </div> |
| | | |
| | | <div class="flex-1 overflow-auto"> |
| | | <Empty |
| | | v-if="rules.length === 0" |
| | | :image="Empty.PRESENTED_IMAGE_SIMPLE" |
| | | description="è¿æ²¡æå¤å®è§åï¼æ£éªé¡¹å°æè§æ ¼ä¸ä¸éå¤å®" |
| | | /> |
| | | <div v-else class="flex flex-col gap-3"> |
| | | <div |
| | | v-for="(rule, index) in rules" |
| | | :key="rule.id ?? index" |
| | | class="rounded border border-border p-3" |
| | | :class="rule.enabled === false ? 'opacity-60' : ''" |
| | | > |
| | | <div class="mb-2 flex items-center gap-2"> |
| | | <Input |
| | | v-model:value="rule.name" |
| | | size="small" |
| | | placeholder="è§ååç§°ï¼åºéæ¶ä¼å¸¦ä¸è¿ä¸ªåå" |
| | | class="flex-1" |
| | | /> |
| | | <Select |
| | | v-model:value="rule.scope" |
| | | :options="SCOPE_OPTIONS" |
| | | size="small" |
| | | class="w-28" |
| | | /> |
| | | <Switch v-model:checked="rule.enabled" size="small" /> |
| | | <Popconfirm title="å é¤è¯¥è§åï¼" @confirm="handleRemove(index)"> |
| | | <Button size="small" type="text" danger> |
| | | <IconifyIcon icon="lucide:trash-2" /> |
| | | </Button> |
| | | </Popconfirm> |
| | | </div> |
| | | <Input.TextArea |
| | | v-model:value="rule.expression" |
| | | :rows="2" |
| | | :placeholder="EXPRESSION_PLACEHOLDER[rule.scope ?? 'item']" |
| | | class="font-mono text-xs" |
| | | /> |
| | | <div v-if="ruleErrors[index]" class="mt-1 text-xs text-red-500"> |
| | | {{ ruleErrors[index] }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <Button block type="dashed" @click="handleAdd"> |
| | | <IconifyIcon icon="lucide:plus" class="mr-1" /> |
| | | æ°å¢è§å |
| | | </Button> |
| | | </div> |
| | | </Drawer> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { Editor, EditorConfig } from 'grapesjs'; |
| | | |
| | | import type { QualityAssemblySkip, QualityDraftComponent } from '#/components/quality'; |
| | | import type { QcReportVersionApi } from '#/api/mes/qc/report/version'; |
| | | |
| | | import { ref, shallowRef } from 'vue'; |
| | | |
| | | import grapesjs from 'grapesjs'; |
| | | |
| | | import { |
| | | assembleQualityCanvas, |
| | | bindQualityCanvasSync, |
| | | buildQualityBlocks, |
| | | registerAllQualityComponents, |
| | | registerBindingTraitType, |
| | | restoreQualityTraits, |
| | | QUALITY_TYPE_ATTR, |
| | | } from '#/components/quality'; |
| | | |
| | | import { PAPER_COLOR, resolveCanvasSize, SCHEMA_VERSION } from './constants'; |
| | | |
| | | import 'grapesjs/dist/css/grapes.min.css'; |
| | | |
| | | /** GrapesJS å
é¨é¡µé¢è®¾å¤çæ è¯åç¼ï¼åç¼ä¸ºç»å¸å°ºå¯¸ï¼å°ºå¯¸éçº¸å¼ è®¾ç½®å¨æåå */ |
| | | const PAGE_DEVICE_PREFIX = 'qc-report-page'; |
| | | |
| | | /** ç»å¸ä¸ç¨å¼å¯¼æ ·å¼ç style å
ç´ idï¼é建ç»å¸å¸§åé å®å¤æææ²¡æé夿³¨å
¥ */ |
| | | const CANVAS_BINDING_GUIDE_ID = 'qc-canvas-binding-guide'; |
| | | |
| | | /** |
| | | * åªè¦çãå¼è¿åå¨å ä½ç¬¦ãç hiddenï¼ä¸å¨çæ£ç hidden="hidden"ã |
| | | * æ¶çªå° td æ¯å 为è¿ä¸ªå±æ§ç®ååªç¨å¨åå
æ ¼ä¸ï¼åªå¤©çç¨å°å«çæ ç¾ä¸ï¼ |
| | | * ä¹è¦æé£ä¸ªæ ç¾ç display åå䏿¡ï¼ä¸è½ä¸æ¡éç¨è§åç³è¿å»ã |
| | | */ |
| | | const CANVAS_BINDING_GUIDE_CSS = 'td[hidden*="{{"] { display: table-cell; }'; |
| | | |
| | | export interface DesignerInitOptions { |
| | | /** ç»ä»¶é¢æ¿å®¹å¨ï¼å·¦ä¾§ï¼ */ |
| | | blockContainer: HTMLElement; |
| | | /** ç»å¸å®¹å¨ï¼ä¸é´ï¼ */ |
| | | canvasContainer: HTMLElement; |
| | | /** æ ·å¼é¢æ¿å®¹å¨ï¼å³ä¾§ä¸åï¼ */ |
| | | styleContainer: HTMLElement; |
| | | /** 屿§é¢æ¿å®¹å¨ï¼å³ä¾§ä¸åï¼ */ |
| | | traitContainer: HTMLElement; |
| | | } |
| | | |
| | | /** |
| | | * GrapesJS 设计å¨å°è£
|
| | | * <p> |
| | | * åªè´è´£ç»å¸ä¸ Schema çåå转æ¢ï¼ä¸æ¿æ
ä¿å/åå¸çä¸å¡å¨ä½ï¼ä¾¿äºåç»æ¿æ¢æ¸²æå
æ ¸ã |
| | | */ |
| | | export function useDesigner() { |
| | | const editor = shallowRef<Editor>(); |
| | | const zoom = ref(100); |
| | | const selectedLabel = ref(''); |
| | | const canUndo = ref(false); |
| | | const canRedo = ref(false); |
| | | |
| | | let canvasSize = resolveCanvasSize(); |
| | | |
| | | /** |
| | | * å¤å®è§åãéçæ¬ä¸èµ·è½½å
¥ãé collectSchema ä¸èµ·äº§åºã |
| | | * <p> |
| | | * ç¼è¾å¨ä½å¨è§åæ½å±éï¼è¿éåªè´è´£ææè¿ä»½æ°æ®ã |
| | | */ |
| | | const rules = ref<QcReportVersionApi.SchemaRule[]>([]); |
| | | |
| | | /** |
| | | * æç»å¸è°å°æå®çº¸å¼ 尺寸ã |
| | | * <p> |
| | | * GrapesJS åªå¨ device åå¼åçååæ¶æéæç»å¸å¸§ï¼æä»¥è®¾å¤ id é带ä¸å°ºå¯¸ï¼ |
| | | * æ¯æ¬¡æ¢çº¸å¼ 齿¯ä¸æ¬¡"çæ£çæ°è®¾å¤"ï¼æ¢å®åææ§å°ºå¯¸çè®¾å¤æ¸
æã |
| | | */ |
| | | function setPageDevice(width: number, height: number) { |
| | | const instance = editor.value; |
| | | if (!instance) { |
| | | return; |
| | | } |
| | | const id = `${PAGE_DEVICE_PREFIX}-${width}x${height}`; |
| | | instance.Devices.add({ |
| | | id, |
| | | name: '页é¢', |
| | | width: `${width}px`, |
| | | height: `${height}px`, |
| | | // çº¸å¼ æ¨¡æ¿çæ ·å¼ä¸è½æå¨æä¸ªè®¾å¤å®½åº¦ç @media ä¸ï¼å¦åæ¢çº¸å¼ åæ ·å¼ä¼å¤±æ |
| | | widthMedia: '', |
| | | }); |
| | | instance.setDevice(id); |
| | | instance.Devices.getDevices() |
| | | .filter((device) => { |
| | | const deviceId = device.get('id'); |
| | | return ( |
| | | typeof deviceId === 'string' && |
| | | deviceId !== id && |
| | | deviceId.startsWith(PAGE_DEVICE_PREFIX) |
| | | ); |
| | | }) |
| | | .forEach((device) => instance.Devices.remove(device)); |
| | | } |
| | | |
| | | /** |
| | | * ç»å¸ä¸ççº¸å¼ å¤è§ï¼ç½åº + 页边è·çç½ã |
| | | * <p> |
| | | * ä»
ä½è§è§åèï¼ç´æ¥åå¨ç»å¸ body ä¸ï¼ä¸ä¼è¿å
¥ Schemaï¼ |
| | | * frameStyle åªå¨é¦ä¸ªç»å¸å¸§çæï¼é建页é¢åè¦å¨è¿éè¡¥ä¸æ¬¡ç½åºã |
| | | */ |
| | | function applyFrameGuide() { |
| | | const instance = editor.value; |
| | | if (!instance) { |
| | | return; |
| | | } |
| | | const doc = instance.Canvas.getDocument(); |
| | | if (!doc) { |
| | | return; |
| | | } |
| | | const { marginPx } = canvasSize; |
| | | doc.body.style.backgroundColor = PAPER_COLOR; |
| | | doc.body.style.padding = `${marginPx.top}px ${marginPx.right}px ${marginPx.bottom}px ${marginPx.left}px`; |
| | | doc.body.style.boxSizing = 'border-box'; |
| | | applyBindingGuide(doc); |
| | | } |
| | | |
| | | /** |
| | | * ç»å¸æ {{...}} åæ ·çç䏿±è§£ï¼äºæ¯ç»åæ ¼ä¸ç hidden="{{item.group.hidden}}" |
| | | * 卿µè§å¨ç¼éå°±æ¯ãæ hidden 屿§ãââæ´æ ¼ display:noneï¼è¿ä¸è¡åé¢çæ ¼æ´ä½å·¦ç§»ä¸æ ¼ï¼ |
| | | * æ«ååªå©è¡¨å¤´ãè¢«æ¤æç«æç»æ¡ï¼è®¾è®¡å¨éçå°çæçè·åºä»¶ç»æå¯¹ä¸ä¸ã |
| | | * è¿éåªå¨ç»å¸ææ¡£å
æãå¼è¿åå¨å ä½ç¬¦ãç hidden è¿åæå¯è§æ ¼ï¼åºä»¶æ¶è¯¥å¼å·²è§£æ |
| | | * 空串æ hiddenï¼éæ©å¨ä¸åå½ä¸ï¼æ¥å渲æä¸ä¸ªåé½ä¸åå½±åã |
| | | */ |
| | | function applyBindingGuide(doc: Document) { |
| | | if (doc.getElementById(CANVAS_BINDING_GUIDE_ID)) { |
| | | return; |
| | | } |
| | | const style = doc.createElement('style'); |
| | | style.id = CANVAS_BINDING_GUIDE_ID; |
| | | style.textContent = CANVAS_BINDING_GUIDE_CSS; |
| | | doc.head.append(style); |
| | | } |
| | | |
| | | function syncHistoryState() { |
| | | const instance = editor.value; |
| | | if (!instance) { |
| | | return; |
| | | } |
| | | const undoManager = instance.UndoManager; |
| | | canUndo.value = undoManager.hasUndo(); |
| | | canRedo.value = undoManager.hasRedo(); |
| | | } |
| | | |
| | | function init(options: DesignerInitOptions): Editor { |
| | | // ç»ä»¶é¢æ¿çå
容å
¨é¨æ¥èªè´¨éç»ä»¶æ³¨å表ï¼è¿éå
ä¿è¯å
ç½®ç»ä»¶å·²æ³¨å |
| | | registerAllQualityComponents(); |
| | | const config: EditorConfig = { |
| | | container: options.canvasContainer, |
| | | height: '100%', |
| | | width: 'auto', |
| | | fromElement: false, |
| | | // ç±æ¬å¹³å°èªå·±çæ¥å£ä¿åï¼ä¸ä½¿ç¨ GrapesJS å
ç½®åå¨ |
| | | storageManager: false, |
| | | // é¡¶æ ç±é¡µé¢èªç»ï¼å»æ GrapesJS é»è®¤é¢æ¿ |
| | | panels: { defaults: [] }, |
| | | blockManager: { |
| | | appendTo: options.blockContainer, |
| | | blocks: buildQualityBlocks(), |
| | | }, |
| | | traitManager: { appendTo: options.traitContainer }, |
| | | styleManager: { |
| | | appendTo: options.styleContainer, |
| | | sectors: [ |
| | | { name: 'å¸å±', open: false, properties: ['display', 'width', 'height', 'margin', 'padding'] }, |
| | | { name: 'æç', open: false, properties: ['font-size', 'font-weight', 'color', 'text-align', 'line-height'] }, |
| | | { name: 'èæ¯ä¸è¾¹æ¡', open: false, properties: ['background-color', 'border', 'border-radius'] }, |
| | | ], |
| | | }, |
| | | canvas: { |
| | | // frameStyle ä¼è¢«åæ ·æ¼è¿ç»å¸ iframe ç <style>ï¼å¿
é¡»ä¼ CSS åç¬¦ä¸²ï¼ |
| | | // ä¼ å¯¹è±¡åªä¼å¾å°åé¢é [object Object]ã页边è·çç½ç± applyFrameGuide æçº¸å¼ 设置åå
¥ã |
| | | frameStyle: `body { background-color: ${PAPER_COLOR} }`, |
| | | }, |
| | | // ç»å¸å°ºå¯¸ç± setPageDevice æçº¸å¼ è®¾ç½®å¨ææ³¨åï¼è¿éä¸é¢ç½®è®¾å¤ |
| | | }; |
| | | |
| | | const instance = grapesjs.init(config); |
| | | editor.value = instance; |
| | | registerBindingTraitType(instance); |
| | | |
| | | instance.on('canvas:frame:load', () => { |
| | | applyFrameGuide(); |
| | | }); |
| | | instance.on('component:selected', (component) => { |
| | | const attrs = component?.getAttributes() ?? {}; |
| | | selectedLabel.value = String(attrs[QUALITY_TYPE_ATTR] ?? component?.get('tagName') ?? ''); |
| | | }); |
| | | instance.on('component:deselected', () => { |
| | | selectedLabel.value = ''; |
| | | }); |
| | | ['undo', 'redo', 'update'].forEach((event) => { |
| | | instance.on(event, syncHistoryState); |
| | | }); |
| | | bindQualityCanvasSync(instance); |
| | | |
| | | setPageDevice(canvasSize.width, canvasSize.height); |
| | | applyFrameGuide(); |
| | | syncHistoryState(); |
| | | return instance; |
| | | } |
| | | |
| | | /** åªæçº¸å¼ è®¾ç½®éæç»å¸å°ºå¯¸ä¸é¡µè¾¹è·åè线ï¼ä¸å¨ç»å¸éçå·²æå
容 */ |
| | | function applyPageSize(page?: QcReportVersionApi.SchemaPage) { |
| | | canvasSize = resolveCanvasSize(page); |
| | | setPageDevice(canvasSize.width, canvasSize.height); |
| | | applyFrameGuide(); |
| | | } |
| | | |
| | | /** æ Schema ççº¸å¼ è®¾ç½®éæç»å¸ï¼å¹¶è½½å
¥ GrapesJS åå§æ°æ® */ |
| | | function loadSchema(schema?: QcReportVersionApi.ReportTemplateSchema) { |
| | | const instance = editor.value; |
| | | if (!instance) { |
| | | return; |
| | | } |
| | | // æ·ä¸å±åæ¾è¿å»ï¼æ½å±éæ¹çæ¯è¿ä»½å¯æ¬ï¼åçæ¬æ¶ä¸è¯¥ææ¹å¨å¸¦å°å¦ä¸ä¸ªçæ¬ä¸ã |
| | | // åæ¶æå¼æç缺çå£å¾è½ææç¡®å¼ï¼scope è¯å«ä¸åºæé项ãenabled 缺çå³å¯ç¨ï¼ï¼ |
| | | // å¦åçé¢ä¸çå¼å
³ä¼ä¸å®é
å¤å®å¯¹ä¸ä¸ï¼çèµ·æ¥åç¨äºå
¶å®è¿å¨å¤ã |
| | | rules.value = (schema?.rules ?? []).map((rule) => ({ |
| | | ...rule, |
| | | scope: rule.scope === 'report' ? 'report' : 'item', |
| | | enabled: rule.enabled !== false, |
| | | })); |
| | | applyPageSize(schema?.page); |
| | | |
| | | // grapes å¿
须带 pages ææ¯å¯ç¨é¡¹ç®æ°æ®ï¼å¦åç»å¸ä¼å è½½åºä¸ä¸ªæ²¡æé¡µé¢ç空ç¼è¾å¨ |
| | | const projectData = schema?.grapes; |
| | | const pages = projectData?.pages; |
| | | if (Array.isArray(pages) && pages.length > 0) { |
| | | instance.loadProjectData(projectData as Parameters<Editor['loadProjectData']>[0]); |
| | | } else { |
| | | instance.loadProjectData({ pages: [{ component: { style: { padding: '0' } } }] }); |
| | | } |
| | | applyFrameGuide(); |
| | | // å¿
须卿¸
空åå²ä¹åè¡¥å±æ§å段ï¼trait åå§åä¼å䏿¬¡ç»ä»¶å±æ§ï¼çä¸çå岿²¡ææä¹ |
| | | restoreQualityTraits(instance); |
| | | instance.UndoManager.clear(); |
| | | syncHistoryState(); |
| | | } |
| | | |
| | | /** |
| | | * ä»ç»å¸æ¶é Schemaï¼grapes ä¿ååå§æ°æ®ï¼components ä¿åè´¨éç»ä»¶ä¸å¡è¯ä¹ã |
| | | * <p> |
| | | * 产åºçæ¯<b>æ´ä»½</b> Schemaï¼å端ä¿åæ¶æ´ä½è¦çï¼æä»¥è®¾è®¡å¨è®¤è¯çåæ®µå¿
é¡»å¨è¿éåå
¨ââ |
| | | * æ¼æåªä¸ªå°±çäºæé£ä¸ªå段æ¸
空ï¼rules æ¾ç»å°±è¿æ ·è¢«æ¸
æè¿ï¼è§ RuleEditor ç说æï¼ã |
| | | * å°æ¥ Schema æ°å¢å段æ¶ï¼è¦ä¹å¨è¿é产åºï¼è¦ä¹æ³æ¸
æ¥ç±è°è´è´£ä¿çã |
| | | */ |
| | | function collectSchema( |
| | | page: QcReportVersionApi.SchemaPage, |
| | | ): QcReportVersionApi.ReportTemplateSchema { |
| | | const instance = editor.value; |
| | | if (!instance) { |
| | | return { schemaVersion: SCHEMA_VERSION, page, rules: rules.value }; |
| | | } |
| | | const projectData = instance.getProjectData() as unknown as Record<string, unknown>; |
| | | const components: QcReportVersionApi.QualityComponentNode[] = []; |
| | | // getWrapper å¨ç±»å䏿¯å¯ç©ºçï¼åä¸å°å°±åªæ¶éä¸å°ä¸å¡è¯ä¹ï¼ä¸è½å æ¤ä¸¢æ grapes ç»å¸æ°æ® |
| | | instance |
| | | .getWrapper() |
| | | ?.find(`[${QUALITY_TYPE_ATTR}]`) |
| | | .forEach((component, index) => { |
| | | const attrs = component.getAttributes(); |
| | | components.push({ |
| | | id: component.getId(), |
| | | // ç»ä»¶å¨ææ¡£ä¸ç顺åºï¼ä¾åç»æ°æ®ç»å®ä¸æ¸²æå®ä½ |
| | | index, |
| | | qualityType: attrs[QUALITY_TYPE_ATTR], |
| | | attributes: attrs, |
| | | }); |
| | | }); |
| | | |
| | | return { |
| | | schemaVersion: SCHEMA_VERSION, |
| | | page, |
| | | grapes: projectData, |
| | | components, |
| | | rules: rules.value, |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * æ AI è稿è£
å°ç»å¸ä¸ã |
| | | * <p> |
| | | * components ä¼ ç©ºæ°ç»ï¼ç»å¸ææ¯çç¸ï¼ä¿åæ¶ç± collectSchema éæ°äº§åºè¯ä¹æ¸
åã |
| | | * å¡è稿è¿å»ä¼å¶é 第äºä¸ªçç¸æ¥æºï¼èä¸å½¢ç¶ä¹ä¸å¯¹ï¼èç¨¿æ¯ {type, props}ï¼ |
| | | * è¯ä¹å±æ¯ {id, index, qualityType, attributes}ï¼ã |
| | | * <p> |
| | | * rules ä¼ å½åå¼ï¼loadSchema ä¼ç¨ä¼ å
¥ç rules è¦çï¼ä¼ 空æ°ç»çäºæç¨æ·å·²å好çå¤å®è§åé黿¸
空ã |
| | | * <p> |
| | | * page å¿
é¡»ç±è°ç¨æ¹ç»å®ï¼è稿没çåºçº¸å¼ æ¶ä¸è½ä¼ undefinedï¼é£ä¼è®© applyPageSize éåé»è®¤ A4ï¼ |
| | | * æç¨æ·å·²ç»è°å¥½ççº¸å¼ æææ¹æã |
| | | */ |
| | | function loadCanvasFromDraft( |
| | | components: QualityDraftComponent[] | undefined, |
| | | page: QcReportVersionApi.SchemaPage, |
| | | ): { nodeCount: number; problems: string[]; skipped: QualityAssemblySkip[] } { |
| | | const assembled = assembleQualityCanvas(components); |
| | | const nodeCount = countCanvasNodes(assembled.grapes); |
| | | // ä¸ä¸ªç»ä»¶é½è£
ä¸è¿å»æ¶ä¸è¦å¨ç»å¸ï¼loadSchema 伿¸
ææ¤éæ ä¸å·²æå
å®¹ï¼ |
| | | // ç½ç½æè¿å»ä¸çç»å¸å´ä»ä¹é½æ¢ä¸æ¥ |
| | | if (nodeCount === 0) { |
| | | return { nodeCount, problems: assembled.problems, skipped: assembled.skipped }; |
| | | } |
| | | loadSchema({ |
| | | schemaVersion: SCHEMA_VERSION, |
| | | page, |
| | | grapes: assembled.grapes, |
| | | components: [], |
| | | rules: rules.value, |
| | | }); |
| | | return { nodeCount, problems: assembled.problems, skipped: assembled.skipped }; |
| | | } |
| | | |
| | | function undo() { |
| | | editor.value?.UndoManager.undo(); |
| | | } |
| | | |
| | | function redo() { |
| | | editor.value?.UndoManager.redo(); |
| | | } |
| | | |
| | | function setZoom(next: number) { |
| | | zoom.value = next; |
| | | editor.value?.Canvas.setZoom(next); |
| | | } |
| | | |
| | | function destroy() { |
| | | editor.value?.destroy(); |
| | | editor.value = undefined; |
| | | } |
| | | |
| | | return { |
| | | canRedo, |
| | | canUndo, |
| | | editor, |
| | | rules, |
| | | zoom, |
| | | selectedLabel, |
| | | applyPageSize, |
| | | collectSchema, |
| | | destroy, |
| | | init, |
| | | loadCanvasFromDraft, |
| | | loadSchema, |
| | | redo, |
| | | setZoom, |
| | | undo, |
| | | }; |
| | | } |
| | | |
| | | /** æ°ä¸ä¸è£
é
产ç©éçé¡¶å±ç»ä»¶æ°ï¼ç»æç± assembleQualityCanvas åºå®ï¼åä¸å°æ 0 ç® */ |
| | | function countCanvasNodes(grapes: Record<string, unknown>): number { |
| | | const pages = grapes.pages as |
| | | | { component?: { components?: unknown[] } }[] |
| | | | undefined; |
| | | return pages?.[0]?.component?.components?.length ?? 0; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { QcReportTemplateApi } from '#/api/mes/qc/report/template'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | copyTemplate, |
| | | deleteTemplate, |
| | | getTemplatePage, |
| | | } from '#/api/mes/qc/report/template'; |
| | | import { $t } from '#/locales'; |
| | | import { router } from '#/router'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const pageSizeOptions = getDictOptions(DICT_TYPE.QC_REPORT_PAGE_SIZE, 'string'); |
| | | const orientationOptions = getDictOptions( |
| | | DICT_TYPE.QC_REPORT_ORIENTATION, |
| | | 'string', |
| | | ); |
| | | |
| | | function getDictLabel(options: typeof pageSizeOptions, value?: string): string { |
| | | return options.find((item) => item.value === value)?.label ?? value ?? ''; |
| | | } |
| | | |
| | | function getPageSetting(row: QcReportTemplateApi.Template): string { |
| | | const size = getDictLabel(pageSizeOptions, row.pageSize); |
| | | const orientation = getDictLabel(orientationOptions, row.orientation); |
| | | return [size, orientation].filter(Boolean).join(' / '); |
| | | } |
| | | |
| | | /** å·æ°è¡¨æ ¼ */ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** å建æ¥åæ¨¡æ¿ */ |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | /** ç¼è¾æ¥åæ¨¡æ¿ */ |
| | | function handleEdit(row: QcReportTemplateApi.Template) { |
| | | formModalApi.setData(row).open(); |
| | | } |
| | | |
| | | /** è¿å
¥æ¨¡æ¿è®¾è®¡å¨ */ |
| | | function handleDesign(row: QcReportTemplateApi.Template) { |
| | | router.push({ |
| | | name: 'QcReportTemplateDesigner', |
| | | query: { id: row.id }, |
| | | }); |
| | | } |
| | | |
| | | /** å¤å¶æ¥åæ¨¡æ¿ */ |
| | | async function handleCopy(row: QcReportTemplateApi.Template) { |
| | | const hideLoading = message.loading({ |
| | | content: `æ£å¨å¤å¶ã${row.templateName}ã...`, |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await copyTemplate(row.id!); |
| | | message.success(`ã${row.templateName}ãå¤å¶æå`); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | /** å 餿¥åæ¨¡æ¿ */ |
| | | async function handleDelete(row: QcReportTemplateApi.Template) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting', [row.templateName]), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deleteTemplate(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.templateName])); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getTemplatePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<QcReportTemplateApi.Template>, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | |
| | | <Grid table-title="æ¥å模æ¿å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['æ¥å模æ¿']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['qc-report:template:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #pageSetting="{ row }"> |
| | | {{ getPageSetting(row) }} |
| | | </template> |
| | | <template #currentVersion="{ row }"> |
| | | <Tag v-if="row.currentVersion" color="green"> |
| | | {{ row.currentVersion }} |
| | | </Tag> |
| | | <span v-else class="text-gray-400">æªåå¸</span> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: '设计', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['qc-report:template:update'], |
| | | onClick: handleDesign.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['qc-report:template:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å¤å¶', |
| | | type: 'link', |
| | | icon: ACTION_ICON.COPY, |
| | | auth: ['qc-report:template:create'], |
| | | onClick: handleCopy.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['qc-report:template:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.templateName]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { QcReportTemplateApi } from '#/api/mes/qc/report/template'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { |
| | | createTemplate, |
| | | getTemplate, |
| | | updateTemplate, |
| | | } from '#/api/mes/qc/report/template'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<QcReportTemplateApi.Template>(); |
| | | const getTitle = computed(() => { |
| | | return formData.value?.id |
| | | ? $t('ui.actionTitle.edit', ['æ¥å模æ¿']) |
| | | : $t('ui.actionTitle.create', ['æ¥å模æ¿']); |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 100, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = (await formApi.getValues()) as QcReportTemplateApi.Template; |
| | | try { |
| | | await (formData.value?.id ? updateTemplate(data) : createTemplate(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | formApi.setState({ schema: useFormSchema() }); |
| | | const data = modalApi.getData<QcReportTemplateApi.Template>(); |
| | | if (!data || !data.id) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getTemplate(data.id); |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-1/2"> |
| | | <Form class="mx-4" /> |
| | | </Modal> |
| | | </template> |
| | |
| | | import { deleteRqc, exportRqc, getRqcPage } from '#/api/mes/qc/rqc'; |
| | | import { $t } from '#/locales'; |
| | | import QcExportRangeModal from '#/views/mes/qc/components/export-range-modal.vue'; |
| | | import QcReportGenerateModal from '#/views/mes/qc/components/report-generate-modal.vue'; |
| | | |
| | | import { useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | /** æ¬é¡µå¯¹åº mes_qc_type çå¼ï¼4 éè´§æ£éª */ |
| | | const QC_TYPE = 4; |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | |
| | | |
| | | const [ExportRangeModal, exportRangeModalApi] = useVbenModal({ |
| | | connectedComponent: QcExportRangeModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ReportModal, reportModalApi] = useVbenModal({ |
| | | connectedComponent: QcReportGenerateModal, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | |
| | | .open(); |
| | | } |
| | | |
| | | /** åºæ¥åï¼æ°æ®ç±å端ä»è¯¥è´¨æ£ååï¼å端åªéæ¨¡æ¿ */ |
| | | function handleReport(row: MesQcRqcApi.Rqc) { |
| | | reportModalApi |
| | | .setData({ qcType: QC_TYPE, qcId: row.id, qcNo: row.code }) |
| | | .open(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ExportRangeModal /> |
| | | <ReportModal /> |
| | | <Grid table-title="éè´§æ£éªåå表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'åºæ¥å', |
| | | type: 'link', |
| | | icon: ACTION_ICON.AUDIT, |
| | | auth: ['qc-report:instance:generate'], |
| | | ifShow: row.status === MesQcStatusEnum.FINISHED, |
| | | onClick: handleReport.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | |
| | | proxy: { |
| | | "/admin-api": { |
| | | changeOrigin: true, |
| | | target: "http://192.168.0.10:48080", |
| | | target: "http://localhost:48080", |
| | | ws: true, |
| | | }, |
| | | }, |