From 35722562e9e13f0504acc15b740d042ecb810199 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期日, 20 九月 2026 09:06:11 +0800
Subject: [PATCH] feat(mes): 添加质检指标条目类型支持并实现报告生成功能

---
 src/components/quality/header/sample-info.ts                          |  140 
 src/components/quality/core/trait-binding.ts                          |  161 
 .qc-conformance/verify-grouped-table.ts                               |  391 ++
 src/components/quality/base/text.ts                                   |   48 
 src/components/quality/core/icons.ts                                  |   37 
 src/views/mes/qc/report/template/designer/modules/ai-import-modal.vue |  341 +
 src/components/quality/header/report-header.ts                        |   80 
 src/views/mes/qc/report/template/designer/use-designer.ts             |  363 ++
 src/views/mes/qc/report/template/data.ts                              |  229 +
 src/api/mes/qc/report/ai/index.ts                                     |   68 
 src/components/quality/core/canvas-sync.ts                            |   64 
 pnpm-lock.yaml                                                        |  104 
 src/components/quality/index.ts                                       |   97 
 src/components/quality/header/report-footer.ts                        |   41 
 src/components/quality/engine/report-evaluator.ts                     |  277 +
 src/components/quality/inspection/inspection-item.ts                  |   83 
 src/components/quality/base/table.ts                                  |   72 
 src/views/mes/qc/report/template/designer/constants.ts                |   54 
 src/components/quality/core/lookup.ts                                 |   18 
 src/components/quality/base/divider.ts                                |   28 
 src/api/mes/qc/report/template/index.ts                               |   71 
 src/views/mes/qc/report/template/modules/form.vue                     |   83 
 .qc-conformance/render-ts.ts                                          |  111 
 src/components/quality/base/spacer.ts                                 |   20 
 src/components/quality/engine/context.ts                              |  169 
 src/views/mes/qc/report/instance/index.vue                            |  147 
 .qc-conformance/verify-columns.ts                                     |  437 ++
 CHDL.bat                                                              |  451 ++
 src/components/quality/core/factory.ts                                |  129 
 src/components/quality/result/report-result.ts                        |   69 
 src/views/mes/qc/indicator/index.vue                                  |   18 
 src/views/mes/qc/report/instance/data.ts                              |  165 
 src/views/mes/qc/report/template/designer/index.vue                   |  637 +++
 src/components/quality/engine/rule-engine.ts                          |  711 ++++
 KYZN.bat                                                              |  451 ++
 src/views/mes/qc/report/instance/modules/detail.vue                   |  197 +
 .qc-conformance/verify-undecidable.ts                                 |  106 
 src/packages/constants/src/dict-enum.ts                               |   11 
 src/components/quality/core/serializer.ts                             |   56 
 pnpm-workspace.yaml                                                   |    1 
 src/components/quality/core/validator.ts                              |   94 
 src/views/mes/qc/iqc/index.vue                                        |   25 
 .qc-conformance/group-table-render.html                               |   17 
 src/api/mes/qc/report/instance/index.ts                               |  117 
 .qc-conformance/group-probe-p5.html                                   |   17 
 src/components/quality/core/catalog.ts                                |   90 
 src/components/quality/core/column-spec.ts                            |  137 
 src/components/quality/engine/render.ts                               |  333 +
 src/views/mes/qc/indicator/data.ts                                    |  112 
 src/views/mes/qc/pendinginspect/index.vue                             |    4 
 .qc-conformance/assemble-ts.ts                                        |  337 +
 vite.config.ts                                                        |    2 
 src/views/mes/qc/components/report-generate-modal.vue                 |  251 +
 src/components/quality/engine/binding.ts                              |   60 
 src/views/mes/qc/rqc/index.vue                                        |   25 
 src/components/quality/engine/page.ts                                 |   50 
 .qc-conformance/group-table-degrade.html                              |   17 
 src/components/quality/base/image.ts                                  |   48 
 .qc-conformance/verify-group-repeat.ts                                |  265 +
 .qc-conformance/grouped-table-render.png                              |    0 
 src/api/mes/qc/indicator/index.ts                                     |   12 
 src/api/mes/qc/report/version/index.ts                                |  141 
 src/views/mes/qc/report/template/designer/modules/rule-editor.vue     |  156 
 src/components/quality/core/assembler.ts                              |  134 
 src/components/quality/engine/path.ts                                 |  105 
 src/packages/constants/src/biz-mes-enum.ts                            |    6 
 .qc-conformance/dump-catalog.ts                                       |   26 
 src/components/quality/base/heading.ts                                |   73 
 src/components/quality/core/registry.ts                               |   60 
 src/components/quality/core/html.ts                                   |   30 
 src/views/mes/qc/ipqc/index.vue                                       |   25 
 src/views/mes/qc/oqc/index.vue                                        |   25 
 src/components/quality/core/attrs.ts                                  |   47 
 .qc-conformance/group-probe-p4.html                                   |   17 
 src/components/quality/inspection/grouped-quality-table.ts            |  324 +
 src/views/mes/qc/report/template/index.vue                            |  195 +
 package.json                                                          |    1 
 src/components/quality/inspection/quality-table.ts                    |  139 
 src/components/quality/core/types.ts                                  |  133 
 src/router/routes/modules/qcreport.ts                                 |   28 
 80 files changed, 10,400 insertions(+), 14 deletions(-)

