@veltra/desktop 1.7.13 → 1.7.16
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 +9 -3
- package/dist/components/batch-edit/batch-edit-form.js.map +1 -1
- package/dist/components/batch-edit/batch-edit-list.js +2 -1
- package/dist/components/batch-edit/batch-edit-list.js.map +1 -1
- package/dist/components/batch-edit/batch-edit.js +57 -30
- package/dist/components/batch-edit/batch-edit.js.map +1 -1
- package/dist/components/batch-edit/style.d.ts +1 -0
- package/dist/components/batch-edit/style.js +1 -0
- package/dist/components/batch-edit/style2.css +14 -0
- package/dist/components/batch-edit/use-dialog-effect.js +20 -0
- package/dist/components/batch-edit/use-dialog-effect.js.map +1 -0
- package/dist/components/batch-edit/use-edit-state.js +22 -3
- package/dist/components/batch-edit/use-edit-state.js.map +1 -1
- package/dist/components/batch-edit/use-handlers.js +5 -0
- package/dist/components/batch-edit/use-handlers.js.map +1 -1
- package/dist/components/batch-edit/use-shortcut-key.js +5 -2
- package/dist/components/batch-edit/use-shortcut-key.js.map +1 -1
- package/dist/components/button/button.js +2 -2
- package/dist/components/button/button.js.map +1 -1
- package/dist/components/file-viewer/file-viewer.js +21 -14
- package/dist/components/file-viewer/file-viewer.js.map +1 -1
- package/dist/components/file-viewer/helper.js +2 -0
- package/dist/components/file-viewer/helper.js.map +1 -1
- package/dist/components/file-viewer/previewers/ofd-previewer.js +196 -0
- package/dist/components/file-viewer/previewers/ofd-previewer.js.map +1 -0
- package/dist/components/file-viewer/style2.css +27 -0
- package/dist/components/form-item/style2.css +17 -2
- package/dist/components/table-editor/style2.css +48 -5
- package/dist/components/table-editor/table-editor.js +228 -63
- package/dist/components/table-editor/table-editor.js.map +1 -1
- package/dist/components/tip/style2.css +63 -0
- package/dist/components/tip/tip.js +6 -1
- package/dist/components/tip/tip.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/packages/ofd-core/dist/color.js +69 -0
- package/dist/packages/ofd-core/dist/color.js.map +1 -0
- package/dist/packages/ofd-core/dist/ctm.js +55 -0
- package/dist/packages/ofd-core/dist/ctm.js.map +1 -0
- package/dist/packages/ofd-core/dist/error.js +14 -0
- package/dist/packages/ofd-core/dist/error.js.map +1 -0
- package/dist/packages/ofd-core/dist/font.js +384 -0
- package/dist/packages/ofd-core/dist/font.js.map +1 -0
- package/dist/packages/ofd-core/dist/index.js +6 -0
- package/dist/packages/ofd-core/dist/number.js +13 -0
- package/dist/packages/ofd-core/dist/number.js.map +1 -0
- package/dist/packages/ofd-core/dist/page-objects.js +181 -0
- package/dist/packages/ofd-core/dist/page-objects.js.map +1 -0
- package/dist/packages/ofd-core/dist/parse.js +65 -0
- package/dist/packages/ofd-core/dist/parse.js.map +1 -0
- package/dist/packages/ofd-core/dist/render.js +361 -0
- package/dist/packages/ofd-core/dist/render.js.map +1 -0
- package/dist/packages/ofd-core/dist/xml.js +199 -0
- package/dist/packages/ofd-core/dist/xml.js.map +1 -0
- package/dist/packages/ofd-core/dist/zip.js +93 -0
- package/dist/packages/ofd-core/dist/zip.js.map +1 -0
- package/dist/style.d.ts +1 -1
- package/dist/style.js +1 -1
- package/dist/types/batch-edit.d.ts +6 -0
- package/dist/types/file-viewer.d.ts +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/table-editor.d.ts +28 -7
- package/dist/types/tip.d.ts +5 -1
- package/package.json +8 -7
- package/src/components/batch-edit/__test__/batch-edit.test.ts +191 -4
- package/src/components/batch-edit/batch-edit-form.vue +17 -3
- package/src/components/batch-edit/batch-edit-list.vue +2 -1
- package/src/components/batch-edit/batch-edit.vue +22 -4
- package/src/components/batch-edit/style.scss +20 -0
- package/src/components/batch-edit/style.ts +1 -0
- package/src/components/batch-edit/use-dialog-effect.ts +9 -3
- package/src/components/batch-edit/use-edit-state.ts +32 -4
- package/src/components/batch-edit/use-handlers.ts +10 -0
- package/src/components/batch-edit/use-shortcut-key.ts +6 -2
- package/src/components/button/button.vue +3 -2
- package/src/components/file-viewer/__test__/helper.test.ts +13 -0
- package/src/components/file-viewer/file-viewer.vue +23 -16
- package/src/components/file-viewer/helper.ts +2 -0
- package/src/components/file-viewer/previewers/ofd-previewer.vue +216 -0
- package/src/components/file-viewer/style.scss +33 -0
- package/src/components/form-item/style.scss +11 -1
- package/src/components/table-editor/__test__/table-editor.test.ts +479 -0
- package/src/components/table-editor/style.scss +57 -5
- package/src/components/table-editor/table-editor.vue +286 -38
- package/src/components/tip/style.scss +15 -0
- package/src/components/tip/tip.vue +1 -1
- package/src/types/batch-edit.ts +6 -0
- package/src/types/file-viewer.ts +1 -1
- package/src/types/table-editor.ts +29 -7
- package/src/types/tip.ts +6 -1
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createApp, h, nextTick, shallowRef, ref, type Ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import type { TableColumn, TableEditorColumn } from '../../../../types'
|
|
5
|
+
import { UInput } from '../../input'
|
|
6
|
+
import UTableEditor from '../table-editor.vue'
|
|
7
|
+
|
|
8
|
+
const columns: TableColumn[] = [
|
|
9
|
+
{ key: 'name', name: '姓名' },
|
|
10
|
+
{ key: 'age', name: '年龄' },
|
|
11
|
+
{ key: 'city', name: '城市' }
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
const ruleColumns: TableEditorColumn[] = [
|
|
15
|
+
{ key: 'name', name: '姓名', rules: { required: true } },
|
|
16
|
+
{ key: 'age', name: '年龄', rules: { min: 0 } },
|
|
17
|
+
{ key: 'city', name: '城市' }
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const rows = [
|
|
21
|
+
{ name: 'Alice', age: 18, city: '杭州' },
|
|
22
|
+
{ name: 'Bob', age: 20, city: '上海' }
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const click = (el: Element) => el.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
26
|
+
|
|
27
|
+
/** 编辑列插槽工厂:原生 input 展开插槽 model(含 change 校验钩子)并绑定值 */
|
|
28
|
+
const renderInput = (testId: string) => (scope: any) =>
|
|
29
|
+
h('input', {
|
|
30
|
+
...scope.model,
|
|
31
|
+
'data-test': testId,
|
|
32
|
+
value: scope.model.modelValue ?? '',
|
|
33
|
+
onInput: (e: Event) => scope.model['onUpdate:modelValue']((e.target as HTMLInputElement).value)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const renderNameInput = renderInput('name-input')
|
|
37
|
+
const renderAgeInput = renderInput('age-input')
|
|
38
|
+
|
|
39
|
+
function mountTableEditor(
|
|
40
|
+
slots: Record<string, any> = {},
|
|
41
|
+
data: Record<string, any>[] = rows,
|
|
42
|
+
tableColumns: TableEditorColumn[] = columns,
|
|
43
|
+
extraProps: Record<string, any> = {}
|
|
44
|
+
) {
|
|
45
|
+
const host = document.createElement('div')
|
|
46
|
+
document.body.appendChild(host)
|
|
47
|
+
|
|
48
|
+
const model = ref(data) as Ref<Record<string, any>[]>
|
|
49
|
+
const editorRef = shallowRef<any>()
|
|
50
|
+
const emitted: Record<string, any>[][] = []
|
|
51
|
+
|
|
52
|
+
const app = createApp({
|
|
53
|
+
render() {
|
|
54
|
+
return h(
|
|
55
|
+
UTableEditor,
|
|
56
|
+
{
|
|
57
|
+
ref: editorRef,
|
|
58
|
+
columns: tableColumns,
|
|
59
|
+
modelValue: model.value,
|
|
60
|
+
...extraProps,
|
|
61
|
+
'onUpdate:modelValue': (value: Record<string, any>[]) => {
|
|
62
|
+
model.value = value
|
|
63
|
+
emitted.push(value)
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
slots
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
app.mount(host)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
host,
|
|
75
|
+
emitted,
|
|
76
|
+
getEditor: () => editorRef.value,
|
|
77
|
+
setModel: (value: Record<string, any>[]) => {
|
|
78
|
+
model.value = value
|
|
79
|
+
},
|
|
80
|
+
getDataRows: () => [...host.querySelectorAll('tr.u-table__row')],
|
|
81
|
+
unmount() {
|
|
82
|
+
app.unmount()
|
|
83
|
+
host.remove()
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 按文本查找表头单元格 */
|
|
89
|
+
const findHeader = (host: Element, text: string) =>
|
|
90
|
+
[...host.querySelectorAll('th')].find((th) => th.textContent?.includes(text))
|
|
91
|
+
|
|
92
|
+
describe('UTableEditor 行内编辑', () => {
|
|
93
|
+
it('初始渲染即挂载编辑控件,未声明插槽的列渲染字段原始值', async () => {
|
|
94
|
+
const { host, getDataRows, unmount } = mountTableEditor({
|
|
95
|
+
'column:name': renderNameInput,
|
|
96
|
+
'column:age': renderAgeInput
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await nextTick()
|
|
101
|
+
// 每个数据行的编辑控件初始即挂载
|
|
102
|
+
expect(host.querySelectorAll('[data-test="name-input"]')).toHaveLength(2)
|
|
103
|
+
expect(host.querySelectorAll('[data-test="age-input"]')).toHaveLength(2)
|
|
104
|
+
|
|
105
|
+
const [row0] = getDataRows()
|
|
106
|
+
// 未声明 #column:key 的列渲染字段原始值
|
|
107
|
+
expect(row0.textContent).toContain('杭州')
|
|
108
|
+
} finally {
|
|
109
|
+
unmount()
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('编辑输入经 update:modelValue 写回,且不因数据回流丢焦点', async () => {
|
|
114
|
+
// 用独立副本,编辑写回的是行对象本身,避免污染模块级共享数据
|
|
115
|
+
const { getDataRows, emitted, unmount } = mountTableEditor(
|
|
116
|
+
{ 'column:name': renderNameInput },
|
|
117
|
+
rows.map((row) => ({ ...row }))
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await nextTick()
|
|
122
|
+
const [row0] = getDataRows()
|
|
123
|
+
|
|
124
|
+
const input = row0.querySelector('input')!
|
|
125
|
+
input.focus()
|
|
126
|
+
input.value = 'Alicia'
|
|
127
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
128
|
+
await nextTick()
|
|
129
|
+
await nextTick()
|
|
130
|
+
|
|
131
|
+
expect(emitted.at(-1)![0].name).toBe('Alicia')
|
|
132
|
+
expect(document.activeElement).toBe(input)
|
|
133
|
+
} finally {
|
|
134
|
+
unmount()
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('UTableEditor 行操作', () => {
|
|
140
|
+
it('复制/新增到下一行/删除经 update:modelValue 生效', async () => {
|
|
141
|
+
const { getDataRows, emitted, unmount } = mountTableEditor()
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await nextTick()
|
|
145
|
+
let dataRows = getDataRows()
|
|
146
|
+
|
|
147
|
+
click(dataRows[0].querySelector('[title="复制到下一行"]')!)
|
|
148
|
+
await nextTick()
|
|
149
|
+
expect(emitted.at(-1)).toHaveLength(3)
|
|
150
|
+
expect(emitted.at(-1)![1]).toEqual(rows[0])
|
|
151
|
+
|
|
152
|
+
dataRows = getDataRows()
|
|
153
|
+
click(dataRows[0].querySelector('[title="新增到下一行"]')!)
|
|
154
|
+
await nextTick()
|
|
155
|
+
expect(emitted.at(-1)).toEqual([rows[0], {}, rows[0], rows[1]])
|
|
156
|
+
|
|
157
|
+
dataRows = getDataRows()
|
|
158
|
+
click(dataRows[0].querySelector('[title="移除"]')!)
|
|
159
|
+
await nextTick()
|
|
160
|
+
expect(emitted.at(-1)).toEqual([{}, rows[0], rows[1]])
|
|
161
|
+
} finally {
|
|
162
|
+
unmount()
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('空态展示添加按钮并新增空行', async () => {
|
|
167
|
+
const { host, emitted, unmount } = mountTableEditor({}, [])
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await nextTick()
|
|
171
|
+
const addBtn = [...host.querySelectorAll('button')].find((b) =>
|
|
172
|
+
b.textContent?.includes('添加')
|
|
173
|
+
)
|
|
174
|
+
expect(addBtn).toBeTruthy()
|
|
175
|
+
|
|
176
|
+
click(addBtn!)
|
|
177
|
+
await nextTick()
|
|
178
|
+
expect(emitted.at(-1)).toEqual([{}])
|
|
179
|
+
} finally {
|
|
180
|
+
unmount()
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
describe('UTableEditor 只读模式', () => {
|
|
186
|
+
it('输入控件经插槽 model 收到 readonly', async () => {
|
|
187
|
+
const { getDataRows, unmount } = mountTableEditor(
|
|
188
|
+
{ 'column:name': renderNameInput },
|
|
189
|
+
rows.map((row) => ({ ...row })),
|
|
190
|
+
columns,
|
|
191
|
+
{ readonly: true }
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
await nextTick()
|
|
196
|
+
const inputs = getDataRows().map((tr) => tr.querySelector('input')!)
|
|
197
|
+
expect(inputs).toHaveLength(2)
|
|
198
|
+
// readonly 经插槽 model 注入到输入控件
|
|
199
|
+
for (const input of inputs) expect(input.hasAttribute('readonly')).toBe(true)
|
|
200
|
+
} finally {
|
|
201
|
+
unmount()
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('库输入控件(u-input)只读下按展示态渲染值,不可修改', async () => {
|
|
206
|
+
const { getDataRows, emitted, unmount } = mountTableEditor(
|
|
207
|
+
{ 'column:name': (scope: any) => h(UInput, scope.model) },
|
|
208
|
+
rows.map((row) => ({ ...row })),
|
|
209
|
+
columns,
|
|
210
|
+
{ readonly: true }
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
await nextTick()
|
|
215
|
+
const [row0] = getDataRows()
|
|
216
|
+
// u-input 收到 readonly 后按只读展示态渲染值,不再是可编辑控件
|
|
217
|
+
expect(row0.querySelector('input')).toBeFalsy()
|
|
218
|
+
expect(row0.textContent).toContain('Alice')
|
|
219
|
+
expect(emitted).toHaveLength(0)
|
|
220
|
+
} finally {
|
|
221
|
+
unmount()
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('不渲染操作列:无「操作」表头与单元格操作按钮', async () => {
|
|
226
|
+
const { host, unmount } = mountTableEditor({}, rows, columns, { readonly: true })
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
await nextTick()
|
|
230
|
+
expect(findHeader(host, '操作')).toBeFalsy()
|
|
231
|
+
expect(host.querySelector('[title="移除"]')).toBeFalsy()
|
|
232
|
+
expect(host.querySelector('[title="新增到下一行"]')).toBeFalsy()
|
|
233
|
+
expect(host.querySelector('[title="复制到下一行"]')).toBeFalsy()
|
|
234
|
+
} finally {
|
|
235
|
+
unmount()
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('空表时不渲染「添加」按钮', async () => {
|
|
240
|
+
const { host, emitted, unmount } = mountTableEditor({}, [], columns, { readonly: true })
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
await nextTick()
|
|
244
|
+
const addBtn = [...host.querySelectorAll('button')].find((b) =>
|
|
245
|
+
b.textContent?.includes('添加')
|
|
246
|
+
)
|
|
247
|
+
expect(addBtn).toBeFalsy()
|
|
248
|
+
expect(emitted).toHaveLength(0)
|
|
249
|
+
} finally {
|
|
250
|
+
unmount()
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
describe('UTableEditor 按列校验', () => {
|
|
256
|
+
it('编辑列 change 时触发校验,输入(update)过程不校验;错误见表头列级指示', async () => {
|
|
257
|
+
// 用独立副本,编辑写回的是行对象本身,避免污染模块级共享数据
|
|
258
|
+
const { host, getDataRows, unmount } = mountTableEditor(
|
|
259
|
+
{ 'column:name': renderNameInput },
|
|
260
|
+
rows.map((row) => ({ ...row })),
|
|
261
|
+
ruleColumns
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
await nextTick()
|
|
266
|
+
const [row0] = getDataRows()
|
|
267
|
+
const headerText = () =>
|
|
268
|
+
findHeader(host, '姓名')?.querySelector('.u-table-editor__header-text')
|
|
269
|
+
const nameCellError = () =>
|
|
270
|
+
row0.querySelector('[data-test="name-input"]')?.parentElement?.className
|
|
271
|
+
|
|
272
|
+
// 输入过程(input 事件)不触发校验:清空后表头无错误指示,单元格无错误标记
|
|
273
|
+
const input = row0.querySelector('input')!
|
|
274
|
+
input.value = ''
|
|
275
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
276
|
+
await nextTick()
|
|
277
|
+
await nextTick()
|
|
278
|
+
expect(headerText()?.className).not.toContain('is-error')
|
|
279
|
+
expect(findHeader(host, '姓名')?.querySelector('.u-table-editor__header-icon')).toBeFalsy()
|
|
280
|
+
expect(nameCellError()).not.toContain('is-error')
|
|
281
|
+
|
|
282
|
+
// change 事件(如失焦提交)触发校验:该列表头出现错误指示,单元格挂 is-error
|
|
283
|
+
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
284
|
+
await nextTick()
|
|
285
|
+
await nextTick()
|
|
286
|
+
expect(headerText()?.className).toContain('is-error')
|
|
287
|
+
expect(findHeader(host, '姓名')?.querySelector('.u-table-editor__header-icon')).toBeTruthy()
|
|
288
|
+
expect(nameCellError()).toContain('is-error')
|
|
289
|
+
|
|
290
|
+
// 填回合法值并 change,错误消失
|
|
291
|
+
input.value = 'Alice'
|
|
292
|
+
input.dispatchEvent(new Event('input', { bubbles: true }))
|
|
293
|
+
input.dispatchEvent(new Event('change', { bubbles: true }))
|
|
294
|
+
await nextTick()
|
|
295
|
+
await nextTick()
|
|
296
|
+
expect(headerText()?.className).not.toContain('is-error')
|
|
297
|
+
expect(nameCellError()).not.toContain('is-error')
|
|
298
|
+
} finally {
|
|
299
|
+
unmount()
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('validate 全量校验:某行存在错误仍继续校验其后的行', async () => {
|
|
304
|
+
const { host, getEditor, unmount } = mountTableEditor(
|
|
305
|
+
{},
|
|
306
|
+
[
|
|
307
|
+
{ name: 'Alice', age: 18, city: '杭州' },
|
|
308
|
+
{ name: '', age: 20, city: '上海' },
|
|
309
|
+
{ name: '', age: 22, city: '北京' }
|
|
310
|
+
],
|
|
311
|
+
ruleColumns
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
await nextTick()
|
|
316
|
+
await expect(getEditor().validate()).resolves.toBe(false)
|
|
317
|
+
await nextTick()
|
|
318
|
+
|
|
319
|
+
// 第 2、3 行 name 均校验失败:该列表头标红并出现感叹号图标,失败单元格全部挂 is-error
|
|
320
|
+
const nameHeader = findHeader(host, '姓名')!
|
|
321
|
+
expect(nameHeader.querySelector('.u-table-editor__header-text')?.className).toContain(
|
|
322
|
+
'is-error'
|
|
323
|
+
)
|
|
324
|
+
const icon = nameHeader.querySelector('.u-table-editor__header-icon')!
|
|
325
|
+
expect(icon).toBeTruthy()
|
|
326
|
+
expect(host.querySelectorAll('td .is-error')).toHaveLength(2)
|
|
327
|
+
|
|
328
|
+
// 悬停图标查看气泡:包含第 2、3 行明细
|
|
329
|
+
icon.dispatchEvent(new MouseEvent('mouseenter'))
|
|
330
|
+
await nextTick()
|
|
331
|
+
const bubble = document.querySelector('.u-tip__content')
|
|
332
|
+
expect(bubble?.textContent).toContain('第 2 行')
|
|
333
|
+
expect(bubble?.textContent).toContain('第 3 行')
|
|
334
|
+
// 等弹层异步定位计算完成,避免与卸载竞争产生未处理拒绝
|
|
335
|
+
await new Promise((resolve) => setTimeout(resolve))
|
|
336
|
+
} finally {
|
|
337
|
+
unmount()
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('rules 含 required 的列表头渲染红星,其余列不渲染', async () => {
|
|
342
|
+
const { host, unmount } = mountTableEditor({}, rows, ruleColumns)
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
await nextTick()
|
|
346
|
+
// 仅 name 列 rules 含 required
|
|
347
|
+
expect(host.querySelectorAll('.u-table-editor__required-mark')).toHaveLength(1)
|
|
348
|
+
|
|
349
|
+
const mark = findHeader(host, '姓名')?.querySelector('.u-table-editor__required-mark')
|
|
350
|
+
expect(mark).toBeTruthy()
|
|
351
|
+
// 断言可见星号字形,而非仅节点存在
|
|
352
|
+
expect(mark?.textContent).toBe('*')
|
|
353
|
+
} finally {
|
|
354
|
+
unmount()
|
|
355
|
+
}
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('required 列自定义 `#header:key` 插槽时内容跟在星号后', async () => {
|
|
359
|
+
const { host, unmount } = mountTableEditor(
|
|
360
|
+
{ 'header:name': () => h('em', { 'data-test': 'custom-header' }, '自定义姓名') },
|
|
361
|
+
rows,
|
|
362
|
+
ruleColumns
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
await nextTick()
|
|
367
|
+
const header = findHeader(host, '自定义姓名')!
|
|
368
|
+
expect(header.querySelector('.u-table-editor__required-mark')).toBeTruthy()
|
|
369
|
+
expect(header.querySelector('[data-test="custom-header"]')).toBeTruthy()
|
|
370
|
+
} finally {
|
|
371
|
+
unmount()
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('列存在未通过项时表头文字标红并出现感叹号图标,修正后恢复', async () => {
|
|
376
|
+
const { host, getEditor, setModel, unmount } = mountTableEditor(
|
|
377
|
+
{},
|
|
378
|
+
[
|
|
379
|
+
{ name: 'Alice', age: 18, city: '杭州' },
|
|
380
|
+
{ name: '', age: 20, city: '上海' }
|
|
381
|
+
],
|
|
382
|
+
ruleColumns
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
await nextTick()
|
|
387
|
+
const nameHeader = findHeader(host, '姓名')!
|
|
388
|
+
const headerText = () => nameHeader?.querySelector('.u-table-editor__header-text')
|
|
389
|
+
|
|
390
|
+
// 初始无错误:表头无错误态、无图标
|
|
391
|
+
expect(headerText()?.className).not.toContain('is-error')
|
|
392
|
+
expect(nameHeader?.querySelector('.u-table-editor__header-icon')).toBeFalsy()
|
|
393
|
+
|
|
394
|
+
await expect(getEditor().validate()).resolves.toBe(false)
|
|
395
|
+
await nextTick()
|
|
396
|
+
|
|
397
|
+
// 该列存在未通过项:文字标红 + 感叹号图标(tip 触发器)出现
|
|
398
|
+
expect(headerText()?.className).toContain('is-error')
|
|
399
|
+
expect(nameHeader?.querySelector('.u-table-editor__header-icon')).toBeTruthy()
|
|
400
|
+
|
|
401
|
+
// 修正后表头恢复
|
|
402
|
+
setModel([
|
|
403
|
+
{ name: 'Alice', age: 18, city: '杭州' },
|
|
404
|
+
{ name: 'Bob', age: 20, city: '上海' }
|
|
405
|
+
])
|
|
406
|
+
await expect(getEditor().validate()).resolves.toBe(true)
|
|
407
|
+
await nextTick()
|
|
408
|
+
expect(headerText()?.className).not.toContain('is-error')
|
|
409
|
+
expect(nameHeader?.querySelector('.u-table-editor__header-icon')).toBeFalsy()
|
|
410
|
+
} finally {
|
|
411
|
+
unmount()
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
it('validate() 全表校验:全部通过 true,存在失败 false 并同步错误展示', async () => {
|
|
416
|
+
// 用独立副本,避免与其它用例共享可变行数据
|
|
417
|
+
const { host, getEditor, setModel, unmount } = mountTableEditor(
|
|
418
|
+
{},
|
|
419
|
+
rows.map((row) => ({ ...row })),
|
|
420
|
+
ruleColumns
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
await nextTick()
|
|
425
|
+
await expect(getEditor().validate()).resolves.toBe(true)
|
|
426
|
+
|
|
427
|
+
setModel([{ name: '', age: 18, city: '杭州' }])
|
|
428
|
+
await nextTick()
|
|
429
|
+
await expect(getEditor().validate()).resolves.toBe(false)
|
|
430
|
+
await nextTick()
|
|
431
|
+
// validate 后失败列的表头直接出现错误指示,失败单元格挂 is-error
|
|
432
|
+
const headerText = () =>
|
|
433
|
+
findHeader(host, '姓名')?.querySelector('.u-table-editor__header-text')
|
|
434
|
+
expect(headerText()?.className).toContain('is-error')
|
|
435
|
+
expect(host.querySelector('td .is-error')).toBeTruthy()
|
|
436
|
+
|
|
437
|
+
setModel([{ name: 'Alice', age: 18, city: '杭州' }])
|
|
438
|
+
await nextTick()
|
|
439
|
+
await expect(getEditor().validate()).resolves.toBe(true)
|
|
440
|
+
await nextTick()
|
|
441
|
+
expect(host.querySelector('td .is-error')).toBeFalsy()
|
|
442
|
+
} finally {
|
|
443
|
+
unmount()
|
|
444
|
+
}
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('删除行后同步清理对应行的错误信息', async () => {
|
|
448
|
+
const { host, getEditor, getDataRows, unmount } = mountTableEditor(
|
|
449
|
+
{},
|
|
450
|
+
[
|
|
451
|
+
{ name: '', age: 18, city: '杭州' },
|
|
452
|
+
{ name: 'Bob', age: 20, city: '上海' }
|
|
453
|
+
],
|
|
454
|
+
ruleColumns
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
await nextTick()
|
|
459
|
+
const headerText = () =>
|
|
460
|
+
findHeader(host, '姓名')?.querySelector('.u-table-editor__header-text')
|
|
461
|
+
|
|
462
|
+
await expect(getEditor().validate()).resolves.toBe(false)
|
|
463
|
+
await nextTick()
|
|
464
|
+
expect(headerText()?.className).toContain('is-error')
|
|
465
|
+
|
|
466
|
+
// 删除无错误的行,其余行的错误保留
|
|
467
|
+
click(getDataRows()[1].querySelector('[title="移除"]')!)
|
|
468
|
+
await nextTick()
|
|
469
|
+
expect(headerText()?.className).toContain('is-error')
|
|
470
|
+
|
|
471
|
+
// 删除有错误的行,错误随之清理
|
|
472
|
+
click(getDataRows()[0].querySelector('[title="移除"]')!)
|
|
473
|
+
await nextTick()
|
|
474
|
+
expect(headerText()?.className).not.toContain('is-error')
|
|
475
|
+
} finally {
|
|
476
|
+
unmount()
|
|
477
|
+
}
|
|
478
|
+
})
|
|
479
|
+
})
|
|
@@ -8,15 +8,67 @@ $root-name: table-editor;
|
|
|
8
8
|
@include m.b($root-name) {
|
|
9
9
|
@include m.bem(table, body) {
|
|
10
10
|
@include m.bem(table, cell) {
|
|
11
|
-
padding:
|
|
11
|
+
padding: 4px;
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// 单元格内行内级控件(inline-flex 输入框)参与基线对齐,
|
|
14
|
+
// 会在行盒下方留出 descender 空隙,顶对齐抵消
|
|
15
|
+
[class^='u-'] {
|
|
16
|
+
vertical-align: top;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 校验未通过的单元格:与表单项错误态一致,内嵌控件描红边(含 hover)、聚焦光晕转红,纯文本标红
|
|
22
|
+
.is-error {
|
|
23
|
+
color: fn.use-var(color, danger);
|
|
24
|
+
|
|
25
|
+
#{fn.bem(input)},
|
|
26
|
+
#{fn.bem(multi-select)},
|
|
27
|
+
#{fn.bem(multi-tree-select)},
|
|
28
|
+
#{fn.bem(auto-complete)},
|
|
29
|
+
#{fn.bem(textarea--more)} {
|
|
30
|
+
&,
|
|
31
|
+
&:hover,
|
|
32
|
+
&.is-focus {
|
|
33
|
+
border-color: fn.use-var(color, danger);
|
|
15
34
|
}
|
|
16
35
|
|
|
17
|
-
|
|
18
|
-
box-shadow:
|
|
36
|
+
&.is-focus {
|
|
37
|
+
box-shadow: fn.use-var(focus-ring, danger);
|
|
19
38
|
}
|
|
20
39
|
}
|
|
21
40
|
}
|
|
41
|
+
|
|
42
|
+
// 操作列按钮容器:fixed right 列由 u-table 独立分层渲染,此处只做按钮排布
|
|
43
|
+
@include m.bem($root-name, operations) {
|
|
44
|
+
display: flex;
|
|
45
|
+
justify-content: center;
|
|
46
|
+
gap: 4px;
|
|
47
|
+
|
|
48
|
+
// 图标按钮收窄水平 padding,保证三枚按钮在操作列宽内不溢出
|
|
49
|
+
.u-button {
|
|
50
|
+
padding: 0 0.4em;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// required 列表头红星标识
|
|
55
|
+
@include m.e(required-mark) {
|
|
56
|
+
padding-right: 2px;
|
|
57
|
+
color: fn.use-var(color, danger);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 表头文字:该列存在未通过项时标红
|
|
61
|
+
@include m.e(header-text) {
|
|
62
|
+
@include m.is(error) {
|
|
63
|
+
color: fn.use-var(color, danger);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 表头错误感叹号图标(tip 触发器)
|
|
68
|
+
@include m.e(header-icon) {
|
|
69
|
+
color: fn.use-var(color, danger);
|
|
70
|
+
margin-left: 2px;
|
|
71
|
+
vertical-align: -2px;
|
|
72
|
+
cursor: help;
|
|
73
|
+
}
|
|
22
74
|
}
|