component-auto-docs 0.1.0 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "component-auto-docs",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Component docs automation CLI for uni-app component libraries.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "SHARING.md"
15
15
  ],
16
16
  "peerDependencies": {
17
+ "@vue/compiler-sfc": ">=3.4.0",
17
18
  "typescript": ">=5.0.0"
18
19
  },
19
20
  "engines": {
@@ -69,7 +69,7 @@ pnpm exec component-auto-docs gen
69
69
  pnpm exec component-auto-docs check
70
70
  ```
71
71
 
72
- `scaffold` 会复制 `docs.config.mjs`、`src/docs/runtime/*`、`src/docs/index.vue` 和本文档规范,并向 `package.json` 写入 `docs:init`、`docs:gen`、`docs:check`、`docs:dev` 脚本。已有文件默认跳过;如需覆盖模板文件和脚本,可使用:
72
+ `scaffold` 会复制 `docs.config.mjs`、`src/docs/runtime/*`、`src/docs/index.vue` 和本文档规范,并向 `package.json` 写入 `docs:scaffold`、`docs:init`、`docs:gen`、`docs:check`、`docs:dev` 脚本。已有文件默认跳过;如需覆盖模板文件和脚本,可使用:
73
73
 
74
74
  ```bash
75
75
  pnpm exec component-auto-docs scaffold --force
@@ -1,4 +1,6 @@
1
1
  <script setup lang="ts">
2
+ import { computed, ref } from 'vue';
3
+
2
4
  type DocItem = {
3
5
  name: string;
4
6
  title?: string;
@@ -26,13 +28,49 @@ type ComponentDoc = {
26
28
  related: string[];
27
29
  };
28
30
 
29
- withDefaults(defineProps<{
30
- doc: ComponentDoc;
31
- showControls?: boolean;
32
- }>(), {
33
- showControls: true,
31
+ const props = withDefaults(
32
+ defineProps<{
33
+ doc: ComponentDoc;
34
+ showControls?: boolean;
35
+ }>(),
36
+ {
37
+ showControls: true,
38
+ },
39
+ );
40
+
41
+ const activeTab = ref('preview');
42
+
43
+ const docTabs = computed(() => {
44
+ const tabs = [{ key: 'preview', label: '预览' }];
45
+
46
+ if (props.doc.props.length || props.doc.events.length || props.doc.slots.length) {
47
+ tabs.push({ key: 'api', label: 'API' });
48
+ }
49
+
50
+ if (props.doc.examples.length) {
51
+ tabs.push({ key: 'examples', label: '示例' });
52
+ }
53
+
54
+ if (
55
+ props.doc.useWhen.length ||
56
+ props.doc.avoidWhen.length ||
57
+ props.doc.notes.length ||
58
+ props.doc.related.length
59
+ ) {
60
+ tabs.push({ key: 'guide', label: '说明' });
61
+ }
62
+
63
+ return tabs;
64
+ });
65
+
66
+ const currentTab = computed(() => {
67
+ return docTabs.value.some((tab) => tab.key === activeTab.value) ? activeTab.value : 'preview';
34
68
  });
35
69
 
70
+ function chooseTab(tabKey: string) {
71
+ activeTab.value = tabKey;
72
+ }
73
+
36
74
  function formatDefault(value: unknown) {
37
75
  if (value === null || value === undefined || value === '') return '-';
38
76
 
@@ -49,96 +87,116 @@ function formatDefault(value: unknown) {
49
87
  <text class="doc-description">{{ doc.description }}</text>
50
88
  </view>
51
89
 
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>
90
+ <view class="doc-tabs">
91
+ <text
92
+ v-for="tab in docTabs"
93
+ :key="tab.key"
94
+ class="doc-tab"
95
+ :class="{ active: currentTab === tab.key }"
96
+ @click="chooseTab(tab.key)"
97
+ >
98
+ {{ tab.label }}
99
+ </text>
64
100
  </view>
65
101
 
66
- <view v-if="$slots.code" class="doc-section">
67
- <text class="section-title">当前代码片段预览</text>
68
- <slot name="code"></slot>
69
- </view>
102
+ <view v-if="currentTab === 'preview'" class="doc-tab-panel">
103
+ <view class="doc-section">
104
+ <text class="section-title">真实组件预览</text>
105
+ <view class="preview-panel">
106
+ <slot name="preview">
107
+ <text class="empty-text">暂无可交互预览</text>
108
+ </slot>
109
+ </view>
110
+ </view>
70
111
 
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>
112
+ <view v-if="showControls && $slots.controls" class="doc-section">
113
+ <text class="section-title">Props 控制面板</text>
114
+ <slot name="controls"></slot>
115
+ </view>
75
116
 
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 }}
117
+ <view v-if="$slots.code" class="doc-section">
118
+ <text class="section-title">当前代码片段预览</text>
119
+ <slot name="code"></slot>
80
120
  </view>
81
121
  </view>
82
122
 
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>
123
+ <view v-else-if="currentTab === 'api'" class="doc-tab-panel">
124
+ <view v-if="doc.props.length" class="doc-section">
125
+ <text class="section-title">Props</text>
126
+ <view v-for="prop in doc.props" :key="prop.name" class="api-card">
127
+ <view class="api-card-head">
128
+ <text class="api-name">{{ prop.name }}</text>
129
+ <view class="api-head-meta">
130
+ <text class="api-pill">{{ prop.type }}</text>
131
+ <text class="api-pill">default: {{ formatDefault(prop.default) }}</text>
132
+ <text v-if="prop.required" class="api-pill is-required">必填</text>
133
+ </view>
134
+ </view>
135
+ <text class="api-desc">{{ prop.description || '暂无说明' }}</text>
136
+ <view v-if="prop.values?.length" class="chip-list">
137
+ <text v-for="value in prop.values" :key="value" class="chip">{{ value }}</text>
92
138
  </view>
93
139
  </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>
140
+ </view>
141
+
142
+ <view v-if="doc.events.length" class="doc-section">
143
+ <text class="section-title">Events</text>
144
+ <view v-for="event in doc.events" :key="event.name" class="api-card">
145
+ <view class="api-card-head">
146
+ <text class="api-name">{{ event.name }}</text>
147
+ <text class="api-pill">{{ event.type }}</text>
148
+ </view>
149
+ <text class="api-desc">{{ event.description || '暂无说明' }}</text>
97
150
  </view>
98
151
  </view>
99
- </view>
100
152
 
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>
153
+ <view v-if="doc.slots.length" class="doc-section">
154
+ <text class="section-title">Slots</text>
155
+ <view v-for="slot in doc.slots" :key="slot.name" class="api-card">
156
+ <view class="api-card-head">
157
+ <text class="api-name">{{ slot.name }}</text>
158
+ </view>
159
+ <text class="api-desc">{{ slot.description || '暂无说明' }}</text>
107
160
  </view>
108
- <text class="api-desc">{{ event.description || '暂无说明' }}</text>
109
161
  </view>
110
162
  </view>
111
163
 
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>
164
+ <view v-else-if="currentTab === 'examples'" class="doc-tab-panel">
165
+ <view class="doc-section">
166
+ <text class="section-title">示例代码</text>
167
+ <view v-for="example in doc.examples" :key="example.code" class="example-card">
168
+ <text class="example-title">{{ example.title }}</text>
169
+ <text v-if="example.description" class="example-desc">{{ example.description }}</text>
170
+ <view class="code-card">
171
+ <text class="code-text">{{ example.code }}</text>
172
+ </view>
117
173
  </view>
118
- <text class="api-desc">{{ slot.description || '暂无说明' }}</text>
119
174
  </view>
120
175
  </view>
121
176
 
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>
177
+ <view v-else-if="currentTab === 'guide'" class="doc-tab-panel">
178
+ <view v-if="doc.useWhen.length" class="doc-section">
179
+ <text class="section-title">何时使用</text>
180
+ <view v-for="item in doc.useWhen" :key="item" class="text-card">{{ item }}</view>
181
+ </view>
126
182
 
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>
183
+ <view v-if="doc.avoidWhen.length" class="doc-section">
184
+ <text class="section-title">不建议使用</text>
185
+ <view v-for="item in doc.avoidWhen" :key="item" class="text-card is-warning">
186
+ {{ item }}
134
187
  </view>
135
188
  </view>
136
- </view>
137
189
 
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>
190
+ <view v-if="doc.notes.length" class="doc-section">
191
+ <text class="section-title">注意事项</text>
192
+ <view v-for="item in doc.notes" :key="item" class="text-card">{{ item }}</view>
193
+ </view>
194
+
195
+ <view v-if="doc.related.length" class="doc-section">
196
+ <text class="section-title">相关组件</text>
197
+ <view class="chip-list">
198
+ <text v-for="item in doc.related" :key="item" class="chip">{{ item }}</text>
199
+ </view>
142
200
  </view>
143
201
  </view>
144
202
  </view>
@@ -211,6 +269,43 @@ page {
211
269
  font-size: 26rpx;
212
270
  }
213
271
 
272
+ .doc-tabs {
273
+ position: sticky;
274
+ top: 0;
275
+ z-index: 5;
276
+ display: flex;
277
+ flex-direction: row;
278
+ gap: 10rpx;
279
+ padding: 18rpx 0 8rpx;
280
+ overflow-x: auto;
281
+ background: #f4f7fb;
282
+ }
283
+
284
+ .doc-tab {
285
+ box-sizing: border-box;
286
+ flex: 0 0 auto;
287
+ min-width: 124rpx;
288
+ padding: 13rpx 20rpx;
289
+ font-size: 25rpx;
290
+ font-weight: 600;
291
+ line-height: 1.35;
292
+ color: #475569;
293
+ text-align: center;
294
+ background: #fff;
295
+ border: 1rpx solid #d9e2ef;
296
+ border-radius: 8rpx;
297
+ }
298
+
299
+ .doc-tab.active {
300
+ color: #fff;
301
+ background: #2563eb;
302
+ border-color: #2563eb;
303
+ }
304
+
305
+ .doc-tab-panel {
306
+ width: 100%;
307
+ }
308
+
214
309
  .doc-section {
215
310
  display: flex;
216
311
  flex-direction: column;
@@ -1,4 +1,4 @@
1
- import { computed, reactive, watch } from 'vue';
1
+ import { computed, reactive, ref, watch } from 'vue';
2
2
 
3
3
  export type DocItem = {
4
4
  name: string;
@@ -11,6 +11,18 @@ export type DocItem = {
11
11
  code?: string;
12
12
  };
13
13
 
14
+ export type ComponentPreview = {
15
+ source?: string;
16
+ kind?: 'inline' | 'form' | 'data-list' | 'measure' | 'overlay' | 'page-shell' | 'native' | 'composite';
17
+ code?: string;
18
+ props?: Record<string, unknown>;
19
+ slots?: Record<string, string>;
20
+ vModel?: {
21
+ prop?: string;
22
+ variable?: string;
23
+ };
24
+ };
25
+
14
26
  export type ComponentDoc = {
15
27
  name: string;
16
28
  title: string;
@@ -19,6 +31,7 @@ export type ComponentDoc = {
19
31
  props: DocItem[];
20
32
  events: DocItem[];
21
33
  slots: DocItem[];
34
+ preview?: ComponentPreview | null;
22
35
  examples: DocItem[];
23
36
  useWhen: string[];
24
37
  avoidWhen: string[];
@@ -28,6 +41,7 @@ export type ComponentDoc = {
28
41
 
29
42
  export function useAutoComponentDoc(doc: ComponentDoc) {
30
43
  const propControls = reactive<Record<string, unknown>>({});
44
+ const overlayPreviewVisible = ref(false);
31
45
 
32
46
  function isBooleanProp(prop: DocItem) {
33
47
  return (prop.type || '').toLowerCase().includes('boolean');
@@ -51,20 +65,6 @@ export function useAutoComponentDoc(doc: ComponentDoc) {
51
65
  return type.includes('object') || type.includes('Object');
52
66
  }
53
67
 
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
68
  function parseNumberValue(value: unknown) {
69
69
  if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;
70
70
  if (typeof value !== 'string') return undefined;
@@ -78,36 +78,28 @@ export function useAutoComponentDoc(doc: ComponentDoc) {
78
78
  return Number.isFinite(numberValue) ? numberValue : undefined;
79
79
  }
80
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);
81
+ function hasPreviewProp(propName: string) {
82
+ return Boolean(doc.preview?.props && Object.prototype.hasOwnProperty.call(doc.preview.props, propName));
83
+ }
84
+
85
+ function formatInitialControlValue(prop: DocItem, value: unknown) {
86
+ if ((isArrayProp(prop) || isObjectProp(prop)) && typeof value !== 'string') {
87
+ return JSON.stringify(value ?? (isArrayProp(prop) ? [] : {}), null, 2);
88
+ }
89
+
90
+ return value;
91
+ }
84
92
 
85
- return prop.default;
93
+ function getSampleValue(prop: DocItem) {
94
+ if (hasPreviewProp(prop.name)) {
95
+ return formatInitialControlValue(prop, doc.preview?.props?.[prop.name]);
86
96
  }
87
97
 
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
- );
98
+ if (prop.default !== null && prop.default !== undefined && prop.default !== '') {
99
+ if (isNumberProp(prop)) return parseNumberValue(prop.default) ?? '';
100
+
101
+ return formatInitialControlValue(prop, prop.default);
103
102
  }
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
103
 
112
104
  return '';
113
105
  }
@@ -147,6 +139,8 @@ export function useAutoComponentDoc(doc: ComponentDoc) {
147
139
  }
148
140
 
149
141
  function resetControls() {
142
+ overlayPreviewVisible.value = false;
143
+
150
144
  for (const prop of doc.props) {
151
145
  propControls[prop.name] = getSampleValue(prop);
152
146
  }
@@ -166,18 +160,61 @@ export function useAutoComponentDoc(doc: ComponentDoc) {
166
160
  return result;
167
161
  });
168
162
 
163
+ const isOverlayPreview = computed(() => doc.preview?.kind === 'overlay');
164
+
165
+ const overlayModelProp = computed(() => {
166
+ if (!isOverlayPreview.value) return '';
167
+
168
+ const configuredProp = doc.preview?.vModel?.prop;
169
+ if (configuredProp && doc.props.some((prop) => prop.name === configuredProp)) return configuredProp;
170
+
171
+ const modelProp = doc.props.find((prop) => prop.name === 'modelValue' && isBooleanProp(prop));
172
+ return modelProp?.name || '';
173
+ });
174
+
175
+ const renderedPreviewProps = computed(() => {
176
+ const result = { ...previewProps.value };
177
+
178
+ if (isOverlayPreview.value && overlayModelProp.value) {
179
+ result[overlayModelProp.value] = overlayPreviewVisible.value;
180
+ }
181
+
182
+ return result;
183
+ });
184
+
169
185
  const slotText = computed(() => {
170
- if (!doc.slots.length) return '示例内容';
186
+ return doc.preview?.slots?.default || '';
187
+ });
171
188
 
172
- return doc.slots.some((slot) => slot.name === 'default') ? '示例内容' : '';
189
+ const previewKey = computed(() => {
190
+ return JSON.stringify({
191
+ props: renderedPreviewProps.value,
192
+ slot: slotText.value,
193
+ });
173
194
  });
174
195
 
196
+ const overlayTriggerText = computed(() => {
197
+ return overlayPreviewVisible.value ? '重新打开预览' : '打开预览';
198
+ });
199
+
200
+ function openOverlayPreview() {
201
+ overlayPreviewVisible.value = true;
202
+ }
203
+
204
+ function handlePreviewModelValueUpdate(propName: string, value: unknown) {
205
+ setControlValue(propName, value);
206
+
207
+ if (isOverlayPreview.value && propName === overlayModelProp.value) {
208
+ overlayPreviewVisible.value = Boolean(value);
209
+ }
210
+ }
211
+
175
212
  const codeSnippet = computed(() => {
176
213
  const attrs = doc.props
177
214
  .map((prop) => {
178
215
  const value = parseControlValue(prop, propControls[prop.name]);
179
216
  if (value === '' || value === null || value === undefined) return '';
180
- if (prop.name === 'modelValue') return 'v-model="value"';
217
+ if (prop.name === 'modelValue') return `v-model="${doc.preview?.vModel?.variable || 'value'}"`;
181
218
  if (typeof value === 'boolean') return value ? prop.name : `:${prop.name}="false"`;
182
219
  if (typeof value === 'number') return `:${prop.name}="${value}"`;
183
220
  if (Array.isArray(value) || typeof value === 'object') return `:${prop.name}="${prop.name}"`;
@@ -194,8 +231,14 @@ export function useAutoComponentDoc(doc: ComponentDoc) {
194
231
  return {
195
232
  propControls,
196
233
  previewProps,
234
+ renderedPreviewProps,
235
+ previewKey,
197
236
  slotText,
198
237
  codeSnippet,
238
+ isOverlayPreview,
239
+ overlayTriggerText,
240
+ openOverlayPreview,
241
+ handlePreviewModelValueUpdate,
199
242
  isBooleanProp,
200
243
  isNumberProp,
201
244
  isArrayProp,