Release/test 合并上生产 #2

Merged
admin merged 736 commits from release/test into main 2026-09-17 11:50:56 +08:00
30 changed files with 270 additions and 240 deletions
Showing only changes of commit b0de9c026f - Show all commits
+1
View File
@@ -4,6 +4,7 @@
1. 用“模块快速路由”选择最小业务模块,进入后读取该目录最近的 `AGENTS.md`;按 `routes.ts → pages/ → api/types/stores/composables` 下钻,不先扫描全部 `src/`。
2. 页面、布局或交互任务同时读取 `../../docs/Admin-Client前端原型还原验收规则.md` 和后台原型 `../../docs/悦/货代SAAS平台后台/AGENTS.md`。
订单样板和国内仓样式统一还须读取 `../../docs/Admin高密度界面规范.md`;以其已确认密度与彩色页签为视觉基线,逐操作保留业务语义和承载方式,公共样式按已确认菜单范围接入。
3. 标准列表或公共组件任务读取 `src/shared/components/AGENTS.md`;国内/海外 HTTP、Token 或刷新任务读取 `src/shared/api/AGENTS.md`;Finance 任务读取 `src/modules/finance/AGENTS.md`;海外仓全流程任务先读 `../../docs/海外仓业务/README.md`。
4. 平台/客户层级与权限公共语义读取 `../../docs/平台客户层级与权限契约.md`;双区域 API、海外 Token 和跨区事件读取 `../../docs/跨区域认证与服务通信契约.md`。
@@ -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)')
+16 -4
View File
@@ -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; }
+1 -1
View File
@@ -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',
+3 -1
View File
@@ -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; }
+62
View File
@@ -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; }