diff --git a/.qc-conformance/assemble-ts.ts b/.qc-conformance/assemble-ts.ts
new file mode 100644
index 0000000..35d8491
--- /dev/null
+++ b/.qc-conformance/assemble-ts.ts
@@ -0,0 +1,337 @@
+/**
+ * 瀵规媿鑴氭湰锛欰I 鑽夌 鈫� 鐢诲竷瑁呴厤鍣紙assembleQualityCanvas锛夌殑寰�杩斾竴鑷存�с��
+ *
+ * 鏍稿績鏂█鏄�岃閰嶅櫒涓� collectSchema 浜掍负閫嗚繍绠椼�嶏細
+ * 瑁呴厤鍐欒繘鐢诲竷鐨勫睘鎬э紝缁� attributesToProps 杩樺師鍚庯紝蹇呴』涓� resolveQualityProps 鐨勭粨鏋滃畬鍏ㄧ浉绛夈��
+ * 涓嶇浉绛夊氨鎰忓懗鐫�銆孉I 鐢熸垚 鈫� 淇濆瓨 鈫� 鍐嶆墦寮�銆嶄細鎺夊睘鎬ф垨鏀瑰彉绫诲瀷銆�
+ *
+ * 鐢ㄦ硶锛歯px 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}` : ''}`);
+}
+
+/** 鍙瘮杈冨0鏄庤繃鐨勫瓧娈碉紱undefined 瑙嗗悓缂哄腑锛坧ropsToAttributes 浼氳烦杩囧畠锛� */
+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') {
+      // 鍒楀畾涔夋槸鏈夎娉曠殑鏂囨湰灞炴�э紙鍒楁爣棰�=缁戝畾琛ㄨ揪寮忥級锛氬 `鍊糔-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 瀵煎叆璐ㄩ噺鐨勫湴鍩猴細灏戝啓涓�鏉★紝妯″瀷灏卞湪閭d釜缁勪欢涓婇��鍥炵瀻鐚�
+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('&lt;br&gt;'),
+    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);
diff --git a/.qc-conformance/dump-catalog.ts b/.qc-conformance/dump-catalog.ts
new file mode 100644
index 0000000..81d447a
--- /dev/null
+++ b/.qc-conformance/dump-catalog.ts
@@ -0,0 +1,26 @@
+/**
+ * 涓存椂鑴氭湰锛氭妸鐪熷疄鐨勭Н鏈ㄦ竻鍗曪紙鍚� aiHint锛夊鍑烘垚 JSON 鏂囦欢銆�
+ *
+ * 瀛樺湪鐨勭悊鐢辨槸銆岃仈璋冩椂鍙戠粰鍚庣鐨勬竻鍗曞繀椤绘槸鐪熻揣銆嶏細鎵嬫暡涓�浠芥竻鍗曞幓娴嬶紝娴嬬殑鏄垜鏁茬殑閭d唤锛�
+ * 鑰屼笉鏄墠绔湡姝d細鍙戝嚭鍘荤殑閭d唤鈥斺�攈int 鏈夋病鏈夌湡鐨勮 catalog 甯﹀嚭鏉ワ紝鍙湁杩欐潯璺緞鑳借瘉鏄庛��
+ * 鐢ㄦ硶锛歯px 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('鐢ㄦ硶锛歯px 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}`);
diff --git a/.qc-conformance/group-probe-p4.html b/.qc-conformance/group-probe-p4.html
new file mode 100644
index 0000000..6596851
--- /dev/null
+++ b/.qc-conformance/group-probe-p4.html
@@ -0,0 +1,17 @@
+<!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">姝d竵閱囧惈閲�</td><td rowspan="2">绾害 = 100% - 姘村垎 - 閰稿惈閲� - 鍏跺畠鏉傝川</td></tr><tbody><tr><td>姝d竵閱囧惈閲� /%</td><td>鈮�99.5</td><td>99.7</td><td>%</td><td></td><td></td></tr></tbody></tbody></tbody></table></div></body>
+</html>
\ No newline at end of file
diff --git a/.qc-conformance/group-probe-p5.html b/.qc-conformance/group-probe-p5.html
new file mode 100644
index 0000000..7112686
--- /dev/null
+++ b/.qc-conformance/group-probe-p5.html
@@ -0,0 +1,17 @@
+<!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">姝d竵閱囧惈閲�</td><td rowspan="1">绾害 = 100% - 姘村垎 - 閰稿惈閲� - 鍏跺畠鏉傝川</td><td>姝d竵閱囧惈閲� /%</td><td>鈮�99.5</td><td>99.7</td><td>%</td><td>鏃犲垽瀹氳鍒�</td></tr></tbody></table></div></body>
+</html>
\ No newline at end of file
diff --git a/.qc-conformance/group-table-degrade.html b/.qc-conformance/group-table-degrade.html
new file mode 100644
index 0000000..6a33ecf
--- /dev/null
+++ b/.qc-conformance/group-table-degrade.html
@@ -0,0 +1,17 @@
+<!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>
\ No newline at end of file
diff --git a/.qc-conformance/group-table-render.html b/.qc-conformance/group-table-render.html
new file mode 100644
index 0000000..6276c77
--- /dev/null
+++ b/.qc-conformance/group-table-render.html
@@ -0,0 +1,17 @@
+<!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>
\ No newline at end of file
diff --git a/.qc-conformance/grouped-table-render.png b/.qc-conformance/grouped-table-render.png
new file mode 100644
index 0000000..02a7270
--- /dev/null
+++ b/.qc-conformance/grouped-table-render.png
Binary files differ
diff --git a/.qc-conformance/render-ts.ts b/.qc-conformance/render-ts.ts
new file mode 100644
index 0000000..6e334d8
--- /dev/null
+++ b/.qc-conformance/render-ts.ts
@@ -0,0 +1,111 @@
+/**
+ * 涓存椂瀵规媿鑴氭湰锛氱敤鍓嶇娓叉煋/瑙勫垯寮曟搸璺戝悓涓�浠� fixture锛屼骇鍑烘湡鏈涗骇鐗╋紝
+ * 渚涘悗绔� Java 寮曟搸鐨勪竴鑷存�ф祴璇曢�愬瓧姣斿銆傜敤瀹屽嵆鍒狅紝涓嶅叆搴撱��
+ *
+ * 鐢ㄦ硶锛歯px 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(', ')}`);
\ No newline at end of file
diff --git a/.qc-conformance/verify-columns.ts b/.qc-conformance/verify-columns.ts
new file mode 100644
index 0000000..3fd71ee
--- /dev/null
+++ b/.qc-conformance/verify-columns.ts
@@ -0,0 +1,437 @@
+/**
+ * 鎺㈤拡锛氬彲鑷畾鍒楃殑妫�楠岃〃锛圙roupedQualityTable.columns / headerSpans锛変笌鍙厤瀛楁鐨勬牱鍝佷俊鎭�
+ * 锛圫ampleInfo.fields锛夈��
+ *
+ * 鍙锛屼笉鏀逛唬鐮併�佷笉纰版暟鎹簱銆傜敤娉曪細npx tsx .qc-conformance/verify-columns.ts
+ *
+ * 瀹堜笁浠朵簨锛�
+ * 1. **灞炴�х己甯椂浜х墿閫愬瓧涓嶅彉**鈥斺�旇繖鏄湰杞殑绗竴浼樺厛绾э細瀛橀噺鍜屽凡鍙戝竷妯℃澘涓�涓瓧鑺傞兘涓嶈鍔ㄣ��
+ *    杩欓噷姣斿鐨勬槸鏁存浜х墿瀛楃涓茬殑 sha256锛岃�屼笉鏄�屽垪鏁拌繕鏄� 7銆嶈繖绉嶅急鏂█锛�
+ *    鍒楁暟涓嶅彉浣� style 椤哄簭鍙樹簡銆佸睘鎬ф惉瀹朵簡锛屼竴鏍蜂細璁╁凡鍙戝竷鎶ュ憡閲嶅嚭鏃跺彉鏍枫��
+ * 2. 灞炴�у湪鍦烘椂鍒楁暟銆佸垪搴忋�佸悎骞跺睘鎬ц惤鐐广�佷袱绾ц〃澶磋法鍒楅兘瀵瑰緱涓婏紱
+ * 3. 鍐欓敊鏃� validate 鎶ュ緱鍑恒�佷笖鎶ョ殑鏄兘鏀圭殑閭d竴澶勶紙涓嶆槸绗肩粺涓�鍙ャ�屾牸寮忛敊璇�嶏級銆�
+ *
+ * 鐩存帴鎵撳湪鐪熷疄 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. 缂哄腑 = 閫愬瓧涓嶅彉锛堢涓�浼樺厛绾э級
+
+// 鏀硅繖杞箣鍓嶇殑浜х墿鎸囩汗銆傚垪鏁般�乻tyle 椤哄簭銆佸悎骞跺睘鎬ц惤鐐癸紝浠讳綍涓�澶勫姩浜嗚繖涓� 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锛坉efaults 閲屽埢鎰忎笉鏀� 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('鏍峰搧淇℃伅锛歞efaults 閲屾病鏈� 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('鏍峰搧淇℃伅锛歠ields 浼犵┖涓蹭粛鍑洪粯璁� 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 鍒楃畻锛岄潬鎬绘暟鍏滃簳锛坧arser 鐨勬棦鏈夌害瀹氾級
+  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 姝f枃瀹藉彧鏈� 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 鎶ョ殑鏄兘鏀圭殑閭d竴澶� =====');
+{
+  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));
+
+  // 鍒楀畾涔夋湁閿欐椂涓嶅啀鍙犲姞璺ㄥ垪鏁版牎楠岋細閭f椂鍒楁暟涓嶅彲淇★紝鎶ヤ簡鍙細鎶婁汉寰�閿欑殑鏂瑰悜甯�
+  const both = validate(groupedQualityTableDefinition, {
+    columns: '搴忓彿={{index}}|妫�楠岄」鐩�',
+    headerSpans: '妫�楠岀粨鏋淾5',
+  });
+  expect('鍒楀畾涔夐敊 + 琛ㄥご璺ㄥ垪涔熼敊 鈬� 鍙姤鍒楀畾涔�', both.length, 1);
+  expectTrue('鎶ョ殑鏄垪瀹氫箟閭f潯', 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 灏变笉鍐嶆姤閭f潯璇姤', 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,
+  );
+
+  // 鏁磋鍑戞弧鏃朵竴鏍奸兘涓嶈琛ワ細榛樿瀛楁姝f槸 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('&quot;&gt;&lt;script&gt;alert(1)&lt;/script&gt;'),
+    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('&quot;&gt;&lt;script&gt;'), sample.slice(0, 400));
+  expectTrue('娌℃湁閫冨嚭灞炴�у鐨� script', !sample.includes('<script>'), sample.slice(0, 400));
+}
+
+// ------------------------------------------------- 7. 瑙f瀽鍣ㄨ竟鐣岋紙涓や釜缁勪欢鍏辩敤鐨勯偅涓�浠斤級
+
+// eslint-disable-next-line no-console
+console.log('\n===== 7. 鍏辩敤瑙f瀽鍣細绌烘璺宠繃銆�= 鍙垏绗竴涓�乛 浠庢渶鍚庡垏 =====');
+{
+  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;
diff --git a/.qc-conformance/verify-group-repeat.ts b/.qc-conformance/verify-group-repeat.ts
new file mode 100644
index 0000000..d936426
--- /dev/null
+++ b/.qc-conformance/verify-group-repeat.ts
@@ -0,0 +1,265 @@
+/**
+ * 鎺㈤拡锛氬垎缁勬楠岄」锛堢埗椤� + 瀛愰」锛夊湪鐜版湁娓叉煋寮曟搸涓嬭兘琛ㄨ揪鎴愪粈涔堟牱銆�
+ *
+ * 鐩殑鍙湁涓�涓細鎶娿�宺owspan 鍚堝苟鍗曞厓鏍艰涓嶈鍔ㄥ紩鎿庛�嶈繖浠朵簨浠庢帹鏂彉鎴愬疄娴嬨��
+ * 鍙锛屼笉鏀逛唬鐮併�佷笉纰版暟鎹簱銆�
+ *
+ * 鍏抽敭鍓嶆彁锛堣 report-evaluator.ts:237 寰楀埌锛夛細鍒ゅ畾闃舵浼氭妸涓婁笅鏂囬噸寤烘垚
+ * `{ report, inspectionItems }` 涓や釜閿紝浠讳綍鑷畾涔夌殑椤跺眰鏁扮粍閮借繘涓嶄簡娓叉煋浣滅敤鍩熴��
+ * 鎵�浠ユ湰鎺㈤拡鍚屾椂楠岃瘉銆岄《灞傛暟缁勪笉鍙敤銆嶄笌銆屾寕鍦ㄦ瘡涓楠岄」涓婄殑瀛楁鍙敤銆嶄袱鏉°��
+ *
+ * 鐢ㄦ硶锛歯px 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: '姝d竵閱囧惈閲�', standardValue: '绾害 = 100% - 姘村垎 - 閰稿惈閲� - 鍏跺畠鏉傝川', actualValue: '', unit: '', groupSpan: 2 },
+  { itemName: '姝d竵閱囧惈閲� /%', 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 鎵佸钩锛歳epeat over inspectionItems锛岀粍澶翠俊鎭寕鍦ㄨ涓�
+
+const p1 = run(
+  'P1 鎵佸钩锛歳epeat 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 椤跺眰鑷畾涔夋暟缁勶紙棰勬湡涓虹┖锛岃瘉鏄庝笂涓嬫枃琚敹鍙o級
+
+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: '姝d竵閱囧惈閲�',
+    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: '姝d竵閱囧惈閲� /%', standardValue: '鈮�99.5', actualValue: '99.7', unit: '%', group: { name: '姝d竵閱囧惈閲�', 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=')}`,
+  );
+});
diff --git a/.qc-conformance/verify-grouped-table.ts b/.qc-conformance/verify-grouped-table.ts
new file mode 100644
index 0000000..0941bc4
--- /dev/null
+++ b/.qc-conformance/verify-grouped-table.ts
@@ -0,0 +1,391 @@
+/**
+ * 鎺㈤拡锛氬垎缁勬楠岄」琛紙GroupedQualityTable锛夌殑鐪熷疄娓叉煋浜х墿楠岃瘉銆�
+ *
+ * 鍙锛屼笉鏀逛唬鐮併�佷笉纰版暟鎹簱銆傜敤娉曪細npx tsx .qc-conformance/verify-grouped-table.ts
+ *
+ * 瀹堜笁浠朵簨锛�
+ * 1. 缁勫悕鏍笺�屽悎骞跺嚑琛屻�嶄笌銆屽瓙椤硅璁╀綅銆嶉兘鐢辨暟鎹笅鍙戯紙rowspan / hidden锛夆�斺��
+ *    娓叉煋鏈熺畻涓嶅嚭鏉ワ細璺緞鍦ㄦ暟缁勪笂缁х画鍙栧睘鎬т細鎸夊厓绱� pluck锛宍group.children.length` 鍙栦笉鍒板�硷紱
+ * 2. 璁╀綅鍚庣殑琛屼粛瑕佸崰婊� 7 鍒椼�佷笉涓蹭綅銆傝繖鏄�岄殣钘忎竴鏍间細涓嶄細鎶婃暣琛屽墿浣欐牸瀛愬乏绉讳竴鍒椼�嶇殑鐪熼棶棰橈紝
+ *    HTML 缁撴瀯灞傞潰鍙兘楠屽埌銆宼d 鏁般�嶏紝鐪熸鐨勮〃鏍煎竷灞�鐢辨祻瑙堝櫒瀹氾紝鎵�浠ヨ剼鏈妸娓叉煋浜х墿鍐欏埌
+ *    .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>锛孏rapesJS 瑙f瀽鎴愬甫 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 浜х墿閫愭牸鍚屾瀯鐨勮妯℃澘锛圙rapesJS 鎶婇偅娈� HTML 瑙f瀽鎴愮殑鏍戝氨鏄繖涓舰鐘讹級 */
+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: '' },
+});
+
+/** 缁勫唴瀛愰」琛岋細鑷繁鐨勫悕瀛楄繘銆屽瓙椤广�嶅垪锛屽悎骞舵牸璁╃粰缁勫ご閭d竴琛� */
+const child = (name: string, actual: string, unit: string): Item => ({
+  itemName: name,
+  requirement: '',
+  actualValue: actual,
+  unit,
+  group: { childName: name, span: 1, hidden: 'hidden' },
+});
+
+/** 缁勫唴瀛愰」閲岀湡姝e甫瑙勬牸銆佽兘鍒ゅ畾鐨勪竴鏉� */
+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 瀵煎叆璧拌嚜瀹氬垪锛坈olumns 闈炵┖锛夛細椤圭洰鍒椾粛瑕佹寜缁勭旱鍚戝悎骞� =====');
+{
+  // 绗� 1~3 鑺傛墦鐨勬槸**榛樿鍒�**鍒嗘敮锛坱ableSchema 鎵嬪伐鍚屾瀯锛夛紝鑰� AI 瀵煎叆涓�寰嬪~ columns锛�
+  // 璧扮殑鏄� effectiveColumns 鐨勫彟涓�鏉″垎鏀�斺�斾袱鏉″垎鏀悇鑷嫾 row锛屽悎骞跺睘鎬ф湁娌℃湁璺熺潃璧帮紝
+  // 榛樿鍒楃豢涓嶄唬琛ㄨ嚜瀹氬垪缁裤�傝繖閲岀敤妯″瀷鐪熷疄浜у嚭锛堟ā鏉� 33 閭d唤鑽夌鐨勬渶鍚庝竴杞級閫愬瓧澶嶇幇銆�
+  //
+  // 鍙獙鍒扮粨鏋勪负姝細瑁呴厤浜х墿浜ょ粰 GrapesJS 瑙f瀽鍓嶅悗鐨勫睘鎬ф槸涓�瀛椾笉宸殑锛岃�屻�宻pan=N + hidden=''
+  // 浼氭覆鎴愬彲瑙佺殑 rowspan=N銆乻pan=1 + hidden='hidden' 浼氳浣嶃�嶈繖灞傝涔夌敱绗� 2 鑺傝礋璐b�斺��
+  // 閭e灞炴�у绾︿笌瀹冭惤鍦ㄥ摢涓�鍒楁棤鍏筹紝涓嶅繀鍦ㄨ嚜瀹氬垪涓婇噸璺戜竴閬嶆覆鏌撱��
+  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('涓婂眰銆屾楠岀粨鏋溿�峜olspan=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);
+
+  // 鍘绘帀 # 灏辫涓嶅悎骞� 鈥斺�� 椤圭洰鍒楁瘡琛岄兘閲嶅鎵撲竴閬嶇粍鍚嶏紝姝f槸銆屾湁鐐逛笐銆嶇殑閭g鐗堝紡銆�
+  // 鏂█瀹冪‘瀹炲彉浜嗭紝闃叉 # 琚綋鎴愯楗般�佹湁鏃犻兘涓嶅奖鍝嶇粨鏋�
+  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;
diff --git a/.qc-conformance/verify-undecidable.ts b/.qc-conformance/verify-undecidable.ts
new file mode 100644
index 0000000..4adb53f
--- /dev/null
+++ b/.qc-conformance/verify-undecidable.ts
@@ -0,0 +1,106 @@
+/**
+ * 鎺㈤拡锛氱‘璁ゅ墠绔垽瀹氬櫒鎶娿�屾棤鍒ゅ畾瑙勫垯銆嶅拰銆屽緟鍒ゅ畾銆嶅垎寮�锛屼笖鍓嶈�呬笉璁″叆鍚堟牸鐜囦笌鎶ュ憡缁撹銆�
+ *
+ * 鑳屾櫙锛氭姤鍛婇噷甯告湁銆岃瘯鏍疯川閲� 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;
diff --git a/CHDL.bat b/CHDL.bat
new file mode 100644
index 0000000..f83060d
--- /dev/null
+++ b/CHDL.bat
@@ -0,0 +1,451 @@
+@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
\ No newline at end of file
diff --git a/KYZN.bat b/KYZN.bat
new file mode 100644
index 0000000..c47cde3
--- /dev/null
+++ b/KYZN.bat
@@ -0,0 +1,451 @@
+@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
\ No newline at end of file
diff --git a/package.json b/package.json
index f8cce41..a6a2e45 100644
--- a/package.json
+++ b/package.json
@@ -61,6 +61,7 @@
     "dhtmlx-gantt": "catalog:",
     "diagram-js": "catalog:",
     "fast-xml-parser": "catalog:",
+    "grapesjs": "catalog:",
     "highlight.js": "catalog:",
     "livekit-client": "^2.20.0",
     "pinia": "catalog:",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 81f3c62..fa4be39 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -312,6 +312,9 @@
     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
@@ -644,6 +647,9 @@
       fast-xml-parser:
         specifier: 'catalog:'
         version: 4.5.6
+      grapesjs:
+        specifier: 'catalog:'
+        version: 0.23.6
       highlight.js:
         specifier: 'catalog:'
         version: 11.11.1
@@ -3150,6 +3156,9 @@
   '@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==}
 
@@ -4413,6 +4422,9 @@
   '@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==}
 
@@ -4451,6 +4463,9 @@
 
   '@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==}
@@ -4511,6 +4526,9 @@
 
   '@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==}
@@ -5093,6 +5111,13 @@
     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==}
 
@@ -5379,6 +5404,12 @@
   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==}
@@ -6614,6 +6645,9 @@
   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}
@@ -6689,6 +6723,9 @@
 
   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==}
@@ -8059,6 +8096,9 @@
     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==}
 
@@ -8740,8 +8780,8 @@
     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:
@@ -9204,6 +9244,9 @@
 
   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==}
@@ -9880,8 +9923,8 @@
   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==}
@@ -11478,6 +11521,8 @@
 
   '@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
@@ -12462,7 +12507,7 @@
       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:
@@ -12472,6 +12517,11 @@
   '@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:
@@ -12511,6 +12561,8 @@
   '@types/estree@1.0.9': {}
 
   '@types/html-minifier-terser@7.0.2': {}
+
+  '@types/jquery@4.0.1': {}
 
   '@types/jsesc@2.5.1': {}
 
@@ -12564,6 +12616,8 @@
       '@types/estree': 1.0.9
 
   '@types/trusted-types@2.0.7': {}
+
+  '@types/underscore@1.13.0': {}
 
   '@types/video.js@7.3.58': {}
 
@@ -13304,6 +13358,15 @@
     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: {}
@@ -13632,6 +13695,10 @@
   clsx@2.1.1: {}
 
   cluster-key-slot@1.1.1: {}
+
+  codemirror-formatting@1.0.0: {}
+
+  codemirror@5.63.0: {}
 
   codemirror@5.65.21: {}
 
@@ -14608,7 +14675,7 @@
 
   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
 
@@ -14987,6 +15054,17 @@
 
   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
@@ -15054,6 +15132,8 @@
       lru-cache: 7.5.1
 
   htm@3.1.1: {}
+
+  html-entities@1.4.0: {}
 
   html-minifier-terser@7.2.0:
     dependencies:
@@ -16442,6 +16522,8 @@
 
   process@0.11.10: {}
 
+  promise-polyfill@8.3.0: {}
+
   prosemirror-changeset@2.4.1:
     dependencies:
       prosemirror-transform: 1.12.0
@@ -17220,7 +17302,7 @@
       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
@@ -17455,7 +17537,7 @@
   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
@@ -17469,7 +17551,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'
 
@@ -17751,6 +17833,8 @@
       estree-walker: 3.0.3
       magic-string: 0.30.21
       unplugin: 2.3.11
+
+  underscore@1.13.8: {}
 
   undici-types@7.24.6: {}
 
@@ -18516,7 +18600,7 @@
 
   zeebe-bpmn-moddle@1.15.0: {}
 
-  zimmerframe@1.1.4: {}
+  zimmerframe@1.1.5: {}
 
   zip-stream@6.0.1:
     dependencies:
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 6105520..838e297 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -128,6 +128,7 @@
   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
diff --git a/src/api/mes/qc/indicator/index.ts b/src/api/mes/qc/indicator/index.ts
index 163f11d..f658676 100644
--- a/src/api/mes/qc/indicator/index.ts
+++ b/src/api/mes/qc/indicator/index.ts
@@ -10,6 +10,11 @@
     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; // 澶囨敞
@@ -32,6 +37,13 @@
   );
 }
 
+/** 鏌ヨ鍒嗙粍椤圭簿绠�鍒楄〃锛堛�屾墍灞炲垎缁勩�嶄笅鎷夐�夐」锛� */
+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);
diff --git a/src/api/mes/qc/report/ai/index.ts b/src/api/mes/qc/report/ai/index.ts
new file mode 100644
index 0000000..4ce64a7
--- /dev/null
+++ b/src/api/mes/qc/report/ai/index.ts
@@ -0,0 +1,68 @@
+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 瀵硅繖浠芥枃妗g殑涓�鍙ヨ瘽璇存槑 */
+    summary?: string;
+    /** 杞彁绀猴細琚烦杩囩殑椤点�佹棤娉曡〃杈剧殑鍖哄潡绛夛紝蹇呴』灞曠ず缁欑敤鎴� */
+    warnings?: string[];
+    durationMs?: number;
+  }
+
+  export interface DraftReq {
+    /**
+     * 涓婁紶鏂囦欢瀵瑰簲鐨� blobId锛�**椤哄簭鍗虫枃妗i〉搴�**銆�
+     * <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 },
+  );
+}
diff --git a/src/api/mes/qc/report/instance/index.ts b/src/api/mes/qc/report/instance/index.ts
new file mode 100644
index 0000000..9f9024b
--- /dev/null
+++ b/src/api/mes/qc/report/instance/index.ts
@@ -0,0 +1,117 @@
+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 鐩存帴娓叉煋鍗充负銆岄瑙堛�嶏紝
+   * 涓嶉渶瑕侀噸鏂板嚭浠躲�傛暟鎹揩鐓т笉鍦ㄨ繖涓帴鍙i噷锛岃鐪嬪緱鍗曠嫭璋� {@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[]; // 娓叉煋鏈熸暟鎹己鍙o紙妯℃澘缁戝畾浜嗕絾娌℃暟鎹殑瀛楁锛�
+  }
+}
+
+/** 鏌ヨ鎶ュ憡瀹炰緥鍒嗛〉 */
+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锛堟墦鍗板疄渚嬪凡瀛樼殑娓叉煋浜х墿骞跺綊妗e埌闄勪欢搴擄紝鍙噸澶嶈皟鐢級 */
+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,
+  );
+}
diff --git a/src/api/mes/qc/report/template/index.ts b/src/api/mes/qc/report/template/index.ts
new file mode 100644
index 0000000..34e1327
--- /dev/null
+++ b/src/api/mes/qc/report/template/index.ts
@@ -0,0 +1,71 @@
+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>
+ * 鍚庣宸叉寜銆屾姤鍛婄被鍨嬩竴鑷� + 妯℃澘鍚敤 + 褰撳墠鐗堟湰宸插彂甯冦�嶈繃婊ゅ畬姣曪紝
+ * 璋冪敤鏂逛笉瑕佸啀鑷繁鎺ㄥ杩欏瑙勫垯锛歝urrentVersion 闈炵┖骞朵笉浠h〃閭d釜鐗堟湰杩樿兘鐢�
+ * 鈥斺�斿仠鐢ㄤ竴涓増鏈笉浼氭竻绌烘ā鏉跨殑 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}`);
+}
\ No newline at end of file
diff --git a/src/api/mes/qc/report/version/index.ts b/src/api/mes/qc/report/version/index.ts
new file mode 100644
index 0000000..4085416
--- /dev/null
+++ b/src/api/mes/qc/report/version/index.ts
@@ -0,0 +1,141 @@
+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;
+    /** 缁勪欢鍦ㄦ枃妗d腑鐨勯『搴忥紝渚涚粦瀹氫笌瀹氫綅浣跨敤 */
+    index?: number;
+    /** 缁勪欢绫诲瀷锛屽嵆鐢诲竷鑺傜偣涓婄殑 data-quality-type */
+    qualityType?: string;
+    /** 缁勪欢鐨勪笟鍔″睘鎬э紝鍙栧�兼槸 data-qc-* / data-quality-type 杩欑被灞炴�� */
+    attributes?: Record<string, unknown>;
+  }
+
+  /** 鍒ゅ畾瑙勫垯锛歴cope 涓� item 鏃堕�愰」鍒ゅ畾锛屼负 report 鏃跺喅瀹氭暣浠芥姤鍛婄殑缁撹 */
+  export interface SchemaRule {
+    id?: string;
+    name?: string;
+    scope?: 'item' | 'report';
+    expression?: string;
+    enabled?: boolean;
+  }
+
+  /**
+   * 妯℃澘 Schema 鈥斺�� 璁捐鍣ㄤ骇鐗╋紝涔熸槸鎶ュ憡鍑轰欢鐨勫敮涓�杈撳叆
+   *
+   * 瀛楁鍒嗗伐锛歡rapes 鏄� GrapesJS 鍘熷椤圭洰鏁版嵁锛堣璁″櫒杩樺師鐢诲竷鐢紝娓叉煋寮曟搸涔熻瀹冿紝
+   * 姝f枃缁撴瀯鍦ㄨ繖閲岋級锛沜omponents 鏄川閲忕粍浠朵笟鍔¤涔夋竻鍗曪紙娓叉煋涓嶈锛夛紱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;
+  }
+
+  /** 鍒涘缓鐗堟湰鍏ュ弬锛歷ersion 鐣欑┖鐢卞悗绔寜鏈�鏂扮増鏈嚜澧� */
+  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}`);
+}
\ No newline at end of file
diff --git a/src/components/quality/base/divider.ts b/src/components/quality/base/divider.ts
new file mode 100644
index 0000000..a3b084c
--- /dev/null
+++ b/src/components/quality/base/divider.ts
@@ -0,0 +1,28 @@
+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')}`,
+    },
+  }),
+};
\ No newline at end of file
diff --git a/src/components/quality/base/heading.ts b/src/components/quality/base/heading.ts
new file mode 100644
index 0000000..4b2430f
--- /dev/null
+++ b/src/components/quality/base/heading.ts
@@ -0,0 +1,73 @@
+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: '姝f枃閲岀殑鍒嗚妭灏忔爣棰橈紙濡傘�屾楠岀粨鏋溿�嶃�岃川閲忚瘎瀹氥�嶏級銆傛枃妗f渶椤堕儴鐨勬姤鍛婃爣棰樹笉瑕佺敤鎴戯紝鐢� 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'),
+      },
+    };
+  },
+};
\ No newline at end of file
diff --git a/src/components/quality/base/image.ts b/src/components/quality/base/image.ts
new file mode 100644
index 0000000..7e008cd
--- /dev/null
+++ b/src/components/quality/base/image.ts
@@ -0,0 +1,48 @@
+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)}%`,
+    },
+  }),
+};
\ No newline at end of file
diff --git a/src/components/quality/base/spacer.ts b/src/components/quality/base/spacer.ts
new file mode 100644
index 0000000..98efd5a
--- /dev/null
+++ b/src/components/quality/base/spacer.ts
@@ -0,0 +1,20 @@
+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` },
+  }),
+};
\ No newline at end of file
diff --git a/src/components/quality/base/table.ts b/src/components/quality/base/table.ts
new file mode 100644
index 0000000..582a02b
--- /dev/null
+++ b/src/components/quality/base/table.ts
@@ -0,0 +1,72 @@
+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' })}">&nbsp;</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>`;
+}
+
+/** 瑙f瀽閫楀彿鍒嗛殧鐨勮〃澶存枃瀛� */
+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' },
+  }),
+};
\ No newline at end of file
diff --git a/src/components/quality/base/text.ts b/src/components/quality/base/text.ts
new file mode 100644
index 0000000..2930066
--- /dev/null
+++ b/src/components/quality/base/text.ts
@@ -0,0 +1,48 @@
+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'),
+    },
+  }),
+};
\ No newline at end of file
diff --git a/src/components/quality/core/assembler.ts b/src/components/quality/core/assembler.ts
new file mode 100644
index 0000000..d780407
--- /dev/null
+++ b/src/components/quality/core/assembler.ts
@@ -0,0 +1,134 @@
+/**
+ * AI 缁勪欢鑽夌 鈫� 鐢诲竷鏁版嵁锛堣涔夊眰鐨勯�嗚繍绠楋級
+ * <p>
+ * 涓� use-designer 鐨� collectSchema 鏋勬垚寰�杩旓細collectSchema 浠庣敾甯冩敹闆� {type, props}锛�
+ * 鏈ā鍧楁妸 {type, props} 瑁呭洖鐢诲竷銆備袱鑰呭繀椤婚�愰」瀵归綈锛屽惁鍒欍�孉I 鐢熸垚 鈫� 淇濆瓨 鈫� 鍐嶆墦寮�銆�
+ * 浼氭帀缁勪欢鎴栭敊浣嶅睘鎬с��
+ * <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銆俻rops 鐨� 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锛�
+ * 閮戒笉浼氳鏁存壒澶辫触鈥斺�擜I 鐨勪骇鐗╂湰鏉ュ氨涓嶅彲淇★紝鍏ㄦ湁鎴栧叏鏃犲彧浼氳鐢ㄦ埛鐧界瓑涓�娆¤瘑鍒��
+ */
+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 鍋氱殑姝f槸寰�杩旀墍闇�鐨勫洓浠朵簨锛歞efaults 鎵撳簳銆乥uildContent 鐢熸垚缁撴瀯銆�
+    // propsToAttributes 鍙緭鍑哄0鏄庤繃鐨� key 骞跺瓧绗︿覆鍖栥�佸啓鍏� data-quality-type銆�
+    const content = buildQualityContent(definition, normalized.props) as Record<string, unknown>;
+    // traits 涓嶈繘鐢诲竷鏁版嵁锛氳浇鍏ュ悗鐢� restoreQualityTraits 鎸� data-quality-type 閲嶅缓锛�
+    // 涓庛�屾墦寮�涓�浠藉凡淇濆瓨鐨勭増鏈�嶈蛋鐨勬槸鍚屼竴鏉¤矾锛坱raits 鏈潵涔熶笉鍙備笌椤圭洰鏁版嵁搴忓垪鍖栵級銆�
+    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 鏄惁澹版槑杩囷細鏈0鏄庣殑 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 };
+}
diff --git a/src/components/quality/core/attrs.ts b/src/components/quality/core/attrs.ts
new file mode 100644
index 0000000..a0fc8f1
--- /dev/null
+++ b/src/components/quality/core/attrs.ts
@@ -0,0 +1,47 @@
+/**
+ * 璐ㄩ噺缁勪欢鍦ㄧ敾甯冧笂鐨勬爣璁扮害瀹氥��
+ * <p>
+ * 鐢诲竷鑺傜偣闈犲睘鎬ц嚜鎻忚堪锛歞ata-quality-type 澹版槑瀹冩槸鍝釜缁勪欢锛�
+ * data-qc-<key> 鎵胯浇涓氬姟灞炴�у�硷紝涓よ�呬竴璧疯鐢诲竷锛坓rapes 鍘熷鏁版嵁锛夋湰韬氨鑳借繕鍘熶笟鍔¤涔夈��
+ */
+
+/** 鏍囪缁勪欢绫诲瀷鐨勫睘鎬у悕 */
+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);
+}
\ No newline at end of file
diff --git a/src/components/quality/core/canvas-sync.ts b/src/components/quality/core/canvas-sync.ts
new file mode 100644
index 0000000..95fd2f9
--- /dev/null
+++ b/src/components/quality/core/canvas-sync.ts
@@ -0,0 +1,64 @@
+/**
+ * 鐢诲竷鏂囧瓧涓庝笟鍔″睘鎬х殑鍙屽悜鍚屾
+ * <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-* 涓哄噯
+ * 锛坱rait 鍒濆鍖栦紭鍏堣缁勪欢灞炴�э紝璇讳笉鍒版墠鐢ㄩ粯璁ゅ�硷級锛屾墍浠ヤ笉浼氭妸鐢ㄦ埛鏀硅繃鐨勫�煎啿鎺夈��
+ */
+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);
+  });
+}
\ No newline at end of file
diff --git a/src/components/quality/core/catalog.ts b/src/components/quality/core/catalog.ts
new file mode 100644
index 0000000..6bcb9ca
--- /dev/null
+++ b/src/components/quality/core/catalog.ts
@@ -0,0 +1,90 @@
+/**
+ * 璐ㄩ噺缁勪欢娉ㄥ唽琛� 鈫� AI 绉湪娓呭崟
+ * <p>
+ * 娓呭崟鏄敞鍐岃〃鐨�**娲剧敓瑙嗗浘**锛氬墺鎺� icon / buildContent / validate 杩欎簺涓嶅彲搴忓垪鍖栫殑鎴愬憳锛�
+ * 鍙暀涓嬫ā鍨嬮渶瑕佺煡閬撶殑銆屾湁鍝簺缁勪欢銆佹瘡涓粍浠惰兘濉摢浜涘瓧娈点�嶃��
+ * <p>
+ * 杩欎唤娓呭崟闅忚姹備紶缁欏悗绔嫾杩涙彁绀鸿瘝锛屼絾瀹�<b>涓嶆槸</b>鍚堟硶鎬х殑鍒ゆ嵁锛氱湡姝f妸鍏崇殑鏄閰嶅櫒
+ * 瀵圭潃娲绘敞鍐岃〃鏌� 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;
+}
diff --git a/src/components/quality/core/column-spec.ts b/src/components/quality/core/column-spec.ts
new file mode 100644
index 0000000..d6a5e8c
--- /dev/null
+++ b/src/components/quality/core/column-spec.ts
@@ -0,0 +1,137 @@
+/**
+ * 缁勪欢鍒楀畾涔� / 琛ㄥご璺ㄥ垪鐨勫瓧绗︿覆瑙f瀽銆�
+ * <p>
+ * 銆屽垪銆嶈繖浠朵簨蹇呴』鑳借缂栫爜鎴�**涓�涓爣閲忓瓧绗︿覆**锛氱粍浠朵笟鍔″睘鎬х殑绫诲瀷鍙湁
+ * boolean / enum / number / string / text锛堣 {@link QualityFieldSchema}锛夛紝
+ * 鏈�缁堣繕浼氳惤鍒扮敾甯冭妭鐐圭殑 attributes 涓婏紝鑰岄偅閲屼篃鏄瓧绗︿覆鏄犲皠銆�
+ * 鐢� JSON 缂栫爜鍒楁暟缁勪細瑕佹眰妯″瀷杈撳嚭杞箟鍚庣殑宓屽 JSON锛屾槸瀹冨嚭閿欑殑閲嶇伨鍖猴紱
+ * 涓�琛� `鏍囬=缁戝畾|鏍囬=缁戝畾` 淇℃伅閲忕浉鍚岋紝妯″瀷銆佷汉閮藉ソ鍐欍��
+ * <p>
+ * 蹇呴』鍗曡锛氳繖涓蹭笢瑗挎渶缁堟槸 HTML 灞炴�у�硷紝鎹㈣鍦ㄥ睘鎬ч噷鍚堟硶浣嗗鏄撹
+ * 缂栬緫鍣�/搴忓垪鍖栫幆鑺傛敼鍐欙紝缁熶竴鎸夊崟琛屽鐞嗐��
+ * <p>
+ * 瑙f瀽鏀惧湪杩欓噷鑰屼笉鏄悇缁勪欢鍐呴儴锛屾槸鍥犱负鏍¢獙鍒椾笌琛ㄥご璺ㄥ垪閮戒緷璧栧悓涓�濂楄娉曪紝
+ * 涓や唤瀹炵幇杩熸棭婕傜Щ锛岃�屾紓绉荤殑琛ㄧ幇鏄�屽垪鏁板涓嶄笂銆嶈繖绉嶅緢闅炬煡鐨勬覆鏌撲簨鏁呫��
+ */
+
+/** 涓�鍒楋細琛ㄥご鏂囧瓧 + 琛屽唴鍙栧�艰〃杈惧紡 */
+export interface QualityColumnSpec {
+  /** 琛ㄥご鏂囧瓧锛屽凡鍓ユ帀鍚堝苟鏍囪鍓嶇紑 */
+  label: string;
+  /** 琛屽唴鍙栧�艰〃杈惧紡锛屽 {{item.itemName}} */
+  expr: string;
+  /** 鏄惁绾靛悜鍚堝苟璺ㄤ綇鏁寸粍锛堟爣棰樺墠鍐欎簡 #锛� */
+  merge: boolean;
+}
+
+/** 涓婂眰琛ㄥご鐨勪竴涓崟鍏冩牸 */
+export interface QualityHeaderSpan {
+  label: string;
+  span: number;
+}
+
+/** 瑙f瀽缁撴灉锛氳兘瑙f瀽鍑哄灏戠畻澶氬皯锛岄敊璇�愭潯鍥炴姤锛岀敱璋冪敤鏂瑰喅瀹氭槸鍚︽嫤涓嬩繚瀛� */
+export interface QualitySpecParseResult<T> {
+  items: T[];
+  errors: string[];
+}
+
+/** 鍒椾笌鍒椾箣闂寸殑鍒嗛殧绗� */
+const SEGMENT_SEPARATOR = '|';
+
+/** 鍚堝苟鍒楃殑鏍囪鍓嶇紑 */
+const MERGE_MARK = '#';
+
+/** 琛ㄥご璺ㄥ垪鐨勮鏁版爣璁� */
+const SPAN_MARK = '^';
+
+/**
+ * 瑙f瀽鍒楀畾涔変覆銆�
+ *
+ * @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 };
+}
+
+/**
+ * 瑙f瀽涓婂眰琛ㄥご涓层��
+ *
+ * @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);
+}
diff --git a/src/components/quality/core/factory.ts b/src/components/quality/core/factory.ts
new file mode 100644
index 0000000..edf1629
--- /dev/null
+++ b/src/components/quality/core/factory.ts
@@ -0,0 +1,129 @@
+/**
+ * 璐ㄩ噺缁勪欢 鈫� GrapesJS 浜х墿
+ * <p>
+ * 鍗忚锛圦ualityComponentDefinition锛変笌 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 + 灞炴�� 鈫� 鐢诲竷鍐呭锛宎ttributes 閲屽甫涓婄被鍨嬫爣璁颁笌鍏ㄩ儴涓氬姟灞炴�� */
+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锛宼rait 鍊肩洿鎺ュ啓鍥炵粍浠剁殑 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',
+        // 鏄惧紡澹版槑鐪�/鍋囧瓧闈㈤噺锛孌OM 灞炴�у彧鑳芥槸瀛楃涓诧紝闈犲畠鎵嶈兘绋冲畾寰�杩�
+        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'],
+    // 鐐瑰嚮缁勪欢鍧楄拷鍔犲埌绾稿紶鏈熬銆侴rapesJS 榛樿杩藉姞杩涖�屽綋鍓嶉�変腑鐨勭粍浠躲�嶅唴閮紝
+    // 鍦ㄦ姤鍛婅繖绉嶆墎骞崇粨鏋勯噷浼氭妸鏂扮粍浠跺杩涙爣棰樸�佽〃鏍煎崟鍏冩牸绛夊厓绱狅紝杩樹細椤舵帀瀹冧滑鍘熸湁鐨勬枃鏈��
+    // 闇�瑕佸祵濂楁椂浠嶅彲鐢ㄦ嫋鎷界簿纭惤鐐广��
+    onClick: (block, editor) => {
+      const content = block.get('content') as ComponentAdd;
+      editor.getWrapper()?.append(content);
+    },
+  };
+}
+
+/** 娉ㄥ唽琛ㄩ噷鐨勫叏閮ㄧ粍浠� 鈫� GrapesJS 缁勪欢鍧楀垪琛� */
+export function buildQualityBlocks(): BlockProperties[] {
+  return listQualityComponents().map(buildQualityBlock);
+}
\ No newline at end of file
diff --git a/src/components/quality/core/html.ts b/src/components/quality/core/html.ts
new file mode 100644
index 0000000..e8fd5c7
--- /dev/null
+++ b/src/components/quality/core/html.ts
@@ -0,0 +1,30 @@
+/**
+ * 鐢诲竷鍐呭涓庢覆鏌撲骇鐗╅兘璧� HTML 瀛楃涓诧紝浠讳綍鏉ヨ嚜鐢ㄦ埛锛堣璁″櫒灞炴�ч潰鏉匡級鐨勬枃鏈�
+ * 閮藉繀椤诲厛杞箟鍐嶆嫾杩涙爣绛撅紝鍚﹀垯鍦ㄦ覆鏌撴湡浼氬彉鎴愭敞鍏ョ偣銆�
+ */
+export function escapeHtml(value: unknown): string {
+  return String(value ?? '')
+    .replaceAll('&', '&amp;')
+    .replaceAll('<', '&lt;')
+    .replaceAll('>', '&gt;')
+    .replaceAll('"', '&quot;')
+    .replaceAll("'", '&#39;');
+}
+
+/**
+ * 杞箟涔嬪悗鍐嶆妸鎹㈣鎶樻垚 <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(';');
+}
\ No newline at end of file
diff --git a/src/components/quality/core/icons.ts b/src/components/quality/core/icons.ts
new file mode 100644
index 0000000..bf83191
--- /dev/null
+++ b/src/components/quality/core/icons.ts
@@ -0,0 +1,37 @@
+/** 宸︿晶缁勪欢闈㈡澘鍥炬爣锛氬唴鑱� 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;
\ No newline at end of file
diff --git a/src/components/quality/core/lookup.ts b/src/components/quality/core/lookup.ts
new file mode 100644
index 0000000..ce3f61e
--- /dev/null
+++ b/src/components/quality/core/lookup.ts
@@ -0,0 +1,18 @@
+/**
+ * 鐢诲竷鑺傜偣 鈫� 璐ㄩ噺缁勪欢瀹氫箟鐨勫弽鏌ャ��
+ * <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] ?? ''));
+}
\ No newline at end of file
diff --git a/src/components/quality/core/registry.ts b/src/components/quality/core/registry.ts
new file mode 100644
index 0000000..f8ede31
--- /dev/null
+++ b/src/components/quality/core/registry.ts
@@ -0,0 +1,60 @@
+/**
+ * 璐ㄩ噺缁勪欢娉ㄥ唽琛�
+ * <p>
+ * 鍏ㄥ钩鍙板敮涓�鐨勭粍浠舵潵婧愶細璁捐鍣ㄥ乏渚ч潰鏉裤�佸睘鎬ч潰鏉裤�佹覆鏌撳紩鎿庛�佹牎楠岄兘浠庤繖閲屽彇瀹氫箟銆�
+ * 鏂板缁勪欢鍙渶瑕佸湪 index.ts 閲屽娉ㄥ唽涓�涓� definition锛屼笉鏀瑰姩 GrapesJS 闆嗘垚浠g爜銆�
+ */
+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;
+}
\ No newline at end of file
diff --git a/src/components/quality/core/serializer.ts b/src/components/quality/core/serializer.ts
new file mode 100644
index 0000000..ec479c5
--- /dev/null
+++ b/src/components/quality/core/serializer.ts
@@ -0,0 +1,56 @@
+/**
+ * 璐ㄩ噺灞炴�� 鈫� 鐢诲竷 attributes 鐨勫弻鍚戣浆鎹�
+ * <p>
+ * 涓氬姟灞炴�х粺涓�钀藉湪鐢诲竷鑺傜偣鐨� data-qc-<key> 涓婏紝鐢诲竷鏁版嵁锛坓rapes锛夋湰韬嵆鑷弿杩帮紝
+ * 淇濆瓨銆佸洖鏄俱�佹覆鏌撻兘涓嶉渶瑕佸啀棰濆缁存姢涓�浠芥槧灏勮〃銆�
+ */
+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 鈫� 涓氬姟灞炴�э紙鎸夊瓧娈靛0鏄庤繕鍘熺被鍨嬶級 */
+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;
+}
\ No newline at end of file
diff --git a/src/components/quality/core/trait-binding.ts b/src/components/quality/core/trait-binding.ts
new file mode 100644
index 0000000..70ef886
--- /dev/null
+++ b/src/components/quality/core/trait-binding.ts
@@ -0,0 +1,161 @@
+/**
+ * 銆岀粦瀹氭暟鎹�嶅睘鎬ф帶浠�
+ * <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}} 浜ゆ覆鏌撴湡瑙f瀽锛宲ath 蹇呴』淇濇寔瑁歌矾寰� */
+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);
+      }
+    },
+  });
+}
\ No newline at end of file
diff --git a/src/components/quality/core/types.ts b/src/components/quality/core/types.ts
new file mode 100644
index 0000000..47f2857
--- /dev/null
+++ b/src/components/quality/core/types.ts
@@ -0,0 +1,133 @@
+/**
+ * 璐ㄩ噺缁勪欢鍗忚锛圦uality Component Protocol锛�
+ * <p>
+ * 璁捐鍣ㄥ彧璁よ繖濂楀崗璁紝涓嶈鍏蜂綋涓氬姟缁勪欢锛氭柊澧炶川閲忕粍浠讹紳娉ㄥ唽涓�涓柊 definition锛�
+ * 涓嶉渶瑕佹敼鍔� GrapesJS 闆嗘垚浠g爜锛屼篃涓嶉渶瑕佹敼鍔ㄨ璁″櫒椤甸潰锛堟柟妗堣璁� 搂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 鏃跺睘鎬ч潰鏉夸笉鎻愪緵銆岀粦瀹氭暟鎹�嶅叆鍙c��
+   */
+  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;
+  /** 鑻辨枃鍚嶏紝鐢ㄤ簬浠g爜鍐呭紩鐢� */
+  name: string;
+  /** 璁捐鍣ㄥ乏渚ч潰鏉夸笌灞炴�ч潰鏉跨殑鏄剧ず鍚� */
+  label: string;
+  category: QualityCategory;
+  /** 宸︿晶闈㈡澘鐨勫浘鏍囷紙鍐呰仈 SVG 瀛楃涓诧級 */
+  icon: string;
+  /** 鏂扮粍浠惰惤鍒扮敾甯冩椂鐨勫垵濮嬩笟鍔″睘鎬� */
+  defaults: QualityProps;
+  /** 璁捐鏈熷彲缂栬緫鐨勪笟鍔″睘鎬� */
+  propertySchema: QualityFieldSchema[];
+  /** 娓叉煋鏈熷彲缁戝畾鐨勬暟鎹瓧娈� */
+  dataSchema: QualityFieldSchema[];
+  /**
+   * 缁勪欢鐨勫彲瑙佹枃鏈搴旂殑灞炴�� key銆�
+   * <p>
+   * 澹版槑鍚庯紝鐢ㄦ埛鍦ㄧ敾甯冨唴鐩存帴鏀规枃瀛椾細鍚屾鍥炶灞炴�э紝灞炴�ч潰鏉挎敼鍊间篃浼氬悓姝ュ洖鐢诲竷锛�
+   * 鍐呭鐢卞涓厓绱犳嫾鎴愮殑缁勪欢锛堥〉鐪夈�佽〃鏍硷級涓嶅0鏄庯紝閬垮厤鏁村潡 HTML 琚綋鎴愭枃鏈��
+   */
+  contentKey?: string;
+  /**
+   * 缁� AI 瀵煎叆鐢ㄧ殑閫夊瀷璇存槑锛氳繖涓粍浠剁敤鏉ユ斁浠�涔堛�佷笉瑕佺敤鏉ユ斁浠�涔堛��
+   * <p>
+   * 涓嶈繘璁捐鍣ㄧ晫闈紝鍙殢绉湪娓呭崟杩涙彁绀鸿瘝銆傚啓瀹冩槸鍥犱负娓呭崟閲屽彧鏈� label 鏃讹紝
+   * 妯″瀷闈㈠銆屾姤鍛婃姮澶淬�嶈繖绫诲唴瀹逛細鍦� Heading / Text / ReportHeader 涔嬮棿鐬庣寽锛�
+   * 鑰屽彧浼氱湅鍒颁腑鏂囨爣绛剧殑瀹冩病鏈変换浣曠悊鐢变笉鎸戞渶閫氱敤鐨勯偅涓��
+   * <p>
+   * 蹇呴』鍖呭惈銆屼笉瑕佺敤鏉ユ斁浠�涔堛�嶏細鍙鐢ㄩ�旀尅涓嶄綇璇敤锛岃娓呰竟鐣屾墠鏈夌敤銆�
+   */
+  aiHint?: string;
+  /**
+   * 鐢熸垚鐢诲竷鍐呭銆俻roperties 宸茬粡鏀舵暃杩囬粯璁ゅ�硷紝瀹炵幇閲屽彲浠ョ洿鎺ョ敤銆�
+   */
+  buildContent: (properties: QualityProps) => QualityComponentContent;
+  /**
+   * 鏍¢獙涓氬姟灞炴�э紝杩斿洖閿欒淇℃伅鏁扮粍锛涚┖鏁扮粍琛ㄧず閫氳繃銆�
+   * 浠呭湪淇濆瓨/鍙戝竷鍓嶈皟鐢紝姝e父璁捐杩囩▼涓嶅簲瑙﹀彂銆�
+   */
+  validate?: (properties: QualityProps) => string[];
+}
\ No newline at end of file
diff --git a/src/components/quality/core/validator.ts b/src/components/quality/core/validator.ts
new file mode 100644
index 0000000..e4b613b
--- /dev/null
+++ b/src/components/quality/core/validator.ts
@@ -0,0 +1,94 @@
+/**
+ * 璐ㄩ噺缁勪欢灞炴�ф牎楠屼笌榛樿鍊兼敹鏁�
+ * <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;
+}
\ No newline at end of file
diff --git a/src/components/quality/engine/binding.ts b/src/components/quality/engine/binding.ts
new file mode 100644
index 0000000..1966492
--- /dev/null
+++ b/src/components/quality/engine/binding.ts
@@ -0,0 +1,60 @@
+/**
+ * 鏁版嵁缁戝畾锛氭ā鏉挎枃鏈噷鐨� {{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];
+}
+
+/** 瑙f瀽鍗曚釜缁戝畾璺緞锛屽彇涓嶅埌杩斿洖 undefined */
+export function resolveBinding(path: string, context: unknown): unknown {
+  return readPath(context, path.trim());
+}
+
+/**
+ * 鏇挎崲鏂囨湰閲岀殑鍏ㄩ儴缁戝畾琛ㄨ揪寮忥紝骞跺洖鎶ュ彇涓嶅埌鍊肩殑璺緞銆�
+ * <p>
+ * 娓叉煋鏃剁粦瀹氫笉涓婇�氬父鎰忓懗鐫�鏁版嵁缂哄彛锛岄潤榛樻覆鏌撴垚绌轰細璁╂姤鍛婄湅璧锋潵"姝e父浣嗙己鍐呭"锛�
+ * 鎵�浠ユ妸缂哄彛浜ょ粰璋冪敤鏂癸紝鐢辨覆鏌撳紩鎿庢眹鎬绘垚鍙鐨勯棶棰樻竻鍗曘��
+ */
+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();
+}
\ No newline at end of file
diff --git a/src/components/quality/engine/context.ts b/src/components/quality/engine/context.ts
new file mode 100644
index 0000000..273691f
--- /dev/null
+++ b/src/components/quality/engine/context.ts
@@ -0,0 +1,169 @@
+/**
+ * 鎶ュ憡涓婁笅鏂囷紙Context锛�
+ * <p>
+ * 妯℃澘閲岀殑 {{path}} 鍏ㄩ儴鐩稿杩欎釜缁撴瀯瑙f瀽锛歳eport 鏄姤鍛婄骇瀛楁锛宨nspectionItems 鏄楠岄」鏁扮粍銆�
+ * 涓氬姟鍗曟嵁 鈫� 涓婁笅鏂囩殑鏄犲皠鐢变笂涓嬫枃鏋勫缓鍣ㄨ礋璐o紝妯℃澘鏈韩鍙杩欎釜鏍囧噯缁撴瀯锛�
+ * 鎹㈣涓氥�佹崲鍗曟嵁鏉ユ簮閮戒笉闇�瑕佹敼妯℃澘涓庣粍浠躲��
+ */
+
+/** 鍒ゅ畾缁撴灉鍙栧�硷紝缁勪欢涓庤鍒欏紩鎿庡叡鐢� */
+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,
+  };
+}
\ No newline at end of file
diff --git a/src/components/quality/engine/page.ts b/src/components/quality/engine/page.ts
new file mode 100644
index 0000000..a0d4a17
--- /dev/null
+++ b/src/components/quality/engine/page.ts
@@ -0,0 +1,50 @@
+/**
+ * 绾稿紶灏哄
+ * <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;
+}
+
+/** 瑙f瀽绾稿紶灏哄涓庨〉杈硅窛锛堟绫筹級锛屾í鍚戞椂缈昏浆瀹介珮 */
+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,
+    },
+  };
+}
\ No newline at end of file
diff --git a/src/components/quality/engine/path.ts b/src/components/quality/engine/path.ts
new file mode 100644
index 0000000..29d4769
--- /dev/null
+++ b/src/components/quality/engine/path.ts
@@ -0,0 +1,105 @@
+/**
+ * 涓婁笅鏂囧彇鍊�
+ * <p>
+ * 鍙仛鐧藉悕鍗曞紡鐨勫睘鎬у鑸紝涓嶈В鏋愩�佷笉鎵ц浠讳綍浠g爜锛�
+ * 鏀寔 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] : [];
+}
\ No newline at end of file
diff --git a/src/components/quality/engine/render.ts b/src/components/quality/engine/render.ts
new file mode 100644
index 0000000..3ae07a2
--- /dev/null
+++ b/src/components/quality/engine/render.ts
@@ -0,0 +1,333 @@
+/**
+ * 鎶ュ憡娓叉煋寮曟搸
+ * <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;
+  /** 姝f枃鐗囨锛屼究浜庡祵鍏ラ〉闈㈤瑙� */
+  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銆乻pan銆乭2锛夛紝
+ * 琛ㄦ牸缁撴瀯锛坱able/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');
+
+/** 娓叉煋鎶ュ憡锛氬垽瀹� 鈫� 鐢熸垚姝f枃 鈫� 鎷兼牱寮� 鈫� 缁勮鏂囨。 */
+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)}"` : '';
+}
+
+/** 鍙栨枃鏈噷鐨勭粦瀹氬�硷紝鍙栦笉鍒扮殑璺緞璁拌繘闂娓呭崟锛堟湭瑙f瀽鐨� {{}} 涓嶄細鐣欏湪浜х墿閲岋級 */
+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 鏂囨湰锛汫rapesJS 鐢� #缁勪欢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"]`);
+}
+
+/** 缁勮鏈�缁堟枃妗o細绾稿紶涓庨〉杈硅窛浜ょ粰 @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');
+}
\ No newline at end of file
diff --git a/src/components/quality/engine/report-evaluator.ts b/src/components/quality/engine/report-evaluator.ts
new file mode 100644
index 0000000..55b45ac
--- /dev/null
+++ b/src/components/quality/engine/report-evaluator.ts
@@ -0,0 +1,277 @@
+/**
+ * 鎶ュ憡鍒ゅ畾
+ * <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 閫愰」鍒ゅ畾锛宺eport 鎶ュ憡绾ф眹鎬� */
+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>
+ * 鍗曚釜妫�楠岄」鐨勫垽瀹氫紭鍏堢骇锛歩tem 浣滅敤鍩熻鍒� 鈫� 瑙勬牸涓婁笅闄� 鈫� 鏁版嵁鑷甫缁撴灉銆�
+ * 鎶ュ憡绾у垽瀹氶粯璁ゅ彇銆屽叏閮ㄦ楠岄」鍚堟牸銆嶏紝鏈� 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;
+}
\ No newline at end of file
diff --git a/src/components/quality/engine/rule-engine.ts b/src/components/quality/engine/rule-engine.ts
new file mode 100644
index 0000000..9581512
--- /dev/null
+++ b/src/components/quality/engine/rule-engine.ts
@@ -0,0 +1,711 @@
+/**
+ * 鍒ゅ畾瑙勫垯寮曟搸
+ * <p>
+ * 閫掑綊涓嬮檷瑙f瀽鎴� 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} 鐨勪綅缃笉姝g‘`, token.position);
+      }
+      return { type: 'path', path: token.value };
+    }
+
+    throw new RuleSyntaxError(
+      token.type === 'eof' ? '琛ㄨ揪寮忎笉瀹屾暣' : `鏃犳硶瑙f瀽鐨勫唴瀹癸細${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;
+  }
+}
+
+/** 瑙f瀽瑙勫垯鏂囨湰锛岃娉曢敊璇細鎶� 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);
+  },
+  /** 涓嶅悎鏍肩巼锛堢櫨鍒嗘瘮锛夛紝鍙e緞涓� 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));
+}
+
+/** 瀵瑰凡瑙f瀽鐨勮娉曟爲姹傚�硷紝閬垮厤鍚屼竴鏉¤鍒欏湪澶氳涓婇噸澶嶈В鏋� */
+export function evaluateRuleNode(node: RuleNode, context: unknown): unknown {
+  return evaluateNode(node, context);
+}
+
+/** 瀵瑰凡瑙f瀽鐨勮娉曟爲鍋氬竷灏斿垽瀹� */
+export function evaluateConditionNode(node: RuleNode, context: unknown): boolean {
+  return truthy(evaluateRuleNode(node, context));
+}
+
+/** 瑙f瀽鍚庣殑璺緞渚濊禆锛屾覆鏌撳墠鍙嵁姝ゆ鏌ユ暟鎹槸鍚﹂綈澶� */
+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('.')))];
+}
\ No newline at end of file
diff --git a/src/components/quality/header/report-footer.ts b/src/components/quality/header/report-footer.ts
new file mode 100644
index 0000000..abaafd3
--- /dev/null
+++ b/src/components/quality/header/report-footer.ts
@@ -0,0 +1,41 @@
+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',
+    },
+  }),
+};
\ No newline at end of file
diff --git a/src/components/quality/header/report-header.ts b/src/components/quality/header/report-header.ts
new file mode 100644
index 0000000..1cc291a
--- /dev/null
+++ b/src/components/quality/header/report-header.ts
@@ -0,0 +1,80 @@
+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' },
+    };
+  },
+};
\ No newline at end of file
diff --git a/src/components/quality/header/sample-info.ts b/src/components/quality/header/sample-info.ts
new file mode 100644
index 0000000..87346c7
--- /dev/null
+++ b/src/components/quality/header/sample-info.ts
@@ -0,0 +1,140 @@
+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}锛夛紝
+ * 鍏嶅緱鍚屼竴涓�岄敭鍊煎鍒楄〃銆嶉暱鍑轰袱绉嶅啓娉曘�傚悎骞舵爣璁� `#` 鍦ㄨ繖閲屾病鏈夋剰涔夛紝瑙f瀽鍑烘潵涔熷彧鏄涓㈡帀锛�
+ * 鎵�浠� 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;
+  },
+};
diff --git a/src/components/quality/index.ts b/src/components/quality/index.ts
new file mode 100644
index 0000000..38b6ce0
--- /dev/null
+++ b/src/components/quality/index.ts
@@ -0,0 +1,97 @@
+/**
+ * 璐ㄩ噺缁勪欢缁熶竴娉ㄥ唽鍏ュ彛
+ * <p>
+ * 璁捐鍣ㄣ�佹覆鏌撳紩鎿庨兘浠庤繖閲屽彇缁勪欢锛涙柊澧炰竴涓川閲忕粍浠讹紳鍦ㄤ笅闈㈢殑娓呭崟閲屽姞涓�涓� definition锛�
+ * 涓嶆敼鍔� GrapesJS 闆嗘垚浠g爜锛屼篃涓嶆敼鍔ㄨ璁″櫒椤甸潰銆�
+ */
+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';
\ No newline at end of file
diff --git a/src/components/quality/inspection/grouped-quality-table.ts b/src/components/quality/inspection/grouped-quality-table.ts
new file mode 100644
index 0000000..b4d5d81
--- /dev/null
+++ b/src/components/quality/inspection/grouped-quality-table.ts
@@ -0,0 +1,324 @@
+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 姝f枃瀹藉害锛屾祻瑙堝櫒鍙兘鎶婅〃鏍兼拺鍑虹焊寮狅紝鏈�鏈竴鍒楄鎸ゆ垚鍗佸嚑涓儚绱犵殑绔栨潯銆�
+ * <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 瑙f瀽鎴愯妭鐐� style 瀵硅薄锛�
+ * 鑰屾覆鏌撴湡瀵� style 瀵硅薄涓嶈窇缁戝畾锛坮enderStyle锛夛紝缁戝畾涓蹭細琚暣鏉′涪鎺夛紱
+ * 灞炴�у垯鍘熸牱淇濈暀锛屼袱渚ф覆鏌撳櫒閮戒細鎶� {{}} 瑙f垚鍊笺��
+ * <p>
+ * hidden 鐢ㄥ瓧绗︿覆鑰岄潪甯冨皵锛氳繖涓睘鎬ч潬銆屽湪涓嶅湪銆嶈捣浣滅敤锛坔idden="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>
+ * 鑷畾涔夊垪鏃�**涓嶅啀鐪嬪悇鏄剧ず寮�鍏�**锛歝olumns 宸茬粡鎶婂垪涓庨『搴忚鍏ㄤ簡锛�
+ * 鍐嶅彔涓�灞傘�屽簭鍙疯闅愯棌浜嗕絾鐢ㄦ埛鍙堝啓鍥炴潵浜嗐�嶇殑寮�鍏冲垽鏂紝鍙細璁╀袱杈规墦鏋躲��
+ *
+ * @return custom 涓虹湡琛ㄧず杩欏紶琛ㄧ収 columns 鏉ワ紙鎷夸笉鍒颁换浣曞搴﹀0鏄庯紝鍗曞厓鏍艰鍏佽鎶樿锛�
+ */
+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();
+}
+
+/** 涓婂眰鐨勫嚭琛ㄦ柟妗堬細涓婂眰閭d竴琛岀殑鍗曞厓鏍硷紝浠ュ強涓嬪眰瑕佽烦杩囧摢浜涘垪 */
+interface HeaderPlan {
+  cells: string;
+  covered: Set<number>;
+}
+
+/**
+ * 瑙勫垝涓婂眰琛ㄥご銆�
+ * <p>
+ * 琛ㄥご閲岀殑鏍煎瓙鏈変袱绉嶅舰鎬侊紝閮戒粠鍚屼竴涓� `鏍囬^璺ㄥ垪鏁癭 閲岃鍑烘潵锛�
+ * <ul>
+ *   <li>鍒嗙粍鏍囬锛氭í鍚戣法浣忎笅闈㈣嫢骞插垪锛屽 `妫�楠岀粨鏋淾5`锛�</li>
+ *   <li>绾靛悜鍚堝苟鏍硷細鏌愪竴鍒楄嚜宸辨棦鏄垪鍚嶅張鏄笂鏍硷紝鍘熶欢閲屾槸涓�鏍艰法浣忎笂涓嬩袱灞�
+ *       锛堟彁鍙栨枃鏈噷琛ㄧ幇涓轰笅灞傝〃澶撮偅涓�鏍煎啓鐫�銆屸啈鍚屼笂銆嶏級锛屽 `缁撹^1`銆�
+ *       璁ゅ嚭瀹冮潬銆岃娈垫爣棰樹笌瀹冩涓嬫柟閭d竴鍒楃殑鍒楁爣棰樹竴瀛椾笉宸�佷笖鍙法 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:
+        '鍙湁琛ㄥご鍒嗕笂涓嬩袱灞傛椂鎵嶅~锛屽啓鐨勬槸涓婇潰閭d竴灞傦細姣忎釜鍗曞厓鏍煎啓浣� 鏍囬^璺ㄨ秺鍒楁暟锛屽娈电敤 | 鍒嗛殧锛�' +
+        '濡� 妫�楠岀粨鏋淾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;
+  },
+};
diff --git a/src/components/quality/inspection/inspection-item.ts b/src/components/quality/inspection/inspection-item.ts
new file mode 100644
index 0000000..5f9a3b4
--- /dev/null
+++ b/src/components/quality/inspection/inspection-item.ts
@@ -0,0 +1,83 @@
+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',
+      },
+    };
+  },
+};
\ No newline at end of file
diff --git a/src/components/quality/inspection/quality-table.ts b/src/components/quality/inspection/quality-table.ts
new file mode 100644
index 0000000..8ef52a4
--- /dev/null
+++ b/src/components/quality/inspection/quality-table.ts
@@ -0,0 +1,139 @@
+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;
+  },
+};
\ No newline at end of file
diff --git a/src/components/quality/result/report-result.ts b/src/components/quality/result/report-result.ts
new file mode 100644
index 0000000..81ca884
--- /dev/null
+++ b/src/components/quality/result/report-result.ts
@@ -0,0 +1,69 @@
+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>
+ * 缁撹涓庡悎鏍肩巼鏉ヨ嚜鎶ュ憡涓婁笅鏂囷紝鐢辫鍒欏紩鎿庡湪娓叉煋鍓嶇畻濂斤紱
+ * 杩欓噷鍙礋璐e睍绀猴紝涓嶅湪缁勪欢閲屽仛浠讳綍鍒ゅ畾璁$畻銆�
+ */
+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',
+      },
+    };
+  },
+};
\ No newline at end of file
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index c84f3fc..0d7693a 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -476,6 +476,12 @@
   FILE: 5,
 } as const;
 
