优化
This commit is contained in:
@@ -12,12 +12,13 @@ describe('贴标附件回显', () => {
|
||||
const wrapper = mountPanel(); await flushPromises()
|
||||
const rows = wrapper.findAll('.el-table__body tbody tr')
|
||||
expect(rows).toHaveLength(4)
|
||||
expect(wrapper.text()).toContain('箱唛.pdf'); expect(wrapper.text()).toContain('SKU.pdf')
|
||||
expect(wrapper.text()).toContain('面单.pdf'); expect(wrapper.text()).toContain('明细.pdf')
|
||||
expect(wrapper.text()).toContain('BX-1 / SKU-A')
|
||||
expect(wrapper.text()).toContain('箱唛贴标文件'); expect(wrapper.text()).toContain('SKU 贴标文件')
|
||||
expect(wrapper.text()).toContain('面单文件'); expect(wrapper.text()).toContain('明细文件')
|
||||
expect(wrapper.text()).not.toContain('BX-1 / SKU-A'); expect(wrapper.text()).not.toContain('文件名称'); expect(wrapper.text()).not.toContain('查看'); expect(wrapper.text()).not.toContain('箱唛.pdf')
|
||||
expect(wrapper.findAll('thead th')).toHaveLength(2)
|
||||
expect(getRelabelOrder).not.toHaveBeenCalled()
|
||||
})
|
||||
it('预览请求使用申请号、文件分类及明细编号,并可重试失败请求', async () => {
|
||||
it('下载请求使用申请号、文件分类及明细编号,并可重试失败请求', async () => {
|
||||
vi.mocked(downloadRelabelDocument).mockRejectedValue(new Error('文件暂不可用'))
|
||||
const wrapper = mountPanel(); await flushPromises()
|
||||
const rows = wrapper.findAll('.el-table__body tbody tr')
|
||||
@@ -32,6 +33,6 @@ describe('贴标附件回显', () => {
|
||||
const wrapper = mount(RelabelDocumentsPanel, { props: { relabelNo: 'RL-1' }, global: { plugins: [ElementPlus] } }); await flushPromises()
|
||||
expect(wrapper.get('[role="alert"]').text()).toContain('资料加载失败')
|
||||
await wrapper.get('[role="alert"] button').trigger('click'); await flushPromises()
|
||||
expect(wrapper.text()).toContain('箱唛.pdf')
|
||||
expect(wrapper.text()).toContain('箱唛贴标文件')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,9 +7,6 @@ const loaded = ref<RelabelDetailDto | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const busy = ref(false)
|
||||
const previewUrl = ref('')
|
||||
const previewOpen = ref(false)
|
||||
const previewTitle = ref('')
|
||||
let generation = 0
|
||||
const current = computed(() => props.detail || loaded.value)
|
||||
/** 各申请分别加载,失败只影响资料区,不阻断订单详情。 */
|
||||
@@ -22,28 +19,26 @@ async function load() {
|
||||
catch (cause) { if (ticket === generation) error.value = cause instanceof Error ? cause.message : '贴标资料加载失败' }
|
||||
finally { if (ticket === generation) loading.value = false }
|
||||
}
|
||||
watch(() => [props.relabelNo, props.detail], () => { clearPreview(); void load() }, { immediate: true })
|
||||
interface DocumentRow { key: string; kind: string; fileId: string; fileName: string; scope: string; itemId?: string; context?: string }
|
||||
watch(() => [props.relabelNo, props.detail], () => { void load() }, { immediate: true })
|
||||
interface DocumentRow { key: string; kind: string; fileId: string; fileName: string; scope: string; itemId?: string }
|
||||
const documents = computed<DocumentRow[]>(() => {
|
||||
const result: DocumentRow[] = []
|
||||
const application = current.value?.application
|
||||
if (!application) return result
|
||||
// 旧总文件仅作为兼容回显,避免与已分类型的总文件重复展示。
|
||||
const add = (kind: string, id: string | null | undefined, name: string | null | undefined, scope: string, itemId?: string, context?: string) => {
|
||||
if (id) result.push({ key: scope + ':' + (itemId || '') + ':' + id, kind, fileId: id, fileName: name || '未命名文件', scope, itemId, context })
|
||||
const add = (kind: string, id: string | null | undefined, name: string | null | undefined, scope: string, itemId?: string) => {
|
||||
if (id) result.push({ key: scope + ':' + (itemId || '') + ':' + id, kind, fileId: id, fileName: name || '未命名文件', scope, itemId })
|
||||
}
|
||||
add('箱唛总文件', application.cartonTotalFileId, application.cartonTotalFileName, 'CARTON_TOTAL')
|
||||
add('SKU 标签总文件', application.skuTotalFileId, application.skuTotalFileName, 'SKU_TOTAL')
|
||||
add('箱唛贴标文件', application.cartonTotalFileId, application.cartonTotalFileName, 'CARTON_TOTAL')
|
||||
add('SKU 贴标文件', application.skuTotalFileId, application.skuTotalFileName, 'SKU_TOTAL')
|
||||
if (application.totalFileId !== application.cartonTotalFileId && application.totalFileId !== application.skuTotalFileId)
|
||||
add('贴标总文件', application.totalFileId, application.totalFileName, 'TOTAL')
|
||||
add('面单文件', application.shippingLabelFileId, application.shippingLabelFileName, 'SHIPPING')
|
||||
for (const item of current.value?.items || []) add('明细文件', item.relabelFileId, item.relabelFileName, 'ITEM', String(item.id), [item.cartonMark, item.skuCode].filter(Boolean).join(' / '))
|
||||
for (const item of current.value?.items || []) add('明细文件', item.relabelFileId, item.relabelFileName, 'ITEM', String(item.id))
|
||||
return result
|
||||
})
|
||||
/** 切换申请及关闭预览时释放文件对象。 */
|
||||
function clearPreview() { if (previewUrl.value) URL.revokeObjectURL(previewUrl.value); previewUrl.value = ''; previewOpen.value = false }
|
||||
/** 使用带身份认证的下载接口,预览和保存共享同一归属校验。 */
|
||||
async function openFile(key: string, download = false) {
|
||||
/** 使用带身份认证的下载接口,保留文件归属校验和原始文件名。 */
|
||||
async function downloadFile(key: string) {
|
||||
const row = documents.value.find(document => document.key === key)
|
||||
if (!row || busy.value) return
|
||||
busy.value = true
|
||||
@@ -51,32 +46,25 @@ async function openFile(key: string, download = false) {
|
||||
try {
|
||||
const blob = await downloadRelabelDocument(props.relabelNo, row.fileId, row.scope, row.itemId)
|
||||
if (ticket !== generation) return
|
||||
if (download) {
|
||||
const url = URL.createObjectURL(blob); const link = document.createElement('a')
|
||||
link.href = url; link.download = row.fileName; link.click(); window.setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||
} else { clearPreview(); previewUrl.value = URL.createObjectURL(blob); previewTitle.value = row.fileName; previewOpen.value = true }
|
||||
const url = URL.createObjectURL(blob); const link = document.createElement('a')
|
||||
link.href = url; link.download = row.fileName; link.click(); window.setTimeout(() => URL.revokeObjectURL(url), 1000)
|
||||
} catch (cause) { ElMessage.error(cause instanceof Error ? cause.message : '文件读取失败,请重试') }
|
||||
finally { busy.value = false }
|
||||
}
|
||||
onBeforeUnmount(() => { ++generation; clearPreview() })
|
||||
onBeforeUnmount(() => { ++generation })
|
||||
</script>
|
||||
<template>
|
||||
<section class="relabel-documents" aria-label="客户上传的贴标文件">
|
||||
<h3>贴标文件<span v-if="relabelNo"> · {{ relabelNo }}</span></h3>
|
||||
<h3>贴标文件</h3>
|
||||
<p v-if="loading">资料加载中…</p>
|
||||
<p v-else-if="error" role="alert">{{ error }} <el-button link type="primary" @click="load">重试</el-button></p>
|
||||
<el-table v-else :data="documents" row-key="key" border empty-text="暂无上传的贴标文件">
|
||||
<el-table-column prop="kind" label="文件类型" width="150" />
|
||||
<el-table-column prop="context" label="对应箱唛 / SKU" min-width="180" />
|
||||
<el-table-column prop="fileName" label="文件名称" min-width="240" />
|
||||
<el-table-column label="操作" width="150"><template #default="{ row }"><el-button link type="primary" :disabled="busy" @click="openFile(String(row.key))">查看</el-button><el-button link type="primary" :disabled="busy" @click="openFile(String(row.key), true)">下载</el-button></template></el-table-column>
|
||||
<el-table-column prop="kind" label="文件类型" min-width="150" />
|
||||
<el-table-column label="操作" width="90"><template #default="{ row }"><el-button link type="primary" :disabled="busy" @click="downloadFile(String(row.key))">下载</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<el-dialog v-model="previewOpen" :title="previewTitle" width="80%" append-to-body @closed="clearPreview"><iframe v-if="previewUrl" :src="previewUrl" :title="previewTitle" class="relabel-document-preview" /></el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
<style scoped>
|
||||
.relabel-documents { margin: 16px 0; }
|
||||
.relabel-documents h3 { margin: 0 0 12px; font-size: 14px; }
|
||||
.relabel-documents h3 span { font-weight: 400; }
|
||||
.relabel-document-preview { width: 100%; height: 70vh; border: 0; }
|
||||
</style>
|
||||
|
||||
@@ -1,96 +1,87 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import { defineComponent, h } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import WarehouseTransferReviewDialog from './WarehouseTransferReviewDialog.vue'
|
||||
import { getWarehouseTransfer, reviewWarehouseTransfer } from '../api/warehouseTransfers'
|
||||
|
||||
vi.mock('../api/warehouseTransfers', async importOriginal => ({
|
||||
...await importOriginal<typeof import('../api/warehouseTransfers')>(),
|
||||
const reviewedTransfer = { id: '1', transferNo: 'WT-1', packageCount: 2, needRelabel: false, status: 'WAIT_PDA_RECEIVING', items: [], cartons: [], pallets: [], relabelBindings: [] }
|
||||
|
||||
vi.mock('../api/warehouseTransfers', () => ({
|
||||
getWarehouseTransfer: vi.fn(),
|
||||
reviewWarehouseTransfer: vi.fn(),
|
||||
}))
|
||||
vi.mock('element-plus', () => ({ ElMessage: { success: vi.fn(), error: vi.fn() } }))
|
||||
|
||||
const AppDialogStub = defineComponent({
|
||||
name: 'AppDialog',
|
||||
props: { modelValue: Boolean },
|
||||
props: { modelValue: Boolean, confirmLoading: Boolean, title: String, confirmText: String, cancelText: String },
|
||||
emits: ['confirm', 'update:modelValue'],
|
||||
setup(props, { slots }) {
|
||||
return () => props.modelValue ? h('section', { class: 'dialog' }, slots.default?.()) : null
|
||||
setup(props, { slots, emit }) {
|
||||
return () => props.modelValue ? h('section', [
|
||||
h('h2', props.title), slots.default?.(),
|
||||
h('button', { class: 'back', disabled: props.confirmLoading, onClick: () => emit('update:modelValue', false) }, props.cancelText),
|
||||
h('button', { class: 'approve', disabled: props.confirmLoading, onClick: () => emit('confirm') }, props.confirmText),
|
||||
]) : null
|
||||
},
|
||||
})
|
||||
|
||||
const DataTableStub = defineComponent({
|
||||
name: 'DataTable',
|
||||
props: { data: { type: Array, default: () => [] } },
|
||||
setup(props) {
|
||||
return () => h('div', { class: 'data-table' }, JSON.stringify(props.data))
|
||||
},
|
||||
})
|
||||
|
||||
function detail(transferNo: string) {
|
||||
return {
|
||||
id: `id-${transferNo}`,
|
||||
transferNo,
|
||||
packageCount: 2,
|
||||
needRelabel: false,
|
||||
status: 'PENDING_REVIEW',
|
||||
items: [{ id: 'item-1', skuCode: 'SKU-1', productNameZh: '测试货物', packageCount: 2 }],
|
||||
cartons: [],
|
||||
pallets: [],
|
||||
relabelBindings: [],
|
||||
}
|
||||
}
|
||||
|
||||
function mountDialog(props: { modelValue: boolean; customerId: string; customerName: string; transferNo: string }) {
|
||||
function mountDialog() {
|
||||
return mount(WarehouseTransferReviewDialog, {
|
||||
props,
|
||||
global: {
|
||||
stubs: {
|
||||
AppDialog: AppDialogStub,
|
||||
DataTable: DataTableStub,
|
||||
LoadErrorState: true,
|
||||
ApiErrorMeta: true,
|
||||
ElForm: { template: '<form><slot /></form>' },
|
||||
ElFormItem: { template: '<label><slot /></label>' },
|
||||
ElRadioGroup: { template: '<div><slot /></div>' },
|
||||
ElRadioButton: { template: '<button type="button"><slot /></button>' },
|
||||
ElInput: { template: '<textarea />' },
|
||||
},
|
||||
},
|
||||
props: { modelValue: true, customerId: '20001', customerName: '测试客户', transferNo: 'WT-1' },
|
||||
global: { stubs: { AppDialog: AppDialogStub } },
|
||||
})
|
||||
}
|
||||
|
||||
describe('WarehouseTransferReviewDialog', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(getWarehouseTransfer).mockReset()
|
||||
vi.mocked(reviewWarehouseTransfer).mockReset()
|
||||
beforeEach(() => { vi.clearAllMocks() })
|
||||
|
||||
it('shows a concise confirmation without fetching detail tables or review inputs', () => {
|
||||
const wrapper = mountDialog()
|
||||
expect(wrapper.text()).toContain('仓储转运单审核')
|
||||
expect(wrapper.text()).toContain('审核通过后,转运单将进入海外仓收货打托作业。')
|
||||
expect(wrapper.find('.back').text()).toBe('返回')
|
||||
expect(wrapper.find('.approve').text()).toBe('审核通过')
|
||||
expect(wrapper.find('textarea, input, table').exists()).toBe(false)
|
||||
expect(getWarehouseTransfer).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('loads and renders review details when first mounted already open', async () => {
|
||||
vi.mocked(getWarehouseTransfer).mockResolvedValue(detail('WT-1'))
|
||||
|
||||
const wrapper = mountDialog({ modelValue: true, customerId: '20001', customerName: '测试客户', transferNo: 'WT-1' })
|
||||
await flushPromises()
|
||||
|
||||
expect(getWarehouseTransfer).toHaveBeenCalledTimes(1)
|
||||
expect(getWarehouseTransfer).toHaveBeenCalledWith('20001', 'WT-1')
|
||||
expect(wrapper.text()).toContain('WT-1')
|
||||
expect(wrapper.text()).toContain('测试货物')
|
||||
wrapper.unmount()
|
||||
it('return closes without approving or rejecting the order', async () => {
|
||||
const wrapper = mountDialog()
|
||||
await wrapper.find('.back').trigger('click')
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([[false]])
|
||||
expect(reviewWarehouseTransfer).not.toHaveBeenCalled()
|
||||
expect(wrapper.emitted('reviewed')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('reloads details when an open dialog switches to another transfer', async () => {
|
||||
vi.mocked(getWarehouseTransfer)
|
||||
.mockResolvedValueOnce(detail('WT-1'))
|
||||
.mockResolvedValueOnce(detail('WT-2'))
|
||||
|
||||
const wrapper = mountDialog({ modelValue: true, customerId: '20001', customerName: '测试客户', transferNo: 'WT-1' })
|
||||
it('approves in the selected customer scope once and refreshes only after success', async () => {
|
||||
let finish!: () => void
|
||||
vi.mocked(reviewWarehouseTransfer).mockReturnValue(new Promise(resolve => { finish = () => resolve(reviewedTransfer) }))
|
||||
const wrapper = mountDialog()
|
||||
const dialog = wrapper.findComponent(AppDialogStub)
|
||||
dialog.vm.$emit('confirm')
|
||||
dialog.vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
await wrapper.setProps({ customerId: '20002', transferNo: 'WT-2' })
|
||||
expect(reviewWarehouseTransfer).toHaveBeenCalledExactlyOnceWith('20001', 'WT-1', 'APPROVE')
|
||||
expect(dialog.props('confirmLoading')).toBe(true)
|
||||
expect(wrapper.emitted('reviewed')).toBeUndefined()
|
||||
finish()
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('reviewed')).toHaveLength(1)
|
||||
expect(wrapper.emitted('update:modelValue')).toEqual([[false]])
|
||||
expect(dialog.props('confirmLoading')).toBe(false)
|
||||
})
|
||||
|
||||
expect(getWarehouseTransfer).toHaveBeenLastCalledWith('20002', 'WT-2')
|
||||
expect(wrapper.text()).toContain('WT-2')
|
||||
wrapper.unmount()
|
||||
it('keeps the dialog open and allows retry after an approval failure', async () => {
|
||||
vi.mocked(reviewWarehouseTransfer).mockRejectedValueOnce(new Error('审核失败')).mockResolvedValueOnce(reviewedTransfer)
|
||||
const wrapper = mountDialog()
|
||||
await wrapper.find('.approve').trigger('click')
|
||||
await flushPromises()
|
||||
expect(ElMessage.error).toHaveBeenCalled()
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect(wrapper.emitted('reviewed')).toBeUndefined()
|
||||
expect(wrapper.findComponent(AppDialogStub).props('confirmLoading')).toBe(false)
|
||||
await wrapper.find('.approve').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.emitted('reviewed')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,87 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { CheckCircle2, XCircle } from '@lucide/vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import { getApiErrorDetails } from '@/shared/api/http'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import { getWarehouseTransfer, reviewWarehouseTransfer, type WarehouseTransferDetailDto } from '../api/warehouseTransfers'
|
||||
import { reviewWarehouseTransfer } from '../api/warehouseTransfers'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean; customerId: string; customerName: string; transferNo: string }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean]; reviewed: [] }>()
|
||||
const open = computed({ get: () => props.modelValue, set: (value) => emit('update:modelValue', value) })
|
||||
const detail = ref<WarehouseTransferDetailDto | null>(null)
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref<ApiErrorDetails | null>(null)
|
||||
const form = reactive<{ decision: 'APPROVE' | 'REJECT'; remark: string }>({ decision: 'APPROVE', remark: '' })
|
||||
const itemColumns: DataTableColumn[] = [
|
||||
{ key: 'skuCode', label: 'SKU', minWidth: 140 }, { key: 'productNameZh', label: '品名', minWidth: 160 },
|
||||
{ key: 'packageCount', label: '箱数', width: 80, align: 'right' }, { key: 'shippingMark', label: '客户箱唛', minWidth: 160 },
|
||||
]
|
||||
const bindingColumns: DataTableColumn[] = [
|
||||
{ key: 'operationType', label: '换标类型', minWidth: 120 }, { key: 'customerCartonMark', label: '客户箱唛', minWidth: 150 },
|
||||
{ key: 'originalSku', label: '原 SKU', minWidth: 120 }, { key: 'newSku', label: '新 SKU', minWidth: 120 },
|
||||
{ key: 'quantity', label: '数量', width: 80, align: 'right' }, { key: 'fbaCartonMark', label: '新箱唛', minWidth: 150 },
|
||||
]
|
||||
const palletColumns: DataTableColumn[] = [
|
||||
{ key: 'sequenceNo', label: '托序', width: 70, align: 'center' }, { key: 'palletNo', label: '托盘号', minWidth: 150 },
|
||||
{ key: 'cartonCount', label: '箱数', width: 80, align: 'right' }, { key: 'volumeCbm', label: '托盘总体积(m³)', width: 150, align: 'right' },
|
||||
{ key: 'grossWeightKg', label: '托盘总重量(kg)', width: 150, align: 'right' },
|
||||
]
|
||||
|
||||
async function load() {
|
||||
if (!props.customerId || !props.transferNo) return
|
||||
loading.value = true; error.value = null; detail.value = null
|
||||
try { detail.value = await getWarehouseTransfer(props.customerId, props.transferNo) }
|
||||
catch (cause) { error.value = getApiErrorDetails(cause, '转运单审核资料加载失败,请重试') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
async function submit() {
|
||||
if (saving.value) return
|
||||
if (form.decision === 'REJECT' && !form.remark.trim()) { ElMessage.warning('驳回时请填写审核原因'); return }
|
||||
if (saving.value || !open.value || !props.customerId || !props.transferNo) return
|
||||
saving.value = true
|
||||
try {
|
||||
await reviewWarehouseTransfer(props.customerId, props.transferNo, form.decision, form.remark.trim() || undefined)
|
||||
ElMessage.success(form.decision === 'APPROVE' ? '转运单审核通过,已推送 PDA 收货打托' : '转运单已驳回')
|
||||
open.value = false; emit('reviewed')
|
||||
} catch (cause) { ElMessage.error(getApiErrorDetails(cause, '审核提交失败,请重试').message) }
|
||||
finally { saving.value = false }
|
||||
await reviewWarehouseTransfer(props.customerId, props.transferNo, 'APPROVE')
|
||||
ElMessage.success('转运单审核通过,已推送 PDA 收货打托')
|
||||
open.value = false
|
||||
emit('reviewed')
|
||||
} catch (cause) {
|
||||
ElMessage.error(getApiErrorDetails(cause, '审核提交失败,请重试').message)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => [props.modelValue, props.customerId, props.transferNo] as const,
|
||||
([value]) => {
|
||||
if (!value) return
|
||||
form.decision = 'APPROVE'
|
||||
form.remark = ''
|
||||
void load()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog class="admin-density-dialog" v-model="open" title="审核仓储转运单" width="min(920px, calc(100vw - 28px))" confirm-text="提交审核" :confirm-loading="saving" :loading="loading" @confirm="submit">
|
||||
<LoadErrorState v-if="error" compact title="审核资料加载失败" :message="error.message" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<el-form v-else-if="detail" label-position="top" class="review-form density-form">
|
||||
<div class="review-heading"><strong>{{ detail.transferNo }}</strong><span>{{ customerName }} · {{ detail.packageCount }} 箱 · {{ detail.needRelabel ? '需要换标' : '无需换标' }}</span></div>
|
||||
<h3>货物明细</h3><DataTable :data="detail.items" :columns="itemColumns" :show-pagination="false" row-key="id" empty-text="暂无货物明细" />
|
||||
<template v-if="detail.needRelabel"><h3>换标绑定明细</h3><DataTable :data="detail.relabelBindings" :columns="bindingColumns" :show-pagination="false" row-key="customerCartonMark" empty-text="暂无换标绑定明细" /></template>
|
||||
<template v-if="detail.pallets?.length"><h3>PDA 打托实测</h3><DataTable :data="detail.pallets" :columns="palletColumns" :show-pagination="false" row-key="id" empty-text="暂无打托数据" /></template>
|
||||
<el-form-item label="审核结论" required><el-radio-group v-model="form.decision"><el-radio-button value="APPROVE"><CheckCircle2 :size="15" />审核通过</el-radio-button><el-radio-button value="REJECT"><XCircle :size="15" />驳回</el-radio-button></el-radio-group></el-form-item>
|
||||
<el-form-item :label="form.decision === 'REJECT' ? '驳回原因' : '审核备注'" :required="form.decision === 'REJECT'"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="1000" show-word-limit placeholder="请输入审核备注(选填)" /></el-form-item>
|
||||
</el-form>
|
||||
<AppDialog
|
||||
v-model="open"
|
||||
class="admin-density-dialog density-confirm-dialog"
|
||||
title="仓储转运单审核"
|
||||
width="min(420px, calc(100vw - 28px))"
|
||||
align-center
|
||||
cancel-text="返回"
|
||||
confirm-text="审核通过"
|
||||
:confirm-loading="saving"
|
||||
@confirm="submit"
|
||||
>
|
||||
<div>审核通过后,转运单将进入海外仓收货打托作业。</div>
|
||||
</AppDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.review-form { display: grid; gap: var(--admin-space-5); }
|
||||
.review-heading { display: flex; align-items: baseline; justify-content: space-between; gap: var(--admin-space-5); }
|
||||
.review-heading span { color: var(--cc-muted); font-size: var(--admin-font-caption); }
|
||||
h3 { margin: 2px 0 0; font-size: var(--admin-font-section); }
|
||||
.review-form :deep(.el-form-item) { margin: var(--admin-space-1) 0 0; }
|
||||
.review-form :deep(.el-radio-button__inner) { display: inline-flex; align-items: center; gap: var(--admin-space-2); }
|
||||
</style>
|
||||
|
||||
@@ -53,6 +53,10 @@
|
||||
.admin-density-dialog.el-dialog .el-dialog__title { font-size: var(--admin-font-title, 16px); line-height: 24px; font-weight: 600; }
|
||||
.admin-density-dialog.el-dialog .el-dialog__body { min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: var(--admin-space-5, 12px) var(--admin-space-6, 16px); }
|
||||
.admin-density-dialog.el-dialog .el-dialog__footer { flex: 0 0 auto; padding: var(--admin-space-3, 8px) var(--admin-space-6, 16px); border-top: 1px solid var(--cc-border); }
|
||||
/* Compact confirmations keep one message and right-aligned actions without section dividers. */
|
||||
.admin-density-dialog.density-confirm-dialog.el-dialog:not(.is-fullscreen) { margin: auto; }
|
||||
.admin-density-dialog.density-confirm-dialog.el-dialog .el-dialog__header { border-bottom: 0; }
|
||||
.admin-density-dialog.density-confirm-dialog.el-dialog .el-dialog__footer { border-top: 0; padding-bottom: var(--admin-space-5, 12px); }
|
||||
.admin-density-dialog.el-dialog .el-table { font-size: var(--admin-font-body, 13px); }
|
||||
.admin-density-dialog.el-dialog .el-table__cell { height: var(--admin-row-height, 36px); padding: var(--admin-space-1, 4px) 0; }
|
||||
.admin-density-dialog.el-dialog .el-table .cell { line-height: 22px; }
|
||||
|
||||
@@ -171,6 +171,15 @@ const cssRoot = (css: string) => {
|
||||
}
|
||||
|
||||
describe('approved Admin density ownership', () => {
|
||||
it('opts transfer approval into the shared centered confirmation appearance', () => {
|
||||
const dialog = read('modules/overseas-warehouse/components/WarehouseTransferReviewDialog.vue')
|
||||
const css = read('styles/adminDensity.css')
|
||||
expect(dialog).toContain('class="admin-density-dialog density-confirm-dialog"')
|
||||
expect(dialog).toContain('align-center')
|
||||
expect(css).toContain('.admin-density-dialog.density-confirm-dialog.el-dialog:not(.is-fullscreen) { margin: auto; }')
|
||||
expect(css).toContain('.admin-density-dialog.density-confirm-dialog.el-dialog .el-dialog__header { border-bottom: 0; }')
|
||||
expect(css).toContain('.admin-density-dialog.density-confirm-dialog.el-dialog .el-dialog__footer { border-top: 0;')
|
||||
})
|
||||
it('keeps appointment dialog content scrollable with fixed controls and unclipped calendar days', () => {
|
||||
const declarations = (file: string, selector: string) => {
|
||||
const values: Record<string, string> = {}
|
||||
@@ -443,6 +452,10 @@ describe('approved Admin density ownership', () => {
|
||||
for (const node of nodes) {
|
||||
if (node.type !== 1) continue
|
||||
const classes = classesOf(node)
|
||||
if (classes.includes('density-confirm-dialog')) {
|
||||
expect(classes, `${file}: confirmation density`).toContain('admin-density-dialog')
|
||||
checked += 1
|
||||
}
|
||||
if (node.tag === 'DataTable' || node.tag === 'LoadingListDetailDrawer' || classes.some(name => ['density-action', 'density-actions', 'density-tabs', 'density-fields', 'density-table', 'density-tab-icon', 'density-back'].includes(name))) {
|
||||
expect(insideDensity, `${file}: ${classes.join(' ')} lacks its density ancestor`).toBe(true)
|
||||
checked += 1
|
||||
|
||||
Reference in New Issue
Block a user