front/src/views/h5/customer-center/components/H5StoreOrderPullSheet.vue
zzh 501f0edc8a feat: H5 店铺订单与进站优化,PC 店铺订单工具栏精简
新增 H5 店铺订单列表/创建运单流程,优化登录首屏与分包加载;店铺订单状态标签素雅化;PC 露出创建运单/代发运单并移除仓库产品下拉。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 17:39:44 +08:00

363 lines
8.4 KiB
Vue

<template>
<a-drawer
:visible="visible"
placement="bottom"
height="auto"
:footer="false"
:header="false"
unmount-on-close
:drawer-style="{ maxHeight: '88vh', borderRadius: '16px 16px 0 0' }"
popup-container="body"
class="h5-so-pull-drawer"
@cancel="close"
>
<div class="h5-so-pull">
<div class="h5-so-pull__handle" aria-hidden="true" />
<header class="h5-so-pull__head">
<div>
<h3>{{ t('h5.storeOrders.pull.title') }}</h3>
<p>{{ t('h5.storeOrders.pull.hint') }}</p>
</div>
<button type="button" class="h5-so-pull__close" @click="close">
<icon-close />
</button>
</header>
<a-spin :loading="storeLoading" class="h5-so-pull__body">
<p class="h5-so-pull__label">{{ t('h5.storeOrders.pull.stores') }}</p>
<H5EmptyState
v-if="!storeOptions.length && !storeLoading"
:icon="IconStorage"
:text="t('h5.storeOrders.pull.noStore')"
inline
/>
<div v-else class="h5-so-pull__stores">
<button
v-for="store in storeOptions"
:key="store.value"
type="button"
class="h5-so-pull__store"
:class="{ 'is-on': storeIds.includes(store.value) }"
@click="toggleStore(store.value)"
>
<strong>{{ store.name }}</strong>
<em>{{ store.platform }}</em>
</button>
</div>
<p class="h5-so-pull__label">{{ t('h5.storeOrders.pull.range') }}</p>
<div class="h5-so-pull__ranges">
<button
v-for="opt in rangeOptions"
:key="opt.key"
type="button"
class="h5-so-pull__chip"
:class="{ 'is-on': rangeKey === opt.key }"
@click="rangeKey = opt.key"
>
{{ opt.label }}
</button>
</div>
</a-spin>
<div class="h5-so-pull__foot">
<button type="button" class="h5-so-pull__cancel" @click="close">
{{ t('h5.storeOrders.pull.cancel') }}
</button>
<button
type="button"
class="h5-so-pull__submit"
:disabled="submitting || !storeIds.length"
@click="submit"
>
{{ submitting ? t('h5.storeOrders.pull.submitting') : t('h5.storeOrders.pull.submit') }}
</button>
</div>
</div>
</a-drawer>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import dayjs from 'dayjs';
import { Message } from '@arco-design/web-vue';
import { IconClose, IconStorage } from '@arco-design/web-vue/es/icon';
import { getPlatformStoreList, pullPlatformStoreOrders } from '@/api/order/platformStore';
import { pickField } from '@/utils/warehouseListMapper';
import H5EmptyState from './H5EmptyState.vue';
defineOptions({ name: 'H5StoreOrderPullSheet' });
const props = defineProps<{
visible: boolean;
}>();
const emit = defineEmits<{
'update:visible': [value: boolean];
success: [];
}>();
const { t } = useI18n();
const storeLoading = ref(false);
const submitting = ref(false);
const storeOptions = ref<Array<{ value: number; name: string; platform: string }>>([]);
const storeIds = ref<number[]>([]);
const rangeKey = ref<'1' | '7' | '30'>('7');
const DEFAULT_STATUSES = [
'unpaid',
'pending',
'canceled',
'ready_to_ship',
'delivered',
'returned',
'shipped',
'failed',
'topack',
'toship',
'shipping',
'lost',
];
const rangeOptions = computed(() => [
{ key: '1' as const, label: t('h5.storeOrders.pull.today') },
{ key: '7' as const, label: t('h5.storeOrders.pull.days7') },
{ key: '30' as const, label: t('h5.storeOrders.pull.days30') },
]);
const visible = computed({
get: () => props.visible,
set: (v: boolean) => emit('update:visible', v),
});
function close() {
visible.value = false;
}
function toggleStore(id: number) {
if (storeIds.value.includes(id)) {
storeIds.value = storeIds.value.filter((x) => x !== id);
} else {
storeIds.value = [...storeIds.value, id];
}
}
watch(
() => props.visible,
async (open) => {
if (!open) return;
rangeKey.value = '7';
storeLoading.value = true;
try {
const { items } = await getPlatformStoreList({ page: 1, pageSize: 200 });
storeOptions.value = (items || [])
.map((raw: Record<string, unknown>) => {
const id = Number(pickField(raw, 'id', 'Id'));
const name = String(pickField(raw, 'storeNickname', 'StoreNickname') ?? `#${id}`);
const platform = String(pickField(raw, 'platformCode', 'PlatformCode') ?? '').toUpperCase();
return { value: id, name, platform };
})
.filter((x) => x.value > 0);
storeIds.value = storeOptions.value.map((s) => s.value);
} catch {
storeOptions.value = [];
storeIds.value = [];
} finally {
storeLoading.value = false;
}
},
);
async function submit() {
if (!storeIds.value.length) {
Message.warning(t('h5.storeOrders.pull.selectStore'));
return;
}
const days = Number(rangeKey.value) || 7;
const end = dayjs().endOf('day');
const start = dayjs().subtract(days - 1, 'day').startOf('day');
submitting.value = true;
try {
const result = (await pullPlatformStoreOrders({
storeIds: storeIds.value,
orderStatuses: DEFAULT_STATUSES,
timeStart: start.format('YYYY-MM-DD HH:mm:ss'),
timeEnd: end.format('YYYY-MM-DD HH:mm:ss'),
})) as { message?: string; successCount?: number; failCount?: number } | undefined;
Message[result?.failCount ? 'warning' : 'success'](
result?.message ||
t('h5.storeOrders.pull.result', {
success: result?.successCount ?? 0,
fail: result?.failCount ?? 0,
}),
);
emit('success');
close();
} catch (e: any) {
Message.error(e?.message || t('h5.storeOrders.pull.failed'));
} finally {
submitting.value = false;
}
}
</script>
<style lang="less" scoped>
.h5-so-pull {
padding: 8px 16px calc(16px + env(safe-area-inset-bottom));
}
.h5-so-pull__handle {
width: 36px;
height: 4px;
margin: 0 auto 12px;
border-radius: 2px;
background: #e5e6eb;
}
.h5-so-pull__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
h3 {
margin: 0 0 4px;
font-size: 17px;
font-weight: 700;
color: #1d2129;
}
p {
margin: 0;
font-size: 12px;
color: #86909c;
line-height: 1.4;
}
}
.h5-so-pull__close {
width: 32px;
height: 32px;
border: none;
border-radius: 8px;
background: #f2f3f5;
color: #4e5969;
font-size: 16px;
cursor: pointer;
}
.h5-so-pull__body {
display: block;
max-height: 52vh;
overflow: auto;
}
.h5-so-pull__label {
margin: 0 0 8px;
font-size: 13px;
font-weight: 600;
color: #4e5969;
}
.h5-so-pull__stores {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.h5-so-pull__store {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
width: 100%;
padding: 12px 14px;
border: 1.5px solid #e5e6eb;
border-radius: 12px;
background: #fff;
text-align: left;
cursor: pointer;
strong {
font-size: 14px;
color: #1d2129;
}
em {
font-style: normal;
font-size: 12px;
color: #86909c;
}
&.is-on {
border-color: var(--app-primary-color);
background: color-mix(in srgb, var(--app-primary-color) 8%, #fff);
}
}
.h5-so-pull__ranges {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.h5-so-pull__chip {
height: 30px;
padding: 0 12px;
border: 1px solid #e5e6eb;
border-radius: 15px;
background: #fff;
font-size: 13px;
color: #4e5969;
cursor: pointer;
&.is-on {
border-color: var(--app-primary-color);
color: var(--app-primary-color);
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
font-weight: 600;
}
}
.h5-so-pull__foot {
display: flex;
gap: 10px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #f2f3f5;
}
.h5-so-pull__cancel,
.h5-so-pull__submit {
height: 40px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
.h5-so-pull__cancel {
flex: 0 0 auto;
padding: 0 16px;
border: 1px solid #e5e6eb;
background: #fff;
color: #4e5969;
}
.h5-so-pull__submit {
flex: 1;
border: none;
background: var(--app-primary-color);
color: #fff;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
</style>