@neatui/vue 5.1.10 → 5.2.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/CHANGELOG.md +35 -0
- package/README.md +2 -0
- package/package.json +3 -2
- package/src/components/basic/IFollowView.vue +156 -121
- package/src/components/basic/LayerView/Layer.vue +244 -42
- package/src/components/form/DateView.vue +1 -1
- package/src/components/form/MoreSelect.vue +1 -1
- package/src/components/form/MoreSelectTree.vue +1 -1
- package/src/components/form/PageMoreSelect.vue +1 -1
- package/src/components/form/PageSelect.vue +1 -1
- package/src/components/form/SearchMoreSelect.vue +1 -1
- package/src/components/form/SearchSelect.vue +1 -1
- package/src/components/form/Select.vue +31 -19
- package/src/components/form/SelectTree/SelectTree.vue +1 -1
- package/src/components/loader/FormLoader/FormLoader.vue +4 -4
- package/src/components/loader/FormLoader/FormRender.vue +7 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @neatui/vue 更新记录
|
|
2
|
+
|
|
3
|
+
## 5.2.0
|
|
4
|
+
|
|
5
|
+
### Layer
|
|
6
|
+
- 新增 `on.cancel` 回调,修正 show / hide 回调的语义。
|
|
7
|
+
- 加强注册逻辑;动画失败时有兜底;点击遮罩关闭更可靠。
|
|
8
|
+
- 离开分组时释放遮罩;校验挂载区域的容器。
|
|
9
|
+
- 排队中的弹层在同一时刻先 show 再 hide 时,仍按队列规则处理;任意 hide 都会丢弃排队中的请求。
|
|
10
|
+
- 关闭或立即挂起后,下层弹层能正确恢复。
|
|
11
|
+
|
|
12
|
+
### IFollowView
|
|
13
|
+
- 重写点击外部关闭的逻辑;同步嵌套浮层、`v-model` 和 hover 的状态。
|
|
14
|
+
- 挂载时就是打开状态的,也会初始化定位。
|
|
15
|
+
- 手动切换开关时取消 hover 计时器;鼠标在嵌套子浮层上时父浮层保持打开。
|
|
16
|
+
- 释放已失效的 Follow 实例。
|
|
17
|
+
|
|
18
|
+
### Select
|
|
19
|
+
- 选中“其他”后立即显示输入框,有独立字段时也不再要求先有值;输入框会自动聚焦。
|
|
20
|
+
- 回车和失焦确认不再重复触发 `change`。
|
|
21
|
+
- 有独立字段时,空输入会保留已选的“其他”,等待补填。
|
|
22
|
+
- 新增 `footer` 插槽,可通过插槽参数拿到 `close` 方法。
|
|
23
|
+
|
|
24
|
+
### DateView
|
|
25
|
+
- 手机端宽度撑满。
|
|
26
|
+
|
|
27
|
+
### 依赖
|
|
28
|
+
- CSS 依赖更新为 `@neatui/css@^5.2.0`。Grid 间距的不兼容变更见 5.1.11 一节。
|
|
29
|
+
|
|
30
|
+
## 5.1.11
|
|
31
|
+
|
|
32
|
+
- FormLoader 新增 `mode="read"`:保留表单控件,统一设置 `disabled`;禁止字段写回、数组增删和排序,隐藏校验提示。插槽的 `bind.disabled` 同步为 `true`,自定义控件需绑定该属性。
|
|
33
|
+
- FormLoader 的 `字段名-value` 插槽仅在 `view` 模式使用;`read` 使用普通控件或字段插槽。
|
|
34
|
+
- Select、树选择、多选、分页选择和搜索选择的搜索区域在移动端撑满宽度。
|
|
35
|
+
- CSS 依赖更新为 `@neatui/css@^5.1.11`。该版本包含不兼容的 Grid 间距语法调整:`ui-grid="3 mm"` 改为 `ui-grid="3" class="gap-mm"`,裸 `lm` 改为左侧垂直居中。响应式间距的前缀规则不同,请阅读 CSS 包随附的 `CHANGELOG.md` 进行迁移。
|
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neatui/vue",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "basic component ui",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"@fekit/scrollto": "^3.0.3",
|
|
37
37
|
"@fekit/toast": "^2.7.2",
|
|
38
38
|
"@fekit/utils": "^6.0.0",
|
|
39
|
-
"@neatui/css": "^5.
|
|
39
|
+
"@neatui/css": "^5.2.0",
|
|
40
40
|
"@rollup/plugin-commonjs": "^26.0.1",
|
|
41
41
|
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
42
42
|
"@vue/compiler-sfc": "^3.4.30",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"files": [
|
|
53
53
|
"src",
|
|
54
54
|
"README.md",
|
|
55
|
+
"CHANGELOG.md",
|
|
55
56
|
"MIGRATION-5.0.md"
|
|
56
57
|
]
|
|
57
58
|
}
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
:style="`position: fixed; pointer-events: none; ${tipStyle}`"
|
|
13
13
|
:ui-tips="uiTips"
|
|
14
14
|
:ui-tips-view="state.show ? 1 : 0"
|
|
15
|
-
@mouseover="
|
|
16
|
-
@mouseout="
|
|
15
|
+
@mouseover="tipHover(1)"
|
|
16
|
+
@mouseout="tipHover(0)"
|
|
17
17
|
@click="clickhide"
|
|
18
18
|
>
|
|
19
|
-
<div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="
|
|
19
|
+
<div ref="box" :ui-tips-box="state.pos" :class="tipBoxClass" :style="tipBoxStyle">
|
|
20
20
|
<div ui-hide=">mob" class="mob-follow-tips w-full n-ss" ui-flex="row xm">
|
|
21
21
|
<div class="w-xs*s"></div>
|
|
22
22
|
<div>
|
|
@@ -35,8 +35,15 @@
|
|
|
35
35
|
</Teleport>
|
|
36
36
|
</div>
|
|
37
37
|
</template>
|
|
38
|
+
<script lang="ts">
|
|
39
|
+
// 每个实例只登记自己的后代,Teleport 后仍可辨认嵌套浮层。
|
|
40
|
+
type FollowBoundary = { contains: (target: Node) => boolean; hide: () => void };
|
|
41
|
+
// hover:Teleport 内容的鼠标进出不会冒泡到祖先,需要逐级通知。
|
|
42
|
+
type FollowContext = { children: Set<FollowBoundary>; hover: (on: boolean) => void };
|
|
43
|
+
const followContextKey = Symbol('follow-context');
|
|
44
|
+
</script>
|
|
38
45
|
<script setup lang="ts">
|
|
39
|
-
import { reactive, ref, onMounted, onUnmounted, watch } from 'vue';
|
|
46
|
+
import { reactive, ref, onMounted, onUnmounted, watch, inject, provide } from 'vue';
|
|
40
47
|
import Follow from '@fekit/follow';
|
|
41
48
|
|
|
42
49
|
const emits = defineEmits(['update:modelValue', 'onhide', 'onshow']);
|
|
@@ -55,6 +62,7 @@
|
|
|
55
62
|
tipBoxStyle?: string;
|
|
56
63
|
arrow?: any;
|
|
57
64
|
clickTipHide?: boolean;
|
|
65
|
+
/** @deprecated 已移除,不再生效,将在 6.0 删除该属性。 */
|
|
58
66
|
dragTipHide?: boolean;
|
|
59
67
|
mobile?: boolean;
|
|
60
68
|
title?: string;
|
|
@@ -75,7 +83,7 @@
|
|
|
75
83
|
tipBoxStyle: '',
|
|
76
84
|
arrow: {},
|
|
77
85
|
clickTipHide: false,
|
|
78
|
-
dragTipHide:
|
|
86
|
+
dragTipHide: undefined,
|
|
79
87
|
mobile: true,
|
|
80
88
|
title: '',
|
|
81
89
|
zIndex: undefined,
|
|
@@ -87,87 +95,23 @@
|
|
|
87
95
|
const teleportReady = ref(false);
|
|
88
96
|
const state = reactive({
|
|
89
97
|
view: 0,
|
|
90
|
-
show: 0,
|
|
98
|
+
show: props.modelValue ? 1 : 0,
|
|
91
99
|
pos: props.pos,
|
|
92
100
|
hover: 0,
|
|
93
|
-
tipdom: false,
|
|
94
|
-
isMobile: false,
|
|
95
101
|
});
|
|
96
102
|
|
|
103
|
+
if (props.dragTipHide !== undefined) {
|
|
104
|
+
console.warn('「IFollowView」 dragTipHide 已移除且不再生效,将在 6.0 删除该属性,请移除 drag-tip-hide。');
|
|
105
|
+
}
|
|
106
|
+
|
|
97
107
|
const showTimer: any = ref(null);
|
|
98
108
|
const hideTimer: any = ref(null);
|
|
99
|
-
const dragPointerId = ref<number | null>(null);
|
|
100
|
-
const dragFromInternal = ref(false);
|
|
101
|
-
const dragStartX = ref(0);
|
|
102
|
-
const dragStartY = ref(0);
|
|
103
|
-
const dragPassing = ref(false);
|
|
104
|
-
const DRAG_HIDE_PX = 6;
|
|
105
|
-
|
|
106
109
|
const containsPoint = (el: HTMLElement | null, x: number, y: number) => {
|
|
107
110
|
if (!el) return false;
|
|
108
111
|
const rect = el.getBoundingClientRect();
|
|
109
112
|
return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
|
|
110
113
|
};
|
|
111
114
|
|
|
112
|
-
const isOutsideFollow = (x: number, y: number) => {
|
|
113
|
-
return !containsPoint(box.value, x, y) && !containsPoint(dom.value, x, y);
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const isInternalDragSource = (target: EventTarget | null) => {
|
|
117
|
-
if (!(target instanceof Element)) return false;
|
|
118
|
-
if (target.closest('input, textarea, select, [contenteditable="true"], [role="slider"]')) return true;
|
|
119
|
-
const stop = box.value?.parentElement || null;
|
|
120
|
-
let el: Element | null = target;
|
|
121
|
-
while (el && el !== stop) {
|
|
122
|
-
const style = window.getComputedStyle(el);
|
|
123
|
-
const canScroll =
|
|
124
|
-
((style.overflowY === 'auto' || style.overflowY === 'scroll') && el.scrollHeight > el.clientHeight) ||
|
|
125
|
-
((style.overflowX === 'auto' || style.overflowX === 'scroll') && el.scrollWidth > el.clientWidth);
|
|
126
|
-
if (canScroll) return true;
|
|
127
|
-
el = el.parentElement;
|
|
128
|
-
}
|
|
129
|
-
return false;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const clearDragSession = () => {
|
|
133
|
-
dragPointerId.value = null;
|
|
134
|
-
dragFromInternal.value = false;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
const hideFromDrag = () => {
|
|
138
|
-
dragPassing.value = true;
|
|
139
|
-
state.show = 0;
|
|
140
|
-
state.hover = 0;
|
|
141
|
-
clearDragSession();
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const onDragPointerDown = (ev: PointerEvent) => {
|
|
145
|
-
if (!props.dragTipHide || !state.show || ev.button !== 0) return;
|
|
146
|
-
if (!box.value?.contains(ev.target as Node)) return;
|
|
147
|
-
dragPointerId.value = ev.pointerId;
|
|
148
|
-
dragFromInternal.value = isInternalDragSource(ev.target);
|
|
149
|
-
dragStartX.value = ev.clientX;
|
|
150
|
-
dragStartY.value = ev.clientY;
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
const onDragPointerMove = (ev: PointerEvent) => {
|
|
154
|
-
if (!props.dragTipHide || dragPointerId.value !== ev.pointerId || ev.buttons === 0) return;
|
|
155
|
-
if (isOutsideFollow(ev.clientX, ev.clientY)) {
|
|
156
|
-
hideFromDrag();
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (dragFromInternal.value) return;
|
|
160
|
-
const dx = ev.clientX - dragStartX.value;
|
|
161
|
-
const dy = ev.clientY - dragStartY.value;
|
|
162
|
-
if (dx * dx + dy * dy >= DRAG_HIDE_PX * DRAG_HIDE_PX) {
|
|
163
|
-
hideFromDrag();
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
const onDragPointerUp = (ev: PointerEvent) => {
|
|
168
|
-
if (dragPointerId.value === ev.pointerId) clearDragSession();
|
|
169
|
-
};
|
|
170
|
-
|
|
171
115
|
watch(
|
|
172
116
|
() => state.hover,
|
|
173
117
|
() => {
|
|
@@ -183,26 +127,45 @@
|
|
|
183
127
|
}, 100);
|
|
184
128
|
}
|
|
185
129
|
},
|
|
130
|
+
// 同步创建定时器,使同一事件中随后的 hide()/show() 能够取消它。
|
|
131
|
+
{ flush: 'sync' },
|
|
186
132
|
);
|
|
187
133
|
|
|
134
|
+
// 主动切换状态时取消待执行的 hover 定时器,避免被延迟回调覆盖。
|
|
135
|
+
const setShow = (value: number) => {
|
|
136
|
+
clearTimeout(showTimer.value);
|
|
137
|
+
clearTimeout(hideTimer.value);
|
|
138
|
+
state.show = value;
|
|
139
|
+
};
|
|
140
|
+
|
|
188
141
|
// 开关
|
|
189
142
|
const toggle = () => {
|
|
190
143
|
if (props.event === 'click') {
|
|
191
|
-
state.show
|
|
144
|
+
setShow(state.show ? 0 : 1);
|
|
192
145
|
}
|
|
193
146
|
};
|
|
194
147
|
|
|
148
|
+
// 外部 v-model 控制显示状态。
|
|
195
149
|
watch(
|
|
196
|
-
|
|
197
|
-
() => {
|
|
198
|
-
|
|
150
|
+
() => props.modelValue,
|
|
151
|
+
(value) => {
|
|
152
|
+
const next = value ? 1 : 0;
|
|
153
|
+
if (next !== state.show) setShow(next);
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// 只在实际显示状态变化时通知;父层关闭时同步关闭后代浮层(Teleport 后不会随父层隐藏)。
|
|
158
|
+
watch(
|
|
159
|
+
() => state.show,
|
|
160
|
+
(show) => {
|
|
161
|
+
if (show) {
|
|
199
162
|
emits('onshow');
|
|
200
163
|
} else {
|
|
164
|
+
children.forEach(item => item.hide());
|
|
201
165
|
emits('onhide');
|
|
202
166
|
}
|
|
203
|
-
emits('update:modelValue',
|
|
167
|
+
emits('update:modelValue', show);
|
|
204
168
|
},
|
|
205
|
-
{ deep: true },
|
|
206
169
|
);
|
|
207
170
|
|
|
208
171
|
const hovershow = () => {
|
|
@@ -219,35 +182,112 @@
|
|
|
219
182
|
|
|
220
183
|
const clickhide = () => {
|
|
221
184
|
if (props.clickTipHide) {
|
|
222
|
-
|
|
185
|
+
setShow(0);
|
|
223
186
|
}
|
|
224
187
|
};
|
|
225
188
|
|
|
226
189
|
const show = () => {
|
|
227
|
-
|
|
228
|
-
state.show = 1;
|
|
190
|
+
setShow(1);
|
|
229
191
|
};
|
|
230
192
|
|
|
231
193
|
const hide = () => {
|
|
232
|
-
|
|
233
|
-
|
|
194
|
+
setShow(0);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const parentContext = inject<FollowContext | null>(followContextKey, null);
|
|
198
|
+
const parentChildren = parentContext?.children ?? null;
|
|
199
|
+
const children = new Set<FollowBoundary>();
|
|
200
|
+
// 后代浮层内容的进出视同本层;本层 tip 也是 Teleport 内容,继续向上通知。
|
|
201
|
+
const notifyHover = (on: boolean) => {
|
|
202
|
+
if (props.event === 'hover') state.hover = on ? 1 : 0;
|
|
203
|
+
parentContext?.hover(on);
|
|
204
|
+
};
|
|
205
|
+
const tipHover = (on: number) => notifyHover(!!on);
|
|
206
|
+
provide(followContextKey, { children, hover: notifyHover });
|
|
207
|
+
const inChildren = (target: Node): boolean => [...children].some(item => item.contains(target));
|
|
208
|
+
const containsFollow: FollowBoundary = {
|
|
209
|
+
contains: target => !!dom.value?.contains(target) || (!!state.show && (!!tip.value?.contains(target) || inChildren(target))),
|
|
210
|
+
hide: () => {
|
|
211
|
+
state.hover = 0;
|
|
212
|
+
setShow(0);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
parentChildren?.add(containsFollow);
|
|
216
|
+
const isInside = (target: Node) => !!dom.value?.contains(target) || !!box.value?.contains(target) || inChildren(target);
|
|
217
|
+
const isBackdrop = (target: EventTarget | null) =>
|
|
218
|
+
props.mobile && window.matchMedia('(max-width: 799px)').matches && target === tip.value;
|
|
219
|
+
|
|
220
|
+
const dismissOutside = () => {
|
|
221
|
+
// 暗层 touchstart 被取消默认行为时,主动失焦,让表单先提交再关闭。
|
|
222
|
+
const active = document.activeElement;
|
|
223
|
+
if (active instanceof HTMLElement && isInside(active)) active.blur();
|
|
224
|
+
clearTimeout(showTimer.value);
|
|
225
|
+
clearTimeout(hideTimer.value);
|
|
226
|
+
state.hover = 0;
|
|
227
|
+
hide();
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// 记录本次按下是否发生在浮层内:在内部按下、拖到外部松开(拖选文字、拖滑块等)
|
|
231
|
+
// 产生的 click 目标是共同祖先,不能当作外部点击。
|
|
232
|
+
let pressedInside = false;
|
|
233
|
+
const onPressStart = (event: PointerEvent) => {
|
|
234
|
+
pressedInside = !!state.show && event.target instanceof Node && isInside(event.target);
|
|
235
|
+
};
|
|
236
|
+
// 松开后紧随的 click 处理完再清除;没有 click(触摸滚动等)时也不残留到下一次键盘点击。
|
|
237
|
+
const onPressEnd = () => {
|
|
238
|
+
if (pressedInside) setTimeout(() => { pressedInside = false; }, 0);
|
|
234
239
|
};
|
|
235
240
|
|
|
236
|
-
//
|
|
237
|
-
const cancel = (
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
// 外部点击以内容盒子为边界;按下位置在内部时不关闭。
|
|
242
|
+
const cancel = (event?: MouseEvent) => {
|
|
243
|
+
const fromInside = pressedInside;
|
|
244
|
+
pressedInside = false;
|
|
245
|
+
if (!state.show) return;
|
|
246
|
+
if (event && fromInside) return;
|
|
247
|
+
const target = event?.target;
|
|
248
|
+
if (target instanceof Node && isInside(target)) return;
|
|
249
|
+
if (event && isBackdrop(target ?? null)) {
|
|
250
|
+
event.preventDefault();
|
|
251
|
+
// 同一 document 上父浮层的监听器也不能继续处理子暗层点击。
|
|
252
|
+
event.stopImmediatePropagation();
|
|
240
253
|
}
|
|
254
|
+
dismissOutside();
|
|
241
255
|
};
|
|
242
256
|
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
257
|
+
// 轻触跟踪:暗层轻触由组件自己处理(阻止点穿);外部轻触在 touchend 关闭,
|
|
258
|
+
// 因为 iOS/WebKit 轻触非可点击元素不会派发 click。
|
|
259
|
+
const TAP_SLOP = 10;
|
|
260
|
+
let outsideTouch: { id: number; x: number; y: number; moved: boolean; backdrop: boolean } | null = null;
|
|
261
|
+
const onTouchStart = (event: TouchEvent) => {
|
|
262
|
+
outsideTouch = null;
|
|
263
|
+
if (!state.show) return;
|
|
264
|
+
const target = event.target;
|
|
265
|
+
const backdrop = isBackdrop(target);
|
|
266
|
+
if (!backdrop && (!(target instanceof Node) || isInside(target))) return;
|
|
267
|
+
if (backdrop) event.preventDefault();
|
|
268
|
+
const touch = event.touches[0];
|
|
269
|
+
if (event.touches.length === 1 && touch) {
|
|
270
|
+
outsideTouch = { id: touch.identifier, x: touch.clientX, y: touch.clientY, moved: false, backdrop };
|
|
249
271
|
}
|
|
250
272
|
};
|
|
273
|
+
const onTouchMove = (event: TouchEvent) => {
|
|
274
|
+
if (!outsideTouch) return;
|
|
275
|
+
// 暗层上阻止页面滚动;外部区域保持原生滚动,滚动则不视为轻触。
|
|
276
|
+
if (outsideTouch.backdrop) event.preventDefault();
|
|
277
|
+
const start = outsideTouch;
|
|
278
|
+
const touch = [...event.touches].find(item => item.identifier === start.id);
|
|
279
|
+
if (!touch || event.touches.length !== 1 || Math.hypot(touch.clientX - start.x, touch.clientY - start.y) > TAP_SLOP) start.moved = true;
|
|
280
|
+
};
|
|
281
|
+
const onTouchEnd = (event: TouchEvent) => {
|
|
282
|
+
const start = outsideTouch;
|
|
283
|
+
outsideTouch = null;
|
|
284
|
+
if (!start) return;
|
|
285
|
+
if (start.backdrop) event.preventDefault();
|
|
286
|
+
const touch = [...event.changedTouches].find(item => item.identifier === start.id);
|
|
287
|
+
if (!state.show || start.moved || !touch || Math.hypot(touch.clientX - start.x, touch.clientY - start.y) > TAP_SLOP) return;
|
|
288
|
+
if (start.backdrop ? isBackdrop(event.target) && !containsPoint(box.value, touch.clientX, touch.clientY) : true) dismissOutside();
|
|
289
|
+
};
|
|
290
|
+
const onTouchCancel = () => { outsideTouch = null; };
|
|
251
291
|
|
|
252
292
|
const ensureTeleportTarget = () => {
|
|
253
293
|
let area = document.querySelector('#ifollow');
|
|
@@ -261,24 +301,21 @@
|
|
|
261
301
|
|
|
262
302
|
onMounted(() => {
|
|
263
303
|
ensureTeleportTarget();
|
|
304
|
+
document.addEventListener('pointerdown', onPressStart, true);
|
|
305
|
+
document.addEventListener('pointerup', onPressEnd, true);
|
|
306
|
+
document.addEventListener('pointercancel', onPressEnd, true);
|
|
264
307
|
document.addEventListener('click', cancel, true);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
document.addEventListener('touchstart', preventTouchScroll, {
|
|
271
|
-
passive: false,
|
|
272
|
-
});
|
|
273
|
-
document.addEventListener('touchmove', preventTouchScroll, {
|
|
274
|
-
passive: false,
|
|
275
|
-
});
|
|
276
|
-
// 判断是否为移动端
|
|
277
|
-
state.isMobile = window.innerWidth < 800;
|
|
308
|
+
// 暗层需要阻止默认行为,触摸监听不能是 passive。
|
|
309
|
+
document.addEventListener('touchstart', onTouchStart, { passive: false });
|
|
310
|
+
document.addEventListener('touchmove', onTouchMove, { passive: false });
|
|
311
|
+
document.addEventListener('touchend', onTouchEnd, { passive: false });
|
|
312
|
+
document.addEventListener('touchcancel', onTouchCancel);
|
|
278
313
|
});
|
|
279
314
|
|
|
280
315
|
const ex: any = ref(null);
|
|
281
316
|
watch([() => dom.value, () => tip.value], () => {
|
|
317
|
+
// 替换或移除 tips 前先注销旧实例的监听。
|
|
318
|
+
ex.value?.remove();
|
|
282
319
|
if (dom.value && tip.value) {
|
|
283
320
|
ex.value = new Follow(
|
|
284
321
|
dom.value,
|
|
@@ -300,6 +337,8 @@
|
|
|
300
337
|
},
|
|
301
338
|
{ hideOnScrollOut: false, zIndex: props.zIndex },
|
|
302
339
|
);
|
|
340
|
+
// 初始即显示(如 modelValue=1)时 state.show 不会再变化,需要立即启动定位。
|
|
341
|
+
if (state.show) ex.value.listen();
|
|
303
342
|
} else {
|
|
304
343
|
ex.value = null;
|
|
305
344
|
}
|
|
@@ -313,14 +352,16 @@
|
|
|
313
352
|
);
|
|
314
353
|
|
|
315
354
|
onUnmounted(() => {
|
|
355
|
+
document.removeEventListener('pointerdown', onPressStart, true);
|
|
356
|
+
document.removeEventListener('pointerup', onPressEnd, true);
|
|
357
|
+
document.removeEventListener('pointercancel', onPressEnd, true);
|
|
316
358
|
document.removeEventListener('click', cancel, true);
|
|
317
|
-
document.removeEventListener('
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
document.removeEventListener('touchmove', preventTouchScroll);
|
|
359
|
+
document.removeEventListener('touchstart', onTouchStart);
|
|
360
|
+
document.removeEventListener('touchmove', onTouchMove);
|
|
361
|
+
document.removeEventListener('touchend', onTouchEnd);
|
|
362
|
+
document.removeEventListener('touchcancel', onTouchCancel);
|
|
363
|
+
parentChildren?.delete(containsFollow);
|
|
364
|
+
outsideTouch = null;
|
|
324
365
|
state.show = 0;
|
|
325
366
|
|
|
326
367
|
if (ex.value) {
|
|
@@ -331,11 +372,6 @@
|
|
|
331
372
|
watch(
|
|
332
373
|
() => state.show,
|
|
333
374
|
(show: any) => {
|
|
334
|
-
if (show) {
|
|
335
|
-
dragPassing.value = false;
|
|
336
|
-
} else {
|
|
337
|
-
clearDragSession();
|
|
338
|
-
}
|
|
339
375
|
if (ex.value) {
|
|
340
376
|
if (show) {
|
|
341
377
|
ex.value.listen();
|
|
@@ -371,7 +407,6 @@
|
|
|
371
407
|
&[ui-tips-view='1'] {
|
|
372
408
|
background-color: rgba(0, 0, 0, 0.5);
|
|
373
409
|
pointer-events: auto !important;
|
|
374
|
-
touch-action: none !important;
|
|
375
410
|
}
|
|
376
411
|
|
|
377
412
|
&[ui-tips] [ui-tips-box] {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
:view="state.view"
|
|
13
13
|
class="fekit-layer"
|
|
14
14
|
>
|
|
15
|
-
<div :ui-flex="`col ${flex}`" :style="wrapStyle" class="fekit-layer-wrap" @animationend="anim" @click="_sys_hide">
|
|
15
|
+
<div ref="wrapRef" :ui-flex="`col ${flex}`" :style="wrapStyle" class="fekit-layer-wrap" @animationend="anim" @pointerdown="_sys_press" @pointerup="_sys_release" @click="_sys_hide">
|
|
16
16
|
<div v-if="state.coverDark !== ''" class="fekit-layer-cover-dark"></div>
|
|
17
17
|
<slot :param="_tasks[id]?.params"></slot>
|
|
18
18
|
</div>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
</div>
|
|
22
22
|
</template>
|
|
23
23
|
<script lang="ts" setup>
|
|
24
|
-
import { computed, nextTick, onBeforeUnmount, reactive, watch } from 'vue';
|
|
24
|
+
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from 'vue';
|
|
25
25
|
import { env, isFunction } from '@fekit/utils';
|
|
26
26
|
|
|
27
27
|
// 弹层队列
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
el: HTMLElement | null;
|
|
74
74
|
listening: boolean;
|
|
75
75
|
owners: Set<string>;
|
|
76
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
76
77
|
view: number;
|
|
77
78
|
};
|
|
78
79
|
queue: Array<{ id: string; kind: 'open' | 'show'; params: any; then: any }>;
|
|
@@ -186,9 +187,65 @@
|
|
|
186
187
|
return task.view === 1 && !!task.parkAfterHide;
|
|
187
188
|
};
|
|
188
189
|
|
|
190
|
+
const parseTimeList = (value: string) =>
|
|
191
|
+
value.split(',').map((item) => {
|
|
192
|
+
const text = item.trim();
|
|
193
|
+
const num = Number.parseFloat(text) || 0;
|
|
194
|
+
return text.endsWith('ms') ? num : num * 1000;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 读取元素当前动画的最长耗时(ms);没有动画返回 0
|
|
198
|
+
const getAnimationWait = (el: Element | null | undefined) => {
|
|
199
|
+
if (!el || !env.client) return 0;
|
|
200
|
+
const style = window.getComputedStyle(el);
|
|
201
|
+
const names = style.animationName.split(',');
|
|
202
|
+
const durations = parseTimeList(style.animationDuration);
|
|
203
|
+
const delays = parseTimeList(style.animationDelay);
|
|
204
|
+
let wait = 0;
|
|
205
|
+
names.forEach((name, index) => {
|
|
206
|
+
if (name.trim() === 'none') return;
|
|
207
|
+
const duration = durations[index % durations.length] || 0;
|
|
208
|
+
const delay = delays[index % delays.length] || 0;
|
|
209
|
+
wait = Math.max(wait, duration + delay);
|
|
210
|
+
});
|
|
211
|
+
return wait;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// animationend 兜底:动画缺失/被禁用/事件丢失时也能推进状态
|
|
215
|
+
const ANIMATION_FALLBACK_SLACK = 100;
|
|
216
|
+
|
|
217
|
+
const onGlobalDarkAnimationDone = (group: AreaGroup) => {
|
|
218
|
+
if (group.dark.timer) {
|
|
219
|
+
clearTimeout(group.dark.timer);
|
|
220
|
+
group.dark.timer = undefined;
|
|
221
|
+
}
|
|
222
|
+
if (group.dark.view === 2) setGlobalDarkView(group, 3);
|
|
223
|
+
else if (group.dark.view === 1) {
|
|
224
|
+
setGlobalDarkView(group, 0);
|
|
225
|
+
const afterHide = group.dark.afterHide;
|
|
226
|
+
group.dark.afterHide = undefined;
|
|
227
|
+
afterHide?.();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
189
231
|
const setGlobalDarkView = (group: AreaGroup, view: number) => {
|
|
190
232
|
group.dark.view = view;
|
|
191
|
-
if (group.dark.
|
|
233
|
+
if (group.dark.timer) {
|
|
234
|
+
clearTimeout(group.dark.timer);
|
|
235
|
+
group.dark.timer = undefined;
|
|
236
|
+
}
|
|
237
|
+
const el = group.dark.el;
|
|
238
|
+
if (!el) return;
|
|
239
|
+
el.setAttribute('view', `${view}`);
|
|
240
|
+
if (view !== 1 && view !== 2) return;
|
|
241
|
+
const wait = getAnimationWait(el);
|
|
242
|
+
group.dark.timer = setTimeout(
|
|
243
|
+
() => {
|
|
244
|
+
group.dark.timer = undefined;
|
|
245
|
+
if (group.dark.view === view) onGlobalDarkAnimationDone(group);
|
|
246
|
+
},
|
|
247
|
+
wait > 0 ? wait + ANIMATION_FALLBACK_SLACK : 0,
|
|
248
|
+
);
|
|
192
249
|
};
|
|
193
250
|
|
|
194
251
|
const ensureGlobalDarkEl = (area: string, group: AreaGroup) => {
|
|
@@ -208,13 +265,7 @@
|
|
|
208
265
|
group.dark.el = el;
|
|
209
266
|
el.addEventListener('animationend', (ev) => {
|
|
210
267
|
if (ev.target !== el) return;
|
|
211
|
-
|
|
212
|
-
if (group.dark.view === 1) {
|
|
213
|
-
setGlobalDarkView(group, 0);
|
|
214
|
-
const afterHide = group.dark.afterHide;
|
|
215
|
-
group.dark.afterHide = undefined;
|
|
216
|
-
afterHide?.();
|
|
217
|
-
}
|
|
268
|
+
onGlobalDarkAnimationDone(group);
|
|
218
269
|
});
|
|
219
270
|
group.dark.listening = true;
|
|
220
271
|
return el;
|
|
@@ -316,8 +367,17 @@
|
|
|
316
367
|
syncStackIndex(group);
|
|
317
368
|
};
|
|
318
369
|
|
|
370
|
+
const runQueuedCancel = (job: { params: any }) => {
|
|
371
|
+
const cancel = job.params?.on?.cancel;
|
|
372
|
+
if (typeof cancel === 'function') cancel();
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// 撤掉队列中的显示请求:该请求不会再显示,视为取消
|
|
319
376
|
const removeFromQueue = (group: AreaGroup, layerId: string) => {
|
|
377
|
+
const removed = group.queue.filter((job) => job.id === layerId);
|
|
378
|
+
if (!removed.length) return;
|
|
320
379
|
group.queue = group.queue.filter((job) => job.id !== layerId);
|
|
380
|
+
removed.forEach(runQueuedCancel);
|
|
321
381
|
};
|
|
322
382
|
|
|
323
383
|
const getVisibleTop = (group: AreaGroup, exceptId = '') => {
|
|
@@ -506,13 +566,23 @@
|
|
|
506
566
|
wrapStyle: '',
|
|
507
567
|
});
|
|
508
568
|
|
|
509
|
-
|
|
510
|
-
|
|
569
|
+
// 容器不存在时:仅 #id 形式可自动创建;其它选择器无法据此建出可匹配的节点,只提示
|
|
570
|
+
if (env.client && props.area) {
|
|
571
|
+
let _area: Element | null = null;
|
|
572
|
+
try {
|
|
573
|
+
_area = document.querySelector(props.area);
|
|
574
|
+
} catch {
|
|
575
|
+
_area = null;
|
|
576
|
+
}
|
|
511
577
|
if (!_area) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
578
|
+
if (/^#[\w-]+$/.test(props.area)) {
|
|
579
|
+
const el = document.createElement('div');
|
|
580
|
+
el.id = props.area.substring(1);
|
|
581
|
+
el.style.position = 'absolute';
|
|
582
|
+
document.body.appendChild(el);
|
|
583
|
+
} else {
|
|
584
|
+
console.warn('Layer 的 area 容器不存在,且只有 #id 形式可自动创建:', props.area);
|
|
585
|
+
}
|
|
516
586
|
}
|
|
517
587
|
}
|
|
518
588
|
|
|
@@ -528,6 +598,18 @@
|
|
|
528
598
|
view: 0,
|
|
529
599
|
});
|
|
530
600
|
const scrollLockOwner = {};
|
|
601
|
+
const wrapRef = ref<HTMLElement | null>(null);
|
|
602
|
+
// 本实例注册到 _tasks 的对象;同 id 被其它实例覆盖后不再写全局
|
|
603
|
+
let self: any = null;
|
|
604
|
+
const ownsRegistration = () => !!self && _tasks[props.id] === self;
|
|
605
|
+
|
|
606
|
+
let animTimer: ReturnType<typeof setTimeout> | undefined;
|
|
607
|
+
const clearAnimTimer = () => {
|
|
608
|
+
if (animTimer) {
|
|
609
|
+
clearTimeout(animTimer);
|
|
610
|
+
animTimer = undefined;
|
|
611
|
+
}
|
|
612
|
+
};
|
|
531
613
|
|
|
532
614
|
const layerStyle = computed(() => {
|
|
533
615
|
let style = '';
|
|
@@ -539,12 +621,12 @@
|
|
|
539
621
|
});
|
|
540
622
|
|
|
541
623
|
const syncTaskView = (view: number) => {
|
|
542
|
-
if (!
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
624
|
+
if (!self) return;
|
|
625
|
+
self.view = view;
|
|
626
|
+
self.openMode = state.openMode;
|
|
627
|
+
self.parkAfterHide = state.parkAfterHide;
|
|
628
|
+
self.participates = participatesInGroup(props.dark);
|
|
629
|
+
self.dark = props.dark;
|
|
548
630
|
};
|
|
549
631
|
|
|
550
632
|
const syncOwnDarkOccupancy = () => {
|
|
@@ -575,6 +657,7 @@
|
|
|
575
657
|
else releaseScrollLock(scrollLockOwner);
|
|
576
658
|
syncOwnDarkOccupancy();
|
|
577
659
|
state.immediate = false;
|
|
660
|
+
armAnimFallback();
|
|
578
661
|
if (participatesInGroup(props.dark) && props.id) {
|
|
579
662
|
nextTick(() => recomputeRecess(getAreaGroup(props.area)));
|
|
580
663
|
}
|
|
@@ -584,8 +667,30 @@
|
|
|
584
667
|
|
|
585
668
|
watch(
|
|
586
669
|
() => props.dark,
|
|
587
|
-
() => {
|
|
670
|
+
(dark, prevDark) => {
|
|
671
|
+
const was = participatesInGroup(prevDark);
|
|
672
|
+
const now = participatesInGroup(dark);
|
|
588
673
|
syncTaskView(state.view);
|
|
674
|
+
if (was !== now && env.client && props.id && ownsRegistration()) {
|
|
675
|
+
const group = getAreaGroup(props.area);
|
|
676
|
+
if (!now) {
|
|
677
|
+
// 退出分组:释放暗层占用并移出栈/队列;不恢复其下被 park 的弹层(不视为关闭)
|
|
678
|
+
removeFromQueue(group, props.id);
|
|
679
|
+
removeFromStack(group, props.id);
|
|
680
|
+
releaseGlobalDark(props.area, group, props.id);
|
|
681
|
+
state.coverDark = '';
|
|
682
|
+
state.recess = 0;
|
|
683
|
+
state.stackIndex = 0;
|
|
684
|
+
recomputeRecess(group);
|
|
685
|
+
scheduleFlushQueue(group);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
// 加入分组:已打开时补上栈位置,暗层占用由 syncOwnDarkOccupancy 接管
|
|
689
|
+
if (state.view > 0) {
|
|
690
|
+
ensureOnStack(group, props.id);
|
|
691
|
+
recomputeRecess(group);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
589
694
|
syncOwnDarkOccupancy();
|
|
590
695
|
},
|
|
591
696
|
);
|
|
@@ -593,7 +698,7 @@
|
|
|
593
698
|
watch(
|
|
594
699
|
() => props.escClose,
|
|
595
700
|
(escClose) => {
|
|
596
|
-
if (
|
|
701
|
+
if (self) self.escClose = escClose;
|
|
597
702
|
},
|
|
598
703
|
{ immediate: true },
|
|
599
704
|
);
|
|
@@ -601,7 +706,7 @@
|
|
|
601
706
|
watch(
|
|
602
707
|
() => props.darkClickClose,
|
|
603
708
|
(darkClickClose) => {
|
|
604
|
-
if (
|
|
709
|
+
if (self) self.darkClickClose = darkClickClose;
|
|
605
710
|
},
|
|
606
711
|
{ immediate: true },
|
|
607
712
|
);
|
|
@@ -612,7 +717,10 @@
|
|
|
612
717
|
hide: [],
|
|
613
718
|
open: [],
|
|
614
719
|
none: [],
|
|
720
|
+
cancel: [],
|
|
615
721
|
};
|
|
722
|
+
// 已发起显示但尚未完成(show 回调未执行)
|
|
723
|
+
let showPending = false;
|
|
616
724
|
|
|
617
725
|
const resolveMode = (params: any): LayerMode => {
|
|
618
726
|
const mode = params?.mode;
|
|
@@ -623,14 +731,28 @@
|
|
|
623
731
|
const bindOpenCallbacks = (params: any = {}, then = () => {}) => {
|
|
624
732
|
if (params?.on?.show) on.show.push(params.on.show);
|
|
625
733
|
if (params?.on?.hide) on.hide.push(params.on.hide);
|
|
734
|
+
if (params?.on?.cancel) on.cancel.push(params.on.cancel);
|
|
626
735
|
on.show.push(then);
|
|
627
736
|
};
|
|
628
737
|
|
|
629
|
-
|
|
630
|
-
|
|
738
|
+
// 回调执行前,LayerById(id).view 与分组栈都已更新为最终状态
|
|
739
|
+
const runCallbacks = (key: 'show' | 'hide' | 'none' | 'cancel') => {
|
|
740
|
+
const list = on[key].splice(0);
|
|
741
|
+
if (key === 'show') {
|
|
742
|
+
showPending = false;
|
|
743
|
+
on.cancel.length = 0;
|
|
744
|
+
}
|
|
745
|
+
list.forEach((fun: any) => {
|
|
631
746
|
if (isFunction(fun)) fun();
|
|
632
747
|
});
|
|
633
|
-
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
// 显示尚未完成就被关闭:丢弃 show 回调,改为触发 cancel
|
|
751
|
+
const cancelPendingShow = () => {
|
|
752
|
+
if (!showPending) return;
|
|
753
|
+
showPending = false;
|
|
754
|
+
on.show.length = 0;
|
|
755
|
+
runCallbacks('cancel');
|
|
634
756
|
};
|
|
635
757
|
|
|
636
758
|
const flushQueue = (group: AreaGroup) => {
|
|
@@ -639,6 +761,7 @@
|
|
|
639
761
|
if (!job) return;
|
|
640
762
|
const task = _tasks[job.id];
|
|
641
763
|
if (!task) {
|
|
764
|
+
runQueuedCancel(job);
|
|
642
765
|
flushQueue(group);
|
|
643
766
|
return;
|
|
644
767
|
}
|
|
@@ -666,7 +789,9 @@
|
|
|
666
789
|
const group = getAreaGroup(props.area);
|
|
667
790
|
removeFromStack(group, layerId);
|
|
668
791
|
const topId = group.stack[group.stack.length - 1];
|
|
669
|
-
|
|
792
|
+
// 恢复已停放或正在退往停放的下层(unpark 自行判断)
|
|
793
|
+
const top = topId ? _tasks[topId] : undefined;
|
|
794
|
+
if (top && (top.view === 4 || top.parkAfterHide)) top.unpark();
|
|
670
795
|
recomputeRecess(group);
|
|
671
796
|
scheduleFlushQueue(group);
|
|
672
797
|
};
|
|
@@ -726,16 +851,24 @@
|
|
|
726
851
|
state.parkAfterHide = false;
|
|
727
852
|
state.immediate = true;
|
|
728
853
|
state.view = 4;
|
|
854
|
+
syncTaskView(4);
|
|
729
855
|
return;
|
|
730
856
|
}
|
|
731
857
|
if (state.view === 2 || state.view === 3) {
|
|
732
858
|
state.parkAfterHide = true;
|
|
733
|
-
syncTaskView(state.view);
|
|
734
859
|
state.view = 1;
|
|
860
|
+
syncTaskView(1);
|
|
735
861
|
}
|
|
736
862
|
};
|
|
737
863
|
|
|
738
864
|
const unpark = (options: any = {}) => {
|
|
865
|
+
// 退场动画未结束(尚未进入停放):取消停放,重新显示
|
|
866
|
+
if (state.view === 1 && state.parkAfterHide) {
|
|
867
|
+
state.parkAfterHide = false;
|
|
868
|
+
state.view = 2;
|
|
869
|
+
syncTaskView(2);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
739
872
|
if (state.view !== 4) return;
|
|
740
873
|
state.parkAfterHide = false;
|
|
741
874
|
if (options.immediate) {
|
|
@@ -746,16 +879,38 @@
|
|
|
746
879
|
state.view = 2;
|
|
747
880
|
};
|
|
748
881
|
|
|
882
|
+
// animationend 兜底:am 无对应样式、动画被禁用或事件丢失时,按动画时长推进 1→0 / 2→3
|
|
883
|
+
function armAnimFallback() {
|
|
884
|
+
clearAnimTimer();
|
|
885
|
+
const view = state.view;
|
|
886
|
+
if (!env.client || (view !== 1 && view !== 2)) return;
|
|
887
|
+
nextTick(() => {
|
|
888
|
+
if (state.view !== view) return;
|
|
889
|
+
clearAnimTimer();
|
|
890
|
+
const wait = getAnimationWait(wrapRef.value);
|
|
891
|
+
animTimer = setTimeout(
|
|
892
|
+
() => {
|
|
893
|
+
animTimer = undefined;
|
|
894
|
+
if (state.view === view) anim();
|
|
895
|
+
},
|
|
896
|
+
wait > 0 ? wait + ANIMATION_FALLBACK_SLACK : 0,
|
|
897
|
+
);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
749
901
|
// 注入参数
|
|
750
902
|
const init = (params: any = {}) => {
|
|
751
|
-
|
|
903
|
+
self.params = params;
|
|
752
904
|
if (params?.on?.show) {
|
|
753
905
|
on.show.push(params.on.show);
|
|
754
906
|
}
|
|
755
907
|
if (params?.on?.hide) {
|
|
756
908
|
on.hide.push(params.on.hide);
|
|
757
909
|
}
|
|
758
|
-
|
|
910
|
+
if (params?.on?.cancel) {
|
|
911
|
+
on.cancel.push(params.on.cancel);
|
|
912
|
+
}
|
|
913
|
+
return self;
|
|
759
914
|
};
|
|
760
915
|
|
|
761
916
|
// 显示弹层
|
|
@@ -763,23 +918,35 @@
|
|
|
763
918
|
if (tryEnqueue('show', params, then)) return;
|
|
764
919
|
if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
|
|
765
920
|
bindOpenCallbacks(params, then);
|
|
921
|
+
showPending = true;
|
|
766
922
|
state.openMode = resolveMode(params);
|
|
767
923
|
applyIncomingRelation(state.openMode, params, false);
|
|
768
924
|
state.parkAfterHide = false;
|
|
769
925
|
state.view = 2;
|
|
926
|
+
// 立即同步全局状态,同一调用栈内随后的 queue 请求才能看到本层在显示
|
|
927
|
+
syncTaskView(2);
|
|
770
928
|
closeReplacedLayers(state.openMode, params, false);
|
|
771
929
|
syncGroupPresentation();
|
|
772
930
|
};
|
|
773
931
|
|
|
774
932
|
// 隐藏弹层
|
|
775
933
|
const hide = (_params: any = {}, then = () => {}) => {
|
|
934
|
+
// 先撤销排队中的显示请求(触发其 cancel),无论当前是否在显示
|
|
935
|
+
if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
|
|
936
|
+
// 已关闭:目标状态已达成,直接回调
|
|
937
|
+
if (state.view === 0) {
|
|
938
|
+
if (isFunction(then)) then();
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
cancelPendingShow();
|
|
776
942
|
if (state.view === 4) {
|
|
777
943
|
on.hide.push(then);
|
|
778
944
|
state.parkAfterHide = false;
|
|
779
945
|
state.immediate = true;
|
|
780
946
|
state.view = 0;
|
|
781
|
-
|
|
947
|
+
syncTaskView(0);
|
|
782
948
|
onGroupLayerClosed(id);
|
|
949
|
+
runCallbacks('hide');
|
|
783
950
|
return;
|
|
784
951
|
}
|
|
785
952
|
if (state.view === 2 || state.view === 3) {
|
|
@@ -787,7 +954,12 @@
|
|
|
787
954
|
syncTaskView(state.view);
|
|
788
955
|
state.view = 1;
|
|
789
956
|
on.hide.push(then);
|
|
957
|
+
return;
|
|
790
958
|
}
|
|
959
|
+
// 正在关闭:回调并入本次关闭;若是 park 的退场过渡,改为真正关闭
|
|
960
|
+
state.parkAfterHide = false;
|
|
961
|
+
syncTaskView(state.view);
|
|
962
|
+
on.hide.push(then);
|
|
791
963
|
};
|
|
792
964
|
|
|
793
965
|
// 打开弹层(没有动画过程)
|
|
@@ -800,6 +972,7 @@
|
|
|
800
972
|
state.parkAfterHide = false;
|
|
801
973
|
state.immediate = true;
|
|
802
974
|
state.view = 3;
|
|
975
|
+
syncTaskView(3);
|
|
803
976
|
closeReplacedLayers(state.openMode, params, true);
|
|
804
977
|
syncGroupPresentation();
|
|
805
978
|
runCallbacks('show');
|
|
@@ -808,18 +981,34 @@
|
|
|
808
981
|
const none = (_params: any = {}, then = () => {}) => {
|
|
809
982
|
const wasOpen = state.view > 0;
|
|
810
983
|
if (participatesInGroup(props.dark) && props.id) removeFromQueue(getAreaGroup(props.area), props.id);
|
|
984
|
+
cancelPendingShow();
|
|
811
985
|
state.parkAfterHide = false;
|
|
812
986
|
state.immediate = true;
|
|
813
987
|
state.view = 0;
|
|
988
|
+
syncTaskView(0);
|
|
814
989
|
on.none.push(then);
|
|
990
|
+
if (wasOpen) onGroupLayerClosed(id);
|
|
815
991
|
// 立即执行回调,因为没有动画
|
|
816
992
|
runCallbacks('none');
|
|
817
|
-
if (wasOpen) onGroupLayerClosed(id);
|
|
818
993
|
};
|
|
819
994
|
|
|
820
|
-
//
|
|
821
|
-
|
|
822
|
-
|
|
995
|
+
// 点暗层关闭:按下与松开都必须落在暗层(wrap 本身)上。
|
|
996
|
+
// 按下和松开分处弹窗内外时(如拖选输入框文本拖出弹窗),click 会派发到共同祖先 wrap,不能据此关闭。
|
|
997
|
+
let pressedOnBackdrop = false;
|
|
998
|
+
let releasedOnBackdrop = false;
|
|
999
|
+
const _sys_press = (ev: PointerEvent) => {
|
|
1000
|
+
pressedOnBackdrop = ev.target === ev.currentTarget;
|
|
1001
|
+
releasedOnBackdrop = false;
|
|
1002
|
+
};
|
|
1003
|
+
const _sys_release = (ev: PointerEvent) => {
|
|
1004
|
+
releasedOnBackdrop = ev.target === ev.currentTarget;
|
|
1005
|
+
};
|
|
1006
|
+
const _sys_hide = (ev: MouseEvent) => {
|
|
1007
|
+
const fromBackdrop = pressedOnBackdrop && releasedOnBackdrop;
|
|
1008
|
+
pressedOnBackdrop = false;
|
|
1009
|
+
releasedOnBackdrop = false;
|
|
1010
|
+
if (!fromBackdrop || ev.target !== ev.currentTarget) return;
|
|
1011
|
+
if (props.darkClickClose && state.view === 3) {
|
|
823
1012
|
hide();
|
|
824
1013
|
}
|
|
825
1014
|
};
|
|
@@ -827,6 +1016,7 @@
|
|
|
827
1016
|
// 监听动画
|
|
828
1017
|
const anim = (ev?: AnimationEvent) => {
|
|
829
1018
|
if (ev && ev.target !== ev.currentTarget) return;
|
|
1019
|
+
clearAnimTimer();
|
|
830
1020
|
if (state.view === 1) {
|
|
831
1021
|
if (state.parkAfterHide) {
|
|
832
1022
|
state.parkAfterHide = false;
|
|
@@ -834,11 +1024,14 @@
|
|
|
834
1024
|
return;
|
|
835
1025
|
}
|
|
836
1026
|
state.view = 0;
|
|
837
|
-
|
|
1027
|
+
syncTaskView(0);
|
|
838
1028
|
onGroupLayerClosed(id);
|
|
1029
|
+
runCallbacks('hide');
|
|
1030
|
+
return;
|
|
839
1031
|
}
|
|
840
1032
|
if (state.view === 2) {
|
|
841
1033
|
state.view = 3;
|
|
1034
|
+
syncTaskView(3);
|
|
842
1035
|
runCallbacks('show');
|
|
843
1036
|
}
|
|
844
1037
|
};
|
|
@@ -849,7 +1042,7 @@
|
|
|
849
1042
|
console.error('Layer 有重复 id', id);
|
|
850
1043
|
}
|
|
851
1044
|
if (id) {
|
|
852
|
-
|
|
1045
|
+
self = {
|
|
853
1046
|
dark: props.dark,
|
|
854
1047
|
darkClickClose: props.darkClickClose,
|
|
855
1048
|
escClose: props.escClose,
|
|
@@ -868,6 +1061,7 @@
|
|
|
868
1061
|
unpark,
|
|
869
1062
|
view: state.view,
|
|
870
1063
|
};
|
|
1064
|
+
_tasks[id] = self;
|
|
871
1065
|
ensureEscapeListener();
|
|
872
1066
|
} else {
|
|
873
1067
|
console.error('Layer 必须填写全局统一的 id');
|
|
@@ -875,7 +1069,11 @@
|
|
|
875
1069
|
|
|
876
1070
|
// 卸载弹层
|
|
877
1071
|
onBeforeUnmount(() => {
|
|
878
|
-
|
|
1072
|
+
clearAnimTimer();
|
|
1073
|
+
cancelPendingShow();
|
|
1074
|
+
// 同 id 已被新实例接管(路由过渡/HMR 时新实例先挂载):只释放本实例私有资源,不动全局注册与按 id 记录的栈/队列/暗层
|
|
1075
|
+
const owns = ownsRegistration();
|
|
1076
|
+
if (owns && participatesInGroup(props.dark) && id) {
|
|
879
1077
|
const group = getAreaGroup(props.area);
|
|
880
1078
|
removeFromQueue(group, id);
|
|
881
1079
|
if (state.view > 0) {
|
|
@@ -885,10 +1083,14 @@
|
|
|
885
1083
|
} else {
|
|
886
1084
|
removeFromStack(group, id);
|
|
887
1085
|
}
|
|
1086
|
+
// 卸载后 view watcher 可能不再执行,显式释放暗层占用
|
|
1087
|
+
releaseGlobalDark(props.area, group, id, true);
|
|
1088
|
+
}
|
|
1089
|
+
if (owns) {
|
|
1090
|
+
removeEscapeStack(id);
|
|
1091
|
+
delete _tasks[id];
|
|
888
1092
|
}
|
|
889
|
-
removeEscapeStack(id);
|
|
890
1093
|
releaseScrollLock(scrollLockOwner);
|
|
891
|
-
delete _tasks[id];
|
|
892
1094
|
});
|
|
893
1095
|
</script>
|
|
894
1096
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div ui-date="@a" class="fekit-date-view pr" :class="`${frame ? 'b-solid b-line b-xs r-sm' : ''}`" ui-flex="col xy" style="width: 22em">
|
|
2
|
+
<div ui-date="@a" class="mob:min-w-full fekit-date-view pr" :class="`${frame ? 'b-solid b-line b-xs r-sm' : ''}`" ui-flex="col xy" style="width: 22em">
|
|
3
3
|
<div ui-date-head="" class="flex-fixed nx-ms mt-ss ny-sm b-solid b-line" ui-flex="row xm">
|
|
4
4
|
<div ui-flex="row lm" class="o-lm">
|
|
5
5
|
<div ui-btn="@a s none :square" @click="prev()">
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</Input>
|
|
14
14
|
</slot>
|
|
15
15
|
<template #tips>
|
|
16
|
-
<div v-if="search" class="ny-sm nx-sl">
|
|
16
|
+
<div v-if="search" class="ny-sm nx-sl mob:w-full">
|
|
17
17
|
<Input v-model="state.search" :theme="theme" sz="s">
|
|
18
18
|
<template #suffix>
|
|
19
19
|
<i ui-form-search="" class="co-note"></i>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<template #suffix><i ui-form-select=""></i></template>
|
|
10
10
|
</Input>
|
|
11
11
|
<template #tips v-if="!disabled && !readonly">
|
|
12
|
-
<div v-if="search" class="nb-sm nx-sl">
|
|
12
|
+
<div v-if="search" class="nb-sm nx-sl mob:w-full">
|
|
13
13
|
<Input v-model="state.search" :theme="theme" sz="s">
|
|
14
14
|
<template #suffix>
|
|
15
15
|
<i ui-form-search="" class="co-note"></i>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
</Input>
|
|
8
8
|
</slot>
|
|
9
9
|
<template #tips>
|
|
10
|
-
<div v-if="filterable" class="ny-sm nx-sl" style="height: 3em">
|
|
10
|
+
<div v-if="filterable" class="ny-sm nx-sl mob:w-full" style="height: 3em">
|
|
11
11
|
<div :ui-form="`${theme} type:input sz:s :block`">
|
|
12
12
|
<input type="text" :placeholder="props.placeholder" v-model="state.search" />
|
|
13
13
|
<div ui-form-suffix="">
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<template #suffix><i ui-form-select=""></i></template>
|
|
6
6
|
</Input>
|
|
7
7
|
<template #tips>
|
|
8
|
-
<div v-if="filterable" class="ny-sm nx-sl" style="height: 3em">
|
|
8
|
+
<div v-if="filterable" class="ny-sm nx-sl mob:w-full" style="height: 3em">
|
|
9
9
|
<div :ui-form="`${theme} type:input sz:s :block`">
|
|
10
10
|
<input type="text" :placeholder="props.placeholder" v-model="state.search" />
|
|
11
11
|
<div ui-form-suffix="">
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<template #suffix><i ui-form-select=""></i></template>
|
|
6
6
|
</Input>
|
|
7
7
|
<template #tips>
|
|
8
|
-
<div class="ny-sm nx-sl" style="height: 3em">
|
|
8
|
+
<div class="ny-sm nx-sl mob:w-full" style="height: 3em">
|
|
9
9
|
<div :ui-form="`${theme} type:input sz:s :block`">
|
|
10
10
|
<input type="text" :placeholder="props.placeholder" v-model="state.search" />
|
|
11
11
|
<div ui-form-suffix="">
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<template #suffix><i ui-form-select=""></i></template>
|
|
10
10
|
</Input>
|
|
11
11
|
<template #tips v-if="!readonly || !disabled">
|
|
12
|
-
<div v-if="search" class="ny-sm nx-sl">
|
|
12
|
+
<div v-if="search" class="ny-sm nx-sl mob:w-full">
|
|
13
13
|
<Input v-model="state.search" :theme="theme" sz="s">
|
|
14
14
|
<template #suffix>
|
|
15
15
|
<i ui-form-search="" class="co-note"></i>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
</template>
|
|
13
13
|
</Input>
|
|
14
14
|
<template #tips v-if="!readonly && !disabled">
|
|
15
|
-
<div v-if="search" class="ny-sm nx-sl">
|
|
15
|
+
<div v-if="search" class="ny-sm nx-sl mob:w-full">
|
|
16
16
|
<Input v-model="state.search" :theme="theme" :sz="sz">
|
|
17
17
|
<template #suffix>
|
|
18
18
|
<i ui-form-search="" class="co-note"></i>
|
|
@@ -60,6 +60,9 @@
|
|
|
60
60
|
</div>
|
|
61
61
|
</li>
|
|
62
62
|
</ul>
|
|
63
|
+
<div v-if="$slots.footer" class="nx-sm ny-ss">
|
|
64
|
+
<slot name="footer" :close="closeDropdown"></slot>
|
|
65
|
+
</div>
|
|
63
66
|
</template>
|
|
64
67
|
</IFollowView>
|
|
65
68
|
</div>
|
|
@@ -72,6 +75,7 @@
|
|
|
72
75
|
import { matchOptionSearch } from './optionSearch';
|
|
73
76
|
const ifollow: any = ref(null);
|
|
74
77
|
const customInputRef: any = ref(null);
|
|
78
|
+
const closeDropdown = () => ifollow.value?.hide();
|
|
75
79
|
|
|
76
80
|
const emits = defineEmits(['update:modelValue', 'change', 'onshow', 'onhide', 'update:other']);
|
|
77
81
|
|
|
@@ -187,6 +191,8 @@
|
|
|
187
191
|
|
|
188
192
|
// 自定义输入值
|
|
189
193
|
const customInputValue = ref('');
|
|
194
|
+
// 选择其他后立即显示输入框,不依赖已有自定义值。
|
|
195
|
+
const typing = ref(false);
|
|
190
196
|
|
|
191
197
|
const state = reactive({
|
|
192
198
|
show: 0,
|
|
@@ -194,9 +200,10 @@
|
|
|
194
200
|
isInputting: computed(() => {
|
|
195
201
|
// 如果配置了"其他"选项且当前选中的是"其他"
|
|
196
202
|
if (props.other) {
|
|
203
|
+
if (typing.value) return true;
|
|
197
204
|
if (props.other.field) {
|
|
198
|
-
// 情况2
|
|
199
|
-
return props.modelValue === props.other.value
|
|
205
|
+
// 情况2:独立字段可以尚未填写,主值选中其他即显示输入框。
|
|
206
|
+
return props.modelValue === props.other.value;
|
|
200
207
|
} else {
|
|
201
208
|
// 情况1:无独立字段,当值不在枚举中时显示输入框
|
|
202
209
|
if (props.modelValue && props.modelValue !== '') {
|
|
@@ -323,33 +330,36 @@
|
|
|
323
330
|
change('');
|
|
324
331
|
}
|
|
325
332
|
customInputValue.value = '';
|
|
333
|
+
typing.value = true;
|
|
326
334
|
nextTick(() => {
|
|
327
|
-
|
|
335
|
+
// Input 未暴露 focus 方法,聚焦其原生输入框。
|
|
336
|
+
customInputRef.value?.$el?.querySelector?.('input')?.focus();
|
|
328
337
|
});
|
|
329
338
|
} else {
|
|
339
|
+
typing.value = false;
|
|
330
340
|
change(item.value);
|
|
331
|
-
|
|
341
|
+
closeDropdown();
|
|
332
342
|
}
|
|
333
343
|
};
|
|
334
344
|
|
|
335
|
-
//
|
|
345
|
+
// 确认自定义输入(回车与失焦都会触发,需保持幂等,避免重复派发 change)
|
|
336
346
|
const confirmCustomInput = () => {
|
|
337
|
-
|
|
347
|
+
const text = customInputValue.value.trim();
|
|
348
|
+
if (text) {
|
|
338
349
|
if (props.other?.field) {
|
|
339
350
|
// 如果指定了 field,则主字段存储选项值,field 存储自定义输入
|
|
340
|
-
change(props.other.value);
|
|
341
|
-
emits('update:other',
|
|
342
|
-
} else {
|
|
351
|
+
if (props.modelValue !== props.other.value) change(props.other.value);
|
|
352
|
+
if (props.otherValue !== text) emits('update:other', text);
|
|
353
|
+
} else if (props.modelValue !== text) {
|
|
343
354
|
// 如果没有指定 field,则直接在主字段存储自定义输入
|
|
344
|
-
change(
|
|
345
|
-
}
|
|
346
|
-
ifollow.value.cancel();
|
|
347
|
-
} else {
|
|
348
|
-
// 如果输入为空,清空值
|
|
349
|
-
change(null);
|
|
350
|
-
if (props.other?.field) {
|
|
351
|
-
emits('update:other', '');
|
|
355
|
+
change(text);
|
|
352
356
|
}
|
|
357
|
+
closeDropdown();
|
|
358
|
+
} else if (!props.other?.field) {
|
|
359
|
+
// 无独立字段时空输入即无值;有独立字段时保留已选的“其他”,等待补填。
|
|
360
|
+
if (props.modelValue != null && props.modelValue !== '') change(null);
|
|
361
|
+
} else if (props.otherValue) {
|
|
362
|
+
emits('update:other', '');
|
|
353
363
|
}
|
|
354
364
|
};
|
|
355
365
|
|
|
@@ -378,8 +388,9 @@
|
|
|
378
388
|
};
|
|
379
389
|
// 清空
|
|
380
390
|
const remove = () => {
|
|
391
|
+
typing.value = false;
|
|
381
392
|
change(null);
|
|
382
|
-
|
|
393
|
+
closeDropdown();
|
|
383
394
|
};
|
|
384
395
|
// 取消
|
|
385
396
|
const cancel = () => {
|
|
@@ -392,6 +403,7 @@
|
|
|
392
403
|
// 折叠
|
|
393
404
|
const onhide = () => {
|
|
394
405
|
state.search = '';
|
|
406
|
+
typing.value = false;
|
|
395
407
|
emits('onhide', state.selectd);
|
|
396
408
|
};
|
|
397
409
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<template #suffix><i ui-form-select=""></i></template>
|
|
10
10
|
</Input>
|
|
11
11
|
<template #tips v-if="!readonly || !disabled">
|
|
12
|
-
<div v-if="search" class="nb-sm nx-sl">
|
|
12
|
+
<div v-if="search" class="nb-sm nx-sl mob:w-full">
|
|
13
13
|
<Input v-model="state.search" :theme="theme" sz="s">
|
|
14
14
|
<template #suffix>
|
|
15
15
|
<i ui-form-search="" class="co-note"></i>
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
<FieldMessage :col="col" :mode="mode" />
|
|
93
93
|
</div>
|
|
94
94
|
|
|
95
|
-
<FormRender v-else :col="col" :mode="mode" :entry="resolveEntry(col)" :interacted="touched" :ctx="context()" :custom="(
|
|
96
|
-
<slot v-if="
|
|
95
|
+
<FormRender v-else :col="col" :mode="mode" :entry="resolveEntry(col)" :interacted="touched" :ctx="context()" :custom="(mode === 'view' && hasSlot(`${col.field}-value`)) || !resolveEntry(col)">
|
|
96
|
+
<slot v-if="mode === 'view' && hasSlot(`${col.field}-value`)" :name="`${col.field}-value`" v-bind="fieldScope(col)"></slot>
|
|
97
97
|
<template v-else>
|
|
98
98
|
<slot v-if="col.slot" :name="col.slot" v-bind="fieldScope(col)"></slot>
|
|
99
99
|
<slot v-else-if="col.model[0]" :name="col.model[0]" v-bind="fieldScope(col)"></slot>
|
|
@@ -591,7 +591,7 @@
|
|
|
591
591
|
type: Boolean,
|
|
592
592
|
default: false,
|
|
593
593
|
},
|
|
594
|
-
// edit 编辑 | view 查看
|
|
594
|
+
// edit 编辑 | read 禁用控件 | view 查看
|
|
595
595
|
mode: {
|
|
596
596
|
type: String,
|
|
597
597
|
default: 'edit',
|
|
@@ -693,7 +693,7 @@
|
|
|
693
693
|
shared.revision++;
|
|
694
694
|
if (!props.root) shared.dataVersion.value++;
|
|
695
695
|
}, { deep: true, flush: 'sync' });
|
|
696
|
-
const editable = computed(() => props.mode !== 'view');
|
|
696
|
+
const editable = computed(() => props.mode !== 'view' && props.mode !== 'read');
|
|
697
697
|
// 调用过 validate 后所有字段展示校验状态
|
|
698
698
|
const touched = computed(() => props.interacted || shared.state.validated);
|
|
699
699
|
|
|
@@ -252,7 +252,7 @@
|
|
|
252
252
|
};
|
|
253
253
|
// 内置控件和插槽使用相同写回规则,查看模式不接受写入。
|
|
254
254
|
export const writeValue = (col: any, value: any, ctx: FormContext) => {
|
|
255
|
-
if (!col.field || ctx.mode === 'view') return;
|
|
255
|
+
if (!col.field || ctx.mode === 'view' || ctx.mode === 'read') return;
|
|
256
256
|
const binding = col.value;
|
|
257
257
|
col._data[col.field] = valueBinding(binding) && typeof binding.set === 'function' ? binding.set(value, ctx) : value;
|
|
258
258
|
};
|
|
@@ -281,7 +281,7 @@
|
|
|
281
281
|
// native:控件自己用 ui-form-tips 显示错误,这里只留隐藏文本给读屏关联
|
|
282
282
|
props: ['col', 'mode', 'native'],
|
|
283
283
|
// 没有内容时返回空数组:客户端渲染不留占位注释
|
|
284
|
-
setup: (props) => () => props.mode !== 'view' ? [
|
|
284
|
+
setup: (props) => () => props.mode !== 'view' && props.mode !== 'read' ? [
|
|
285
285
|
props.col.model?.[1]?.verify && props.col._checked
|
|
286
286
|
? h('i', { 'ui-form-verify': props.col._error ? 'no' : isEmpty(props.col._data?.[props.col.field]) ? '' : 'ok', 'aria-hidden': 'true' }) : null,
|
|
287
287
|
props.col._error ? h('p', { id: `${props.col._fid}-error`, 'data-field-error': props.col.field,
|
|
@@ -466,6 +466,7 @@
|
|
|
466
466
|
return rest;
|
|
467
467
|
});
|
|
468
468
|
const update = (value: any) => {
|
|
469
|
+
if (props.mode === 'view' || props.mode === 'read') return;
|
|
469
470
|
props.col._touched = true;
|
|
470
471
|
writeValue(props.col, value, props.ctx);
|
|
471
472
|
events.value['update:modelValue']?.(value);
|
|
@@ -483,7 +484,7 @@
|
|
|
483
484
|
const controlProps = computed(() => {
|
|
484
485
|
const { col, entry, mode } = props;
|
|
485
486
|
const options = col.model?.[1] || {};
|
|
486
|
-
const error = mode !== 'view' && nativeTips.value ? col._error : null;
|
|
487
|
+
const error = mode !== 'view' && mode !== 'read' && nativeTips.value ? col._error : null;
|
|
487
488
|
const merged: Record<string, any> = {
|
|
488
489
|
interacted: props.interacted,
|
|
489
490
|
options: entry?.options ? col.enums : undefined,
|
|
@@ -494,7 +495,8 @@
|
|
|
494
495
|
tips: error?.msg || options.tips,
|
|
495
496
|
...(error ? { interacted: true } : {}),
|
|
496
497
|
readonly: mode === 'view' ? true : options.readonly,
|
|
497
|
-
|
|
498
|
+
// read 保留控件,统一使用 disabled 呈现不可编辑状态。
|
|
499
|
+
disabled: mode === 'view' || mode === 'read' ? true : options.disabled,
|
|
498
500
|
};
|
|
499
501
|
// 没有声明 props 的自定义注册控件可能通过 $attrs 读取这些参数,保持透传。
|
|
500
502
|
formOnly.forEach((key) => { if (!declaresOf(entry, key)) delete merged[key]; });
|
|
@@ -516,7 +518,7 @@
|
|
|
516
518
|
const el = root.value;
|
|
517
519
|
// 非 DOM 渲染环境(SSR、自定义渲染器)没有查询接口
|
|
518
520
|
if (!el || typeof el.querySelectorAll !== 'function') return;
|
|
519
|
-
const errorId = props.col._error && props.mode !== 'view' ? `${props.col._fid}-error` : '';
|
|
521
|
+
const errorId = props.col._error && props.mode !== 'view' && props.mode !== 'read' ? `${props.col._fid}-error` : '';
|
|
520
522
|
el.querySelectorAll<HTMLElement>(inputs).forEach((node) => {
|
|
521
523
|
if (errorId) {
|
|
522
524
|
node.setAttribute('aria-invalid', 'true');
|