component-auto-docs 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
+ import { parse as parseSfc } from '@vue/compiler-sfc';
4
5
  import ts from 'typescript';
5
6
 
6
7
  const rootDir = process.cwd();
@@ -19,6 +20,7 @@ const defaultDocsConfig = {
19
20
  markdownDir: 'docs/components',
20
21
  pageDataRoot: 'src/docs/components',
21
22
  indexDataFile: 'src/docs/data.json',
23
+ pageDataModuleFileName: 'data.js',
22
24
  catalogFile: 'components.catalog.json',
23
25
  aiIndexFile: 'components.index.md',
24
26
  llmsFile: 'llms.txt',
@@ -107,7 +109,7 @@ function createPageRouteEntry(routePath, title) {
107
109
  "style": {
108
110
  "navigationBarTitleText": "${title}"
109
111
  }
110
- },
112
+ },
111
113
  `;
112
114
  }
113
115
 
@@ -157,6 +159,57 @@ function writeText(filePath, content) {
157
159
  fs.writeFileSync(filePath, `${content.trimEnd()}\n`);
158
160
  }
159
161
 
162
+ function hashText(text) {
163
+ let hash = 5381;
164
+
165
+ for (const char of String(text)) {
166
+ hash = (hash * 33) ^ char.codePointAt(0);
167
+ }
168
+
169
+ return (hash >>> 0).toString(36);
170
+ }
171
+
172
+ function toDataModuleIdentifier(name, fallback) {
173
+ const words = String(name).match(/[A-Za-z0-9]+/g) || [];
174
+ const base = words
175
+ .map((word, index) => {
176
+ const lower = word.toLowerCase();
177
+ return index === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
178
+ })
179
+ .join('');
180
+ const identifierBase = base && /^[A-Za-z_$]/.test(base) ? base : fallback;
181
+
182
+ return `${identifierBase}Data_${hashText(name)}`;
183
+ }
184
+
185
+ function serializeJsValue(value) {
186
+ return JSON.stringify(value, null, 2)
187
+ .replace(/\u2028/g, '\\u2028')
188
+ .replace(/\u2029/g, '\\u2029');
189
+ }
190
+
191
+ function renderDataModule(data, identifier) {
192
+ return `// @generated by ${generatedBy}. Keep this file in sync by running docs:gen.
193
+ const ${identifier} = ${serializeJsValue(data)};
194
+
195
+ export default ${identifier};
196
+ `;
197
+ }
198
+
199
+ function writeDataModule(filePath, data, identifier) {
200
+ writeText(filePath, renderDataModule(data, identifier));
201
+ }
202
+
203
+ function getIndexDataModuleFile() {
204
+ const parsed = path.parse(indexDataFile);
205
+
206
+ return path.join(parsed.dir, `${parsed.name}.js`);
207
+ }
208
+
209
+ function getPageDataModuleFileName() {
210
+ return docsConfig.output.pageDataModuleFileName || 'data.js';
211
+ }
212
+
160
213
  function normalizeTypeText(typeText) {
161
214
  return typeText
162
215
  .replace(/\s+/g, ' ')
@@ -280,12 +333,29 @@ function readLiteralValue(node, sourceFile) {
280
333
  return node.getText(sourceFile);
281
334
  }
282
335
 
283
- function extractSfcBlock(source, tagName, matcher = () => true) {
284
- const blockPattern = new RegExp(`<${tagName}\\b([^>]*)>([\\s\\S]*?)<\\/${tagName}>`, 'gi');
285
- const blocks = [...source.matchAll(blockPattern)];
286
- const block = blocks.find((item) => matcher(item[1]));
336
+ function formatSfcParseError(error) {
337
+ if (typeof error === 'string') return error;
338
+ if (error?.message) return error.message;
339
+
340
+ return String(error);
341
+ }
342
+
343
+ function readSfcBlocks(filePath) {
344
+ const source = fs.readFileSync(filePath, 'utf8');
345
+ const { descriptor, errors } = parseSfc(source, { filename: filePath });
346
+
347
+ if (errors.length) {
348
+ throw new Error(
349
+ `Failed to parse Vue SFC ${toPosix(path.relative(rootDir, filePath))}: ${errors
350
+ .map(formatSfcParseError)
351
+ .join('; ')}`,
352
+ );
353
+ }
287
354
 
288
- return block?.[2] || '';
355
+ return {
356
+ script: descriptor.scriptSetup?.content || descriptor.script?.content || '',
357
+ template: descriptor.template?.content || '',
358
+ };
289
359
  }
290
360
 
291
361
  function parseJsdoc(script) {
@@ -675,6 +745,7 @@ function normalizeExamples(metaExamples = [], jsdocExamples = []) {
675
745
  const normalized = typeof example === 'string' ? { title: '示例', description: '', code: example } : example;
676
746
  const code = normalized.code?.trim();
677
747
  if (!code || seen.has(code)) continue;
748
+ if (normalized.title === '基础用法' && normalized.description?.startsWith('自动根据 ')) continue;
678
749
 
679
750
  seen.add(code);
680
751
  examples.push({
@@ -687,6 +758,525 @@ function normalizeExamples(metaExamples = [], jsdocExamples = []) {
687
758
  return examples;
688
759
  }
689
760
 
761
+ function escapeRegExp(text) {
762
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
763
+ }
764
+
765
+ function kebabToCamel(text) {
766
+ return text.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
767
+ }
768
+
769
+ function decodeHtmlText(text) {
770
+ return text
771
+ .replace(/&lt;/g, '<')
772
+ .replace(/&gt;/g, '>')
773
+ .replace(/&quot;/g, '"')
774
+ .replace(/&#39;/g, "'")
775
+ .replace(/&amp;/g, '&');
776
+ }
777
+
778
+ function getObjectKeyText(nameNode, sourceFile) {
779
+ if (!nameNode) return '';
780
+ if (ts.isIdentifier(nameNode) || ts.isStringLiteralLike(nameNode) || ts.isNumericLiteral(nameNode)) {
781
+ return nameNode.text;
782
+ }
783
+
784
+ return nameNode.getText(sourceFile).replace(/^['"]|['"]$/g, '');
785
+ }
786
+
787
+ function getIdentifierSampleValue(name) {
788
+ const samples = {
789
+ text: '这是一段足够长的公告文字,用于触发滚动预览效果',
790
+ title: '标题',
791
+ content: '内容',
792
+ label: '选项',
793
+ value: '1',
794
+ name: '名称',
795
+ status: 'done',
796
+ time: '10:30',
797
+ src: '/static/icon/avatar.svg',
798
+ url: '/static/icon/avatar.svg',
799
+ poster: '/static/icon/avatar.svg',
800
+ };
801
+
802
+ return samples[name] || name;
803
+ }
804
+
805
+ function evaluateStaticNode(node, sourceFile, bindings = {}, options = {}) {
806
+ if (!node) return undefined;
807
+
808
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
809
+ return evaluateStaticNode(node.expression, sourceFile, bindings, options);
810
+ }
811
+
812
+ if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) return decodeHtmlText(node.text);
813
+ if (ts.isNumericLiteral(node)) return Number(node.text);
814
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
815
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
816
+ if (node.kind === ts.SyntaxKind.NullKeyword) return null;
817
+
818
+ if (ts.isIdentifier(node)) {
819
+ if (Object.prototype.hasOwnProperty.call(bindings, node.text)) return bindings[node.text];
820
+
821
+ return options.allowIdentifierFallback ? getIdentifierSampleValue(node.text) : undefined;
822
+ }
823
+
824
+ if (ts.isArrayLiteralExpression(node)) {
825
+ return node.elements
826
+ .map((item) => evaluateStaticNode(item, sourceFile, bindings, { ...options, allowIdentifierFallback: true }))
827
+ .filter((item) => item !== undefined);
828
+ }
829
+
830
+ if (ts.isObjectLiteralExpression(node)) {
831
+ const result = {};
832
+
833
+ for (const property of node.properties) {
834
+ if (ts.isPropertyAssignment(property)) {
835
+ const key = getObjectKeyText(property.name, sourceFile);
836
+ if (!key) continue;
837
+ const value = evaluateStaticNode(property.initializer, sourceFile, bindings, {
838
+ ...options,
839
+ allowIdentifierFallback: true,
840
+ });
841
+ if (value !== undefined) result[key] = value;
842
+ }
843
+
844
+ if (ts.isShorthandPropertyAssignment(property)) {
845
+ const key = property.name.text;
846
+ result[key] = Object.prototype.hasOwnProperty.call(bindings, key) ? bindings[key] : getIdentifierSampleValue(key);
847
+ }
848
+ }
849
+
850
+ return result;
851
+ }
852
+
853
+ return undefined;
854
+ }
855
+
856
+ function evaluateStaticExpression(expression, bindings = {}) {
857
+ const source = `const __value = (${expression});`;
858
+ const sourceFile = ts.createSourceFile('preview-expression.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
859
+ const statement = sourceFile.statements[0];
860
+ const declaration = statement?.declarationList?.declarations?.[0];
861
+
862
+ return evaluateStaticNode(declaration?.initializer, sourceFile, bindings, { allowIdentifierFallback: false });
863
+ }
864
+
865
+ function parseExampleBindings(code) {
866
+ const bindings = {};
867
+ const declarationPattern = /(?:const|let|var)\s+([\w$\u00a0-\uffff]+)\s*=\s*([^;\n]+)/g;
868
+ let match;
869
+
870
+ while ((match = declarationPattern.exec(code))) {
871
+ const [, name, expression] = match;
872
+ const value = evaluateStaticExpression(expression, bindings);
873
+ if (value !== undefined) bindings[name] = value;
874
+ }
875
+
876
+ return bindings;
877
+ }
878
+
879
+ function parseExampleExpression(expression, bindings = {}) {
880
+ const value = expression.trim();
881
+ if (!value) return undefined;
882
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
883
+ if (value === 'true') return true;
884
+ if (value === 'false') return false;
885
+ if (value === 'null') return null;
886
+
887
+ const quoted = value.match(/^(['"])([\s\S]*)\1$/);
888
+ if (quoted) return decodeHtmlText(quoted[2]);
889
+
890
+ if (value.startsWith('{') || value.startsWith('[')) {
891
+ try {
892
+ return JSON.parse(value);
893
+ } catch {
894
+ // Fall through to the TypeScript AST evaluator for JS-style literals.
895
+ }
896
+ }
897
+
898
+ return evaluateStaticExpression(value, bindings);
899
+ }
900
+
901
+ function coerceStaticExampleValue(rawValue, prop) {
902
+ if (rawValue === undefined) return true;
903
+
904
+ const type = prop?.type || '';
905
+ const value = decodeHtmlText(rawValue);
906
+ if (type.includes('array') || type.includes('Array') || type.includes('object') || type.includes('Object')) {
907
+ const parsed = parseExampleExpression(value);
908
+ if (parsed !== undefined) return parsed;
909
+ }
910
+
911
+ if (type.includes('boolean')) return value === 'true';
912
+ if (type.includes('number') || type.includes('Number')) {
913
+ const numberValue = Number(value);
914
+ return Number.isFinite(numberValue) ? numberValue : value;
915
+ }
916
+
917
+ return value;
918
+ }
919
+
920
+ function normalizeSlotContent(content) {
921
+ return decodeHtmlText(content)
922
+ .replace(/<[^>]+>/g, '')
923
+ .replace(/\s+/g, ' ')
924
+ .trim();
925
+ }
926
+
927
+ const previewKinds = new Set([
928
+ 'inline',
929
+ 'form',
930
+ 'data-list',
931
+ 'measure',
932
+ 'overlay',
933
+ 'page-shell',
934
+ 'native',
935
+ 'composite',
936
+ ]);
937
+
938
+ const commentPreviewKindRules = [
939
+ {
940
+ kind: 'page-shell',
941
+ keywords: ['页面根', '页面容器', '页面壳', '页面布局', '标题栏', '导航栏', '状态栏', '安全区', 'safearea', 'status bar'],
942
+ },
943
+ {
944
+ kind: 'measure',
945
+ keywords: ['跑马灯', '内容超出', '超出时', '横向滚动', '循环滚动', '滚动到', '吸顶', '尺寸测量', '测量', 'selectorquery'],
946
+ },
947
+ {
948
+ kind: 'overlay',
949
+ keywords: ['弹窗', '弹层', '浮层', '遮罩', 'popover', 'popup', 'dialog', 'modal'],
950
+ },
951
+ {
952
+ kind: 'native',
953
+ keywords: ['上传', '图片预览', '预览图片', '二维码', 'qrcode', 'canvas', 'video', '视频', '拨打', '联系', '原生能力', '相册'],
954
+ },
955
+ {
956
+ kind: 'composite',
957
+ keywords: ['装修', '设计页', '楼层', '复合', '业务组件', '复杂业务'],
958
+ },
959
+ {
960
+ kind: 'data-list',
961
+ keywords: ['列表', '数据列表', '树形', '树节点', '时间线', '步骤', '瀑布流', 'tabs', '选项卡'],
962
+ },
963
+ {
964
+ kind: 'form',
965
+ keywords: ['输入', '表单', '选择', '选择器', '勾选', '单选', '多选', '开关', '验证码', 'textarea', 'input', 'radio', 'checkbox', 'switch'],
966
+ },
967
+ ];
968
+
969
+ function normalizePreviewKind(kind) {
970
+ return previewKinds.has(kind) ? kind : '';
971
+ }
972
+
973
+ function textIncludesAny(text, keywords) {
974
+ const normalizedText = text.toLowerCase();
975
+
976
+ return keywords.some((keyword) => normalizedText.includes(keyword.toLowerCase()));
977
+ }
978
+
979
+ function inferPreviewKindFromText(text) {
980
+ if (!text) return '';
981
+
982
+ for (const rule of commentPreviewKindRules) {
983
+ if (textIncludesAny(text, rule.keywords)) return rule.kind;
984
+ }
985
+
986
+ return '';
987
+ }
988
+
989
+ function inferPreviewKindFromStructure(componentName, props, events, slots) {
990
+ const lowerName = componentName.toLowerCase();
991
+ const propNames = new Set(props.map((prop) => prop.name));
992
+ const hasModel = propNames.has('modelValue');
993
+ const hasListData = props.some((prop) => ['items', 'list', 'range', 'tabs', 'arr', 'data', 'options'].includes(prop.name));
994
+ const hasUploadLike = /upload|qrcode|video|contact/.test(lowerName);
995
+ const hasPageShellName = /page|header|footer|safearea|status-bar/.test(lowerName);
996
+ const hasOverlayName = /dialog|pop|popover|modal|popup/.test(lowerName);
997
+ const hasMeasureName = /marquee|tabs-scroll|swiper/.test(lowerName);
998
+ const hasDataName = /list|tree|timeline|steps|tabs/.test(lowerName);
999
+
1000
+ if (hasPageShellName) return 'page-shell';
1001
+ if (hasMeasureName) return 'measure';
1002
+ if (hasOverlayName) return 'overlay';
1003
+ if (hasUploadLike) return 'native';
1004
+ if (/design|calculate|citys/.test(lowerName)) return 'composite';
1005
+ if (hasDataName || hasListData) return 'data-list';
1006
+ if (hasModel || /input|textarea|radio|checkbox|switch|slider|sms|search/.test(lowerName)) return 'form';
1007
+ if (events.some((event) => event.name === 'click') || slots.length) return 'inline';
1008
+
1009
+ return 'inline';
1010
+ }
1011
+
1012
+ function inferPreviewKind(componentName, props, events, slots, jsdoc, metaPreview) {
1013
+ const explicitKind = normalizePreviewKind(metaPreview?.kind);
1014
+ if (explicitKind) return explicitKind;
1015
+
1016
+ const jsdocKind = inferPreviewKindFromText(jsdoc.description);
1017
+ if (jsdocKind) return jsdocKind;
1018
+
1019
+ return inferPreviewKindFromStructure(componentName, props, events, slots);
1020
+ }
1021
+
1022
+ function isBooleanType(type = '') {
1023
+ return type.toLowerCase().includes('boolean');
1024
+ }
1025
+
1026
+ function isNumberType(type = '') {
1027
+ return type.includes('number') || type.includes('Number');
1028
+ }
1029
+
1030
+ function isArrayType(type = '') {
1031
+ return type.includes('array') || type.includes('Array') || type.endsWith('[]');
1032
+ }
1033
+
1034
+ function isObjectType(type = '') {
1035
+ return type.includes('object') || type.includes('Object') || /\{[\s\S]*\}/.test(type);
1036
+ }
1037
+
1038
+ function getPropByName(props, name) {
1039
+ return props.find((prop) => prop.name === name);
1040
+ }
1041
+
1042
+ function getFirstOptionValue(previewProps) {
1043
+ const options = previewProps.items || previewProps.options || previewProps.range || previewProps.tabs || previewProps.list;
1044
+ if (!Array.isArray(options) || !options.length) return undefined;
1045
+
1046
+ const first = options[0];
1047
+ if (first && typeof first === 'object') return first.value ?? first.id ?? first.key ?? first.name ?? first.label;
1048
+
1049
+ return first;
1050
+ }
1051
+
1052
+ function createListMock(componentName, propName) {
1053
+ if (/timeline/.test(componentName)) {
1054
+ return [
1055
+ { label: '提交订单', time: '10:00' },
1056
+ { label: '商家确认', time: '10:30' },
1057
+ ];
1058
+ }
1059
+
1060
+ if (/steps/.test(componentName)) {
1061
+ return [
1062
+ { name: '提交订单', status: 'done' },
1063
+ { name: '支付成功', status: 'active' },
1064
+ { name: '等待发货', status: 'todo' },
1065
+ ];
1066
+ }
1067
+
1068
+ if (/tree/.test(componentName)) {
1069
+ return [
1070
+ {
1071
+ label: '一级节点',
1072
+ value: '1',
1073
+ children: [{ label: '二级节点', value: '1-1' }],
1074
+ },
1075
+ ];
1076
+ }
1077
+
1078
+ if (/radar/.test(componentName)) {
1079
+ return [
1080
+ { n: '服务', p: 90 },
1081
+ { n: '物流', p: 80 },
1082
+ { n: '质量', p: 88 },
1083
+ ];
1084
+ }
1085
+
1086
+ if (propName === 'tabs') return ['精选', '最新', '销量'];
1087
+ if (propName === 'range') return ['男', '女', '保密'];
1088
+ if (propName === 'arr') {
1089
+ return [
1090
+ { src: '/static/icon/avatar.svg', title: '示例图片' },
1091
+ { src: '/static/icon/tab-home.png', title: '示例图片' },
1092
+ ];
1093
+ }
1094
+
1095
+ return [
1096
+ { label: '选项一', value: '1' },
1097
+ { label: '选项二', value: '2' },
1098
+ ];
1099
+ }
1100
+
1101
+ function createObjectMock(propName) {
1102
+ if (propName === 'value') return { label: '示例', value: '1' };
1103
+
1104
+ return { label: '示例', value: '1' };
1105
+ }
1106
+
1107
+ function createNamedPropMock(componentName, prop, kind, previewProps) {
1108
+ const { name, type } = prop;
1109
+
1110
+ if (name === 'modelValue') {
1111
+ const firstOptionValue = getFirstOptionValue(previewProps);
1112
+ if (firstOptionValue !== undefined) return firstOptionValue;
1113
+ if (kind === 'overlay' && isBooleanType(type)) return false;
1114
+ if (isBooleanType(type)) return true;
1115
+ if (isNumberType(type)) return 1;
1116
+
1117
+ return '1';
1118
+ }
1119
+
1120
+ if (name === 'checked') return true;
1121
+ if (name === 'active') return 0;
1122
+ if (name === 'percent') return 60;
1123
+ if (name === 'num' || name === 'count') return 3;
1124
+ if (name === 'phone') return '13800138000';
1125
+ if (name === 'text') {
1126
+ return kind === 'measure' || /marquee/.test(componentName)
1127
+ ? '这是一段足够长的公告文字,用于触发跑马灯滚动预览效果'
1128
+ : '示例文本';
1129
+ }
1130
+ if (name === 'title') return '标题';
1131
+ if (name === 'placeholder') return '请选择';
1132
+ if (name === 'src' || name === 'poster' || name === 'icon') return '/static/icon/avatar.svg';
1133
+ if (name === 'code') return 'preview-code';
1134
+ if (name === 'label') return 'label';
1135
+ if (name === 'time') return 'time';
1136
+ if (name === 'valueKey') return 'value';
1137
+ if (name === 'labelKey') return 'label';
1138
+
1139
+ if (['items', 'list', 'range', 'tabs', 'arr', 'options'].includes(name) || isArrayType(type)) {
1140
+ return createListMock(componentName, name);
1141
+ }
1142
+
1143
+ if (isObjectType(type)) return createObjectMock(name);
1144
+ if (isBooleanType(type)) return false;
1145
+ if (isNumberType(type)) return 1;
1146
+
1147
+ return '';
1148
+ }
1149
+
1150
+ function shouldAutoMockProp(prop, kind) {
1151
+ if (kind === 'overlay') return ['modelValue', 'title'].includes(prop.name);
1152
+ if (kind === 'measure') return ['text', 'tabs', 'arr', 'list'].includes(prop.name);
1153
+ if (kind === 'data-list') return ['items', 'list', 'range', 'tabs', 'arr', 'options', 'active', 'modelValue'].includes(prop.name);
1154
+ if (kind === 'form') return ['items', 'range', 'options', 'tabs', 'modelValue'].includes(prop.name);
1155
+
1156
+ return false;
1157
+ }
1158
+
1159
+ function applyPreviewMocks(componentName, props, preview, kind) {
1160
+ const previewProps = { ...(preview.props || {}) };
1161
+
1162
+ for (const prop of props) {
1163
+ if (Object.prototype.hasOwnProperty.call(previewProps, prop.name)) continue;
1164
+ if (!shouldAutoMockProp(prop, kind)) continue;
1165
+
1166
+ const value = createNamedPropMock(componentName, prop, kind, previewProps);
1167
+ if (value !== '' && value !== undefined) previewProps[prop.name] = value;
1168
+ }
1169
+
1170
+ if (kind === 'overlay') {
1171
+ const modelProp = getPropByName(props, 'modelValue');
1172
+ if (modelProp && isBooleanType(modelProp.type)) previewProps.modelValue = false;
1173
+ }
1174
+
1175
+ const previewSlots = { ...(preview.slots || {}) };
1176
+ if (!previewSlots.default && kind === 'overlay') previewSlots.default = '这里是弹层内容';
1177
+ if (!previewSlots.default && props.length === 0) previewSlots.default = '内容';
1178
+
1179
+ return {
1180
+ ...preview,
1181
+ props: previewProps,
1182
+ slots: previewSlots,
1183
+ };
1184
+ }
1185
+
1186
+ function inferPreviewFromExamples(componentName, props, examples, kind) {
1187
+ const escapedName = escapeRegExp(componentName);
1188
+ const propMap = new Map(props.map((prop) => [prop.name.toLowerCase(), prop]));
1189
+
1190
+ for (const example of examples) {
1191
+ const code = example.code || '';
1192
+ const bindings = parseExampleBindings(code);
1193
+ const closedMatch = code.match(new RegExp(`<${escapedName}\\b([^>]*)>([\\s\\S]*?)<\\/${escapedName}>`, 'i'));
1194
+ const selfClosingMatch = closedMatch ? null : code.match(new RegExp(`<${escapedName}\\b([^>]*)\\/\\s*>`, 'i'));
1195
+ const match = closedMatch || selfClosingMatch;
1196
+ if (!match) continue;
1197
+
1198
+ const attrs = match[1] || '';
1199
+ const propsFromExample = {};
1200
+ const slots = {};
1201
+ const preview = {
1202
+ source: 'example',
1203
+ code: example.code,
1204
+ props: propsFromExample,
1205
+ slots,
1206
+ };
1207
+ const attrPattern = /([:@]?[\w:$-]+|v-model(?::[\w-]+)?)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
1208
+ let attrMatch;
1209
+
1210
+ while ((attrMatch = attrPattern.exec(attrs))) {
1211
+ const rawName = attrMatch[1];
1212
+ const rawValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
1213
+ if (!rawName || rawName.startsWith('@')) continue;
1214
+
1215
+ if (rawName.startsWith('v-model')) {
1216
+ const [, modelName] = rawName.split(':');
1217
+ const propName = modelName ? kebabToCamel(modelName) : 'modelValue';
1218
+ const prop = propMap.get(propName.toLowerCase());
1219
+ preview.vModel = {
1220
+ prop: propName,
1221
+ variable: rawValue || 'value',
1222
+ };
1223
+ if (prop?.default !== null && prop?.default !== undefined && prop.default !== '') {
1224
+ propsFromExample[propName] = prop.default;
1225
+ }
1226
+ continue;
1227
+ }
1228
+
1229
+ const isDynamic = rawName.startsWith(':');
1230
+ const attrName = isDynamic ? rawName.slice(1) : rawName;
1231
+ const propName = kebabToCamel(attrName);
1232
+ const prop = propMap.get(propName.toLowerCase());
1233
+ if (!prop) continue;
1234
+
1235
+ let value = isDynamic ? parseExampleExpression(rawValue || '', bindings) : coerceStaticExampleValue(rawValue, prop);
1236
+ if (isDynamic && value === undefined) {
1237
+ value = createNamedPropMock(componentName, prop, kind, propsFromExample);
1238
+ }
1239
+ if (value !== undefined && value !== '') propsFromExample[prop.name] = value;
1240
+ }
1241
+
1242
+ if (closedMatch) {
1243
+ const defaultSlot = normalizeSlotContent(closedMatch[2] || '');
1244
+ if (defaultSlot) slots.default = defaultSlot;
1245
+ }
1246
+
1247
+ return preview;
1248
+ }
1249
+
1250
+ return null;
1251
+ }
1252
+
1253
+ function normalizePreviewConfig(preview) {
1254
+ if (!preview || typeof preview !== 'object') return null;
1255
+
1256
+ return {
1257
+ source: preview.source || 'meta',
1258
+ kind: normalizePreviewKind(preview.kind),
1259
+ code: preview.code,
1260
+ props: preview.props && typeof preview.props === 'object' ? preview.props : {},
1261
+ slots: preview.slots && typeof preview.slots === 'object' ? preview.slots : preview.slot ? { default: preview.slot } : {},
1262
+ vModel: preview.vModel,
1263
+ };
1264
+ }
1265
+
1266
+ function buildPreview(componentName, props, events, slots, examples, jsdoc, metaPreview) {
1267
+ const kind = inferPreviewKind(componentName, props, events, slots, jsdoc, metaPreview);
1268
+ const preview = normalizePreviewConfig(metaPreview) || inferPreviewFromExamples(componentName, props, examples, kind) || {
1269
+ source: 'inferred',
1270
+ props: {},
1271
+ slots: {},
1272
+ };
1273
+
1274
+ return {
1275
+ ...applyPreviewMocks(componentName, props, preview, kind),
1276
+ kind: preview.kind || kind,
1277
+ };
1278
+ }
1279
+
690
1280
  function findAllComponentDirs() {
691
1281
  if (!fs.existsSync(componentRoot)) return [];
692
1282
 
@@ -758,69 +1348,10 @@ function buildUsageHints(componentName, title, props, events, slots) {
758
1348
  return { useWhen, avoidWhen, notes };
759
1349
  }
760
1350
 
761
- function buildSamplePropValue(prop) {
762
- if (prop.default !== null && prop.default !== undefined && prop.default !== '') return prop.default;
763
- if (prop.values?.length) return prop.values[0];
764
-
765
- const type = prop.type || '';
766
- const name = prop.name.toLowerCase();
767
-
768
- if (type.includes('boolean')) return false;
769
- if (type.includes('number') || type.includes('Number')) {
770
- if (name.includes('percent')) return 60;
771
- if (name.includes('max')) return 10;
772
- if (name.includes('min')) return 0;
773
- if (name.includes('size')) return 32;
774
- if (name === 'w' || name.includes('width')) return 240;
775
- if (name === 'h' || name.includes('height')) return 80;
776
- return 1;
777
- }
778
- if (type.includes('Array') || type.includes('array') || ['items', 'list', 'range', 'tabs', 'arr'].includes(prop.name)) {
779
- return [
780
- { label: '选项一', value: 'one' },
781
- { label: '选项二', value: 'two' },
782
- ];
783
- }
784
- if (type.includes('Object') || type.includes('object')) return {};
785
- if (name.includes('placeholder')) return '请输入';
786
- if (name.includes('title')) return '标题';
787
- if (name.includes('label')) return '标签';
788
- if (name.includes('text') || name.includes('content')) return '示例内容';
789
- if (name.includes('phone')) return '13800000000';
790
- if (name === 'icon') return '/static/icon/avatar.svg';
791
- if (name === 'src' || name.includes('poster')) return '';
792
-
793
- return '';
794
- }
795
-
796
- function renderExampleFromProps(componentName, props) {
797
- const importantProps = props
798
- .filter((prop) => prop.required || ['modelValue', 'title', 'text', 'label', 'items', 'range', 'tabs', 'src', 'icon', 'type', 'size'].includes(prop.name))
799
- .slice(0, 5);
800
- const attrs = importantProps
801
- .map((prop) => {
802
- if (prop.name === 'modelValue') return 'v-model="value"';
803
- const value = buildSamplePropValue(prop);
804
- if (typeof value === 'boolean') return value ? prop.name : `:${prop.name}="false"`;
805
- if (typeof value === 'number') return `:${prop.name}="${value}"`;
806
- if (Array.isArray(value) || (value && typeof value === 'object')) return `:${prop.name}="${prop.name}"`;
807
- if (!value) return '';
808
-
809
- return `${prop.name}="${value}"`;
810
- })
811
- .filter(Boolean)
812
- .join(' ');
813
- const attrText = attrs ? ` ${attrs}` : '';
814
-
815
- return `<${componentName}${attrText}>示例内容</${componentName}>`;
816
- }
817
-
818
1351
  function inferComponentInfo(componentDir) {
819
1352
  const componentName = path.basename(componentDir);
820
1353
  const componentFile = findComponentFile(componentDir);
821
- const sfcSource = fs.readFileSync(componentFile, 'utf8');
822
- const script = extractSfcBlock(sfcSource, 'script', (attrs) => attrs.includes('setup'));
823
- const template = extractSfcBlock(sfcSource, 'template');
1354
+ const { script, template } = readSfcBlocks(componentFile);
824
1355
  const jsdoc = parseJsdoc(script);
825
1356
  const aliases = collectTypeAliases(componentDir);
826
1357
  const props = extractProps(script, componentFile, aliases, jsdoc);
@@ -842,7 +1373,6 @@ function inferInitialMeta(componentDir) {
842
1373
  const examples = normalizeExamples([], jsdoc.examples);
843
1374
  const title = jsdoc.description?.split('\n')[0]?.trim() || toTitleCase(componentName);
844
1375
  const usageHints = buildUsageHints(componentName, title, props, events, slots);
845
- const exampleCode = renderExampleFromProps(componentName, props);
846
1376
 
847
1377
  return {
848
1378
  title,
@@ -852,15 +1382,7 @@ function inferInitialMeta(componentDir) {
852
1382
  avoidWhen: usageHints.avoidWhen,
853
1383
  related: [],
854
1384
  notes: usageHints.notes,
855
- examples: examples.length
856
- ? examples
857
- : [
858
- {
859
- title: '基础用法',
860
- description: `自动根据 ${componentName} 的 props 生成,可按业务语义继续调整。`,
861
- code: exampleCode,
862
- },
863
- ],
1385
+ examples,
864
1386
  };
865
1387
  }
866
1388
 
@@ -881,6 +1403,10 @@ function mergeMeta(existingMeta, inferredMeta) {
881
1403
  }
882
1404
  }
883
1405
 
1406
+ if (Array.isArray(merged.examples)) {
1407
+ merged.examples = normalizeExamples(merged.examples, inferredMeta.examples || []);
1408
+ }
1409
+
884
1410
  return merged;
885
1411
  }
886
1412
 
@@ -892,7 +1418,30 @@ function isAutoGeneratedDocPage(filePath) {
892
1418
  }
893
1419
 
894
1420
  function renderGeneratedControlStyles() {
895
- return `.control-panel,
1421
+ return `.preview-actions {
1422
+ display: flex;
1423
+ flex-direction: row;
1424
+ justify-content: flex-start;
1425
+ width: 100%;
1426
+ margin-bottom: 16rpx;
1427
+ }
1428
+
1429
+ .preview-open-btn {
1430
+ box-sizing: border-box;
1431
+ display: inline-flex;
1432
+ align-items: center;
1433
+ justify-content: center;
1434
+ min-height: 64rpx;
1435
+ padding: 0 24rpx;
1436
+ font-size: 26rpx;
1437
+ font-weight: 600;
1438
+ line-height: 1.35;
1439
+ color: #fff;
1440
+ background: #2563eb;
1441
+ border-radius: 8rpx;
1442
+ }
1443
+
1444
+ .control-panel,
896
1445
  .control-list {
897
1446
  box-sizing: border-box;
898
1447
  display: flex;
@@ -1035,6 +1584,7 @@ function renderAutoDocPage(componentName, componentFile) {
1035
1584
  const importName = componentName
1036
1585
  .replace(/(^|-)(\w)/g, (_, __, char) => char.toUpperCase())
1037
1586
  .replace(/[^\w]/g, '');
1587
+ const dataModuleImport = `./${getPageDataModuleFileName()}`;
1038
1588
  const {
1039
1589
  componentDocPageImport,
1040
1590
  autoDocHookImport,
@@ -1048,13 +1598,18 @@ function renderAutoDocPage(componentName, componentFile) {
1048
1598
  import ${componentDocPageName} from '${componentDocPageImport}';
1049
1599
  import { ${autoDocHookName} } from '${autoDocHookImport}';
1050
1600
  import ${importName} from '${componentImportBase}/${componentName}/${fileName}';
1051
- import doc from './data.json';
1601
+ import doc from '${dataModuleImport}';
1052
1602
 
1053
1603
  const {
1054
1604
  propControls,
1055
- previewProps,
1605
+ renderedPreviewProps,
1606
+ previewKey,
1056
1607
  slotText,
1057
1608
  codeSnippet,
1609
+ isOverlayPreview,
1610
+ overlayTriggerText,
1611
+ openOverlayPreview,
1612
+ handlePreviewModelValueUpdate,
1058
1613
  isBooleanProp,
1059
1614
  isNumberProp,
1060
1615
  isArrayProp,
@@ -1070,7 +1625,11 @@ const {
1070
1625
  <${componentDocPageName} :doc="doc" :show-controls="doc.props.length > 0">
1071
1626
  <template #preview>
1072
1627
  <view class="preview-frame">
1073
- <${importName} v-bind="previewProps">
1628
+ <view v-if="isOverlayPreview" class="preview-actions">
1629
+ <text class="preview-open-btn" @click="openOverlayPreview">{{ overlayTriggerText }}</text>
1630
+ </view>
1631
+
1632
+ <${importName} :key="previewKey" v-bind="renderedPreviewProps" @update:modelValue="handlePreviewModelValueUpdate('modelValue', $event)">
1074
1633
  {{ slotText }}
1075
1634
  </${importName}>
1076
1635
  </view>
@@ -1144,10 +1703,11 @@ ${renderGeneratedControlStyles()}
1144
1703
 
1145
1704
  function renderGenericDocPage() {
1146
1705
  const { componentDocPageImport, componentDocPageName } = docsConfig.runtime;
1706
+ const dataModuleImport = `./${getPageDataModuleFileName()}`;
1147
1707
 
1148
1708
  return `<script setup lang="ts">
