front/src/views/order/platformStore/index.vue
zzh 25445bc146 feat: 完善日夜间主题与揽收下单体验
统一暗色模式页面/顶栏/Handsontable 选中色,调整下单到仓方式文案与提示,CustomLeftMenu 卡片统一 60px;补充揽收单、问题件处理与 H5 相关页面。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 11:49:23 +08:00

692 lines
21 KiB
Vue

<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { Modal } from '@arco-design/web-vue'
import HandsontablePro from '@/components/HandsontablePro/index.vue'
import PaginationPro from '@/components/PaginationPro/index.vue'
import ModalPro from '@/components/ModalPro/index.vue'
import useModal from '@/components/ModalPro/useModal'
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
import { deletePlatformStore, getPlatformStoreAuthUrl, getPlatformStoreList, savePlatformStore } from '@/api/order/platformStore'
import { $baseMessage } from '@/utils'
import { hasPermission } from '@/utils/rbac-permission'
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
defineOptions({ name: 'PlatformStore' })
const { t } = useI18n()
const { tableContainerRef, tableHeight } = useTableContainerHeight()
const {
isAdmin,
customerId: adminCustomerId,
customerOptions,
withCustomerId,
ensureMutationCustomerId,
} = useWarehouseAdminCustomer()
const PLATFORM_TAB_ALL = 'all'
const platformCode = ref<string>(PLATFORM_TAB_ALL)
const keyword = ref('')
const listLoading = ref(false)
const tableRows = ref<any[]>([])
const currentPage = ref(1)
const pageSize = ref(30)
const totalCount = ref(0)
const platformDefs = [
{ code: 'tiktok', label: 'TikTok' },
{ code: 'temu', label: 'Temu' },
{ code: 'shopee', label: 'Shopee' },
{ code: 'lazada', label: 'Lazada' },
] as const
const platformTabs = computed(() => [
{ value: PLATFORM_TAB_ALL, label: t('common.all') },
...platformDefs.map((p) => ({ value: p.code, label: p.label })),
])
const lazadaSiteOptions = computed(() => [
{ value: 'cb', label: t('order.platformStore.site.crossBorder') },
{ value: 'sg', label: '新加坡' },
{ value: 'my', label: '马来西亚' },
{ value: 'th', label: '泰国' },
{ value: 'vn', label: '越南' },
{ value: 'ph', label: '菲律宾' },
{ value: 'id', label: '印度尼西亚' },
])
const [registerFormModal, { openModal, closeModal }] = useModal() as any
const saving = ref(false)
const formModel = reactive({
id: 0,
platformCode: '',
storeNickname: '',
platformAccount: '',
siteCountry: 'cb',
isAutoFulfill: false,
remark: '',
authorizationCode: '',
})
const isEdit = computed(() => formModel.id > 0)
const showStoreFields = computed(() => !!formModel.platformCode || isEdit.value)
const modalTitle = computed(() =>
isEdit.value ? t('order.platformStore.modal.edit') : t('order.platformStore.modal.add'),
)
const primaryActionLabel = computed(() =>
isEdit.value ? t('warehouse.common.save') : t('order.platformStore.actions.startAuth'),
)
const hotColumns = computed(() => {
const cols = [
{ key: 'platformName', label: t('order.platformStore.table.platform'), width: 100 },
{ key: 'storeNickname', label: t('order.platformStore.table.nickname'), width: 160 },
{ key: 'platformAccount', label: t('order.platformStore.table.account'), width: 140 },
{ key: 'siteCountry', label: t('order.platformStore.table.siteCountry'), width: 90 },
{ key: 'authStatus', label: t('order.platformStore.table.authStatus'), width: 100 },
{ key: 'isAutoFulfill', label: t('order.platformStore.table.autoFulfill'), width: 90 },
{ key: 'authTime', label: t('order.platformStore.table.authTime'), width: 160 },
{ key: 'tokenExpireTime', label: t('order.platformStore.table.tokenExpire'), width: 160 },
{ key: 'createTime', label: t('order.platformStore.table.createTime'), width: 160 },
]
if (isAdmin.value) {
cols.unshift(
{ key: 'customerName', label: t('order.myOrders.table.customerName'), width: 120 },
{ key: 'customerCode', label: t('order.myOrders.table.customerCode'), width: 110 },
)
}
return cols
})
function platformLabel(code: string) {
return platformDefs.find((p) => p.code === code)?.label ?? code
}
function mapRow(raw: Record<string, unknown>) {
const row = normalizeListRow(raw)
const authStatus = String(pickField(row, 'authStatus', 'AuthStatus') ?? '')
const autoFulfill = Boolean(pickField(row, 'isAutoFulfill', 'IsAutoFulfill'))
return {
id: pickField<number>(row, 'id', 'Id'),
customerId: pickField<number>(row, 'customerId', 'CustomerId'),
customerName: pickField(row, 'customerName', 'CustomerName') ?? '',
customerCode: pickField(row, 'customerCode', 'CustomerCode') ?? '',
platformName: pickField(row, 'platformName', 'PlatformName') ?? pickField(row, 'platformCode', 'PlatformCode') ?? '-',
storeNickname: pickField(row, 'storeNickname', 'StoreNickname') ?? '-',
platformAccount: pickField(row, 'platformAccount', 'PlatformAccount') ?? '-',
siteCountry: pickField(row, 'siteCountry', 'SiteCountry') ?? '-',
authStatus: t(`order.platformStore.auth.${authStatus}`, authStatus),
isAutoFulfill: autoFulfill ? t('warehouse.common.yes') : t('warehouse.common.no'),
authTime: formatDateTime(pickField(row, 'authTime', 'AuthTime')),
tokenExpireTime: formatDateTime(pickField(row, 'tokenExpireTime', 'TokenExpireTime')),
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')),
_raw: row,
}
}
async function loadList() {
listLoading.value = true
try {
const { items, total } = await getPlatformStoreList(
withCustomerId({
page: currentPage.value,
pageSize: pageSize.value,
platformCode: platformCode.value === PLATFORM_TAB_ALL ? undefined : platformCode.value,
storeNickname: keyword.value || undefined,
}),
)
tableRows.value = (items || []).map((item: Record<string, unknown>) => mapRow(item))
totalCount.value = Number(total || 0)
} catch (e: any) {
$baseMessage(e?.message || t('order.platformStore.message.loadFailed'), 'error')
} finally {
listLoading.value = false
}
}
function handlePageChange(page: number) {
currentPage.value = page
loadList()
}
function handlePageSizeChange(size: number) {
pageSize.value = size
currentPage.value = 1
loadList()
}
function handleSearch() {
currentPage.value = 1
loadList()
}
function handlePlatformTabChange() {
currentPage.value = 1
loadList()
}
function resetForm() {
Object.assign(formModel, {
id: 0,
platformCode: '',
storeNickname: '',
platformAccount: '',
siteCountry: 'cb',
isAutoFulfill: false,
remark: '',
authorizationCode: '',
})
}
function handleAdd() {
if (!hasPermission('Order.PlatformStore.Save')) return
if (isAdmin.value && ensureMutationCustomerId() == null) return
resetForm()
if (platformCode.value !== PLATFORM_TAB_ALL) {
formModel.platformCode = platformCode.value
if (platformCode.value === 'lazada') formModel.siteCountry = 'cb'
if (platformCode.value === 'temu') formModel.siteCountry = 'US'
}
openModal(true)
}
function handleEdit(row: any) {
if (!hasPermission('Order.PlatformStore.Save')) return
if (isAdmin.value && ensureMutationCustomerId() == null) return
const raw = row._raw || row
Object.assign(formModel, {
id: Number(pickField(raw, 'id', 'Id') ?? 0),
platformCode: String(pickField(raw, 'platformCode', 'PlatformCode') ?? ''),
storeNickname: String(pickField(raw, 'storeNickname', 'StoreNickname') ?? ''),
platformAccount: String(pickField(raw, 'platformAccount', 'PlatformAccount') ?? ''),
siteCountry: String(pickField(raw, 'siteCountry', 'SiteCountry') ?? 'cb'),
isAutoFulfill: Boolean(pickField(raw, 'isAutoFulfill', 'IsAutoFulfill')),
remark: String(pickField(raw, 'remark', 'Remark') ?? ''),
authorizationCode: '',
})
openModal(true)
}
function selectPlatform(code: string) {
if (isEdit.value) return
formModel.platformCode = code
if (code === 'lazada') formModel.siteCountry = 'cb'
if (code === 'temu') formModel.siteCountry = 'US'
}
function validateForm() {
if (!formModel.platformCode) {
$baseMessage(t('order.platformStore.message.platformRequired'), 'warning')
return false
}
if (!formModel.storeNickname.trim()) {
$baseMessage(t('order.platformStore.message.required'), 'warning')
return false
}
if (formModel.platformCode === 'temu' && !formModel.authorizationCode.trim()) {
$baseMessage(t('order.platformStore.message.authCodeRequired'), 'warning')
return false
}
return true
}
async function handleSave() {
if (!validateForm()) return
const mutationCustomerId = isAdmin.value ? ensureMutationCustomerId() : undefined
if (isAdmin.value && mutationCustomerId == null) return
saving.value = true
try {
await savePlatformStore(withCustomerId({ ...formModel }))
$baseMessage(t('order.platformStore.message.saveSuccess'), 'success')
closeModal()
await loadList()
} catch (e: any) {
$baseMessage(e?.message || t('order.platformStore.message.saveFailed'), 'error')
} finally {
saving.value = false
}
}
const PENDING_AUTH_KEY = 'oms_platform_store_auth_pending'
async function handleStartAuth() {
if (!validateForm()) return
const mutationCustomerId = isAdmin.value ? ensureMutationCustomerId() : undefined
if (isAdmin.value && mutationCustomerId == null) return
saving.value = true
try {
const result = await getPlatformStoreAuthUrl(
withCustomerId({
platformCode: formModel.platformCode,
storeNickname: formModel.storeNickname,
siteCountry: formModel.siteCountry,
authorizationCode: formModel.authorizationCode || undefined,
}),
)
const authUrl = String(result?.authUrl || result?.AuthUrl || '')
const state = result?.state || result?.State
if (authUrl === '/' || !authUrl) {
$baseMessage(result?.message || t('order.platformStore.message.authSuccess'), 'success')
closeModal()
await loadList()
return
}
localStorage.setItem(
PENDING_AUTH_KEY,
JSON.stringify({
platformCode: formModel.platformCode,
storeNickname: formModel.storeNickname,
siteCountry: formModel.siteCountry,
state,
customerId: mutationCustomerId,
}),
)
window.open(authUrl, '_blank')
$baseMessage(result?.message || t('order.platformStore.message.authStarted'), 'success')
closeModal()
} catch (e: any) {
$baseMessage(e?.message || t('order.platformStore.message.authFailed'), 'error')
} finally {
saving.value = false
}
}
async function handlePrimaryAction() {
if (isEdit.value) {
await handleSave()
return
}
await handleStartAuth()
}
async function handleAuth(row: any) {
if (!hasPermission('Order.PlatformStore.Auth')) return
const mutationCustomerId = isAdmin.value ? ensureMutationCustomerId() : undefined
if (isAdmin.value && mutationCustomerId == null) return
const raw = row._raw || row
const code = String(pickField(raw, 'platformCode', 'PlatformCode') ?? '')
const nick = String(pickField(raw, 'storeNickname', 'StoreNickname') ?? '')
const site = String(pickField(raw, 'siteCountry', 'SiteCountry') ?? '')
if (!code) {
$baseMessage(t('order.platformStore.message.platformRequired'), 'warning')
return
}
try {
const result = await getPlatformStoreAuthUrl(
withCustomerId({
platformCode: code,
storeNickname: nick || 'renew',
siteCountry: site || undefined,
}),
)
const authUrl = String(result?.authUrl || result?.AuthUrl || '')
const state = result?.state || result?.State
if (authUrl === '/' || !authUrl) {
$baseMessage(result?.message || t('order.platformStore.message.authSuccess'), 'success')
await loadList()
return
}
localStorage.setItem(
PENDING_AUTH_KEY,
JSON.stringify({
platformCode: code,
storeNickname: nick,
siteCountry: site,
state,
customerId: mutationCustomerId,
}),
)
window.open(authUrl, '_blank')
$baseMessage(result?.message || t('order.platformStore.message.authStarted'), 'info')
} catch (e: any) {
$baseMessage(e?.message || t('order.platformStore.message.authFailed'), 'error')
}
}
function onAuthMessage(event: MessageEvent) {
if (event.origin !== window.location.origin) return
if (event.data?.type === 'oms-platform-store-auth' && event.data?.ok) {
loadList()
}
}
onMounted(() => {
loadList()
window.addEventListener('message', onAuthMessage)
})
function handleDelete(row: any) {
if (!hasPermission('Order.PlatformStore.Delete')) return
if (isAdmin.value && ensureMutationCustomerId() == null) return
Modal.confirm({
title: t('order.platformStore.actions.delete'),
content: row.storeNickname,
onOk: async () => {
await deletePlatformStore([row.id])
$baseMessage(t('order.platformStore.message.deleteSuccess'), 'success')
await loadList()
},
})
}
function actionRenderer() {
const parts: string[] = []
if (hasPermission('Order.PlatformStore.Save')) {
parts.push(renderWarehouseActionBtn('edit', t('warehouse.common.edit')))
}
if (hasPermission('Order.PlatformStore.Auth')) {
parts.push(renderWarehouseActionBtn('auth', t('order.platformStore.actions.auth'), 'auth'))
}
if (hasPermission('Order.PlatformStore.Delete')) {
parts.push(renderWarehouseActionBtn('delete', t('warehouse.common.delete')))
}
return wrapWarehouseActions(parts)
}
function handleActionClick(row: any, _i: number, action?: string) {
if (action === 'edit') handleEdit(row)
if (action === 'auth') handleAuth(row)
if (action === 'delete') handleDelete(row)
}
</script>
<template>
<div class="platform-store-container warehouse-list-page">
<div class="warehouse-list-page-card">
<div class="tabs-filter-section">
<a-tabs
v-model:active-key="platformCode"
class="status-tabs"
hide-content
@change="handlePlatformTabChange"
>
<a-tab-pane v-for="tab in platformTabs" :key="tab.value">
<template #title>{{ tab.label }}</template>
</a-tab-pane>
</a-tabs>
<div class="toolbar-left">
<a-space wrap :size="8" class="list-filters">
<a-select
v-if="isAdmin"
v-model="adminCustomerId"
allow-clear
allow-search
:placeholder="t('warehouse.common.selectCustomer')"
:options="customerOptions"
style="width: 220px"
@change="handleSearch"
/>
<a-input
v-model="keyword"
allow-clear
:placeholder="t('order.platformStore.search.nickname')"
style="width: 200px"
@press-enter="handleSearch"
/>
<a-button class="app-btn-search" @click="handleSearch">
{{ t('warehouse.common.search') }}
</a-button>
<a-button v-if="hasPermission('Order.PlatformStore.Save')" class="btn-add app-btn-primary-soft" @click="handleAdd">
{{ t('warehouse.common.add') }}
</a-button>
</a-space>
</div>
</div>
<div class="table-toolbar">
<div class="toolbar-right">
<PaginationPro
:total-count="totalCount"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 30, 50, 100]"
@page-change="handlePageChange"
@page-size-change="handlePageSizeChange"
/>
</div>
</div>
<div ref="tableContainerRef" class="list-wrap">
<HandsontablePro
:columns="hotColumns"
:data="tableRows"
:height="tableHeight"
:loading="listLoading"
:stretch-columns="true"
show-action-column
:action-renderer="actionRenderer"
@action-click="handleActionClick"
/>
</div>
</div>
<ModalPro
:title="modalTitle"
:width="560"
:mask-closable="false"
@register="registerFormModal"
@cancel="closeModal"
>
<div class="store-auth-modal">
<div class="store-auth-section">
<div class="store-auth-section__title">
{{ t('order.platformStore.form.platform') }}
<span class="store-auth-required">*</span>
</div>
<div v-if="isEdit" class="store-auth-platform-readonly">
<a-tag color="arcoblue">{{ platformLabel(formModel.platformCode) }}</a-tag>
</div>
<div v-else class="store-auth-platform-grid">
<button
v-for="p in platformDefs"
:key="p.code"
type="button"
class="store-auth-platform-item"
:class="{ 'is-active': formModel.platformCode === p.code }"
@click="selectPlatform(p.code)"
>
<span class="store-auth-platform-item__name">
{{ p.label }}
</span>
</button>
</div>
</div>
<template v-if="showStoreFields">
<a-divider class="store-auth-divider" />
<a-form
:model="formModel"
layout="horizontal"
:label-col-props="{ span: 6 }"
:wrapper-col-props="{ span: 16 }"
>
<a-form-item :label="t('order.platformStore.form.nickname')" required>
<a-input
v-model="formModel.storeNickname"
:placeholder="t('order.platformStore.form.nicknamePlaceholder')"
allow-clear
/>
</a-form-item>
<a-form-item v-if="formModel.platformCode === 'lazada'" :label="t('order.platformStore.form.siteCountry')">
<a-select
v-model="formModel.siteCountry"
:options="lazadaSiteOptions"
allow-search
:placeholder="t('order.platformStore.form.sitePlaceholder')"
/>
</a-form-item>
<a-form-item v-if="formModel.platformCode === 'temu'" :label="t('order.platformStore.form.authCode')" required>
<a-input
v-model="formModel.authorizationCode"
:placeholder="t('order.platformStore.form.authCodePlaceholder')"
allow-clear
/>
</a-form-item>
<a-form-item v-if="formModel.platformCode === 'temu'" :label="t('order.platformStore.form.siteCountry')">
<a-select v-model="formModel.siteCountry" :options="[{ value: 'US', label: '美国 US' }]" />
</a-form-item>
</a-form>
<a-alert
v-if="formModel.platformCode && !isEdit"
type="info"
:show-icon="true"
class="store-auth-alert"
>
{{ t('order.platformStore.form.authRenewTip') }}
</a-alert>
</template>
<div v-else class="store-auth-empty-hint">
{{ t('order.platformStore.form.selectPlatformHint') }}
</div>
</div>
<template #footer>
<a-space :size="8">
<a-button @click="closeModal">{{ t('warehouse.common.cancel') }}</a-button>
<a-button
type="primary"
:loading="saving"
:disabled="!showStoreFields"
@click="handlePrimaryAction"
>
{{ primaryActionLabel }}
</a-button>
</a-space>
</template>
</ModalPro>
</div>
</template>
<style scoped lang="less">
@import '@/styles/warehouse-list-page.less';
.tabs-filter-section {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
flex-shrink: 0;
padding-right: 16px;
border-bottom: 1px solid var(--color-border-2);
.status-tabs {
flex: 0 1 auto;
width: auto;
max-width: 100%;
padding: 0 0 0 16px;
border-bottom: none;
:deep(.arco-tabs-nav::before) {
display: none;
}
}
.toolbar-left {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 8px 0;
max-width: 100%;
flex: 1;
min-width: 0;
}
}
.table-toolbar {
justify-content: flex-end;
.toolbar-right {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
}
}
.store-auth-modal {
padding: 4px 0 0;
}
.store-auth-section__title {
margin-bottom: 12px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-1);
}
.store-auth-required {
margin-left: 4px;
color: rgb(var(--danger-6));
}
.store-auth-platform-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.store-auth-platform-item {
height: 40px;
padding: 0 12px;
border: 1px solid var(--color-border-2);
border-radius: 4px;
background: var(--color-bg-2);
color: var(--color-text-1);
cursor: pointer;
transition: all 0.2s ease;
&:hover {
border-color: var(--app-primary-color);
color: var(--app-primary-color);
}
&.is-active {
border-color: var(--app-primary-color);
background: var(--color-fill-2);
color: var(--app-primary-color);
font-weight: 500;
}
}
.store-auth-platform-item__name {
font-size: 13px;
}
.store-auth-platform-readonly {
padding: 4px 0;
}
.store-auth-divider {
margin: 16px 0;
}
.store-auth-alert {
margin-top: 4px;
}
.store-auth-empty-hint {
margin-top: 8px;
padding: 24px 12px;
text-align: center;
font-size: 13px;
color: var(--color-text-3);
background: var(--color-fill-1);
border-radius: 4px;
}
</style>