n-design-readonly-plugin 1.0.2 → 1.0.4

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,414 +0,0 @@
1
- // composables/withReadonly.ts
2
- import { defineComponent, h, computed, inject, provide, ref, watch, useAttrs, type PropType, type Component, type VNode, Ref } from 'vue';
3
-
4
- type FormModelValue = string | number | boolean | any[] | null | undefined;
5
- type ValueToLabelFn = (value: FormModelValue) => string;
6
-
7
- // ========================
8
- // 工具函数:提取静态文本(用于单个 checkbox/radio/select )
9
- // ========================
10
- function extractStaticText(vnode: any): string {
11
- if (vnode == null) return '';
12
- if (typeof vnode === 'string') return vnode;
13
- if (Array.isArray(vnode)) return vnode.map(extractStaticText).join('');
14
- if (typeof vnode === 'object') {
15
- if (typeof vnode.children === 'string') {
16
- return vnode.children;
17
- }
18
- // 处理动态插槽内容
19
- if (vnode?.default && typeof vnode.default === 'function') {
20
- const dynamicContent = vnode.default();
21
- if (Array.isArray(dynamicContent)) {
22
- return extractStaticText(dynamicContent);
23
- }
24
- }
25
- if (vnode.children) {
26
- extractStaticText(vnode.children);
27
- }
28
- }
29
- return '';
30
- }
31
-
32
- // ========================
33
- // 从插槽中提取 value → label 映射(支持 n-option / n-radio / n-checkbox / n-select)
34
- // ========================
35
- function collectOptionLabelMapFromSlots(vnodes: VNode | VNode[] | undefined, fieldNames: Record<string, string> = {}): Map<any, string> {
36
- const labelMap = new Map<any, string>();
37
- const { value: valKey = 'value', label: labelKey = 'label' } = fieldNames;
38
-
39
- if (!vnodes) return labelMap;
40
-
41
- const nodes = Array.isArray(vnodes) ? vnodes : [vnodes];
42
-
43
- for (const vnode of nodes) {
44
- if (!vnode || typeof vnode !== 'object') continue;
45
-
46
- // 处理组件节点
47
- if (vnode.type && (typeof vnode.type === 'object' || typeof vnode.type === 'function')) {
48
- let compName = (vnode.type as any).name?.toLowerCase() || '';
49
- if (typeof vnode.type === 'function') {
50
- compName = (vnode.type as any).displayName?.toLowerCase() || '';
51
- } else if (typeof vnode.type === 'string') {
52
- compName = (vnode.type as any)?.toLowerCase() || '';
53
- }
54
- if (/option|radio|checkbox/i.test(compName)) {
55
- const props = vnode.props || {};
56
- const value = props[valKey] ?? props.value;
57
- const labelText = props[labelKey] ?? props.label;
58
- if (value != null) {
59
- const label = extractStaticText(vnode.children) || labelText || String(value);
60
- labelMap.set(value, label);
61
- }
62
- }
63
- }
64
-
65
- // 递归子节点
66
- if (vnode.children) {
67
- const childMap = collectOptionLabelMapFromSlots(vnode.children as any, fieldNames);
68
- childMap.forEach((label, val) => {
69
- if (!labelMap.has(val)) labelMap.set(val, label);
70
- });
71
- }
72
- }
73
-
74
- return labelMap;
75
- }
76
-
77
- // ========================
78
- // Cascader: 递归查找 label 路径
79
- // ========================
80
- function findCascaderLabels(options: any[], value: any[], fieldNames: Record<string, string>): string[] {
81
- const { value: valKey = 'value', label: labelKey = 'label', children: childrenKey = 'children' } = fieldNames;
82
- const labels: string[] = [];
83
- let current = options;
84
-
85
- for (const v of value) {
86
- const node = current.find((item: any) => item[valKey] === v);
87
- if (node) {
88
- labels.push(node[labelKey] || node[valKey] || String(v));
89
- current = node[childrenKey] || [];
90
- } else {
91
- labels.push(String(v));
92
- break;
93
- }
94
- }
95
- return labels;
96
- }
97
-
98
- // ========================
99
- // TreeSelect: 从 treeData 递归查找 label
100
- // ========================
101
- function findTreeSelectLabels(treeData: any[], value: any, fieldNames: Record<string, string>): string[] {
102
- const { value: valKey = 'value', label: labelKey = 'title', children: childrenKey = 'children' } = fieldNames;
103
- const labels: string[] = [];
104
- const targetValues = new Set(Array.isArray(value) ? value : [value]);
105
-
106
- function dfs(nodes: any[]) {
107
- for (const node of nodes) {
108
- if (targetValues.has(node[valKey])) {
109
- labels.push(node[labelKey] || node[valKey]);
110
- }
111
- if (node[childrenKey]) {
112
- dfs(node[childrenKey]);
113
- }
114
- }
115
- }
116
-
117
- dfs(treeData);
118
- return labels;
119
- }
120
-
121
- // ========================
122
- // 获取只读显示文本
123
- // ========================
124
- function getDisplayText({
125
- modelValue,
126
- options,
127
- treeData,
128
- valueToLabel,
129
- fieldNames,
130
- slots,
131
- isSelect,
132
- isRadioGroup,
133
- isCheckboxGroup,
134
- isCheckbox,
135
- isRadio,
136
- isCascader,
137
- isTreeSelect,
138
- isSwitch,
139
- attrs,
140
- emptyText,
141
- }: {
142
- modelValue: FormModelValue;
143
- options?: any[];
144
- treeData?: any[];
145
- valueToLabel?: ValueToLabelFn;
146
- fieldNames: Record<string, string>;
147
- slots?: any;
148
- isSelect: boolean;
149
- isRadioGroup: boolean;
150
- isCheckboxGroup: boolean;
151
- isCheckbox: boolean;
152
- isRadio: boolean;
153
- isCascader: boolean;
154
- isTreeSelect: boolean;
155
- isSwitch: boolean;
156
- attrs: Record<string, any>;
157
- emptyText: string;
158
- }): string {
159
- // 1. 自定义映射
160
- if (valueToLabel) {
161
- try {
162
- return valueToLabel(modelValue) || emptyText;
163
- } catch (e) {
164
- console.warn('[ReadonlyHOC] valueToLabel error', e);
165
- }
166
- }
167
-
168
- // 2. Switch 特殊处理
169
- if (isSwitch) {
170
- const checkedVal = attrs.checkedValue ?? attrs['checked-value'] ?? true;
171
- const uncheckedVal = attrs.uncheckedValue ?? attrs['un-checked-value'] ?? false;
172
- if (modelValue === checkedVal) return attrs.checkedChildren ?? attrs['checked-children'] ?? '开启';
173
- if (modelValue === uncheckedVal) return attrs.uncheckedChildren ?? attrs['un-checked-children'] ?? '关闭';
174
- return String(modelValue || emptyText);
175
- }
176
-
177
- // 3. Cascader
178
- if (isCascader && Array.isArray(modelValue) && options?.length) {
179
- return findCascaderLabels(options, modelValue, fieldNames).join(' / ') || emptyText;
180
- }
181
-
182
- // 4. TreeSelect
183
- if (isTreeSelect && treeData?.length) {
184
- return findTreeSelectLabels(treeData, modelValue, fieldNames).join(', ') || emptyText;
185
- }
186
-
187
- // 5. 动态 options(Select / RadioGroup / CheckboxGroup)
188
- if (options?.length) {
189
- const valKey = fieldNames.value || 'value';
190
- const labelKey = fieldNames.label || 'label';
191
- const getLabel = (val: any) => {
192
- const opt = options.find(item => item[valKey] === val);
193
- return opt ? opt[labelKey] || opt[valKey] : val === undefined || val === null ? '' : String(val);
194
- };
195
- if (Array.isArray(modelValue)) {
196
- return modelValue.map(getLabel).join(', ') || emptyText;
197
- } else {
198
- return getLabel(modelValue) || emptyText;
199
- }
200
- }
201
-
202
- // 6. 静态插槽 fallback(Select / RadioGroup / CheckboxGroup)
203
- else if ((isSelect || isRadioGroup || isCheckboxGroup) && slots?.default) {
204
- try {
205
- const slotContent = slots.default();
206
-
207
- const labelMap = collectOptionLabelMapFromSlots(slotContent, fieldNames);
208
- if (labelMap.size > 0) {
209
- const getLabel = (val: any) => (val === undefined || val === null ? '' : labelMap.get(val) || String(val));
210
- if (Array.isArray(modelValue)) {
211
- return modelValue.map(getLabel).join(', ') || emptyText;
212
- } else {
213
- return getLabel(modelValue) || emptyText;
214
- }
215
- }
216
- } catch (e) {
217
- console.warn('[ReadonlyHOC] Failed to parse slot options', e);
218
- }
219
- }
220
-
221
- // 7. 单个 Checkbox / Radio 静态文本兜底
222
- else if (slots?.default && (isCheckbox || isRadio) && typeof modelValue === 'boolean') {
223
- try {
224
- const content = slots.default();
225
- const text = extractStaticText(content);
226
- if (text) return text;
227
- } catch (e) {
228
- /* ignore */
229
- }
230
- }
231
-
232
- // 8. 最终兜底
233
- if (modelValue == null || modelValue === '') return emptyText;
234
- return Array.isArray(modelValue) ? modelValue.join(', ') : String(modelValue);
235
- }
236
-
237
- // ========================
238
- // 高阶组件
239
- // ========================
240
- export function withReadonly(BaseComponent: Component) {
241
- const baseName = BaseComponent.name || '';
242
- const componentName = baseName.toLowerCase();
243
-
244
- const isSelect = /select/i.test(componentName) && !/tree|cascader/i.test(componentName);
245
- const isRadio = /radio/i.test(componentName) && !/group/i.test(componentName);
246
- const isRadioGroup = /radio.*group/i.test(componentName);
247
- const isCheckbox = /checkbox/i.test(componentName) && !/group/i.test(componentName);
248
- const isCheckboxGroup = /checkbox.*group/i.test(componentName);
249
- const isSwitch = /switch/i.test(componentName);
250
- const isCascader = /cascader/i.test(componentName);
251
- const isTreeSelect = /tree.*select/i.test(componentName);
252
-
253
- const isCheckType = isCheckbox || isSwitch || isRadio;
254
- const nativeProp = isCheckType ? 'checked' : 'value';
255
- const updateEvent = `update:${nativeProp}`;
256
-
257
- const isForm = /form$/i.test(componentName);
258
- const isFormItem = /form.*item/i.test(componentName);
259
-
260
- return defineComponent({
261
- name: baseName,
262
- inheritAttrs: false,
263
- props: {
264
- modelValue: { type: [String, Number, Boolean, Array, Object] as PropType<FormModelValue>, default: undefined },
265
- [nativeProp]: { type: [String, Number, Boolean, Array, Object] as PropType<FormModelValue>, default: undefined },
266
- readonly: { type: Boolean, default: undefined },
267
- emptyText: { type: String, default: '--' },
268
- valueToLabel: { type: Function as PropType<ValueToLabelFn>, default: null },
269
- },
270
- emits: ['update:modelValue', updateEvent],
271
- setup(props, { emit, slots, expose }) {
272
- const globalReadonly = inject<Ref<boolean>>('nkReadonly', ref(false));
273
- const isReadonly = computed(() => props.readonly ?? globalReadonly.value);
274
- const formReadonly = computed(() => props.readonly ?? globalReadonly.value);
275
-
276
- if (isForm) provide('nkReadonly', formReadonly);
277
-
278
- const finalValue = computed(() => (props[nativeProp] !== undefined ? props[nativeProp] : props.modelValue));
279
-
280
- const emitUpdate = (val: FormModelValue) => {
281
- emit('update:modelValue', val);
282
- emit(updateEvent, val);
283
- };
284
-
285
- // ✅ 使用 useAttrs() 保证响应式
286
- const attrs = useAttrs();
287
-
288
- const displayText = computed(() => {
289
- if (!isReadonly.value) return '';
290
-
291
- const fieldNames = attrs.fieldNames ?? attrs['field-names'] ?? {};
292
- const options = attrs.options as any[] | undefined;
293
- const treeData = (attrs.treeData ?? attrs['tree-data']) as any[] | undefined;
294
-
295
- return getDisplayText({
296
- modelValue: finalValue.value as FormModelValue,
297
- options,
298
- treeData,
299
- valueToLabel: props.valueToLabel as ValueToLabelFn,
300
- fieldNames: fieldNames as Record<string, string>,
301
- slots,
302
- isSelect,
303
- isRadioGroup,
304
- isCheckboxGroup,
305
- isCheckbox,
306
- isRadio,
307
- isCascader,
308
- isTreeSelect,
309
- isSwitch,
310
- attrs,
311
- emptyText: props.emptyText as string,
312
- });
313
- });
314
-
315
- // ⚠️ 开发警告
316
- if (import.meta?.env?.DEV) {
317
- watch(
318
- () => isReadonly.value,
319
- readonly => {
320
- if (
321
- readonly &&
322
- !attrs.options &&
323
- !attrs.treeData &&
324
- !props.valueToLabel &&
325
- slots?.default &&
326
- (isSelect || isRadioGroup || isCheckboxGroup || isCascader || isTreeSelect)
327
- ) {
328
- console.warn(
329
- `[ReadonlyHOC] Detected slot-only usage for ${baseName} in readonly mode. ` +
330
- `This works for static content, but will fail for async/dynamic data. ` +
331
- `✅ Recommended: Use \`options\` or \`treeData\` prop instead.`
332
- );
333
- }
334
- },
335
- { immediate: true }
336
- );
337
- }
338
-
339
- const readonlyStyle = {
340
- lineHeight: '32px',
341
- padding: '0 6px',
342
- display: 'inline-block',
343
- minHeight: '32px',
344
- border: 'none',
345
- backgroundColor: 'none',
346
- cursor: 'default',
347
- wordBreak: 'break-all',
348
- color: 'rgba(0, 0, 0, 0.65)',
349
- };
350
-
351
- const formRef = ref<any>();
352
- if (isForm) {
353
- expose({
354
- validate: () => (isReadonly.value ? Promise.resolve({}) : formRef.value?.validate?.()),
355
- validateFields: (names?: string[]) => (isReadonly.value ? Promise.resolve({}) : formRef.value?.validateFields?.(names)),
356
- resetFields: (names?: string[]) => !isReadonly.value && formRef.value?.resetFields?.(names),
357
- clearValidate: (names?: string[]) => !isReadonly.value && formRef.value?.clearValidate?.(names),
358
- scrollToField: (name: string) => !isReadonly.value && formRef.value?.scrollToField?.(name),
359
- });
360
- }
361
-
362
- return () => {
363
- if (isFormItem) {
364
- return h(
365
- BaseComponent,
366
- {
367
- ...attrs,
368
- style: { ...(attrs.style || {}), marginBottom: isReadonly.value ? '10px' : undefined },
369
- },
370
- slots
371
- );
372
- }
373
-
374
- if (isForm) {
375
- return h(
376
- BaseComponent,
377
- {
378
- ...attrs,
379
- rules: formReadonly.value ? null : attrs.rules,
380
- disabled: formReadonly.value ? true : attrs.disabled,
381
- ref: formRef,
382
- },
383
- slots
384
- );
385
- }
386
-
387
- if (isReadonly.value) {
388
- return h(
389
- 'span',
390
- {
391
- class: 'nk-readonly-wrapper',
392
- style: readonlyStyle,
393
- role: 'text',
394
- tabindex: 0,
395
- 'aria-readonly': 'true',
396
- },
397
- displayText.value
398
- );
399
- }
400
-
401
- return h(
402
- BaseComponent,
403
- {
404
- ...attrs,
405
- [nativeProp]: finalValue.value,
406
- [`onUpdate:${nativeProp}`]: emitUpdate,
407
- ref: isForm ? formRef : undefined,
408
- },
409
- slots
410
- );
411
- };
412
- },
413
- });
414
- }
package/tsconfig.json DELETED
@@ -1,19 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES2020",
4
- "useDefineForClassFields": true,
5
- "module": "ESNext",
6
- "lib": ["ES2020", "DOM"],
7
- "moduleResolution": "bundler",
8
- "strict": true,
9
- "jsx": "preserve",
10
- "esModuleInterop": true,
11
- "skipLibCheck": true,
12
- "declaration": true,
13
- "declarationDir": "./dist",
14
- "outDir": "./dist",
15
- "allowSyntheticDefaultImports": true,
16
- "forceConsistentCasingInFileNames": true
17
- },
18
- "include": ["src"]
19
- }
@@ -1,17 +0,0 @@
1
- import { defineConfig } from 'vite'
2
- import vue from '@vitejs/plugin-vue'
3
- import { resolve } from 'path'
4
-
5
- export default defineConfig({
6
- root: 'playground',
7
- plugins: [vue()],
8
- resolve: {
9
- alias: {
10
- '@': resolve(__dirname, 'src'),
11
- },
12
- },
13
- server: {
14
- host: '0.0.0.0',
15
- port: 5200,
16
- },
17
- })
package/vite.config.ts DELETED
@@ -1,20 +0,0 @@
1
- import { resolve } from 'path';
2
- import { defineConfig } from 'vite';
3
- import dts from 'vite-plugin-dts';
4
-
5
- export default defineConfig({
6
- build: {
7
- lib: {
8
- entry: resolve(__dirname, 'src/index.ts'),
9
- name: 'NDesignReadolyPlugin',
10
- fileName: (format) => `index.${format}.js`,
11
- formats: ['es', 'cjs']
12
- },
13
- rollupOptions: {
14
- // external 排除 vue 和 n-designv3
15
- external: ['vue','n-designv3'],
16
- output: { globals: { vue: 'Vue' } }
17
- }
18
- },
19
- plugins: [dts({ insertTypesEntry: true })]
20
- });