1149
1709
  import ${componentDocPageName} from '${componentDocPageImport}';
1150
- import doc from './data.json';
1710
+ import doc from '${dataModuleImport}';
1151
1711
  </script>
1152
1712
 
1153
1713
  <template>
@@ -1271,15 +1831,15 @@ function collectComponentDoc(componentDir) {
1271
1831
  if (!componentFile) return null;
1272
1832
 
1273
1833
  const componentName = path.basename(componentDir);
1274
- const sfcSource = fs.readFileSync(componentFile, 'utf8');
1275
- const script = extractSfcBlock(sfcSource, 'script', (attrs) => attrs.includes('setup'));
1276
- const template = extractSfcBlock(sfcSource, 'template');
1834
+ const { script, template } = readSfcBlocks(componentFile);
1277
1835
  const meta = readJson(getMetaFile(componentDir));
1278
1836
  const jsdoc = parseJsdoc(script);
1279
1837
  const aliases = collectTypeAliases(componentDir);
1280
1838
  const props = extractProps(script, componentFile, aliases, jsdoc);
1281
1839
  const events = extractEvents(script, componentFile, jsdoc);
1282
1840
  const slots = extractSlots(template);
1841
+ const examples = normalizeExamples(meta.examples, jsdoc.examples);
1842
+ const preview = buildPreview(componentName, props, events, slots, examples, jsdoc, meta.preview);
1283
1843
 
1284
1844
  const component = {
1285
1845
  name: componentName,
@@ -1291,7 +1851,8 @@ function collectComponentDoc(componentDir) {
1291
1851
  props,
1292
1852
  events,
1293
1853
  slots,
1294
- examples: normalizeExamples(meta.examples, jsdoc.examples),
1854
+ preview,
1855
+ examples,
1295
1856
  useWhen: meta.useWhen || [],
1296
1857
  avoidWhen: meta.avoidWhen || [],
1297
1858
  notes: meta.notes || [],
@@ -1621,13 +2182,24 @@ function main() {
1621
2182
  writeJson(path.join(aiDocsDir, docsConfig.output.catalogFile), catalog);
1622
2183
  writeText(path.join(aiDocsDir, docsConfig.output.aiIndexFile), renderAiIndex(catalog));
1623
2184
  writeText(path.join(aiDocsDir, docsConfig.output.llmsFile), renderLlmsTxt(catalog));
1624
- writeJson(indexDataFile, buildPageIndex(components));
2185
+ const pageIndex = buildPageIndex(components);
2186
+ writeJson(indexDataFile, pageIndex);
2187
+ writeDataModule(
2188
+ getIndexDataModuleFile(),
2189
+ pageIndex,
2190
+ toDataModuleIdentifier(docsConfig.projectName || 'docs-index', 'docsIndex'),
2191
+ );
1625
2192
  const qualityReport = collectQualityReport(components);
1626
2193
  writeJson(path.join(aiDocsDir, docsConfig.output.qualityFile), qualityReport);
1627
2194
 
1628
2195
  for (const component of components) {
1629
2196
  writeText(path.join(markdownDocsDir, `${component.name}.md`), renderComponentMarkdown(component));
1630
2197
  writeJson(path.join(pageDataRoot, component.name, 'data.json'), component);
2198
+ writeDataModule(
2199
+ path.join(pageDataRoot, component.name, getPageDataModuleFileName()),
2200
+ component,
2201
+ toDataModuleIdentifier(component.name, 'componentDoc'),
2202
+ );
1631
2203
  }
1632
2204
 
1633
2205
  console.log(`Generated component docs for ${components.length} component(s): ${components.map((item) => item.name).join(', ')}`);