前端提示优化

This commit is contained in:
文睿 张 2026-09-21 16:40:43 +08:00
parent f0722eed00
commit 85d9683754
6 changed files with 110 additions and 12 deletions

View File

@ -0,0 +1,90 @@
import { flushPromises, mount } from '@vue/test-utils'
import ElementPlus from 'element-plus'
import { describe, expect, test, vi } from 'vitest'
import DeleteConfirm from './DeleteConfirm.vue'
const CONFIRM_TEXT = '确认作废'
function mountConfirm(onConfirm) {
return mount(DeleteConfirm, {
props: { modelValue: true, confirmText: CONFIRM_TEXT, onConfirm },
global: {
plugins: [ElementPlus],
stubs: { teleport: true }
}
})
}
function confirmButton(wrapper) {
return wrapper.findAll('button').find(button => button.text().includes(CONFIRM_TEXT))
}
// 弹窗关闭体现为子组件把 modelValue 置回 false
function closedByEmit(wrapper) {
return (wrapper.emitted('update:modelValue') || []).some(args => args[0] === false)
}
describe('DeleteConfirm 的关闭时机', () => {
test('父操作成功后关闭弹窗', async () => {
const onConfirm = vi.fn().mockResolvedValue()
const wrapper = mountConfirm(onConfirm)
await flushPromises()
await confirmButton(wrapper).trigger('click')
await flushPromises()
expect(onConfirm).toHaveBeenCalledTimes(1)
expect(closedByEmit(wrapper)).toBe(true)
wrapper.unmount()
})
test('父操作失败时弹窗保持打开,让用户能直接重试', async () => {
const onConfirm = vi.fn().mockRejectedValue(new Error('接口失败'))
const wrapper = mountConfirm(onConfirm)
await flushPromises()
await confirmButton(wrapper).trigger('click')
await flushPromises()
expect(onConfirm).toHaveBeenCalledTimes(1)
expect(closedByEmit(wrapper)).toBe(false)
wrapper.unmount()
})
test('失败后原地点一次即可重试,不必重走确认流程', async () => {
const onConfirm = vi.fn()
.mockRejectedValueOnce(new Error('接口失败'))
.mockResolvedValueOnce()
const wrapper = mountConfirm(onConfirm)
await flushPromises()
await confirmButton(wrapper).trigger('click')
await flushPromises()
expect(closedByEmit(wrapper)).toBe(false)
// 弹窗还开着,直接再点一次
await confirmButton(wrapper).trigger('click')
await flushPromises()
expect(onConfirm).toHaveBeenCalledTimes(2)
expect(closedByEmit(wrapper)).toBe(true)
wrapper.unmount()
})
test('操作进行中按钮转 loading,避免重复提交', async () => {
let resolveRequest
const onConfirm = vi.fn(() => new Promise((resolve) => { resolveRequest = resolve }))
const wrapper = mountConfirm(onConfirm)
await flushPromises()
await confirmButton(wrapper).trigger('click')
await flushPromises()
expect(confirmButton(wrapper).classes()).toContain('is-loading')
resolveRequest()
await flushPromises()
expect(confirmButton(wrapper).classes()).not.toContain('is-loading')
wrapper.unmount()
})
})

View File

@ -24,10 +24,17 @@ const props = defineProps({
content: { content: {
type: String, type: String,
default: '确定要删除此数据吗?此操作不可恢复。' default: '确定要删除此数据吗?此操作不可恢复。'
},
// 确认后执行的实际操作,由父组件传入并返回 Promise。
// 这里刻意不用 emit('confirm'):emit 是同步的,拿不到父组件异步操作的结果,
// 会退化成"请求刚发出去弹窗就关了",失败时用户得把确认流程重走一遍。
onConfirm: {
type: Function,
required: true
} }
}) })
const emit = defineEmits(['update:modelValue', 'confirm']) const emit = defineEmits(['update:modelValue'])
const visible = ref(props.modelValue) const visible = ref(props.modelValue)
const loading = ref(false) const loading = ref(false)
@ -43,8 +50,11 @@ watch(visible, (val) => {
async function handleConfirm() { async function handleConfirm() {
loading.value = true loading.value = true
try { try {
emit('confirm') await props.onConfirm()
// 只有父组件的操作成功返回才关闭弹窗:失败时留着,用户可以直接再点一次
visible.value = false visible.value = false
} catch (e) {
// 失败原因已由 axios 拦截器统一提示,这里只负责别把弹窗关掉
} finally { } finally {
loading.value = false loading.value = false
} }

View File

@ -125,7 +125,7 @@
</div> </div>
</el-dialog> </el-dialog>
<DeleteConfirm v-model="deleteDialogVisible" @confirm="confirmDelete" /> <DeleteConfirm v-model="deleteDialogVisible" :on-confirm="confirmDelete" />
</div> </div>
</template> </template>

View File

@ -144,7 +144,7 @@
title="确认作废该物料?" title="确认作废该物料?"
content="作废后该物料不能再用于新业务;已有业务记录仍会保留。" content="作废后该物料不能再用于新业务;已有业务记录仍会保留。"
confirm-text="确认作废" confirm-text="确认作废"
@confirm="confirmDelete" :on-confirm="confirmDelete"
/> />
<AlertTip :message="alertMessage" :type="alertType" /> <AlertTip :message="alertMessage" :type="alertType" />
@ -394,13 +394,11 @@ function handleDelete(id) {
deleteDialogVisible.value = true deleteDialogVisible.value = true
} }
// 这里不能吞异常:DeleteConfirm 靠它判断该不该关弹窗,吞掉会让失败时误关。
// 错误提示本来由 axios 拦截器统一负责,不吞也不会漏给用户看。
async function confirmDelete() { async function confirmDelete() {
try {
await materialsStore.obsoleteMaterial(deleteId.value) await materialsStore.obsoleteMaterial(deleteId.value)
ElMessage.success('物料已作废') ElMessage.success('物料已作废')
} catch (e) {
// 错误已由 API 层处理
}
} }
async function handleRestore(id) { async function handleRestore(id) {

View File

@ -79,7 +79,7 @@
</template> </template>
</el-dialog> </el-dialog>
<DeleteConfirm v-model="deleteDialogVisible" @confirm="confirmDelete" /> <DeleteConfirm v-model="deleteDialogVisible" :on-confirm="confirmDelete" />
<el-dialog v-model="detailVisible" title="出库单详情" width="600px"> <el-dialog v-model="detailVisible" title="出库单详情" width="600px">
<div v-if="currentRow"> <div v-if="currentRow">

View File

@ -80,7 +80,7 @@
</template> </template>
</el-dialog> </el-dialog>
<DeleteConfirm v-model="deleteDialogVisible" @confirm="confirmDelete" /> <DeleteConfirm v-model="deleteDialogVisible" :on-confirm="confirmDelete" />
</div> </div>
</template> </template>