@neatui/vue 4.8.2 → 5.0.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.
- package/MIGRATION-5.0.md +66 -0
- package/README.md +3 -0
- package/package.json +16 -8
- package/src/components/basic/IFollowView.vue +1 -1
- package/src/components/basic/LayerView/Layer.vue +0 -2
- package/src/components/display/Avatar.vue +124 -1
- package/src/components/display/Tree.vue +10 -4
- package/src/components/display/TreeView.vue +12 -5
- package/src/components/display/index.ts +2 -1
- package/src/components/form/Cascader.vue +1 -1
- package/src/components/form/DateRangeView.vue +6 -6
- package/src/components/form/Gallery.vue +50 -0
- package/src/components/form/Input.vue +5 -2
- package/src/components/form/PageSelect.vue +3 -2
- package/src/components/form/Radio.vue +27 -0
- package/src/components/form/RichTextInput.vue +99 -0
- package/src/components/form/SearchMoreSelect.vue +3 -2
- package/src/components/form/Select.vue +4 -4
- package/src/components/form/SelectPicker.vue +3 -2
- package/src/components/form/Textarea.vue +169 -57
- package/src/components/form/TimePicker.vue +43 -7
- package/src/components/form/Upload.vue +1 -1
- package/src/components/form/index.ts +7 -0
- package/src/components/loader/FormLoader/FormLoader.vue +908 -349
- package/src/components/loader/FormLoader/FormRender.vue +409 -0
- package/src/components/loader/TableLoader/TableLoader.vue +338 -32
- package/src/components/loader/TableLoader/virtual.ts +27 -0
- package/src/components/loader/index.ts +2 -0
- package/src/index.ts +2 -0
- package/.prettierrc +0 -12
- package/rollup.config.js +0 -75
- package/tsconfig.json +0 -23
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="flex-block pr" @focusout="col._touched = true" :aria-describedby="col._error && mode !== 'view' ? `${col._fid}-error` : undefined">
|
|
3
|
+
<slot v-if="custom" />
|
|
4
|
+
<button v-else-if="col.model[0] === 'Button'" type="button" ui-btn="@a s" v-bind="col.model[1]" v-on="events"><FieldExtra :value="displayOf(col, ctx)" /></button>
|
|
5
|
+
<div v-else-if="col.model[0] === 'Html'" v-bind="col.model[1]" v-on="events" v-html="displayOf(col, ctx)"></div>
|
|
6
|
+
<div v-else-if="col.model[0] === 'Text'" v-bind="col.model[1]" v-on="events"><FieldExtra :value="displayOf(col, ctx)" /></div>
|
|
7
|
+
<div v-else-if="(mode === 'view' && !entry?.view) || entry?.component === FieldView" class="flex-block o-ls" :class="clh(col.model?.[1] || {})" v-on="events">{{ text }}</div>
|
|
8
|
+
<component
|
|
9
|
+
v-else-if="entry"
|
|
10
|
+
:is="entry.component"
|
|
11
|
+
:modelValue="displayOf(col, ctx)"
|
|
12
|
+
:interacted="interacted"
|
|
13
|
+
:options="entry.options ? col.enums : undefined"
|
|
14
|
+
:class="entry.class"
|
|
15
|
+
v-bind="col.model[1] || {}"
|
|
16
|
+
:rules="[]"
|
|
17
|
+
:verify="false"
|
|
18
|
+
:co="col._error?.type || col.model[1]?.co"
|
|
19
|
+
:readonly="mode === 'view' ? true : col.model[1]?.readonly"
|
|
20
|
+
:disabled="mode === 'view' ? true : col.model[1]?.disabled"
|
|
21
|
+
v-on="events"
|
|
22
|
+
@update:modelValue="update"
|
|
23
|
+
/>
|
|
24
|
+
<FieldMessage :col="col" :mode="mode" />
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
import { defineComponent, h, isVNode, markRaw } from 'vue';
|
|
29
|
+
import type { Component } from 'vue';
|
|
30
|
+
import { isArray, isObject, isString, isFunction } from '@fekit/utils';
|
|
31
|
+
|
|
32
|
+
/** 最近一层数组的操作上下文;行引用在排序后仍指向原行。 */
|
|
33
|
+
export interface FormArrayContext {
|
|
34
|
+
readonly list: any[];
|
|
35
|
+
readonly row: any;
|
|
36
|
+
readonly index: number;
|
|
37
|
+
readonly canAdd: boolean;
|
|
38
|
+
readonly canRemove: boolean;
|
|
39
|
+
readonly canMoveUp: boolean;
|
|
40
|
+
readonly canMoveDown: boolean;
|
|
41
|
+
add: () => void;
|
|
42
|
+
insertAfter: (index?: number) => void;
|
|
43
|
+
remove: () => void;
|
|
44
|
+
moveUp: () => void;
|
|
45
|
+
moveDown: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 联动上下文 */
|
|
49
|
+
export interface FormContext {
|
|
50
|
+
// 根表单数据
|
|
51
|
+
root: any;
|
|
52
|
+
// 当前层数据:分组内是嵌套对象,数组内是当前行
|
|
53
|
+
data: any;
|
|
54
|
+
// 数组行下标,不在数组内时为 undefined
|
|
55
|
+
index?: number;
|
|
56
|
+
array?: FormArrayContext;
|
|
57
|
+
// 当前层在根数据中的路径,比如 'business.address'、'members.0'
|
|
58
|
+
path: string;
|
|
59
|
+
mode: string;
|
|
60
|
+
route: any;
|
|
61
|
+
router: any;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 动态属性函数:(根数据只读副本, 当前字段, 上下文) */
|
|
65
|
+
export type FormSetup = (data: any, item: any, ctx: Omit<FormContext, 'path'> & { path: string; formPath: string; [key: string]: any }, router: any) => any;
|
|
66
|
+
|
|
67
|
+
/** 带监听的动态属性配置;watch 用点路径,'.' 开头表示当前层 */
|
|
68
|
+
export interface FormLinkage<T = any> {
|
|
69
|
+
value?: T;
|
|
70
|
+
setup?: FormSetup;
|
|
71
|
+
watch?: string[] | true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 可以写成固定值、函数或联动配置的属性 */
|
|
75
|
+
export type Dynamic<T> = T | FormSetup | FormLinkage<T>;
|
|
76
|
+
|
|
77
|
+
/** 校验规则 */
|
|
78
|
+
export interface FormRule {
|
|
79
|
+
required?: boolean;
|
|
80
|
+
validator?: (value: any) => boolean | Promise<boolean>;
|
|
81
|
+
msg?: string;
|
|
82
|
+
// 兼容旧版提示字段;同时设置时以 msg 为准(包括空字符串)。
|
|
83
|
+
message?: string;
|
|
84
|
+
type?: 'risk' | 'warn' | string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 字段值:default 初始化,get 显示/校验,set 写回。普通 value 可直接表示内容。 */
|
|
88
|
+
export interface FormValue {
|
|
89
|
+
default?: any;
|
|
90
|
+
get?: (value: any, ctx: FormContext) => any;
|
|
91
|
+
set?: (value: any, ctx: FormContext) => any;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 数组配置 */
|
|
95
|
+
export interface FormArray {
|
|
96
|
+
// 每一行的字段,兼容旧版 lists
|
|
97
|
+
child?: FormItem[];
|
|
98
|
+
lists?: FormItem[];
|
|
99
|
+
min?: number;
|
|
100
|
+
max?: number;
|
|
101
|
+
// 是否显示上移/下移
|
|
102
|
+
sortable?: boolean;
|
|
103
|
+
// 添加按钮文案,false 不显示
|
|
104
|
+
add?: string | false;
|
|
105
|
+
// 删除按钮文案,false 不显示
|
|
106
|
+
del?: string | false;
|
|
107
|
+
// 行内栅格
|
|
108
|
+
rows?: string;
|
|
109
|
+
// 绑定到每一行容器上
|
|
110
|
+
attrs?: Record<string, any>;
|
|
111
|
+
// 新行默认值
|
|
112
|
+
item?: Record<string, any> | (() => Record<string, any>);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 基础节点:标题和子节点复用同一套配置,不为装饰或交互增加专用字段。 */
|
|
116
|
+
export interface FormItem {
|
|
117
|
+
label?: Dynamic<string | [string, Record<string, any>] | FormItem[]>;
|
|
118
|
+
field?: string;
|
|
119
|
+
value?: any | FormValue;
|
|
120
|
+
child?: Dynamic<FormItem[]>;
|
|
121
|
+
attrs?: Dynamic<Record<string, any>>;
|
|
122
|
+
clear?: Dynamic<boolean | { value: boolean; restore?: boolean }>;
|
|
123
|
+
enums?: Dynamic<any[]>;
|
|
124
|
+
array?: Dynamic<FormArray | string>;
|
|
125
|
+
event?: Record<string, (ctx: FormContext & { item: any; node: any }, ...args: any[]) => any>;
|
|
126
|
+
model?: Dynamic<string | [string, Record<string, any>?]>;
|
|
127
|
+
rules?: Dynamic<FormRule[]>;
|
|
128
|
+
// 兼容原有布局和数据路径;普通容器布局也可直接使用 attrs。
|
|
129
|
+
media?: Dynamic<string>;
|
|
130
|
+
vertical?: Dynamic<boolean>;
|
|
131
|
+
paths?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 校验结果 */
|
|
135
|
+
export interface FormError {
|
|
136
|
+
field: string;
|
|
137
|
+
path: string;
|
|
138
|
+
label: string;
|
|
139
|
+
msg: string;
|
|
140
|
+
type: string;
|
|
141
|
+
}
|
|
142
|
+
export interface FormValidateResult {
|
|
143
|
+
valid: boolean;
|
|
144
|
+
errors: FormError[];
|
|
145
|
+
warnings: FormError[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** FormLoader 对外方法 */
|
|
149
|
+
export interface FormLoaderExpose {
|
|
150
|
+
validate: (options?: { scroll?: boolean }) => Promise<FormValidateResult>;
|
|
151
|
+
reset: () => void;
|
|
152
|
+
clearValidate: () => void;
|
|
153
|
+
scrollToField: (path: string) => void;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** 动态属性任务 */
|
|
157
|
+
export interface LinkageTask {
|
|
158
|
+
idx: number;
|
|
159
|
+
key: string;
|
|
160
|
+
value: any;
|
|
161
|
+
setup: FormSetup;
|
|
162
|
+
// true 表示监听全部数据
|
|
163
|
+
watch: string[] | true;
|
|
164
|
+
hasInitialResult: boolean;
|
|
165
|
+
initialResult: any;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 控件注册信息 */
|
|
169
|
+
export interface FieldEntry {
|
|
170
|
+
component: Component;
|
|
171
|
+
// 查看模式使用控件自身的 readonly 展示(如图片、富文本)。
|
|
172
|
+
view?: boolean;
|
|
173
|
+
validate?: (value: any, props: any) => string | null | Promise<string | null>;
|
|
174
|
+
// 是否把 enums 作为 options 传入
|
|
175
|
+
options?: boolean;
|
|
176
|
+
// 控件默认 class
|
|
177
|
+
class?: string;
|
|
178
|
+
// 字段缺失时的初始值,函数参数为 model[1]
|
|
179
|
+
emptyValue?: any | ((props: any) => any);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 计算Label行高
|
|
183
|
+
export const clh = ({ sz = '' }: any = {}) => {
|
|
184
|
+
const lhs: any = { xs: 'lh-sm', s: 'lh-mm', m: 'lh-ls', l: 'lh-ll', xl: 'lh-xl' };
|
|
185
|
+
return lhs[sz] || 'lh-ls';
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// 数组里全是空值也算空,比如日期范围的 ['', '']
|
|
189
|
+
const isBlank = (value: any) => value === '' || value === null || value === undefined;
|
|
190
|
+
export const isEmpty = (value: any) => isBlank(value) || (isArray(value) && value.every(isBlank));
|
|
191
|
+
|
|
192
|
+
// 标题既可以是文字,也可以是一棵普通节点树。
|
|
193
|
+
export const labelNodes = (label: any): FormItem[] | null =>
|
|
194
|
+
isArray(label) && label.every((item: any) => isObject(item) && !isArray(item)) ? label : null;
|
|
195
|
+
export const labelText = (label: any): string => {
|
|
196
|
+
const nodes = labelNodes(label);
|
|
197
|
+
if (nodes) return nodes.map((node) => typeof node.value === 'string' ? node.value : labelText(node.label)).join('');
|
|
198
|
+
return String((isArray(label) ? label[0] : label) ?? '');
|
|
199
|
+
};
|
|
200
|
+
export const labelAttrs = (label: any) => (!labelNodes(label) && isArray(label) ? label[1] || {} : {});
|
|
201
|
+
export const rulesOf = (col: any = {}): FormRule[] => {
|
|
202
|
+
// model 参数与旧版 v-bind 一致,可以覆盖节点 rules(包括显式空数组)。
|
|
203
|
+
const rules = col.model?.[1]?.rules ?? col.rules;
|
|
204
|
+
return isArray(rules) ? rules.map((rule: FormRule) => rule && rule.msg == null && rule.message !== undefined ? { ...rule, msg: rule.message } : rule) : [];
|
|
205
|
+
};
|
|
206
|
+
export const valueBinding = (value: any): value is FormValue =>
|
|
207
|
+
isObject(value) && !isArray(value) && ['default', 'get', 'set'].some((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
208
|
+
export const displayOf = (col: any = {}, ctx?: any) => {
|
|
209
|
+
const raw = col._data?.[col.field];
|
|
210
|
+
if (valueBinding(col.value)) return isFunction(col.value.get) ? col.value.get(raw, ctx) : raw;
|
|
211
|
+
return col.value !== undefined ? col.value : raw;
|
|
212
|
+
};
|
|
213
|
+
// 内置控件和插槽使用相同写回规则,查看模式不接受写入。
|
|
214
|
+
export const writeValue = (col: any, value: any, ctx: FormContext) => {
|
|
215
|
+
if (!col.field || ctx.mode === 'view') return;
|
|
216
|
+
const binding = col.value;
|
|
217
|
+
col._data[col.field] = valueBinding(binding) && typeof binding.set === 'function' ? binding.set(value, ctx) : value;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const eventsOf = (col: any, ctx: FormContext) => {
|
|
221
|
+
const handlers: Record<string, (...args: any[]) => any> = {};
|
|
222
|
+
Object.entries(col.event || {}).forEach(([name, fn]) => {
|
|
223
|
+
if (isFunction(fn)) handlers[name] = (...args: any[]) => (fn as Function)({ ...ctx, item: col, node: col }, ...args);
|
|
224
|
+
});
|
|
225
|
+
return handlers;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// 内容渲染:字符串按文本渲染;VNode 原样渲染;{ text, html, attrs } 只有 html 为 true 时才按 HTML 渲染
|
|
229
|
+
const render = (value: any): any => {
|
|
230
|
+
if (value === null || value === undefined || value === '') return null;
|
|
231
|
+
if (isVNode(value)) return value;
|
|
232
|
+
if (isArray(value)) return value.map(render);
|
|
233
|
+
if (isObject(value)) {
|
|
234
|
+
const { text = '', html = false, attrs = {} } = value;
|
|
235
|
+
return html ? h('span', { ...attrs, innerHTML: text }) : h('span', attrs, text);
|
|
236
|
+
}
|
|
237
|
+
return h('span', String(value));
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export const FieldMessage = defineComponent({
|
|
241
|
+
props: ['col', 'mode'],
|
|
242
|
+
setup: (props) => () => props.mode !== 'view' ? [
|
|
243
|
+
props.col.model?.[1]?.verify && props.col._checked
|
|
244
|
+
? h('i', { 'ui-form-verify': props.col._error ? 'no' : isEmpty(props.col._data?.[props.col.field]) ? '' : 'ok', 'aria-hidden': 'true' }) : null,
|
|
245
|
+
props.col._error ? h('p', { id: `${props.col._fid}-error`, 'data-field-error': props.col.field,
|
|
246
|
+
role: props.col._error.type === 'warn' ? 'status' : 'alert',
|
|
247
|
+
class: `mt-xs ${props.col._error.type === 'warn' ? 'co-warn' : 'co-risk'}` }, props.col._error.msg) : null,
|
|
248
|
+
] : null,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
export const FieldExtra = defineComponent({
|
|
252
|
+
name: 'FieldExtra',
|
|
253
|
+
props: {
|
|
254
|
+
value: { type: null, default: null },
|
|
255
|
+
},
|
|
256
|
+
setup(props) {
|
|
257
|
+
return () => render(props.value);
|
|
258
|
+
},
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
import {
|
|
262
|
+
Radio,
|
|
263
|
+
Gallery,
|
|
264
|
+
RichTextInput,
|
|
265
|
+
Input,
|
|
266
|
+
InputRange,
|
|
267
|
+
Switch,
|
|
268
|
+
Checkbox,
|
|
269
|
+
CheckboxGroup,
|
|
270
|
+
Textarea,
|
|
271
|
+
Select,
|
|
272
|
+
SelectList,
|
|
273
|
+
SelectTags,
|
|
274
|
+
SelectPicker,
|
|
275
|
+
SearchSelect,
|
|
276
|
+
PageSelect,
|
|
277
|
+
SelectTree,
|
|
278
|
+
MoreSelect,
|
|
279
|
+
MoreSelectList,
|
|
280
|
+
MoreSelectTags,
|
|
281
|
+
MoreSelectPicker,
|
|
282
|
+
MoreSelectTree,
|
|
283
|
+
DatePicker,
|
|
284
|
+
TimePicker,
|
|
285
|
+
DateRangePicker,
|
|
286
|
+
SearchMoreSelect,
|
|
287
|
+
ImgUpload,
|
|
288
|
+
Upload,
|
|
289
|
+
Cascader,
|
|
290
|
+
} from '../../form';
|
|
291
|
+
import { timeError } from '../../form/TimePicker.vue';
|
|
292
|
+
import { Tree } from '../../display';
|
|
293
|
+
|
|
294
|
+
const option = (component: any, extra: Partial<FieldEntry> = {}): FieldEntry => ({ component: markRaw(component), options: true, class: 'w-full', ...extra });
|
|
295
|
+
const multiple = { emptyValue: () => [] };
|
|
296
|
+
|
|
297
|
+
// View 由当前渲染器直接展示;注册表保留标记以兼容自定义注册。
|
|
298
|
+
const FieldView = defineComponent({ name: 'FormView', render: () => null });
|
|
299
|
+
|
|
300
|
+
// 占位:只占栅格,不渲染内容
|
|
301
|
+
const Blank = defineComponent({ name: 'FormBlank', render: () => null });
|
|
302
|
+
|
|
303
|
+
// model[0] => 控件,未注册的 model 走插槽
|
|
304
|
+
const registry: Record<string, FieldEntry> = {
|
|
305
|
+
Radio: option(Radio),
|
|
306
|
+
Gallery: option(Gallery, { view: true, emptyValue: (props: any) => props?.multiple ? [] : '' }),
|
|
307
|
+
RichTextInput: { component: markRaw(RichTextInput), view: true, class: 'w-full' },
|
|
308
|
+
Input: { component: markRaw(Input), class: 'w-full' },
|
|
309
|
+
InputRange: { component: markRaw(InputRange), class: 'w-full', emptyValue: () => [] },
|
|
310
|
+
Select: option(Select),
|
|
311
|
+
SelectList: option(SelectList),
|
|
312
|
+
SelectTags: option(SelectTags, multiple),
|
|
313
|
+
SelectPicker: option(SelectPicker),
|
|
314
|
+
SearchSelect: option(SearchSelect),
|
|
315
|
+
MoreSelect: option(MoreSelect, multiple),
|
|
316
|
+
MoreSelectList: option(MoreSelectList, multiple),
|
|
317
|
+
MoreSelectTags: option(MoreSelectTags, multiple),
|
|
318
|
+
MoreSelectPicker: option(MoreSelectPicker, multiple),
|
|
319
|
+
SearchMoreSelect: option(SearchMoreSelect, multiple),
|
|
320
|
+
MoreSelectTree: option(MoreSelectTree, multiple),
|
|
321
|
+
PageSelect: option(PageSelect),
|
|
322
|
+
SelectTree: option(SelectTree),
|
|
323
|
+
Cascader: option(Cascader, { emptyValue: (props: any) => (props?.multiple ? [] : '') }),
|
|
324
|
+
DatePicker: { component: markRaw(DatePicker), class: 'w-full' },
|
|
325
|
+
TimePicker: { component: markRaw(TimePicker), validate: timeError, class: 'w-full' },
|
|
326
|
+
DateRangePicker: { component: markRaw(DateRangePicker), class: 'w-full', emptyValue: () => [] },
|
|
327
|
+
ImgUpload: { component: markRaw(ImgUpload) },
|
|
328
|
+
Upload: { component: markRaw(Upload) },
|
|
329
|
+
Textarea: { component: markRaw(Textarea) },
|
|
330
|
+
Switch: { component: markRaw(Switch), emptyValue: false },
|
|
331
|
+
// Checkbox 按值是否为数组区分单选/多选:options 是 { label, value } 列表时初始化为 []
|
|
332
|
+
Checkbox: {
|
|
333
|
+
component: markRaw(Checkbox),
|
|
334
|
+
emptyValue: (props: any) => (Array.isArray(props?.options) && props.options.some((item: any) => item && typeof item === 'object' && 'label' in item) ? [] : false),
|
|
335
|
+
},
|
|
336
|
+
CheckboxGroup: { component: markRaw(CheckboxGroup), options: true, emptyValue: () => [] },
|
|
337
|
+
Tree: { component: markRaw(Tree), class: 'mt-sm', emptyValue: () => [] },
|
|
338
|
+
View: { component: markRaw(FieldView), options: true },
|
|
339
|
+
Text: { component: markRaw(FieldView) },
|
|
340
|
+
Html: { component: markRaw(FieldView) },
|
|
341
|
+
Button: { component: markRaw(FieldView) },
|
|
342
|
+
Blank: { component: markRaw(Blank) },
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// 仅补名称别名;显式注册同名控件时仍优先使用注册项。
|
|
346
|
+
const fieldAliases: Record<string, string> = { TextArea: 'Textarea', view: 'View', SelectImg: 'Gallery' };
|
|
347
|
+
export const getField = (model: string = ''): FieldEntry | undefined => registry[model] || registry[fieldAliases[model]];
|
|
348
|
+
|
|
349
|
+
// 注册自定义控件,比如业务里的图片库、富文本;注册后 model 直接写名字即可
|
|
350
|
+
export const registerField = (name: string, entry: FieldEntry) => {
|
|
351
|
+
registry[name] = { ...entry, component: markRaw(entry.component as any) };
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// 字段缺失时的初始值
|
|
355
|
+
export const emptyValueOf = (model: any[] = []) => {
|
|
356
|
+
const entry = getField(model[0]);
|
|
357
|
+
const value = entry?.emptyValue;
|
|
358
|
+
if (typeof value === 'function') return value(model[1]);
|
|
359
|
+
return value === undefined ? '' : value;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
</script>
|
|
363
|
+
<script lang="ts" setup>
|
|
364
|
+
import { computed } from 'vue';
|
|
365
|
+
const props = defineProps<{
|
|
366
|
+
col: any;
|
|
367
|
+
entry?: FieldEntry;
|
|
368
|
+
interacted: boolean;
|
|
369
|
+
mode?: string;
|
|
370
|
+
ctx: FormContext;
|
|
371
|
+
custom?: boolean;
|
|
372
|
+
}>();
|
|
373
|
+
// 在选项树里按 value 找 label
|
|
374
|
+
const findLabel = (options: any[] = [], value: any): any => {
|
|
375
|
+
for (const option of isArray(options) ? options : []) {
|
|
376
|
+
if (option && typeof option === 'object') {
|
|
377
|
+
if (`${option.value}` === `${value}`) return option.label;
|
|
378
|
+
const found = findLabel(option.children, value);
|
|
379
|
+
if (found !== undefined) return found;
|
|
380
|
+
} else if (`${option}` === `${value}`) {
|
|
381
|
+
return option;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return undefined;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// 原控件类型:model 为 View 时由 model[1].type 指定,否则就是 model[0]
|
|
388
|
+
const type = computed(() => (['View', 'view'].includes(props.col.model?.[0]) ? props.col.model?.[1]?.type : props.col.model?.[0]));
|
|
389
|
+
|
|
390
|
+
const text = computed(() => {
|
|
391
|
+
const { col } = props;
|
|
392
|
+
const value = displayOf(col, props.ctx);
|
|
393
|
+
const format = col.model?.[1]?.format;
|
|
394
|
+
if (isFunction(format)) return format(value, col);
|
|
395
|
+
if (isEmpty(value)) return col.model?.[1]?.empty ?? '-';
|
|
396
|
+
if (typeof value === 'boolean') return value ? '是' : '否';
|
|
397
|
+
const one = (v: any) => findLabel(col.enums, v) ?? v;
|
|
398
|
+
if (isArray(value)) {
|
|
399
|
+
// 日期范围等控件的空值是 ['', '']
|
|
400
|
+
const list = value.filter((v: any) => !isEmpty(v));
|
|
401
|
+
if (!list.length) return col.model?.[1]?.empty ?? '-';
|
|
402
|
+
const sep = type.value === 'DateRangePicker' ? ' ~ ' : type.value === 'Cascader' ? ' / ' : '、';
|
|
403
|
+
return list.map(one).join(sep);
|
|
404
|
+
}
|
|
405
|
+
return String(one(value));
|
|
406
|
+
});
|
|
407
|
+
const update = (value: any) => { props.col._touched = true; writeValue(props.col, value, props.ctx); };
|
|
408
|
+
const events = computed(() => eventsOf(props.col, props.ctx));
|
|
409
|
+
</script>
|