+/** MES 璐ㄦ鎸囨爣鏉$洰绫诲瀷鏋氫妇锛堝榻愬悗绔� MesQcIndicatorItemTypeEnum锛� */
+export const MesQcIndicatorItemType = {
+  ENTRY: 1,
+  GROUP: 2,
+} as const;
+
 /** MES 涓氬姟绫诲瀷甯搁噺 */
 export const MesBizTypeEnum = {
   // WM 浠撳簱妯″潡 [100, 200)
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index 2a62e5a..53e2121 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -298,6 +298,16 @@
   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 寰�鏉ヤ紒涓氱被鍨�
@@ -352,6 +362,7 @@
   ...MP_DICT,
   ...PAY_DICT,
   ...MALL_DICT,
+  ...QC_REPORT_DICT,
   ...SYSTEM_DICT,
   ...COMMON_DICT,
 } as const;
diff --git a/src/router/routes/modules/qcreport.ts b/src/router/routes/modules/qcreport.ts
new file mode 100644
index 0000000..7c5e05e
--- /dev/null
+++ b/src/router/routes/modules/qcreport.ts
@@ -0,0 +1,28 @@
+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;
\ No newline at end of file
diff --git a/src/views/mes/qc/components/report-generate-modal.vue b/src/views/mes/qc/components/report-generate-modal.vue
new file mode 100644
index 0000000..5dbb09c
--- /dev/null
+++ b/src/views/mes/qc/components/report-generate-modal.vue
@@ -0,0 +1,251 @@
+<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"锛夛紝鎷裤�孖QC銆嶈繖绫昏嫳鏂囧悕鍘绘瘮浼氫竴涓兘鏌ヤ笉鍒帮紱
+ * 涓� 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>
diff --git a/src/views/mes/qc/indicator/data.ts b/src/views/mes/qc/indicator/data.ts
index a20cc84..947d209 100644
--- a/src/views/mes/qc/indicator/data.ts
+++ b/src/views/mes/qc/indicator/data.ts
@@ -7,13 +7,16 @@
 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';
 
