front/src/views/system/warehouseAddress/index.vue
zzh 640562654d
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m25s
feat: 仓库地址排序备注与映射明细性能、脏检查误报修复
下单/H5 仓库地址展示与排序备注;映射明细加载优化与发布后保留选中;接口明细加载期不再误报未保存。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-09 14:12:32 +08:00

681 lines
20 KiB
Vue

<template>
<div class="warehouse-address-page">
<div class="warehouse-address-page-card">
<div class="page-header">
<h2 class="page-title">{{ $t('system.warehouseAddress.pageTitle') }}</h2>
<div class="page-header-actions">
<a-button type="primary" @click="handleAdd">{{ $t('system.warehouseAddress.actions.add') }}</a-button>
<a-button @click="loadList">{{ $t('system.warehouseAddress.actions.refresh') }}</a-button>
</div>
</div>
<div class="card-grid">
<div v-for="item in list" :key="item.id" class="warehouse-card-shell">
<a-card class="warehouse-card-arco">
<template #title>
<div class="warehouse-card-title">
<span class="warehouse-card-icon" aria-hidden="true">
<icon-home />
</span>
<span class="warehouse-card-title-text" :title="item.warehouseName">{{ item.warehouseName }}</span>
<a-tag
v-if="item.sort != null && item.sort !== ''"
size="small"
class="warehouse-card-sort-tag"
>
{{ $t('system.warehouseAddress.fields.sort') }} {{ item.sort }}
</a-tag>
</div>
</template>
<template #extra>
<div class="card-extra">
<a-tag size="small" color="arcoblue" class="warehouse-card-type-tag">
{{ getWarehouseTypeDisplay(item) }}
</a-tag>
<div class="card-header-actions">
<a-tooltip :content="$t('system.warehouseAddress.actions.copy')">
<a-button type="text" size="small" class="action-icon-btn" @click.stop="handleCopy(item)">
<template #icon><icon-copy /></template>
</a-button>
</a-tooltip>
<a-tooltip :content="$t('system.warehouseAddress.actions.edit')">
<a-button type="text" size="small" class="action-icon-btn" @click.stop="handleEdit(item)">
<template #icon><icon-edit /></template>
</a-button>
</a-tooltip>
<a-tooltip :content="$t('system.warehouseAddress.actions.delete')">
<a-button type="text" size="small" class="action-icon-btn action-icon-btn--danger" @click.stop="handleDelete(item)">
<template #icon><icon-delete /></template>
</a-button>
</a-tooltip>
</div>
</div>
</template>
<div class="card-body">
<div class="card-body-scroll">
<div class="card-row">
<span class="label">{{ $t('system.warehouseAddress.fields.recipient') }}:</span>
<span class="value" :title="displayRecipient(item)">{{ displayRecipient(item) }}</span>
</div>
<div class="card-row">
<span class="label">{{ $t('system.warehouseAddress.fields.contactPhone') }}:</span>
<span class="value" :title="item.contactPhone || '-'">{{ item.contactPhone || '-' }}</span>
</div>
<div class="card-row card-row--address">
<span class="label">{{ $t('system.warehouseAddress.fields.shippingAddress') }}:</span>
<span class="value value-address" :title="displayShippingAddress(item)">{{ displayShippingAddress(item) }}</span>
</div>
<div v-if="item.remark" class="card-row card-row--remark">
<span class="label">{{ $t('system.warehouseAddress.fields.remark') }}:</span>
<span class="value" :title="item.remark">{{ item.remark }}</span>
</div>
</div>
</div>
</a-card>
</div>
</div>
</div>
<a-modal
v-model:visible="modalVisible"
:title="isEdit ? $t('system.warehouseAddress.modal.editTitle') : $t('system.warehouseAddress.modal.addTitle')"
:width="640"
:mask-closable="false"
unmount-on-close
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-alert type="info" :show-icon="false" class="modal-form-tip">
{{
isEdit
? $t('system.warehouseAddress.modal.tipEdit')
: $t('system.warehouseAddress.modal.tip')
}}
</a-alert>
<a-form :model="formData" layout="vertical" class="warehouse-form">
<div class="form-section-title">{{ $t('system.warehouseAddress.section.basic') }}</div>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="$t('system.warehouseAddress.fields.warehouseName')" required>
<a-input
v-model="formData.warehouseName"
:placeholder="$t('system.warehouseAddress.placeholder.input')"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="$t('system.warehouseAddress.fields.warehouseType')">
<a-select
v-model="formData.warehouseType"
:placeholder="$t('system.warehouseAddress.placeholder.select')"
allow-clear
>
<a-option v-for="opt in warehouseTypeOptions" :key="opt.value" :value="opt.value">
{{ opt.label }}
</a-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="$t('system.warehouseAddress.fields.sort')">
<a-input-number
v-model="formData.sort"
:placeholder="$t('system.warehouseAddress.placeholder.sort')"
:precision="0"
allow-clear
hide-button
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<div class="form-section-title">{{ $t('system.warehouseAddress.section.contact') }}</div>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item :label="$t('system.warehouseAddress.fields.recipient')">
<a-input
v-model="formData.recipient"
:placeholder="$t('system.warehouseAddress.placeholder.input')"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item :label="$t('system.warehouseAddress.fields.contactPhone')">
<a-input
v-model="formData.contactPhone"
:placeholder="$t('system.warehouseAddress.placeholder.input')"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :label="$t('system.warehouseAddress.fields.shippingAddress')">
<a-textarea
v-model="formData.shippingAddress"
:placeholder="$t('system.warehouseAddress.placeholder.input')"
:auto-size="{ minRows: 2, maxRows: 4 }"
/>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item :label="$t('system.warehouseAddress.fields.remark')">
<a-textarea
v-model="formData.remark"
:placeholder="$t('system.warehouseAddress.placeholder.remark')"
:max-length="512"
show-word-limit
:auto-size="{ minRows: 2, maxRows: 4 }"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { IconHome, IconCopy, IconEdit, IconDelete } from '@arco-design/web-vue/es/icon';
import { Modal } from '@arco-design/web-vue';
import { $baseMessage } from '@/utils';
import { resolveCurrentCustomer } from '@/api/customer/resolve';
import { getDictListAll } from '@/api/system/dict';
import {
createWarehouse,
deleteWarehouse,
getWarehouseList,
updateWarehouse,
} from '@/api/system/warehouse';
import { useRbacUserStore } from '@/store/modules/rbac-user';
import { getMapIdFromAccessToken } from '@/utils/jwt';
import {
compareWarehouseSort,
formatWarehouseShipAddress,
formatWarehouseShipRecipient,
} from '@/utils/warehouseShipAddress';
const { t } = useI18n();
const rbacUserStore = useRbacUserStore();
defineOptions({
name: 'WarehouseAddress',
});
const list = ref<any[]>([]);
const modalVisible = ref(false);
const isEdit = ref(false);
const editingId = ref<string | number | null>(null);
const warehouseTypeOptions = ref<Array<{ value: string; label: string }>>([]);
const currentCustomerCode = ref('');
function displayRecipient(item: { recipient?: string }) {
return formatWarehouseShipRecipient(item.recipient, currentCustomerCode.value);
}
function displayShippingAddress(item: { shippingAddress?: string }) {
return formatWarehouseShipAddress(item.shippingAddress, currentCustomerCode.value);
}
async function loadCurrentCustomerCode() {
try {
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
const account = rbacUserStore.userInfo?.account?.trim() || '';
const data = await resolveCurrentCustomer({ mapId, account });
currentCustomerCode.value = String(data?.customerCode ?? '').trim();
} catch {
currentCustomerCode.value = '';
}
}
async function loadWarehouseTypeDict() {
try {
const items = await getDictListAll({ dictType: 'warehouse_type' });
const sorted = [...items].sort(
(a, b) => (Number(a.dictSort) || 0) - (Number(b.dictSort) || 0)
);
warehouseTypeOptions.value = sorted
.map((row) => {
const value = String(row.dictCode ?? '').trim();
const label = String(row.dictName ?? '').trim() || value;
return { value, label };
})
.filter((o) => o.value);
} catch (e: any) {
warehouseTypeOptions.value = [];
$baseMessage(e?.message || t('system.warehouseAddress.message.dictLoadFailed'), 'warning');
}
}
/** 卡片展示:以字典为准(改 dictName 后标签同步),无匹配再用接口里的 warehouseTypeName */
function getWarehouseTypeDisplay(item: {
warehouseType?: string | number;
warehouseTypeName?: string;
}) {
const code =
item.warehouseType != null && item.warehouseType !== ''
? String(item.warehouseType).trim()
: '';
if (code) {
const fromDict = warehouseTypeOptions.value.find((o) => o.value === code)?.label;
if (fromDict) return fromDict;
}
const name = item.warehouseTypeName != null ? String(item.warehouseTypeName).trim() : '';
if (name) return name;
if (code) return code;
return '-';
}
const formData = reactive({
warehouseName: '',
warehouseType: undefined as string | undefined,
sort: undefined as number | undefined,
recipient: '',
contactPhone: '',
shippingAddress: '',
remark: '',
});
function handleDelete(item: any) {
if (item?.id == null) return;
Modal.confirm({
title: t('system.warehouseAddress.actions.delete'),
content: t('system.warehouseAddress.message.deleteConfirm'),
onOk: async () => {
try {
await deleteWarehouse(item.id);
$baseMessage(t('system.warehouseAddress.message.deleteSuccess'), 'success');
await loadList();
} catch (e: any) {
$baseMessage(e?.message || t('system.warehouseAddress.message.deleteFailed'), 'error');
throw e;
}
},
});
}
async function loadList() {
try {
const { items } = await getWarehouseList({ page: 1, pageSize: 200 });
list.value = [...(items || [])].sort(compareWarehouseSort);
} catch (e: any) {
$baseMessage(e?.message || '加载仓库列表失败', 'error');
list.value = [];
}
}
function handleAdd() {
isEdit.value = false;
editingId.value = null;
Object.assign(formData, {
warehouseName: '',
warehouseType: undefined,
sort: undefined,
recipient: '',
contactPhone: '',
shippingAddress: '',
remark: '',
});
modalVisible.value = true;
}
function handleCopy(item: any) {
const lines = [
`${t('system.warehouseAddress.fields.recipient')}: ${displayRecipient(item)}`,
`${t('system.warehouseAddress.fields.contactPhone')}: ${item.contactPhone || '-'}`,
`${t('system.warehouseAddress.fields.shippingAddress')}: ${displayShippingAddress(item)}`,
];
const text = lines.join('\n');
navigator.clipboard.writeText(text).then(
() => $baseMessage(t('system.warehouseAddress.message.copySuccess'), 'success'),
() => $baseMessage(t('system.warehouseAddress.message.copyFailed'), 'error')
);
}
function handleEdit(item: any) {
isEdit.value = true;
editingId.value = item.id ?? null;
const sortRaw = item.sort ?? item.Sort;
Object.assign(formData, {
warehouseName: item.warehouseName,
warehouseType: item.warehouseType,
sort: sortRaw == null || sortRaw === '' ? undefined : Number(sortRaw),
recipient: item.recipient,
contactPhone: item.contactPhone,
shippingAddress: item.shippingAddress,
remark: item.remark || '',
});
modalVisible.value = true;
}
async function handleModalOk() {
if (!formData.warehouseName?.trim()) {
$baseMessage(t('system.warehouseAddress.message.nameRequired'), 'warning');
return;
}
const payload = {
warehouseName: formData.warehouseName.trim(),
warehouseType: formData.warehouseType,
sort: formData.sort == null || Number.isNaN(Number(formData.sort)) ? null : Number(formData.sort),
recipient: formData.recipient,
contactPhone: formData.contactPhone,
shippingAddress: formData.shippingAddress,
remark: formData.remark?.trim() || null,
};
try {
if (isEdit.value && editingId.value != null) {
await updateWarehouse({ ...payload, id: editingId.value });
$baseMessage(t('system.warehouseAddress.message.editSuccess'), 'success');
} else {
await createWarehouse(payload);
$baseMessage(t('system.warehouseAddress.message.addSuccess'), 'success');
}
modalVisible.value = false;
await loadList();
} catch (e: any) {
$baseMessage(e?.message || '保存失败', 'error');
}
}
function handleModalCancel() {
modalVisible.value = false;
}
onMounted(() => {
loadWarehouseTypeDict();
loadList();
loadCurrentCustomerCode();
});
</script>
<style lang="less" scoped>
.warehouse-address-page {
width: 100%;
box-sizing: border-box;
padding: 16px 16px 24px;
background-color: var(--app-page-bg);
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow-x: hidden;
overflow-y: hidden;
}
.warehouse-address-page-card {
background: transparent;
border-radius: 0;
padding: 0;
box-shadow: none;
box-sizing: border-box;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.page-header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 0 4px;
gap: 12px;
.page-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--color-text-1);
}
}
.page-header-actions {
display: flex;
align-items: center;
gap: var(--app-button-gap, 8px);
flex-shrink: 0;
}
.card-grid {
flex: 1;
min-height: 0;
overflow: auto;
padding: 12px 0 0;
box-sizing: border-box;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
align-items: start;
}
@warehouse-card-height: 220px;
.warehouse-card-shell {
min-width: 0;
min-height: 0;
height: @warehouse-card-height;
max-height: @warehouse-card-height;
overflow: hidden;
box-sizing: border-box;
:deep(.arco-card) {
min-height: 0;
height: 100%;
max-height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
border: 1px solid var(--app-acrylic-border);
border-radius: 16px;
background: var(--app-acrylic-bg);
box-shadow: none;
backdrop-filter: var(--app-acrylic-blur);
-webkit-backdrop-filter: var(--app-acrylic-blur);
transition:
border-color 0.2s ease,
background 0.2s ease;
}
:deep(.arco-card:hover) {
border-color: var(--app-acrylic-border-strong);
background: var(--app-acrylic-hover-bg);
}
:deep(.arco-card-header) {
flex-shrink: 0;
height: auto;
padding: 16px 18px 12px;
border-bottom: 1px solid color-mix(in srgb, var(--app-acrylic-border) 70%, transparent);
background: transparent;
}
:deep(.arco-card-header-title) {
flex: 1;
min-width: 0;
}
:deep(.arco-card-header-extra) {
flex-shrink: 0;
margin-left: 12px;
}
:deep(.arco-card-body) {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
padding: 12px 18px 16px;
background: transparent;
}
.warehouse-card-title {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.warehouse-card-icon {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 10px;
border: 1px solid color-mix(in srgb, var(--app-acrylic-border) 80%, transparent);
background: rgba(var(--primary-6), 0.1);
color: rgb(var(--primary-6));
font-size: 16px;
}
.warehouse-card-title-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: 600;
color: var(--color-text-1);
}
.warehouse-card-sort-tag {
flex-shrink: 0;
}
.card-extra {
display: flex;
align-items: center;
gap: 8px;
}
.warehouse-card-type-tag {
flex-shrink: 0;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.card-header-actions {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
.action-icon-btn {
width: 32px;
height: 32px;
padding: 0;
font-size: 16px;
color: var(--color-text-3);
&:hover {
color: rgb(var(--primary-6));
}
}
.action-icon-btn--danger:hover {
color: rgb(var(--danger-6));
background: rgb(var(--danger-1));
}
}
.card-body {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.card-body-scroll {
flex: 1;
min-height: 0;
overflow: auto;
padding-right: 4px;
}
.card-row {
display: flex;
gap: 8px;
margin-bottom: 10px;
font-size: 13px;
line-height: 1.5;
.label {
flex-shrink: 0;
width: 88px;
color: var(--color-text-3);
}
.value {
flex: 1;
min-width: 0;
color: var(--color-text-1);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&--address .value-address {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
white-space: normal;
word-break: break-all;
}
}
@media (max-width: 1600px) {
.card-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1200px) {
.card-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.card-grid {
grid-template-columns: 1fr;
}
}
.modal-form-tip {
margin-bottom: 16px;
border-radius: 6px;
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
border: 1px solid color-mix(in srgb, var(--app-primary-color) 18%, transparent);
color: var(--color-text-2);
font-size: 13px;
line-height: 1.5;
}
.form-section-title {
margin: 0 0 12px;
padding: 0 0 8px;
font-size: 13px;
font-weight: 600;
color: var(--color-text-2);
border-bottom: 1px solid var(--color-border-2);
~ .form-section-title {
margin-top: 8px;
}
}
.warehouse-form {
:deep(.arco-form-item) {
margin-bottom: 16px;
}
}
</style>