@neatui/vue 4.8.2 → 5.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.
- package/MIGRATION-5.0.md +73 -0
- package/README.md +3 -0
- package/package.json +12 -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/SearchMoreSelect.vue +3 -2
- package/src/components/form/Select.vue +22 -28
- 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 +5 -0
- package/src/components/loader/FormLoader/FormLoader.vue +1098 -351
- package/src/components/loader/FormLoader/FormRender.vue +503 -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
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
other?: {
|
|
115
115
|
value: string;
|
|
116
116
|
field?: string;
|
|
117
|
-
config?:
|
|
117
|
+
config?: Record<string, any>;
|
|
118
118
|
};
|
|
119
119
|
otherValue?: string;
|
|
120
120
|
tools?: boolean;
|
|
@@ -235,15 +235,20 @@
|
|
|
235
235
|
};
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// 值不在当前选项中(选项未加载、分页或远程搜索)时显示原始值,避免看起来像未选择
|
|
239
|
+
if (props.modelValue !== null && props.modelValue !== undefined && props.modelValue !== '') {
|
|
240
|
+
return { label: String(props.modelValue), value: props.modelValue };
|
|
241
|
+
}
|
|
242
|
+
|
|
238
243
|
return {};
|
|
239
244
|
}),
|
|
240
|
-
displayLabel: computed(() => {
|
|
245
|
+
displayLabel: computed((): string => {
|
|
241
246
|
return state.selectd.label || '';
|
|
242
247
|
}),
|
|
243
|
-
availableCount: computed(() => {
|
|
248
|
+
availableCount: computed((): number => {
|
|
244
249
|
return state.filter?.filter((item: any) => !item.disabled)?.length || 0;
|
|
245
250
|
}),
|
|
246
|
-
filter: computed(() => {
|
|
251
|
+
filter: computed((): any[] => {
|
|
247
252
|
let filteredOptions = props.options || [];
|
|
248
253
|
|
|
249
254
|
if (state.search) {
|
|
@@ -285,35 +290,24 @@
|
|
|
285
290
|
{ immediate: true },
|
|
286
291
|
);
|
|
287
292
|
|
|
288
|
-
//
|
|
293
|
+
// 值是否在选项中(含 all 选项)
|
|
294
|
+
const inOptions = (value: any, options: any[] | undefined, allOption: any) =>
|
|
295
|
+
(allOption && value === allOption.value) || !!options?.some((option: any) => `${option.value}` === `${value}`);
|
|
296
|
+
|
|
297
|
+
// 只在选项变化使原本有效的值失效时清空(如级联切换上级后)。
|
|
298
|
+
// 值由外部写入(加载记录)、选项异步未到、分页或远程搜索尚未包含当前值时保留,避免丢失数据。
|
|
289
299
|
watch(
|
|
290
300
|
[() => props.modelValue, () => props.options, () => props.all, () => props.other],
|
|
291
|
-
([value, options, allOption, otherConfig]) => {
|
|
301
|
+
([value, options, allOption, otherConfig], [previousValue, previousOptions]) => {
|
|
292
302
|
// 如果配置了 other,则允许任意值(可能是用户自定义输入)
|
|
293
|
-
if (otherConfig)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
let isValid = false;
|
|
300
|
-
|
|
301
|
-
// 检查是否是 all 选项
|
|
302
|
-
if (allOption && value === allOption.value) {
|
|
303
|
-
isValid = true;
|
|
304
|
-
} else {
|
|
305
|
-
// 检查是否在普通选项中
|
|
306
|
-
isValid = options.some((option: any) => `${option.value}` === `${value}`);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (!isValid) {
|
|
310
|
-
nextTick(() => {
|
|
311
|
-
change(null);
|
|
312
|
-
});
|
|
313
|
-
}
|
|
303
|
+
if (otherConfig || value == null || value === '' || !options?.length) return;
|
|
304
|
+
const invalidated = value === previousValue && options !== previousOptions && inOptions(value, previousOptions, allOption) && !inOptions(value, options, allOption);
|
|
305
|
+
if (invalidated) {
|
|
306
|
+
nextTick(() => {
|
|
307
|
+
change(null);
|
|
308
|
+
});
|
|
314
309
|
}
|
|
315
310
|
},
|
|
316
|
-
{ immediate: true },
|
|
317
311
|
);
|
|
318
312
|
|
|
319
313
|
// 选择
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
</template>
|
|
29
29
|
|
|
30
30
|
<script setup lang="ts">
|
|
31
|
+
import type { PropType } from 'vue';
|
|
31
32
|
import { reactive, ref, computed } from 'vue';
|
|
32
33
|
import Input from './Input.vue';
|
|
33
34
|
import { IPickerView, IScrollView } from '../basic';
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
const emits = defineEmits(['update:modelValue']);
|
|
37
38
|
const props = defineProps({
|
|
38
39
|
theme: { type: String, default: '@a' },
|
|
39
|
-
sz: { type: String
|
|
40
|
+
sz: { type: String as PropType<'' | 'xs' | 's' | 'm' | 'l' | 'xl'>, default: 'm' },
|
|
40
41
|
co: { type: String, default: '' },
|
|
41
42
|
block: { type: Boolean, default: true },
|
|
42
43
|
tips: { type: String, default: '' },
|
|
@@ -50,7 +51,7 @@
|
|
|
50
51
|
default: '',
|
|
51
52
|
},
|
|
52
53
|
rules: {
|
|
53
|
-
type: Array
|
|
54
|
+
type: Array as PropType<object[]>,
|
|
54
55
|
default: () => [],
|
|
55
56
|
},
|
|
56
57
|
});
|
|
@@ -44,8 +44,91 @@
|
|
|
44
44
|
</div>
|
|
45
45
|
</template>
|
|
46
46
|
|
|
47
|
+
<script lang="ts">
|
|
48
|
+
import { nextTick } from 'vue';
|
|
49
|
+
|
|
50
|
+
interface MeasurePlan {
|
|
51
|
+
hold?: () => void;
|
|
52
|
+
collapse: () => void;
|
|
53
|
+
read: () => void;
|
|
54
|
+
apply: () => void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 所有 Textarea 共用的测量批处理。
|
|
59
|
+
* 逐个实例「写 height → 读 scrollHeight → 写 height」会让每个实例都强制重排一次整页,
|
|
60
|
+
* 表格里上千个单元格就是上千次重排。这里同一轮先全部塌缩、再全部读取、最后全部落高,
|
|
61
|
+
* 整批只触发一两次布局计算。塌缩前先锁住外框高度,测量期间页面其余部分不动,也不会因容器变矮丢失滚动位置。
|
|
62
|
+
*/
|
|
63
|
+
const pendingMeasures = new Set<() => MeasurePlan | null>();
|
|
64
|
+
let measureFlushQueued = false;
|
|
65
|
+
|
|
66
|
+
const flushMeasures = () => {
|
|
67
|
+
measureFlushQueued = false;
|
|
68
|
+
const plans: MeasurePlan[] = [];
|
|
69
|
+
pendingMeasures.forEach((createPlan) => {
|
|
70
|
+
const plan = createPlan();
|
|
71
|
+
if (plan) plans.push(plan);
|
|
72
|
+
});
|
|
73
|
+
pendingMeasures.clear();
|
|
74
|
+
plans.forEach((plan) => plan.hold?.());
|
|
75
|
+
plans.forEach((plan) => plan.collapse());
|
|
76
|
+
plans.forEach((plan) => plan.read());
|
|
77
|
+
plans.forEach((plan) => plan.apply());
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const queueMeasure = (createPlan: () => MeasurePlan | null) => {
|
|
81
|
+
pendingMeasures.add(createPlan);
|
|
82
|
+
if (measureFlushQueued) return;
|
|
83
|
+
measureFlushQueued = true;
|
|
84
|
+
nextTick(flushMeasures);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const cancelMeasure = (createPlan: () => MeasurePlan | null) => {
|
|
88
|
+
pendingMeasures.delete(createPlan);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 共用一个 ResizeObserver。宽度取自回调条目本身,不调用 getBoundingClientRect,
|
|
93
|
+
* 挂载时也不预读宽度,避免每个实例各自强制布局。
|
|
94
|
+
*/
|
|
95
|
+
const widthWatchers = new WeakMap<Element, { width: number | null; onChange: () => void }>();
|
|
96
|
+
let sharedResizeObserver: ResizeObserver | undefined;
|
|
97
|
+
|
|
98
|
+
const entryWidth = (entry: ResizeObserverEntry) => {
|
|
99
|
+
const box = entry.borderBoxSize as ResizeObserverSize | readonly ResizeObserverSize[] | undefined;
|
|
100
|
+
const size = Array.isArray(box) ? box[0] : box;
|
|
101
|
+
return size && typeof (size as ResizeObserverSize).inlineSize === 'number'
|
|
102
|
+
? (size as ResizeObserverSize).inlineSize
|
|
103
|
+
: entry.contentRect.width;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const observeWidth = (node: Element, onChange: () => void) => {
|
|
107
|
+
if (typeof ResizeObserver === 'undefined') return;
|
|
108
|
+
sharedResizeObserver ||= new ResizeObserver((entries) => {
|
|
109
|
+
entries.forEach((entry) => {
|
|
110
|
+
const watcher = widthWatchers.get(entry.target);
|
|
111
|
+
if (!watcher) return;
|
|
112
|
+
const width = entryWidth(entry);
|
|
113
|
+
// 首次回调也测一次:挂载后到首次布局之间宽度可能已变(如表格列宽在单元格全部挂载后才确定)
|
|
114
|
+
if (watcher.width === null || Math.abs(width - watcher.width) > 0.5) {
|
|
115
|
+
watcher.width = width;
|
|
116
|
+
watcher.onChange();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
widthWatchers.set(node, { width: null, onChange });
|
|
121
|
+
sharedResizeObserver.observe(node);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const unobserveWidth = (node: Element) => {
|
|
125
|
+
widthWatchers.delete(node);
|
|
126
|
+
sharedResizeObserver?.unobserve(node);
|
|
127
|
+
};
|
|
128
|
+
</script>
|
|
129
|
+
|
|
47
130
|
<script setup lang="ts">
|
|
48
|
-
import { reactive, watch, ref,
|
|
131
|
+
import { reactive, watch, ref, onMounted, onBeforeUnmount, computed } from 'vue';
|
|
49
132
|
import { isObject, isFunction } from '@fekit/utils';
|
|
50
133
|
|
|
51
134
|
const emits = defineEmits(['update:modelValue', 'click', 'blur', 'focus', 'change', 'input', 'clear']);
|
|
@@ -71,6 +154,7 @@
|
|
|
71
154
|
* prefix 前缀内容
|
|
72
155
|
* suffix 后缀内容
|
|
73
156
|
* interacted 用户是否交互
|
|
157
|
+
* emptyValue 清空或删空内容时回传的值,默认 null;字符串 v-model 可传 ''
|
|
74
158
|
* autoHeight 是否自动调整高度;同时传入 minRows 与 maxRows 时默认开启;单独开启时无限增高
|
|
75
159
|
* rows 初始行数(未开启自动高度,或开启但未同时设 min/max 时)
|
|
76
160
|
* minRows 与 maxRows 同时传入时生效的最小行数
|
|
@@ -97,6 +181,7 @@
|
|
|
97
181
|
prefix?: Array<any>;
|
|
98
182
|
suffix?: Array<any>;
|
|
99
183
|
interacted?: boolean | number | null;
|
|
184
|
+
emptyValue?: null | string;
|
|
100
185
|
autoHeight?: boolean | null;
|
|
101
186
|
rows?: number;
|
|
102
187
|
minRows?: number;
|
|
@@ -121,6 +206,7 @@
|
|
|
121
206
|
prefix: () => [],
|
|
122
207
|
suffix: () => [],
|
|
123
208
|
interacted: null,
|
|
209
|
+
emptyValue: null,
|
|
124
210
|
// Boolean 未传时 Vue 会落到 false,无法与「显式关闭」区分
|
|
125
211
|
autoHeight: null,
|
|
126
212
|
// textarea 特有属性默认值
|
|
@@ -152,58 +238,93 @@
|
|
|
152
238
|
// 有 min/max 时从最小行起算;仅 autoHeight 时保留 rows,并无限增高
|
|
153
239
|
const displayRows = computed(() => (isAutoHeight.value && hasRowRange.value ? resolvedMinRows.value : props.rows));
|
|
154
240
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
let observedWidth = 0;
|
|
158
|
-
|
|
159
|
-
const fMeasureHeight = () => {
|
|
160
|
-
measurementQueued = false;
|
|
241
|
+
// 测量拆成「塌缩 → 读取 → 落高」三步,交给模块级批处理统一调度(见文件末尾 queueMeasure)
|
|
242
|
+
const fMeasurePlan = (): MeasurePlan | null => {
|
|
161
243
|
const node = el.value;
|
|
162
|
-
if (!node) return;
|
|
244
|
+
if (!node) return null;
|
|
163
245
|
|
|
164
246
|
if (!isAutoHeight.value) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
247
|
+
return {
|
|
248
|
+
collapse() {
|
|
249
|
+
node.style.height = '';
|
|
250
|
+
node.style.overflowY = '';
|
|
251
|
+
},
|
|
252
|
+
read() {},
|
|
253
|
+
apply() {
|
|
254
|
+
isOverflowing.value = false;
|
|
255
|
+
},
|
|
256
|
+
};
|
|
169
257
|
}
|
|
170
258
|
|
|
259
|
+
// 塌缩期间把外框锁在当前高度,落高后还原
|
|
260
|
+
const frame = node.parentElement;
|
|
261
|
+
const frameMinHeight = frame?.style.minHeight || '';
|
|
262
|
+
let frameHeight = 0;
|
|
263
|
+
const hold = () => {
|
|
264
|
+
frameHeight = frame?.offsetHeight || 0;
|
|
265
|
+
};
|
|
266
|
+
const lockFrame = () => {
|
|
267
|
+
if (frame && frameHeight) frame.style.minHeight = `${frameHeight}px`;
|
|
268
|
+
};
|
|
269
|
+
const releaseFrame = () => {
|
|
270
|
+
if (frame && frameHeight) frame.style.minHeight = frameMinHeight;
|
|
271
|
+
};
|
|
272
|
+
|
|
171
273
|
// 未同时设 min/max:跟内容无限增高,不封顶
|
|
172
274
|
if (!hasRowRange.value) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
275
|
+
let contentHeight = 0;
|
|
276
|
+
return {
|
|
277
|
+
hold,
|
|
278
|
+
collapse() {
|
|
279
|
+
lockFrame();
|
|
280
|
+
node.style.height = 'auto';
|
|
281
|
+
node.style.overflowY = 'hidden';
|
|
282
|
+
},
|
|
283
|
+
read() {
|
|
284
|
+
contentHeight = node.scrollHeight;
|
|
285
|
+
},
|
|
286
|
+
apply() {
|
|
287
|
+
node.style.height = `${contentHeight + 1}px`;
|
|
288
|
+
releaseFrame();
|
|
289
|
+
isOverflowing.value = false;
|
|
290
|
+
},
|
|
291
|
+
};
|
|
178
292
|
}
|
|
179
293
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
294
|
+
let targetHeight = 0;
|
|
295
|
+
let overflowing = false;
|
|
296
|
+
return {
|
|
297
|
+
hold,
|
|
298
|
+
collapse() {
|
|
299
|
+
lockFrame();
|
|
300
|
+
node.style.height = '0px';
|
|
301
|
+
node.style.overflowY = 'hidden';
|
|
302
|
+
},
|
|
303
|
+
read() {
|
|
304
|
+
const style = getComputedStyle(node);
|
|
305
|
+
let lineHeight = parseFloat(style.lineHeight);
|
|
306
|
+
if (!Number.isFinite(lineHeight) || lineHeight <= 0) {
|
|
307
|
+
lineHeight = (parseFloat(style.fontSize) || 14) * 1.5;
|
|
308
|
+
}
|
|
188
309
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
310
|
+
const paddingBlock = (parseFloat(style.paddingTop) || 0) + (parseFloat(style.paddingBottom) || 0);
|
|
311
|
+
const minHeight = resolvedMinRows.value * lineHeight + paddingBlock;
|
|
312
|
+
const maxHeight = resolvedMaxRows.value * lineHeight + paddingBlock;
|
|
313
|
+
const contentHeight = node.scrollHeight;
|
|
314
|
+
overflowing = contentHeight > maxHeight + 1;
|
|
315
|
+
const measuredHeight = contentHeight <= minHeight + 1 ? minHeight : contentHeight;
|
|
316
|
+
targetHeight = Math.min(Math.max(measuredHeight, minHeight), maxHeight);
|
|
317
|
+
},
|
|
318
|
+
apply() {
|
|
319
|
+
node.style.height = `${Math.ceil(targetHeight)}px`;
|
|
320
|
+
node.style.overflowY = overflowing ? 'auto' : 'hidden';
|
|
321
|
+
releaseFrame();
|
|
322
|
+
isOverflowing.value = overflowing;
|
|
323
|
+
},
|
|
324
|
+
};
|
|
200
325
|
};
|
|
201
326
|
|
|
202
|
-
const fAutoHeight = () =>
|
|
203
|
-
if (measurementQueued) return;
|
|
204
|
-
measurementQueued = true;
|
|
205
|
-
nextTick(fMeasureHeight);
|
|
206
|
-
};
|
|
327
|
+
const fAutoHeight = () => queueMeasure(fMeasurePlan);
|
|
207
328
|
|
|
208
329
|
watch(
|
|
209
330
|
[() => isAutoHeight.value, () => resolvedMinRows.value, () => resolvedMaxRows.value, () => props.modelValue, () => props.rows, () => props.sz, () => props.theme],
|
|
@@ -282,7 +403,7 @@
|
|
|
282
403
|
|
|
283
404
|
// 清空
|
|
284
405
|
function clear() {
|
|
285
|
-
emits('update:modelValue',
|
|
406
|
+
emits('update:modelValue', props.emptyValue);
|
|
286
407
|
emits('clear');
|
|
287
408
|
}
|
|
288
409
|
// 输入
|
|
@@ -302,7 +423,7 @@
|
|
|
302
423
|
}
|
|
303
424
|
// 输入事件
|
|
304
425
|
emits('input', e, state);
|
|
305
|
-
emits('update:modelValue', value ||
|
|
426
|
+
emits('update:modelValue', value || props.emptyValue);
|
|
306
427
|
|
|
307
428
|
// 自动高度
|
|
308
429
|
fAutoHeight();
|
|
@@ -333,25 +454,16 @@
|
|
|
333
454
|
return { ..._event };
|
|
334
455
|
};
|
|
335
456
|
|
|
336
|
-
//
|
|
457
|
+
// 组件挂载后初始化自动高度;宽度变化(换行数随之变化)时重新测量
|
|
337
458
|
onMounted(() => {
|
|
338
459
|
const node = el.value;
|
|
339
|
-
if (node
|
|
340
|
-
observedWidth = node.getBoundingClientRect().width;
|
|
341
|
-
resizeObserver = new ResizeObserver(() => {
|
|
342
|
-
const width = node.getBoundingClientRect().width;
|
|
343
|
-
if (Math.abs(width - observedWidth) > 0.5) {
|
|
344
|
-
observedWidth = width;
|
|
345
|
-
fAutoHeight();
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
resizeObserver.observe(node);
|
|
349
|
-
}
|
|
460
|
+
if (node) observeWidth(node, fAutoHeight);
|
|
350
461
|
fAutoHeight();
|
|
351
462
|
});
|
|
352
463
|
|
|
353
464
|
onBeforeUnmount(() => {
|
|
354
|
-
|
|
355
|
-
|
|
465
|
+
if (el.value) unobserveWidth(el.value);
|
|
466
|
+
cancelMeasure(fMeasurePlan);
|
|
356
467
|
});
|
|
357
468
|
</script>
|
|
469
|
+
|
|
@@ -1,11 +1,47 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
2
|
+
<div :ui-form="`${theme} type:input ${disabled ? 'disabled' : ''}`">
|
|
3
|
+
<input type="time" :value="modelValue || ''" :min="min" :max="max" :step="step" :disabled="disabled" :readonly="readonly"
|
|
4
|
+
:aria-invalid="error ? true : undefined" :aria-label="label" :required="required" @input="update" @change="change" @blur="emit('blur', $event)" @focus="emit('focus', $event)" />
|
|
5
|
+
<span v-if="verify && error" role="alert" class="co-risk">{{ error }}</span>
|
|
6
|
+
</div>
|
|
3
7
|
</template>
|
|
4
|
-
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
export interface TimeConstraints { min?: string; max?: string; step?: number; required?: boolean }
|
|
10
|
+
const milliseconds = (value: unknown): number | null => {
|
|
11
|
+
const match = typeof value === 'string' && /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?$/.exec(value);
|
|
12
|
+
if (!match || +match[1] > 23 || +match[2] > 59 || +(match[3] || 0) > 59) return null;
|
|
13
|
+
return ((+match[1] * 60 + +match[2]) * 60 + +(match[3] || 0)) * 1000 + +(match[4] || '').padEnd(3, '0');
|
|
14
|
+
};
|
|
15
|
+
// 独立控件提示和 FormLoader 提交校验共用,不依赖浏览器语言或 DOM。
|
|
16
|
+
export const timeError = (value: unknown, options: TimeConstraints = {}): string | null => {
|
|
17
|
+
if (value === '' || value == null) return options.required ? '请选择时间' : null;
|
|
18
|
+
const time = milliseconds(value);
|
|
19
|
+
if (time === null) return '请输入有效时间';
|
|
20
|
+
const min = milliseconds(options.min), max = milliseconds(options.max);
|
|
21
|
+
const outside = min !== null && max !== null && min > max
|
|
22
|
+
? time < min && time > max : (min !== null && time < min) || (max !== null && time > max);
|
|
23
|
+
if (outside) return `时间须在 ${options.min || '00:00'} 至 ${options.max || '23:59:59'} 之间`;
|
|
24
|
+
const step = Number.isFinite(options.step) && options.step! > 0 ? options.step! : 60;
|
|
25
|
+
if ((time - (min ?? 0)) % Math.round(step * 1000)) return `时间须按 ${step} 秒间隔选择`;
|
|
26
|
+
return null;
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
5
29
|
<script setup lang="ts">
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
30
|
+
import { computed } from 'vue';
|
|
31
|
+
const props = withDefaults(defineProps<{
|
|
32
|
+
modelValue?: string | null; min?: string; max?: string; step?: number;
|
|
33
|
+
disabled?: boolean; readonly?: boolean; required?: boolean; label?: string; theme?: string; verify?: boolean;
|
|
34
|
+
}>(), { modelValue: '', step: 60, disabled: false, readonly: false, theme: '@a', verify: true });
|
|
35
|
+
const emit = defineEmits(['update:modelValue', 'change', 'invalid', 'blur', 'focus']);
|
|
36
|
+
const error = computed(() => timeError(props.modelValue, props));
|
|
37
|
+
const update = (event: Event) => {
|
|
38
|
+
if (props.disabled || props.readonly) return;
|
|
39
|
+
const value = (event.target as HTMLInputElement).value;
|
|
40
|
+
emit('update:modelValue', value);
|
|
41
|
+
const message = timeError(value, props);
|
|
42
|
+
if (message) emit('invalid', message);
|
|
43
|
+
};
|
|
44
|
+
const change = (event: Event) => {
|
|
45
|
+
if (!props.disabled && !props.readonly) emit('change', (event.target as HTMLInputElement).value);
|
|
46
|
+
};
|
|
11
47
|
</script>
|
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
uploadStyle?: string;
|
|
204
204
|
tips?: string;
|
|
205
205
|
accept?: string | 'image/*' | 'video/*' | 'audio/*';
|
|
206
|
-
upload?: (formData: FormData) => Promise<any>;
|
|
206
|
+
upload?: (formData: FormData, fileExtension?: string) => Promise<any>;
|
|
207
207
|
maxSize?: number;
|
|
208
208
|
compress?: boolean;
|
|
209
209
|
disabled?: boolean;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import Radio from './Radio.vue';
|
|
2
|
+
import Gallery from './Gallery.vue';
|
|
1
3
|
import Input from './Input.vue';
|
|
2
4
|
import InputRange from './InputRange.vue';
|
|
3
5
|
import Switch from './Switch.vue';
|
|
@@ -30,6 +32,9 @@ import Upload from './Upload.vue';
|
|
|
30
32
|
import Cascader from './Cascader.vue';
|
|
31
33
|
|
|
32
34
|
export {
|
|
35
|
+
Radio,
|
|
36
|
+
Gallery,
|
|
37
|
+
Gallery as SelectImg,
|
|
33
38
|
Input,
|
|
34
39
|
InputRange,
|
|
35
40
|
Switch,
|