增加订单号查询
This commit is contained in:
@@ -62,6 +62,8 @@ import {
|
||||
maskPhone,
|
||||
} from "./mappers.js";
|
||||
import { createHttpError } from "../../../../utils/http.js";
|
||||
import { query as dbQuery } from "../../../../db/client.js";
|
||||
import { parseJsonObject } from "../../../../utils/task-json.js";
|
||||
|
||||
import type {
|
||||
AdminCloudtentaclesCatalogQueryInput,
|
||||
@@ -79,6 +81,54 @@ import type {
|
||||
|
||||
type JsonObject = Record<string, any>;
|
||||
|
||||
type LocalCloudtentaclesTask = {
|
||||
taskId: number;
|
||||
taskNo: string;
|
||||
platformOrderId: string;
|
||||
orderShopId: string;
|
||||
orderShopName: string;
|
||||
taskStatus: string;
|
||||
deliveryStatus: string;
|
||||
resultCode: string;
|
||||
resultMessage: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
skuCode: string;
|
||||
skuName: string;
|
||||
sourceKey: string;
|
||||
vnId: number;
|
||||
vnPhone: string;
|
||||
roleName: string;
|
||||
roleId: string;
|
||||
localDispatchStatus: string;
|
||||
localDispatchAt: string;
|
||||
consumeShopId: string;
|
||||
consumeShopName: string;
|
||||
};
|
||||
|
||||
type LocalCloudtentaclesTaskRow = {
|
||||
id: number;
|
||||
task_no: string;
|
||||
platform_order_id: string;
|
||||
shop_id: string;
|
||||
shop_name: string;
|
||||
task_status: string;
|
||||
delivery_status: string;
|
||||
result_code: string;
|
||||
result_message: string;
|
||||
context_json: unknown;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
sku_code?: string;
|
||||
sku_name?: string;
|
||||
kuaishou_source_key?: string;
|
||||
kuaishou_vn_phone?: string;
|
||||
kuaishou_role_name?: string;
|
||||
kuaishou_role_id?: string;
|
||||
kuaishou_dispatch_status?: string;
|
||||
kuaishou_consume_status?: string;
|
||||
};
|
||||
|
||||
export function listAdminCloudtentaclesSources() {
|
||||
const list = listCloudtentaclesSources();
|
||||
const sessions = getAllCloudtentaclesSessionStates();
|
||||
@@ -333,14 +383,35 @@ export async function getAdminCloudtentaclesKnapsack(
|
||||
export async function listAdminCloudtentaclesDeliveryRecords(
|
||||
payload: AdminCloudtentaclesDeliveryRecordQueryInput = {}
|
||||
) {
|
||||
const context = resolveAdminCloudtentaclesSessionPayload(payload);
|
||||
const platformOrderId = normalizeText(payload.platformOrderId);
|
||||
const preferredSourceKey = normalizeText(payload.sourceKey);
|
||||
const orderMatchedTasks = platformOrderId
|
||||
? await listLocalCloudtentaclesTasks({
|
||||
platformOrderId,
|
||||
limit: 50,
|
||||
})
|
||||
: [];
|
||||
const orderSourceKey =
|
||||
orderMatchedTasks.find((task) => !preferredSourceKey || task.sourceKey === preferredSourceKey)
|
||||
?.sourceKey || orderMatchedTasks.find((task) => task.sourceKey)?.sourceKey;
|
||||
const resolvedSourceKey =
|
||||
orderSourceKey ||
|
||||
preferredSourceKey ||
|
||||
"default";
|
||||
const context = resolveAdminCloudtentaclesSessionPayload({
|
||||
...payload,
|
||||
sourceKey: resolvedSourceKey,
|
||||
});
|
||||
const recordWindow = resolveRecordQueryWindow(payload, orderMatchedTasks);
|
||||
const recordPage = platformOrderId ? 1 : payload.page;
|
||||
const recordSize = platformOrderId ? 1000 : payload.size;
|
||||
const query = {
|
||||
...context,
|
||||
...pickDefined({
|
||||
page: payload.page,
|
||||
size: payload.size,
|
||||
startDate: payload.startDate,
|
||||
endDate: payload.endDate,
|
||||
page: recordPage,
|
||||
size: recordSize,
|
||||
startDate: recordWindow.startDate,
|
||||
endDate: recordWindow.endDate,
|
||||
recordCode: payload.recordCode,
|
||||
}),
|
||||
};
|
||||
@@ -368,16 +439,59 @@ export async function listAdminCloudtentaclesDeliveryRecords(
|
||||
}
|
||||
|
||||
const recordItems: JsonObject[] = Array.isArray(records.items) ? records.items : [];
|
||||
const localTasks =
|
||||
platformOrderId || recordItems.length === 0
|
||||
? orderMatchedTasks
|
||||
: await listLocalCloudtentaclesTasks({
|
||||
sourceKey: resolvedSourceKey,
|
||||
startDate: recordWindow.startDate,
|
||||
endDate: recordWindow.endDate,
|
||||
limit: 800,
|
||||
});
|
||||
const enrichedItems = recordItems
|
||||
.map((record) => {
|
||||
const localMatch = findBestLocalTaskForRecord(record, localTasks);
|
||||
return {
|
||||
...record,
|
||||
...(skuImageByName.get(normalizeSkuName(record.name)) || {
|
||||
skuId: 0,
|
||||
skuName: "",
|
||||
skuImage: "",
|
||||
}),
|
||||
...mapLocalTaskRecordEnhancement(localMatch?.task || null, localMatch?.confidence || ""),
|
||||
};
|
||||
})
|
||||
.filter((record) => {
|
||||
if (!platformOrderId) {
|
||||
return true;
|
||||
}
|
||||
return normalizeText(record.platformOrderId) === platformOrderId;
|
||||
});
|
||||
const fallbackItems = platformOrderId
|
||||
? orderMatchedTasks
|
||||
.filter((task) => !enrichedItems.some((record) => Number(record.taskId || 0) === task.taskId))
|
||||
.map((task) => ({
|
||||
...createLocalTaskFallbackRecord(task),
|
||||
...(skuImageByName.get(normalizeSkuName(task.skuName)) || {
|
||||
skuId: 0,
|
||||
skuName: "",
|
||||
skuImage: "",
|
||||
}),
|
||||
...mapLocalTaskRecordEnhancement(task, "local"),
|
||||
}))
|
||||
: [];
|
||||
const finalItems = platformOrderId ? [...enrichedItems, ...fallbackItems] : enrichedItems;
|
||||
|
||||
return {
|
||||
...records,
|
||||
items: recordItems.map((record) => ({
|
||||
...record,
|
||||
...(skuImageByName.get(normalizeSkuName(record.name)) || {
|
||||
skuId: 0,
|
||||
skuName: "",
|
||||
skuImage: "",
|
||||
}),
|
||||
})),
|
||||
sourceKey: resolvedSourceKey,
|
||||
queryStartDate: recordWindow.startDate,
|
||||
queryEndDate: recordWindow.endDate,
|
||||
total: platformOrderId ? finalItems.length : records.total,
|
||||
page: platformOrderId ? 1 : records.page,
|
||||
size: platformOrderId ? recordSize : records.size,
|
||||
localOrderMatched: platformOrderId ? orderMatchedTasks.length > 0 : false,
|
||||
items: finalItems,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -478,3 +592,284 @@ function pickDefined(values: JsonObject = {}) {
|
||||
function normalizeSkuName(value: unknown) {
|
||||
return String(value || "").trim().replace(/\s+/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
function normalizeText(value: unknown) {
|
||||
return String(value || "").trim();
|
||||
}
|
||||
|
||||
function normalizePhone(value: unknown) {
|
||||
return normalizeText(value).replace(/\D+/g, "");
|
||||
}
|
||||
|
||||
function normalizeNumber(value: unknown) {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): JsonObject {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as JsonObject)
|
||||
: {};
|
||||
}
|
||||
|
||||
function pickFirstNonEmptyText(values: unknown[]) {
|
||||
for (const value of values) {
|
||||
const normalized = normalizeText(value);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function resolveRecordQueryWindow(
|
||||
payload: AdminCloudtentaclesDeliveryRecordQueryInput = {},
|
||||
tasks: LocalCloudtentaclesTask[] = []
|
||||
) {
|
||||
if (tasks.length === 0) {
|
||||
return {
|
||||
startDate: payload.startDate,
|
||||
endDate: payload.endDate,
|
||||
};
|
||||
}
|
||||
|
||||
const timestamps = tasks
|
||||
.flatMap((task) => [task.createdAt, task.updatedAt, task.localDispatchAt])
|
||||
.map((value) => new Date(value).getTime())
|
||||
.filter((value) => Number.isFinite(value));
|
||||
if (timestamps.length === 0) {
|
||||
return {
|
||||
startDate: payload.startDate,
|
||||
endDate: payload.endDate,
|
||||
};
|
||||
}
|
||||
|
||||
const hourMs = 60 * 60 * 1000;
|
||||
return {
|
||||
startDate: new Date(Math.min(...timestamps) - hourMs).toISOString(),
|
||||
endDate: new Date(Math.max(...timestamps) + 6 * hourMs).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function listLocalCloudtentaclesTasks(options: {
|
||||
platformOrderId?: string | undefined;
|
||||
sourceKey?: string | undefined;
|
||||
startDate?: string | undefined;
|
||||
endDate?: string | undefined;
|
||||
limit?: number;
|
||||
} = {}): Promise<LocalCloudtentaclesTask[]> {
|
||||
const filters: string[] = ["ft.executor_key = 'kuaishou_ct_assisted'"];
|
||||
const params: unknown[] = [];
|
||||
const platformOrderId = normalizeText(options.platformOrderId);
|
||||
const sourceKey = normalizeText(options.sourceKey);
|
||||
const startDate = normalizeText(options.startDate);
|
||||
const endDate = normalizeText(options.endDate);
|
||||
|
||||
if (platformOrderId) {
|
||||
params.push(`%${platformOrderId}%`);
|
||||
filters.push(`ft.platform_order_id ILIKE $${params.length}`);
|
||||
}
|
||||
|
||||
if (sourceKey) {
|
||||
params.push(sourceKey);
|
||||
filters.push(
|
||||
`COALESCE(NULLIF(kcts.source_key, ''), ft.context_json #>> '{kuaishouCloudFulfillment,binding,resolvedSourceKey}', '') = $${params.length}`
|
||||
);
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
params.push(startDate);
|
||||
filters.push(`ft.updated_at >= ($${params.length})::timestamptz - interval '1 day'`);
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
params.push(endDate);
|
||||
filters.push(`ft.created_at <= ($${params.length})::timestamptz + interval '1 day'`);
|
||||
}
|
||||
|
||||
params.push(Math.min(Math.max(Number(options.limit || 500), 1), 1000));
|
||||
const result = await dbQuery<LocalCloudtentaclesTaskRow>(
|
||||
`
|
||||
SELECT
|
||||
ft.id,
|
||||
ft.task_no,
|
||||
ft.platform_order_id,
|
||||
ft.shop_id,
|
||||
ft.shop_name,
|
||||
ft.task_status,
|
||||
ft.delivery_status,
|
||||
ft.result_code,
|
||||
ft.result_message,
|
||||
ft.context_json,
|
||||
ft.created_at,
|
||||
ft.updated_at,
|
||||
oi.sku_code,
|
||||
oi.sku_name,
|
||||
kcts.source_key AS kuaishou_source_key,
|
||||
kcts.vn_phone AS kuaishou_vn_phone,
|
||||
kcts.role_name AS kuaishou_role_name,
|
||||
kcts.role_id AS kuaishou_role_id,
|
||||
kcts.dispatch_status AS kuaishou_dispatch_status,
|
||||
kcts.consume_status AS kuaishou_consume_status
|
||||
FROM fulfillment_tasks ft
|
||||
LEFT JOIN order_items oi ON oi.id = ft.order_item_id
|
||||
LEFT JOIN kuaishou_cloud_task_states kcts ON kcts.task_id = ft.id
|
||||
WHERE ${filters.join(" AND ")}
|
||||
ORDER BY ft.id DESC
|
||||
LIMIT $${params.length}
|
||||
`,
|
||||
params
|
||||
);
|
||||
|
||||
return result.rows.map(mapLocalCloudtentaclesTask);
|
||||
}
|
||||
|
||||
function mapLocalCloudtentaclesTask(row: LocalCloudtentaclesTaskRow): LocalCloudtentaclesTask {
|
||||
const context = parseJsonObject(row.context_json);
|
||||
const flow = asRecord(context.kuaishouCloudFulfillment);
|
||||
const binding = asRecord(flow.binding);
|
||||
const role = asRecord(flow.role);
|
||||
const dispatch = asRecord(flow.dispatch);
|
||||
const consume = asRecord(flow.consume);
|
||||
|
||||
return {
|
||||
taskId: Number(row.id || 0),
|
||||
taskNo: normalizeText(row.task_no),
|
||||
platformOrderId: normalizeText(row.platform_order_id),
|
||||
orderShopId: normalizeText(row.shop_id),
|
||||
orderShopName: normalizeText(row.shop_name),
|
||||
taskStatus: normalizeText(row.task_status),
|
||||
deliveryStatus: normalizeText(row.delivery_status),
|
||||
resultCode: normalizeText(row.result_code),
|
||||
resultMessage: normalizeText(row.result_message),
|
||||
createdAt: normalizeText(row.created_at),
|
||||
updatedAt: normalizeText(row.updated_at),
|
||||
skuCode: normalizeText(row.sku_code),
|
||||
skuName: pickFirstNonEmptyText([row.sku_name, flow.internalSkuName]),
|
||||
sourceKey: pickFirstNonEmptyText([row.kuaishou_source_key, binding.resolvedSourceKey]),
|
||||
vnId: normalizeNumber(binding.vnId),
|
||||
vnPhone: pickFirstNonEmptyText([row.kuaishou_vn_phone, binding.vnPhone]),
|
||||
roleName: pickFirstNonEmptyText([row.kuaishou_role_name, role.name, binding.roleName]),
|
||||
roleId: pickFirstNonEmptyText([row.kuaishou_role_id, role.rid, binding.roleId]),
|
||||
localDispatchStatus: pickFirstNonEmptyText([row.kuaishou_dispatch_status, dispatch.status]),
|
||||
localDispatchAt: normalizeText(dispatch.dispatchAt),
|
||||
consumeShopId: normalizeText(consume.shopId),
|
||||
consumeShopName: normalizeText(consume.shopName),
|
||||
};
|
||||
}
|
||||
|
||||
function findBestLocalTaskForRecord(
|
||||
record: JsonObject,
|
||||
tasks: LocalCloudtentaclesTask[] = []
|
||||
) {
|
||||
let best: { task: LocalCloudtentaclesTask; score: number; confidence: string } | null = null;
|
||||
|
||||
for (const task of tasks) {
|
||||
const score = scoreLocalTaskRecordMatch(record, task);
|
||||
if (score <= 0 || (best && score <= best.score)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
best = {
|
||||
task,
|
||||
score,
|
||||
confidence: score >= 80 ? "high" : score >= 45 ? "medium" : "low",
|
||||
};
|
||||
}
|
||||
|
||||
return best && best.score >= 35 ? best : null;
|
||||
}
|
||||
|
||||
function scoreLocalTaskRecordMatch(record: JsonObject, task: LocalCloudtentaclesTask) {
|
||||
let score = 0;
|
||||
const recordVnId = normalizeNumber(record.virtualNumberId);
|
||||
const recordPhone = normalizePhone(record.phone);
|
||||
const taskPhone = normalizePhone(task.vnPhone);
|
||||
const recordName = normalizeSkuName(record.name);
|
||||
const taskSkuName = normalizeSkuName(task.skuName);
|
||||
const recordRoleName = normalizeSkuName(record.fulfillUser);
|
||||
const taskRoleName = normalizeSkuName(task.roleName);
|
||||
|
||||
if (recordVnId > 0 && task.vnId > 0 && recordVnId === task.vnId) {
|
||||
score += 50;
|
||||
}
|
||||
if (recordPhone && taskPhone && recordPhone === taskPhone) {
|
||||
score += 30;
|
||||
}
|
||||
if (recordName && taskSkuName && recordName === taskSkuName) {
|
||||
score += 20;
|
||||
}
|
||||
if (recordRoleName && taskRoleName && recordRoleName.includes(taskRoleName)) {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
const recordTime = new Date(record.createdAt).getTime();
|
||||
const dispatchTime = new Date(task.localDispatchAt || task.updatedAt || task.createdAt).getTime();
|
||||
if (Number.isFinite(recordTime) && Number.isFinite(dispatchTime)) {
|
||||
const distanceHours = Math.abs(recordTime - dispatchTime) / (60 * 60 * 1000);
|
||||
if (distanceHours <= 6) {
|
||||
score += 15;
|
||||
} else if (distanceHours <= 24) {
|
||||
score += 6;
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
function mapLocalTaskRecordEnhancement(
|
||||
task: LocalCloudtentaclesTask | null,
|
||||
confidence = ""
|
||||
) {
|
||||
return {
|
||||
platformOrderId: task?.platformOrderId || "",
|
||||
taskId: task?.taskId || 0,
|
||||
taskNo: task?.taskNo || "",
|
||||
orderShopId: task?.orderShopId || "",
|
||||
orderShopName: task?.orderShopName || "",
|
||||
consumeShopId: task?.consumeShopId || "",
|
||||
consumeShopName: task?.consumeShopName || "",
|
||||
roleName: task?.roleName || "",
|
||||
roleId: task?.roleId || "",
|
||||
localTaskStatus: task?.taskStatus || "",
|
||||
localDeliveryStatus: task?.deliveryStatus || "",
|
||||
localDispatchStatus: task?.localDispatchStatus || "",
|
||||
localDispatchAt: task?.localDispatchAt || "",
|
||||
localResultCode: task?.resultCode || "",
|
||||
localResultMessage: task?.resultMessage || "",
|
||||
matchConfidence: confidence,
|
||||
};
|
||||
}
|
||||
|
||||
function createLocalTaskFallbackRecord(task: LocalCloudtentaclesTask) {
|
||||
return {
|
||||
createdAt: task.localDispatchAt || task.updatedAt || task.createdAt,
|
||||
recordId: "",
|
||||
virtualNumberId: task.vnId,
|
||||
userId: 0,
|
||||
count: 1,
|
||||
cdk: "",
|
||||
exchangeUrl: "",
|
||||
name: task.skuName,
|
||||
phone: task.vnPhone,
|
||||
status: mapLocalDispatchStatusToCloudStatus(task.localDispatchStatus),
|
||||
fulfillUser: task.roleName,
|
||||
operatorName: "",
|
||||
raw: {},
|
||||
};
|
||||
}
|
||||
|
||||
function mapLocalDispatchStatusToCloudStatus(status: string) {
|
||||
const normalized = normalizeText(status);
|
||||
if (normalized === "success") {
|
||||
return 4;
|
||||
}
|
||||
if (normalized === "failed") {
|
||||
return 3;
|
||||
}
|
||||
if (normalized === "pending") {
|
||||
return 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ export type AdminCloudtentaclesDeliveryRecordQueryInput = AdminCloudtentaclesCat
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
recordCode?: number | string
|
||||
platformOrderId?: string
|
||||
}
|
||||
|
||||
export type AdminCloudtentaclesSkuBuyInput = {
|
||||
|
||||
@@ -195,6 +195,7 @@ export function fetchAdminCloudtentaclesDeliveryRecords(payload: {
|
||||
size?: number
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
platformOrderId?: string
|
||||
}) {
|
||||
return apiPost<AdminCloudtentaclesDeliveryRecordListResult>(
|
||||
'/api/v1/admin/cloudtentacles-records',
|
||||
|
||||
@@ -137,6 +137,22 @@ export interface AdminCloudtentaclesDeliveryRecordItem {
|
||||
status: number
|
||||
fulfillUser: string
|
||||
operatorName: string
|
||||
platformOrderId: string
|
||||
taskId: number
|
||||
taskNo: string
|
||||
orderShopId: string
|
||||
orderShopName: string
|
||||
consumeShopId: string
|
||||
consumeShopName: string
|
||||
roleName: string
|
||||
roleId: string
|
||||
localTaskStatus: string
|
||||
localDeliveryStatus: string
|
||||
localDispatchStatus: string
|
||||
localDispatchAt: string
|
||||
localResultCode: string
|
||||
localResultMessage: string
|
||||
matchConfidence: string
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
@@ -145,6 +161,10 @@ export interface AdminCloudtentaclesDeliveryRecordListResult {
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
sourceKey?: string
|
||||
queryStartDate?: string
|
||||
queryEndDate?: string
|
||||
localOrderMatched?: boolean
|
||||
items: AdminCloudtentaclesDeliveryRecordItem[]
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
+100
-12
@@ -23,6 +23,7 @@ const loading = ref(false)
|
||||
const sourceLoading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const sourceKey = ref('')
|
||||
const platformOrderId = ref('')
|
||||
const dateRange = ref<[string, string]>(createDefaultDateRange())
|
||||
const page = ref(1)
|
||||
const pageSize = ref(100)
|
||||
@@ -58,7 +59,8 @@ async function loadSources() {
|
||||
}
|
||||
|
||||
async function loadRecords(nextPage = page.value) {
|
||||
if (!sourceKey.value) {
|
||||
const orderKeyword = platformOrderId.value.trim()
|
||||
if (!sourceKey.value && !orderKeyword) {
|
||||
showError('请先选择 cloudtentacles 账号')
|
||||
return
|
||||
}
|
||||
@@ -74,12 +76,16 @@ async function loadRecords(nextPage = page.value) {
|
||||
|
||||
try {
|
||||
const response = await fetchAdminCloudtentaclesDeliveryRecords({
|
||||
sourceKey: sourceKey.value,
|
||||
sourceKey: sourceKey.value || undefined,
|
||||
page: nextPage,
|
||||
size: pageSize.value,
|
||||
startDate,
|
||||
endDate,
|
||||
platformOrderId: orderKeyword || undefined,
|
||||
})
|
||||
if (response.data.sourceKey && sourceKey.value !== response.data.sourceKey) {
|
||||
sourceKey.value = response.data.sourceKey
|
||||
}
|
||||
page.value = response.data.page
|
||||
pageSize.value = response.data.size
|
||||
total.value = response.data.total
|
||||
@@ -92,6 +98,7 @@ async function loadRecords(nextPage = page.value) {
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
platformOrderId.value = ''
|
||||
dateRange.value = createDefaultDateRange()
|
||||
page.value = 1
|
||||
pageSize.value = 100
|
||||
@@ -145,6 +152,22 @@ function getStatusType(status: number) {
|
||||
return 'info'
|
||||
}
|
||||
|
||||
function getLocalDispatchStatusLabel(status: string) {
|
||||
const normalized = String(status || '').trim()
|
||||
if (normalized === 'success') return '本地已发货'
|
||||
if (normalized === 'failed') return '本地发货失败'
|
||||
if (normalized === 'pending') return '本地待发货'
|
||||
return normalized || '-'
|
||||
}
|
||||
|
||||
function getMatchConfidenceLabel(value: string) {
|
||||
if (value === 'high') return '高'
|
||||
if (value === 'medium') return '中'
|
||||
if (value === 'low') return '低'
|
||||
if (value === 'local') return '本地'
|
||||
return ''
|
||||
}
|
||||
|
||||
function createDefaultDateRange(): [string, string] {
|
||||
const end = new Date()
|
||||
const start = new Date(end)
|
||||
@@ -165,7 +188,7 @@ async function drawRecordImage(record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const scale = window.devicePixelRatio || 1
|
||||
const width = 720
|
||||
const height = 260
|
||||
const height = 330
|
||||
canvas.width = width * scale
|
||||
canvas.height = height * scale
|
||||
canvas.style.width = `${width}px`
|
||||
@@ -193,6 +216,7 @@ async function drawRecordImage(record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
drawTicketCard(ctx, record, productImage)
|
||||
drawRecordMeta(ctx, record)
|
||||
return canvas.toDataURL('image/png')
|
||||
}
|
||||
|
||||
@@ -246,7 +270,7 @@ function drawTicketCard(
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
const contentX = x + imageWidth + 16
|
||||
const orderText = `订单号:${record.virtualNumberId || record.recordId.slice(-6) || '-'}`
|
||||
const orderText = `订单号:${record.platformOrderId || record.recordId.slice(-8) || '-'}`
|
||||
ctx.fillStyle = '#1d5ea8'
|
||||
ctx.font = '16px sans-serif'
|
||||
drawEllipsisText(ctx, record.name || '未命名商品', contentX, y + 31, 170)
|
||||
@@ -275,6 +299,23 @@ function drawTicketCard(
|
||||
drawEllipsisText(ctx, `账号:${record.fulfillUser || record.phone || '-'}`, contentX, y + 90, 250)
|
||||
}
|
||||
|
||||
function drawRecordMeta(ctx: CanvasRenderingContext2D, record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
const left = 80
|
||||
const top = 250
|
||||
const lineHeight = 22
|
||||
const values = [
|
||||
`订单店铺:${record.orderShopName || record.orderShopId || '-'}`,
|
||||
`核销店铺:${record.consumeShopName || record.consumeShopId || '-'}`,
|
||||
`绑定角色:${record.roleName || record.fulfillUser || '-'}${record.roleId ? `(${record.roleId})` : ''}`,
|
||||
]
|
||||
|
||||
ctx.fillStyle = '#374151'
|
||||
ctx.font = '13px sans-serif'
|
||||
values.forEach((value, index) => {
|
||||
drawEllipsisText(ctx, value, left, top + index * lineHeight, 560)
|
||||
})
|
||||
}
|
||||
|
||||
async function loadRecordProductImage(imageUrl: string) {
|
||||
const normalizedUrl = String(imageUrl || '').trim()
|
||||
if (!normalizedUrl) {
|
||||
@@ -352,7 +393,9 @@ function drawEllipsisText(
|
||||
|
||||
function buildRecordImageFileName(record: AdminCloudtentaclesDeliveryRecordItem) {
|
||||
const name = sanitizeFileName(record.name || '发货记录')
|
||||
const id = sanitizeFileName(record.recordId.slice(0, 8) || String(record.virtualNumberId || 'record'))
|
||||
const id = sanitizeFileName(
|
||||
record.platformOrderId || record.recordId.slice(0, 8) || String(record.virtualNumberId || 'record'),
|
||||
)
|
||||
return `${name}-${id}.png`
|
||||
}
|
||||
|
||||
@@ -370,7 +413,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="cloud-records-page list-page">
|
||||
<AdminPageHeader title="查询发货记录" description="按 cloudtentacles 账号和时间范围查询平台发货记录。">
|
||||
<AdminPageHeader title="查询发货记录" description="按订单号、cloudtentacles 账号和时间范围查询平台发货记录。">
|
||||
<template #extra>
|
||||
<span class="total-badge">共 {{ total }} 条记录</span>
|
||||
</template>
|
||||
@@ -380,7 +423,7 @@ onMounted(async () => {
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">查询条件</span>
|
||||
<span class="card-desc">查询 cloudtentacles 的已发货记录,记录码默认使用平台发货记录。</span>
|
||||
<span class="card-desc">输入订单号时会先关联本地任务,再匹配 cloudtentacles 发货记录。</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -399,6 +442,13 @@ onMounted(async () => {
|
||||
:disabled="!source.enabled || !source.hasToken"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model.trim="platformOrderId"
|
||||
class="filter-control"
|
||||
clearable
|
||||
placeholder="订单号"
|
||||
@keyup.enter="loadRecords(1)"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
class="filter-control cloud-records-date-range"
|
||||
@@ -450,6 +500,24 @@ onMounted(async () => {
|
||||
<span class="cell-subline" :title="row.fulfillUser">
|
||||
账号:{{ row.fulfillUser || '-' }}
|
||||
</span>
|
||||
<span v-if="row.roleName || row.roleId" class="cell-subline" :title="`${row.roleName} ${row.roleId}`">
|
||||
角色:{{ row.roleName || '-' }}{{ row.roleId ? `(${row.roleId})` : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单 / 店铺" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-stack">
|
||||
<span class="cell-title" :title="row.platformOrderId">
|
||||
{{ row.platformOrderId || '-' }}
|
||||
</span>
|
||||
<span class="cell-subline" :title="row.orderShopName || row.orderShopId">
|
||||
订单店铺:{{ row.orderShopName || row.orderShopId || '-' }}
|
||||
</span>
|
||||
<span class="cell-subline" :title="row.consumeShopName || row.consumeShopId">
|
||||
核销店铺:{{ row.consumeShopName || row.consumeShopId || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -463,9 +531,14 @@ onMounted(async () => {
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)" effect="plain">
|
||||
{{ getStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
<div class="status-stack">
|
||||
<el-tag :type="getStatusType(row.status)" effect="plain">
|
||||
{{ getStatusLabel(row.status) }}
|
||||
</el-tag>
|
||||
<span v-if="row.localDispatchStatus" class="cell-subline">
|
||||
{{ getLocalDispatchStatusLabel(row.localDispatchStatus) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作人" min-width="120">
|
||||
@@ -476,7 +549,15 @@ onMounted(async () => {
|
||||
</el-table-column>
|
||||
<el-table-column label="记录 ID" min-width="210">
|
||||
<template #default="{ row }">
|
||||
<span class="cell-subline" :title="row.recordId">{{ row.recordId || '-' }}</span>
|
||||
<div class="cell-stack">
|
||||
<span class="cell-subline" :title="row.recordId">{{ row.recordId || '-' }}</span>
|
||||
<span v-if="row.taskNo" class="cell-subline" :title="row.taskNo">
|
||||
任务:{{ row.taskNo }}
|
||||
<template v-if="getMatchConfidenceLabel(row.matchConfidence)">
|
||||
· 匹配{{ getMatchConfidenceLabel(row.matchConfidence) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="136" fixed="right">
|
||||
@@ -519,7 +600,7 @@ onMounted(async () => {
|
||||
@import '@/styles/admin-list-pages.css';
|
||||
|
||||
.cloud-records-filter {
|
||||
grid-template-columns: minmax(180px, 0.34fr) minmax(360px, 1fr) auto;
|
||||
grid-template-columns: minmax(180px, 0.28fr) minmax(220px, 0.36fr) minmax(360px, 1fr) auto;
|
||||
}
|
||||
|
||||
.cloud-records-date-range {
|
||||
@@ -549,6 +630,13 @@ onMounted(async () => {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.status-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.cloud-records-filter {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in New Issue
Block a user