All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m30s
排除登录路由进页签;FormPro 内部 model 优先;侧栏仓库选择与地址弹窗合并并可切换。 Co-authored-by: Cursor <cursoragent@cursor.com>
484 lines
13 KiB
Vue
484 lines
13 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { Message } from '@arco-design/web-vue'
|
|
import ModalPro from '@/components/ModalPro/index.vue'
|
|
import useModal from '@/components/ModalPro/useModal'
|
|
import { getWarehouseList, type WarehouseDto } from '@/api/system/warehouse'
|
|
import { resolveCurrentCustomer } from '@/api/customer/resolve'
|
|
import {
|
|
compareWarehouseSort,
|
|
formatWarehouseShipAddress,
|
|
formatWarehouseShipRecipient,
|
|
} from '@/utils/warehouseShipAddress'
|
|
import { getMapIdFromAccessToken } from '@/utils/jwt'
|
|
import { useRbacUserStore } from '@/store/modules/rbac-user'
|
|
import { useCurrentWarehouseStore } from '@/store'
|
|
|
|
const props = defineProps<{
|
|
visible: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
'update:visible': [value: boolean]
|
|
}>()
|
|
|
|
const { t } = useI18n()
|
|
const [registerModal] = useModal()
|
|
const rbacUserStore = useRbacUserStore()
|
|
const warehouseStore = useCurrentWarehouseStore()
|
|
|
|
const loading = ref(false)
|
|
const list = ref<WarehouseDto[]>([])
|
|
const customerCode = ref('')
|
|
|
|
const currentId = computed(() => warehouseStore.warehouseId)
|
|
|
|
const orderedList = computed(() => {
|
|
const rows = [...list.value]
|
|
const cur = currentId.value
|
|
if (!cur) return rows
|
|
return rows.sort((a, b) => {
|
|
const aCur = Number(a.id) === cur ? 0 : 1
|
|
const bCur = Number(b.id) === cur ? 0 : 1
|
|
if (aCur !== bCur) return aCur - bCur
|
|
return compareWarehouseSort(a, b)
|
|
})
|
|
})
|
|
|
|
function pick(raw: any, ...keys: string[]) {
|
|
for (const k of keys) {
|
|
const v = raw?.[k]
|
|
if (v !== undefined && v !== null && String(v).trim() !== '') return String(v)
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function normalize(row: any): WarehouseDto {
|
|
const sortRaw = row?.sort ?? row?.Sort
|
|
const sortNum = sortRaw == null || sortRaw === '' ? null : Number(sortRaw)
|
|
return {
|
|
id: row?.id ?? row?.Id,
|
|
warehouseName: pick(row, 'warehouseName', 'WarehouseName'),
|
|
warehouseType: pick(row, 'warehouseType', 'WarehouseType') || undefined,
|
|
warehouseTypeName: pick(row, 'warehouseTypeName', 'WarehouseTypeName') || undefined,
|
|
recipient: pick(row, 'recipient', 'Recipient'),
|
|
contactPhone: pick(row, 'contactPhone', 'ContactPhone'),
|
|
shippingAddress: pick(row, 'shippingAddress', 'ShippingAddress'),
|
|
sort: sortNum != null && Number.isFinite(sortNum) ? sortNum : null,
|
|
remark: pick(row, 'remark', 'Remark') || undefined,
|
|
}
|
|
}
|
|
|
|
function displayRecipient(item: WarehouseDto) {
|
|
return formatWarehouseShipRecipient(item.recipient, customerCode.value)
|
|
}
|
|
|
|
function displayAddress(item: WarehouseDto) {
|
|
return formatWarehouseShipAddress(item.shippingAddress, customerCode.value)
|
|
}
|
|
|
|
function buildCopyText(item: WarehouseDto) {
|
|
return [
|
|
item.warehouseName || '-',
|
|
`${t('system.warehouseAddress.fields.recipient')}: ${displayRecipient(item)}`,
|
|
`${t('system.warehouseAddress.fields.contactPhone')}: ${item.contactPhone || '-'}`,
|
|
`${t('system.warehouseAddress.fields.shippingAddress')}: ${displayAddress(item)}`,
|
|
].join('\n')
|
|
}
|
|
|
|
async function copyText(text: string) {
|
|
try {
|
|
await navigator.clipboard.writeText(text)
|
|
Message.success(t('system.warehouseAddress.message.copySuccess'))
|
|
} catch {
|
|
Message.error(t('system.warehouseAddress.message.copyFailed'))
|
|
}
|
|
}
|
|
|
|
function copyItem(item: WarehouseDto) {
|
|
void copyText(buildCopyText(item))
|
|
}
|
|
|
|
function copyField(value: string) {
|
|
const text = String(value || '').trim()
|
|
if (!text || text === '-') {
|
|
Message.warning(t('nav.warehousePeek.emptyField'))
|
|
return
|
|
}
|
|
void copyText(text)
|
|
}
|
|
|
|
async function loadCustomerCode() {
|
|
try {
|
|
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken)
|
|
const account = rbacUserStore.userInfo?.account?.trim() || ''
|
|
const data = await resolveCurrentCustomer({ mapId, account })
|
|
customerCode.value = String(data?.customerCode ?? '').trim()
|
|
} catch {
|
|
customerCode.value = ''
|
|
}
|
|
}
|
|
|
|
function syncWarehouseOptions(rows: WarehouseDto[]) {
|
|
warehouseStore.setOptions(
|
|
rows
|
|
.filter((row) => row.id != null)
|
|
.map((row) => ({
|
|
label: row.warehouseName || String(row.id),
|
|
value: Number(row.id),
|
|
})),
|
|
)
|
|
}
|
|
|
|
function selectWarehouse(item: WarehouseDto) {
|
|
const id = Number(item.id)
|
|
if (!id) return
|
|
if (id === currentId.value) return
|
|
syncWarehouseOptions(list.value)
|
|
warehouseStore.setWarehouseId(id)
|
|
Message.success(
|
|
t('nav.warehousePeek.switched', {
|
|
name: item.warehouseName || String(id),
|
|
}),
|
|
)
|
|
}
|
|
|
|
async function loadList() {
|
|
loading.value = true
|
|
try {
|
|
const [res] = await Promise.all([
|
|
getWarehouseList({ page: 1, pageSize: 100 }),
|
|
loadCustomerCode(),
|
|
])
|
|
list.value = (res.items ?? []).map(normalize).sort(compareWarehouseSort)
|
|
syncWarehouseOptions(list.value)
|
|
} catch {
|
|
list.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function onUpdateVisible(val: boolean) {
|
|
emit('update:visible', val)
|
|
}
|
|
|
|
function handleClose() {
|
|
emit('update:visible', false)
|
|
}
|
|
|
|
watch(
|
|
() => props.visible,
|
|
(open) => {
|
|
if (open) void loadList()
|
|
},
|
|
)
|
|
</script>
|
|
|
|
<template>
|
|
<ModalPro
|
|
:visible="visible"
|
|
:title="t('nav.warehousePeek.title')"
|
|
:width="420"
|
|
:mask-closable="true"
|
|
:footer="false"
|
|
unmount-on-close
|
|
@register="registerModal"
|
|
@update:visible="onUpdateVisible"
|
|
@cancel="handleClose"
|
|
>
|
|
<template #footer>
|
|
<span />
|
|
</template>
|
|
|
|
<p class="peek-tip">
|
|
{{ t('nav.warehousePeek.tip') }}
|
|
<span v-if="customerCode" class="peek-tip__code">{{ customerCode }}</span>
|
|
</p>
|
|
<p class="peek-hint">{{ t('nav.warehousePeek.selectHint') }}</p>
|
|
|
|
<a-spin :loading="loading" class="peek-spin">
|
|
<div v-if="!orderedList.length && !loading" class="peek-empty">
|
|
{{ t('nav.warehousePeek.empty') }}
|
|
</div>
|
|
|
|
<article
|
|
v-for="item in orderedList"
|
|
:key="String(item.id)"
|
|
class="peek-card"
|
|
:class="{ 'is-current': Number(item.id) === currentId }"
|
|
role="button"
|
|
tabindex="0"
|
|
:aria-pressed="Number(item.id) === currentId"
|
|
@click="selectWarehouse(item)"
|
|
@keydown.enter.prevent="selectWarehouse(item)"
|
|
@keydown.space.prevent="selectWarehouse(item)"
|
|
>
|
|
<header class="peek-card__head">
|
|
<div class="peek-card__title">
|
|
<icon-home class="peek-card__icon" />
|
|
<strong>{{ item.warehouseName || '-' }}</strong>
|
|
<span v-if="Number(item.id) === currentId" class="peek-card__badge">
|
|
{{ t('nav.warehousePeek.current') }}
|
|
</span>
|
|
</div>
|
|
<div class="peek-card__actions" @click.stop>
|
|
<button
|
|
v-if="Number(item.id) !== currentId"
|
|
type="button"
|
|
class="peek-card__use"
|
|
@click="selectWarehouse(item)"
|
|
>
|
|
{{ t('nav.warehousePeek.useThis') }}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="peek-card__copy-all"
|
|
:title="t('system.warehouseAddress.actions.copy')"
|
|
@click="copyItem(item)"
|
|
>
|
|
<icon-copy />
|
|
<span>{{ t('nav.warehousePeek.copyAll') }}</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="peek-card__rows">
|
|
<div class="peek-row">
|
|
<span class="peek-row__label">{{ t('system.warehouseAddress.fields.recipient') }}</span>
|
|
<span class="peek-row__value" :title="displayRecipient(item)">{{ displayRecipient(item) }}</span>
|
|
<button type="button" class="peek-row__copy" @click.stop="copyField(displayRecipient(item))">
|
|
<icon-copy />
|
|
</button>
|
|
</div>
|
|
<div class="peek-row">
|
|
<span class="peek-row__label">{{ t('system.warehouseAddress.fields.contactPhone') }}</span>
|
|
<span class="peek-row__value" :title="item.contactPhone || '-'">{{ item.contactPhone || '-' }}</span>
|
|
<button type="button" class="peek-row__copy" @click.stop="copyField(item.contactPhone || '')">
|
|
<icon-copy />
|
|
</button>
|
|
</div>
|
|
<div class="peek-row peek-row--address">
|
|
<span class="peek-row__label">{{ t('system.warehouseAddress.fields.shippingAddress') }}</span>
|
|
<span class="peek-row__value" :title="displayAddress(item)">{{ displayAddress(item) }}</span>
|
|
<button type="button" class="peek-row__copy" @click.stop="copyField(displayAddress(item))">
|
|
<icon-copy />
|
|
</button>
|
|
</div>
|
|
<div v-if="item.remark" class="peek-row">
|
|
<span class="peek-row__label">{{ t('system.warehouseAddress.fields.remark') }}</span>
|
|
<span class="peek-row__value" :title="item.remark">{{ item.remark }}</span>
|
|
<button type="button" class="peek-row__copy" @click.stop="copyField(item.remark || '')">
|
|
<icon-copy />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</a-spin>
|
|
</ModalPro>
|
|
</template>
|
|
|
|
<style scoped lang="less">
|
|
.peek-tip {
|
|
margin: 0 0 6px;
|
|
padding: 8px 10px;
|
|
border-radius: 6px;
|
|
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 8%, #fff);
|
|
border: 1px solid color-mix(in srgb, var(--app-primary-color, #1d7281) 16%, transparent);
|
|
color: var(--color-text-2);
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
|
|
&__code {
|
|
margin-left: 4px;
|
|
font-weight: 700;
|
|
color: var(--app-primary-color, #1d7281);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
}
|
|
|
|
.peek-hint {
|
|
margin: 0 0 12px;
|
|
color: var(--color-text-3);
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.peek-spin {
|
|
display: block;
|
|
width: 100%;
|
|
max-height: min(60vh, 480px);
|
|
overflow: auto;
|
|
}
|
|
|
|
.peek-empty {
|
|
padding: 28px 12px;
|
|
text-align: center;
|
|
color: var(--color-text-3);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.peek-card {
|
|
margin-bottom: 10px;
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--color-border-2, #e5e6eb);
|
|
background: #fff;
|
|
cursor: pointer;
|
|
transition: border-color 0.14s ease, background-color 0.14s ease, box-shadow 0.14s ease;
|
|
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
&:hover {
|
|
border-color: color-mix(in srgb, var(--app-primary-color, #1d7281) 28%, transparent);
|
|
}
|
|
|
|
&.is-current {
|
|
border-color: color-mix(in srgb, var(--app-primary-color, #1d7281) 35%, transparent);
|
|
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 5%, #fff);
|
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--app-primary-color, #1d7281) 12%, transparent);
|
|
cursor: default;
|
|
}
|
|
|
|
&__head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
&__actions {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
&__use {
|
|
height: 26px;
|
|
padding: 0 8px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
background: var(--app-primary-color, #1d7281);
|
|
color: #fff;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
|
|
&:hover {
|
|
filter: brightness(1.05);
|
|
}
|
|
}
|
|
|
|
&__title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
font-size: 14px;
|
|
color: var(--color-text-1);
|
|
|
|
strong {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
font-weight: 650;
|
|
}
|
|
}
|
|
|
|
&__icon {
|
|
flex-shrink: 0;
|
|
color: var(--app-primary-color, #1d7281);
|
|
font-size: 15px;
|
|
}
|
|
|
|
&__badge {
|
|
flex-shrink: 0;
|
|
padding: 1px 6px;
|
|
border-radius: 999px;
|
|
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 14%, #fff);
|
|
color: var(--app-primary-color, #1d7281);
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
&__copy-all {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
flex-shrink: 0;
|
|
height: 26px;
|
|
padding: 0 8px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 10%, #fff);
|
|
color: var(--app-primary-color, #1d7281);
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
|
|
&:hover {
|
|
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 16%, #fff);
|
|
}
|
|
}
|
|
}
|
|
|
|
.peek-row {
|
|
display: grid;
|
|
grid-template-columns: 64px minmax(0, 1fr) 24px;
|
|
align-items: start;
|
|
gap: 6px;
|
|
padding: 5px 0;
|
|
border-top: 1px dashed var(--color-border-2, #e5e6eb);
|
|
|
|
&:first-child {
|
|
border-top: none;
|
|
padding-top: 0;
|
|
}
|
|
|
|
&__label {
|
|
color: var(--color-text-3);
|
|
font-size: 12px;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
&__value {
|
|
min-width: 0;
|
|
color: var(--color-text-1);
|
|
font-size: 13px;
|
|
line-height: 1.45;
|
|
word-break: break-word;
|
|
}
|
|
|
|
&__copy {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 24px;
|
|
height: 24px;
|
|
margin-top: -1px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
background: transparent;
|
|
color: var(--color-text-4, #c9cdd4);
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
|
|
&:hover {
|
|
color: var(--app-primary-color, #1d7281);
|
|
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 8%, #fff);
|
|
}
|
|
}
|
|
|
|
&--address .peek-row__value {
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
</style>
|