Merge pull request 'feat: H5 门户切换与充值审核,订单编辑支持仓库件重尺及 COS 资源代理修复' (#7) from test into master
All checks were successful
Build and Deploy Front / deploy (push) Successful in 1m53s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 1m53s
Reviewed-on: #7
This commit is contained in:
commit
cd02a48017
@ -155,20 +155,19 @@ server {
|
||||
location ^~ /marketing/commissionWithdraw { try_files $uri @spa; }
|
||||
location ^~ /marketing/referralTree { try_files $uri @spa; }
|
||||
|
||||
# 面单 PDF 同源代理:/__cos__/<host>/<path> → https://<host>/<path>
|
||||
# 与 Vite 开发代理一致,避免浏览器直打 cos.* 触发 CORS
|
||||
# 面单/截图同源代理:/__cos__/<host>/<path> → http://<host>/<path>
|
||||
# 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;
|
||||
|
||||
@ -72,7 +72,7 @@ export interface CustomerProfileCompleteness {
|
||||
}
|
||||
|
||||
function mapUpdateBody(id: string | number, data: Partial<CustomerDto>) {
|
||||
return {
|
||||
const body: Record<string, unknown> = {
|
||||
id,
|
||||
customerCode: data.customerCode,
|
||||
customerName: data.customerName,
|
||||
@ -92,6 +92,8 @@ function mapUpdateBody(id: string | number, data: Partial<CustomerDto>) {
|
||||
memberLevelId: data.memberLevelId,
|
||||
priceTierKey: data.priceTierKey,
|
||||
}
|
||||
// 去掉 undefined,避免局部字段被序列化成 null 后把后端原值冲掉
|
||||
return Object.fromEntries(Object.entries(body).filter(([, v]) => v !== undefined))
|
||||
}
|
||||
|
||||
export interface CustomerServiceAdvisorDto {
|
||||
|
||||
@ -103,12 +103,29 @@ export async function omsPost<T = any>(path: string, body?: Record<string, any>)
|
||||
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 绝对前缀) */
|
||||
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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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': '通过',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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<T extends FeeRowLike>(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<T extends FeeRowLike>(
|
||||
rows: T[],
|
||||
options?: { includeOutbound?: boolean },
|
||||
): T[] {
|
||||
const includeOutbound = options?.includeOutbound !== false
|
||||
const winners = new Map<string, T>()
|
||||
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<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[]) {
|
||||
const totals = new Map<string, number>()
|
||||
for (const row of list) {
|
||||
|
||||
@ -535,11 +535,22 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
||||
const leftMenuData = ref<any[]>([]);
|
||||
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[]>(() => [
|
||||
{
|
||||
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 {
|
||||
|
||||
@ -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 `<span data-role="view-pay-image" data-image-url="${imageEscaped}" style="color:var(--app-primary-color);cursor:pointer;">查看</span>`
|
||||
// 用行号取图,避免签名 URL 中的 & 写入属性被截断导致预览空白
|
||||
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') {
|
||||
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
|
||||
|
||||
@ -84,6 +84,15 @@
|
||||
</a-spin>
|
||||
|
||||
<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">
|
||||
{{ t('h5.orders.detail.close') }}
|
||||
</button>
|
||||
@ -95,13 +104,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
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 { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||
import { filterEffectiveStageFees, normalizeFeeType } from '@/utils/feeTotals';
|
||||
import {
|
||||
getOrderStatusMeta,
|
||||
resolveOrderStatusCode,
|
||||
type OrderStatusCode,
|
||||
} from '@/constants/orderStatus';
|
||||
import H5EmptyState from '@/views/h5/customer-center/components/H5EmptyState.vue';
|
||||
|
||||
@ -115,8 +126,18 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
edit: [orderId: number | string];
|
||||
}>();
|
||||
|
||||
/** 与客户 H5 / 运单管理可改阶段对齐:入库前可编辑 */
|
||||
const H5_ADMIN_MODIFY_STATUSES: OrderStatusCode[] = [
|
||||
'draft',
|
||||
'predeclared',
|
||||
'pendingAllocation',
|
||||
'pendingPayment',
|
||||
'pendingShip',
|
||||
];
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const tabs = ['overview', 'track', 'fee'] as const;
|
||||
@ -149,6 +170,9 @@ const waybillNo = computed(
|
||||
() => pick(detail.value, 'serviceNo', 'ServiceNo', 'waybillNo', 'WaybillNo') || '',
|
||||
);
|
||||
const statusCode = computed(() => resolveOrderStatusCode(detail.value));
|
||||
const canModify = computed(() =>
|
||||
H5_ADMIN_MODIFY_STATUSES.includes(statusCode.value as OrderStatusCode),
|
||||
);
|
||||
|
||||
const overviewRows = computed(() => {
|
||||
const d = detail.value || {};
|
||||
@ -253,7 +277,12 @@ const feeList = computed(() => {
|
||||
const d = detail.value || {};
|
||||
const fees = d.feeList ?? d.FeeList ?? d.orderFees ?? d.fees ?? [];
|
||||
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),
|
||||
amount: pick(f, 'amount', 'Amount', 'fee') ?? '-',
|
||||
}));
|
||||
@ -316,6 +345,11 @@ function close() {
|
||||
emit('update:visible', false);
|
||||
}
|
||||
|
||||
function openEdit() {
|
||||
if (!props.orderId || !canModify.value) return;
|
||||
emit('edit', props.orderId);
|
||||
}
|
||||
|
||||
watch(activeTab, (tab) => {
|
||||
if (tab === 'track' && props.visible) void loadTrack();
|
||||
});
|
||||
@ -544,14 +578,43 @@ watch(
|
||||
.h5-detail__foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
||||
background: #fff;
|
||||
border-top: 1px solid #eef0f3;
|
||||
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 {
|
||||
flex: 1;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
|
||||
@ -21,25 +21,61 @@ export default {
|
||||
'h5Admin.login.privacy': 'Privacy Policy',
|
||||
'h5Admin.login.terms': 'Terms of Service',
|
||||
'h5Admin.login.and': 'and',
|
||||
'h5.login.switchToAdmin': 'Switch to staff login',
|
||||
'h5.login.switchToCustomer': 'Switch to customer login',
|
||||
|
||||
'h5Admin.me.title': 'Profile',
|
||||
'h5Admin.me.account': 'Account',
|
||||
'h5Admin.me.org': 'Organization',
|
||||
'h5Admin.me.roles': 'Roles',
|
||||
'h5Admin.me.email': 'Email',
|
||||
'h5Admin.me.rechargeAudit': 'Recharge review',
|
||||
'h5Admin.me.logout': 'Sign out',
|
||||
'h5Admin.me.logoutConfirm': 'Sign out of this account?',
|
||||
'h5Admin.me.emptyRole': 'No roles',
|
||||
'h5Admin.me.emptyOrg': 'No organization',
|
||||
'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.afternoon': 'Good afternoon',
|
||||
'h5Admin.workplace.greeting.evening': 'Good evening',
|
||||
'h5Admin.workplace.subtitle': 'Clear high-priority queues first',
|
||||
'h5Admin.workplace.rechargeAudit': 'Recharge review',
|
||||
'h5Admin.workplace.opsTitle': 'Today’s queue',
|
||||
'h5Admin.workplace.refresh': 'Refresh',
|
||||
'h5Admin.workplace.updated': 'Updated {time}',
|
||||
'h5Admin.workplace.pendingRecharge': 'Recharge pending',
|
||||
'h5Admin.workplace.todayCreated': 'Created today',
|
||||
'h5Admin.workplace.pendingPayment': 'Unpaid',
|
||||
'h5Admin.workplace.pendingShip': 'To ship',
|
||||
|
||||
@ -21,25 +21,61 @@ export default {
|
||||
'h5Admin.login.privacy': '隐私政策',
|
||||
'h5Admin.login.terms': '服务条款',
|
||||
'h5Admin.login.and': '与',
|
||||
'h5.login.switchToAdmin': '切换至管理登录',
|
||||
'h5.login.switchToCustomer': '切换至客户登录',
|
||||
|
||||
'h5Admin.me.title': '我的',
|
||||
'h5Admin.me.account': '账号',
|
||||
'h5Admin.me.org': '组织',
|
||||
'h5Admin.me.roles': '角色',
|
||||
'h5Admin.me.email': '邮箱',
|
||||
'h5Admin.me.rechargeAudit': '充值审核',
|
||||
'h5Admin.me.logout': '退出登录',
|
||||
'h5Admin.me.logoutConfirm': '确定退出当前账号?',
|
||||
'h5Admin.me.emptyRole': '暂无角色',
|
||||
'h5Admin.me.emptyOrg': '未分配组织',
|
||||
'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.afternoon': '下午好',
|
||||
'h5Admin.workplace.greeting.evening': '晚上好',
|
||||
'h5Admin.workplace.subtitle': '先处理高优先级待办,再看今日概况',
|
||||
'h5Admin.workplace.rechargeAudit': '充值审核',
|
||||
'h5Admin.workplace.opsTitle': '今日待办',
|
||||
'h5Admin.workplace.refresh': '刷新',
|
||||
'h5Admin.workplace.updated': '更新于 {time}',
|
||||
'h5Admin.workplace.pendingRecharge': '充值待审',
|
||||
'h5Admin.workplace.todayCreated': '今日创建',
|
||||
'h5Admin.workplace.pendingPayment': '待支付',
|
||||
'h5Admin.workplace.pendingShip': '待出库',
|
||||
|
||||
@ -73,6 +73,8 @@
|
||||
>
|
||||
<div class="h5-login-agreement">{{ agreementContent }}</div>
|
||||
</a-modal>
|
||||
|
||||
<H5LoginPortalSwitch target="customer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -89,6 +91,7 @@ import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
import { ADMIN_H5_HOME_PATH, isAdminH5Path } from '@/utils/login-portal';
|
||||
import logoImg from '@/assets/logo2.png';
|
||||
import LoginLangToggle from '@/views/login/components/LoginLangToggle.vue';
|
||||
import H5LoginPortalSwitch from '@/views/h5/components/H5LoginPortalSwitch.vue';
|
||||
|
||||
defineOptions({ name: 'H5AdminLogin' });
|
||||
|
||||
|
||||
@ -1,24 +1,43 @@
|
||||
<template>
|
||||
<div class="h5-order-detail-page">
|
||||
<H5AdminOrderDetailSheet
|
||||
v-model:visible="sheetVisible"
|
||||
:key="detailKey"
|
||||
:visible="sheetVisible && !editVisible"
|
||||
:order-id="orderId"
|
||||
:initial-tab="initialTab"
|
||||
@update:visible="onDetailVisible"
|
||||
@edit="openEdit"
|
||||
/>
|
||||
|
||||
<H5PlaceOrderSheet
|
||||
v-if="editVisible"
|
||||
v-model:visible="editVisible"
|
||||
:order-id="editOrderId"
|
||||
variant="admin"
|
||||
@success="onEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 H5AdminOrderDetailSheet from '../components/H5AdminOrderDetailSheet.vue';
|
||||
|
||||
defineOptions({ name: 'H5AdminOrderDetail' });
|
||||
|
||||
const H5PlaceOrderSheet = defineAsyncComponent(
|
||||
() => import('@/views/h5/customer-center/components/H5PlaceOrderSheet.vue'),
|
||||
);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
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 initialTab = computed(() => {
|
||||
const tab = String(route.query.tab || '');
|
||||
@ -34,8 +53,34 @@ function leave() {
|
||||
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) => {
|
||||
if (!visible) leave();
|
||||
if (!visible && !editVisible.value) leave();
|
||||
});
|
||||
|
||||
watch(editVisible, (visible) => {
|
||||
if (!visible) {
|
||||
editOrderId.value = null;
|
||||
sheetVisible.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
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"
|
||||
class="h5-wp__ops-card"
|
||||
: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>
|
||||
<strong class="h5-wp__ops-value">{{ formatNum(item.value) }}</strong>
|
||||
@ -121,6 +121,7 @@ import {
|
||||
type OrderWorkplaceStats,
|
||||
type OrderWorkplaceTrendPoint,
|
||||
} from '@/api/workplace';
|
||||
import { getRechargeRecordList } from '@/api/recharge/rechargeRecord';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
|
||||
defineOptions({ name: 'H5AdminWorkplace' });
|
||||
@ -133,6 +134,7 @@ const loading = ref(false);
|
||||
const trendLoading = ref(false);
|
||||
const loadError = ref(false);
|
||||
const stats = ref<OrderWorkplaceStats | null>(null);
|
||||
const pendingRechargeCount = ref(0);
|
||||
const trendPoints = ref<OrderWorkplaceTrendPoint[]>([]);
|
||||
const trendDimension = ref<'day' | 'week' | 'month'>('day');
|
||||
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;
|
||||
return [
|
||||
{
|
||||
key: 'pendingRecharge',
|
||||
labelKey: 'h5Admin.workplace.pendingRecharge',
|
||||
value: Math.round(Number(pendingRechargeCount.value) || 0),
|
||||
status: '',
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
action: 'recharge',
|
||||
},
|
||||
{
|
||||
key: 'problematic',
|
||||
labelKey: 'h5Admin.workplace.problematic',
|
||||
@ -172,6 +193,7 @@ const opsItems = computed(() => {
|
||||
status: 'problematic',
|
||||
tone: 'danger',
|
||||
alert: true,
|
||||
action: 'orders',
|
||||
},
|
||||
{
|
||||
key: 'claimed',
|
||||
@ -180,6 +202,7 @@ const opsItems = computed(() => {
|
||||
status: 'claimed',
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
action: 'orders',
|
||||
},
|
||||
{
|
||||
key: 'pendingShip',
|
||||
@ -188,6 +211,7 @@ const opsItems = computed(() => {
|
||||
status: 'pendingShip',
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
action: 'orders',
|
||||
},
|
||||
{
|
||||
key: 'pendingPayment',
|
||||
@ -196,6 +220,7 @@ const opsItems = computed(() => {
|
||||
status: 'pendingPayment',
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
action: 'orders',
|
||||
},
|
||||
{
|
||||
key: 'todayCreated',
|
||||
@ -204,6 +229,7 @@ const opsItems = computed(() => {
|
||||
status: '',
|
||||
tone: 'primary',
|
||||
alert: false,
|
||||
action: 'orders',
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -296,6 +322,31 @@ function goOrders(status: string) {
|
||||
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') {
|
||||
if (dim === 'week') return 4;
|
||||
if (dim === 'month') return 6;
|
||||
@ -327,7 +378,11 @@ async function load() {
|
||||
loading.value = true;
|
||||
loadError.value = false;
|
||||
try {
|
||||
const [statsRes] = await Promise.all([getOrderWorkplaceStats(), loadTrend()]);
|
||||
const [statsRes] = await Promise.all([
|
||||
getOrderWorkplaceStats(),
|
||||
loadTrend(),
|
||||
loadPendingRechargeCount(),
|
||||
]);
|
||||
stats.value = statsRes;
|
||||
updatedAt.value = new Date();
|
||||
} catch {
|
||||
@ -385,10 +440,15 @@ onMounted(() => {
|
||||
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-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 { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||
import { filterCustomerFacingFees, normalizeFeeType } from '@/utils/feeTotals';
|
||||
import { h5Navigate } from '@/utils/device';
|
||||
import {
|
||||
getOrderStatusMeta,
|
||||
@ -332,7 +333,10 @@ const feeList = computed(() => {
|
||||
const d = detail.value || {};
|
||||
const fees = d.feeList ?? d.FeeList ?? d.orderFees ?? d.fees ?? [];
|
||||
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),
|
||||
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 { getOrderDetail, postOrderPay } from '@/api/order';
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||
import {
|
||||
filterCustomerFacingFees,
|
||||
formatFeeTotals,
|
||||
normalizeFeeType,
|
||||
sumFeesByCurrency,
|
||||
} from '@/utils/feeTotals';
|
||||
import H5EmptyState from './H5EmptyState.vue';
|
||||
|
||||
defineOptions({ name: 'H5OrderPaySheet' });
|
||||
@ -162,7 +168,7 @@ const grandTotalText = computed(() => {
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
const fromApi = row?.costItemName ?? row?.CostItemName;
|
||||
if (fromApi != null && fromApi !== '') return String(fromApi);
|
||||
@ -278,10 +260,12 @@ async function loadPreviews() {
|
||||
try {
|
||||
const detail = await getOrderDetail(id);
|
||||
const feesRaw = detail?.feeList ?? detail?.FeeList ?? detail?.orderFees ?? detail?.fees;
|
||||
const receivableFees = (Array.isArray(feesRaw) ? feesRaw : []).filter(
|
||||
(row) => normalizeFeeType(row) === 'receivable',
|
||||
const receivableFees = filterCustomerFacingFees(
|
||||
(Array.isArray(feesRaw) ? feesRaw : []).filter(
|
||||
(row) => normalizeFeeType(row) === 'receivable',
|
||||
),
|
||||
);
|
||||
const totals = sumReceivableFees(receivableFees);
|
||||
const totals = sumFeesByCurrency(receivableFees);
|
||||
const totalAmount = [...totals.values()].reduce((sum, val) => sum + val, 0);
|
||||
const waybillNo =
|
||||
pick(detail, 'waybillNo', 'WaybillNo', 'serviceNo', 'ServiceNo') || fallbackWaybill;
|
||||
@ -289,7 +273,7 @@ async function loadPreviews() {
|
||||
id,
|
||||
waybillNo: String(waybillNo),
|
||||
fees: mapFeeRows(receivableFees),
|
||||
totalText: formatTotals(totals),
|
||||
totalText: formatFeeTotals(totals),
|
||||
totalAmount,
|
||||
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>
|
||||
</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">
|
||||
<label class="h5-po__label">{{ t('h5.placeOrder.product') }}</label>
|
||||
<a-select
|
||||
@ -154,7 +172,7 @@
|
||||
<a-select
|
||||
v-model="form.warehouseId"
|
||||
allow-clear
|
||||
:disabled="isEditMode"
|
||||
allow-search
|
||||
:error="fieldHasError('warehouseId')"
|
||||
>
|
||||
<a-option v-for="opt in warehouseOptions" :key="opt.value" :value="opt.value">
|
||||
@ -167,6 +185,31 @@
|
||||
</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">
|
||||
<label class="h5-po__label">
|
||||
{{ t('h5.placeOrder.weight') }} / {{ t('h5.placeOrder.dimensions') }}
|
||||
@ -175,25 +218,21 @@
|
||||
<a-input
|
||||
v-model="form.actualWeight"
|
||||
allow-clear
|
||||
:disabled="isEditMode"
|
||||
:error="fieldHasError('actualWeight')"
|
||||
:placeholder="t('h5.placeOrder.weightPh')"
|
||||
/>
|
||||
<a-input
|
||||
v-model="form.length"
|
||||
:disabled="isEditMode"
|
||||
:error="fieldHasError('length')"
|
||||
:placeholder="t('h5.placeOrder.length')"
|
||||
/>
|
||||
<a-input
|
||||
v-model="form.width"
|
||||
:disabled="isEditMode"
|
||||
:error="fieldHasError('width')"
|
||||
:placeholder="t('h5.placeOrder.width')"
|
||||
/>
|
||||
<a-input
|
||||
v-model="form.height"
|
||||
:disabled="isEditMode"
|
||||
:error="fieldHasError('height')"
|
||||
:placeholder="t('h5.placeOrder.height')"
|
||||
/>
|
||||
@ -1172,6 +1211,7 @@ import {
|
||||
postOrderParcelCreate,
|
||||
postOrderReceiverParseAddress,
|
||||
postOrderUpdate,
|
||||
postOrderWeightUpdate,
|
||||
} from '@/api/order';
|
||||
import { listAiProviders, listAiSkills, type AiSkillItem } from '@/api/ai';
|
||||
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 props = defineProps<{
|
||||
visible: boolean;
|
||||
/** 查价带入草稿;不传则尝试读本地缓存 */
|
||||
draft?: Record<string, any> | null;
|
||||
/** 传入则为编辑模式:加载详情并走 /order/update */
|
||||
orderId?: number | string | null;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
visible: boolean;
|
||||
/** 查价带入草稿;不传则尝试读本地缓存 */
|
||||
draft?: Record<string, any> | null;
|
||||
/** 传入则为编辑模式:加载详情并走 /order/update */
|
||||
orderId?: number | string | null;
|
||||
/** admin:管理端编辑(可改跟踪号) */
|
||||
variant?: 'customer' | 'admin';
|
||||
}>(),
|
||||
{ variant: 'customer' },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
@ -1247,8 +1292,12 @@ const isEditMode = computed(() => {
|
||||
const id = Number(props.orderId);
|
||||
return Number.isFinite(id) && id > 0;
|
||||
});
|
||||
const isAdminVariant = computed(() => props.variant === 'admin');
|
||||
const canEditTrackNo = computed(() => isEditMode.value && isAdminVariant.value);
|
||||
/** 详情是否已有发件人(更新接口不能新建 sender) */
|
||||
const editHasSender = ref(false);
|
||||
/** 编辑态件重尺类型(默认预报重) */
|
||||
const editWeightType = ref('forecast');
|
||||
const countryIdToCode = ref<Record<string, string>>({});
|
||||
|
||||
const step = ref(0);
|
||||
@ -1499,7 +1548,11 @@ const form = reactive({
|
||||
actualWeight: '',
|
||||
quantity: '1',
|
||||
declaredCurrency: 'USD',
|
||||
downstreamServiceNo: '',
|
||||
trackNo: '',
|
||||
isCod: false,
|
||||
codCurrency: '',
|
||||
codSum: '',
|
||||
receiver: {
|
||||
recipientsName: '',
|
||||
phone: '',
|
||||
@ -1884,6 +1937,12 @@ function resetForm() {
|
||||
form.actualWeight = '';
|
||||
form.quantity = '1';
|
||||
form.declaredCurrency = 'USD';
|
||||
form.downstreamServiceNo = '';
|
||||
form.trackNo = '';
|
||||
form.isCod = false;
|
||||
form.codCurrency = '';
|
||||
form.codSum = '';
|
||||
editWeightType.value = 'forecast';
|
||||
declaredTotalInput.value = '';
|
||||
declaredTotalInputFocused.value = false;
|
||||
selectedAdditionalServices.value = [];
|
||||
@ -2672,6 +2731,10 @@ function applyOrderDetail(detail: Record<string, any>) {
|
||||
{};
|
||||
|
||||
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.cargoAttribute = normalizeCargoAttributeCode(
|
||||
pickDetail(detail, 'cargoAttribute', 'CargoAttribute') ?? form.cargoAttribute,
|
||||
@ -2688,8 +2751,19 @@ function applyOrderDetail(detail: Record<string, any>) {
|
||||
pickDetail(detail, 'declaredCurrency', 'DeclaredCurrency') ?? form.declaredCurrency,
|
||||
);
|
||||
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 =
|
||||
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.width = String(pickDetail(weight, 'width', 'Width') ?? form.width ?? '');
|
||||
form.height = String(pickDetail(weight, 'height', 'Height') ?? form.height ?? '');
|
||||
@ -2779,13 +2853,21 @@ function buildUpdatePayload() {
|
||||
skuImage: normalizeSkuImageForStorage(row.skuImage),
|
||||
};
|
||||
});
|
||||
const declaredSum =
|
||||
parseNum(declaredTotalInput.value) ??
|
||||
payloadItems.reduce((sum, row) => sum + (Number(row.amount) || 0), 0);
|
||||
const body: Record<string, any> = {
|
||||
id: Number(props.orderId),
|
||||
clientNo: trim(form.clientNo),
|
||||
downstreamServiceNo: trim(form.downstreamServiceNo),
|
||||
cargoAttribute: normalizeCargoAttributeCode(form.cargoAttribute),
|
||||
quantity: trim(form.quantity) || '1',
|
||||
warehouseId: parseNum(form.warehouseId) ?? 0,
|
||||
quantity: parseNum(form.quantity) ?? 1,
|
||||
declaredCurrency: trim(form.declaredCurrency),
|
||||
isCod: false,
|
||||
declaredSum: declaredSum ?? 0,
|
||||
isCod: Boolean(form.isCod),
|
||||
codCurrency: trim(form.codCurrency),
|
||||
codSum: parseNum(form.codSum),
|
||||
receiver: {
|
||||
recipientsName: trim(form.receiver.recipientsName),
|
||||
companyName: trim(form.receiver.companyName),
|
||||
@ -2809,6 +2891,9 @@ function buildUpdatePayload() {
|
||||
},
|
||||
items: payloadItems,
|
||||
};
|
||||
if (canEditTrackNo.value) {
|
||||
body.trackNo = trim(form.trackNo);
|
||||
}
|
||||
if (editHasSender.value || trim(form.sender.senderName)) {
|
||||
body.sender = {
|
||||
senderName: trim(form.sender.senderName),
|
||||
@ -2920,6 +3005,25 @@ async function submit() {
|
||||
try {
|
||||
if (isEditMode.value) {
|
||||
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'));
|
||||
emit('success');
|
||||
emit('update:visible', false);
|
||||
|
||||
@ -295,6 +295,8 @@ export default {
|
||||
'h5.login.failed': 'Sign-in failed. Check your account and password.',
|
||||
'h5.login.newUserHint': 'No account?',
|
||||
'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.amount': 'Amount',
|
||||
@ -348,6 +350,15 @@ export default {
|
||||
'h5.placeOrder.handoffFilled': 'Filled: {items}',
|
||||
'h5.placeOrder.clientNo': 'Customer order No.',
|
||||
'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.productPh': 'Select product',
|
||||
'h5.placeOrder.additionalServices': 'Additional services',
|
||||
|
||||
@ -294,6 +294,8 @@ export default {
|
||||
'h5.login.failed': '登录失败,请检查账号密码',
|
||||
'h5.login.newUserHint': '没有账号?',
|
||||
'h5.login.goRegister': '微信一键注册',
|
||||
'h5.login.switchToAdmin': '切换至管理登录',
|
||||
'h5.login.switchToCustomer': '切换至客户登录',
|
||||
|
||||
'h5.recharge.title': '账户充值',
|
||||
'h5.recharge.amount': '充值金额',
|
||||
@ -347,6 +349,15 @@ export default {
|
||||
'h5.placeOrder.handoffFilled': '已带入:{items}',
|
||||
'h5.placeOrder.clientNo': '客户单号',
|
||||
'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.productPh': '选择销售产品',
|
||||
'h5.placeOrder.additionalServices': '附加服务',
|
||||
|
||||
@ -94,13 +94,7 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<router-link
|
||||
class="h5-login-admin-entry"
|
||||
to="/h5/admin/login"
|
||||
replace
|
||||
>
|
||||
admin
|
||||
</router-link>
|
||||
<H5LoginPortalSwitch target="admin" />
|
||||
</template>
|
||||
|
||||
<a-modal
|
||||
@ -130,6 +124,7 @@ import { isWeChatBrowser } from '@/utils/device';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
import logoImg from '@/assets/logo2.png';
|
||||
import LoginLangToggle from '@/views/login/components/LoginLangToggle.vue';
|
||||
import H5LoginPortalSwitch from '@/views/h5/components/H5LoginPortalSwitch.vue';
|
||||
import WechatOAuthLoginButton from '../components/WechatOAuthLoginButton.vue';
|
||||
import { wxOAuthLogin } from '@/api/wechat';
|
||||
import {
|
||||
@ -624,30 +619,6 @@ onMounted(async () => {
|
||||
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 {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@ -143,7 +143,24 @@
|
||||
<div class="detail-field-value">
|
||||
<template v-if="basicEditing && isOrderFieldEditable(fieldKey)">
|
||||
<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]"
|
||||
allow-clear
|
||||
size="small"
|
||||
@ -703,6 +720,7 @@ import {
|
||||
type OrderOperationLogItem,
|
||||
} from '@/api/order';
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||
import { getWarehouseList } from '@/api/system/warehouse';
|
||||
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
||||
import { getBooleanTagStyle } from '@/utils/statusTag';
|
||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||
@ -850,6 +868,7 @@ interface FormFieldDef {
|
||||
const orderEditFieldDefs: FormFieldDef[] = [
|
||||
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
||||
{ key: 'downstreamServiceNo', labelKey: 'order.myOrders.drawer.downstreamServiceNo' },
|
||||
{ key: 'warehouse', labelKey: 'order.myOrders.drawer.warehouse', inputType: 'select' },
|
||||
{
|
||||
key: 'cargoAttribute',
|
||||
labelKey: 'order.myOrders.drawer.cargoAttribute',
|
||||
@ -903,6 +922,7 @@ function getRecipientFormKey(key: string) {
|
||||
const basicEditForm = reactive({
|
||||
clientNo: '',
|
||||
downstreamServiceNo: '',
|
||||
warehouseId: undefined as number | undefined,
|
||||
cargoAttribute: 'general',
|
||||
isCod: false,
|
||||
codSum: undefined as number | undefined,
|
||||
@ -913,6 +933,40 @@ const basicEditForm = reactive({
|
||||
|
||||
const currencyOptions = ref<Array<{ value: string; label: string }>>([]);
|
||||
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) {
|
||||
currencyOptionsLoading.value = true;
|
||||
@ -1904,6 +1958,10 @@ function fillBasicEditForm() {
|
||||
if (!o?.id) return;
|
||||
basicEditForm.clientNo = String(o.clientNo ?? o.customerOrderNo ?? '');
|
||||
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.isCod = !!(o.isCod ?? o.IsCod);
|
||||
basicEditForm.codSum = o.codSum != null ? Number(o.codSum) : undefined;
|
||||
@ -1973,6 +2031,10 @@ function startBasicEdit() {
|
||||
}
|
||||
fillBasicEditForm();
|
||||
void ensureCurrencyOptions(basicEditForm.declaredCurrency);
|
||||
void ensureWarehouseOptions({
|
||||
id: basicEditForm.warehouseId,
|
||||
name: String(detail.value?.warehouse ?? detail.value?.warehouseName ?? ''),
|
||||
});
|
||||
basicEditing.value = true;
|
||||
void focusFirstEditableField('.order-detail-drawer .basic-info');
|
||||
}
|
||||
@ -2008,6 +2070,7 @@ async function handleBasicEditSave() {
|
||||
id,
|
||||
clientNo: basicEditForm.clientNo,
|
||||
downstreamServiceNo: basicEditForm.downstreamServiceNo,
|
||||
warehouseId: basicEditForm.warehouseId ?? 0,
|
||||
cargoAttribute: basicEditForm.cargoAttribute,
|
||||
isCod: basicEditForm.isCod,
|
||||
codSum: basicEditForm.codSum,
|
||||
|
||||
@ -59,29 +59,41 @@ const modalVisible = computed({
|
||||
set: (v: boolean) => emit('update:visible', v),
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (open) => {
|
||||
if (!open) return
|
||||
storeLoading.value = true
|
||||
try {
|
||||
const { items } = await getPlatformStoreList({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
customerId: props.customerId,
|
||||
})
|
||||
storeOptions.value = (items || []).map((raw: Record<string, unknown>) => {
|
||||
async function loadStoreOptions() {
|
||||
storeIds.value = []
|
||||
storeLoading.value = true
|
||||
try {
|
||||
const { items } = await getPlatformStoreList({
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
customerId: props.customerId,
|
||||
})
|
||||
storeOptions.value = (items || [])
|
||||
.map((raw: Record<string, unknown>) => {
|
||||
const id = Number(pickField(raw, 'id', 'Id'))
|
||||
const name = String(pickField(raw, 'storeNickname', 'StoreNickname') ?? `#${id}`)
|
||||
const platform = String(pickField(raw, 'platformCode', 'PlatformCode') ?? '')
|
||||
return { label: platform ? `${name} (${platform})` : name, value: id }
|
||||
}).filter((x) => x.value > 0)
|
||||
} catch {
|
||||
storeOptions.value = []
|
||||
} finally {
|
||||
storeLoading.value = false
|
||||
})
|
||||
.filter((x) => x.value > 0)
|
||||
if (!storeOptions.value.length) {
|
||||
$baseMessage(t('order.platformStoreOrder.message.noStore'), 'warning')
|
||||
}
|
||||
} catch (e: any) {
|
||||
storeOptions.value = []
|
||||
$baseMessage(e?.message || t('order.platformStoreOrder.message.loadStoreFailed'), 'error')
|
||||
} finally {
|
||||
storeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** v-if 挂载时 visible 已是 true,必须 immediate,否则不会请求店铺列表 */
|
||||
watch(
|
||||
() => props.visible,
|
||||
(open) => {
|
||||
if (open) void loadStoreOptions()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function handleSubmit(): Promise<boolean> {
|
||||
|
||||
@ -134,7 +134,24 @@
|
||||
<div class="detail-field-value">
|
||||
<template v-if="basicEditing && isOrderFieldEditable(fieldKey)">
|
||||
<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]"
|
||||
allow-clear
|
||||
size="small"
|
||||
@ -980,6 +997,7 @@ import {
|
||||
} from '@/api/order';
|
||||
import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesProduct';
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||
import { getWarehouseList } from '@/api/system/warehouse';
|
||||
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
||||
import { getBooleanTagStyle } from '@/utils/statusTag';
|
||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||
@ -1109,6 +1127,7 @@ const orderEditFieldDefs: FormFieldDef[] = [
|
||||
{ key: 'clientNo', labelKey: 'order.myOrders.drawer.clientNo' },
|
||||
{ key: 'trackNo', labelKey: 'order.myOrders.drawer.trackNo' },
|
||||
{ key: 'downstreamServiceNo', labelKey: 'order.myOrders.drawer.downstreamServiceNo' },
|
||||
{ key: 'warehouse', labelKey: 'order.myOrders.drawer.warehouse', inputType: 'select' },
|
||||
{
|
||||
key: 'cargoAttribute',
|
||||
labelKey: 'order.myOrders.drawer.cargoAttribute',
|
||||
@ -1160,6 +1179,7 @@ const basicEditForm = reactive({
|
||||
clientNo: '',
|
||||
trackNo: '',
|
||||
downstreamServiceNo: '',
|
||||
warehouseId: undefined as number | undefined,
|
||||
cargoAttribute: 'general',
|
||||
isCod: false,
|
||||
codSum: undefined as number | undefined,
|
||||
@ -1170,6 +1190,40 @@ const basicEditForm = reactive({
|
||||
|
||||
const currencyOptions = ref<Array<{ value: string; label: string }>>([]);
|
||||
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) {
|
||||
currencyOptionsLoading.value = true;
|
||||
@ -2392,6 +2446,10 @@ function fillBasicEditForm() {
|
||||
basicEditForm.clientNo = String(o.clientNo ?? o.customerOrderNo ?? '');
|
||||
basicEditForm.trackNo = String(o.trackNo ?? o.trackingNo ?? '');
|
||||
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.isCod = !!(o.isCod ?? o.IsCod);
|
||||
basicEditForm.codSum = o.codSum != null ? Number(o.codSum) : undefined;
|
||||
@ -2457,6 +2515,10 @@ async function focusFirstEditableField(rootSelector: string) {
|
||||
function startBasicEdit() {
|
||||
fillBasicEditForm();
|
||||
void ensureCurrencyOptions(basicEditForm.declaredCurrency);
|
||||
void ensureWarehouseOptions({
|
||||
id: basicEditForm.warehouseId,
|
||||
name: String(detail.value?.warehouse ?? detail.value?.warehouseName ?? ''),
|
||||
});
|
||||
basicEditing.value = true;
|
||||
void focusFirstEditableField('.order-detail-drawer .basic-info');
|
||||
}
|
||||
@ -2494,6 +2556,7 @@ async function handleBasicEditSave() {
|
||||
clientNo: basicEditForm.clientNo,
|
||||
trackNo: basicEditForm.trackNo,
|
||||
downstreamServiceNo: basicEditForm.downstreamServiceNo,
|
||||
warehouseId: basicEditForm.warehouseId ?? 0,
|
||||
cargoAttribute: basicEditForm.cargoAttribute,
|
||||
isCod: basicEditForm.isCod,
|
||||
codSum: basicEditForm.codSum,
|
||||
|
||||
@ -276,9 +276,9 @@ export default defineConfig(({ mode }) => {
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
},
|
||||
// 开发环境面单 PDF 预览:同源代理 COS 签名链接(dev 后端桶可能与 printUrl 不一致)
|
||||
// 开发环境面单/截图:同源代理 COS(自定义域名 https 不可用,上游走 http)
|
||||
'/__cos__': {
|
||||
target: 'https://cos.ideship.com',
|
||||
target: 'http://cos.ideship.com',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
xfwd: false,
|
||||
@ -287,7 +287,7 @@ export default defineConfig(({ mode }) => {
|
||||
router: (req) => {
|
||||
const raw = String(req.originalUrl || req.url || '').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__\/[^/]+/, '') || '/',
|
||||
configure: (proxy) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user