@@ -63,6 +66,61 @@
       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',
@@ -83,6 +141,30 @@
       },
     },
     {
+      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',
@@ -93,6 +175,11 @@
           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',
     },
@@ -147,6 +234,19 @@
       },
     },
     {
+      fieldName: 'itemType',
+      label: '鏉$洰绫诲瀷',
+      component: 'Select',
+      componentProps: {
+        allowClear: true,
+        options: [
+          { label: '褰曞叆椤�', value: MesQcIndicatorItemType.ENTRY },
+          { label: '鍒嗙粍椤�', value: MesQcIndicatorItemType.GROUP },
+        ],
+        placeholder: '璇烽�夋嫨鏉$洰绫诲瀷',
+      },
+    },
+    {
       fieldName: 'type',
       label: '妫�娴嬮」绫诲瀷',
       component: 'Select',
@@ -183,6 +283,18 @@
       minWidth: 160,
     },
     {
+      field: 'itemType',
+      title: '鏉$洰绫诲瀷',
+      width: 100,
+      slots: { default: 'itemType' },
+    },
+    {
+      field: 'parentName',
+      title: '鎵�灞炲垎缁�',
+      width: 140,
+      slots: { default: 'parentName' },
+    },
+    {
       field: 'type',
       title: '妫�娴嬮」绫诲瀷',
       width: 120,
diff --git a/src/views/mes/qc/indicator/index.vue b/src/views/mes/qc/indicator/index.vue
index ac34922..b49abd3 100644
--- a/src/views/mes/qc/indicator/index.vue
+++ b/src/views/mes/qc/indicator/index.vue
@@ -13,7 +13,7 @@
   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';
 
@@ -40,6 +40,14 @@
 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 ? '鍒嗙粍椤�' : '褰曞叆椤�';
 }
 
 /** 鍒锋柊琛ㄦ牸 */
