fix: 店铺拉单弹窗按 TikTok 状态筛选,并避免表格刷新打断弹窗
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m16s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-16 15:17:47 +08:00
parent e5175e6cc1
commit 108083d332
5 changed files with 237 additions and 56 deletions

View File

@ -1475,6 +1475,9 @@
manualColumnResize: true, manualColumnResize: true,
manualColumnMove: props.enableColumnReorder, manualColumnMove: props.enableColumnReorder,
stretchH: props.stretchColumns ? 'all' : 'none', stretchH: props.stretchColumns ? 'all' : 'none',
// 列宽由列定义给出。开启后 loadData 会走 GhostTable 测量,
// 单元格节点缺失时抛 getBoundingClientRect,并中断外层页面更新。
autoColumnSize: false,
// stretch 时锁定操作/序号/复选框列,避免被均分拉宽 // stretch 时锁定操作/序号/复选框列,避免被均分拉宽
beforeStretchingColumnWidth: lockFixedColumnStretchWidth, beforeStretchingColumnWidth: lockFixedColumnStretchWidth,
...(props.rowHeight != null && props.rowHeight > 0 ...(props.rowHeight != null && props.rowHeight > 0
@ -2362,6 +2365,8 @@
hotInstance.loadData([]); hotInstance.loadData([]);
hotInstance.render(); hotInstance.render();
publishSelection([], new Set()); publishSelection([], new Set());
} catch (error) {
console.warn('[HandsontablePro] clear failed', error);
} finally { } finally {
selectionLock -= 1; selectionLock -= 1;
} }
@ -2393,6 +2398,8 @@
} else { } else {
applySelectedRowStyles(); applySelectedRowStyles();
} }
} catch (error) {
console.warn('[HandsontablePro] refresh failed', error);
} finally { } finally {
selectionLock -= 1; selectionLock -= 1;
} }

View File

