Files
anxinyan/admin-web/src/pages/orders/index.vue

1306 lines
47 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
adminApi,
type AdminExpressCompanyItem,
type AdminExpressCompanyRecognitionCandidate,
type AdminManualOrderCreatePayload,
type AdminManualOrderMeta,
type AdminOrderDetail,
type AdminOrderListItem,
type AdminOrderWarehouseOption,
type AdminServicePricePackage,
} from "../../api/admin";
import OrderStatusTag from "../../components/OrderStatusTag.vue";
import { recognizeReturnAddress } from "../../utils/address-recognition";
const loading = ref(false);
const detailLoading = ref(false);
const drawerVisible = ref(false);
const receiveSubmitting = ref(false);
const returnReceiveSubmitting = ref(false);
const warehouseSubmitting = ref(false);
const warehouseDialogVisible = ref(false);
const warehouseOptionsLoading = ref(false);
const warehouseOptions = ref<AdminOrderWarehouseOption[]>([]);
const selectedWarehouseId = ref(0);
const returnDialogVisible = ref(false);
const returnSubmitting = ref(false);
const returnExpressCompany = ref("");
const returnTrackingNo = ref("");
const returnRecognitionLoading = ref(false);
const returnRecognitionCandidates = ref<AdminExpressCompanyRecognitionCandidate[]>([]);
const expressCompanyLoading = ref(false);
const expressCompanyOptions = ref<AdminExpressCompanyItem[]>([]);
const defaultExpressCompany = ref("");
const manualDialogVisible = ref(false);
const manualSubmitting = ref(false);
const manualMetaLoading = ref(false);
const manualMeta = ref<AdminManualOrderMeta>({ categories: [], brands: [], service_price_packages: [] });
const manualForm = ref<AdminManualOrderCreatePayload>(createManualOrderForm());
const manualAddressRecognitionText = ref("");
const keyword = ref("");
const serviceProvider = ref("");
const status = ref("");
const sourceChannel = ref("");
const orders = ref<AdminOrderListItem[]>([]);
const detail = ref<AdminOrderDetail | null>(null);
const providerOptions = [
{ label: "全部服务", value: "" },
{ label: "实物鉴定", value: "anxinyan" },
{ label: "中检鉴定", value: "zhongjian" },
];
const statusOptions = [
{ label: "全部状态", value: "" },
{ label: "待补资料", value: "pending_supplement" },
{ label: "待寄送", value: "pending_shipping" },
{ label: "鉴定中", value: "in_first_review" },
{ label: "待寄回", value: "report_published" },
{ label: "回寄途中", value: "returning" },
{ label: "已完成签收", value: "completed_signed" },
];
const sourceChannelOptions = [
{ label: "全部渠道", value: "" },
{ label: "小程序", value: "mini_program" },
{ label: "H5", value: "h5" },
{ label: "大客户推送订单", value: "enterprise_push" },
{ label: "后台补录订单", value: "manual_entry" },
];
const usageStatusMap: Record<string, string> = {
new: "全新未使用",
light_use: "轻微使用痕迹",
used: "长期使用",
};
const usageStatusText = computed(() => {
const value = detail.value?.extra_info.usage_status || "";
return value ? usageStatusMap[value] || value : "-";
});
const productTitle = computed(() => {
if (!detail.value) {
return "待完善物品信息";
}
return detail.value.product_info.product_name || "待完善物品信息";
});
const productMetaText = computed(() => {
if (!detail.value) {
return "物品信息待完善";
}
const parts = [
detail.value.product_info.category_name,
detail.value.product_info.brand_name,
].filter(Boolean);
return parts.length ? parts.join(" / ") : "物品信息待完善";
});
const canMarkReceived = computed(() => {
if (!detail.value) {
return false;
}
if (detail.value.order_info.can_mark_received) {
return true;
}
return (
detail.value.order_info.order_status === "pending_shipping" &&
Boolean(detail.value.logistics_info?.tracking_no) &&
detail.value.logistics_info?.tracking_status !== "received"
);
});
const logisticsActionText = computed(() => {
if (!detail.value?.logistics_info) {
return "";
}
return canMarkReceived.value ? "用户已提交/寄出,待鉴定中心签收" : detail.value.logistics_info.tracking_status_text;
});
const canSubmitReturnLogistics = computed(() => Boolean(detail.value?.order_info.can_submit_return_logistics));
const returnLogisticsBlockReason = computed(() => detail.value?.order_info.return_logistics_block_reason || "");
const canMarkReturnReceived = computed(() => Boolean(detail.value?.order_info.can_mark_return_received));
const expressCompanySelectOptions = computed(() => {
if (!returnExpressCompany.value || expressCompanyOptions.value.some((item) => item.company_name === returnExpressCompany.value)) {
return expressCompanyOptions.value;
}
return [
{
id: 0,
company_name: returnExpressCompany.value,
company_code: returnExpressCompany.value,
status: "enabled",
status_text: "启用中",
is_default: false,
sort_order: 0,
remark: "",
created_at: "",
updated_at: "",
},
...expressCompanyOptions.value,
];
});
const manualServicePackages = computed<AdminServicePricePackage[]>(() =>
manualMeta.value.service_price_packages.find((item) => item.service_provider === manualForm.value.service_provider)?.packages.filter((item) => item.is_enabled) || [],
);
let returnRecognitionTimer: ReturnType<typeof setTimeout> | undefined;
function createManualOrderForm(): AdminManualOrderCreatePayload {
return {
service_provider: "anxinyan",
price_package_id: 0,
price_package_code: "",
product_info: {
category_id: 0,
brand_id: 0,
brand_name: "",
product_name: "",
color: "",
size_spec: "",
serial_no: "",
},
extra_info: {
purchase_channel: "",
purchase_price: 0,
usage_status: "",
condition_desc: "",
remark: "",
},
return_address: {
consignee: "",
mobile: "",
province: "",
city: "",
district: "",
detail_address: "",
},
materials: [
{
item_code: "manual_initial",
item_name: "补录资料",
is_required: false,
files: [],
},
],
};
}
async function fetchOrders() {
loading.value = true;
try {
const response = await adminApi.getOrders({
keyword: keyword.value,
service_provider: serviceProvider.value,
status: status.value,
source_channel: sourceChannel.value,
});
orders.value = response.data.list;
} catch (error) {
console.error(error);
ElMessage.error("订单列表加载失败");
} finally {
loading.value = false;
}
}
async function ensureManualMeta() {
if (manualMeta.value.categories.length) return;
manualMetaLoading.value = true;
try {
const response = await adminApi.getManualOrderMeta();
manualMeta.value = response.data;
applyManualDefaultPackage(true);
} catch (error) {
console.error(error);
ElMessage.error("补录订单选项加载失败");
} finally {
manualMetaLoading.value = false;
}
}
function applyManualDefaultPackage(force = false) {
const current = manualServicePackages.value.find((item) => item.id === manualForm.value.price_package_id);
if (current && !force) {
manualForm.value.price_package_code = current.package_code;
return;
}
const target = manualServicePackages.value.find((item) => item.is_default) || manualServicePackages.value[0];
manualForm.value.price_package_id = target?.id || 0;
manualForm.value.price_package_code = target?.package_code || "";
}
async function openManualDialog() {
manualForm.value = createManualOrderForm();
manualAddressRecognitionText.value = "";
manualDialogVisible.value = true;
await ensureManualMeta();
applyManualDefaultPackage(true);
}
async function ensureExpressCompanyOptions() {
if (expressCompanyOptions.value.length) return;
expressCompanyLoading.value = true;
try {
const response = await adminApi.getExpressCompanies({ enabled_only: 1 });
expressCompanyOptions.value = response.data.list;
defaultExpressCompany.value = response.data.default_company;
} catch (error) {
console.error(error);
ElMessage.error("快递公司列表加载失败");
} finally {
expressCompanyLoading.value = false;
}
}
function applyRecognizedManualAddress() {
const result = recognizeReturnAddress(manualAddressRecognitionText.value);
if (!result.ok || !result.address) {
ElMessage.warning(result.message || "寄回地址识别失败");
return;
}
manualForm.value.return_address = {
...manualForm.value.return_address,
...result.address,
};
ElMessage.success("寄回地址已识别并填入");
}
function validateManualForm() {
const form = manualForm.value;
if (!form.price_package_id) {
ElMessage.warning("请选择价格套餐");
return false;
}
if (!form.product_info.category_id) {
ElMessage.warning("请选择品类");
return false;
}
const address = form.return_address;
if (!address.consignee.trim() || !address.mobile.trim() || !address.province.trim() || !address.city.trim() || !address.district.trim() || !address.detail_address.trim()) {
ElMessage.warning("请完整填写寄回收件信息");
return false;
}
return true;
}
async function submitManualOrder() {
if (!validateManualForm()) return;
manualSubmitting.value = true;
try {
const payload: AdminManualOrderCreatePayload = JSON.parse(JSON.stringify(manualForm.value));
payload.product_info.brand_id = 0;
payload.product_info.brand_name = payload.product_info.brand_name.trim();
const response = await adminApi.createManualOrder(payload);
ElMessage.success(`补录订单已创建:${response.data.order_no} / ${response.data.appraisal_no}`);
manualDialogVisible.value = false;
await fetchOrders();
} catch (error) {
console.error(error);
ElMessage.error(error instanceof Error ? error.message : "补录订单创建失败");
} finally {
manualSubmitting.value = false;
}
}
async function openDetail(row: AdminOrderListItem) {
detailLoading.value = true;
drawerVisible.value = true;
try {
const response = await adminApi.getOrderDetail(row.id);
detail.value = response.data;
} catch (error) {
console.error(error);
ElMessage.error("订单详情加载失败");
} finally {
detailLoading.value = false;
}
}
async function reloadDetail() {
if (!detail.value) return;
detailLoading.value = true;
try {
const response = await adminApi.getOrderDetail(detail.value.order_info.id);
detail.value = response.data;
} catch (error) {
console.error(error);
ElMessage.error("订单详情刷新失败");
} finally {
detailLoading.value = false;
}
}
async function markReceived() {
if (!detail.value) return;
receiveSubmitting.value = true;
try {
const response = await adminApi.receiveOrderLogistics(detail.value.order_info.id);
ElMessage.success(response.message || "已标记签收");
await reloadDetail();
await fetchOrders();
} catch (error) {
console.error(error);
ElMessage.error("标记签收失败");
} finally {
receiveSubmitting.value = false;
}
}
async function openWarehouseDialog() {
if (!detail.value) return;
warehouseOptionsLoading.value = true;
warehouseDialogVisible.value = true;
selectedWarehouseId.value = detail.value.shipping_target?.warehouse_id || 0;
try {
const response = await adminApi.getOrderWarehouseOptions(detail.value.order_info.id);
warehouseOptions.value = response.data.list;
} catch (error) {
console.error(error);
ElMessage.error("仓库列表加载失败");
} finally {
warehouseOptionsLoading.value = false;
}
}
async function submitWarehouseReassign() {
if (!detail.value || !selectedWarehouseId.value) {
ElMessage.warning("请先选择一个目标仓库");
return;
}
try {
await ElMessageBox.confirm("改派后,用户寄送页将展示新的收货仓库地址。确定继续吗?", "改派仓库", {
type: "warning",
confirmButtonText: "确认改派",
cancelButtonText: "取消",
});
} catch {
return;
}
warehouseSubmitting.value = true;
try {
const response = await adminApi.reassignOrderWarehouse(detail.value.order_info.id, selectedWarehouseId.value);
ElMessage.success(response.message || "仓库已改派");
warehouseDialogVisible.value = false;
await reloadDetail();
} catch (error) {
console.error(error);
ElMessage.error("仓库改派失败");
} finally {
warehouseSubmitting.value = false;
}
}
async function openReturnDialog() {
if (!detail.value) return;
if (!canSubmitReturnLogistics.value) {
ElMessage.warning(returnLogisticsBlockReason.value || "当前订单暂不支持登记回寄运单");
return;
}
await ensureExpressCompanyOptions();
returnExpressCompany.value = detail.value.return_logistics?.express_company || defaultExpressCompany.value || expressCompanyOptions.value[0]?.company_name || "";
returnTrackingNo.value = detail.value.return_logistics?.tracking_no || "";
returnRecognitionCandidates.value = [];
returnDialogVisible.value = true;
if (returnTrackingNo.value) {
scheduleReturnRecognition();
}
}
async function recognizeReturnExpressCompany() {
const trackingNo = returnTrackingNo.value.trim();
if (!returnDialogVisible.value || !trackingNo) {
returnRecognitionCandidates.value = [];
return;
}
returnRecognitionLoading.value = true;
try {
const response = await adminApi.recognizeExpressCompany({
tracking_no: trackingNo,
company_name: returnExpressCompany.value.trim(),
});
const result = response.data;
returnRecognitionCandidates.value = result.candidates || [];
if (result.resolved) {
returnExpressCompany.value = result.resolved.company_name;
} else if (result.candidates.length === 1) {
returnExpressCompany.value = result.candidates[0].company_name;
}
} catch (error) {
console.error(error);
returnRecognitionCandidates.value = [];
} finally {
returnRecognitionLoading.value = false;
}
}
function scheduleReturnRecognition() {
if (returnRecognitionTimer) {
clearTimeout(returnRecognitionTimer);
}
returnRecognitionTimer = setTimeout(() => {
void recognizeReturnExpressCompany();
}, 500);
}
function chooseReturnRecognitionCandidate(candidate: AdminExpressCompanyRecognitionCandidate) {
returnExpressCompany.value = candidate.company_name;
returnRecognitionCandidates.value = [candidate];
}
async function submitReturnLogistics() {
if (!canSubmitReturnLogistics.value) {
ElMessage.warning(returnLogisticsBlockReason.value || "当前订单暂不支持登记回寄运单");
return;
}
if (!detail.value || !returnExpressCompany.value.trim() || !returnTrackingNo.value.trim()) {
ElMessage.warning("请完整填写回寄快递公司和运单号");
return;
}
returnSubmitting.value = true;
try {
const response = await adminApi.saveOrderReturnLogistics({
id: detail.value.order_info.id,
express_company: returnExpressCompany.value.trim(),
tracking_no: returnTrackingNo.value.trim(),
});
ElMessage.success(response.message || "回寄运单已登记");
returnDialogVisible.value = false;
await reloadDetail();
await fetchOrders();
} catch (error) {
console.error(error);
ElMessage.error(error instanceof Error ? error.message : "回寄运单登记失败");
} finally {
returnSubmitting.value = false;
}
}
watch(returnTrackingNo, () => {
if (returnDialogVisible.value) {
scheduleReturnRecognition();
}
});
watch(() => manualForm.value.service_provider, () => {
applyManualDefaultPackage(true);
});
onBeforeUnmount(() => {
if (returnRecognitionTimer) {
clearTimeout(returnRecognitionTimer);
}
});
async function markReturnReceived() {
if (!detail.value) return;
returnReceiveSubmitting.value = true;
try {
const response = await adminApi.receiveOrderReturnLogistics(detail.value.order_info.id);
ElMessage.success(response.message || "已标记用户签收");
await reloadDetail();
await fetchOrders();
} catch (error) {
console.error(error);
ElMessage.error("标记用户签收失败");
} finally {
returnReceiveSubmitting.value = false;
}
}
onMounted(fetchOrders);
</script>
<template>
<el-card class="panel-card" shadow="never">
<div class="filters-row">
<el-input v-model="keyword" placeholder="搜索订单号 / 鉴定单号 / 商品名称" clearable style="width: 320px" />
<el-select v-model="serviceProvider" placeholder="服务类型" style="width: 160px">
<el-option v-for="item in providerOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="status" placeholder="订单状态" style="width: 160px">
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-select v-model="sourceChannel" placeholder="下单渠道" style="width: 170px">
<el-option v-for="item in sourceChannelOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<el-button type="primary" @click="fetchOrders">查询</el-button>
<el-button @click="openManualDialog">补录订单</el-button>
</div>
</el-card>
<el-card class="panel-card orders-table" shadow="never">
<el-table v-loading="loading" :data="orders" stripe>
<el-table-column prop="order_no" label="订单号" min-width="170" />
<el-table-column prop="appraisal_no" label="鉴定单号" min-width="180" />
<el-table-column prop="product_name" label="商品名称" min-width="220" />
<el-table-column prop="service_provider_text" label="服务类型" min-width="120" />
<el-table-column prop="price_package_name" label="价格套餐" min-width="150">
<template #default="{ row }">{{ row.price_package_name || "-" }}</template>
</el-table-column>
<el-table-column label="下单渠道" min-width="150">
<template #default="{ row }">
<span>{{ row.source_channel_text }}</span>
<div v-if="row.source_customer_id" class="table-subtext">客户ID{{ row.source_customer_id }}</div>
</template>
</el-table-column>
<el-table-column label="订单状态" min-width="150">
<template #default="{ row }">
<OrderStatusTag :status="row.display_status" />
</template>
</el-table-column>
<el-table-column prop="estimated_finish_time" label="预计完成时间" min-width="170" />
<el-table-column prop="pay_amount" label="金额" min-width="100">
<template #default="{ row }">¥{{ row.pay_amount }}</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" min-width="170" />
<el-table-column label="操作" fixed="right" width="110">
<template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)">查看详情</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-drawer v-model="drawerVisible" size="68%" title="订单详情">
<div v-loading="detailLoading" v-if="detail" class="order-detail-shell">
<div class="detail-card order-detail-hero">
<div class="order-detail-hero__main">
<div class="order-detail-hero__eyebrow">订单履约工作区</div>
<div class="order-detail-hero__title">{{ productTitle }}</div>
<div class="order-detail-hero__meta">{{ productMetaText }}</div>
</div>
<div class="order-detail-hero__side">
<div class="order-detail-hero__tags">
<OrderStatusTag :status="detail.order_info.display_status" />
<span class="order-detail-chip">{{ detail.order_info.service_provider_text }}</span>
</div>
<div class="order-detail-hero__actions">
<el-button
v-if="canMarkReceived"
type="primary"
:loading="receiveSubmitting"
@click="markReceived"
>
标记鉴定中心签收
</el-button>
<el-button
v-if="detail.order_info.can_reassign_warehouse"
type="primary"
plain
:loading="warehouseOptionsLoading"
@click="openWarehouseDialog"
>
手动改派仓库
</el-button>
<el-button
v-if="canSubmitReturnLogistics || returnLogisticsBlockReason"
type="primary"
plain
:disabled="!canSubmitReturnLogistics"
@click="openReturnDialog"
>
{{ detail.return_logistics?.tracking_no ? '更新回寄运单' : '登记回寄运单' }}
</el-button>
<el-button
v-if="canMarkReturnReceived"
type="primary"
:loading="returnReceiveSubmitting"
@click="markReturnReceived"
>
标记用户签收
</el-button>
</div>
</div>
</div>
<div class="detail-grid">
<div class="detail-card">
<div class="detail-card__title">订单概览</div>
<div class="order-detail-grid">
<div class="order-detail-item">
<div class="order-detail-item__label">订单号</div>
<div class="order-detail-item__value">{{ detail.order_info.order_no }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">鉴定单号</div>
<div class="order-detail-item__value">{{ detail.order_info.appraisal_no }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">服务类型</div>
<div class="order-detail-item__value">{{ detail.order_info.service_provider_text }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">价格套餐</div>
<div class="order-detail-item__value">{{ detail.order_info.price_package_name || "-" }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">下单渠道</div>
<div class="order-detail-item__value">{{ detail.order_info.source_channel_text || "-" }}</div>
</div>
<div class="order-detail-item" v-if="detail.order_info.source_customer_id">
<div class="order-detail-item__label">大客户客户 ID</div>
<div class="order-detail-item__value">{{ detail.order_info.source_customer_id }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">当前状态</div>
<div class="order-detail-item__value"><OrderStatusTag :status="detail.order_info.display_status" /></div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">订单金额</div>
<div class="order-detail-item__value">¥{{ detail.order_info.pay_amount }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">预计完成</div>
<div class="order-detail-item__value">{{ detail.order_info.estimated_finish_time || "-" }}</div>
</div>
</div>
</div>
<div class="detail-card">
<div class="detail-card__title">商品信息</div>
<div class="order-detail-grid">
<div class="order-detail-item">
<div class="order-detail-item__label">商品名称</div>
<div class="order-detail-item__value">{{ detail.product_info.product_name || "-" }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">品类 / 品牌</div>
<div class="order-detail-item__value">{{ detail.product_info.category_name || "-" }} / {{ detail.product_info.brand_name || "-" }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">颜色 / 规格</div>
<div class="order-detail-item__value">{{ detail.product_info.color || "-" }} / {{ detail.product_info.size_spec || "-" }}</div>
</div>
</div>
</div>
<div class="detail-card">
<div class="detail-card__title">补充信息</div>
<div class="order-detail-grid">
<div class="order-detail-item">
<div class="order-detail-item__label">购买渠道</div>
<div class="order-detail-item__value">{{ detail.extra_info.purchase_channel || "-" }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">购买价格</div>
<div class="order-detail-item__value">¥{{ detail.extra_info.purchase_price }}</div>
</div>
<div class="order-detail-item order-detail-item--full">
<div class="order-detail-item__label">使用情况</div>
<div class="order-detail-item__value">{{ usageStatusText }}</div>
</div>
<div class="order-detail-item order-detail-item--full">
<div class="order-detail-item__label">补充说明</div>
<div class="order-detail-item__value">{{ detail.extra_info.condition_desc || detail.extra_info.remark || "-" }}</div>
</div>
</div>
</div>
<div class="detail-card" v-if="detail.shipping_target">
<div class="detail-card__title">收货仓库</div>
<div class="order-detail-grid">
<div class="order-detail-item">
<div class="order-detail-item__label">仓库名称 / 编码</div>
<div class="order-detail-item__value">{{ detail.shipping_target.warehouse_name }} / {{ detail.shipping_target.warehouse_code }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">收件人 / 联系电话</div>
<div class="order-detail-item__value">{{ detail.shipping_target.receiver_name }} / {{ detail.shipping_target.receiver_mobile }}</div>
</div>
<div class="order-detail-item order-detail-item--full">
<div class="order-detail-item__label">收件地址</div>
<div class="order-detail-item__value">{{ detail.shipping_target.full_address }}</div>
</div>
<div class="order-detail-item order-detail-item--full">
<div class="order-detail-item__label">服务时间</div>
<div class="order-detail-item__value">{{ detail.shipping_target.service_time }}</div>
</div>
</div>
</div>
<div class="detail-card">
<div class="detail-card__title">寄回地址</div>
<div v-if="detail.return_address" class="order-detail-grid">
<div class="order-detail-item">
<div class="order-detail-item__label">收件人 / 联系电话</div>
<div class="order-detail-item__value">{{ detail.return_address.consignee }} / {{ detail.return_address.mobile }}</div>
</div>
<div class="order-detail-item order-detail-item--full">
<div class="order-detail-item__label">寄回地址</div>
<div class="order-detail-item__value">{{ detail.return_address.full_address }}</div>
</div>
</div>
<el-empty v-else description="用户暂未确认寄回地址" :image-size="64" />
</div>
<div class="detail-card" v-if="detail.logistics_info">
<div class="detail-card__title">物流信息</div>
<div class="order-detail-grid">
<div class="order-detail-item">
<div class="order-detail-item__label">快递公司 / 运单号</div>
<div class="order-detail-item__value">{{ detail.logistics_info.express_company || "-" }} / {{ detail.logistics_info.tracking_no || "-" }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">物流状态</div>
<div class="order-detail-item__value">{{ logisticsActionText }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">快递100状态</div>
<div class="order-detail-item__value">{{ detail.logistics_info.provider_status_text || detail.logistics_info.sync_status_text || "-" }}</div>
</div>
<div class="order-detail-item order-detail-item--full">
<div class="order-detail-item__label">最新节点</div>
<div class="order-detail-item__value">{{ detail.logistics_info.latest_desc || "-" }}</div>
</div>
<div class="order-detail-item order-detail-item--full" v-if="detail.logistics_info.sync_error">
<div class="order-detail-item__label">同步异常</div>
<div class="order-detail-item__value">{{ detail.logistics_info.sync_error }}</div>
</div>
<div class="order-detail-item order-detail-item--full" v-if="detail.logistics_info.latest_time">
<div class="order-detail-item__label">最新更新时间</div>
<div class="order-detail-item__value">{{ detail.logistics_info.latest_time }}</div>
</div>
</div>
<div v-if="canMarkReceived" class="detail-card__desc" style="margin-top: 16px;">
<el-alert title="待签收操作" description="物流信息已提交,确认鉴定中心实际收货后再执行签收。" type="warning" :closable="false" show-icon />
</div>
</div>
<div class="detail-card" v-if="detail.return_logistics">
<div class="detail-card__title">回寄物流</div>
<div class="order-detail-grid">
<div class="order-detail-item">
<div class="order-detail-item__label">快递公司 / 运单号</div>
<div class="order-detail-item__value">{{ detail.return_logistics.express_company || "-" }} / {{ detail.return_logistics.tracking_no || "-" }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">物流状态</div>
<div class="order-detail-item__value">{{ detail.return_logistics.tracking_status_text }}</div>
</div>
<div class="order-detail-item">
<div class="order-detail-item__label">快递100状态</div>
<div class="order-detail-item__value">{{ detail.return_logistics.provider_status_text || detail.return_logistics.sync_status_text || "-" }}</div>
</div>
<div class="order-detail-item order-detail-item--full">
<div class="order-detail-item__label">最新节点</div>
<div class="order-detail-item__value">{{ detail.return_logistics.latest_desc || "-" }}</div>
</div>
<div class="order-detail-item order-detail-item--full" v-if="detail.return_logistics.sync_error">
<div class="order-detail-item__label">同步异常</div>
<div class="order-detail-item__value">{{ detail.return_logistics.sync_error }}</div>
</div>
<div class="order-detail-item order-detail-item--full" v-if="detail.return_logistics.latest_time">
<div class="order-detail-item__label">最新更新时间</div>
<div class="order-detail-item__value">{{ detail.return_logistics.latest_time }}</div>
</div>
</div>
</div>
<div class="detail-card" v-if="detail.report_summary">
<div class="detail-card__title">报告信息</div>
<el-alert
v-if="returnLogisticsBlockReason"
type="warning"
:closable="false"
show-icon
:title="returnLogisticsBlockReason"
description="请先在报告中心发布订单报告,发布后再登记回寄运单。"
style="margin-top: 12px;"
/>
<div class="detail-card__desc">
<div class="detail-label">报告编号</div>
<div class="detail-value">{{ detail.report_summary.report_no }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">报告标题</div>
<div class="detail-value">{{ detail.report_summary.report_title }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">发布时间</div>
<div class="detail-value">{{ detail.report_summary.publish_time }}</div>
</div>
</div>
<div class="detail-card" style="grid-column: 1 / -1">
<div class="detail-card__title">时间轴</div>
<div class="timeline-list" style="margin-top: 14px">
<div v-for="item in detail.timeline" :key="`${item.node_text}-${item.occurred_at}`" class="timeline-node">
<div class="timeline-node__title">{{ item.node_text }}</div>
<div class="timeline-node__time">{{ item.occurred_at }}</div>
<div class="timeline-node__desc">{{ item.node_desc }}</div>
</div>
</div>
</div>
<div class="detail-card" v-if="detail.logistics_info" style="grid-column: 1 / -1">
<div class="detail-card__title">物流轨迹</div>
<div class="timeline-list" style="margin-top: 14px">
<div v-for="item in detail.logistics_info.nodes" :key="`${item.node_time}-${item.node_desc}`" class="timeline-node">
<div class="timeline-node__title">{{ item.node_desc }}</div>
<div class="timeline-node__time">{{ item.node_time }}</div>
<div class="timeline-node__desc">{{ item.node_location || "-" }}</div>
</div>
</div>
</div>
<div class="detail-card" v-if="detail.return_logistics" style="grid-column: 1 / -1">
<div class="detail-card__title">回寄物流轨迹</div>
<div v-if="detail.return_logistics.nodes.length" class="timeline-list" style="margin-top: 14px">
<div v-for="item in detail.return_logistics.nodes" :key="`${item.node_time}-${item.node_desc}`" class="timeline-node">
<div class="timeline-node__title">{{ item.node_desc }}</div>
<div class="timeline-node__time">{{ item.node_time }}</div>
<div class="timeline-node__desc">{{ item.node_location || "-" }}</div>
</div>
</div>
<el-empty v-else description="暂无回寄物流轨迹" :image-size="64" />
</div>
<div class="detail-card" v-if="detail.supplement_task" style="grid-column: 1 / -1">
<div class="detail-card__title">补图任务</div>
<div class="detail-card__desc">
<div class="detail-label">补图原因</div>
<div class="detail-value">{{ detail.supplement_task.reason }}</div>
</div>
<div class="detail-card__desc">
<div class="detail-label">截止时间</div>
<div class="detail-value">{{ detail.supplement_task.deadline }}</div>
</div>
<div class="timeline-list" style="margin-top: 14px">
<div v-for="item in detail.supplement_task.items" :key="item.item_name" class="timeline-node">
<div class="timeline-node__title">{{ item.item_name }}</div>
<div class="timeline-node__desc">{{ item.guide_text }}</div>
</div>
</div>
</div>
</div>
</div>
</el-drawer>
<el-dialog v-model="warehouseDialogVisible" title="改派收货仓库" width="720px">
<div v-loading="warehouseOptionsLoading" style="display: grid; gap: 14px;">
<div
v-for="item in warehouseOptions"
:key="item.id"
:style="{
border: selectedWarehouseId === item.id ? '1px solid #c8a45d' : '1px solid var(--admin-border)',
borderRadius: '14px',
padding: '16px 18px',
cursor: 'pointer',
background: selectedWarehouseId === item.id ? 'rgba(200, 164, 93, 0.08)' : '#fff',
}"
@click="selectedWarehouseId = item.id"
>
<div style="display:flex; justify-content:space-between; gap: 16px; align-items:center;">
<div style="font-weight:700;">{{ item.warehouse_name }}</div>
<div style="color: var(--admin-text-subtle);">{{ item.is_default ? '默认仓库' : '可选仓库' }}</div>
</div>
<div style="margin-top: 8px; color: var(--admin-text-subtle);">{{ item.service_provider_text }} / {{ item.warehouse_code }}</div>
<div style="margin-top: 8px;">{{ item.receiver_name }} / {{ item.receiver_mobile }}</div>
<div style="margin-top: 8px;">{{ item.full_address }}</div>
<div style="margin-top: 8px; color: var(--admin-text-subtle);">{{ item.service_time }}</div>
</div>
</div>
<template #footer>
<el-button @click="warehouseDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="warehouseSubmitting" @click="submitWarehouseReassign">确认改派</el-button>
</template>
</el-dialog>
<el-dialog v-model="returnDialogVisible" title="登记回寄运单" width="520px">
<el-form label-position="top">
<el-form-item label="回寄快递公司">
<el-select
v-model="returnExpressCompany"
:loading="expressCompanyLoading"
filterable
placeholder="请选择回寄快递公司"
style="width: 100%"
>
<el-option
v-for="item in expressCompanySelectOptions"
:key="`${item.id}-${item.company_name}`"
:label="item.company_name"
:value="item.company_name"
/>
</el-select>
</el-form-item>
<el-form-item label="回寄运单号">
<el-input v-model="returnTrackingNo" placeholder="请输入回寄运单号" />
</el-form-item>
<div v-if="returnRecognitionLoading" style="margin: -8px 0 12px; color: var(--admin-text-subtle); font-size: 12px;">
正在识别快递公司...
</div>
<div v-if="returnRecognitionCandidates.length" style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px;">
<el-tag
v-for="candidate in returnRecognitionCandidates"
:key="`${candidate.company_code}-${candidate.company_name}`"
effect="plain"
style="cursor: pointer;"
@click="chooseReturnRecognitionCandidate(candidate)"
>
{{ candidate.company_name }}
</el-tag>
</div>
<el-alert
v-if="detail?.return_address"
type="info"
:closable="false"
show-icon
title="当前寄回地址"
:description="`${detail.return_address.consignee} / ${detail.return_address.mobile} / ${detail.return_address.full_address}`"
/>
<el-alert
v-else
type="warning"
:closable="false"
show-icon
title="用户尚未确认寄回地址"
description="请先提醒用户在订单详情中确认寄回地址,再登记回寄运单。"
/>
</el-form>
<template #footer>
<el-button @click="returnDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="returnSubmitting" :disabled="!canSubmitReturnLogistics" @click="submitReturnLogistics">确认登记</el-button>
</template>
</el-dialog>
<el-dialog v-model="manualDialogVisible" title="补录订单" width="860px" destroy-on-close>
<div v-loading="manualMetaLoading" class="manual-order-form">
<el-alert
type="info"
:closable="false"
show-icon
title="补录订单创建后为待入库状态"
description="创建成功后,可在入库台使用订单号或鉴定单号匹配并绑定内部流转挂牌,不需要填写寄入快递单号。"
/>
<div class="manual-section">
<div class="manual-section__title">订单与商品</div>
<el-form label-position="top">
<div class="manual-grid">
<el-form-item label="服务类型">
<el-select v-model="manualForm.service_provider" style="width: 100%">
<el-option label="实物鉴定" value="anxinyan" />
<el-option label="中检鉴定" value="zhongjian" />
</el-select>
</el-form-item>
<el-form-item label="价格套餐">
<el-select
v-model="manualForm.price_package_id"
style="width: 100%"
:disabled="manualServicePackages.length === 0"
@change="applyManualDefaultPackage(false)"
>
<el-option
v-for="item in manualServicePackages"
:key="item.id"
:label="`${item.package_name} / ¥${item.price}`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="品类">
<el-select v-model="manualForm.product_info.category_id" filterable style="width: 100%">
<el-option v-for="item in manualMeta.categories" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</el-form-item>
<el-form-item label="品牌(选填)">
<el-input v-model="manualForm.product_info.brand_name" maxlength="128" placeholder="请输入品牌名称,可不填" />
</el-form-item>
<el-form-item label="商品名称">
<el-input v-model="manualForm.product_info.product_name" placeholder="可选例如Classic Flap 手袋" />
</el-form-item>
<el-form-item label="颜色">
<el-input v-model="manualForm.product_info.color" placeholder="可选" />
</el-form-item>
<el-form-item label="规格 / 尺寸">
<el-input v-model="manualForm.product_info.size_spec" placeholder="可选" />
</el-form-item>
<el-form-item label="序列号">
<el-input v-model="manualForm.product_info.serial_no" placeholder="可选" />
</el-form-item>
</div>
</el-form>
</div>
<div class="manual-section">
<div class="manual-section__title">寄回信息</div>
<el-form label-position="top">
<el-form-item label="自动识别寄回地址">
<div class="manual-address-recognition">
<el-input
v-model="manualAddressRecognitionText"
type="textarea"
:rows="5"
resize="none"
placeholder="粘贴收货人、收货电话、收货地址,自动识别后填入下方字段"
/>
<el-button @click="applyRecognizedManualAddress">识别并填入</el-button>
</div>
</el-form-item>
<div class="manual-grid">
<el-form-item label="收件人">
<el-input v-model="manualForm.return_address.consignee" placeholder="用于匹配或创建用户" />
</el-form-item>
<el-form-item label="手机号">
<el-input v-model="manualForm.return_address.mobile" placeholder="按手机号复用已有用户" />
</el-form-item>
<el-form-item label="省份">
<el-input v-model="manualForm.return_address.province" placeholder="例如:广东省" />
</el-form-item>
<el-form-item label="城市">
<el-input v-model="manualForm.return_address.city" placeholder="例如:深圳市" />
</el-form-item>
<el-form-item label="区县">
<el-input v-model="manualForm.return_address.district" placeholder="例如:南山区" />
</el-form-item>
<el-form-item label="详细地址">
<el-input v-model="manualForm.return_address.detail_address" placeholder="街道、门牌号" />
</el-form-item>
</div>
</el-form>
</div>
</div>
<template #footer>
<el-button @click="manualDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="manualSubmitting" :disabled="manualMetaLoading" @click="submitManualOrder">创建补录订单</el-button>
</template>
</el-dialog>
</template>
<style scoped>
.order-detail-shell {
display: flex;
flex-direction: column;
gap: 18px;
}
.order-detail-hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
padding: 24px;
background:
radial-gradient(circle at top right, rgba(200, 164, 93, 0.12), transparent 30%),
linear-gradient(135deg, #fffdfa 0%, #fbf8f1 100%);
}
.order-detail-hero__main {
min-width: 0;
}
.order-detail-hero__eyebrow {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 12px;
border-radius: 999px;
background: rgba(200, 164, 93, 0.12);
color: #7a5a21;
font-size: 12px;
font-weight: 700;
}
.order-detail-hero__title {
margin-top: 14px;
color: var(--admin-text-main);
font-size: 28px;
font-weight: 800;
line-height: 1.2;
}
.order-detail-hero__meta {
margin-top: 10px;
color: var(--admin-text-subtle);
font-size: 14px;
line-height: 1.6;
}
.order-detail-hero__side {
min-width: 260px;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 14px;
}
.order-detail-hero__tags {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 10px;
}
.order-detail-hero__actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 12px;
}
.order-detail-chip {
display: inline-flex;
align-items: center;
min-height: 30px;
padding: 0 12px;
border-radius: 999px;
background: rgba(72, 104, 133, 0.1);
color: var(--admin-progress);
font-size: 12px;
font-weight: 700;
}
.table-subtext {
margin-top: 4px;
color: var(--admin-text-subtle);
font-size: 12px;
line-height: 1.4;
}
.order-detail-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 16px;
}
.order-detail-item {
padding: 14px 16px;
border: 1px solid #efe8d9;
border-radius: 16px;
background: #fcfaf5;
}
.order-detail-item--full {
grid-column: 1 / -1;
}
.order-detail-item__label {
color: var(--admin-text-subtle);
font-size: 12px;
}
.order-detail-item__value {
margin-top: 8px;
color: var(--admin-text-main);
font-size: 16px;
font-weight: 700;
line-height: 1.5;
word-break: break-word;
}
.manual-order-form {
display: grid;
gap: 18px;
}
.manual-section {
display: grid;
gap: 14px;
}
.manual-section__title {
color: var(--admin-text-main);
font-size: 16px;
font-weight: 800;
}
.manual-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0 18px;
}
.manual-address-recognition {
display: grid;
gap: 10px;
}
.manual-address-recognition .el-button {
justify-self: flex-start;
}
.manual-upload-head {
display: flex;
align-items: center;
gap: 14px;
}
.manual-upload-hint {
color: var(--admin-text-subtle);
font-size: 13px;
}
.manual-file-list {
display: grid;
gap: 10px;
}
.manual-file-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-width: 0;
padding: 10px 12px;
border: 1px solid var(--admin-border);
border-radius: 8px;
background: #fffdfa;
}
.manual-file-item span {
min-width: 0;
overflow: hidden;
color: var(--admin-text-main);
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 1280px) {
.order-detail-hero {
grid-template-columns: 1fr;
display: grid;
}
.order-detail-hero__side {
min-width: 0;
align-items: flex-start;
}
.order-detail-hero__tags,
.order-detail-hero__actions {
justify-content: flex-start;
}
}
@media (max-width: 960px) {
.order-detail-grid {
grid-template-columns: 1fr;
}
.manual-grid {
grid-template-columns: 1fr;
}
}
</style>