| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740 |
- <!-- 结算单详情:凭证缩略图/视频可播放 + 订单树形表(含商品行) -->
- <template>
- <div class="sheet-detail-panel">
- <div class="view-hero">
- <div class="view-hero-icon">
- <el-icon><Document /></el-icon>
- </div>
- <div class="view-hero-main">
- <h2>{{ detail.settlementNo || '-' }}</h2>
- <div class="view-hero-tags">
- <el-tag type="primary" size="small">{{ counterpartyLabel }}:{{ counterpartyName || '-' }}</el-tag>
- <el-tag type="success" size="small">订单数 {{ detail.orderCount || 0 }}</el-tag>
- </div>
- <p class="view-hero-meta">
- 结算金额:<span class="amount-text">¥{{ formatAmount(detail.totalAmount) }}</span>
- <span class="dot">·</span>
- 结算时间:{{ detail.settlementAt || '-' }}
- <span class="dot">·</span>
- 操作人:{{ detail.createName || '-' }}
- </p>
- </div>
- </div>
- <div class="view-section">
- <h3>基础信息</h3>
- <el-descriptions :column="2" border>
- <el-descriptions-item label="结算单号" :span="2">{{ detail.settlementNo }}</el-descriptions-item>
- <el-descriptions-item :label="counterpartyLabel">{{
- counterpartyName || '-'
- }}</el-descriptions-item>
- <el-descriptions-item label="订单数">{{ detail.orderCount }}</el-descriptions-item>
- <el-descriptions-item label="结算金额(元)">
- <span class="amount-text">¥{{ formatAmount(detail.totalAmount) }}</span>
- </el-descriptions-item>
- <el-descriptions-item label="结算时间">{{ detail.settlementAt || '-' }}</el-descriptions-item>
- <el-descriptions-item label="操作人">{{ detail.createName || '-' }}</el-descriptions-item>
- <el-descriptions-item label="备注" :span="2">{{ detail.remark || '-' }}</el-descriptions-item>
- </el-descriptions>
- </div>
- <div class="view-section">
- <div class="detail-block-title">结算凭证</div>
- <div v-if="voucherUrls.length" class="voucher-preview-grid">
- <div
- v-for="(url, i) in voucherUrls"
- :key="i"
- class="voucher-tile"
- @click="openVoucher(url)"
- >
- <template v-if="isVideoUrl(url)">
- <video :src="url" class="voucher-thumb" muted preload="metadata" />
- <div class="voucher-tile-play">
- <el-icon :size="28"><VideoPlay /></el-icon>
- </div>
- </template>
- <el-image v-else :src="url" fit="cover" class="voucher-thumb" />
- </div>
- </div>
- <el-empty v-else description="无凭证" :image-size="56" />
- </div>
- <div class="view-section">
- <div class="detail-block-title">包含订单</div>
- <div class="detail-orders-list list-body list-wrap">
- <el-table
- ref="ordersTableRef"
- :data="orders"
- row-key="id"
- size="small"
- stripe
- max-height="480"
- class="orders-table"
- >
- <el-table-column type="expand" width="48">
- <template #default="{ row: ord }">
- <div class="order-row-expand-wrap">
- <div
- v-if="Array.isArray(ord.children) && ord.children.length"
- class="settlement-row-expand-table-wrap sqxp-subtable-wrap"
- >
- <el-table :data="ord.children" stripe size="small" class="order-row-expand-table sqxp-subtable">
- <el-table-column label="商品" min-width="180">
- <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"
- :preview-src-list="[line.imageUrl]"
- preview-teleported
- />
- <span v-else class="expand-product-thumb placeholder">无图</span>
- </div>
- <div class="expand-product-text">
- <span class="expand-product-name">{{ line.productName || '—' }}</span>
- <span v-if="line.note" class="expand-product-note">备注:{{ line.note }}</span>
- </div>
- </div>
- </template>
- </el-table-column>
- <el-table-column label="计量" width="88" align="center">
- <template #default="{ row: line }">{{ formatMeasureValue(line.measureValue) }}</template>
- </el-table-column>
- <el-table-column prop="unitName" label="单位" width="70" align="center" />
- <el-table-column label="数量" width="88" align="right">
- <template #default="{ row: line }">{{ formatQty(line.quantity) }}</template>
- </el-table-column>
- <el-table-column label="供货单价(元)" width="110" align="right">
- <template #default="{ row: line }">{{ formatAmount(line.supplierUnitPrice) }}</template>
- </el-table-column>
- <el-table-column label="供货金额(元)" width="110" align="right">
- <template #default="{ row: line }">
- <span class="amount-text">{{ formatAmount(line.supplierAmount) }}</span>
- </template>
- </el-table-column>
- <el-table-column label="售价单价(元)" width="110" align="right">
- <template #default="{ row: line }">{{ formatAmount(line.sellingUnitPrice) }}</template>
- </el-table-column>
- <el-table-column label="售出金额(元)" width="110" align="right">
- <template #default="{ row: line }">
- <span class="amount-text">{{ formatAmount(line.sellingAmount) }}</span>
- </template>
- </el-table-column>
- </el-table>
- <div class="order-row-expand-summary sqxp-subtable-summary">
- <span
- >供货总金额(元):<span class="sqxp-subtable-summary__amount"
- >¥{{ formatAmount(orderSupplierTotal(ord)) }}</span
- ></span
- >
- <span
- >售出总金额(元):<span class="sqxp-subtable-summary__amount"
- >¥{{ formatAmount(orderSellingTotal(ord)) }}</span
- ></span
- >
- </div>
- </div>
- <el-empty v-else description="暂无商品明细" :image-size="72" />
- </div>
- </template>
- </el-table-column>
- <el-table-column label="订单编号" min-width="180">
- <template #default="{ row: ord }">
- <el-button link type="primary" @click="toggleOrderExpand(ord)">{{ ord.orderNo }}</el-button>
- </template>
- </el-table-column>
- <el-table-column prop="clientName" label="客户名称" min-width="150" show-overflow-tooltip />
- <el-table-column label="区域" min-width="110" show-overflow-tooltip>
- <template #default="{ row: ord }">{{ ord.regionName || '—' }}</template>
- </el-table-column>
- <el-table-column label="订单金额(元)" min-width="130" align="right">
- <template #default="{ row: ord }">
- <span class="amount-text">¥{{ formatAmount(orderTotalAmount(ord)) }}</span>
- </template>
- </el-table-column>
- <el-table-column label="供货总金额(元)" min-width="140" align="right">
- <template #default="{ row: ord }">
- <span class="amount-text">¥{{ formatAmount(orderSupplierTotal(ord)) }}</span>
- </template>
- </el-table-column>
- <el-table-column label="订单状态" min-width="120">
- <template #default="{ row: ord }">
- <el-tag :type="orderStatusTagType(resolveOrderStatus(ord))" size="small">
- {{ orderStatusLabel(resolveOrderStatus(ord)) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="配送状态" min-width="120">
- <template #default="{ row: ord }">
- <el-tag :type="deliveryStatusTagType(resolveDeliveryStatus(ord))" size="small">
- {{ deliveryStatusLabel(resolveDeliveryStatus(ord)) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="支付状态" min-width="120">
- <template #default="{ row: ord }">
- <el-tag :type="payStatusTagType(resolvePayStatus(ord))" size="small">
- {{ payStatusLabel(resolvePayStatus(ord)) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="支付方式" min-width="110" align="center">
- <template #default="{ row: ord }">
- <el-tag :type="resolvePaymentMethod(ord) === 'offline' ? 'warning' : 'primary'" size="small">
- {{ paymentMethodLabel(resolvePaymentMethod(ord)) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="下单时间" min-width="170">
- <template #default="{ row: ord }">{{ ord.orderCreateAt || '—' }}</template>
- </el-table-column>
- </el-table>
- </div>
- </div>
- <el-dialog v-model="imagePreviewVisible" title="图片预览" width="820px" destroy-on-close>
- <el-image
- v-if="previewImageUrl"
- :src="previewImageUrl"
- fit="contain"
- style="width: 100%; height: 520px"
- />
- </el-dialog>
- <el-dialog v-model="videoPreviewVisible" title="视频播放" width="820px" destroy-on-close>
- <video
- v-if="previewVideoUrl"
- :src="previewVideoUrl"
- controls
- autoplay
- style="width: 100%; max-height: 520px"
- />
- </el-dialog>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, ref } from 'vue'
- import { Document, VideoPlay } from '@element-plus/icons-vue'
- const ordersTableRef = ref()
- const props = withDefaults(
- defineProps<{
- detail: Record<string, any>
- counterpartyLabel?: string
- counterpartyName?: string
- }>(),
- {
- counterpartyLabel: '客户',
- counterpartyName: ''
- }
- )
- const VIDEO_URL_RE = /\.(mp4|webm|mov|m4v|avi|mkv)(\?|#|$)/i
- const voucherUrls = computed(() => {
- const v = props.detail?.voucherUrls
- return Array.isArray(v) ? v.filter(Boolean) : []
- })
- const treeItems = computed(() => {
- const items = props.detail?.items
- return Array.isArray(items) ? items : []
- })
- const orders = computed(() => treeItems.value.filter((x: any) => x && x.rowType === 'order'))
- const formatAmount = (val: number) => (Number(val || 0) || 0).toFixed(2)
- const formatQty = (val: number) => {
- const n = Number(val || 0)
- if (!Number.isFinite(n)) return '0'
- return Number.isInteger(n) ? String(n) : n.toFixed(4).replace(/\.?0+$/, '')
- }
- const formatMeasure = (row: any) => {
- const u = row.unitName || ''
- const m = row.measureValue
- if (m != null && m !== '') return `${m} · ${u || '—'}`
- return u || '—'
- }
- const formatMeasureValue = (v: unknown) => {
- if (v == null || v === '') return '-'
- const n = Number(v)
- if (Number.isNaN(n)) return '-'
- return n.toFixed(3).replace(/\.?0+$/, '')
- }
- const toggleOrderExpand = (ord: any) => {
- ordersTableRef.value?.toggleRowExpansion?.(ord)
- }
- const orderSupplierTotal = (orderRow: any) => {
- const children = Array.isArray(orderRow?.children) ? orderRow.children : []
- return children.reduce((acc: number, ln: any) => acc + Number(ln?.supplierAmount || 0), 0)
- }
- const orderSellingTotal = (orderRow: any) => {
- const children = Array.isArray(orderRow?.children) ? orderRow.children : []
- return children.reduce((acc: number, ln: any) => acc + Number(ln?.sellingAmount || 0), 0)
- }
- const isVideoUrl = (url: string) => VIDEO_URL_RE.test(url || '')
- const imagePreviewVisible = ref(false)
- const videoPreviewVisible = ref(false)
- const previewImageUrl = ref('')
- const previewVideoUrl = ref('')
- const openVoucher = (url: string) => {
- if (!url) return
- if (isVideoUrl(url)) {
- previewVideoUrl.value = url
- videoPreviewVisible.value = true
- } else {
- previewImageUrl.value = url
- imagePreviewVisible.value = true
- }
- }
- const ORDER_STATUS_LABEL: Record<string, string> = {
- pending_payment: '待支付',
- pending_review_cancel: '待审核取消',
- pending_delivery: '待分配配送',
- delivering: '配送中',
- partial_delivered: '部分送达',
- pending_confirm: '待确认收货',
- completed: '已完结',
- cancelled: '已取消'
- }
- const orderStatusLabel = (s: string) => ORDER_STATUS_LABEL[s] || s || '—'
- const resolveOrderStatus = (row: any) => {
- if (row?.cancelStatus === 'pending_review') return 'pending_review_cancel'
- return row?.orderStatus || row?.status || ''
- }
- const orderStatusTagType = (s: string) => {
- if (s === 'completed') return 'success'
- if (s === 'cancelled') return 'info'
- if (s === 'pending_payment') return 'warning'
- return 'primary'
- }
- const DELIVERY_STATUS_LABEL: Record<string, string> = {
- pending_dispatch: '待分配配送',
- pending_delivery: '待配送',
- delivering: '配送中',
- partial_delivered: '部分送达',
- delivered: '配送完成',
- pending: '待取货',
- picking: '取货中',
- completed: '已完成'
- }
- const resolveDeliveryStatus = (row: any) =>
- row?.deliveryStatus ||
- (['pending_payment', 'pending_dispatch', 'pending_delivery', 'pending_review_cancel'].includes(
- resolveOrderStatus(row)
- )
- ? 'pending_dispatch'
- : resolveOrderStatus(row) === 'delivering'
- ? 'delivering'
- : resolveOrderStatus(row) === 'partial_delivered'
- ? 'partial_delivered'
- : ['pending_confirm', 'completed'].includes(resolveOrderStatus(row))
- ? 'delivered'
- : 'pending_dispatch')
- const deliveryStatusLabel = (s: string) => DELIVERY_STATUS_LABEL[s] || s || '—'
- const deliveryStatusTagType = (s: string) => {
- if (s === 'delivered' || s === 'completed') return 'success'
- if (s === 'delivering') return 'info'
- if (s === 'partial_delivered' || s === 'pending_dispatch' || s === 'pending_delivery') return 'warning'
- if (s === 'picking') return 'primary'
- return 'info'
- }
- const resolvePayStatus = (row: any) =>
- row?.payStatus || (Number(row?.paidAmount || 0) > 0 ? 'paid' : 'pending_payment')
- const payStatusLabel = (s: string) => (s === 'paid' ? '已支付' : s === 'pending_payment' ? '待支付' : s || '—')
- const payStatusTagType = (s: string) => (s === 'paid' ? 'success' : s === 'pending_payment' ? 'warning' : 'info')
- const resolvePaymentMethod = (row: any) => String(row?.paymentMethod || '').toLowerCase()
- const paymentMethodLabel = (m: string) => {
- if (['online', 'wechat', 'wx'].includes(m)) return '线上支付'
- if (m === 'offline') return '线下支付'
- return m || '-'
- }
- const orderTotalAmount = (row: any) =>
- Number(row?.orderAmount ?? row?.totalAmount ?? row?.sellingTotalAmount ?? row?.sellingAmount ?? 0)
- const orderReceivableAmount = (row: any) => Number(row?.receivableAmount ?? orderTotalAmount(row))
- const paymentLabel = (m: string) => {
- if (m === 'offline') return '线下结算'
- if (m === 'online') return '线上支付'
- return m || '—'
- }
- </script>
- <style scoped lang="scss">
- .sheet-detail-panel {
- .view-hero {
- display: flex;
- gap: 16px;
- padding: 20px 22px;
- background: linear-gradient(135deg, #ecfdf5 0%, #ffffff 55%);
- border-bottom: 1px solid var(--mint-border, #d9e8e2);
- align-items: center;
- }
- .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;
- }
- .view-hero-main h2 {
- margin: 0 0 8px;
- font-size: 18px;
- font-weight: 700;
- color: var(--mint-text, #1e293b);
- }
- .view-hero-tags {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- flex-wrap: wrap;
- }
- .view-hero-meta {
- margin: 0;
- font-size: 13px;
- color: var(--mint-text-muted, #64748b);
- line-height: 1.5;
- }
- .dot {
- margin: 0 6px;
- opacity: 0.7;
- }
- .view-section {
- padding: 18px 22px;
- border-bottom: 1px solid var(--mint-border, #d9e8e2);
- }
- .view-section:last-child {
- border-bottom: none;
- }
- .view-section h3 {
- margin: 0 0 14px;
- font-size: 13px;
- font-weight: 700;
- letter-spacing: 0.06em;
- color: var(--mint-text-muted, #64748b);
- }
- .amount-text {
- color: #dc2626;
- font-weight: 700;
- }
- .detail-block-title {
- margin: 18px 0 12px;
- font-weight: 600;
- font-size: 15px;
- color: var(--mint-text, #1e293b);
- padding-left: 12px;
- border-left: 4px solid #10b981;
- }
- :deep(.el-descriptions) {
- --el-descriptions-border-color: var(--mint-border, #d9e8e2);
- border-radius: 10px;
- overflow: hidden;
- background: #fff;
- }
- :deep(.el-descriptions__label) {
- color: var(--mint-text-muted, #64748b);
- font-size: 12px;
- font-weight: 600;
- background: #f8fafc;
- }
- :deep(.el-descriptions__content) {
- color: var(--mint-text, #1e293b);
- font-weight: 500;
- }
- .voucher-preview-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 12px;
- }
- .voucher-tile {
- position: relative;
- width: 120px;
- height: 100px;
- border-radius: 10px;
- overflow: hidden;
- cursor: pointer;
- border: 1px solid var(--mint-border, #d9e8e2);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
- background: #fff;
- &:hover .voucher-tile-play {
- opacity: 1;
- }
- }
- .voucher-thumb {
- width: 100%;
- height: 100%;
- object-fit: cover;
- display: block;
- }
- .voucher-tile-play {
- position: absolute;
- inset: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(0, 0, 0, 0.35);
- color: #fff;
- opacity: 0.8;
- transition: opacity 0.2s;
- }
- .detail-orders-list {
- margin-top: 4px;
- border: 1px solid var(--mint-border, #d9e8e2);
- border-radius: 10px;
- overflow: hidden;
- background: #fff;
- }
- .orders-table {
- margin-top: 0;
- }
- .orders-table :deep(th.el-table__cell > .cell),
- .orders-table :deep(td.el-table__cell > .cell) {
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .orders-table :deep(td.el-table__cell.el-table__expand-column > .cell) {
- font-size: 0;
- line-height: 0;
- overflow: hidden;
- }
- .orders-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: #fff;
- font-size: 0;
- line-height: 0;
- }
- .orders-table :deep(.el-table__expand-icon:hover) {
- background: var(--mint-primary-bg, #ecfdf5);
- border-color: #10b981;
- color: var(--mint-primary-dark, #059669);
- }
- .orders-table :deep(.el-table__expand-icon--expanded) {
- background: var(--mint-primary-bg, #ecfdf5);
- border-color: #10b981;
- color: var(--mint-primary-dark, #059669);
- }
- .orders-table :deep(.el-table__expand-icon svg) {
- width: 14px;
- height: 14px;
- transition: transform 0.15s ease;
- }
- .orders-table :deep(.el-table__expand-icon--expanded svg) {
- transform: rotate(180deg);
- }
- .order-row-expand-wrap {
- padding: 0;
- background: #f8fafc;
- border-radius: 0;
- }
- :deep(td.el-table__expanded-cell) {
- padding: 0 !important;
- text-align: left !important;
- }
- :deep(td.el-table__expanded-cell > .cell) {
- display: block !important;
- padding: 0 !important;
- text-align: left !important;
- }
- .order-row-expand-table {
- width: 100%;
- max-width: 100%;
- border: 1px solid var(--mint-border, #d9e8e2);
- border-radius: 8px;
- overflow: hidden;
- background: #fff;
- :deep(.el-table__body-wrapper) {
- overflow-x: auto;
- }
- :deep(th.el-table__cell) {
- background: #e2e8f0 !important;
- color: var(--mint-text-muted, #64748b);
- font-size: 11px;
- font-weight: 600;
- }
- :deep(.el-table__cell) {
- padding: 8px 10px;
- }
- }
- .settlement-row-expand-table-wrap {
- display: block;
- width: 100%;
- max-width: 100%;
- overflow: hidden;
- border-radius: 0;
- text-align: left;
- position: relative;
- background: #f8fafc;
- padding: 12px 384px 14px 16px;
- :deep(.el-table__inner-wrapper),
- :deep(.el-table__header-wrapper),
- :deep(.el-table__body-wrapper) {
- width: 100%;
- }
- }
- .settlement-row-expand-table-wrap::before {
- content: '';
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- width: 3px;
- background: #10b981;
- }
- .order-row-expand-summary {
- margin-top: 10px;
- display: flex;
- gap: 20px;
- justify-content: flex-end;
- color: #606266;
- font-size: 13px;
- }
- .order-row-head {
- display: flex;
- align-items: center;
- gap: 8px;
- flex-wrap: wrap;
- margin-bottom: 4px;
- }
- .order-row-meta {
- font-size: 12px;
- line-height: 1.5;
- color: #606266;
- &.muted {
- color: #909399;
- }
- }
- .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-text {
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-width: 0;
- }
- .expand-product-name {
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- font-size: 13px;
- }
- .expand-product-note {
- font-size: 12px;
- color: #909399;
- min-width: 0;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .cell-muted {
- color: #c0c4cc;
- }
- .ml-1 {
- margin-left: 4px;
- }
- }
- </style>
|