@ -1079,9 +1079,25 @@ export default {
'order.platformStoreOrder.createFulfillWms.success': 'Created {count} fulfill order(s)', 'order.platformStoreOrder.createFulfillWms.success': 'Created {count} fulfill order(s)',
'order.platformStoreOrder.pull.store': 'Store', 'order.platformStoreOrder.pull.store': 'Store',
'order.platformStoreOrder.pull.storePlaceholder': 'Select stores', 'order.platformStoreOrder.pull.storePlaceholder': 'Select stores',
'order.platformStoreOrder.pull.storeAll': 'All stores',
'order.platformStoreOrder.pull.status': 'Status', 'order.platformStoreOrder.pull.status': 'Status',
'order.platformStoreOrder.pull.statusAll': 'All',
'order.platformStoreOrder.pull.statusClear': 'Clear',
'order.platformStoreOrder.pull.statusCount': '{count} selected',
'order.platformStoreOrder.pull.time': 'Time', 'order.platformStoreOrder.pull.time': 'Time',
'order.platformStoreOrder.pull.timeHint': 'Defaults to today', 'order.platformStoreOrder.pull.timeHint': 'Filter by order created time. Defaults to today.',
'order.platformStoreOrder.pull.today': 'Today',
'order.platformStoreOrder.pull.last7': '7 days',
'order.platformStoreOrder.pull.last15': '15 days',
'order.platformStoreOrder.pull.status.UNPAID': 'Unpaid',
'order.platformStoreOrder.pull.status.ON_HOLD': 'On hold',
'order.platformStoreOrder.pull.status.PARTIALLY_SHIPPING': 'Partially shipping',
'order.platformStoreOrder.pull.status.AWAITING_SHIPMENT': 'Awaiting shipment',
'order.platformStoreOrder.pull.status.AWAITING_COLLECTION': 'Awaiting collection',
'order.platformStoreOrder.pull.status.IN_TRANSIT': 'In transit',
'order.platformStoreOrder.pull.status.DELIVERED': 'Delivered',
'order.platformStoreOrder.pull.status.COMPLETED': 'Completed',
'order.platformStoreOrder.pull.status.CANCELLED': 'Cancelled',
'order.platformStoreOrder.message.loadFailed': 'Failed to load orders', 'order.platformStoreOrder.message.loadFailed': 'Failed to load orders',
'order.platformStoreOrder.message.detailFailed': 'Failed to load detail', 'order.platformStoreOrder.message.detailFailed': 'Failed to load detail',
'order.platformStoreOrder.message.matchSuccess': 'Inventory matched', 'order.platformStoreOrder.message.matchSuccess': 'Inventory matched',

View File

@ -1070,9 +1070,25 @@ export default {
'order.platformStoreOrder.createFulfillWms.success': '成功创建 {count} 张代发订单', 'order.platformStoreOrder.createFulfillWms.success': '成功创建 {count} 张代发订单',
'order.platformStoreOrder.pull.store': '店铺', 'order.platformStoreOrder.pull.store': '店铺',
'order.platformStoreOrder.pull.storePlaceholder': '请选择店铺', 'order.platformStoreOrder.pull.storePlaceholder': '请选择店铺',
'order.platformStoreOrder.pull.status': '类型', 'order.platformStoreOrder.pull.storeAll': '全选店铺',
'order.platformStoreOrder.pull.status': '状态',
'order.platformStoreOrder.pull.statusAll': '全选',
'order.platformStoreOrder.pull.statusClear': '清空',
'order.platformStoreOrder.pull.statusCount': '已选 {count} 个',
'order.platformStoreOrder.pull.time': '时间', 'order.platformStoreOrder.pull.time': '时间',
'order.platformStoreOrder.pull.timeHint': '默认时间为当天', 'order.platformStoreOrder.pull.timeHint': '按订单创建时间拉取,默认当天',
'order.platformStoreOrder.pull.today': '今天',
'order.platformStoreOrder.pull.last7': '近7天',
'order.platformStoreOrder.pull.last15': '近15天',
'order.platformStoreOrder.pull.status.UNPAID': '未付款',
'order.platformStoreOrder.pull.status.ON_HOLD': '待处理',
'order.platformStoreOrder.pull.status.PARTIALLY_SHIPPING': '部分发货',
'order.platformStoreOrder.pull.status.AWAITING_SHIPMENT': '待发货',
'order.platformStoreOrder.pull.status.AWAITING_COLLECTION': '待揽收',
'order.platformStoreOrder.pull.status.IN_TRANSIT': '运输中',
'order.platformStoreOrder.pull.status.DELIVERED': '已送达',
'order.platformStoreOrder.pull.status.COMPLETED': '已完成',
'order.platformStoreOrder.pull.status.CANCELLED': '已取消',
'order.platformStoreOrder.message.loadFailed': '加载订单失败', 'order.platformStoreOrder.message.loadFailed': '加载订单失败',
'order.platformStoreOrder.message.detailFailed': '加载详情失败', 'order.platformStoreOrder.message.detailFailed': '加载详情失败',
'order.platformStoreOrder.message.matchSuccess': '库存匹配成功', 'order.platformStoreOrder.message.matchSuccess': '库存匹配成功',

View File

@ -9,6 +9,7 @@ import { pickField } from '@/utils/warehouseListMapper'
const props = defineProps<{ const props = defineProps<{
visible: boolean visible: boolean
customerId?: number customerId?: number
platformCode?: string
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
@ -16,51 +17,77 @@ const emit = defineEmits<{
success: [] success: []
}>() }>()
/** TikTok 订单搜索状态,与平台 order_status 一致 */
const TIKTOK_PULL_STATUSES = [
'UNPAID',
'ON_HOLD',
'PARTIALLY_SHIPPING',
'AWAITING_SHIPMENT',
'AWAITING_COLLECTION',
'IN_TRANSIT',
'DELIVERED',
'COMPLETED',
'CANCELLED',
] as const
type StoreOption = { label: string; value: number; platform: string }
const { t } = useI18n() const { t } = useI18n()
const loading = ref(false) const loading = ref(false)
const storeLoading = ref(false) const storeLoading = ref(false)
const storeOptions = ref<Array<{ label: string; value: number }>>([]) const storeOptions = ref<StoreOption[]>([])
const storeIds = ref<number[]>([]) const storeIds = ref<number[]>([])
const orderStatuses = ref<string[]>([ const orderStatuses = ref<string[]>([...TIKTOK_PULL_STATUSES])
'unpaid', const dateRange = ref<[string, string]>(todayRange())
'pending',
'canceled',
'ready_to_ship',
'delivered',
'returned',
'shipped',
'failed',
'topack',
'toship',
'shipping',
'lost',
])
const dateRange = ref<[string, string] | undefined>()
const statusOptions = computed(() =>
[
'unpaid',
'pending',
'canceled',
'ready_to_ship',
'delivered',
'returned',
'shipped',
'failed',
'topack',
'toship',
'shipping',
'lost',
].map((value) => ({ label: value, value })),
)
const modalVisible = computed({ const modalVisible = computed({
get: () => props.visible, get: () => props.visible,
set: (v: boolean) => emit('update:visible', v), set: (v: boolean) => emit('update:visible', v),
}) })
const visibleStoreOptions = computed(() => {
const platform = (props.platformCode || '').trim().toLowerCase()
if (!platform || platform === 'all') return storeOptions.value
return storeOptions.value.filter((item) => item.platform === platform)
})
const allStatusesChecked = computed(
() => orderStatuses.value.length === TIKTOK_PULL_STATUSES.length,
)
const allStoresChecked = computed(
() =>
visibleStoreOptions.value.length > 0
&& storeIds.value.length === visibleStoreOptions.value.length,
)
const canSubmit = computed(
() => storeIds.value.length > 0 && orderStatuses.value.length > 0 && !storeLoading.value,
)
function todayRange(): [string, string] {
return [dayjs().format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')]
}
function applyRange(days: number) {
dateRange.value = [
dayjs().subtract(days - 1, 'day').format('YYYY-MM-DD'),
dayjs().format('YYYY-MM-DD'),
]
}
function toggleAllStatuses() {
orderStatuses.value = allStatusesChecked.value ? [] : [...TIKTOK_PULL_STATUSES]
}
function toggleAllStores() {
storeIds.value = allStoresChecked.value
? []
: visibleStoreOptions.value.map((item) => item.value)
}
async function loadStoreOptions() { async function loadStoreOptions() {
storeIds.value = [] storeIds.value = []
orderStatuses.value = [...TIKTOK_PULL_STATUSES]
dateRange.value = todayRange()
storeLoading.value = true storeLoading.value = true
try { try {
const { items } = await getPlatformStoreList({ const { items } = await getPlatformStoreList({
@ -72,12 +99,12 @@ async function loadStoreOptions() {
.map((raw: Record<string, unknown>) => { .map((raw: Record<string, unknown>) => {
const id = Number(pickField(raw, 'id', 'Id')) const id = Number(pickField(raw, 'id', 'Id'))
const name = String(pickField(raw, 'storeNickname', 'StoreNickname') ?? `#${id}`) const name = String(pickField(raw, 'storeNickname', 'StoreNickname') ?? `#${id}`)
const platform = String(pickField(raw, 'platformCode', 'PlatformCode') ?? '') const platform = String(pickField(raw, 'platformCode', 'PlatformCode') ?? '').toLowerCase()
return { label: platform ? `${name} (${platform})` : name, value: id } return { label: platform ? `${name} (${platform})` : name, value: id, platform }
}) })
.filter((x) => x.value > 0) .filter((item) => item.value > 0)
if (!storeOptions.value.length) { if (visibleStoreOptions.value.length === 1) {
$baseMessage(t('order.platformStoreOrder.message.noStore'), 'warning') storeIds.value = [visibleStoreOptions.value[0].value]
} }
} catch (e: any) { } catch (e: any) {
storeOptions.value = [] storeOptions.value = []
@ -145,38 +172,152 @@ async function handleSubmit(): Promise<boolean> {
<a-modal <a-modal
v-model:visible="modalVisible" v-model:visible="modalVisible"
:title="t('order.platformStoreOrder.actions.pullOrders')" :title="t('order.platformStoreOrder.actions.pullOrders')"
:width="720"
:ok-loading="loading" :ok-loading="loading"
:ok-text="t('order.platformStoreOrder.actions.startPull')" :ok-text="t('order.platformStoreOrder.actions.startPull')"
:ok-button-props="{ disabled: !canSubmit }"
unmount-on-close unmount-on-close
:on-before-ok="handleSubmit" :on-before-ok="handleSubmit"
> >
<a-form :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 16 }"> <a-form class="pull-form" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 16 }">
<a-form-item :label="t('order.platformStoreOrder.pull.store')" required> <a-form-item :label="t('order.platformStoreOrder.pull.store')" required>
<a-select <div class="pull-block">
v-model="storeIds" <div class="pull-row">
multiple <a-select
allow-clear v-model="storeIds"
:loading="storeLoading" class="pull-grow"
:options="storeOptions" multiple
:placeholder="t('order.platformStoreOrder.pull.storePlaceholder')" allow-clear
:max-tag-count="3" :loading="storeLoading"
/> :options="visibleStoreOptions"
:placeholder="t('order.platformStoreOrder.pull.storePlaceholder')"
:max-tag-count="2"
/>
<a-button
type="text"
size="small"
:disabled="!visibleStoreOptions.length"
@click="toggleAllStores"
>
{{ allStoresChecked ? t('order.platformStoreOrder.pull.statusClear') : t('order.platformStoreOrder.pull.storeAll') }}
</a-button>
</div>
<div v-if="!storeLoading && !visibleStoreOptions.length" class="hint">
{{ t('order.platformStoreOrder.message.noStore') }}
</div>
</div>
</a-form-item> </a-form-item>
<a-form-item :label="t('order.platformStoreOrder.pull.status')" required> <a-form-item :label="t('order.platformStoreOrder.pull.status')" required>
<a-checkbox-group v-model="orderStatuses" :options="statusOptions" /> <div class="pull-block">
<div class="pull-row pull-row--between">
<span class="hint">
{{ t('order.platformStoreOrder.pull.statusCount', { count: orderStatuses.length }) }}
</span>
<a-button type="text" size="small" @click="toggleAllStatuses">
{{ allStatusesChecked ? t('order.platformStoreOrder.pull.statusClear') : t('order.platformStoreOrder.pull.statusAll') }}
</a-button>
</div>
<a-checkbox-group v-model="orderStatuses" class="status-grid">
<a-checkbox v-for="status in TIKTOK_PULL_STATUSES" :key="status" :value="status">
<span class="status-name">{{ t(`order.platformStoreOrder.pull.status.${status}`) }}</span>
<span class="status-code">{{ status }}</span>
</a-checkbox>
</a-checkbox-group>
</div>
</a-form-item> </a-form-item>
<a-form-item :label="t('order.platformStoreOrder.pull.time')"> <a-form-item :label="t('order.platformStoreOrder.pull.time')">
<a-range-picker v-model="dateRange" style="width: 100%" /> <div class="pull-block">
<div class="hint">{{ t('order.platformStoreOrder.pull.timeHint') }}</div> <a-range-picker
v-model="dateRange"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
<div class="pull-row">
<a-button type="text" size="small" @click="applyRange(1)">
{{ t('order.platformStoreOrder.pull.today') }}
</a-button>
<a-button type="text" size="small" @click="applyRange(7)">
{{ t('order.platformStoreOrder.pull.last7') }}
</a-button>
<a-button type="text" size="small" @click="applyRange(15)">
{{ t('order.platformStoreOrder.pull.last15') }}
</a-button>
</div>
<div class="hint">{{ t('order.platformStoreOrder.pull.timeHint') }}</div>
</div>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
</template> </template>
<style scoped lang="less"> <style scoped lang="less">
.hint { .pull-form {
margin-top: 8px; :deep(.arco-form-item) {
align-items: flex-start;
margin-bottom: 16px;
}
:deep(.arco-form-item-label-col) {
line-height: 32px;
}
:deep(.arco-form-item-content) {
min-width: 0;
}
}
.pull-block {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 8px;
width: 100%;
}
.pull-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.pull-row--between {
justify-content: space-between;
}
.pull-grow {
flex: 1;
min-width: 0;
}
.status-grid {
display: grid !important;
grid-template-columns: 1fr 1fr;
gap: 10px 12px;
width: 100%;
:deep(.arco-checkbox) {
margin: 0;
align-items: center;
line-height: 22px;
white-space: nowrap;
}
}
.status-name {
color: var(--color-text-1);
}
.status-code {
margin-left: 6px;
color: var(--color-text-3); color: var(--color-text-3);
font-size: 12px; font-size: 12px;
} }
.hint {
color: var(--color-text-3);
font-size: 12px;
line-height: 20px;
}
</style> </style>

View File

@ -860,6 +860,7 @@ watch(
v-if="pullVisible" v-if="pullVisible"
v-model:visible="pullVisible" v-model:visible="pullVisible"
:customer-id="withCustomerId({}).customerId" :customer-id="withCustomerId({}).customerId"
:platform-code="platformCode"
@success="() => { loadSummary(); loadList() }" @success="() => { loadSummary(); loadList() }"
/> />