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>
385 lines
8.8 KiB
Vue
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>
|