component-auto-docs 0.1.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.
@@ -0,0 +1,473 @@
1
+ <script setup lang="ts">
2
+ type DocItem = {
3
+ name: string;
4
+ title?: string;
5
+ type?: string;
6
+ default?: unknown;
7
+ required?: boolean;
8
+ values?: string[];
9
+ description?: string;
10
+ props?: string[];
11
+ code?: string;
12
+ };
13
+
14
+ type ComponentDoc = {
15
+ name: string;
16
+ title: string;
17
+ category: string;
18
+ description: string;
19
+ props: DocItem[];
20
+ events: DocItem[];
21
+ slots: DocItem[];
22
+ examples: DocItem[];
23
+ useWhen: string[];
24
+ avoidWhen: string[];
25
+ notes: string[];
26
+ related: string[];
27
+ };
28
+
29
+ withDefaults(defineProps<{
30
+ doc: ComponentDoc;
31
+ showControls?: boolean;
32
+ }>(), {
33
+ showControls: true,
34
+ });
35
+
36
+ function formatDefault(value: unknown) {
37
+ if (value === null || value === undefined || value === '') return '-';
38
+
39
+ return String(value);
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <app-page :title="`${doc.name} 文档`" bg-color="#f4f7fb">
45
+ <view class="doc-page">
46
+ <view class="doc-hero">
47
+ <view class="doc-category">{{ doc.category }}</view>
48
+ <text class="doc-title">{{ doc.name }} · {{ doc.title }}</text>
49
+ <text class="doc-description">{{ doc.description }}</text>
50
+ </view>
51
+
52
+ <view class="doc-section">
53
+ <text class="section-title">真实组件预览</text>
54
+ <view class="preview-panel">
55
+ <slot name="preview">
56
+ <text class="empty-text">暂无可交互预览</text>
57
+ </slot>
58
+ </view>
59
+ </view>
60
+
61
+ <view v-if="showControls && $slots.controls" class="doc-section">
62
+ <text class="section-title">Props 控制面板</text>
63
+ <slot name="controls"></slot>
64
+ </view>
65
+
66
+ <view v-if="$slots.code" class="doc-section">
67
+ <text class="section-title">当前代码片段预览</text>
68
+ <slot name="code"></slot>
69
+ </view>
70
+
71
+ <view class="doc-section">
72
+ <text class="section-title">何时使用</text>
73
+ <view v-for="item in doc.useWhen" :key="item" class="text-card">{{ item }}</view>
74
+ </view>
75
+
76
+ <view class="doc-section">
77
+ <text class="section-title">不建议使用</text>
78
+ <view v-for="item in doc.avoidWhen" :key="item" class="text-card is-warning">
79
+ {{ item }}
80
+ </view>
81
+ </view>
82
+
83
+ <view class="doc-section">
84
+ <text class="section-title">Props</text>
85
+ <view v-for="prop in doc.props" :key="prop.name" class="api-card">
86
+ <view class="api-card-head">
87
+ <text class="api-name">{{ prop.name }}</text>
88
+ <view class="api-head-meta">
89
+ <text class="api-pill">{{ prop.type }}</text>
90
+ <text class="api-pill">default: {{ formatDefault(prop.default) }}</text>
91
+ <text v-if="prop.required" class="api-pill is-required">必填</text>
92
+ </view>
93
+ </view>
94
+ <text class="api-desc">{{ prop.description || '暂无说明' }}</text>
95
+ <view v-if="prop.values?.length" class="chip-list">
96
+ <text v-for="value in prop.values" :key="value" class="chip">{{ value }}</text>
97
+ </view>
98
+ </view>
99
+ </view>
100
+
101
+ <view class="doc-section">
102
+ <text class="section-title">Events</text>
103
+ <view v-for="event in doc.events" :key="event.name" class="api-card">
104
+ <view class="api-card-head">
105
+ <text class="api-name">{{ event.name }}</text>
106
+ <text class="api-pill">{{ event.type }}</text>
107
+ </view>
108
+ <text class="api-desc">{{ event.description || '暂无说明' }}</text>
109
+ </view>
110
+ </view>
111
+
112
+ <view class="doc-section">
113
+ <text class="section-title">Slots</text>
114
+ <view v-for="slot in doc.slots" :key="slot.name" class="api-card">
115
+ <view class="api-card-head">
116
+ <text class="api-name">{{ slot.name }}</text>
117
+ </view>
118
+ <text class="api-desc">{{ slot.description || '暂无说明' }}</text>
119
+ </view>
120
+ </view>
121
+
122
+ <view class="doc-section">
123
+ <text class="section-title">注意事项</text>
124
+ <view v-for="item in doc.notes" :key="item" class="text-card">{{ item }}</view>
125
+ </view>
126
+
127
+ <view class="doc-section">
128
+ <text class="section-title">示例代码</text>
129
+ <view v-for="example in doc.examples" :key="example.code" class="example-card">
130
+ <text class="example-title">{{ example.title }}</text>
131
+ <text v-if="example.description" class="example-desc">{{ example.description }}</text>
132
+ <view class="code-card">
133
+ <text class="code-text">{{ example.code }}</text>
134
+ </view>
135
+ </view>
136
+ </view>
137
+
138
+ <view v-if="doc.related.length" class="doc-section">
139
+ <text class="section-title">相关组件</text>
140
+ <view class="chip-list">
141
+ <text v-for="item in doc.related" :key="item" class="chip">{{ item }}</text>
142
+ </view>
143
+ </view>
144
+ </view>
145
+ </app-page>
146
+ </template>
147
+
148
+ <style lang="scss">
149
+ page {
150
+ overflow-x: hidden;
151
+ background: #f4f7fb;
152
+ }
153
+
154
+ .doc-page {
155
+ box-sizing: border-box;
156
+ width: 100%;
157
+ min-height: 100vh;
158
+ padding: 24rpx 20rpx 48rpx;
159
+ overflow-x: hidden;
160
+ color: #0f172a;
161
+ }
162
+
163
+ .doc-hero,
164
+ .text-card,
165
+ .api-card,
166
+ .example-card,
167
+ .code-card {
168
+ box-sizing: border-box;
169
+ width: 100%;
170
+ background: #fff;
171
+ border: 1rpx solid #d9e2ef;
172
+ border-radius: 12rpx;
173
+ }
174
+
175
+ .doc-hero {
176
+ display: flex;
177
+ flex-direction: column;
178
+ gap: 14rpx;
179
+ padding: 28rpx 24rpx;
180
+ box-shadow: 0 10rpx 28rpx rgb(15 23 42 / 4%);
181
+ }
182
+
183
+ .doc-category {
184
+ width: fit-content;
185
+ padding: 7rpx 14rpx;
186
+ font-size: 22rpx;
187
+ color: #1d4ed8;
188
+ background: #eff6ff;
189
+ border: 1rpx solid #bfdbfe;
190
+ border-radius: 8rpx;
191
+ }
192
+
193
+ .doc-title {
194
+ font-size: 36rpx;
195
+ font-weight: 700;
196
+ line-height: 1.3;
197
+ overflow-wrap: anywhere;
198
+ }
199
+
200
+ .doc-description,
201
+ .api-desc,
202
+ .example-desc,
203
+ .text-card {
204
+ font-size: 24rpx;
205
+ line-height: 1.6;
206
+ color: #42526b;
207
+ overflow-wrap: anywhere;
208
+ }
209
+
210
+ .doc-description {
211
+ font-size: 26rpx;
212
+ }
213
+
214
+ .doc-section {
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: 16rpx;
218
+ margin-top: 24rpx;
219
+ }
220
+
221
+ .section-title {
222
+ position: relative;
223
+ padding-left: 18rpx;
224
+ font-size: 28rpx;
225
+ font-weight: 700;
226
+ line-height: 1.35;
227
+ }
228
+
229
+ .section-title::before {
230
+ position: absolute;
231
+ top: 7rpx;
232
+ bottom: 6rpx;
233
+ left: 0;
234
+ width: 6rpx;
235
+ content: '';
236
+ background: #2563eb;
237
+ border-radius: 8rpx;
238
+ }
239
+
240
+ .preview-panel,
241
+ .text-card,
242
+ .api-card,
243
+ .example-card {
244
+ padding: 20rpx;
245
+ }
246
+
247
+ .preview-panel,
248
+ .api-card,
249
+ .example-card {
250
+ display: flex;
251
+ flex-direction: column;
252
+ gap: 16rpx;
253
+ box-shadow: 0 8rpx 24rpx rgb(15 23 42 / 4%);
254
+ }
255
+
256
+ .preview-panel {
257
+ box-sizing: border-box;
258
+ width: 100%;
259
+ padding: 0;
260
+ background: transparent;
261
+ border: 0;
262
+ border-radius: 0;
263
+ box-shadow: none;
264
+ }
265
+
266
+ .text-card.is-warning {
267
+ color: #7f1d1d;
268
+ background: #fff5f5;
269
+ border-color: #fecaca;
270
+ }
271
+
272
+ .empty-text {
273
+ font-size: 24rpx;
274
+ color: #64748b;
275
+ }
276
+
277
+ .doc-page .preview-value {
278
+ font-size: 24rpx;
279
+ color: #64748b;
280
+ }
281
+
282
+ .doc-page .event-panel {
283
+ display: flex;
284
+ flex-wrap: wrap;
285
+ gap: 8rpx;
286
+ }
287
+
288
+ .doc-page .event-chip {
289
+ max-width: 100%;
290
+ padding: 6rpx 10rpx;
291
+ font-family: Menlo, Monaco, Consolas, monospace;
292
+ font-size: 21rpx;
293
+ line-height: 1.35;
294
+ color: #475569;
295
+ overflow-wrap: anywhere;
296
+ background: #f8fafc;
297
+ border: 1rpx solid #e2e8f0;
298
+ border-radius: 8rpx;
299
+ }
300
+
301
+ .api-card-head {
302
+ display: flex;
303
+ flex-wrap: wrap;
304
+ gap: 12rpx;
305
+ align-items: flex-start;
306
+ justify-content: space-between;
307
+ }
308
+
309
+ .api-name {
310
+ min-width: 0;
311
+ font-family: Menlo, Monaco, Consolas, monospace;
312
+ font-size: 30rpx;
313
+ font-weight: 600;
314
+ line-height: 1.35;
315
+ color: #111827;
316
+ overflow-wrap: anywhere;
317
+ }
318
+
319
+ .api-head-meta {
320
+ display: flex;
321
+ flex: 1 1 320rpx;
322
+ flex-wrap: wrap;
323
+ gap: 8rpx;
324
+ justify-content: flex-end;
325
+ min-width: 0;
326
+ }
327
+
328
+ .api-pill,
329
+ .chip {
330
+ max-width: 100%;
331
+ padding: 8rpx 12rpx;
332
+ font-family: Menlo, Monaco, Consolas, monospace;
333
+ font-size: 22rpx;
334
+ line-height: 1.35;
335
+ color: #344256;
336
+ overflow-wrap: anywhere;
337
+ background: #f7f9fc;
338
+ border: 1rpx solid #e2e8f0;
339
+ border-radius: 8rpx;
340
+ }
341
+
342
+ .api-pill.is-required {
343
+ color: #9a3412;
344
+ background: #fff7ed;
345
+ border-color: #fed7aa;
346
+ }
347
+
348
+ .chip-list {
349
+ display: flex;
350
+ flex-wrap: wrap;
351
+ gap: 8rpx;
352
+ }
353
+
354
+ .example-title {
355
+ font-size: 26rpx;
356
+ font-weight: 700;
357
+ }
358
+
359
+ .code-card {
360
+ padding: 18rpx;
361
+ background: #0f172a;
362
+ border-color: #0f172a;
363
+ }
364
+
365
+ .code-text {
366
+ display: block;
367
+ width: 100%;
368
+ font-family: Menlo, Monaco, Consolas, monospace;
369
+ font-size: 22rpx;
370
+ line-height: 1.5;
371
+ color: #e5e7eb;
372
+ overflow-wrap: anywhere;
373
+ white-space: pre-wrap;
374
+ }
375
+
376
+ .control-panel,
377
+ .control-list,
378
+ .control-item {
379
+ display: flex;
380
+ flex-direction: column;
381
+ gap: 12rpx;
382
+ width: 100%;
383
+ }
384
+
385
+ .doc-page .control-panel,
386
+ .doc-page .control-list {
387
+ box-sizing: border-box;
388
+ padding: 18rpx;
389
+ background: #f8fafc;
390
+ border: 1rpx solid #e2e8f0;
391
+ border-radius: 10rpx;
392
+ }
393
+
394
+ .doc-page .control-item {
395
+ display: flex;
396
+ flex-direction: column;
397
+ gap: 8rpx;
398
+ padding-bottom: 4rpx;
399
+ }
400
+
401
+ .doc-page .control-item.is-inline,
402
+ .doc-page .control-row {
403
+ display: flex;
404
+ gap: 10rpx;
405
+ align-items: center;
406
+ justify-content: space-between;
407
+ width: 100%;
408
+ }
409
+
410
+ .doc-page .control-label {
411
+ font-size: 22rpx;
412
+ font-weight: 600;
413
+ color: #475569;
414
+ }
415
+
416
+ .doc-page .control-input {
417
+ box-sizing: border-box;
418
+ width: 100%;
419
+ min-height: 60rpx;
420
+ padding: 0 16rpx;
421
+ font-size: 26rpx;
422
+ line-height: 60rpx;
423
+ color: #111827;
424
+ background: #fff;
425
+ border: 1rpx solid #d7e0ee;
426
+ border-radius: 8rpx;
427
+ }
428
+
429
+ .doc-page .tag-list {
430
+ display: flex;
431
+ flex-wrap: wrap;
432
+ gap: 8rpx;
433
+ width: 100%;
434
+ }
435
+
436
+ .doc-page .option-tag {
437
+ box-sizing: border-box;
438
+ max-width: 100%;
439
+ padding: 8rpx 12rpx;
440
+ font-size: 22rpx;
441
+ line-height: 1.35;
442
+ color: #374151;
443
+ overflow-wrap: anywhere;
444
+ background: #fff;
445
+ border: 1rpx solid #e2e8f0;
446
+ border-radius: 8rpx;
447
+ }
448
+
449
+ .doc-page .option-tag.active {
450
+ color: #1d4ed8;
451
+ background: #eff6ff;
452
+ border-color: #93c5fd;
453
+ }
454
+
455
+ .doc-page .code-card-mini {
456
+ box-sizing: border-box;
457
+ width: 100%;
458
+ padding: 16rpx;
459
+ background: #111827;
460
+ border-radius: 10rpx;
461
+ }
462
+
463
+ .doc-page .code-text-mini {
464
+ display: block;
465
+ width: 100%;
466
+ font-family: Menlo, Monaco, Consolas, monospace;
467
+ font-size: 22rpx;
468
+ line-height: 1.5;
469
+ color: #e5e7eb;
470
+ overflow-wrap: anywhere;
471
+ white-space: pre-wrap;
472
+ }
473
+ </style>
@@ -0,0 +1,208 @@
1
+ import { computed, reactive, watch } from 'vue';
2
+
3
+ export type DocItem = {
4
+ name: string;
5
+ title?: string;
6
+ type?: string;
7
+ default?: unknown;
8
+ required?: boolean;
9
+ values?: string[];
10
+ description?: string;
11
+ code?: string;
12
+ };
13
+
14
+ export type ComponentDoc = {
15
+ name: string;
16
+ title: string;
17
+ category: string;
18
+ description: string;
19
+ props: DocItem[];
20
+ events: DocItem[];
21
+ slots: DocItem[];
22
+ examples: DocItem[];
23
+ useWhen: string[];
24
+ avoidWhen: string[];
25
+ notes: string[];
26
+ related: string[];
27
+ };
28
+
29
+ export function useAutoComponentDoc(doc: ComponentDoc) {
30
+ const propControls = reactive<Record<string, unknown>>({});
31
+
32
+ function isBooleanProp(prop: DocItem) {
33
+ return (prop.type || '').toLowerCase().includes('boolean');
34
+ }
35
+
36
+ function isNumberProp(prop: DocItem) {
37
+ const type = prop.type || '';
38
+
39
+ return type.includes('number') || type.includes('Number');
40
+ }
41
+
42
+ function isArrayProp(prop: DocItem) {
43
+ const type = prop.type || '';
44
+
45
+ return type.includes('array') || type.includes('Array') || ['items', 'list', 'range', 'tabs', 'arr'].includes(prop.name);
46
+ }
47
+
48
+ function isObjectProp(prop: DocItem) {
49
+ const type = prop.type || '';
50
+
51
+ return type.includes('object') || type.includes('Object');
52
+ }
53
+
54
+ function getFallbackNumber(prop: DocItem) {
55
+ const name = prop.name.toLowerCase();
56
+
57
+ if (name === 'modelvalue') return 1;
58
+ if (name.includes('percent')) return 60;
59
+ if (name.includes('size') || name.includes('font')) return 32;
60
+ if (name === 'w' || name.includes('width')) return 240;
61
+ if (name === 'h' || name.includes('height')) return 80;
62
+ if (name.includes('max')) return 10;
63
+ if (name.includes('min')) return 0;
64
+
65
+ return 1;
66
+ }
67
+
68
+ function parseNumberValue(value: unknown) {
69
+ if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;
70
+ if (typeof value !== 'string') return undefined;
71
+
72
+ const trimmed = value.trim();
73
+ if (!trimmed) return undefined;
74
+ if (trimmed === 'Number.MAX_SAFE_INTEGER') return Number.MAX_SAFE_INTEGER;
75
+ if (trimmed === 'Number.MIN_SAFE_INTEGER') return Number.MIN_SAFE_INTEGER;
76
+
77
+ const numberValue = Number(trimmed);
78
+ return Number.isFinite(numberValue) ? numberValue : undefined;
79
+ }
80
+
81
+ function getSampleValue(prop: DocItem) {
82
+ if (prop.default !== null && prop.default !== undefined && prop.default !== '') {
83
+ if (isNumberProp(prop)) return parseNumberValue(prop.default) ?? getFallbackNumber(prop);
84
+
85
+ return prop.default;
86
+ }
87
+
88
+ if (prop.values?.length) return prop.values[0];
89
+
90
+ const name = prop.name.toLowerCase();
91
+
92
+ if (isBooleanProp(prop)) return false;
93
+ if (isNumberProp(prop)) return getFallbackNumber(prop);
94
+ if (isArrayProp(prop)) {
95
+ return JSON.stringify(
96
+ [
97
+ { label: '选项一', value: 'one' },
98
+ { label: '选项二', value: 'two' },
99
+ ],
100
+ null,
101
+ 2,
102
+ );
103
+ }
104
+ if (isObjectProp(prop)) return '{}';
105
+ if (name.includes('placeholder')) return '请输入';
106
+ if (name.includes('title')) return '标题';
107
+ if (name.includes('label')) return '标签';
108
+ if (name.includes('text') || name.includes('content')) return '示例内容';
109
+ if (name.includes('phone')) return '13800000000';
110
+ if (name === 'icon') return '/static/icon/avatar.svg';
111
+
112
+ return '';
113
+ }
114
+
115
+ function parseControlValue(prop: DocItem, value: unknown) {
116
+ if (isArrayProp(prop) || isObjectProp(prop)) {
117
+ if (typeof value !== 'string') return value;
118
+
119
+ try {
120
+ return JSON.parse(value);
121
+ } catch {
122
+ return isArrayProp(prop) ? [] : {};
123
+ }
124
+ }
125
+
126
+ if (isNumberProp(prop)) return parseNumberValue(value);
127
+
128
+ return value;
129
+ }
130
+
131
+ function setBooleanProp(propName: string, value: boolean) {
132
+ propControls[propName] = value;
133
+ }
134
+
135
+ function setOptionProp(propName: string, value: string) {
136
+ propControls[propName] = value;
137
+ }
138
+
139
+ function setControlValue(propName: string, value: unknown) {
140
+ propControls[propName] = value;
141
+ }
142
+
143
+ function formatControlDefault(value: unknown) {
144
+ if (value === null || value === undefined || value === '') return '-';
145
+
146
+ return String(value);
147
+ }
148
+
149
+ function resetControls() {
150
+ for (const prop of doc.props) {
151
+ propControls[prop.name] = getSampleValue(prop);
152
+ }
153
+ }
154
+
155
+ watch(() => doc.name, resetControls, { immediate: true });
156
+
157
+ const previewProps = computed(() => {
158
+ const result: Record<string, unknown> = {};
159
+
160
+ for (const prop of doc.props) {
161
+ const value = parseControlValue(prop, propControls[prop.name]);
162
+ if (value === '' || value === null || value === undefined) continue;
163
+ result[prop.name] = value;
164
+ }
165
+
166
+ return result;
167
+ });
168
+
169
+ const slotText = computed(() => {
170
+ if (!doc.slots.length) return '示例内容';
171
+
172
+ return doc.slots.some((slot) => slot.name === 'default') ? '示例内容' : '';
173
+ });
174
+
175
+ const codeSnippet = computed(() => {
176
+ const attrs = doc.props
177
+ .map((prop) => {
178
+ const value = parseControlValue(prop, propControls[prop.name]);
179
+ if (value === '' || value === null || value === undefined) return '';
180
+ if (prop.name === 'modelValue') return 'v-model="value"';
181
+ if (typeof value === 'boolean') return value ? prop.name : `:${prop.name}="false"`;
182
+ if (typeof value === 'number') return `:${prop.name}="${value}"`;
183
+ if (Array.isArray(value) || typeof value === 'object') return `:${prop.name}="${prop.name}"`;
184
+
185
+ return `${prop.name}="${value}"`;
186
+ })
187
+ .filter(Boolean)
188
+ .join(' ');
189
+ const attrText = attrs ? ` ${attrs}` : '';
190
+
191
+ return `<${doc.name}${attrText}>${slotText.value}</${doc.name}>`;
192
+ });
193
+
194
+ return {
195
+ propControls,
196
+ previewProps,
197
+ slotText,
198
+ codeSnippet,
199
+ isBooleanProp,
200
+ isNumberProp,
201
+ isArrayProp,
202
+ isObjectProp,
203
+ formatControlDefault,
204
+ setBooleanProp,
205
+ setControlValue,
206
+ setOptionProp,
207
+ };
208
+ }