feat: H5 门户切换与充值审核,订单编辑支持仓库件重尺及 COS 资源代理修复
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m4s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m4s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b011a5caee
commit
e5175e6cc1
@ -155,20 +155,19 @@ server {
|
|||||||
location ^~ /marketing/commissionWithdraw { try_files $uri @spa; }
|
location ^~ /marketing/commissionWithdraw { try_files $uri @spa; }
|
||||||
location ^~ /marketing/referralTree { try_files $uri @spa; }
|
location ^~ /marketing/referralTree { try_files $uri @spa; }
|
||||||
|
|
||||||
# 面单 PDF 同源代理:/__cos__/<host>/<path> → https://<host>/<path>
|
# 面单/截图同源代理:/__cos__/<host>/<path> → http://<host>/<path>
|
||||||
# 与 Vite 开发代理一致,避免浏览器直打 cos.* 触发 CORS
|
# cos.ideship.com 自定义域名 https 证书不可用,上游走 http;避免浏览器直打 CORS / 混合内容
|
||||||
location ~ ^/__cos__/([^/]+)(/.*)$ {
|
location ~ ^/__cos__/([^/]+)(/.*)$ {
|
||||||
set $cos_host $1;
|
set $cos_host $1;
|
||||||
set $cos_uri $2;
|
set $cos_uri $2;
|
||||||
resolver 1.1.1.1 8.8.8.8 ipv6=off valid=300s;
|
resolver 1.1.1.1 8.8.8.8 ipv6=off valid=300s;
|
||||||
resolver_timeout 5s;
|
resolver_timeout 5s;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_ssl_server_name on;
|
|
||||||
proxy_set_header Host $cos_host;
|
proxy_set_header Host $cos_host;
|
||||||
proxy_set_header Authorization "";
|
proxy_set_header Authorization "";
|
||||||
proxy_set_header Cookie "";
|
proxy_set_header Cookie "";
|
||||||
proxy_set_header X-Real-IP "";
|
proxy_set_header X-Real-IP "";
|
||||||
proxy_pass https://$cos_host$cos_uri$is_args$args;
|
proxy_pass http://$cos_host$cos_uri$is_args$args;
|
||||||
proxy_connect_timeout 10s;
|
proxy_connect_timeout 10s;
|
||||||
proxy_read_timeout 120s;
|
proxy_read_timeout 120s;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
|
|||||||
@ -72,7 +72,7 @@ export interface CustomerProfileCompleteness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapUpdateBody(id: string | number, data: Partial<CustomerDto>) {
|
function mapUpdateBody(id: string | number, data: Partial<CustomerDto>) {
|
||||||
return {
|
const body: Record<string, unknown> = {
|
||||||
id,
|
id,
|
||||||
customerCode: data.customerCode,
|
customerCode: data.customerCode,
|
||||||
customerName: data.customerName,
|
customerName: data.customerName,
|
||||||
@ -92,6 +92,8 @@ function mapUpdateBody(id: string | number, data: Partial<CustomerDto>) {
|
|||||||
memberLevelId: data.memberLevelId,
|
memberLevelId: data.memberLevelId,
|
||||||
priceTierKey: data.priceTierKey,
|
priceTierKey: data.priceTierKey,
|
||||||
}
|
}
|
||||||
|
// 去掉 undefined,避免局部字段被序列化成 null 后把后端原值冲掉
|
||||||
|
return Object.fromEntries(Object.entries(body).filter(([, v]) => v !== undefined))
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomerServiceAdvisorDto {
|
export interface CustomerServiceAdvisorDto {
|
||||||
|
|||||||
@ -103,12 +103,29 @@ export async function omsPost<T = any>(path: string, body?: Record<string, any>)
|
|||||||
return unwrapData<T>(res.data)
|
return unwrapData<T>(res.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* COS 绝对地址 → 同源 `/__cos__/<host>/...`(开发 Vite / 生产 nginx 反代)。
|
||||||
|
* cos.ideship.com 等自定义域名 https 不可用,直链 http 又会在 https 页面触发混合内容。
|
||||||
|
*/
|
||||||
|
function toCosSameOriginProxyUrl(absoluteUrl: string): string | null {
|
||||||
|
try {
|
||||||
|
const u = new URL(absoluteUrl)
|
||||||
|
const host = u.hostname.toLowerCase()
|
||||||
|
if (!host.startsWith('cos.') && !host.includes('myqcloud.com')) return null
|
||||||
|
return `/__cos__/${u.host}${u.pathname}${u.search}`
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 将 /uploads/... 相对路径转为可访问 URL(同源相对,或 VITE_API_ASSET_ORIGIN 绝对前缀) */
|
/** 将 /uploads/... 相对路径转为可访问 URL(同源相对,或 VITE_API_ASSET_ORIGIN 绝对前缀) */
|
||||||
export function resolveOmsAssetUrl(path?: string | null): string {
|
export function resolveOmsAssetUrl(path?: string | null): string {
|
||||||
if (!path) return ''
|
if (!path) return ''
|
||||||
const value = path.trim()
|
const value = path.trim()
|
||||||
if (!value) return ''
|
if (!value) return ''
|
||||||
if (/^https?:\/\//i.test(value) || value.startsWith('data:') || value.startsWith('blob:')) {
|
if (/^https?:\/\//i.test(value) || value.startsWith('data:') || value.startsWith('blob:')) {
|
||||||
|
const cosProxied = toCosSameOriginProxyUrl(value)
|
||||||
|
if (cosProxied) return cosProxied
|
||||||
// 纠正常见错链:api.nanafa.com/uploads → api.nanafa.com/admin/uploads
|
// 纠正常见错链:api.nanafa.com/uploads → api.nanafa.com/admin/uploads
|
||||||
return value.replace(
|
return value.replace(
|
||||||
/^(https?:\/\/api\.(?:nanafa|ideship)\.com)\/uploads\//i,
|
/^(https?:\/\/api\.(?:nanafa|ideship)\.com)\/uploads\//i,
|
||||||
|
|||||||
@ -1094,6 +1094,8 @@ export default {
|
|||||||
'order.platformStoreOrder.message.batchResult': 'Batch done: {success} succeeded, {fail} failed',
|
'order.platformStoreOrder.message.batchResult': 'Batch done: {success} succeeded, {fail} failed',
|
||||||
'order.platformStoreOrder.message.selectOrders': 'Select orders first',
|
'order.platformStoreOrder.message.selectOrders': 'Select orders first',
|
||||||
'order.platformStoreOrder.message.selectStore': 'Select stores first',
|
'order.platformStoreOrder.message.selectStore': 'Select stores first',
|
||||||
|
'order.platformStoreOrder.message.noStore': 'No authorized stores for this customer. Bind one under Platform Store first.',
|
||||||
|
'order.platformStoreOrder.message.loadStoreFailed': 'Failed to load stores',
|
||||||
'order.platformStoreOrder.message.selectStatus': 'Select status types',
|
'order.platformStoreOrder.message.selectStatus': 'Select status types',
|
||||||
'order.platformStoreOrder.message.syncConfirm': 'Sync status for {count} orders?',
|
'order.platformStoreOrder.message.syncConfirm': 'Sync status for {count} orders?',
|
||||||
'order.platformStoreOrder.message.rtsConfirm': 'Ready-to-ship {count} orders?',
|
'order.platformStoreOrder.message.rtsConfirm': 'Ready-to-ship {count} orders?',
|
||||||
@ -4570,8 +4572,10 @@ export default {
|
|||||||
'user.recharge.record.table.payType': 'Payment Method',
|
'user.recharge.record.table.payType': 'Payment Method',
|
||||||
'user.recharge.record.table.payAmount': 'Recharge Amount',
|
'user.recharge.record.table.payAmount': 'Recharge Amount',
|
||||||
'user.recharge.record.table.payImage': 'Recharge Screenshot',
|
'user.recharge.record.table.payImage': 'Recharge Screenshot',
|
||||||
|
'user.recharge.record.table.viewImage': 'View',
|
||||||
'user.recharge.record.table.payStatus': 'Recharge Status',
|
'user.recharge.record.table.payStatus': 'Recharge Status',
|
||||||
'user.recharge.record.table.remarks': 'Remarks',
|
'user.recharge.record.table.remarks': 'Remarks',
|
||||||
|
'user.recharge.record.message.noPayImage': 'No screenshot available',
|
||||||
'user.recharge.record.payment.alipay': 'Alipay',
|
'user.recharge.record.payment.alipay': 'Alipay',
|
||||||
'user.recharge.record.payment.wechat': 'WeChat',
|
'user.recharge.record.payment.wechat': 'WeChat',
|
||||||
'user.recharge.record.actions.approve': 'Approve',
|
'user.recharge.record.actions.approve': 'Approve',
|
||||||
|
|||||||
@ -1085,6 +1085,8 @@ export default {
|
|||||||
'order.platformStoreOrder.message.batchResult': '批量匹配完成:成功 {success},失败 {fail}',
|
'order.platformStoreOrder.message.batchResult': '批量匹配完成:成功 {success},失败 {fail}',
|
||||||
'order.platformStoreOrder.message.selectOrders': '请选择订单',
|
'order.platformStoreOrder.message.selectOrders': '请选择订单',
|
||||||
'order.platformStoreOrder.message.selectStore': '请选择店铺',
|
'order.platformStoreOrder.message.selectStore': '请选择店铺',
|
||||||
|
'order.platformStoreOrder.message.noStore': '当前客户下暂无已授权店铺,请先在「店铺授权」完成绑定',
|
||||||
|
'order.platformStoreOrder.message.loadStoreFailed': '加载店铺列表失败',
|
||||||
'order.platformStoreOrder.message.selectStatus': '请选择同步类型',
|
'order.platformStoreOrder.message.selectStatus': '请选择同步类型',
|
||||||
'order.platformStoreOrder.message.syncConfirm': '是否手动同步 {count} 个订单的状态?',
|
'order.platformStoreOrder.message.syncConfirm': '是否手动同步 {count} 个订单的状态?',
|
||||||
'order.platformStoreOrder.message.rtsConfirm': '是否对 {count} 个订单执行准备发货(RTS)?',
|
'order.platformStoreOrder.message.rtsConfirm': '是否对 {count} 个订单执行准备发货(RTS)?',
|
||||||
@ -4596,8 +4598,10 @@ export default {
|
|||||||
'user.recharge.record.table.payType': '支付方式',
|
'user.recharge.record.table.payType': '支付方式',
|
||||||
'user.recharge.record.table.payAmount': '充值金额',
|
'user.recharge.record.table.payAmount': '充值金额',
|
||||||
'user.recharge.record.table.payImage': '充值截图',
|
'user.recharge.record.table.payImage': '充值截图',
|
||||||
|
'user.recharge.record.table.viewImage': '查看',
|
||||||
'user.recharge.record.table.payStatus': '充值状态',
|
'user.recharge.record.table.payStatus': '充值状态',
|
||||||
'user.recharge.record.table.remarks': '备注',
|
'user.recharge.record.table.remarks': '备注',
|
||||||
|
'user.recharge.record.message.noPayImage': '暂无充值截图',
|
||||||
'user.recharge.record.payment.alipay': '支付宝',
|
'user.recharge.record.payment.alipay': '支付宝',
|
||||||
'user.recharge.record.payment.wechat': '微信',
|
'user.recharge.record.payment.wechat': '微信',
|
||||||
'user.recharge.record.actions.approve': '通过',
|
'user.recharge.record.actions.approve': '通过',
|
||||||
|
|||||||
@ -152,6 +152,17 @@ const router = createRouter({
|
|||||||
adminPortalOnly: true,
|
adminPortalOnly: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'recharge',
|
||||||
|
name: 'H5AdminRecharge',
|
||||||
|
component: () => import('@/views/h5/admin-center/recharge/index.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
locale: 'h5Admin.recharge.title',
|
||||||
|
h5HideHeader: true,
|
||||||
|
adminPortalOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ':pathMatch(.*)*',
|
path: ':pathMatch(.*)*',
|
||||||
name: 'H5AdminCenterFallback',
|
name: 'H5AdminCenterFallback',
|
||||||
|
|||||||
@ -1,50 +1,82 @@
|
|||||||
export type FeeRowLike = {
|
export type FeeRowLike = {
|
||||||
feeType?: string
|
feeType?: string
|
||||||
|
FeeType?: string
|
||||||
type?: string
|
type?: string
|
||||||
currency?: string
|
currency?: string
|
||||||
amount?: number
|
amount?: number
|
||||||
costItem?: string
|
costItem?: string
|
||||||
|
CostItem?: string
|
||||||
chargingStage?: string
|
chargingStage?: string
|
||||||
|
ChargingStage?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeFeeType(row: FeeRowLike): 'receivable' | 'payable' | '' {
|
export function normalizeFeeType(row: FeeRowLike): 'receivable' | 'payable' | '' {
|
||||||
const raw = String(row?.feeType ?? row?.type ?? '').toLowerCase()
|
const raw = String(row?.feeType ?? row?.FeeType ?? row?.type ?? '').toLowerCase()
|
||||||
if (raw === 'receivable' || raw === 'payable') return raw
|
if (raw === 'receivable' || raw === 'payable') return raw
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
function getChargingStageRank(stage?: string) {
|
function getChargingStageRank(stage?: string, options?: { includeOutbound?: boolean }) {
|
||||||
const s = String(stage ?? '').toLowerCase()
|
const s = String(stage ?? '').toLowerCase()
|
||||||
if (s === 'outbound') return 3
|
const includeOutbound = options?.includeOutbound !== false
|
||||||
|
if (includeOutbound && s === 'outbound') return 3
|
||||||
if (s === 'inbound') return 2
|
if (s === 'inbound') return 2
|
||||||
if (s === 'forecast') return 1
|
if (s === 'forecast') return 1
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function pickCostItem(row: FeeRowLike) {
|
||||||
* 每个费用类型 + 费用项只保留最高计费阶段(出库 > 入库 > 预报),
|
return String(row?.costItem ?? row?.CostItem ?? '').trim() || '_'
|
||||||
* 避免预报与入库明细叠算进合计。
|
}
|
||||||
*/
|
|
||||||
export function filterEffectiveStageFees<T extends FeeRowLike>(rows: T[]): T[] {
|
function pickChargingStage(row: FeeRowLike) {
|
||||||
|
return String(row?.chargingStage ?? row?.ChargingStage ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOutboundStage(row: FeeRowLike) {
|
||||||
|
return pickChargingStage(row).toLowerCase() === 'outbound'
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickStageWinners<T extends FeeRowLike>(
|
||||||
|
rows: T[],
|
||||||
|
options?: { includeOutbound?: boolean },
|
||||||
|
): T[] {
|
||||||
|
const includeOutbound = options?.includeOutbound !== false
|
||||||
const winners = new Map<string, T>()
|
const winners = new Map<string, T>()
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
if (!includeOutbound && isOutboundStage(row)) continue
|
||||||
const feeType = normalizeFeeType(row) || 'unknown'
|
const feeType = normalizeFeeType(row) || 'unknown'
|
||||||
const costItem = String(row?.costItem ?? '').trim() || '_'
|
const key = `${feeType}|${pickCostItem(row)}`
|
||||||
const key = `${feeType}|${costItem}`
|
|
||||||
const current = winners.get(key)
|
const current = winners.get(key)
|
||||||
if (!current) {
|
if (!current) {
|
||||||
winners.set(key, row)
|
winners.set(key, row)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const rowStage = String((row as any)?.chargingStage ?? (row as any)?.ChargingStage ?? '')
|
const rankOpts = { includeOutbound }
|
||||||
const curStage = String((current as any)?.chargingStage ?? (current as any)?.ChargingStage ?? '')
|
if (
|
||||||
if (getChargingStageRank(rowStage) > getChargingStageRank(curStage)) {
|
getChargingStageRank(pickChargingStage(row), rankOpts)
|
||||||
|
> getChargingStageRank(pickChargingStage(current), rankOpts)
|
||||||
|
) {
|
||||||
winners.set(key, row)
|
winners.set(key, row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [...winners.values()]
|
return [...winners.values()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理端 / 内部:同费用项保留最高阶段(出库 > 入库 > 预报)。
|
||||||
|
*/
|
||||||
|
export function filterEffectiveStageFees<T extends FeeRowLike>(rows: T[]): T[] {
|
||||||
|
return pickStageWinners(rows, { includeOutbound: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户侧展示 / 应付:不含出库,同费用项只保留入库 > 预报。
|
||||||
|
*/
|
||||||
|
export function filterCustomerFacingFees<T extends FeeRowLike>(rows: T[]): T[] {
|
||||||
|
return pickStageWinners(rows, { includeOutbound: false })
|
||||||
|
}
|
||||||
|
|
||||||
export function sumFeesByCurrency(list: FeeRowLike[]) {
|
export function sumFeesByCurrency(list: FeeRowLike[]) {
|
||||||
const totals = new Map<string, number>()
|
const totals = new Map<string, number>()
|
||||||
for (const row of list) {
|
for (const row of list) {
|
||||||
|
|||||||
@ -535,11 +535,22 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
|||||||
const leftMenuData = ref<any[]>([]);
|
const leftMenuData = ref<any[]>([]);
|
||||||
const leftExpandedKeys = ref<string[]>(['category-list']);
|
const leftExpandedKeys = ref<string[]>(['category-list']);
|
||||||
|
|
||||||
|
/** 左侧关键字本地过滤(后端 list 暂无 Keyword) */
|
||||||
|
const filteredLeftMenuData = computed(() => {
|
||||||
|
const keyword = String(searchModel.keyword ?? '').trim().toLowerCase();
|
||||||
|
if (!keyword) return leftMenuData.value;
|
||||||
|
return leftMenuData.value.filter((item) =>
|
||||||
|
String(item.name ?? '')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(keyword)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const leftTreeData = computed<CustomLeftMenuTreeNode[]>(() => [
|
const leftTreeData = computed<CustomLeftMenuTreeNode[]>(() => [
|
||||||
{
|
{
|
||||||
key: 'category-list',
|
key: 'category-list',
|
||||||
title: t('channel.area.listTitle'),
|
title: t('channel.area.listTitle'),
|
||||||
children: leftMenuData.value.map((item) => ({
|
children: filteredLeftMenuData.value.map((item) => ({
|
||||||
key: `item-${item.id}`,
|
key: `item-${item.id}`,
|
||||||
title: item.name,
|
title: item.name,
|
||||||
isLeaf: true,
|
isLeaf: true,
|
||||||
@ -882,9 +893,9 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
|||||||
leftLoading.value = true;
|
leftLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const response: any = await postForewarningServiceKpiAreaGetKpiAreas({
|
const response: any = await postForewarningServiceKpiAreaGetKpiAreas({
|
||||||
pageIndex: 1,
|
page: 1,
|
||||||
pageSize: 500,
|
pageSize: 500,
|
||||||
...searchModel,
|
areaType: searchModel.areaType,
|
||||||
});
|
});
|
||||||
leftMenuData.value = filterByAreaType(response?.items ?? response?.data?.items ?? []);
|
leftMenuData.value = filterByAreaType(response?.items ?? response?.data?.items ?? []);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -160,6 +160,14 @@ interface RechargeRecord {
|
|||||||
remarks?: string
|
remarks?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 无截图占位(提交时写 "0");GetPublicUrl("0") 也会返回空串 */
|
||||||
|
function resolvePayImageUrl(raw: unknown): string | undefined {
|
||||||
|
const s = String(raw ?? '').trim()
|
||||||
|
if (!s || s === '0' || /^https?:\/\/[^/]+\/0$/i.test(s)) return undefined
|
||||||
|
const url = resolveOmsAssetUrl(s).trim()
|
||||||
|
return url || undefined
|
||||||
|
}
|
||||||
|
|
||||||
function mapRow(item: any): RechargeRecord {
|
function mapRow(item: any): RechargeRecord {
|
||||||
const pt = Number(item.payType)
|
const pt = Number(item.payType)
|
||||||
// 接口约定:1-微信 2-支付宝
|
// 接口约定:1-微信 2-支付宝
|
||||||
@ -170,10 +178,7 @@ function mapRow(item: any): RechargeRecord {
|
|||||||
payTime: item.rechargeTime ?? '',
|
payTime: item.rechargeTime ?? '',
|
||||||
payType,
|
payType,
|
||||||
payAmount: item.rechargeAmount ?? 0,
|
payAmount: item.rechargeAmount ?? 0,
|
||||||
payImage:
|
payImage: resolvePayImageUrl(item.rechargeScreenshot ?? item.RechargeScreenshot),
|
||||||
item.rechargeScreenshot && String(item.rechargeScreenshot) !== '0'
|
|
||||||
? resolveOmsAssetUrl(item.rechargeScreenshot)
|
|
||||||
: undefined,
|
|
||||||
payStatus: String(item.rechargeStatus ?? ''),
|
payStatus: String(item.rechargeStatus ?? ''),
|
||||||
remarks: item.remark ?? '',
|
remarks: item.remark ?? '',
|
||||||
}
|
}
|
||||||
@ -307,8 +312,8 @@ function customCellRenderer(row: number, col: number, value: any, rowData: any):
|
|||||||
}
|
}
|
||||||
if (colKey === 'payImage') {
|
if (colKey === 'payImage') {
|
||||||
if (!rowData?.payImage) return '--'
|
if (!rowData?.payImage) return '--'
|
||||||
const imageEscaped = String(rowData.payImage).replace(/"/g, '"')
|
// 用行号取图,避免签名 URL 中的 & 写入属性被截断导致预览空白
|
||||||
return `<span data-role="view-pay-image" data-image-url="${imageEscaped}" style="color:var(--app-primary-color);cursor:pointer;">查看</span>`
|
return `<span data-role="view-pay-image" data-row-index="${row}" style="color:var(--app-primary-color);cursor:pointer;">${t('user.recharge.record.table.viewImage')}</span>`
|
||||||
}
|
}
|
||||||
if (colKey === 'payStatus') {
|
if (colKey === 'payStatus') {
|
||||||
const status = rowData?.payStatus
|
const status = rowData?.payStatus
|
||||||
@ -388,8 +393,15 @@ async function handleRejectSubmit() {
|
|||||||
function handleViewPayImageClick(event: MouseEvent) {
|
function handleViewPayImageClick(event: MouseEvent) {
|
||||||
const trigger = (event.target as HTMLElement | null)?.closest?.('[data-role="view-pay-image"]')
|
const trigger = (event.target as HTMLElement | null)?.closest?.('[data-role="view-pay-image"]')
|
||||||
if (!trigger) return
|
if (!trigger) return
|
||||||
const imageUrl = trigger.getAttribute('data-image-url')
|
const rowIndex = Number(trigger.getAttribute('data-row-index'))
|
||||||
if (!imageUrl) return
|
const imageUrl =
|
||||||
|
(Number.isFinite(rowIndex) ? tableRows.value[rowIndex]?.payImage : '') ||
|
||||||
|
trigger.getAttribute('data-image-url') ||
|
||||||
|
''
|
||||||
|
if (!imageUrl) {
|
||||||
|
Message.warning(t('user.recharge.record.message.noPayImage'))
|
||||||
|
return
|
||||||
|
}
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
previewImageUrl.value = imageUrl
|
previewImageUrl.value = imageUrl
|
||||||
|
|||||||
@ -84,6 +84,15 @@
|
|||||||
</a-spin>
|
</a-spin>
|
||||||
|
|
||||||
<footer class="h5-detail__foot">
|
<footer class="h5-detail__foot">
|
||||||
|
<button
|
||||||
|
v-if="canModify"
|
||||||
|
type="button"
|
||||||
|
class="h5-detail__icon-btn"
|
||||||
|
:aria-label="t('h5.orders.actions.edit')"
|
||||||
|
@click="openEdit"
|
||||||
|
>
|
||||||
|
<IconEdit class="h5-detail__icon" />
|
||||||
|
</button>
|
||||||
<button type="button" class="h5-detail__btn is-ghost" @click="close">
|
<button type="button" class="h5-detail__btn is-ghost" @click="close">
|
||||||
{{ t('h5.orders.detail.close') }}
|
{{ t('h5.orders.detail.close') }}
|
||||||
</button>
|
</button>
|
||||||
@ -95,13 +104,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { IconClose, IconClockCircle, IconCommon } from '@arco-design/web-vue/es/icon';
|
import { IconClose, IconClockCircle, IconCommon, IconEdit } from '@arco-design/web-vue/es/icon';
|
||||||
import { getOrderDetail } from '@/api/order';
|
import { getOrderDetail } from '@/api/order';
|
||||||
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
||||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||||
|
import { filterEffectiveStageFees, normalizeFeeType } from '@/utils/feeTotals';
|
||||||
import {
|
import {
|
||||||
getOrderStatusMeta,
|
getOrderStatusMeta,
|
||||||
resolveOrderStatusCode,
|
resolveOrderStatusCode,
|
||||||
|
type OrderStatusCode,
|
||||||
} from '@/constants/orderStatus';
|
} from '@/constants/orderStatus';
|
||||||
import H5EmptyState from '@/views/h5/customer-center/components/H5EmptyState.vue';
|
import H5EmptyState from '@/views/h5/customer-center/components/H5EmptyState.vue';
|
||||||
|
|
||||||
@ -115,8 +126,18 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:visible': [value: boolean];
|
'update:visible': [value: boolean];
|
||||||
|
edit: [orderId: number | string];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
/** 与客户 H5 / 运单管理可改阶段对齐:入库前可编辑 */
|
||||||
|
const H5_ADMIN_MODIFY_STATUSES: OrderStatusCode[] = [
|
||||||
|
'draft',
|
||||||
|
'predeclared',
|
||||||
|
'pendingAllocation',
|
||||||
|
'pendingPayment',
|
||||||
|
'pendingShip',
|
||||||
|
];
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
const tabs = ['overview', 'track', 'fee'] as const;
|
const tabs = ['overview', 'track', 'fee'] as const;
|
||||||
@ -149,6 +170,9 @@ const waybillNo = computed(
|
|||||||
() => pick(detail.value, 'serviceNo', 'ServiceNo', 'waybillNo', 'WaybillNo') || '',
|
() => pick(detail.value, 'serviceNo', 'ServiceNo', 'waybillNo', 'WaybillNo') || '',
|
||||||
);
|
);
|
||||||
const statusCode = computed(() => resolveOrderStatusCode(detail.value));
|
const statusCode = computed(() => resolveOrderStatusCode(detail.value));
|
||||||
|
const canModify = computed(() =>
|
||||||
|
H5_ADMIN_MODIFY_STATUSES.includes(statusCode.value as OrderStatusCode),
|
||||||
|
);
|
||||||
|
|
||||||
const overviewRows = computed(() => {
|
const overviewRows = computed(() => {
|
||||||
const d = detail.value || {};
|
const d = detail.value || {};
|
||||||
@ -253,7 +277,12 @@ const feeList = computed(() => {
|
|||||||
const d = detail.value || {};
|
const d = detail.value || {};
|
||||||
const fees = d.feeList ?? d.FeeList ?? d.orderFees ?? d.fees ?? [];
|
const fees = d.feeList ?? d.FeeList ?? d.orderFees ?? d.fees ?? [];
|
||||||
if (!Array.isArray(fees)) return [];
|
if (!Array.isArray(fees)) return [];
|
||||||
return fees.map((f: any) => ({
|
// 同费用项只保留最高计费阶段;有类型时按应收/应付分别去重
|
||||||
|
const receivable = fees.filter((f: any) => normalizeFeeType(f) === 'receivable');
|
||||||
|
const payable = fees.filter((f: any) => normalizeFeeType(f) === 'payable');
|
||||||
|
const typed = [...receivable, ...payable];
|
||||||
|
const source = typed.length ? typed : fees;
|
||||||
|
return filterEffectiveStageFees(source).map((f: any) => ({
|
||||||
name: getCostItemLabel(f),
|
name: getCostItemLabel(f),
|
||||||
amount: pick(f, 'amount', 'Amount', 'fee') ?? '-',
|
amount: pick(f, 'amount', 'Amount', 'fee') ?? '-',
|
||||||
}));
|
}));
|
||||||
@ -316,6 +345,11 @@ function close() {
|
|||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openEdit() {
|
||||||
|
if (!props.orderId || !canModify.value) return;
|
||||||
|
emit('edit', props.orderId);
|
||||||
|
}
|
||||||
|
|
||||||
watch(activeTab, (tab) => {
|
watch(activeTab, (tab) => {
|
||||||
if (tab === 'track' && props.visible) void loadTrack();
|
if (tab === 'track' && props.visible) void loadTrack();
|
||||||
});
|
});
|
||||||
@ -544,14 +578,43 @@ watch(
|
|||||||
.h5-detail__foot {
|
.h5-detail__foot {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-top: 1px solid #eef0f3;
|
border-top: 1px solid #eef0f3;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h5-detail__icon-btn {
|
||||||
|
flex: 0 0 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
color: #4e5969;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #f7f8fa;
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-detail__icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
font-size: 22px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.h5-detail__btn {
|
.h5-detail__btn {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|||||||
@ -21,25 +21,61 @@ export default {
|
|||||||
'h5Admin.login.privacy': 'Privacy Policy',
|
'h5Admin.login.privacy': 'Privacy Policy',
|
||||||
'h5Admin.login.terms': 'Terms of Service',
|
'h5Admin.login.terms': 'Terms of Service',
|
||||||
'h5Admin.login.and': 'and',
|
'h5Admin.login.and': 'and',
|
||||||
|
'h5.login.switchToAdmin': 'Switch to staff login',
|
||||||
|
'h5.login.switchToCustomer': 'Switch to customer login',
|
||||||
|
|
||||||
'h5Admin.me.title': 'Profile',
|
'h5Admin.me.title': 'Profile',
|
||||||
'h5Admin.me.account': 'Account',
|
'h5Admin.me.account': 'Account',
|
||||||
'h5Admin.me.org': 'Organization',
|
'h5Admin.me.org': 'Organization',
|
||||||
'h5Admin.me.roles': 'Roles',
|
'h5Admin.me.roles': 'Roles',
|
||||||
'h5Admin.me.email': 'Email',
|
'h5Admin.me.email': 'Email',
|
||||||
|
'h5Admin.me.rechargeAudit': 'Recharge review',
|
||||||
'h5Admin.me.logout': 'Sign out',
|
'h5Admin.me.logout': 'Sign out',
|
||||||
'h5Admin.me.logoutConfirm': 'Sign out of this account?',
|
'h5Admin.me.logoutConfirm': 'Sign out of this account?',
|
||||||
'h5Admin.me.emptyRole': 'No roles',
|
'h5Admin.me.emptyRole': 'No roles',
|
||||||
'h5Admin.me.emptyOrg': 'No organization',
|
'h5Admin.me.emptyOrg': 'No organization',
|
||||||
'h5Admin.me.emptyEmail': 'Not set',
|
'h5Admin.me.emptyEmail': 'Not set',
|
||||||
|
|
||||||
|
'h5Admin.recharge.title': 'Recharge review',
|
||||||
|
'h5Admin.recharge.subtitle': 'Approve customer top-ups',
|
||||||
|
'h5Admin.recharge.refresh': 'Refresh',
|
||||||
|
'h5Admin.recharge.search': 'Search',
|
||||||
|
'h5Admin.recharge.searchPh': 'Search serial No.',
|
||||||
|
'h5Admin.recharge.empty': 'No recharge records',
|
||||||
|
'h5Admin.recharge.loadMore': 'Load more',
|
||||||
|
'h5Admin.recharge.loadFailed': 'Failed to load',
|
||||||
|
'h5Admin.recharge.tab.pending': 'Pending',
|
||||||
|
'h5Admin.recharge.tab.all': 'All',
|
||||||
|
'h5Admin.recharge.tab.success': 'Approved',
|
||||||
|
'h5Admin.recharge.tab.failed': 'Rejected',
|
||||||
|
'h5Admin.recharge.customer': 'Customer',
|
||||||
|
'h5Admin.recharge.serialNo': 'Serial No.',
|
||||||
|
'h5Admin.recharge.time': 'Time',
|
||||||
|
'h5Admin.recharge.remark': 'Remark',
|
||||||
|
'h5Admin.recharge.viewImage': 'Screenshot',
|
||||||
|
'h5Admin.recharge.approve': 'Approve',
|
||||||
|
'h5Admin.recharge.reject': 'Reject',
|
||||||
|
'h5Admin.recharge.approveTitle': 'Confirm approval',
|
||||||
|
'h5Admin.recharge.approveConfirm': 'Approve ¥{amount} for {customer}?',
|
||||||
|
'h5Admin.recharge.approveOk': 'Approved',
|
||||||
|
'h5Admin.recharge.approveFail': 'Approval failed',
|
||||||
|
'h5Admin.recharge.rejectTitle': 'Reject recharge',
|
||||||
|
'h5Admin.recharge.rejectConfirm': 'Confirm reject',
|
||||||
|
'h5Admin.recharge.rejectRemarkPh': 'Reason (optional)',
|
||||||
|
'h5Admin.recharge.rejectOk': 'Rejected',
|
||||||
|
'h5Admin.recharge.rejectFail': 'Reject failed',
|
||||||
|
'h5Admin.recharge.pay.alipay': 'Alipay',
|
||||||
|
'h5Admin.recharge.pay.wechat': 'WeChat',
|
||||||
|
|
||||||
'h5Admin.workplace.greeting.morning': 'Good morning',
|
'h5Admin.workplace.greeting.morning': 'Good morning',
|
||||||
'h5Admin.workplace.greeting.afternoon': 'Good afternoon',
|
'h5Admin.workplace.greeting.afternoon': 'Good afternoon',
|
||||||
'h5Admin.workplace.greeting.evening': 'Good evening',
|
'h5Admin.workplace.greeting.evening': 'Good evening',
|
||||||
'h5Admin.workplace.subtitle': 'Clear high-priority queues first',
|
'h5Admin.workplace.subtitle': 'Clear high-priority queues first',
|
||||||
|
'h5Admin.workplace.rechargeAudit': 'Recharge review',
|
||||||
'h5Admin.workplace.opsTitle': 'Today’s queue',
|
'h5Admin.workplace.opsTitle': 'Today’s queue',
|
||||||
'h5Admin.workplace.refresh': 'Refresh',
|
'h5Admin.workplace.refresh': 'Refresh',
|
||||||
'h5Admin.workplace.updated': 'Updated {time}',
|
'h5Admin.workplace.updated': 'Updated {time}',
|
||||||
|
'h5Admin.workplace.pendingRecharge': 'Recharge pending',
|
||||||
'h5Admin.workplace.todayCreated': 'Created today',
|
'h5Admin.workplace.todayCreated': 'Created today',
|
||||||
'h5Admin.workplace.pendingPayment': 'Unpaid',
|
'h5Admin.workplace.pendingPayment': 'Unpaid',
|
||||||
'h5Admin.workplace.pendingShip': 'To ship',
|
'h5Admin.workplace.pendingShip': 'To ship',
|
||||||
|
|||||||
@ -21,25 +21,61 @@ export default {
|
|||||||
'h5Admin.login.privacy': '隐私政策',
|
'h5Admin.login.privacy': '隐私政策',
|
||||||
'h5Admin.login.terms': '服务条款',
|
'h5Admin.login.terms': '服务条款',
|
||||||
'h5Admin.login.and': '与',
|
'h5Admin.login.and': '与',
|
||||||
|
'h5.login.switchToAdmin': '切换至管理登录',
|
||||||
|
'h5.login.switchToCustomer': '切换至客户登录',
|
||||||
|
|
||||||
'h5Admin.me.title': '我的',
|
'h5Admin.me.title': '我的',
|
||||||
'h5Admin.me.account': '账号',
|
'h5Admin.me.account': '账号',
|
||||||
'h5Admin.me.org': '组织',
|
'h5Admin.me.org': '组织',
|
||||||
'h5Admin.me.roles': '角色',
|
'h5Admin.me.roles': '角色',
|
||||||
'h5Admin.me.email': '邮箱',
|
'h5Admin.me.email': '邮箱',
|
||||||
|
'h5Admin.me.rechargeAudit': '充值审核',
|
||||||
'h5Admin.me.logout': '退出登录',
|
'h5Admin.me.logout': '退出登录',
|
||||||
'h5Admin.me.logoutConfirm': '确定退出当前账号?',
|
'h5Admin.me.logoutConfirm': '确定退出当前账号?',
|
||||||
'h5Admin.me.emptyRole': '暂无角色',
|
'h5Admin.me.emptyRole': '暂无角色',
|
||||||
'h5Admin.me.emptyOrg': '未分配组织',
|
'h5Admin.me.emptyOrg': '未分配组织',
|
||||||
'h5Admin.me.emptyEmail': '未填写',
|
'h5Admin.me.emptyEmail': '未填写',
|
||||||
|
|
||||||
|
'h5Admin.recharge.title': '充值审核',
|
||||||
|
'h5Admin.recharge.subtitle': '审核客户充值申请',
|
||||||
|
'h5Admin.recharge.refresh': '刷新',
|
||||||
|
'h5Admin.recharge.search': '搜索',
|
||||||
|
'h5Admin.recharge.searchPh': '搜索流水号',
|
||||||
|
'h5Admin.recharge.empty': '暂无充值记录',
|
||||||
|
'h5Admin.recharge.loadMore': '加载更多',
|
||||||
|
'h5Admin.recharge.loadFailed': '加载失败',
|
||||||
|
'h5Admin.recharge.tab.pending': '待审核',
|
||||||
|
'h5Admin.recharge.tab.all': '全部',
|
||||||
|
'h5Admin.recharge.tab.success': '已通过',
|
||||||
|
'h5Admin.recharge.tab.failed': '已驳回',
|
||||||
|
'h5Admin.recharge.customer': '客户',
|
||||||
|
'h5Admin.recharge.serialNo': '流水号',
|
||||||
|
'h5Admin.recharge.time': '时间',
|
||||||
|
'h5Admin.recharge.remark': '备注',
|
||||||
|
'h5Admin.recharge.viewImage': '查看截图',
|
||||||
|
'h5Admin.recharge.approve': '通过',
|
||||||
|
'h5Admin.recharge.reject': '驳回',
|
||||||
|
'h5Admin.recharge.approveTitle': '确认通过',
|
||||||
|
'h5Admin.recharge.approveConfirm': '确定通过 {customer} 的 ¥{amount} 充值申请?',
|
||||||
|
'h5Admin.recharge.approveOk': '已通过',
|
||||||
|
'h5Admin.recharge.approveFail': '审核失败',
|
||||||
|
'h5Admin.recharge.rejectTitle': '驳回充值',
|
||||||
|
'h5Admin.recharge.rejectConfirm': '确认驳回',
|
||||||
|
'h5Admin.recharge.rejectRemarkPh': '驳回原因(选填)',
|
||||||
|
'h5Admin.recharge.rejectOk': '已驳回',
|
||||||
|
'h5Admin.recharge.rejectFail': '驳回失败',
|
||||||
|
'h5Admin.recharge.pay.alipay': '支付宝',
|
||||||
|
'h5Admin.recharge.pay.wechat': '微信',
|
||||||
|
|
||||||
'h5Admin.workplace.greeting.morning': '早上好',
|
'h5Admin.workplace.greeting.morning': '早上好',
|
||||||
'h5Admin.workplace.greeting.afternoon': '下午好',
|
'h5Admin.workplace.greeting.afternoon': '下午好',
|
||||||
'h5Admin.workplace.greeting.evening': '晚上好',
|
'h5Admin.workplace.greeting.evening': '晚上好',
|
||||||
'h5Admin.workplace.subtitle': '先处理高优先级待办,再看今日概况',
|
'h5Admin.workplace.subtitle': '先处理高优先级待办,再看今日概况',
|
||||||
|
'h5Admin.workplace.rechargeAudit': '充值审核',
|
||||||
'h5Admin.workplace.opsTitle': '今日待办',
|
'h5Admin.workplace.opsTitle': '今日待办',
|
||||||
'h5Admin.workplace.refresh': '刷新',
|
'h5Admin.workplace.refresh': '刷新',
|
||||||
'h5Admin.workplace.updated': '更新于 {time}',
|
'h5Admin.workplace.updated': '更新于 {time}',
|
||||||
|
'h5Admin.workplace.pendingRecharge': '充值待审',
|
||||||
'h5Admin.workplace.todayCreated': '今日创建',
|
'h5Admin.workplace.todayCreated': '今日创建',
|
||||||
'h5Admin.workplace.pendingPayment': '待支付',
|
'h5Admin.workplace.pendingPayment': '待支付',
|
||||||
'h5Admin.workplace.pendingShip': '待出库',
|
'h5Admin.workplace.pendingShip': '待出库',
|
||||||
|
|||||||
@ -73,6 +73,8 @@
|
|||||||
>
|
>
|
||||||
<div class="h5-login-agreement">{{ agreementContent }}</div>
|
<div class="h5-login-agreement">{{ agreementContent }}</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
<H5LoginPortalSwitch target="customer" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -89,6 +91,7 @@ import { useRbacUserStore } from '@/store/modules/rbac-user';
|
|||||||
import { ADMIN_H5_HOME_PATH, isAdminH5Path } from '@/utils/login-portal';
|
import { ADMIN_H5_HOME_PATH, isAdminH5Path } from '@/utils/login-portal';
|
||||||
import logoImg from '@/assets/logo2.png';
|
import logoImg from '@/assets/logo2.png';
|
||||||
import LoginLangToggle from '@/views/login/components/LoginLangToggle.vue';
|
import LoginLangToggle from '@/views/login/components/LoginLangToggle.vue';
|
||||||
|
import H5LoginPortalSwitch from '@/views/h5/components/H5LoginPortalSwitch.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'H5AdminLogin' });
|
defineOptions({ name: 'H5AdminLogin' });
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h5-order-detail-page">
|
<div class="h5-order-detail-page">
|
||||||
<H5AdminOrderDetailSheet
|
<H5AdminOrderDetailSheet
|
||||||
v-model:visible="sheetVisible"
|
:key="detailKey"
|
||||||
|
:visible="sheetVisible && !editVisible"
|
||||||
:order-id="orderId"
|
:order-id="orderId"
|
||||||
:initial-tab="initialTab"
|
:initial-tab="initialTab"
|
||||||
|
@update:visible="onDetailVisible"
|
||||||
|
@edit="openEdit"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<H5PlaceOrderSheet
|
||||||
|
v-if="editVisible"
|
||||||
|
v-model:visible="editVisible"
|
||||||
|
:order-id="editOrderId"
|
||||||
|
variant="admin"
|
||||||
|
@success="onEditSuccess"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import H5AdminOrderDetailSheet from '../components/H5AdminOrderDetailSheet.vue';
|
import H5AdminOrderDetailSheet from '../components/H5AdminOrderDetailSheet.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'H5AdminOrderDetail' });
|
defineOptions({ name: 'H5AdminOrderDetail' });
|
||||||
|
|
||||||
|
const H5PlaceOrderSheet = defineAsyncComponent(
|
||||||
|
() => import('@/views/h5/customer-center/components/H5PlaceOrderSheet.vue'),
|
||||||
|
);
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const sheetVisible = ref(true);
|
const sheetVisible = ref(true);
|
||||||
|
const editVisible = ref(false);
|
||||||
|
const editOrderId = ref<number | string | null>(null);
|
||||||
|
const detailKey = ref(0);
|
||||||
|
|
||||||
const orderId = computed(() => String(route.params.id || ''));
|
const orderId = computed(() => String(route.params.id || ''));
|
||||||
const initialTab = computed(() => {
|
const initialTab = computed(() => {
|
||||||
const tab = String(route.query.tab || '');
|
const tab = String(route.query.tab || '');
|
||||||
@ -34,8 +53,34 @@ function leave() {
|
|||||||
router.replace('/h5/admin/orders');
|
router.replace('/h5/admin/orders');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onDetailVisible(visible: boolean) {
|
||||||
|
if (!visible && !editVisible.value) {
|
||||||
|
sheetVisible.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(id: number | string) {
|
||||||
|
if (id == null || id === '') return;
|
||||||
|
editOrderId.value = id;
|
||||||
|
editVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEditSuccess() {
|
||||||
|
editVisible.value = false;
|
||||||
|
editOrderId.value = null;
|
||||||
|
detailKey.value += 1;
|
||||||
|
sheetVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
watch(sheetVisible, (visible) => {
|
watch(sheetVisible, (visible) => {
|
||||||
if (!visible) leave();
|
if (!visible && !editVisible.value) leave();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(editVisible, (visible) => {
|
||||||
|
if (!visible) {
|
||||||
|
editOrderId.value = null;
|
||||||
|
sheetVisible.value = true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
726
src/views/h5/admin-center/recharge/index.vue
Normal file
726
src/views/h5/admin-center/recharge/index.vue
Normal file
@ -0,0 +1,726 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h5-ar">
|
||||||
|
<header class="h5-ar__hero h5-enter">
|
||||||
|
<div class="h5-ar__hero-text">
|
||||||
|
<h1>{{ t('h5Admin.recharge.title') }}</h1>
|
||||||
|
<p>{{ t('h5Admin.recharge.subtitle') }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="h5-ar__refresh" :disabled="loading" @click="reload">
|
||||||
|
{{ t('h5Admin.recharge.refresh') }}
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="h5-ar__tabs h5-enter">
|
||||||
|
<button
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.key"
|
||||||
|
type="button"
|
||||||
|
class="h5-ar__tab"
|
||||||
|
:class="{ 'is-active': statusFilter === tab.key }"
|
||||||
|
@click="setStatus(tab.key)"
|
||||||
|
>
|
||||||
|
{{ t(tab.labelKey) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h5-ar__search h5-enter">
|
||||||
|
<icon-search class="h5-ar__search-icon" />
|
||||||
|
<input
|
||||||
|
v-model="keyword"
|
||||||
|
class="h5-ar__search-input"
|
||||||
|
type="search"
|
||||||
|
enterkeyhint="search"
|
||||||
|
:placeholder="t('h5Admin.recharge.searchPh')"
|
||||||
|
@keyup.enter="reload"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="keyword"
|
||||||
|
type="button"
|
||||||
|
class="h5-ar__search-clear"
|
||||||
|
aria-label="clear"
|
||||||
|
@click="keyword = ''; reload()"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
<button type="button" class="h5-ar__search-btn" @click="reload">
|
||||||
|
{{ t('h5Admin.recharge.search') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading && !rows.length" class="h5-skel">
|
||||||
|
<div v-for="n in 3" :key="n" class="h5-skel-card" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :loading="loading && rows.length > 0" class="h5-ar__spin">
|
||||||
|
<H5EmptyState
|
||||||
|
v-if="!loading && !rows.length"
|
||||||
|
:icon="IconCommon"
|
||||||
|
:text="t('h5Admin.recharge.empty')"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<article
|
||||||
|
v-for="(row, index) in rows"
|
||||||
|
:key="row.id || row.serialNo"
|
||||||
|
class="h5-ar__card h5-enter"
|
||||||
|
:style="{ animationDelay: `${Math.min(index, 6) * 40}ms` }"
|
||||||
|
>
|
||||||
|
<div class="h5-ar__card-top">
|
||||||
|
<div class="h5-ar__card-main">
|
||||||
|
<strong class="h5-ar__amount">¥{{ formatAmount(row.payAmount) }}</strong>
|
||||||
|
<span class="h5-ar__status" :class="`is-${statusTone(row.payStatus)}`">
|
||||||
|
{{ statusLabel(row.payStatus) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="h5-ar__pay">
|
||||||
|
<img :src="payIcon(row.payType)" alt="" />
|
||||||
|
{{ payLabel(row.payType) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h5-ar__meta">
|
||||||
|
<p>
|
||||||
|
<span>{{ t('h5Admin.recharge.customer') }}</span>
|
||||||
|
{{ row.customerCode || row.customerPhone || row.customerId || '—' }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>{{ t('h5Admin.recharge.serialNo') }}</span>
|
||||||
|
{{ row.serialNo || '—' }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>{{ t('h5Admin.recharge.time') }}</span>
|
||||||
|
{{ formatTime(row.payTime) }}
|
||||||
|
</p>
|
||||||
|
<p v-if="row.remarks">
|
||||||
|
<span>{{ t('h5Admin.recharge.remark') }}</span>
|
||||||
|
{{ row.remarks }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h5-ar__actions">
|
||||||
|
<button
|
||||||
|
v-if="row.payImage"
|
||||||
|
type="button"
|
||||||
|
class="h5-ar__btn is-ghost"
|
||||||
|
@click="previewImage(row.payImage)"
|
||||||
|
>
|
||||||
|
{{ t('h5Admin.recharge.viewImage') }}
|
||||||
|
</button>
|
||||||
|
<template v-if="row.payStatus === '1'">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h5-ar__btn is-danger"
|
||||||
|
:disabled="actingId === row.id"
|
||||||
|
@click="openReject(row)"
|
||||||
|
>
|
||||||
|
{{ t('h5Admin.recharge.reject') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h5-ar__btn is-primary"
|
||||||
|
:disabled="actingId === row.id"
|
||||||
|
@click="confirmApprove(row)"
|
||||||
|
>
|
||||||
|
{{ t('h5Admin.recharge.approve') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<a-button
|
||||||
|
v-if="hasMore"
|
||||||
|
long
|
||||||
|
class="h5-ar__more"
|
||||||
|
:loading="loadingMore"
|
||||||
|
@click="loadMore"
|
||||||
|
>
|
||||||
|
{{ t('h5Admin.recharge.loadMore') }}
|
||||||
|
</a-button>
|
||||||
|
</a-spin>
|
||||||
|
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="imageVisible"
|
||||||
|
:title="t('h5Admin.recharge.viewImage')"
|
||||||
|
:footer="false"
|
||||||
|
unmount-on-close
|
||||||
|
width="92%"
|
||||||
|
>
|
||||||
|
<div class="h5-ar__preview">
|
||||||
|
<img v-if="previewUrl" :src="previewUrl" alt="" />
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="rejectVisible"
|
||||||
|
:title="t('h5Admin.recharge.rejectTitle')"
|
||||||
|
:ok-text="t('h5Admin.recharge.rejectConfirm')"
|
||||||
|
:mask-closable="false"
|
||||||
|
unmount-on-close
|
||||||
|
@before-ok="submitReject"
|
||||||
|
@cancel="rejectVisible = false"
|
||||||
|
>
|
||||||
|
<a-textarea
|
||||||
|
v-model="rejectRemark"
|
||||||
|
:placeholder="t('h5Admin.recharge.rejectRemarkPh')"
|
||||||
|
:auto-size="{ minRows: 3, maxRows: 6 }"
|
||||||
|
/>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
|
import { IconCommon, IconSearch } from '@arco-design/web-vue/es/icon';
|
||||||
|
import {
|
||||||
|
approveRechargeRecord,
|
||||||
|
getRechargeRecordList,
|
||||||
|
rejectRechargeRecord,
|
||||||
|
resolveOmsAssetUrl,
|
||||||
|
} from '@/api/recharge/rechargeRecord';
|
||||||
|
import H5EmptyState from '@/views/h5/customer-center/components/H5EmptyState.vue';
|
||||||
|
import zfbIcon from '@/assets/zfb.png';
|
||||||
|
import wxIcon from '@/assets/wx.png';
|
||||||
|
|
||||||
|
defineOptions({ name: 'H5AdminRecharge' });
|
||||||
|
|
||||||
|
type StatusKey = '1' | 'all' | '2' | '3';
|
||||||
|
|
||||||
|
interface RechargeRow {
|
||||||
|
id?: number | string;
|
||||||
|
serialNo: string;
|
||||||
|
payTime: string;
|
||||||
|
payType: 'zfb' | 'wx' | '';
|
||||||
|
payAmount: number;
|
||||||
|
payImage?: string;
|
||||||
|
payStatus: string;
|
||||||
|
remarks?: string;
|
||||||
|
customerId?: number | string;
|
||||||
|
customerCode?: string;
|
||||||
|
customerPhone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const tabs: Array<{ key: StatusKey; labelKey: string }> = [
|
||||||
|
{ key: '1', labelKey: 'h5Admin.recharge.tab.pending' },
|
||||||
|
{ key: 'all', labelKey: 'h5Admin.recharge.tab.all' },
|
||||||
|
{ key: '2', labelKey: 'h5Admin.recharge.tab.success' },
|
||||||
|
{ key: '3', labelKey: 'h5Admin.recharge.tab.failed' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusFilter = ref<StatusKey>('1');
|
||||||
|
const keyword = ref('');
|
||||||
|
const rows = ref<RechargeRow[]>([]);
|
||||||
|
const page = ref(1);
|
||||||
|
const pageSize = 20;
|
||||||
|
const total = ref(0);
|
||||||
|
const loading = ref(false);
|
||||||
|
const loadingMore = ref(false);
|
||||||
|
const actingId = ref<number | string | null>(null);
|
||||||
|
|
||||||
|
const imageVisible = ref(false);
|
||||||
|
const previewUrl = ref('');
|
||||||
|
const rejectVisible = ref(false);
|
||||||
|
const rejectRemark = ref('');
|
||||||
|
const pendingReject = ref<RechargeRow | null>(null);
|
||||||
|
|
||||||
|
const hasMore = computed(() => rows.value.length < total.value);
|
||||||
|
|
||||||
|
function resolvePayImageUrl(raw: unknown): string | undefined {
|
||||||
|
const s = String(raw ?? '').trim();
|
||||||
|
if (!s || s === '0' || /^https?:\/\/[^/]+\/0$/i.test(s)) return undefined;
|
||||||
|
const url = resolveOmsAssetUrl(s).trim();
|
||||||
|
return url || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapRow(item: any): RechargeRow {
|
||||||
|
const pt = Number(item.payType ?? item.PayType);
|
||||||
|
return {
|
||||||
|
id: item.id ?? item.Id,
|
||||||
|
serialNo: String(item.serialNumber ?? item.SerialNumber ?? ''),
|
||||||
|
payTime: String(item.rechargeTime ?? item.RechargeTime ?? ''),
|
||||||
|
payType: pt === 2 ? 'zfb' : pt === 1 ? 'wx' : '',
|
||||||
|
payAmount: Number(item.rechargeAmount ?? item.RechargeAmount ?? 0) || 0,
|
||||||
|
payImage: resolvePayImageUrl(item.rechargeScreenshot ?? item.RechargeScreenshot),
|
||||||
|
payStatus: String(item.rechargeStatus ?? item.RechargeStatus ?? ''),
|
||||||
|
remarks: String(item.remark ?? item.Remark ?? '') || undefined,
|
||||||
|
customerId: item.customerId ?? item.CustomerId,
|
||||||
|
customerCode: String(item.customerCode ?? item.CustomerCode ?? '') || undefined,
|
||||||
|
customerPhone: String(item.customerPhone ?? item.CustomerPhone ?? '') || undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(n: number) {
|
||||||
|
return Number.isFinite(n) ? n.toFixed(2) : '0.00';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(v?: string) {
|
||||||
|
if (!v) return '—';
|
||||||
|
const d = dayjs(v);
|
||||||
|
return d.isValid() ? d.format('YYYY-MM-DD HH:mm') : String(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLabel(code: string) {
|
||||||
|
if (code === '1') return t('h5Admin.recharge.tab.pending');
|
||||||
|
if (code === '2') return t('h5Admin.recharge.tab.success');
|
||||||
|
if (code === '3') return t('h5Admin.recharge.tab.failed');
|
||||||
|
return code || '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTone(code: string) {
|
||||||
|
if (code === '1') return 'wait';
|
||||||
|
if (code === '2') return 'ok';
|
||||||
|
if (code === '3') return 'fail';
|
||||||
|
return 'mute';
|
||||||
|
}
|
||||||
|
|
||||||
|
function payIcon(type: RechargeRow['payType']) {
|
||||||
|
return type === 'zfb' ? zfbIcon : wxIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
function payLabel(type: RechargeRow['payType']) {
|
||||||
|
if (type === 'zfb') return t('h5Admin.recharge.pay.alipay');
|
||||||
|
if (type === 'wx') return t('h5Admin.recharge.pay.wechat');
|
||||||
|
return '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParams(pageNo: number) {
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
page: pageNo,
|
||||||
|
pageSize,
|
||||||
|
};
|
||||||
|
if (statusFilter.value !== 'all') {
|
||||||
|
params.rechargeStatus = Number(statusFilter.value);
|
||||||
|
}
|
||||||
|
const sn = keyword.value.trim();
|
||||||
|
if (sn) params.serialNumber = sn;
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPage(pageNo: number, append: boolean) {
|
||||||
|
const res = await getRechargeRecordList(buildParams(pageNo));
|
||||||
|
const list = (res.items || []).map(mapRow);
|
||||||
|
total.value = Number(res.total || 0);
|
||||||
|
rows.value = append ? [...rows.value, ...list] : list;
|
||||||
|
page.value = pageNo;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reload() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
await fetchPage(1, false);
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('h5Admin.recharge.loadFailed'));
|
||||||
|
rows.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadMore() {
|
||||||
|
if (!hasMore.value || loadingMore.value) return;
|
||||||
|
loadingMore.value = true;
|
||||||
|
try {
|
||||||
|
await fetchPage(page.value + 1, true);
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('h5Admin.recharge.loadFailed'));
|
||||||
|
} finally {
|
||||||
|
loadingMore.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(key: StatusKey) {
|
||||||
|
if (statusFilter.value === key) return;
|
||||||
|
statusFilter.value = key;
|
||||||
|
void reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
function previewImage(url: string) {
|
||||||
|
previewUrl.value = url;
|
||||||
|
imageVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmApprove(row: RechargeRow) {
|
||||||
|
if (!row.id) return;
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('h5Admin.recharge.approveTitle'),
|
||||||
|
content: t('h5Admin.recharge.approveConfirm', {
|
||||||
|
amount: formatAmount(row.payAmount),
|
||||||
|
customer: row.customerCode || row.customerPhone || row.serialNo || '',
|
||||||
|
}),
|
||||||
|
okText: t('h5Admin.recharge.approve'),
|
||||||
|
onOk: async () => {
|
||||||
|
actingId.value = row.id!;
|
||||||
|
try {
|
||||||
|
await approveRechargeRecord({ id: row.id! });
|
||||||
|
Message.success(t('h5Admin.recharge.approveOk'));
|
||||||
|
await reload();
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('h5Admin.recharge.approveFail'));
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
actingId.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openReject(row: RechargeRow) {
|
||||||
|
pendingReject.value = row;
|
||||||
|
rejectRemark.value = '';
|
||||||
|
rejectVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitReject() {
|
||||||
|
const row = pendingReject.value;
|
||||||
|
if (!row?.id) {
|
||||||
|
rejectVisible.value = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
actingId.value = row.id;
|
||||||
|
try {
|
||||||
|
await rejectRechargeRecord({
|
||||||
|
id: row.id,
|
||||||
|
remark: rejectRemark.value.trim() || undefined,
|
||||||
|
});
|
||||||
|
Message.success(t('h5Admin.recharge.rejectOk'));
|
||||||
|
rejectVisible.value = false;
|
||||||
|
pendingReject.value = null;
|
||||||
|
await reload();
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('h5Admin.recharge.rejectFail'));
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
actingId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
void reload();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.h5-ar {
|
||||||
|
padding: 12px 16px 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__hero {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__hero-text {
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--h5-text, #1d2129);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--h5-text-3, #86909c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__refresh {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--h5-border, #e5e6eb);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--h5-text-2, #4e5969);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__tab {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid var(--h5-border, #e5e6eb);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--h5-text-2, #4e5969);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
border-color: color-mix(in srgb, var(--app-primary-color) 35%, #fff);
|
||||||
|
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 10px 0 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--h5-border, #e5e6eb);
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__search-icon {
|
||||||
|
color: #c9cdd4;
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--h5-text, #1d2129);
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__search-clear {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f2f3f5;
|
||||||
|
color: #86909c;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__search-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--app-primary-color);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__spin {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-skel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-skel-card {
|
||||||
|
height: 140px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(90deg, #f2f3f5 25%, #fafafa 37%, #f2f3f5 63%);
|
||||||
|
background-size: 400% 100%;
|
||||||
|
animation: h5-ar-skel 1.2s ease infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes h5-ar-skel {
|
||||||
|
0% {
|
||||||
|
background-position: 100% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--h5-border-soft, #f2f3f5);
|
||||||
|
box-shadow: var(--h5-shadow, 0 4px 16px rgba(15, 23, 42, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__card-main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__amount {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--h5-text, #1d2129);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 22px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.is-wait {
|
||||||
|
color: #ff7d00;
|
||||||
|
background: #fff7e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-ok {
|
||||||
|
color: #00b42a;
|
||||||
|
background: #e8ffea;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-fail {
|
||||||
|
color: #f53f3f;
|
||||||
|
background: #ffece8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-mute {
|
||||||
|
color: #86909c;
|
||||||
|
background: #f2f3f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__pay {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--h5-text-2, #4e5969);
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--h5-text, #1d2129);
|
||||||
|
line-height: 1.4;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
color: var(--h5-text-3, #86909c);
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__btn {
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-ghost {
|
||||||
|
border: 1px solid var(--h5-border, #e5e6eb);
|
||||||
|
background: #fff;
|
||||||
|
color: var(--h5-text-2, #4e5969);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-primary {
|
||||||
|
border: none;
|
||||||
|
background: var(--app-primary-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-danger {
|
||||||
|
border: 1px solid color-mix(in srgb, #f53f3f 28%, #fff);
|
||||||
|
background: #ffece8;
|
||||||
|
color: #f53f3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__more {
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-ar__preview {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 70vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -26,7 +26,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="h5-wp__ops-card"
|
class="h5-wp__ops-card"
|
||||||
:class="[`is-${item.tone}`, { 'is-alert': item.alert && item.value > 0 }]"
|
:class="[`is-${item.tone}`, { 'is-alert': item.alert && item.value > 0 }]"
|
||||||
@click="goOrders(item.status)"
|
@click="onOpsClick(item)"
|
||||||
>
|
>
|
||||||
<span class="h5-wp__ops-label">{{ t(item.labelKey) }}</span>
|
<span class="h5-wp__ops-label">{{ t(item.labelKey) }}</span>
|
||||||
<strong class="h5-wp__ops-value">{{ formatNum(item.value) }}</strong>
|
<strong class="h5-wp__ops-value">{{ formatNum(item.value) }}</strong>
|
||||||
@ -121,6 +121,7 @@ import {
|
|||||||
type OrderWorkplaceStats,
|
type OrderWorkplaceStats,
|
||||||
type OrderWorkplaceTrendPoint,
|
type OrderWorkplaceTrendPoint,
|
||||||
} from '@/api/workplace';
|
} from '@/api/workplace';
|
||||||
|
import { getRechargeRecordList } from '@/api/recharge/rechargeRecord';
|
||||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||||
|
|
||||||
defineOptions({ name: 'H5AdminWorkplace' });
|
defineOptions({ name: 'H5AdminWorkplace' });
|
||||||
@ -133,6 +134,7 @@ const loading = ref(false);
|
|||||||
const trendLoading = ref(false);
|
const trendLoading = ref(false);
|
||||||
const loadError = ref(false);
|
const loadError = ref(false);
|
||||||
const stats = ref<OrderWorkplaceStats | null>(null);
|
const stats = ref<OrderWorkplaceStats | null>(null);
|
||||||
|
const pendingRechargeCount = ref(0);
|
||||||
const trendPoints = ref<OrderWorkplaceTrendPoint[]>([]);
|
const trendPoints = ref<OrderWorkplaceTrendPoint[]>([]);
|
||||||
const trendDimension = ref<'day' | 'week' | 'month'>('day');
|
const trendDimension = ref<'day' | 'week' | 'month'>('day');
|
||||||
const trendDims = ['day', 'week', 'month'] as const;
|
const trendDims = ['day', 'week', 'month'] as const;
|
||||||
@ -162,9 +164,28 @@ const updatedLabel = computed(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const opsItems = computed(() => {
|
type OpsItem = {
|
||||||
|
key: string;
|
||||||
|
labelKey: string;
|
||||||
|
value: number;
|
||||||
|
status: string;
|
||||||
|
tone: string;
|
||||||
|
alert: boolean;
|
||||||
|
action?: 'orders' | 'recharge';
|
||||||
|
};
|
||||||
|
|
||||||
|
const opsItems = computed<OpsItem[]>(() => {
|
||||||
const s = stats.value;
|
const s = stats.value;
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
key: 'pendingRecharge',
|
||||||
|
labelKey: 'h5Admin.workplace.pendingRecharge',
|
||||||
|
value: Math.round(Number(pendingRechargeCount.value) || 0),
|
||||||
|
status: '',
|
||||||
|
tone: 'warning',
|
||||||
|
alert: true,
|
||||||
|
action: 'recharge',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'problematic',
|
key: 'problematic',
|
||||||
labelKey: 'h5Admin.workplace.problematic',
|
labelKey: 'h5Admin.workplace.problematic',
|
||||||
@ -172,6 +193,7 @@ const opsItems = computed(() => {
|
|||||||
status: 'problematic',
|
status: 'problematic',
|
||||||
tone: 'danger',
|
tone: 'danger',
|
||||||
alert: true,
|
alert: true,
|
||||||
|
action: 'orders',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'claimed',
|
key: 'claimed',
|
||||||
@ -180,6 +202,7 @@ const opsItems = computed(() => {
|
|||||||
status: 'claimed',
|
status: 'claimed',
|
||||||
tone: 'warning',
|
tone: 'warning',
|
||||||
alert: true,
|
alert: true,
|
||||||
|
action: 'orders',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'pendingShip',
|
key: 'pendingShip',
|
||||||
@ -188,6 +211,7 @@ const opsItems = computed(() => {
|
|||||||
status: 'pendingShip',
|
status: 'pendingShip',
|
||||||
tone: 'warning',
|
tone: 'warning',
|
||||||
alert: true,
|
alert: true,
|
||||||
|
action: 'orders',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'pendingPayment',
|
key: 'pendingPayment',
|
||||||
@ -196,6 +220,7 @@ const opsItems = computed(() => {
|
|||||||
status: 'pendingPayment',
|
status: 'pendingPayment',
|
||||||
tone: 'warning',
|
tone: 'warning',
|
||||||
alert: true,
|
alert: true,
|
||||||
|
action: 'orders',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'todayCreated',
|
key: 'todayCreated',
|
||||||
@ -204,6 +229,7 @@ const opsItems = computed(() => {
|
|||||||
status: '',
|
status: '',
|
||||||
tone: 'primary',
|
tone: 'primary',
|
||||||
alert: false,
|
alert: false,
|
||||||
|
action: 'orders',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@ -296,6 +322,31 @@ function goOrders(status: string) {
|
|||||||
go('/h5/admin/orders', { status });
|
go('/h5/admin/orders', { status });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goRecharge() {
|
||||||
|
go('/h5/admin/recharge');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOpsClick(item: OpsItem) {
|
||||||
|
if (item.action === 'recharge') {
|
||||||
|
goRecharge();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goOrders(item.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPendingRechargeCount() {
|
||||||
|
try {
|
||||||
|
const res = await getRechargeRecordList({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 1,
|
||||||
|
rechargeStatus: 1,
|
||||||
|
});
|
||||||
|
pendingRechargeCount.value = Number(res.total || 0);
|
||||||
|
} catch {
|
||||||
|
pendingRechargeCount.value = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resolveTrendCount(dim: 'day' | 'week' | 'month') {
|
function resolveTrendCount(dim: 'day' | 'week' | 'month') {
|
||||||
if (dim === 'week') return 4;
|
if (dim === 'week') return 4;
|
||||||
if (dim === 'month') return 6;
|
if (dim === 'month') return 6;
|
||||||
@ -327,7 +378,11 @@ async function load() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
loadError.value = false;
|
loadError.value = false;
|
||||||
try {
|
try {
|
||||||
const [statsRes] = await Promise.all([getOrderWorkplaceStats(), loadTrend()]);
|
const [statsRes] = await Promise.all([
|
||||||
|
getOrderWorkplaceStats(),
|
||||||
|
loadTrend(),
|
||||||
|
loadPendingRechargeCount(),
|
||||||
|
]);
|
||||||
stats.value = statsRes;
|
stats.value = statsRes;
|
||||||
updatedAt.value = new Date();
|
updatedAt.value = new Date();
|
||||||
} catch {
|
} catch {
|
||||||
@ -385,10 +440,15 @@ onMounted(() => {
|
|||||||
color: var(--h5-text-2, #4e5969);
|
color: var(--h5-text-2, #4e5969);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.55;
|
opacity: 0.55;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-wp__updated,
|
.h5-wp__updated,
|
||||||
|
|||||||
65
src/views/h5/components/H5LoginPortalSwitch.vue
Normal file
65
src/views/h5/components/H5LoginPortalSwitch.vue
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<router-link
|
||||||
|
class="h5-login-portal-switch"
|
||||||
|
:to="to"
|
||||||
|
replace
|
||||||
|
:title="label"
|
||||||
|
:aria-label="label"
|
||||||
|
>
|
||||||
|
<component :is="icon" :size="18" />
|
||||||
|
</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { IconSettings, IconUser } from '@arco-design/web-vue/es/icon';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 当前页面对侧:admin=去管理登录;customer=去客户登录 */
|
||||||
|
target: 'admin' | 'customer';
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const to = computed(() =>
|
||||||
|
props.target === 'admin' ? '/h5/admin/login' : '/h5/login',
|
||||||
|
);
|
||||||
|
|
||||||
|
const icon = computed(() =>
|
||||||
|
props.target === 'admin' ? IconSettings : IconUser,
|
||||||
|
);
|
||||||
|
|
||||||
|
const label = computed(() =>
|
||||||
|
props.target === 'admin'
|
||||||
|
? t('h5.login.switchToAdmin')
|
||||||
|
: t('h5.login.switchToCustomer'),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.h5-login-portal-switch {
|
||||||
|
position: fixed;
|
||||||
|
right: 14px;
|
||||||
|
bottom: calc(14px + env(safe-area-inset-bottom, 0px));
|
||||||
|
z-index: 30;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #c9cdd4;
|
||||||
|
text-decoration: none;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
opacity: 0.55;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
transition: opacity 0.15s ease, color 0.15s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.85;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -149,6 +149,7 @@ import { IconClose, IconClockCircle, IconCommon, IconEdit } from '@arco-design/w
|
|||||||
import { getOrderDetail, postOrderPreDeclare } from '@/api/order';
|
import { getOrderDetail, postOrderPreDeclare } from '@/api/order';
|
||||||
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
||||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||||
|
import { filterCustomerFacingFees, normalizeFeeType } from '@/utils/feeTotals';
|
||||||
import { h5Navigate } from '@/utils/device';
|
import { h5Navigate } from '@/utils/device';
|
||||||
import {
|
import {
|
||||||
getOrderStatusMeta,
|
getOrderStatusMeta,
|
||||||
@ -332,7 +333,10 @@ const feeList = computed(() => {
|
|||||||
const d = detail.value || {};
|
const d = detail.value || {};
|
||||||
const fees = d.feeList ?? d.FeeList ?? d.orderFees ?? d.fees ?? [];
|
const fees = d.feeList ?? d.FeeList ?? d.orderFees ?? d.fees ?? [];
|
||||||
if (!Array.isArray(fees)) return [];
|
if (!Array.isArray(fees)) return [];
|
||||||
return fees.map((f: any) => ({
|
// 客户侧只展示应收;不含出库,同费用项只保留入库>预报
|
||||||
|
const receivable = fees.filter((f: any) => normalizeFeeType(f) === 'receivable');
|
||||||
|
const source = receivable.length ? receivable : fees;
|
||||||
|
return filterCustomerFacingFees(source).map((f: any) => ({
|
||||||
name: getCostItemLabel(f),
|
name: getCostItemLabel(f),
|
||||||
amount: pick(f, 'amount', 'Amount', 'fee') ?? '-',
|
amount: pick(f, 'amount', 'Amount', 'fee') ?? '-',
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -104,6 +104,12 @@ import { Message } from '@arco-design/web-vue';
|
|||||||
import { IconClose, IconSafe } from '@arco-design/web-vue/es/icon';
|
import { IconClose, IconSafe } from '@arco-design/web-vue/es/icon';
|
||||||
import { getOrderDetail, postOrderPay } from '@/api/order';
|
import { getOrderDetail, postOrderPay } from '@/api/order';
|
||||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||||
|
import {
|
||||||
|
filterCustomerFacingFees,
|
||||||
|
formatFeeTotals,
|
||||||
|
normalizeFeeType,
|
||||||
|
sumFeesByCurrency,
|
||||||
|
} from '@/utils/feeTotals';
|
||||||
import H5EmptyState from './H5EmptyState.vue';
|
import H5EmptyState from './H5EmptyState.vue';
|
||||||
|
|
||||||
defineOptions({ name: 'H5OrderPaySheet' });
|
defineOptions({ name: 'H5OrderPaySheet' });
|
||||||
@ -162,7 +168,7 @@ const grandTotalText = computed(() => {
|
|||||||
totals.set(fee.currency, (totals.get(fee.currency) ?? 0) + amount);
|
totals.set(fee.currency, (totals.get(fee.currency) ?? 0) + amount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return formatTotals(totals);
|
return formatFeeTotals(totals);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 单币种时突出显示数字,多币种保留原文 */
|
/** 单币种时突出显示数字,多币种保留原文 */
|
||||||
@ -189,30 +195,6 @@ function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeFeeType(row: any): 'receivable' | 'payable' | '' {
|
|
||||||
const raw = String(row?.type ?? row?.feeType ?? row?.FeeType ?? '').toLowerCase();
|
|
||||||
if (raw === 'receivable' || raw === 'payable') return raw;
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function sumReceivableFees(list: any[]) {
|
|
||||||
const totals = new Map<string, number>();
|
|
||||||
for (const row of list) {
|
|
||||||
const currency = String(row?.currency ?? 'CNY').trim() || 'CNY';
|
|
||||||
const amount = Number(row?.amount);
|
|
||||||
if (!Number.isFinite(amount)) continue;
|
|
||||||
totals.set(currency, (totals.get(currency) ?? 0) + amount);
|
|
||||||
}
|
|
||||||
return totals;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTotals(totals: Map<string, number>) {
|
|
||||||
if (!totals.size) return '--';
|
|
||||||
return [...totals.entries()]
|
|
||||||
.map(([currency, amount]) => `${currency} ${amount.toFixed(2)}`)
|
|
||||||
.join(' + ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCostItemLabel(row: any) {
|
function getCostItemLabel(row: any) {
|
||||||
const fromApi = row?.costItemName ?? row?.CostItemName;
|
const fromApi = row?.costItemName ?? row?.CostItemName;
|
||||||
if (fromApi != null && fromApi !== '') return String(fromApi);
|
if (fromApi != null && fromApi !== '') return String(fromApi);
|
||||||
@ -278,10 +260,12 @@ async function loadPreviews() {
|
|||||||
try {
|
try {
|
||||||
const detail = await getOrderDetail(id);
|
const detail = await getOrderDetail(id);
|
||||||
const feesRaw = detail?.feeList ?? detail?.FeeList ?? detail?.orderFees ?? detail?.fees;
|
const feesRaw = detail?.feeList ?? detail?.FeeList ?? detail?.orderFees ?? detail?.fees;
|
||||||
const receivableFees = (Array.isArray(feesRaw) ? feesRaw : []).filter(
|
const receivableFees = filterCustomerFacingFees(
|
||||||
|
(Array.isArray(feesRaw) ? feesRaw : []).filter(
|
||||||
(row) => normalizeFeeType(row) === 'receivable',
|
(row) => normalizeFeeType(row) === 'receivable',
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const totals = sumReceivableFees(receivableFees);
|
const totals = sumFeesByCurrency(receivableFees);
|
||||||
const totalAmount = [...totals.values()].reduce((sum, val) => sum + val, 0);
|
const totalAmount = [...totals.values()].reduce((sum, val) => sum + val, 0);
|
||||||
const waybillNo =
|
const waybillNo =
|
||||||
pick(detail, 'waybillNo', 'WaybillNo', 'serviceNo', 'ServiceNo') || fallbackWaybill;
|
pick(detail, 'waybillNo', 'WaybillNo', 'serviceNo', 'ServiceNo') || fallbackWaybill;
|
||||||
@ -289,7 +273,7 @@ async function loadPreviews() {
|
|||||||
id,
|
id,
|
||||||
waybillNo: String(waybillNo),
|
waybillNo: String(waybillNo),
|
||||||
fees: mapFeeRows(receivableFees),
|
fees: mapFeeRows(receivableFees),
|
||||||
totalText: formatTotals(totals),
|
totalText: formatFeeTotals(totals),
|
||||||
totalAmount,
|
totalAmount,
|
||||||
error: totalAmount <= 0 ? t('h5.pay.invalidAmount') : undefined,
|
error: totalAmount <= 0 ? t('h5.pay.invalidAmount') : undefined,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -103,6 +103,24 @@
|
|||||||
<p v-if="fieldError('clientNo')" class="h5-po__field-err">{{ fieldError('clientNo') }}</p>
|
<p v-if="fieldError('clientNo')" class="h5-po__field-err">{{ fieldError('clientNo') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isEditMode" class="h5-po__field">
|
||||||
|
<label class="h5-po__label">{{ t('h5.placeOrder.downstreamServiceNo') }}</label>
|
||||||
|
<a-input
|
||||||
|
v-model="form.downstreamServiceNo"
|
||||||
|
allow-clear
|
||||||
|
:placeholder="t('h5.placeOrder.downstreamServiceNoPh')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="canEditTrackNo" class="h5-po__field">
|
||||||
|
<label class="h5-po__label">{{ t('h5.placeOrder.trackNo') }}</label>
|
||||||
|
<a-input
|
||||||
|
v-model="form.trackNo"
|
||||||
|
allow-clear
|
||||||
|
:placeholder="t('h5.placeOrder.trackNoPh')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="h5-po__field">
|
<div class="h5-po__field">
|
||||||
<label class="h5-po__label">{{ t('h5.placeOrder.product') }}</label>
|
<label class="h5-po__label">{{ t('h5.placeOrder.product') }}</label>
|
||||||
<a-select
|
<a-select
|
||||||
@ -154,7 +172,7 @@
|
|||||||
<a-select
|
<a-select
|
||||||
v-model="form.warehouseId"
|
v-model="form.warehouseId"
|
||||||
allow-clear
|
allow-clear
|
||||||
:disabled="isEditMode"
|
allow-search
|
||||||
:error="fieldHasError('warehouseId')"
|
:error="fieldHasError('warehouseId')"
|
||||||
>
|
>
|
||||||
<a-option v-for="opt in warehouseOptions" :key="opt.value" :value="opt.value">
|
<a-option v-for="opt in warehouseOptions" :key="opt.value" :value="opt.value">
|
||||||
@ -167,6 +185,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-if="isEditMode">
|
||||||
|
<div class="h5-po__field">
|
||||||
|
<label class="h5-po__label">{{ t('h5.placeOrder.isCod') }}</label>
|
||||||
|
<a-switch v-model="form.isCod" />
|
||||||
|
</div>
|
||||||
|
<div v-if="form.isCod" class="h5-po__row">
|
||||||
|
<div class="h5-po__field">
|
||||||
|
<label class="h5-po__label">{{ t('h5.placeOrder.codCurrency') }}</label>
|
||||||
|
<a-select v-model="form.codCurrency" allow-clear allow-search>
|
||||||
|
<a-option v-for="opt in currencyOptions" :key="opt.value" :value="opt.value">
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
</div>
|
||||||
|
<div class="h5-po__field">
|
||||||
|
<label class="h5-po__label">{{ t('h5.placeOrder.codSum') }}</label>
|
||||||
|
<a-input
|
||||||
|
v-model="form.codSum"
|
||||||
|
allow-clear
|
||||||
|
:placeholder="t('h5.placeOrder.codSumPh')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="h5-po__field">
|
<div class="h5-po__field">
|
||||||
<label class="h5-po__label">
|
<label class="h5-po__label">
|
||||||
{{ t('h5.placeOrder.weight') }} / {{ t('h5.placeOrder.dimensions') }}
|
{{ t('h5.placeOrder.weight') }} / {{ t('h5.placeOrder.dimensions') }}
|
||||||
@ -175,25 +218,21 @@
|
|||||||
<a-input
|
<a-input
|
||||||
v-model="form.actualWeight"
|
v-model="form.actualWeight"
|
||||||
allow-clear
|
allow-clear
|
||||||
:disabled="isEditMode"
|
|
||||||
:error="fieldHasError('actualWeight')"
|
:error="fieldHasError('actualWeight')"
|
||||||
:placeholder="t('h5.placeOrder.weightPh')"
|
:placeholder="t('h5.placeOrder.weightPh')"
|
||||||
/>
|
/>
|
||||||
<a-input
|
<a-input
|
||||||
v-model="form.length"
|
v-model="form.length"
|
||||||
:disabled="isEditMode"
|
|
||||||
:error="fieldHasError('length')"
|
:error="fieldHasError('length')"
|
||||||
:placeholder="t('h5.placeOrder.length')"
|
:placeholder="t('h5.placeOrder.length')"
|
||||||
/>
|
/>
|
||||||
<a-input
|
<a-input
|
||||||
v-model="form.width"
|
v-model="form.width"
|
||||||
:disabled="isEditMode"
|
|
||||||
:error="fieldHasError('width')"
|
:error="fieldHasError('width')"
|
||||||
:placeholder="t('h5.placeOrder.width')"
|
:placeholder="t('h5.placeOrder.width')"
|
||||||
/>
|
/>
|
||||||
<a-input
|
<a-input
|
||||||
v-model="form.height"
|
v-model="form.height"
|
||||||
:disabled="isEditMode"
|
|
||||||
:error="fieldHasError('height')"
|
:error="fieldHasError('height')"
|
||||||
:placeholder="t('h5.placeOrder.height')"
|
:placeholder="t('h5.placeOrder.height')"
|
||||||
/>
|
/>
|
||||||
@ -1172,6 +1211,7 @@ import {
|
|||||||
postOrderParcelCreate,
|
postOrderParcelCreate,
|
||||||
postOrderReceiverParseAddress,
|
postOrderReceiverParseAddress,
|
||||||
postOrderUpdate,
|
postOrderUpdate,
|
||||||
|
postOrderWeightUpdate,
|
||||||
} from '@/api/order';
|
} from '@/api/order';
|
||||||
import { listAiProviders, listAiSkills, type AiSkillItem } from '@/api/ai';
|
import { listAiProviders, listAiSkills, type AiSkillItem } from '@/api/ai';
|
||||||
import { createSenderAddress, getSenderAddressList } from '@/api/order/senderAddress';
|
import { createSenderAddress, getSenderAddressList } from '@/api/order/senderAddress';
|
||||||
@ -1227,13 +1267,18 @@ defineOptions({ name: 'H5PlaceOrderSheet' });
|
|||||||
|
|
||||||
const PRICE_CHECK_ORDER_DRAFT_KEY = 'oms_price_check_order_draft';
|
const PRICE_CHECK_ORDER_DRAFT_KEY = 'oms_price_check_order_draft';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
/** 查价带入草稿;不传则尝试读本地缓存 */
|
/** 查价带入草稿;不传则尝试读本地缓存 */
|
||||||
draft?: Record<string, any> | null;
|
draft?: Record<string, any> | null;
|
||||||
/** 传入则为编辑模式:加载详情并走 /order/update */
|
/** 传入则为编辑模式:加载详情并走 /order/update */
|
||||||
orderId?: number | string | null;
|
orderId?: number | string | null;
|
||||||
}>();
|
/** admin:管理端编辑(可改跟踪号) */
|
||||||
|
variant?: 'customer' | 'admin';
|
||||||
|
}>(),
|
||||||
|
{ variant: 'customer' },
|
||||||
|
);
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'update:visible': [value: boolean];
|
'update:visible': [value: boolean];
|
||||||
@ -1247,8 +1292,12 @@ const isEditMode = computed(() => {
|
|||||||
const id = Number(props.orderId);
|
const id = Number(props.orderId);
|
||||||
return Number.isFinite(id) && id > 0;
|
return Number.isFinite(id) && id > 0;
|
||||||
});
|
});
|
||||||
|
const isAdminVariant = computed(() => props.variant === 'admin');
|
||||||
|
const canEditTrackNo = computed(() => isEditMode.value && isAdminVariant.value);
|
||||||
/** 详情是否已有发件人(更新接口不能新建 sender) */
|
/** 详情是否已有发件人(更新接口不能新建 sender) */
|
||||||
const editHasSender = ref(false);
|
const editHasSender = ref(false);
|
||||||
|
/** 编辑态件重尺类型(默认预报重) */
|
||||||
|
const editWeightType = ref('forecast');
|
||||||
const countryIdToCode = ref<Record<string, string>>({});
|
const countryIdToCode = ref<Record<string, string>>({});
|
||||||
|
|
||||||
const step = ref(0);
|
const step = ref(0);
|
||||||
@ -1499,7 +1548,11 @@ const form = reactive({
|
|||||||
actualWeight: '',
|
actualWeight: '',
|
||||||
quantity: '1',
|
quantity: '1',
|
||||||
declaredCurrency: 'USD',
|
declaredCurrency: 'USD',
|
||||||
|
downstreamServiceNo: '',
|
||||||
|
trackNo: '',
|
||||||
isCod: false,
|
isCod: false,
|
||||||
|
codCurrency: '',
|
||||||
|
codSum: '',
|
||||||
receiver: {
|
receiver: {
|
||||||
recipientsName: '',
|
recipientsName: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
@ -1884,6 +1937,12 @@ function resetForm() {
|
|||||||
form.actualWeight = '';
|
form.actualWeight = '';
|
||||||
form.quantity = '1';
|
form.quantity = '1';
|
||||||
form.declaredCurrency = 'USD';
|
form.declaredCurrency = 'USD';
|
||||||
|
form.downstreamServiceNo = '';
|
||||||
|
form.trackNo = '';
|
||||||
|
form.isCod = false;
|
||||||
|
form.codCurrency = '';
|
||||||
|
form.codSum = '';
|
||||||
|
editWeightType.value = 'forecast';
|
||||||
declaredTotalInput.value = '';
|
declaredTotalInput.value = '';
|
||||||
declaredTotalInputFocused.value = false;
|
declaredTotalInputFocused.value = false;
|
||||||
selectedAdditionalServices.value = [];
|
selectedAdditionalServices.value = [];
|
||||||
@ -2672,6 +2731,10 @@ function applyOrderDetail(detail: Record<string, any>) {
|
|||||||
{};
|
{};
|
||||||
|
|
||||||
form.clientNo = String(pickDetail(detail, 'clientNo', 'ClientNo') ?? '');
|
form.clientNo = String(pickDetail(detail, 'clientNo', 'ClientNo') ?? '');
|
||||||
|
form.downstreamServiceNo = String(
|
||||||
|
pickDetail(detail, 'downstreamServiceNo', 'DownstreamServiceNo') ?? '',
|
||||||
|
);
|
||||||
|
form.trackNo = String(pickDetail(detail, 'trackNo', 'TrackNo', 'trackingNo') ?? '');
|
||||||
form.productCode = String(pickDetail(detail, 'productCode', 'ProductCode') ?? '');
|
form.productCode = String(pickDetail(detail, 'productCode', 'ProductCode') ?? '');
|
||||||
form.cargoAttribute = normalizeCargoAttributeCode(
|
form.cargoAttribute = normalizeCargoAttributeCode(
|
||||||
pickDetail(detail, 'cargoAttribute', 'CargoAttribute') ?? form.cargoAttribute,
|
pickDetail(detail, 'cargoAttribute', 'CargoAttribute') ?? form.cargoAttribute,
|
||||||
@ -2688,8 +2751,19 @@ function applyOrderDetail(detail: Record<string, any>) {
|
|||||||
pickDetail(detail, 'declaredCurrency', 'DeclaredCurrency') ?? form.declaredCurrency,
|
pickDetail(detail, 'declaredCurrency', 'DeclaredCurrency') ?? form.declaredCurrency,
|
||||||
);
|
);
|
||||||
form.quantity = String(pickDetail(detail, 'quantity', 'Quantity') ?? (form.quantity || '1'));
|
form.quantity = String(pickDetail(detail, 'quantity', 'Quantity') ?? (form.quantity || '1'));
|
||||||
|
{
|
||||||
|
const rawCod = pickDetail(detail, 'isCod', 'IsCod');
|
||||||
|
form.isCod = rawCod === true || rawCod === 1 || rawCod === '1';
|
||||||
|
}
|
||||||
|
form.codCurrency = String(pickDetail(detail, 'codCurrency', 'CodCurrency') ?? '');
|
||||||
|
form.codSum =
|
||||||
|
pickDetail(detail, 'codSum', 'CodSum') != null
|
||||||
|
? String(pickDetail(detail, 'codSum', 'CodSum'))
|
||||||
|
: '';
|
||||||
form.inboundMethod =
|
form.inboundMethod =
|
||||||
String(pickDetail(detail, 'inboundMethod', 'InboundMethod') ?? 'selfShip') || 'selfShip';
|
String(pickDetail(detail, 'inboundMethod', 'InboundMethod') ?? 'selfShip') || 'selfShip';
|
||||||
|
editWeightType.value =
|
||||||
|
String(pickDetail(weight, 'type', 'Type') ?? 'forecast').trim() || 'forecast';
|
||||||
form.length = String(pickDetail(weight, 'length', 'Length') ?? form.length ?? '');
|
form.length = String(pickDetail(weight, 'length', 'Length') ?? form.length ?? '');
|
||||||
form.width = String(pickDetail(weight, 'width', 'Width') ?? form.width ?? '');
|
form.width = String(pickDetail(weight, 'width', 'Width') ?? form.width ?? '');
|
||||||
form.height = String(pickDetail(weight, 'height', 'Height') ?? form.height ?? '');
|
form.height = String(pickDetail(weight, 'height', 'Height') ?? form.height ?? '');
|
||||||
@ -2779,13 +2853,21 @@ function buildUpdatePayload() {
|
|||||||
skuImage: normalizeSkuImageForStorage(row.skuImage),
|
skuImage: normalizeSkuImageForStorage(row.skuImage),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const declaredSum =
|
||||||
|
parseNum(declaredTotalInput.value) ??
|
||||||
|
payloadItems.reduce((sum, row) => sum + (Number(row.amount) || 0), 0);
|
||||||
const body: Record<string, any> = {
|
const body: Record<string, any> = {
|
||||||
id: Number(props.orderId),
|
id: Number(props.orderId),
|
||||||
clientNo: trim(form.clientNo),
|
clientNo: trim(form.clientNo),
|
||||||
|
downstreamServiceNo: trim(form.downstreamServiceNo),
|
||||||
cargoAttribute: normalizeCargoAttributeCode(form.cargoAttribute),
|
cargoAttribute: normalizeCargoAttributeCode(form.cargoAttribute),
|
||||||
quantity: trim(form.quantity) || '1',
|
warehouseId: parseNum(form.warehouseId) ?? 0,
|
||||||
|
quantity: parseNum(form.quantity) ?? 1,
|
||||||
declaredCurrency: trim(form.declaredCurrency),
|
declaredCurrency: trim(form.declaredCurrency),
|
||||||
isCod: false,
|
declaredSum: declaredSum ?? 0,
|
||||||
|
isCod: Boolean(form.isCod),
|
||||||
|
codCurrency: trim(form.codCurrency),
|
||||||
|
codSum: parseNum(form.codSum),
|
||||||
receiver: {
|
receiver: {
|
||||||
recipientsName: trim(form.receiver.recipientsName),
|
recipientsName: trim(form.receiver.recipientsName),
|
||||||
companyName: trim(form.receiver.companyName),
|
companyName: trim(form.receiver.companyName),
|
||||||
@ -2809,6 +2891,9 @@ function buildUpdatePayload() {
|
|||||||
},
|
},
|
||||||
items: payloadItems,
|
items: payloadItems,
|
||||||
};
|
};
|
||||||
|
if (canEditTrackNo.value) {
|
||||||
|
body.trackNo = trim(form.trackNo);
|
||||||
|
}
|
||||||
if (editHasSender.value || trim(form.sender.senderName)) {
|
if (editHasSender.value || trim(form.sender.senderName)) {
|
||||||
body.sender = {
|
body.sender = {
|
||||||
senderName: trim(form.sender.senderName),
|
senderName: trim(form.sender.senderName),
|
||||||
@ -2920,6 +3005,25 @@ async function submit() {
|
|||||||
try {
|
try {
|
||||||
if (isEditMode.value) {
|
if (isEditMode.value) {
|
||||||
await postOrderUpdate(buildUpdatePayload());
|
await postOrderUpdate(buildUpdatePayload());
|
||||||
|
try {
|
||||||
|
await postOrderWeightUpdate({
|
||||||
|
id: Number(props.orderId),
|
||||||
|
weights: [
|
||||||
|
{
|
||||||
|
type: editWeightType.value || 'forecast',
|
||||||
|
length: parseNum(form.length) ?? undefined,
|
||||||
|
width: parseNum(form.width) ?? undefined,
|
||||||
|
height: parseNum(form.height) ?? undefined,
|
||||||
|
actualWeight: parseNum(form.actualWeight) ?? undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
} catch (weightErr: any) {
|
||||||
|
Message.warning(weightErr?.message || t('h5.placeOrder.editWeightSaveFailed'));
|
||||||
|
emit('success');
|
||||||
|
emit('update:visible', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Message.success(t('h5.placeOrder.editSaved'));
|
Message.success(t('h5.placeOrder.editSaved'));
|
||||||
emit('success');
|
emit('success');
|
||||||
emit('update:visible', false);
|
emit('update:visible', false);
|
||||||
|
|||||||
@ -295,6 +295,8 @@ export default {
|
|||||||
'h5.login.failed': 'Sign-in failed. Check your account and password.',
|
'h5.login.failed': 'Sign-in failed. Check your account and password.',
|
||||||
'h5.login.newUserHint': 'No account?',
|
'h5.login.newUserHint': 'No account?',
|
||||||
'h5.login.goRegister': 'Register with WeChat',
|
'h5.login.goRegister': 'Register with WeChat',
|
||||||
|
'h5.login.switchToAdmin': 'Switch to staff login',
|
||||||
|
'h5.login.switchToCustomer': 'Switch to customer login',
|
||||||
|
|
||||||
'h5.recharge.title': 'Recharge',
|
'h5.recharge.title': 'Recharge',
|
||||||
'h5.recharge.amount': 'Amount',
|
'h5.recharge.amount': 'Amount',
|
||||||
@ -348,6 +350,15 @@ export default {
|
|||||||
'h5.placeOrder.handoffFilled': 'Filled: {items}',
|
'h5.placeOrder.handoffFilled': 'Filled: {items}',
|
||||||
'h5.placeOrder.clientNo': 'Customer order No.',
|
'h5.placeOrder.clientNo': 'Customer order No.',
|
||||||
'h5.placeOrder.clientNoPh': 'Auto-generated or enter your own',
|
'h5.placeOrder.clientNoPh': 'Auto-generated or enter your own',
|
||||||
|
'h5.placeOrder.downstreamServiceNo': 'Carrier tracking No.',
|
||||||
|
'h5.placeOrder.downstreamServiceNoPh': 'Optional',
|
||||||
|
'h5.placeOrder.trackNo': 'Tracking No.',
|
||||||
|
'h5.placeOrder.trackNoPh': 'Optional',
|
||||||
|
'h5.placeOrder.isCod': 'Cash on delivery (COD)',
|
||||||
|
'h5.placeOrder.codCurrency': 'COD currency',
|
||||||
|
'h5.placeOrder.codSum': 'COD amount',
|
||||||
|
'h5.placeOrder.codSumPh': 'Enter amount',
|
||||||
|
'h5.placeOrder.editWeightSaveFailed': 'Order saved, but weight update failed',
|
||||||
'h5.placeOrder.product': 'Shipping method',
|
'h5.placeOrder.product': 'Shipping method',
|
||||||
'h5.placeOrder.productPh': 'Select product',
|
'h5.placeOrder.productPh': 'Select product',
|
||||||
'h5.placeOrder.additionalServices': 'Additional services',
|
'h5.placeOrder.additionalServices': 'Additional services',
|
||||||
|
|||||||
@ -294,6 +294,8 @@ export default {
|
|||||||
'h5.login.failed': '登录失败,请检查账号密码',
|
'h5.login.failed': '登录失败,请检查账号密码',
|
||||||
'h5.login.newUserHint': '没有账号?',
|
'h5.login.newUserHint': '没有账号?',
|
||||||
'h5.login.goRegister': '微信一键注册',
|
'h5.login.goRegister': '微信一键注册',
|
||||||
|
'h5.login.switchToAdmin': '切换至管理登录',
|
||||||
|
'h5.login.switchToCustomer': '切换至客户登录',
|
||||||
|
|
||||||
'h5.recharge.title': '账户充值',
|
'h5.recharge.title': '账户充值',
|
||||||
'h5.recharge.amount': '充值金额',
|
'h5.recharge.amount': '充值金额',
|
||||||
@ -347,6 +349,15 @@ export default {
|
|||||||
'h5.placeOrder.handoffFilled': '已带入:{items}',
|
'h5.placeOrder.handoffFilled': '已带入:{items}',
|
||||||
'h5.placeOrder.clientNo': '客户单号',
|
'h5.placeOrder.clientNo': '客户单号',
|
||||||
'h5.placeOrder.clientNoPh': '可自动生成,也可自行填写',
|
'h5.placeOrder.clientNoPh': '可自动生成,也可自行填写',
|
||||||
|
'h5.placeOrder.downstreamServiceNo': '服务商单号',
|
||||||
|
'h5.placeOrder.downstreamServiceNoPh': '可选填',
|
||||||
|
'h5.placeOrder.trackNo': '跟踪号',
|
||||||
|
'h5.placeOrder.trackNoPh': '可选填',
|
||||||
|
'h5.placeOrder.isCod': '货到付款 COD',
|
||||||
|
'h5.placeOrder.codCurrency': 'COD 币种',
|
||||||
|
'h5.placeOrder.codSum': 'COD 金额',
|
||||||
|
'h5.placeOrder.codSumPh': '请输入金额',
|
||||||
|
'h5.placeOrder.editWeightSaveFailed': '基本信息已保存,件重尺保存失败',
|
||||||
'h5.placeOrder.product': '运输方式',
|
'h5.placeOrder.product': '运输方式',
|
||||||
'h5.placeOrder.productPh': '选择销售产品',
|
'h5.placeOrder.productPh': '选择销售产品',
|
||||||
'h5.placeOrder.additionalServices': '附加服务',
|
'h5.placeOrder.additionalServices': '附加服务',
|
||||||
|
|||||||
@ -94,13 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<router-link
|
<H5LoginPortalSwitch target="admin" />
|
||||||
class="h5-login-admin-entry"
|
|
||||||
to="/h5/admin/login"
|
|
||||||
replace
|
|
||||||
>
|
|
||||||
admin
|
|
||||||
</router-link>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<a-modal
|
<a-modal
|
||||||
@ -130,6 +124,7 @@ import { isWeChatBrowser } from '@/utils/device';
|
|||||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||||
import logoImg from '@/assets/logo2.png';
|
import logoImg from '@/assets/logo2.png';
|
||||||
import LoginLangToggle from '@/views/login/components/LoginLangToggle.vue';
|
import LoginLangToggle from '@/views/login/components/LoginLangToggle.vue';
|
||||||
|
import H5LoginPortalSwitch from '@/views/h5/components/H5LoginPortalSwitch.vue';
|
||||||
import WechatOAuthLoginButton from '../components/WechatOAuthLoginButton.vue';
|
import WechatOAuthLoginButton from '../components/WechatOAuthLoginButton.vue';
|
||||||
import { wxOAuthLogin } from '@/api/wechat';
|
import { wxOAuthLogin } from '@/api/wechat';
|
||||||
import {
|
import {
|
||||||
@ -624,30 +619,6 @@ onMounted(async () => {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-login-admin-entry {
|
|
||||||
position: fixed;
|
|
||||||
right: 16px;
|
|
||||||
bottom: calc(16px + env(safe-area-inset-bottom, 0px));
|
|
||||||
z-index: 30;
|
|
||||||
padding: 6px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
color: #86909c;
|
|
||||||
text-decoration: none;
|
|
||||||
background: rgba(255, 255, 255, 0.72);
|
|
||||||
border: 1px solid rgba(229, 230, 235, 0.9);
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
-webkit-backdrop-filter: blur(8px);
|
|
||||||
-webkit-tap-highlight-color: transparent;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
color: var(--app-primary-color);
|
|
||||||
transform: scale(0.96);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes h5-login-rise {
|
@keyframes h5-login-rise {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@ -143,7 +143,24 @@
|
|||||||
<div class="detail-field-value">
|
<div class="detail-field-value">
|
||||||
<template v-if="basicEditing && isOrderFieldEditable(fieldKey)">
|
<template v-if="basicEditing && isOrderFieldEditable(fieldKey)">
|
||||||
<a-select
|
<a-select
|
||||||
v-if="getOrderEditMeta(fieldKey)?.inputType === 'select'"
|
v-if="fieldKey === 'warehouse'"
|
||||||
|
v-model="basicEditForm.warehouseId"
|
||||||
|
allow-clear
|
||||||
|
allow-search
|
||||||
|
size="small"
|
||||||
|
:loading="warehouseOptionsLoading"
|
||||||
|
:placeholder="$t('order.myOrders.drawer.warehouse')"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="opt in warehouseOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
<a-select
|
||||||
|
v-else-if="getOrderEditMeta(fieldKey)?.inputType === 'select'"
|
||||||
v-model="basicEditForm[fieldKey]"
|
v-model="basicEditForm[fieldKey]"
|
||||||
allow-clear
|
allow-clear
|
||||||
size="small"
|
size="small"
|
||||||
@ -703,6 +720,7 @@ import {
|
|||||||
type OrderOperationLogItem,
|
type OrderOperationLogItem,
|
||||||
} from '@/api/order';
|
} from '@/api/order';
|
||||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||||
|
import { getWarehouseList } from '@/api/system/warehouse';
|
||||||
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
||||||
import { getBooleanTagStyle } from '@/utils/statusTag';
|
import { getBooleanTagStyle } from '@/utils/statusTag';
|
||||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||||
@ -850,6 +868,7 @@ interface FormFieldDef {
|
|||||||
const orderEditFieldDefs: FormFieldDef[] = [
|
const orderEditFieldDefs: FormFieldDef[] = [
|
||||||
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
||||||
{ key: 'downstreamServiceNo', labelKey: 'order.myOrders.drawer.downstreamServiceNo' },
|
{ key: 'downstreamServiceNo', labelKey: 'order.myOrders.drawer.downstreamServiceNo' },
|
||||||
|
{ key: 'warehouse', labelKey: 'order.myOrders.drawer.warehouse', inputType: 'select' },
|
||||||
{
|
{
|
||||||
key: 'cargoAttribute',
|
key: 'cargoAttribute',
|
||||||
labelKey: 'order.myOrders.drawer.cargoAttribute',
|
labelKey: 'order.myOrders.drawer.cargoAttribute',
|
||||||
@ -903,6 +922,7 @@ function getRecipientFormKey(key: string) {
|
|||||||
const basicEditForm = reactive({
|
const basicEditForm = reactive({
|
||||||
clientNo: '',
|
clientNo: '',
|
||||||
downstreamServiceNo: '',
|
downstreamServiceNo: '',
|
||||||
|
warehouseId: undefined as number | undefined,
|
||||||
cargoAttribute: 'general',
|
cargoAttribute: 'general',
|
||||||
isCod: false,
|
isCod: false,
|
||||||
codSum: undefined as number | undefined,
|
codSum: undefined as number | undefined,
|
||||||
@ -913,6 +933,40 @@ const basicEditForm = reactive({
|
|||||||
|
|
||||||
const currencyOptions = ref<Array<{ value: string; label: string }>>([]);
|
const currencyOptions = ref<Array<{ value: string; label: string }>>([]);
|
||||||
const currencyOptionsLoading = ref(false);
|
const currencyOptionsLoading = ref(false);
|
||||||
|
const warehouseOptions = ref<Array<{ value: number; label: string }>>([]);
|
||||||
|
const warehouseOptionsLoading = ref(false);
|
||||||
|
|
||||||
|
async function ensureWarehouseOptions(preferred?: {
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
}) {
|
||||||
|
warehouseOptionsLoading.value = true;
|
||||||
|
try {
|
||||||
|
if (warehouseOptions.value.length === 0) {
|
||||||
|
const { items } = await getWarehouseList({ page: 1, pageSize: 200 });
|
||||||
|
warehouseOptions.value = (items || [])
|
||||||
|
.map((row: any) => {
|
||||||
|
const id = Number(row?.id ?? row?.Id);
|
||||||
|
if (!Number.isFinite(id) || id <= 0) return null;
|
||||||
|
const name = String(row?.warehouseName ?? row?.WarehouseName ?? '').trim();
|
||||||
|
return { value: id, label: name || `#${id}` };
|
||||||
|
})
|
||||||
|
.filter((item): item is { value: number; label: string } => !!item);
|
||||||
|
}
|
||||||
|
const id = preferred?.id != null && preferred.id > 0 ? preferred.id : undefined;
|
||||||
|
const name = String(preferred?.name ?? '').trim();
|
||||||
|
if (id && !warehouseOptions.value.some((opt) => opt.value === id)) {
|
||||||
|
warehouseOptions.value = [
|
||||||
|
...warehouseOptions.value,
|
||||||
|
{ value: id, label: name || `#${id}` },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (warehouseOptions.value.length === 0) warehouseOptions.value = [];
|
||||||
|
} finally {
|
||||||
|
warehouseOptionsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureCurrencyOptions(preferredCode?: string) {
|
async function ensureCurrencyOptions(preferredCode?: string) {
|
||||||
currencyOptionsLoading.value = true;
|
currencyOptionsLoading.value = true;
|
||||||
@ -1904,6 +1958,10 @@ function fillBasicEditForm() {
|
|||||||
if (!o?.id) return;
|
if (!o?.id) return;
|
||||||
basicEditForm.clientNo = String(o.clientNo ?? o.customerOrderNo ?? '');
|
basicEditForm.clientNo = String(o.clientNo ?? o.customerOrderNo ?? '');
|
||||||
basicEditForm.downstreamServiceNo = String(o.downstreamServiceNo ?? o.DownstreamServiceNo ?? '');
|
basicEditForm.downstreamServiceNo = String(o.downstreamServiceNo ?? o.DownstreamServiceNo ?? '');
|
||||||
|
const warehouseIdRaw = o.warehouseId ?? o.WarehouseId;
|
||||||
|
const warehouseIdNum = warehouseIdRaw != null ? Number(warehouseIdRaw) : NaN;
|
||||||
|
basicEditForm.warehouseId =
|
||||||
|
Number.isFinite(warehouseIdNum) && warehouseIdNum > 0 ? warehouseIdNum : undefined;
|
||||||
basicEditForm.cargoAttribute = String(o.cargoAttribute ?? 'general');
|
basicEditForm.cargoAttribute = String(o.cargoAttribute ?? 'general');
|
||||||
basicEditForm.isCod = !!(o.isCod ?? o.IsCod);
|
basicEditForm.isCod = !!(o.isCod ?? o.IsCod);
|
||||||
basicEditForm.codSum = o.codSum != null ? Number(o.codSum) : undefined;
|
basicEditForm.codSum = o.codSum != null ? Number(o.codSum) : undefined;
|
||||||
@ -1973,6 +2031,10 @@ function startBasicEdit() {
|
|||||||
}
|
}
|
||||||
fillBasicEditForm();
|
fillBasicEditForm();
|
||||||
void ensureCurrencyOptions(basicEditForm.declaredCurrency);
|
void ensureCurrencyOptions(basicEditForm.declaredCurrency);
|
||||||
|
void ensureWarehouseOptions({
|
||||||
|
id: basicEditForm.warehouseId,
|
||||||
|
name: String(detail.value?.warehouse ?? detail.value?.warehouseName ?? ''),
|
||||||
|
});
|
||||||
basicEditing.value = true;
|
basicEditing.value = true;
|
||||||
void focusFirstEditableField('.order-detail-drawer .basic-info');
|
void focusFirstEditableField('.order-detail-drawer .basic-info');
|
||||||
}
|
}
|
||||||
@ -2008,6 +2070,7 @@ async function handleBasicEditSave() {
|
|||||||
id,
|
id,
|
||||||
clientNo: basicEditForm.clientNo,
|
clientNo: basicEditForm.clientNo,
|
||||||
downstreamServiceNo: basicEditForm.downstreamServiceNo,
|
downstreamServiceNo: basicEditForm.downstreamServiceNo,
|
||||||
|
warehouseId: basicEditForm.warehouseId ?? 0,
|
||||||
cargoAttribute: basicEditForm.cargoAttribute,
|
cargoAttribute: basicEditForm.cargoAttribute,
|
||||||
isCod: basicEditForm.isCod,
|
isCod: basicEditForm.isCod,
|
||||||
codSum: basicEditForm.codSum,
|
codSum: basicEditForm.codSum,
|
||||||
|
|||||||
@ -59,10 +59,8 @@ const modalVisible = computed({
|
|||||||
set: (v: boolean) => emit('update:visible', v),
|
set: (v: boolean) => emit('update:visible', v),
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
async function loadStoreOptions() {
|
||||||
() => props.visible,
|
storeIds.value = []
|
||||||
async (open) => {
|
|
||||||
if (!open) return
|
|
||||||
storeLoading.value = true
|
storeLoading.value = true
|
||||||
try {
|
try {
|
||||||
const { items } = await getPlatformStoreList({
|
const { items } = await getPlatformStoreList({
|
||||||
@ -70,18 +68,32 @@ watch(
|
|||||||
pageSize: 200,
|
pageSize: 200,
|
||||||
customerId: props.customerId,
|
customerId: props.customerId,
|
||||||
})
|
})
|
||||||
storeOptions.value = (items || []).map((raw: Record<string, unknown>) => {
|
storeOptions.value = (items || [])
|
||||||
|
.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') ?? '')
|
||||||
return { label: platform ? `${name} (${platform})` : name, value: id }
|
return { label: platform ? `${name} (${platform})` : name, value: id }
|
||||||
}).filter((x) => x.value > 0)
|
})
|
||||||
} catch {
|
.filter((x) => x.value > 0)
|
||||||
|
if (!storeOptions.value.length) {
|
||||||
|
$baseMessage(t('order.platformStoreOrder.message.noStore'), 'warning')
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
storeOptions.value = []
|
storeOptions.value = []
|
||||||
|
$baseMessage(e?.message || t('order.platformStoreOrder.message.loadStoreFailed'), 'error')
|
||||||
} finally {
|
} finally {
|
||||||
storeLoading.value = false
|
storeLoading.value = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** v-if 挂载时 visible 已是 true,必须 immediate,否则不会请求店铺列表 */
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(open) => {
|
||||||
|
if (open) void loadStoreOptions()
|
||||||
},
|
},
|
||||||
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
async function handleSubmit(): Promise<boolean> {
|
async function handleSubmit(): Promise<boolean> {
|
||||||
|
|||||||
@ -134,7 +134,24 @@
|
|||||||
<div class="detail-field-value">
|
<div class="detail-field-value">
|
||||||
<template v-if="basicEditing && isOrderFieldEditable(fieldKey)">
|
<template v-if="basicEditing && isOrderFieldEditable(fieldKey)">
|
||||||
<a-select
|
<a-select
|
||||||
v-if="getOrderEditMeta(fieldKey)?.inputType === 'select'"
|
v-if="fieldKey === 'warehouse'"
|
||||||
|
v-model="basicEditForm.warehouseId"
|
||||||
|
allow-clear
|
||||||
|
allow-search
|
||||||
|
size="small"
|
||||||
|
:loading="warehouseOptionsLoading"
|
||||||
|
:placeholder="$t('order.myOrders.drawer.warehouse')"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="opt in warehouseOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
|
<a-select
|
||||||
|
v-else-if="getOrderEditMeta(fieldKey)?.inputType === 'select'"
|
||||||
v-model="basicEditForm[fieldKey]"
|
v-model="basicEditForm[fieldKey]"
|
||||||
allow-clear
|
allow-clear
|
||||||
size="small"
|
size="small"
|
||||||
@ -980,6 +997,7 @@ import {
|
|||||||
} from '@/api/order';
|
} from '@/api/order';
|
||||||
import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesProduct';
|
import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesProduct';
|
||||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||||
|
import { getWarehouseList } from '@/api/system/warehouse';
|
||||||
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
||||||
import { getBooleanTagStyle } from '@/utils/statusTag';
|
import { getBooleanTagStyle } from '@/utils/statusTag';
|
||||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||||
@ -1109,6 +1127,7 @@ const orderEditFieldDefs: FormFieldDef[] = [
|
|||||||
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
||||||
{ key: 'trackNo', labelKey: 'order.myOrders.drawer.trackNo' },
|
{ key: 'trackNo', labelKey: 'order.myOrders.drawer.trackNo' },
|
||||||
{ key: 'downstreamServiceNo', labelKey: 'order.myOrders.drawer.downstreamServiceNo' },
|
{ key: 'downstreamServiceNo', labelKey: 'order.myOrders.drawer.downstreamServiceNo' },
|
||||||
|
{ key: 'warehouse', labelKey: 'order.myOrders.drawer.warehouse', inputType: 'select' },
|
||||||
{
|
{
|
||||||
key: 'cargoAttribute',
|
key: 'cargoAttribute',
|
||||||
labelKey: 'order.myOrders.drawer.cargoAttribute',
|
labelKey: 'order.myOrders.drawer.cargoAttribute',
|
||||||
@ -1160,6 +1179,7 @@ const basicEditForm = reactive({
|
|||||||
clientNo: '',
|
clientNo: '',
|
||||||
trackNo: '',
|
trackNo: '',
|
||||||
downstreamServiceNo: '',
|
downstreamServiceNo: '',
|
||||||
|
warehouseId: undefined as number | undefined,
|
||||||
cargoAttribute: 'general',
|
cargoAttribute: 'general',
|
||||||
isCod: false,
|
isCod: false,
|
||||||
codSum: undefined as number | undefined,
|
codSum: undefined as number | undefined,
|
||||||
@ -1170,6 +1190,40 @@ const basicEditForm = reactive({
|
|||||||
|
|
||||||
const currencyOptions = ref<Array<{ value: string; label: string }>>([]);
|
const currencyOptions = ref<Array<{ value: string; label: string }>>([]);
|
||||||
const currencyOptionsLoading = ref(false);
|
const currencyOptionsLoading = ref(false);
|
||||||
|
const warehouseOptions = ref<Array<{ value: number; label: string }>>([]);
|
||||||
|
const warehouseOptionsLoading = ref(false);
|
||||||
|
|
||||||
|
async function ensureWarehouseOptions(preferred?: {
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
}) {
|
||||||
|
warehouseOptionsLoading.value = true;
|
||||||
|
try {
|
||||||
|
if (warehouseOptions.value.length === 0) {
|
||||||
|
const { items } = await getWarehouseList({ page: 1, pageSize: 200 });
|
||||||
|
warehouseOptions.value = (items || [])
|
||||||
|
.map((row: any) => {
|
||||||
|
const id = Number(row?.id ?? row?.Id);
|
||||||
|
if (!Number.isFinite(id) || id <= 0) return null;
|
||||||
|
const name = String(row?.warehouseName ?? row?.WarehouseName ?? '').trim();
|
||||||
|
return { value: id, label: name || `#${id}` };
|
||||||
|
})
|
||||||
|
.filter((item): item is { value: number; label: string } => !!item);
|
||||||
|
}
|
||||||
|
const id = preferred?.id != null && preferred.id > 0 ? preferred.id : undefined;
|
||||||
|
const name = String(preferred?.name ?? '').trim();
|
||||||
|
if (id && !warehouseOptions.value.some((opt) => opt.value === id)) {
|
||||||
|
warehouseOptions.value = [
|
||||||
|
...warehouseOptions.value,
|
||||||
|
{ value: id, label: name || `#${id}` },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (warehouseOptions.value.length === 0) warehouseOptions.value = [];
|
||||||
|
} finally {
|
||||||
|
warehouseOptionsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureCurrencyOptions(preferredCode?: string) {
|
async function ensureCurrencyOptions(preferredCode?: string) {
|
||||||
currencyOptionsLoading.value = true;
|
currencyOptionsLoading.value = true;
|
||||||
@ -2392,6 +2446,10 @@ function fillBasicEditForm() {
|
|||||||
basicEditForm.clientNo = String(o.clientNo ?? o.customerOrderNo ?? '');
|
basicEditForm.clientNo = String(o.clientNo ?? o.customerOrderNo ?? '');
|
||||||
basicEditForm.trackNo = String(o.trackNo ?? o.trackingNo ?? '');
|
basicEditForm.trackNo = String(o.trackNo ?? o.trackingNo ?? '');
|
||||||
basicEditForm.downstreamServiceNo = String(o.downstreamServiceNo ?? o.DownstreamServiceNo ?? '');
|
basicEditForm.downstreamServiceNo = String(o.downstreamServiceNo ?? o.DownstreamServiceNo ?? '');
|
||||||
|
const warehouseIdRaw = o.warehouseId ?? o.WarehouseId;
|
||||||
|
const warehouseIdNum = warehouseIdRaw != null ? Number(warehouseIdRaw) : NaN;
|
||||||
|
basicEditForm.warehouseId =
|
||||||
|
Number.isFinite(warehouseIdNum) && warehouseIdNum > 0 ? warehouseIdNum : undefined;
|
||||||
basicEditForm.cargoAttribute = String(o.cargoAttribute ?? 'general');
|
basicEditForm.cargoAttribute = String(o.cargoAttribute ?? 'general');
|
||||||
basicEditForm.isCod = !!(o.isCod ?? o.IsCod);
|
basicEditForm.isCod = !!(o.isCod ?? o.IsCod);
|
||||||
basicEditForm.codSum = o.codSum != null ? Number(o.codSum) : undefined;
|
basicEditForm.codSum = o.codSum != null ? Number(o.codSum) : undefined;
|
||||||
@ -2457,6 +2515,10 @@ async function focusFirstEditableField(rootSelector: string) {
|
|||||||
function startBasicEdit() {
|
function startBasicEdit() {
|
||||||
fillBasicEditForm();
|
fillBasicEditForm();
|
||||||
void ensureCurrencyOptions(basicEditForm.declaredCurrency);
|
void ensureCurrencyOptions(basicEditForm.declaredCurrency);
|
||||||
|
void ensureWarehouseOptions({
|
||||||
|
id: basicEditForm.warehouseId,
|
||||||
|
name: String(detail.value?.warehouse ?? detail.value?.warehouseName ?? ''),
|
||||||
|
});
|
||||||
basicEditing.value = true;
|
basicEditing.value = true;
|
||||||
void focusFirstEditableField('.order-detail-drawer .basic-info');
|
void focusFirstEditableField('.order-detail-drawer .basic-info');
|
||||||
}
|
}
|
||||||
@ -2494,6 +2556,7 @@ async function handleBasicEditSave() {
|
|||||||
clientNo: basicEditForm.clientNo,
|
clientNo: basicEditForm.clientNo,
|
||||||
trackNo: basicEditForm.trackNo,
|
trackNo: basicEditForm.trackNo,
|
||||||
downstreamServiceNo: basicEditForm.downstreamServiceNo,
|
downstreamServiceNo: basicEditForm.downstreamServiceNo,
|
||||||
|
warehouseId: basicEditForm.warehouseId ?? 0,
|
||||||
cargoAttribute: basicEditForm.cargoAttribute,
|
cargoAttribute: basicEditForm.cargoAttribute,
|
||||||
isCod: basicEditForm.isCod,
|
isCod: basicEditForm.isCod,
|
||||||
codSum: basicEditForm.codSum,
|
codSum: basicEditForm.codSum,
|
||||||
|
|||||||
@ -276,9 +276,9 @@ export default defineConfig(({ mode }) => {
|
|||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
},
|
},
|
||||||
// 开发环境面单 PDF 预览:同源代理 COS 签名链接(dev 后端桶可能与 printUrl 不一致)
|
// 开发环境面单/截图:同源代理 COS(自定义域名 https 不可用,上游走 http)
|
||||||
'/__cos__': {
|
'/__cos__': {
|
||||||
target: 'https://cos.ideship.com',
|
target: 'http://cos.ideship.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: false,
|
secure: false,
|
||||||
xfwd: false,
|
xfwd: false,
|
||||||
@ -287,7 +287,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
router: (req) => {
|
router: (req) => {
|
||||||
const raw = String(req.originalUrl || req.url || '').split('?')[0];
|
const raw = String(req.originalUrl || req.url || '').split('?')[0];
|
||||||
const host = raw.replace(/^\/__cos__\//, '').split('/')[0];
|
const host = raw.replace(/^\/__cos__\//, '').split('/')[0];
|
||||||
return host ? `https://${host}` : 'https://cos.ideship.com';
|
return host ? `http://${host}` : 'http://cos.ideship.com';
|
||||||
},
|
},
|
||||||
rewrite: (path) => path.replace(/^\/__cos__\/[^/]+/, '') || '/',
|
rewrite: (path) => path.replace(/^\/__cos__\/[^/]+/, '') || '/',
|
||||||
configure: (proxy) => {
|
configure: (proxy) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user