front/src/views/h5/customer-center/components/H5PickupOrdersSheet.vue
zzh abfca7a21a feat: add H5 customer center, marketing admin, and employee profile UX
Ship customer portal and marketing pages, user edit drawer with employee archive, IDE employee numbers, channel config refactor, and related nginx/route updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 23:44:02 +08:00

385 lines
8.8 KiB
Vue

<template>
<a-drawer
:visible="visible"
placement="bottom"
height="auto"
:footer="false"
:header="false"
unmount-on-close
:drawer-style="drawerStyle"
popup-container="body"
class="h5-pickup-drawer"
@cancel="close"
>
<div class="h5-pk">
<div class="h5-pk__handle" aria-hidden="true" />
<header class="h5-pk__head">
<h3>{{ t('h5.pickup.title') }}</h3>
<button type="button" class="h5-pk__close" @click="close">
<icon-close />
</button>
</header>
<a-spin :loading="loading" class="h5-pk__body">
<H5EmptyState
v-if="!rows.length && !loading"
:icon="IconCalendarClock"
:text="t('h5.pickup.empty')"
/>
<article v-for="row in rows" :key="row.id" class="pk-card">
<div class="pk-card__top">
<strong>{{ row.serviceNo }}</strong>
<span class="pk-card__status" :style="statusStyle(row.status)">
{{ statusLabel(row.status) }}
</span>
</div>
<p class="pk-card__line">
<span>{{ t('h5.pickup.pickupTime') }}</span>
<b>{{ row.pickupTime || '--' }}</b>
</p>
<p class="pk-card__line">
<span>{{ t('h5.pickup.warehouse') }}</span>
<b>{{ row.warehouse || '--' }}</b>
</p>
<p class="pk-card__line">
<span>{{ t('h5.pickup.cargo') }}</span>
<b>{{ row.cargoName || '--' }} · {{ row.actualWeight || '--' }}kg</b>
</p>
<p v-if="row.productCode" class="pk-card__meta">
{{ row.productCode }}
<template v-if="row.clientNo && row.clientNo !== '-'"> · {{ row.clientNo }}</template>
</p>
</article>
<button
v-if="hasMore"
type="button"
class="h5-pk__more"
:disabled="loadingMore"
@click="loadMore"
>
{{ loadingMore ? t('h5.pickup.loading') : t('h5.pickup.loadMore') }}
</button>
</a-spin>
</div>
</a-drawer>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { Message } from '@arco-design/web-vue';
import { IconCalendarClock, IconClose } from '@arco-design/web-vue/es/icon';
import { postOrderList } from '@/api/order';
import { getOrderStatusMeta } from '@/constants/orderStatus';
import { pickField } from '@/utils/warehouseListMapper';
import H5EmptyState from './H5EmptyState.vue';
defineOptions({ name: 'H5PickupOrdersSheet' });
type PickupRow = {
id: number | string;
serviceNo: string;
clientNo: string;
productCode: string;
warehouse: string;
pickupTime: string;
cargoName: string;
actualWeight: string | number;
status: string;
createTime: string;
};
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
'update:visible': [value: boolean];
loaded: [total: number];
}>();
const { t } = useI18n();
const loading = ref(false);
const loadingMore = ref(false);
const rows = ref<PickupRow[]>([]);
const total = ref(0);
const page = ref(1);
const pageSize = 20;
const drawerStyle = {
borderRadius: '20px 20px 0 0',
overflow: 'hidden',
maxHeight: 'min(92vh, 780px)',
};
const hasMore = computed(() => rows.value.length < total.value);
function close() {
emit('update:visible', false);
}
function formatTime(v: unknown) {
if (v == null || v === '') return '';
const s = String(v);
return s.length > 19 ? s.slice(0, 19).replace('T', ' ') : s.replace('T', ' ');
}
function mapRow(raw: Record<string, unknown>): PickupRow | null {
const id = pickField(raw, 'id', 'Id');
if (id == null || id === '') return null;
const items = (pickField(raw, 'items', 'Items') as any[]) || [];
const firstItem = items[0] || {};
const weights = (pickField(raw, 'weightList', 'WeightList') as any[]) || [];
const forecast =
weights.find((w) => String(pickField(w, 'type', 'Type') || '').toLowerCase() === 'forecast') ||
weights[0] ||
{};
const status =
String(
pickField(raw, 'orderStatus', 'OrderStatus') ||
pickField(raw, 'status', 'Status') ||
'',
) || '';
return {
id: id as number | string,
serviceNo: String(pickField(raw, 'serviceNo', 'ServiceNo') || '--'),
clientNo: String(pickField(raw, 'clientNo', 'ClientNo') || '-'),
productCode: String(pickField(raw, 'productCode', 'ProductCode') || ''),
warehouse: String(pickField(raw, 'warehouse', 'Warehouse') || ''),
pickupTime: formatTime(pickField(raw, 'pickupTime', 'PickupTime')),
cargoName: String(
pickField(firstItem, 'cnName', 'CnName') ||
pickField(firstItem, 'enName', 'EnName') ||
pickField(raw, 'cargoName', 'CargoName') ||
'',
),
actualWeight: (pickField(forecast, 'actualWeight', 'ActualWeight') as any) ?? '',
status,
createTime: formatTime(pickField(raw, 'createTime', 'CreateTime')),
};
}
function statusLabel(code: string) {
const meta = getOrderStatusMeta(code);
if (!meta) return code || '--';
return t(meta.labelKey);
}
function statusStyle(code: string) {
const meta = getOrderStatusMeta(code);
return {
color: meta?.tagColor || '#4e5969',
background: meta?.tagBgColor || '#f2f3f5',
};
}
async function fetchPage(nextPage: number, append: boolean) {
const res = await postOrderList({
page: nextPage,
pageSize,
status: 'all',
orderSource: 'pickup',
keyword: '',
});
const mapped = (res.items || [])
.map((raw: any) => mapRow(raw))
.filter(Boolean) as PickupRow[];
rows.value = append ? [...rows.value, ...mapped] : mapped;
total.value = Number(res.total) || 0;
page.value = nextPage;
emit('loaded', total.value);
}
async function load() {
loading.value = true;
try {
await fetchPage(1, false);
} catch (e: any) {
rows.value = [];
total.value = 0;
emit('loaded', 0);
Message.error(e?.message || t('h5.pickup.loadFailed'));
} finally {
loading.value = false;
}
}
async function loadMore() {
if (loadingMore.value || !hasMore.value) return;
loadingMore.value = true;
try {
await fetchPage(page.value + 1, true);
} catch (e: any) {
Message.error(e?.message || t('h5.pickup.loadFailed'));
} finally {
loadingMore.value = false;
}
}
watch(
() => props.visible,
(visible) => {
if (visible) void load();
},
);
</script>
<style lang="less">
.h5-pickup-drawer {
.arco-drawer-content {
border-radius: 20px 20px 0 0;
overflow: hidden;
max-height: min(92vh, 780px);
}
.arco-drawer-body {
padding: 0;
display: flex;
flex-direction: column;
max-height: min(92vh, 780px);
}
}
</style>
<style lang="less" scoped>
.h5-pk {
display: flex;
flex-direction: column;
max-height: min(92vh, 780px);
padding-bottom: env(safe-area-inset-bottom, 0px);
background: #f5f7f8;
}
.h5-pk__handle {
width: 36px;
height: 4px;
margin: 10px auto 0;
border-radius: 999px;
background: #c9cdd4;
}
.h5-pk__head {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 48px;
flex-shrink: 0;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #1d2129;
}
}
.h5-pk__close {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 36px;
height: 36px;
border: none;
border-radius: 10px;
background: transparent;
color: #86909c;
font-size: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.h5-pk__body {
flex: 1;
min-height: 220px;
overflow: auto;
display: block;
width: 100%;
padding: 0 16px 16px;
box-sizing: border-box;
}
.pk-card {
background: #fff;
border-radius: 16px;
padding: 14px 14px 12px;
margin-bottom: 10px;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
}
.pk-card__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 10px;
strong {
font-size: 15px;
font-weight: 700;
color: #1d2129;
word-break: break-all;
}
}
.pk-card__status {
flex-shrink: 0;
height: 24px;
padding: 0 8px;
border-radius: 8px;
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 600;
}
.pk-card__line {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin: 0 0 6px;
font-size: 13px;
span {
color: #86909c;
flex-shrink: 0;
}
b {
font-weight: 500;
color: #1d2129;
text-align: right;
}
}
.pk-card__meta {
margin: 8px 0 0;
font-size: 12px;
color: #86909c;
}
.h5-pk__more {
width: 100%;
height: 40px;
margin-top: 4px;
border: none;
border-radius: 12px;
background: #fff;
color: var(--app-primary-color);
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.04);
&:disabled {
opacity: 0.6;
}
}
</style>