优化
This commit is contained in:
@@ -152,13 +152,14 @@ it('allows editing after unknown failure and submits a new command', async () =>
|
||||
expect(vi.mocked(commandException).mock.calls[1]![3].idempotencyKey).not.toBe(payload.idempotencyKey); expect(vi.mocked(commandException).mock.calls[1]![3]).toEqual(expect.objectContaining({ handlingResult: 'changed' }))
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('keeps a conflict visible and lets the operator refresh voluntarily', async () => {
|
||||
it('keeps a conflict visible and reloads when the operator reopens the detail', async () => {
|
||||
vi.mocked(getExceptionDetail).mockResolvedValueOnce(snapshot()).mockResolvedValueOnce(snapshot('EX-1', 7, 2))
|
||||
vi.mocked(commandException).mockRejectedValueOnce(new ApiResponseError('版本变化', 'A0409', 409))
|
||||
const wrapper = open(); await flushPromises(); await fill(wrapper)
|
||||
await wrapper.get('[data-test="complete"]').trigger('click'); await flushPromises()
|
||||
expect(wrapper.text()).toContain('版本变化'); expect(commandException).toHaveBeenCalledTimes(1); expect(getExceptionDetail).toHaveBeenCalledTimes(1)
|
||||
await wrapper.findAll('button').find(button => button.text() === '重新读取详情')!.trigger('click'); await flushPromises()
|
||||
expect(wrapper.text()).not.toContain('重新读取详情')
|
||||
await wrapper.setProps({ active: false }); await wrapper.setProps({ active: true }); await flushPromises()
|
||||
await fill(wrapper); await wrapper.get('[data-test="complete"]').trigger('click'); await flushPromises()
|
||||
expect(vi.mocked(commandException).mock.calls[1]![3].expectedVersion).toBe(7)
|
||||
expect(vi.mocked(commandException).mock.calls[1]![3].idempotencyKey).not.toBe(vi.mocked(commandException).mock.calls[0]![3].idempotencyKey)
|
||||
@@ -284,3 +285,21 @@ it('twenty-one historical files do not get resubmitted automatically with the ne
|
||||
expect(vi.mocked(commandException).mock.calls[0]![3].evidenceFileIds).toEqual(['991'])
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('moves processing controls into the dialog footer target', async () => {
|
||||
const footer = document.createElement('div')
|
||||
footer.id = 'exception-test-footer'
|
||||
document.body.append(footer)
|
||||
const wrapper = open()
|
||||
try {
|
||||
await flushPromises()
|
||||
await wrapper.setProps({ actionsTarget: '#exception-test-footer' })
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-test="complete"]').exists()).toBe(false)
|
||||
expect(footer.querySelector('[data-test="complete"]')).not.toBeNull()
|
||||
expect(wrapper.text()).not.toContain('重新读取详情')
|
||||
} finally {
|
||||
wrapper.unmount()
|
||||
footer.remove()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import { formatOverseasDateTime } from '../lastMilePresentation'
|
||||
import { overseasWarehousePermissions as permission } from '../permissions'
|
||||
import { returnSourceKey, returnSourceReference } from '../returnSource'
|
||||
|
||||
const props = defineProps<{ customerId: string; exceptionNo: string; active: boolean }>()
|
||||
const props = defineProps<{ customerId: string; exceptionNo: string; active: boolean; actionsTarget?: string }>()
|
||||
const emit = defineEmits<{ changed: [] }>()
|
||||
const access = useAccessPreviewStore()
|
||||
const canProcess = computed(() => access.can('platform:overseas-warehouse:exception-order:process'))
|
||||
@@ -156,9 +156,8 @@ watch(() => [props.customerId, props.exceptionNo, props.active], () => { ++seque
|
||||
<template>
|
||||
<section v-loading="loading" aria-label="异常处理详情">
|
||||
<p v-if="error" role="alert">{{ error }}</p><p v-if="message" role="status">{{ message }}</p>
|
||||
<el-button :disabled="busy" @click="load">重新读取详情</el-button>
|
||||
<template v-if="detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions class="density-descriptions exception-detail-fields" :column="2" border>
|
||||
<el-descriptions-item label="异常单号">{{ detail.order.exceptionNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="处理状态">{{ ({ PENDING: '待处理', PROCESSING: '处理中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record<string, string>)[detail.order.status] || detail.order.status }}</el-descriptions-item>
|
||||
<el-descriptions-item label="客户">{{ detail.order.customerName || '—' }}</el-descriptions-item>
|
||||
@@ -180,7 +179,7 @@ watch(() => [props.customerId, props.exceptionNo, props.active], () => { ++seque
|
||||
<section v-if="detail.returnReceipts.length" aria-label="退回实收与重派关联" class="exception-return-history workbench-evidence">
|
||||
<h3>退回实收与重派关联</h3>
|
||||
<div v-for="receipt in detail.returnReceipts" :key="receipt.receiptNo" class="exception-return-batch">
|
||||
<el-descriptions :column="3" border>
|
||||
<el-descriptions class="density-descriptions" :column="3" border>
|
||||
<el-descriptions-item label="退回收货号">{{ receipt.receiptNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="本批实收">{{ receipt.receivedPackages }} 箱</el-descriptions-item>
|
||||
<el-descriptions-item label="实收时间">{{ formatOverseasDateTime(receipt.receivedAt) }}</el-descriptions-item>
|
||||
@@ -200,8 +199,8 @@ watch(() => [props.customerId, props.exceptionNo, props.active], () => { ++seque
|
||||
</section>
|
||||
<p v-if="detail.order.evidenceFileIds?.length">已保存 {{ detail.order.evidenceFileIds.length }} 份历史证据;本次可按需选用,不自动全部提交。</p>
|
||||
<template v-if="canProcess">
|
||||
<el-form label-position="top" :disabled="locked">
|
||||
<el-form-item label="处理说明 / 取消原因(选填)"><el-input v-model="notes" data-test="notes" type="textarea" :rows="2" maxlength="1000" /></el-form-item>
|
||||
<el-form class="density-form density-form--outlined exception-processing-form" label-position="left" label-width="140px" :disabled="locked">
|
||||
<el-form-item label="处理说明 / 取消原因(选填)" label-width="220px"><el-input v-model="notes" data-test="notes" type="textarea" :rows="2" maxlength="1000" placeholder="请输入处理说明或取消原因(选填)" /></el-form-item>
|
||||
<template v-if="allowed('COMPLETE_EXCEPTION')">
|
||||
<label>处理证据 <input data-test="evidence" type="file" :disabled="locked" @change="upload" /></label>
|
||||
<div v-for="(fileId, index) in detail.order.evidenceFileIds || []" :key="fileId"><label><input :data-test="`history-${index}`" type="checkbox" :checked="selectedHistory.includes(fileId)" :disabled="locked" @change="selectHistory(fileId, $event)" />选用历史证据 {{ index + 1 }}</label></div>
|
||||
@@ -219,15 +218,21 @@ watch(() => [props.customerId, props.exceptionNo, props.active], () => { ++seque
|
||||
<p v-else-if="detail.order.exceptionType === 'RETURN'">关闭此异常只记录处理结果,不代表货物交接完成,也不会释放车辆或司机。</p>
|
||||
</template>
|
||||
</el-form>
|
||||
<el-button v-if="allowed('START_PROCESSING')" data-test="start" :disabled="busy || (!!pending && pending.action !== 'start-processing')" @click="execute('start-processing')">{{ '开始处理' }}</el-button>
|
||||
<el-button v-if="allowed('CANCEL_EXCEPTION')" data-test="cancel" :disabled="busy || (!!pending && pending.action !== 'cancel')" @click="execute('cancel')">{{ '取消异常' }}</el-button>
|
||||
<el-button v-if="allowed('COMPLETE_EXCEPTION')" data-test="complete" :disabled="busy || (!!pending && pending.action !== 'complete')" @click="execute('complete')">{{ warehouseReturn ? '提交本批实收' : '完成处理' }}</el-button>
|
||||
<Teleport :to="actionsTarget || 'body'" :disabled="!actionsTarget" defer>
|
||||
<div class="density-actions density-actions--footer">
|
||||
<el-button v-if="allowed('START_PROCESSING')" data-test="start" type="primary" :disabled="busy || (!!pending && pending.action !== 'start-processing')" @click="execute('start-processing')">{{ '开始处理' }}</el-button>
|
||||
<el-button v-if="allowed('CANCEL_EXCEPTION')" data-test="cancel" type="danger" plain :disabled="busy || (!!pending && pending.action !== 'cancel')" @click="execute('cancel')">{{ '取消异常' }}</el-button>
|
||||
<el-button v-if="allowed('COMPLETE_EXCEPTION')" data-test="complete" type="primary" :disabled="busy || (!!pending && pending.action !== 'complete')" @click="execute('complete')">{{ warehouseReturn ? '提交本批实收' : '完成处理' }}</el-button>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.exception-processing-form { margin-top: var(--admin-space-5); }
|
||||
.exception-detail-fields :deep(.el-descriptions__table) { table-layout: fixed; }
|
||||
.exception-return-history { margin: 12px 0; }
|
||||
.exception-return-history h3 { margin: 0 0 8px; font-size: 14px; }
|
||||
.exception-return-batch + .exception-return-batch { margin-top: 12px; }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import '@/styles/adminWorkbench.css'
|
||||
import '@/styles/adminDensity.css'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { FileUp } from '@lucide/vue'
|
||||
import { FileUp, ClipboardList, Clock, CircleCheck } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
@@ -158,14 +158,27 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage class="admin-density admin-workbench">
|
||||
<AdminPage spacing="compact" class="customer-visits-page admin-density admin-workbench">
|
||||
<section class="visit-summary-grid" aria-label="回访统计">
|
||||
<article class="workbench-metric workbench-tone is-compact" data-tone="blue">
|
||||
<div class="workbench-metric-heading"><span class="workbench-icon is-compact"><ClipboardList aria-hidden="true" /></span><h2 class="workbench-metric-label">可回访订单</h2></div>
|
||||
<div class="workbench-metric-value"><strong>{{ summary.visitable.toLocaleString('zh-CN') }}</strong><span>单</span></div>
|
||||
</article>
|
||||
<article class="workbench-metric workbench-tone is-compact" data-tone="purple">
|
||||
<div class="workbench-metric-heading"><span class="workbench-icon is-compact"><Clock aria-hidden="true" /></span><h2 class="workbench-metric-label">待回访</h2></div>
|
||||
<div class="workbench-metric-value"><strong>{{ summary.pending.toLocaleString('zh-CN') }}</strong><span>单</span></div>
|
||||
</article>
|
||||
<article class="workbench-metric workbench-tone is-compact" data-tone="green">
|
||||
<div class="workbench-metric-heading"><span class="workbench-icon is-compact"><CircleCheck aria-hidden="true" /></span><h2 class="workbench-metric-label">已完成回访</h2></div>
|
||||
<div class="workbench-metric-value"><strong>{{ summary.completed.toLocaleString('zh-CN') }}</strong><span>单</span></div>
|
||||
</article>
|
||||
</section>
|
||||
<ListPageLayout aria-label="客户回访列表">
|
||||
|
||||
|
||||
<section><div class="workbench-table is-page-surface"><DataTable appearance="integrated" column-settings-mode="show" :results-visible="!error" v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" preference-key="overseas-customer-visits" row-key="id" empty-text="暂无可回访派送单" @update:page="load()" @update:page-size="page = 1; load()">
|
||||
<template #toolbar-start><h1>客户回访</h1></template>
|
||||
<template #filters>
|
||||
<dl class="visit-summary density-detail-overview" aria-label="回访统计"><div><dt>可回访订单</dt><dd>{{ summary.visitable }}</dd></div><div><dt>待回访</dt><dd>{{ summary.pending }}</dd></div><div><dt>已完成回访</dt><dd>{{ summary.completed }}</dd></div></dl>
|
||||
<QueryToolbar v-model="filters" :fields="fields" :loading="loading" ariaLabel="客户回访筛选" preference-key="overseas-customer-visits-query" :search-on-reset="false" @search="search" @reset="reset" />
|
||||
</template>
|
||||
<template #notices><template v-if="error"><LoadErrorState title="客户回访列表加载失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState></template></template>
|
||||
@@ -212,7 +225,8 @@ onMounted(load)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.visit-summary { flex-wrap: wrap; }
|
||||
.admin-workbench.customer-visits-page.admin-page.is-spacing-compact { gap: 0; }
|
||||
.visit-summary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--workbench-grid-gap); }
|
||||
.visit-order-summary.density-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); margin-bottom: var(--admin-space-5); }
|
||||
.inbound-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--admin-space-3); width: 100%; }
|
||||
.inbound-option { display: grid; min-width: 0; gap: var(--admin-space-2); padding: var(--admin-space-3); border: 1px solid var(--workbench-border); border-radius: var(--admin-radius); cursor: pointer; color: var(--workbench-muted); overflow-wrap: anywhere; }
|
||||
@@ -222,5 +236,5 @@ onMounted(load)
|
||||
.attachment-list, .attachment-empty { width: 100%; margin-top: var(--admin-space-3); }
|
||||
.attachment-list > div { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: var(--admin-space-3); padding: var(--admin-space-2) 0; border-bottom: 1px solid var(--workbench-border); overflow-wrap: anywhere; }
|
||||
.attachment-list small, .attachment-empty { color: var(--workbench-muted); font-size: var(--admin-font-caption); }
|
||||
@media (max-width: 720px) { .visit-summary { flex-wrap: wrap; } .visit-order-summary.density-fields, .inbound-options { grid-template-columns: 1fr; } .file-tip { display: block; margin: 8px 0 0; } }
|
||||
@media (max-width: 720px) { .visit-summary-grid { grid-template-columns: 1fr; } .visit-order-summary.density-fields, .inbound-options { grid-template-columns: 1fr; } .file-tip { display: block; margin: 8px 0 0; } }
|
||||
</style>
|
||||
|
||||
@@ -110,7 +110,8 @@ onMounted(load)
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
<AppDialog class="admin-density-dialog admin-workbench is-dialog" v-model="detailOpen" title="异常订单详情" width="min(760px, calc(100vw - 28px))" :show-confirm="false" cancel-text="关闭">
|
||||
<ExceptionProcessingPanel v-if="detailRow" :customer-id="detailRow.customerId" :exception-no="detailRow.exceptionNo" :active="detailOpen" @changed="load()" />
|
||||
<ExceptionProcessingPanel v-if="detailRow" :customer-id="detailRow.customerId" :exception-no="detailRow.exceptionNo" :active="detailOpen" actions-target="#exception-detail-actions" @changed="load()" />
|
||||
<template #footer="{ close }"><div class="density-actions density-actions--footer"><el-button @click="close">关闭</el-button><div id="exception-detail-actions"></div></div></template>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user