Compare commits
3 Commits
cd02a48017
...
7f49b26ac7
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f49b26ac7 | |||
| b178a2a35d | |||
| 108083d332 |
@ -252,6 +252,9 @@ server {
|
||||
location = /system/announcement {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location = /system/country {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
location ^~ /system/users {
|
||||
try_files /index.html =200;
|
||||
}
|
||||
|
||||
@ -142,6 +142,7 @@ server {
|
||||
location = /system/dict { try_files /index.html =200; }
|
||||
location = /system/warehouseAddress { try_files /index.html =200; }
|
||||
location = /system/announcement { try_files /index.html =200; }
|
||||
location = /system/country { try_files /index.html =200; }
|
||||
location ^~ /system/users { try_files /index.html =200; }
|
||||
location ^~ /system/roles { try_files /index.html =200; }
|
||||
location ^~ /system/menus { try_files /index.html =200; }
|
||||
|
||||
@ -177,6 +177,7 @@ server {
|
||||
location = /system/dict { try_files $uri @spa; }
|
||||
location = /system/warehouseAddress { try_files $uri @spa; }
|
||||
location = /system/announcement { try_files $uri @spa; }
|
||||
location = /system/country { try_files $uri @spa; }
|
||||
location ^~ /system/users { try_files $uri @spa; }
|
||||
location ^~ /system/roles { try_files $uri @spa; }
|
||||
location ^~ /system/menus { try_files $uri @spa; }
|
||||
|
||||
@ -63,6 +63,25 @@ function normalizeRegionTrend(raw: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
export type MonitorDimension = 'day' | 'week' | 'month'
|
||||
|
||||
export interface MonitorOpsStats {
|
||||
dimension: MonitorDimension
|
||||
orderCount: number
|
||||
orderCountGrowthPercent: number | null
|
||||
revenue: number
|
||||
revenueGrowthPercent: number | null
|
||||
grossProfit: number
|
||||
grossProfitGrowthPercent: number | null
|
||||
}
|
||||
|
||||
export interface MonitorQueueStats {
|
||||
pendingPayment: number
|
||||
pendingOnline: number
|
||||
pendingSign: number
|
||||
problematic: number
|
||||
}
|
||||
|
||||
export interface MonitorOverview {
|
||||
order: {
|
||||
todayCreated: number
|
||||
@ -76,6 +95,8 @@ export interface MonitorOverview {
|
||||
todayRevenue: number
|
||||
todayRevenueGrowthPercent: number | null
|
||||
}
|
||||
ops: MonitorOpsStats
|
||||
queue: MonitorQueueStats
|
||||
customer: {
|
||||
totalCustomers: number
|
||||
totalCustomersMonthGrowthPercent: number | null
|
||||
@ -85,7 +106,11 @@ export interface MonitorOverview {
|
||||
inactiveLogin7d: number
|
||||
inactiveLogin7dDelta: number
|
||||
}
|
||||
orderTrend: Array<{ label: string; count: number }>
|
||||
revenueTrend: Array<{ label: string; revenue: number; grossProfit: number }>
|
||||
/** @deprecated 兼容旧字段;优先用 orderTrend */
|
||||
orderTrend7d: Array<{ label: string; count: number }>
|
||||
/** @deprecated 兼容旧字段;优先用 revenueTrend */
|
||||
revenueTrend7d: Array<{ label: string; revenue: number; grossProfit: number }>
|
||||
regionOrderTrend: ReturnType<typeof normalizeRegionTrend>
|
||||
regionReceivableTrend: ReturnType<typeof normalizeRegionTrend>
|
||||
@ -97,26 +122,110 @@ export interface MonitorOverview {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOrderTrend(raw: unknown): Array<{ label: string; count: number }> {
|
||||
if (!Array.isArray(raw)) return []
|
||||
return raw.map((item) => {
|
||||
const row = item as Record<string, unknown>
|
||||
return {
|
||||
label: String(row.label ?? row.Label ?? ''),
|
||||
count: num(row.count ?? row.Count),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeRevenueTrend(
|
||||
raw: unknown,
|
||||
): Array<{ label: string; revenue: number; grossProfit: number }> {
|
||||
if (!Array.isArray(raw)) return []
|
||||
return raw.map((item) => {
|
||||
const row = item as Record<string, unknown>
|
||||
return {
|
||||
label: String(row.label ?? row.Label ?? ''),
|
||||
revenue: num(row.revenue ?? row.Revenue),
|
||||
grossProfit: num(row.grossProfit ?? row.GrossProfit),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeOverview(raw: Record<string, unknown>): MonitorOverview {
|
||||
const order = (raw.order ?? raw.Order ?? {}) as Record<string, unknown>
|
||||
const ops = (raw.ops ?? raw.Ops ?? {}) as Record<string, unknown>
|
||||
const queue = (raw.queue ?? raw.Queue ?? {}) as Record<string, unknown>
|
||||
const customer = (raw.customer ?? raw.Customer ?? {}) as Record<string, unknown>
|
||||
const track = (raw.trackRates ?? raw.TrackRates ?? {}) as Record<string, unknown>
|
||||
const orderTrendRaw = raw.orderTrend7d ?? raw.OrderTrend7d
|
||||
const revenueTrendRaw = raw.revenueTrend7d ?? raw.RevenueTrend7d
|
||||
const signRatesRaw = raw.regionSignRates ?? raw.RegionSignRates
|
||||
|
||||
const orderTrend =
|
||||
normalizeOrderTrend(raw.orderTrend ?? raw.OrderTrend).length > 0
|
||||
? normalizeOrderTrend(raw.orderTrend ?? raw.OrderTrend)
|
||||
: normalizeOrderTrend(raw.orderTrend7d ?? raw.OrderTrend7d)
|
||||
const revenueTrend =
|
||||
normalizeRevenueTrend(raw.revenueTrend ?? raw.RevenueTrend).length > 0
|
||||
? normalizeRevenueTrend(raw.revenueTrend ?? raw.RevenueTrend)
|
||||
: normalizeRevenueTrend(raw.revenueTrend7d ?? raw.RevenueTrend7d)
|
||||
|
||||
const pendingPayment = num(
|
||||
queue.pendingPayment ?? queue.PendingPayment ?? order.pendingPayment ?? order.PendingPayment,
|
||||
)
|
||||
const pendingOnline = num(
|
||||
queue.pendingOnline ?? queue.PendingOnline ?? order.pendingOnline ?? order.PendingOnline,
|
||||
)
|
||||
const pendingSign = num(
|
||||
queue.pendingSign ?? queue.PendingSign ?? order.pendingSign ?? order.PendingSign,
|
||||
)
|
||||
const problematic = num(
|
||||
queue.problematic ?? queue.Problematic ?? order.problematic ?? order.Problematic,
|
||||
)
|
||||
|
||||
const dimRaw = String(ops.dimension ?? ops.Dimension ?? 'day').toLowerCase()
|
||||
const dimension: MonitorDimension =
|
||||
dimRaw === 'week' || dimRaw === 'month' ? dimRaw : 'day'
|
||||
|
||||
return {
|
||||
order: {
|
||||
todayCreated: num(order.todayCreated ?? order.TodayCreated),
|
||||
todayCreatedGrowthPercent: growth(order.todayCreatedGrowthPercent ?? order.TodayCreatedGrowthPercent),
|
||||
todayCreatedGrowthPercent: growth(
|
||||
order.todayCreatedGrowthPercent ?? order.TodayCreatedGrowthPercent,
|
||||
),
|
||||
total: num(order.total ?? order.Total),
|
||||
totalMonthGrowthPercent: growth(order.totalMonthGrowthPercent ?? order.TotalMonthGrowthPercent),
|
||||
pendingPayment: num(order.pendingPayment ?? order.PendingPayment),
|
||||
pendingOnline: num(order.pendingOnline ?? order.PendingOnline),
|
||||
pendingSign: num(order.pendingSign ?? order.PendingSign),
|
||||
problematic: num(order.problematic ?? order.Problematic),
|
||||
totalMonthGrowthPercent: growth(
|
||||
order.totalMonthGrowthPercent ?? order.TotalMonthGrowthPercent,
|
||||
),
|
||||
pendingPayment,
|
||||
pendingOnline,
|
||||
pendingSign,
|
||||
problematic,
|
||||
todayRevenue: num(order.todayRevenue ?? order.TodayRevenue),
|
||||
todayRevenueGrowthPercent: growth(order.todayRevenueGrowthPercent ?? order.TodayRevenueGrowthPercent),
|
||||
todayRevenueGrowthPercent: growth(
|
||||
order.todayRevenueGrowthPercent ?? order.TodayRevenueGrowthPercent,
|
||||
),
|
||||
},
|
||||
ops: {
|
||||
dimension,
|
||||
orderCount: num(ops.orderCount ?? ops.OrderCount ?? order.todayCreated ?? order.TodayCreated),
|
||||
orderCountGrowthPercent: growth(
|
||||
ops.orderCountGrowthPercent ??
|
||||
ops.OrderCountGrowthPercent ??
|
||||
order.todayCreatedGrowthPercent ??
|
||||
order.TodayCreatedGrowthPercent,
|
||||
),
|
||||
revenue: num(ops.revenue ?? ops.Revenue ?? order.todayRevenue ?? order.TodayRevenue),
|
||||
revenueGrowthPercent: growth(
|
||||
ops.revenueGrowthPercent ??
|
||||
ops.RevenueGrowthPercent ??
|
||||
order.todayRevenueGrowthPercent ??
|
||||
order.TodayRevenueGrowthPercent,
|
||||
),
|
||||
grossProfit: num(ops.grossProfit ?? ops.GrossProfit),
|
||||
grossProfitGrowthPercent: growth(
|
||||
ops.grossProfitGrowthPercent ?? ops.GrossProfitGrowthPercent,
|
||||
),
|
||||
},
|
||||
queue: {
|
||||
pendingPayment,
|
||||
pendingOnline,
|
||||
pendingSign,
|
||||
problematic,
|
||||
},
|
||||
customer: {
|
||||
totalCustomers: num(customer.totalCustomers ?? customer.TotalCustomers),
|
||||
@ -131,27 +240,14 @@ function normalizeOverview(raw: Record<string, unknown>): MonitorOverview {
|
||||
inactiveLogin7d: num(customer.inactiveLogin7d ?? customer.InactiveLogin7d),
|
||||
inactiveLogin7dDelta: num(customer.inactiveLogin7dDelta ?? customer.InactiveLogin7dDelta),
|
||||
},
|
||||
orderTrend7d: Array.isArray(orderTrendRaw)
|
||||
? orderTrendRaw.map((item) => {
|
||||
const row = item as Record<string, unknown>
|
||||
return {
|
||||
label: String(row.label ?? row.Label ?? ''),
|
||||
count: num(row.count ?? row.Count),
|
||||
}
|
||||
})
|
||||
: [],
|
||||
revenueTrend7d: Array.isArray(revenueTrendRaw)
|
||||
? revenueTrendRaw.map((item) => {
|
||||
const row = item as Record<string, unknown>
|
||||
return {
|
||||
label: String(row.label ?? row.Label ?? ''),
|
||||
revenue: num(row.revenue ?? row.Revenue),
|
||||
grossProfit: num(row.grossProfit ?? row.GrossProfit),
|
||||
}
|
||||
})
|
||||
: [],
|
||||
orderTrend,
|
||||
revenueTrend,
|
||||
orderTrend7d: orderTrend,
|
||||
revenueTrend7d: revenueTrend,
|
||||
regionOrderTrend: normalizeRegionTrend(raw.regionOrderTrend ?? raw.RegionOrderTrend),
|
||||
regionReceivableTrend: normalizeRegionTrend(raw.regionReceivableTrend ?? raw.RegionReceivableTrend),
|
||||
regionReceivableTrend: normalizeRegionTrend(
|
||||
raw.regionReceivableTrend ?? raw.RegionReceivableTrend,
|
||||
),
|
||||
regionSignRates: Array.isArray(signRatesRaw)
|
||||
? signRatesRaw.map((item) => {
|
||||
const row = item as Record<string, unknown>
|
||||
@ -169,9 +265,13 @@ function normalizeOverview(raw: Record<string, unknown>): MonitorOverview {
|
||||
}
|
||||
}
|
||||
|
||||
/** 实时监控概览 GET /monitor/overview */
|
||||
export async function getMonitorOverview(): Promise<MonitorOverview> {
|
||||
const res = await axiosInstance.get(monitorUrl('/monitor/overview'))
|
||||
/** 实时监控概览 GET /monitor/overview?dimension=day|week|month */
|
||||
export async function getMonitorOverview(
|
||||
dimension: MonitorDimension = 'day',
|
||||
): Promise<MonitorOverview> {
|
||||
const res = await axiosInstance.get(monitorUrl('/monitor/overview'), {
|
||||
params: { dimension },
|
||||
})
|
||||
assertBusinessOk(res.data)
|
||||
const payload = res.data as Record<string, unknown>
|
||||
const data = (payload.data ?? payload.Data ?? payload) as Record<string, unknown>
|
||||
|
||||
@ -23,7 +23,8 @@ export function omsUrl(path: string): string {
|
||||
}
|
||||
|
||||
export function assertBusinessOk(body: any) {
|
||||
if (body == null) throw new Error('空响应')
|
||||
// Task/void 接口常返回 HTTP 200 空 body,视为成功
|
||||
if (body == null || body === '') return
|
||||
const code = body.code ?? body.Code
|
||||
const ok =
|
||||
code === undefined ||
|
||||
|
||||
@ -35,3 +35,14 @@ export const fetchCustoms9610ManifestBatch = (ids: Array<number | string>) =>
|
||||
|
||||
export const fetchCustoms9610DeclarationFormBatch = (ids: Array<number | string>) =>
|
||||
postOk('/order/customs-9610/fetch-declaration-form-batch', { ids })
|
||||
|
||||
export interface Customs9610CreateFromOrdersBody {
|
||||
orderIds: number[]
|
||||
/** 不传则按各运单客户的默认出口资质备案 */
|
||||
exportQualificationId?: number
|
||||
platformCode: string
|
||||
}
|
||||
|
||||
/** 从已有运单批量创建 9610 汇总申报单;逐单返回成功/失败 */
|
||||
export const createCustoms9610FromOrders = (body: Customs9610CreateFromOrdersBody) =>
|
||||
postOk('/order/customs-9610/create-from-orders', body as unknown as Record<string, unknown>)
|
||||
|
||||
@ -271,9 +271,94 @@ async function postOrderById(path: string, body: OrderIdRequest): Promise<void>
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
export interface OrderPayRequest extends OrderIdRequest {
|
||||
/** 不传沿用订单已绑定券;0 不用券;大于 0 使用指定券 */
|
||||
couponInstanceId?: number
|
||||
}
|
||||
|
||||
/** POST /order/pay */
|
||||
export function postOrderPay(body: OrderIdRequest) {
|
||||
return postOrderById('/order/pay', body)
|
||||
export async function postOrderPay(body: OrderPayRequest): Promise<void> {
|
||||
const payload: Record<string, unknown> = { id: body.id }
|
||||
if (body.couponInstanceId != null) payload.couponInstanceId = body.couponInstanceId
|
||||
const res = await axiosInstance.post(orderUrl('/order/pay'), payload)
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
export interface OrderPayCouponOption {
|
||||
couponInstanceId: number
|
||||
couponCode: string
|
||||
templateName: string
|
||||
couponType: string
|
||||
minOrderAmount: number
|
||||
faceValue?: number
|
||||
discountRate?: number
|
||||
maxDiscountAmount?: number
|
||||
validEndTime?: string
|
||||
canUse: boolean
|
||||
unavailableReason?: string
|
||||
estimatedDiscount?: number
|
||||
}
|
||||
|
||||
export interface OrderPayPreview {
|
||||
id: number
|
||||
receivableSubtotal: number
|
||||
baseFreightAmount: number
|
||||
levelDiscountAmount: number
|
||||
couponDiscountAmount: number
|
||||
payableAmount: number
|
||||
memberLevelName?: string
|
||||
appliedCouponInstanceId?: number
|
||||
availableCoupons: OrderPayCouponOption[]
|
||||
}
|
||||
|
||||
function toOptionalNumber(v: unknown): number | undefined {
|
||||
if (v === null || v === undefined || v === '') return undefined
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n : undefined
|
||||
}
|
||||
|
||||
function normalizeOrderPayCoupon(row: any): OrderPayCouponOption {
|
||||
return {
|
||||
couponInstanceId: Number(row?.couponInstanceId ?? row?.CouponInstanceId ?? 0),
|
||||
couponCode: String(row?.couponCode ?? row?.CouponCode ?? ''),
|
||||
templateName: String(row?.templateName ?? row?.TemplateName ?? ''),
|
||||
couponType: String(row?.couponType ?? row?.CouponType ?? ''),
|
||||
minOrderAmount: Number(row?.minOrderAmount ?? row?.MinOrderAmount ?? 0) || 0,
|
||||
faceValue: toOptionalNumber(row?.faceValue ?? row?.FaceValue),
|
||||
discountRate: toOptionalNumber(row?.discountRate ?? row?.DiscountRate),
|
||||
maxDiscountAmount: toOptionalNumber(row?.maxDiscountAmount ?? row?.MaxDiscountAmount),
|
||||
validEndTime: row?.validEndTime ?? row?.ValidEndTime ?? undefined,
|
||||
canUse: Boolean(row?.canUse ?? row?.CanUse),
|
||||
unavailableReason: row?.unavailableReason ?? row?.UnavailableReason ?? undefined,
|
||||
estimatedDiscount: toOptionalNumber(row?.estimatedDiscount ?? row?.EstimatedDiscount),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付预览 GET /order/pay-preview
|
||||
* couponInstanceId:不传自动推荐券;0 不用券;大于 0 试算指定券
|
||||
*/
|
||||
export async function getOrderPayPreview(
|
||||
id: number | string,
|
||||
couponInstanceId?: number,
|
||||
): Promise<OrderPayPreview> {
|
||||
const params: Record<string, unknown> = { id }
|
||||
if (couponInstanceId != null) params.couponInstanceId = couponInstanceId
|
||||
const res = await axiosInstance.get(orderUrl('/order/pay-preview'), { params })
|
||||
assertBusinessOk(res.data)
|
||||
const d = res.data?.data ?? res.data?.Data ?? res.data ?? {}
|
||||
const coupons = d.availableCoupons ?? d.AvailableCoupons
|
||||
return {
|
||||
id: Number(d.id ?? d.Id ?? id),
|
||||
receivableSubtotal: Number(d.receivableSubtotal ?? d.ReceivableSubtotal ?? 0) || 0,
|
||||
baseFreightAmount: Number(d.baseFreightAmount ?? d.BaseFreightAmount ?? 0) || 0,
|
||||
levelDiscountAmount: Number(d.levelDiscountAmount ?? d.LevelDiscountAmount ?? 0) || 0,
|
||||
couponDiscountAmount: Number(d.couponDiscountAmount ?? d.CouponDiscountAmount ?? 0) || 0,
|
||||
payableAmount: Number(d.payableAmount ?? d.PayableAmount ?? 0) || 0,
|
||||
memberLevelName: d.memberLevelName ?? d.MemberLevelName ?? undefined,
|
||||
appliedCouponInstanceId: toOptionalNumber(d.appliedCouponInstanceId ?? d.AppliedCouponInstanceId),
|
||||
availableCoupons: (Array.isArray(coupons) ? coupons : []).map(normalizeOrderPayCoupon),
|
||||
}
|
||||
}
|
||||
|
||||
/** POST /order/pre-declare */
|
||||
|
||||
@ -1,8 +1,124 @@
|
||||
import { axiosInstance } from '@/api/rbac'
|
||||
import { omsUrl, parseListPayload } from '@/api/oms/common'
|
||||
import { assertBusinessOk, omsUrl, parseListPayload, unwrapData } from '@/api/oms/common'
|
||||
import * as XLSX from 'xlsx'
|
||||
|
||||
/** GET /system/country/list — data 为数组,含 chineseName、countryCode 等 */
|
||||
export interface CountryDto {
|
||||
id: number
|
||||
chineseName: string
|
||||
englishName: string
|
||||
countryCode: string
|
||||
createTime?: string
|
||||
updateTime?: string
|
||||
}
|
||||
|
||||
export interface CountryImportResult {
|
||||
successCount: number
|
||||
skipCount: number
|
||||
failCount: number
|
||||
deletedCount: number
|
||||
failures: Array<{ rowNumber: number; reason: string }>
|
||||
}
|
||||
|
||||
function pickString(row: Record<string, unknown>, ...keys: string[]) {
|
||||
for (const key of keys) {
|
||||
const value = row[key]
|
||||
if (value != null && String(value).trim()) return String(value).trim()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function mapCountryRow(row: Record<string, unknown>): CountryDto {
|
||||
const id = Number(row.id ?? row.Id)
|
||||
return {
|
||||
id: Number.isFinite(id) ? id : 0,
|
||||
chineseName: pickString(row, 'chineseName', 'ChineseName'),
|
||||
englishName: pickString(row, 'englishName', 'EnglishName'),
|
||||
countryCode: pickString(row, 'countryCode', 'CountryCode').toUpperCase(),
|
||||
createTime: pickString(row, 'createTime', 'CreateTime') || undefined,
|
||||
updateTime: pickString(row, 'updateTime', 'UpdateTime') || undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/** GET /system/country/list */
|
||||
export async function getCountryList() {
|
||||
const res = await axiosInstance.get(omsUrl('/system/country/list'))
|
||||
return parseListPayload(res.data)
|
||||
}
|
||||
|
||||
export async function createCountry(body: {
|
||||
chineseName: string
|
||||
englishName: string
|
||||
countryCode: string
|
||||
}) {
|
||||
const res = await axiosInstance.post(omsUrl('/system/country/create'), body)
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
export async function updateCountry(body: {
|
||||
id: number
|
||||
chineseName: string
|
||||
englishName: string
|
||||
countryCode: string
|
||||
}) {
|
||||
const res = await axiosInstance.post(omsUrl('/system/country/update'), body)
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
export async function deleteCountry(id: number) {
|
||||
const res = await axiosInstance.post(omsUrl('/system/country/delete'), { id })
|
||||
assertBusinessOk(res.data)
|
||||
}
|
||||
|
||||
const TEMPLATE_HEADERS = ['国家二字码', '中文名', '英文名'] as const
|
||||
|
||||
function writeCountryWorkbook(rows: Array<Record<string, string>>, fileName: string) {
|
||||
const wb = XLSX.utils.book_new()
|
||||
const sheet = XLSX.utils.json_to_sheet(rows, { header: [...TEMPLATE_HEADERS] })
|
||||
sheet['!cols'] = [{ wch: 14 }, { wch: 18 }, { wch: 24 }]
|
||||
XLSX.utils.book_append_sheet(wb, sheet, '导入模板')
|
||||
XLSX.writeFile(wb, fileName.endsWith('.xlsx') ? fileName : `${fileName}.xlsx`)
|
||||
}
|
||||
|
||||
export function downloadCountryImportTemplate() {
|
||||
writeCountryWorkbook(
|
||||
[
|
||||
{ 国家二字码: 'CN', 中文名: '中国', 英文名: 'China' },
|
||||
{ 国家二字码: 'US', 中文名: '美国', 英文名: 'United States' },
|
||||
],
|
||||
'国家地区导入模板.xlsx'
|
||||
)
|
||||
}
|
||||
|
||||
export function exportCountryExcel(rows: CountryDto[], fileName = '国家地区.xlsx') {
|
||||
writeCountryWorkbook(
|
||||
rows.map((row) => ({
|
||||
国家二字码: row.countryCode,
|
||||
中文名: row.chineseName,
|
||||
英文名: row.englishName,
|
||||
})),
|
||||
fileName
|
||||
)
|
||||
}
|
||||
|
||||
export async function importCountry(
|
||||
file: File,
|
||||
importMode: 'incremental' | 'full'
|
||||
): Promise<CountryImportResult> {
|
||||
const form = new FormData()
|
||||
form.append('importMode', importMode)
|
||||
form.append('file', file)
|
||||
const res = await axiosInstance.post(omsUrl('/system/country/import'), form)
|
||||
assertBusinessOk(res.data)
|
||||
const d = unwrapData<Record<string, unknown>>(res.data) ?? {}
|
||||
const rawFailures = (d.failures ?? d.Failures ?? []) as Array<Record<string, unknown>>
|
||||
return {
|
||||
successCount: Number(d.successCount ?? d.SuccessCount ?? 0),
|
||||
skipCount: Number(d.skipCount ?? d.SkipCount ?? 0),
|
||||
failCount: Number(d.failCount ?? d.FailCount ?? 0),
|
||||
deletedCount: Number(d.deletedCount ?? d.DeletedCount ?? 0),
|
||||
failures: rawFailures.map((item) => ({
|
||||
rowNumber: Number(item.rowNumber ?? item.RowNumber ?? 0),
|
||||
reason: String(item.reason ?? item.Reason ?? ''),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +51,8 @@ export async function getNoticeList(params: {
|
||||
receiverType?: string
|
||||
keyword?: string
|
||||
onlyUnread?: boolean
|
||||
/** 管理端查看全部(管理员);工作台/H5 勿传 */
|
||||
manage?: boolean
|
||||
}) {
|
||||
const res = await axiosInstance.get(omsUrl('/system/notice/list'), { params })
|
||||
const { items, total } = parseListPayload(res.data)
|
||||
|
||||
7
src/assets/icons/workplace/customer-inactive.svg
Normal file
7
src/assets/icons/workplace/customer-inactive.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="18" r="8" fill="#86909C"/>
|
||||
<path d="M9 41c0-7.5 6.7-11.5 15-11.5S39 33.5 39 41v3H9v-3z" fill="#86909C"/>
|
||||
<circle cx="39" cy="16" r="9" fill="#FF7D00"/>
|
||||
<path d="M39 11.5v5.2l3.2 2.2" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<ellipse cx="21.5" cy="15.2" rx="2.2" ry="1.5" fill="#fff" opacity="0.28"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 474 B |
13
src/assets/icons/workplace/customer-new.svg
Normal file
13
src/assets/icons/workplace/customer-new.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="22" cy="18" r="8" fill="#007979"/>
|
||||
<path d="M7 41c0-7.5 6.7-11.5 15-11.5S37 33.5 37 41v3H7v-3z" fill="url(#cn-body)"/>
|
||||
<circle cx="39" cy="36" r="9" fill="#FF7D00"/>
|
||||
<path d="M39 31.2v9.6M34.2 36h9.6" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/>
|
||||
<ellipse cx="19.8" cy="15.2" rx="2.2" ry="1.5" fill="#fff" opacity="0.3"/>
|
||||
<defs>
|
||||
<linearGradient id="cn-body" x1="7" y1="29" x2="37" y2="44" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00A0A0"/>
|
||||
<stop offset="1" stop-color="#007979"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 677 B |
8
src/assets/icons/workplace/customer-online.svg
Normal file
8
src/assets/icons/workplace/customer-online.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="27" cy="18" r="8" fill="#00B42A"/>
|
||||
<path d="M12 41c0-7.5 6.7-11.5 15-11.5S42 33.5 42 41v3H12v-3z" fill="#00B42A"/>
|
||||
<circle cx="39" cy="14" r="6" fill="#fff"/>
|
||||
<circle cx="39" cy="14" r="4.2" fill="#00B42A"/>
|
||||
<circle cx="39" cy="14" r="2" fill="#fff"/>
|
||||
<ellipse cx="24.5" cy="15.2" rx="2.2" ry="1.5" fill="#fff" opacity="0.35"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 455 B |
7
src/assets/icons/workplace/customer-total.svg
Normal file
7
src/assets/icons/workplace/customer-total.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg width="54" height="54" viewBox="0 0 54 54" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="20" cy="18" r="7" fill="#2D8C8C"/>
|
||||
<path d="M6 40c0-7.2 6.3-11 14-11s14 3.8 14 11v3H6v-3z" fill="#2D8C8C"/>
|
||||
<circle cx="36" cy="17" r="6" fill="#5BB3B3"/>
|
||||
<path d="M27 39.5c1.2-4.8 5.6-7.5 9.8-7.5 4.4 0 8.2 2.6 9.2 7.2V43H27v-3.5z" fill="#5BB3B3"/>
|
||||
<ellipse cx="18.2" cy="15.5" rx="2" ry="1.4" fill="#fff" opacity="0.35"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 448 B |
@ -1475,6 +1475,9 @@
|
||||
manualColumnResize: true,
|
||||
manualColumnMove: props.enableColumnReorder,
|
||||
stretchH: props.stretchColumns ? 'all' : 'none',
|
||||
// 列宽由列定义给出。开启后 loadData 会走 GhostTable 测量,
|
||||
// 单元格节点缺失时抛 getBoundingClientRect,并中断外层页面更新。
|
||||
autoColumnSize: false,
|
||||
// stretch 时锁定操作/序号/复选框列,避免被均分拉宽
|
||||
beforeStretchingColumnWidth: lockFixedColumnStretchWidth,
|
||||
...(props.rowHeight != null && props.rowHeight > 0
|
||||
@ -2362,6 +2365,8 @@
|
||||
hotInstance.loadData([]);
|
||||
hotInstance.render();
|
||||
publishSelection([], new Set());
|
||||
} catch (error) {
|
||||
console.warn('[HandsontablePro] clear failed', error);
|
||||
} finally {
|
||||
selectionLock -= 1;
|
||||
}
|
||||
@ -2393,6 +2398,8 @@
|
||||
} else {
|
||||
applySelectedRowStyles();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[HandsontablePro] refresh failed', error);
|
||||
} finally {
|
||||
selectionLock -= 1;
|
||||
}
|
||||
|
||||
9
src/constants/feeCostItems.ts
Normal file
9
src/constants/feeCostItems.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/** 营销抵扣费用项:支付时由后端写入的负数应收行,不可在价格配置/手工加费中选择 */
|
||||
export const COUPON_DISCOUNT_COST_ITEM = 'coupon_discount'
|
||||
export const MEMBER_DISCOUNT_COST_ITEM = 'member_discount'
|
||||
|
||||
const DISCOUNT_COST_ITEMS = new Set([COUPON_DISCOUNT_COST_ITEM, MEMBER_DISCOUNT_COST_ITEM])
|
||||
|
||||
export function isDiscountCostItem(costItem: unknown) {
|
||||
return DISCOUNT_COST_ITEMS.has(String(costItem ?? '').trim().toLowerCase())
|
||||
}
|
||||
@ -626,7 +626,7 @@ const CORE_GROUPS: PageButtonGroup[] = [
|
||||
{ code: 'Reset', name: '重置条件' },
|
||||
{ code: 'Update', name: '编辑' },
|
||||
{ code: 'View', name: '详情' },
|
||||
{ code: 'DeclareBatch', name: '批量申报' },
|
||||
{ code: 'DeclareBatch', name: '创建9610订单' },
|
||||
{ code: 'FetchManifestBatch', name: '批量获取清单数据' },
|
||||
{ code: 'FetchDeclarationFormBatch', name: '预报下游' },
|
||||
]),
|
||||
@ -1030,6 +1030,14 @@ const CORE_GROUPS: PageButtonGroup[] = [
|
||||
{ code: 'Delete', name: '删除' },
|
||||
{ code: 'Copy', name: '复制' },
|
||||
]),
|
||||
B('System.Country', [
|
||||
{ code: 'Query', name: '查询' },
|
||||
{ code: 'Create', name: '新增' },
|
||||
{ code: 'Update', name: '修改' },
|
||||
{ code: 'Delete', name: '删除' },
|
||||
{ code: 'Import', name: '导入' },
|
||||
{ code: 'Export', name: '导出' },
|
||||
]),
|
||||
B('System.OtherConfig', [
|
||||
{ code: 'Query', name: '查询' },
|
||||
{ code: 'AddConfig', name: '新增配置' },
|
||||
|
||||
161
src/hooks/orderPayCoupon.ts
Normal file
161
src/hooks/orderPayCoupon.ts
Normal file
@ -0,0 +1,161 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import {
|
||||
getOrderPayPreview,
|
||||
type OrderPayCouponOption,
|
||||
type OrderPayPreview,
|
||||
} from '@/api/order';
|
||||
import { runWithConcurrency } from '@/utils/runWithConcurrency';
|
||||
|
||||
type OrderKey = number | string;
|
||||
|
||||
export interface OrderPayCouponEntry {
|
||||
pricing: OrderPayPreview | null;
|
||||
/** 0 表示不用券 */
|
||||
selectedCouponId: number;
|
||||
loading: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface UseOrderPayCouponOptions {
|
||||
/** 选了券但后端按会员折计算(券未生效) */
|
||||
onNotApplied?: () => void;
|
||||
onError?: (message: string) => void;
|
||||
}
|
||||
|
||||
const PREVIEW_CONCURRENCY = 4;
|
||||
|
||||
export { runWithConcurrency };
|
||||
|
||||
export function formatPayMoney(amount: number | undefined | null) {
|
||||
const n = Number(amount);
|
||||
return `¥${(Number.isFinite(n) ? n : 0).toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** 券面优惠描述:优先本单预估抵扣,否则按券面值/折扣率 */
|
||||
export function formatCouponBenefit(coupon: OrderPayCouponOption) {
|
||||
if (coupon.estimatedDiscount != null && coupon.estimatedDiscount > 0) {
|
||||
return `-${formatPayMoney(coupon.estimatedDiscount)}`;
|
||||
}
|
||||
if (coupon.couponType === 'percent_off' && coupon.discountRate) {
|
||||
return `-${Math.round((1 - coupon.discountRate) * 100)}%`;
|
||||
}
|
||||
return coupon.faceValue != null ? formatPayMoney(coupon.faceValue) : '';
|
||||
}
|
||||
|
||||
export function useOrderPayCoupon(options: UseOrderPayCouponOptions = {}) {
|
||||
const entries = ref<Record<string, OrderPayCouponEntry>>({});
|
||||
|
||||
function entryOf(id: OrderKey): OrderPayCouponEntry | undefined {
|
||||
return entries.value[String(id)];
|
||||
}
|
||||
|
||||
function takenByOthers(id: OrderKey) {
|
||||
const self = String(id);
|
||||
const taken = new Set<number>();
|
||||
for (const [key, entry] of Object.entries(entries.value)) {
|
||||
if (key !== self && entry.selectedCouponId > 0) taken.add(entry.selectedCouponId);
|
||||
}
|
||||
return taken;
|
||||
}
|
||||
|
||||
function isCouponTakenByOther(id: OrderKey, couponId: number) {
|
||||
return takenByOthers(id).has(couponId);
|
||||
}
|
||||
|
||||
async function loadAll(orderIds: OrderKey[]) {
|
||||
const next: Record<string, OrderPayCouponEntry> = {};
|
||||
for (const id of orderIds) {
|
||||
next[String(id)] = { pricing: null, selectedCouponId: 0, loading: true };
|
||||
}
|
||||
entries.value = next;
|
||||
|
||||
await runWithConcurrency(orderIds, PREVIEW_CONCURRENCY, async (id) => {
|
||||
const entry = entryOf(id);
|
||||
if (!entry) return;
|
||||
try {
|
||||
const pricing = await getOrderPayPreview(id);
|
||||
entry.pricing = pricing;
|
||||
entry.selectedCouponId = pricing.appliedCouponInstanceId ?? 0;
|
||||
} catch (e: any) {
|
||||
entry.error = e?.message || String(e);
|
||||
} finally {
|
||||
entry.loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 各单独立推荐可能撞同一张券:按勾选顺序先到先得,后者改用下一张或不用券
|
||||
const taken = new Set<number>();
|
||||
for (const id of orderIds) {
|
||||
const entry = entryOf(id);
|
||||
if (!entry?.pricing || entry.selectedCouponId <= 0) continue;
|
||||
if (!taken.has(entry.selectedCouponId)) {
|
||||
taken.add(entry.selectedCouponId);
|
||||
continue;
|
||||
}
|
||||
const alt = entry.pricing.availableCoupons.find(
|
||||
(c) => c.canUse && !taken.has(c.couponInstanceId),
|
||||
);
|
||||
entry.loading = true;
|
||||
try {
|
||||
const pricing = await getOrderPayPreview(id, alt?.couponInstanceId ?? 0);
|
||||
entry.pricing = pricing;
|
||||
entry.selectedCouponId = pricing.appliedCouponInstanceId ?? 0;
|
||||
} catch (e: any) {
|
||||
entry.error = e?.message || String(e);
|
||||
} finally {
|
||||
entry.loading = false;
|
||||
}
|
||||
if (entry.selectedCouponId > 0) taken.add(entry.selectedCouponId);
|
||||
}
|
||||
}
|
||||
|
||||
function removeEntry(id: OrderKey) {
|
||||
const rest = { ...entries.value };
|
||||
delete rest[String(id)];
|
||||
entries.value = rest;
|
||||
}
|
||||
|
||||
async function selectCoupon(id: OrderKey, couponId: number) {
|
||||
const entry = entryOf(id);
|
||||
if (!entry || entry.loading || entry.selectedCouponId === couponId) return;
|
||||
entry.loading = true;
|
||||
try {
|
||||
const pricing = await getOrderPayPreview(id, couponId);
|
||||
entry.pricing = pricing;
|
||||
entry.selectedCouponId = pricing.appliedCouponInstanceId ?? 0;
|
||||
if (couponId > 0 && entry.selectedCouponId !== couponId) options.onNotApplied?.();
|
||||
} catch (e: any) {
|
||||
options.onError?.(e?.message || String(e));
|
||||
} finally {
|
||||
entry.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
entries.value = {};
|
||||
}
|
||||
|
||||
const anyLoading = computed(() => Object.values(entries.value).some((x) => x.loading));
|
||||
|
||||
const allReady = computed(() => {
|
||||
const list = Object.values(entries.value);
|
||||
return list.length > 0 && list.every((x) => !x.loading && !x.error && x.pricing);
|
||||
});
|
||||
|
||||
const totalPayable = computed(() =>
|
||||
Object.values(entries.value).reduce((sum, x) => sum + (x.pricing?.payableAmount ?? 0), 0),
|
||||
);
|
||||
|
||||
return {
|
||||
entries,
|
||||
entryOf,
|
||||
isCouponTakenByOther,
|
||||
loadAll,
|
||||
selectCoupon,
|
||||
removeEntry,
|
||||
reset,
|
||||
anyLoading,
|
||||
allReady,
|
||||
totalPayable,
|
||||
};
|
||||
}
|
||||
@ -654,6 +654,38 @@ export default {
|
||||
'order.customs9610.table.createTime': 'Created',
|
||||
'order.customs9610.actions.declare': 'Declare',
|
||||
'order.customs9610.actions.batchDeclare': 'Batch declare',
|
||||
'order.customs9610.actions.createFromOrders': 'Create 9610 orders',
|
||||
'order.customs9610.create.title': 'Create 9610 orders',
|
||||
'order.customs9610.create.tip': 'Pick orders and fill in the export qualification and e-commerce platform. The orders are switched to 9610 summary declaration and pending declarations are created. Deleted orders, orders that already have a 9610 record, or orders without declaration items will fail without affecting the others.',
|
||||
'order.customs9610.create.sectionSupplement': 'Declaration info',
|
||||
'order.customs9610.create.sectionOrders': 'Select orders',
|
||||
'order.customs9610.create.customer': 'Customer',
|
||||
'order.customs9610.create.customerPh': 'Select customer',
|
||||
'order.customs9610.create.exportQualification': 'Export qualification',
|
||||
'order.customs9610.create.exportQualificationPh': "Leave empty to use the customer's default",
|
||||
'order.customs9610.create.manageQualification': 'Manage',
|
||||
'order.customs9610.create.defaultSuffix': ' (default)',
|
||||
'order.customs9610.create.platform': 'E-commerce platform',
|
||||
'order.customs9610.create.platformPh': 'Select platform',
|
||||
'order.customs9610.create.keywordPh': 'Waybill / client no. / tracking no.',
|
||||
'order.customs9610.create.selected': '{count} selected',
|
||||
'order.customs9610.create.clearSelected': 'Clear',
|
||||
'order.customs9610.create.col.serviceNo': 'Waybill no.',
|
||||
'order.customs9610.create.col.clientNo': 'Client no.',
|
||||
'order.customs9610.create.col.customer': 'Customer',
|
||||
'order.customs9610.create.col.country': 'Destination',
|
||||
'order.customs9610.create.col.status': 'Status',
|
||||
'order.customs9610.create.col.createTime': 'Created',
|
||||
'order.customs9610.create.col.remark': 'Note',
|
||||
'order.customs9610.create.already9610': 'Already 9610',
|
||||
'order.customs9610.create.selectCustomerFirst': 'Select a customer first',
|
||||
'order.customs9610.create.emptyOrders': 'No orders',
|
||||
'order.customs9610.create.submit': 'Create 9610 orders ({count})',
|
||||
'order.customs9610.create.failTitle': '{count} failed',
|
||||
'order.customs9610.create.message.loadOrdersFailed': 'Failed to load orders',
|
||||
'order.customs9610.create.message.allSuccess': '{count} 9610 orders created',
|
||||
'order.customs9610.create.message.partial': '{success} succeeded, {fail} failed. See reasons below.',
|
||||
'order.customs9610.create.message.failed': 'Create failed',
|
||||
'order.customs9610.actions.batchFetchManifest': 'Batch fetch manifest',
|
||||
'order.customs9610.actions.batchFetchDeclarationForm': 'Forward Downstream',
|
||||
'order.customs9610.modal.edit': 'Edit 9610 declaration',
|
||||
@ -969,6 +1001,9 @@ export default {
|
||||
'order.platformStoreOrder.table.customsOrderNo': 'Order No',
|
||||
'order.platformStoreOrder.table.store': 'Store',
|
||||
'order.platformStoreOrder.table.status': 'Status',
|
||||
'order.platformStoreOrder.table.platformStatus': 'Platform Status',
|
||||
'order.platformStoreOrder.table.fulfillmentType': 'Fulfillment Type',
|
||||
'order.platformStoreOrder.table.shippingProvider': 'Shipping Provider',
|
||||
'order.platformStoreOrder.table.amount': 'Amount',
|
||||
'order.platformStoreOrder.table.orderTime': 'Order Time',
|
||||
'order.platformStoreOrder.table.linkedServiceNo': 'Linked Service No',
|
||||
@ -982,8 +1017,8 @@ export default {
|
||||
'order.platformStoreOrder.drawer.sectionShipping': 'Fulfillment & Shipping',
|
||||
'order.platformStoreOrder.drawer.sectionLines': 'Line Items',
|
||||
'order.platformStoreOrder.drawer.platformStatus': 'Platform Status',
|
||||
'order.platformStoreOrder.drawer.customTags': 'Custom Tags',
|
||||
'order.platformStoreOrder.drawer.multiLabel': 'Multi Label',
|
||||
'order.platformStoreOrder.drawer.shippingProvider': 'Shipping Provider',
|
||||
'order.platformStoreOrder.drawer.fulfillmentType': 'Fulfillment Type',
|
||||
'order.platformStoreOrder.drawer.openLabel': 'Open Label',
|
||||
'order.platformStoreOrder.recipient.name': 'Recipient',
|
||||
'order.platformStoreOrder.recipient.company': 'Company',
|
||||
@ -1004,7 +1039,6 @@ export default {
|
||||
'order.platformStoreOrder.line.image': 'Image',
|
||||
'order.platformStoreOrder.line.variant': 'Variant',
|
||||
'order.platformStoreOrder.line.price': 'Unit Price',
|
||||
'order.platformStoreOrder.line.excluded': 'Excluded',
|
||||
'order.platformStoreOrder.actions.match': 'Match Inventory',
|
||||
'order.platformStoreOrder.actions.batchMatch': 'Batch Match',
|
||||
'order.platformStoreOrder.actions.suspend': 'Suspend',
|
||||
@ -1015,6 +1049,7 @@ export default {
|
||||
'order.platformStoreOrder.actions.syncStatus': 'Sync Status',
|
||||
'order.platformStoreOrder.actions.readyToShip': 'Ready to Ship',
|
||||
'order.platformStoreOrder.actions.fetchLabel': 'Re-fetch Label',
|
||||
'order.platformStoreOrder.actions.fetchLabelProgress': 'Fetching {done}/{total}',
|
||||
'order.platformStoreOrder.actions.createLogistics': 'Create Waybill',
|
||||
'order.platformStoreOrder.actions.createFulfill': 'Create Fulfill & Waybill',
|
||||
'order.platformStoreOrder.actions.createFulfillWms': 'Create Fulfill Order',
|
||||
@ -1079,15 +1114,33 @@ export default {
|
||||
'order.platformStoreOrder.createFulfillWms.success': 'Created {count} fulfill order(s)',
|
||||
'order.platformStoreOrder.pull.store': 'Store',
|
||||
'order.platformStoreOrder.pull.storePlaceholder': 'Select stores',
|
||||
'order.platformStoreOrder.pull.storeAll': 'All stores',
|
||||
'order.platformStoreOrder.pull.status': 'Status',
|
||||
'order.platformStoreOrder.pull.statusAll': 'All',
|
||||
'order.platformStoreOrder.pull.statusClear': 'Clear',
|
||||
'order.platformStoreOrder.pull.statusCount': '{count} selected',
|
||||
'order.platformStoreOrder.pull.time': 'Time',
|
||||
'order.platformStoreOrder.pull.timeHint': 'Defaults to today',
|
||||
'order.platformStoreOrder.pull.timeHint': 'Filter by order created time. Defaults to today.',
|
||||
'order.platformStoreOrder.pull.today': 'Today',
|
||||
'order.platformStoreOrder.pull.last7': '7 days',
|
||||
'order.platformStoreOrder.pull.last15': '15 days',
|
||||
'order.platformStoreOrder.pull.status.UNPAID': 'Unpaid',
|
||||
'order.platformStoreOrder.pull.status.ON_HOLD': 'On hold',
|
||||
'order.platformStoreOrder.pull.status.PARTIALLY_SHIPPING': 'Partially shipping',
|
||||
'order.platformStoreOrder.pull.status.AWAITING_SHIPMENT': 'Awaiting shipment',
|
||||
'order.platformStoreOrder.pull.status.AWAITING_COLLECTION': 'Awaiting collection',
|
||||
'order.platformStoreOrder.pull.status.IN_TRANSIT': 'In transit',
|
||||
'order.platformStoreOrder.pull.status.DELIVERED': 'Delivered',
|
||||
'order.platformStoreOrder.pull.status.COMPLETED': 'Completed',
|
||||
'order.platformStoreOrder.pull.status.CANCELLED': 'Cancelled',
|
||||
'order.platformStoreOrder.message.loadFailed': 'Failed to load orders',
|
||||
'order.platformStoreOrder.message.detailFailed': 'Failed to load detail',
|
||||
'order.platformStoreOrder.message.matchSuccess': 'Inventory matched',
|
||||
'order.platformStoreOrder.message.matchFailed': 'Match failed',
|
||||
'order.platformStoreOrder.message.warehouseRequired': 'Select warehouse',
|
||||
'order.platformStoreOrder.message.suspendSuccess': 'Status updated',
|
||||
'order.platformStoreOrder.message.suspendConfirm': 'Suspend the selected {count} order(s)?',
|
||||
'order.platformStoreOrder.message.resumeConfirm': 'Resume the selected {count} order(s)?',
|
||||
'order.platformStoreOrder.message.deleteSuccess': 'Deleted',
|
||||
'order.platformStoreOrder.message.batchEmpty': 'No pending orders to match on this page',
|
||||
'order.platformStoreOrder.message.batchConfirm': 'Match {count} pending orders on this page and create parcel orders?',
|
||||
@ -1100,12 +1153,15 @@ export default {
|
||||
'order.platformStoreOrder.message.syncConfirm': 'Sync status for {count} orders?',
|
||||
'order.platformStoreOrder.message.rtsConfirm': 'Ready-to-ship {count} orders?',
|
||||
'order.platformStoreOrder.message.labelConfirm': 'Re-fetch shipping label for this order?',
|
||||
'order.platformStoreOrder.message.labelConfirmBatch': 'Re-fetch labels for the selected {count} order(s)?',
|
||||
'order.platformStoreOrder.message.labelResult': 'Label fetch done: {success} succeeded, {fail} failed',
|
||||
'order.platformStoreOrder.message.syncResult': 'Sync done: {success} succeeded, {fail} failed',
|
||||
'order.platformStoreOrder.message.rtsResult': 'RTS done: {success} succeeded, {fail} failed',
|
||||
'order.platformStoreOrder.message.pullResult': 'Pull done: {success} stores ok, {fail} failed',
|
||||
'order.platformStoreOrder.message.pullFailed': 'Pull failed',
|
||||
'order.platformStoreOrder.message.labelSuccess': 'Label updated',
|
||||
'order.platformStoreOrder.message.labelFailed': 'Failed to fetch label',
|
||||
'order.platformStoreOrder.message.printNoLabel': 'No printable label available',
|
||||
'order.platformStoreOrder.message.temuNoRts': 'Temu does not support RTS',
|
||||
'order.platformStoreOrder.message.actionFailed': 'Action failed',
|
||||
'menu.channel': 'Channel',
|
||||
@ -1292,6 +1348,7 @@ export default {
|
||||
'menu.system.interface': 'System Interface',
|
||||
'menu.system.interface.config': 'Interface Config',
|
||||
'menu.system.warehouseAddress': 'Warehouse Address',
|
||||
'menu.system.country': 'Countries & Regions',
|
||||
'menu.system.batchEmail': 'Batch Email',
|
||||
'menu.system.aiAssistant': 'AI Assistant',
|
||||
'menu.system.aiSkills': 'AI Skills',
|
||||
@ -1773,6 +1830,40 @@ export default {
|
||||
'order.myOrders.modal.payFailed': 'Payment failed',
|
||||
'order.myOrders.modal.payLoadFailed': 'Failed to load fee details',
|
||||
'order.myOrders.modal.payInvalidAmount': 'Invalid receivable amount',
|
||||
'order.myOrders.modal.payPendingOnly': 'Only orders pending payment can be paid. Please reselect.',
|
||||
'order.myOrders.modal.payCoupon': 'Coupon',
|
||||
'order.myOrders.modal.payCouponNone': 'No coupon',
|
||||
'order.myOrders.modal.payCouponEmpty': 'No coupons available',
|
||||
'order.myOrders.modal.payCouponUsedByOther': 'Used by another order',
|
||||
'order.myOrders.modal.payCouponNotApplied': 'This coupon is not better than your member discount; member discount applied',
|
||||
'order.myOrders.modal.paySubtotal': 'Fees total',
|
||||
'order.myOrders.modal.payLevelDiscount': 'Member discount',
|
||||
'order.myOrders.modal.payCouponDiscount': 'Coupon discount',
|
||||
'order.myOrders.modal.payPayable': 'To pay',
|
||||
'order.myOrders.modal.payTitleBatch': 'Confirm batch payment ({count} orders)',
|
||||
'order.myOrders.modal.paySummaryPayable': 'Total to pay',
|
||||
'order.myOrders.modal.paySummarySaved': 'Saved {amount}',
|
||||
'order.myOrders.modal.paySummaryOrders': 'Orders',
|
||||
'order.myOrders.modal.payOrdersUnit': '{count}',
|
||||
'order.myOrders.modal.payBalance': 'Balance',
|
||||
'order.myOrders.modal.payBalanceShort': 'Insufficient balance, {amount} short. Top up or pay fewer orders.',
|
||||
'order.myOrders.modal.payBalanceShortCustomer': '{name}: insufficient balance, {amount} short',
|
||||
'order.myOrders.modal.paySkipHint': '{count} order(s) cannot be paid now and will be skipped',
|
||||
'order.myOrders.modal.payOrderList': 'Orders',
|
||||
'order.myOrders.modal.payExpandAll': 'Expand all',
|
||||
'order.myOrders.modal.payCollapseAll': 'Collapse all',
|
||||
'order.myOrders.modal.payCouponUsableCount': '{count} available',
|
||||
'order.myOrders.modal.payRemove': 'Remove from this payment',
|
||||
'order.myOrders.modal.payStatusPaying': 'Paying',
|
||||
'order.myOrders.modal.payStatusSuccess': 'Paid',
|
||||
'order.myOrders.modal.payStatusFailed': 'Failed',
|
||||
'order.myOrders.modal.payStatusSkipped': 'Skipped',
|
||||
'order.myOrders.modal.payConfirmAmount': 'Pay {amount}',
|
||||
'order.myOrders.modal.payRetryFailed': 'Retry {amount}',
|
||||
'order.myOrders.modal.payProgress': 'Paying {done}/{total}',
|
||||
'order.myOrders.modal.payBatchSuccess': '{count} orders paid',
|
||||
'order.myOrders.modal.payPartialResult': '{success} paid, {failed} failed. Check the reasons and retry.',
|
||||
'order.myOrders.modal.payMethodBalance': 'Payment method: account balance',
|
||||
'order.myOrders.modal.payEmpty': 'No orders to pay',
|
||||
'order.myOrders.actions.intercept': 'Intercept',
|
||||
'order.myOrders.actions.cancelIntercept': 'Cancel intercept',
|
||||
@ -2358,6 +2449,9 @@ export default {
|
||||
'order.myOrders.drawer.declarationImHsCode': 'Import HS code',
|
||||
'order.myOrders.drawer.declarationExHsCode': 'Export HS code',
|
||||
'order.myOrders.drawer.declarationDeclareElement': 'Export declare element',
|
||||
'order.myOrders.drawer.declarationProductId': 'Product ID',
|
||||
'order.myOrders.drawer.declarationNonStdProductId': 'Non-standard product ID',
|
||||
'order.myOrders.drawer.declarationStdProductId': 'Standard product ID',
|
||||
'order.myOrders.drawer.declarationSkuImage': 'Image',
|
||||
'order.myOrders.drawer.declarationSkuImageView': 'View image',
|
||||
'order.myOrders.drawer.declarationCategory': 'Category',
|
||||
@ -2447,6 +2541,7 @@ export default {
|
||||
'order.myOrders.drawer.fee.payableTitle': 'Payable Details',
|
||||
'order.myOrders.drawer.fee.viewAll': 'All',
|
||||
'order.myOrders.drawer.fee.itemCount': '{count} items',
|
||||
'order.myOrders.drawer.fee.includesDiscount': 'Incl. discount {amount}',
|
||||
'order.myOrders.drawer.fee.emptyReceivable': 'No receivable fees',
|
||||
'order.myOrders.drawer.fee.emptyPayable': 'No payable fees',
|
||||
'order.myOrders.drawer.fee.emptyAll': 'No fee data',
|
||||
@ -4236,6 +4331,57 @@ export default {
|
||||
'system.warehouseAddress.message.dictLoadFailed': 'Failed to load warehouse type dictionary',
|
||||
'system.warehouseAddress.message.wechatQrSaveFailed': 'Failed to save WeChat QR image',
|
||||
|
||||
'system.country.searchPlaceholder': 'Code / Chinese / English name',
|
||||
'system.country.fields.countryCode': 'Country Code',
|
||||
'system.country.fields.chineseName': 'Chinese Name',
|
||||
'system.country.fields.englishName': 'English Name',
|
||||
'system.country.fields.createTime': 'Created At',
|
||||
'system.country.fields.updateTime': 'Updated At',
|
||||
'system.country.table.index': 'No.',
|
||||
'system.country.placeholder.input': 'Please enter',
|
||||
'system.country.placeholder.countryCode': 'e.g. CN',
|
||||
'system.country.modal.addTitle': 'Add Country / Region',
|
||||
'system.country.modal.editTitle': 'Edit Country / Region',
|
||||
'system.country.actions.edit': 'Edit',
|
||||
'system.country.actions.delete': 'Delete',
|
||||
'system.country.actions.import': 'Import',
|
||||
'system.country.actions.export': 'Export',
|
||||
'system.country.rules.countryCodeRequired': 'Please enter the country code',
|
||||
'system.country.rules.countryCodeFormat': 'Use 2 letters, e.g. CN, US',
|
||||
'system.country.rules.chineseNameRequired': 'Please enter the Chinese name',
|
||||
'system.country.rules.englishNameRequired': 'Please enter the English name',
|
||||
'system.country.message.createSuccess': 'Created',
|
||||
'system.country.message.updateSuccess': 'Saved',
|
||||
'system.country.message.saveFailed': 'Save failed',
|
||||
'system.country.message.deleteConfirm': 'Delete "{name}"?',
|
||||
'system.country.message.deleteSuccess': 'Deleted',
|
||||
'system.country.message.deleteFailed': 'Delete failed',
|
||||
'system.country.message.loadFailed': 'Load failed',
|
||||
'system.country.message.exportEmpty': 'Nothing to export',
|
||||
'system.country.message.exportSuccess': 'Exported {count} rows',
|
||||
'system.country.message.exportFailed': 'Export failed',
|
||||
'system.country.import.modalTitle': 'Import Countries',
|
||||
'system.country.import.tip': 'Download the template first. Country code, Chinese name, and English name are required. Code is ISO alpha-2, e.g. CN, US.',
|
||||
'system.country.import.modeLabel': 'Import mode',
|
||||
'system.country.import.mode.incremental': 'Incremental',
|
||||
'system.country.import.mode.full': 'Full',
|
||||
'system.country.import.mode.incrementalTip': 'Insert or update by country code. Countries missing from the file are kept.',
|
||||
'system.country.import.mode.fullTip': 'Insert or update by country code, then soft-delete countries missing from the file. Export a backup first.',
|
||||
'system.country.import.mode.fullConfirm': 'Full import will soft-delete countries that are not in the Excel file. Continue?',
|
||||
'system.country.import.warningTitle': 'Confirm full import',
|
||||
'system.country.import.downloadTemplate': 'Download template',
|
||||
'system.country.import.submit': 'Import',
|
||||
'system.country.import.cancel': 'Cancel',
|
||||
'system.country.import.downloadSuccess': 'Template downloaded',
|
||||
'system.country.import.downloadFailed': 'Template download failed',
|
||||
'system.country.import.needFile': 'Please choose an Excel file',
|
||||
'system.country.import.success': 'Imported',
|
||||
'system.country.import.failed': 'Import failed',
|
||||
'system.country.import.partialSuccess': 'Some rows failed. See the details below.',
|
||||
'system.country.import.allFailed': 'Import failed. Check the file contents.',
|
||||
'system.country.import.resultSummary': 'Success {success}, skipped {skip}, failed {fail}, deleted {deleted}',
|
||||
'system.country.import.failRow': 'Row {row}: {reason}',
|
||||
|
||||
// System - batch email (parity with desktop emailAuto.py)
|
||||
'system.batchEmail.import': 'Import file',
|
||||
'system.batchEmail.template': 'Template',
|
||||
|
||||
@ -647,6 +647,38 @@ export default {
|
||||
'order.customs9610.table.createTime': '创建时间',
|
||||
'order.customs9610.actions.declare': '申报',
|
||||
'order.customs9610.actions.batchDeclare': '批量申报',
|
||||
'order.customs9610.actions.createFromOrders': '创建9610订单',
|
||||
'order.customs9610.create.title': '创建9610订单',
|
||||
'order.customs9610.create.tip': '选择运单并补充出口资质备案、电商平台后,系统会把运单设为 9610 汇总申报并生成待申报单。已删除、已有 9610 单或没有申报明细的运单会创建失败,不影响其它运单。',
|
||||
'order.customs9610.create.sectionSupplement': '补充申报信息',
|
||||
'order.customs9610.create.sectionOrders': '选择运单',
|
||||
'order.customs9610.create.customer': '客户',
|
||||
'order.customs9610.create.customerPh': '请选择客户',
|
||||
'order.customs9610.create.exportQualification': '出口资质备案',
|
||||
'order.customs9610.create.exportQualificationPh': '不选则使用客户默认备案',
|
||||
'order.customs9610.create.manageQualification': '管理备案',
|
||||
'order.customs9610.create.defaultSuffix': '(默认)',
|
||||
'order.customs9610.create.platform': '电商平台',
|
||||
'order.customs9610.create.platformPh': '请选择电商平台',
|
||||
'order.customs9610.create.keywordPh': '运单号 / 客户单号 / 跟踪号',
|
||||
'order.customs9610.create.selected': '已选 {count} 单',
|
||||
'order.customs9610.create.clearSelected': '清空',
|
||||
'order.customs9610.create.col.serviceNo': '运单号',
|
||||
'order.customs9610.create.col.clientNo': '客户单号',
|
||||
'order.customs9610.create.col.customer': '客户',
|
||||
'order.customs9610.create.col.country': '目的国',
|
||||
'order.customs9610.create.col.status': '订单状态',
|
||||
'order.customs9610.create.col.createTime': '创建时间',
|
||||
'order.customs9610.create.col.remark': '说明',
|
||||
'order.customs9610.create.already9610': '已是9610',
|
||||
'order.customs9610.create.selectCustomerFirst': '请先选择客户',
|
||||
'order.customs9610.create.emptyOrders': '暂无运单',
|
||||
'order.customs9610.create.submit': '创建 9610 订单({count})',
|
||||
'order.customs9610.create.failTitle': '{count} 单创建失败',
|
||||
'order.customs9610.create.message.loadOrdersFailed': '加载运单失败',
|
||||
'order.customs9610.create.message.allSuccess': '已创建 {count} 个 9610 订单',
|
||||
'order.customs9610.create.message.partial': '成功 {success} 单,失败 {fail} 单,请查看失败原因',
|
||||
'order.customs9610.create.message.failed': '创建失败',
|
||||
'order.customs9610.actions.batchFetchManifest': '批量获取清单数据',
|
||||
'order.customs9610.actions.batchFetchDeclarationForm': '预报下游',
|
||||
'order.customs9610.modal.edit': '编辑 9610 申报',
|
||||
@ -958,6 +990,9 @@ export default {
|
||||
'order.platformStoreOrder.table.customsOrderNo': '平台订单号',
|
||||
'order.platformStoreOrder.table.store': '店铺',
|
||||
'order.platformStoreOrder.table.status': '状态',
|
||||
'order.platformStoreOrder.table.platformStatus': '平台状态',
|
||||
'order.platformStoreOrder.table.fulfillmentType': '履约类型',
|
||||
'order.platformStoreOrder.table.shippingProvider': '物流商',
|
||||
'order.platformStoreOrder.table.amount': '金额',
|
||||
'order.platformStoreOrder.table.orderTime': '下单时间',
|
||||
'order.platformStoreOrder.table.linkedServiceNo': '关联服务单号',
|
||||
@ -971,8 +1006,8 @@ export default {
|
||||
'order.platformStoreOrder.drawer.sectionShipping': '履约与物流',
|
||||
'order.platformStoreOrder.drawer.sectionLines': '商品明细',
|
||||
'order.platformStoreOrder.drawer.platformStatus': '平台状态',
|
||||
'order.platformStoreOrder.drawer.customTags': '自定义标签',
|
||||
'order.platformStoreOrder.drawer.multiLabel': '多面单',
|
||||
'order.platformStoreOrder.drawer.shippingProvider': '物流商',
|
||||
'order.platformStoreOrder.drawer.fulfillmentType': '履约类型',
|
||||
'order.platformStoreOrder.drawer.openLabel': '打开面单',
|
||||
'order.platformStoreOrder.recipient.name': '收件人',
|
||||
'order.platformStoreOrder.recipient.company': '公司',
|
||||
@ -993,7 +1028,6 @@ export default {
|
||||
'order.platformStoreOrder.line.image': '图片',
|
||||
'order.platformStoreOrder.line.variant': '规格',
|
||||
'order.platformStoreOrder.line.price': '单价',
|
||||
'order.platformStoreOrder.line.excluded': '拒发',
|
||||
'order.platformStoreOrder.actions.match': '匹配库存',
|
||||
'order.platformStoreOrder.actions.batchMatch': '批量匹配',
|
||||
'order.platformStoreOrder.actions.suspend': '搁置',
|
||||
@ -1004,6 +1038,7 @@ export default {
|
||||
'order.platformStoreOrder.actions.syncStatus': '同步状态',
|
||||
'order.platformStoreOrder.actions.readyToShip': '准备发货',
|
||||
'order.platformStoreOrder.actions.fetchLabel': '重新获取面单',
|
||||
'order.platformStoreOrder.actions.fetchLabelProgress': '获取中 {done}/{total}',
|
||||
'order.platformStoreOrder.actions.createLogistics': '创建运单',
|
||||
'order.platformStoreOrder.actions.createFulfill': '创建代发&运单',
|
||||
'order.platformStoreOrder.actions.createFulfillWms': '创建代发订单',
|
||||
@ -1070,15 +1105,33 @@ export default {
|
||||
'order.platformStoreOrder.createFulfillWms.success': '成功创建 {count} 张代发订单',
|
||||
'order.platformStoreOrder.pull.store': '店铺',
|
||||
'order.platformStoreOrder.pull.storePlaceholder': '请选择店铺',
|
||||
'order.platformStoreOrder.pull.status': '类型',
|
||||
'order.platformStoreOrder.pull.storeAll': '全选店铺',
|
||||
'order.platformStoreOrder.pull.status': '状态',
|
||||
'order.platformStoreOrder.pull.statusAll': '全选',
|
||||
'order.platformStoreOrder.pull.statusClear': '清空',
|
||||
'order.platformStoreOrder.pull.statusCount': '已选 {count} 个',
|
||||
'order.platformStoreOrder.pull.time': '时间',
|
||||
'order.platformStoreOrder.pull.timeHint': '默认时间为当天',
|
||||
'order.platformStoreOrder.pull.timeHint': '按订单创建时间拉取,默认当天',
|
||||
'order.platformStoreOrder.pull.today': '今天',
|
||||
'order.platformStoreOrder.pull.last7': '近7天',
|
||||
'order.platformStoreOrder.pull.last15': '近15天',
|
||||
'order.platformStoreOrder.pull.status.UNPAID': '未付款',
|
||||
'order.platformStoreOrder.pull.status.ON_HOLD': '待处理',
|
||||
'order.platformStoreOrder.pull.status.PARTIALLY_SHIPPING': '部分发货',
|
||||
'order.platformStoreOrder.pull.status.AWAITING_SHIPMENT': '待发货',
|
||||
'order.platformStoreOrder.pull.status.AWAITING_COLLECTION': '待揽收',
|
||||
'order.platformStoreOrder.pull.status.IN_TRANSIT': '运输中',
|
||||
'order.platformStoreOrder.pull.status.DELIVERED': '已送达',
|
||||
'order.platformStoreOrder.pull.status.COMPLETED': '已完成',
|
||||
'order.platformStoreOrder.pull.status.CANCELLED': '已取消',
|
||||
'order.platformStoreOrder.message.loadFailed': '加载订单失败',
|
||||
'order.platformStoreOrder.message.detailFailed': '加载详情失败',
|
||||
'order.platformStoreOrder.message.matchSuccess': '库存匹配成功',
|
||||
'order.platformStoreOrder.message.matchFailed': '库存匹配失败',
|
||||
'order.platformStoreOrder.message.warehouseRequired': '请选择发货仓库',
|
||||
'order.platformStoreOrder.message.suspendSuccess': '状态已更新',
|
||||
'order.platformStoreOrder.message.suspendConfirm': '确认搁置选中的 {count} 单?',
|
||||
'order.platformStoreOrder.message.resumeConfirm': '确认恢复选中的 {count} 单?',
|
||||
'order.platformStoreOrder.message.deleteSuccess': '删除成功',
|
||||
'order.platformStoreOrder.message.batchEmpty': '当前页没有可匹配的待处理订单',
|
||||
'order.platformStoreOrder.message.batchConfirm': '将匹配当前页 {count} 个待处理订单并创建小包单,是否继续?',
|
||||
@ -1091,12 +1144,15 @@ export default {
|
||||
'order.platformStoreOrder.message.syncConfirm': '是否手动同步 {count} 个订单的状态?',
|
||||
'order.platformStoreOrder.message.rtsConfirm': '是否对 {count} 个订单执行准备发货(RTS)?',
|
||||
'order.platformStoreOrder.message.labelConfirm': '是否重新获取该订单面单?',
|
||||
'order.platformStoreOrder.message.labelConfirmBatch': '确认重新获取选中的 {count} 单面单?',
|
||||
'order.platformStoreOrder.message.labelResult': '面单获取完成:成功 {success} 单,失败 {fail} 单',
|
||||
'order.platformStoreOrder.message.syncResult': '同步完成:成功 {success},失败 {fail}',
|
||||
'order.platformStoreOrder.message.rtsResult': 'RTS 完成:成功 {success},失败 {fail}',
|
||||
'order.platformStoreOrder.message.pullResult': '拉单完成:成功 {success} 店,失败 {fail} 店',
|
||||
'order.platformStoreOrder.message.pullFailed': '拉单失败',
|
||||
'order.platformStoreOrder.message.labelSuccess': '面单已更新',
|
||||
'order.platformStoreOrder.message.labelFailed': '获取面单失败',
|
||||
'order.platformStoreOrder.message.printNoLabel': '暂无可打印面单',
|
||||
'order.platformStoreOrder.message.temuNoRts': 'Temu 不支持 RTS',
|
||||
'order.platformStoreOrder.message.actionFailed': '操作失败',
|
||||
|
||||
@ -1285,6 +1341,7 @@ export default {
|
||||
'menu.system.interface.config': '接口配置',
|
||||
'menu.system.message': '我的信息',
|
||||
'menu.system.warehouseAddress': '仓库地址',
|
||||
'menu.system.country': '国家地区',
|
||||
'menu.system.batchEmail': '批量发送邮件',
|
||||
'menu.system.aiAssistant': 'AI 助手',
|
||||
'menu.system.aiSkills': 'AI Skill',
|
||||
@ -1601,6 +1658,40 @@ export default {
|
||||
'order.myOrders.modal.payFailed': '支付失败',
|
||||
'order.myOrders.modal.payLoadFailed': '加载费用明细失败',
|
||||
'order.myOrders.modal.payInvalidAmount': '应收金额无效,无法支付',
|
||||
'order.myOrders.modal.payPendingOnly': '仅待支付状态的订单可支付,请重新选择',
|
||||
'order.myOrders.modal.payCoupon': '优惠券',
|
||||
'order.myOrders.modal.payCouponNone': '不使用优惠券',
|
||||
'order.myOrders.modal.payCouponEmpty': '暂无可用券',
|
||||
'order.myOrders.modal.payCouponUsedByOther': '已用于其他订单',
|
||||
'order.myOrders.modal.payCouponNotApplied': '该券优惠不高于会员折扣,已按会员折扣计算',
|
||||
'order.myOrders.modal.paySubtotal': '费用合计',
|
||||
'order.myOrders.modal.payLevelDiscount': '会员折扣',
|
||||
'order.myOrders.modal.payCouponDiscount': '优惠券抵扣',
|
||||
'order.myOrders.modal.payPayable': '实付',
|
||||
'order.myOrders.modal.payTitleBatch': '批量支付确认({count} 单)',
|
||||
'order.myOrders.modal.paySummaryPayable': '实付合计',
|
||||
'order.myOrders.modal.paySummarySaved': '已优惠 {amount}',
|
||||
'order.myOrders.modal.paySummaryOrders': '本次支付',
|
||||
'order.myOrders.modal.payOrdersUnit': '{count} 单',
|
||||
'order.myOrders.modal.payBalance': '账户余额',
|
||||
'order.myOrders.modal.payBalanceShort': '账户余额不足,还差 {amount},请先充值或减少本次支付的订单',
|
||||
'order.myOrders.modal.payBalanceShortCustomer': '{name} 余额不足,还差 {amount}',
|
||||
'order.myOrders.modal.paySkipHint': '{count} 单暂不可支付,确认支付时将自动跳过',
|
||||
'order.myOrders.modal.payOrderList': '订单明细',
|
||||
'order.myOrders.modal.payExpandAll': '全部展开',
|
||||
'order.myOrders.modal.payCollapseAll': '全部收起',
|
||||
'order.myOrders.modal.payCouponUsableCount': '{count} 张可用',
|
||||
'order.myOrders.modal.payRemove': '移出本次支付',
|
||||
'order.myOrders.modal.payStatusPaying': '支付中',
|
||||
'order.myOrders.modal.payStatusSuccess': '已支付',
|
||||
'order.myOrders.modal.payStatusFailed': '支付失败',
|
||||
'order.myOrders.modal.payStatusSkipped': '将跳过',
|
||||
'order.myOrders.modal.payConfirmAmount': '确认支付 {amount}',
|
||||
'order.myOrders.modal.payRetryFailed': '重新支付 {amount}',
|
||||
'order.myOrders.modal.payProgress': '支付中 {done}/{total}',
|
||||
'order.myOrders.modal.payBatchSuccess': '已成功支付 {count} 单',
|
||||
'order.myOrders.modal.payPartialResult': '成功 {success} 单,失败 {failed} 单,请查看失败原因后重试',
|
||||
'order.myOrders.modal.payMethodBalance': '支付方式:账户余额',
|
||||
'order.myOrders.modal.payEmpty': '暂无可支付订单',
|
||||
'order.myOrders.actions.intercept': '拦截',
|
||||
'order.myOrders.actions.cancelIntercept': '取消拦截',
|
||||
@ -2219,6 +2310,9 @@ export default {
|
||||
'order.myOrders.drawer.declarationImHsCode': '进口HsCode',
|
||||
'order.myOrders.drawer.declarationExHsCode': '出口HsCode',
|
||||
'order.myOrders.drawer.declarationDeclareElement': '出口申报要素',
|
||||
'order.myOrders.drawer.declarationProductId': '产品标识编码',
|
||||
'order.myOrders.drawer.declarationNonStdProductId': '非标产品标识编码',
|
||||
'order.myOrders.drawer.declarationStdProductId': '标准产品标识编码',
|
||||
'order.myOrders.drawer.declarationSkuImage': '图片',
|
||||
'order.myOrders.drawer.declarationSkuImageView': '查看图片',
|
||||
'order.myOrders.drawer.declarationCategory': '类别',
|
||||
@ -2307,6 +2401,7 @@ export default {
|
||||
'order.myOrders.drawer.fee.payableTitle': '应付费用明细',
|
||||
'order.myOrders.drawer.fee.viewAll': '全部',
|
||||
'order.myOrders.drawer.fee.itemCount': '{count} 项',
|
||||
'order.myOrders.drawer.fee.includesDiscount': '含优惠 {amount}',
|
||||
'order.myOrders.drawer.fee.emptyReceivable': '暂无应收费用',
|
||||
'order.myOrders.drawer.fee.emptyPayable': '暂无应付费用',
|
||||
'order.myOrders.drawer.fee.emptyAll': '暂无费用数据',
|
||||
@ -4262,6 +4357,57 @@ export default {
|
||||
'system.warehouseAddress.message.dictLoadFailed': '仓库类型字典加载失败',
|
||||
'system.warehouseAddress.message.wechatQrSaveFailed': '客服微信二维码保存失败',
|
||||
|
||||
'system.country.searchPlaceholder': '二字码 / 中文名 / 英文名',
|
||||
'system.country.fields.countryCode': '国家二字码',
|
||||
'system.country.fields.chineseName': '中文名',
|
||||
'system.country.fields.englishName': '英文名',
|
||||
'system.country.fields.createTime': '创建时间',
|
||||
'system.country.fields.updateTime': '更新时间',
|
||||
'system.country.table.index': '序号',
|
||||
'system.country.placeholder.input': '请输入',
|
||||
'system.country.placeholder.countryCode': '如 CN',
|
||||
'system.country.modal.addTitle': '新增国家地区',
|
||||
'system.country.modal.editTitle': '编辑国家地区',
|
||||
'system.country.actions.edit': '编辑',
|
||||
'system.country.actions.delete': '删除',
|
||||
'system.country.actions.import': '导入',
|
||||
'system.country.actions.export': '导出',
|
||||
'system.country.rules.countryCodeRequired': '请输入国家二字码',
|
||||
'system.country.rules.countryCodeFormat': '须为 2 位字母,如 CN、US',
|
||||
'system.country.rules.chineseNameRequired': '请输入中文名',
|
||||
'system.country.rules.englishNameRequired': '请输入英文名',
|
||||
'system.country.message.createSuccess': '新增成功',
|
||||
'system.country.message.updateSuccess': '保存成功',
|
||||
'system.country.message.saveFailed': '保存失败',
|
||||
'system.country.message.deleteConfirm': '确定删除「{name}」吗?',
|
||||
'system.country.message.deleteSuccess': '删除成功',
|
||||
'system.country.message.deleteFailed': '删除失败',
|
||||
'system.country.message.loadFailed': '加载失败',
|
||||
'system.country.message.exportEmpty': '没有可导出的数据',
|
||||
'system.country.message.exportSuccess': '已导出 {count} 条',
|
||||
'system.country.message.exportFailed': '导出失败',
|
||||
'system.country.import.modalTitle': '导入国家地区',
|
||||
'system.country.import.tip': '请先下载模板。国家二字码、中文名、英文名均必填。二字码为 ISO 两位字母,如 CN、US。',
|
||||
'system.country.import.modeLabel': '导入方式',
|
||||
'system.country.import.mode.incremental': '增量导入',
|
||||
'system.country.import.mode.full': '全量导入',
|
||||
'system.country.import.mode.incrementalTip': '按二字码新增或更新已有国家,不会删除文件中没有的国家。',
|
||||
'system.country.import.mode.fullTip': '按二字码新增或更新,并软删除文件中未出现的国家(请先导出备份)。',
|
||||
'system.country.import.mode.fullConfirm': '全量导入将软删除 Excel 中未出现的国家,确认继续?',
|
||||
'system.country.import.warningTitle': '确认全量导入',
|
||||
'system.country.import.downloadTemplate': '下载模板',
|
||||
'system.country.import.submit': '开始导入',
|
||||
'system.country.import.cancel': '取消',
|
||||
'system.country.import.downloadSuccess': '模板下载成功',
|
||||
'system.country.import.downloadFailed': '模板下载失败',
|
||||
'system.country.import.needFile': '请选择要上传的 Excel 文件',
|
||||
'system.country.import.success': '导入成功',
|
||||
'system.country.import.failed': '导入失败',
|
||||
'system.country.import.partialSuccess': '部分行导入失败,请查看失败明细',
|
||||
'system.country.import.allFailed': '导入失败,请检查文件内容',
|
||||
'system.country.import.resultSummary': '成功 {success} 条,跳过重复 {skip} 条,失败 {fail} 条,删除 {deleted} 条',
|
||||
'system.country.import.failRow': '第 {row} 行:{reason}',
|
||||
|
||||
// 系统 - 批量邮件(对齐桌面 emailAuto.py)
|
||||
'system.batchEmail.import': '导入文件',
|
||||
'system.batchEmail.template': '发送模板',
|
||||
|
||||
@ -79,6 +79,16 @@ const SYSTEM_MANAGEMENT: AppRouteRecordRaw = {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'country',
|
||||
name: 'SystemCountry',
|
||||
component: () => import('@/views/system/country/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.system.country',
|
||||
requiresAuth: true,
|
||||
menuPermission: 'System.Country.Query',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'otherConfig',
|
||||
name: 'SystemOtherConfig',
|
||||
|
||||
@ -92,6 +92,7 @@ export const MENU_NAME_TO_LOCALE_KEY: Record<string, string> = {
|
||||
系统接口: 'menu.system.interface',
|
||||
接口配置: 'menu.system.interface.config',
|
||||
仓库地址: 'menu.system.warehouseAddress',
|
||||
国家地区: 'menu.system.country',
|
||||
批量发送邮件: 'menu.system.batchEmail',
|
||||
'AI 助手': 'menu.system.aiAssistant',
|
||||
'AI Skill': 'menu.system.aiSkills',
|
||||
|
||||
@ -77,6 +77,7 @@ export const SUB_MENU_DEFAULT_ICONS: Record<string, string> = {
|
||||
'menu.system.module': 'IconApps',
|
||||
'menu.system.menu': 'IconMenu',
|
||||
'menu.system.warehouseAddress': 'IconHome',
|
||||
'menu.system.country': 'IconPublic',
|
||||
'menu.system.otherConfig': 'IconSettings',
|
||||
'menu.system.aiAssistant': 'IconRobot',
|
||||
'menu.system.aiSkills': 'IconBook',
|
||||
|
||||
@ -35,6 +35,9 @@ function mapItem(raw: Record<string, any>) {
|
||||
imHsCode: pick(raw, 'imHsCode', 'ImHsCode') ?? hs,
|
||||
exHsCode: pick(raw, 'exHsCode', 'ExHsCode') ?? hs,
|
||||
declareElement: pick(raw, 'declareElement', 'DeclareElement'),
|
||||
productId: pick(raw, 'productId', 'ProductId'),
|
||||
nonStdProductId: pick(raw, 'nonStdProductId', 'NonStdProductId'),
|
||||
stdProductId: pick(raw, 'stdProductId', 'StdProductId'),
|
||||
skuImage: pick(raw, 'skuImage', 'SkuImage'),
|
||||
category: pick(raw, 'category', 'Category'),
|
||||
brand: pick(raw, 'brand', 'Brand'),
|
||||
|
||||
16
src/utils/runWithConcurrency.ts
Normal file
16
src/utils/runWithConcurrency.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/** 限并发执行,避免批量操作一次打出几十个请求 */
|
||||
export async function runWithConcurrency<T>(
|
||||
items: T[],
|
||||
limit: number,
|
||||
worker: (item: T, index: number) => Promise<void>,
|
||||
) {
|
||||
let cursor = 0
|
||||
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
||||
while (cursor < items.length) {
|
||||
const index = cursor
|
||||
cursor += 1
|
||||
await worker(items[index], index)
|
||||
}
|
||||
})
|
||||
await Promise.all(runners)
|
||||
}
|
||||
@ -14,6 +14,14 @@ import {
|
||||
type RoutePageMenuDef,
|
||||
} from '@/utils/syncMenuRouteCatalog';
|
||||
|
||||
function readSyncError(error: unknown): string {
|
||||
if (error && typeof error === 'object' && 'body' in error) {
|
||||
const body = (error as { body?: { message?: string } }).body;
|
||||
if (body?.message) return body.message;
|
||||
}
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
||||
export interface SyncLevel3ButtonsResult {
|
||||
created: number;
|
||||
updated: number;
|
||||
@ -450,7 +458,16 @@ export async function syncMenuTree(menu: MenuTreeDto): Promise<SyncMenuTreeResul
|
||||
let currentParent = menu;
|
||||
|
||||
for (const pageDef of pageDefs) {
|
||||
const { menu: pageMenu, created } = await ensurePageMenu(currentParent, pageDef);
|
||||
let pageMenu: MenuTreeDto;
|
||||
let created = false;
|
||||
try {
|
||||
const ensured = await ensurePageMenu(currentParent, pageDef);
|
||||
pageMenu = ensured.menu;
|
||||
created = ensured.created;
|
||||
} catch (error: unknown) {
|
||||
level3FailedPages.push(`${pageDef.name}: ${readSyncError(error)}`);
|
||||
continue;
|
||||
}
|
||||
if (created) {
|
||||
level2Created += 1;
|
||||
const refreshedParent = await reloadMenuNode(menu.id);
|
||||
|
||||
@ -89,6 +89,9 @@ export const ITEM_FIELDS: FieldRow[] = [
|
||||
{ name: 'imHsCode', type: 'string', required: false, desc: '进口 HS' },
|
||||
{ name: 'exHsCode', type: 'string', required: false, desc: '出口 HS' },
|
||||
{ name: 'declareElement', type: 'string', required: false, desc: '出口申报要素' },
|
||||
{ name: 'productId', type: 'string', required: false, desc: '产品标识编码,最长 100' },
|
||||
{ name: 'nonStdProductId', type: 'string', required: false, desc: '非标产品标识编码,最长 100' },
|
||||
{ name: 'stdProductId', type: 'string', required: false, desc: '标准产品标识编码,最长 100' },
|
||||
{ name: 'skuImage', type: 'string', required: false, desc: 'SKU 图片 URL' },
|
||||
{ name: 'category', type: 'string', required: false, desc: '品类' },
|
||||
{ name: 'brand', type: 'string', required: false, desc: '品牌' },
|
||||
@ -180,7 +183,10 @@ export const SAMPLE_PARCEL_BODY = `{
|
||||
"enName": "T-Shirt",
|
||||
"quantity": 1,
|
||||
"unitPrice": 12.5,
|
||||
"unitWeight": 0.2
|
||||
"unitWeight": 0.2,
|
||||
"productId": "P10001",
|
||||
"nonStdProductId": "",
|
||||
"stdProductId": ""
|
||||
}
|
||||
]
|
||||
}`;
|
||||
|
||||
@ -153,6 +153,9 @@ export const ORDER_TABLE_FIELDS: OrderTableFieldDef[] = [
|
||||
{ table: 't_order_declaration', field: 'ImHsCode', label: '进口HS编码', valueType: 'string' },
|
||||
{ table: 't_order_declaration', field: 'ExHsCode', label: '出口HS编码', valueType: 'string' },
|
||||
{ table: 't_order_declaration', field: 'DeclareElement', label: '出口申报要素', valueType: 'string' },
|
||||
{ table: 't_order_declaration', field: 'ProductId', label: '产品标识编码', valueType: 'string' },
|
||||
{ table: 't_order_declaration', field: 'NonStdProductId', label: '非标产品标识编码', valueType: 'string' },
|
||||
{ table: 't_order_declaration', field: 'StdProductId', label: '标准产品标识编码', valueType: 'string' },
|
||||
{ table: 't_order_declaration', field: 'Category', label: '申报类别', valueType: 'string' },
|
||||
{ table: 't_order_declaration', field: 'Brand', label: '品牌', valueType: 'string' },
|
||||
{ table: 't_order_declaration', field: 'BrandType', label: '品牌类型', valueType: 'string' },
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
updateSalesProductPrice,
|
||||
} from '@/api/channel/salesProduct';
|
||||
import { getLabelTemplateList } from '@/api/channel/labelTemplate';
|
||||
import { isDiscountCostItem } from '@/constants/feeCostItems';
|
||||
import { postForewarningServiceKpiAreaGetKpiAreas } from '@/api/channel/kpiArea';
|
||||
import { getWarehouseList } from '@/api/system/warehouse';
|
||||
import { postWeightRuleList } from '@/api/channel/weightRule';
|
||||
@ -2707,7 +2708,9 @@
|
||||
chargingCondition: '无',
|
||||
});
|
||||
|
||||
const costItemOptions = computed(() => mapDictToSelectOptions('fee_item'));
|
||||
const costItemOptions = computed(() =>
|
||||
mapDictToSelectOptions('fee_item').filter((o) => !isDiscountCostItem(o.value)),
|
||||
);
|
||||
|
||||
/** 同一产品下已占用的费用项(编辑当前条时排除自身) */
|
||||
function isCostItemAlreadyUsed(costItem: string) {
|
||||
|
||||
@ -1411,6 +1411,7 @@ import { Message, Modal, Input } from '@arco-design/web-vue'
|
||||
import ModalPro from '@/components/ModalPro/index.vue'
|
||||
import useModal from '@/components/ModalPro/useModal'
|
||||
import PublishDiffPanel from '@/components/PublishDiff/index.vue'
|
||||
import { isDiscountCostItem } from '@/constants/feeCostItems'
|
||||
import PriceListHotTable, {
|
||||
type PriceListHotColumn,
|
||||
} from '@/views/channel/salesProducts/components/PriceListHotTable.vue'
|
||||
@ -4414,7 +4415,9 @@ const factorOptions = computed(() =>
|
||||
label: pricingFactorColumnLabel(opt.value),
|
||||
}))
|
||||
)
|
||||
const costItemOptions = computed(() => mapDictToSelectOptions('fee_item'))
|
||||
const costItemOptions = computed(() =>
|
||||
mapDictToSelectOptions('fee_item').filter((o) => !isDiscountCostItem(o.value)),
|
||||
)
|
||||
|
||||
/** 同一渠道下已占用的费用项(编辑当前条时排除自身) */
|
||||
function isCostItemAlreadyUsed(costItem: string) {
|
||||
|
||||
@ -16,11 +16,6 @@ export default {
|
||||
'h5Admin.login.failed': 'Sign-in failed. Check account or password.',
|
||||
'h5Admin.login.accountRequired': 'Enter account',
|
||||
'h5Admin.login.passwordRequired': 'Enter password',
|
||||
'h5Admin.login.needAgree': 'Please agree to the privacy policy and terms',
|
||||
'h5Admin.login.agreePrefix': 'By signing in you agree to the',
|
||||
'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',
|
||||
|
||||
@ -105,9 +100,26 @@ export default {
|
||||
'h5Admin.monitor.refresh': 'Refresh',
|
||||
'h5Admin.monitor.tab.order': 'Orders',
|
||||
'h5Admin.monitor.tab.customer': 'Customers',
|
||||
'h5Admin.monitor.dim.day': 'Day',
|
||||
'h5Admin.monitor.dim.week': 'Week',
|
||||
'h5Admin.monitor.dim.month': 'Month',
|
||||
'h5Admin.monitor.ops.title': 'Ops overview',
|
||||
'h5Admin.monitor.customer.title': 'Customers',
|
||||
'h5Admin.monitor.track.title': 'Track KPIs',
|
||||
'h5Admin.monitor.ops.orderCount.day': 'Orders today',
|
||||
'h5Admin.monitor.ops.orderCount.week': 'Orders this week',
|
||||
'h5Admin.monitor.ops.orderCount.month': 'Orders this month',
|
||||
'h5Admin.monitor.ops.revenue.day': 'Revenue today',
|
||||
'h5Admin.monitor.ops.revenue.week': 'Revenue this week',
|
||||
'h5Admin.monitor.ops.revenue.month': 'Revenue this month',
|
||||
'h5Admin.monitor.ops.grossProfit.day': 'Gross profit today',
|
||||
'h5Admin.monitor.ops.grossProfit.week': 'Gross profit this week',
|
||||
'h5Admin.monitor.ops.grossProfit.month': 'Gross profit this month',
|
||||
'h5Admin.monitor.queue.title': 'Live backlog',
|
||||
'h5Admin.monitor.kpi.todayCreated': 'Created today',
|
||||
'h5Admin.monitor.kpi.pendingPayment': 'Unpaid',
|
||||
'h5Admin.monitor.kpi.pendingOnline': 'Pending online',
|
||||
'h5Admin.monitor.kpi.pendingSign': 'Pending delivery',
|
||||
'h5Admin.monitor.kpi.problematic': 'Issues',
|
||||
'h5Admin.monitor.kpi.todayRevenue': 'Revenue today',
|
||||
'h5Admin.monitor.kpi.total': 'Total shipments',
|
||||
@ -116,10 +128,19 @@ export default {
|
||||
'h5Admin.monitor.kpi.online': 'Online',
|
||||
'h5Admin.monitor.kpi.inactive7d': 'Inactive 7d',
|
||||
'h5Admin.monitor.chart.orders7d': 'Orders · last 7 days',
|
||||
'h5Admin.monitor.chart.ordersDay': 'Orders · last 7 days',
|
||||
'h5Admin.monitor.chart.ordersWeek': 'Orders · last 8 weeks',
|
||||
'h5Admin.monitor.chart.ordersMonth': 'Orders · last 6 months',
|
||||
'h5Admin.monitor.chart.revenueGrossDay': 'Revenue & gross · last 7 days',
|
||||
'h5Admin.monitor.chart.revenueGrossWeek': 'Revenue & gross · last 8 weeks',
|
||||
'h5Admin.monitor.chart.revenueGrossMonth': 'Revenue & gross · last 6 months',
|
||||
'h5Admin.monitor.series.revenue': 'Revenue',
|
||||
'h5Admin.monitor.series.grossProfit': 'Gross profit',
|
||||
'h5Admin.monitor.track.complete': 'Track complete',
|
||||
'h5Admin.monitor.track.timely': 'Track on-time',
|
||||
'h5Admin.monitor.track.otd': 'OTD rate',
|
||||
'h5Admin.monitor.loadFailed': 'Failed to load monitor data',
|
||||
'h5Admin.monitor.vsYesterday': 'vs yesterday',
|
||||
'h5Admin.monitor.vsLastWeek': 'vs last week',
|
||||
'h5Admin.monitor.vsLastMonth': 'vs last month',
|
||||
};
|
||||
|
||||
@ -16,11 +16,6 @@ export default {
|
||||
'h5Admin.login.failed': '登录失败,请检查账号或密码',
|
||||
'h5Admin.login.accountRequired': '请输入账号',
|
||||
'h5Admin.login.passwordRequired': '请输入密码',
|
||||
'h5Admin.login.needAgree': '请先同意隐私政策与服务条款',
|
||||
'h5Admin.login.agreePrefix': '登录即表示同意',
|
||||
'h5Admin.login.privacy': '隐私政策',
|
||||
'h5Admin.login.terms': '服务条款',
|
||||
'h5Admin.login.and': '与',
|
||||
'h5.login.switchToAdmin': '切换至管理登录',
|
||||
'h5.login.switchToCustomer': '切换至客户登录',
|
||||
|
||||
@ -105,9 +100,26 @@ export default {
|
||||
'h5Admin.monitor.refresh': '刷新',
|
||||
'h5Admin.monitor.tab.order': '订单',
|
||||
'h5Admin.monitor.tab.customer': '客户',
|
||||
'h5Admin.monitor.dim.day': '日',
|
||||
'h5Admin.monitor.dim.week': '周',
|
||||
'h5Admin.monitor.dim.month': '月',
|
||||
'h5Admin.monitor.ops.title': '经营概况',
|
||||
'h5Admin.monitor.customer.title': '客户概况',
|
||||
'h5Admin.monitor.track.title': '轨迹达成',
|
||||
'h5Admin.monitor.ops.orderCount.day': '今日订单量',
|
||||
'h5Admin.monitor.ops.orderCount.week': '本周订单量',
|
||||
'h5Admin.monitor.ops.orderCount.month': '本月订单量',
|
||||
'h5Admin.monitor.ops.revenue.day': '今日营收',
|
||||
'h5Admin.monitor.ops.revenue.week': '本周营收',
|
||||
'h5Admin.monitor.ops.revenue.month': '本月营收',
|
||||
'h5Admin.monitor.ops.grossProfit.day': '今日毛利',
|
||||
'h5Admin.monitor.ops.grossProfit.week': '本周毛利',
|
||||
'h5Admin.monitor.ops.grossProfit.month': '本月毛利',
|
||||
'h5Admin.monitor.queue.title': '实时积压',
|
||||
'h5Admin.monitor.kpi.todayCreated': '今日创建',
|
||||
'h5Admin.monitor.kpi.pendingPayment': '待支付',
|
||||
'h5Admin.monitor.kpi.pendingOnline': '待上网',
|
||||
'h5Admin.monitor.kpi.pendingSign': '待签收',
|
||||
'h5Admin.monitor.kpi.problematic': '问题件',
|
||||
'h5Admin.monitor.kpi.todayRevenue': '今日营收',
|
||||
'h5Admin.monitor.kpi.total': '运单总量',
|
||||
@ -116,10 +128,19 @@ export default {
|
||||
'h5Admin.monitor.kpi.online': '在线客户',
|
||||
'h5Admin.monitor.kpi.inactive7d': '7日未登录',
|
||||
'h5Admin.monitor.chart.orders7d': '近7日订单量',
|
||||
'h5Admin.monitor.chart.ordersDay': '近7日订单量',
|
||||
'h5Admin.monitor.chart.ordersWeek': '近8周订单量',
|
||||
'h5Admin.monitor.chart.ordersMonth': '近6月订单量',
|
||||
'h5Admin.monitor.chart.revenueGrossDay': '近7日营收 & 毛利',
|
||||
'h5Admin.monitor.chart.revenueGrossWeek': '近8周营收 & 毛利',
|
||||
'h5Admin.monitor.chart.revenueGrossMonth': '近6月营收 & 毛利',
|
||||
'h5Admin.monitor.series.revenue': '营收',
|
||||
'h5Admin.monitor.series.grossProfit': '毛利',
|
||||
'h5Admin.monitor.track.complete': '轨迹完整率',
|
||||
'h5Admin.monitor.track.timely': '轨迹及时率',
|
||||
'h5Admin.monitor.track.otd': '准时签收率',
|
||||
'h5Admin.monitor.loadFailed': '监控数据加载失败',
|
||||
'h5Admin.monitor.vsYesterday': '较昨日',
|
||||
'h5Admin.monitor.vsLastWeek': '较上周',
|
||||
'h5Admin.monitor.vsLastMonth': '较上月',
|
||||
};
|
||||
|
||||
@ -46,34 +46,10 @@
|
||||
>
|
||||
{{ loading ? t('h5Admin.login.submitting') : t('h5Admin.login.submit') }}
|
||||
</a-button>
|
||||
|
||||
<a-checkbox v-model="agreementChecked" class="h5-login-agree">
|
||||
<span>
|
||||
{{ t('h5Admin.login.agreePrefix') }}
|
||||
<a class="h5-login-link" @click.prevent="openAgreement('privacy')">
|
||||
{{ t('h5Admin.login.privacy') }}
|
||||
</a>
|
||||
{{ t('h5Admin.login.and') }}
|
||||
<a class="h5-login-link" @click.prevent="openAgreement('terms')">
|
||||
{{ t('h5Admin.login.terms') }}
|
||||
</a>
|
||||
</span>
|
||||
</a-checkbox>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="agreementVisible"
|
||||
:title="agreementTitle"
|
||||
:footer="false"
|
||||
unmount-on-close
|
||||
width="auto"
|
||||
modal-class="h5-login-modal"
|
||||
>
|
||||
<div class="h5-login-agreement">{{ agreementContent }}</div>
|
||||
</a-modal>
|
||||
|
||||
<H5LoginPortalSwitch target="customer" />
|
||||
</div>
|
||||
</template>
|
||||
@ -101,33 +77,8 @@ const router = useRouter();
|
||||
const rbacUserStore = useRbacUserStore();
|
||||
|
||||
const form = reactive({ account: '', password: '' });
|
||||
const agreementChecked = ref(false);
|
||||
const loading = ref(false);
|
||||
const errorBox = ref('');
|
||||
const agreementVisible = ref(false);
|
||||
const agreementTitle = ref('');
|
||||
const agreementContent = ref('');
|
||||
|
||||
const privacyText = `一、隐私保护告知
|
||||
员工账号仅用于内部运营处理。系统将按职责范围记录登录与操作日志,用于安全审计与业务追溯,不会向无关第三方出售。
|
||||
二、您的义务
|
||||
请妥善保管账号与密码,勿共享账号;发现异常请立即联系管理员。`;
|
||||
|
||||
const termsText = `一、使用须知
|
||||
仅授权员工可登录运营管理端。禁止将客户数据用于职责以外用途。
|
||||
二、服务说明
|
||||
展示的待办与监控数据以实时接口为准;处理运单请遵循公司 SOP。`;
|
||||
|
||||
function openAgreement(type: 'privacy' | 'terms') {
|
||||
if (type === 'privacy') {
|
||||
agreementTitle.value = t('h5Admin.login.privacy');
|
||||
agreementContent.value = privacyText;
|
||||
} else {
|
||||
agreementTitle.value = t('h5Admin.login.terms');
|
||||
agreementContent.value = termsText;
|
||||
}
|
||||
agreementVisible.value = true;
|
||||
}
|
||||
|
||||
function resolveRedirectPath() {
|
||||
const redirect = route.query.redirect;
|
||||
@ -158,10 +109,6 @@ async function handleSubmit() {
|
||||
if (loading.value) return;
|
||||
errorBox.value = '';
|
||||
|
||||
if (!agreementChecked.value) {
|
||||
Message.warning(t('h5Admin.login.needAgree'));
|
||||
return;
|
||||
}
|
||||
if (!form.account.trim()) {
|
||||
errorBox.value = t('h5Admin.login.accountRequired');
|
||||
return;
|
||||
@ -330,33 +277,9 @@ onMounted(() => {
|
||||
|
||||
.h5-login-submit {
|
||||
margin-top: 4px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
height: 52px !important;
|
||||
border-radius: 12px !important;
|
||||
font-size: 16px !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.h5-login-agree {
|
||||
margin-top: 14px;
|
||||
font-size: 12px;
|
||||
color: #86909c;
|
||||
align-items: flex-start;
|
||||
|
||||
:deep(.arco-checkbox-label) {
|
||||
color: #86909c;
|
||||
line-height: 1.45;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-login-link {
|
||||
color: var(--app-primary-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.h5-login-agreement {
|
||||
max-width: min(360px, 86vw);
|
||||
white-space: pre-wrap;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
color: #4e5969;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -32,61 +32,154 @@
|
||||
|
||||
<a-spin :loading="loading && !overview" style="width: 100%">
|
||||
<template v-if="tab === 'order'">
|
||||
<section class="h5-mon__kpi h5-enter">
|
||||
<button
|
||||
v-for="card in orderCards"
|
||||
:key="card.key"
|
||||
type="button"
|
||||
class="h5-mon__kpi-card"
|
||||
:class="{ 'is-warning': card.warning }"
|
||||
@click="onKpiClick(card)"
|
||||
>
|
||||
<span class="h5-mon__kpi-label">{{ t(card.labelKey) }}</span>
|
||||
<strong class="h5-mon__kpi-value">{{ card.display }}</strong>
|
||||
<span v-if="card.growthText" class="h5-mon__kpi-growth" :class="card.growthUp ? 'up' : 'down'">
|
||||
{{ card.growthText }} {{ t(card.compareKey) }}
|
||||
</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="h5-mon__chart h5-enter">
|
||||
<h2>{{ t('h5Admin.monitor.chart.orders7d') }}</h2>
|
||||
<div class="h5-mon__bars">
|
||||
<section class="h5-mon__section h5-enter">
|
||||
<div class="h5-mon__section-head h5-mon__section-head--row">
|
||||
<h2>{{ t('h5Admin.monitor.ops.title') }}</h2>
|
||||
<div class="h5-mon__dims">
|
||||
<button
|
||||
v-for="dim in dimensions"
|
||||
:key="dim"
|
||||
type="button"
|
||||
class="h5-mon__dim"
|
||||
:class="{ 'is-active': opsDimension === dim }"
|
||||
@click="setDimension(dim)"
|
||||
>
|
||||
{{ t(`h5Admin.monitor.dim.${dim}`) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h5-mon__kpi">
|
||||
<div
|
||||
v-for="point in orderTrend"
|
||||
:key="point.label"
|
||||
class="h5-mon__bar-col"
|
||||
v-for="card in opsCards"
|
||||
:key="card.key"
|
||||
class="h5-mon__kpi-card"
|
||||
>
|
||||
<div class="h5-mon__bar-track">
|
||||
<div class="h5-mon__bar-fill" :style="{ height: `${point.pct}%` }" />
|
||||
</div>
|
||||
<span class="h5-mon__bar-val">{{ point.count }}</span>
|
||||
<span class="h5-mon__bar-label">{{ point.label }}</span>
|
||||
<span class="h5-mon__kpi-main">
|
||||
<span class="h5-mon__kpi-meta">
|
||||
<img class="h5-mon__kpi-icon" :src="card.icon" alt="" />
|
||||
<span class="h5-mon__kpi-label">{{ t(card.labelKey) }}</span>
|
||||
<span
|
||||
v-if="card.growthText"
|
||||
class="h5-mon__kpi-growth"
|
||||
:class="card.growthUp ? 'up' : 'down'"
|
||||
>
|
||||
{{ card.growthText }} {{ t(opsCompareKey) }}
|
||||
</span>
|
||||
</span>
|
||||
<strong class="h5-mon__kpi-value">{{ card.display }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="h5-mon__rates h5-enter">
|
||||
<div v-for="item in trackRates" :key="item.key" class="h5-mon__rate">
|
||||
<span>{{ t(item.labelKey) }}</span>
|
||||
<strong>{{ item.display }}</strong>
|
||||
<section class="h5-mon__section h5-enter">
|
||||
<div class="h5-mon__section-head">
|
||||
<h2>{{ t('h5Admin.monitor.queue.title') }}</h2>
|
||||
</div>
|
||||
<div class="h5-mon__kpi h5-mon__kpi--queue">
|
||||
<button
|
||||
v-for="card in queueCards"
|
||||
:key="card.key"
|
||||
type="button"
|
||||
class="h5-mon__kpi-card"
|
||||
:class="{ 'is-warning': card.warning }"
|
||||
@click="onKpiClick(card)"
|
||||
>
|
||||
<span class="h5-mon__kpi-main">
|
||||
<span class="h5-mon__kpi-meta">
|
||||
<img class="h5-mon__kpi-icon" :src="card.icon" alt="" />
|
||||
<span class="h5-mon__kpi-label">{{ t(card.labelKey) }}</span>
|
||||
</span>
|
||||
<strong class="h5-mon__kpi-value">{{ card.display }}</strong>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="h5-mon__section h5-enter">
|
||||
<div class="h5-mon__section-head">
|
||||
<h2>{{ t(orderTrendTitleKey) }}</h2>
|
||||
</div>
|
||||
<div class="h5-mon__bars">
|
||||
<div v-for="point in orderTrendBars" :key="point.label" class="h5-mon__bar-col">
|
||||
<div class="h5-mon__bar-track">
|
||||
<div class="h5-mon__bar-fill" :style="{ height: `${point.pct}%` }" />
|
||||
</div>
|
||||
<span class="h5-mon__bar-val">{{ point.count }}</span>
|
||||
<span class="h5-mon__bar-label">{{ point.shortLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="h5-mon__section h5-enter">
|
||||
<div class="h5-mon__section-head">
|
||||
<h2>{{ t(revenueTrendTitleKey) }}</h2>
|
||||
</div>
|
||||
<div class="h5-mon__legend">
|
||||
<span class="is-revenue">{{ t('h5Admin.monitor.series.revenue') }}</span>
|
||||
<span class="is-gross">{{ t('h5Admin.monitor.series.grossProfit') }}</span>
|
||||
</div>
|
||||
<div class="h5-mon__bars h5-mon__bars--pair">
|
||||
<div v-for="point in revenueTrendBars" :key="point.label" class="h5-mon__bar-col">
|
||||
<div class="h5-mon__bar-pair">
|
||||
<div class="h5-mon__bar-track">
|
||||
<div
|
||||
class="h5-mon__bar-fill is-revenue"
|
||||
:style="{ height: `${point.revenuePct}%` }"
|
||||
/>
|
||||
</div>
|
||||
<div class="h5-mon__bar-track">
|
||||
<div
|
||||
class="h5-mon__bar-fill is-gross"
|
||||
:style="{ height: `${point.grossPct}%` }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="h5-mon__bar-label">{{ point.shortLabel }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="h5-mon__section h5-enter">
|
||||
<div class="h5-mon__section-head">
|
||||
<h2>{{ t('h5Admin.monitor.track.title') }}</h2>
|
||||
</div>
|
||||
<div class="h5-mon__rates">
|
||||
<div v-for="item in trackRates" :key="item.key" class="h5-mon__rate">
|
||||
<span>{{ t(item.labelKey) }}</span>
|
||||
<strong>{{ item.display }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<section class="h5-mon__kpi h5-enter">
|
||||
<div
|
||||
v-for="card in customerCards"
|
||||
:key="card.key"
|
||||
class="h5-mon__kpi-card"
|
||||
:class="{ 'is-warning': card.warning }"
|
||||
>
|
||||
<span class="h5-mon__kpi-label">{{ t(card.labelKey) }}</span>
|
||||
<strong class="h5-mon__kpi-value">{{ card.display }}</strong>
|
||||
<span v-if="card.growthText" class="h5-mon__kpi-growth" :class="card.growthUp ? 'up' : 'down'">
|
||||
{{ card.growthText }} {{ t(card.compareKey) }}
|
||||
</span>
|
||||
<section class="h5-mon__section h5-enter">
|
||||
<div class="h5-mon__section-head">
|
||||
<h2>{{ t('h5Admin.monitor.customer.title') }}</h2>
|
||||
</div>
|
||||
<div class="h5-mon__kpi h5-mon__kpi--queue">
|
||||
<div
|
||||
v-for="card in customerCards"
|
||||
:key="card.key"
|
||||
class="h5-mon__kpi-card"
|
||||
:class="{ 'is-warning': card.warning }"
|
||||
>
|
||||
<span class="h5-mon__kpi-main">
|
||||
<span class="h5-mon__kpi-meta">
|
||||
<img class="h5-mon__kpi-icon" :src="card.icon" alt="" />
|
||||
<span class="h5-mon__kpi-label">{{ t(card.labelKey) }}</span>
|
||||
<span
|
||||
v-if="card.growthText"
|
||||
class="h5-mon__kpi-growth"
|
||||
:class="card.growthUp ? 'up' : 'down'"
|
||||
>
|
||||
{{ card.growthText }} {{ t(card.compareKey) }}
|
||||
</span>
|
||||
</span>
|
||||
<strong class="h5-mon__kpi-value">{{ card.display }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@ -102,12 +195,22 @@ import {
|
||||
formatMonitorCurrency,
|
||||
formatMonitorNumber,
|
||||
getMonitorOverview,
|
||||
type MonitorDimension,
|
||||
type MonitorOverview,
|
||||
} from '@/api/monitor';
|
||||
import iconOrderNew from '@/assets/icons/workplace/order-new.svg?url';
|
||||
import iconPayment from '@/assets/icons/workplace/order-payment.svg?url';
|
||||
import iconGrowth from '@/assets/icons/workplace/order-growth.svg?url';
|
||||
import iconPendingOnline from '@/assets/icons/workplace/order-pending-online.svg?url';
|
||||
import iconProblematic from '@/assets/icons/workplace/order-problematic.svg?url';
|
||||
import iconShipped from '@/assets/icons/workplace/order-shipped.svg?url';
|
||||
import iconCustomerTotal from '@/assets/icons/workplace/customer-total.svg?url';
|
||||
import iconCustomerNew from '@/assets/icons/workplace/customer-new.svg?url';
|
||||
import iconCustomerOnline from '@/assets/icons/workplace/customer-online.svg?url';
|
||||
import iconCustomerInactive from '@/assets/icons/workplace/customer-inactive.svg?url';
|
||||
|
||||
function formatPercent(value: number) {
|
||||
const n = Number(value) || 0;
|
||||
// 接口多为 0–100;兼容 0–1 小数
|
||||
const pct = n > 0 && n <= 1 ? n * 100 : n;
|
||||
return `${pct.toFixed(1)}%`;
|
||||
}
|
||||
@ -118,73 +221,106 @@ const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const tab = ref<'order' | 'customer'>('order');
|
||||
const opsDimension = ref<MonitorDimension>('day');
|
||||
const dimensions: MonitorDimension[] = ['day', 'week', 'month'];
|
||||
const loading = ref(false);
|
||||
const loadError = ref(false);
|
||||
const overview = ref<MonitorOverview | null>(null);
|
||||
|
||||
function growthMeta(value: number | null | undefined, compareKey: string) {
|
||||
function growthMeta(value: number | null | undefined) {
|
||||
if (value == null || Number.isNaN(Number(value))) {
|
||||
return { growthText: '', growthUp: true, compareKey };
|
||||
return { growthText: '', growthUp: true };
|
||||
}
|
||||
const n = Number(value);
|
||||
return {
|
||||
growthText: `${n > 0 ? '+' : ''}${n.toFixed(1)}%`,
|
||||
growthUp: n >= 0,
|
||||
compareKey,
|
||||
};
|
||||
}
|
||||
|
||||
const orderCards = computed(() => {
|
||||
const o = overview.value?.order;
|
||||
const opsCompareKey = computed(() => {
|
||||
if (opsDimension.value === 'week') return 'h5Admin.monitor.vsLastWeek';
|
||||
if (opsDimension.value === 'month') return 'h5Admin.monitor.vsLastMonth';
|
||||
return 'h5Admin.monitor.vsYesterday';
|
||||
});
|
||||
|
||||
const orderTrendTitleKey = computed(() => {
|
||||
if (opsDimension.value === 'week') return 'h5Admin.monitor.chart.ordersWeek';
|
||||
if (opsDimension.value === 'month') return 'h5Admin.monitor.chart.ordersMonth';
|
||||
return 'h5Admin.monitor.chart.ordersDay';
|
||||
});
|
||||
|
||||
const revenueTrendTitleKey = computed(() => {
|
||||
if (opsDimension.value === 'week') return 'h5Admin.monitor.chart.revenueGrossWeek';
|
||||
if (opsDimension.value === 'month') return 'h5Admin.monitor.chart.revenueGrossMonth';
|
||||
return 'h5Admin.monitor.chart.revenueGrossDay';
|
||||
});
|
||||
|
||||
const opsCards = computed(() => {
|
||||
const ops = overview.value?.ops;
|
||||
const dim = opsDimension.value;
|
||||
return [
|
||||
{
|
||||
key: 'todayCreated',
|
||||
labelKey: 'h5Admin.monitor.kpi.todayCreated',
|
||||
display: formatMonitorNumber(o?.todayCreated || 0),
|
||||
warning: false,
|
||||
path: '/h5/admin/orders',
|
||||
...growthMeta(o?.todayCreatedGrowthPercent, 'h5Admin.monitor.vsYesterday'),
|
||||
key: 'orderCount',
|
||||
labelKey: `h5Admin.monitor.ops.orderCount.${dim}`,
|
||||
display: formatMonitorNumber(ops?.orderCount || 0),
|
||||
icon: iconOrderNew,
|
||||
...growthMeta(ops?.orderCountGrowthPercent),
|
||||
},
|
||||
{
|
||||
key: 'revenue',
|
||||
labelKey: `h5Admin.monitor.ops.revenue.${dim}`,
|
||||
display: formatMonitorCurrency(ops?.revenue || 0),
|
||||
icon: iconPayment,
|
||||
...growthMeta(ops?.revenueGrowthPercent),
|
||||
},
|
||||
{
|
||||
key: 'grossProfit',
|
||||
labelKey: `h5Admin.monitor.ops.grossProfit.${dim}`,
|
||||
display: formatMonitorCurrency(ops?.grossProfit || 0),
|
||||
icon: iconGrowth,
|
||||
...growthMeta(ops?.grossProfitGrowthPercent),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const queueCards = computed(() => {
|
||||
const queue = overview.value?.queue ?? overview.value?.order;
|
||||
return [
|
||||
{
|
||||
key: 'pendingPayment',
|
||||
labelKey: 'h5Admin.monitor.kpi.pendingPayment',
|
||||
display: formatMonitorNumber(o?.pendingPayment || 0),
|
||||
warning: (o?.pendingPayment || 0) > 0,
|
||||
display: formatMonitorNumber(queue?.pendingPayment || 0),
|
||||
warning: (queue?.pendingPayment || 0) > 0,
|
||||
icon: iconPayment,
|
||||
path: '/h5/admin/orders',
|
||||
query: { status: 'pendingPayment' },
|
||||
...growthMeta(null, 'h5Admin.monitor.vsYesterday'),
|
||||
},
|
||||
{
|
||||
key: 'pendingOnline',
|
||||
labelKey: 'h5Admin.monitor.kpi.pendingOnline',
|
||||
display: formatMonitorNumber(o?.pendingOnline || 0),
|
||||
warning: (o?.pendingOnline || 0) > 0,
|
||||
display: formatMonitorNumber(queue?.pendingOnline || 0),
|
||||
warning: (queue?.pendingOnline || 0) > 0,
|
||||
icon: iconPendingOnline,
|
||||
path: '/h5/admin/orders',
|
||||
...growthMeta(null, 'h5Admin.monitor.vsYesterday'),
|
||||
},
|
||||
{
|
||||
key: 'pendingSign',
|
||||
labelKey: 'h5Admin.monitor.kpi.pendingSign',
|
||||
display: formatMonitorNumber(queue?.pendingSign || 0),
|
||||
warning: false,
|
||||
icon: iconShipped,
|
||||
path: '/h5/admin/orders',
|
||||
query: { status: 'shipping' },
|
||||
},
|
||||
{
|
||||
key: 'problematic',
|
||||
labelKey: 'h5Admin.monitor.kpi.problematic',
|
||||
display: formatMonitorNumber(o?.problematic || 0),
|
||||
warning: (o?.problematic || 0) > 0,
|
||||
display: formatMonitorNumber(queue?.problematic || 0),
|
||||
warning: (queue?.problematic || 0) > 0,
|
||||
icon: iconProblematic,
|
||||
path: '/h5/admin/orders',
|
||||
query: { status: 'problematic' },
|
||||
...growthMeta(null, 'h5Admin.monitor.vsYesterday'),
|
||||
},
|
||||
{
|
||||
key: 'todayRevenue',
|
||||
labelKey: 'h5Admin.monitor.kpi.todayRevenue',
|
||||
display: formatMonitorCurrency(o?.todayRevenue || 0),
|
||||
warning: false,
|
||||
...growthMeta(o?.todayRevenueGrowthPercent, 'h5Admin.monitor.vsYesterday'),
|
||||
},
|
||||
{
|
||||
key: 'total',
|
||||
labelKey: 'h5Admin.monitor.kpi.total',
|
||||
display: formatMonitorNumber(o?.total || 0),
|
||||
warning: false,
|
||||
path: '/h5/admin/orders',
|
||||
...growthMeta(o?.totalMonthGrowthPercent, 'h5Admin.monitor.vsLastMonth'),
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -197,42 +333,68 @@ const customerCards = computed(() => {
|
||||
labelKey: 'h5Admin.monitor.kpi.totalCustomers',
|
||||
display: formatMonitorNumber(c?.totalCustomers || 0),
|
||||
warning: false,
|
||||
...growthMeta(c?.totalCustomersMonthGrowthPercent, 'h5Admin.monitor.vsLastMonth'),
|
||||
icon: iconCustomerTotal,
|
||||
compareKey: 'h5Admin.monitor.vsLastMonth',
|
||||
...growthMeta(c?.totalCustomersMonthGrowthPercent),
|
||||
},
|
||||
{
|
||||
key: 'newToday',
|
||||
labelKey: 'h5Admin.monitor.kpi.newToday',
|
||||
display: formatMonitorNumber(c?.newCustomersToday || 0),
|
||||
warning: false,
|
||||
...growthMeta(c?.newCustomersGrowthPercent, 'h5Admin.monitor.vsYesterday'),
|
||||
icon: iconCustomerNew,
|
||||
compareKey: 'h5Admin.monitor.vsYesterday',
|
||||
...growthMeta(c?.newCustomersGrowthPercent),
|
||||
},
|
||||
{
|
||||
key: 'online',
|
||||
labelKey: 'h5Admin.monitor.kpi.online',
|
||||
display: formatMonitorNumber(c?.onlineCustomers || 0),
|
||||
warning: false,
|
||||
...growthMeta(null, 'h5Admin.monitor.vsYesterday'),
|
||||
icon: iconCustomerOnline,
|
||||
compareKey: 'h5Admin.monitor.vsYesterday',
|
||||
...growthMeta(null),
|
||||
},
|
||||
{
|
||||
key: 'inactive7d',
|
||||
labelKey: 'h5Admin.monitor.kpi.inactive7d',
|
||||
display: formatMonitorNumber(c?.inactiveLogin7d || 0),
|
||||
warning: (c?.inactiveLogin7d || 0) > 0,
|
||||
...growthMeta(null, 'h5Admin.monitor.vsYesterday'),
|
||||
icon: iconCustomerInactive,
|
||||
compareKey: 'h5Admin.monitor.vsYesterday',
|
||||
...growthMeta(null),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const orderTrend = computed(() => {
|
||||
const points = overview.value?.orderTrend7d || [];
|
||||
const orderTrendBars = computed(() => {
|
||||
const points = overview.value?.orderTrend || overview.value?.orderTrend7d || [];
|
||||
const max = Math.max(...points.map((p) => p.count), 1);
|
||||
return points.map((p) => ({
|
||||
...p,
|
||||
label: String(p.label || '').slice(-5),
|
||||
pct: Math.max(8, Math.round((p.count / max) * 100)),
|
||||
shortLabel: String(p.label || '').slice(-5),
|
||||
pct: Math.max(p.count > 0 ? 8 : 0, Math.round((p.count / max) * 100)),
|
||||
}));
|
||||
});
|
||||
|
||||
const revenueTrendBars = computed(() => {
|
||||
const points = overview.value?.revenueTrend || overview.value?.revenueTrend7d || [];
|
||||
const max = Math.max(
|
||||
...points.map((p) => Math.max(Number(p.revenue) || 0, Number(p.grossProfit) || 0)),
|
||||
1,
|
||||
);
|
||||
return points.map((p) => {
|
||||
const revenue = Number(p.revenue) || 0;
|
||||
const gross = Number(p.grossProfit) || 0;
|
||||
return {
|
||||
label: p.label,
|
||||
shortLabel: String(p.label || '').slice(-5),
|
||||
revenuePct: Math.max(revenue > 0 ? 8 : 0, Math.round((revenue / max) * 100)),
|
||||
grossPct: Math.max(gross > 0 ? 8 : 0, Math.round((gross / max) * 100)),
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const trackRates = computed(() => {
|
||||
const r = overview.value?.trackRates;
|
||||
return [
|
||||
@ -263,7 +425,7 @@ async function load() {
|
||||
loading.value = true;
|
||||
loadError.value = false;
|
||||
try {
|
||||
overview.value = await getMonitorOverview();
|
||||
overview.value = await getMonitorOverview(opsDimension.value);
|
||||
} catch {
|
||||
loadError.value = true;
|
||||
} finally {
|
||||
@ -271,6 +433,12 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function setDimension(dim: MonitorDimension) {
|
||||
if (opsDimension.value === dim) return;
|
||||
opsDimension.value = dim;
|
||||
void load();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void load();
|
||||
});
|
||||
@ -345,42 +513,137 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.h5-mon__section {
|
||||
margin-top: 16px;
|
||||
padding: 14px;
|
||||
border-radius: var(--h5-radius, 16px);
|
||||
background: var(--h5-surface, #fff);
|
||||
border: 1px solid var(--h5-border-soft, #f2f3f5);
|
||||
box-shadow: var(--h5-shadow, 0 4px 16px rgba(15, 23, 42, 0.05));
|
||||
}
|
||||
|
||||
.h5-mon__section-head {
|
||||
margin-bottom: 12px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--h5-text, #1d2129);
|
||||
}
|
||||
|
||||
&--row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-mon__dims {
|
||||
display: inline-flex;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
background: #eceff3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.h5-mon__dim {
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--h5-text-2, #4e5969);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-active {
|
||||
background: #fff;
|
||||
color: var(--app-primary-color);
|
||||
font-weight: 600;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.h5-mon__kpi {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
|
||||
&--queue {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.h5-mon__kpi-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
min-height: 92px;
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--h5-border-soft, #f2f3f5);
|
||||
background: var(--h5-surface, #fff);
|
||||
box-shadow: var(--h5-shadow, 0 4px 16px rgba(15, 23, 42, 0.05));
|
||||
align-items: center;
|
||||
min-height: 76px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--h5-border, #e5e6eb);
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
cursor: default;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&.is-warning {
|
||||
background: var(--h5-warning-bg, #fff7e8);
|
||||
&[type='button'] {
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-warning .h5-mon__kpi-value {
|
||||
color: var(--h5-warning, #ff7d00);
|
||||
}
|
||||
}
|
||||
|
||||
.h5-mon__kpi-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.h5-mon__kpi-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.h5-mon__kpi-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.h5-mon__kpi-label {
|
||||
max-width: 100%;
|
||||
font-size: 12px;
|
||||
color: var(--h5-text-3, #86909c);
|
||||
line-height: 1.3;
|
||||
color: var(--h5-text-2, #4e5969);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.h5-mon__kpi-value {
|
||||
flex-shrink: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--h5-text, #1d2129);
|
||||
line-height: 1.15;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.h5-mon__kpi-growth {
|
||||
@ -395,18 +658,29 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.h5-mon__chart {
|
||||
margin-top: 14px;
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
background: var(--h5-surface, #fff);
|
||||
border: 1px solid var(--h5-border-soft, #f2f3f5);
|
||||
box-shadow: var(--h5-shadow, 0 4px 16px rgba(15, 23, 42, 0.05));
|
||||
.h5-mon__legend {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
margin: 0 0 10px;
|
||||
font-size: 11px;
|
||||
color: var(--h5-text-3, #86909c);
|
||||
|
||||
h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
span::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 6px;
|
||||
border-radius: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.is-revenue::before {
|
||||
background: #00b42a;
|
||||
}
|
||||
|
||||
.is-gross::before {
|
||||
background: #ff7d00;
|
||||
}
|
||||
}
|
||||
|
||||
@ -416,6 +690,10 @@ onMounted(() => {
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
min-height: 140px;
|
||||
|
||||
&--pair {
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-mon__bar-col {
|
||||
@ -427,6 +705,14 @@ onMounted(() => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.h5-mon__bar-pair {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 3px;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.h5-mon__bar-track {
|
||||
width: 100%;
|
||||
max-width: 28px;
|
||||
@ -438,6 +724,10 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.h5-mon__bar-pair .h5-mon__bar-track {
|
||||
max-width: 12px;
|
||||
}
|
||||
|
||||
.h5-mon__bar-fill {
|
||||
width: 100%;
|
||||
border-radius: 8px 8px 0 0;
|
||||
@ -447,6 +737,14 @@ onMounted(() => {
|
||||
var(--app-primary-color)
|
||||
);
|
||||
transition: height 0.35s ease;
|
||||
|
||||
&.is-revenue {
|
||||
background: #00b42a;
|
||||
}
|
||||
|
||||
&.is-gross {
|
||||
background: #ff7d00;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-mon__bar-val {
|
||||
@ -464,7 +762,6 @@ onMounted(() => {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.h5-mon__rate {
|
||||
@ -473,8 +770,8 @@ onMounted(() => {
|
||||
gap: 6px;
|
||||
padding: 12px 8px;
|
||||
border-radius: 12px;
|
||||
background: var(--h5-surface, #fff);
|
||||
border: 1px solid var(--h5-border-soft, #f2f3f5);
|
||||
background: transparent;
|
||||
border: 1px solid var(--h5-border, #e5e6eb);
|
||||
text-align: center;
|
||||
|
||||
span {
|
||||
|
||||
@ -28,9 +28,16 @@
|
||||
:class="[`is-${item.tone}`, { 'is-alert': item.alert && item.value > 0 }]"
|
||||
@click="onOpsClick(item)"
|
||||
>
|
||||
<span class="h5-wp__ops-label">{{ t(item.labelKey) }}</span>
|
||||
<strong class="h5-wp__ops-value">{{ formatNum(item.value) }}</strong>
|
||||
<span class="h5-wp__ops-unit">{{ t('h5Admin.workplace.unit') }}</span>
|
||||
<span class="h5-wp__ops-main">
|
||||
<span class="h5-wp__ops-meta">
|
||||
<img class="h5-wp__ops-icon" :src="item.icon" alt="" />
|
||||
<span class="h5-wp__ops-label">{{ t(item.labelKey) }}</span>
|
||||
</span>
|
||||
<span class="h5-wp__ops-body">
|
||||
<strong class="h5-wp__ops-value">{{ formatNum(item.value) }}</strong>
|
||||
<span class="h5-wp__ops-unit">{{ t('h5Admin.workplace.unit') }}</span>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</a-spin>
|
||||
@ -42,8 +49,13 @@
|
||||
</div>
|
||||
<div class="h5-wp__grid">
|
||||
<div v-for="item in fulfillItems" :key="item.key" class="h5-wp__stat">
|
||||
<span>{{ t(item.labelKey) }}</span>
|
||||
<strong>{{ formatNum(item.value) }}</strong>
|
||||
<span class="h5-wp__stat-main">
|
||||
<span class="h5-wp__stat-meta">
|
||||
<img class="h5-wp__stat-icon" :src="item.icon" alt="" />
|
||||
<span class="h5-wp__stat-label">{{ t(item.labelKey) }}</span>
|
||||
</span>
|
||||
<strong class="h5-wp__stat-value">{{ formatNum(item.value) }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -54,8 +66,13 @@
|
||||
</div>
|
||||
<div class="h5-wp__grid">
|
||||
<div v-for="item in pickupItems" :key="item.key" class="h5-wp__stat">
|
||||
<span>{{ t(item.labelKey) }}</span>
|
||||
<strong>{{ formatNum(item.value) }}</strong>
|
||||
<span class="h5-wp__stat-main">
|
||||
<span class="h5-wp__stat-meta">
|
||||
<img class="h5-wp__stat-icon" :src="item.icon" alt="" />
|
||||
<span class="h5-wp__stat-label">{{ t(item.labelKey) }}</span>
|
||||
</span>
|
||||
<strong class="h5-wp__stat-value">{{ formatNum(item.value) }}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -123,6 +140,18 @@ import {
|
||||
} from '@/api/workplace';
|
||||
import { getRechargeRecordList } from '@/api/recharge/rechargeRecord';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
import iconOrderNew from '@/assets/icons/workplace/order-new.svg?url';
|
||||
import iconPendingShip from '@/assets/icons/workplace/order-pending-ship.svg?url';
|
||||
import iconPayment from '@/assets/icons/workplace/order-payment.svg?url';
|
||||
import iconProblematic from '@/assets/icons/workplace/order-problematic.svg?url';
|
||||
import iconGrowth from '@/assets/icons/workplace/order-growth.svg?url';
|
||||
import iconBind from '@/assets/icons/workplace/order-bind.svg?url';
|
||||
import iconTransit from '@/assets/icons/workplace/order-transit.svg?url';
|
||||
import iconPick from '@/assets/icons/workplace/order-pick.svg?url';
|
||||
import iconShipped from '@/assets/icons/workplace/order-shipped.svg?url';
|
||||
import iconDispatch from '@/assets/icons/workplace/order-dispatch.svg?url';
|
||||
import iconPickup from '@/assets/icons/workplace/order-pickup.svg?url';
|
||||
import iconInbound from '@/assets/icons/workplace/order-inbound.svg?url';
|
||||
|
||||
defineOptions({ name: 'H5AdminWorkplace' });
|
||||
|
||||
@ -171,6 +200,7 @@ type OpsItem = {
|
||||
status: string;
|
||||
tone: string;
|
||||
alert: boolean;
|
||||
icon: string;
|
||||
action?: 'orders' | 'recharge';
|
||||
};
|
||||
|
||||
@ -184,6 +214,7 @@ const opsItems = computed<OpsItem[]>(() => {
|
||||
status: '',
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
icon: iconPayment,
|
||||
action: 'recharge',
|
||||
},
|
||||
{
|
||||
@ -193,6 +224,7 @@ const opsItems = computed<OpsItem[]>(() => {
|
||||
status: 'problematic',
|
||||
tone: 'danger',
|
||||
alert: true,
|
||||
icon: iconProblematic,
|
||||
action: 'orders',
|
||||
},
|
||||
{
|
||||
@ -202,6 +234,7 @@ const opsItems = computed<OpsItem[]>(() => {
|
||||
status: 'claimed',
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
icon: iconGrowth,
|
||||
action: 'orders',
|
||||
},
|
||||
{
|
||||
@ -211,6 +244,7 @@ const opsItems = computed<OpsItem[]>(() => {
|
||||
status: 'pendingShip',
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
icon: iconPendingShip,
|
||||
action: 'orders',
|
||||
},
|
||||
{
|
||||
@ -220,6 +254,7 @@ const opsItems = computed<OpsItem[]>(() => {
|
||||
status: 'pendingPayment',
|
||||
tone: 'warning',
|
||||
alert: true,
|
||||
icon: iconPayment,
|
||||
action: 'orders',
|
||||
},
|
||||
{
|
||||
@ -229,6 +264,7 @@ const opsItems = computed<OpsItem[]>(() => {
|
||||
status: '',
|
||||
tone: 'primary',
|
||||
alert: false,
|
||||
icon: iconOrderNew,
|
||||
action: 'orders',
|
||||
},
|
||||
];
|
||||
@ -241,21 +277,25 @@ const fulfillItems = computed(() => {
|
||||
key: 'pendingBind',
|
||||
labelKey: 'h5Admin.workplace.fulfill.pendingBind',
|
||||
value: Number(f?.pendingBind) || 0,
|
||||
icon: iconBind,
|
||||
},
|
||||
{
|
||||
key: 'inTransit',
|
||||
labelKey: 'h5Admin.workplace.fulfill.inTransit',
|
||||
value: Number(f?.domesticInTransit) || 0,
|
||||
icon: iconTransit,
|
||||
},
|
||||
{
|
||||
key: 'arrived',
|
||||
labelKey: 'h5Admin.workplace.fulfill.arrived',
|
||||
value: Number(f?.arrivedPendingPick) || 0,
|
||||
icon: iconPick,
|
||||
},
|
||||
{
|
||||
key: 'outbound',
|
||||
labelKey: 'h5Admin.workplace.fulfill.outbound',
|
||||
value: Number(f?.outboundDone) || 0,
|
||||
icon: iconShipped,
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -267,21 +307,25 @@ const pickupItems = computed(() => {
|
||||
key: 'pendingPayment',
|
||||
labelKey: 'h5Admin.workplace.pickup.pendingPayment',
|
||||
value: Number(p?.pendingPayment) || 0,
|
||||
icon: iconPayment,
|
||||
},
|
||||
{
|
||||
key: 'pendingSchedule',
|
||||
labelKey: 'h5Admin.workplace.pickup.pendingSchedule',
|
||||
value: Number(p?.pendingSchedule) || 0,
|
||||
icon: iconDispatch,
|
||||
},
|
||||
{
|
||||
key: 'pendingPickup',
|
||||
labelKey: 'h5Admin.workplace.pickup.pendingPickup',
|
||||
value: Number(p?.pendingPickup) || 0,
|
||||
icon: iconPickup,
|
||||
},
|
||||
{
|
||||
key: 'pendingInbound',
|
||||
labelKey: 'h5Admin.workplace.pickup.pendingInbound',
|
||||
value: Number(p?.pendingInbound) || 0,
|
||||
icon: iconInbound,
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -523,30 +567,22 @@ onMounted(() => {
|
||||
.h5-wp__ops-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
min-height: 88px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--h5-border-soft, #f2f3f5);
|
||||
align-items: center;
|
||||
min-height: 76px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--h5-border, #e5e6eb);
|
||||
border-radius: 12px;
|
||||
background: #fafbfc;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
&.is-danger.is-alert {
|
||||
background: var(--h5-danger-bg, #ffece8);
|
||||
border-color: color-mix(in srgb, var(--h5-danger) 28%, #fff);
|
||||
&.is-danger.is-alert .h5-wp__ops-value {
|
||||
color: var(--h5-danger, #f53f3f);
|
||||
}
|
||||
|
||||
&.is-warning.is-alert {
|
||||
background: var(--h5-warning-bg, #fff7e8);
|
||||
border-color: color-mix(in srgb, var(--h5-warning) 28%, #fff);
|
||||
}
|
||||
|
||||
&.is-primary {
|
||||
background: var(--h5-primary-softer);
|
||||
&.is-warning.is-alert .h5-wp__ops-value {
|
||||
color: var(--h5-warning, #ff7d00);
|
||||
}
|
||||
|
||||
&:active {
|
||||
@ -554,20 +590,61 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.h5-wp__ops-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.h5-wp__ops-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.h5-wp__ops-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.h5-wp__ops-label {
|
||||
max-width: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
color: var(--h5-text-2, #4e5969);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.h5-wp__ops-body {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: flex-end;
|
||||
gap: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.h5-wp__ops-value {
|
||||
font-size: 26px;
|
||||
line-height: 1.1;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--h5-text, #1d2129);
|
||||
}
|
||||
|
||||
.h5-wp__ops-unit {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--h5-text-3, #86909c);
|
||||
}
|
||||
|
||||
@ -579,22 +656,57 @@ onMounted(() => {
|
||||
|
||||
.h5-wp__stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px;
|
||||
align-items: center;
|
||||
min-height: 64px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 12px;
|
||||
background: #f7f8fa;
|
||||
border: 1px solid var(--h5-border, #e5e6eb);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: var(--h5-text-3, #86909c);
|
||||
}
|
||||
.h5-wp__stat-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--h5-text, #1d2129);
|
||||
}
|
||||
.h5-wp__stat-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.h5-wp__stat-icon {
|
||||
flex: 0 0 auto;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.h5-wp__stat-label {
|
||||
max-width: 100%;
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
color: var(--h5-text-3, #86909c);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.h5-wp__stat-value {
|
||||
flex-shrink: 0;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--h5-text, #1d2129);
|
||||
}
|
||||
|
||||
.h5-wp__legend {
|
||||
|
||||
@ -6,14 +6,14 @@
|
||||
:title="label"
|
||||
:aria-label="label"
|
||||
>
|
||||
<component :is="icon" :size="18" />
|
||||
<icon-swap :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';
|
||||
import { IconSwap } from '@arco-design/web-vue/es/icon';
|
||||
|
||||
const props = defineProps<{
|
||||
/** 当前页面对侧:admin=去管理登录;customer=去客户登录 */
|
||||
@ -26,10 +26,6 @@ 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')
|
||||
@ -53,13 +49,13 @@ const label = computed(() =>
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
opacity: 0.55;
|
||||
opacity: 0.28;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: opacity 0.15s ease, color 0.15s ease;
|
||||
|
||||
&:active {
|
||||
opacity: 0.85;
|
||||
color: #86909c;
|
||||
opacity: 0.55;
|
||||
color: #c9cdd4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
198
src/views/h5/customer-center/components/H5NoticePopup.vue
Normal file
198
src/views/h5/customer-center/components/H5NoticePopup.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="current?.title || t('h5.noticePopup.title')"
|
||||
:mask-closable="false"
|
||||
:esc-to-close="false"
|
||||
:closable="false"
|
||||
unmount-on-close
|
||||
width="auto"
|
||||
modal-class="h5-notice-popup-modal"
|
||||
>
|
||||
<div class="h5-notice-popup">
|
||||
<p v-if="formattedCreateTime" class="h5-notice-popup__time">
|
||||
{{ t('h5.noticePopup.createTime') }}{{ formattedCreateTime }}
|
||||
</p>
|
||||
<div class="h5-notice-popup__content">{{ current?.content }}</div>
|
||||
<div class="h5-notice-popup__actions">
|
||||
<a-button long size="large" :disabled="readLoading" @click="handleLater">
|
||||
{{ t('h5.noticePopup.later') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
long
|
||||
size="large"
|
||||
class="h5-notice-popup__primary"
|
||||
:loading="readLoading"
|
||||
@click="handleMarkRead"
|
||||
>
|
||||
{{ t('h5.noticePopup.markRead') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import dayjs from 'dayjs';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { confirmNoticeRead, getNoticeList, type NoticeDto } from '@/api/system/notice';
|
||||
import { isLogin } from '@/utils/auth';
|
||||
import { getNoticePopupSnoozeIds, snoozeNoticePopup } from '@/utils/notice-unread';
|
||||
|
||||
defineOptions({ name: 'H5NoticePopup' });
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const visible = ref(false);
|
||||
const current = ref<NoticeDto | null>(null);
|
||||
const queue = ref<NoticeDto[]>([]);
|
||||
const readLoading = ref(false);
|
||||
|
||||
const formattedCreateTime = computed(() => {
|
||||
const raw = current.value?.createTime;
|
||||
if (!raw) return '';
|
||||
const d = dayjs(raw);
|
||||
return d.isValid() ? d.format('YYYY-MM-DD HH:mm') : String(raw);
|
||||
});
|
||||
|
||||
function isPopupNotice(row: NoticeDto) {
|
||||
return Number(row.isPopup) === 1;
|
||||
}
|
||||
|
||||
function showNext() {
|
||||
if (queue.value.length === 0) {
|
||||
current.value = null;
|
||||
visible.value = false;
|
||||
return;
|
||||
}
|
||||
current.value = queue.value.shift() ?? null;
|
||||
visible.value = !!current.value;
|
||||
}
|
||||
|
||||
async function loadPopupQueue() {
|
||||
if (!isLogin()) return;
|
||||
try {
|
||||
const snoozed = getNoticePopupSnoozeIds();
|
||||
const { items } = await getNoticeList({
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
noticeType: 'notice',
|
||||
onlyUnread: true,
|
||||
});
|
||||
const list = (items ?? []).filter(
|
||||
(row) =>
|
||||
row.id != null &&
|
||||
isPopupNotice(row) &&
|
||||
!row.isRead &&
|
||||
!snoozed.has(String(row.id)),
|
||||
);
|
||||
list.sort((a, b) => (a.createTime || '').localeCompare(b.createTime || ''));
|
||||
queue.value = list;
|
||||
showNext();
|
||||
} catch {
|
||||
queue.value = [];
|
||||
visible.value = false;
|
||||
current.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleLater() {
|
||||
const row = current.value;
|
||||
if (row?.id != null) {
|
||||
snoozeNoticePopup(row.id);
|
||||
}
|
||||
showNext();
|
||||
}
|
||||
|
||||
async function handleMarkRead() {
|
||||
const row = current.value;
|
||||
if (row?.id == null) {
|
||||
showNext();
|
||||
return;
|
||||
}
|
||||
readLoading.value = true;
|
||||
try {
|
||||
await confirmNoticeRead(row.id);
|
||||
Message.success(t('h5.noticePopup.readSuccess'));
|
||||
showNext();
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : t('h5.noticePopup.readFailed');
|
||||
Message.error(msg);
|
||||
} finally {
|
||||
readLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadPopupQueue();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.h5-notice-popup-modal {
|
||||
.arco-modal {
|
||||
max-width: min(420px, 92vw);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.arco-modal-header {
|
||||
padding: 16px 16px 8px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.arco-modal-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: var(--h5-text, #1d2129);
|
||||
}
|
||||
|
||||
.arco-modal-body {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.arco-modal-footer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.h5-notice-popup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.h5-notice-popup__time {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--h5-text-3, #86909c);
|
||||
}
|
||||
|
||||
.h5-notice-popup__content {
|
||||
max-height: min(52vh, 420px);
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
color: var(--h5-text-2, #4e5969);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.h5-notice-popup__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.h5-notice-popup__primary {
|
||||
height: 44px !important;
|
||||
border-radius: 10px !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@ -56,9 +56,96 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-if="previews.length > 1" class="h5-pay__subtotal">
|
||||
<span>{{ t('h5.pay.orderTotal') }}</span>
|
||||
<strong>{{ preview.totalText }}</strong>
|
||||
<p v-if="!preview.error && couponEntry(preview.id)?.error" class="h5-pay__error">
|
||||
{{ couponEntry(preview.id)?.error }}
|
||||
</p>
|
||||
<template v-else-if="!preview.error && couponEntry(preview.id)?.pricing">
|
||||
<div class="h5-pay__coupon">
|
||||
<button
|
||||
type="button"
|
||||
class="h5-pay__coupon-row"
|
||||
:disabled="!couponEntry(preview.id)?.pricing?.availableCoupons.length"
|
||||
@click="toggleCouponList(preview.id)"
|
||||
>
|
||||
<span class="h5-pay__coupon-label">
|
||||
<icon-gift />
|
||||
{{ t('h5.pay.coupon') }}
|
||||
</span>
|
||||
<span
|
||||
class="h5-pay__coupon-value"
|
||||
:class="{ 'is-active': (couponEntry(preview.id)?.selectedCouponId ?? 0) > 0 }"
|
||||
>
|
||||
<span v-if="couponEntry(preview.id)?.loading" class="h5-pay__coupon-spin" aria-hidden="true" />
|
||||
{{ couponSummaryText(preview.id) }}
|
||||
</span>
|
||||
<icon-right
|
||||
v-if="couponEntry(preview.id)?.pricing?.availableCoupons.length"
|
||||
class="h5-pay__coupon-arrow"
|
||||
:class="{ 'is-open': expandedCouponOrder === String(preview.id) }"
|
||||
/>
|
||||
</button>
|
||||
|
||||
<ul v-if="expandedCouponOrder === String(preview.id)" class="h5-pay__coupon-list">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
class="h5-pay__coupon-opt"
|
||||
:class="{ 'is-selected': couponEntry(preview.id)?.selectedCouponId === 0 }"
|
||||
:disabled="couponEntry(preview.id)?.loading"
|
||||
@click="handlePickCoupon(preview.id, 0)"
|
||||
>
|
||||
<span class="h5-pay__coupon-opt-main">
|
||||
<span class="h5-pay__coupon-opt-name">{{ t('h5.pay.couponNone') }}</span>
|
||||
</span>
|
||||
<icon-check v-if="couponEntry(preview.id)?.selectedCouponId === 0" class="h5-pay__coupon-check" />
|
||||
</button>
|
||||
</li>
|
||||
<li v-for="c in couponEntry(preview.id)?.pricing?.availableCoupons ?? []" :key="c.couponInstanceId">
|
||||
<button
|
||||
type="button"
|
||||
class="h5-pay__coupon-opt"
|
||||
:class="{ 'is-selected': couponEntry(preview.id)?.selectedCouponId === c.couponInstanceId }"
|
||||
:disabled="couponEntry(preview.id)?.loading || !isCouponSelectable(preview.id, c)"
|
||||
@click="handlePickCoupon(preview.id, c.couponInstanceId)"
|
||||
>
|
||||
<span class="h5-pay__coupon-opt-benefit">{{ formatCouponBenefit(c) }}</span>
|
||||
<span class="h5-pay__coupon-opt-main">
|
||||
<span class="h5-pay__coupon-opt-name">{{ c.templateName || c.couponCode }}</span>
|
||||
<small>{{ couponHintText(preview.id, c) }}</small>
|
||||
</span>
|
||||
<icon-check
|
||||
v-if="couponEntry(preview.id)?.selectedCouponId === c.couponInstanceId"
|
||||
class="h5-pay__coupon-check"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul v-if="hasDiscount(preview.id)" class="h5-pay__breakdown">
|
||||
<li>
|
||||
<span>{{ t('h5.pay.subtotal') }}</span>
|
||||
<span>{{ formatPayMoney(couponEntry(preview.id)?.pricing?.receivableSubtotal) }}</span>
|
||||
</li>
|
||||
<li v-if="(couponEntry(preview.id)?.pricing?.levelDiscountAmount ?? 0) > 0">
|
||||
<span>
|
||||
{{ t('h5.pay.levelDiscount') }}
|
||||
<template v-if="couponEntry(preview.id)?.pricing?.memberLevelName">
|
||||
({{ couponEntry(preview.id)?.pricing?.memberLevelName }})
|
||||
</template>
|
||||
</span>
|
||||
<span class="is-discount">-{{ formatPayMoney(couponEntry(preview.id)?.pricing?.levelDiscountAmount) }}</span>
|
||||
</li>
|
||||
<li v-if="(couponEntry(preview.id)?.pricing?.couponDiscountAmount ?? 0) > 0">
|
||||
<span>{{ t('h5.pay.couponDiscount') }}</span>
|
||||
<span class="is-discount">-{{ formatPayMoney(couponEntry(preview.id)?.pricing?.couponDiscountAmount) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<div v-if="previews.length > 1 || hasDiscount(preview.id)" class="h5-pay__subtotal">
|
||||
<span>{{ hasDiscount(preview.id) ? t('h5.pay.payable') : t('h5.pay.orderTotal') }}</span>
|
||||
<strong>{{ orderPayableText(preview) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -101,8 +188,19 @@
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { IconClose, IconSafe } from '@arco-design/web-vue/es/icon';
|
||||
import { getOrderDetail, postOrderPay } from '@/api/order';
|
||||
import {
|
||||
IconCheck,
|
||||
IconClose,
|
||||
IconGift,
|
||||
IconRight,
|
||||
IconSafe,
|
||||
} from '@arco-design/web-vue/es/icon';
|
||||
import { getOrderDetail, postOrderPay, type OrderPayCouponOption } from '@/api/order';
|
||||
import {
|
||||
formatCouponBenefit,
|
||||
formatPayMoney,
|
||||
useOrderPayCoupon,
|
||||
} from '@/hooks/orderPayCoupon';
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||
import {
|
||||
filterCustomerFacingFees,
|
||||
@ -171,8 +269,15 @@ const grandTotalText = computed(() => {
|
||||
return formatFeeTotals(totals);
|
||||
});
|
||||
|
||||
/** 单币种时突出显示数字,多币种保留原文 */
|
||||
const couponPricing = useOrderPayCoupon({
|
||||
onNotApplied: () => Message.warning(t('h5.pay.couponNotApplied')),
|
||||
onError: (msg) => Message.error(msg),
|
||||
});
|
||||
const expandedCouponOrder = ref<string | null>(null);
|
||||
|
||||
/** 单币种时突出显示数字,多币种保留原文;营销试算就绪后以后端应付为准 */
|
||||
const displayGrandTotal = computed(() => {
|
||||
if (couponPricing.allReady.value) return formatPayMoney(couponPricing.totalPayable.value);
|
||||
const text = grandTotalText.value;
|
||||
if (!text || text === '--') return '--';
|
||||
if (text.includes('+')) return text;
|
||||
@ -183,9 +288,60 @@ const canConfirm = computed(
|
||||
() =>
|
||||
previews.value.length > 0 &&
|
||||
!loading.value &&
|
||||
couponPricing.allReady.value &&
|
||||
previews.value.every((item) => !item.error && item.totalAmount > 0),
|
||||
);
|
||||
|
||||
function couponEntry(id: number | string) {
|
||||
return couponPricing.entryOf(id);
|
||||
}
|
||||
|
||||
function isCouponSelectable(id: number | string, coupon: OrderPayCouponOption) {
|
||||
return coupon.canUse && !couponPricing.isCouponTakenByOther(id, coupon.couponInstanceId);
|
||||
}
|
||||
|
||||
function couponSummaryText(id: number | string) {
|
||||
const entry = couponEntry(id);
|
||||
if (!entry?.pricing) return '';
|
||||
const { pricing } = entry;
|
||||
if (entry.selectedCouponId > 0) return `-${formatPayMoney(pricing.couponDiscountAmount)}`;
|
||||
const count = pricing.availableCoupons.filter((c) => isCouponSelectable(id, c)).length;
|
||||
return count > 0 ? t('h5.pay.couponAvailableCount', { count }) : t('h5.pay.couponEmpty');
|
||||
}
|
||||
|
||||
function couponHintText(id: number | string, coupon: OrderPayCouponOption) {
|
||||
if (couponPricing.isCouponTakenByOther(id, coupon.couponInstanceId)) {
|
||||
return t('h5.pay.couponUsedByOther');
|
||||
}
|
||||
if (!coupon.canUse && coupon.unavailableReason) return coupon.unavailableReason;
|
||||
const threshold =
|
||||
coupon.minOrderAmount > 0
|
||||
? t('h5.pay.couponMinAmount', { amount: coupon.minOrderAmount })
|
||||
: t('h5.pay.couponNoThreshold');
|
||||
const until = coupon.validEndTime ? String(coupon.validEndTime).slice(0, 10) : '';
|
||||
return until ? `${threshold} · ${t('h5.pay.couponValidUntil', { date: until })}` : threshold;
|
||||
}
|
||||
|
||||
function hasDiscount(id: number | string) {
|
||||
const pricing = couponEntry(id)?.pricing;
|
||||
return !!pricing && (pricing.levelDiscountAmount > 0 || pricing.couponDiscountAmount > 0);
|
||||
}
|
||||
|
||||
function orderPayableText(preview: PayOrderPreview) {
|
||||
const pricing = couponEntry(preview.id)?.pricing;
|
||||
return pricing ? formatPayMoney(pricing.payableAmount) : preview.totalText;
|
||||
}
|
||||
|
||||
function toggleCouponList(id: number | string) {
|
||||
const key = String(id);
|
||||
expandedCouponOrder.value = expandedCouponOrder.value === key ? null : key;
|
||||
}
|
||||
|
||||
async function handlePickCoupon(id: number | string, couponId: number) {
|
||||
await couponPricing.selectCoupon(id, couponId);
|
||||
expandedCouponOrder.value = null;
|
||||
}
|
||||
|
||||
function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
|
||||
if (!raw) return undefined;
|
||||
for (const k of keys) {
|
||||
@ -289,6 +445,7 @@ async function loadPreviews() {
|
||||
}
|
||||
}
|
||||
previews.value = next;
|
||||
await couponPricing.loadAll(next.filter((x) => !x.error).map((x) => x.id));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -304,7 +461,10 @@ async function handleConfirm() {
|
||||
submitting.value = true;
|
||||
try {
|
||||
for (const preview of previews.value) {
|
||||
await postOrderPay({ id: preview.id });
|
||||
await postOrderPay({
|
||||
id: preview.id,
|
||||
couponInstanceId: couponEntry(preview.id)?.selectedCouponId ?? 0,
|
||||
});
|
||||
}
|
||||
Message.success(t('h5.pay.success'));
|
||||
emit('success');
|
||||
@ -325,6 +485,8 @@ watch(
|
||||
} else {
|
||||
previews.value = [];
|
||||
submitting.value = false;
|
||||
couponPricing.reset();
|
||||
expandedCouponOrder.value = null;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@ -545,6 +707,168 @@ watch(
|
||||
}
|
||||
}
|
||||
|
||||
.h5-pay__coupon {
|
||||
margin-top: 4px;
|
||||
border-top: 1px solid #f2f3f5;
|
||||
}
|
||||
|
||||
.h5-pay__coupon-row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font-size: 14px;
|
||||
color: #1d2129;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-pay__coupon-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.arco-icon {
|
||||
color: #f53f3f;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-pay__coupon-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
color: #86909c;
|
||||
text-align: right;
|
||||
|
||||
&.is-active {
|
||||
color: #f53f3f;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-pay__coupon-spin {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #e5e6eb;
|
||||
border-top-color: var(--app-primary-color);
|
||||
border-radius: 50%;
|
||||
animation: h5-pay-spin 0.7s linear infinite;
|
||||
}
|
||||
|
||||
.h5-pay__coupon-arrow {
|
||||
flex-shrink: 0;
|
||||
color: #c9cdd4;
|
||||
font-size: 14px;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&.is-open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.h5-pay__coupon-list {
|
||||
list-style: none;
|
||||
margin: 0 0 8px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.h5-pay__coupon-opt {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&.is-selected {
|
||||
border-color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 6%, #fff);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-pay__coupon-opt-benefit {
|
||||
flex: 0 0 64px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #f53f3f;
|
||||
font-variant-numeric: tabular-nums;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.h5-pay__coupon-opt-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
small {
|
||||
font-size: 11px;
|
||||
color: #86909c;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-pay__coupon-opt-name {
|
||||
font-size: 14px;
|
||||
color: #1d2129;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.h5-pay__coupon-check {
|
||||
flex-shrink: 0;
|
||||
color: var(--app-primary-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.h5-pay__breakdown {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 8px 0 4px;
|
||||
border-top: 1px solid #f2f3f5;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 4px 0;
|
||||
font-size: 13px;
|
||||
color: #4e5969;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.is-discount {
|
||||
color: #f53f3f;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-pay__method {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
@ -871,6 +871,22 @@
|
||||
<a-textarea v-model="row.declareElement" :auto-size="{ minRows: 2, maxRows: 3 }" />
|
||||
</div>
|
||||
|
||||
<div class="h5-po__field">
|
||||
<label class="h5-po__label">{{ t('h5.placeOrder.productId') }}</label>
|
||||
<a-input v-model="row.productId" allow-clear :max-length="100" />
|
||||
</div>
|
||||
|
||||
<div class="h5-po__row">
|
||||
<div class="h5-po__field">
|
||||
<label class="h5-po__label">{{ t('h5.placeOrder.nonStdProductId') }}</label>
|
||||
<a-input v-model="row.nonStdProductId" allow-clear :max-length="100" />
|
||||
</div>
|
||||
<div class="h5-po__field">
|
||||
<label class="h5-po__label">{{ t('h5.placeOrder.stdProductId') }}</label>
|
||||
<a-input v-model="row.stdProductId" allow-clear :max-length="100" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="h5-po__row">
|
||||
<div class="h5-po__field">
|
||||
<label class="h5-po__label">{{ t('h5.placeOrder.brand') }}</label>
|
||||
@ -1477,6 +1493,9 @@ type DeclareItem = {
|
||||
imHsCode: string;
|
||||
exHsCode: string;
|
||||
declareElement: string;
|
||||
productId: string;
|
||||
nonStdProductId: string;
|
||||
stdProductId: string;
|
||||
skuImage: string;
|
||||
brand: string;
|
||||
brandType: string;
|
||||
@ -1511,6 +1530,9 @@ function createEmptyItem(): DeclareItem {
|
||||
imHsCode: '',
|
||||
exHsCode: '',
|
||||
declareElement: '',
|
||||
productId: '',
|
||||
nonStdProductId: '',
|
||||
stdProductId: '',
|
||||
skuImage: '',
|
||||
brand: '',
|
||||
brandType: '',
|
||||
@ -2691,6 +2713,9 @@ function mapDetailItems(rawItems: any[]): DeclareItem[] {
|
||||
imHsCode: String(pickDetail(raw, 'imHsCode', 'ImHsCode') ?? hs),
|
||||
exHsCode: String(pickDetail(raw, 'exHsCode', 'ExHsCode') ?? hs),
|
||||
declareElement: String(pickDetail(raw, 'declareElement', 'DeclareElement') ?? ''),
|
||||
productId: String(pickDetail(raw, 'productId', 'ProductId') ?? ''),
|
||||
nonStdProductId: String(pickDetail(raw, 'nonStdProductId', 'NonStdProductId') ?? ''),
|
||||
stdProductId: String(pickDetail(raw, 'stdProductId', 'StdProductId') ?? ''),
|
||||
skuImage: normalizeSkuImageForStorage(
|
||||
String(pickDetail(raw, 'skuImage', 'SkuImage') ?? ''),
|
||||
),
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
v-model:visible="placeOrderVisible"
|
||||
/>
|
||||
<H5FollowOfficialModal v-if="followModalReady" />
|
||||
<H5NoticePopup v-if="noticePopupReady" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -58,6 +59,9 @@ const H5CreateOrderFlow = defineAsyncComponent(
|
||||
const H5FollowOfficialModal = defineAsyncComponent(
|
||||
() => import('./components/H5FollowOfficialModal.vue'),
|
||||
);
|
||||
const H5NoticePopup = defineAsyncComponent(
|
||||
() => import('./components/H5NoticePopup.vue'),
|
||||
);
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const route = useRoute();
|
||||
@ -88,6 +92,7 @@ async function openPlaceOrder() {
|
||||
const showTabBar = computed(() => route.meta?.h5HideTabBar !== true);
|
||||
const showHeader = computed(() => route.meta?.h5HideHeader !== true);
|
||||
const followModalReady = computed(() => isLogin());
|
||||
const noticePopupReady = computed(() => isLogin());
|
||||
|
||||
const headerTitle = computed(() => {
|
||||
const key = String(route.meta?.locale || 'h5.center.title');
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
export default {
|
||||
'h5.center.title': 'Customer Center',
|
||||
'h5.noticePopup.title': 'Notice',
|
||||
'h5.noticePopup.markRead': 'Mark as read',
|
||||
'h5.noticePopup.later': 'Later',
|
||||
'h5.noticePopup.readSuccess': 'Marked as read',
|
||||
'h5.noticePopup.readFailed': 'Could not mark as read. Please try again.',
|
||||
'h5.noticePopup.createTime': 'Published: ',
|
||||
'h5.tab.assistant': 'AI Advisor',
|
||||
'h5.tab.price': 'Rates',
|
||||
'h5.tab.createOrder': 'Order',
|
||||
@ -450,6 +456,9 @@ export default {
|
||||
'h5.placeOrder.imHsCode': 'Import HS',
|
||||
'h5.placeOrder.exHsCode': 'Export HS',
|
||||
'h5.placeOrder.declareElement': 'Export declare element',
|
||||
'h5.placeOrder.productId': 'Product ID',
|
||||
'h5.placeOrder.nonStdProductId': 'Non-standard product ID',
|
||||
'h5.placeOrder.stdProductId': 'Standard product ID',
|
||||
'h5.placeOrder.brand': 'Brand',
|
||||
'h5.placeOrder.model': 'Model',
|
||||
'h5.placeOrder.material': 'Material',
|
||||
@ -921,6 +930,19 @@ export default {
|
||||
'h5.pay.balanceLow': 'Your balance is not enough for this payment. Recharge first, then come back to pay.',
|
||||
'h5.pay.goRecharge': 'Recharge',
|
||||
'h5.pay.later': 'Later',
|
||||
'h5.pay.coupon': 'Coupon',
|
||||
'h5.pay.couponNone': 'No coupon',
|
||||
'h5.pay.couponEmpty': 'No coupons available',
|
||||
'h5.pay.couponAvailableCount': '{count} available',
|
||||
'h5.pay.couponUsedByOther': 'Used by another order',
|
||||
'h5.pay.couponMinAmount': 'Base freight ≥ {amount}',
|
||||
'h5.pay.couponNoThreshold': 'No minimum',
|
||||
'h5.pay.couponValidUntil': 'Valid until {date}',
|
||||
'h5.pay.couponNotApplied': 'This coupon is not better than your member discount; member discount applied',
|
||||
'h5.pay.subtotal': 'Fees total',
|
||||
'h5.pay.levelDiscount': 'Member discount',
|
||||
'h5.pay.couponDiscount': 'Coupon discount',
|
||||
'h5.pay.payable': 'To pay',
|
||||
'h5.me.balance': 'Balance',
|
||||
'h5.me.recharge': 'Recharge',
|
||||
'h5.me.points': '12-month points',
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
export default {
|
||||
'h5.center.title': '客户中心',
|
||||
'h5.noticePopup.title': '公告',
|
||||
'h5.noticePopup.markRead': '已读',
|
||||
'h5.noticePopup.later': '稍后',
|
||||
'h5.noticePopup.readSuccess': '已标记已读',
|
||||
'h5.noticePopup.readFailed': '标记已读失败,请稍后重试',
|
||||
'h5.noticePopup.createTime': '发布时间:',
|
||||
'h5.tab.assistant': '顾问',
|
||||
'h5.tab.price': '查价',
|
||||
'h5.tab.createOrder': '下单',
|
||||
@ -445,6 +451,9 @@ export default {
|
||||
'h5.placeOrder.imHsCode': '进口HS',
|
||||
'h5.placeOrder.exHsCode': '出口HS',
|
||||
'h5.placeOrder.declareElement': '出口申报要素',
|
||||
'h5.placeOrder.productId': '产品标识编码',
|
||||
'h5.placeOrder.nonStdProductId': '非标产品标识编码',
|
||||
'h5.placeOrder.stdProductId': '标准产品标识编码',
|
||||
'h5.placeOrder.brand': '品牌',
|
||||
'h5.placeOrder.model': '型号',
|
||||
'h5.placeOrder.material': '材质',
|
||||
@ -906,6 +915,19 @@ export default {
|
||||
'h5.pay.balanceLow': '当前余额不足以完成本次支付,请先充值后再回来付款',
|
||||
'h5.pay.goRecharge': '去充值',
|
||||
'h5.pay.later': '稍后再说',
|
||||
'h5.pay.coupon': '优惠券',
|
||||
'h5.pay.couponNone': '不使用优惠券',
|
||||
'h5.pay.couponEmpty': '暂无可用券',
|
||||
'h5.pay.couponAvailableCount': '{count} 张可用',
|
||||
'h5.pay.couponUsedByOther': '已用于其他订单',
|
||||
'h5.pay.couponMinAmount': '基础运费满 {amount} 可用',
|
||||
'h5.pay.couponNoThreshold': '无门槛',
|
||||
'h5.pay.couponValidUntil': '有效期至 {date}',
|
||||
'h5.pay.couponNotApplied': '该券优惠不高于会员折扣,已按会员折扣计算',
|
||||
'h5.pay.subtotal': '费用合计',
|
||||
'h5.pay.levelDiscount': '会员折扣',
|
||||
'h5.pay.couponDiscount': '优惠券抵扣',
|
||||
'h5.pay.payable': '实付',
|
||||
'h5.me.balance': '账户余额',
|
||||
'h5.me.recharge': '去充值',
|
||||
'h5.me.points': '近12个月积分',
|
||||
|
||||
@ -15,30 +15,40 @@
|
||||
class="monitor-top-tabs"
|
||||
>
|
||||
<a-tab-pane key="order" :title="$t('monitor.logistics.tab.order')">
|
||||
<div class="monitor-ops-toolbar">
|
||||
<span class="monitor-ops-toolbar__label">{{ $t('monitor.logistics.filter.opsPeriod') }}</span>
|
||||
<a-radio-group v-model="opsDimension" type="button" size="small">
|
||||
<a-radio value="day">{{ $t('monitor.logistics.filter.periodDay') }}</a-radio>
|
||||
<a-radio value="week">{{ $t('monitor.logistics.filter.periodWeek') }}</a-radio>
|
||||
<a-radio value="month">{{ $t('monitor.logistics.filter.periodMonth') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="[12, 12]" align="stretch" class="logistics-monitor-stats">
|
||||
<a-col
|
||||
v-for="card in orderStatCards"
|
||||
v-for="card in opsStatCards"
|
||||
:key="card.key"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="12"
|
||||
:xl="6"
|
||||
:md="8"
|
||||
:lg="8"
|
||||
:xl="8"
|
||||
>
|
||||
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
|
||||
<div class="stat-item">
|
||||
<div class="stat-item__head">
|
||||
<div class="stat-label">{{ $t(card.labelKey) }}</div>
|
||||
<div class="stat-item__icon stat-item__icon--primary">
|
||||
<icon-bar-chart />
|
||||
<img v-if="card.iconSrc" :src="card.iconSrc" alt="" class="stat-item__icon-img" />
|
||||
<icon-bar-chart v-else />
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value" :class="{ 'stat-value--warning': card.warning }">{{ card.value }}</div>
|
||||
<div class="stat-value">{{ card.value }}</div>
|
||||
<div v-if="card.growth.show" class="stat-footer">
|
||||
<div class="stat-trend" :class="card.growth.up ? 'up' : 'down'">
|
||||
<icon-arrow-up v-if="card.growth.up" />
|
||||
<icon-arrow-down v-else />
|
||||
{{ card.growth.text }} {{ $t(card.compareKey) }}
|
||||
{{ card.growth.text }} {{ $t(opsCompareKey) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -46,15 +56,46 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<p class="monitor-queue-title">{{ $t('monitor.logistics.queue.title') }}</p>
|
||||
<a-row :gutter="[12, 12]" align="stretch" class="logistics-monitor-stats">
|
||||
<a-col
|
||||
v-for="card in queueStatCards"
|
||||
:key="card.key"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="6"
|
||||
:xl="6"
|
||||
>
|
||||
<a-card hoverable class="stat-card" :body-style="{ padding: '12px', height: '100%' }">
|
||||
<div class="stat-item">
|
||||
<div class="stat-item__head">
|
||||
<div class="stat-label">{{ $t(card.labelKey) }}</div>
|
||||
<div
|
||||
class="stat-item__icon"
|
||||
:class="card.warning ? 'stat-item__icon--warning' : 'stat-item__icon--primary'"
|
||||
>
|
||||
<img v-if="card.iconSrc" :src="card.iconSrc" alt="" class="stat-item__icon-img" />
|
||||
<icon-bar-chart v-else />
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-value" :class="{ 'stat-value--warning': card.warning }">
|
||||
{{ card.value }}
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="[16, 16]" class="logistics-monitor-trend-charts">
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card :title="$t('monitor.logistics.chart.orders7d')" class="chart-card">
|
||||
<a-card :title="$t(orderTrendTitleKey)" class="chart-card">
|
||||
<div ref="orderChartRef" class="chart-host"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<a-col :xs="24" :lg="12">
|
||||
<a-card :title="$t('monitor.logistics.chart.revenueGross7d')" class="chart-card">
|
||||
<a-card :title="$t(revenueTrendTitleKey)" class="chart-card">
|
||||
<div ref="profitChartRef" class="chart-host"></div>
|
||||
</a-card>
|
||||
</a-col>
|
||||
@ -235,8 +276,15 @@
|
||||
formatMonitorNumber,
|
||||
formatMonitorCurrency,
|
||||
formatMonitorGrowth,
|
||||
type MonitorDimension,
|
||||
type MonitorOverview,
|
||||
} from '@/api/monitor';
|
||||
import iconOrderNew from '@/assets/icons/workplace/order-new.svg?url';
|
||||
import iconPayment from '@/assets/icons/workplace/order-payment.svg?url';
|
||||
import iconGrowth from '@/assets/icons/workplace/order-growth.svg?url';
|
||||
import iconPendingOnline from '@/assets/icons/workplace/order-pending-online.svg?url';
|
||||
import iconProblematic from '@/assets/icons/workplace/order-problematic.svg?url';
|
||||
import iconShipped from '@/assets/icons/workplace/order-shipped.svg?url';
|
||||
|
||||
defineOptions({ name: 'Monitor' });
|
||||
|
||||
@ -244,11 +292,12 @@
|
||||
|
||||
const overview = ref<MonitorOverview | null>(null);
|
||||
const overviewLoading = ref(true);
|
||||
const opsDimension = ref<MonitorDimension>('day');
|
||||
|
||||
async function loadOverview() {
|
||||
overviewLoading.value = true;
|
||||
try {
|
||||
overview.value = await getMonitorOverview();
|
||||
overview.value = await getMonitorOverview(opsDimension.value);
|
||||
} catch {
|
||||
overview.value = null;
|
||||
} finally {
|
||||
@ -265,71 +314,82 @@
|
||||
nextTick(() => onResize());
|
||||
}
|
||||
|
||||
const orderStatCards = computed(() => {
|
||||
const order = overview.value?.order;
|
||||
const opsCompareKey = computed(() => {
|
||||
if (opsDimension.value === 'week') return 'monitor.logistics.vsLastWeek';
|
||||
if (opsDimension.value === 'month') return 'monitor.logistics.vsLastMonth';
|
||||
return 'monitor.logistics.vsYesterday';
|
||||
});
|
||||
|
||||
const orderTrendTitleKey = computed(() => {
|
||||
if (opsDimension.value === 'week') return 'monitor.logistics.chart.ordersWeek';
|
||||
if (opsDimension.value === 'month') return 'monitor.logistics.chart.ordersMonth';
|
||||
return 'monitor.logistics.chart.ordersDay';
|
||||
});
|
||||
|
||||
const revenueTrendTitleKey = computed(() => {
|
||||
if (opsDimension.value === 'week') return 'monitor.logistics.chart.revenueGrossWeek';
|
||||
if (opsDimension.value === 'month') return 'monitor.logistics.chart.revenueGrossMonth';
|
||||
return 'monitor.logistics.chart.revenueGrossDay';
|
||||
});
|
||||
|
||||
const opsStatCards = computed(() => {
|
||||
const ops = overview.value?.ops;
|
||||
const dim = opsDimension.value;
|
||||
return [
|
||||
{
|
||||
key: 'todayOrders',
|
||||
labelKey: 'monitor.logistics.stat.todayOrders',
|
||||
value: formatMonitorNumber(order?.todayCreated ?? 0),
|
||||
growth: formatMonitorGrowth(order?.todayCreatedGrowthPercent),
|
||||
compareKey: 'monitor.logistics.vsYesterday',
|
||||
warning: false,
|
||||
icon: 'calendar',
|
||||
key: 'orderCount',
|
||||
labelKey: `monitor.logistics.ops.orderCount.${dim}`,
|
||||
value: formatMonitorNumber(ops?.orderCount ?? 0),
|
||||
growth: formatMonitorGrowth(ops?.orderCountGrowthPercent),
|
||||
iconSrc: iconOrderNew,
|
||||
},
|
||||
{
|
||||
key: 'historicalOrders',
|
||||
labelKey: 'monitor.logistics.stat.historicalOrders',
|
||||
value: formatMonitorNumber(order?.total ?? 0),
|
||||
growth: formatMonitorGrowth(order?.totalMonthGrowthPercent),
|
||||
compareKey: 'monitor.logistics.vsLastMonth',
|
||||
warning: false,
|
||||
icon: 'history',
|
||||
key: 'revenue',
|
||||
labelKey: `monitor.logistics.ops.revenue.${dim}`,
|
||||
value: formatMonitorCurrency(ops?.revenue ?? 0),
|
||||
growth: formatMonitorGrowth(ops?.revenueGrowthPercent),
|
||||
iconSrc: iconPayment,
|
||||
},
|
||||
{
|
||||
key: 'grossProfit',
|
||||
labelKey: `monitor.logistics.ops.grossProfit.${dim}`,
|
||||
value: formatMonitorCurrency(ops?.grossProfit ?? 0),
|
||||
growth: formatMonitorGrowth(ops?.grossProfitGrowthPercent),
|
||||
iconSrc: iconGrowth,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const queueStatCards = computed(() => {
|
||||
const queue = overview.value?.queue ?? overview.value?.order;
|
||||
return [
|
||||
{
|
||||
key: 'pendingPayment',
|
||||
labelKey: 'monitor.logistics.stat.pendingPayment',
|
||||
value: formatMonitorNumber(order?.pendingPayment ?? 0),
|
||||
growth: { show: false, up: true, text: '' },
|
||||
compareKey: 'monitor.logistics.vsYesterday',
|
||||
warning: true,
|
||||
icon: 'stamp',
|
||||
value: formatMonitorNumber(queue?.pendingPayment ?? 0),
|
||||
warning: (queue?.pendingPayment ?? 0) > 0,
|
||||
iconSrc: iconPayment,
|
||||
},
|
||||
{
|
||||
key: 'pendingOnline',
|
||||
labelKey: 'monitor.logistics.stat.pendingOnline',
|
||||
value: formatMonitorNumber(order?.pendingOnline ?? 0),
|
||||
growth: { show: false, up: true, text: '' },
|
||||
compareKey: 'monitor.logistics.vsYesterday',
|
||||
warning: false,
|
||||
icon: 'send',
|
||||
value: formatMonitorNumber(queue?.pendingOnline ?? 0),
|
||||
warning: (queue?.pendingOnline ?? 0) > 0,
|
||||
iconSrc: iconPendingOnline,
|
||||
},
|
||||
{
|
||||
key: 'pendingSign',
|
||||
labelKey: 'monitor.logistics.stat.pendingSign',
|
||||
value: formatMonitorNumber(order?.pendingSign ?? 0),
|
||||
growth: { show: false, up: true, text: '' },
|
||||
compareKey: 'monitor.logistics.vsYesterday',
|
||||
value: formatMonitorNumber(queue?.pendingSign ?? 0),
|
||||
warning: false,
|
||||
icon: 'check',
|
||||
iconSrc: iconShipped,
|
||||
},
|
||||
{
|
||||
key: 'orderExceptions',
|
||||
labelKey: 'monitor.logistics.stat.orderExceptions',
|
||||
value: formatMonitorNumber(order?.problematic ?? 0),
|
||||
growth: { show: false, up: false, text: '' },
|
||||
compareKey: 'monitor.logistics.vsYesterday',
|
||||
warning: true,
|
||||
icon: 'exception',
|
||||
},
|
||||
{
|
||||
key: 'todayRevenue',
|
||||
labelKey: 'monitor.logistics.stat.todayRevenue',
|
||||
value: formatMonitorCurrency(order?.todayRevenue ?? 0),
|
||||
growth: formatMonitorGrowth(order?.todayRevenueGrowthPercent),
|
||||
compareKey: 'monitor.logistics.vsYesterday',
|
||||
warning: false,
|
||||
icon: 'chart',
|
||||
value: formatMonitorNumber(queue?.problematic ?? 0),
|
||||
warning: (queue?.problematic ?? 0) > 0,
|
||||
iconSrc: iconProblematic,
|
||||
},
|
||||
];
|
||||
});
|
||||
@ -791,8 +851,8 @@
|
||||
function ensureOrderLineCharts() {
|
||||
const primary = readCssVar('--app-primary-color', '#2D8C8C');
|
||||
const axisLine = readCssVar('--color-border-2', '#e5e6eb');
|
||||
const orderTrend = overview.value?.orderTrend7d ?? [];
|
||||
const revenueTrend = overview.value?.revenueTrend7d ?? [];
|
||||
const orderTrend = overview.value?.orderTrend ?? overview.value?.orderTrend7d ?? [];
|
||||
const revenueTrend = overview.value?.revenueTrend ?? overview.value?.revenueTrend7d ?? [];
|
||||
const dayLabels = orderTrend.length
|
||||
? orderTrend.map((item) => item.label)
|
||||
: revenueTrend.map((item) => item.label);
|
||||
@ -941,6 +1001,10 @@
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(opsDimension, () => {
|
||||
void loadOverview();
|
||||
});
|
||||
|
||||
watch([filterLine, filterSignDays], () => {
|
||||
nextTick(() => {
|
||||
if (monitorTab.value === 'track') renderSignRateChart();
|
||||
@ -1079,6 +1143,26 @@
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.monitor-ops-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.monitor-ops-toolbar__label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monitor-queue-title {
|
||||
margin: 16px 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.logistics-monitor-stats {
|
||||
margin-bottom: 10px;
|
||||
|
||||
@ -1183,6 +1267,12 @@
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.stat-item__icon-img {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.stat-item__icon--primary {
|
||||
background: var(--app-primary-color-light);
|
||||
color: var(--app-primary-color);
|
||||
|
||||
@ -8,18 +8,35 @@ export default {
|
||||
'monitor.logistics.stat.pendingSign': 'Pending delivery',
|
||||
'monitor.logistics.stat.orderExceptions': 'Order exceptions',
|
||||
'monitor.logistics.stat.todayRevenue': 'Revenue today',
|
||||
'monitor.logistics.ops.orderCount.day': 'Orders today',
|
||||
'monitor.logistics.ops.orderCount.week': 'Orders this week',
|
||||
'monitor.logistics.ops.orderCount.month': 'Orders this month',
|
||||
'monitor.logistics.ops.revenue.day': 'Revenue today',
|
||||
'monitor.logistics.ops.revenue.week': 'Revenue this week',
|
||||
'monitor.logistics.ops.revenue.month': 'Revenue this month',
|
||||
'monitor.logistics.ops.grossProfit.day': 'Gross profit today',
|
||||
'monitor.logistics.ops.grossProfit.week': 'Gross profit this week',
|
||||
'monitor.logistics.ops.grossProfit.month': 'Gross profit this month',
|
||||
'monitor.logistics.queue.title': 'Live backlog',
|
||||
'monitor.logistics.stat.totalCustomers': 'Total customers',
|
||||
'monitor.logistics.stat.newCustomers': 'New customers',
|
||||
'monitor.logistics.stat.onlineCustomers': 'Online customers',
|
||||
'monitor.logistics.stat.inactiveLogin7d': 'No login 7+ days',
|
||||
'monitor.logistics.vsYesterday': 'vs yesterday',
|
||||
'monitor.logistics.vsLastWeek': 'vs last week',
|
||||
'monitor.logistics.vsLastMonth': 'vs last month',
|
||||
'monitor.logistics.hint.newCustomersToday': 'New sign-ups today',
|
||||
'monitor.logistics.hint.onlineRealtime': 'Active sessions (live)',
|
||||
'monitor.logistics.hint.inactiveLogin7d': 'Customers with no login in 7+ days',
|
||||
'monitor.logistics.hint.inactiveLogin7dCompare': '+5 vs last week',
|
||||
'monitor.logistics.chart.orders7d': 'Order volume (last 7 days)',
|
||||
'monitor.logistics.chart.ordersDay': 'Orders · last 7 days',
|
||||
'monitor.logistics.chart.ordersWeek': 'Orders · last 8 weeks',
|
||||
'monitor.logistics.chart.ordersMonth': 'Orders · last 6 months',
|
||||
'monitor.logistics.chart.revenueGross7d': 'Revenue & gross profit (last 7 days)',
|
||||
'monitor.logistics.chart.revenueGrossDay': 'Revenue & gross · last 7 days',
|
||||
'monitor.logistics.chart.revenueGrossWeek': 'Revenue & gross · last 8 weeks',
|
||||
'monitor.logistics.chart.revenueGrossMonth': 'Revenue & gross · last 6 months',
|
||||
'monitor.logistics.chart.customerTrendsSection': 'By lane · week / month trends',
|
||||
'monitor.logistics.chart.customerOrdersByLine': 'Order volume trend',
|
||||
'monitor.logistics.chart.customerReceivableByLine': 'Accounts receivable trend',
|
||||
@ -49,6 +66,8 @@ export default {
|
||||
'monitor.logistics.axis.d6': 'Day 6',
|
||||
'monitor.logistics.axis.today': 'Today',
|
||||
'monitor.logistics.filter.statPeriod': 'Period',
|
||||
'monitor.logistics.filter.opsPeriod': 'Ops period',
|
||||
'monitor.logistics.filter.periodDay': 'Day',
|
||||
'monitor.logistics.filter.periodWeek': 'Week',
|
||||
'monitor.logistics.filter.periodMonth': 'Month',
|
||||
'monitor.logistics.pie.sectionTitle': 'Track & OTD (by lane / period)',
|
||||
|
||||
@ -8,18 +8,35 @@ export default {
|
||||
'monitor.logistics.stat.pendingSign': '待签收订单',
|
||||
'monitor.logistics.stat.orderExceptions': '订单异常',
|
||||
'monitor.logistics.stat.todayRevenue': '今日总营收',
|
||||
'monitor.logistics.ops.orderCount.day': '今日订单量',
|
||||
'monitor.logistics.ops.orderCount.week': '本周订单量',
|
||||
'monitor.logistics.ops.orderCount.month': '本月订单量',
|
||||
'monitor.logistics.ops.revenue.day': '今日营收',
|
||||
'monitor.logistics.ops.revenue.week': '本周营收',
|
||||
'monitor.logistics.ops.revenue.month': '本月营收',
|
||||
'monitor.logistics.ops.grossProfit.day': '今日毛利',
|
||||
'monitor.logistics.ops.grossProfit.week': '本周毛利',
|
||||
'monitor.logistics.ops.grossProfit.month': '本月毛利',
|
||||
'monitor.logistics.queue.title': '实时积压',
|
||||
'monitor.logistics.stat.totalCustomers': '客户总数',
|
||||
'monitor.logistics.stat.newCustomers': '新增客户数',
|
||||
'monitor.logistics.stat.onlineCustomers': '在线客户数',
|
||||
'monitor.logistics.stat.inactiveLogin7d': '超7天未登录',
|
||||
'monitor.logistics.vsYesterday': '较昨日',
|
||||
'monitor.logistics.vsLastWeek': '较上周',
|
||||
'monitor.logistics.vsLastMonth': '较上月',
|
||||
'monitor.logistics.hint.newCustomersToday': '统计口径:今日新注册',
|
||||
'monitor.logistics.hint.onlineRealtime': '当前在线会话数(实时)',
|
||||
'monitor.logistics.hint.inactiveLogin7d': '超7天未产生登录行为的客户',
|
||||
'monitor.logistics.hint.inactiveLogin7dCompare': '较上周 +5 人',
|
||||
'monitor.logistics.chart.orders7d': '近7日订单数趋势',
|
||||
'monitor.logistics.chart.ordersDay': '近7日订单量',
|
||||
'monitor.logistics.chart.ordersWeek': '近8周订单量',
|
||||
'monitor.logistics.chart.ordersMonth': '近6月订单量',
|
||||
'monitor.logistics.chart.revenueGross7d': '近7日营收 & 毛利趋势',
|
||||
'monitor.logistics.chart.revenueGrossDay': '近7日营收 & 毛利',
|
||||
'monitor.logistics.chart.revenueGrossWeek': '近8周营收 & 毛利',
|
||||
'monitor.logistics.chart.revenueGrossMonth': '近6月营收 & 毛利',
|
||||
'monitor.logistics.chart.customerTrendsSection': '按线路 · 周/月趋势',
|
||||
'monitor.logistics.chart.customerOrdersByLine': '单量趋势',
|
||||
'monitor.logistics.chart.customerReceivableByLine': '应收趋势',
|
||||
@ -49,6 +66,8 @@ export default {
|
||||
'monitor.logistics.axis.d6': '6日',
|
||||
'monitor.logistics.axis.today': '今日',
|
||||
'monitor.logistics.filter.statPeriod': '统计周期',
|
||||
'monitor.logistics.filter.opsPeriod': '经营周期',
|
||||
'monitor.logistics.filter.periodDay': '日',
|
||||
'monitor.logistics.filter.periodWeek': '周',
|
||||
'monitor.logistics.filter.periodMonth': '月',
|
||||
'monitor.logistics.pie.sectionTitle': '轨迹与 OTD 达成(按线路 / 周期)',
|
||||
|
||||
63
src/views/login/components/LoginPortalSwitch.vue
Normal file
63
src/views/login/components/LoginPortalSwitch.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<a-tooltip :content="label" position="tl">
|
||||
<router-link
|
||||
class="login-portal-switch"
|
||||
:to="to"
|
||||
replace
|
||||
:aria-label="label"
|
||||
>
|
||||
<icon-swap :size="16" />
|
||||
</router-link>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { IconSwap } from '@arco-design/web-vue/es/icon';
|
||||
import { ADMIN_LOGIN_PATH, CUSTOMER_LOGIN_PATH } from '@/utils/login-portal';
|
||||
|
||||
const props = defineProps<{
|
||||
/** 当前页面对侧:admin=去管理登录;customer=去客户登录 */
|
||||
target: 'admin' | 'customer';
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const to = computed(() =>
|
||||
props.target === 'admin' ? ADMIN_LOGIN_PATH : CUSTOMER_LOGIN_PATH,
|
||||
);
|
||||
|
||||
const label = computed(() =>
|
||||
props.target === 'admin'
|
||||
? t('login.switchToAdmin')
|
||||
: t('login.switchToCustomer'),
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.login-portal-switch {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
z-index: 2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
color: rgba(26, 45, 74, 0.22);
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
opacity: 0.7;
|
||||
transition: color 0.15s ease, opacity 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: rgba(26, 45, 74, 0.42);
|
||||
opacity: 1;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -73,7 +73,7 @@
|
||||
{{ loading ? t('login.submitting') : t('login.form.login') }}
|
||||
</button>
|
||||
|
||||
<div class="agreement-area">
|
||||
<div v-if="isCustomerLogin" class="agreement-area">
|
||||
<a-checkbox v-model="agreementChecked">
|
||||
<span class="agreement-line">
|
||||
{{ t('login.agreement.readPrefix') }}
|
||||
@ -344,7 +344,7 @@ function validateForm(): boolean {
|
||||
async function handleSubmit() {
|
||||
if (loading.value) return;
|
||||
|
||||
if (!agreementChecked.value) {
|
||||
if (isCustomerLogin.value && !agreementChecked.value) {
|
||||
Message.warning('请先阅读并勾选同意隐私政策与使用须知');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -87,6 +87,7 @@
|
||||
@switch-pwd="setLoginMode('pwd')"
|
||||
/>
|
||||
</div>
|
||||
<LoginPortalSwitch :target="isCustomerLogin ? 'admin' : 'customer'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -110,6 +111,7 @@ import { IconSunFill, IconMoonFill } from '@arco-design/web-vue/es/icon';
|
||||
import LoginForm from './components/pwd-login/index.vue';
|
||||
import LoginHero from './components/LoginHero.vue';
|
||||
import LoginLangToggle from './components/LoginLangToggle.vue';
|
||||
import LoginPortalSwitch from './components/LoginPortalSwitch.vue';
|
||||
import {
|
||||
LOGIN_CHARACTERS_KEY,
|
||||
type LoginCharactersApi,
|
||||
|
||||
@ -29,6 +29,8 @@
|
||||
'login.wechat.expired': 'QR code expired',
|
||||
'login.wechat.expiredTip': 'Refresh before scanning to avoid a failed login',
|
||||
'login.wechat.longPress': 'In WeChat, long-press the QR code to recognize',
|
||||
'login.switchToAdmin': 'Switch to staff login',
|
||||
'login.switchToCustomer': 'Switch to customer login',
|
||||
'login.theme.toDay': 'Switch to day mode',
|
||||
'login.theme.toNight': 'Switch to night mode',
|
||||
'login.lang.toEn': 'Switch to English',
|
||||
|
||||
@ -29,6 +29,8 @@
|
||||
'login.wechat.refreshExpired': '点击刷新二维码',
|
||||
'login.wechat.expired': '二维码已过期',
|
||||
'login.wechat.expiredTip': '请先刷新后再扫码,避免扫码无效',
|
||||
'login.switchToAdmin': '切换至管理登录',
|
||||
'login.switchToCustomer': '切换至客户登录',
|
||||
'login.theme.toDay': '切换为日间模式',
|
||||
'login.theme.toNight': '切换为夜间模式',
|
||||
'login.lang.toEn': '切换为 English',
|
||||
|
||||
@ -216,6 +216,9 @@ export const ORDER_DETAIL_DECLARATION_COLUMNS = [
|
||||
{ key: 'imHsCode', labelKey: 'order.myOrders.drawer.declarationImHsCode', width: 100 },
|
||||
{ key: 'exHsCode', labelKey: 'order.myOrders.drawer.declarationExHsCode', width: 100 },
|
||||
{ key: 'declareElement', labelKey: 'order.myOrders.drawer.declarationDeclareElement', width: 160 },
|
||||
{ key: 'productId', labelKey: 'order.myOrders.drawer.declarationProductId', width: 130 },
|
||||
{ key: 'nonStdProductId', labelKey: 'order.myOrders.drawer.declarationNonStdProductId', width: 140 },
|
||||
{ key: 'stdProductId', labelKey: 'order.myOrders.drawer.declarationStdProductId', width: 140 },
|
||||
{ key: 'skuImage', labelKey: 'order.myOrders.drawer.declarationSkuImage', width: 56 },
|
||||
{ key: 'category', labelKey: 'order.myOrders.drawer.declarationCategory', width: 80 },
|
||||
{ key: 'brand', labelKey: 'order.myOrders.drawer.declarationBrand', width: 80 },
|
||||
@ -243,6 +246,9 @@ export function normalizeDeclarationItemRow(row: Record<string, any>) {
|
||||
imHsCode,
|
||||
exHsCode,
|
||||
declareElement: row.declareElement ?? row.DeclareElement ?? '',
|
||||
productId: row.productId ?? row.ProductId ?? '',
|
||||
nonStdProductId: row.nonStdProductId ?? row.NonStdProductId ?? '',
|
||||
stdProductId: row.stdProductId ?? row.StdProductId ?? '',
|
||||
hsCode: row.hsCode ?? row.HsCode ?? exHsCode ?? imHsCode ?? '',
|
||||
isMagnetic: !!row.isMagnetic,
|
||||
};
|
||||
@ -266,6 +272,9 @@ export function buildDeclarationPayloadFromRows(rows: Record<string, any>[]) {
|
||||
imHsCode: row.imHsCode || row.exHsCode || row.hsCode || undefined,
|
||||
exHsCode: row.exHsCode || row.imHsCode || row.hsCode || undefined,
|
||||
declareElement: row.declareElement || undefined,
|
||||
productId: row.productId || undefined,
|
||||
nonStdProductId: row.nonStdProductId || undefined,
|
||||
stdProductId: row.stdProductId || undefined,
|
||||
skuImage: row.skuImage || undefined,
|
||||
category: row.category || undefined,
|
||||
brand: row.brand || undefined,
|
||||
|
||||
@ -0,0 +1,519 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { TableColumnData, TableRowSelection } from '@arco-design/web-vue'
|
||||
import ModalPro from '@/components/ModalPro/index.vue'
|
||||
import useModal from '@/components/ModalPro/useModal'
|
||||
import { postOrderList } from '@/api/order'
|
||||
import { createCustoms9610FromOrders } from '@/api/order/customs9610'
|
||||
import { getExportQualificationList } from '@/api/customer/exportQualification'
|
||||
import { getDictListAll } from '@/api/system/dict'
|
||||
import { getOrderStatusMeta, resolveOrderStatusCode } from '@/constants/orderStatus'
|
||||
import { $baseMessage } from '@/utils'
|
||||
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
|
||||
|
||||
interface OrderPickRow {
|
||||
id: number
|
||||
serviceNo: string
|
||||
clientNo: string
|
||||
customerName: string
|
||||
country: string
|
||||
statusName: string
|
||||
createTime: string
|
||||
/** 已是 9610 汇总申报的运单不可再选 */
|
||||
is9610: boolean
|
||||
/** a-table 行级禁用勾选 */
|
||||
disabled: boolean
|
||||
}
|
||||
|
||||
interface FailRow {
|
||||
orderId: number
|
||||
serviceNo: string
|
||||
message: string
|
||||
}
|
||||
|
||||
const ORDER_PAGE_SIZE = 30
|
||||
const SUMMARY_MODE = '9610summary'
|
||||
|
||||
const props = defineProps<{
|
||||
isAdmin: boolean
|
||||
customerOptions: Array<{ label: string; value: number }>
|
||||
/** 列表页已选客户,作为管理端默认客户 */
|
||||
defaultCustomerId?: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const [registerModal, { openModal, closeModal }] = useModal() as any
|
||||
|
||||
const submitting = ref(false)
|
||||
const form = reactive({
|
||||
customerId: undefined as number | undefined,
|
||||
exportQualificationId: undefined as number | undefined,
|
||||
platformCode: '',
|
||||
})
|
||||
const qualificationOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
const qualificationLoading = ref(false)
|
||||
const platformOptions = ref<Array<{ label: string; value: string }>>([])
|
||||
|
||||
const keyword = ref('')
|
||||
const orderLoading = ref(false)
|
||||
const orderRows = ref<OrderPickRow[]>([])
|
||||
const orderPage = ref(1)
|
||||
const orderTotal = ref(0)
|
||||
const selectedKeys = ref<number[]>([])
|
||||
/** 跨页保留已选运单信息,用于结果展示 */
|
||||
const selectedMap = ref<Map<number, OrderPickRow>>(new Map())
|
||||
const failRows = ref<FailRow[]>([])
|
||||
|
||||
const needCustomer = computed(() => props.isAdmin && !form.customerId)
|
||||
|
||||
const orderColumns = computed<TableColumnData[]>(() => [
|
||||
{ title: t('order.customs9610.create.col.serviceNo'), dataIndex: 'serviceNo', width: 180, ellipsis: true, tooltip: true },
|
||||
{ title: t('order.customs9610.create.col.clientNo'), dataIndex: 'clientNo', width: 150, ellipsis: true, tooltip: true },
|
||||
...(props.isAdmin
|
||||
? [{ title: t('order.customs9610.create.col.customer'), dataIndex: 'customerName', width: 140, ellipsis: true, tooltip: true }]
|
||||
: []),
|
||||
{ title: t('order.customs9610.create.col.country'), dataIndex: 'country', width: 90 },
|
||||
{ title: t('order.customs9610.create.col.status'), dataIndex: 'statusName', width: 100 },
|
||||
{ title: t('order.customs9610.create.col.createTime'), dataIndex: 'createTime', width: 160 },
|
||||
{ title: t('order.customs9610.create.col.remark'), slotName: 'remark', width: 120 },
|
||||
])
|
||||
|
||||
const rowSelection = computed<TableRowSelection>(() => ({
|
||||
type: 'checkbox',
|
||||
showCheckedAll: true,
|
||||
onlyCurrent: false,
|
||||
}))
|
||||
|
||||
const orderPagination = computed(() => ({
|
||||
current: orderPage.value,
|
||||
pageSize: ORDER_PAGE_SIZE,
|
||||
total: orderTotal.value,
|
||||
size: 'small' as const,
|
||||
showTotal: true,
|
||||
}))
|
||||
|
||||
const canSubmit = computed(
|
||||
() =>
|
||||
!submitting.value
|
||||
&& !needCustomer.value
|
||||
&& selectedKeys.value.length > 0
|
||||
&& !!form.platformCode,
|
||||
)
|
||||
|
||||
function statusName(code: string) {
|
||||
const meta = getOrderStatusMeta(code)
|
||||
return meta ? t(meta.labelKey) : code
|
||||
}
|
||||
|
||||
function mapOrderRow(raw: any): OrderPickRow | null {
|
||||
const row = normalizeListRow(raw)
|
||||
const id = Number(pickField(row, 'id', 'Id') || 0)
|
||||
if (!id) return null
|
||||
const status = resolveOrderStatusCode(row) || String(pickField(row, 'status', 'Status') || '')
|
||||
if (status === 'deleted') return null
|
||||
const mode = String(pickField(row, 'exportCustomsMode', 'ExportCustomsMode') || '').toLowerCase()
|
||||
const is9610 = mode === SUMMARY_MODE
|
||||
return {
|
||||
id,
|
||||
serviceNo: String(pickField(row, 'serviceNo', 'ServiceNo') || '-'),
|
||||
clientNo: String(pickField(row, 'clientNo', 'ClientNo') || '-'),
|
||||
customerName: String(pickField(row, 'customerName', 'CustomerName', 'customerCode', 'CustomerCode') || '-'),
|
||||
country: String(pickField(row, 'consigneeCountry', 'ConsigneeCountry') || '-'),
|
||||
statusName: statusName(status),
|
||||
createTime: formatDateTime(pickField(row, 'createTime', 'CreateTime')) || '-',
|
||||
is9610,
|
||||
disabled: is9610,
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrders(page = 1) {
|
||||
if (needCustomer.value) {
|
||||
orderRows.value = []
|
||||
orderTotal.value = 0
|
||||
return
|
||||
}
|
||||
orderLoading.value = true
|
||||
orderPage.value = page
|
||||
try {
|
||||
const body: Record<string, unknown> = {
|
||||
page,
|
||||
pageSize: ORDER_PAGE_SIZE,
|
||||
status: 'all',
|
||||
keyword: keyword.value.trim(),
|
||||
}
|
||||
if (props.isAdmin && form.customerId) body.customerId = form.customerId
|
||||
const res: any = await postOrderList(body as any)
|
||||
orderRows.value = (res.items || [])
|
||||
.map((raw: any) => mapOrderRow(raw))
|
||||
.filter((x: OrderPickRow | null): x is OrderPickRow => !!x)
|
||||
orderTotal.value = Number(res.total ?? res.totalCount ?? orderRows.value.length) || 0
|
||||
} catch (e: any) {
|
||||
orderRows.value = []
|
||||
orderTotal.value = 0
|
||||
$baseMessage(e?.message || t('order.customs9610.create.message.loadOrdersFailed'), 'error')
|
||||
} finally {
|
||||
orderLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSelectionChange(keys: Array<string | number>) {
|
||||
const ids = (keys || []).map(Number).filter((id) => id > 0)
|
||||
const next = new Map<number, OrderPickRow>()
|
||||
for (const id of ids) {
|
||||
const row = orderRows.value.find((r) => r.id === id) ?? selectedMap.value.get(id)
|
||||
if (row && !row.is9610) next.set(id, row)
|
||||
}
|
||||
selectedMap.value = next
|
||||
selectedKeys.value = [...next.keys()]
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedMap.value = new Map()
|
||||
selectedKeys.value = []
|
||||
}
|
||||
|
||||
async function loadQualifications() {
|
||||
qualificationOptions.value = []
|
||||
form.exportQualificationId = undefined
|
||||
if (needCustomer.value) return
|
||||
qualificationLoading.value = true
|
||||
try {
|
||||
const body: Record<string, unknown> = { page: 1, pageSize: 200 }
|
||||
if (props.isAdmin && form.customerId) body.customerId = form.customerId
|
||||
const res: any = await getExportQualificationList(body)
|
||||
const items = Array.isArray(res?.items) ? res.items : []
|
||||
let defaultId: number | undefined
|
||||
qualificationOptions.value = items
|
||||
.map((raw: any) => {
|
||||
const id = Number(pickField(raw, 'id', 'Id') || 0)
|
||||
if (!id) return null
|
||||
const name = String(pickField(raw, 'companyName', 'CompanyName') || pickField(raw, 'socialCreditCode', 'SocialCreditCode') || `#${id}`)
|
||||
const isDefault = Boolean(pickField(raw, 'isDefault', 'IsDefault'))
|
||||
if (isDefault) defaultId = id
|
||||
return {
|
||||
label: isDefault ? `${name}${t('order.customs9610.create.defaultSuffix')}` : name,
|
||||
value: id,
|
||||
}
|
||||
})
|
||||
.filter((x: any): x is { label: string; value: number } => !!x)
|
||||
form.exportQualificationId = defaultId ?? qualificationOptions.value[0]?.value
|
||||
} catch {
|
||||
qualificationOptions.value = []
|
||||
} finally {
|
||||
qualificationLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function ensurePlatformOptions() {
|
||||
if (platformOptions.value.length) return
|
||||
try {
|
||||
const rows = await getDictListAll({ dictType: 'e-commerce_platform' })
|
||||
platformOptions.value = rows
|
||||
.map((r: any) => ({
|
||||
label: String(r.dictName ?? r.dictCode ?? '').trim(),
|
||||
value: String(r.dictCode ?? '').trim(),
|
||||
}))
|
||||
.filter((x) => x.value)
|
||||
} catch {
|
||||
platformOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function goQualificationManage() {
|
||||
router.push({ path: '/user/exportQualification' })
|
||||
}
|
||||
|
||||
async function open() {
|
||||
form.customerId = props.isAdmin ? props.defaultCustomerId : undefined
|
||||
form.platformCode = ''
|
||||
keyword.value = ''
|
||||
failRows.value = []
|
||||
clearSelection()
|
||||
openModal()
|
||||
await Promise.all([ensurePlatformOptions(), loadQualifications(), loadOrders(1)])
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (submitting.value) return
|
||||
closeModal()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!canSubmit.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const res: any = await createCustoms9610FromOrders({
|
||||
orderIds: [...selectedKeys.value],
|
||||
exportQualificationId: form.exportQualificationId,
|
||||
platformCode: form.platformCode,
|
||||
})
|
||||
const items: any[] = Array.isArray(res?.items ?? res?.Items) ? (res.items ?? res.Items) : []
|
||||
const successCount = Number(pickField(res, 'successCount', 'SuccessCount') ?? 0)
|
||||
const failed = items.filter((x) => !pickField(x, 'success', 'Success'))
|
||||
if (successCount > 0) emit('success')
|
||||
|
||||
if (!failed.length) {
|
||||
$baseMessage(t('order.customs9610.create.message.allSuccess', { count: successCount }), 'success')
|
||||
closeModal()
|
||||
return
|
||||
}
|
||||
|
||||
failRows.value = failed.map((x) => {
|
||||
const orderId = Number(pickField(x, 'orderId', 'OrderId') || 0)
|
||||
return {
|
||||
orderId,
|
||||
serviceNo: String(pickField(x, 'serviceNo', 'ServiceNo') || selectedMap.value.get(orderId)?.serviceNo || orderId),
|
||||
message: String(pickField(x, 'message', 'Message') || t('order.customs9610.create.message.failed')),
|
||||
}
|
||||
})
|
||||
// 成功的运单从已选中移除,保留失败的便于调整后重试
|
||||
const failedIds = new Set(failRows.value.map((x) => x.orderId))
|
||||
const next = new Map([...selectedMap.value].filter(([id]) => failedIds.has(id)))
|
||||
selectedMap.value = next
|
||||
selectedKeys.value = [...next.keys()]
|
||||
$baseMessage(
|
||||
t('order.customs9610.create.message.partial', { success: successCount, fail: failed.length }),
|
||||
'warning',
|
||||
)
|
||||
if (successCount > 0) void loadOrders(orderPage.value)
|
||||
} catch (e: any) {
|
||||
$baseMessage(e?.message || t('order.customs9610.create.message.failed'), 'error')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => form.customerId,
|
||||
(val, old) => {
|
||||
if (val === old) return
|
||||
clearSelection()
|
||||
failRows.value = []
|
||||
void loadQualifications()
|
||||
void loadOrders(1)
|
||||
},
|
||||
)
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalPro
|
||||
@register="registerModal"
|
||||
:title="t('order.customs9610.create.title')"
|
||||
width="1000px"
|
||||
:mask-closable="false"
|
||||
:closable="!submitting"
|
||||
@cancel="close"
|
||||
>
|
||||
<a-alert type="info" :show-icon="false" class="c9610-create__tip">
|
||||
{{ t('order.customs9610.create.tip') }}
|
||||
</a-alert>
|
||||
|
||||
<div class="c9610-create__section-title">{{ t('order.customs9610.create.sectionSupplement') }}</div>
|
||||
<a-form :model="form" layout="vertical" class="c9610-create__form">
|
||||
<a-row :gutter="20">
|
||||
<a-col v-if="isAdmin" :span="8">
|
||||
<a-form-item :label="t('order.customs9610.create.customer')" required>
|
||||
<a-select
|
||||
v-model="form.customerId"
|
||||
allow-search
|
||||
:options="customerOptions"
|
||||
:placeholder="t('order.customs9610.create.customerPh')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="isAdmin ? 8 : 12">
|
||||
<a-form-item :label="t('order.customs9610.create.exportQualification')">
|
||||
<a-select
|
||||
v-model="form.exportQualificationId"
|
||||
allow-clear
|
||||
:loading="qualificationLoading"
|
||||
:disabled="needCustomer"
|
||||
:options="qualificationOptions"
|
||||
:placeholder="t('order.customs9610.create.exportQualificationPh')"
|
||||
/>
|
||||
<template #extra>
|
||||
<a-link :hoverable="false" @click="goQualificationManage">
|
||||
{{ t('order.customs9610.create.manageQualification') }}
|
||||
</a-link>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="isAdmin ? 8 : 12">
|
||||
<a-form-item :label="t('order.customs9610.create.platform')" required>
|
||||
<a-select
|
||||
v-model="form.platformCode"
|
||||
allow-search
|
||||
:options="platformOptions"
|
||||
:placeholder="t('order.customs9610.create.platformPh')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
|
||||
<div class="c9610-create__section-title">
|
||||
{{ t('order.customs9610.create.sectionOrders') }}
|
||||
</div>
|
||||
<div class="c9610-create__toolbar">
|
||||
<a-input
|
||||
v-model="keyword"
|
||||
allow-clear
|
||||
:disabled="needCustomer"
|
||||
:placeholder="t('order.customs9610.create.keywordPh')"
|
||||
style="width: 260px"
|
||||
@press-enter="loadOrders(1)"
|
||||
/>
|
||||
<a-button class="app-btn-search" :loading="orderLoading" :disabled="needCustomer" @click="loadOrders(1)">
|
||||
{{ t('order.myOrders.search.actions.search') }}
|
||||
</a-button>
|
||||
<span class="c9610-create__selected">
|
||||
{{ t('order.customs9610.create.selected', { count: selectedKeys.length }) }}
|
||||
<a-link v-if="selectedKeys.length" :hoverable="false" @click="clearSelection">
|
||||
{{ t('order.customs9610.create.clearSelected') }}
|
||||
</a-link>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
row-key="id"
|
||||
size="small"
|
||||
:loading="orderLoading"
|
||||
:data="orderRows"
|
||||
:columns="orderColumns"
|
||||
:row-selection="rowSelection"
|
||||
:selected-keys="selectedKeys"
|
||||
:pagination="orderPagination"
|
||||
:scroll="{ y: 300 }"
|
||||
:row-class="(record: OrderPickRow) => (record.is9610 ? 'c9610-create__row--disabled' : '')"
|
||||
@selection-change="handleSelectionChange"
|
||||
@page-change="loadOrders"
|
||||
>
|
||||
<template #remark="{ record }">
|
||||
<a-tag v-if="record.is9610" size="small" color="gray">
|
||||
{{ t('order.customs9610.create.already9610') }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div class="c9610-create__empty">
|
||||
{{ needCustomer ? t('order.customs9610.create.selectCustomerFirst') : t('order.customs9610.create.emptyOrders') }}
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<div v-if="failRows.length" class="c9610-create__fail">
|
||||
<div class="c9610-create__fail-title">
|
||||
{{ t('order.customs9610.create.failTitle', { count: failRows.length }) }}
|
||||
</div>
|
||||
<ul>
|
||||
<li v-for="row in failRows" :key="row.orderId">
|
||||
<b>{{ row.serviceNo }}</b>
|
||||
<span>{{ row.message }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="c9610-create__footer">
|
||||
<a-button :disabled="submitting" @click="close">{{ t('common.cancel') }}</a-button>
|
||||
<a-button type="primary" :loading="submitting" :disabled="!canSubmit" @click="handleSubmit">
|
||||
{{ t('order.customs9610.create.submit', { count: selectedKeys.length }) }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</ModalPro>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.c9610-create__tip {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.c9610-create__section-title {
|
||||
margin-bottom: 12px;
|
||||
padding-left: 8px;
|
||||
border-left: 3px solid var(--app-primary-color);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.c9610-create__form {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.c9610-create__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.c9610-create__selected {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.c9610-create__empty {
|
||||
padding: 24px 12px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
:deep(.c9610-create__row--disabled) {
|
||||
color: var(--color-text-4);
|
||||
}
|
||||
|
||||
.c9610-create__fail {
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 4px;
|
||||
background: rgb(var(--danger-1));
|
||||
|
||||
ul {
|
||||
margin: 6px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 2px 0;
|
||||
font-size: 12px;
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
}
|
||||
|
||||
.c9610-create__fail-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--danger-6));
|
||||
}
|
||||
|
||||
.c9610-create__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
:deep(.arco-table-th) {
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
</style>
|
||||
@ -9,7 +9,6 @@ import useModal from '@/components/ModalPro/useModal'
|
||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
||||
import {
|
||||
declareCustoms9610Batch,
|
||||
fetchCustoms9610DeclarationFormBatch,
|
||||
fetchCustoms9610ManifestBatch,
|
||||
getCustoms9610Detail,
|
||||
@ -22,6 +21,7 @@ import { hasPermission } from '@/utils/rbac-permission'
|
||||
import { formatDateTime, normalizeListRow, pickField } from '@/utils/warehouseListMapper'
|
||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
||||
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||
import CreateCustoms9610Modal from './components/CreateCustoms9610Modal.vue'
|
||||
|
||||
defineOptions({ name: 'OrderCustoms9610' })
|
||||
|
||||
@ -44,7 +44,7 @@ const pageSize = ref(50)
|
||||
const totalCount = ref(0)
|
||||
const selectedIds = ref<number[]>([])
|
||||
const statusCounts = ref({ total: 0, pending: 0, declared: 0 })
|
||||
const declaring = ref(false)
|
||||
const createModalRef = ref<InstanceType<typeof CreateCustoms9610Modal> | null>(null)
|
||||
const fetchingManifest = ref(false)
|
||||
const fetchingDeclarationForm = ref(false)
|
||||
const downstreamForwardModalVisible = ref(false)
|
||||
@ -543,34 +543,12 @@ async function handleSave() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchDeclare() {
|
||||
const ids = selectedIds.value
|
||||
if (!ids.length) {
|
||||
$baseMessage(t('order.customs9610.message.selectFirst'), 'warning')
|
||||
return
|
||||
}
|
||||
Modal.confirm({
|
||||
title: t('order.customs9610.confirm.batchDeclareTitle'),
|
||||
content: t('order.customs9610.confirm.batchDeclareContent', { count: ids.length }),
|
||||
onOk: async () => {
|
||||
declaring.value = true
|
||||
try {
|
||||
const res = await declareCustoms9610Batch(ids)
|
||||
const successCount = Number(pickField(res, 'successCount', 'SuccessCount') ?? 0)
|
||||
const failCount = Number(pickField(res, 'failCount', 'FailCount') ?? 0)
|
||||
$baseMessage(
|
||||
t('order.customs9610.message.batchResult', { success: successCount, fail: failCount }),
|
||||
failCount > 0 ? 'warning' : 'success',
|
||||
)
|
||||
await loadList()
|
||||
await loadCounts()
|
||||
} catch (e: any) {
|
||||
$baseMessage(e?.message || t('order.customs9610.message.declareFailed'), 'error')
|
||||
} finally {
|
||||
declaring.value = false
|
||||
}
|
||||
},
|
||||
})
|
||||
function handleOpenCreate() {
|
||||
void createModalRef.value?.open()
|
||||
}
|
||||
|
||||
async function handleCreateSuccess() {
|
||||
await Promise.all([loadList(), loadCounts()])
|
||||
}
|
||||
|
||||
async function runBatchFetch(
|
||||
@ -745,11 +723,9 @@ onMounted(() => {
|
||||
<a-button
|
||||
v-if="hasPermission('Order.Customs9610.DeclareBatch')"
|
||||
class="app-btn-toolbar"
|
||||
:loading="declaring"
|
||||
:disabled="!selectedIds.length || declaring"
|
||||
@click="handleBatchDeclare"
|
||||
@click="handleOpenCreate"
|
||||
>
|
||||
{{ t('order.customs9610.actions.batchDeclare') }}
|
||||
{{ t('order.customs9610.actions.createFromOrders') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('Order.Customs9610.FetchManifestBatch')"
|
||||
@ -810,6 +786,14 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CreateCustoms9610Modal
|
||||
ref="createModalRef"
|
||||
:is-admin="isAdmin"
|
||||
:customer-options="customerOptions"
|
||||
:default-customer-id="adminCustomerId"
|
||||
@success="handleCreateSuccess"
|
||||
/>
|
||||
|
||||
<ModalPro
|
||||
@register="registerEditModal"
|
||||
:title="modalTitle"
|
||||
|
||||
@ -7,6 +7,7 @@ import ModalPro from '@/components/ModalPro/index.vue'
|
||||
import useModal from '@/components/ModalPro/useModal'
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict'
|
||||
import { postOrderFeeAdd } from '@/api/order'
|
||||
import { isDiscountCostItem } from '@/constants/feeCostItems'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const props = defineProps<{
|
||||
@ -63,7 +64,7 @@ async function ensureDictOptions() {
|
||||
getDictListAll({ dictType: 'fee_item' }),
|
||||
getDictListAll({ dictType: 'currency' }),
|
||||
])
|
||||
feeItemOptions.value = mapDictOptions(feeItems)
|
||||
feeItemOptions.value = mapDictOptions(feeItems).filter((item) => !isDiscountCostItem(item.value))
|
||||
currencyOptions.value = mapDictOptions(currencies)
|
||||
if (!currencyOptions.value.some((item) => item.value === formState.currency)) {
|
||||
formState.currency = currencyOptions.value[0]?.value || 'CNY'
|
||||
|
||||
@ -404,6 +404,9 @@
|
||||
<span class="fee-summary-meta">
|
||||
{{ $t('order.myOrders.drawer.fee.itemCount', { count: receivableFeeList.length }) }}
|
||||
</span>
|
||||
<span v-if="receivableDiscountTotals.size" class="fee-summary-discount">
|
||||
{{ $t('order.myOrders.drawer.fee.includesDiscount', { amount: formatFeeTotals(receivableDiscountTotals) }) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -431,7 +434,10 @@
|
||||
:class="col.key"
|
||||
>
|
||||
<template v-if="col.key === 'amount'">
|
||||
<span class="fee-amount amount-receivable">
|
||||
<span
|
||||
class="fee-amount"
|
||||
:class="isDiscountFeeRow(row) ? 'amount-discount' : 'amount-receivable'"
|
||||
>
|
||||
{{ formatFeeAmount(row.amount) }}
|
||||
</span>
|
||||
</template>
|
||||
@ -722,6 +728,7 @@ import {
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||
import { getWarehouseList } from '@/api/system/warehouse';
|
||||
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
||||
import { isDiscountCostItem } from '@/constants/feeCostItems';
|
||||
import { getBooleanTagStyle } from '@/utils/statusTag';
|
||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
@ -1711,6 +1718,15 @@ const payableFeeList = computed(() =>
|
||||
);
|
||||
|
||||
const receivableTotals = computed(() => sumFeesByCurrency(receivableFeeList.value));
|
||||
|
||||
function isDiscountFeeRow(row: any) {
|
||||
return isDiscountCostItem(row?.costItem ?? row?.CostItem);
|
||||
}
|
||||
|
||||
/** 支付写入的优惠券抵扣 / 会员折扣(负数),用于应收卡片「含优惠」提示 */
|
||||
const receivableDiscountTotals = computed(() =>
|
||||
sumFeesByCurrency(receivableFeeList.value.filter(isDiscountFeeRow)),
|
||||
);
|
||||
const payableTotals = computed(() => sumFeesByCurrency(payableFeeList.value));
|
||||
|
||||
function calcGrossProfitTotals(
|
||||
@ -3056,6 +3072,12 @@ defineExpose({
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.fee-summary-discount {
|
||||
font-size: 12px;
|
||||
color: #ff7d00;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.fee-view-switch {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -3197,6 +3219,10 @@ defineExpose({
|
||||
color: #00b42a;
|
||||
}
|
||||
|
||||
&.amount-discount {
|
||||
color: #ff7d00;
|
||||
}
|
||||
|
||||
&.amount-payable {
|
||||
color: #f53f3f;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1109,6 +1109,10 @@
|
||||
|
||||
// 操作按钮处理(依赖表格多选行;导出按当前 tab 条件,无需选行)
|
||||
function openPayConfirm(rows: any[]) {
|
||||
if (rows.some((row) => resolveOrderStatusCode(row) !== 'pendingPayment')) {
|
||||
Message.warning(t('order.myOrders.modal.payPendingOnly'));
|
||||
return;
|
||||
}
|
||||
payConfirmOrders.value = rows
|
||||
.filter((row) => getOrderRowId(row) != null)
|
||||
.map((row) => ({
|
||||
|
||||
@ -20,6 +20,9 @@ export const PLACE_ORDER_DECLARATION_COLUMN_CATALOG: OrderListColumnDef[] = [
|
||||
{ key: 'imHsCode', label: '进口HsCode' },
|
||||
{ key: 'exHsCode', label: '出口HsCode' },
|
||||
{ key: 'declareElement', label: '出口申报要素' },
|
||||
{ key: 'productId', label: '产品标识编码' },
|
||||
{ key: 'nonStdProductId', label: '非标产品标识编码' },
|
||||
{ key: 'stdProductId', label: '标准产品标识编码' },
|
||||
{ key: 'skuImage', label: '图片' },
|
||||
{ key: 'category', label: '类别' },
|
||||
{ key: 'brand', label: '品牌' },
|
||||
|
||||
@ -540,6 +540,9 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
{ key: 'items[0].imHsCode', desc: '进口HsCode', example: '' },
|
||||
{ key: 'items[0].exHsCode', desc: '出口HsCode', example: '' },
|
||||
{ key: 'items[0].declareElement', desc: '出口申报要素', example: '' },
|
||||
{ key: 'items[0].productId', desc: '产品标识编码', example: '' },
|
||||
{ key: 'items[0].nonStdProductId', desc: '非标产品标识编码', example: '' },
|
||||
{ key: 'items[0].stdProductId', desc: '标准产品标识编码', example: '' },
|
||||
{ key: 'items[0].skuImage', desc: '商品图片URL', example: '' },
|
||||
{ key: 'items[0].category', desc: '类别', example: '' },
|
||||
{ key: 'items[0].brand', desc: '品牌', example: '' },
|
||||
@ -668,6 +671,9 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
出口海关编码: 'items[0].exHsCode',
|
||||
出口申报要素: 'items[0].declareElement',
|
||||
申报要素: 'items[0].declareElement',
|
||||
产品标识编码: 'items[0].productId',
|
||||
非标产品标识编码: 'items[0].nonStdProductId',
|
||||
标准产品标识编码: 'items[0].stdProductId',
|
||||
商品图片URL: 'items[0].skuImage',
|
||||
图片: 'items[0].skuImage',
|
||||
类别: 'items[0].category',
|
||||
@ -733,6 +739,9 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
'items[0].imHsCode',
|
||||
'items[0].exHsCode',
|
||||
'items[0].declareElement',
|
||||
'items[0].productId',
|
||||
'items[0].nonStdProductId',
|
||||
'items[0].stdProductId',
|
||||
'items[0].skuImage',
|
||||
'items[0].category',
|
||||
'items[0].brand',
|
||||
@ -2148,6 +2157,9 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
imHsCode: '',
|
||||
exHsCode: '',
|
||||
declareElement: '',
|
||||
productId: '',
|
||||
nonStdProductId: '',
|
||||
stdProductId: '',
|
||||
skuImage: '',
|
||||
category: '',
|
||||
brand: '',
|
||||
@ -2426,6 +2438,9 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
{ key: 'imHsCode', label: '进口HsCode' },
|
||||
{ key: 'exHsCode', label: '出口HsCode' },
|
||||
{ key: 'declareElement', label: '出口申报要素' },
|
||||
{ key: 'productId', label: '产品标识编码' },
|
||||
{ key: 'nonStdProductId', label: '非标产品标识编码' },
|
||||
{ key: 'stdProductId', label: '标准产品标识编码' },
|
||||
{ key: 'skuImage', label: '图片' },
|
||||
{ key: 'category', label: '类别' },
|
||||
{ key: 'brand', label: '品牌' },
|
||||
@ -3194,6 +3209,9 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
imHsCode: '',
|
||||
exHsCode: '',
|
||||
declareElement: '',
|
||||
productId: '',
|
||||
nonStdProductId: '',
|
||||
stdProductId: '',
|
||||
skuImage: '',
|
||||
category: '',
|
||||
brand: '',
|
||||
@ -3353,6 +3371,9 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
imHsCode: '',
|
||||
exHsCode: '',
|
||||
declareElement: '',
|
||||
productId: '',
|
||||
nonStdProductId: '',
|
||||
stdProductId: '',
|
||||
skuImage: '',
|
||||
category: 'Accessories',
|
||||
brand: 'DemoBrand',
|
||||
@ -3426,6 +3447,9 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
||||
imHsCode: '',
|
||||
exHsCode: '',
|
||||
declareElement: '',
|
||||
productId: '',
|
||||
nonStdProductId: '',
|
||||
stdProductId: '',
|
||||
skuImage: '',
|
||||
category: '',
|
||||
brand: '',
|
||||
|
||||
@ -40,6 +40,8 @@ export type BindDraftItem = {
|
||||
|
||||
type OrderDraft = {
|
||||
storeOrderId: number
|
||||
customerId?: number
|
||||
warehouseId?: number
|
||||
platformOrderNo: string
|
||||
orderCountry: string
|
||||
recipientName: string
|
||||
@ -55,7 +57,6 @@ type OrderDraft = {
|
||||
productName: string
|
||||
quantity: number
|
||||
unitPrice: string
|
||||
isShipExcluded: boolean
|
||||
}>
|
||||
receiverMissing: boolean
|
||||
linesMissing: boolean
|
||||
@ -117,9 +118,7 @@ const title = computed(() => {
|
||||
|
||||
const activeDraft = computed(() => drafts.value.find((d) => d.storeOrderId === activeId.value))
|
||||
|
||||
const activeShipLines = computed(() =>
|
||||
(activeDraft.value?.lines || []).filter((x) => !x.isShipExcluded),
|
||||
)
|
||||
const activeShipLines = computed(() => activeDraft.value?.lines || [])
|
||||
|
||||
const orderQtyTotal = computed(() =>
|
||||
activeShipLines.value.reduce((sum, x) => sum + (Number(x.quantity) || 0), 0),
|
||||
@ -151,7 +150,7 @@ const readyCount = computed(() => {
|
||||
})
|
||||
|
||||
function draftOrderQty(d: OrderDraft) {
|
||||
return d.lines.filter((x) => !x.isShipExcluded).reduce((s, x) => s + (Number(x.quantity) || 0), 0)
|
||||
return d.lines.reduce((s, x) => s + (Number(x.quantity) || 0), 0)
|
||||
}
|
||||
|
||||
function draftBindQty(d: OrderDraft) {
|
||||
@ -204,12 +203,15 @@ function mapDetail(raw: Record<string, unknown>): OrderDraft {
|
||||
productName: String(pickField(r, 'productName', 'ProductName') ?? '-'),
|
||||
quantity: Number(pickField(r, 'quantity', 'Quantity') ?? 0),
|
||||
unitPrice: String(pickField(r, 'unitPrice', 'UnitPrice') ?? '-'),
|
||||
isShipExcluded: Boolean(pickField(r, 'isShipExcluded', 'IsShipExcluded')),
|
||||
}
|
||||
})
|
||||
const shipLines = lines.filter((x) => !x.isShipExcluded)
|
||||
const shipLines = lines
|
||||
const customerIdRaw = Number(pickField(row, 'customerId', 'CustomerId') ?? 0)
|
||||
const warehouseIdRaw = Number(pickField(row, 'warehouseId', 'WarehouseId') ?? 0)
|
||||
return {
|
||||
storeOrderId: Number(pickField(row, 'id', 'Id')),
|
||||
customerId: Number.isFinite(customerIdRaw) && customerIdRaw > 0 ? customerIdRaw : undefined,
|
||||
warehouseId: Number.isFinite(warehouseIdRaw) && warehouseIdRaw > 0 ? warehouseIdRaw : undefined,
|
||||
platformOrderNo: String(pickField(row, 'platformOrderNo', 'PlatformOrderNo') ?? ''),
|
||||
orderCountry: String(pickField(row, 'orderCountry', 'OrderCountry') ?? ''),
|
||||
recipientName,
|
||||
@ -227,6 +229,16 @@ function mapDetail(raw: Record<string, unknown>): OrderDraft {
|
||||
}
|
||||
}
|
||||
|
||||
/** 草稿模式 orderId=0,必须带上店铺订单所属客户,才能查到该客户库存(对齐运单绑定用订单客户)。 */
|
||||
function resolveCandidatesCustomerId(): number | undefined {
|
||||
const fromActive = activeDraft.value?.customerId
|
||||
if (fromActive && fromActive > 0) return fromActive
|
||||
const fromDrafts = drafts.value.find((d) => d.customerId && d.customerId > 0)?.customerId
|
||||
if (fromDrafts && fromDrafts > 0) return fromDrafts
|
||||
const fromProp = Number(props.customerId ?? 0)
|
||||
return Number.isFinite(fromProp) && fromProp > 0 ? fromProp : undefined
|
||||
}
|
||||
|
||||
async function loadDrafts() {
|
||||
loading.value = true
|
||||
drafts.value = []
|
||||
@ -239,6 +251,11 @@ async function loadDrafts() {
|
||||
}
|
||||
drafts.value = list
|
||||
activeId.value = list[0]?.storeOrderId
|
||||
// 优先用店铺订单已绑定仓库,避免默认仓与库存仓不一致导致空列表
|
||||
const orderWarehouseId = list.find((d) => d.warehouseId)?.warehouseId
|
||||
if (orderWarehouseId && props.warehouseOptions.some((w) => w.value === orderWarehouseId)) {
|
||||
warehouseId.value = orderWarehouseId
|
||||
}
|
||||
} catch (e: any) {
|
||||
$baseMessage(e?.message || t('order.platformStoreOrder.message.detailFailed'), 'error')
|
||||
close()
|
||||
@ -253,12 +270,13 @@ async function loadCandidates() {
|
||||
parcelCandidates.value = []
|
||||
return
|
||||
}
|
||||
const customerId = resolveCandidatesCustomerId()
|
||||
candidatesLoading.value = true
|
||||
try {
|
||||
const body = {
|
||||
orderId: 0,
|
||||
warehouseId: warehouseId.value,
|
||||
customerId: props.customerId,
|
||||
customerId,
|
||||
keyword: keyword.value || undefined,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
@ -270,9 +288,10 @@ async function loadCandidates() {
|
||||
const res = await postOrderStockBindParcelCandidates({ ...body, lifecycleStatus: 'all' })
|
||||
parcelCandidates.value = res.items || []
|
||||
}
|
||||
} catch {
|
||||
} catch (e: any) {
|
||||
inventoryCandidates.value = []
|
||||
parcelCandidates.value = []
|
||||
$baseMessage(e?.message || t('order.myOrders.stockBind.loadFailed'), 'error')
|
||||
} finally {
|
||||
candidatesLoading.value = false
|
||||
}
|
||||
@ -285,6 +304,7 @@ function scheduleSearch() {
|
||||
|
||||
function selectOrder(id: number) {
|
||||
activeId.value = id
|
||||
if (isFulfill.value) void loadCandidates()
|
||||
}
|
||||
|
||||
function bindKey(item: {
|
||||
@ -604,6 +624,7 @@ async function handleSubmit(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/** v-if 挂载时 visible 已是 true,必须 immediate,否则不会拉草稿/库存(对齐 PullOrdersModal / StockBindModal) */
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (open) => {
|
||||
@ -618,6 +639,7 @@ watch(
|
||||
await loadDrafts()
|
||||
if (isFulfill.value) await loadCandidates()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(warehouseId, () => {
|
||||
|
||||
@ -9,6 +9,7 @@ import { pickField } from '@/utils/warehouseListMapper'
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
customerId?: number
|
||||
platformCode?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -16,51 +17,77 @@ const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
/** TikTok 订单搜索状态,与平台 order_status 一致 */
|
||||
const TIKTOK_PULL_STATUSES = [
|
||||
'UNPAID',
|
||||
'ON_HOLD',
|
||||
'PARTIALLY_SHIPPING',
|
||||
'AWAITING_SHIPMENT',
|
||||
'AWAITING_COLLECTION',
|
||||
'IN_TRANSIT',
|
||||
'DELIVERED',
|
||||
'COMPLETED',
|
||||
'CANCELLED',
|
||||
] as const
|
||||
|
||||
type StoreOption = { label: string; value: number; platform: string }
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(false)
|
||||
const storeLoading = ref(false)
|
||||
const storeOptions = ref<Array<{ label: string; value: number }>>([])
|
||||
const storeOptions = ref<StoreOption[]>([])
|
||||
const storeIds = ref<number[]>([])
|
||||
const orderStatuses = ref<string[]>([
|
||||
'unpaid',
|
||||
'pending',
|
||||
'canceled',
|
||||
'ready_to_ship',
|
||||
'delivered',
|
||||
'returned',
|
||||
'shipped',
|
||||
'failed',
|
||||
'topack',
|
||||
'toship',
|
||||
'shipping',
|
||||
'lost',
|
||||
])
|
||||
const dateRange = ref<[string, string] | undefined>()
|
||||
|
||||
const statusOptions = computed(() =>
|
||||
[
|
||||
'unpaid',
|
||||
'pending',
|
||||
'canceled',
|
||||
'ready_to_ship',
|
||||
'delivered',
|
||||
'returned',
|
||||
'shipped',
|
||||
'failed',
|
||||
'topack',
|
||||
'toship',
|
||||
'shipping',
|
||||
'lost',
|
||||
].map((value) => ({ label: value, value })),
|
||||
)
|
||||
const orderStatuses = ref<string[]>([...TIKTOK_PULL_STATUSES])
|
||||
const dateRange = ref<[string, string]>(todayRange())
|
||||
|
||||
const modalVisible = computed({
|
||||
get: () => props.visible,
|
||||
set: (v: boolean) => emit('update:visible', v),
|
||||
})
|
||||
|
||||
const visibleStoreOptions = computed(() => {
|
||||
const platform = (props.platformCode || '').trim().toLowerCase()
|
||||
if (!platform || platform === 'all') return storeOptions.value
|
||||
return storeOptions.value.filter((item) => item.platform === platform)
|
||||
})
|
||||
|
||||
const allStatusesChecked = computed(
|
||||
() => orderStatuses.value.length === TIKTOK_PULL_STATUSES.length,
|
||||
)
|
||||
const allStoresChecked = computed(
|
||||
() =>
|
||||
visibleStoreOptions.value.length > 0
|
||||
&& storeIds.value.length === visibleStoreOptions.value.length,
|
||||
)
|
||||
const canSubmit = computed(
|
||||
() => storeIds.value.length > 0 && orderStatuses.value.length > 0 && !storeLoading.value,
|
||||
)
|
||||
|
||||
function todayRange(): [string, string] {
|
||||
return [dayjs().format('YYYY-MM-DD'), dayjs().format('YYYY-MM-DD')]
|
||||
}
|
||||
|
||||
function applyRange(days: number) {
|
||||
dateRange.value = [
|
||||
dayjs().subtract(days - 1, 'day').format('YYYY-MM-DD'),
|
||||
dayjs().format('YYYY-MM-DD'),
|
||||
]
|
||||
}
|
||||
|
||||
function toggleAllStatuses() {
|
||||
orderStatuses.value = allStatusesChecked.value ? [] : [...TIKTOK_PULL_STATUSES]
|
||||
}
|
||||
|
||||
function toggleAllStores() {
|
||||
storeIds.value = allStoresChecked.value
|
||||
? []
|
||||
: visibleStoreOptions.value.map((item) => item.value)
|
||||
}
|
||||
|
||||
async function loadStoreOptions() {
|
||||
storeIds.value = []
|
||||
orderStatuses.value = [...TIKTOK_PULL_STATUSES]
|
||||
dateRange.value = todayRange()
|
||||
storeLoading.value = true
|
||||
try {
|
||||
const { items } = await getPlatformStoreList({
|
||||
@ -72,12 +99,12 @@ async function loadStoreOptions() {
|
||||
.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 }
|
||||
const platform = String(pickField(raw, 'platformCode', 'PlatformCode') ?? '').toLowerCase()
|
||||
return { label: platform ? `${name} (${platform})` : name, value: id, platform }
|
||||
})
|
||||
.filter((x) => x.value > 0)
|
||||
if (!storeOptions.value.length) {
|
||||
$baseMessage(t('order.platformStoreOrder.message.noStore'), 'warning')
|
||||
.filter((item) => item.value > 0)
|
||||
if (visibleStoreOptions.value.length === 1) {
|
||||
storeIds.value = [visibleStoreOptions.value[0].value]
|
||||
}
|
||||
} catch (e: any) {
|
||||
storeOptions.value = []
|
||||
@ -145,38 +172,152 @@ async function handleSubmit(): Promise<boolean> {
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="t('order.platformStoreOrder.actions.pullOrders')"
|
||||
:width="720"
|
||||
:ok-loading="loading"
|
||||
:ok-text="t('order.platformStoreOrder.actions.startPull')"
|
||||
:ok-button-props="{ disabled: !canSubmit }"
|
||||
unmount-on-close
|
||||
:on-before-ok="handleSubmit"
|
||||
>
|
||||
<a-form :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 16 }">
|
||||
<a-form class="pull-form" :label-col-props="{ span: 6 }" :wrapper-col-props="{ span: 16 }">
|
||||
<a-form-item :label="t('order.platformStoreOrder.pull.store')" required>
|
||||
<a-select
|
||||
v-model="storeIds"
|
||||
multiple
|
||||
allow-clear
|
||||
:loading="storeLoading"
|
||||
:options="storeOptions"
|
||||
:placeholder="t('order.platformStoreOrder.pull.storePlaceholder')"
|
||||
:max-tag-count="3"
|
||||
/>
|
||||
<div class="pull-block">
|
||||
<div class="pull-row">
|
||||
<a-select
|
||||
v-model="storeIds"
|
||||
class="pull-grow"
|
||||
multiple
|
||||
allow-clear
|
||||
:loading="storeLoading"
|
||||
:options="visibleStoreOptions"
|
||||
:placeholder="t('order.platformStoreOrder.pull.storePlaceholder')"
|
||||
:max-tag-count="2"
|
||||
/>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
:disabled="!visibleStoreOptions.length"
|
||||
@click="toggleAllStores"
|
||||
>
|
||||
{{ allStoresChecked ? t('order.platformStoreOrder.pull.statusClear') : t('order.platformStoreOrder.pull.storeAll') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div v-if="!storeLoading && !visibleStoreOptions.length" class="hint">
|
||||
{{ t('order.platformStoreOrder.message.noStore') }}
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.platformStoreOrder.pull.status')" required>
|
||||
<a-checkbox-group v-model="orderStatuses" :options="statusOptions" />
|
||||
<div class="pull-block">
|
||||
<div class="pull-row pull-row--between">
|
||||
<span class="hint">
|
||||
{{ t('order.platformStoreOrder.pull.statusCount', { count: orderStatuses.length }) }}
|
||||
</span>
|
||||
<a-button type="text" size="small" @click="toggleAllStatuses">
|
||||
{{ allStatusesChecked ? t('order.platformStoreOrder.pull.statusClear') : t('order.platformStoreOrder.pull.statusAll') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<a-checkbox-group v-model="orderStatuses" class="status-grid">
|
||||
<a-checkbox v-for="status in TIKTOK_PULL_STATUSES" :key="status" :value="status">
|
||||
<span class="status-name">{{ t(`order.platformStoreOrder.pull.status.${status}`) }}</span>
|
||||
<span class="status-code">{{ status }}</span>
|
||||
</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('order.platformStoreOrder.pull.time')">
|
||||
<a-range-picker v-model="dateRange" style="width: 100%" />
|
||||
<div class="hint">{{ t('order.platformStoreOrder.pull.timeHint') }}</div>
|
||||
<div class="pull-block">
|
||||
<a-range-picker
|
||||
v-model="dateRange"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="pull-row">
|
||||
<a-button type="text" size="small" @click="applyRange(1)">
|
||||
{{ t('order.platformStoreOrder.pull.today') }}
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="applyRange(7)">
|
||||
{{ t('order.platformStoreOrder.pull.last7') }}
|
||||
</a-button>
|
||||
<a-button type="text" size="small" @click="applyRange(15)">
|
||||
{{ t('order.platformStoreOrder.pull.last15') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="hint">{{ t('order.platformStoreOrder.pull.timeHint') }}</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.hint {
|
||||
margin-top: 8px;
|
||||
.pull-form {
|
||||
:deep(.arco-form-item) {
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.arco-form-item-label-col) {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
:deep(.arco-form-item-content) {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pull-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pull-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.pull-row--between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pull-grow {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid !important;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px 12px;
|
||||
width: 100%;
|
||||
|
||||
:deep(.arco-checkbox) {
|
||||
margin: 0;
|
||||
align-items: center;
|
||||
line-height: 22px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.status-name {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.status-code {
|
||||
margin-left: 6px;
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -47,7 +47,6 @@ const display = computed(() => {
|
||||
const linesRaw = (pickField(row, 'lines', 'Lines') as any[]) || []
|
||||
const lines = linesRaw.map((line) => {
|
||||
const r = normalizeListRow(line as Record<string, unknown>)
|
||||
const isShipExcluded = Boolean(pickField(r, 'isShipExcluded', 'IsShipExcluded'))
|
||||
return {
|
||||
id: pickField(r, 'id', 'Id'),
|
||||
platformSku: pickField(r, 'platformSku', 'PlatformSku') || '-',
|
||||
@ -57,8 +56,6 @@ const display = computed(() => {
|
||||
unitPriceDisplay: formatMoney(pickField(r, 'unitPrice', 'UnitPrice'), pickField(r, 'currency', 'Currency') || currency),
|
||||
quantity: pickField(r, 'quantity', 'Quantity') ?? 0,
|
||||
mappedSkuCode: pickField(r, 'mappedSkuCode', 'MappedSkuCode') || '-',
|
||||
isShipExcluded,
|
||||
excludedLabel: isShipExcluded ? t('warehouse.common.yes') : t('warehouse.common.no'),
|
||||
}
|
||||
})
|
||||
|
||||
@ -75,12 +72,12 @@ const display = computed(() => {
|
||||
platformOrderTime: formatDateTime(pickField(row, 'platformOrderTime', 'PlatformOrderTime')),
|
||||
lastSyncTime: formatDateTime(pickField(row, 'lastSyncTime', 'LastSyncTime')),
|
||||
platformStatusLabel: pickField(row, 'platformStatusLabel', 'PlatformStatusLabel') || '-',
|
||||
customTags: pickField(row, 'customTags', 'CustomTags') || '-',
|
||||
shippingProvider: pickField(row, 'shippingProvider', 'ShippingProvider') || '-',
|
||||
fulfillmentType: pickField(row, 'fulfillmentType', 'FulfillmentType') || '-',
|
||||
customerName: pickField(row, 'customerName', 'CustomerName') || '-',
|
||||
customerCode: pickField(row, 'customerCode', 'CustomerCode') || '-',
|
||||
shippingLabelNo: pickField(row, 'shippingLabelNo', 'ShippingLabelNo') || '-',
|
||||
shippingLabelUrl: labelUrl,
|
||||
isMultiLabel: Boolean(pickField(row, 'isMultiLabel', 'IsMultiLabel')),
|
||||
warehouseName: resolveWarehouseLabel(warehouseId, props.warehouseNameMap || new Map()),
|
||||
linkedServiceNo: pickField(row, 'linkedServiceNo', 'LinkedServiceNo') || '-',
|
||||
linkedOrderId: pickField(row, 'linkedOrderId', 'LinkedOrderId'),
|
||||
@ -110,7 +107,7 @@ const basicFields = computed(() => {
|
||||
{ label: t('order.platformStoreOrder.table.itemCount'), value: String(d.itemCount ?? '-') },
|
||||
{ label: t('order.platformStoreOrder.table.orderTime'), value: d.platformOrderTime },
|
||||
{ label: t('order.platformStoreOrder.drawer.platformStatus'), value: d.platformStatusLabel },
|
||||
{ label: t('order.platformStoreOrder.drawer.customTags'), value: d.customTags },
|
||||
{ label: t('order.platformStoreOrder.drawer.fulfillmentType'), value: d.fulfillmentType },
|
||||
{ label: t('order.myOrders.table.customerName'), value: d.customerName },
|
||||
{ label: t('order.myOrders.table.customerCode'), value: d.customerCode },
|
||||
]
|
||||
@ -140,7 +137,7 @@ const shippingFields = computed(() => {
|
||||
return [
|
||||
{ label: t('order.platformStoreOrder.table.warehouse'), value: d.warehouseName },
|
||||
{ label: t('order.platformStoreOrder.table.shippingLabel'), value: d.shippingLabelNo },
|
||||
{ label: t('order.platformStoreOrder.drawer.multiLabel'), value: d.isMultiLabel ? t('warehouse.common.yes') : t('warehouse.common.no') },
|
||||
{ label: t('order.platformStoreOrder.drawer.shippingProvider'), value: d.shippingProvider },
|
||||
{ label: t('order.platformStoreOrder.table.linkedServiceNo'), value: d.linkedServiceNo },
|
||||
{ label: t('order.platformStoreOrder.table.lastSync'), value: d.lastSyncTime },
|
||||
]
|
||||
@ -154,7 +151,6 @@ const linesHotColumns = computed(() => [
|
||||
{ key: 'unitPriceDisplay', label: t('order.platformStoreOrder.line.price'), width: 100 },
|
||||
{ key: 'quantity', label: t('order.platformStoreOrder.line.qty'), width: 70 },
|
||||
{ key: 'mappedSkuCode', label: t('order.platformStoreOrder.line.mappedSku'), width: 110 },
|
||||
{ key: 'excludedLabel', label: t('order.platformStoreOrder.line.excluded'), width: 80 },
|
||||
])
|
||||
|
||||
function linesCellRenderer(_row: number, col: number, _value: any, rowData: any): string | null {
|
||||
@ -200,11 +196,6 @@ function handleCancel() {
|
||||
detail.value = null
|
||||
}
|
||||
|
||||
function openLabelUrl() {
|
||||
const url = display.value?.shippingLabelUrl
|
||||
if (url) window.open(url, '_blank')
|
||||
}
|
||||
|
||||
defineExpose({ open, close: handleCancel })
|
||||
</script>
|
||||
|
||||
@ -213,13 +204,6 @@ defineExpose({ open, close: handleCancel })
|
||||
<template #footer>
|
||||
<div class="drawer-footer-banner">
|
||||
<a-button @click="handleCancel">{{ t('warehouse.common.cancel') }}</a-button>
|
||||
<a-button
|
||||
v-if="display?.shippingLabelUrl"
|
||||
type="primary"
|
||||
@click="openLabelUrl"
|
||||
>
|
||||
{{ t('order.platformStoreOrder.drawer.openLabel') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
||||
import { getSalesProductList } from '@/api/channel/salesProduct'
|
||||
import {
|
||||
batchMatchPlatformStoreOrderInventory,
|
||||
deletePlatformStoreOrder,
|
||||
fetchPlatformStoreOrderLabel,
|
||||
getPlatformStoreOrderList,
|
||||
@ -23,6 +22,7 @@ import {
|
||||
import { getWarehouseList } from '@/api/system/warehouse'
|
||||
import { $baseMessage } from '@/utils'
|
||||
import { hasPermission } from '@/utils/rbac-permission'
|
||||
import { runWithConcurrency } from '@/utils/runWithConcurrency'
|
||||
import {
|
||||
buildWarehouseNameMap,
|
||||
formatDateTime,
|
||||
@ -38,6 +38,7 @@ import { WMS_PRODUCT_GROUP, type CreateLogisticsMode } from './constants'
|
||||
const PullOrdersModal = defineAsyncComponent(() => import('./PullOrdersModal.vue'))
|
||||
const StoreOrderDetailDrawer = defineAsyncComponent(() => import('./StoreOrderDetailDrawer.vue'))
|
||||
const CreateLogisticsModal = defineAsyncComponent(() => import('./CreateLogisticsModal.vue'))
|
||||
const OrderPrintModal = defineAsyncComponent(() => import('@/components/PrintTemplate/OrderPrintModal.vue'))
|
||||
|
||||
defineOptions({ name: 'PlatformStoreOrder' })
|
||||
|
||||
@ -95,7 +96,12 @@ const currentPage = ref(1)
|
||||
const pageSize = ref(50)
|
||||
const totalCount = ref(0)
|
||||
const pullVisible = ref(false)
|
||||
const actionLoading = ref(false)
|
||||
type StoreOrderBatchAction = 'sync' | 'rts' | 'label' | 'suspend' | 'resume'
|
||||
/** 正在执行的批量动作:只有对应按钮转圈,其余批量按钮仅禁用 */
|
||||
const runningAction = ref<StoreOrderBatchAction | null>(null)
|
||||
const actionLoading = computed(() => runningAction.value !== null)
|
||||
const labelProgress = ref({ done: 0, total: 0 })
|
||||
const LABEL_FETCH_CONCURRENCY = 3
|
||||
const matchWarehouseId = ref<number>()
|
||||
const matchProductCode = ref<string>()
|
||||
const detailDrawerMounted = ref(false)
|
||||
@ -115,6 +121,7 @@ async function ensureStoreDetailDrawer() {
|
||||
}
|
||||
const createModalMode = ref<CreateLogisticsMode>('logistics')
|
||||
const createModalIds = ref<number[]>([])
|
||||
const createModalCustomerId = ref<number>()
|
||||
|
||||
const platformTabs = computed(() => [
|
||||
{ value: PLATFORM_TAB_ALL, label: t('common.all') },
|
||||
@ -161,10 +168,13 @@ const hotColumns = computed(() => {
|
||||
Boolean(row._suspended),
|
||||
),
|
||||
},
|
||||
{ key: 'platformStatusLabel', label: t('order.platformStoreOrder.table.platformStatus'), width: 140 },
|
||||
{ key: 'fulfillmentType', label: t('order.platformStoreOrder.table.fulfillmentType'), width: 160 },
|
||||
{ key: 'shippingProvider', label: t('order.platformStoreOrder.table.shippingProvider'), width: 120 },
|
||||
{ key: 'shippingLabelNo', label: t('order.platformStoreOrder.table.shippingLabel'), width: 140 },
|
||||
{ key: 'itemCount', label: t('order.platformStoreOrder.table.itemCount'), width: 70 },
|
||||
{ key: 'totalAmountDisplay', label: t('order.platformStoreOrder.table.amount'), width: 110 },
|
||||
{ key: 'linkedServiceNo', label: t('order.platformStoreOrder.table.linkedServiceNo'), width: 150 },
|
||||
{ key: 'shippingLabelNo', label: t('order.platformStoreOrder.table.shippingLabel'), width: 140 },
|
||||
{ key: 'warehouseName', label: t('order.platformStoreOrder.table.warehouse'), width: 120 },
|
||||
{ key: 'platformOrderTime', label: t('order.platformStoreOrder.table.orderTime'), width: 160 },
|
||||
{ key: 'lastSyncTime', label: t('order.platformStoreOrder.table.lastSync'), width: 160 },
|
||||
@ -203,10 +213,14 @@ function mapRow(raw: Record<string, unknown>) {
|
||||
orderCountry: pickField(row, 'orderCountry', 'OrderCountry') ?? '-',
|
||||
recipientName: pickField(row, 'recipientName', 'RecipientName') || '-',
|
||||
fulfillStatus: statusLabel,
|
||||
platformStatusLabel: pickField(row, 'platformStatusLabel', 'PlatformStatusLabel') || '-',
|
||||
fulfillmentType: pickField(row, 'fulfillmentType', 'FulfillmentType') || '-',
|
||||
shippingProvider: pickField(row, 'shippingProvider', 'ShippingProvider') || '-',
|
||||
itemCount: pickField<number>(row, 'itemCount', 'ItemCount') ?? '-',
|
||||
totalAmountDisplay: formatMoney(amount, currency),
|
||||
linkedServiceNo: pickField(row, 'linkedServiceNo', 'LinkedServiceNo') || '-',
|
||||
shippingLabelNo: pickField(row, 'shippingLabelNo', 'ShippingLabelNo') ?? '-',
|
||||
shippingLabelUrl: String(pickField(row, 'shippingLabelUrl', 'ShippingLabelUrl') ?? ''),
|
||||
warehouseName: resolveWarehouseLabel(whId, warehouseNameMap.value),
|
||||
platformOrderTime: formatDateTime(pickField(row, 'platformOrderTime', 'PlatformOrderTime')),
|
||||
lastSyncTime: formatDateTime(pickField(row, 'lastSyncTime', 'LastSyncTime')),
|
||||
@ -399,51 +413,6 @@ async function handleMatch(row: any) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchMatch() {
|
||||
if (!hasPermission('Order.PlatformStoreOrder.MatchInventory')) return
|
||||
if (!matchWarehouseId.value) {
|
||||
$baseMessage(t('order.platformStoreOrder.message.warehouseRequired'), 'warning')
|
||||
return
|
||||
}
|
||||
const customerId = mutationCustomerId()
|
||||
if (isAdmin.value && customerId == null) return
|
||||
|
||||
const pendingIds = tableRows.value
|
||||
.filter((row) => row._status === 'pending' && !pickField(row._raw, 'linkedOrderId', 'LinkedOrderId'))
|
||||
.map((row) => Number(row.id))
|
||||
|
||||
if (!pendingIds.length) {
|
||||
$baseMessage(t('order.platformStoreOrder.message.batchEmpty'), 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: t('order.platformStoreOrder.actions.batchMatch'),
|
||||
content: t('order.platformStoreOrder.message.batchConfirm', { count: pendingIds.length }),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const result = await batchMatchPlatformStoreOrderInventory({
|
||||
ids: pendingIds,
|
||||
warehouseId: matchWarehouseId.value,
|
||||
productCode: matchProductCode.value || undefined,
|
||||
customerId,
|
||||
})
|
||||
$baseMessage(
|
||||
t('order.platformStoreOrder.message.batchResult', {
|
||||
success: result?.successCount ?? 0,
|
||||
fail: result?.failCount ?? 0,
|
||||
}),
|
||||
(result?.failCount ?? 0) > 0 ? 'warning' : 'success',
|
||||
)
|
||||
await loadSummary()
|
||||
await loadList()
|
||||
} catch (e: any) {
|
||||
$baseMessage(e?.message || t('order.platformStoreOrder.message.matchFailed'), 'error')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function resolveCreatableSelectedIds(): number[] | null {
|
||||
const selected = selectedRows.value
|
||||
if (!selected.length) {
|
||||
@ -498,6 +467,8 @@ function openCreateModal(mode: CreateLogisticsMode) {
|
||||
}
|
||||
createModalMode.value = mode
|
||||
createModalIds.value = ids
|
||||
// 管理端显式传入客户,避免草稿查库存(orderId=0)丢客户上下文
|
||||
createModalCustomerId.value = customerId ?? withCustomerId({}).customerId
|
||||
createModalVisible.value = true
|
||||
}
|
||||
|
||||
@ -519,7 +490,7 @@ function handleSyncStatus() {
|
||||
title: t('order.platformStoreOrder.actions.syncStatus'),
|
||||
content: t('order.platformStoreOrder.message.syncConfirm', { count: ids.length }),
|
||||
onOk: async () => {
|
||||
actionLoading.value = true
|
||||
runningAction.value = 'sync'
|
||||
try {
|
||||
const result = await syncPlatformStoreOrderStatus(ids, customerId)
|
||||
$baseMessage(
|
||||
@ -535,7 +506,7 @@ function handleSyncStatus() {
|
||||
} catch (e: any) {
|
||||
$baseMessage(e?.message || t('order.platformStoreOrder.message.actionFailed'), 'error')
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
runningAction.value = null
|
||||
}
|
||||
},
|
||||
})
|
||||
@ -553,7 +524,7 @@ function handleReadyToShip(ids?: number[]) {
|
||||
title: t('order.platformStoreOrder.actions.readyToShip'),
|
||||
content: t('order.platformStoreOrder.message.rtsConfirm', { count: targetIds.length }),
|
||||
onOk: async () => {
|
||||
actionLoading.value = true
|
||||
runningAction.value = 'rts'
|
||||
try {
|
||||
const result = await readyToShipPlatformStoreOrder(targetIds, customerId)
|
||||
$baseMessage(
|
||||
@ -569,54 +540,145 @@ function handleReadyToShip(ids?: number[]) {
|
||||
} catch (e: any) {
|
||||
$baseMessage(e?.message || t('order.platformStoreOrder.message.actionFailed'), 'error')
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
runningAction.value = null
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleFetchLabel(row: any) {
|
||||
function handleFetchLabel() {
|
||||
if (!hasPermission('Order.PlatformStoreOrder.Label')) return
|
||||
const id = Number(row.id ?? pickField(row._raw || row, 'id', 'Id'))
|
||||
if (!id) return
|
||||
const ids = getSelectedIds()
|
||||
if (!ids.length) {
|
||||
$baseMessage(t('order.platformStoreOrder.message.selectOrders'), 'warning')
|
||||
return
|
||||
}
|
||||
const customerId = withCustomerId({}).customerId
|
||||
Modal.confirm({
|
||||
title: t('order.platformStoreOrder.actions.fetchLabel'),
|
||||
content: t('order.platformStoreOrder.message.labelConfirm'),
|
||||
onOk: async () => {
|
||||
actionLoading.value = true
|
||||
content: t('order.platformStoreOrder.message.labelConfirmBatch', { count: ids.length }),
|
||||
onOk: () => {
|
||||
void runFetchLabels(ids, customerId)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 逐单拉面单(限并发),确认框立即关闭,进度显示在按钮上 */
|
||||
async function runFetchLabels(ids: number[], customerId?: number) {
|
||||
runningAction.value = 'label'
|
||||
labelProgress.value = { done: 0, total: ids.length }
|
||||
let success = 0
|
||||
let fail = 0
|
||||
try {
|
||||
await runWithConcurrency(ids, LABEL_FETCH_CONCURRENCY, async (id) => {
|
||||
try {
|
||||
const result = await fetchPlatformStoreOrderLabel(id, customerId)
|
||||
if (result?.success === false) {
|
||||
$baseMessage(result?.message || t('order.platformStoreOrder.message.labelFailed'), 'error')
|
||||
return
|
||||
if (result?.success === false) fail += 1
|
||||
else success += 1
|
||||
} catch {
|
||||
fail += 1
|
||||
} finally {
|
||||
labelProgress.value = { ...labelProgress.value, done: labelProgress.value.done + 1 }
|
||||
}
|
||||
})
|
||||
$baseMessage(
|
||||
t('order.platformStoreOrder.message.labelResult', { success, fail }),
|
||||
fail > 0 ? 'warning' : 'success',
|
||||
)
|
||||
await loadList()
|
||||
} finally {
|
||||
runningAction.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleSuspendBatch(suspend: boolean) {
|
||||
if (!hasPermission('Order.PlatformStoreOrder.ToggleSuspend')) return
|
||||
const customerId = mutationCustomerId()
|
||||
if (isAdmin.value && customerId == null) return
|
||||
const ids = getSelectedIds()
|
||||
if (!ids.length) {
|
||||
$baseMessage(t('order.platformStoreOrder.message.selectOrders'), 'warning')
|
||||
return
|
||||
}
|
||||
Modal.confirm({
|
||||
title: suspend
|
||||
? t('order.platformStoreOrder.actions.suspend')
|
||||
: t('order.platformStoreOrder.actions.resume'),
|
||||
content: suspend
|
||||
? t('order.platformStoreOrder.message.suspendConfirm', { count: ids.length })
|
||||
: t('order.platformStoreOrder.message.resumeConfirm', { count: ids.length }),
|
||||
onOk: async () => {
|
||||
runningAction.value = suspend ? 'suspend' : 'resume'
|
||||
try {
|
||||
for (const id of ids) {
|
||||
await togglePlatformStoreOrderSuspend({
|
||||
id,
|
||||
isSuspended: suspend,
|
||||
customerId,
|
||||
})
|
||||
}
|
||||
$baseMessage(result?.message || t('order.platformStoreOrder.message.labelSuccess'), 'success')
|
||||
const url = result?.shippingLabelUrl || result?.ShippingLabelUrl
|
||||
if (url) window.open(String(url), '_blank')
|
||||
$baseMessage(t('order.platformStoreOrder.message.suspendSuccess'), 'success')
|
||||
await loadSummary()
|
||||
await loadList()
|
||||
} catch (e: any) {
|
||||
$baseMessage(e?.message || t('order.platformStoreOrder.message.labelFailed'), 'error')
|
||||
$baseMessage(e?.message || t('order.platformStoreOrder.message.actionFailed'), 'error')
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
runningAction.value = null
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSuspend(row: any) {
|
||||
if (!hasPermission('Order.PlatformStoreOrder.ToggleSuspend')) return
|
||||
const customerId = mutationCustomerId()
|
||||
if (isAdmin.value && customerId == null) return
|
||||
function isUsableLabelUrl(raw: unknown) {
|
||||
const value = String(raw ?? '').trim()
|
||||
if (!value) return false
|
||||
if (value.includes('获取中')) return false
|
||||
return /^https?:\/\//i.test(value)
|
||||
}
|
||||
|
||||
const singlePrintVisible = ref(false)
|
||||
const singlePrintPreparing = ref(false)
|
||||
const singlePrintItems = ref<Array<{ id: number | string; label: string; url: string }>>([])
|
||||
|
||||
async function handleRowPrint(row: any) {
|
||||
const raw = row._raw || row
|
||||
await togglePlatformStoreOrderSuspend({
|
||||
id: Number(pickField(raw, 'id', 'Id')),
|
||||
isSuspended: !Boolean(pickField(raw, 'isSuspended', 'IsSuspended')),
|
||||
customerId,
|
||||
})
|
||||
$baseMessage(t('order.platformStoreOrder.message.suspendSuccess'), 'success')
|
||||
await loadSummary()
|
||||
await loadList()
|
||||
const id = Number(row.id ?? pickField(raw, 'id', 'Id'))
|
||||
if (!id) return
|
||||
const label = String(row.platformOrderNo || pickField(raw, 'platformOrderNo', 'PlatformOrderNo') || id)
|
||||
let url = String(row.shippingLabelUrl || pickField(raw, 'shippingLabelUrl', 'ShippingLabelUrl') || '')
|
||||
|
||||
if (isUsableLabelUrl(url)) {
|
||||
singlePrintVisible.value = true
|
||||
singlePrintPreparing.value = false
|
||||
singlePrintItems.value = [{ id, label, url }]
|
||||
return
|
||||
}
|
||||
|
||||
if (!hasPermission('Order.PlatformStoreOrder.Label')) {
|
||||
$baseMessage(t('order.platformStoreOrder.message.printNoLabel'), 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
singlePrintVisible.value = true
|
||||
singlePrintPreparing.value = true
|
||||
singlePrintItems.value = [{ id, label, url: '' }]
|
||||
try {
|
||||
const customerId = withCustomerId({}).customerId
|
||||
const result = await fetchPlatformStoreOrderLabel(id, customerId)
|
||||
url = String(result?.shippingLabelUrl || result?.ShippingLabelUrl || '')
|
||||
singlePrintPreparing.value = false
|
||||
if (!isUsableLabelUrl(url)) {
|
||||
$baseMessage(t('order.platformStoreOrder.message.printNoLabel'), 'warning')
|
||||
singlePrintVisible.value = false
|
||||
return
|
||||
}
|
||||
singlePrintItems.value = [{ id, label, url }]
|
||||
await loadList()
|
||||
} catch (e: any) {
|
||||
singlePrintPreparing.value = false
|
||||
singlePrintVisible.value = false
|
||||
$baseMessage(e?.message || t('order.platformStoreOrder.message.labelFailed'), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row: any) {
|
||||
@ -635,12 +697,6 @@ function handleDelete(row: any) {
|
||||
})
|
||||
}
|
||||
|
||||
function canRts(row: any) {
|
||||
if (row._suspended) return false
|
||||
if (row._platformCode === 'temu') return false
|
||||
return row._status === 'pending' || row._status === 'toShip'
|
||||
}
|
||||
|
||||
function actionRenderer(row: any) {
|
||||
const parts = [
|
||||
renderWarehouseActionBtn('detail', t('warehouse.common.detail'), 'view'),
|
||||
@ -653,19 +709,7 @@ function actionRenderer(row: any) {
|
||||
) {
|
||||
parts.push(renderWarehouseActionBtn('match', t('order.platformStoreOrder.actions.match'), 'match'))
|
||||
}
|
||||
if (hasPermission('Order.PlatformStoreOrder.Rts') && canRts(row)) {
|
||||
parts.push(renderWarehouseActionBtn('rts', t('order.platformStoreOrder.actions.readyToShip'), 'ship'))
|
||||
}
|
||||
if (hasPermission('Order.PlatformStoreOrder.Label') && !row._suspended) {
|
||||
parts.push(renderWarehouseActionBtn('label', t('order.platformStoreOrder.actions.fetchLabel'), 'label'))
|
||||
}
|
||||
if (hasPermission('Order.PlatformStoreOrder.ToggleSuspend')) {
|
||||
parts.push(
|
||||
row._suspended
|
||||
? renderWarehouseActionBtn('suspend', t('order.platformStoreOrder.actions.resume'), 'resume')
|
||||
: renderWarehouseActionBtn('suspend', t('order.platformStoreOrder.actions.suspend'), 'suspend'),
|
||||
)
|
||||
}
|
||||
parts.push(renderWarehouseActionBtn('print', t('order.myOrders.actions.print'), 'print'))
|
||||
if (hasPermission('Order.PlatformStoreOrder.Delete')) {
|
||||
parts.push(renderWarehouseActionBtn('delete', t('warehouse.common.delete')))
|
||||
}
|
||||
@ -675,15 +719,7 @@ function actionRenderer(row: any) {
|
||||
function handleActionClick(row: any, _i: number, action?: string) {
|
||||
if (action === 'detail') showDetail(row)
|
||||
if (action === 'match') handleMatch(row)
|
||||
if (action === 'rts') {
|
||||
if (row._platformCode === 'temu') {
|
||||
$baseMessage(t('order.platformStoreOrder.message.temuNoRts'), 'warning')
|
||||
return
|
||||
}
|
||||
handleReadyToShip([Number(row.id)])
|
||||
}
|
||||
if (action === 'label') handleFetchLabel(row)
|
||||
if (action === 'suspend') handleSuspend(row)
|
||||
if (action === 'print') void handleRowPrint(row)
|
||||
if (action === 'delete') handleDelete(row)
|
||||
}
|
||||
|
||||
@ -757,18 +793,10 @@ watch(
|
||||
<a-button
|
||||
v-if="hasPermission('Order.PlatformStoreOrder.Pull')"
|
||||
class="app-btn-toolbar"
|
||||
:loading="actionLoading"
|
||||
@click="openPullModal"
|
||||
>
|
||||
{{ t('order.platformStoreOrder.actions.pullOrders') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('Order.PlatformStoreOrder.MatchInventory')"
|
||||
class="app-btn-toolbar"
|
||||
@click="handleBatchMatch"
|
||||
>
|
||||
{{ t('order.platformStoreOrder.actions.batchMatch') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -777,7 +805,7 @@ watch(
|
||||
<a-button
|
||||
v-if="hasPermission('Order.PlatformStoreOrder.Sync')"
|
||||
class="app-btn-toolbar"
|
||||
:loading="actionLoading"
|
||||
:loading="runningAction === 'sync'"
|
||||
:disabled="!hasSelectedRows || actionLoading"
|
||||
@click="handleSyncStatus"
|
||||
>
|
||||
@ -786,12 +814,42 @@ watch(
|
||||
<a-button
|
||||
v-if="hasPermission('Order.PlatformStoreOrder.Rts')"
|
||||
class="app-btn-toolbar"
|
||||
:loading="actionLoading"
|
||||
:loading="runningAction === 'rts'"
|
||||
:disabled="!hasSelectedRows || actionLoading"
|
||||
@click="handleReadyToShip()"
|
||||
>
|
||||
{{ t('order.platformStoreOrder.actions.readyToShip') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('Order.PlatformStoreOrder.Label')"
|
||||
class="app-btn-toolbar"
|
||||
:loading="runningAction === 'label'"
|
||||
:disabled="!hasSelectedRows || actionLoading"
|
||||
@click="handleFetchLabel"
|
||||
>
|
||||
<template v-if="runningAction === 'label' && labelProgress.total > 1">
|
||||
{{ t('order.platformStoreOrder.actions.fetchLabelProgress', labelProgress) }}
|
||||
</template>
|
||||
<template v-else>{{ t('order.platformStoreOrder.actions.fetchLabel') }}</template>
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('Order.PlatformStoreOrder.ToggleSuspend')"
|
||||
class="app-btn-toolbar"
|
||||
:loading="runningAction === 'suspend'"
|
||||
:disabled="!hasSelectedRows || actionLoading"
|
||||
@click="handleSuspendBatch(true)"
|
||||
>
|
||||
{{ t('order.platformStoreOrder.actions.suspend') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('Order.PlatformStoreOrder.ToggleSuspend')"
|
||||
class="app-btn-toolbar"
|
||||
:loading="runningAction === 'resume'"
|
||||
:disabled="!hasSelectedRows || actionLoading"
|
||||
@click="handleSuspendBatch(false)"
|
||||
>
|
||||
{{ t('order.platformStoreOrder.actions.resume') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="hasPermission('Order.PlatformStoreOrder.CreateLogistics')"
|
||||
class="app-btn-toolbar"
|
||||
@ -860,6 +918,7 @@ watch(
|
||||
v-if="pullVisible"
|
||||
v-model:visible="pullVisible"
|
||||
:customer-id="withCustomerId({}).customerId"
|
||||
:platform-code="platformCode"
|
||||
@success="() => { loadSummary(); loadList() }"
|
||||
/>
|
||||
|
||||
@ -872,9 +931,17 @@ watch(
|
||||
:warehouse-options="warehouseOptions"
|
||||
:default-product-code="createModalDefaultProductCode"
|
||||
:default-warehouse-id="matchWarehouseId"
|
||||
:customer-id="withCustomerId({}).customerId"
|
||||
:customer-id="createModalCustomerId"
|
||||
@success="onCreateLogisticsSuccess"
|
||||
/>
|
||||
|
||||
<OrderPrintModal
|
||||
v-if="singlePrintVisible"
|
||||
v-model:visible="singlePrintVisible"
|
||||
mode="single"
|
||||
:items="singlePrintItems"
|
||||
:preparing="singlePrintPreparing"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import ModalPro from '@/components/ModalPro/index.vue'
|
||||
import useModal from '@/components/ModalPro/useModal'
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict'
|
||||
import { postOrderFeeAdd } from '@/api/order'
|
||||
import { isDiscountCostItem } from '@/constants/feeCostItems'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const props = defineProps<{
|
||||
@ -63,7 +64,7 @@ async function ensureDictOptions() {
|
||||
getDictListAll({ dictType: 'fee_item' }),
|
||||
getDictListAll({ dictType: 'currency' }),
|
||||
])
|
||||
feeItemOptions.value = mapDictOptions(feeItems)
|
||||
feeItemOptions.value = mapDictOptions(feeItems).filter((item) => !isDiscountCostItem(item.value))
|
||||
currencyOptions.value = mapDictOptions(currencies)
|
||||
if (!currencyOptions.value.some((item) => item.value === formState.currency)) {
|
||||
formState.currency = currencyOptions.value[0]?.value || 'CNY'
|
||||
|
||||
@ -458,6 +458,9 @@
|
||||
<span class="fee-summary-meta">
|
||||
{{ $t('order.myOrders.drawer.fee.itemCount', { count: receivableFeeList.length }) }}
|
||||
</span>
|
||||
<span v-if="receivableDiscountTotals.size" class="fee-summary-discount">
|
||||
{{ $t('order.myOrders.drawer.fee.includesDiscount', { amount: formatFeeTotals(receivableDiscountTotals) }) }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -595,7 +598,10 @@
|
||||
:class="col.key"
|
||||
>
|
||||
<template v-if="col.key === 'amount'">
|
||||
<span class="fee-amount" :class="section.amountClass">
|
||||
<span
|
||||
class="fee-amount"
|
||||
:class="isDiscountFeeRow(row) ? 'amount-discount' : section.amountClass"
|
||||
>
|
||||
{{ formatFeeAmount(row.amount) }}
|
||||
</span>
|
||||
</template>
|
||||
@ -999,6 +1005,7 @@ import { getSalesProductDetail, getSalesProductList } from '@/api/channel/salesP
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||
import { getWarehouseList } from '@/api/system/warehouse';
|
||||
import { getOrderStatusMeta, getOrderStatusTagStyle, resolveOrderStatusCode } from '@/constants/orderStatus';
|
||||
import { isDiscountCostItem } from '@/constants/feeCostItems';
|
||||
import { getBooleanTagStyle } from '@/utils/statusTag';
|
||||
import { resolveOmsAssetUrl } from '@/api/oms/common';
|
||||
import SlidingTabs from '@/components/SlidingTabs/index.vue';
|
||||
@ -1946,6 +1953,20 @@ const payableFeeList = computed(() =>
|
||||
);
|
||||
|
||||
const receivableTotals = computed(() => sumFeesByCurrency(receivableFeeList.value));
|
||||
|
||||
function isDiscountFeeRow(row: any) {
|
||||
return isDiscountCostItem(row?.costItem ?? row?.CostItem);
|
||||
}
|
||||
|
||||
/** 支付写入的优惠券抵扣 / 会员折扣(负数),用于应收卡片「含优惠」提示 */
|
||||
const receivableDiscountTotals = computed(() =>
|
||||
sumFeesByCurrency(receivableFeeList.value.filter(isDiscountFeeRow)),
|
||||
);
|
||||
|
||||
/** 折前应收:预报快照不含营销抵扣,差额对比需同口径 */
|
||||
const receivableGrossTotals = computed(() =>
|
||||
sumFeesByCurrency(receivableFeeList.value.filter((row) => !isDiscountFeeRow(row))),
|
||||
);
|
||||
const payableTotals = computed(() => sumFeesByCurrency(payableFeeList.value));
|
||||
|
||||
function calcGrossProfitTotals(
|
||||
@ -2000,7 +2021,7 @@ const showForecastDiff = computed(() => forecastReceivableAmount.value != null);
|
||||
|
||||
const currentReceivablePrimary = computed(() =>
|
||||
pickPrimaryTotal(
|
||||
receivableTotals.value,
|
||||
receivableGrossTotals.value,
|
||||
detail.value?.receivableAmount ?? detail.value?.ReceivableAmount
|
||||
)
|
||||
);
|
||||
@ -3621,6 +3642,12 @@ defineExpose({
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.fee-summary-discount {
|
||||
font-size: 12px;
|
||||
color: #ff7d00;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.fee-view-switch {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@ -3762,6 +3789,10 @@ defineExpose({
|
||||
color: #00b42a;
|
||||
}
|
||||
|
||||
&.amount-discount {
|
||||
color: #ff7d00;
|
||||
}
|
||||
|
||||
&.amount-payable {
|
||||
color: #f53f3f;
|
||||
}
|
||||
|
||||
@ -1,368 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import { getOrderDetail, postOrderPay } from '@/api/order'
|
||||
import { getDictListAll, type DictDto } from '@/api/system/dict'
|
||||
|
||||
interface PayOrderInput {
|
||||
id: number | string
|
||||
waybillNo?: string
|
||||
serviceNo?: string
|
||||
}
|
||||
|
||||
interface PayFeeRow {
|
||||
costItem: string
|
||||
amount: string
|
||||
currency: string
|
||||
chargingStage: string
|
||||
}
|
||||
|
||||
interface PayOrderPreview {
|
||||
id: number | string
|
||||
waybillNo: string
|
||||
fees: PayFeeRow[]
|
||||
totalText: string
|
||||
totalAmount: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
orders: PayOrderInput[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const previews = ref<PayOrderPreview[]>([])
|
||||
const feeItemDictRows = ref<DictDto[]>([])
|
||||
|
||||
const columns = computed(() => [
|
||||
{ title: t('order.myOrders.drawer.costItem'), dataIndex: 'costItem', width: 160 },
|
||||
{ title: t('order.myOrders.drawer.amount'), dataIndex: 'amount', width: 100 },
|
||||
{ title: t('order.myOrders.drawer.currency'), dataIndex: 'currency', width: 80 },
|
||||
{ title: t('order.myOrders.drawer.chargingStage'), dataIndex: 'chargingStage', ellipsis: true },
|
||||
])
|
||||
|
||||
const grandTotalText = computed(() => {
|
||||
const totals = new Map<string, number>()
|
||||
for (const preview of previews.value) {
|
||||
if (preview.error) continue
|
||||
for (const fee of preview.fees) {
|
||||
const amount = Number(fee.amount)
|
||||
if (!Number.isFinite(amount)) continue
|
||||
totals.set(fee.currency, (totals.get(fee.currency) ?? 0) + amount)
|
||||
}
|
||||
}
|
||||
return formatTotals(totals)
|
||||
})
|
||||
|
||||
const canConfirm = computed(() =>
|
||||
previews.value.length > 0
|
||||
&& !loading.value
|
||||
&& previews.value.every((item) => !item.error && item.totalAmount > 0),
|
||||
)
|
||||
|
||||
function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
|
||||
if (!raw) return undefined
|
||||
for (const k of keys) {
|
||||
const v = raw[k]
|
||||
if (v !== undefined && v !== null && v !== '') return v
|
||||
}
|
||||
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)
|
||||
|
||||
const code = row?.costItem ?? row?.CostItem
|
||||
if (code == null || code === '') return '--'
|
||||
|
||||
const c = String(code).trim()
|
||||
const hit = feeItemDictRows.value.find(
|
||||
(r) => String(r.dictCode ?? (r as any).DictCode ?? '').trim() === c,
|
||||
)
|
||||
if (hit?.dictName) return hit.dictName
|
||||
|
||||
const byName = feeItemDictRows.value.find(
|
||||
(r) => String(r.dictName ?? (r as any).DictName ?? '').trim() === c,
|
||||
)
|
||||
return byName?.dictName || c
|
||||
}
|
||||
|
||||
function getChargingStageLabel(stage: string) {
|
||||
if (!stage) return '--'
|
||||
const key = `finance.receivablePayable.chargingStage.${stage}`
|
||||
const translated = t(key)
|
||||
return translated !== key ? translated : stage
|
||||
}
|
||||
|
||||
function mapFeeRows(list: any[]): PayFeeRow[] {
|
||||
return list.map((row) => {
|
||||
const stage = String(row?.chargingStage ?? row?.ChargingStage ?? '').trim()
|
||||
const amount = Number(row?.amount)
|
||||
return {
|
||||
costItem: getCostItemLabel(row),
|
||||
amount: Number.isFinite(amount) ? amount.toFixed(2) : '--',
|
||||
currency: String(row?.currency ?? 'CNY').trim() || 'CNY',
|
||||
chargingStage: getChargingStageLabel(stage),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function ensureFeeItemDictLoaded() {
|
||||
if (feeItemDictRows.value.length) return
|
||||
try {
|
||||
feeItemDictRows.value = await getDictListAll({ dictType: 'fee_item' })
|
||||
} catch {
|
||||
feeItemDictRows.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPreviews() {
|
||||
if (!props.orders.length) {
|
||||
previews.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
previews.value = []
|
||||
try {
|
||||
await ensureFeeItemDictLoaded()
|
||||
const next: PayOrderPreview[] = []
|
||||
for (const order of props.orders) {
|
||||
const id = order.id
|
||||
const fallbackWaybill = order.waybillNo || order.serviceNo || String(id)
|
||||
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 totals = sumReceivableFees(receivableFees)
|
||||
const totalAmount = [...totals.values()].reduce((sum, val) => sum + val, 0)
|
||||
const waybillNo = pick(detail, 'waybillNo', 'WaybillNo', 'serviceNo', 'ServiceNo') || fallbackWaybill
|
||||
next.push({
|
||||
id,
|
||||
waybillNo: String(waybillNo),
|
||||
fees: mapFeeRows(receivableFees),
|
||||
totalText: formatTotals(totals),
|
||||
totalAmount,
|
||||
error: totalAmount <= 0 ? t('order.myOrders.modal.payInvalidAmount') : undefined,
|
||||
})
|
||||
} catch (e: any) {
|
||||
next.push({
|
||||
id,
|
||||
waybillNo: fallbackWaybill,
|
||||
fees: [],
|
||||
totalText: '--',
|
||||
totalAmount: 0,
|
||||
error: e?.message || t('order.myOrders.modal.payLoadFailed'),
|
||||
})
|
||||
}
|
||||
}
|
||||
previews.value = next
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
if (!canConfirm.value || submitting.value) return
|
||||
submitting.value = true
|
||||
try {
|
||||
for (const preview of previews.value) {
|
||||
await postOrderPay({ id: preview.id })
|
||||
}
|
||||
Message.success(t('order.myOrders.modal.paySuccess'))
|
||||
emit('success')
|
||||
closeModal()
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || t('order.myOrders.modal.payFailed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.visible, props.orders] as const,
|
||||
([visible]) => {
|
||||
if (visible) {
|
||||
loadPreviews()
|
||||
} else {
|
||||
previews.value = []
|
||||
submitting.value = false
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
:visible="visible"
|
||||
:title="t('order.myOrders.modal.payTitle')"
|
||||
:width="760"
|
||||
:mask-closable="false"
|
||||
unmount-on-close
|
||||
@cancel="closeModal"
|
||||
>
|
||||
<a-spin :loading="loading" class="pay-confirm-spin">
|
||||
<div v-if="previews.length" class="pay-confirm-body">
|
||||
<div
|
||||
v-for="preview in previews"
|
||||
:key="String(preview.id)"
|
||||
class="pay-order-block"
|
||||
>
|
||||
<div class="pay-order-head">
|
||||
<span class="pay-order-label">{{ t('order.myOrders.table.waybillNo') }}</span>
|
||||
<span class="pay-order-value">{{ preview.waybillNo }}</span>
|
||||
<span class="pay-order-total">
|
||||
{{ t('order.myOrders.modal.payOrderTotal') }}:{{ preview.totalText }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<a-alert
|
||||
v-if="preview.error"
|
||||
type="warning"
|
||||
show-icon
|
||||
class="pay-order-alert"
|
||||
>
|
||||
{{ preview.error }}
|
||||
</a-alert>
|
||||
|
||||
<a-table
|
||||
v-else
|
||||
:columns="columns"
|
||||
:data="preview.fees"
|
||||
:pagination="false"
|
||||
size="small"
|
||||
bordered
|
||||
class="pay-fee-table"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="previews.length > 1" class="pay-grand-total">
|
||||
<span>{{ t('order.myOrders.modal.payGrandTotal') }}</span>
|
||||
<strong>{{ grandTotalText }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-else-if="!loading" :description="t('order.myOrders.modal.payEmpty')" />
|
||||
</a-spin>
|
||||
|
||||
<template #footer>
|
||||
<a-button @click="closeModal">{{ t('warehouse.common.cancel') }}</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!canConfirm"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ t('order.myOrders.modal.payConfirm') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="less">
|
||||
.pay-confirm-spin {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.pay-confirm-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pay-order-block {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 8px;
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
|
||||
.pay-order-head {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pay-order-label {
|
||||
color: var(--color-text-3);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pay-order-value {
|
||||
color: var(--color-text-1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pay-order-total {
|
||||
margin-left: auto;
|
||||
color: var(--app-primary-color);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.pay-order-alert {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pay-fee-table {
|
||||
background: var(--app-card-bg);
|
||||
}
|
||||
|
||||
.pay-grand-total {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 4px;
|
||||
font-size: 14px;
|
||||
|
||||
strong {
|
||||
color: var(--app-primary-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -110,7 +110,7 @@
|
||||
() => import('./components/OrderDetailDrawer.vue'),
|
||||
);
|
||||
const OrderPayConfirmModal = defineAsyncComponent(
|
||||
() => import('./components/OrderPayConfirmModal.vue'),
|
||||
() => import('../myOrders/components/OrderPayConfirmModal.vue'),
|
||||
);
|
||||
const MarkProblematicModal = defineAsyncComponent(
|
||||
() => import('./components/MarkProblematicModal.vue'),
|
||||
@ -1193,6 +1193,10 @@
|
||||
|
||||
// 操作按钮处理(依赖表格多选行;导出按当前 tab 条件,无需选行)
|
||||
function openPayConfirm(rows: any[]) {
|
||||
if (rows.some((row) => resolveOrderStatusCode(row) !== 'pendingPayment')) {
|
||||
Message.warning(t('order.myOrders.modal.payPendingOnly'));
|
||||
return;
|
||||
}
|
||||
payConfirmOrders.value = rows
|
||||
.filter((row) => getOrderRowId(row) != null)
|
||||
.map((row) => ({
|
||||
|
||||
@ -344,7 +344,8 @@ const formRules = computed(() => ({
|
||||
}))
|
||||
|
||||
function customCellRenderer(_row: number, col: number, _value: any, rowData: NoticeDto): string | null {
|
||||
const dataColIndex = col - 1
|
||||
// 固定列序:操作 + 序号,数据列从 index 2 起
|
||||
const dataColIndex = col - 2
|
||||
if (dataColIndex < 0 || dataColIndex >= hotColumns.value.length) return null
|
||||
const key = hotColumns.value[dataColIndex].key
|
||||
if (key === 'title') {
|
||||
@ -362,15 +363,16 @@ function customCellRenderer(_row: number, col: number, _value: any, rowData: Not
|
||||
return rowData.isPopup === 1 ? t('system.announcement.popup.yes') : t('system.announcement.popup.no')
|
||||
}
|
||||
if (key === 'receiverTypeName') return formatReceiverTypeName(rowData)
|
||||
if (key === 'readCount' || key === 'receiverCount') {
|
||||
if (key === 'receiverCount') {
|
||||
const ids = rowData.receiverSelectionIds ?? rowData.receiverIds ?? []
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return t('system.announcement.receiverType.all')
|
||||
}
|
||||
if (key === 'readCount') {
|
||||
return String(rowData.readCount ?? 0)
|
||||
}
|
||||
if (key === 'receiverCount') {
|
||||
const ids = rowData.receiverSelectionIds ?? rowData.receiverIds ?? []
|
||||
// 广播:接收对象列已显示「全部」,人数列显示 —,避免与已读人数混淆
|
||||
if (!Array.isArray(ids) || ids.length === 0) {
|
||||
return '—'
|
||||
}
|
||||
const val = rowData[key as keyof NoticeDto]
|
||||
return val != null ? String(val) : '0'
|
||||
return String(rowData.receiverCount ?? ids.length)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@ -407,6 +409,7 @@ async function loadData() {
|
||||
pageSize: pageSize.value,
|
||||
noticeType: activeTab.value,
|
||||
keyword: keyword.value.trim() || undefined,
|
||||
manage: canManage.value,
|
||||
})
|
||||
dataSource.value = items || []
|
||||
totalCount.value = total || 0
|
||||
@ -640,6 +643,11 @@ async function handleModalOk() {
|
||||
}
|
||||
modalVisible.value = false
|
||||
reloadTable()
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err)
|
||||
if (msg && !/cancel|abort/i.test(msg)) {
|
||||
Message.error(msg)
|
||||
}
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
|
||||
232
src/views/system/country/CountryImportModal.vue
Normal file
232
src/views/system/country/CountryImportModal.vue
Normal file
@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
:title="t('system.country.import.modalTitle')"
|
||||
:width="520"
|
||||
:ok-loading="importing"
|
||||
:ok-text="t('system.country.import.submit')"
|
||||
:cancel-text="t('system.country.import.cancel')"
|
||||
unmount-on-close
|
||||
@before-ok="handleBeforeOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="country-import-modal">
|
||||
<div class="import-tip">{{ t('system.country.import.tip') }}</div>
|
||||
<div class="import-mode">
|
||||
<div class="import-mode-label">{{ t('system.country.import.modeLabel') }}</div>
|
||||
<SlidingTabs
|
||||
v-model="importMode"
|
||||
size="mini"
|
||||
:aria-label="t('system.country.import.modeLabel')"
|
||||
:items="importModeTabs"
|
||||
/>
|
||||
<div class="import-mode-tip">
|
||||
{{
|
||||
importMode === 'full'
|
||||
? t('system.country.import.mode.fullTip')
|
||||
: t('system.country.import.mode.incrementalTip')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-actions">
|
||||
<a-button class="app-btn-primary-soft" @click="handleDownloadTemplate">
|
||||
<template #icon>
|
||||
<IconDownload />
|
||||
</template>
|
||||
{{ t('system.country.import.downloadTemplate') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<a-upload
|
||||
draggable
|
||||
:auto-upload="false"
|
||||
:show-file-list="true"
|
||||
:limit="1"
|
||||
accept=".xlsx,.xls"
|
||||
:file-list="fileList"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
<div v-if="importResult" class="import-result">
|
||||
<a-alert type="info" :show-icon="false">
|
||||
{{
|
||||
t('system.country.import.resultSummary', {
|
||||
success: importResult.successCount,
|
||||
skip: importResult.skipCount,
|
||||
fail: importResult.failCount,
|
||||
deleted: importResult.deletedCount,
|
||||
})
|
||||
}}
|
||||
</a-alert>
|
||||
<ul v-if="importResult.failures.length" class="import-fail-list">
|
||||
<li v-for="item in importResult.failures" :key="`${item.rowNumber}-${item.reason}`">
|
||||
{{ t('system.country.import.failRow', { row: item.rowNumber, reason: item.reason }) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { FileItem } from '@arco-design/web-vue'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconDownload } from '@arco-design/web-vue/es/icon'
|
||||
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||
import {
|
||||
downloadCountryImportTemplate,
|
||||
importCountry,
|
||||
type CountryImportResult,
|
||||
} from '@/api/system/country'
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const visible = ref(false)
|
||||
const fileList = ref<FileItem[]>([])
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const importing = ref(false)
|
||||
const importMode = ref<'incremental' | 'full'>('incremental')
|
||||
const importResult = ref<CountryImportResult | null>(null)
|
||||
const importModeTabs = computed(() => [
|
||||
{ key: 'incremental', label: t('system.country.import.mode.incremental') },
|
||||
{ key: 'full', label: t('system.country.import.mode.full') },
|
||||
])
|
||||
|
||||
function open() {
|
||||
fileList.value = []
|
||||
selectedFile.value = null
|
||||
importResult.value = null
|
||||
importMode.value = 'incremental'
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
function handleFileChange(_: FileItem[], fileItem: FileItem) {
|
||||
fileList.value = fileItem ? [fileItem] : []
|
||||
selectedFile.value = (fileItem?.file as File) ?? null
|
||||
importResult.value = null
|
||||
}
|
||||
|
||||
function handleDownloadTemplate() {
|
||||
try {
|
||||
downloadCountryImportTemplate()
|
||||
Message.success(t('system.country.import.downloadSuccess'))
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string }
|
||||
Message.error(err?.message || t('system.country.import.downloadFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
function confirmFullImport(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
Modal.warning({
|
||||
title: t('system.country.import.warningTitle'),
|
||||
content: t('system.country.import.mode.fullConfirm'),
|
||||
hideCancel: false,
|
||||
okText: t('system.country.import.submit'),
|
||||
cancelText: t('system.country.import.cancel'),
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => resolve(false),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function handleBeforeOk() {
|
||||
if (!selectedFile.value) {
|
||||
Message.warning(t('system.country.import.needFile'))
|
||||
return false
|
||||
}
|
||||
|
||||
if (importMode.value === 'full') {
|
||||
const confirmed = await confirmFullImport()
|
||||
if (!confirmed) return false
|
||||
}
|
||||
|
||||
importing.value = true
|
||||
try {
|
||||
const result = await importCountry(selectedFile.value, importMode.value)
|
||||
importResult.value = result
|
||||
if (result.successCount > 0 || result.skipCount > 0 || result.deletedCount > 0) {
|
||||
emit('success')
|
||||
}
|
||||
if (result.failCount === 0) {
|
||||
Message.success(t('system.country.import.success'))
|
||||
visible.value = false
|
||||
return true
|
||||
}
|
||||
if (result.successCount > 0 || result.skipCount > 0) {
|
||||
Message.warning(t('system.country.import.partialSuccess'))
|
||||
} else {
|
||||
Message.error(t('system.country.import.allFailed'))
|
||||
}
|
||||
return false
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string }
|
||||
Message.error(err?.message || t('system.country.import.failed'))
|
||||
return false
|
||||
} finally {
|
||||
importing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.country-import-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.import-tip {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-2);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.import-mode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.import-mode-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.import-mode-tip {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.import-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.import-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.import-fail-list {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
font-size: 12px;
|
||||
color: #f53f3f;
|
||||
}
|
||||
</style>
|
||||
395
src/views/system/country/index.vue
Normal file
395
src/views/system/country/index.vue
Normal file
@ -0,0 +1,395 @@
|
||||
<template>
|
||||
<div class="country-page">
|
||||
<div class="country-page-card">
|
||||
<div class="table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<a-input
|
||||
v-model="keyword"
|
||||
:placeholder="t('system.country.searchPlaceholder')"
|
||||
allow-clear
|
||||
style="width: 280px"
|
||||
@press-enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
<a-button class="app-btn-search" @click="handleSearch">
|
||||
{{ t('common.search') }}
|
||||
</a-button>
|
||||
<a-button class="btn-add app-btn-primary-soft" @click="handleAdd">
|
||||
{{ t('common.add') }}
|
||||
</a-button>
|
||||
<a-button @click="openImport">
|
||||
{{ t('system.country.actions.import') }}
|
||||
</a-button>
|
||||
<a-button :loading="exporting" @click="handleExport">
|
||||
{{ t('system.country.actions.export') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<PaginationPro
|
||||
:total-count="totalCount"
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
@page-change="handlePageChange"
|
||||
@page-size-change="handlePageSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ref="tableContainerRef" class="list-wrap">
|
||||
<HandsontablePro
|
||||
:columns="hotColumns"
|
||||
:data="pageRows"
|
||||
:total="totalCount"
|
||||
:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:show-action-column="true"
|
||||
:action-renderer="actionRenderer"
|
||||
:stretch-columns="true"
|
||||
:height="handsontableHeight"
|
||||
:read-only="true"
|
||||
:enable-column-filter="false"
|
||||
:serial-no-header="t('system.country.table.index')"
|
||||
:action-column-header="t('common.actions')"
|
||||
@action-click="handleActionClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="modalVisible"
|
||||
:title="isEdit ? t('system.country.modal.editTitle') : t('system.country.modal.addTitle')"
|
||||
:confirm-loading="modalLoading"
|
||||
width="560px"
|
||||
:mask-closable="false"
|
||||
unmount-on-close
|
||||
@before-ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="formRules"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 16 }"
|
||||
>
|
||||
<a-form-item :label="t('system.country.fields.countryCode')" field="countryCode">
|
||||
<a-input
|
||||
v-model="formState.countryCode"
|
||||
:placeholder="t('system.country.placeholder.countryCode')"
|
||||
:max-length="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('system.country.fields.chineseName')" field="chineseName">
|
||||
<a-input v-model="formState.chineseName" :placeholder="t('system.country.placeholder.input')" />
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('system.country.fields.englishName')" field="englishName">
|
||||
<a-input v-model="formState.englishName" :placeholder="t('system.country.placeholder.input')" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<CountryImportModal ref="importModalRef" @success="loadList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||
import { renderWarehouseActionBtn, wrapWarehouseActions } from '@/utils/warehouseActionRenderer'
|
||||
import {
|
||||
createCountry,
|
||||
deleteCountry,
|
||||
exportCountryExcel,
|
||||
getCountryList,
|
||||
mapCountryRow,
|
||||
updateCountry,
|
||||
type CountryDto,
|
||||
} from '@/api/system/country'
|
||||
import CountryImportModal from './CountryImportModal.vue'
|
||||
|
||||
defineOptions({ name: 'SystemCountry' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const { tableContainerRef, tableHeight } = useTableContainerHeight()
|
||||
const handsontableHeight = computed(() => Math.max(tableHeight.value || 0, 400))
|
||||
|
||||
const keyword = ref('')
|
||||
const appliedKeyword = ref('')
|
||||
const allRows = ref<CountryDto[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(50)
|
||||
const exporting = ref(false)
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
const formRef = ref()
|
||||
const importModalRef = ref<{ open: () => void } | null>(null)
|
||||
|
||||
const formState = reactive({
|
||||
countryCode: '',
|
||||
chineseName: '',
|
||||
englishName: '',
|
||||
})
|
||||
|
||||
const formRules = computed(() => ({
|
||||
countryCode: [
|
||||
{ required: true, message: t('system.country.rules.countryCodeRequired') },
|
||||
{
|
||||
validator: (value: string, callback: (error?: string) => void) => {
|
||||
if (!/^[A-Za-z]{2}$/.test(String(value || '').trim())) {
|
||||
callback(t('system.country.rules.countryCodeFormat'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
},
|
||||
],
|
||||
chineseName: [{ required: true, message: t('system.country.rules.chineseNameRequired') }],
|
||||
englishName: [{ required: true, message: t('system.country.rules.englishNameRequired') }],
|
||||
}))
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const key = appliedKeyword.value.trim().toLowerCase()
|
||||
if (!key) return allRows.value
|
||||
return allRows.value.filter((row) => {
|
||||
return (
|
||||
row.countryCode.toLowerCase().includes(key) ||
|
||||
row.chineseName.toLowerCase().includes(key) ||
|
||||
row.englishName.toLowerCase().includes(key)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const totalCount = computed(() => filteredRows.value.length)
|
||||
|
||||
const pageRows = computed(() => {
|
||||
const start = (currentPage.value - 1) * pageSize.value
|
||||
return filteredRows.value.slice(start, start + pageSize.value)
|
||||
})
|
||||
|
||||
const hotColumns = computed(() => [
|
||||
{ key: 'countryCode', label: t('system.country.fields.countryCode'), width: 120 },
|
||||
{ key: 'chineseName', label: t('system.country.fields.chineseName'), width: 180 },
|
||||
{ key: 'englishName', label: t('system.country.fields.englishName'), width: 220 },
|
||||
{ key: 'createTime', label: t('system.country.fields.createTime'), width: 170 },
|
||||
{ key: 'updateTime', label: t('system.country.fields.updateTime'), width: 170 },
|
||||
])
|
||||
|
||||
function actionRenderer() {
|
||||
return wrapWarehouseActions([
|
||||
renderWarehouseActionBtn('edit', t('system.country.actions.edit')),
|
||||
renderWarehouseActionBtn('delete', t('system.country.actions.delete')),
|
||||
])
|
||||
}
|
||||
|
||||
function handleActionClick(row: CountryDto, _rowIndex: number, action?: string) {
|
||||
if (action === 'edit') {
|
||||
handleEdit(row)
|
||||
return
|
||||
}
|
||||
if (action === 'delete') {
|
||||
handleDelete(row)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
appliedKeyword.value = keyword.value
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page
|
||||
}
|
||||
|
||||
function handlePageSizeChange(size: number) {
|
||||
pageSize.value = size
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formState.countryCode = ''
|
||||
formState.chineseName = ''
|
||||
formState.englishName = ''
|
||||
editingId.value = null
|
||||
isEdit.value = false
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function handleEdit(row: CountryDto) {
|
||||
isEdit.value = true
|
||||
editingId.value = row.id
|
||||
formState.countryCode = row.countryCode
|
||||
formState.chineseName = row.chineseName
|
||||
formState.englishName = row.englishName
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
function handleModalCancel() {
|
||||
modalVisible.value = false
|
||||
}
|
||||
|
||||
async function handleModalOk() {
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
modalLoading.value = true
|
||||
const payload = {
|
||||
countryCode: formState.countryCode.trim().toUpperCase(),
|
||||
chineseName: formState.chineseName.trim(),
|
||||
englishName: formState.englishName.trim(),
|
||||
}
|
||||
try {
|
||||
if (isEdit.value && editingId.value) {
|
||||
await updateCountry({ id: editingId.value, ...payload })
|
||||
Message.success(t('system.country.message.updateSuccess'))
|
||||
} else {
|
||||
await createCountry(payload)
|
||||
Message.success(t('system.country.message.createSuccess'))
|
||||
}
|
||||
modalVisible.value = false
|
||||
await loadList()
|
||||
return true
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string }
|
||||
Message.error(err?.message || t('system.country.message.saveFailed'))
|
||||
return false
|
||||
} finally {
|
||||
modalLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDelete(row: CountryDto) {
|
||||
Modal.warning({
|
||||
title: t('system.country.actions.delete'),
|
||||
content: t('system.country.message.deleteConfirm', {
|
||||
name: row.chineseName || row.countryCode,
|
||||
}),
|
||||
hideCancel: false,
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('system.country.import.cancel'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
await deleteCountry(row.id)
|
||||
Message.success(t('system.country.message.deleteSuccess'))
|
||||
await loadList()
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string }
|
||||
Message.error(err?.message || t('system.country.message.deleteFailed'))
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function openImport() {
|
||||
importModalRef.value?.open()
|
||||
}
|
||||
|
||||
function handleExport() {
|
||||
if (!filteredRows.value.length) {
|
||||
Message.warning(t('system.country.message.exportEmpty'))
|
||||
return
|
||||
}
|
||||
exporting.value = true
|
||||
try {
|
||||
exportCountryExcel(filteredRows.value)
|
||||
Message.success(t('system.country.message.exportSuccess', { count: filteredRows.value.length }))
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string }
|
||||
Message.error(err?.message || t('system.country.message.exportFailed'))
|
||||
} finally {
|
||||
exporting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadList() {
|
||||
try {
|
||||
const { items } = await getCountryList()
|
||||
allRows.value = (items as Record<string, unknown>[]).map(mapCountryRow).filter((row) => row.id > 0)
|
||||
const maxPage = Math.max(1, Math.ceil(filteredRows.value.length / pageSize.value) || 1)
|
||||
if (currentPage.value > maxPage) currentPage.value = maxPage
|
||||
} catch (e: unknown) {
|
||||
const err = e as { message?: string }
|
||||
Message.error(err?.message || t('system.country.message.loadFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.country-page {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 16px 16px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--app-page-bg);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.country-page-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--app-card-bg);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 16px 0;
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.list-wrap {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0 16px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-wrap :deep(.handsontable-pro-container) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
@ -344,6 +344,8 @@ export const OMS_DICT_BATCH_SEED: DictSeedRow[] = [
|
||||
['proxyPacking', '代打包费'],
|
||||
['fuelSurcharge', '燃油附加费'],
|
||||
['remoteFee', '偏远费'],
|
||||
['coupon_discount', '优惠券抵扣'],
|
||||
['member_discount', '会员折扣'],
|
||||
]),
|
||||
...pack('order_handle_status', [
|
||||
['pending', '待处理'],
|
||||
|
||||
@ -354,7 +354,7 @@ export default defineConfig(({ mode }) => {
|
||||
secure: false,
|
||||
bypass(req) {
|
||||
const path = req.url?.split('?')[0] ?? '';
|
||||
const systemSpaExact = ['/system/dict', '/system/warehouseAddress', '/system/announcement'];
|
||||
const systemSpaExact = ['/system/dict', '/system/warehouseAddress', '/system/announcement', '/system/country'];
|
||||
if (systemSpaExact.some((p) => path === p || path === `${p}/`)) {
|
||||
return '/index.html';
|
||||
}
|
||||
@ -547,6 +547,7 @@ export default defineConfig(({ mode }) => {
|
||||
'/system/dict',
|
||||
'/system/warehouseAddress',
|
||||
'/system/announcement',
|
||||
'/system/country',
|
||||
// 仓储页面路径与 API 同段,必须精确匹配(/warehouse/parcel/list 走代理)
|
||||
'/warehouse/parcel',
|
||||
'/warehouse/inventory',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user