@@ -133,6 +141,14 @@
           ]"
         />
       </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) }}
diff --git a/src/views/mes/qc/ipqc/index.vue b/src/views/mes/qc/ipqc/index.vue
index 832a85a..2d47360 100644
--- a/src/views/mes/qc/ipqc/index.vue
+++ b/src/views/mes/qc/ipqc/index.vue
@@ -11,9 +11,13 @@
 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,
@@ -22,6 +26,11 @@
 
 const [ExportRangeModal, exportRangeModalApi] = useVbenModal({
   connectedComponent: QcExportRangeModal,
+  destroyOnClose: true,
+});
+
+const [ReportModal, reportModalApi] = useVbenModal({
+  connectedComponent: QcReportGenerateModal,
   destroyOnClose: true,
 });
 
@@ -71,6 +80,13 @@
     .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(),
@@ -106,6 +122,7 @@
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
     <ExportRangeModal />
+    <ReportModal />
     <Grid table-title="杩囩▼妫�楠屽崟鍒楄〃">
       <template #toolbar-tools>
         <TableAction
@@ -136,6 +153,14 @@
         <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,
diff --git a/src/views/mes/qc/iqc/index.vue b/src/views/mes/qc/iqc/index.vue
index 761305f..5759607 100644
--- a/src/views/mes/qc/iqc/index.vue
+++ b/src/views/mes/qc/iqc/index.vue
@@ -14,11 +14,15 @@
 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();
 
