This commit is contained in:
2026-08-14 14:27:03 +08:00
parent 724848feb6
commit a81af4a664
64 changed files with 961 additions and 176 deletions
+53 -3
View File
@@ -10,9 +10,13 @@ import { UI_PREFERENCE_SCOPE, preferenceStorageKey } from '@/shared/utils/uiLayo
const FieldStub = defineComponent({
inheritAttrs: false,
props: { placeholder: String },
template: '<input :data-placeholder="placeholder" />',
emits: ['keyup'],
template: '<input :data-placeholder="placeholder" @keyup="$emit(\'keyup\', $event)" />',
})
const ButtonStub = defineComponent({
props: { disabled: Boolean, loading: Boolean },
template: '<button type="button" :disabled="disabled" :data-loading="String(loading)"><slot /></button>',
})
const ButtonStub = defineComponent({ template: '<button type="button"><slot /></button>' })
const PopoverStub = defineComponent({ template: '<div><slot name="reference" /><slot /></div>' })
const fields: QueryField[] = [
@@ -21,7 +25,7 @@ const fields: QueryField[] = [
{ key: 'createdRange', type: 'daterange', label: '创建日期', placeholder: '创建日期' },
]
function mountToolbar() {
function mountToolbar(props: Record<string, unknown> = {}) {
return shallowMount(QueryToolbar, {
props: {
modelValue: { keyword: '', status: '', createdRange: [] },
@@ -29,6 +33,7 @@ function mountToolbar() {
ariaLabel: '订单筛选',
fields,
preferenceKey: 'orders-query',
...props,
},
global: {
provide: { [UI_PREFERENCE_SCOPE as symbol]: computed(() => 'user-1') },
@@ -70,4 +75,49 @@ describe('QueryToolbar layout preferences', () => {
const source = readFileSync(resolve(process.cwd(), 'src/shared/components/QueryToolbar.vue'), 'utf8')
expect(source).toContain('.query-toolbar__tail { display: flex; align-items: flex-end; width: auto; flex: 0 0 auto; gap: 10px; }')
})
it('submits the current filters once when Enter is released in a text input', async () => {
const wrapper = mountToolbar({ modelValue: { keyword: 'AKR1', status: 'ACTIVE', createdRange: [] } })
const textInput = wrapper.findAll('input')[0]
await textInput.trigger('keyup', { key: 'Enter' })
expect(wrapper.emitted('search')).toEqual([[
{ keyword: 'AKR1', status: 'ACTIVE', createdRange: [] },
]])
})
it('does not submit Enter from select or date controls, composition, or repeated key events', async () => {
const wrapper = mountToolbar()
const controls = wrapper.findAll('input')
await controls[1].trigger('keyup', { key: 'Enter' })
await controls[2].trigger('keyup', { key: 'Enter' })
await controls[0].trigger('keyup', { key: 'Enter', isComposing: true })
await controls[0].trigger('keyup', { key: 'Enter', repeat: true })
expect(wrapper.emitted('search')).toBeUndefined()
})
it('resets all filters once and blocks search and reset while loading', async () => {
const wrapper = mountToolbar({
modelValue: { keyword: 'AKR1', status: 'ACTIVE', createdRange: ['2026-08-01', '2026-08-14'] },
})
;(wrapper.vm as unknown as { reset: () => void }).reset()
expect(wrapper.emitted('reset')).toEqual([[
{ keyword: '', status: '', createdRange: [] },
]])
expect(wrapper.emitted('search')).toEqual([[
{ keyword: '', status: '', createdRange: [] },
]])
await wrapper.setProps({ loading: true })
;(wrapper.vm as unknown as { search: () => void; reset: () => void }).search()
;(wrapper.vm as unknown as { search: () => void; reset: () => void }).reset()
expect(wrapper.emitted('search')).toHaveLength(1)
expect(wrapper.emitted('reset')).toHaveLength(1)
expect(wrapper.findAllComponents(ButtonStub)[0].props('loading')).toBe(true)
expect(wrapper.findAllComponents(ButtonStub)[1].props('disabled')).toBe(true)
})
})
+14 -4
View File
@@ -40,10 +40,12 @@ const props = withDefaults(defineProps<{
searchText?: string
resetText?: string
searchOnReset?: boolean
loading?: boolean
}>(), {
searchText: '查询',
resetText: '重置',
searchOnReset: true,
loading: false,
})
const model = defineModel<QueryModel>({ required: true })
@@ -140,10 +142,12 @@ function resetValue(field: QueryField): QueryValue {
}
function search() {
if (props.loading) return
emit('search', model.value)
}
function reset() {
if (props.loading) return
const next = { ...model.value }
for (const field of props.fields) next[field.key] = resetValue(field)
model.value = next
@@ -151,6 +155,11 @@ function reset() {
if (props.searchOnReset) emit('search', next)
}
function searchFromTextInput(event: KeyboardEvent) {
if (event.isComposing || event.repeat) return
search()
}
defineExpose({ search, reset })
</script>
@@ -175,6 +184,7 @@ defineExpose({ search, reset })
clearable
v-bind="field.props"
@update:model-value="updateField(field.key, $event)"
@keyup.enter.exact="searchFromTextInput"
>
<template #prefix><Search :size="16" /></template>
</el-input>
@@ -203,8 +213,8 @@ defineExpose({ search, reset })
/>
</div>
<div v-if="index === orderedFields.length - 1" class="query-toolbar__actions">
<el-button type="primary" @click="search"><Search :size="16" />{{ searchText }}</el-button>
<el-button @click="reset"><RotateCcw :size="16" />{{ resetText }}</el-button>
<el-button type="primary" :loading="loading" @click="search"><Search :size="16" />{{ searchText }}</el-button>
<el-button :disabled="loading" @click="reset"><RotateCcw :size="16" />{{ resetText }}</el-button>
<slot name="actions" />
<el-popover v-if="preferenceEnabled" placement="bottom-end" :width="340" trigger="click" popper-class="cc-layout-preference-popper">
<template #reference>
@@ -216,8 +226,8 @@ defineExpose({ search, reset })
</div>
</div>
<div v-if="orderedFields.length === 0" class="query-toolbar__actions">
<el-button type="primary" @click="search"><Search :size="16" />{{ searchText }}</el-button>
<el-button @click="reset"><RotateCcw :size="16" />{{ resetText }}</el-button>
<el-button type="primary" :loading="loading" @click="search"><Search :size="16" />{{ searchText }}</el-button>
<el-button :disabled="loading" @click="reset"><RotateCcw :size="16" />{{ resetText }}</el-button>
<slot name="actions" />
<el-popover v-if="preferenceEnabled" placement="bottom-end" :width="340" trigger="click" popper-class="cc-layout-preference-popper">
<template #reference>