From e5175e6cc171dee8e35f71025d6e4731368a15e7 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Thu, 15 Oct 2026 19:53:24 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20H5=20=E9=97=A8=E6=88=B7=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E4=B8=8E=E5=85=85=E5=80=BC=E5=AE=A1=E6=A0=B8=EF=BC=8C?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E7=BC=96=E8=BE=91=E6=94=AF=E6=8C=81=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E4=BB=B6=E9=87=8D=E5=B0=BA=E5=8F=8A=20COS=20=E8=B5=84?= =?UTF-8?q?=E6=BA=90=E4=BB=A3=E7=90=86=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- nginx-k3s.conf | 7 +- src/api/customer/index.ts | 4 +- src/api/oms/common.ts | 17 + src/locale/en-US.ts | 4 + src/locale/zh-CN.ts | 4 + src/router/index.ts | 11 + src/utils/feeTotals.ts | 58 +- .../channel/composables/useKpiAreaPage.ts | 17 +- src/views/finance/rechargeRecord/index.vue | 28 +- .../components/H5AdminOrderDetailSheet.vue | 69 +- src/views/h5/admin-center/locale/en-US.ts | 36 + src/views/h5/admin-center/locale/zh-CN.ts | 36 + src/views/h5/admin-center/login/index.vue | 3 + src/views/h5/admin-center/orders/detail.vue | 51 +- src/views/h5/admin-center/recharge/index.vue | 726 ++++++++++++++++++ src/views/h5/admin-center/workplace/index.vue | 66 +- .../h5/components/H5LoginPortalSwitch.vue | 65 ++ .../components/H5OrderDetailSheet.vue | 6 +- .../components/H5OrderPaySheet.vue | 42 +- .../components/H5PlaceOrderSheet.vue | 132 +++- src/views/h5/customer-center/locale/en-US.ts | 11 + src/views/h5/customer-center/locale/zh-CN.ts | 11 + src/views/h5/customer-center/login/index.vue | 33 +- .../myOrders/components/OrderDetailDrawer.vue | 65 +- .../platformStoreOrder/PullOrdersModal.vue | 46 +- .../components/OrderDetailDrawer.vue | 65 +- vite.config.ts | 6 +- 27 files changed, 1484 insertions(+), 135 deletions(-) create mode 100644 src/views/h5/admin-center/recharge/index.vue create mode 100644 src/views/h5/components/H5LoginPortalSwitch.vue diff --git a/nginx-k3s.conf b/nginx-k3s.conf index e6dbbc8..178c991 100644 --- a/nginx-k3s.conf +++ b/nginx-k3s.conf @@ -155,20 +155,19 @@ server { location ^~ /marketing/commissionWithdraw { try_files $uri @spa; } location ^~ /marketing/referralTree { try_files $uri @spa; } - # 面单 PDF 同源代理:/__cos__// → https:/// - # 与 Vite 开发代理一致,避免浏览器直打 cos.* 触发 CORS + # 面单/截图同源代理:/__cos__// → http:/// + # cos.ideship.com 自定义域名 https 证书不可用,上游走 http;避免浏览器直打 CORS / 混合内容 location ~ ^/__cos__/([^/]+)(/.*)$ { set $cos_host $1; set $cos_uri $2; resolver 1.1.1.1 8.8.8.8 ipv6=off valid=300s; resolver_timeout 5s; proxy_http_version 1.1; - proxy_ssl_server_name on; proxy_set_header Host $cos_host; proxy_set_header Authorization ""; proxy_set_header Cookie ""; 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_read_timeout 120s; proxy_buffering off; diff --git a/src/api/customer/index.ts b/src/api/customer/index.ts index 6515e85..5633bda 100644 --- a/src/api/customer/index.ts +++ b/src/api/customer/index.ts @@ -72,7 +72,7 @@ export interface CustomerProfileCompleteness { } function mapUpdateBody(id: string | number, data: Partial) { - return { + const body: Record = { id, customerCode: data.customerCode, customerName: data.customerName, @@ -92,6 +92,8 @@ function mapUpdateBody(id: string | number, data: Partial) { memberLevelId: data.memberLevelId, priceTierKey: data.priceTierKey, } + // 去掉 undefined,避免局部字段被序列化成 null 后把后端原值冲掉 + return Object.fromEntries(Object.entries(body).filter(([, v]) => v !== undefined)) } export interface CustomerServiceAdvisorDto { diff --git a/src/api/oms/common.ts b/src/api/oms/common.ts index 8d15f5e..d0e17d2 100644 --- a/src/api/oms/common.ts +++ b/src/api/oms/common.ts @@ -103,12 +103,29 @@ export async function omsPost(path: string, body?: Record) return unwrapData(res.data) } +/** + * COS 绝对地址 → 同源 `/__cos__//...`(开发 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 绝对前缀) */ export function resolveOmsAssetUrl(path?: string | null): string { if (!path) return '' const value = path.trim() if (!value) return '' 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 return value.replace( /^(https?:\/\/api\.(?:nanafa|ideship)\.com)\/uploads\//i, diff --git a/src/locale/en-US.ts b/src/locale/en-US.ts index bf46064..fd64edb 100644 --- a/src/locale/en-US.ts +++ b/src/locale/en-US.ts @@ -1094,6 +1094,8 @@ export default { 'order.platformStoreOrder.message.batchResult': 'Batch done: {success} succeeded, {fail} failed', 'order.platformStoreOrder.message.selectOrders': 'Select orders 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.syncConfirm': 'Sync status for {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.payAmount': 'Recharge Amount', 'user.recharge.record.table.payImage': 'Recharge Screenshot', + 'user.recharge.record.table.viewImage': 'View', 'user.recharge.record.table.payStatus': 'Recharge Status', 'user.recharge.record.table.remarks': 'Remarks', + 'user.recharge.record.message.noPayImage': 'No screenshot available', 'user.recharge.record.payment.alipay': 'Alipay', 'user.recharge.record.payment.wechat': 'WeChat', 'user.recharge.record.actions.approve': 'Approve', diff --git a/src/locale/zh-CN.ts b/src/locale/zh-CN.ts index 1a01c91..b2de6d1 100644 --- a/src/locale/zh-CN.ts +++ b/src/locale/zh-CN.ts @@ -1085,6 +1085,8 @@ export default { 'order.platformStoreOrder.message.batchResult': '批量匹配完成:成功 {success},失败 {fail}', 'order.platformStoreOrder.message.selectOrders': '请选择订单', 'order.platformStoreOrder.message.selectStore': '请选择店铺', + 'order.platformStoreOrder.message.noStore': '当前客户下暂无已授权店铺,请先在「店铺授权」完成绑定', + 'order.platformStoreOrder.message.loadStoreFailed': '加载店铺列表失败', 'order.platformStoreOrder.message.selectStatus': '请选择同步类型', 'order.platformStoreOrder.message.syncConfirm': '是否手动同步 {count} 个订单的状态?', 'order.platformStoreOrder.message.rtsConfirm': '是否对 {count} 个订单执行准备发货(RTS)?', @@ -4596,8 +4598,10 @@ export default { 'user.recharge.record.table.payType': '支付方式', 'user.recharge.record.table.payAmount': '充值金额', 'user.recharge.record.table.payImage': '充值截图', + 'user.recharge.record.table.viewImage': '查看', 'user.recharge.record.table.payStatus': '充值状态', 'user.recharge.record.table.remarks': '备注', + 'user.recharge.record.message.noPayImage': '暂无充值截图', 'user.recharge.record.payment.alipay': '支付宝', 'user.recharge.record.payment.wechat': '微信', 'user.recharge.record.actions.approve': '通过', diff --git a/src/router/index.ts b/src/router/index.ts index 9106d81..cd8bf70 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -152,6 +152,17 @@ const router = createRouter({ 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(.*)*', name: 'H5AdminCenterFallback', diff --git a/src/utils/feeTotals.ts b/src/utils/feeTotals.ts index ed5d686..893ad33 100644 --- a/src/utils/feeTotals.ts +++ b/src/utils/feeTotals.ts @@ -1,50 +1,82 @@ export type FeeRowLike = { feeType?: string + FeeType?: string type?: string currency?: string amount?: number costItem?: string + CostItem?: string chargingStage?: string + ChargingStage?: string } 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 return '' } -function getChargingStageRank(stage?: string) { +function getChargingStageRank(stage?: string, options?: { includeOutbound?: boolean }) { 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 === 'forecast') return 1 return 0 } -/** - * 每个费用类型 + 费用项只保留最高计费阶段(出库 > 入库 > 预报), - * 避免预报与入库明细叠算进合计。 - */ -export function filterEffectiveStageFees(rows: T[]): T[] { +function pickCostItem(row: FeeRowLike) { + return String(row?.costItem ?? row?.CostItem ?? '').trim() || '_' +} + +function pickChargingStage(row: FeeRowLike) { + return String(row?.chargingStage ?? row?.ChargingStage ?? '') +} + +function isOutboundStage(row: FeeRowLike) { + return pickChargingStage(row).toLowerCase() === 'outbound' +} + +function pickStageWinners( + rows: T[], + options?: { includeOutbound?: boolean }, +): T[] { + const includeOutbound = options?.includeOutbound !== false const winners = new Map() for (const row of rows) { + if (!includeOutbound && isOutboundStage(row)) continue const feeType = normalizeFeeType(row) || 'unknown' - const costItem = String(row?.costItem ?? '').trim() || '_' - const key = `${feeType}|${costItem}` + const key = `${feeType}|${pickCostItem(row)}` const current = winners.get(key) if (!current) { winners.set(key, row) continue } - const rowStage = String((row as any)?.chargingStage ?? (row as any)?.ChargingStage ?? '') - const curStage = String((current as any)?.chargingStage ?? (current as any)?.ChargingStage ?? '') - if (getChargingStageRank(rowStage) > getChargingStageRank(curStage)) { + const rankOpts = { includeOutbound } + if ( + getChargingStageRank(pickChargingStage(row), rankOpts) + > getChargingStageRank(pickChargingStage(current), rankOpts) + ) { winners.set(key, row) } } return [...winners.values()] } +/** + * 管理端 / 内部:同费用项保留最高阶段(出库 > 入库 > 预报)。 + */ +export function filterEffectiveStageFees(rows: T[]): T[] { + return pickStageWinners(rows, { includeOutbound: true }) +} + +/** + * 客户侧展示 / 应付:不含出库,同费用项只保留入库 > 预报。 + */ +export function filterCustomerFacingFees(rows: T[]): T[] { + return pickStageWinners(rows, { includeOutbound: false }) +} + export function sumFeesByCurrency(list: FeeRowLike[]) { const totals = new Map() for (const row of list) { diff --git a/src/views/channel/composables/useKpiAreaPage.ts b/src/views/channel/composables/useKpiAreaPage.ts index b8e6e79..d286df7 100644 --- a/src/views/channel/composables/useKpiAreaPage.ts +++ b/src/views/channel/composables/useKpiAreaPage.ts @@ -535,11 +535,22 @@ export function useKpiAreaPage(areaType: KpiAreaType) { const leftMenuData = ref([]); const leftExpandedKeys = ref(['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(() => [ { key: 'category-list', title: t('channel.area.listTitle'), - children: leftMenuData.value.map((item) => ({ + children: filteredLeftMenuData.value.map((item) => ({ key: `item-${item.id}`, title: item.name, isLeaf: true, @@ -882,9 +893,9 @@ export function useKpiAreaPage(areaType: KpiAreaType) { leftLoading.value = true; try { const response: any = await postForewarningServiceKpiAreaGetKpiAreas({ - pageIndex: 1, + page: 1, pageSize: 500, - ...searchModel, + areaType: searchModel.areaType, }); leftMenuData.value = filterByAreaType(response?.items ?? response?.data?.items ?? []); } catch { diff --git a/src/views/finance/rechargeRecord/index.vue b/src/views/finance/rechargeRecord/index.vue index be7157d..d96cf51 100644 --- a/src/views/finance/rechargeRecord/index.vue +++ b/src/views/finance/rechargeRecord/index.vue @@ -160,6 +160,14 @@ interface RechargeRecord { 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 { const pt = Number(item.payType) // 接口约定:1-微信 2-支付宝 @@ -170,10 +178,7 @@ function mapRow(item: any): RechargeRecord { payTime: item.rechargeTime ?? '', payType, payAmount: item.rechargeAmount ?? 0, - payImage: - item.rechargeScreenshot && String(item.rechargeScreenshot) !== '0' - ? resolveOmsAssetUrl(item.rechargeScreenshot) - : undefined, + payImage: resolvePayImageUrl(item.rechargeScreenshot ?? item.RechargeScreenshot), payStatus: String(item.rechargeStatus ?? ''), remarks: item.remark ?? '', } @@ -307,8 +312,8 @@ function customCellRenderer(row: number, col: number, value: any, rowData: any): } if (colKey === 'payImage') { if (!rowData?.payImage) return '--' - const imageEscaped = String(rowData.payImage).replace(/"/g, '"') - return `查看` + // 用行号取图,避免签名 URL 中的 & 写入属性被截断导致预览空白 + return `${t('user.recharge.record.table.viewImage')}` } if (colKey === 'payStatus') { const status = rowData?.payStatus @@ -388,8 +393,15 @@ async function handleRejectSubmit() { function handleViewPayImageClick(event: MouseEvent) { const trigger = (event.target as HTMLElement | null)?.closest?.('[data-role="view-pay-image"]') if (!trigger) return - const imageUrl = trigger.getAttribute('data-image-url') - if (!imageUrl) return + const rowIndex = Number(trigger.getAttribute('data-row-index')) + 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.stopPropagation() previewImageUrl.value = imageUrl diff --git a/src/views/h5/admin-center/components/H5AdminOrderDetailSheet.vue b/src/views/h5/admin-center/components/H5AdminOrderDetailSheet.vue index 9e78485..6434e66 100644 --- a/src/views/h5/admin-center/components/H5AdminOrderDetailSheet.vue +++ b/src/views/h5/admin-center/components/H5AdminOrderDetailSheet.vue @@ -84,6 +84,15 @@