Files
shanxia2.0/web-admin/src/views/system/order/index.vue
T
2026-05-20 09:28:36 +08:00

3814 lines
126 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.
<!-- 订单管理页面 -->
<template>
<div class="page-container" :class="{ 'order-embedded': embeddedMode, 'order-bridge': bridgeMode }">
<div class="page-head">
<h1>订单列表</h1>
<p class="page-desc">集中查看各区域订单状态、配送进度与支付情况,支持筛选、展开查看商品明细与运营处理。</p>
</div>
<div class="region-tabs region-tab-strip">
<div class="tab-button-group region-tab-group">
<el-button
v-for="r in allowedRegionList"
:key="r.id"
class="region-tab-btn"
:type="activeRegionId === r.id ? 'primary' : 'default'"
@click="selectRegionTab(r.id)"
>
<span class="region-tab-inner">
<span class="region-tab-name">{{ r.name }}</span>
<span v-if="r.isActive === false" class="region-tab-badge">禁用</span>
</span>
</el-button>
</div>
</div>
<section class="panel">
<div class="panel-top">
<h2>订单列表</h2>
</div>
<div class="filter-block">
<el-form :model="searchForm" class="filter-form" label-position="top">
<p class="filter-hint">支持按订单状态、配送状态、支付状态等维度快速筛选;可展开更多条件进行精细检索。</p>
<div class="filter-grid order-filter-main-grid">
<el-form-item label="关键词">
<el-input
v-model="searchForm.keyword"
placeholder="订单编号/客户名称"
clearable
@keyup.enter="handleSearch"
/>
</el-form-item>
<el-form-item label="订单状态">
<el-select v-model="searchForm.orderStatus" placeholder="全部" clearable>
<el-option label="全部" value="" />
<el-option label="待支付" value="pending_payment" />
<el-option label="待收货" value="pending_confirm" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item label="配送状态">
<el-select v-model="searchForm.deliveryStatus" placeholder="全部" clearable>
<el-option label="全部" value="" />
<el-option label="待分配配送" value="pending_dispatch" />
<el-option label="配送中" value="delivering" />
<el-option label="配送完成" value="delivered" />
</el-select>
</el-form-item>
<el-form-item label="支付状态">
<el-select v-model="searchForm.payStatus" placeholder="全部" clearable>
<el-option label="全部" value="" />
<el-option label="待支付" value="pending_payment" />
<el-option label="已支付" value="paid" />
</el-select>
</el-form-item>
</div>
<div v-show="filterExpanded" class="filter-extra">
<div class="filter-grid order-filter-extra-grid">
<el-form-item label="支付方式">
<el-select v-model="searchForm.paymentMethod" placeholder="全部" clearable>
<el-option label="全部" value="" />
<el-option label="线上支付" value="online" />
<el-option label="线下支付" value="offline" />
</el-select>
</el-form-item>
</div>
</div>
<div class="filter-actions">
<el-button
class="btn-link btn-filter-toggle"
:aria-expanded="String(filterExpanded)"
@click="filterExpanded = !filterExpanded"
>
<el-icon class="btn-filter-toggle-ic">
<ArrowDown v-if="!filterExpanded" />
<ArrowUp v-else />
</el-icon>
{{ filterExpanded ? '收起更多条件' : '展开更多条件' }}
</el-button>
<div class="filter-actions-end">
<el-button class="btn-ghost" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<!-- 数据表格:单元格 flex 纵向居中,避免订单状态列 el-space 与其它列 el-tag 错位 -->
<div class="list-body list-wrap">
<el-table
ref="orderListTableRef"
class="order-list-table list-table"
:data="tableData"
v-loading="loading"
stripe
@expand-change="handleOrderRowExpand"
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
/>
<el-table-column type="expand" width="48">
<template #default="{ row }">
<div class="order-row-expand-wrap" v-loading="!!expandedRowLoadingMap[row.id]">
<div
v-if="getExpandedItems(row.id).length"
class="order-row-expand-table-wrap sqxp-subtable-wrap"
>
<el-table
:data="getExpandedItems(row.id)"
stripe
size="small"
class="order-row-expand-table sqxp-subtable"
@row-dblclick="openOrderProductView"
>
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" :index="index => index + 1" />
<el-table-column prop="productName" label="商品" min-width="220">
<template #default="{ row: line }">
<div class="expand-product-cell">
<div class="expand-product-thumb-wrap">
<el-image v-if="line.imageUrl" :src="line.imageUrl" fit="cover" class="expand-product-thumb" />
<span v-else class="expand-product-thumb placeholder">无图</span>
</div>
<span class="expand-product-name">{{ line.productName || '-' }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="supplierName" label="供货商" min-width="120" />
<el-table-column label="计量" width="88" align="center">
<template #default="{ row: line }">{{ formatMeasureCell(line.measureValue) }}</template>
</el-table-column>
<el-table-column prop="unitName" label="单位" width="70" align="center" />
<el-table-column prop="quantity" label="数量" width="80" align="right" />
<el-table-column label="供货价" min-width="110" align="right">
<template #default="{ row: line }">¥{{ formatAmount(line.supplierUnitPrice) }}</template>
</el-table-column>
<el-table-column label="供货金额" min-width="110" align="right">
<template #default="{ row: line }">¥{{ formatAmount(line.supplierAmount) }}</template>
</el-table-column>
<el-table-column label="商品售价" min-width="110" align="right">
<template #default="{ row: line }">¥{{ formatAmount(line.sellingUnitPrice ?? line.unitPrice) }}</template>
</el-table-column>
<el-table-column label="商品金额" min-width="110" align="right">
<template #default="{ row: line }">¥{{ formatAmount(line.sellingAmount ?? line.amount) }}</template>
</el-table-column>
</el-table>
<div class="order-row-expand-summary sqxp-subtable-summary">
<span
>供货总金额(元):<span class="sqxp-subtable-summary__amount"
>¥{{ formatAmount(getExpandedSummary(row.id).supplierTotal) }}</span
></span
>
<span
>售出总金额(元):<span class="sqxp-subtable-summary__amount"
>¥{{ formatAmount(getExpandedSummary(row.id).sellingTotal) }}</span
></span
>
</div>
</div>
<el-empty v-else-if="!expandedRowLoadingMap[row.id]" description="暂无商品明细" :image-size="72" />
</div>
</template>
</el-table-column>
<el-table-column prop="orderNo" label="订单编号" min-width="200">
<template #default="{ row }">
<el-button link type="primary" @click="toggleRowExpand(row)">{{ row.orderNo }}</el-button>
</template>
</el-table-column>
<el-table-column prop="clientName" label="客户名称" min-width="150" />
<el-table-column prop="regionName" label="区域" min-width="100" />
<el-table-column prop="supplierTotalAmount" label="供货总金额(元)" min-width="140">
<template #default="{ row }">
<span class="amount-text">¥{{ formatAmount(row.supplierTotalAmount) }}</span>
</template>
</el-table-column>
<el-table-column prop="totalAmount" label="订单金额(元)" min-width="120">
<template #default="{ row }">
<span class="amount-text">¥{{ row.totalAmount }}</span>
</template>
</el-table-column>
<el-table-column prop="orderStatus" label="订单状态" min-width="120">
<template #default="{ row }">
<el-space :size="6">
<el-tag :type="getOrderStatusType(resolveOrderStatus(row))" size="small">{{
getOrderStatusText(resolveOrderStatus(row))
}}</el-tag>
<el-tag v-if="row.cancelStatus === 'pending_review'" type="warning" size="small"
>取消审核中</el-tag
>
</el-space>
</template>
</el-table-column>
<el-table-column prop="deliveryStatus" label="配送状态" min-width="120">
<template #default="{ row }">
<el-tag :type="getDeliveryStatusType(resolveDeliveryStatus(row))" size="small">{{
getDeliveryStatusText(resolveDeliveryStatus(row))
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="payStatus" label="支付状态" min-width="120">
<template #default="{ row }">
<el-tag :type="getPayStatusType(resolvePayStatus(row))" size="small">{{
getPayStatusText(resolvePayStatus(row))
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="paymentMethod" label="支付方式" min-width="110" align="center">
<template #default="{ row }">
<el-tag :type="row.paymentMethod === 'offline' ? 'warning' : 'primary'" size="small">
{{ getPaymentMethod(row.paymentMethod) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="clientSettlementStatus" label="客户结算" min-width="110" align="center">
<template #default="{ row }">
<el-tag :type="getSettlementTagType(row.clientSettlementStatus)" size="small">
{{ getClientSettlementLabel(row.clientSettlementStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="supplierSettlementStatus" label="供应商结算" min-width="120" align="center">
<template #default="{ row }">
<el-tag :type="getSettlementTagType(row.supplierSettlementStatus)" size="small">
{{ getSupplierSettlementLabel(row.supplierSettlementStatus) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createAt" label="下单时间" min-width="180" />
<el-table-column label="操作" min-width="260" fixed="right" align="center" header-align="center">
<template #default="{ row }">
<el-button class="btn-op-item primary" @click="handleView(row)">查看/运营</el-button>
<el-button class="btn-op-item" v-if="canAssign(row)" @click="handleAssign(row)">分配配送员</el-button>
<el-button class="btn-op-item op-warn" v-if="row.cancelStatus === 'pending_review'" @click="handleReviewCancel(row)"
>审核取消</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination list-pager-bar">
<el-pagination
:current-page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@update:current-page="
(v) => {
pagination.page = v
loadData()
}
"
@update:page-size="
(v) => {
pagination.pageSize = v
pagination.page = 1
loadData()
}
"
/>
</div>
</div>
</section>
<!-- 订单详情:普通模式用 Dialog;嵌入模式下直接渲染内容,避免父层弹窗+子层弹窗叠加 -->
<el-dialog
v-if="!embeddedMode"
v-model="viewVisible"
title="订单详情"
width="1400px"
top="50px"
destroy-on-close
class="order-module-dialog order-detail-dialog"
modal-class="order-module-dialog-mask"
>
<div v-loading="detailLoading" class="dialog-scroll order-detail-dialog-scroll">
<div class="view-hero order-view-hero">
<div class="order-view-hero-icon">
<el-icon><ShoppingBag /></el-icon>
</div>
<div class="view-hero-main">
<h2>{{ displayValue(viewData.orderNo) }}</h2>
<div class="view-hero-tags">
<el-tag :type="getOrderStatusType(resolveOrderStatus(viewData))" size="small">
{{ getOrderStatusText(resolveOrderStatus(viewData)) }}
</el-tag>
<el-tag :type="getDeliveryStatusType(resolveDeliveryStatus(viewData))" size="small">
{{ getDeliveryStatusText(resolveDeliveryStatus(viewData)) }}
</el-tag>
<el-tag :type="getPayStatusType(resolvePayStatus(viewData))" size="small">
{{ getPayStatusText(resolvePayStatus(viewData)) }}
</el-tag>
<el-tag v-if="viewData.cancelStatus === 'pending_review'" type="warning" size="small">取消审核中</el-tag>
</div>
<p class="view-hero-meta">
客户:{{ displayValue(viewData.clientName) }}
<span class="dot">·</span>
区域:{{ displayValue(viewData.regionName) }}
<span class="dot">·</span>
下单时间:{{ displayValue(viewData.createAt) }}
</p>
</div>
</div>
<!-- Tab切换 -->
<div class="detail-tabs">
<div
v-for="tab in detailTabs"
:key="tab.key"
class="detail-tab"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</div>
</div>
<!-- 订单锁定提示 -->
<div v-if="viewData.lockInfo && viewData.lockInfo.isLocked" class="lock-tip">
<el-icon><WarningFilled /></el-icon>
<span>
此订单已被 <strong>{{ viewData.lockInfo.lockBy }}</strong> 锁定编辑中,锁定时间:{{
viewData.lockInfo.lockTime
}}<br />
请等待其完成操作或联系解除锁定后再进行操作。
</span>
<el-button type="danger" size="small">强制解锁</el-button>
</div>
<!-- Tab: 订单信息 -->
<div v-show="activeTab === 'order'" class="order-tab-pane">
<div class="view-section">
<h3>基础信息</h3>
<dl class="view-kv-grid">
<div class="view-kv">
<dt>订单编号</dt>
<dd>{{ displayValue(viewData.orderNo) }}</dd>
</div>
<div class="view-kv">
<dt>价格版本</dt>
<dd>
<el-button
v-if="viewData.priceListId"
class="price-version-link"
@click="openOrderPriceVersionView"
>
V{{ viewData.priceVersion ?? '-' }}
</el-button>
<span v-else>V{{ viewData.priceVersion ?? '-' }}</span>
</dd>
</div>
<div class="view-kv">
<dt>客户名称</dt>
<dd>{{ displayValue(viewData.clientName) }}</dd>
</div>
<div class="view-kv">
<dt>所属区域</dt>
<dd>{{ displayValue(viewData.regionName) }}</dd>
</div>
<div class="view-kv">
<dt>支付方式</dt>
<dd>{{ getPaymentMethod(viewData.paymentMethod) }}</dd>
</div>
<div class="view-kv">
<dt>客户结算</dt>
<dd class="view-kv-settlement-dd">
<el-tag :type="getSettlementTagType(viewData.clientSettlementStatus)" size="small">
{{ getClientSettlementLabel(viewData.clientSettlementStatus) }}
</el-tag>
<span class="view-kv-settlement-time">{{
settlementAtDisplay(viewData.clientSettlementStatus, viewData.clientSettlementAt)
}}</span>
</dd>
</div>
<div class="view-kv">
<dt>供应商结算</dt>
<dd class="view-kv-settlement-dd">
<el-tag :type="getSettlementTagType(viewData.supplierSettlementStatus)" size="small">
{{ getSupplierSettlementLabel(viewData.supplierSettlementStatus) }}
</el-tag>
<span class="view-kv-settlement-time">{{
settlementAtDisplay(viewData.supplierSettlementStatus, viewData.supplierSettlementAt)
}}</span>
</dd>
</div>
<div class="view-kv">
<dt>支付时间</dt>
<dd>{{ displayValue(viewData.payTime) }}</dd>
</div>
<div class="view-kv view-multiline">
<dt>备注</dt>
<dd>{{ displayValue(viewData.remark) }}</dd>
</div>
</dl>
</div>
<h4 class="section-title">订单明细</h4>
<div class="order-detail-items-scroll">
<el-table
:data="viewData.items"
stripe
:fit="false"
table-layout="auto"
class="order-detail-items-table"
header-align="center"
@row-dblclick="openOrderProductView"
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="index => index + 1"
/>
<el-table-column
prop="productName"
label="商品名称"
min-width="220"
align="left"
header-align="left"
class-name="order-detail-product-col"
>
<template #default="{ row }">
<div class="order-item-name-cell">
<div class="order-item-thumb-wrap">
<el-image
v-if="row.imageUrl"
:src="row.imageUrl"
fit="cover"
class="order-item-thumb"
/>
<span v-else class="order-item-thumb placeholder">无图</span>
</div>
<el-tooltip
:content="orderProductNameTooltip(row.productName)"
placement="top"
:disabled="!isOrderProductNameTruncated(row.productName)"
>
<span class="order-item-name-text">{{
formatOrderProductNameDisplay(row.productName)
}}</span>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column
prop="categoryName"
label="分类"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatOrderCategory(row) }}
</template>
</el-table-column>
<el-table-column prop="supplierName" label="供货商" min-width="130" align="center" />
<el-table-column label="计量" width="100" align="center">
<template #default="{ row }">{{ formatMeasureCell(row.measureValue) }}</template>
</el-table-column>
<el-table-column prop="unitName" label="单位" min-width="80" align="center" />
<el-table-column prop="quantity" label="数量" min-width="88" align="center" />
<el-table-column prop="supplierUnitPrice" label="供货价" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.supplierUnitPrice) }}</template>
</el-table-column>
<el-table-column prop="supplierAmount" label="供货金额" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.supplierAmount) }}</template>
</el-table-column>
<el-table-column prop="sellingUnitPrice" label="商品售价" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.sellingUnitPrice ?? row.unitPrice) }}</template>
</el-table-column>
<el-table-column prop="sellingAmount" label="商品金额" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.sellingAmount ?? row.amount) }}</template>
</el-table-column>
<el-table-column
prop="deliveredQuantity"
class-name="order-delivered-cell"
min-width="200"
align="center"
>
<template #header>
<span>已配送</span>
<el-tooltip
placement="top"
content="本列表示该行商品「已向客户登记送达」的数量(与配送员送达填报一致)。尚未登记时,括号内为 0;若配送员尚未取货,会显示「待取货」等阶段说明,与整单配送状态不同。"
>
<el-icon class="order-delivered-th-icon"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<template #default="{ row }">
<span class="order-delivered-text">{{ formatDeliveredDisplay(row) }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="getItemStatusType(row.status)" size="small">{{
getItemStatusText(row.status)
}}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<div class="amount-summary">
<div class="amount-item">
<span class="label">供货总金额(元):</span>
<span class="value">¥{{ formatAmount(viewData.supplierTotalAmount) }}</span>
</div>
<div class="amount-item">
<span class="label">订单总金额(元):</span>
<span class="value">¥{{ formatAmount(viewData.totalAmount) }}</span>
</div>
<div class="amount-item">
<span class="label">已付金额(元):</span>
<span class="value success">¥{{ formatAmount(viewData.paidAmount) }}</span>
</div>
</div>
</div>
<!-- Tab: 配送信息 -->
<div v-show="activeTab === 'delivery'" class="order-delivery-tab">
<div class="view-section order-delivery-top">
<h3>配送概要</h3>
<dl class="view-kv-grid">
<div class="view-kv">
<dt>配送单号</dt>
<dd>{{ displayValue(viewData.deliveryTaskNo) }}</dd>
</div>
<div class="view-kv">
<dt>订单配送状态</dt>
<dd>
<el-tag :type="getDeliveryStatusType(viewData.deliveryStatus)" size="small">
{{ getDeliveryStatusText(viewData.deliveryStatus) }}
</el-tag>
</dd>
</div>
<div class="view-kv">
<dt>配送员状态</dt>
<dd>
<el-tag
v-if="viewData.deliveryTaskDelivererStatus"
:type="getDelivererTaskStatusType(viewData.deliveryTaskDelivererStatus)"
size="small"
>
{{ getDelivererTaskStatusText(viewData.deliveryTaskDelivererStatus) }}
</el-tag>
<span v-else>-</span>
</dd>
</div>
<div class="view-kv">
<dt>供应商备货状态</dt>
<dd>
<el-tag
v-if="viewData.deliveryTaskSupplierPrepareStatus"
:type="getSupplierPrepareStatusType(viewData.deliveryTaskSupplierPrepareStatus)"
size="small"
>
{{ getSupplierPrepareStatusText(viewData.deliveryTaskSupplierPrepareStatus) }}
</el-tag>
<span v-else>-</span>
</dd>
</div>
<div class="view-kv">
<dt>配送员</dt>
<dd>{{ displayValue(viewData.deliverName) }}</dd>
</div>
<div class="view-kv">
<dt>配送员电话</dt>
<dd>{{ displayValue(viewData.deliverPhone) }}</dd>
</div>
<div class="view-kv">
<dt>配送时效</dt>
<dd>{{ displayValue(formatOrderDetailTime(viewData.expectedTime)) }}</dd>
</div>
<div class="view-kv view-multiline">
<dt>配送备注(运营分配)</dt>
<dd>{{ displayValue(viewData.deliveryDispatchRemark) }}</dd>
</div>
</dl>
</div>
<div class="order-delivery-address-blocks">
<el-card shadow="never" class="od-address-card od-address-card--supplier">
<template #header>
<span class="od-address-card-title">供应商与取货地址</span>
</template>
<div class="od-address-card-body">
<div class="od-address-line">
<span class="od-label">供应商</span>
<span class="od-value">{{ displayValue(viewData.supplierName) }}</span>
</div>
<div v-if="viewData.supplierPhone" class="od-address-line">
<span class="od-label">联系电话</span>
<span class="od-value">{{ viewData.supplierPhone }}</span>
</div>
<div class="od-address-line">
<span class="od-label">取货地址</span>
<span class="od-value">{{ supplierPickupDisplay.address || '-' }}</span>
</div>
<div
v-if="supplierPickupDisplay.contactPerson || supplierPickupDisplay.contactPhone"
class="od-address-line"
>
<span class="od-label">地址联系人</span>
<span class="od-value">
{{ supplierPickupDisplay.contactPerson || '-' }}
<template v-if="supplierPickupDisplay.contactPhone">
/ {{ supplierPickupDisplay.contactPhone }}</template
>
</span>
</div>
<div class="od-address-line">
<span class="od-label">取货时间</span>
<span class="od-value">{{
formatOrderDetailTime(viewData.deliveryPickupTime) || '-'
}}</span>
</div>
<div class="od-address-line">
<span class="od-label">取货备注</span>
<span class="od-value od-text-wrap">{{
displayValue(viewData.deliveryPickupRemark)
}}</span>
</div>
<div class="od-address-line od-address-line--block">
<span class="od-label">备货凭证</span>
<div class="od-value">
<div v-if="(viewData.deliveryPrepareImages || []).length" class="evidence-grid">
<el-image
v-for="(img, idx) in viewData.deliveryPrepareImages"
:key="`prepare-${idx}`"
:src="img"
:preview-src-list="viewData.deliveryPrepareImages"
:initial-index="idx"
fit="cover"
class="evidence-image"
/>
</div>
<span v-else>-</span>
</div>
</div>
</div>
</el-card>
<el-card shadow="never" class="od-address-card od-address-card--customer">
<template #header>
<span class="od-address-card-title">客户送货信息</span>
</template>
<div class="od-address-card-body">
<div class="od-address-line">
<span class="od-label">送货地址</span>
<span class="od-value">{{ displayValue(viewData.deliveryAddress) }}</span>
</div>
<div class="od-address-line">
<span class="od-label">联系人</span>
<span class="od-value">{{ displayValue(viewData.contactPerson) }}</span>
</div>
<div class="od-address-line">
<span class="od-label">联系电话</span>
<span class="od-value">{{ displayValue(viewData.contactPhone) }}</span>
</div>
<div class="od-address-line">
<span class="od-label">送达时间</span>
<span class="od-value">{{
formatOrderDetailTime(viewData.deliveryDeliveryTime) || '-'
}}</span>
</div>
<div class="od-address-line">
<span class="od-label">送达备注</span>
<span class="od-value od-text-wrap">{{
displayValue(viewData.deliveryDeliveryRemark)
}}</span>
</div>
<div class="od-address-line od-address-line--block">
<span class="od-label">取货凭证</span>
<div class="od-value">
<div v-if="(viewData.deliveryPickupImages || []).length" class="evidence-grid">
<el-image
v-for="(img, idx) in viewData.deliveryPickupImages"
:key="`pickup-${idx}`"
:src="img"
:preview-src-list="viewData.deliveryPickupImages"
:initial-index="idx"
fit="cover"
class="evidence-image"
/>
</div>
<span v-else>-</span>
</div>
</div>
<div class="od-address-line od-address-line--block">
<span class="od-label">送达凭证</span>
<div class="od-value">
<div v-if="(viewData.deliveryDeliverImages || []).length" class="evidence-grid">
<el-image
v-for="(img, idx) in viewData.deliveryDeliverImages"
:key="`deliver-${idx}`"
:src="img"
:preview-src-list="viewData.deliveryDeliverImages"
:initial-index="idx"
fit="cover"
class="evidence-image"
/>
</div>
<span v-else>-</span>
</div>
</div>
</div>
</el-card>
</div>
<h4 class="section-title">配送进度</h4>
<el-empty
v-if="!viewData.deliveryTimeline || viewData.deliveryTimeline.length === 0"
description="暂无配送进度记录"
/>
<div v-else class="timeline-horizontal">
<div class="timeline-horizontal-list">
<div
v-for="(item, index) in viewData.deliveryTimeline"
:key="index"
class="timeline-horizontal-item"
>
<div class="timeline-index">{{ index + 1 }}</div>
<div class="timeline-content">
<div class="timeline-time">{{ formatOrderDetailTime(item.time) || '-' }}</div>
<div class="timeline-title">{{ item.title }}</div>
<div v-if="item.description" class="timeline-desc">{{ item.description }}</div>
</div>
<div
v-if="index < viewData.deliveryTimeline.length - 1"
class="timeline-horizontal-arrow"
>→</div
>
</div>
</div>
</div>
</div>
<!-- Tab: 操作记录 -->
<div v-show="activeTab === 'logs'" class="order-logs-tab">
<h4 class="section-title">状态变更记录</h4>
<el-empty
v-if="!viewData.operationLogs || viewData.operationLogs.length === 0"
description="暂无状态变更记录"
/>
<div v-else class="timeline-horizontal">
<div class="timeline-horizontal-list">
<div
v-for="(item, index) in viewData.operationLogs"
:key="index"
class="timeline-horizontal-item"
>
<div class="timeline-index">{{ index + 1 }}</div>
<div class="timeline-content">
<div class="timeline-time">{{ item.time || '-' }}</div>
<div class="timeline-title">{{ item.title }}</div>
<div class="timeline-desc">{{ item.operator }}</div>
<div v-if="item.description" class="timeline-desc" :style="{ color: item.color }">{{
item.description
}}</div>
<template v-if="item.cancelDetail">
<div v-if="item.cancelDetail.note" class="timeline-desc cancel-note">
取消备注:{{ item.cancelDetail.note }}
</div>
<div
v-if="item.cancelDetail.evidenceImageUrls?.length"
class="timeline-cancel-images"
>
<el-image
v-for="(img, idx) in item.cancelDetail.evidenceImageUrls"
:key="`cancel-${index}-${idx}`"
:src="img"
:preview-src-list="item.cancelDetail.evidenceImageUrls"
:initial-index="idx"
fit="cover"
class="timeline-cancel-image"
/>
</div>
</template>
</div>
<div
v-if="index < viewData.operationLogs.length - 1"
class="timeline-horizontal-arrow"
>→</div
>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button class="btn-ghost" @click="viewVisible = false"><el-icon><Close /></el-icon>关闭</el-button>
<template v-if="showOrderDetailActions">
<el-button v-if="showCancelOrderAction" type="danger" @click="showCancelDialog">
<el-icon><Close /></el-icon>取消订单
</el-button>
<el-button
v-if="showDelivererAssignAction"
class="btn-main"
type="primary"
@click="showAssignDialogFromDetail"
>
<el-icon><Check /></el-icon>{{ delivererActionLabel }}
</el-button>
</template>
</template>
</el-dialog>
<div v-else v-show="viewVisible" class="order-embedded-detail">
<div v-loading="detailLoading" class="dialog-scroll order-detail-dialog-scroll">
<div class="view-hero order-view-hero">
<div class="order-view-hero-icon">
<el-icon><ShoppingBag /></el-icon>
</div>
<div class="view-hero-main">
<h2>{{ displayValue(viewData.orderNo) }}</h2>
<div class="view-hero-tags">
<el-tag :type="getOrderStatusType(resolveOrderStatus(viewData))" size="small">
{{ getOrderStatusText(resolveOrderStatus(viewData)) }}
</el-tag>
<el-tag :type="getDeliveryStatusType(resolveDeliveryStatus(viewData))" size="small">
{{ getDeliveryStatusText(resolveDeliveryStatus(viewData)) }}
</el-tag>
<el-tag :type="getPayStatusType(resolvePayStatus(viewData))" size="small">
{{ getPayStatusText(resolvePayStatus(viewData)) }}
</el-tag>
<el-tag v-if="viewData.cancelStatus === 'pending_review'" type="warning" size="small">取消审核中</el-tag>
</div>
<p class="view-hero-meta">
客户:{{ displayValue(viewData.clientName) }}
<span class="dot">·</span>
区域:{{ displayValue(viewData.regionName) }}
<span class="dot">·</span>
下单时间:{{ displayValue(viewData.createAt) }}
</p>
</div>
</div>
<!-- Tab切换 -->
<div class="detail-tabs">
<div
v-for="tab in detailTabs"
:key="tab.key"
class="detail-tab"
:class="{ active: activeTab === tab.key }"
@click="activeTab = tab.key"
>
{{ tab.label }}
</div>
</div>
<!-- 订单锁定提示 -->
<div v-if="viewData.lockInfo && viewData.lockInfo.isLocked" class="lock-tip">
<el-icon><WarningFilled /></el-icon>
<span>
此订单已被 <strong>{{ viewData.lockInfo.lockBy }}</strong> 锁定编辑中,锁定时间:{{
viewData.lockInfo.lockTime
}}<br />
请等待其完成操作或联系解除锁定后再进行操作。
</span>
<el-button type="danger" size="small">强制解锁</el-button>
</div>
<!-- Tab: 订单信息 -->
<div v-show="activeTab === 'order'" class="order-tab-pane">
<div class="view-section">
<h3>基础信息</h3>
<dl class="view-kv-grid">
<div class="view-kv">
<dt>订单编号</dt>
<dd>{{ displayValue(viewData.orderNo) }}</dd>
</div>
<div class="view-kv">
<dt>价格版本</dt>
<dd>
<el-button
v-if="viewData.priceListId"
class="price-version-link"
@click="openOrderPriceVersionView"
>
V{{ viewData.priceVersion ?? '-' }}
</el-button>
<span v-else>V{{ viewData.priceVersion ?? '-' }}</span>
</dd>
</div>
<div class="view-kv">
<dt>客户名称</dt>
<dd>{{ displayValue(viewData.clientName) }}</dd>
</div>
<div class="view-kv">
<dt>所属区域</dt>
<dd>{{ displayValue(viewData.regionName) }}</dd>
</div>
<div class="view-kv">
<dt>支付方式</dt>
<dd>{{ getPaymentMethod(viewData.paymentMethod) }}</dd>
</div>
<div class="view-kv">
<dt>客户结算</dt>
<dd class="view-kv-settlement-dd">
<el-tag :type="getSettlementTagType(viewData.clientSettlementStatus)" size="small">
{{ getClientSettlementLabel(viewData.clientSettlementStatus) }}
</el-tag>
<span class="view-kv-settlement-time">{{
settlementAtDisplay(viewData.clientSettlementStatus, viewData.clientSettlementAt)
}}</span>
</dd>
</div>
<div class="view-kv">
<dt>供应商结算</dt>
<dd class="view-kv-settlement-dd">
<el-tag :type="getSettlementTagType(viewData.supplierSettlementStatus)" size="small">
{{ getSupplierSettlementLabel(viewData.supplierSettlementStatus) }}
</el-tag>
<span class="view-kv-settlement-time">{{
settlementAtDisplay(viewData.supplierSettlementStatus, viewData.supplierSettlementAt)
}}</span>
</dd>
</div>
<div class="view-kv">
<dt>支付时间</dt>
<dd>{{ displayValue(viewData.payTime) }}</dd>
</div>
<div class="view-kv view-multiline">
<dt>备注</dt>
<dd>{{ displayValue(viewData.remark) }}</dd>
</div>
</dl>
</div>
<h4 class="section-title">订单明细</h4>
<div class="order-detail-items-scroll">
<el-table
:data="viewData.items"
stripe
:fit="false"
table-layout="auto"
class="order-detail-items-table"
header-align="center"
@row-dblclick="openOrderProductView"
>
<el-table-column
type="index"
label="序号"
width="68"
align="center"
header-align="center"
:index="index => index + 1"
/>
<el-table-column
prop="productName"
label="商品名称"
min-width="220"
align="left"
header-align="left"
class-name="order-detail-product-col"
>
<template #default="{ row }">
<div class="order-item-name-cell">
<div class="order-item-thumb-wrap">
<el-image
v-if="row.imageUrl"
:src="row.imageUrl"
fit="cover"
class="order-item-thumb"
/>
<span v-else class="order-item-thumb placeholder">无图</span>
</div>
<el-tooltip
:content="orderProductNameTooltip(row.productName)"
placement="top"
:disabled="!isOrderProductNameTruncated(row.productName)"
>
<span class="order-item-name-text">{{
formatOrderProductNameDisplay(row.productName)
}}</span>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column
prop="categoryName"
label="分类"
min-width="140"
align="center"
show-overflow-tooltip
>
<template #default="{ row }">
{{ formatOrderCategory(row) }}
</template>
</el-table-column>
<el-table-column prop="supplierName" label="供货商" min-width="130" align="center" />
<el-table-column label="计量" width="100" align="center">
<template #default="{ row }">{{ formatMeasureCell(row.measureValue) }}</template>
</el-table-column>
<el-table-column prop="unitName" label="单位" min-width="80" align="center" />
<el-table-column prop="quantity" label="数量" min-width="88" align="center" />
<el-table-column prop="supplierUnitPrice" label="供货价" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.supplierUnitPrice) }}</template>
</el-table-column>
<el-table-column prop="supplierAmount" label="供货金额" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.supplierAmount) }}</template>
</el-table-column>
<el-table-column prop="sellingUnitPrice" label="商品售价" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.sellingUnitPrice ?? row.unitPrice) }}</template>
</el-table-column>
<el-table-column prop="sellingAmount" label="商品金额" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.sellingAmount ?? row.amount) }}</template>
</el-table-column>
<el-table-column
prop="deliveredQuantity"
class-name="order-delivered-cell"
min-width="200"
align="center"
>
<template #header>
<span>已配送</span>
<el-tooltip
placement="top"
content="本列表示该行商品「已向客户登记送达」的数量(与配送员送达填报一致)。尚未登记时,括号内为 0;若配送员尚未取货,会显示「待取货」等阶段说明,与整单配送状态不同。"
>
<el-icon class="order-delivered-th-icon"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<template #default="{ row }">
<span class="order-delivered-text">{{ formatDeliveredDisplay(row) }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" min-width="100" align="center">
<template #default="{ row }">
<el-tag :type="getItemStatusType(row.status)" size="small">{{
getItemStatusText(row.status)
}}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<div class="amount-summary">
<div class="amount-item">
<span class="label">供货总金额(元):</span>
<span class="value">¥{{ formatAmount(viewData.supplierTotalAmount) }}</span>
</div>
<div class="amount-item">
<span class="label">订单总金额(元):</span>
<span class="value">¥{{ formatAmount(viewData.totalAmount) }}</span>
</div>
<div class="amount-item">
<span class="label">已付金额(元):</span>
<span class="value">¥{{ formatAmount(viewData.paidAmount) }}</span>
</div>
<div class="amount-item">
<span class="label">应付金额(元):</span>
<span class="value">¥{{ formatAmount(viewData.payableAmount) }}</span>
</div>
</div>
</div>
<!-- Tab: 配送信息 -->
<div v-show="activeTab === 'delivery'" class="delivery-tab-pane">
<!-- 原弹窗内容继续复用(此处无需 footer 按钮) -->
</div>
<!-- Tab: 操作记录 -->
<div v-show="activeTab === 'logs'" class="order-logs-tab">
<h4 class="section-title">状态变更记录</h4>
<el-empty
v-if="!viewData.operationLogs || viewData.operationLogs.length === 0"
description="暂无状态变更记录"
/>
<div v-else class="timeline-horizontal">
<div class="timeline-horizontal-list">
<div
v-for="(item, index) in viewData.operationLogs"
:key="index"
class="timeline-horizontal-item"
>
<div class="timeline-index">{{ index + 1 }}</div>
<div class="timeline-content">
<div class="timeline-time">{{ item.time || '-' }}</div>
<div class="timeline-title">{{ item.title }}</div>
<div class="timeline-desc">{{ item.operator }}</div>
<div v-if="item.description" class="timeline-desc" :style="{ color: item.color }">{{
item.description
}}</div>
<template v-if="item.cancelDetail">
<div v-if="item.cancelDetail.note" class="timeline-desc cancel-note">
取消备注:{{ item.cancelDetail.note }}
</div>
<div
v-if="item.cancelDetail.evidenceImageUrls?.length"
class="timeline-cancel-images"
>
<el-image
v-for="(img, idx) in item.cancelDetail.evidenceImageUrls"
:key="`cancel-${index}-${idx}`"
:src="img"
:preview-src-list="item.cancelDetail.evidenceImageUrls"
:initial-index="idx"
fit="cover"
class="timeline-cancel-image"
/>
</div>
</template>
</div>
<div
v-if="index < viewData.operationLogs.length - 1"
class="timeline-horizontal-arrow"
>→</div
>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 分配 / 更换配送员 -->
<el-dialog
v-model="assignVisible"
:title="assignReassignMode ? '更换配送员' : '分配配送员'"
width="560px"
destroy-on-close
class="order-module-dialog"
modal-class="order-module-dialog-mask"
:close-on-click-modal="false"
>
<el-alert v-if="assignReassignMode" type="info" :closable="false" class="mb-4">
当前订单已分配配送员,提交后将更新为新的配送员(与运营端分配接口一致)。
</el-alert>
<el-form :model="assignForm" label-position="top">
<div class="form-grid-2">
<el-form-item label="选择配送员" class="form-col-span-2">
<el-select
v-model="assignForm.deliverId"
:placeholder="assignForm.regionId ? '请选择本区域配送员' : '订单无区域信息'"
filterable
:disabled="!assignForm.regionId"
>
<el-option
v-for="d in deliverList"
:key="d.id"
:label="d.realName || d.username"
:value="d.id"
/>
</el-select>
</el-form-item>
<el-form-item label="配送时效" required class="form-col-span-2">
<el-date-picker
v-model="assignForm.deliveryDeadlineAt"
type="datetime"
placeholder="截止时间(年月日时分)"
format="YYYY-MM-DD HH:mm"
value-format="YYYY-MM-DDTHH:mm:00"
style="width: 100%"
/>
<div class="hint-text">
默认已按「当前时间 +
区域预计配送(分钟)」计算;区域未配置时使用系统设置中的默认分钟数。须在此时间前完成配送;区域「预警提前」内未完成将出现在时效预警列表。
</div>
</el-form-item>
<el-form-item label="配送备注" class="form-col-span-2">
<el-input
v-model="assignForm.dispatchRemark"
type="textarea"
:rows="2"
maxlength="500"
show-word-limit
placeholder="给配送员或运营的说明(可选)"
/>
</el-form-item>
</div>
</el-form>
<template #footer>
<el-button class="btn-ghost" @click="assignVisible = false"><el-icon><Close /></el-icon>取消</el-button>
<el-button class="btn-main" type="primary" @click="confirmAssign" :loading="submitLoading">
<el-icon><Check /></el-icon>确定
</el-button>
</template>
</el-dialog>
<!-- 审核取消弹窗 -->
<el-dialog v-model="cancelVisible" title="审核取消" width="500px">
<el-form :model="cancelForm" label-width="100px">
<el-form-item label="审核结果">
<el-radio-group v-model="cancelForm.approved">
<el-radio :label="true">通过</el-radio>
<el-radio :label="false">拒绝</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核备注">
<el-input
v-model="cancelForm.note"
type="textarea"
placeholder="请输入审核备注"
:rows="3"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cancelVisible = false"><el-icon><Close /></el-icon>取消</el-button>
<el-button type="primary" @click="confirmCancel" :loading="submitLoading"><el-icon><Check /></el-icon>确定</el-button>
</template>
</el-dialog>
<!-- 取消订单弹窗 -->
<el-dialog v-model="cancelDialogVisible" title="取消订单" width="500px">
<el-alert type="warning" :closable="false" class="mb-4">
取消订单后,订单将被标记为已取消状态,已支付的款项将原路退回。
</el-alert>
<el-form :model="cancelDialogForm" :rules="cancelDialogRules" label-width="100px">
<el-form-item label="订单编号">
<span class="info-text">{{ cancelDialogForm.orderId }}</span>
</el-form-item>
<el-form-item label="取消原因" prop="reason">
<el-input
v-model="cancelDialogForm.reason"
type="textarea"
placeholder="请输入取消原因"
:rows="3"
/>
</el-form-item>
<el-form-item label="取消备注" prop="note">
<el-input
v-model="cancelDialogForm.note"
type="textarea"
placeholder="请输入备注(可选)"
:rows="2"
/>
</el-form-item>
<el-form-item label="佐证图片">
<el-upload
action="#"
list-type="picture-card"
:auto-upload="false"
:on-change="handleCancelEvidenceChange"
:on-remove="handleCancelEvidenceRemove"
:limit="6"
>
<el-icon><Plus /></el-icon>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cancelDialogVisible = false"><el-icon><Close /></el-icon>取消</el-button>
<el-button type="danger" @click="confirmCancelOrder" :loading="cancelDialogLoading"
><el-icon><Check /></el-icon>确认取消</el-button
>
</template>
</el-dialog>
<!-- 调整订单弹窗 -->
<el-dialog
v-model="adjustDialogVisible"
title="订单调整"
width="1100px"
@opened="loadOrderItemsForAdjust"
>
<el-alert type="info" :closable="false" class="mb-4">
双击商品行可查看商品详情;新数量默认等于原数量,支持增量、减量和删除(新数量为0)。
</el-alert>
<el-table
:data="adjustOrderItems"
stripe
class="adjust-order-items-table"
max-height="360"
v-loading="adjustItemsLoading"
@row-dblclick="openOrderProductView"
>
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" :index="index => index + 1" />
<el-table-column
prop="productName"
label="商品名称"
min-width="240"
align="left"
header-align="left"
class-name="adjust-product-col"
>
<template #default="{ row }">
<div class="order-item-name-cell">
<div class="order-item-thumb-wrap">
<el-image v-if="row.imageUrl" :src="row.imageUrl" fit="cover" class="order-item-thumb" />
<span v-else class="order-item-thumb placeholder">无图</span>
</div>
<span class="order-item-name-text">{{ formatOrderProductNameDisplay(row.productName) }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="supplierName" label="供货商" min-width="130" align="center" />
<el-table-column label="计量" min-width="88" align="center">
<template #default="{ row }">{{ formatMeasureCell(row.measureValue) }}</template>
</el-table-column>
<el-table-column prop="unitName" label="单位" min-width="88" align="center" />
<el-table-column prop="quantity" label="数量" min-width="100" align="center" />
<el-table-column label="供货价" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.supplierUnitPrice) }}</template>
</el-table-column>
<el-table-column label="供货金额" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.supplierAmount) }}</template>
</el-table-column>
<el-table-column label="商品售价" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.sellingUnitPrice ?? row.unitPrice) }}</template>
</el-table-column>
<el-table-column label="商品金额" min-width="110" align="center">
<template #default="{ row }">¥{{ formatAmount(row.sellingAmount ?? row.amount) }}</template>
</el-table-column>
<el-table-column label="新数量" min-width="140" align="center">
<template #default="{ row }">
<el-input
type="number"
step="any"
v-model="row.newQuantity"
:min="0"
:controls-position="'right'"
@change="(val) => handleAdjustQuantityCommit(row, val)"
@dblclick.stop
/>
</template>
</el-table-column>
</el-table>
<el-form :model="adjustAddressForm" label-width="100px" class="mt-4">
<el-form-item label="收货地址">
<el-input v-model="adjustAddressForm.deliveryAddress" placeholder="请输入送货地址" maxlength="500" />
</el-form-item>
<el-form-item label="联系人">
<el-input v-model="adjustAddressForm.contactPerson" placeholder="请输入联系人" maxlength="100" />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="adjustAddressForm.contactPhone" placeholder="请输入联系电话" maxlength="20" />
</el-form-item>
<el-form-item label="调整原因">
<el-input v-model="adjustForm.reason" type="textarea" placeholder="商品调整原因(有商品调整时必填)" :rows="2" />
</el-form-item>
<el-form-item label="地址备注">
<el-input
v-model="adjustAddressForm.reason"
type="textarea"
:rows="2"
placeholder="地址调整说明(仅调整地址时可选)"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="adjustDialogVisible = false"><el-icon><Close /></el-icon>取消</el-button>
<el-button type="primary" @click="confirmAdjustOrder" :loading="adjustDialogLoading"><el-icon><Check /></el-icon>确认提交</el-button>
</template>
</el-dialog>
<ProductViewDialog v-model="productViewDialogVisible" :product-id="productViewTargetId" />
<!-- 订单所用价格版本明细(只读,对齐询价「选中供应商报价」信息结构) -->
<el-dialog
v-model="orderPriceVersionVisible"
title="价格版本明细"
width="1100px"
destroy-on-close
class="order-module-dialog order-price-version-dialog"
modal-class="order-module-dialog-mask"
@opened="loadOrderPriceVersionDetail"
>
<div v-loading="orderPriceVersionLoading" class="order-pv-body">
<template v-if="orderPriceVersionDetail">
<div class="view-hero order-pv-hero">
<div class="order-view-hero-icon">
<el-icon><Setting /></el-icon>
</div>
<div class="view-hero-main">
<h2>价格版本 V{{ orderPriceVersionDetail.version ?? orderPriceVersionDetail.versionNo ?? '-' }}</h2>
<div class="view-hero-tags">
<el-tag type="success" size="small">{{ orderPriceVersionDetail.supplierName || '-' }}</el-tag>
<el-tag type="primary" size="small">{{ orderPriceVersionDetail.regionName || '-' }}</el-tag>
</div>
<p class="view-hero-meta">用于订单下单时的价格快照,仅供查看,不可编辑。</p>
</div>
</div>
<div class="view-section order-pv-info-section">
<h3>基础信息</h3>
<dl class="view-kv-grid">
<div class="view-kv">
<dt>区域</dt>
<dd>{{ orderPriceVersionDetail.regionName || '-' }}</dd>
</div>
<div class="view-kv">
<dt>供应商</dt>
<dd>{{ orderPriceVersionDetail.supplierName || '-' }}</dd>
</div>
<div class="view-kv">
<dt>版本号</dt>
<dd>V{{ orderPriceVersionDetail.version ?? orderPriceVersionDetail.versionNo ?? '-' }}</dd>
</div>
<div class="view-kv view-multiline">
<dt>来源询价</dt>
<dd>{{ orderPriceVersionDetail.inquiryTitle || '-' }}</dd>
</div>
</dl>
</div>
</template>
<el-table
v-if="orderPriceVersionDetail?.items?.length"
:data="orderPriceVersionDetail.items"
stripe
max-height="480"
header-align="center"
@row-dblclick="openOrderProductView"
>
<el-table-column type="index" label="序号" width="68" align="center" header-align="center" :index="index => index + 1" />
<el-table-column prop="productName" label="商品名称" min-width="200" align="center">
<template #default="{ row }">
<div class="order-pv-name-cell">
<div class="order-item-thumb-wrap">
<el-image
v-if="row.imageUrl"
:src="row.imageUrl"
fit="cover"
class="order-item-thumb"
/>
<span v-else class="order-item-thumb placeholder">无图</span>
</div>
<el-tooltip
:content="orderProductNameTooltip(row.productName)"
placement="top"
:disabled="!isOrderProductNameTruncated(row.productName)"
>
<span class="order-pv-name-text">{{
formatOrderProductNameDisplay(row.productName)
}}</span>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column
prop="categoryName"
label="分类"
min-width="120"
align="center"
show-overflow-tooltip
/>
<el-table-column label="供货商" min-width="120" align="center">
<template #default>{{ orderPriceVersionDetail?.supplierName || '-' }}</template>
</el-table-column>
<el-table-column label="计量" width="88" align="center">
<template #default="{ row }">{{ formatMeasureCell(row.measureValue) }}</template>
</el-table-column>
<el-table-column prop="unitName" label="单位" min-width="72" align="center" />
<el-table-column label="数量" min-width="80" align="center">
<template #default>-</template>
</el-table-column>
<el-table-column label="供货价" min-width="110" align="center">
<template #default="{ row }">
<span v-if="!orderPriceVersionDetail?.quotationId">-</span>
<span v-else>¥{{ (Number(row.supplierPrice || 0) || 0).toFixed(2) }}</span>
</template>
</el-table-column>
<el-table-column label="供货金额" min-width="110" align="center">
<template #default>-</template>
</el-table-column>
<el-table-column label="商品售价" min-width="110" align="center">
<template #default="{ row }"
>¥{{ (Number(row.finalPrice ?? row.price) || 0).toFixed(2) }}</template
>
</el-table-column>
<el-table-column label="商品金额" min-width="110" align="center">
<template #default>-</template>
</el-table-column>
<el-table-column label="调价系数(%)" min-width="110" align="center">
<template #default="{ row }"
>{{ (Number(row.pricingCoefficient) || 0).toFixed(2) }}%</template
>
</el-table-column>
</el-table>
<el-empty v-else-if="!orderPriceVersionLoading" description="暂无明细" />
</div>
<template #footer>
<el-button class="btn-ghost" @click="orderPriceVersionVisible = false"><el-icon><Close /></el-icon>关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import {
ref,
reactive,
computed,
onMounted,
onActivated,
onDeactivated,
onUnmounted,
watch
} from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
import {
ArrowDown,
ArrowUp,
Check,
Close,
Plus,
QuestionFilled,
RefreshLeft,
Search,
Setting,
ShoppingBag,
WarningFilled
} from '@element-plus/icons-vue'
import ProductViewDialog from '@/components/business/product-view-dialog/index.vue'
import {
fetchOrderList,
fetchOrderDetail,
fetchAssignDelivery,
fetchReviewCancel,
fetchCancelOrder,
fetchOrderItems,
fetchAdjustOrder,
fetchUploadAttachment,
fetchOrderAdjusts,
fetchPriceVersionDetail,
fetchSystemConfig
} from '@/api/business'
import { fetchNotDeletedRegions, fetchUserList } from '@/api/admin'
import { useUserStore } from '@/store/modules/user'
import { fetchAllPagedRecords } from '@/utils/fetch-paged-list'
import { ensureCategoryPathDataLoaded, formatCategoryPathLabel } from '@/utils/category-path-display'
defineOptions({ name: 'OrderManagement' })
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const embeddedMode = computed(() => String(route.query.embedded || '') === '1')
const bridgeMode = computed(() => String(route.query.bridge || '') === '1')
const applyEmbeddedTransparent = () => {
if (!embeddedMode.value && !bridgeMode.value) return
try {
document.documentElement.style.background = 'transparent'
document.body.style.background = 'transparent'
} catch {}
}
const resetEmbeddedTransparent = () => {
try {
document.documentElement.style.background = ''
document.body.style.background = ''
} catch {}
}
const applyBridgeShellMode = () => {
if (!bridgeMode.value) return
try {
document.documentElement.classList.add('sqxp-order-bridge-mode')
document.body.classList.add('sqxp-order-bridge-mode')
} catch {}
}
const resetBridgeShellMode = () => {
try {
document.documentElement.classList.remove('sqxp-order-bridge-mode')
document.body.classList.remove('sqxp-order-bridge-mode')
} catch {}
}
const searchForm = reactive({
keyword: '',
orderStatus: '',
deliveryStatus: '',
payStatus: '',
paymentMethod: '',
regionId: ''
})
const orderListTableRef = ref()
const expandedOrderItemsMap = ref<Record<string, any[]>>({})
const expandedRowLoadingMap = ref<Record<string, boolean>>({})
const tableData = ref<any[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
const regionList = ref<any[]>([])
const activeRegionId = ref('')
const allowedRegionIds = computed<string[]>(() => {
const ids = Array.isArray(userStore.getUserInfo?.regionIds) ? userStore.getUserInfo.regionIds : []
if (ids.length > 0) return ids.map((x: any) => String(x))
return userStore.getUserInfo?.regionId ? [String(userStore.getUserInfo.regionId)] : []
})
const allowedRegionList = computed(() => {
if (userStore.getUserInfo?.role === 'super_admin') return regionList.value
const allow = new Set(allowedRegionIds.value)
return regionList.value.filter((r: any) => allow.has(String(r.id)))
})
const filterExpanded = ref(false)
const viewVisible = ref(false)
const detailLoading = ref(false)
const activeTab = ref('order')
const detailTabs = [
{ key: 'order', label: '订单信息' },
{ key: 'delivery', label: '配送信息' },
{ key: 'logs', label: '操作记录' }
]
const viewData = ref<any>({ items: [], deliveryTimeline: [], operationLogs: [], adjustLogs: [] })
const productViewDialogVisible = ref(false)
const productViewTargetId = ref('')
const orderPriceVersionVisible = ref(false)
const orderPriceVersionLoading = ref(false)
const orderPriceVersionDetail = ref<any>(null)
const formatMeasureCell = (v: unknown) => {
if (v == null || v === '') return '-'
const n = Number(v)
if (Number.isNaN(n)) return '-'
return n.toFixed(3).replace(/\.?0+$/, '')
}
/** 订单明细 / 价格版本弹窗:商品名称过长时省略并 tooltip 展示全文 */
const ORDER_ITEM_PRODUCT_NAME_MAX_LEN = 12
const formatOrderProductNameDisplay = (name?: string | null) => {
const s = name != null && String(name).trim() !== '' ? String(name).trim() : '-'
if (s === '-') return s
return s.length > ORDER_ITEM_PRODUCT_NAME_MAX_LEN
? `${s.slice(0, ORDER_ITEM_PRODUCT_NAME_MAX_LEN)}...`
: s
}
const isOrderProductNameTruncated = (name?: string | null) => {
const s = name != null && String(name).trim() !== '' ? String(name).trim() : ''
return s.length > ORDER_ITEM_PRODUCT_NAME_MAX_LEN
}
const orderProductNameTooltip = (name?: string | null) => {
const s = name != null && String(name).trim() !== '' ? String(name).trim() : '-'
return s
}
const formatOrderCategory = (row: any) => {
return formatCategoryPathLabel(row?.categoryId ?? row?.category_id, row?.categoryName)
}
/** 已配送数量展示:数量×计量 单位(计量取自商品计量列,与明细表一致) */
const formatDeliveredQtyMeasureUnit = (row: any, q: number) => {
const u =
row?.unitName != null && String(row.unitName).trim() !== '' ? String(row.unitName).trim() : ''
const m = formatMeasureCell(row?.measureValue)
const hasMeasure = m !== '' && m !== '-'
if (hasMeasure && u) return `${q}×${m} ${u}`
if (hasMeasure) return `${q}×${m}`
if (u) return `${q}${u}`
return String(q)
}
/** 订单明细「已配送」:按行已登记送达数量展示;与订单/任务配送阶段区分,避免配送中仍显示「未配送」的误解 */
const formatDeliveredDisplay = (row: any) => {
const dq = Number(row?.deliveredQuantity ?? 0)
const qty = Number(row?.quantity ?? 0)
if (dq > 0) {
if (qty > 0 && dq >= qty) return `已全部送达 ${formatDeliveredQtyMeasureUnit(row, dq)}`
return `已送 ${formatDeliveredQtyMeasureUnit(row, dq)} / ${formatDeliveredQtyMeasureUnit(row, qty)}`
}
const delivererSt = String(viewData.value?.deliveryTaskDelivererStatus || '').toLowerCase()
const legacyTaskSt = String(viewData.value?.deliveryTaskStatus || '').toLowerCase()
const orderDelivery = resolveDeliveryStatus(viewData.value)
if (delivererSt === 'pending_pickup' || legacyTaskSt === 'assigned') {
return `待取货 · 已登记送达 ${formatDeliveredQtyMeasureUnit(row, dq)}`
}
if (delivererSt === 'delivering' || legacyTaskSt === 'delivering') return '配送中(待发量)'
if (orderDelivery === 'delivering' || orderDelivery === 'partial_delivered')
return '配送中(待发量)'
if (delivererSt === 'delivered' || orderDelivery === 'delivered')
return qty > 0
? `已送 ${formatDeliveredQtyMeasureUnit(row, 0)} / ${formatDeliveredQtyMeasureUnit(row, qty)}`
: '—'
return '待发运'
}
const openOrderProductView = (row: any) => {
const pid = row?.productId
if (!pid) {
ElMessage.warning('无法获取商品信息')
return
}
productViewTargetId.value = String(pid)
productViewDialogVisible.value = true
}
const openOrderPriceVersionView = () => {
if (!viewData.value.priceListId) {
ElMessage.warning('暂无关联价格清单')
return
}
orderPriceVersionDetail.value = null
orderPriceVersionVisible.value = true
}
const loadOrderPriceVersionDetail = async () => {
const id = viewData.value.priceListId
if (!id) return
orderPriceVersionLoading.value = true
try {
orderPriceVersionDetail.value = await fetchPriceVersionDetail(String(id))
} catch (e) {
console.error(e)
orderPriceVersionDetail.value = null
} finally {
orderPriceVersionLoading.value = false
}
}
const assignVisible = ref(false)
const assignReassignMode = ref(false)
const assignForm = reactive({
orderId: '',
regionId: '',
deliverId: '',
deliveryDeadlineAt: '' as string,
dispatchRemark: ''
})
/** 区域未配置预计配送时,与后端 system-config 默认一致(接口未返回前先用 600) */
const defaultRegionExpectedDeliveryMinutes = ref(600)
const resolveAssignOffsetMinutes = (regionMinutes: unknown): number => {
const fallback = defaultRegionExpectedDeliveryMinutes.value
if (regionMinutes == null || regionMinutes === '') return fallback
const n = Number(regionMinutes)
if (!Number.isFinite(n) || n <= 0) return fallback
return Math.floor(n)
}
/** 配送截止时间:当前时间 + 区域预计配送分钟(未配置则用系统默认) */
const computeAssignDeadline = (regionExpectedDeliveryMinutes?: unknown) => {
const minutes = resolveAssignOffsetMinutes(regionExpectedDeliveryMinutes)
const d = new Date()
d.setMinutes(d.getMinutes() + minutes, 0, 0)
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:00`
}
const deliverList = ref<any[]>([])
const cancelVisible = ref(false)
const cancelForm = reactive({ orderId: '', approved: true, note: '' })
const submitLoading = ref(false)
// 取消订单弹窗
const cancelDialogVisible = ref(false)
const cancelDialogForm = reactive({
orderId: '',
reason: '',
note: '',
evidenceImageUrls: [] as string[]
})
const cancelDialogLoading = ref(false)
const cancelDialogRules = {
reason: [{ required: true, message: '请输入取消原因', trigger: 'blur' }]
}
// 调整订单弹窗
const adjustDialogVisible = ref(false)
const adjustDialogLoading = ref(false)
const adjustItemsLoading = ref(false)
const adjustLoadSeq = ref(0)
const adjustOrderItems = ref<any[]>([])
const adjustAddressForm = reactive({
deliveryAddress: '',
contactPerson: '',
contactPhone: '',
reason: ''
})
const adjustForm = reactive({
reason: ''
})
const changedAdjustItems = computed(() =>
(adjustOrderItems.value || []).filter((item) => {
const original = Number(item.quantity || 0)
const after = Number(item.newQuantity ?? original)
return after >= 0 && after !== original
})
)
const handleAdjustQuantityCommit = (row: any, val: number | string | undefined) => {
const original = Number(row?.quantity || 0)
const next = Number(val ?? row?.newQuantity ?? original)
let normalized = next
if (!Number.isFinite(next)) {
normalized = original
} else if (next < 0) {
normalized = 0
}
row.newQuantity = normalized
console.log('[order-adjust] quantity updated', {
rowId: row?.id,
original,
input: val,
normalized,
amount: Number(row.unitPrice || 0) * normalized
})
}
const getOrderStatusType = (status: string) => {
const map: Record<string, string> = {
pending_payment: 'warning',
pending_confirm: 'primary',
pending: 'primary',
pending_review_cancel: 'warning',
completed: 'success',
cancelled: 'info'
}
return map[status] || 'info'
}
const getOrderStatusText = (status: string) => {
const map: Record<string, string> = {
pending_payment: '待支付',
pending_confirm: '待收货',
pending: '待收货',
pending_review_cancel: '待审核取消',
completed: '已完成',
cancelled: '已取消'
}
return map[status] || status
}
const getPayStatusType = (status: string) => {
const map: Record<string, string> = {
pending_payment: 'warning',
paid: 'success'
}
return map[status] || 'info'
}
const getPayStatusText = (status: string) => {
const map: Record<string, string> = {
pending_payment: '待支付',
paid: '已支付'
}
return map[status] || status
}
const getItemStatusType = (status: string) => {
const map: Record<string, string> = {
normal: '',
pending: 'warning',
delivered: 'success',
partial: 'warning'
}
return map[status] || ''
}
const getItemStatusText = (status: string) => {
const map: Record<string, string> = {
normal: '正常',
pending: '待配送',
delivered: '已送达',
partial: '部分送达'
}
return map[status] || status
}
const getDeliveryStatusType = (status: string) => {
const map: Record<string, string> = {
pending_dispatch: 'warning',
pending_delivery: 'warning',
delivering: 'info',
partial_delivered: 'warning',
delivered: 'success',
pending: 'warning',
picking: 'primary',
completed: 'success'
}
return map[status] || 'info'
}
const getDeliveryStatusText = (status: string) => {
const map: Record<string, string> = {
pending_dispatch: '待分配配送',
pending_delivery: '待配送',
delivering: '配送中',
partial_delivered: '部分送达',
delivered: '配送完成',
pending: '待取货',
picking: '取货中',
completed: '已完成'
}
return map[status] || status
}
const getDelivererTaskStatusType = (status: string) => {
const map: Record<string, string> = {
pending_pickup: 'warning',
delivering: 'primary',
delivered: 'success'
}
const k = (status || '').toLowerCase()
return map[k] || 'info'
}
const getDelivererTaskStatusText = (status: string) => {
const map: Record<string, string> = {
pending_pickup: '待取货',
delivering: '送货中',
delivered: '送货成功'
}
const k = (status || '').toLowerCase()
return (k && map[k]) || status || '-'
}
const getSupplierPrepareStatusType = (status: string) => {
const map: Record<string, string> = {
pending_prepare: 'warning',
prepared: 'primary',
picked_up: 'success'
}
const k = (status || '').toLowerCase()
return map[k] || 'info'
}
const getSupplierPrepareStatusText = (status: string) => {
const map: Record<string, string> = {
pending_prepare: '待备货',
prepared: '备货完成',
picked_up: '取货完成'
}
const k = (status || '').toLowerCase()
return (k && map[k]) || status || '-'
}
const formatOrderDetailTime = (t: string | undefined | null) => {
if (t == null || t === '') return ''
return String(t).replace('T', ' ').slice(0, 19)
}
/** 结算时间:待结算或无时间戳时统一显示「—」 */
const settlementAtDisplay = (status: string | undefined, at: string | undefined | null) => {
const s = (status || '').toLowerCase()
if (s !== 'settled') return '—'
const formatted = formatOrderDetailTime(at)
return formatted || '—'
}
const resolveOrderStatus = (row: any) => {
if (row?.cancelStatus === 'pending_review') return 'pending_review_cancel'
if (row?.orderStatus) return row.orderStatus
if (row?.status === 'pending_payment') return 'pending_payment'
if (['completed'].includes(row?.status)) return 'completed'
if (['cancelled'].includes(row?.status)) return 'cancelled'
return 'pending_confirm'
}
/** 已完成、已取消订单在详情弹窗中不展示调整/取消/分配或更换配送员 */
const showOrderDetailActions = computed(() => {
const s = resolveOrderStatus(viewData.value)
return s !== 'completed' && s !== 'cancelled'
})
/** 订单配送已完成则不再允许分配/更换配送员 */
const showDelivererAssignAction = computed(() => {
if (!showOrderDetailActions.value) return false
return canAssign(viewData.value)
})
/** 调整仅在配送过程中的订单展示 */
const showAdjustOrderAction = computed(() => {
if (!showOrderDetailActions.value) return false
const s = resolveOrderStatus(viewData.value)
return ['pending_delivery', 'delivering', 'partial_delivered', 'pending_confirm'].includes(s)
})
/** 取消按钮在已申请取消审核状态下隐藏 */
const showCancelOrderAction = computed(() => {
if (!showOrderDetailActions.value) return false
return resolveOrderStatus(viewData.value) !== 'pending_review_cancel'
})
const delivererActionLabel = computed(() => {
if (viewData.value.deliverId || viewData.value.deliverName) return '更换配送员'
return '分配配送员'
})
const supplierPickupDisplay = computed(() => {
const p = viewData.value.supplierPickup
if (p && typeof p === 'object') {
return {
address: p.address || '',
contactPerson: p.contactPerson || '',
contactPhone: p.contactPhone || ''
}
}
return { address: '', contactPerson: '', contactPhone: '' }
})
const resolveDeliveryStatus = (row: any) =>
row.deliveryStatus ||
(['pending_payment', 'pending_dispatch', 'pending_delivery', 'pending_review_cancel'].includes(
row.status
)
? 'pending_dispatch'
: row.status === 'delivering'
? 'delivering'
: row.status === 'partial_delivered'
? 'partial_delivered'
: ['pending_confirm', 'completed'].includes(row.status)
? 'delivered'
: 'pending_dispatch')
const resolvePayStatus = (row: any) =>
row.payStatus ||
(row.status === 'refunding' ? 'refunding' : row.paidAmount > 0 ? 'paid' : 'pending_payment')
const canAssign = (row: any) =>
resolveOrderStatus(row) === 'pending_confirm' &&
['pending_dispatch', 'pending_delivery'].includes(resolveDeliveryStatus(row))
const getPaymentMethod = (method: string) => {
const m = (method || '').toLowerCase()
const map: Record<string, string> = {
online: '线上支付',
wechat: '线上支付',
wx: '线上支付',
offline: '线下支付'
}
return map[m] || method || '-'
}
const getSettlementTagType = (status: string | undefined) => {
const s = (status || 'pending').toLowerCase()
return s === 'settled' ? 'success' : 'info'
}
const getClientSettlementLabel = (status: string | undefined) => {
const s = (status || 'pending').toLowerCase()
return s === 'settled' ? '已与客户结算' : '待客户结算'
}
const getSupplierSettlementLabel = (status: string | undefined) => {
const s = (status || 'pending').toLowerCase()
return s === 'settled' ? '已与供应商结算' : '待供应商结算'
}
const formatAmount = (amount: number | string) => {
const num = typeof amount === 'string' ? parseFloat(amount) : amount
return isNaN(num) ? '0.00' : num.toFixed(2)
}
const displayValue = (value: any) => {
if (value === null || value === undefined) return '-'
if (typeof value === 'string' && value.trim() === '') return '-'
return value
}
const getExpandedItems = (orderId: string) => expandedOrderItemsMap.value[orderId] || []
const getExpandedSummary = (orderId: string) => {
const rows = getExpandedItems(orderId)
return rows.reduce(
(acc, it) => {
acc.supplierTotal += Number(it.supplierAmount || 0)
acc.sellingTotal += Number(it.sellingAmount ?? it.amount ?? 0)
return acc
},
{ supplierTotal: 0, sellingTotal: 0 }
)
}
const ensureExpandedItems = async (orderId: string) => {
if (expandedOrderItemsMap.value[orderId]) return
expandedRowLoadingMap.value = { ...expandedRowLoadingMap.value, [orderId]: true }
try {
const res: any = await fetchOrderItems(orderId)
expandedOrderItemsMap.value = {
...expandedOrderItemsMap.value,
[orderId]: Array.isArray(res?.list) ? res.list : []
}
} finally {
expandedRowLoadingMap.value = { ...expandedRowLoadingMap.value, [orderId]: false }
}
}
const handleOrderRowExpand = async (row: any, expandedRows: any[]) => {
const expanded = expandedRows.some((r) => r.id === row.id)
if (!expanded) return
await ensureExpandedItems(row.id)
}
const toggleRowExpand = async (row: any) => {
orderListTableRef.value?.toggleRowExpansion?.(row)
await ensureExpandedItems(row.id)
}
const loadData = async () => {
loading.value = true
try {
const res = await fetchOrderList({
page: pagination.page,
pageSize: pagination.pageSize,
keyword: searchForm.keyword || undefined,
orderStatus: searchForm.orderStatus || undefined,
deliveryStatus: searchForm.deliveryStatus || undefined,
payStatus: searchForm.payStatus || undefined,
paymentMethod: searchForm.paymentMethod || undefined,
regionId: searchForm.regionId || undefined
})
tableData.value = res.list
pagination.total = res.total
} catch (error) {
console.error('加载数据失败:', error)
} finally {
loading.value = false
}
}
const AUTO_REFRESH_INTERVAL_MS = 3000
let lastAutoRefreshAt = 0
const tryAutoRefresh = () => {
const now = Date.now()
if (loading.value) return
if (now - lastAutoRefreshAt < AUTO_REFRESH_INTERVAL_MS) return
lastAutoRefreshAt = now
void loadData()
}
const handleWindowFocus = () => {
tryAutoRefresh()
}
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
tryAutoRefresh()
}
}
const addAutoRefreshListeners = () => {
window.addEventListener('focus', handleWindowFocus)
document.addEventListener('visibilitychange', handleVisibilityChange)
}
const loadRegions = async () => {
try {
const res = await fetchNotDeletedRegions()
regionList.value = res
if (!activeRegionId.value && allowedRegionList.value.length > 0) {
activeRegionId.value = allowedRegionList.value[0].id
}
if (activeRegionId.value) {
searchForm.regionId = activeRegionId.value
}
} catch (error) {
console.error('加载区域失败:', error)
}
}
const selectRegionTab = (regionId: string) => {
if (!regionId || activeRegionId.value === regionId) return
activeRegionId.value = regionId
searchForm.regionId = regionId
pagination.page = 1
loadData()
}
const loadDeliverList = async () => {
if (!assignForm.regionId) {
deliverList.value = []
return
}
try {
const all = await fetchAllPagedRecords((page, pageSize) =>
fetchUserList({ role: 'deliver', regionId: assignForm.regionId, page, pageSize })
)
deliverList.value = all
} catch (error) {
console.error('加载配送员失败:', error)
}
}
watch(
() => assignVisible.value,
(visible) => {
if (visible) void loadDeliverList()
}
)
const handleSearch = () => {
pagination.page = 1
loadData()
}
const tryParseCancelReasonDetail = (rawText?: string) => {
if (!rawText || !rawText.trim().startsWith('{')) return null
try {
const parsed = JSON.parse(rawText) as Record<string, unknown>
const note = typeof parsed.note === 'string' ? parsed.note.trim() : ''
const evidenceImageUrls = Array.isArray(parsed.evidenceImageUrls)
? parsed.evidenceImageUrls.filter((it): it is string => typeof it === 'string' && !!it.trim())
: []
if (!note && evidenceImageUrls.length === 0) return null
return { note, evidenceImageUrls }
} catch {
return null
}
}
const handleReset = () => {
Object.assign(searchForm, {
keyword: '',
orderStatus: '',
deliveryStatus: '',
payStatus: '',
paymentMethod: '',
regionId: activeRegionId.value || ''
})
filterExpanded.value = false
pagination.page = 1
loadData()
}
watch(
() => searchForm.regionId,
(val) => {
if (val && val !== activeRegionId.value) {
activeRegionId.value = val
}
}
)
const handleView = async (row: any, opts?: { preserveTab?: boolean }) => {
const tabToUse = opts?.preserveTab === true ? activeTab.value : 'order'
detailLoading.value = true
viewVisible.value = true
try {
const res: any = await fetchOrderDetail(row.id)
const statusLogs = Array.isArray(res.statusLogs) ? res.statusLogs : []
const operationLogs = statusLogs.map((log: any) => {
const rawNote = log.note as string | undefined
const rawReason = log.reason as string | undefined
const cancelDetail = tryParseCancelReasonDetail(rawReason) || tryParseCancelReasonDetail(rawNote)
let description = rawNote || ''
if (cancelDetail && rawNote && rawNote.trim().startsWith('{')) description = ''
if (rawReason && rawReason.trim().startsWith('{')) {
try {
const o = JSON.parse(rawReason) as Record<string, string | null | undefined>
if (o.toDeliverName != null || o.fromDeliverName != null) {
description = `配送员:${o.fromDeliverName ?? '—'} → ${o.toDeliverName ?? '—'}`
if (log.note) description += `;${log.note}`
} else if (!description && !cancelDetail) {
description = rawReason
}
} catch {
if (!description && !cancelDetail) description = rawReason
}
} else if (rawReason && !description) {
description = rawReason
}
const fromT = (log.fromStatusText as string | undefined)?.trim()
const toT = (log.statusText as string | undefined)?.trim() || (log.status as string) || '-'
const title = fromT ? `${fromT} → ${toT}` : toT
return {
time: log.createAt || '-',
title,
operator: log.operatorName || '-',
type: 'primary',
hollow: false,
description,
cancelDetail
}
})
viewData.value = {
...res,
priceListId: res.priceListId,
priceVersion: res.priceVersion ?? '-',
payStatus: res.payStatus || '',
payTime: res.paymentTime || '',
deliverId: res.deliverId || '',
deliverName: res.deliverName || '',
deliverPhone: res.deliverPhone || '',
deliveryStatus: res.deliveryStatus || '',
expectedTime: res.expectedTime || '',
deliveryDispatchRemark: res.deliveryDispatchRemark || '',
supplierId: res.supplierId || '',
supplierName: res.supplierName || '',
supplierPhone: res.supplierPhone || '',
regionExpectedDeliveryMinutes: res.regionExpectedDeliveryMinutes ?? null,
supplierPickup: res.supplierPickup ?? null,
deliveryTaskNo: res.deliveryTaskNo || '',
deliveryTaskStatus: res.deliveryTaskStatus || '',
deliveryTaskDelivererStatus: res.deliveryTaskDelivererStatus || '',
deliveryTaskSupplierPrepareStatus: res.deliveryTaskSupplierPrepareStatus || '',
deliveryPickupTime: res.deliveryPickupTime || '',
deliveryPickupRemark: res.deliveryPickupRemark || '',
deliveryDeliveryTime: res.deliveryDeliveryTime || '',
deliveryDeliveryRemark: res.deliveryDeliveryRemark || '',
lockInfo: res.lockInfo || { isLocked: false },
deliveryTimeline: Array.isArray(res.deliveryTimeline) ? res.deliveryTimeline : [],
operationLogs,
items: Array.isArray(res.items) ? res.items : []
}
const adjustRes: any = await fetchOrderAdjusts(row.id)
viewData.value.adjustLogs = Array.isArray(adjustRes?.list) ? adjustRes.list : []
activeTab.value = tabToUse
} catch (error) {
console.error('加载详情失败:', error)
viewVisible.value = false
} finally {
detailLoading.value = false
}
}
watch(
() => viewVisible.value,
(v) => {
if (v) return
if (!embeddedMode.value && !bridgeMode.value) return
try {
window.parent?.postMessage?.({ type: 'sqxp-order-operate-close' }, '*')
} catch {}
}
)
const handleEmbeddedMessage = (ev: MessageEvent) => {
if (!embeddedMode.value && !bridgeMode.value) return
const data = ev?.data as any
if (!data || typeof data !== 'object') return
if (data.type === 'sqxp-order-operate-open') {
const id = typeof data.orderId === 'string' ? data.orderId.trim() : ''
if (!id) return
void handleView({ id })
}
}
/** 工作台等入口:带 viewOrderId 进入时打开与「查看/运营」相同的详情弹窗 */
watch(
() => route.query.viewOrderId,
async (raw) => {
const id = typeof raw === 'string' ? raw.trim() : ''
if (!id) return
await handleView({ id })
const q = { ...route.query } as Record<string, string | string[] | undefined | null>
delete q.viewOrderId
await router.replace({ path: route.path, query: q })
},
{ immediate: true }
)
const handleAssign = (row: any) => {
assignReassignMode.value = false
assignForm.orderId = row.id
assignForm.regionId = row.regionId || ''
assignForm.deliverId = ''
assignForm.deliveryDeadlineAt = computeAssignDeadline(row.regionExpectedDeliveryMinutes)
assignForm.dispatchRemark = ''
assignVisible.value = true
}
const showAssignDialogFromDetail = () => {
assignReassignMode.value = !!(viewData.value.deliverId || viewData.value.deliverName)
assignForm.orderId = viewData.value.id
assignForm.regionId = viewData.value.regionId || ''
assignForm.deliverId = assignReassignMode.value ? viewData.value.deliverId || '' : ''
assignForm.deliveryDeadlineAt = computeAssignDeadline(
viewData.value.regionExpectedDeliveryMinutes
)
assignForm.dispatchRemark = viewData.value.deliveryDispatchRemark || ''
assignVisible.value = true
}
const confirmAssign = async () => {
if (!assignForm.regionId) {
ElMessage.warning('订单缺少区域信息,无法分配')
return
}
if (!assignForm.deliverId) {
ElMessage.warning('请选择配送员')
return
}
if (!assignForm.deliveryDeadlineAt) {
ElMessage.warning('请选择配送时效')
return
}
const orderIdSaved = assignForm.orderId
const wasReassign = assignReassignMode.value
submitLoading.value = true
try {
await fetchAssignDelivery({
orderId: assignForm.orderId,
deliverId: assignForm.deliverId,
deliveryDeadlineAt: assignForm.deliveryDeadlineAt,
dispatchRemark: assignForm.dispatchRemark
})
ElMessage.success(wasReassign ? '更换成功' : '分配成功')
assignVisible.value = false
await loadData()
if (viewVisible.value && viewData.value.id === orderIdSaved) {
await handleView({ id: orderIdSaved }, { preserveTab: true })
}
} catch (error) {
console.error('分配失败:', error)
} finally {
submitLoading.value = false
}
}
const handleReviewCancel = (row: any) => {
cancelForm.orderId = row.id
cancelForm.approved = true
cancelForm.note = ''
cancelVisible.value = true
}
const confirmCancel = async () => {
submitLoading.value = true
try {
await fetchReviewCancel({
orderId: cancelForm.orderId,
approved: cancelForm.approved,
note: cancelForm.note
})
ElMessage.success('审核完成')
cancelVisible.value = false
loadData()
} catch (error) {
console.error('审核失败:', error)
} finally {
submitLoading.value = false
}
}
const showCancelDialog = () => {
cancelDialogForm.orderId = viewData.value.id
cancelDialogForm.reason = ''
cancelDialogForm.note = ''
cancelDialogForm.evidenceImageUrls = []
cancelDialogVisible.value = true
}
const handleCancelEvidenceChange = async (uploadFile: any) => {
const file = uploadFile?.raw
if (!file) return
const fd = new FormData()
fd.append('business_type', 'order')
fd.append('business_id', cancelDialogForm.orderId)
fd.append('file', file)
const res: any = await fetchUploadAttachment(fd)
if (res?.fileUrl) cancelDialogForm.evidenceImageUrls.push(res.fileUrl)
}
const handleCancelEvidenceRemove = (_file: any, fileList: any[]) => {
const urls = fileList.map((f: any) => f?.response?.fileUrl || f?.url).filter(Boolean)
cancelDialogForm.evidenceImageUrls = urls
}
const confirmCancelOrder = async () => {
if (!cancelDialogForm.reason) {
ElMessage.warning('请输入取消原因')
return
}
cancelDialogLoading.value = true
try {
await withFullscreenLoading(async () => {
await fetchCancelOrder({
orderId: cancelDialogForm.orderId,
reason: cancelDialogForm.reason,
note: cancelDialogForm.note,
evidenceImageUrls: cancelDialogForm.evidenceImageUrls
})
ElMessage.success('订单取消成功')
cancelDialogVisible.value = false
viewVisible.value = false
loadData()
}, fullscreenLoadingText.delete)
} catch (error) {
console.error('取消订单失败:', error)
ElMessage.error('取消订单失败,请重试')
} finally {
cancelDialogLoading.value = false
}
}
const showAdjustDialog = () => {
adjustForm.reason = ''
adjustAddressForm.deliveryAddress = viewData.value.deliveryAddress || ''
adjustAddressForm.contactPerson = viewData.value.contactPerson || ''
adjustAddressForm.contactPhone = viewData.value.contactPhone || ''
adjustAddressForm.reason = ''
adjustOrderItems.value = []
console.log('[order-adjust] open dialog', { orderId: viewData.value.id })
adjustDialogVisible.value = true
}
// 加载订单商品用于调整
const loadOrderItemsForAdjust = async () => {
if (!viewData.value.id) return
adjustItemsLoading.value = true
const seq = ++adjustLoadSeq.value
console.log('[order-adjust] start load items', { orderId: viewData.value.id, seq })
try {
const res: any = await fetchOrderItems(viewData.value.id)
if (seq !== adjustLoadSeq.value) {
console.log('[order-adjust] skip stale load result', { seq, currentSeq: adjustLoadSeq.value })
return
}
const src = Array.isArray(res?.list) ? res.list : viewData.value.items || []
adjustOrderItems.value = src.map((item: any) => ({
...item,
newQuantity: Number(item.quantity || 0)
}))
console.log('[order-adjust] load items success', {
seq,
total: adjustOrderItems.value.length,
sample: adjustOrderItems.value.slice(0, 3).map((it: any) => ({
id: it.id,
quantity: it.quantity,
unitPrice: it.unitPrice
}))
})
} catch (error) {
console.error('加载订单商品失败:', error)
const src = Array.isArray(viewData.value.items) ? viewData.value.items : []
adjustOrderItems.value = src.map((item: any) => ({
...item,
newQuantity: Number(item.quantity || 0)
}))
console.log('[order-adjust] fallback items from detail', { total: adjustOrderItems.value.length })
} finally {
if (seq === adjustLoadSeq.value) adjustItemsLoading.value = false
}
}
const confirmAdjustOrder = async () => {
console.log('[order-adjust] submit check', {
changedCount: changedAdjustItems.value.length,
changedItems: changedAdjustItems.value.map((item) => ({
id: item.id,
original: item.quantity,
newQuantity: item.newQuantity
}))
})
const hasAddressChanged =
(adjustAddressForm.deliveryAddress || '') !== (viewData.value.deliveryAddress || '') ||
(adjustAddressForm.contactPerson || '') !== (viewData.value.contactPerson || '') ||
(adjustAddressForm.contactPhone || '') !== (viewData.value.contactPhone || '')
if (changedAdjustItems.value.length === 0 && !hasAddressChanged) {
ElMessage.warning('请至少调整商品或修改地址信息')
return
}
if (changedAdjustItems.value.length > 0 && !String(adjustForm.reason || '').trim()) {
ElMessage.warning('有商品调整时,调整原因必填')
return
}
const invalidQty = changedAdjustItems.value.some((item) => {
const original = Number(item.quantity || 0)
const after = Number(item.newQuantity ?? original)
return Number.isNaN(after) || after < 0
})
if (invalidQty) {
ElMessage.warning('新数量不能小于0')
return
}
try {
await ElMessageBox.confirm('确认提交本次订单调整吗?', '确认调整', {
type: 'warning',
confirmButtonText: '确认提交',
cancelButtonText: '取消'
})
} catch {
return
}
adjustDialogLoading.value = true
try {
const adjustments = changedAdjustItems.value.map((item) => {
const originalQty = Number(item.quantity ?? 0)
const afterQty = Number(item.newQuantity ?? item.quantity ?? 0)
const adjustType: 'increase' | 'reduce' | 'delete' =
afterQty === 0 ? 'delete' : afterQty > originalQty ? 'increase' : 'reduce'
return {
orderItemId: item.id,
adjustType,
newQuantity: afterQty,
reason: adjustForm.reason
}
})
const addressUpdate = hasAddressChanged
? {
deliveryAddress: adjustAddressForm.deliveryAddress,
contactPerson: adjustAddressForm.contactPerson,
contactPhone: adjustAddressForm.contactPhone,
reason: adjustAddressForm.reason || '运营调整收货地址'
}
: undefined
await fetchAdjustOrder({
orderId: viewData.value.id,
adjustments,
addressUpdate
})
ElMessage.success('订单调整已提交')
adjustDialogVisible.value = false
await loadData()
await handleView({ id: viewData.value.id }, { preserveTab: true })
activeTab.value = 'logs'
} catch (error) {
console.error('订单调整失败:', error)
ElMessage.error('订单调整失败,请重试')
} finally {
adjustDialogLoading.value = false
}
}
const loadSystemDefaultsForAssign = async () => {
try {
const res: any = await fetchSystemConfig()
const m = res?.order?.defaultRegionExpectedDeliveryMinutes
if (m != null && Number.isFinite(Number(m)) && Number(m) > 0) {
defaultRegionExpectedDeliveryMinutes.value = Math.floor(Number(m))
}
} catch (e) {
console.error('加载系统默认配送时长失败:', e)
}
}
onMounted(() => {
applyEmbeddedTransparent()
applyBridgeShellMode()
if (embeddedMode.value || bridgeMode.value) {
window.addEventListener('message', handleEmbeddedMessage)
}
void ensureCategoryPathDataLoaded()
const kw = route.query.keyword
if (typeof kw === 'string' && kw.trim()) {
searchForm.keyword = kw.trim()
}
void loadRegions().then(() => loadData())
loadSystemDefaultsForAssign()
addAutoRefreshListeners()
})
onActivated(() => {
addAutoRefreshListeners()
tryAutoRefresh()
})
const removeAutoRefreshListeners = () => {
window.removeEventListener('focus', handleWindowFocus)
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
onDeactivated(() => {
removeAutoRefreshListeners()
})
onUnmounted(() => {
removeAutoRefreshListeners()
if (embeddedMode.value || bridgeMode.value) {
window.removeEventListener('message', handleEmbeddedMessage)
resetEmbeddedTransparent()
resetBridgeShellMode()
}
})
</script>
<style lang="scss" scoped>
.order-embedded {
/* 嵌入模式:隐藏订单列表底层,只保留 el-dialog 详情弹窗 */
> .page-head,
> .panel {
display: none !important;
}
background: transparent !important;
}
.order-bridge {
> .page-head,
> .panel {
display: none !important;
}
background: transparent !important;
}
.order-list-table :deep(th.el-table__cell > .cell),
.order-list-table :deep(td.el-table__cell > .cell) {
display: flex;
align-items: center;
justify-content: center;
}
.page-container {
display: flex;
flex-direction: column;
gap: 16px;
}
/* 订单检索区:对齐 fresh-mint-green.html 的筛选块节奏 */
.filter-hint {
margin: 0 0 10px;
font-size: 11px;
color: var(--mint-text-muted, #64748b);
line-height: 1.45;
}
.order-filter-main-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.order-filter-extra-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
@media (max-width: 1100px) {
.order-filter-main-grid,
.order-filter-extra-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 560px) {
.order-filter-main-grid,
.order-filter-extra-grid {
grid-template-columns: 1fr;
}
}
.filter-form :deep(.el-form-item) {
margin-bottom: 0;
min-width: 0;
}
.filter-form :deep(.el-form-item__label) {
font-size: 13px;
font-weight: 500;
color: var(--mint-text-muted, #64748b);
padding: 0 0 4px;
line-height: 1.2;
}
.filter-extra {
padding-top: 10px;
margin-top: 8px;
border-top: 0; /* 设计图:筛选内部不加分割线 */
}
.filter-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
padding-top: 10px;
margin-top: 10px;
border-top: 0; /* 设计图:筛选内部不加分割线 */
}
.filter-actions-end {
display: inline-flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.region-tab-btn {
position: relative;
min-width: 108px;
padding: 0 12px;
}
.region-tab-btn.el-button > :deep(span) {
position: static !important;
display: inline-flex;
width: 100%;
justify-content: center;
}
.region-tab-inner {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
}
.region-tab-badge {
position: absolute;
top: -8px;
right: -8px;
padding: 0 4px;
border-radius: 999px;
font-size: 10px;
line-height: 14px;
color: #b91c1c;
background: #fef2f2;
border: 1px solid rgba(248, 113, 113, 0.6);
pointer-events: none;
}
.btn-filter-toggle {
height: 36px;
padding: 0 8px;
}
.btn-filter-toggle-ic {
margin-right: 4px;
}
.order-row-expand-wrap {
padding: 0;
background: #f8fafc;
border-radius: 0;
}
.order-list-table :deep(td.el-table__expanded-cell) {
padding: 0 !important;
text-align: left !important;
}
.order-list-table :deep(td.el-table__expanded-cell > .cell) {
display: block !important;
padding: 0 !important;
text-align: left !important;
}
/* 展开按钮:边框 + hover + 展开态旋转(对齐设计图树形列表) */
.order-list-table :deep(.el-table__expand-icon) {
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid var(--mint-border, #d9e8e2);
display: inline-flex;
align-items: center;
justify-content: center;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
color: var(--mint-text-muted, #64748b);
background: var(--mint-surface, #fff);
}
.order-list-table :deep(.el-table__expand-icon:hover) {
background: var(--mint-primary-bg, #ecfdf5);
border-color: #10b981;
color: var(--mint-primary-dark, #059669);
}
.order-list-table :deep(.el-table__expand-icon--expanded) {
background: var(--mint-primary-bg, #ecfdf5);
border-color: #10b981;
color: var(--mint-primary-dark, #059669);
}
.order-list-table :deep(.el-table__expand-icon svg) {
transition: transform 0.15s ease;
}
.order-list-table :deep(.el-table__expand-icon--expanded svg) {
transform: rotate(180deg);
}
.order-row-expand-table {
width: max-content;
max-width: 100%;
:deep(.el-table__body-wrapper) {
overflow-x: auto;
}
}
.order-row-expand-table-wrap {
display: block;
width: 100%;
max-width: 100%;
overflow: hidden;
border-radius: 0;
text-align: left;
:deep(.el-table__inner-wrapper),
:deep(.el-table__header-wrapper),
:deep(.el-table__body-wrapper) {
width: max-content;
}
}
/* 树形展开子表(对齐 fresh-mint-green.html / 规范 §8) */
.order-row-expand-wrap {
padding: 0;
}
.order-row-expand-wrap .sqxp-subtable-wrap {
width: 100%;
background: #f8fafc;
}
.order-row-expand-wrap .sqxp-subtable-wrap::before {
content: '';
display: block;
height: 0;
}
.order-row-expand-wrap .sqxp-subtable-wrap {
border: 0;
}
.order-row-expand-table-wrap.sqxp-subtable-wrap {
padding: 0;
}
.order-row-expand-wrap .sqxp-subtable-wrap {
position: relative;
}
.order-row-expand-wrap .sqxp-subtable-wrap::after {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: #10b981;
}
.order-row-expand-wrap .order-row-expand-table-wrap {
padding: 12px 12px 14px 16px;
}
.order-row-expand-wrap .order-row-expand-table {
width: 100%;
border: 1px solid var(--mint-border, #d9e8e2);
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.order-row-expand-wrap .order-row-expand-table :deep(th.el-table__cell) {
background: #e2e8f0 !important;
color: var(--mint-text-muted, #64748b);
font-size: 11px;
font-weight: 600;
}
.order-row-expand-wrap .order-row-expand-table :deep(td.el-table__cell) {
font-size: 13px;
}
.order-row-expand-wrap .order-row-expand-table :deep(.el-table__cell) {
padding: 8px 10px;
}
.order-row-expand-summary {
margin-top: 10px;
display: flex;
gap: 16px;
justify-content: flex-end;
color: var(--mint-text-muted, #64748b);
font-size: 13px;
}
.order-row-expand-summary {
margin-top: 10px;
display: flex;
gap: 20px;
justify-content: flex-end;
color: #606266;
font-size: 13px;
}
.expand-product-cell {
display: inline-flex;
align-items: center;
gap: 10px;
max-width: 100%;
min-width: 0;
}
.expand-product-thumb-wrap {
flex: 0 0 auto;
}
.expand-product-thumb {
width: 34px;
height: 34px;
border-radius: 6px;
display: block;
}
.expand-product-thumb.placeholder {
width: 34px;
height: 34px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: #909399;
background: #f5f7fa;
border: 1px dashed #dcdfe6;
}
.expand-product-name {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.amount-text {
color: #dc2626;
font-weight: 600;
}
.amount-text.danger {
color: #dc2626;
}
.mt-4 {
margin-top: 16px;
}
.mb-2 {
margin-bottom: 8px;
}
.font-medium {
font-weight: 500;
}
.mb-4 {
margin-bottom: 16px;
}
.order-delivered-th-icon {
margin-left: 4px;
vertical-align: middle;
cursor: help;
color: #c0c4cc;
font-size: 14px;
}
.hint-text {
margin-top: 6px;
font-size: 12px;
color: #909399;
line-height: 1.5;
}
.section-title {
font-size: 15px;
font-weight: 600;
margin-bottom: 12px;
padding-left: 12px;
border-left: 4px solid #10b981;
}
.detail-tabs {
display: flex;
gap: 4px;
background: #f5f7fa;
padding: 4px;
border-radius: 8px;
margin: 8px 22px 16px;
width: fit-content;
}
.order-tab-pane {
margin-top: 6px;
padding: 0 22px 8px;
}
.order-logs-tab {
padding: 0 22px 8px;
}
.price-version-link {
border: 0 !important;
background: transparent !important;
padding: 0 !important;
height: auto !important;
min-height: 0 !important;
color: #059669 !important;
font-weight: 600;
}
.price-version-link:hover,
.price-version-link:focus {
color: #047857 !important;
text-decoration: underline;
}
.detail-tab {
padding: 10px 20px;
cursor: pointer;
border-radius: 6px;
font-size: 14px;
color: #606266;
transition: all 0.3s;
&.active {
background: #fff;
color: #059669;
font-weight: 500;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
&:hover:not(.active) {
color: #059669;
}
}
.lock-tip {
background: #fff1f0;
border: 1px solid #ffccc7;
border-radius: 8px;
padding: 12px 16px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
color: #dc2626;
font-size: 14px;
strong {
color: #dc2626;
}
}
.amount-summary {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 40px;
padding: 16px 0;
border-top: 2px solid #10b981;
margin-top: 16px;
.amount-item {
.label {
font-size: 16px;
color: #606266;
}
.value {
font-size: 20px;
font-weight: 600;
color: #dc2626;
margin-left: 8px;
&.success {
color: #059669;
}
}
}
}
.order-delivery-tab {
padding: 0 22px 8px;
}
.order-delivery-address-blocks {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
@media (max-width: 960px) {
.order-delivery-address-blocks {
grid-template-columns: 1fr;
}
}
.od-address-card {
border-radius: 12px;
:deep(.el-card__header) {
padding: 12px 16px;
background: #f5f7fa;
}
:deep(.el-card__body) {
padding: 16px;
}
}
.od-address-card--supplier :deep(.el-card__header) {
border-left: 4px solid #d97706;
}
.od-address-card--customer :deep(.el-card__header) {
border-left: 4px solid #059669;
}
.od-address-card-title {
font-weight: 600;
font-size: 14px;
color: #303133;
}
.od-address-card-body {
font-size: 13px;
color: #606266;
}
.od-address-line {
display: flex;
gap: 12px;
margin-bottom: 10px;
align-items: flex-start;
.od-label {
flex-shrink: 0;
width: 88px;
color: #909399;
}
.od-value {
flex: 1;
min-width: 0;
word-break: break-all;
}
.od-value.od-text-wrap {
white-space: pre-wrap;
}
}
.od-address-line--block {
align-items: flex-start;
}
.evidence-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.evidence-image {
width: 72px;
height: 72px;
border-radius: 6px;
border: 1px solid #ebeef5;
overflow: hidden;
}
.order-delivery-tab {
padding-bottom: 8px;
}
.order-delivery-address-blocks {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 20px;
}
@media (max-width: 960px) {
.order-delivery-address-blocks {
grid-template-columns: 1fr;
}
}
.od-address-card {
border-radius: 12px;
:deep(.el-card__header) {
padding: 12px 16px;
background: #f5f7fa;
}
:deep(.el-card__body) {
padding: 16px;
}
}
.od-address-card--supplier :deep(.el-card__header) {
border-left: 4px solid #d97706;
}
.od-address-card--customer :deep(.el-card__header) {
border-left: 4px solid #059669;
}
.od-address-card-title {
font-weight: 600;
font-size: 14px;
color: #303133;
}
.od-address-card-body {
font-size: 13px;
color: #606266;
}
.od-address-line {
display: flex;
gap: 12px;
margin-bottom: 10px;
align-items: flex-start;
.od-label {
flex-shrink: 0;
width: 88px;
color: #909399;
}
.od-value {
flex: 1;
min-width: 0;
word-break: break-all;
}
.od-value.od-text-wrap {
white-space: pre-wrap;
}
}
.timeline-content {
width: 240px;
min-width: 240px;
max-width: 240px;
min-height: 116px;
height: 100%;
padding: 10px 12px;
border: 1px solid #e4e7ed;
border-radius: 10px;
background: #fff;
display: flex;
flex-direction: column;
.timeline-time {
font-size: 12px;
color: #909399;
margin-bottom: 6px;
}
.timeline-title {
font-size: 14px;
color: #303133;
font-weight: 500;
}
.timeline-desc {
font-size: 13px;
color: #909399;
margin-top: 4px;
}
.cancel-note {
color: #606266;
white-space: pre-wrap;
word-break: break-all;
}
.timeline-cancel-images {
margin-top: 6px;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.timeline-cancel-image {
width: 48px;
height: 48px;
border-radius: 6px;
border: 1px solid #e4e7ed;
overflow: hidden;
}
}
.timeline-horizontal {
width: 100%;
overflow: hidden;
padding: 8px 0 14px;
}
.timeline-horizontal-list {
display: flex;
align-items: stretch;
flex-wrap: wrap;
gap: 8px;
}
.timeline-horizontal-item {
display: flex;
align-items: stretch;
gap: 8px;
max-width: 100%;
}
.timeline-index {
width: 22px;
height: 22px;
border-radius: 50%;
background: #10b981;
color: #fff;
font-size: 12px;
font-weight: 600;
line-height: 22px;
text-align: center;
flex: 0 0 22px;
margin-top: 8px;
}
.timeline-horizontal-item:last-child .timeline-horizontal-arrow {
display: none;
}
@media (max-width: 1200px) {
.timeline-content {
width: 210px;
min-width: 210px;
max-width: 210px;
}
}
.timeline-horizontal-arrow {
color: #c0c4cc;
font-size: 16px;
font-weight: 600;
user-select: none;
align-self: center;
}
.info-text {
color: #606266;
}
.unit-text {
margin-left: 8px;
color: #909399;
}
.adjust-summary {
margin-bottom: 16px;
}
.adjust-preview {
margin-top: 16px;
}
/* 覆盖全局表格居中:调整弹窗内商品名称左对齐 */
.adjust-order-items-table :deep(.el-table__cell.adjust-product-col > .cell) {
justify-content: flex-start;
text-align: left;
}
.adjust-order-items-table :deep(.el-table__cell:not(.adjust-product-col) > .cell) {
justify-content: center;
text-align: center;
}
.adjust-order-items-table :deep(.el-table__body-wrapper) {
overflow-y: auto;
}
.text-danger {
color: #dc2626;
}
/* 订单明细:横向滚动,列保持 min-width 可读 */
.order-detail-items-scroll {
width: 100%;
max-width: 100%;
overflow-x: hidden;
overflow-y: visible;
margin-bottom: 12px;
-webkit-overflow-scrolling: touch;
}
/*
* 全局 el-ui.scss 对 .el-table / .el-table__inner-wrapper 使用了 width:100%!important,
* 会把列压进弹窗宽度导致末列被裁切且无横向滚动。此处覆盖为按列宽撑开并由外层滚动。
*/
.order-detail-items-scroll :deep(.el-table) {
width: 100% !important;
min-width: 100%;
}
.order-detail-items-scroll :deep(.el-table .el-table__inner-wrapper) {
width: 100% !important;
min-width: 100%;
}
.order-detail-items-scroll :deep(.el-table__body-wrapper) {
overflow-x: auto !important;
overflow-y: auto !important;
}
.order-detail-items-scroll :deep(.el-table__header-wrapper) {
overflow-x: hidden !important;
}
/* 已配送列:全局表格 nowrap 会截断长文案,此处允许换行完整展示 */
.order-detail-items-table :deep(.el-table__cell.order-delivered-cell .cell) {
white-space: normal;
word-break: break-word;
line-height: 1.45;
overflow: visible;
text-overflow: clip;
}
.order-detail-items-table :deep(.order-delivered-text) {
display: inline-block;
max-width: 100%;
text-align: center;
}
/* 订单明细:除商品名称外均居中(覆盖全局 el-table 与下方通用规则) */
.order-detail-items-table :deep(.el-table__cell:not(.order-detail-product-col)) {
text-align: center;
}
.order-detail-items-table :deep(.el-table__cell:not(.order-detail-product-col) > .cell) {
justify-content: center;
text-align: center;
}
/*
* showOverflowTooltip 会给 tbody 内层 .cell 设置固定像素宽,块级时贴齐单元格一侧,
* 表头仍按整格居中,易出现「分类」等列表头与数据纵向不对齐。
*/
.order-detail-items-table
:deep(td.el-table__cell:not(.order-detail-product-col) .cell.el-tooltip) {
width: 100% !important;
max-width: 100%;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.order-detail-items-table :deep(th.el-table__cell:not(.order-detail-product-col) > .cell) {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
box-sizing: border-box;
text-align: center;
}
.order-detail-items-table :deep(.el-table__cell.order-detail-product-col) {
text-align: left;
}
.order-detail-items-table :deep(.el-table__cell.order-detail-product-col > .cell) {
justify-content: flex-start;
text-align: left;
}
.order-detail-items-table :deep(.el-table__cell) {
vertical-align: middle;
}
.order-item-name-cell {
display: inline-flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
max-width: 100%;
}
.order-item-thumb-wrap {
flex-shrink: 0;
}
.order-item-thumb {
width: 40px;
height: 40px;
border-radius: 4px;
display: block;
}
.order-item-thumb.placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
color: #909399;
background: #f5f7fa;
border: 1px dashed #dcdfe6;
}
.order-item-name-text {
text-align: left;
flex: 1;
min-width: 0;
}
.order-pv-name-cell {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
max-width: 100%;
}
.order-pv-name-text {
text-align: left;
flex: 1;
min-width: 0;
}
.order-pv-body {
min-height: 120px;
}
</style>
<style lang="scss" scoped>
.order-embedded-detail {
height: 100%;
}
.order-embedded .page-head,
.order-embedded .panel,
.order-embedded .pagination,
.order-embedded .list-toolbar,
.order-embedded .region-tabs,
.order-bridge .page-head,
.order-bridge .panel,
.order-bridge .pagination,
.order-bridge .list-toolbar,
.order-bridge .region-tabs {
display: none !important;
}
.order-embedded .order-embedded-detail :deep(.order-detail-dialog-scroll) {
padding: 12px 16px;
}
</style>
<!-- Teleport 到 body,需非 scoped 才能命中 .order-detail-dialog -->
<style lang="scss">
.sqxp-order-bridge-mode #app-sidebar,
.sqxp-order-bridge-mode #app-header {
display: none !important;
}
html.sqxp-order-bridge-mode,
body.sqxp-order-bridge-mode,
.sqxp-order-bridge-mode #app,
.sqxp-order-bridge-mode #app-main,
.sqxp-order-bridge-mode #app-content,
.sqxp-order-bridge-mode .app-layout,
.sqxp-order-bridge-mode .layout-content {
background: transparent !important;
}
.sqxp-order-bridge-mode #app-main,
.sqxp-order-bridge-mode #app-content {
margin: 0 !important;
padding: 0 !important;
width: 100% !important;
max-width: none !important;
}
.order-module-dialog.el-dialog {
display: flex;
flex-direction: column;
max-height: calc(100vh - 64px);
}
.order-module-dialog .el-dialog__header {
flex-shrink: 0;
background: linear-gradient(135deg, #ecfdf5 0%, #ffffff 70%);
}
.order-module-dialog .el-dialog__body {
flex: 1 1 auto;
min-height: 0;
padding: 0;
overflow: hidden;
}
.order-module-dialog .dialog-scroll {
height: min(720px, calc(100vh - 180px));
max-height: min(720px, calc(100vh - 180px));
overflow-y: auto;
overflow-x: hidden;
padding: 0;
}
.order-module-dialog .el-dialog__footer {
flex-shrink: 0;
background: #f8fafc;
}
.order-module-dialog .el-dialog__footer .el-button {
height: 36px;
border-radius: 8px;
font-weight: 600;
display: inline-flex;
align-items: center;
justify-content: center;
}
.order-module-dialog .el-dialog__footer .el-button .el-icon {
margin-right: 6px;
}
.order-module-dialog-mask {
backdrop-filter: none !important;
}
.order-view-hero {
align-items: center;
border-bottom: 1px solid #d9e8e2;
}
.order-view-hero-icon {
width: 72px;
height: 72px;
border-radius: 12px;
border: 1px solid rgba(16, 185, 129, 0.2);
background: rgba(16, 185, 129, 0.08);
color: #059669;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 28px;
flex: 0 0 auto;
}
.order-module-dialog .view-hero {
display: flex;
gap: 16px;
padding: 20px 22px;
background: linear-gradient(135deg, #ecfdf5 0%, #ffffff 55%);
}
.order-module-dialog .view-hero-main h2 {
margin: 0 0 8px;
font-size: 18px;
font-weight: 700;
color: #1e293b;
}
.order-module-dialog .view-hero-tags {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
flex-wrap: wrap;
}
.order-module-dialog .view-hero-meta {
margin: 0;
font-size: 13px;
color: #64748b;
line-height: 1.5;
}
.order-module-dialog .view-hero-meta .dot {
margin: 0 6px;
opacity: 0.7;
}
.order-module-dialog .view-section {
padding: 18px 22px;
}
.order-module-dialog .view-section h3 {
margin: 0 0 14px;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.06em;
color: #64748b;
}
.view-kv-settlement-dd {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.view-kv-settlement-time {
font-size: 12px;
color: #64748b;
}
.order-module-dialog .view-kv-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 28px;
}
.order-module-dialog .view-kv {
display: grid;
grid-template-columns: 96px 1fr;
gap: 8px 12px;
font-size: 13px;
align-items: start;
}
.order-module-dialog .view-kv dt {
color: #64748b;
font-weight: 500;
}
.order-module-dialog .view-kv dd {
color: #1e293b;
word-break: break-word;
}
.order-module-dialog .view-kv.view-multiline {
grid-column: 1 / -1;
}
@media (max-width: 760px) {
.order-module-dialog .view-kv-grid {
grid-template-columns: 1fr;
}
.order-module-dialog .view-kv {
grid-template-columns: 1fr;
gap: 4px;
}
.order-module-dialog .view-kv.view-multiline {
grid-column: auto;
}
}
</style>