component-auto-docs 0.1.1 → 0.1.2

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();
@@ -107,7 +108,7 @@ function createPageRouteEntry(routePath, title) {
107
108
  "style": {
108
109
  "navigationBarTitleText": "${title}"
109
110
  }
110
- },
111
+ },
111
112
  `;
112
113
  }
113
114
 
@@ -280,12 +281,29 @@ function readLiteralValue(node, sourceFile) {
280
281
  return node.getText(sourceFile);
281
282
  }
282
283
 
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]));
284
+ function formatSfcParseError(error) {
285
+ if (typeof error === 'string') return error;
286
+ if (error?.message) return error.message;
287
287
 
288
- return block?.[2] || '';
288
+ return String(error);
289
+ }
290
+
291
+ function readSfcBlocks(filePath) {
292
+ const source = fs.readFileSync(filePath, 'utf8');
293
+ const { descriptor, errors } = parseSfc(source, { filename: filePath });
294
+
295
+ if (errors.length) {
296
+ throw new Error(
297
+ `Failed to parse Vue SFC ${toPosix(path.relative(rootDir, filePath))}: ${errors
298
+ .map(formatSfcParseError)
299
+ .join('; ')}`,
300
+ );
301
+ }
302
+
303
+ return {
304
+ script: descriptor.scriptSetup?.content || descriptor.script?.content || '',
305
+ template: descriptor.template?.content || '',
306
+ };
289
307
  }
290
308
 
291
309
  function parseJsdoc(script) {
@@ -675,6 +693,7 @@ function normalizeExamples(metaExamples = [], jsdocExamples = []) {
675
693
  const normalized = typeof example === 'string' ? { title: '示例', description: '', code: example } : example;
676
694
  const code = normalized.code?.trim();
677
695
  if (!code || seen.has(code)) continue;
696
+ if (normalized.title === '基础用法' && normalized.description?.startsWith('自动根据 ')) continue;
678
697
 
679
698
  seen.add(code);
680
699
  examples.push({
@@ -687,6 +706,525 @@ function normalizeExamples(metaExamples = [], jsdocExamples = []) {
687
706
  return examples;
688
707
  }
689
708
 
709
+ function escapeRegExp(text) {
710
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
711
+ }
712
+
713
+ function kebabToCamel(text) {
714
+ return text.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
715
+ }
716
+
717
+ function decodeHtmlText(text) {
718
+ return text
719
+ .replace(/&lt;/g, '<')
720
+ .replace(/&gt;/g, '>')
721
+ .replace(/&quot;/g, '"')
722
+ .replace(/&#39;/g, "'")
723
+ .replace(/&amp;/g, '&');
724
+ }
725
+
726
+ function getObjectKeyText(nameNode, sourceFile) {
727
+ if (!nameNode) return '';
728
+ if (ts.isIdentifier(nameNode) || ts.isStringLiteralLike(nameNode) || ts.isNumericLiteral(nameNode)) {
729
+ return nameNode.text;
730
+ }
731
+
732
+ return nameNode.getText(sourceFile).replace(/^['"]|['"]$/g, '');
733
+ }
734
+
735
+ function getIdentifierSampleValue(name) {
736
+ const samples = {
737
+ text: '这是一段足够长的公告文字,用于触发滚动预览效果',
738
+ title: '标题',
739
+ content: '内容',
740
+ label: '选项',
741
+ value: '1',
742
+ name: '名称',
743
+ status: 'done',
744
+ time: '10:30',
745
+ src: '/static/icon/avatar.svg',
746
+ url: '/static/icon/avatar.svg',
747
+ poster: '/static/icon/avatar.svg',
748
+ };
749
+
750
+ return samples[name] || name;
751
+ }
752
+
753
+ function evaluateStaticNode(node, sourceFile, bindings = {}, options = {}) {
754
+ if (!node) return undefined;
755
+
756
+ if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
757
+ return evaluateStaticNode(node.expression, sourceFile, bindings, options);
758
+ }
759
+
760
+ if (ts.isStringLiteralLike(node) || ts.isNoSubstitutionTemplateLiteral(node)) return decodeHtmlText(node.text);
761
+ if (ts.isNumericLiteral(node)) return Number(node.text);
762
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
763
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
764
+ if (node.kind === ts.SyntaxKind.NullKeyword) return null;
765
+
766
+ if (ts.isIdentifier(node)) {
767
+ if (Object.prototype.hasOwnProperty.call(bindings, node.text)) return bindings[node.text];
768
+
769
+ return options.allowIdentifierFallback ? getIdentifierSampleValue(node.text) : undefined;
770
+ }
771
+
772
+ if (ts.isArrayLiteralExpression(node)) {
773
+ return node.elements
774
+ .map((item) => evaluateStaticNode(item, sourceFile, bindings, { ...options, allowIdentifierFallback: true }))
775
+ .filter((item) => item !== undefined);
776
+ }
777
+
778
+ if (ts.isObjectLiteralExpression(node)) {
779
+ const result = {};
780
+
781
+ for (const property of node.properties) {
782
+ if (ts.isPropertyAssignment(property)) {
783
+ const key = getObjectKeyText(property.name, sourceFile);
784
+ if (!key) continue;
785
+ const value = evaluateStaticNode(property.initializer, sourceFile, bindings, {
786
+ ...options,
787
+ allowIdentifierFallback: true,
788
+ });
789
+ if (value !== undefined) result[key] = value;
790
+ }
791
+
792
+ if (ts.isShorthandPropertyAssignment(property)) {
793
+ const key = property.name.text;
794
+ result[key] = Object.prototype.hasOwnProperty.call(bindings, key) ? bindings[key] : getIdentifierSampleValue(key);
795
+ }
796
+ }
797
+
798
+ return result;
799
+ }
800
+
801
+ return undefined;
802
+ }
803
+
804
+ function evaluateStaticExpression(expression, bindings = {}) {
805
+ const source = `const __value = (${expression});`;
806
+ const sourceFile = ts.createSourceFile('preview-expression.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
807
+ const statement = sourceFile.statements[0];
808
+ const declaration = statement?.declarationList?.declarations?.[0];
809
+
810
+ return evaluateStaticNode(declaration?.initializer, sourceFile, bindings, { allowIdentifierFallback: false });
811
+ }
812
+
813
+ function parseExampleBindings(code) {
814
+ const bindings = {};
815
+ const declarationPattern = /(?:const|let|var)\s+([\w$\u00a0-\uffff]+)\s*=\s*([^;\n]+)/g;
816
+ let match;
817
+
818
+ while ((match = declarationPattern.exec(code))) {
819
+ const [, name, expression] = match;
820
+ const value = evaluateStaticExpression(expression, bindings);
821
+ if (value !== undefined) bindings[name] = value;
822
+ }
823
+
824
+ return bindings;
825
+ }
826
+
827
+ function parseExampleExpression(expression, bindings = {}) {
828
+ const value = expression.trim();
829
+ if (!value) return undefined;
830
+ if (/^-?\d+(?:\.\d+)?$/.test(value)) return Number(value);
831
+ if (value === 'true') return true;
832
+ if (value === 'false') return false;
833
+ if (value === 'null') return null;
834
+
835
+ const quoted = value.match(/^(['"])([\s\S]*)\1$/);
836
+ if (quoted) return decodeHtmlText(quoted[2]);
837
+
838
+ if (value.startsWith('{') || value.startsWith('[')) {
839
+ try {
840
+ return JSON.parse(value);
841
+ } catch {
842
+ // Fall through to the TypeScript AST evaluator for JS-style literals.
843
+ }
844
+ }
845
+
846
+ return evaluateStaticExpression(value, bindings);
847
+ }
848
+
849
+ function coerceStaticExampleValue(rawValue, prop) {
850
+ if (rawValue === undefined) return true;
851
+
852
+ const type = prop?.type || '';
853
+ const value = decodeHtmlText(rawValue);
854
+ if (type.includes('array') || type.includes('Array') || type.includes('object') || type.includes('Object')) {
855
+ const parsed = parseExampleExpression(value);
856
+ if (parsed !== undefined) return parsed;
857
+ }
858
+
859
+ if (type.includes('boolean')) return value === 'true';
860
+ if (type.includes('number') || type.includes('Number')) {
861
+ const numberValue = Number(value);
862
+ return Number.isFinite(numberValue) ? numberValue : value;
863
+ }
864
+
865
+ return value;
866
+ }
867
+
868
+ function normalizeSlotContent(content) {
869
+ return decodeHtmlText(content)
870
+ .replace(/<[^>]+>/g, '')
871
+ .replace(/\s+/g, ' ')
872
+ .trim();
873
+ }
874
+
875
+ const previewKinds = new Set([
876
+ 'inline',
877
+ 'form',
878
+ 'data-list',
879
+ 'measure',
880
+ 'overlay',
881
+ 'page-shell',
882
+ 'native',
883
+ 'composite',
884
+ ]);
885
+
886
+ const commentPreviewKindRules = [
887
+ {
888
+ kind: 'page-shell',
889
+ keywords: ['页面根', '页面容器', '页面壳', '页面布局', '标题栏', '导航栏', '状态栏', '安全区', 'safearea', 'status bar'],
890
+ },
891
+ {
892
+ kind: 'measure',
893
+ keywords: ['跑马灯', '内容超出', '超出时', '横向滚动', '循环滚动', '滚动到', '吸顶', '尺寸测量', '测量', 'selectorquery'],
894
+ },
895
+ {
896
+ kind: 'overlay',
897
+ keywords: ['弹窗', '弹层', '浮层', '遮罩', 'popover', 'popup', 'dialog', 'modal'],
898
+ },
899
+ {
900
+ kind: 'native',
901
+ keywords: ['上传', '图片预览', '预览图片', '二维码', 'qrcode', 'canvas', 'video', '视频', '拨打', '联系', '原生能力', '相册'],
902
+ },
903
+ {
904
+ kind: 'composite',
905
+ keywords: ['装修', '设计页', '楼层', '复合', '业务组件', '复杂业务'],
906
+ },
907
+ {
908
+ kind: 'data-list',
909
+ keywords: ['列表', '数据列表', '树形', '树节点', '时间线', '步骤', '瀑布流', 'tabs', '选项卡'],
910
+ },
911
+ {
912
+ kind: 'form',
913
+ keywords: ['输入', '表单', '选择', '选择器', '勾选', '单选', '多选', '开关', '验证码', 'textarea', 'input', 'radio', 'checkbox', 'switch'],
914
+ },
915
+ ];
916
+
917
+ function normalizePreviewKind(kind) {
918
+ return previewKinds.has(kind) ? kind : '';
919
+ }
920
+
921
+ function textIncludesAny(text, keywords) {
922
+ const normalizedText = text.toLowerCase();
923
+
924
+ return keywords.some((keyword) => normalizedText.includes(keyword.toLowerCase()));
925
+ }
926
+
927
+ function inferPreviewKindFromText(text) {
928
+ if (!text) return '';
929
+
930
+ for (const rule of commentPreviewKindRules) {
931
+ if (textIncludesAny(text, rule.keywords)) return rule.kind;
932
+ }
933
+
934
+ return '';
935
+ }
936
+
937
+ function inferPreviewKindFromStructure(componentName, props, events, slots) {
938
+ const lowerName = componentName.toLowerCase();
939
+ const propNames = new Set(props.map((prop) => prop.name));
940
+ const hasModel = propNames.has('modelValue');
941
+ const hasListData = props.some((prop) => ['items', 'list', 'range', 'tabs', 'arr', 'data', 'options'].includes(prop.name));
942
+ const hasUploadLike = /upload|qrcode|video|contact/.test(lowerName);
943
+ const hasPageShellName = /page|header|footer|safearea|status-bar/.test(lowerName);
944
+ const hasOverlayName = /dialog|pop|popover|modal|popup/.test(lowerName);
945
+ const hasMeasureName = /marquee|tabs-scroll|swiper/.test(lowerName);
946
+ const hasDataName = /list|tree|timeline|steps|tabs/.test(lowerName);
947
+
948
+ if (hasPageShellName) return 'page-shell';
949
+ if (hasMeasureName) return 'measure';
950
+ if (hasOverlayName) return 'overlay';
951
+ if (hasUploadLike) return 'native';
952
+ if (/design|calculate|citys/.test(lowerName)) return 'composite';
953
+ if (hasDataName || hasListData) return 'data-list';
954
+ if (hasModel || /input|textarea|radio|checkbox|switch|slider|sms|search/.test(lowerName)) return 'form';
955
+ if (events.some((event) => event.name === 'click') || slots.length) return 'inline';
956
+
957
+ return 'inline';
958
+ }
959
+
960
+ function inferPreviewKind(componentName, props, events, slots, jsdoc, metaPreview) {
961
+ const explicitKind = normalizePreviewKind(metaPreview?.kind);
962
+ if (explicitKind) return explicitKind;
963
+
964
+ const jsdocKind = inferPreviewKindFromText(jsdoc.description);
965
+ if (jsdocKind) return jsdocKind;
966
+
967
+ return inferPreviewKindFromStructure(componentName, props, events, slots);
968
+ }
969
+
970
+ function isBooleanType(type = '') {
971
+ return type.toLowerCase().includes('boolean');
972
+ }
973
+
974
+ function isNumberType(type = '') {
975
+ return type.includes('number') || type.includes('Number');
976
+ }
977
+
978
+ function isArrayType(type = '') {
979
+ return type.includes('array') || type.includes('Array') || type.endsWith('[]');
980
+ }
981
+
982
+ function isObjectType(type = '') {
983
+ return type.includes('object') || type.includes('Object') || /\{[\s\S]*\}/.test(type);
984
+ }
985
+
986
+ function getPropByName(props, name) {
987
+ return props.find((prop) => prop.name === name);
988
+ }
989
+
990
+ function getFirstOptionValue(previewProps) {
991
+ const options = previewProps.items || previewProps.options || previewProps.range || previewProps.tabs || previewProps.list;
992
+ if (!Array.isArray(options) || !options.length) return undefined;
993
+
994
+ const first = options[0];
995
+ if (first && typeof first === 'object') return first.value ?? first.id ?? first.key ?? first.name ?? first.label;
996
+
997
+ return first;
998
+ }
999
+
1000
+ function createListMock(componentName, propName) {
1001
+ if (/timeline/.test(componentName)) {
1002
+ return [
1003
+ { label: '提交订单', time: '10:00' },
1004
+ { label: '商家确认', time: '10:30' },
1005
+ ];
1006
+ }
1007
+
1008
+ if (/steps/.test(componentName)) {
1009
+ return [
1010
+ { name: '提交订单', status: 'done' },
1011
+ { name: '支付成功', status: 'active' },
1012
+ { name: '等待发货', status: 'todo' },
1013
+ ];
1014
+ }
1015
+
1016
+ if (/tree/.test(componentName)) {
1017
+ return [
1018
+ {
1019
+ label: '一级节点',
1020
+ value: '1',
1021
+ children: [{ label: '二级节点', value: '1-1' }],
1022
+ },
1023
+ ];
1024
+ }
1025
+
1026
+ if (/radar/.test(componentName)) {
1027
+ return [
1028
+ { n: '服务', p: 90 },
1029
+ { n: '物流', p: 80 },
1030
+ { n: '质量', p: 88 },
1031
+ ];
1032
+ }
1033
+
1034
+ if (propName === 'tabs') return ['精选', '最新', '销量'];
1035
+ if (propName === 'range') return ['男', '女', '保密'];
1036
+ if (propName === 'arr') {
1037
+ return [
1038
+ { src: '/static/icon/avatar.svg', title: '示例图片' },
1039
+ { src: '/static/icon/tab-home.png', title: '示例图片' },
1040
+ ];
1041
+ }
1042
+
1043
+ return [
1044
+ { label: '选项一', value: '1' },
1045
+ { label: '选项二', value: '2' },
1046
+ ];
1047
+ }
1048
+
1049
+ function createObjectMock(propName) {
1050
+ if (propName === 'value') return { label: '示例', value: '1' };
1051
+
1052
+ return { label: '示例', value: '1' };
1053
+ }
1054
+
1055
+ function createNamedPropMock(componentName, prop, kind, previewProps) {
1056
+ const { name, type } = prop;
1057
+
1058
+ if (name === 'modelValue') {
1059
+ const firstOptionValue = getFirstOptionValue(previewProps);
1060
+ if (firstOptionValue !== undefined) return firstOptionValue;
1061
+ if (kind === 'overlay' && isBooleanType(type)) return false;
1062
+ if (isBooleanType(type)) return true;
1063
+ if (isNumberType(type)) return 1;
1064
+
1065
+ return '1';
1066
+ }
1067
+
1068
+ if (name === 'checked') return true;
1069
+ if (name === 'active') return 0;
1070
+ if (name === 'percent') return 60;
1071
+ if (name === 'num' || name === 'count') return 3;
1072
+ if (name === 'phone') return '13800138000';
1073
+ if (name === 'text') {
1074
+ return kind === 'measure' || /marquee/.test(componentName)
1075
+ ? '这是一段足够长的公告文字,用于触发跑马灯滚动预览效果'
1076
+ : '示例文本';
1077
+ }
1078
+ if (name === 'title') return '标题';
1079
+ if (name === 'placeholder') return '请选择';
1080
+ if (name === 'src' || name === 'poster' || name === 'icon') return '/static/icon/avatar.svg';
1081
+ if (name === 'code') return 'preview-code';
1082
+ if (name === 'label') return 'label';
1083
+ if (name === 'time') return 'time';
1084
+ if (name === 'valueKey') return 'value';
1085
+ if (name === 'labelKey') return 'label';
1086
+
1087
+ if (['items', 'list', 'range', 'tabs', 'arr', 'options'].includes(name) || isArrayType(type)) {
1088
+ return createListMock(componentName, name);
1089
+ }
1090
+
1091
+ if (isObjectType(type)) return createObjectMock(name);
1092
+ if (isBooleanType(type)) return false;
1093
+ if (isNumberType(type)) return 1;
1094
+
1095
+ return '';
1096
+ }
1097
+
1098
+ function shouldAutoMockProp(prop, kind) {
1099
+ if (kind === 'overlay') return ['modelValue', 'title'].includes(prop.name);
1100
+ if (kind === 'measure') return ['text', 'tabs', 'arr', 'list'].includes(prop.name);
1101
+ if (kind === 'data-list') return ['items', 'list', 'range', 'tabs', 'arr', 'options', 'active', 'modelValue'].includes(prop.name);
1102
+ if (kind === 'form') return ['items', 'range', 'options', 'tabs', 'modelValue'].includes(prop.name);
1103
+
1104
+ return false;
1105
+ }
1106
+
1107
+ function applyPreviewMocks(componentName, props, preview, kind) {
1108
+ const previewProps = { ...(preview.props || {}) };
1109
+
1110
+ for (const prop of props) {
1111
+ if (Object.prototype.hasOwnProperty.call(previewProps, prop.name)) continue;
1112
+ if (!shouldAutoMockProp(prop, kind)) continue;
1113
+
1114
+ const value = createNamedPropMock(componentName, prop, kind, previewProps);
1115
+ if (value !== '' && value !== undefined) previewProps[prop.name] = value;
1116
+ }
1117
+
1118
+ if (kind === 'overlay') {
1119
+ const modelProp = getPropByName(props, 'modelValue');
1120
+ if (modelProp && isBooleanType(modelProp.type)) previewProps.modelValue = false;
1121
+ }
1122
+
1123
+ const previewSlots = { ...(preview.slots || {}) };
1124
+ if (!previewSlots.default && kind === 'overlay') previewSlots.default = '这里是弹层内容';
1125
+ if (!previewSlots.default && props.length === 0) previewSlots.default = '内容';
1126
+
1127
+ return {
1128
+ ...preview,
1129
+ props: previewProps,
1130
+ slots: previewSlots,
1131
+ };
1132
+ }
1133
+
1134
+ function inferPreviewFromExamples(componentName, props, examples, kind) {
1135
+ const escapedName = escapeRegExp(componentName);
1136
+ const propMap = new Map(props.map((prop) => [prop.name.toLowerCase(), prop]));
1137
+
1138
+ for (const example of examples) {
1139
+ const code = example.code || '';
1140
+ const bindings = parseExampleBindings(code);
1141
+ const closedMatch = code.match(new RegExp(`<${escapedName}\\b([^>]*)>([\\s\\S]*?)<\\/${escapedName}>`, 'i'));
1142
+ const selfClosingMatch = closedMatch ? null : code.match(new RegExp(`<${escapedName}\\b([^>]*)\\/\\s*>`, 'i'));
1143
+ const match = closedMatch || selfClosingMatch;
1144
+ if (!match) continue;
1145
+
1146
+ const attrs = match[1] || '';
1147
+ const propsFromExample = {};
1148
+ const slots = {};
1149
+ const preview = {
1150
+ source: 'example',
1151
+ code: example.code,
1152
+ props: propsFromExample,
1153
+ slots,
1154
+ };
1155
+ const attrPattern = /([:@]?[\w:$-]+|v-model(?::[\w-]+)?)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
1156
+ let attrMatch;
1157
+
1158
+ while ((attrMatch = attrPattern.exec(attrs))) {
1159
+ const rawName = attrMatch[1];
1160
+ const rawValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
1161
+ if (!rawName || rawName.startsWith('@')) continue;
1162
+
1163
+ if (rawName.startsWith('v-model')) {
1164
+ const [, modelName] = rawName.split(':');
1165
+ const propName = modelName ? kebabToCamel(modelName) : 'modelValue';
1166
+ const prop = propMap.get(propName.toLowerCase());
1167
+ preview.vModel = {
1168
+ prop: propName,
1169
+ variable: rawValue || 'value',
1170
+ };
1171
+ if (prop?.default !== null && prop?.default !== undefined && prop.default !== '') {
1172
+ propsFromExample[propName] = prop.default;
1173
+ }
1174
+ continue;
1175
+ }
1176
+
1177
+ const isDynamic = rawName.startsWith(':');
1178
+ const attrName = isDynamic ? rawName.slice(1) : rawName;
1179
+ const propName = kebabToCamel(attrName);
1180
+ const prop = propMap.get(propName.toLowerCase());
1181
+ if (!prop) continue;
1182
+
1183
+ let value = isDynamic ? parseExampleExpression(rawValue || '', bindings) : coerceStaticExampleValue(rawValue, prop);
1184
+ if (isDynamic && value === undefined) {
1185
+ value = createNamedPropMock(componentName, prop, kind, propsFromExample);
1186
+ }
1187
+ if (value !== undefined && value !== '') propsFromExample[prop.name] = value;
1188
+ }
1189
+
1190
+ if (closedMatch) {
1191
+ const defaultSlot = normalizeSlotContent(closedMatch[2] || '');
1192
+ if (defaultSlot) slots.default = defaultSlot;
1193
+ }
1194
+
1195
+ return preview;
1196
+ }
1197
+
1198
+ return null;
1199
+ }
1200
+
1201
+ function normalizePreviewConfig(preview) {
1202
+ if (!preview || typeof preview !== 'object') return null;
1203
+
1204
+ return {
1205
+ source: preview.source || 'meta',
1206
+ kind: normalizePreviewKind(preview.kind),
1207
+ code: preview.code,
1208
+ props: preview.props && typeof preview.props === 'object' ? preview.props : {},
1209
+ slots: preview.slots && typeof preview.slots === 'object' ? preview.slots : preview.slot ? { default: preview.slot } : {},
1210
+ vModel: preview.vModel,
1211
+ };
1212
+ }
1213
+
1214
+ function buildPreview(componentName, props, events, slots, examples, jsdoc, metaPreview) {
1215
+ const kind = inferPreviewKind(componentName, props, events, slots, jsdoc, metaPreview);
1216
+ const preview = normalizePreviewConfig(metaPreview) || inferPreviewFromExamples(componentName, props, examples, kind) || {
1217
+ source: 'inferred',
1218
+ props: {},
1219
+ slots: {},
1220
+ };
1221
+
1222
+ return {
1223
+ ...applyPreviewMocks(componentName, props, preview, kind),
1224
+ kind: preview.kind || kind,
1225
+ };
1226
+ }
1227
+
690
1228
  function findAllComponentDirs() {
691
1229
  if (!fs.existsSync(componentRoot)) return [];
692
1230
 
@@ -758,69 +1296,10 @@ function buildUsageHints(componentName, title, props, events, slots) {
758
1296
  return { useWhen, avoidWhen, notes };
759
1297
  }
760
1298
 
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
1299
  function inferComponentInfo(componentDir) {
819
1300
  const componentName = path.basename(componentDir);
820
1301
  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');
1302
+ const { script, template } = readSfcBlocks(componentFile);
824
1303
  const jsdoc = parseJsdoc(script);
825
1304
  const aliases = collectTypeAliases(componentDir);
826
1305
  const props = extractProps(script, componentFile, aliases, jsdoc);
@@ -842,7 +1321,6 @@ function inferInitialMeta(componentDir) {
842
1321
  const examples = normalizeExamples([], jsdoc.examples);
843
1322
  const title = jsdoc.description?.split('\n')[0]?.trim() || toTitleCase(componentName);
844
1323
  const usageHints = buildUsageHints(componentName, title, props, events, slots);
845
- const exampleCode = renderExampleFromProps(componentName, props);
846
1324
 
847
1325
  return {
848
1326
  title,
@@ -852,15 +1330,7 @@ function inferInitialMeta(componentDir) {
852
1330
  avoidWhen: usageHints.avoidWhen,
853
1331
  related: [],
854
1332
  notes: usageHints.notes,
855
- examples: examples.length
856
- ? examples
857
- : [
858
- {
859
- title: '基础用法',
860
- description: `自动根据 ${componentName} 的 props 生成,可按业务语义继续调整。`,
861
- code: exampleCode,
862
- },
863
- ],
1333
+ examples,
864
1334
  };
865
1335
  }
866
1336
 
@@ -881,6 +1351,10 @@ function mergeMeta(existingMeta, inferredMeta) {
881
1351
  }
882
1352
  }
883
1353
 
1354
+ if (Array.isArray(merged.examples)) {
1355
+ merged.examples = normalizeExamples(merged.examples, inferredMeta.examples || []);
1356
+ }
1357
+
884
1358
  return merged;
885
1359
  }
886
1360
 
@@ -892,7 +1366,30 @@ function isAutoGeneratedDocPage(filePath) {
892
1366
  }
893
1367
 
894
1368
  function renderGeneratedControlStyles() {
895
- return `.control-panel,
1369
+ return `.preview-actions {
1370
+ display: flex;
1371
+ flex-direction: row;
1372
+ justify-content: flex-start;
1373
+ width: 100%;
1374
+ margin-bottom: 16rpx;
1375
+ }
1376
+
1377
+ .preview-open-btn {
1378
+ box-sizing: border-box;
1379
+ display: inline-flex;
1380
+ align-items: center;
1381
+ justify-content: center;
1382
+ min-height: 64rpx;
1383
+ padding: 0 24rpx;
1384
+ font-size: 26rpx;
1385
+ font-weight: 600;
1386
+ line-height: 1.35;
1387
+ color: #fff;
1388
+ background: #2563eb;
1389
+ border-radius: 8rpx;
1390
+ }
1391
+
1392
+ .control-panel,
896
1393
  .control-list {
897
1394
  box-sizing: border-box;
898
1395
  display: flex;
@@ -1052,9 +1549,14 @@ import doc from './data.json';
1052
1549
 
1053
1550
  const {
1054
1551
  propControls,
1055
- previewProps,
1552
+ renderedPreviewProps,
1553
+ previewKey,
1056
1554
  slotText,
1057
1555
  codeSnippet,
1556
+ isOverlayPreview,
1557
+ overlayTriggerText,
1558
+ openOverlayPreview,
1559
+ handlePreviewModelValueUpdate,
1058
1560
  isBooleanProp,
1059
1561
  isNumberProp,
1060
1562
  isArrayProp,
@@ -1070,7 +1572,11 @@ const {
1070
1572
  <${componentDocPageName} :doc="doc" :show-controls="doc.props.length > 0">
1071
1573
  <template #preview>
1072
1574
  <view class="preview-frame">
1073
- <${importName} v-bind="previewProps">
1575
+ <view v-if="isOverlayPreview" class="preview-actions">
1576
+ <text class="preview-open-btn" @click="openOverlayPreview">{{ overlayTriggerText }}</text>
1577
+ </view>
1578
+
1579
+ <${importName} :key="previewKey" v-bind="renderedPreviewProps" @update:modelValue="handlePreviewModelValueUpdate('modelValue', $event)">
1074
1580
  {{ slotText }}
1075
1581
  </${importName}>
1076
1582
  </view>
@@ -1271,15 +1777,15 @@ function collectComponentDoc(componentDir) {
1271
1777
  if (!componentFile) return null;
1272
1778
 
1273
1779
  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');
1780
+ const { script, template } = readSfcBlocks(componentFile);
1277
1781
  const meta = readJson(getMetaFile(componentDir));
1278
1782
  const jsdoc = parseJsdoc(script);
1279
1783
  const aliases = collectTypeAliases(componentDir);
1280
1784
  const props = extractProps(script, componentFile, aliases, jsdoc);
1281
1785
  const events = extractEvents(script, componentFile, jsdoc);
1282
1786
  const slots = extractSlots(template);
1787
+ const examples = normalizeExamples(meta.examples, jsdoc.examples);
1788
+ const preview = buildPreview(componentName, props, events, slots, examples, jsdoc, meta.preview);
1283
1789
 
1284
1790
  const component = {
1285
1791
  name: componentName,
@@ -1291,7 +1797,8 @@ function collectComponentDoc(componentDir) {
1291
1797
  props,
1292
1798
  events,
1293
1799
  slots,
1294
- examples: normalizeExamples(meta.examples, jsdoc.examples),
1800
+ preview,
1801
+ examples,
1295
1802
  useWhen: meta.useWhen || [],
1296
1803
  avoidWhen: meta.avoidWhen || [],
1297
1804
  notes: meta.notes || [],