优化
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { Plus } from '@lucide/vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
@@ -16,6 +17,7 @@ import { SMART_LOADING_CHANNEL_GROUPS } from '../smartLoadingChannelGroups'
|
||||
const props = withDefaults(defineProps<{
|
||||
loadWarehouseOptions?: () => Promise<PlatformWarehouseSummaryResource[]>
|
||||
successMessage?: string
|
||||
dense?: boolean
|
||||
}>(), { successMessage: '模拟柜已创建,可先排货后关联真实订舱柜' })
|
||||
const emit = defineEmits<{ created: [] }>()
|
||||
const warehouses = ref<PlatformWarehouseSummaryResource[]>([])
|
||||
@@ -93,7 +95,7 @@ async function saveSimulation() {
|
||||
|
||||
<template>
|
||||
<el-button type="primary" @click="openSimulation"><Plus :size="16" />新增模拟柜</el-button>
|
||||
<AppDialog v-model="simulationOpen" title="新增模拟柜" width="min(560px, calc(100vw - 28px))" confirm-text="创建模拟柜" cancel-text="取消" :confirm-loading="simulationSaving" @confirm="saveSimulation">
|
||||
<AppDialog :class="{ 'admin-density-dialog': dense }" v-model="simulationOpen" title="新增模拟柜" width="min(560px, calc(100vw - 28px))" confirm-text="创建模拟柜" cancel-text="取消" :confirm-loading="simulationSaving" @confirm="saveSimulation">
|
||||
<el-alert title="模拟柜用于先排货,服务端会生成稳定的柜次号;此处不填写集装箱号和船期。" type="info" :closable="false" />
|
||||
<el-form ref="simulationFormRef" :model="simulationForm" :rules="simulationRules" label-position="top" class="simulation-form">
|
||||
<el-form-item label="装柜仓库" prop="warehouseId"><el-select :model-value="simulationForm.warehouseId" filterable placeholder="请选择国内装柜仓库" :loading="warehouseLoading" @update:model-value="selectSimulationWarehouse"><el-option v-for="warehouse in warehouses" :key="String(warehouse.id)" :label="warehouse.warehouseName" :value="String(warehouse.id)" /></el-select></el-form-item>
|
||||
|
||||
@@ -45,7 +45,7 @@ class ResizeObserverStub implements ResizeObserver {
|
||||
}
|
||||
|
||||
const tenant: PlatformTenantResource = {
|
||||
id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'ACTIVE', legalName: '柏悦物流有限公司',
|
||||
id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', customerNickname: '柏', status: 'ACTIVE', legalName: '柏悦物流有限公司',
|
||||
legalRepresentative: '张三', unifiedSocialCreditCode: '91440300MA5ABCDE12', contactName: '张三',
|
||||
contactPhone: '13800138000', contactEmail: 'ops@example.com', servicePackage: '基础版', contactWechat: 'baiyue-ops',
|
||||
salesOwnerName: '李四', paymentTermDays: 30, enterpriseType: '有限责任公司', establishedDate: '2020-01-01',
|
||||
|
||||
@@ -258,6 +258,7 @@ onBeforeUnmount(() => {
|
||||
<div class="panel-title"><div><h2>企业基本信息</h2><p>工商档案与合作配置</p></div><Building2 :size="19" /></div>
|
||||
<dl class="info-grid">
|
||||
<div><dt>公司名称</dt><dd>{{ displayText(customer.legalName || customer.tenantName) }}</dd></div>
|
||||
<div><dt>客户昵称</dt><dd>{{ displayText(customer.customerNickname) }}</dd></div>
|
||||
<div><dt>统一社会信用代码</dt><dd>{{ displayText(customer.unifiedSocialCreditCode) }}</dd></div>
|
||||
<div><dt>企业类型</dt><dd>{{ displayText(customer.enterpriseType) }}</dd></div>
|
||||
<div><dt>法定代表人</dt><dd>{{ displayText(customer.legalRepresentative) }}</dd></div>
|
||||
|
||||
@@ -84,7 +84,7 @@ class ResizeObserverStub implements ResizeObserver {
|
||||
}
|
||||
|
||||
const fullTenant: PlatformTenantResource = {
|
||||
id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'PROFILE_REQUIRED',
|
||||
id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', customerNickname: '柏', status: 'PROFILE_REQUIRED',
|
||||
legalName: '柏悦物流有限公司', legalRepresentative: '张三', unifiedSocialCreditCode: '91440300MA5ABCDE12',
|
||||
contactName: '张三', contactPhone: '13800138000', contactEmail: 'ops@example.com',
|
||||
servicePackage: '基础版', contactWechat: 'baiyue-ops', salesOwnerName: '李四', paymentTermDays: 30,
|
||||
@@ -177,6 +177,8 @@ describe('CustomersView', () => {
|
||||
|
||||
it('searches the customer list by customer name only', () => {
|
||||
expect(customersSource).toContain("label: '客户名称', placeholder: '请输入客户名称'")
|
||||
expect(customersSource).toContain("label: '客户昵称'")
|
||||
expect(customersSource).toContain('中文拼音大写首字母,如 SZWL')
|
||||
expect(customersSource).not.toContain('公司名称、客户编号或联系人')
|
||||
expect(customersSource).not.toContain('row.tenantCode.toLowerCase().includes(keyword)')
|
||||
expect(customersSource).not.toContain('String(row.id).includes(keyword)')
|
||||
|
||||
@@ -135,9 +135,9 @@ const queryFields: QueryField[] = [
|
||||
] },
|
||||
]
|
||||
|
||||
type CustomerForm = CreatePlatformTenantRequest & { unifiedSocialCreditCode: string }
|
||||
type CustomerForm = CreatePlatformTenantRequest & { customerNickname: string; unifiedSocialCreditCode: string }
|
||||
const emptyForm = (): CustomerForm => ({
|
||||
tenantName: '', legalName: '', legalRepresentative: '', unifiedSocialCreditCode: '', contactName: '', contactPhone: '',
|
||||
tenantName: '', customerNickname: '', legalName: '', legalRepresentative: '', unifiedSocialCreditCode: '', contactName: '', contactPhone: '',
|
||||
contactEmail: '', contactWechat: '', servicePackage: '基础版', salesOwnerName: '', paymentTermDays: 30,
|
||||
enterpriseType: '', establishedDate: null, registeredCapital: 100000, registeredCapitalCurrency: 'CNY',
|
||||
registeredAddress: '', internalRemark: '',
|
||||
@@ -145,6 +145,16 @@ const emptyForm = (): CustomerForm => ({
|
||||
const form = ref(emptyForm())
|
||||
const rules: FormRules = {
|
||||
tenantName: [{ required: true, message: '请输入公司名称', trigger: 'blur' }],
|
||||
customerNickname: [
|
||||
{ max: 32, message: '客户昵称最多 32 个字符', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (!editingId.value || (typeof value === 'string' && value.trim())) callback()
|
||||
else callback(new Error('请输入客户昵称'))
|
||||
},
|
||||
trigger: ['blur', 'change'],
|
||||
},
|
||||
],
|
||||
legalName: [{ required: true, message: '请输入营业执照上的企业名称', trigger: 'blur' }],
|
||||
legalRepresentative: [{ max: 64, message: '法定代表人最多 64 个字符', trigger: 'blur' }],
|
||||
unifiedSocialCreditCode: [
|
||||
@@ -199,6 +209,7 @@ const unifiedSocialCreditCodePattern = /^[0-9A-Z]{18}$/
|
||||
const customerColumns: DataTableColumn[] = [
|
||||
{ key: 'actions', label: '操作', width: 330, slotName: 'actions' },
|
||||
{ key: 'tenantCode', prop: 'tenantCode', label: '客户编号', width: 220, className: 'customer-code-column' },
|
||||
{ key: 'customerNickname', prop: 'customerNickname', label: '客户昵称', width: 120, formatter: (row: PlatformTenantResource) => displayText(row.customerNickname) },
|
||||
{ key: 'company', label: '公司名称', width: 250, slotName: 'company' },
|
||||
{ key: 'unifiedSocialCreditCode', prop: 'unifiedSocialCreditCode', label: '统一社会信用代码', width: 220, formatter: (row: PlatformTenantResource) => displayText(row.unifiedSocialCreditCode) },
|
||||
{ key: 'contactName', prop: 'contactName', label: '联系人', minWidth: 120, formatter: (row: PlatformTenantResource) => displayText(row.contactName) },
|
||||
@@ -394,7 +405,7 @@ function openCreate() {
|
||||
}
|
||||
function mapTenantToForm(row: PlatformTenantResource): CustomerForm {
|
||||
return {
|
||||
tenantName: row.tenantName, legalName: row.legalName || '', legalRepresentative: row.legalRepresentative || '', unifiedSocialCreditCode: row.unifiedSocialCreditCode || '',
|
||||
tenantName: row.tenantName, customerNickname: row.customerNickname || '', legalName: row.legalName || '', legalRepresentative: row.legalRepresentative || '', unifiedSocialCreditCode: row.unifiedSocialCreditCode || '',
|
||||
contactName: row.contactName || '', contactPhone: row.contactPhone || '', contactEmail: row.contactEmail || '',
|
||||
contactWechat: row.contactWechat || '', servicePackage: row.servicePackage || '基础版', salesOwnerName: row.salesOwnerName || '',
|
||||
paymentTermDays: Number(row.paymentTermDays ?? 0), enterpriseType: row.enterpriseType || '', establishedDate: row.establishedDate || null,
|
||||
@@ -574,7 +585,7 @@ async function saveCustomer() {
|
||||
let saved: PlatformTenantResource
|
||||
if (editingId.value) {
|
||||
const { unifiedSocialCreditCode: _immutable, ...payload } = form.value
|
||||
saved = await updatePlatformTenant(editingId.value, payload)
|
||||
saved = await updatePlatformTenant(editingId.value, { ...payload, customerNickname: form.value.customerNickname.trim() })
|
||||
} else {
|
||||
const { unifiedSocialCreditCode: _qualificationCreditCode, ...payload } = form.value
|
||||
const created = await createPlatformTenant(payload)
|
||||
@@ -1073,6 +1084,7 @@ onMounted(() => { void loadCustomers() })
|
||||
<el-form ref="formRef" v-loading="editorLoading" :model="form" :rules="rules" label-position="top" class="drawer-form" :disabled="editorLoading || saving || submitting || uploadingQualifications">
|
||||
<FormSection title="基础资料" description="维护工商登记和企业主体信息。"><div class="form-grid two-cols">
|
||||
<el-form-item label="公司名称" prop="tenantName" required><el-input v-model="form.tenantName" placeholder="请输入工商登记公司名称" maxlength="128" /></el-form-item>
|
||||
<el-form-item label="客户昵称" prop="customerNickname"><el-input v-model="form.customerNickname" placeholder="不填默认生成中文拼音大写首字母,如 SZWL" maxlength="32" /><small class="field-hint">用于非法定打印与简短展示;报关、合同等法定单据仍使用公司全称。</small></el-form-item>
|
||||
<el-form-item label="营业执照企业名称" prop="legalName" required><el-input v-model="form.legalName" placeholder="请输入营业执照上的企业名称" maxlength="128" /></el-form-item>
|
||||
<el-form-item label="法定代表人" prop="legalRepresentative"><el-input v-model="form.legalRepresentative" placeholder="请输入法定代表人姓名(选填)" maxlength="64" /></el-form-item>
|
||||
<el-form-item label="企业类型" prop="enterpriseType"><el-input v-model="form.enterpriseType" placeholder="如:有限责任公司" maxlength="64" /></el-form-item>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ArrowLeft } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -53,7 +54,7 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="inventory-detail-page">
|
||||
<AdminPage as="div" class="inventory-detail-page admin-density">
|
||||
<header class="detail-header">
|
||||
<el-button aria-label="返回整柜业务" @click="back"><ArrowLeft :size="16" />返回</el-button>
|
||||
<h1>{{ detail?.inventory.containerNo || '整柜库存详情' }}</h1>
|
||||
@@ -96,18 +97,18 @@ onMounted(load)
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.inventory-detail-page { display: grid; min-width: 0; gap: 16px; }
|
||||
.inventory-detail-page { display: grid; min-width: 0; gap: 12px; }
|
||||
.detail-header { display: flex; align-items: center; gap: 12px; min-height: 40px; }
|
||||
.detail-header h1 { margin: 0; font-size: 20px; letter-spacing: 0; }
|
||||
.detail-header h1 { margin: 0; font-size: 16px; letter-spacing: 0; }
|
||||
.detail-section, .transactions { min-width: 0; border: 1px solid var(--cc-border); background: #fff; }
|
||||
.detail-section { padding: 16px 18px 18px; }
|
||||
.detail-section h2, .transactions h2 { margin: 0; font-size: 15px; letter-spacing: 0; }
|
||||
.detail-fields { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 14px 0 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); }
|
||||
.detail-fields > div { min-width: 0; min-height: 58px; padding: 10px 12px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.detail-fields dt { color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-fields dd { margin: 4px 0 0; overflow-wrap: anywhere; }
|
||||
.transactions { padding-top: 16px; overflow: hidden; }
|
||||
.transactions h2 { margin: 0 18px 14px; }
|
||||
.detail-section { padding: 12px; }
|
||||
.detail-section h2, .transactions h2 { margin: 0; font-size: 14px; letter-spacing: 0; }
|
||||
.detail-fields { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 8px 0 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); }
|
||||
.detail-fields > div { min-width: 0; display: grid; grid-template-columns: 112px minmax(0, 1fr); min-height: 36px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
.detail-fields dt { display: flex; align-items: center; padding: 6px 8px; background: #f8fafc; color: var(--cc-muted); font-size: 12px; }
|
||||
.detail-fields dd { align-self: center; margin: 6px 8px; font-size: 13px; overflow-wrap: anywhere; }
|
||||
.transactions { padding-top: 12px; overflow: hidden; }
|
||||
.transactions h2 { margin: 0 12px 8px; }
|
||||
.detail-loading { min-height: 180px; padding: 24px; border: 1px solid var(--cc-border); background: #fff; color: var(--cc-muted); }
|
||||
@media (max-width: 960px) { .detail-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 520px) { .detail-header { flex-wrap: wrap; } .detail-header h1 { width: 100%; font-size: 18px; } .detail-fields { grid-template-columns: 1fr; } }
|
||||
|
||||
@@ -11,7 +11,13 @@ describe('DomesticContainersView', () => {
|
||||
it('reuses the order-list tab panel and keyboard interaction', () => { expect(source).toContain('class="business-query-panel"'); expect(source).toContain('role="tablist"'); expect(source).toContain('role="tab"'); expect(source).toContain('handleTabKeydown'); expect(source).toContain(".status-tab.is-active::after{background:var(--cc-primary)}"); expect(source).not.toContain('<el-tabs') })
|
||||
it('keeps desktop filters and actions on one row with explicit field widths', () => { expect(source).toContain("key: 'warehouseId', type: 'select', width: 210"); expect(source).toContain("key: 'orderNo', type: 'input', width: 220"); expect(source).toContain("key: 'containerNo', type: 'input', width: 220"); expect(source).toContain("key: 'cargoName', type: 'input', width: 220"); expect(source).toContain("key: 'status', type: 'select', width: 180") })
|
||||
it('uses the shared query toolbar flow without a page-level layout override', () => { expect(source).not.toContain('.query-toolbar__filters') })
|
||||
it('matches the prototype four-column inventory summary', () => { expect(source).toContain("import './domesticContainersSummary.css'"); expect(summaryStyles).toContain('grid-template-columns: repeat(4, minmax(0, 1fr))'); expect(summaryStyles).toContain('border: 1px solid #e7edf5'); expect(summaryStyles).toContain('font-size: 20px'); expect(summaryStyles).toContain('.inventory-summary + .data-table > .data-table__toolbar') })
|
||||
it('keeps all four inventory facts in the approved compact summary', () => {
|
||||
for (const fact of ['summary.inboundBoxes', 'summary.outboundBoxes', 'summary.availableBoxes', 'summary.inboundVolumeCbm']) expect(source).toContain(fact)
|
||||
expect(source).toContain("import './domesticContainersSummary.css'")
|
||||
expect(summaryStyles).toContain('display: flex; flex-wrap: wrap')
|
||||
expect(summaryStyles).toContain('font-size: 14px')
|
||||
expect(summaryStyles).not.toContain('position: absolute')
|
||||
})
|
||||
it('does not offer an inventory detail action before an inbound has formed inventory', () => { expect(source).toContain("canViewInventory(row as ContainerOrder | InventoryRecord)"); expect(source).toContain("row.status === 'COMPLETED'") })
|
||||
it('matches backend numeric and container-number input boundaries before submit', () => { expect(source).toContain("pattern: /^[A-Za-z0-9-]{4,32}$/"); expect(source).toContain(':max="999999999"'); expect(source).toContain(':max="999999999999.9999"') })
|
||||
it('requires a refreshed inventory version for completed outbound submission', () => { expect(source).toContain("outboundForm.inventoryVersion == null"); expect(source).toContain('库存版本未就绪,请重新选择集装箱') })
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Eye, Plus } from '@lucide/vue'
|
||||
@@ -20,10 +21,10 @@ import './domesticContainersSummary.css'
|
||||
|
||||
type TabName = 'inbound' | 'outbound' | 'inventory'
|
||||
const router = useRouter(); const access = useAccessPreviewStore(); const activeTab = ref<TabName>('inbound')
|
||||
const businessTabs: Array<{ value: TabName; label: string }> = [
|
||||
{ value: 'inbound', label: '整柜入库单' },
|
||||
{ value: 'outbound', label: '整柜出库单' },
|
||||
{ value: 'inventory', label: '整柜库存' },
|
||||
const businessTabs: Array<{ value: TabName; label: string; icon: string }> = [
|
||||
{ value: 'inbound', label: '整柜入库单', icon: '📥' },
|
||||
{ value: 'outbound', label: '整柜出库单', icon: '📤' },
|
||||
{ value: 'inventory', label: '整柜库存', icon: '📦' },
|
||||
]
|
||||
const loading = ref(false); const loadError = ref<ApiErrorDetails | null>(null); const rows = ref<(ContainerOrder | InventoryRecord)[]>([]); const page = ref(1); const pageSize = ref(20); const total = ref(0)
|
||||
const summary = ref<InventorySummary>({ inboundBoxes: 0, outboundBoxes: 0, availableBoxes: 0, inboundVolumeCbm: 0 })
|
||||
@@ -71,9 +72,9 @@ async function submit() { if (!await formRef.value?.validate().catch(() => false
|
||||
watch([page, pageSize], load); onMounted(async () => { await Promise.all([loadWarehouses(), load()]) })
|
||||
</script>
|
||||
|
||||
<template><AdminPage as="div" class="domestic-containers-page">
|
||||
<template><AdminPage as="div" class="domestic-containers-page admin-density">
|
||||
<LoadErrorState v-if="warehouseError" compact title="国内仓候选加载失败" :message="warehouseError.message" :loading="warehouseLoading" @retry="loadWarehouses"><template #details><ApiErrorMeta :details="warehouseError" /></template></LoadErrorState>
|
||||
<ListPageLayout :aria-label="`${activeTab === 'inbound' ? '整柜入库单' : activeTab === 'outbound' ? '整柜出库单' : '整柜库存'}列表`"><template #toolbar><div class="business-query-panel"><div class="status-tabs" role="tablist" aria-label="整柜业务类型"><button v-for="(tab, index) in businessTabs" :key="tab.value" type="button" role="tab" :aria-selected="activeTab === tab.value" aria-controls="domestic-container-results" :tabindex="activeTab === tab.value ? 0 : -1" :class="['status-tab', { 'is-active': activeTab === tab.value }]" @click="switchTab(tab.value)" @keydown="handleTabKeydown($event, index)">{{ tab.label }}</button></div><QueryToolbar v-model="filters" class="business-query-toolbar" ariaLabel="整柜业务筛选" :fields="queryFields" :preference-key="`domestic-containers-${activeTab}-query`" @search="search" /></div></template><template v-if="loadError" #state><LoadErrorState title="整柜业务加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template><section v-if="!loadError" id="domestic-container-results" class="surface-panel table-panel"><div v-if="activeTab === 'inventory'" class="inventory-summary"><div><span>入库箱数</span><strong>{{ summary.inboundBoxes }}</strong></div><div><span>已出库箱数</span><strong>{{ summary.outboundBoxes }}</strong></div><div><span>当前库存</span><strong>{{ summary.availableBoxes }}</strong></div><div><span>累计入库体积</span><strong>{{ summary.inboundVolumeCbm.toFixed(2) }} m³</strong></div></div><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" stripe row-key="id" :preference-key="`domestic-containers-${activeTab}-table`" :empty-text="activeTab === 'inbound' ? '暂无整柜入库单' : activeTab === 'outbound' ? '暂无整柜出库单' : '暂无整柜库存'"><template #toolbar-actions><el-button v-if="activeTab === 'inbound' && access.can('platform:domestic-container:inbound:create')" type="primary" @click="openInbound"><Plus :size="16" />新建入库单</el-button><el-button v-if="activeTab === 'outbound' && access.can('platform:domestic-container:outbound:create')" type="primary" @click="openOutbound"><Plus :size="16" />新建出库单</el-button></template><template #actions="{ row }"><el-button v-if="canViewInventory(row as ContainerOrder | InventoryRecord)" size="small" type="primary" plain @click="viewInventory(row as ContainerOrder | InventoryRecord)"><Eye :size="15" />查看</el-button><span v-else>—</span></template><template #status="{ row }"><StatusBadge :status="statusLabel(row as ContainerOrder | InventoryRecord)" /></template></DataTable></section></ListPageLayout>
|
||||
<AppDialog v-model="dialogOpen" class="domestic-container-create-dialog" :title="dialogType === 'inbound' ? '新建整柜入库单' : '新建整柜出库单'" width="min(720px, calc(100vw - 28px))" :confirm-loading="saving" :close-on-click-modal="false" :confirm-text="dialogType === 'inbound' ? '创建入库单' : '创建出库单'" @confirm="submit"><LoadErrorState v-if="submitError" compact title="创建失败" :message="submitError.message" :show-retry="false"><template #details><ApiErrorMeta :details="submitError" /></template></LoadErrorState><el-form ref="formRef" :model="currentForm" :rules="rules" label-position="top"><el-row :gutter="16"><template v-if="dialogType === 'inbound'"><el-col :xs="24" :sm="12"><el-form-item label="国内仓" prop="warehouseId"><el-select v-model="inboundForm.warehouseId" :loading="warehouseLoading" :disabled="Boolean(warehouseError)" placeholder="请选择启用的国内仓"><el-option v-for="item in warehouseOptions" :key="String(item.value)" :label="item.label" :value="String(item.value)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="货物名称" prop="cargoName"><el-input v-model="inboundForm.cargoName" maxlength="128" placeholder="请输入货物名称" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="集装箱号" prop="containerNo"><el-input v-model="inboundForm.containerNo" maxlength="32" placeholder="如 MSCU1234567" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="箱子数量" prop="boxCount"><el-input-number v-model="inboundForm.boxCount" :min="1" :max="999999999" :precision="0" controls-position="right" placeholder="请输入入库箱数" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="体积(m³)" prop="volumeCbm"><el-input-number v-model="inboundForm.volumeCbm" :min="0.0001" :max="999999999999.9999" :precision="4" controls-position="right" placeholder="请输入体积" /></el-form-item></el-col></template><template v-else><el-col :xs="24" :sm="12"><el-form-item label="国内仓" prop="warehouseId"><el-select v-model="outboundForm.warehouseId" :loading="warehouseLoading" :disabled="Boolean(warehouseError)" placeholder="请选择启用的国内仓" @change="loadCandidates"><el-option v-for="item in warehouseOptions" :key="String(item.value)" :label="item.label" :value="String(item.value)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="绑定集装箱号" prop="containerNo"><el-select v-model="outboundForm.inventoryId" :loading="candidateLoading" :disabled="!outboundForm.warehouseId || Boolean(candidateError)" placeholder="请选择已有集装箱号" @change="selectInventory"><el-option v-for="item in inventoryCandidates" :key="String(item.id)" :label="`${item.containerNo} · 可用 ${item.availableBoxes} 箱`" :value="String(item.id)" /></el-select><input v-model="outboundForm.containerNo" class="sr-only" tabindex="-1"></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="货物名称"><el-input v-model="outboundForm.cargoName" readonly /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="出库箱数" prop="boxCount"><el-input-number v-model="outboundForm.boxCount" :min="1" :max="999999999" :precision="0" controls-position="right" placeholder="请输入出库箱数" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="可用库存"><el-input :model-value="`${outboundForm.availableBoxes} 箱`" readonly /></el-form-item></el-col><el-col v-if="candidateError" :xs="24"><LoadErrorState compact title="可出库库存加载失败" :message="candidateError.message" @retry="loadCandidates"><template #details><ApiErrorMeta :details="candidateError" /></template></LoadErrorState></el-col></template><el-col :xs="24" :sm="12"><el-form-item :label="dialogType === 'inbound' ? '入库时间' : '出库时间'" prop="operationTime"><el-date-picker v-model="currentForm.operationTime" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="状态" prop="status"><el-select v-model="currentForm.status"><el-option :label="dialogType === 'inbound' ? '已入库' : '已出库'" value="COMPLETED" /><el-option :label="dialogType === 'inbound' ? '待入库' : '待出库'" value="PENDING" /></el-select></el-form-item></el-col><el-col :xs="24"><el-form-item label="备注"><el-input v-model="currentForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注" /></el-form-item></el-col></el-row></el-form></AppDialog></AdminPage></template>
|
||||
<ListPageLayout :aria-label="`${activeTab === 'inbound' ? '整柜入库单' : activeTab === 'outbound' ? '整柜出库单' : '整柜库存'}列表`"><template #toolbar><div class="business-query-panel"><div class="status-tabs" role="tablist" aria-label="整柜业务类型"><button v-for="(tab, index) in businessTabs" :key="tab.value" type="button" role="tab" :aria-selected="activeTab === tab.value" aria-controls="domestic-container-results" :tabindex="activeTab === tab.value ? 0 : -1" :class="['status-tab', { 'is-active': activeTab === tab.value }]" @click="switchTab(tab.value)" @keydown="handleTabKeydown($event, index)"><span class="density-tab-icon" aria-hidden="true">{{ tab.icon }}</span>{{ tab.label }}</button></div><QueryToolbar v-model="filters" class="business-query-toolbar" ariaLabel="整柜业务筛选" :fields="queryFields" :preference-key="`domestic-containers-${activeTab}-query`" @search="search" /></div></template><template v-if="loadError" #state><LoadErrorState title="整柜业务加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState></template><section v-if="!loadError" id="domestic-container-results" class="surface-panel table-panel"><div v-if="activeTab === 'inventory'" class="inventory-summary"><div><span>入库箱数</span><strong>{{ summary.inboundBoxes }}</strong></div><div><span>已出库箱数</span><strong>{{ summary.outboundBoxes }}</strong></div><div><span>当前库存</span><strong>{{ summary.availableBoxes }}</strong></div><div><span>累计入库体积</span><strong>{{ summary.inboundVolumeCbm.toFixed(2) }} m³</strong></div></div><DataTable v-model:page="page" v-model:page-size="pageSize" :data="rows" :columns="columns" :loading="loading" :total="total" stripe row-key="id" :preference-key="`domestic-containers-${activeTab}-table`" :empty-text="activeTab === 'inbound' ? '暂无整柜入库单' : activeTab === 'outbound' ? '暂无整柜出库单' : '暂无整柜库存'"><template #toolbar-actions><el-button v-if="activeTab === 'inbound' && access.can('platform:domestic-container:inbound:create')" type="primary" @click="openInbound"><Plus :size="16" />新建入库单</el-button><el-button v-if="activeTab === 'outbound' && access.can('platform:domestic-container:outbound:create')" type="primary" @click="openOutbound"><Plus :size="16" />新建出库单</el-button></template><template #actions="{ row }"><el-button v-if="canViewInventory(row as ContainerOrder | InventoryRecord)" size="small" type="primary" plain @click="viewInventory(row as ContainerOrder | InventoryRecord)"><Eye :size="15" />查看</el-button><span v-else>—</span></template><template #status="{ row }"><StatusBadge :status="statusLabel(row as ContainerOrder | InventoryRecord)" /></template></DataTable></section></ListPageLayout>
|
||||
<AppDialog v-model="dialogOpen" class="domestic-container-create-dialog admin-density-dialog" :title="dialogType === 'inbound' ? '新建整柜入库单' : '新建整柜出库单'" width="min(720px, calc(100vw - 28px))" :confirm-loading="saving" :close-on-click-modal="false" :confirm-text="dialogType === 'inbound' ? '创建入库单' : '创建出库单'" @confirm="submit"><LoadErrorState v-if="submitError" compact title="创建失败" :message="submitError.message" :show-retry="false"><template #details><ApiErrorMeta :details="submitError" /></template></LoadErrorState><el-form ref="formRef" :model="currentForm" :rules="rules" label-position="top"><el-row :gutter="16"><template v-if="dialogType === 'inbound'"><el-col :xs="24" :sm="12"><el-form-item label="国内仓" prop="warehouseId"><el-select v-model="inboundForm.warehouseId" :loading="warehouseLoading" :disabled="Boolean(warehouseError)" placeholder="请选择启用的国内仓"><el-option v-for="item in warehouseOptions" :key="String(item.value)" :label="item.label" :value="String(item.value)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="货物名称" prop="cargoName"><el-input v-model="inboundForm.cargoName" maxlength="128" placeholder="请输入货物名称" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="集装箱号" prop="containerNo"><el-input v-model="inboundForm.containerNo" maxlength="32" placeholder="如 MSCU1234567" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="箱子数量" prop="boxCount"><el-input-number v-model="inboundForm.boxCount" :min="1" :max="999999999" :precision="0" controls-position="right" placeholder="请输入入库箱数" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="体积(m³)" prop="volumeCbm"><el-input-number v-model="inboundForm.volumeCbm" :min="0.0001" :max="999999999999.9999" :precision="4" controls-position="right" placeholder="请输入体积" /></el-form-item></el-col></template><template v-else><el-col :xs="24" :sm="12"><el-form-item label="国内仓" prop="warehouseId"><el-select v-model="outboundForm.warehouseId" :loading="warehouseLoading" :disabled="Boolean(warehouseError)" placeholder="请选择启用的国内仓" @change="loadCandidates"><el-option v-for="item in warehouseOptions" :key="String(item.value)" :label="item.label" :value="String(item.value)" /></el-select></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="绑定集装箱号" prop="containerNo"><el-select v-model="outboundForm.inventoryId" :loading="candidateLoading" :disabled="!outboundForm.warehouseId || Boolean(candidateError)" placeholder="请选择已有集装箱号" @change="selectInventory"><el-option v-for="item in inventoryCandidates" :key="String(item.id)" :label="`${item.containerNo} · 可用 ${item.availableBoxes} 箱`" :value="String(item.id)" /></el-select><input v-model="outboundForm.containerNo" class="sr-only" tabindex="-1"></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="货物名称"><el-input v-model="outboundForm.cargoName" readonly /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="出库箱数" prop="boxCount"><el-input-number v-model="outboundForm.boxCount" :min="1" :max="999999999" :precision="0" controls-position="right" placeholder="请输入出库箱数" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="可用库存"><el-input :model-value="`${outboundForm.availableBoxes} 箱`" readonly /></el-form-item></el-col><el-col v-if="candidateError" :xs="24"><LoadErrorState compact title="可出库库存加载失败" :message="candidateError.message" @retry="loadCandidates"><template #details><ApiErrorMeta :details="candidateError" /></template></LoadErrorState></el-col></template><el-col :xs="24" :sm="12"><el-form-item :label="dialogType === 'inbound' ? '入库时间' : '出库时间'" prop="operationTime"><el-date-picker v-model="currentForm.operationTime" type="datetime" value-format="YYYY-MM-DDTHH:mm:ss" format="YYYY-MM-DD HH:mm" /></el-form-item></el-col><el-col :xs="24" :sm="12"><el-form-item label="状态" prop="status"><el-select v-model="currentForm.status"><el-option :label="dialogType === 'inbound' ? '已入库' : '已出库'" value="COMPLETED" /><el-option :label="dialogType === 'inbound' ? '待入库' : '待出库'" value="PENDING" /></el-select></el-form-item></el-col><el-col :xs="24"><el-form-item label="备注"><el-input v-model="currentForm.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注" /></el-form-item></el-col></el-row></el-form></AppDialog></AdminPage></template>
|
||||
|
||||
<style scoped>.domestic-containers-page{min-width:0}.business-query-panel{overflow:hidden;border:1px solid var(--cc-border);border-radius:8px;background:#fff}.status-tabs{display:flex;max-width:100%;overflow-x:auto;margin-bottom:0;border-bottom:1px solid #e5e7eb;background:#f9fafb;scrollbar-width:thin}.status-tab{position:relative;flex:0 0 auto;min-height:44px;padding:0 18px;border:0;background:transparent;color:var(--cc-muted);font:inherit;font-size:14px;cursor:pointer}.status-tab::after{position:absolute;right:12px;bottom:0;left:12px;height:3px;background:transparent;content:''}.status-tab:hover{color:var(--cc-primary);background:#f6f9fd}.status-tab:focus-visible{outline:2px solid var(--cc-primary);outline-offset:-2px}.status-tab.is-active{color:var(--cc-primary);font-weight:600}.status-tab.is-active::after{background:var(--cc-primary)}.business-query-toolbar{border:0}.inventory-summary{display:flex;align-items:center;gap:0;margin:0 16px 12px;border:1px solid var(--cc-border);background:#f8fafc}.inventory-summary>div{display:flex;align-items:baseline;gap:8px;min-width:0;padding:9px 16px;border-right:1px solid var(--cc-border)}.inventory-summary>div:last-child{border-right:0}.inventory-summary span{color:var(--cc-muted);font-size:12px}.inventory-summary strong{font-size:15px}.domestic-container-create-dialog :deep(.el-select),.domestic-container-create-dialog :deep(.el-date-editor),.domestic-container-create-dialog :deep(.el-input-number){width:100%}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}@media(max-width:720px){.status-tab{min-height:46px;padding:0 14px}.inventory-summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.inventory-summary>div:nth-child(2){border-right:0}.inventory-summary>div:nth-child(-n+2){border-bottom:1px solid var(--cc-border)}}@media(max-width:360px){.status-tab{padding:0 10px;font-size:13px}.inventory-summary{grid-template-columns:1fr}.inventory-summary>div{border-right:0;border-bottom:1px solid var(--cc-border)}.inventory-summary>div:last-child{border-bottom:0}}</style>
|
||||
<style scoped>.domestic-containers-page{min-width:0}.business-query-panel{overflow:hidden;border:1px solid var(--cc-border);border-radius:4px;background:#fff}.status-tabs{display:flex;max-width:100%;overflow-x:auto;margin-bottom:0;border-bottom:1px solid #e5e7eb;background:#f9fafb;scrollbar-width:thin}.status-tab{position:relative;flex:0 0 auto;display:inline-flex;align-items:center;gap:6px;min-height:38px;padding:0 16px;border:0;background:transparent;color:var(--cc-muted);font:inherit;font-size:13px;cursor:pointer}.status-tab::after{position:absolute;right:12px;bottom:0;left:12px;height:3px;background:transparent;content:''}.status-tab:hover{color:var(--cc-primary);background:#f6f9fd}.status-tab:focus-visible{outline:2px solid var(--cc-primary);outline-offset:-2px}.status-tab.is-active{color:var(--cc-primary);font-weight:600}.status-tab.is-active::after{background:var(--cc-primary)}.business-query-toolbar{border:0}.inventory-summary{display:flex;align-items:center;gap:0;margin:0 16px 12px;border:1px solid var(--cc-border);background:#f8fafc}.inventory-summary>div{display:flex;align-items:baseline;gap:8px;min-width:0;padding:9px 16px;border-right:1px solid var(--cc-border)}.inventory-summary>div:last-child{border-right:0}.inventory-summary span{color:var(--cc-muted);font-size:12px}.inventory-summary strong{font-size:15px}.domestic-container-create-dialog :deep(.el-select),.domestic-container-create-dialog :deep(.el-date-editor),.domestic-container-create-dialog :deep(.el-input-number){width:100%}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}@media(max-width:720px){.status-tab{min-height:46px;padding:0 14px}.inventory-summary{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.inventory-summary>div:nth-child(2){border-right:0}.inventory-summary>div:nth-child(-n+2){border-bottom:1px solid var(--cc-border)}}@media(max-width:360px){.status-tab{padding:0 10px;font-size:13px}.inventory-summary{grid-template-columns:1fr}.inventory-summary>div{border-right:0;border-bottom:1px solid var(--cc-border)}.inventory-summary>div:last-child{border-bottom:0}}</style>
|
||||
|
||||
@@ -1,67 +1,9 @@
|
||||
.domestic-containers-page .table-panel .inventory-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
margin: 0;
|
||||
padding: 14px 18px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
display: flex; flex-wrap: wrap; gap: 6px 28px; margin: 0;
|
||||
padding: 8px 12px; border: 0; border-bottom: 1px solid var(--cc-border); background: #fff;
|
||||
}
|
||||
|
||||
.domestic-containers-page .table-panel .inventory-summary > div,
|
||||
.domestic-containers-page .table-panel .inventory-summary > div:last-child {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #e7edf5;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.domestic-containers-page .table-panel .inventory-summary span {
|
||||
display: block;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.domestic-containers-page .table-panel .inventory-summary strong {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #1e293b;
|
||||
font-size: 20px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
@media (min-width: 721px) {
|
||||
.domestic-containers-page .table-panel:has(.inventory-summary) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.domestic-containers-page .table-panel .inventory-summary {
|
||||
padding-right: 70px;
|
||||
}
|
||||
|
||||
.domestic-containers-page .inventory-summary + .data-table > .data-table__toolbar {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 35px;
|
||||
right: 18px;
|
||||
min-height: 34px;
|
||||
padding: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.domestic-containers-page .table-panel .inventory-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
.domestic-containers-page .table-panel .inventory-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.domestic-containers-page .table-panel .inventory-summary > div {
|
||||
display: flex; align-items: baseline; min-width: 0; gap: 8px; padding: 0; border: 0;
|
||||
}
|
||||
.domestic-containers-page .table-panel .inventory-summary span { color: var(--cc-muted); font-size: 12px; }
|
||||
.domestic-containers-page .table-panel .inventory-summary strong { font-size: 14px; font-weight: 600; }
|
||||
|
||||
@@ -314,7 +314,7 @@ describe('platform IAM roles', () => {
|
||||
it('creates, updates, and changes customer status with the platform tenant contract', async () => {
|
||||
const tenant = { id: '8', tenantCode: 'TEN8', tenantName: '柏悦物流', status: 'ACTIVE' }
|
||||
const create = {
|
||||
tenantName: '柏悦物流', legalName: '柏悦物流有限公司', legalRepresentative: '张三',
|
||||
tenantName: '柏悦物流', customerNickname: '柏', legalName: '柏悦物流有限公司', legalRepresentative: '张三',
|
||||
contactName: '张三', contactPhone: '13800138000', contactEmail: 'ops@example.com', servicePackage: '标准版',
|
||||
contactWechat: 'baiyue-ops', salesOwnerName: '李四', paymentTermDays: 30, enterpriseType: '有限责任公司',
|
||||
establishedDate: '2020-01-01', registeredCapital: 1000, registeredCapitalCurrency: 'CNY',
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface PlatformTenantResource {
|
||||
id: number | string
|
||||
tenantCode: string
|
||||
tenantName: string
|
||||
customerNickname?: string | null
|
||||
status: PlatformTenantStatus
|
||||
dimensionInitializationStatus?: 'PENDING' | 'SENDING' | 'READY' | 'FAILED' | string
|
||||
legalName: string | null
|
||||
@@ -52,6 +53,7 @@ export interface PlatformTenantResource {
|
||||
|
||||
export interface CreatePlatformTenantRequest {
|
||||
tenantName: string
|
||||
customerNickname?: string
|
||||
legalName: string
|
||||
legalRepresentative?: string
|
||||
contactName: string
|
||||
@@ -69,7 +71,7 @@ export interface CreatePlatformTenantRequest {
|
||||
internalRemark: string
|
||||
}
|
||||
|
||||
export type UpdatePlatformTenantRequest = CreatePlatformTenantRequest
|
||||
export type UpdatePlatformTenantRequest = Omit<CreatePlatformTenantRequest, 'customerNickname'> & { customerNickname: string }
|
||||
|
||||
export interface PlatformTenantOrderCount {
|
||||
tenantId: number | string
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import '../measurement/measurementDensity.css'
|
||||
import { computed, onActivated, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
@@ -326,7 +328,7 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="inbound-task-page">
|
||||
<AdminPage as="div" class="inbound-task-page admin-density">
|
||||
<ListPageLayout :aria-label="`${taskLabel}待办`">
|
||||
<template #toolbar>
|
||||
<QueryToolbar
|
||||
@@ -408,13 +410,13 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
v-if="isReview"
|
||||
v-model="reviewDialogOpen"
|
||||
:title="selectedInbound ? `${taskLabel} · ${selectedInbound.inboundCode || '入仓码待补充'}` : taskLabel"
|
||||
width="min(1180px, calc(100vw - 32px))"
|
||||
width="min(1560px, calc(100vw - 48px))"
|
||||
top="4vh"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
:close-on-click-modal="!reviewNavigationLoading"
|
||||
:close-on-press-escape="!reviewNavigationLoading"
|
||||
class="inbound-task-dialog"
|
||||
class="inbound-task-dialog admin-density-dialog admin-density-workspace inbound-density"
|
||||
@closed="clearSelectedInbound"
|
||||
>
|
||||
<el-alert v-if="reviewNavigationError" type="error" :closable="false" show-icon class="review-navigation-error">
|
||||
@@ -466,8 +468,8 @@ onBeforeUnmount(() => { disposed = true; requestSerial += 1; optionsRequestSeria
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.inbound-task-dialog { max-height: 92vh; overflow: hidden; }
|
||||
.inbound-task-dialog .el-dialog__body { max-height: calc(92vh - 62px); overflow: auto; padding: 20px 24px 24px; }
|
||||
.inbound-task-dialog.admin-density-workspace.el-dialog:not(.is-fullscreen) { height: auto; max-height: 92vh; overflow: hidden; }
|
||||
.inbound-task-dialog .el-dialog__body { max-height: calc(92vh - 62px); overflow: auto; padding: 12px 16px; }
|
||||
@media (max-width: 520px) {
|
||||
.inbound-task-dialog .el-dialog__body { padding: 14px; }
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ describe('InboundMeasurementDialog', () => {
|
||||
it('owns the complete measurement workspace behind one centered dialog', () => {
|
||||
expect(source).toContain("import AppDialog from '@/shared/components/AppDialog.vue'")
|
||||
expect(source).toContain('<AppDialog')
|
||||
expect(source).toContain('class="inbound-measurement-workspace-dialog"')
|
||||
expect(source).toContain('width="min(1240px, calc(100vw - 32px))"')
|
||||
expect(source).toContain('class="inbound-measurement-workspace-dialog admin-density-dialog admin-density-workspace inbound-density"')
|
||||
expect(source).toContain('width="min(1560px, calc(100vw - 48px))"')
|
||||
expect(source).toContain('height: 92vh')
|
||||
expect(source).toContain('padding: 10px 20px')
|
||||
expect(source).toContain('<WeightOcrPanel')
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import './measurementDensity.css'
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
@@ -66,9 +68,9 @@ function handleSubmitted() {
|
||||
<template>
|
||||
<AppDialog
|
||||
:model-value="modelValue"
|
||||
class="inbound-measurement-workspace-dialog"
|
||||
class="inbound-measurement-workspace-dialog admin-density-dialog admin-density-workspace inbound-density"
|
||||
:title="title"
|
||||
width="min(1240px, calc(100vw - 32px))"
|
||||
width="min(1560px, calc(100vw - 48px))"
|
||||
top="4vh"
|
||||
fullscreenable
|
||||
destroy-on-close
|
||||
@@ -139,8 +141,8 @@ function handleSubmitted() {
|
||||
<style>
|
||||
.inbound-measurement-workspace-dialog.el-dialog { display: flex; max-width: calc(100vw - 32px); height: 92vh; max-height: 92vh; margin-bottom: 0; flex-direction: column; overflow: hidden; }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__header { flex: 0 0 auto; margin: 0; padding: 10px 20px; border-bottom: 1px solid var(--cc-border); }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__body { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 20px 24px; }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__footer { flex: 0 0 auto; padding: 14px 24px; border-top: 1px solid var(--cc-border); }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__body { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 12px 16px; }
|
||||
.inbound-measurement-workspace-dialog .el-dialog__footer { flex: 0 0 auto; padding: 8px 16px; border-top: 1px solid var(--cc-border); }
|
||||
.inbound-measurement-workspace-dialog .measurement-panel { border: 0; border-radius: 0; box-shadow: none; }
|
||||
.inbound-measurement-workspace-dialog .measurement-panel__heading .measurement-panel__actions,
|
||||
.inbound-measurement-workspace-dialog .measurement-panel__footer { display: none; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
|
||||
import { AlertTriangle, Image, Plus, Save, Send, Upload } from '@lucide/vue'
|
||||
@@ -860,7 +861,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<el-dialog v-model="imagePreviewOpen" title="当前有效测量图片" width="min(720px, 92vw)" destroy-on-close @closed="releaseImagePreview">
|
||||
<el-dialog class="admin-density-dialog" v-model="imagePreviewOpen" title="当前有效测量图片" width="min(720px, 92vw)" destroy-on-close @closed="releaseImagePreview">
|
||||
<div class="measurement-image-preview">
|
||||
<el-skeleton v-if="imagePreviewLoading" :rows="5" animated />
|
||||
<LoadErrorState v-else-if="imagePreviewError" title="图片加载失败" :message="imagePreviewError.message" :show-retry="false">
|
||||
@@ -871,7 +872,7 @@ defineExpose({ saveDraft, submitForReview, updateWeightEvidence, submissionBlock
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="manualEntryOpen" title="手动录入箱规" width="min(680px, 92vw)" destroy-on-close>
|
||||
<el-dialog class="admin-density-dialog" v-model="manualEntryOpen" title="手动录入箱规" width="min(680px, 92vw)" destroy-on-close>
|
||||
<el-alert v-if="manualEntryErrors.length" type="error" :closable="false" show-icon class="manual-entry-alert" title="请修正以下问题">
|
||||
<ul><li v-for="error in manualEntryErrors" :key="error">{{ error }}</li></ul>
|
||||
</el-alert>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@media (min-width: 961px) {
|
||||
.inbound-density.admin-density-dialog .measurement-dialog-weight { padding-bottom: 10px; }
|
||||
.inbound-density.admin-density-dialog .measurement-section { padding: 10px 0; }
|
||||
.inbound-density.admin-density-dialog .measurement-summary-section { gap: 8px; }
|
||||
.inbound-density.admin-density-dialog .measurement-section__heading { align-items: center; margin-bottom: 8px; }
|
||||
.inbound-density.admin-density-dialog .measurement-metrics { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
.inbound-density.admin-density-dialog .measurement-metrics > div { min-height: 36px; padding: 6px 10px; display: flex; align-items: baseline; gap: 8px; }
|
||||
.inbound-density.admin-density-dialog .measurement-metrics dd { font-size: 14px; }
|
||||
.inbound-density.admin-density-dialog .measurement-metrics small { font-size: 12px; }
|
||||
.inbound-density.admin-density-dialog .measurement-data-grid { gap: 8px 12px; }
|
||||
.inbound-density.admin-density-dialog .measurement-declaration { padding-top: 8px; }
|
||||
.inbound-density.admin-density-dialog .measurement-declaration h4 { margin-bottom: 6px; font-size: 13px; }
|
||||
.inbound-density.admin-density-dialog .measurement-panel > .el-alert { margin: 8px 0; }
|
||||
.inbound-density.admin-density-dialog .measurement-summary-header span { font-size: 12px; }
|
||||
.inbound-density.admin-density-dialog .measurement-table .el-table__header th { padding: 4px 0; font-size: 13px; }
|
||||
.inbound-density.admin-density-dialog .weight-heading { flex-wrap: wrap; gap: 8px; padding-bottom: 8px; }
|
||||
.inbound-density.admin-density-dialog .weight-heading__actions { flex-wrap: wrap; gap: 12px; }
|
||||
.inbound-density.admin-density-dialog .pallet-picker { width: 340px; }
|
||||
.inbound-density.admin-density-dialog .weight-list { margin-top: 8px; }
|
||||
.inbound-density.admin-density-dialog .weight-grid { grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); gap: 8px; }
|
||||
.inbound-density.admin-density-dialog .weight-card { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 10px; align-items: start; padding: 8px; border-radius: 2px; }
|
||||
.inbound-density.admin-density-dialog .weight-card .weight-thumb { width: 120px; border-radius: 2px; }
|
||||
.inbound-density.admin-density-dialog .weight-card__body { min-width: 0; padding: 0; text-align: left; }
|
||||
.inbound-density.admin-density-dialog .weight-card__value { min-height: 28px; padding-right: 32px; text-align: left; }
|
||||
.inbound-density.admin-density-dialog .weight-card__value strong { font-size: 14px; }
|
||||
.inbound-density.admin-density-dialog .weight-card__facts { gap: 3px; margin-top: 4px; padding-top: 4px; }
|
||||
.inbound-density.admin-density-dialog .weight-card__facts div { font-size: 12px; }
|
||||
.inbound-density.admin-density-dialog .weight-card__correct { width: auto; margin-top: 6px; }
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('MeasurementReviewPanel', () => {
|
||||
vi.mocked(listMeasurementReviewHistory).mockResolvedValue({ records: [], page: 1, size: 20, total: 0, pages: 0 })
|
||||
})
|
||||
|
||||
it('keeps customer context and pending actions in one non-sticky first-screen toolbar', async () => {
|
||||
it('keeps customer context and pending actions in one sticky toolbar while reviewing long content', async () => {
|
||||
const wrapper = mount(MeasurementReviewPanel, { props: { customerId: '10001', inboundId: '90001' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
@@ -83,7 +83,7 @@ describe('MeasurementReviewPanel', () => {
|
||||
expect(wrapper.text()).not.toContain('审核历史')
|
||||
expect(listMeasurementReviewHistory).not.toHaveBeenCalled()
|
||||
const source = await import('./MeasurementReviewPanel.vue?raw').then((module) => module.default)
|
||||
expect(source).not.toContain('position: sticky')
|
||||
expect(source).toContain('position: sticky')
|
||||
expect(source).toContain('review-toolbar__context')
|
||||
expect(source.indexOf('class="review-toolbar"')).toBeLessThan(source.indexOf('<WeightOcrPanel'))
|
||||
expect(source).not.toContain('reasonDialog')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Check, Pencil, RotateCcw } from '@lucide/vue'
|
||||
@@ -139,7 +140,7 @@ async function confirmApprove() {
|
||||
await ElMessageBox.confirm(
|
||||
`审核通过后,入库单将进入${approvalTargetLabel.value}。系统会尝试生成初算费用,但初算失败不影响审核通过,后续可由平台人员补充调整。是否继续?`,
|
||||
'确认测量审核通过',
|
||||
{ confirmButtonText: approvalButtonLabel.value, cancelButtonText: '返回', type: 'warning' },
|
||||
{ customClass: 'admin-density-confirm', confirmButtonText: approvalButtonLabel.value, cancelButtonText: '返回', type: 'warning' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
@@ -242,7 +243,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
<div v-if="detail.items.length" class="review-table-scroll">
|
||||
<el-table :data="detail.items" border stripe table-layout="fixed" class="review-table">
|
||||
<el-table-column type="index" label="#" width="44" align="center" />
|
||||
<el-table-column label="箱号" min-width="100" show-overflow-tooltip>
|
||||
<el-table-column label="箱号" width="240" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.boxCode || row.itemNo || '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="测量图片" width="98" align="center">
|
||||
@@ -257,7 +258,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="尺寸 长×宽×高(cm)" width="132" align="center">
|
||||
<el-table-column label="尺寸 长×宽×高(cm)" min-width="200" align="center">
|
||||
<template #default="{ row }">{{ formatNumber(row.lengthCm) }} × {{ formatNumber(row.widthCm) }} × {{ formatNumber(row.heightCm) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="packageCount" label="箱数" width="54" align="right">
|
||||
@@ -272,7 +273,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
<el-table-column label="体积重(kg)" width="96" align="right">
|
||||
<template #default="{ row }">{{ formatNumber(row.volumeWeightKg) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="测量时间" width="152" class-name="review-time-cell">
|
||||
<el-table-column label="测量时间" width="190" class-name="review-time-cell">
|
||||
<template #default="{ row }">{{ formatDate(row.deviceMeasuredAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -289,7 +290,7 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
/>
|
||||
</template>
|
||||
|
||||
<el-dialog v-model="imagePreviewOpen" :title="imagePreviewTitle" width="min(760px, 92vw)" destroy-on-close append-to-body @closed="releaseImagePreview">
|
||||
<el-dialog class="admin-density-dialog" v-model="imagePreviewOpen" :title="imagePreviewTitle" width="min(760px, 92vw)" destroy-on-close append-to-body @closed="releaseImagePreview">
|
||||
<div class="review-image-preview">
|
||||
<el-skeleton v-if="imagePreviewLoading" :rows="5" animated />
|
||||
<div v-else-if="imagePreviewError" class="review-image-preview__error">
|
||||
@@ -304,8 +305,8 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.review-panel { display: grid; gap: 16px; color: var(--cc-text); }
|
||||
.review-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; margin: -20px -24px 0; padding: 14px 24px; border-bottom: 1px solid var(--cc-border); background: #fff; }
|
||||
.review-panel { display: grid; gap: 12px; color: var(--cc-text); }
|
||||
.review-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 14px; position: sticky; top: -12px; z-index: 3; flex-wrap: wrap; margin: -12px -16px 0; padding: 8px 16px; border-bottom: 1px solid var(--cc-border); background: #fff; }
|
||||
.review-toolbar__status, .review-toolbar__actions { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 8px; }
|
||||
.review-toolbar__status > span { color: var(--cc-muted); font-size: 12px; }
|
||||
.review-toolbar__actions { justify-content: flex-end; }
|
||||
@@ -324,14 +325,14 @@ defineExpose({ confirmApprove, isPending, canReview, actionLoading })
|
||||
.review-metrics, .review-declaration { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px 16px; min-width: 0; margin: 0; }
|
||||
.review-declaration { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.review-metrics > div, .review-declaration > div { display: flex; min-width: 0; align-items: baseline; gap: 6px; }
|
||||
.review-metrics dt, .review-declaration dt { color: var(--cc-muted); font-size: 11px; line-height: 1.4; }
|
||||
.review-metrics dd { margin: 0; overflow-wrap: anywhere; color: #172033; font-size: 15px; font-weight: 700; line-height: 1.35; }
|
||||
.review-metrics dd small, .review-declaration dd small { color: var(--cc-muted); font-size: 11px; font-weight: 500; }
|
||||
.review-metrics dt, .review-declaration dt { color: var(--cc-muted); font-size: 12px; line-height: 1.4; }
|
||||
.review-metrics dd { margin: 0; overflow-wrap: anywhere; color: #172033; font-size: 14px; font-weight: 600; line-height: 1.35; }
|
||||
.review-metrics dd small, .review-declaration dd small { color: var(--cc-muted); font-size: 12px; font-weight: 500; }
|
||||
.review-metrics .review-metric--primary dd { color: #067647; }
|
||||
.review-declaration dd { margin: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 13px; font-weight: 650; line-height: 1.4; }
|
||||
.review-table-scroll { width: 100%; min-width: 0; overflow: hidden; }
|
||||
.review-table { width: 100%; font-size: 12px; }
|
||||
.review-table :deep(.el-table__header th) { padding: 8px 0; background: #f6f8fa; color: #475467; font-size: 11px; font-weight: 700; }
|
||||
.review-table :deep(.el-table__header th) { padding: 8px 0; background: #f6f8fa; color: #475467; font-size: 12px; font-weight: 700; }
|
||||
.review-table :deep(.el-table__header .cell) { white-space: nowrap; }
|
||||
.review-table :deep(.el-table__cell) { padding: 7px 0; }
|
||||
.review-table :deep(.review-time-cell .cell) { white-space: nowrap; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
@@ -625,7 +626,7 @@ defineExpose({ reload: loadWorkspace })
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<AppDialog
|
||||
<AppDialog class="admin-density-dialog"
|
||||
v-model="previewOpen"
|
||||
title="确认称重图片"
|
||||
:loading="previewLoading"
|
||||
@@ -711,10 +712,10 @@ defineExpose({ reload: loadWorkspace })
|
||||
.weight-card__facts dd { min-width: 0; margin: 0; color: #344054; overflow-wrap: anywhere; text-align: right; }
|
||||
.weight-card__correct { width: 100%; margin: 12px 0 0; }
|
||||
.weight-preview { display: block; width: 100%; max-height: min(50vh, 460px); object-fit: contain; border-radius: 6px; background: #f2f4f7; }
|
||||
.weight-key-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; margin-top: 14px; padding: 14px 0; border-block: 1px solid var(--cc-border); }
|
||||
.weight-key-facts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 12px; margin-top: 8px; padding: 8px 0; border-block: 1px solid var(--cc-border); }
|
||||
.weight-key-facts div { display: grid; min-width: 0; gap: 4px; }
|
||||
.weight-key-facts span { color: var(--cc-muted); font-size: 12px; }
|
||||
.weight-key-facts strong { color: #101828; font-size: 18px; }
|
||||
.weight-key-facts strong { color: #101828; font-size: 14px; }
|
||||
.preview-weight { display: grid; grid-template-columns: minmax(0, 1fr) 120px; align-items: end; gap: 12px; margin-top: 14px; }
|
||||
.preview-weight label { display: grid; gap: 6px; color: #475467; font-size: 12px; }
|
||||
.preview-weight :deep(.el-input-number), .preview-weight :deep(.el-segmented) { width: 100%; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed } from 'vue'
|
||||
import { ArrowLeft, Check, Pencil } from '@lucide/vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
@@ -146,7 +147,7 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-loading="loading" class="loading-detail-content" :aria-label="title" :aria-busy="loading">
|
||||
<section v-loading="loading" class="loading-detail-content" :class="{ 'admin-density preload-density': mode === 'preload' }" :aria-label="title" :aria-busy="loading">
|
||||
<section class="detail-overview" aria-label="清单概览">
|
||||
<header class="detail-page-header" :class="{ 'detail-page-header--embedded': embedded }">
|
||||
<el-button link type="primary" class="detail-page-header__back" @click="emit('back')"><ArrowLeft :size="16" />返回列表</el-button>
|
||||
@@ -155,7 +156,7 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
<p>{{ mode === 'preload' ? '核对运输信息、确认预装并维护逐条仓库备注' : '查看已完成装柜明细和柜次费用' }}</p>
|
||||
</div>
|
||||
<div v-if="detail && summary" class="detail-page-header__actions">
|
||||
<PalletLabelPrintDialog v-if="summary.warehouseId" :plan-id="summary.planId" :warehouse-id="summary.warehouseId" :disabled="!summary.containerNo || summary.status === 'EDITING'" />
|
||||
<PalletLabelPrintDialog :dense="mode === 'preload'" v-if="summary.warehouseId" :plan-id="summary.planId" :warehouse-id="summary.warehouseId" :disabled="!summary.containerNo || summary.status === 'EDITING'" />
|
||||
<template v-if="canEditItemRemark">
|
||||
<el-button v-if="canEditTransport" @click="emit('edit')"><Pencil :size="16" />{{ summary.containerNo ? '纠正集装箱号 / 航次' : '补录集装箱号 / 航次' }}</el-button>
|
||||
<el-button type="primary" :loading="confirmSaving" :disabled="!preloadSummary?.readiness.canConfirm" @click="emit('confirmPreload')"><Check :size="16" />确认预装</el-button>
|
||||
@@ -219,16 +220,13 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
|
||||
<section class="history-section" aria-label="操作历史">
|
||||
<header><h2>操作历史</h2><span>{{ visibleHistories.length }} 条</span></header>
|
||||
<el-empty v-if="visibleHistories.length === 0" description="暂无操作历史" :image-size="72" />
|
||||
<el-timeline v-else>
|
||||
<el-timeline-item v-for="history in visibleHistories" :key="historyEventKey(history) || history.historyId" :timestamp="formatDateTime(history.occurredAt)" placement="top">
|
||||
<div class="history-entry">
|
||||
<strong>{{ actionLabel(history.actionType) }}</strong>
|
||||
<span v-if="history.fromStatus || history.toStatus">{{ loadingListStatusLabel(history.fromStatus) }} → {{ loadingListStatusLabel(history.toStatus) }}</span>
|
||||
<p>{{ history.operatorName || '操作人信息缺失' }}<template v-if="history.operationReason"> · {{ history.operationReason }}</template></p>
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<el-table :data="visibleHistories" border stripe empty-text="暂无操作历史" aria-label="清单操作历史">
|
||||
<el-table-column label="时间" width="170"><template #default="{ row }">{{ formatDateTime(row.occurredAt) }}</template></el-table-column>
|
||||
<el-table-column label="事件" width="140"><template #default="{ row }">{{ actionLabel(row.actionType) }}</template></el-table-column>
|
||||
<el-table-column label="状态变化" min-width="200"><template #default="{ row }"><span v-if="row.fromStatus || row.toStatus">{{ loadingListStatusLabel(row.fromStatus) }} → {{ loadingListStatusLabel(row.toStatus) }}</span><span v-else>—</span></template></el-table-column>
|
||||
<el-table-column label="操作人" min-width="160"><template #default="{ row }">{{ row.operatorName || '操作人信息缺失' }}</template></el-table-column>
|
||||
<el-table-column label="原因 / 备注" min-width="240"><template #default="{ row }">{{ row.operationReason || '—' }}</template></el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</DetailPageLayout>
|
||||
</div>
|
||||
@@ -273,7 +271,21 @@ function formatNumber(value?: number | null, digits = 3) {
|
||||
.history-section header { display: flex; align-items: baseline; gap: 10px; margin-bottom: 18px; }
|
||||
.history-section h2 { margin: 0; font-size: 16px; }
|
||||
.history-section header span { color: var(--cc-muted); font-size: 12px; }
|
||||
.history-entry { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
|
||||
.history-entry > span { color: var(--cc-muted); font-size: 12px; }
|
||||
.history-entry p { width: 100%; margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; }
|
||||
.preload-density { gap: 12px; padding: 12px; border-radius: 4px; }
|
||||
.preload-density .detail-page-header { gap: 12px; padding: 0 0 8px; }
|
||||
.preload-density .detail-page-header__identity h1 { font-size: 16px; overflow-wrap: anywhere; }
|
||||
.preload-density .detail-page-header__actions { flex-wrap: wrap; }
|
||||
.preload-density .detail-summary { gap: 0; padding: 0; border-left: 1px solid var(--cc-border); }
|
||||
.preload-density .detail-summary > div { gap: 0; align-items: stretch; min-height: 36px; border-bottom: 1px solid var(--cc-border); border-right: 1px solid var(--cc-border); }
|
||||
.preload-density .detail-summary > div > span:first-child { display: flex; align-items: center; flex: 0 0 96px; padding: 6px 8px; background: #f8fafc; }
|
||||
.preload-density .detail-summary > div > span:first-child::after { content: none; }
|
||||
.preload-density .detail-summary strong, .preload-density .detail-summary :deep(.status-badge) { align-self: center; margin: 6px 8px; font-weight: 400; }
|
||||
.preload-density .detail-summary__wide { grid-column: span 2; }
|
||||
.preload-density .detail-layout :deep(.detail-page-layout__content) { gap: 12px; }
|
||||
.preload-density .detail-section, .preload-density .history-section { padding-top: 8px; }
|
||||
.preload-density .detail-section__heading h2 { font-size: 14px; }
|
||||
.preload-density .detail-section__heading p, .preload-density small { font-size: 12px; }
|
||||
.preload-density .readiness-note { align-items: baseline; }
|
||||
.preload-density .readiness-note span { white-space: normal; overflow: visible; overflow-wrap: anywhere; }
|
||||
@media (max-width: 720px) { .preload-density .detail-summary { grid-template-columns: 1fr; } .preload-density .detail-summary__wide { grid-column: auto; } }
|
||||
</style>
|
||||
|
||||
@@ -229,9 +229,9 @@ describe('LoadingListsView', () => {
|
||||
expect(source).toContain("item.customerName || '客户信息缺失'")
|
||||
expect(source).not.toContain('item.customerName || item.accountId')
|
||||
expect(detailSource).toContain("item.customerName || '客户信息缺失'")
|
||||
expect(detailSource).toContain("history.operatorName || '操作人信息缺失'")
|
||||
expect(detailSource).toContain("row.operatorName || '操作人信息缺失'")
|
||||
expect(detailSource).not.toContain('item.customerName || item.accountId')
|
||||
expect(detailSource).not.toContain('history.operatorName || history.operatorId')
|
||||
expect(detailSource).not.toContain('row.operatorName || row.operatorId')
|
||||
})
|
||||
|
||||
it('does not retain the retired loading-list customs entry', () => {
|
||||
@@ -244,7 +244,7 @@ describe('LoadingListsView', () => {
|
||||
it('filters pure status no-ops and counts identity-distinct visible history events', () => {
|
||||
for (const marker of ['visibleHistories', 'STATUS_UPDATE', 'operationId', 'PUSH_PRELOAD']) expect(detailSource).toContain(marker)
|
||||
expect(detailSource).toContain('visibleHistories.length }} 条')
|
||||
expect(detailSource).toContain('v-for="history in visibleHistories"')
|
||||
expect(detailSource).toContain(':data="visibleHistories"')
|
||||
expect(detailSource).not.toContain('detail.histories.length }} 条')
|
||||
expect(detailSource).not.toContain('v-for="history in detail.histories"')
|
||||
expect(detailSource).toContain('seen.has(eventKey)')
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ContainerSimulationCreate } from '@/modules/container-loading'
|
||||
@@ -181,7 +182,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<AdminPage>
|
||||
<div class="business-page loading-lists-page">
|
||||
<div class="business-page loading-lists-page" :class="{ 'admin-density': isPreload }">
|
||||
<ListPageLayout :ariaLabel="title">
|
||||
<template #toolbar>
|
||||
<QueryToolbar
|
||||
@@ -215,7 +216,7 @@ onMounted(() => {
|
||||
:aria-label="title"
|
||||
>
|
||||
<template #toolbar-actions>
|
||||
<ContainerSimulationCreate v-if="canCreate" success-message="模拟柜及预装清单已创建" @created="onSimulationCreated" />
|
||||
<ContainerSimulationCreate :dense="isPreload" v-if="canCreate" success-message="模拟柜及预装清单已创建" @created="onSimulationCreated" />
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<div class="row-actions">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Printer } from '@lucide/vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
@@ -7,7 +8,7 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { preparePalletLabels, printPalletLabels, type PalletLabelManifest } from '../api/platformLoadingLists'
|
||||
|
||||
const props = defineProps<{ planId: string; warehouseId: string; disabled?: boolean }>()
|
||||
const props = defineProps<{ planId: string; warehouseId: string; disabled?: boolean; dense?: boolean }>()
|
||||
const open = ref(false)
|
||||
const loading = ref(false)
|
||||
const printing = ref(false)
|
||||
@@ -47,7 +48,7 @@ async function print() {
|
||||
|
||||
<template>
|
||||
<el-button :disabled="disabled" @click="show"><Printer :size="16" />打印托盘码</el-button>
|
||||
<AppDialog v-model="open" title="散货托盘码" width="min(760px, calc(100vw - 28px))" confirm-text="生成 PDF" :loading="loading" :confirm-loading="printing" :confirm-disabled="disabledConfirm" @confirm="print">
|
||||
<AppDialog :class="{ 'admin-density-dialog': dense }" v-model="open" title="散货托盘码" width="min(760px, calc(100vw - 28px))" confirm-text="生成 PDF" :loading="loading" :confirm-loading="printing" :confirm-disabled="disabledConfirm" @confirm="print">
|
||||
<LoadErrorState v-if="error" compact title="托盘码操作失败" :message="error.message" :show-retry="false" />
|
||||
<template v-if="manifest">
|
||||
<el-alert type="info" :closable="false" :title="`编号规则:${manifest.containerNo}-FBA地址内部编号-三位序号;一件代发托盘不在此清单。`" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
@@ -244,12 +245,12 @@ watch([planId, warehouseId], () => void loadDetail())
|
||||
|
||||
<template>
|
||||
<AdminPage>
|
||||
<div class="preload-detail-page">
|
||||
<div class="preload-detail-page admin-density">
|
||||
<OperationErrorToast :details="remarkError || editError || confirmError" />
|
||||
<LoadingListDetailDrawer :mode="'preload'" :loading="loading" :detail="detail" :error="error" :can-edit="canEdit" :can-edit-transport="canCorrectTransport" :transport-note="transportCorrectionNote" :confirm-saving="confirmSaving" @back="goBack" @retry="loadDetail" @edit="openEdit" @edit-warehouse-remark="openWarehouseRemark" @confirm-preload="openConfirmPreload" />
|
||||
</div>
|
||||
|
||||
<AppDialog v-model="editOpen" class="loading-transport-dialog" :title="editDialogTitle" width="min(620px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="editSaving" :confirm-disabled="!canEdit" @confirm="saveTransport">
|
||||
<AppDialog v-model="editOpen" class="loading-transport-dialog admin-density-dialog" :title="editDialogTitle" width="min(620px, calc(100vw - 28px))" confirm-text="保存" :confirm-loading="editSaving" :confirm-disabled="!canEdit" @confirm="saveTransport">
|
||||
<el-form ref="editFormRef" class="transport-form" :model="editForm" :rules="editRules" label-position="top">
|
||||
<el-form-item label="真实集装箱号" prop="containerNo"><el-input v-model="editForm.containerNo" maxlength="64" placeholder="请输入提柜后取得的真实集装箱号" @blur="editForm.containerNo = normalizedContainerNo(editForm.containerNo)" /></el-form-item>
|
||||
<el-form-item label="航次" prop="voyageNo"><el-input v-model="editForm.voyageNo" maxlength="64" placeholder="请输入航次" /></el-form-item>
|
||||
@@ -257,7 +258,7 @@ watch([planId, warehouseId], () => void loadDetail())
|
||||
</el-form>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="remarkOpen" class="warehouse-remark-dialog" title="编辑仓库备注" width="min(560px, calc(100vw - 28px))" confirm-text="保存备注" cancel-text="取消" :confirm-loading="remarkSaving" :confirm-disabled="!canEdit" @confirm="saveWarehouseRemark">
|
||||
<AppDialog v-model="remarkOpen" class="warehouse-remark-dialog admin-density-dialog" title="编辑仓库备注" width="min(560px, calc(100vw - 28px))" confirm-text="保存备注" cancel-text="取消" :confirm-loading="remarkSaving" :confirm-disabled="!canEdit" @confirm="saveWarehouseRemark">
|
||||
<el-alert type="info" :closable="false" show-icon title="仓库备注将随装柜完成资料下发海外仓" />
|
||||
<el-form label-position="top" class="warehouse-remark-form">
|
||||
<el-form-item label="客户备注(只读)"><el-input :model-value="remarkTarget?.customerRemark || '—'" disabled /><small>来源:客户下单</small></el-form-item>
|
||||
@@ -267,7 +268,7 @@ watch([planId, warehouseId], () => void loadDetail())
|
||||
<LoadErrorState v-if="remarkError" compact title="仓库备注保存失败" :message="remarkError.message" :show-retry="false"><template #details><ApiErrorMeta :details="remarkError" /></template></LoadErrorState>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog v-model="confirmOpen" class="preload-confirm-dialog" title="确认整份预装计划" width="min(520px, calc(100vw - 28px))" confirm-text="确认预装" :confirm-loading="confirmSaving" :confirm-disabled="!detail?.summary.readiness.canConfirm" @confirm="submitConfirmPreload">
|
||||
<AppDialog v-model="confirmOpen" class="preload-confirm-dialog admin-density-dialog" title="确认整份预装计划" width="min(520px, calc(100vw - 28px))" confirm-text="确认预装" :confirm-loading="confirmSaving" :confirm-disabled="!detail?.summary.readiness.canConfirm" @confirm="submitConfirmPreload">
|
||||
<div class="confirm-content">
|
||||
<el-alert type="warning" :closable="false" show-icon title="本操作将确认整份预装计划" description="真实集装箱号可在提柜或预装详情补录,缺号不阻断此前流程;PDA 确认装柜完成前由服务端执行最终门禁。系统会原子确认本计划包含的全部入库预装票据,并由服务端按真实条件创建唯一 PDA 任务;当前页面不会伪造 PDA 成功。" />
|
||||
<el-descriptions v-if="detail" :column="1" border>
|
||||
@@ -281,10 +282,10 @@ watch([planId, warehouseId], () => void loadDetail())
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.preload-detail-page { display: grid; min-width: 0; max-width: 1180px; gap: 16px; }
|
||||
.preload-detail-page { display: grid; min-width: 0; width: 100%; gap: 12px; }
|
||||
.transport-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; }
|
||||
.transport-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }
|
||||
.confirm-content { display: grid; gap: 16px; }
|
||||
.confirm-content { display: grid; gap: 12px; }
|
||||
.warehouse-remark-form { margin-top: 14px; }
|
||||
.warehouse-remark-form small { display: block; margin-top: 4px; color: var(--cc-muted); }
|
||||
:global(.loading-transport-dialog.el-dialog) { max-height: 90vh; overflow: hidden; }
|
||||
|
||||
@@ -2190,7 +2190,7 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<AdminPage>
|
||||
<section class="business-page unified-orders-page">
|
||||
<AppDialog class="order-density-dialog" v-model="feeConfirmOpen" title="编辑计费数据" confirm-text="确认并发送" :confirm-loading="confirmFeeSending || feeConfirmReferenceLoading" width="min(920px, 94vw)" :teleported="false" @confirm="executeConfirmAndSendFee">
|
||||
<AppDialog class="admin-density-dialog" v-model="feeConfirmOpen" title="编辑计费数据" confirm-text="确认并发送" :confirm-loading="confirmFeeSending || feeConfirmReferenceLoading" width="min(920px, 94vw)" :teleported="false" @confirm="executeConfirmAndSendFee">
|
||||
<el-alert class="fee-confirm-price-alert" title="价格表单价仅作为参考;本次对客单价可人工修改,客户将一次确认计费量、单价和费用合计。" type="info" :closable="false" show-icon />
|
||||
<el-form label-position="top" class="fee-confirm-form">
|
||||
<el-form-item label="入仓码"><el-input :model-value="detail?.summary.inboundCode || '—'" disabled /></el-form-item>
|
||||
@@ -2677,7 +2677,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</ListPageLayout>
|
||||
<SmartLoadingDialog v-model="smartLoadingOpen" />
|
||||
<AppDialog class="order-density-dialog" v-model="batchApprovalOpen" title="批量审核订单" confirm-text="确认批量审核" confirm-type="success" width="min(1100px, calc(100vw - 28px))" :loading="batchApprovalLoading" :confirm-loading="batchApprovalSaving" :confirm-disabled="batchApprovalLoading || !batchApprovalSelectedRows.length" destroy-on-close @confirm="batchApproveOrders">
|
||||
<AppDialog class="admin-density-dialog" v-model="batchApprovalOpen" title="批量审核订单" confirm-text="确认批量审核" confirm-type="success" width="min(1100px, calc(100vw - 28px))" :loading="batchApprovalLoading" :confirm-loading="batchApprovalSaving" :confirm-disabled="batchApprovalLoading || !batchApprovalSelectedRows.length" destroy-on-close @confirm="batchApproveOrders">
|
||||
<el-alert title="请逐票核对入仓码、客户、中文品名、渠道和目的仓;审核通过后订单进入待测量。" type="info" :closable="false" show-icon />
|
||||
<LoadErrorState v-if="batchApprovalError" compact title="批量审核预览加载失败" :message="batchApprovalError.message" :loading="batchApprovalLoading" @retry="openBatchApproval"><template #details><ApiErrorMeta :details="batchApprovalError" /></template></LoadErrorState>
|
||||
<template v-else>
|
||||
@@ -2693,12 +2693,12 @@ onBeforeUnmount(() => {
|
||||
</el-table>
|
||||
</template>
|
||||
</AppDialog>
|
||||
<AppDialog class="order-density-dialog" v-model="coLoadCreateOpen" title="合柜报关" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="!canCreateCoLoadGroup" @confirm="createCoLoadGroup">
|
||||
<AppDialog class="admin-density-dialog" v-model="coLoadCreateOpen" title="合柜报关" confirm-text="确认建组" :confirm-loading="coLoadSaving" :confirm-disabled="!canCreateCoLoadGroup" @confirm="createCoLoadGroup">
|
||||
<el-alert type="warning" :closable="false" show-icon :title="`所选 ${selectedOrders.length} 票须属于同一国内仓、同一渠道,并全量进入同一柜;目的仓或目的港可以不同。资格由服务端按完整入仓片段校验。`" />
|
||||
<div class="coload-selection"><span v-for="row in selectedOrders" :key="String(row.orderId)">{{ row.inboundCode || '待生成入仓码' }}</span></div>
|
||||
<div v-if="coLoadError" class="coload-error" role="alert"><span>{{ coLoadError.message }}</span><ApiErrorMeta :details="coLoadError" /></div>
|
||||
</AppDialog>
|
||||
<AppDialog class="order-density-dialog" v-model="coLoadDetailOpen" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(720px, 94vw)" :loading="coLoadDetailLoading" :confirm-loading="coLoadCancelSaving">
|
||||
<AppDialog class="admin-density-dialog" v-model="coLoadDetailOpen" title="同柜组成员" :show-confirm="false" cancel-text="关闭" width="min(720px, 94vw)" :loading="coLoadDetailLoading" :confirm-loading="coLoadCancelSaving">
|
||||
<LoadErrorState v-if="coLoadDetailError" title="同柜组成员加载失败" :message="coLoadDetailError.message" :loading="coLoadDetailLoading" @retry="loadCoLoadDetail"><template #details><ApiErrorMeta :details="coLoadDetailError" /></template></LoadErrorState>
|
||||
<template v-else-if="coLoadDetailSummary">
|
||||
<div class="coload-detail-summary"><el-tag size="small" :type="coLoadDetailSummary.status === 'ACTIVE' ? 'warning' : 'info'">{{ coLoadGroupSummary(coLoadDetailSummary) }}</el-tag><span>{{ coLoadDetailSummary.memberCount }} 个成员</span></div>
|
||||
@@ -2725,16 +2725,16 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</AppDrawer>
|
||||
|
||||
<AppDialog class="order-density-dialog" v-model="noteOpen" title="添加内部备注" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
|
||||
<AppDialog class="admin-density-dialog" v-model="noteOpen" title="添加内部备注" confirm-text="保存备注" :confirm-loading="noteSaving" @confirm="saveNote">
|
||||
<el-input v-model="noteContent" type="textarea" :rows="5" maxlength="2000" show-word-limit placeholder="请输入仅平台内部可见的备注" aria-label="内部备注内容" />
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog class="order-density-dialog" v-model="holdDialogOpen" :title="holdAction === 'HOLD' ? '确认扣货' : '确认解除扣货'" :confirm-text="holdAction === 'HOLD' ? '确认扣货' : '确认解除'" :confirm-type="holdAction === 'HOLD' ? 'danger' : 'primary'" :confirm-loading="holdSaving" :confirm-disabled="!holdReason.trim() || holdReason.trim().length > 1000" width="min(600px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction">
|
||||
<AppDialog class="admin-density-dialog" v-model="holdDialogOpen" :title="holdAction === 'HOLD' ? '确认扣货' : '确认解除扣货'" :confirm-text="holdAction === 'HOLD' ? '确认扣货' : '确认解除'" :confirm-type="holdAction === 'HOLD' ? 'danger' : 'primary'" :confirm-loading="holdSaving" :confirm-disabled="!holdReason.trim() || holdReason.trim().length > 1000" width="min(600px, calc(100vw - 28px))" destroy-on-close @confirm="submitHoldAction">
|
||||
<el-alert :title="holdAction === 'HOLD' ? '扣货后服务端将阻止后续实物履约操作。' : '解除后是否可继续履约仍以服务端状态为准。'" :type="holdAction === 'HOLD' ? 'warning' : 'info'" :closable="false" show-icon />
|
||||
<el-form label-position="top" class="hold-command-form"><el-form-item :label="holdAction === 'HOLD' ? '扣货原因' : '解除原因'" required><el-input v-model="holdReason" type="textarea" :rows="4" maxlength="1000" show-word-limit :placeholder="holdAction === 'HOLD' ? '请输入扣货原因' : '请输入解除扣货原因'" aria-label="扣货操作原因" /></el-form-item><el-form-item label="关联文件(选填)"><el-upload :key="holdEvidenceUploadKey" :auto-upload="false" :limit="1" accept=".txt,.pdf,.jpg,.jpeg,.png,text/plain,application/pdf,image/jpeg,image/png" :disabled="holdSaving" :on-change="selectHoldEvidenceFile" :on-remove="removeHoldEvidenceFile"><el-button :disabled="holdSaving"><FileUp :size="15" />选择文件</el-button><template #tip><div class="el-upload__tip">仅支持 TXT、PDF、JPG、PNG,单文件不超过 10MB;服务端将校验文件真实内容。</div></template></el-upload></el-form-item><LoadErrorState v-if="holdEvidenceError" compact title="关联文件处理失败" :message="holdEvidenceError.message" :show-retry="false"><template #details><ApiErrorMeta :details="holdEvidenceError" /></template></LoadErrorState></el-form>
|
||||
</AppDialog>
|
||||
|
||||
<AppDialog class="order-density-dialog" v-model="approvalOpen" title="审核订单" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" destroy-on-close @confirm="approveOrder" @cancel="closeApproval">
|
||||
<AppDialog class="admin-density-dialog" v-model="approvalOpen" title="审核订单" confirm-text="审核通过" confirm-type="success" :confirm-loading="approvalSaving" destroy-on-close @confirm="approveOrder" @cancel="closeApproval">
|
||||
<dl v-if="approvalOrder" class="approval-facts">
|
||||
<div><dt>入仓码</dt><dd>{{ approvalOrder.inboundCode || '—' }}</dd></div>
|
||||
<div><dt>客户</dt><dd>{{ customerName(approvalOrder) }}</dd></div>
|
||||
@@ -2753,7 +2753,7 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
|
||||
<AppDialog
|
||||
class="order-density-dialog"
|
||||
class="admin-density-dialog"
|
||||
v-model="measurementPreviewOpen"
|
||||
:title="measurementPreviewTitle"
|
||||
width="min(920px, calc(100vw - 28px))"
|
||||
@@ -2769,7 +2769,7 @@ onBeforeUnmount(() => {
|
||||
</AppDialog>
|
||||
|
||||
<InboundMeasurementDialog
|
||||
class="order-density-dialog order-measurement-dialog"
|
||||
class="admin-density-dialog"
|
||||
v-if="measurementOrder?.inboundId"
|
||||
v-model="measurementDialogOpen"
|
||||
:customer-id="String(measurementOrder.tenantId)"
|
||||
|
||||
@@ -29,64 +29,8 @@
|
||||
.unified-orders-page .detail-panel .el-empty__image { display: none; }
|
||||
.unified-orders-page .detail-panel .el-empty__description { margin-top: 0; }
|
||||
|
||||
/* Dialogs are teleported; opt in explicitly instead of styling all AppDialog instances. */
|
||||
.order-density-dialog.el-dialog {
|
||||
--el-font-size-base: 13px;
|
||||
--el-component-size: 32px;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.order-density-dialog.el-dialog:not(.is-fullscreen) { margin-top: 6vh; max-height: 88vh; }
|
||||
.order-density-dialog.el-dialog .el-dialog__header { margin: 0; padding: 10px 48px 10px 16px; border-bottom: 1px solid var(--cc-border); }
|
||||
.order-density-dialog.el-dialog .el-dialog__title { font-size: 16px; line-height: 24px; font-weight: 600; }
|
||||
.order-density-dialog.el-dialog .el-dialog__body { padding: 12px 16px; }
|
||||
.order-density-dialog.el-dialog .el-dialog__footer { padding: 8px 16px; border-top: 1px solid var(--cc-border); }
|
||||
.order-density-dialog.el-dialog .el-table { font-size: 13px; }
|
||||
.order-density-dialog.el-dialog .el-table__cell { height: 36px; padding: 4px 0; }
|
||||
.order-density-dialog.el-dialog .el-table .cell { line-height: 22px; }
|
||||
.order-density-dialog.el-dialog .el-form-item { margin-bottom: 14px; }
|
||||
.order-density-dialog.el-dialog .el-form-item__label { font-size: 13px; }
|
||||
.order-density-dialog.el-dialog .el-alert { padding: 8px 10px; border-radius: 3px; }
|
||||
.order-density-dialog.el-dialog .el-empty { padding: 12px 0; }
|
||||
.order-density-dialog.el-dialog .el-empty__image { display: none; }
|
||||
.order-density-dialog.el-dialog .el-empty__description { margin-top: 0; }
|
||||
.order-density-dialog.el-dialog .batch-approval-table .cell { white-space: normal; overflow-wrap: anywhere; }
|
||||
.order-density-dialog.el-dialog .batch-approval-table .el-tag { height: auto; min-height: 24px; white-space: normal; line-height: 20px; }
|
||||
|
||||
@media (min-width: 961px) {
|
||||
.order-measurement-dialog.el-dialog:not(.is-fullscreen) {
|
||||
/* The inherited dialog width is inline; only this order entry opts into the wider workspace. */
|
||||
width: min(1560px, calc(100vw - 48px)) !important;
|
||||
height: 92vh; max-height: 92vh; margin-top: 4vh;
|
||||
}
|
||||
.order-measurement-dialog .measurement-dialog-weight { padding-bottom: 10px; }
|
||||
.order-measurement-dialog .measurement-section { padding: 10px 0; }
|
||||
.order-measurement-dialog .measurement-summary-section { gap: 8px; }
|
||||
.order-measurement-dialog .measurement-section__heading { align-items: center; margin-bottom: 8px; }
|
||||
.order-measurement-dialog .measurement-metrics { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
||||
.order-measurement-dialog .measurement-metrics > div { min-height: 36px; padding: 6px 10px; display: flex; align-items: baseline; gap: 8px; }
|
||||
.order-measurement-dialog .measurement-metrics dd { font-size: 14px; }
|
||||
.order-measurement-dialog .measurement-metrics small { font-size: 12px; }
|
||||
.order-measurement-dialog .measurement-data-grid { gap: 8px 12px; }
|
||||
.order-measurement-dialog .measurement-declaration { padding-top: 8px; }
|
||||
.order-measurement-dialog .measurement-declaration h4 { margin-bottom: 6px; font-size: 13px; }
|
||||
.order-measurement-dialog .measurement-panel > .el-alert { margin: 8px 0; }
|
||||
.order-measurement-dialog .measurement-summary-header span { font-size: 12px; }
|
||||
.order-measurement-dialog .measurement-table .el-table__header th { padding: 4px 0; font-size: 13px; }
|
||||
.order-measurement-dialog .weight-heading { flex-wrap: wrap; gap: 8px; padding-bottom: 8px; }
|
||||
.order-measurement-dialog .weight-heading__actions { flex-wrap: wrap; gap: 12px; }
|
||||
.order-measurement-dialog .pallet-picker { width: 340px; }
|
||||
.order-measurement-dialog .weight-list { margin-top: 8px; }
|
||||
.order-measurement-dialog .weight-grid { grid-template-columns: repeat(auto-fill, minmax(420px, 1fr)); gap: 8px; }
|
||||
.order-measurement-dialog .weight-card { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 10px; align-items: start; padding: 8px; border-radius: 2px; }
|
||||
.order-measurement-dialog .weight-card .weight-thumb { width: 120px; border-radius: 2px; }
|
||||
.order-measurement-dialog .weight-card__body { min-width: 0; padding: 0; text-align: left; }
|
||||
.order-measurement-dialog .weight-card__value { min-height: 28px; padding-right: 32px; text-align: left; }
|
||||
.order-measurement-dialog .weight-card__value strong { font-size: 14px; }
|
||||
.order-measurement-dialog .weight-card__facts { gap: 3px; margin-top: 4px; padding-top: 4px; }
|
||||
.order-measurement-dialog .weight-card__facts div { font-size: 12px; }
|
||||
.order-measurement-dialog .weight-card__correct { width: auto; margin-top: 6px; }
|
||||
}
|
||||
.admin-density-dialog.el-dialog .batch-approval-table .cell { white-space: normal; overflow-wrap: anywhere; }
|
||||
.admin-density-dialog.el-dialog .batch-approval-table .el-tag { height: auto; min-height: 24px; white-space: normal; line-height: 20px; }
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.unified-orders-page .inbound-fee-panel .fee-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { MapPin, Pencil, Plus, Power, Save, Trash2 } from '@lucide/vue'
|
||||
@@ -434,7 +435,7 @@ async function removeLocation(row: PlatformLocationResource) {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除已停用库位“${row.locationCode}”吗?系统会保留审计记录,此操作不能直接恢复。`,
|
||||
'删除库位',
|
||||
{ type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消' },
|
||||
{ customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认删除', cancelButtonText: '取消' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
@@ -480,7 +481,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="location-page">
|
||||
<AdminPage as="div" class="location-page admin-density">
|
||||
<ListPageLayout aria-label="库位列表页面">
|
||||
<template #toolbar>
|
||||
<QueryToolbar
|
||||
@@ -602,7 +603,7 @@ onMounted(() => {
|
||||
v-model="editorOpen"
|
||||
:title="editorTitle"
|
||||
width="min(560px, calc(100vw - 28px))"
|
||||
class="location-editor-dialog"
|
||||
class="location-editor-dialog admin-density-dialog"
|
||||
:confirm-loading="saving"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!saving"
|
||||
@@ -682,22 +683,22 @@ onMounted(() => {
|
||||
.location-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
|
||||
.location-actions :deep(.el-button) { margin-left: 0; }
|
||||
.location-cell { display: flex; min-width: 0; align-items: center; gap: 10px; }
|
||||
.location-cell > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 7px; background: #eef3fa; color: var(--cc-primary); }
|
||||
.location-cell > span { display: grid; width: 20px; height: 20px; flex: 0 0 20px; place-items: center; border-radius: 7px; background: #eef3fa; color: var(--cc-primary); }
|
||||
.location-cell > div, .stacked-value { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
||||
.location-cell strong, .stacked-value strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.location-cell small, .stacked-value small { color: var(--cc-muted); font-size: 11px; }
|
||||
.location-cell strong, .stacked-value strong { overflow-wrap: anywhere; }
|
||||
.location-cell small, .stacked-value small { color: var(--cc-muted); font-size: 12px; }
|
||||
.remark-cell { display: -webkit-box; overflow: hidden; line-height: 1.5; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
|
||||
:global(.location-editor-dialog) { max-height: min(760px, calc(100vh - 32px)); display: flex; flex-direction: column; }
|
||||
:global(.location-editor-dialog .el-dialog__body) { min-height: 0; overflow-y: auto; }
|
||||
:global(.location-editor-dialog .el-dialog__footer) { flex: 0 0 auto; }
|
||||
.editor-form { padding: 8px 24px 12px; }
|
||||
.editor-form { padding: 0; }
|
||||
.editor-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 14px; }
|
||||
.editor-form :deep(.el-input-number), .editor-form :deep(.el-select) { width: 100%; }
|
||||
.editor-form :deep(.el-form-item) { margin-bottom: 18px; }
|
||||
.editor-form :deep(.el-form-item__label) { padding-bottom: 7px; color: #344054; font-size: 12px; font-weight: 650; line-height: 1.4; }
|
||||
.editor-form :deep(.el-form-item) { margin-bottom: 14px; }
|
||||
.editor-form :deep(.el-form-item__label) { padding-bottom: 4px; color: #344054; font-size: 12px; font-weight: 650; line-height: 1.4; }
|
||||
.editor-remark-field { margin-bottom: 0 !important; }
|
||||
.editor-footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.editor-footer > span { color: var(--cc-muted); font-size: 11px; }
|
||||
.editor-footer > span { color: var(--cc-muted); font-size: 12px; }
|
||||
.editor-footer > span i { color: var(--el-color-danger); font-style: normal; }
|
||||
.editor-footer > div { display: flex; gap: 8px; }
|
||||
.editor-footer :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Boxes, Pencil, Plus, Save, Trash2, Warehouse as WarehouseIcon } from '@lucide/vue'
|
||||
@@ -343,7 +344,7 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AdminPage as="div" class="pallet-page">
|
||||
<AdminPage as="div" class="pallet-page admin-density">
|
||||
<section class="summary-grid" aria-label="托盘数量概览">
|
||||
<article><span>托盘总数</span><strong>{{ summary.totalCount }}</strong></article>
|
||||
<article><span>闲置托盘</span><strong class="idle">{{ summary.idleCount }}</strong></article>
|
||||
@@ -423,7 +424,7 @@ onMounted(() => {
|
||||
</section>
|
||||
</ListPageLayout>
|
||||
|
||||
<AppDialog v-model="editorOpen" :title="editorTitle" width="min(560px, calc(100vw - 28px))" class="pallet-editor-dialog" :close-on-click-modal="false" :confirm-loading="saving" :show-confirm="false" destroy-on-close>
|
||||
<AppDialog v-model="editorOpen" :title="editorTitle" width="min(560px, calc(100vw - 28px))" class="pallet-editor-dialog admin-density-dialog" :close-on-click-modal="false" :confirm-loading="saving" :show-confirm="false" destroy-on-close>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" class="editor-form">
|
||||
<div class="editor-field-grid">
|
||||
<el-form-item label="所属仓库" prop="warehouseId" required>
|
||||
@@ -478,29 +479,29 @@ onMounted(() => {
|
||||
|
||||
<style scoped>
|
||||
.pallet-page { display: grid; min-width: 0; gap: 16px; }
|
||||
.summary-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:16px; }
|
||||
.summary-grid article { display:grid; gap:8px; padding:18px 20px; border:1px solid var(--cc-border); border-radius:8px; background:#fff; }
|
||||
.summary-grid span { color:var(--cc-muted); font-size:12px; }.summary-grid strong { font-size:26px; }.summary-grid .idle { color:var(--el-color-success); }.summary-grid .occupied { color:var(--el-color-warning); }
|
||||
.surface-panel { overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }
|
||||
.summary-grid { display:flex; flex-wrap:wrap; gap:8px 28px; padding:8px 12px; border:1px solid var(--cc-border); background:#fff; }
|
||||
.summary-grid article { display:flex; align-items:baseline; gap:8px; }
|
||||
.summary-grid span { color:var(--cc-muted); font-size:12px; }.summary-grid strong { font-size:14px; }.summary-grid .idle { color:var(--el-color-success); }.summary-grid .occupied { color:var(--el-color-warning); }
|
||||
.surface-panel { overflow: hidden; border: 1px solid var(--cc-border); border-radius: 4px; background: #fff; }
|
||||
.table-panel { min-width: 0; }
|
||||
.context-empty { padding: 56px 20px; color: var(--cc-muted); text-align: center; }
|
||||
.context-empty { padding: 16px; color: var(--cc-muted); text-align: center; }
|
||||
.pallet-row-actions { gap: 8px; white-space: nowrap; }
|
||||
.pallet-row-actions :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.pallet-cell { display: flex; align-items: center; gap: 10px; }
|
||||
.stacked-value { display: grid; min-width: 0; gap: 3px; }
|
||||
.stacked-value strong, .stacked-value small { overflow-wrap: anywhere; }
|
||||
.stacked-value small { color: var(--cc-muted); font-size: 11px; }
|
||||
.stacked-value small { color: var(--cc-muted); font-size: 12px; }
|
||||
.pallet-cell > span { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; border-radius: 8px; background: var(--el-color-primary-light-9); color: var(--cc-primary); }
|
||||
.pallet-cell > span { width: 34px; height: 34px; }
|
||||
.pallet-cell > span { width: 20px; height: 20px; }
|
||||
.remark-text { display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-height: 1.55; }
|
||||
:global(.pallet-editor-dialog) { max-height: min(760px, calc(100vh - 32px)); display: flex; flex-direction: column; }
|
||||
:global(.pallet-editor-dialog .el-dialog__body) { min-height: 0; overflow-y: auto; }
|
||||
:global(.pallet-editor-dialog .el-dialog__footer) { flex: 0 0 auto; }
|
||||
.editor-form { padding: 8px 24px 12px; }
|
||||
.editor-form { padding: 0; }
|
||||
.editor-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 14px; }
|
||||
.editor-form :deep(.el-input-number), .editor-form :deep(.el-select) { width: 100%; }
|
||||
.editor-form :deep(.el-form-item) { margin-bottom: 18px; }
|
||||
.editor-form :deep(.el-form-item__label) { padding-bottom: 7px; color: #344054; font-size: 12px; font-weight: 650; line-height: 1.4; }
|
||||
.editor-form :deep(.el-form-item) { margin-bottom: 14px; }
|
||||
.editor-form :deep(.el-form-item__label) { padding-bottom: 4px; color: #344054; font-size: 12px; font-weight: 650; line-height: 1.4; }
|
||||
@media (max-width: 720px) {
|
||||
.summary-grid { grid-template-columns:1fr; }
|
||||
.editor-field-grid { grid-template-columns: minmax(0, 1fr); }
|
||||
@@ -510,7 +511,7 @@ onMounted(() => {
|
||||
.drawer-footer { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.drawer-footer :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
.pallet-editor-footer { align-items: center; justify-content: space-between; }
|
||||
.pallet-editor-footer > span { color: var(--cc-muted); font-size: 11px; }
|
||||
.pallet-editor-footer > span { color: var(--cc-muted); font-size: 12px; }
|
||||
.pallet-editor-footer > span i { color: var(--el-color-danger); font-style: normal; }
|
||||
.pallet-editor-footer > div { display: flex; gap: 8px; }
|
||||
.pallet-editor-footer > div :deep(.el-button + .el-button) { margin-left: 0; }
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/* Explicit opt-in for approved dense business workspaces and teleported dialogs. */
|
||||
.admin-density, .admin-density-dialog {
|
||||
--el-font-size-base: 13px;
|
||||
--el-component-size: 32px;
|
||||
--cc-list-table-font-size: 13px;
|
||||
--cc-list-action-font-size: 13px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.admin-density .surface-panel, .admin-density .query-toolbar {
|
||||
border-radius: 4px; box-shadow: none;
|
||||
}
|
||||
.admin-density .el-table, .admin-density-dialog .el-table {
|
||||
--el-table-header-bg-color: #f8fafc;
|
||||
--el-table-border-color: #e5e7eb;
|
||||
--el-table-row-hover-bg-color: #f3f7fb;
|
||||
font-size: 13px;
|
||||
}
|
||||
.admin-density .data-table .el-table .el-table__cell, .admin-density-dialog .el-table .el-table__cell { height: 36px; padding: 4px 0; }
|
||||
.admin-density .el-table .cell, .admin-density-dialog .el-table .cell { line-height: 22px; }
|
||||
.admin-density .el-table .el-button--small, .admin-density-dialog .el-table .el-button--small { height: 28px; padding: 4px 8px; }
|
||||
.admin-density .el-table__body tr.el-table__row--striped td.el-table__cell { background: #fcfcfd; }
|
||||
.admin-density .data-table .data-table__toolbar { min-height: 44px; padding: 6px 12px; }
|
||||
.admin-density .data-table .data-table__pagination { min-height: 48px; padding: 8px 12px; }
|
||||
.admin-density .el-empty { padding: 12px 0; }
|
||||
.admin-density .el-empty__image { display: none; }
|
||||
.admin-density .el-empty__description { margin-top: 0; }
|
||||
.admin-density .el-empty__description p { font-size: 13px; }
|
||||
.admin-density-dialog .el-form-item__label { line-height: 22px; }
|
||||
.admin-density-dialog .el-descriptions__label.el-descriptions__cell { font-size: 12px; background: #f8fafc; }
|
||||
.admin-density-dialog .el-descriptions__content.el-descriptions__cell { font-size: 13px; overflow-wrap: anywhere; }
|
||||
.admin-density-dialog.el-dialog.is-fullscreen { width: 100%; max-width: 100%; height: 100vh; max-height: 100vh; margin: 0; border-radius: 0; }
|
||||
.admin-density-dialog.el-dialog.admin-density-workspace:not(.is-fullscreen) { height: 92vh; max-height: 92vh; margin-top: 4vh; }
|
||||
.admin-density-dialog .el-dialog__headerbtn { top: 0; }
|
||||
.admin-density-dialog .app-dialog__native-fullscreen-control { top: 6px; }
|
||||
.admin-density .density-tab-icon { display: inline-flex; align-items: center; justify-content: center; width: 18px; font: 17px/1 "Segoe UI Emoji", "Apple Color Emoji", "Noto Color Emoji", sans-serif; }
|
||||
.admin-density-confirm.el-message-box { --el-font-size-base: 13px; border-radius: 4px; padding: 12px 16px; }
|
||||
.admin-density-confirm .el-message-box__title { font-size: 16px; }
|
||||
|
||||
/* Dialogs are teleported; opt in explicitly instead of styling all AppDialog instances. */
|
||||
.admin-density-dialog.el-dialog {
|
||||
--el-font-size-base: 13px;
|
||||
--el-component-size: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.admin-density-dialog.el-dialog:not(.is-fullscreen) { margin-top: 6vh; max-height: 88vh; }
|
||||
.admin-density-dialog.el-dialog .el-dialog__header { flex: 0 0 auto; margin: 0; padding: 10px 48px 10px 16px; border-bottom: 1px solid var(--cc-border); }
|
||||
.admin-density-dialog.el-dialog .el-dialog__title { font-size: 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: 12px 16px; }
|
||||
.admin-density-dialog.el-dialog .el-dialog__footer { flex: 0 0 auto; padding: 8px 16px; border-top: 1px solid var(--cc-border); }
|
||||
.admin-density-dialog.el-dialog .el-table { font-size: 13px; }
|
||||
.admin-density-dialog.el-dialog .el-table__cell { height: 36px; padding: 4px 0; }
|
||||
.admin-density-dialog.el-dialog .el-table .cell { line-height: 22px; }
|
||||
.admin-density-dialog.el-dialog .el-form-item { margin-bottom: 14px; }
|
||||
.admin-density-dialog.el-dialog .el-form-item__label { font-size: 13px; }
|
||||
.admin-density-dialog.el-dialog .el-alert { padding: 8px 10px; border-radius: 3px; }
|
||||
.admin-density-dialog.el-dialog .el-empty { padding: 12px 0; }
|
||||
.admin-density-dialog.el-dialog .el-empty__image { display: none; }
|
||||
.admin-density-dialog.el-dialog .el-empty__description { margin-top: 0; }
|
||||
Reference in New Issue
Block a user