@veltra/desktop 1.7.18 → 1.8.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/dist/components/batch-edit/batch-edit-form.js +40 -26
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit-list.js +17 -5
- package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
- package/dist/components/batch-edit/di.js.map +1 -1
- package/dist/components/batch-edit/use-handlers.js +3 -3
- package/dist/components/batch-edit/use-handlers.js.map +1 -1
- package/dist/components/batch-edit/use-shortcut-key.js +4 -2
- package/dist/components/batch-edit/use-shortcut-key.js.map +1 -1
- package/dist/components/card/style2.css +0 -2
- package/dist/components/dialog/style2.css +1 -1
- package/dist/components/dropdown/dropdown.js +5 -3
- package/dist/components/dropdown/dropdown.js.map +1 -1
- package/dist/components/expression-editor/components/path-preview.js +3 -6
- package/dist/components/expression-editor/components/path-preview.js.map +1 -1
- package/dist/components/expression-editor/components/variable-picker.js +49 -12
- package/dist/components/expression-editor/components/variable-picker.js.map +1 -1
- package/dist/components/expression-editor/core/chip.js +1 -15
- package/dist/components/expression-editor/core/chip.js.map +1 -1
- package/dist/components/expression-editor/core/editor.js +20 -18
- package/dist/components/expression-editor/core/editor.js.map +1 -1
- package/dist/components/expression-editor/expression-editor.js +53 -22
- package/dist/components/expression-editor/expression-editor.js.map +1 -1
- package/dist/components/expression-editor/style2.css +135 -96
- package/dist/components/form/form.js.map +1 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js +0 -1
- package/dist/components/form/form.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/form/style2.css +8 -3
- package/dist/components/index.d.ts +26 -26
- package/dist/components/index.js +52 -52
- package/dist/components/multi-select/multi-select.js +16 -12
- package/dist/components/multi-select/multi-select.js.map +1 -1
- package/dist/components/multi-select/style2.css +7 -0
- package/dist/components/multi-tree-select/multi-tree-select.js +82 -56
- package/dist/components/multi-tree-select/multi-tree-select.js.map +1 -1
- package/dist/components/multi-tree-select/style2.css +7 -0
- package/dist/components/select/select.js +7 -3
- package/dist/components/select/select.js.map +1 -1
- package/dist/components/select/style2.css +7 -0
- package/dist/components/select/use-options.js +14 -3
- package/dist/components/select/use-options.js.map +1 -1
- package/dist/components/tree-select/style2.css +7 -0
- package/dist/components/tree-select/tree-select.js +49 -17
- package/dist/components/tree-select/tree-select.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/types/dropdown.d.ts +10 -0
- package/dist/types/multi-tree-select.d.ts +7 -1
- package/dist/types/tree-select.d.ts +7 -1
- package/package.json +1 -1
- package/src/components/batch-edit/__test__/batch-edit.test.ts +119 -7
- package/src/components/batch-edit/batch-edit-form.vue +21 -2
- package/src/components/batch-edit/batch-edit-list.vue +18 -1
- package/src/components/batch-edit/di.ts +1 -1
- package/src/components/batch-edit/use-handlers.ts +6 -5
- package/src/components/batch-edit/use-shortcut-key.ts +5 -2
- package/src/components/card/style.scss +0 -2
- package/src/components/dialog/style.scss +2 -1
- package/src/components/dropdown/dropdown.vue +9 -4
- package/src/components/expression-editor/__test__/chip.test.ts +3 -14
- package/src/components/expression-editor/components/path-preview.vue +1 -2
- package/src/components/expression-editor/components/variable-picker.vue +54 -13
- package/src/components/expression-editor/core/chip.ts +3 -29
- package/src/components/expression-editor/core/editor.ts +28 -21
- package/src/components/expression-editor/expression-editor.vue +47 -21
- package/src/components/expression-editor/style.scss +167 -111
- package/src/components/form/form.vue +0 -1
- package/src/components/form/style.scss +11 -4
- package/src/components/multi-select/multi-select.vue +11 -2
- package/src/components/multi-select/style.scss +6 -0
- package/src/components/multi-tree-select/multi-tree-select.vue +58 -9
- package/src/components/multi-tree-select/style.scss +6 -0
- package/src/components/select/__test__/select.test.ts +78 -0
- package/src/components/select/select.vue +13 -3
- package/src/components/select/style.scss +6 -0
- package/src/components/select/use-options.ts +25 -4
- package/src/components/tree-select/__test__/tree-select.test.ts +116 -0
- package/src/components/tree-select/style.scss +6 -0
- package/src/components/tree-select/tree-select.vue +63 -13
- package/src/types/dropdown.ts +10 -0
- package/src/types/multi-tree-select.ts +12 -1
- package/src/types/tree-select.ts +12 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import _sfc_main$1 from "../icon/icon.js";
|
|
2
2
|
import _sfc_main$2 from "../dropdown/dropdown.js";
|
|
3
3
|
import input_default from "../input/input.js";
|
|
4
|
-
import _sfc_main$3 from "../
|
|
4
|
+
import _sfc_main$3 from "../loading/loading.js";
|
|
5
|
+
import _sfc_main$4 from "../tree/tree.js";
|
|
5
6
|
import { Fragment, Transition, computed, createBlock, createCommentVNode, createElementBlock, createSlots, createTextVNode, createVNode, defineComponent, mergeModels, mergeProps, nextTick, normalizeClass, openBlock, renderSlot, shallowRef, toDisplayString, unref, useModel, useSlots, useTemplateRef, watch, withCtx, withModifiers } from "vue";
|
|
6
7
|
import { useFormFallbackProps, useUserAction } from "@veltra/compositions";
|
|
7
8
|
import { FORM_EMPTY_CONTENT, bem, fieldKey, injectFormContext } from "@veltra/utils";
|
|
8
9
|
import { ArrowDown, Close } from "@veltra/icons/normal";
|
|
9
|
-
import { dfs, o } from "@cat-kit/core";
|
|
10
|
+
import { debounce, dfs, o } from "@cat-kit/core";
|
|
10
11
|
//#region src/components/tree-select/tree-select.vue
|
|
11
12
|
const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
12
13
|
name: "UTreeSelect",
|
|
@@ -14,6 +15,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
14
15
|
__name: "tree-select",
|
|
15
16
|
props: /*@__PURE__*/ mergeModels({
|
|
16
17
|
modelValue: {},
|
|
18
|
+
data: {},
|
|
17
19
|
placeholder: { default: "请选择" },
|
|
18
20
|
clearable: {
|
|
19
21
|
type: Boolean,
|
|
@@ -49,7 +51,6 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
49
51
|
labelKey: { default: "label" },
|
|
50
52
|
valueKey: { default: "value" },
|
|
51
53
|
childrenKey: {},
|
|
52
|
-
data: {},
|
|
53
54
|
disabledNode: {},
|
|
54
55
|
checkOnClickNode: { type: Boolean },
|
|
55
56
|
checkStrictly: { type: Boolean },
|
|
@@ -77,6 +78,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
77
78
|
"disabled",
|
|
78
79
|
"label",
|
|
79
80
|
"readonly",
|
|
81
|
+
"data",
|
|
80
82
|
"contentClass",
|
|
81
83
|
"contentStyle",
|
|
82
84
|
"minWidth",
|
|
@@ -86,11 +88,37 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
86
88
|
const cls = bem("tree-select");
|
|
87
89
|
const labelKey = computed(() => fieldKey(props.labelKey, "label"));
|
|
88
90
|
const valueKey = computed(() => fieldKey(props.valueKey, "value"));
|
|
89
|
-
|
|
91
|
+
/** 是否可搜索:显式声明或数据源为远程函数 */
|
|
92
|
+
const filterable = computed(() => props.filterable || typeof props.data === "function");
|
|
93
|
+
/** 过滤 */
|
|
90
94
|
const qs = shallowRef("");
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
const treeRef = shallowRef();
|
|
96
|
+
/** 远程搜索返回的树数据 */
|
|
97
|
+
const remoteData = shallowRef();
|
|
98
|
+
/** 面板渲染的树数据:data 为函数时来自远程搜索,否则为静态数据 */
|
|
99
|
+
const treeData = computed(() => typeof props.data === "function" ? remoteData.value : props.data);
|
|
100
|
+
const remoteLoading = shallowRef(false);
|
|
101
|
+
/** 远程请求序号;响应返回时序号不一致说明已有更新的请求,结果作废 */
|
|
102
|
+
let remoteSeq = 0;
|
|
103
|
+
const debouncedRemoteSearch = debounce(async (query, dataFn) => {
|
|
104
|
+
const seq = ++remoteSeq;
|
|
105
|
+
remoteLoading.value = true;
|
|
106
|
+
try {
|
|
107
|
+
const data = await dataFn(query);
|
|
108
|
+
if (seq !== remoteSeq || query !== qs.value) return;
|
|
109
|
+
remoteData.value = data ?? [];
|
|
110
|
+
if (query) nextTick(() => treeRef.value?.expandAll());
|
|
111
|
+
} finally {
|
|
112
|
+
if (seq === remoteSeq) remoteLoading.value = false;
|
|
113
|
+
}
|
|
114
|
+
}, 200);
|
|
115
|
+
watch([qs, () => props.data], ([query, data]) => {
|
|
116
|
+
if (typeof data === "function") {
|
|
117
|
+
debouncedRemoteSearch(query, data);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
treeRef.value?.filter(query);
|
|
121
|
+
}, { immediate: true });
|
|
94
122
|
const model = useModel(__props, "modelValue");
|
|
95
123
|
/** 内部展示文案,仅由 data 推导;通过 update:text 单向通知父级 */
|
|
96
124
|
const label = shallowRef();
|
|
@@ -110,17 +138,17 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
110
138
|
}
|
|
111
139
|
/** 触发输入框的值:查询态下承载查询串,否则展示选中标签 */
|
|
112
140
|
const inputValue = computed(() => {
|
|
113
|
-
if (
|
|
141
|
+
if (filterable.value && querying.value) return qs.value;
|
|
114
142
|
return model.value || model.value === 0 ? label.value : void 0;
|
|
115
143
|
});
|
|
116
144
|
/** 查询态下已选标签降级为占位提示 */
|
|
117
145
|
const inputPlaceholder = computed(() => {
|
|
118
146
|
const display = model.value || model.value === 0 ? label.value : void 0;
|
|
119
|
-
if (
|
|
147
|
+
if (filterable.value && querying.value && display) return display;
|
|
120
148
|
return props.placeholder;
|
|
121
149
|
});
|
|
122
150
|
function handleQueryInput(value) {
|
|
123
|
-
if (!
|
|
151
|
+
if (!filterable.value) return;
|
|
124
152
|
qs.value = value;
|
|
125
153
|
}
|
|
126
154
|
const hovered = shallowRef(false);
|
|
@@ -135,20 +163,19 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
135
163
|
* 非输入区域(箭头、留白)放行,维持原开合行为。
|
|
136
164
|
*/
|
|
137
165
|
function handleTriggerClickCapture(e) {
|
|
138
|
-
if (!
|
|
166
|
+
if (!filterable.value || disabled.value) return;
|
|
139
167
|
if (!(e.target instanceof HTMLInputElement)) return;
|
|
140
168
|
e.stopPropagation();
|
|
141
169
|
if (!dropdownVisible.value) dropdownRef.value?.open();
|
|
142
170
|
}
|
|
143
171
|
watch(dropdownVisible, (visible) => {
|
|
144
|
-
if (visible &&
|
|
172
|
+
if (visible && filterable.value) {
|
|
145
173
|
querying.value = true;
|
|
146
174
|
nextTick(() => inputRef.value?.el?.focus());
|
|
147
175
|
} else if (!visible) querying.value = false;
|
|
148
176
|
});
|
|
149
177
|
const { formProps } = injectFormContext();
|
|
150
178
|
const { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props]);
|
|
151
|
-
const treeRef = shallowRef();
|
|
152
179
|
const dropdownRef = shallowRef();
|
|
153
180
|
const { userAction, isUserActive } = useUserAction();
|
|
154
181
|
/** 清空 */
|
|
@@ -159,7 +186,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
159
186
|
emit("change", void 0);
|
|
160
187
|
});
|
|
161
188
|
/** 外部回显:按 data 查找 label(O(n));用户动作期内跳过 */
|
|
162
|
-
watch([
|
|
189
|
+
watch([treeData, model], ([data, modelVal]) => {
|
|
163
190
|
if (isUserActive()) return;
|
|
164
191
|
if (!data?.length || modelVal === void 0 || modelVal === null || modelVal === "") {
|
|
165
192
|
setLabel(void 0);
|
|
@@ -222,7 +249,7 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
222
249
|
clearable: false,
|
|
223
250
|
"model-value": inputValue.value,
|
|
224
251
|
"onUpdate:modelValue": handleQueryInput,
|
|
225
|
-
"native-readonly": !
|
|
252
|
+
"native-readonly": !filterable.value || !querying.value,
|
|
226
253
|
onClickCapture: handleTriggerClickCapture
|
|
227
254
|
}, createSlots({
|
|
228
255
|
suffix: withCtx(() => [createVNode(Transition, {
|
|
@@ -258,7 +285,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
258
285
|
"model-value",
|
|
259
286
|
"native-readonly"
|
|
260
287
|
])]),
|
|
261
|
-
content: withCtx(() => [createCommentVNode(" 菜单列表 "),
|
|
288
|
+
content: withCtx(() => [createCommentVNode(" 菜单列表 "), remoteLoading.value ? (openBlock(), createElementBlock("div", {
|
|
289
|
+
key: 0,
|
|
290
|
+
class: normalizeClass(unref(cls).e("loading"))
|
|
291
|
+
}, [createVNode(unref(_sfc_main$3), { type: "dual-ring" })], 2)) : (openBlock(), createBlock(unref(_sfc_main$4), mergeProps({ key: 1 }, treeProps.value, {
|
|
292
|
+
data: treeData.value,
|
|
262
293
|
selected: model.value,
|
|
263
294
|
"onUpdate:selected": unref(handleSelect),
|
|
264
295
|
ref_key: "treeRef",
|
|
@@ -268,10 +299,11 @@ const _sfc_main = /*@__PURE__*/ defineComponent({
|
|
|
268
299
|
slots,
|
|
269
300
|
"scroll-to-view": ""
|
|
270
301
|
}), null, 16, [
|
|
302
|
+
"data",
|
|
271
303
|
"selected",
|
|
272
304
|
"onUpdate:selected",
|
|
273
305
|
"class"
|
|
274
|
-
])]),
|
|
306
|
+
]))]),
|
|
275
307
|
_: 3
|
|
276
308
|
}, 16, [
|
|
277
309
|
"class",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree-select.js","names":["$attrs","$slots"],"sources":["../../../src/components/tree-select/tree-select.vue"],"sourcesContent":["<template>\n <u-dropdown\n v-if=\"!readonly\"\n v-bind=\"$attrs\"\n :class=\"[cls.b, bem.is('disabled', disabled)]\"\n trigger=\"click\"\n :content-class=\"[cls.e('panel'), cls.em('panel', size), contentClass]\"\n :content-style=\"contentStyle\"\n ref=\"dropdownRef\"\n v-model:visible=\"dropdownVisible\"\n :min-width=\"minWidth\"\n :width=\"width\"\n :disabled=\"disabled\"\n @update:visible=\"handleDropdownVisible\"\n @mouseenter=\"hovered = true\"\n @mouseleave=\"hovered = false\"\n >\n <template #trigger>\n <u-input\n ref=\"inputRef\"\n :size=\"size\"\n :disabled=\"disabled\"\n :placeholder=\"inputPlaceholder\"\n :clearable=\"false\"\n :model-value=\"inputValue\"\n @update:model-value=\"handleQueryInput\"\n :native-readonly=\"!filterable || !querying\"\n @click.capture=\"handleTriggerClickCapture\"\n >\n <template #prefix v-if=\"$slots.prefix\">\n <slot name=\"prefix\" />\n </template>\n <template #suffix>\n <transition name=\"zoom-in\" mode=\"out-in\">\n <u-icon\n v-if=\"showClear\"\n :class=\"cls.e('clear')\"\n title=\"清除\"\n key=\"clear\"\n @click.stop=\"handleClear\"\n >\n <Close />\n </u-icon>\n <u-icon v-else :class=\"cls.e('arrow')\" key=\"arrow\"><ArrowDown /></u-icon>\n </transition>\n </template>\n </u-input>\n </template>\n <template #content>\n <!-- 菜单列表 -->\n\n <u-tree\n v-bind=\"treeProps\"\n :selected=\"model\"\n @update:selected=\"handleSelect\"\n ref=\"treeRef\"\n selectable\n :class=\"cls.e('content-tree')\"\n :slots=\"slots\"\n scroll-to-view\n />\n </template>\n </u-dropdown>\n\n <template v-else>\n {{ label || FORM_EMPTY_CONTENT }}\n </template>\n</template>\n\n<script lang=\"ts\" setup>\nimport { dfs, o } from '@cat-kit/core'\nimport { useFormFallbackProps, useUserAction } from '@veltra/compositions'\nimport { ArrowDown, Close } from '@veltra/icons/normal'\nimport { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport type { TreeSelectProps, TreeSelectEmits, TreeExposed, InputExposed } from '../../types'\nimport { UDropdown } from '../dropdown'\nimport { UIcon } from '../icon'\nimport { UInput } from '../input'\nimport { UTree } from '../tree'\nimport type { TreeSlotsScope } from '../tree/di'\n\ndefineOptions({ name: 'UTreeSelect', inheritAttrs: false })\n\nconst props = withDefaults(defineProps<TreeSelectProps>(), {\n labelKey: 'label',\n valueKey: 'value',\n placeholder: '请选择',\n expandAll: false,\n clearable: true,\n disabled: undefined,\n readonly: undefined,\n filterable: false,\n minWidth: '280px'\n})\n\nconst emit = defineEmits<TreeSelectEmits>()\n\nconst slots = defineSlots<{\n /** 默认插槽 */\n default?: (props: TreeSlotsScope) => any\n /** 前缀插槽 */\n prefix?: () => any\n}>()\n\nconst treeProps = computed(() => {\n return o(props as Record<string, any>).omit([\n 'tips',\n 'field',\n 'placeholder',\n 'disabled',\n 'label',\n 'readonly',\n 'contentClass',\n 'contentStyle',\n 'minWidth',\n 'width'\n ])\n})\n\nconst cls = bem('tree-select')\n\nconst labelKey = computed(() => fieldKey(props.labelKey, 'label'))\nconst valueKey = computed(() => fieldKey(props.valueKey, 'value'))\n\n/**过滤 */\nconst qs = shallowRef('')\nwatch(qs, (qs) => {\n treeRef.value?.filter(qs)\n})\n\nconst model = defineModel<string | number>()\n\n/** 内部展示文案,仅由 data 推导;通过 update:text 单向通知父级 */\nconst label = shallowRef<string>()\n\nconst dropdownVisible = shallowRef(false)\n\n/**\n * 是否处于查询态:决定触发输入框显示查询串还是选中标签。\n * 与面板可见状态解耦 —— 选择后立即退出查询态,\n * 不等面板关闭动画结束(否则显示值会延迟一个动画时长才恢复)。\n */\nconst querying = shallowRef(false)\n\nconst inputRef = useTemplateRef<InputExposed>('inputRef')\n\n/** 更新内部文案;值变化时单向 emit update:text */\nfunction setLabel(next?: string) {\n if (label.value === next) return\n label.value = next\n emit('update:text', next)\n}\n\n/** 触发输入框的值:查询态下承载查询串,否则展示选中标签 */\nconst inputValue = computed(() => {\n if (props.filterable && querying.value) return qs.value\n return model.value || model.value === 0 ? label.value : undefined\n})\n\n/** 查询态下已选标签降级为占位提示 */\nconst inputPlaceholder = computed(() => {\n const display = model.value || model.value === 0 ? label.value : undefined\n if (props.filterable && querying.value && display) {\n return display\n }\n return props.placeholder\n})\n\nfunction handleQueryInput(value: string) {\n if (!props.filterable) return\n qs.value = value\n}\n\nconst hovered = shallowRef(false)\n\n/** 悬停且存在选中值时展示清除按钮(替代下拉箭头) */\nconst showClear = computed(() => {\n if (!props.clearable || disabled.value || !hovered.value) return false\n return !!(model.value || model.value === 0 ? label.value : undefined)\n})\n\n/**\n * 过滤模式下拦截输入区域的点击:\n * 阻止 dropdown 的 trigger 开合切换,保持面板展开以不中断输入。\n * 非输入区域(箭头、留白)放行,维持原开合行为。\n */\nfunction handleTriggerClickCapture(e: MouseEvent) {\n if (!props.filterable || disabled.value) return\n if (!(e.target instanceof HTMLInputElement)) return\n\n e.stopPropagation()\n if (!dropdownVisible.value) dropdownRef.value?.open()\n}\n\nwatch(dropdownVisible, (visible) => {\n if (visible && props.filterable) {\n // 面板展开后进入查询态并聚焦输入框,可以立即输入查询\n querying.value = true\n nextTick(() => inputRef.value?.el?.focus())\n } else if (!visible) {\n querying.value = false\n }\n})\n\nconst { formProps } = injectFormContext()\n\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])\n\nconst treeRef = shallowRef<TreeExposed>()\n\nconst dropdownRef = shallowRef<InstanceType<typeof UDropdown>>()\n\nconst { userAction, isUserActive } = useUserAction()\n\n/** 清空 */\nconst handleClear = userAction(() => {\n model.value = ''\n setLabel(undefined)\n emit('clear')\n emit('change', undefined)\n})\n\n/** 外部回显:按 data 查找 label(O(n));用户动作期内跳过 */\nwatch(\n [() => props.data, model],\n ([data, modelVal]) => {\n if (isUserActive()) return\n if (!data?.length || modelVal === undefined || modelVal === null || modelVal === '') {\n setLabel(undefined)\n return\n }\n\n let nextLabel: string | undefined\n const childrenKey = props.childrenKey ?? 'children'\n data.some((item) => {\n let founded = false\n dfs(\n item,\n (v) => {\n if (v[valueKey.value] === modelVal) {\n nextLabel = v[labelKey.value]\n founded = true\n return true\n }\n },\n childrenKey\n )\n\n return founded\n })\n setLabel(nextLabel)\n },\n { immediate: true }\n)\n\n/** 用户选择:O(1) 写入值与文案,跳过 data 回显查找 */\nconst handleSelect = userAction(\n (selected?: string | number, selectedData?: Record<string, any>) => {\n model.value = selected ?? ''\n\n if (selectedData) {\n setLabel(o(selectedData).get(labelKey.value))\n } else {\n setLabel(undefined)\n }\n\n emit('change', selectedData)\n // 立即退出查询态,输入框同步恢复显示选中标签\n querying.value = false\n qs.value = ''\n dropdownRef.value?.close()\n }\n)\nconst handleDropdownVisible = (visible: boolean) => {\n if (!visible) {\n qs.value = ''\n }\n}\n</script>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tree-select.js","names":["$attrs","$slots"],"sources":["../../../src/components/tree-select/tree-select.vue"],"sourcesContent":["<template>\n <u-dropdown\n v-if=\"!readonly\"\n v-bind=\"$attrs\"\n :class=\"[cls.b, bem.is('disabled', disabled)]\"\n trigger=\"click\"\n :content-class=\"[cls.e('panel'), cls.em('panel', size), contentClass]\"\n :content-style=\"contentStyle\"\n ref=\"dropdownRef\"\n v-model:visible=\"dropdownVisible\"\n :min-width=\"minWidth\"\n :width=\"width\"\n :disabled=\"disabled\"\n @update:visible=\"handleDropdownVisible\"\n @mouseenter=\"hovered = true\"\n @mouseleave=\"hovered = false\"\n >\n <template #trigger>\n <u-input\n ref=\"inputRef\"\n :size=\"size\"\n :disabled=\"disabled\"\n :placeholder=\"inputPlaceholder\"\n :clearable=\"false\"\n :model-value=\"inputValue\"\n @update:model-value=\"handleQueryInput\"\n :native-readonly=\"!filterable || !querying\"\n @click.capture=\"handleTriggerClickCapture\"\n >\n <template #prefix v-if=\"$slots.prefix\">\n <slot name=\"prefix\" />\n </template>\n <template #suffix>\n <transition name=\"zoom-in\" mode=\"out-in\">\n <u-icon\n v-if=\"showClear\"\n :class=\"cls.e('clear')\"\n title=\"清除\"\n key=\"clear\"\n @click.stop=\"handleClear\"\n >\n <Close />\n </u-icon>\n <u-icon v-else :class=\"cls.e('arrow')\" key=\"arrow\"><ArrowDown /></u-icon>\n </transition>\n </template>\n </u-input>\n </template>\n <template #content>\n <!-- 菜单列表 -->\n\n <div v-if=\"remoteLoading\" :class=\"cls.e('loading')\">\n <ULoading type=\"dual-ring\" />\n </div>\n <u-tree\n v-else\n v-bind=\"treeProps\"\n :data=\"treeData\"\n :selected=\"model\"\n @update:selected=\"handleSelect\"\n ref=\"treeRef\"\n selectable\n :class=\"cls.e('content-tree')\"\n :slots=\"slots\"\n scroll-to-view\n />\n </template>\n </u-dropdown>\n\n <template v-else>\n {{ label || FORM_EMPTY_CONTENT }}\n </template>\n</template>\n\n<script lang=\"ts\" setup>\nimport { debounce, dfs, o } from '@cat-kit/core'\nimport { useFormFallbackProps, useUserAction } from '@veltra/compositions'\nimport { ArrowDown, Close } from '@veltra/icons/normal'\nimport { bem, fieldKey, FORM_EMPTY_CONTENT } from '@veltra/utils'\nimport { injectFormContext } from '@veltra/utils'\nimport { computed, nextTick, shallowRef, useTemplateRef, watch } from 'vue'\n\nimport type { TreeSelectProps, TreeSelectEmits, TreeExposed, InputExposed } from '../../types'\nimport { UDropdown } from '../dropdown'\nimport { UIcon } from '../icon'\nimport { UInput } from '../input'\nimport { ULoading } from '../loading'\nimport { UTree } from '../tree'\nimport type { TreeSlotsScope } from '../tree/di'\n\ndefineOptions({ name: 'UTreeSelect', inheritAttrs: false })\n\nconst props = withDefaults(defineProps<TreeSelectProps>(), {\n labelKey: 'label',\n valueKey: 'value',\n placeholder: '请选择',\n expandAll: false,\n clearable: true,\n disabled: undefined,\n readonly: undefined,\n filterable: false,\n minWidth: '280px'\n})\n\nconst emit = defineEmits<TreeSelectEmits>()\n\nconst slots = defineSlots<{\n /** 默认插槽 */\n default?: (props: TreeSlotsScope) => any\n /** 前缀插槽 */\n prefix?: () => any\n}>()\n\nconst treeProps = computed(() => {\n return o(props as Record<string, any>).omit([\n 'tips',\n 'field',\n 'placeholder',\n 'disabled',\n 'label',\n 'readonly',\n 'data',\n 'contentClass',\n 'contentStyle',\n 'minWidth',\n 'width'\n ])\n})\n\nconst cls = bem('tree-select')\n\nconst labelKey = computed(() => fieldKey(props.labelKey, 'label'))\nconst valueKey = computed(() => fieldKey(props.valueKey, 'value'))\n\n/** 是否可搜索:显式声明或数据源为远程函数 */\nconst filterable = computed(() => props.filterable || typeof props.data === 'function')\n\n/** 过滤 */\nconst qs = shallowRef('')\n\nconst treeRef = shallowRef<TreeExposed>()\n\n/** 远程搜索返回的树数据 */\nconst remoteData = shallowRef<Record<string, any>[]>()\n\n/** 面板渲染的树数据:data 为函数时来自远程搜索,否则为静态数据 */\nconst treeData = computed(() => (typeof props.data === 'function' ? remoteData.value : props.data))\n\nconst remoteLoading = shallowRef(false)\n/** 远程请求序号;响应返回时序号不一致说明已有更新的请求,结果作废 */\nlet remoteSeq = 0\n\nconst debouncedRemoteSearch = debounce(\n async (\n query: string,\n dataFn: (qs: string) => Promise<Record<string, any>[]> | Record<string, any>[]\n ) => {\n const seq = ++remoteSeq\n remoteLoading.value = true\n try {\n const data = await dataFn(query)\n // 响应到达时查询词可能已变化或已有更新的请求,过期结果直接丢弃\n if (seq !== remoteSeq || query !== qs.value) return\n remoteData.value = data ?? []\n // 搜索结果展示完整命中路径,与本地过滤自动展开祖先的行为对齐\n if (query) nextTick(() => treeRef.value?.expandAll())\n } finally {\n if (seq === remoteSeq) remoteLoading.value = false\n }\n },\n 200\n)\n\nwatch(\n [qs, () => props.data],\n ([query, data]) => {\n if (typeof data === 'function') {\n debouncedRemoteSearch(query, data)\n return\n }\n treeRef.value?.filter(query)\n },\n { immediate: true }\n)\n\nconst model = defineModel<string | number>()\n\n/** 内部展示文案,仅由 data 推导;通过 update:text 单向通知父级 */\nconst label = shallowRef<string>()\n\nconst dropdownVisible = shallowRef(false)\n\n/**\n * 是否处于查询态:决定触发输入框显示查询串还是选中标签。\n * 与面板可见状态解耦 —— 选择后立即退出查询态,\n * 不等面板关闭动画结束(否则显示值会延迟一个动画时长才恢复)。\n */\nconst querying = shallowRef(false)\n\nconst inputRef = useTemplateRef<InputExposed>('inputRef')\n\n/** 更新内部文案;值变化时单向 emit update:text */\nfunction setLabel(next?: string) {\n if (label.value === next) return\n label.value = next\n emit('update:text', next)\n}\n\n/** 触发输入框的值:查询态下承载查询串,否则展示选中标签 */\nconst inputValue = computed(() => {\n if (filterable.value && querying.value) return qs.value\n return model.value || model.value === 0 ? label.value : undefined\n})\n\n/** 查询态下已选标签降级为占位提示 */\nconst inputPlaceholder = computed(() => {\n const display = model.value || model.value === 0 ? label.value : undefined\n if (filterable.value && querying.value && display) {\n return display\n }\n return props.placeholder\n})\n\nfunction handleQueryInput(value: string) {\n if (!filterable.value) return\n qs.value = value\n}\n\nconst hovered = shallowRef(false)\n\n/** 悬停且存在选中值时展示清除按钮(替代下拉箭头) */\nconst showClear = computed(() => {\n if (!props.clearable || disabled.value || !hovered.value) return false\n return !!(model.value || model.value === 0 ? label.value : undefined)\n})\n\n/**\n * 过滤模式下拦截输入区域的点击:\n * 阻止 dropdown 的 trigger 开合切换,保持面板展开以不中断输入。\n * 非输入区域(箭头、留白)放行,维持原开合行为。\n */\nfunction handleTriggerClickCapture(e: MouseEvent) {\n if (!filterable.value || disabled.value) return\n if (!(e.target instanceof HTMLInputElement)) return\n\n e.stopPropagation()\n if (!dropdownVisible.value) dropdownRef.value?.open()\n}\n\nwatch(dropdownVisible, (visible) => {\n if (visible && filterable.value) {\n // 面板展开后进入查询态并聚焦输入框,可以立即输入查询\n querying.value = true\n nextTick(() => inputRef.value?.el?.focus())\n } else if (!visible) {\n querying.value = false\n }\n})\n\nconst { formProps } = injectFormContext()\n\nconst { size, disabled, readonly } = useFormFallbackProps([formProps ?? {}, props])\n\nconst dropdownRef = shallowRef<InstanceType<typeof UDropdown>>()\n\nconst { userAction, isUserActive } = useUserAction()\n\n/** 清空 */\nconst handleClear = userAction(() => {\n model.value = ''\n setLabel(undefined)\n emit('clear')\n emit('change', undefined)\n})\n\n/** 外部回显:按 data 查找 label(O(n));用户动作期内跳过 */\nwatch(\n [treeData, model],\n ([data, modelVal]) => {\n if (isUserActive()) return\n if (!data?.length || modelVal === undefined || modelVal === null || modelVal === '') {\n setLabel(undefined)\n return\n }\n\n let nextLabel: string | undefined\n const childrenKey = props.childrenKey ?? 'children'\n data.some((item) => {\n let founded = false\n dfs(\n item,\n (v) => {\n if (v[valueKey.value] === modelVal) {\n nextLabel = v[labelKey.value]\n founded = true\n return true\n }\n },\n childrenKey\n )\n\n return founded\n })\n setLabel(nextLabel)\n },\n { immediate: true }\n)\n\n/** 用户选择:O(1) 写入值与文案,跳过 data 回显查找 */\nconst handleSelect = userAction(\n (selected?: string | number, selectedData?: Record<string, any>) => {\n model.value = selected ?? ''\n\n if (selectedData) {\n setLabel(o(selectedData).get(labelKey.value))\n } else {\n setLabel(undefined)\n }\n\n emit('change', selectedData)\n // 立即退出查询态,输入框同步恢复显示选中标签\n querying.value = false\n qs.value = ''\n dropdownRef.value?.close()\n }\n)\nconst handleDropdownVisible = (visible: boolean) => {\n if (!visible) {\n qs.value = ''\n }\n}\n</script>\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import _sfc_main$68 from "./components/scroll/scroll.js";
|
|
|
4
4
|
import _sfc_main$83 from "./components/tip/tip.js";
|
|
5
5
|
import _sfc_main$28 from "./components/dropdown/dropdown.js";
|
|
6
6
|
import input_default from "./components/input/input.js";
|
|
7
|
+
import _sfc_main$47 from "./components/loading/loading.js";
|
|
7
8
|
import _sfc_main$70 from "./components/select/select.js";
|
|
8
9
|
import _sfc_main$21 from "./components/condition-editor/condition-editor.js";
|
|
9
10
|
import _sfc_main$77 from "./components/tabs/tabs-horizontal.js";
|
|
@@ -30,7 +31,6 @@ import _sfc_main$10 from "./components/card/card-content.js";
|
|
|
30
31
|
import _sfc_main$9 from "./components/card/card-action.js";
|
|
31
32
|
import _sfc_main$23 from "./components/date-panel/date-panel.js";
|
|
32
33
|
import _sfc_main$25 from "./components/date-range-picker/date-range-picker.js";
|
|
33
|
-
import _sfc_main$47 from "./components/loading/loading.js";
|
|
34
34
|
import _sfc_main$64 from "./components/progress-nodes/progress-nodes.js";
|
|
35
35
|
import _sfc_main$1 from "./components/auto-complete/auto-complete.js";
|
|
36
36
|
import _sfc_main$63 from "./components/progress/progress.js";
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import _sfc_main$83 from "./components/tip/tip.js";
|
|
|
6
6
|
import { createEmptyGroup, createEmptyLeaf, evaluate } from "./components/condition-editor/core/evaluator.js";
|
|
7
7
|
import _sfc_main$28 from "./components/dropdown/dropdown.js";
|
|
8
8
|
import input_default from "./components/input/input.js";
|
|
9
|
+
import _sfc_main$47 from "./components/loading/loading.js";
|
|
10
|
+
import { vLoading } from "./components/loading/directive.js";
|
|
9
11
|
import _sfc_main$70 from "./components/select/select.js";
|
|
10
12
|
import _sfc_main$21 from "./components/condition-editor/condition-editor.js";
|
|
11
13
|
import _sfc_main$77 from "./components/tabs/tabs-horizontal.js";
|
|
@@ -33,8 +35,6 @@ import _sfc_main$10 from "./components/card/card-content.js";
|
|
|
33
35
|
import _sfc_main$9 from "./components/card/card-action.js";
|
|
34
36
|
import _sfc_main$23 from "./components/date-panel/date-panel.js";
|
|
35
37
|
import _sfc_main$25 from "./components/date-range-picker/date-range-picker.js";
|
|
36
|
-
import _sfc_main$47 from "./components/loading/loading.js";
|
|
37
|
-
import { vLoading } from "./components/loading/directive.js";
|
|
38
38
|
import _sfc_main$64 from "./components/progress-nodes/progress-nodes.js";
|
|
39
39
|
import _sfc_main$1 from "./components/auto-complete/auto-complete.js";
|
|
40
40
|
import _sfc_main$63 from "./components/progress/progress.js";
|
package/dist/types/dropdown.d.ts
CHANGED
|
@@ -8,6 +8,16 @@ export interface DropdownProps {
|
|
|
8
8
|
* @default 'hover'
|
|
9
9
|
*/
|
|
10
10
|
trigger?: 'hover' | 'click' | 'custom';
|
|
11
|
+
/**
|
|
12
|
+
* 弹出方向;空间不足时仍会自动翻转
|
|
13
|
+
* @default 'bottom'
|
|
14
|
+
*/
|
|
15
|
+
direction?: 'top' | 'bottom' | 'left' | 'right';
|
|
16
|
+
/**
|
|
17
|
+
* 弹出对齐方式
|
|
18
|
+
* @default 'start'
|
|
19
|
+
*/
|
|
20
|
+
alignment?: 'center' | 'start' | 'end';
|
|
11
21
|
/**
|
|
12
22
|
* 宽度
|
|
13
23
|
* @default - 跟随触发宽度
|
|
@@ -3,8 +3,14 @@ import { CSSProperties } from "vue";
|
|
|
3
3
|
import { DeconstructValue, FormComponentProps } from "@veltra/utils";
|
|
4
4
|
//#region src/types/multi-tree-select.d.ts
|
|
5
5
|
/** 树形多选组件组件属性 */
|
|
6
|
-
export interface MultiTreeSelectProps extends FormComponentProps, Omit<TreeProps, 'selected' | 'checked' | 'selectable' | 'checkable'> {
|
|
6
|
+
export interface MultiTreeSelectProps extends FormComponentProps, Omit<TreeProps, 'selected' | 'checked' | 'selectable' | 'checkable' | 'data'> {
|
|
7
7
|
modelValue?: (string | number)[];
|
|
8
|
+
/**
|
|
9
|
+
* 树数据
|
|
10
|
+
* @description 如果传入一个函数,那么filterable会被强制启用;
|
|
11
|
+
* 函数按查询词返回匹配的树,初始以空串调用一次(加载默认树)
|
|
12
|
+
*/
|
|
13
|
+
data?: Record<string, any>[] | ((qs: string) => Promise<Record<string, any>[]> | Record<string, any>[]);
|
|
8
14
|
/**自定义占位文字 */
|
|
9
15
|
placeholder?: string;
|
|
10
16
|
/**
|
|
@@ -3,8 +3,14 @@ import { CSSProperties } from "vue";
|
|
|
3
3
|
import { DeconstructValue, FormComponentProps } from "@veltra/utils";
|
|
4
4
|
//#region src/types/tree-select.d.ts
|
|
5
5
|
/** 树形选择器组件属性 */
|
|
6
|
-
export interface TreeSelectProps extends FormComponentProps, Omit<TreeProps, 'selected' | 'checked' | 'selectable' | 'checkable'> {
|
|
6
|
+
export interface TreeSelectProps extends FormComponentProps, Omit<TreeProps, 'selected' | 'checked' | 'selectable' | 'checkable' | 'data'> {
|
|
7
7
|
modelValue?: string | number;
|
|
8
|
+
/**
|
|
9
|
+
* 树数据
|
|
10
|
+
* @description 如果传入一个函数,那么filterable会被强制启用;
|
|
11
|
+
* 函数按查询词返回匹配的树,初始以空串调用一次(加载默认树)
|
|
12
|
+
*/
|
|
13
|
+
data?: Record<string, any>[] | ((qs: string) => Promise<Record<string, any>[]> | Record<string, any>[]);
|
|
8
14
|
/** 自定义占位文字 */
|
|
9
15
|
placeholder?: string;
|
|
10
16
|
/**
|
package/package.json
CHANGED
|
@@ -154,19 +154,20 @@ describe('UBatchEdit quick-edit 切换编辑行', () => {
|
|
|
154
154
|
unmount()
|
|
155
155
|
})
|
|
156
156
|
|
|
157
|
-
it('
|
|
157
|
+
it('点击行打开表单,再次点击当前行关闭表单', async () => {
|
|
158
158
|
const { host, model, unmount } = mountBatchEdit()
|
|
159
159
|
|
|
160
|
-
await clickRow(host, 0)
|
|
161
|
-
|
|
162
160
|
const aside = host.querySelector<HTMLElement>('.u-batch-edit__form')!
|
|
163
|
-
expect(aside.style.display).toBe('none')
|
|
164
|
-
expect(model.label).toBe('')
|
|
165
161
|
|
|
166
|
-
await
|
|
162
|
+
await clickRow(host, 0)
|
|
167
163
|
expect(aside.style.display).not.toBe('none')
|
|
168
164
|
expect(model.label).toBe('存草稿')
|
|
169
165
|
|
|
166
|
+
// 再次点击当前行:取消选中并关闭表单,model 恢复初始值
|
|
167
|
+
await clickRow(host, 0)
|
|
168
|
+
expect(aside.style.display).toBe('none')
|
|
169
|
+
expect(model.label).toBe('')
|
|
170
|
+
|
|
170
171
|
unmount()
|
|
171
172
|
})
|
|
172
173
|
|
|
@@ -392,6 +393,117 @@ describe('UBatchEdit formMode', () => {
|
|
|
392
393
|
unmount()
|
|
393
394
|
})
|
|
394
395
|
|
|
396
|
+
it('弹框新增「保存并继续」:插入后弹框保持打开、表单重置,可连续录入', async () => {
|
|
397
|
+
const saveMethod = vi.fn()
|
|
398
|
+
const { host, data, model, unmount } = mountBatchEdit({
|
|
399
|
+
props: { formMode: 'dialog', quickEdit: false, saveMethod }
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
clickEl(host.querySelector<HTMLElement>('.u-batch-edit__add-btn')!)
|
|
403
|
+
await waitDialogOpen()
|
|
404
|
+
|
|
405
|
+
const continueBtn = () =>
|
|
406
|
+
queryDialog()!.querySelector<HTMLElement>(
|
|
407
|
+
'.u-batch-edit__form-actions button[title="保存并继续"]'
|
|
408
|
+
)!
|
|
409
|
+
|
|
410
|
+
// 编辑已有行走 update,不显示「保存并继续」由面板用例覆盖;新增弹框内应存在
|
|
411
|
+
expect(continueBtn()).toBeTruthy()
|
|
412
|
+
|
|
413
|
+
setInputValue(queryDialog()!.querySelector<HTMLInputElement>('.u-input input')!, '第一条')
|
|
414
|
+
await nextTick()
|
|
415
|
+
await nextTick()
|
|
416
|
+
clickEl(continueBtn())
|
|
417
|
+
await waitDialogSettled()
|
|
418
|
+
|
|
419
|
+
// 插入一行但弹框保持打开,表单重置为初始值
|
|
420
|
+
expect(data.value).toHaveLength(3)
|
|
421
|
+
expect(data.value[2]).toMatchObject({ label: '第一条' })
|
|
422
|
+
expect(queryDialog()).toBeTruthy()
|
|
423
|
+
expect(queryDialog()!.querySelector<HTMLInputElement>('.u-input input')!.value).toBe('')
|
|
424
|
+
expect(model.label).toBe('')
|
|
425
|
+
|
|
426
|
+
// 继续录入第二条后普通保存:追加到末尾并关闭弹框
|
|
427
|
+
setInputValue(queryDialog()!.querySelector<HTMLInputElement>('.u-input input')!, '第二条')
|
|
428
|
+
await nextTick()
|
|
429
|
+
await nextTick()
|
|
430
|
+
clickEl(querySaveButton(queryDialog()!)!)
|
|
431
|
+
await waitDialogClosed()
|
|
432
|
+
|
|
433
|
+
expect(saveMethod).toHaveBeenCalledTimes(2)
|
|
434
|
+
expect(data.value).toHaveLength(4)
|
|
435
|
+
expect(data.value[3]).toMatchObject({ label: '第二条' })
|
|
436
|
+
|
|
437
|
+
unmount()
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('弹框模式不响应 Ctrl/Cmd + S:不保存、弹框保持打开,快捷键提示不再出现', async () => {
|
|
441
|
+
const saveMethod = vi.fn()
|
|
442
|
+
const { host, data, unmount } = mountBatchEdit({
|
|
443
|
+
props: { formMode: 'dialog', quickEdit: false, saveMethod }
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
clickEl(host.querySelector<HTMLElement>('.u-batch-edit__add-btn')!)
|
|
447
|
+
await waitDialogOpen()
|
|
448
|
+
|
|
449
|
+
setInputValue(queryDialog()!.querySelector<HTMLInputElement>('.u-input input')!, '新名称')
|
|
450
|
+
await nextTick()
|
|
451
|
+
await nextTick()
|
|
452
|
+
|
|
453
|
+
queryDialog()!.dispatchEvent(
|
|
454
|
+
new KeyboardEvent('keydown', {
|
|
455
|
+
key: 's',
|
|
456
|
+
code: 'KeyS',
|
|
457
|
+
ctrlKey: true,
|
|
458
|
+
bubbles: true,
|
|
459
|
+
cancelable: true
|
|
460
|
+
})
|
|
461
|
+
)
|
|
462
|
+
await waitDialogSettled()
|
|
463
|
+
|
|
464
|
+
expect(saveMethod).not.toHaveBeenCalled()
|
|
465
|
+
expect(data.value).toHaveLength(2)
|
|
466
|
+
expect(queryDialog()).toBeTruthy()
|
|
467
|
+
expect(queryDialog()!.querySelector('.u-batch-edit__form-hint')!.textContent).not.toContain(
|
|
468
|
+
'Ctrl + S'
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
// Esc 仍可关闭弹框
|
|
472
|
+
queryDialog()!.dispatchEvent(
|
|
473
|
+
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true })
|
|
474
|
+
)
|
|
475
|
+
await waitDialogClosed()
|
|
476
|
+
expect(saveMethod).not.toHaveBeenCalled()
|
|
477
|
+
|
|
478
|
+
unmount()
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it('面板模式 Ctrl/Cmd + S 保存仍可用(组件获焦时生效)', async () => {
|
|
482
|
+
const saveMethod = vi.fn()
|
|
483
|
+
const { host, unmount } = mountBatchEdit({ props: { quickEdit: false, saveMethod } })
|
|
484
|
+
|
|
485
|
+
await clickEditRow(host, 0)
|
|
486
|
+
|
|
487
|
+
const input = host.querySelector<HTMLInputElement>('.u-batch-edit__form .u-input input')!
|
|
488
|
+
// 面板模式快捷键要求组件获焦:focusin 冒泡到根布局后置位
|
|
489
|
+
input.dispatchEvent(new FocusEvent('focusin', { bubbles: true }))
|
|
490
|
+
input.dispatchEvent(
|
|
491
|
+
new KeyboardEvent('keydown', {
|
|
492
|
+
key: 's',
|
|
493
|
+
code: 'KeyS',
|
|
494
|
+
ctrlKey: true,
|
|
495
|
+
bubbles: true,
|
|
496
|
+
cancelable: true
|
|
497
|
+
})
|
|
498
|
+
)
|
|
499
|
+
await waitDialogSettled()
|
|
500
|
+
|
|
501
|
+
expect(saveMethod).toHaveBeenCalledTimes(1)
|
|
502
|
+
expect(saveMethod.mock.calls[0]![1]).toBe('update')
|
|
503
|
+
|
|
504
|
+
unmount()
|
|
505
|
+
})
|
|
506
|
+
|
|
395
507
|
it('弹框内取消 / 关闭按钮 / 遮罩点击均不保存,且 model 恢复初始值', async () => {
|
|
396
508
|
const saveMethod = vi.fn()
|
|
397
509
|
const { host, data, model, unmount } = mountBatchEdit({
|
|
@@ -460,7 +572,7 @@ describe('UBatchEdit 重入保护', () => {
|
|
|
460
572
|
await nextTick()
|
|
461
573
|
await nextTick()
|
|
462
574
|
|
|
463
|
-
// 同一 tick
|
|
575
|
+
// 同一 tick 内连续两次点击保存(模拟保存进行中的重复触发),中间故意不等待
|
|
464
576
|
const saveBtn = querySaveButton(queryDialog()!)!
|
|
465
577
|
clickEl(saveBtn)
|
|
466
578
|
clickEl(saveBtn)
|
|
@@ -54,7 +54,11 @@
|
|
|
54
54
|
<template v-if="props.readonly"> 只读模式 </template>
|
|
55
55
|
|
|
56
56
|
<span :class="{ [bem.is('concealed')]: !shortcutActive }">
|
|
57
|
-
<template
|
|
57
|
+
<template
|
|
58
|
+
v-if="
|
|
59
|
+
props.formMode === 'panel' && !(state.formActionType === 'update' && props.quickEdit)
|
|
60
|
+
"
|
|
61
|
+
>
|
|
58
62
|
<u-kbd>Ctrl + S</u-kbd> 保存 ·
|
|
59
63
|
</template>
|
|
60
64
|
<u-kbd>Esc</u-kbd> 关闭
|
|
@@ -63,6 +67,15 @@
|
|
|
63
67
|
|
|
64
68
|
<div :class="cls.e('form-actions')">
|
|
65
69
|
<u-button size="small" text :loading="state.loading" @click="handleClose"> 取消 </u-button>
|
|
70
|
+
<u-button
|
|
71
|
+
v-if="showSaveContinueBtn"
|
|
72
|
+
size="small"
|
|
73
|
+
title="保存并继续"
|
|
74
|
+
:loading="state.loading"
|
|
75
|
+
@click="handleSave(true)"
|
|
76
|
+
>
|
|
77
|
+
保存并继续
|
|
78
|
+
</u-button>
|
|
66
79
|
<u-button
|
|
67
80
|
v-if="showSaveBtn"
|
|
68
81
|
type="primary"
|
|
@@ -70,7 +83,7 @@
|
|
|
70
83
|
title="保存"
|
|
71
84
|
:icon="Save"
|
|
72
85
|
:loading="state.loading"
|
|
73
|
-
@click="handleSave"
|
|
86
|
+
@click="handleSave()"
|
|
74
87
|
/>
|
|
75
88
|
</div>
|
|
76
89
|
</footer>
|
|
@@ -151,6 +164,12 @@ const showSaveBtn = computed(() => {
|
|
|
151
164
|
return creatable.value || updatable.value || creatableChild.value
|
|
152
165
|
})
|
|
153
166
|
|
|
167
|
+
/** 弹框模式新增类操作显示「保存并继续」:保存后重置表单继续录入,不关闭弹框 */
|
|
168
|
+
const showSaveContinueBtn = computed(() => {
|
|
169
|
+
if (props.formMode !== 'dialog') return false
|
|
170
|
+
return creatable.value || creatableChild.value
|
|
171
|
+
})
|
|
172
|
+
|
|
154
173
|
const headerInfo = computed(() => FORM_ACTION_HEADER_MAP[state.formActionType])
|
|
155
174
|
|
|
156
175
|
/** 头部图标色调修饰类:样式中 createChild 对应的修饰类为 is-child */
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
:slots="slots"
|
|
5
5
|
:class="[cls.e('table')]"
|
|
6
6
|
:columns="columns"
|
|
7
|
+
highlight-current
|
|
7
8
|
:current="state.row"
|
|
9
|
+
@update:current="handleUpdateCurrentRow"
|
|
8
10
|
@update:checked="emit('update:checked', $event)"
|
|
9
11
|
@update:selected="emit('update:selected', $event)"
|
|
10
12
|
>
|
|
11
13
|
<template #column:__action__="{ row }">
|
|
12
|
-
<u-action-group :max="5" circle>
|
|
14
|
+
<u-action-group :max="5" circle @click.stop>
|
|
13
15
|
<u-action
|
|
14
16
|
v-if="allowed('update', row)"
|
|
15
17
|
:icon="props.readonly ? View : EditPen"
|
|
@@ -95,6 +97,7 @@ const {
|
|
|
95
97
|
staticFeatures,
|
|
96
98
|
dynamicFeatures,
|
|
97
99
|
startEdit,
|
|
100
|
+
handleClose,
|
|
98
101
|
handleCreate,
|
|
99
102
|
handleDelete,
|
|
100
103
|
handleInsertToNext,
|
|
@@ -146,6 +149,20 @@ function canDelete(row: TableRow) {
|
|
|
146
149
|
return !props.readonly && allowed('delete', row)
|
|
147
150
|
}
|
|
148
151
|
|
|
152
|
+
/**
|
|
153
|
+
* 点击行打开编辑/查看表单;再次点击当前行取消选中并关闭表单。
|
|
154
|
+
* update 特性关闭时点击行不响应
|
|
155
|
+
*/
|
|
156
|
+
function handleUpdateCurrentRow(row?: TableRow) {
|
|
157
|
+
if (!allowed('update', row)) return
|
|
158
|
+
|
|
159
|
+
if (row) {
|
|
160
|
+
startEdit(row)
|
|
161
|
+
} else {
|
|
162
|
+
handleClose()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
149
166
|
const showCreateBtn = computed(() => {
|
|
150
167
|
if (props.readonly) return false
|
|
151
168
|
|
|
@@ -24,7 +24,7 @@ export const BatchEditDIKey: InjectionKey<
|
|
|
24
24
|
focused: ShallowRef<boolean>
|
|
25
25
|
/** 是否正在以编程方式重置/回显表单,此期间禁止 quick-edit 回写行数据 */
|
|
26
26
|
syncing: ShallowRef<boolean>
|
|
27
|
-
/**
|
|
27
|
+
/** 开始编辑/查看一行(点击行或操作列「编辑」按钮触发) */
|
|
28
28
|
startEdit: (row: TableRow) => void
|
|
29
29
|
} & EditReturned
|
|
30
30
|
> = Symbol('BatchEditDIKey')
|
|
@@ -40,7 +40,8 @@ interface Options {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export interface EditReturned {
|
|
43
|
-
|
|
43
|
+
/** @param keepOpen 新增类操作保存后不关闭表单(弹框模式「保存并继续」) */
|
|
44
|
+
handleSave: (keepOpen?: boolean) => Promise<void>
|
|
44
45
|
handleClose: () => void
|
|
45
46
|
handleCreate: () => void
|
|
46
47
|
handleDelete: (row: TableRow) => Promise<void>
|
|
@@ -153,8 +154,8 @@ export function useHandlers(options: Options): EditReturned {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
|
|
156
|
-
/**
|
|
157
|
-
const handleSave = runWithLoading(async () => {
|
|
157
|
+
/** 保存;keepOpen 为 true 时(「保存并继续」)新增保存后不关闭表单 */
|
|
158
|
+
const handleSave = runWithLoading(async (keepOpen = false) => {
|
|
158
159
|
const { model, saveMethod } = props
|
|
159
160
|
const valid = await formRef.value?.validate()
|
|
160
161
|
if (!model || !valid) return
|
|
@@ -172,8 +173,8 @@ export function useHandlers(options: Options): EditReturned {
|
|
|
172
173
|
}
|
|
173
174
|
insert(item)
|
|
174
175
|
|
|
175
|
-
//
|
|
176
|
-
if (props.formMode === 'dialog') {
|
|
176
|
+
// 弹框模式保存成功即关闭,连续新增只能由「保存并继续」显式触发
|
|
177
|
+
if (props.formMode === 'dialog' && !keepOpen) {
|
|
177
178
|
resetState()
|
|
178
179
|
return
|
|
179
180
|
}
|
|
@@ -40,10 +40,11 @@ export function useShortcutKey(options: Options) {
|
|
|
40
40
|
/**
|
|
41
41
|
* 键盘快捷键:
|
|
42
42
|
* - Esc 关闭表单
|
|
43
|
-
* - ⌘/Ctrl + S
|
|
43
|
+
* - ⌘/Ctrl + S 保存(仅面板模式;快速编辑模式下编辑行时除外)
|
|
44
44
|
*
|
|
45
45
|
* 面板模式仅组件获焦时生效;弹框模式表单被 teleport 到 body,
|
|
46
|
-
*
|
|
46
|
+
* 组件聚焦状态覆盖不到,改为弹框打开期间生效。
|
|
47
|
+
* 弹框模式不支持 ⌘/Ctrl + S 保存,保存只能走底部按钮
|
|
47
48
|
*/
|
|
48
49
|
function handleKeydown(e: KeyboardEvent) {
|
|
49
50
|
const active = props.formMode === 'dialog' ? state.formVisible : focused.value
|
|
@@ -52,6 +53,8 @@ export function useShortcutKey(options: Options) {
|
|
|
52
53
|
|
|
53
54
|
if (e.key === 'Escape') return runEscape(e)
|
|
54
55
|
|
|
56
|
+
if (props.formMode === 'dialog') return
|
|
57
|
+
|
|
55
58
|
const meta = e.metaKey || e.ctrlKey
|
|
56
59
|
if (!meta) return
|
|
57
60
|
|