@@ -29,6 +33,11 @@
 
 const [ExportRangeModal, exportRangeModalApi] = useVbenModal({
   connectedComponent: QcExportRangeModal,
+  destroyOnClose: true,
+});
+
+const [ReportModal, reportModalApi] = useVbenModal({
+  connectedComponent: QcReportGenerateModal,
   destroyOnClose: true,
 });
 
@@ -78,6 +87,13 @@
     .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(),
@@ -120,6 +136,7 @@
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
     <ExportRangeModal />
+    <ReportModal />
     <Grid table-title="鏉ユ枡妫�楠屽崟鍒楄〃">
       <template #toolbar-tools>
         <TableAction
@@ -150,6 +167,14 @@
         <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,
diff --git a/src/views/mes/qc/oqc/index.vue b/src/views/mes/qc/oqc/index.vue
index f9043b1..9a62cdd 100644
--- a/src/views/mes/qc/oqc/index.vue
+++ b/src/views/mes/qc/oqc/index.vue
@@ -11,9 +11,13 @@
 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,
@@ -22,6 +26,11 @@
 
 const [ExportRangeModal, exportRangeModalApi] = useVbenModal({
   connectedComponent: QcExportRangeModal,
+  destroyOnClose: true,
+});
+
+const [ReportModal, reportModalApi] = useVbenModal({
+  connectedComponent: QcReportGenerateModal,
   destroyOnClose: true,
 });
 
