This commit is contained in:
2026-09-15 09:58:15 +08:00
parent 3f998bdb88
commit 4bf66d9571
90 changed files with 1599 additions and 1388 deletions
@@ -195,10 +195,10 @@ describe('WarehousesView', () => {
it('uses bordered striped tables and semantic operation buttons', () => {
expect(warehousesSource).toContain('border')
expect(warehousesSource).toContain('stripe')
expect(warehousesSource).toContain('size="small" plain type="primary"')
expect(warehousesSource).toMatch(/size="small"\s+plain\s+:type=/)
expect(warehousesSource).toContain('<Pencil :size="15" />')
expect(warehousesSource).toContain('<Power :size="15" />')
expect(warehousesSource).toContain('data-tone="neutral" class="density-action"')
expect(warehousesSource).toMatch(/:data-tone=/)
expect(warehousesSource).toContain('>编辑</el-button>')
expect(warehousesSource).toContain(':data-tone=')
for (const label of ['仓库名称', '仓库编码', '内部编号', '州/省', '城市', '邮编']) expect(warehousesSource).toContain(`label: '${label}'`)
expect(warehousesSource).toContain('@media (max-width: 720px)')
})
@@ -206,7 +206,7 @@ describe('WarehousesView', () => {
expect(warehousesSource).toContain('<AppDialog v-model="editorOpen" :title="editorLabel" width="min(650px, calc(100vw - 28px))"')
expect(warehousesSource).not.toContain('<AppDrawer v-model="editorOpen"')
expect(warehousesSource).toContain(':global(.warehouse-editor-dialog .el-dialog__body) { min-height: 0; overflow-y: auto; }')
expect(warehousesSource).toContain('.drawer-form :deep(.form-grid.two-cols) { grid-template-columns: minmax(0, 1fr); }')
expect(warehousesSource).toContain('.density-form :deep(.form-grid.two-cols) { grid-template-columns: minmax(0, 1fr); }')
})
beforeEach(() => {
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
+31 -36
View File
@@ -1,7 +1,8 @@
<script setup lang="ts">
import '@/styles/adminDensity.css'
import { computed, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type UploadFile } from 'element-plus'
import { ChevronDown, Download, FileSpreadsheet, FileUp, MapPin, Pencil, Plus, Power, Save, Warehouse as WarehouseIcon } from '@lucide/vue'
import { ChevronDown, Download, FileSpreadsheet, FileUp, MapPin, Plus, Save, Warehouse as WarehouseIcon } from '@lucide/vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
@@ -582,7 +583,7 @@ async function toggleStatus(warehouse: WarehouseRow) {
? `停用“${warehouse.name}”后,客户新建头程订单时将无法选择该公共仓库。`
: `确认重新启用“${warehouse.name}”供客户下单选择吗?`,
`${targetText}公共仓库`,
{ confirmButtonText: `确认${targetText}`, cancelButtonText: '取消', type: target === 'INACTIVE' ? 'warning' : 'info' },
{ customClass: 'admin-density-confirm', confirmButtonText: `确认${targetText}`, cancelButtonText: '取消', type: target === 'INACTIVE' ? 'warning' : 'info' },
)
statusLoadingIds.value = new Set(statusLoadingIds.value).add(warehouse.id)
await changePlatformWarehouseStatus(warehouse.id, target)
@@ -689,7 +690,7 @@ onMounted(() => void loadWarehouses())
</script>
<template>
<AdminPage as="div">
<AdminPage class="admin-density" as="div">
<ListPageLayout aria-label="仓库列表页面">
<template #toolbar>
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="仓库筛选" :fields="queryFields" :loading="loading" preference-key="warehouses-query" @search="queryWarehouses" />
@@ -723,24 +724,24 @@ onMounted(() => void loadWarehouses())
<template #toolbar-actions>
<div v-if="access.can('platform:warehouse:create')" class="warehouse-primary-actions">
<el-button :disabled="!warehouseReady" @click="openImport"><FileUp :size="16" />导入仓库</el-button>
<el-dropdown trigger="click" :disabled="!warehouseReady" @command="openCreate($event as WarehouseType)">
<el-dropdown popper-class="admin-density warehouse-create-menu" trigger="click" :disabled="!warehouseReady" @command="openCreate($event as WarehouseType)">
<el-button type="primary" :disabled="!warehouseReady"><Plus :size="16" />新增仓库<ChevronDown :size="15" /></el-button>
<template #dropdown><el-dropdown-menu><el-dropdown-item command="FBA"><FileSpreadsheet :size="15" />新增 FBA 地址</el-dropdown-item><el-dropdown-item command="DOMESTIC"><WarehouseIcon :size="15" />新增国内仓</el-dropdown-item><el-dropdown-item command="OVERSEAS"><MapPin :size="15" />新增海外仓</el-dropdown-item><el-dropdown-item command="OVERSEAS_PARTNER"><MapPin :size="15" />新增海外仓合作仓</el-dropdown-item></el-dropdown-menu></template>
</el-dropdown>
</div>
</template>
<template #actions="{ row }">
<div v-if="access.can('platform:warehouse:edit') || access.can('platform:warehouse:status')" class="table-actions">
<el-button v-if="access.can('platform:warehouse:edit')" size="small" plain type="primary" :disabled="!warehouseReady || editLoading" @click="openEdit(row as WarehouseRow)"><Pencil :size="15" />编辑</el-button>
<el-button
<div v-if="access.can('platform:warehouse:edit') || access.can('platform:warehouse:status')" class="table-actions density-actions">
<el-button data-tone="neutral" class="density-action" v-if="access.can('platform:warehouse:edit')" size="small" :disabled="!warehouseReady || editLoading" @click="openEdit(row as WarehouseRow)">编辑</el-button>
<el-button class="density-action"
v-if="access.can('platform:warehouse:status')"
size="small"
plain
:type="(row as WarehouseRow).status === 'ACTIVE' ? 'danger' : 'success'"
:data-tone="(row as WarehouseRow).status === 'ACTIVE' ? 'danger' : 'success'"
:disabled="!warehouseReady"
:loading="statusLoadingIds.has((row as WarehouseRow).id)"
@click="toggleStatus(row as WarehouseRow)"
><Power :size="15" />{{ (row as WarehouseRow).status === 'ACTIVE' ? '停用' : '启用' }}</el-button>
>{{ (row as WarehouseRow).status === 'ACTIVE' ? '停用' : '启用' }}</el-button>
</div>
<span v-else class="warehouse-subline">仅查看</span>
</template>
@@ -755,9 +756,9 @@ onMounted(() => void loadWarehouses())
</section>
</ListPageLayout>
<AppDialog v-model="editorOpen" :title="editorLabel" width="min(650px, calc(100vw - 28px))" class="warehouse-editor-dialog" :close-on-click-modal="false" :confirm-loading="saving" :show-confirm="false" destroy-on-close>
<el-form :model="form" label-position="top" class="drawer-form" :disabled="saving">
<FormSection title="基础信息">
<AppDialog v-model="editorOpen" :title="editorLabel" width="min(650px, calc(100vw - 28px))" class="warehouse-editor-dialog admin-density-dialog" :close-on-click-modal="false" :confirm-loading="saving" :show-confirm="false" destroy-on-close>
<el-form :model="form" label-position="top" class="density-form" :disabled="saving">
<FormSection class="density-form-section" title="基础信息">
<div class="form-grid two-cols">
<el-form-item v-if="isFbaCreate" label="FBA 编码" required :class="{ 'has-field-error': formErrors.code }"><el-input v-model="form.code" maxlength="50" placeholder="例如 LAX9" @input="form.code = String($event).toUpperCase(); clearFieldError('code')" /><p v-if="formErrors.code" class="field-error" role="alert">{{ formErrors.code }}</p></el-form-item>
<el-form-item v-if="isFbaCreate" label="国家或地区" required>
@@ -783,7 +784,7 @@ onMounted(() => void loadWarehouses())
</div>
</FormSection>
<FormSection title="地址信息">
<FormSection class="density-form-section" title="地址信息">
<div class="form-grid two-cols">
<el-form-item :label="addressRegionLabel" :required="isFbaCreate || usesUsStateSelect" :class="{ 'has-field-error': formErrors.province }">
<el-select v-if="usesUsStateSelect" v-model="form.province" filterable :placeholder="regionPlaceholder" @change="onRegionChange"><el-option v-for="state in usStateOptions" :key="state.value" :label="state.label" :value="state.value" /></el-select>
@@ -796,14 +797,14 @@ onMounted(() => void loadWarehouses())
<el-form-item label="详细地址" required :class="{ 'has-field-error': formErrors.address }"><el-input v-model="form.address" maxlength="255" type="textarea" :rows="3" :placeholder="addressPlaceholder" @input="clearFieldError('address')" /><p v-if="formErrors.address" class="field-error" role="alert">{{ formErrors.address }}</p></el-form-item>
</FormSection>
<FormSection v-if="!isFbaCreate" title="编码与状态">
<FormSection class="density-form-section" v-if="!isFbaCreate" title="编码与状态">
<div class="form-grid two-cols compact-grid">
<el-form-item label="仓库编码" :class="{ 'has-field-error': formErrors.code }"><el-input v-model="form.code" maxlength="50" placeholder="系统自动生成" @input="form.code = String($event).toUpperCase(); clearFieldError('code')" /><p v-if="formErrors.code" class="field-error" role="alert">{{ formErrors.code }}</p></el-form-item>
<el-form-item label="状态"><el-radio-group v-model="form.status" class="warehouse-status-toggle"><el-radio-button value="ACTIVE">启用</el-radio-button><el-radio-button value="INACTIVE">停用</el-radio-button></el-radio-group></el-form-item>
</div>
</FormSection>
<FormSection v-if="!isFbaCreate" title="联系信息">
<FormSection class="density-form-section" v-if="!isFbaCreate" title="联系信息">
<div class="form-grid two-cols compact-grid">
<el-form-item label="联系人"><el-input v-model="form.contactName" maxlength="64" placeholder="选填" /></el-form-item>
<el-form-item label="联系电话"><el-input v-model="form.contactPhone" maxlength="32" inputmode="numeric" placeholder="选填" @input="form.contactPhone = digitsOnly($event)" /></el-form-item>
@@ -815,7 +816,7 @@ onMounted(() => void loadWarehouses())
<template #footer><div class="drawer-footer"><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button type="primary" :loading="saving" @click="saveWarehouse"><Save :size="16" />{{ editorMode === 'EDIT' ? '保存修改' : '确认新增' }}</el-button></div></template>
</AppDialog>
<el-dialog v-model="importOpen" title="导入公共仓库" width="min(760px, calc(100vw - 32px))" :close-on-click-modal="!importing" :close-on-press-escape="!importing">
<el-dialog class="admin-density-dialog" v-model="importOpen" title="导入公共仓库" width="min(760px, calc(100vw - 32px))" :close-on-click-modal="!importing" :close-on-press-escape="!importing">
<div class="import-content">
<div class="import-toolbar">
<el-button :loading="downloadingTemplate" @click="downloadTemplate"><Download :size="16" />下载 XLSX 模板</el-button>
@@ -829,7 +830,7 @@ onMounted(() => void loadWarehouses())
<template #tip><div class="el-upload__tip">最多 5MB、1000 行;请使用标准模板,FBA 名称可不填并默认使用编码。</div></template>
</el-upload>
<div v-if="importResult" class="import-result" role="status">
<dl class="import-summary">
<dl class="import-summary density-detail-overview">
<div><dt>总行数</dt><dd>{{ importResult.totalRows }}</dd></div>
<div><dt>有效</dt><dd>{{ importResult.validRows }}</dd></div>
<div><dt>新增</dt><dd>{{ importResult.createCount }}</dd></div>
@@ -849,40 +850,34 @@ onMounted(() => void loadWarehouses())
</template>
<style scoped>
.state-alert p { margin: 6px 0 10px; line-height: 1.55; }
.warehouse-heading { gap: 16px; }
.warehouse-primary-actions, .import-toolbar, .import-result-heading { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.warehouse-cell { display: flex; align-items: center; gap: 10px; }
.warehouse-cell > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 8px; background: #eef3fa; color: var(--cc-primary); }
.state-alert p { margin: var(--admin-space-2) 0 var(--admin-space-4); line-height: 1.55; }
.warehouse-heading { gap: var(--admin-space-6); }
.warehouse-primary-actions, .import-toolbar, .import-result-heading { display: flex; flex-wrap: wrap; align-items: center; gap: var(--admin-space-4); }
.warehouse-cell { display: flex; align-items: center; gap: var(--admin-space-4); }
.warehouse-cell > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: var(--admin-radius); background: #eef3fa; color: var(--cc-primary); }
.warehouse-cell > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
.warehouse-cell strong { overflow-wrap: anywhere; }
.warehouse-cell small, .warehouse-subline, .warehouse-address small { display: block; color: var(--cc-muted); font-size: 11px; }
.warehouse-cell small, .warehouse-subline, .warehouse-address small { display: block; color: var(--cc-muted); font-size: var(--admin-font-caption); }
.warehouse-address { display: flex; align-items: flex-start; gap: 7px; line-height: 1.55; overflow-wrap: anywhere; }
.warehouse-address svg { flex: 0 0 15px; margin-top: 2px; color: var(--cc-muted); }
.warehouse-address span { min-width: 0; }
.field-error { width: 100%; margin: 6px 0 0; color: var(--el-color-danger); font-size: 12px; line-height: 1.35; overflow-wrap: anywhere; }
.compact-grid { row-gap: 10px; }
.field-error { width: 100%; margin: var(--admin-space-2) 0 0; color: var(--el-color-danger); font-size: var(--admin-font-caption); line-height: 1.35; overflow-wrap: anywhere; }
.compact-grid { row-gap: var(--admin-space-4); }
:global(.warehouse-editor-dialog) { max-height: min(800px, calc(100vh - 32px)); display: flex; flex-direction: column; }
:global(.warehouse-editor-dialog .el-dialog__body) { min-height: 0; overflow-y: auto; }
:global(.warehouse-editor-dialog .el-dialog__footer) { flex: 0 0 auto; }
.warehouse-status-toggle :deep(.el-radio-button__inner) { min-width: 74px; }
@media (max-width: 720px) {
.drawer-form :deep(.form-grid.two-cols) { grid-template-columns: minmax(0, 1fr); }
.density-form :deep(.form-grid.two-cols) { grid-template-columns: minmax(0, 1fr); }
}
.import-content { display: grid; gap: 20px; }
.import-content { display: grid; gap: var(--admin-space-7); }
.import-toolbar { justify-content: space-between; }
.import-result { display: grid; gap: 14px; padding-top: 16px; border-top: 1px solid var(--cc-border); }
.import-summary { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); margin: 0; border: 1px solid var(--cc-border); border-radius: 8px; overflow: hidden; }
.import-summary div { min-width: 0; padding: 10px 12px; border-right: 1px solid var(--cc-border); background: var(--cc-surface-soft, #f8fafc); }
.import-summary div:last-child { border-right: 0; }
.import-summary dt { color: var(--cc-muted); font-size: 11px; }
.import-summary dd { margin: 3px 0 0; color: var(--cc-text); font-size: 16px; font-weight: 700; }
.import-summary dd.is-error { color: var(--el-color-danger); }
.import-result { display: grid; gap: var(--admin-space-5); padding-top: var(--admin-space-6); border-top: 1px solid var(--cc-border); }
.import-result-heading { justify-content: space-between; }
:deep(.has-field-error .el-input__wrapper) { box-shadow: 0 0 0 1px var(--el-color-danger) inset; }
:deep(.has-field-error .el-textarea__inner) { box-shadow: 0 0 0 1px var(--el-color-danger) inset; }
:global(.el-dropdown-menu__item) { gap: 8px; }
:global(.warehouse-create-menu .el-dropdown-menu__item) { gap: var(--admin-space-3); }
@media (max-width: 720px) {
.warehouse-heading { align-items: stretch; }