feat: 仓库与库位补上编辑与停用/启用入口
后端 PUT /warehouses/:id 与 PUT /warehouses/:warehouseId/locations/:locationId 本就接受 name + status(service/repository 都只改这两列),api 层的 update/updateLocation 也早已封装好,缺的只是前端入口——页面此前只有「新增」,无法改状态。 - 仓库标题栏与库位行各加「编辑」按钮,弹窗内改名称 + 启用/停用 - 标题栏按钮必须 @click.stop:el-collapse-item 整条 header 都挂着展开/收起监听 - 库位状态列从直接显示英文 inactive 改为中文 el-tag,与仓库及主数据页统一 - 编辑态编码只读且不参与必填校验(code 不可变,后端 Update 也不接收) - 表单重置走已有的 replaceAllKeys,编辑只灌白名单字段,避免残留 id 让新增变成覆盖更新 - 库位所属仓库只存 id 不存对象,load() 整体替换列表后不会读到旧数据 - save() 显式吞掉错误:提示由 axios 响应拦截器统一负责,同时避免 @click 经 el-button 透传到原生 button、绕过 Vue 异步错误包装而产生的 unhandled rejection - 测试补 8 条:update/updateLocation 的参数与顺序、编辑态不误报编码、 改完再新增仍走 create、状态列中文、编辑按钮不收起面板、失败不关窗
This commit is contained in:
parent
696e90893b
commit
4905e60f8c
|
|
@ -1,5 +1,5 @@
|
|||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import ElementPlus, { ElMessage } from 'element-plus'
|
||||
import ElementPlus, { ElDialog, ElMessage } from 'element-plus'
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import WarehouseSettings from './WarehouseSettings.vue'
|
||||
import { warehousesApi } from '../../api/warehouses'
|
||||
|
|
@ -8,10 +8,26 @@ vi.mock('../../api/warehouses', () => ({
|
|||
warehousesApi: {
|
||||
list: vi.fn(),
|
||||
create: vi.fn(),
|
||||
createLocation: vi.fn()
|
||||
update: vi.fn(),
|
||||
createLocation: vi.fn(),
|
||||
updateLocation: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
// 真实的 ElSelect 在 jsdom 下会因状态双向绑定反复触发自身更新而报
|
||||
// "Maximum recursive updates exceeded"。这里只关心状态下拉提交的内容,用轻量替身绕开。
|
||||
// 与 MasterDataSettings.test.js 里的同款处理保持一致。
|
||||
const SelectStub = {
|
||||
props: ['modelValue', 'placeholder'],
|
||||
emits: ['update:modelValue', 'change'],
|
||||
template: '<div data-test="select"><slot /></div>'
|
||||
}
|
||||
|
||||
const OptionStub = {
|
||||
props: ['label', 'value'],
|
||||
template: '<span class="option">{{ label }}</span>'
|
||||
}
|
||||
|
||||
// 表单项没有 placeholder,按 label 文本定位输入框。
|
||||
function inputByLabel(wrapper, label) {
|
||||
const item = wrapper.findAll('.el-form-item').find(
|
||||
|
|
@ -24,19 +40,47 @@ function findButton(wrapper, text) {
|
|||
return wrapper.findAll('button').find(button => button.text().includes(text))
|
||||
}
|
||||
|
||||
// 页面改造后「编辑」按钮会有多个(每个仓库一个、每个库位一个),
|
||||
// 必须把查找收窄到作用域里,不能再用全局的 findButton 取首个。
|
||||
function findButtonIn(scope, text) {
|
||||
return scope.findAll('button').find(button => button.text().includes(text))
|
||||
}
|
||||
|
||||
async function clickSave(wrapper) {
|
||||
await findButton(wrapper, '保存').trigger('click')
|
||||
await flushPromises()
|
||||
}
|
||||
|
||||
const WAREHOUSE_WITH_LOCATION = {
|
||||
data: [{
|
||||
id: 1,
|
||||
code: 'DEFAULT',
|
||||
name: '默认仓库',
|
||||
status: 'active',
|
||||
locations: [{ id: 9, code: 'MAIN', name: '主库位', status: 'active' }]
|
||||
}]
|
||||
}
|
||||
|
||||
function mountPage() {
|
||||
return mount(WarehouseSettings, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
stubs: { teleport: true, ElSelect: SelectStub, ElOption: OptionStub }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
describe('仓库与库位表单必填校验', () => {
|
||||
let warnSpy
|
||||
|
||||
beforeEach(() => {
|
||||
warnSpy = vi.spyOn(ElMessage, 'warning').mockImplementation(() => {})
|
||||
vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
|
||||
warehousesApi.list.mockReset().mockResolvedValue({ data: [] })
|
||||
warehousesApi.create.mockReset().mockResolvedValue({ data: {} })
|
||||
warehousesApi.update.mockReset().mockResolvedValue({ data: {} })
|
||||
warehousesApi.createLocation.mockReset().mockResolvedValue({ data: {} })
|
||||
warehousesApi.updateLocation.mockReset().mockResolvedValue({ data: {} })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -44,9 +88,7 @@ describe('仓库与库位表单必填校验', () => {
|
|||
})
|
||||
|
||||
test('新增仓库时编码留空被拦下', async () => {
|
||||
const wrapper = mount(WarehouseSettings, {
|
||||
global: { plugins: [ElementPlus], stubs: { teleport: true } }
|
||||
})
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
await findButton(wrapper, '新增仓库').trigger('click')
|
||||
|
|
@ -59,9 +101,7 @@ describe('仓库与库位表单必填校验', () => {
|
|||
})
|
||||
|
||||
test('只填了编码时提示名称', async () => {
|
||||
const wrapper = mount(WarehouseSettings, {
|
||||
global: { plugins: [ElementPlus], stubs: { teleport: true } }
|
||||
})
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
await findButton(wrapper, '新增仓库').trigger('click')
|
||||
|
|
@ -78,9 +118,7 @@ describe('仓库与库位表单必填校验', () => {
|
|||
warehousesApi.list.mockResolvedValue({
|
||||
data: [{ id: 1, code: 'DEFAULT', name: '默认仓库', status: 'active', locations: [] }]
|
||||
})
|
||||
const wrapper = mount(WarehouseSettings, {
|
||||
global: { plugins: [ElementPlus], stubs: { teleport: true } }
|
||||
})
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
await findButton(wrapper, '新增库位').trigger('click')
|
||||
|
|
@ -93,9 +131,7 @@ describe('仓库与库位表单必填校验', () => {
|
|||
})
|
||||
|
||||
test('编码和名称都填好后正常提交', async () => {
|
||||
const wrapper = mount(WarehouseSettings, {
|
||||
global: { plugins: [ElementPlus], stubs: { teleport: true } }
|
||||
})
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
await findButton(wrapper, '新增仓库').trigger('click')
|
||||
|
|
@ -109,3 +145,153 @@ describe('仓库与库位表单必填校验', () => {
|
|||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('仓库与库位停用/启用', () => {
|
||||
let warnSpy
|
||||
|
||||
beforeEach(() => {
|
||||
warnSpy = vi.spyOn(ElMessage, 'warning').mockImplementation(() => {})
|
||||
vi.spyOn(ElMessage, 'success').mockImplementation(() => {})
|
||||
warehousesApi.list.mockReset().mockResolvedValue(WAREHOUSE_WITH_LOCATION)
|
||||
warehousesApi.create.mockReset().mockResolvedValue({ data: {} })
|
||||
warehousesApi.update.mockReset().mockResolvedValue({ data: {} })
|
||||
warehousesApi.createLocation.mockReset().mockResolvedValue({ data: {} })
|
||||
warehousesApi.updateLocation.mockReset().mockResolvedValue({ data: {} })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
test('编辑仓库改状态走 update,不走 create', async () => {
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
const header = wrapper.find('.el-collapse-item__header')
|
||||
await findButtonIn(header, '编辑').trigger('click')
|
||||
await flushPromises()
|
||||
await inputByLabel(wrapper, '名称').setValue('一号仓')
|
||||
await clickSave(wrapper)
|
||||
|
||||
// 编辑只发 name + status:code 不可变,后端 Update 也不接收
|
||||
expect(warehousesApi.update).toHaveBeenCalledWith(1, { name: '一号仓', status: 'active' })
|
||||
expect(warehousesApi.create).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
test('编辑态下把状态改成停用能提交', async () => {
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
await findButtonIn(wrapper.find('.el-collapse-item__header'), '编辑').trigger('click')
|
||||
await flushPromises()
|
||||
// 下拉里选「停用」:emit v-model 事件,跳过 popper 在 jsdom 下的定位问题
|
||||
wrapper.findComponent(SelectStub).vm.$emit('update:modelValue', 'inactive')
|
||||
await flushPromises()
|
||||
await clickSave(wrapper)
|
||||
|
||||
expect(warehousesApi.update).toHaveBeenCalledWith(1, { name: '默认仓库', status: 'inactive' })
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
test('编辑态一个字段都不改也能直接保存,且不误报编码', async () => {
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
await findButtonIn(wrapper.find('.el-collapse-item__header'), '编辑').trigger('click')
|
||||
await flushPromises()
|
||||
await clickSave(wrapper)
|
||||
|
||||
// 编码在编辑态是只读且不参与校验,不该被「请填写编码」拦下
|
||||
expect(warnSpy).not.toHaveBeenCalled()
|
||||
expect(warehousesApi.update).toHaveBeenCalledWith(1, { name: '默认仓库', status: 'active' })
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
test('编辑过之后再点新增,走的仍是 create', async () => {
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
await findButtonIn(wrapper.find('.el-collapse-item__header'), '编辑').trigger('click')
|
||||
await flushPromises()
|
||||
await clickSave(wrapper)
|
||||
expect(warehousesApi.update).toHaveBeenCalledTimes(1)
|
||||
|
||||
await findButton(wrapper, '新增仓库').trigger('click')
|
||||
await flushPromises()
|
||||
await inputByLabel(wrapper, '编码').setValue('WH-004')
|
||||
await inputByLabel(wrapper, '名称').setValue('四号仓')
|
||||
await clickSave(wrapper)
|
||||
|
||||
// 编辑残留的 id 若漏进 form,这里就会变成第二次 update(覆盖别人的记录)
|
||||
expect(warehousesApi.create).toHaveBeenCalledWith({ code: 'WH-004', name: '四号仓', status: 'active' })
|
||||
expect(warehousesApi.update).toHaveBeenCalledTimes(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
test('编辑库位的参数顺序是 (仓库 id, 库位 id, 内容)', async () => {
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
const row = wrapper.find('.el-table__row')
|
||||
expect(row.exists()).toBe(true)
|
||||
await findButtonIn(row, '编辑').trigger('click')
|
||||
await flushPromises()
|
||||
await inputByLabel(wrapper, '名称').setValue('主库位A')
|
||||
await clickSave(wrapper)
|
||||
|
||||
// 顺序写反会打到 /warehouses/{库位id}/locations/undefined
|
||||
expect(warehousesApi.updateLocation).toHaveBeenCalledWith(1, 9, { name: '主库位A', status: 'active' })
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
test('库位状态列显示中文而不是英文原值', async () => {
|
||||
warehousesApi.list.mockResolvedValue({
|
||||
data: [{
|
||||
id: 1,
|
||||
code: 'DEFAULT',
|
||||
name: '默认仓库',
|
||||
status: 'inactive',
|
||||
locations: [{ id: 9, code: 'MAIN', name: '主库位', status: 'inactive' }]
|
||||
}]
|
||||
})
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
const text = wrapper.text()
|
||||
// 先确认库位行真的渲染出来了,否则下面的断言会假通过
|
||||
expect(text).toContain('主库位')
|
||||
expect(text).toContain('停用')
|
||||
expect(text).not.toContain('inactive')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
test('点标题栏编辑不会连带把面板收起来', async () => {
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
await wrapper.find('.el-collapse-item__header').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.el-collapse-item__header').classes()).toContain('is-active')
|
||||
|
||||
await findButtonIn(wrapper.find('.el-collapse-item__header'), '编辑').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.find('.el-collapse-item__header').classes()).toContain('is-active')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
test('保存失败时弹窗不自动关闭', async () => {
|
||||
warehousesApi.update.mockRejectedValueOnce(new Error('接口炸了'))
|
||||
const wrapper = mountPage()
|
||||
await flushPromises()
|
||||
|
||||
await findButtonIn(wrapper.find('.el-collapse-item__header'), '编辑').trigger('click')
|
||||
await flushPromises()
|
||||
await clickSave(wrapper)
|
||||
|
||||
expect(warehousesApi.update).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.findComponent(ElDialog).props('modelValue')).toBe(true)
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,19 +3,41 @@
|
|||
<div class="toolbar"><span>仓库与库位属于库存域,不在物料主数据中维护。</span><el-button type="primary" @click="openWarehouse">新增仓库</el-button></div>
|
||||
<el-collapse v-loading="loading">
|
||||
<el-collapse-item v-for="warehouse in warehouses" :key="warehouse.id" :name="warehouse.id">
|
||||
<template #title><strong>{{ warehouse.code }} - {{ warehouse.name }}</strong><el-tag class="status" :type="warehouse.status === 'active' ? 'success' : 'info'">{{ warehouse.status === 'active' ? '启用' : '停用' }}</el-tag></template>
|
||||
<div class="location-header"><span>库位</span><el-button link type="primary" @click.stop="openLocation(warehouse)">新增库位</el-button></div>
|
||||
<template #title>
|
||||
<strong>{{ warehouse.code }} - {{ warehouse.name }}</strong>
|
||||
<el-tag class="status" :type="warehouse.status === 'active' ? 'success' : 'info'">{{ warehouse.status === 'active' ? '启用' : '停用' }}</el-tag>
|
||||
<!-- 整条 header 都挂着展开/收起的监听,按钮不 .stop 会连带把面板收起来 -->
|
||||
<el-button link type="primary" class="status" @click.stop="openWarehouseEdit(warehouse.id)">编辑</el-button>
|
||||
</template>
|
||||
<div class="location-header"><span>库位</span><el-button link type="primary" @click.stop="openLocation(warehouse.id)">新增库位</el-button></div>
|
||||
<el-table :data="warehouse.locations || []" size="small">
|
||||
<el-table-column prop="code" label="库位编码" /><el-table-column prop="name" label="库位名称" /><el-table-column prop="status" label="状态" />
|
||||
<el-table-column prop="code" label="库位编码" />
|
||||
<el-table-column prop="name" label="库位名称" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : 'info'">{{ row.status === 'active' ? '启用' : '停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openLocationEdit(warehouse.id, row.id)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-empty v-if="!loading && warehouses.length === 0" description="暂无仓库" />
|
||||
|
||||
<el-dialog v-model="formVisible" :title="formType === 'warehouse' ? '新增仓库' : `新增库位 - ${selectedWarehouse?.name || ''}`" width="420px" append-to-body>
|
||||
<el-dialog v-model="formVisible" :title="formTitle" width="420px" append-to-body>
|
||||
<el-form :model="form" label-width="90px">
|
||||
<el-form-item label="编码" required><el-input v-model="form.code" /></el-form-item>
|
||||
<el-form-item label="编码" :required="!isEdit"><el-input v-model="form.code" :disabled="isEdit" /></el-form-item>
|
||||
<el-form-item label="名称" required><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status" style="width: 100%">
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="停用" value="inactive" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="formVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存</el-button></template>
|
||||
</el-dialog>
|
||||
|
|
@ -23,25 +45,83 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { warehousesApi } from '../../api/warehouses'
|
||||
import { replaceAllKeys } from '../../utils/replaceAllKeys'
|
||||
|
||||
const warehouses = ref([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const formVisible = ref(false)
|
||||
const formType = ref('warehouse')
|
||||
const selectedWarehouse = ref(null)
|
||||
const form = reactive({ code: '', name: '', status: 'active' })
|
||||
// 编辑目标的 id,null 表示新增。刻意单独存放、不进 form:
|
||||
// form 上残留的 id 靠 Object.assign 是删不掉的,会导致「点新增却变成覆盖更新」。
|
||||
const editingId = ref(null)
|
||||
// 库位所属仓库只记 id,不记对象:load() 会整体替换 warehouses,
|
||||
// 存下来的对象在保存后会变成改动前的旧数据(弹窗标题会显示旧仓库名)。
|
||||
const selectedWarehouseId = ref(null)
|
||||
const selectedWarehouse = computed(() => warehouses.value.find(item => item.id === selectedWarehouseId.value) || null)
|
||||
const isEdit = computed(() => editingId.value !== null)
|
||||
const form = reactive(emptyForm())
|
||||
|
||||
const formTitle = computed(() => {
|
||||
if (isEdit.value) return formType.value === 'warehouse' ? '编辑仓库' : '编辑库位'
|
||||
return formType.value === 'location' ? `新增库位 - ${selectedWarehouse.value?.name || ''}` : '新增仓库'
|
||||
})
|
||||
|
||||
function emptyForm() { return { code: '', name: '', status: 'active' } }
|
||||
|
||||
async function load() { loading.value = true; try { const response = await warehousesApi.list(); warehouses.value = response.data || [] } finally { loading.value = false } }
|
||||
function resetForm() { Object.assign(form, { code: '', name: '', status: 'active' }) }
|
||||
function openWarehouse() { formType.value = 'warehouse'; selectedWarehouse.value = null; resetForm(); formVisible.value = true }
|
||||
function openLocation(warehouse) { formType.value = 'location'; selectedWarehouse.value = warehouse; resetForm(); formVisible.value = true }
|
||||
|
||||
// 只灌表单认得的字段,不写 Object.assign(form, row):
|
||||
// 这样 row 上的 id / organization_id / locations 在结构上就进不了 form。
|
||||
// status 兜底成 active,否则空值提交上去会被后端 normalize 静默改回启用。
|
||||
function fillForm(row) { replaceAllKeys(form, { code: row.code, name: row.name, status: row.status || 'active' }) }
|
||||
function resetForm() { replaceAllKeys(form, emptyForm()) }
|
||||
|
||||
function openWarehouse() { formType.value = 'warehouse'; editingId.value = null; selectedWarehouseId.value = null; resetForm(); formVisible.value = true }
|
||||
function openWarehouseEdit(warehouseId) {
|
||||
const warehouse = warehouses.value.find(item => item.id === warehouseId)
|
||||
if (!warehouse) return
|
||||
formType.value = 'warehouse'; editingId.value = warehouseId; selectedWarehouseId.value = null; fillForm(warehouse); formVisible.value = true
|
||||
}
|
||||
function openLocation(warehouseId) { formType.value = 'location'; editingId.value = null; selectedWarehouseId.value = warehouseId; resetForm(); formVisible.value = true }
|
||||
function openLocationEdit(warehouseId, locationId) {
|
||||
const warehouse = warehouses.value.find(item => item.id === warehouseId)
|
||||
const location = warehouse?.locations?.find(item => item.id === locationId)
|
||||
if (!location) return
|
||||
formType.value = 'location'; editingId.value = locationId; selectedWarehouseId.value = warehouseId; fillForm(location); formVisible.value = true
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!form.code.trim()) { ElMessage.warning('请填写编码'); return }
|
||||
if (saving.value) return
|
||||
// 编辑态不校验编码:code 不可变,后端 Update 也不接收这个字段
|
||||
if (!isEdit.value && !form.code.trim()) { ElMessage.warning('请填写编码'); return }
|
||||
if (!form.name.trim()) { ElMessage.warning('请填写名称'); return }
|
||||
// 编辑只发 name + status:带上 code 只会让人误以为编码能改
|
||||
const payload = isEdit.value
|
||||
? { name: form.name.trim(), status: form.status }
|
||||
: { code: form.code.trim(), name: form.name.trim(), status: form.status }
|
||||
saving.value = true
|
||||
try { if (formType.value === 'warehouse') await warehousesApi.create(form); else await warehousesApi.createLocation(selectedWarehouse.value.id, form); ElMessage.success('保存成功'); formVisible.value = false; await load() } finally { saving.value = false }
|
||||
try {
|
||||
if (formType.value === 'warehouse') {
|
||||
if (isEdit.value) await warehousesApi.update(editingId.value, payload)
|
||||
else await warehousesApi.create(payload)
|
||||
} else if (isEdit.value) {
|
||||
await warehousesApi.updateLocation(selectedWarehouseId.value, editingId.value, payload)
|
||||
} else {
|
||||
await warehousesApi.createLocation(selectedWarehouseId.value, payload)
|
||||
}
|
||||
ElMessage.success('保存成功')
|
||||
// 关窗放在 await 成功之后:失败时弹窗留着,用户能改完重试
|
||||
formVisible.value = false
|
||||
await load()
|
||||
} catch {
|
||||
// 错误提示由 axios 响应拦截器统一负责,这里不再重复弹一次;
|
||||
// 显式吞掉还避免 save 的 rejection 冒到控制台(@click 经 el-button 透传到原生
|
||||
// button 后不再走 Vue 的异步处理器包装,rejection 会变成 unhandled)。
|
||||
} finally { saving.value = false }
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user