@@ -71,6 +80,13 @@
     .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(),
@@ -106,6 +122,7 @@
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
     <ExportRangeModal />
+    <ReportModal />
     <Grid table-title="鍑鸿揣妫�楠屽崟鍒楄〃">
       <template #toolbar-tools>
         <TableAction
@@ -136,6 +153,14 @@
         <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,
diff --git a/src/views/mes/qc/pendinginspect/index.vue b/src/views/mes/qc/pendinginspect/index.vue
index 486e3ad..cc33a44 100644
--- a/src/views/mes/qc/pendinginspect/index.vue
+++ b/src/views/mes/qc/pendinginspect/index.vue
@@ -96,7 +96,7 @@
     checkQuantity: row.quantity,
     inspectDate: row.recordTime,
     itemId: row.itemId,
-    name: `${row.sourceDocCode} 閫�鏂欐楠屽崟`,
+    name: `${row.sourceDocCode} 閫�璐ф楠屽崟`,
     sourceDocCode: row.sourceDocCode,
     sourceDocId: row.sourceDocId,
     sourceDocType: row.sourceDocType,
@@ -170,7 +170,7 @@
               onClick: handleCreateOqc.bind(null, row),
             },
             {
-              label: '閫�鏂欐楠�',
+              label: '閫�璐ф楠�',
               type: 'link',
               icon: ACTION_ICON.ADD,
               auth: ['mes:qc-rqc:create'],
diff --git a/src/views/mes/qc/report/instance/data.ts b/src/views/mes/qc/report/instance/data.ts
new file mode 100644
index 0000000..b41b555
--- /dev/null
+++ b/src/views/mes/qc/report/instance/data.ts
@@ -0,0 +1,165 @@
+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' },
+    },
+  ];
+}
diff --git a/src/views/mes/qc/report/instance/index.vue b/src/views/mes/qc/report/instance/index.vue
new file mode 100644
index 0000000..2503bae
--- /dev/null
+++ b/src/views/mes/qc/report/instance/index.vue
@@ -0,0 +1,147 @@
+<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锛岄濉埌鎼滅储鏉′欢閲屽苟鐩存帴鏌ヤ竴娆★紝
+ * 鐢ㄦ埛杩涙潵灏辩湅鍒板垰鍑虹殑閭d唤鎶ュ憡锛屼笉鐢ㄨ嚜宸卞啀绛涗竴閬嶃��
+ * <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>
diff --git a/src/views/mes/qc/report/instance/modules/detail.vue b/src/views/mes/qc/report/instance/modules/detail.vue
new file mode 100644
index 0000000..6147261
--- /dev/null
+++ b/src/views/mes/qc/report/instance/modules/detail.vue
@@ -0,0 +1,197 @@
+<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>
diff --git a/src/views/mes/qc/report/template/data.ts b/src/views/mes/qc/report/template/data.ts
new file mode 100644
index 0000000..fe46ea3
--- /dev/null
+++ b/src/views/mes/qc/report/template/data.ts
@@ -0,0 +1,229 @@
+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' },
+    },
+  ];
+}
\ No newline at end of file
diff --git a/src/views/mes/qc/report/template/designer/constants.ts b/src/views/mes/qc/report/template/designer/constants.ts
new file mode 100644
index 0000000..fcae0e6
--- /dev/null
+++ b/src/views/mes/qc/report/template/designer/constants.ts
@@ -0,0 +1,54 @@
+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 },
+};
+
+/** 璁$畻鐢诲竷灏哄锛坧x锛夛紝鎸夌焊寮犳柟鍚戣嚜鍔ㄧ炕杞楂� */
+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: [],
+  };
+}
\ No newline at end of file
diff --git a/src/views/mes/qc/report/template/designer/index.vue b/src/views/mes/qc/report/template/designer/index.vue
new file mode 100644
index 0000000..f07a6ef
--- /dev/null
+++ b/src/views/mes/qc/report/template/designer/index.vue
@@ -0,0 +1,637 @@
+<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} 鏉″垽瀹氳鍒欒繕娌″啓瀹屾暣锛岃鍏堜慨姝e啀淇濆瓨`);
+  return true;
+}
+
+const ruleButtonText = computed(() =>
+  rules.value.length > 0 ? `鍒ゅ畾瑙勫垯锛�${rules.value.length}锛塦 : '鍒ゅ畾瑙勫垯',
+);
+
+/** 鎶藉眽閲屾敼瀹岀殑瑙勫垯鍐欏洖璁捐鍣ㄧ姸鎬侊紝淇濆瓨鏃剁敱 collectSchema 涓�骞朵骇鍑� */
+function handleRulesChange(next: QcReportVersionApi.SchemaRule[]) {
+  rules.value = next;
+}
+
+/** 鐢诲竷涓婂凡鏈夌殑椤跺眰缁勪欢鏁帮紝鐢ㄦ潵鍒ゆ柇 AI 瀵煎叆浼氫笉浼氳鐩栨帀鐢ㄦ埛姝e湪鍋氱殑鍐呭 */
+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;
+  }
+}
+
+/** 鍒囨崲鐗堟湰锛圓ntDV 鐨� 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;
+      }
+    },
+  });
+}
+
+/** 鐢诲竷缂╂斁锛圫elect 鍥炶皟鍊肩被鍨嬫瘮 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 楂樺害锛屽乏鍙虫爮鎵嶈兘姝e父婊氬姩銆�
+ */
+: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 鐨勭粷瀵瑰畾浣嶈垶鍙帮紝绾稿紶姣斿彲瑙嗗尯澶ф椂浼氳瑁佹帀銆�
+ * 杩欓噷鎶婄敾甯冩敼鎴愭櫘閫氭粴鍔ㄥ鍣細甯у鍣ㄥ洖褰掓枃妗f祦锛岀焊寮犵敤 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>
\ No newline at end of file
diff --git a/src/views/mes/qc/report/template/designer/modules/ai-import-modal.vue b/src/views/mes/qc/report/template/designer/modules/ai-import-modal.vue
new file mode 100644
index 0000000..b90d793
--- /dev/null
+++ b/src/views/mes/qc/report/template/designer/modules/ai-import-modal.vue
@@ -0,0 +1,341 @@
+<script lang="ts" setup>
+/**
+ * AI 瀵煎叆寮圭獥锛氫笂浼犱竴浠藉凡鏈夌殑妫�楠屾姤鍛� 鈫� 璇嗗埆鎴愭ā鏉胯崏绋� 鈫� 浜ょ粰璁捐鍣ㄣ��
+ * <p>
+ * 寮圭獥鍙礋璐c�屾嬁鑽夌銆嶈繖涓�娈碉細涓婁紶銆佺粦闄勪欢銆佽姹傝瘑鍒�佺粰鐢ㄦ埛鐪嬬粨鏋溿��
+ * 瑁呭埌鐢诲竷涓婃槸璁捐鍣ㄧ殑浜嬶紝鐢辩埗缁勪欢鍦ㄧ敤鎴风‘璁ゅ悗鏉ュ仛鈥斺��
+ * 鐢诲竷闈炵┖鏃惰鍏堝脊鏇挎崲纭锛岄偅浠藉垽鏂睘浜庤璁″櫒鑰屼笉鏄繖閲屻��
+ */
+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']>;
+
+/** 宸查�夋枃浠舵暟锛汚ntDV 鐨� 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锛屾病鏈夊綊妗e畠浠殑蹇呰銆�
+ * <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">
+            鏀寔鎵弿浠�/鐓х墖锛坧ng銆乯pg銆乯peg銆乥mp銆乬if銆亀ebp锛夈�丳DF銆乄ord(.docx)銆丒xcel(.xlsx)銆佹枃鏈�(txt銆乧sv)锛�
+            涓�娆℃渶澶� {{ 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">
+            姝e湪璇嗗埆锛岄璁� 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>
diff --git a/src/views/mes/qc/report/template/designer/modules/rule-editor.vue b/src/views/mes/qc/report/template/designer/modules/rule-editor.vue
new file mode 100644
index 0000000..4a54d80
--- /dev/null
+++ b/src/views/mes/qc/report/template/designer/modules/rule-editor.vue
@@ -0,0 +1,156 @@
+<script lang="ts" setup>
+/**
+ * 鍒ゅ畾瑙勫垯鎶藉眽銆�
+ * <p>
+ * 瑙勫垯鍒楄〃鐢辫璁″櫒鎸佹湁锛坲se-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>
diff --git a/src/views/mes/qc/report/template/designer/use-designer.ts b/src/views/mes/qc/report/template/designer/use-designer.ts
new file mode 100644
index 0000000..f82a89a
--- /dev/null
+++ b/src/views/mes/qc/report/template/designer/use-designer.ts
@@ -0,0 +1,363 @@
+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锛屼笉鍔ㄧ湡姝g殑 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>
+ * 鍙礋璐g敾甯冧笌 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);
+  }
+
+  /**
+   * 鐢诲竷鎶� {{...}} 鍘熸牱鐣欑潃涓嶆眰瑙o紝浜庢槸缁勫悕鏍间笂鐨� hidden="{{item.group.hidden}}"
+   * 鍦ㄦ祻瑙堝櫒鐪奸噷灏辨槸銆屾湁 hidden 灞炴�с�嶁�斺�旀暣鏍� display:none锛岃繖涓�琛屽悗闈㈢殑鏍兼暣浣撳乏绉讳竴鏍硷紝
+   * 鏈垪鍙墿琛ㄥご銆佽鎸ゆ垚绔栨帓缁嗘潯锛岃璁″櫒閲岀湅鍒扮殑鎺掔増璺熷嚭浠剁粨鏋滃涓嶄笂銆�
+   * 杩欓噷鍙湪鐢诲竷鏂囨。鍐呮妸銆屽�艰繕鍋滃湪鍗犱綅绗︺�嶇殑 hidden 杩樺師鎴愬彲瑙佹牸锛涘嚭浠舵椂璇ュ�煎凡瑙f垚
+   * 绌轰覆鎴� 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,
+      // 鐢辨湰骞冲彴鑷繁鐨勬帴鍙d繚瀛橈紝涓嶄娇鐢� 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;
+    }
+    // 鎷蜂竴灞傚啀鏀捐繘鍘伙細鎶藉眽閲屾敼鐨勬槸杩欎唤鍓湰锛屽垏鐗堟湰鏃朵笉璇ユ妸鏀瑰姩甯﹀埌鍙︿竴涓増鏈笂銆�
+    // 鍚屾椂鎶婂紩鎿庣殑缂虹渷鍙e緞钀芥垚鏄庣‘鍊硷紙scope 璇嗗埆涓嶅嚭鎸夐�愰」銆乪nabled 缂虹渷鍗冲惎鐢級锛�
+    // 鍚﹀垯鐣岄潰涓婄殑寮�鍏充細涓庡疄闄呭垽瀹氬涓嶄笂锛岀湅璧锋潵鍋滅敤浜嗗叾瀹炶繕鍦ㄥ垽銆�
+    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锛歡rapes 淇濆瓨鍘熷鏁版嵁锛宑omponents 淇濆瓨璐ㄩ噺缁勪欢涓氬姟璇箟銆�
+   * <p>
+   * 浜у嚭鐨勬槸<b>鏁翠唤</b> Schema锛屽悗绔繚瀛樻椂鏁翠綋瑕嗙洊锛屾墍浠ヨ璁″櫒璁よ瘑鐨勫瓧娈靛繀椤诲湪杩欓噷鍒楀叏鈥斺��
+   * 婕忔帀鍝釜灏辩瓑浜庢妸閭d釜瀛楁娓呯┖锛坮ules 鏇剧粡灏辫繖鏍疯娓呮帀杩囷紝瑙� RuleEditor 鐨勮鏄庯級銆�
+   * 灏嗘潵 Schema 鏂板瀛楁鏃讹紝瑕佷箞鍦ㄨ繖閲屼骇鍑猴紝瑕佷箞鎯虫竻妤氱敱璋佽礋璐d繚鐣欍��
+   */
+  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(),
+          // 缁勪欢鍦ㄦ枃妗d腑鐨勯『搴忥紝渚涘悗缁暟鎹粦瀹氫笌娓叉煋瀹氫綅
+          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);
+    // 涓�涓粍浠堕兘瑁呬笉杩涘幓鏃朵笉瑕佸姩鐢诲竷锛歭oadSchema 浼氭竻鎺夋挙閿�鏍堜笌宸叉湁鍐呭锛�
+    // 鐧界櫧鎼繘鍘讳竴鐗堢敾甯冨嵈浠�涔堥兘鎹笉鏉�
+    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;
+}
\ No newline at end of file
diff --git a/src/views/mes/qc/report/template/index.vue b/src/views/mes/qc/report/template/index.vue
new file mode 100644
index 0000000..9e46275
--- /dev/null
+++ b/src/views/mes/qc/report/template/index.vue
@@ -0,0 +1,195 @@
+<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: `姝e湪澶嶅埗銆�${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>
\ No newline at end of file
diff --git a/src/views/mes/qc/report/template/modules/form.vue b/src/views/mes/qc/report/template/modules/form.vue
new file mode 100644
index 0000000..ac75b6d
--- /dev/null
+++ b/src/views/mes/qc/report/template/modules/form.vue
@@ -0,0 +1,83 @@
+<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>
\ No newline at end of file
diff --git a/src/views/mes/qc/rqc/index.vue b/src/views/mes/qc/rqc/index.vue
index 2771b4c..64564a9 100644
--- a/src/views/mes/qc/rqc/index.vue
+++ b/src/views/mes/qc/rqc/index.vue
@@ -11,9 +11,13 @@
 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,
@@ -22,6 +26,11 @@
 
 const [ExportRangeModal, exportRangeModalApi] = useVbenModal({
   connectedComponent: QcExportRangeModal,
+  destroyOnClose: true,
+});
+
+const [ReportModal, reportModalApi] = useVbenModal({
+  connectedComponent: QcReportGenerateModal,
   destroyOnClose: true,
 });
 
@@ -71,6 +80,13 @@
     .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(),
@@ -106,6 +122,7 @@
   <Page auto-content-height>
     <FormModal @success="handleRefresh" />
     <ExportRangeModal />
+    <ReportModal />
     <Grid table-title="閫�璐ф楠屽崟鍒楄〃">
       <template #toolbar-tools>
         <TableAction
@@ -136,6 +153,14 @@
         <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,
diff --git a/vite.config.ts b/vite.config.ts
index 2d42c23..f8ca9af 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -83,7 +83,7 @@
         proxy: {
           "/admin-api": {
             changeOrigin: true,
-            target: "http://192.168.0.10:48080",
+            target: "http://localhost:48080",
             ws: true,
           },
         },

--
Gitblit v1.9.3