统一暗色模式页面/顶栏/Handsontable 选中色,调整下单到仓方式文案与提示,CustomLeftMenu 卡片统一 60px;补充揽收单、问题件处理与 H5 相关页面。 Co-authored-by: Cursor <cursoragent@cursor.com>
618 lines
16 KiB
Vue
618 lines
16 KiB
Vue
<template>
|
|
<a-drawer
|
|
:visible="visible"
|
|
placement="bottom"
|
|
height="auto"
|
|
:footer="false"
|
|
:header="false"
|
|
unmount-on-close
|
|
:mask-closable="!submitting"
|
|
:drawer-style="drawerStyle"
|
|
popup-container="body"
|
|
class="h5-pk-merge-drawer"
|
|
@cancel="close"
|
|
>
|
|
<div class="h5-pkm">
|
|
<div class="h5-pkm__handle" aria-hidden="true" />
|
|
<header class="h5-pkm__head">
|
|
<div>
|
|
<p class="h5-pkm__step">{{ t('h5.orders.mergePickup.createSub') }}</p>
|
|
<h3>{{ t('h5.orders.mergePickup.createTitle') }}</h3>
|
|
</div>
|
|
<button type="button" class="h5-pkm__close" :disabled="submitting" @click="close">
|
|
<icon-close />
|
|
</button>
|
|
</header>
|
|
|
|
<a-spin :loading="loadingRefs" class="h5-pkm__body">
|
|
<section class="h5-pkm__summary">
|
|
<div class="h5-pkm__summary-item">
|
|
<span>{{ t('h5.orders.mergePickup.summaryWarehouse') }}</span>
|
|
<strong>{{ warehouseLabel }}</strong>
|
|
</div>
|
|
<div class="h5-pkm__summary-item">
|
|
<span>{{ t('h5.orders.mergePickup.summaryOrders') }}</span>
|
|
<strong>{{ orderIds.length }}</strong>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="h5-pkm__tags">
|
|
<span v-for="row in previewRows" :key="String(row.id)" class="h5-pkm__tag">
|
|
{{ row.serviceNo || row.id }}
|
|
</span>
|
|
</div>
|
|
|
|
<section class="h5-pkm__section">
|
|
<h4>{{ t('order.myOrders.createPickup.senderAddress') }}</h4>
|
|
<H5EmptyState
|
|
v-if="!senderOptions.length && !loadingRefs"
|
|
inline
|
|
:icon="IconLocation"
|
|
:text="t('h5.orders.mergePickup.noSender')"
|
|
/>
|
|
<button
|
|
v-for="opt in senderOptions"
|
|
:key="opt.value"
|
|
type="button"
|
|
class="h5-pkm__sender"
|
|
:class="{ 'is-active': form.senderAddressId === opt.value }"
|
|
@click="form.senderAddressId = opt.value"
|
|
>
|
|
<b>{{ opt.name }}</b>
|
|
<span>{{ opt.phone }}</span>
|
|
<em v-if="opt.address">{{ opt.address }}</em>
|
|
</button>
|
|
</section>
|
|
|
|
<section class="h5-pkm__section">
|
|
<h4>{{ t('order.myOrders.createPickup.expectTime') }}</h4>
|
|
<div class="h5-pkm__time-chips">
|
|
<button
|
|
v-for="preset in timePresets"
|
|
:key="preset.key"
|
|
type="button"
|
|
class="h5-pkm__time-chip"
|
|
:class="{ 'is-active': activeTimePreset === preset.key }"
|
|
@click="applyTimePreset(preset)"
|
|
>
|
|
{{ preset.label }}
|
|
</button>
|
|
</div>
|
|
<a-input
|
|
v-model="form.expectPickupTime"
|
|
allow-clear
|
|
class="h5-pkm__time-input"
|
|
:placeholder="t('h5.journey.ship.pickupTimePh')"
|
|
@input="activeTimePreset = 'custom'"
|
|
/>
|
|
</section>
|
|
|
|
<section class="h5-pkm__section h5-pkm__section--fold">
|
|
<button type="button" class="h5-pkm__fold" @click="showMore = !showMore">
|
|
{{ t('h5.orders.mergePickup.moreOptions') }}
|
|
<icon-down :class="{ 'is-open': showMore }" />
|
|
</button>
|
|
<div v-show="showMore" class="h5-pkm__fold-body">
|
|
<label class="h5-pkm__label">{{ t('order.myOrders.createPickup.carrierService') }}</label>
|
|
<a-select
|
|
v-model="form.carrierService"
|
|
allow-search
|
|
allow-clear
|
|
:placeholder="t('order.myOrders.createPickup.carrierServicePh')"
|
|
>
|
|
<a-option v-for="opt in carrierServiceOptions" :key="opt.value" :value="opt.value">
|
|
{{ opt.label }}
|
|
</a-option>
|
|
</a-select>
|
|
<label class="h5-pkm__label">{{ t('order.myOrders.createPickup.remark') }}</label>
|
|
<a-textarea
|
|
v-model="form.remark"
|
|
:max-length="512"
|
|
allow-clear
|
|
:auto-size="{ minRows: 2, maxRows: 4 }"
|
|
/>
|
|
</div>
|
|
</section>
|
|
</a-spin>
|
|
|
|
<footer class="h5-pkm__foot">
|
|
<button type="button" class="h5-pkm__submit" :disabled="submitting" @click="submit">
|
|
<span v-if="submitting" class="h5-pkm__loading" aria-hidden="true" />
|
|
{{ submitting ? t('h5.placeOrder.submitting') : t('h5.orders.mergePickup.submit') }}
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</a-drawer>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { computed, reactive, ref } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import dayjs from 'dayjs'
|
|
import { Message } from '@arco-design/web-vue'
|
|
import { IconClose, IconDown, IconLocation } from '@arco-design/web-vue/es/icon'
|
|
import { createPickupOrderV2 } from '@/api/order/pickupOrder'
|
|
import { getSenderAddressList } from '@/api/order/senderAddress'
|
|
import { getWarehouseList } from '@/api/system/warehouse'
|
|
import { normalizeListRow, pickField } from '@/utils/warehouseListMapper'
|
|
import {
|
|
loadDomesticPickupProductOptions,
|
|
type DomesticPickupProductOption,
|
|
} from '@/utils/domesticPickupProducts'
|
|
import H5EmptyState from './H5EmptyState.vue'
|
|
|
|
defineOptions({ name: 'H5CreatePickupFromOrdersSheet' })
|
|
|
|
type SenderOption = {
|
|
value: number
|
|
name: string
|
|
phone: string
|
|
address: string
|
|
}
|
|
|
|
const emit = defineEmits<{ success: [payload: { pickupNo?: string; id?: number }] }>()
|
|
|
|
const { t } = useI18n()
|
|
const visible = ref(false)
|
|
const submitting = ref(false)
|
|
const loadingRefs = ref(false)
|
|
const showMore = ref(false)
|
|
const activeTimePreset = ref('tomorrow_am')
|
|
const orderIds = ref<Array<number | string>>([])
|
|
const previewRows = ref<Array<{ id: number | string; serviceNo?: string }>>([])
|
|
const senderOptions = ref<SenderOption[]>([])
|
|
const warehouseOptions = ref<{ value: number; label: string }[]>([])
|
|
const carrierServiceOptions = ref<DomesticPickupProductOption[]>([])
|
|
|
|
const form = reactive({
|
|
senderAddressId: undefined as number | undefined,
|
|
warehouseId: undefined as number | undefined,
|
|
expectPickupTime: '',
|
|
carrierService: '',
|
|
remark: '',
|
|
})
|
|
|
|
const drawerStyle = {
|
|
borderRadius: '20px 20px 0 0',
|
|
overflow: 'hidden',
|
|
maxHeight: 'min(92vh, 780px)',
|
|
}
|
|
|
|
const warehouseLabel = computed(() => {
|
|
const hit = warehouseOptions.value.find((x) => x.value === form.warehouseId)
|
|
return hit?.label || '-'
|
|
})
|
|
|
|
const timePresets = computed(() => [
|
|
{
|
|
key: 'tomorrow_am',
|
|
label: t('h5.orders.mergePickup.timePresetTomorrowAm'),
|
|
value: dayjs().add(1, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss'),
|
|
},
|
|
{
|
|
key: 'tomorrow_pm',
|
|
label: t('h5.orders.mergePickup.timePresetTomorrowPm'),
|
|
value: dayjs().add(1, 'day').hour(14).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss'),
|
|
},
|
|
{
|
|
key: 'day_after_am',
|
|
label: t('h5.orders.mergePickup.timePresetDayAfterAm'),
|
|
value: dayjs().add(2, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss'),
|
|
},
|
|
])
|
|
|
|
function applyTimePreset(preset: { key: string; value: string }) {
|
|
activeTimePreset.value = preset.key
|
|
form.expectPickupTime = preset.value
|
|
}
|
|
|
|
function close() {
|
|
if (submitting.value) return
|
|
visible.value = false
|
|
}
|
|
|
|
async function loadRefs() {
|
|
loadingRefs.value = true
|
|
try {
|
|
const [senderRes, whRes, pickupProducts] = await Promise.all([
|
|
getSenderAddressList({ page: 1, pageSize: 200, addressType: 'sender' }),
|
|
getWarehouseList({ page: 1, pageSize: 200 }),
|
|
loadDomesticPickupProductOptions(),
|
|
])
|
|
senderOptions.value = (senderRes.items || [])
|
|
.map((raw: any) => {
|
|
const row = normalizeListRow(raw)
|
|
const id = Number(pickField(row, 'id', 'Id'))
|
|
const name = String(pickField(row, 'senderName', 'SenderName') || '')
|
|
const phone = String(pickField(row, 'phone', 'Phone') || '')
|
|
const address = [
|
|
pickField(row, 'province', 'Province'),
|
|
pickField(row, 'city', 'City'),
|
|
pickField(row, 'streetAddress1', 'StreetAddress1'),
|
|
]
|
|
.filter(Boolean)
|
|
.join('')
|
|
return { value: id, name, phone, address }
|
|
})
|
|
.filter((x) => x.value > 0)
|
|
if (!form.senderAddressId && senderOptions.value.length) {
|
|
form.senderAddressId = senderOptions.value[0].value
|
|
}
|
|
|
|
warehouseOptions.value = (whRes.items || [])
|
|
.map((raw: any) => {
|
|
const row = normalizeListRow(raw)
|
|
return {
|
|
value: Number(pickField(row, 'id', 'Id')),
|
|
label: String(pickField(row, 'warehouseName', 'WarehouseName', 'name') || ''),
|
|
}
|
|
})
|
|
.filter((x) => x.value > 0)
|
|
|
|
carrierServiceOptions.value = pickupProducts
|
|
} finally {
|
|
loadingRefs.value = false
|
|
}
|
|
}
|
|
|
|
async function open(rows: Array<{ id: number | string; serviceNo?: string; warehouseId?: number | string }>) {
|
|
const list = (rows || []).filter((r) => r?.id != null)
|
|
if (!list.length) {
|
|
Message.warning(t('h5.orders.mergePickup.needSelect'))
|
|
return
|
|
}
|
|
const whIds = [...new Set(list.map((r) => Number(r.warehouseId)).filter((x) => x > 0))]
|
|
if (whIds.length !== 1) {
|
|
Message.warning(t('order.myOrders.createPickup.sameWarehouse'))
|
|
return
|
|
}
|
|
|
|
orderIds.value = list.map((r) => r.id)
|
|
previewRows.value = list.map((r) => ({ id: r.id, serviceNo: r.serviceNo }))
|
|
form.warehouseId = whIds[0]
|
|
form.carrierService = ''
|
|
form.remark = ''
|
|
form.senderAddressId = undefined
|
|
showMore.value = false
|
|
activeTimePreset.value = 'tomorrow_am'
|
|
form.expectPickupTime = dayjs().add(1, 'day').hour(10).minute(0).second(0).format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
visible.value = true
|
|
await loadRefs()
|
|
}
|
|
|
|
async function submit() {
|
|
if (!form.senderAddressId) {
|
|
Message.warning(t('order.myOrders.createPickup.needSender'))
|
|
return
|
|
}
|
|
if (!form.warehouseId) {
|
|
Message.warning(t('order.myOrders.createPickup.needWarehouse'))
|
|
return
|
|
}
|
|
if (!form.expectPickupTime.trim()) {
|
|
Message.warning(t('order.myOrders.createPickup.needTime'))
|
|
return
|
|
}
|
|
|
|
submitting.value = true
|
|
try {
|
|
const res: any = await createPickupOrderV2({
|
|
senderAddressId: form.senderAddressId,
|
|
warehouseId: form.warehouseId,
|
|
carrierService: form.carrierService.trim() || undefined,
|
|
expectPickupTime: form.expectPickupTime.trim(),
|
|
orderIds: orderIds.value,
|
|
remark: form.remark.trim() || undefined,
|
|
})
|
|
const pickupNo = res?.pickupNo || res?.PickupNo
|
|
Message.success(
|
|
t('order.myOrders.createPickup.success', { no: pickupNo || '', count: orderIds.value.length }),
|
|
)
|
|
visible.value = false
|
|
emit('success', { pickupNo, id: res?.id ?? res?.Id })
|
|
} catch (e: any) {
|
|
Message.error(e?.message || t('order.myOrders.createPickup.failed'))
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
defineExpose({ open })
|
|
</script>
|
|
|
|
<style lang="less">
|
|
.h5-pk-merge-drawer {
|
|
.arco-drawer-content {
|
|
border-radius: 20px 20px 0 0;
|
|
overflow: hidden;
|
|
max-height: min(92vh, 780px);
|
|
}
|
|
.arco-drawer-body {
|
|
padding: 0;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="less" scoped>
|
|
.h5-pkm {
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: min(92vh, 780px);
|
|
background: #f5f6f8;
|
|
}
|
|
|
|
.h5-pkm__handle {
|
|
width: 36px;
|
|
height: 4px;
|
|
margin: 10px auto 0;
|
|
border-radius: 999px;
|
|
background: #c9cdd4;
|
|
}
|
|
|
|
.h5-pkm__head {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
padding: 12px 16px 0;
|
|
|
|
h3 {
|
|
margin: 2px 0 0;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: #1d2129;
|
|
}
|
|
}
|
|
|
|
.h5-pkm__step {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
color: var(--app-primary-color);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.h5-pkm__close {
|
|
width: 36px;
|
|
height: 36px;
|
|
border: none;
|
|
border-radius: 12px;
|
|
background: #fff;
|
|
color: #86909c;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
|
}
|
|
|
|
.h5-pkm__body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: auto;
|
|
padding: 12px 16px;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.h5-pkm__summary {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.h5-pkm__summary-item {
|
|
padding: 12px;
|
|
border-radius: 12px;
|
|
background: #fff;
|
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
|
|
|
span {
|
|
display: block;
|
|
font-size: 11px;
|
|
color: #86909c;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
strong {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: #1d2129;
|
|
line-height: 1.35;
|
|
word-break: break-all;
|
|
}
|
|
}
|
|
|
|
.h5-pkm__tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.h5-pkm__tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
max-width: 100%;
|
|
height: 26px;
|
|
padding: 0 10px;
|
|
border-radius: 999px;
|
|
background: #fff;
|
|
border: 1px solid #e5e6eb;
|
|
font-size: 12px;
|
|
color: #4e5969;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.h5-pkm__section {
|
|
margin-bottom: 14px;
|
|
padding: 14px;
|
|
border-radius: 14px;
|
|
background: #fff;
|
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
|
|
|
h4 {
|
|
margin: 0 0 10px;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: #1d2129;
|
|
}
|
|
|
|
&--fold {
|
|
padding-bottom: 10px;
|
|
}
|
|
}
|
|
|
|
.h5-pkm__sender {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 3px;
|
|
margin-bottom: 8px;
|
|
padding: 12px;
|
|
border: 1.5px solid #f0f1f3;
|
|
border-radius: 12px;
|
|
background: #fafbfc;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
transition: border-color 0.15s ease, background 0.15s ease;
|
|
|
|
&:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
b {
|
|
font-size: 14px;
|
|
color: #1d2129;
|
|
}
|
|
|
|
span {
|
|
font-size: 12px;
|
|
color: #4e5969;
|
|
}
|
|
|
|
em {
|
|
font-style: normal;
|
|
font-size: 12px;
|
|
color: #86909c;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
&.is-active {
|
|
border-color: var(--app-primary-color);
|
|
background: var(--h5-primary-softer, #f3faf9);
|
|
}
|
|
}
|
|
|
|
.h5-pkm__time-chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.h5-pkm__time-chip {
|
|
border: 1px solid #e5e6eb;
|
|
border-radius: 999px;
|
|
background: #fafbfc;
|
|
height: 32px;
|
|
padding: 0 12px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: #4e5969;
|
|
cursor: pointer;
|
|
|
|
&.is-active {
|
|
border-color: var(--app-primary-color);
|
|
background: var(--h5-primary-soft, #e6f4f4);
|
|
color: var(--app-primary-color);
|
|
}
|
|
}
|
|
|
|
.h5-pkm__time-input {
|
|
width: 100%;
|
|
}
|
|
|
|
.h5-pkm__fold {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border: none;
|
|
background: transparent;
|
|
padding: 0;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: #4e5969;
|
|
cursor: pointer;
|
|
|
|
:deep(.arco-icon) {
|
|
transition: transform 0.2s ease;
|
|
|
|
&.is-open {
|
|
transform: rotate(180deg);
|
|
}
|
|
}
|
|
}
|
|
|
|
.h5-pkm__fold-body {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.h5-pkm__label {
|
|
display: block;
|
|
margin: 10px 0 6px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: #86909c;
|
|
}
|
|
|
|
.h5-pkm__foot {
|
|
flex-shrink: 0;
|
|
padding: 12px 16px calc(12px + env(safe-area-inset-bottom, 0px));
|
|
background: #fff;
|
|
border-top: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.h5-pkm__submit {
|
|
width: 100%;
|
|
height: 48px;
|
|
border: none;
|
|
border-radius: 24px;
|
|
background: var(--app-primary-color);
|
|
color: #fff;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
box-shadow: 0 8px 20px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
|
|
|
|
&:disabled {
|
|
opacity: 0.65;
|
|
cursor: not-allowed;
|
|
box-shadow: none;
|
|
}
|
|
}
|
|
|
|
.h5-pkm__loading {
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid rgba(255, 255, 255, 0.35);
|
|
border-top-color: #fff;
|
|
border-radius: 50%;
|
|
animation: h5-pkm-spin 0.7s linear infinite;
|
|
}
|
|
|
|
@keyframes h5-pkm-spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
</style>
|