Files
integral-shop/backend-adminend/src/views/integral-external/wa-order/index.vue

748 lines
26 KiB
Vue
Raw Normal View History

<template>
<div class="divBox relative">
<!-- 顶部搜索区 -->
<el-card class="box-card">
<el-form size="small" inline label-width="100px">
<el-form-item label="订单状态:">
<el-radio-group v-model="uiStatus" type="button" size="mini" @change="onStatusChange">
<el-radio-button label="all">全部</el-radio-button>
<el-radio-button label="unPaid">待付款</el-radio-button>
<el-radio-button label="paid">已支付</el-radio-button>
<el-radio-button label="complete">交易完成</el-radio-button>
<el-radio-button label="cancel">已取消</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="时间字段:">
<el-select
v-model="timeField"
placeholder="选择时间维度"
style="width: 140px"
size="mini"
@change="seachList"
>
<el-option label="抢购时间" value="buyTime" />
<el-option label="支付时间" value="payTime" />
<el-option label="完成时间" value="confirmTime" />
<el-option label="创建时间" value="createdAt" />
</el-select>
</el-form-item>
<el-form-item label="日期范围:">
<el-date-picker
v-model="timeVal"
value-format="yyyy-MM-dd"
format="yyyy-MM-dd"
size="mini"
type="daterange"
placeholder="自定义时间"
style="width: 220px"
@change="onTimeChange"
/>
</el-form-item>
<el-form-item label="订单号:">
<el-input
v-model="tableFrom.orderSn"
placeholder="请输入订单号"
clearable
class="selWidth"
size="mini"
@keyup.enter.native="seachList"
/>
</el-form-item>
<el-form-item label="买家ID">
<el-input
v-model.number="tableFrom.buyerId"
placeholder="买家用户ID"
clearable
class="selWidth"
size="mini"
/>
</el-form-item>
<el-form-item label="卖家ID">
<el-input
v-model.number="tableFrom.sellerId"
placeholder="卖家ID0=平台)"
clearable
class="selWidth"
size="mini"
/>
</el-form-item>
<el-form-item label="是否转拍:">
<el-select
v-model="tableFrom.isResell"
placeholder="全部"
clearable
style="width: 100px"
size="mini"
@change="seachList"
>
<el-option :value="1" label="是" />
<el-option :value="0" label="否" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="seachList">查询</el-button>
<el-button icon="el-icon-refresh-left" size="mini" @click="resetHandler">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<!-- 操作按钮区 -->
<el-card class="box-card mt10">
<div class="header-actions">
<el-popover
placement="bottom-end"
width="300"
trigger="click"
v-model="columnPopoverVisible"
>
<div class="column-setting">
<div class="column-setting-actions">
<el-button size="mini" type="text" @click="selectAllColumns">全选</el-button>
<el-button size="mini" type="text" @click="invertColumns">反选</el-button>
<el-button size="mini" type="text" @click="resetColumns">恢复默认</el-button>
</div>
<el-checkbox-group v-model="visibleColumns" class="column-checkbox-group">
<el-checkbox
v-for="col in columnsConfig"
:key="col.prop"
:label="col.prop"
:disabled="col.fixed"
>{{ col.label }}</el-checkbox>
</el-checkbox-group>
</div>
<el-button slot="reference" size="mini" icon="el-icon-setting">列设置</el-button>
</el-popover>
<el-button size="mini" icon="el-icon-refresh" @click="getList" />
</div>
<el-table
v-loading="listLoading"
:data="tableData.data"
size="mini"
class="table"
highlight-current-row
:header-cell-style="{ fontWeight: 'bold' }"
>
<template v-for="col in columnsConfig">
<el-table-column
v-if="col.prop !== 'actions' && visibleColumns.includes(col.prop)"
:key="col.prop"
:label="col.label"
:min-width="col.width"
:show-overflow-tooltip="col.tooltip"
:align="col.align || 'left'"
>
<template slot-scope="scope">
<!-- 商品图片单元格使用 el-image其他文本走 formatCell -->
<el-image
v-if="col.prop === 'merchandiseImage' && scope.row.merchandiseImage"
:src="scope.row.merchandiseImage"
:preview-src-list="[scope.row.merchandiseImage]"
style="width:48px;height:48px;border-radius:4px"
fit="cover"
/>
<span v-else-if="col.prop === 'merchandiseImage'">-</span>
<span v-else v-html="formatCell(col.prop, scope.row)" />
</template>
</el-table-column>
</template>
<el-table-column label="操作" min-width="120" fixed="right" align="center">
<template slot-scope="scope">
<el-button type="text" size="small" @click="openDetail(scope.row.id)">详情</el-button>
</template>
</el-table-column>
</el-table>
<div class="block mt20">
<el-pagination
:page-sizes="[15, 30, 45, 60]"
:page-size="tableFrom.limit"
:current-page="tableFrom.page"
layout="total, sizes, prev, pager, next, jumper"
:total="tableData.total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
<!-- 详情抽屉 -->
<el-drawer
:visible.sync="drawerVisible"
direction="rtl"
size="680px"
:destroy-on-close="true"
:with-header="false"
append-to-body
custom-class="wa-order-drawer"
@close="onDrawerClose"
>
<div v-loading="detailLoading" class="drawer-body">
<!-- 顶部摘要订单号 + 状态徽标 + 关闭 -->
<div class="drawer-header">
<div class="header-left">
<div class="order-sn-row">
<span class="order-sn-label">订单号</span>
<span class="order-sn">{{ detail ? detail.orderSn : '-' }}</span>
<el-button
v-if="detail"
size="mini"
type="text"
icon="el-icon-document-copy"
@click="copyOrderSn"
>复制</el-button>
</div>
<div class="tag-row">
<el-tag v-if="detail" :type="statusTagType(detail)" size="small">{{ statusLabel(detail) }}</el-tag>
<el-tag v-if="detail && detail.isResell === 1" type="warning" size="small" effect="plain">转拍</el-tag>
<el-tag v-if="detail && detail.isCancel === 1" type="danger" size="small" effect="plain">已取消</el-tag>
</div>
</div>
<el-button
class="close-btn"
icon="el-icon-close"
type="text"
@click="drawerVisible = false"
/>
</div>
<div v-if="detail" class="drawer-content">
<!-- 时间链路 -->
<el-card shadow="never" class="block-card">
<div slot="header" class="block-title">订单时间链路</div>
<el-steps :active="stepActive(detail)" finish-status="success" align-center>
<el-step title="抢购下单" :description="detail.buyTime || '-'" />
<el-step
v-if="detail.isResell === 1"
title="转拍生成"
:description="detail.createdAt || '-'"
/>
<el-step title="支付" :description="detail.payTime || '-'" />
<el-step title="完成" :description="detail.confirmTime || '-'" />
</el-steps>
</el-card>
<!-- 订单基础 -->
<el-card shadow="never" class="block-card">
<div slot="header" class="block-title">订单基础</div>
<el-descriptions :column="2" :colon="false" size="small" class="info-desc">
<el-descriptions-item label="订单ID">{{ detail.id }}</el-descriptions-item>
<el-descriptions-item label="订单金额">
<span class="price">{{ detail.totalMoney }}</span>
</el-descriptions-item>
<el-descriptions-item label="商品ID">{{ detail.merchandiseId || '-' }}</el-descriptions-item>
<el-descriptions-item label="商品名称">{{ detail.merchandiseTitle || '-' }}</el-descriptions-item>
<el-descriptions-item label="是否显示">{{ detail.isShow === 1 ? '显示' : '隐藏' }}</el-descriptions-item>
<el-descriptions-item label="原订单ID">{{ detail.oldId || '-' }}</el-descriptions-item>
<el-descriptions-item label="商品图片" :span="2">
<el-image
v-if="detail.merchandiseImage"
:src="detail.merchandiseImage"
:preview-src-list="[detail.merchandiseImage]"
style="width:80px;height:80px;border-radius:4px"
fit="cover"
/>
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 买家 -->
<el-card shadow="never" class="block-card">
<div slot="header" class="block-title">买家信息</div>
<el-descriptions :column="2" :colon="false" size="small" class="info-desc">
<el-descriptions-item label="买家ID">{{ detail.buyerId || '-' }}</el-descriptions-item>
<el-descriptions-item label="买家昵称">{{ detail.buyerName || '-' }}</el-descriptions-item>
<el-descriptions-item label="收货人">{{ detail.consignee || '-' }}</el-descriptions-item>
<el-descriptions-item label="收货电话">{{ detail.phone || '-' }}</el-descriptions-item>
<el-descriptions-item label="收货地区" :span="2">{{ joinArea(detail) }}</el-descriptions-item>
<el-descriptions-item label="详细地址" :span="2">{{ detail.address || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 卖家 -->
<el-card shadow="never" class="block-card">
<div slot="header" class="block-title">卖家信息</div>
<el-descriptions :column="2" :colon="false" size="small" class="info-desc">
<el-descriptions-item label="卖家ID">
{{ detail.sellerId === 0 ? '0平台' : detail.sellerId }}
</el-descriptions-item>
<el-descriptions-item label="卖家昵称">
{{ detail.sellerId === 0 ? '平台' : (detail.sellerName || '-') }}
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 支付与日志 -->
<el-card shadow="never" class="block-card">
<div slot="header" class="block-title">支付与日志</div>
<el-descriptions :column="2" :colon="false" size="small" class="info-desc">
<el-descriptions-item label="抢购时间">{{ detail.buyTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="支付时间">{{ detail.payTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="完成时间">{{ detail.confirmTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detail.createdAt || '-' }}</el-descriptions-item>
<el-descriptions-item label="下单IP">{{ detail.buyIp || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="detail.isCancel === 1" label="取消IP">
{{ detail.cancelIp || '-' }}
</el-descriptions-item>
<el-descriptions-item label="支付凭证" :span="2">
<el-image
v-if="detail.payImg"
:src="detail.payImg"
:preview-src-list="[detail.payImg]"
style="width:80px;height:80px;border-radius:4px"
fit="cover"
/>
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
</el-card>
</div>
<div class="drawer-footer">
<el-button @click="drawerVisible = false"> </el-button>
</div>
</div>
</el-drawer>
</div>
</template>
<script>
import {
getExternalWaOrderList,
getExternalWaOrderInfo,
} from '@/api/integralExternal';
const STORAGE_KEY = 'waOrderColumns:external';
const COLUMN_DEFS = [
{ prop: 'id', label: '订单ID', width: 80, defaultVisible: true, fixed: true, tooltip: false, align: 'left' },
{ prop: 'orderSn', label: '订单号', width: 210, defaultVisible: true, fixed: true, tooltip: true },
{ prop: 'merchandiseId', label: '商品ID', width: 90, defaultVisible: true, align: 'left' },
{ prop: 'merchandiseTitle', label: '商品名称', width: 200, defaultVisible: true, tooltip: true },
{ prop: 'merchandiseImage', label: '商品图片', width: 90, defaultVisible: false, align: 'center' },
{ prop: 'totalMoney', label: '订单金额', width: 110, defaultVisible: true, align: 'right' },
{ prop: 'statusStr', label: '订单状态', width: 110, defaultVisible: true, align: 'center' },
{ prop: 'isResell', label: '是否转拍', width: 90, defaultVisible: false, align: 'center' },
{ prop: 'buyerName', label: '买家昵称', width: 120, defaultVisible: true, tooltip: true },
{ prop: 'buyerId', label: '买家ID', width: 90, defaultVisible: false },
{ prop: 'sellerName', label: '卖家昵称', width: 120, defaultVisible: true, tooltip: true },
{ prop: 'sellerId', label: '卖家ID', width: 90, defaultVisible: false },
{ prop: 'consignee', label: '收货人', width: 100, defaultVisible: false },
{ prop: 'phone', label: '收货电话', width: 130, defaultVisible: false },
{ prop: 'areaText', label: '收货地区', width: 180, defaultVisible: false, tooltip: true },
{ prop: 'address', label: '详细地址', width: 220, defaultVisible: false, tooltip: true },
{ prop: 'buyTime', label: '抢购时间', width: 160, defaultVisible: true },
{ prop: 'createdAt', label: '转拍/创建时间', width: 160, defaultVisible: false },
{ prop: 'payTime', label: '支付时间', width: 160, defaultVisible: true },
{ prop: 'confirmTime', label: '完成时间', width: 160, defaultVisible: true },
{ prop: 'updatedAt', label: '更新时间', width: 160, defaultVisible: false },
{ prop: 'buyIp', label: '下单IP', width: 130, defaultVisible: false },
{ prop: 'cancelIp', label: '取消IP', width: 130, defaultVisible: false },
{ prop: 'isShow', label: '显示', width: 80, defaultVisible: false, align: 'center' },
];
export default {
name: 'IntegralExternalWaOrder',
data() {
return {
listLoading: false,
tableData: { data: [], total: 0 },
tableFrom: {
orderSn: '',
buyerId: '',
sellerId: '',
status: null,
isCancel: null,
isResell: null,
buyTimeStart: '',
buyTimeEnd: '',
confirmTimeStart: '',
confirmTimeEnd: '',
page: 1,
limit: 15,
},
uiStatus: 'all',
timeField: 'buyTime',
timeVal: [],
columnsConfig: COLUMN_DEFS.concat([{ prop: 'actions', label: '操作', fixed: true }]),
visibleColumns: this.loadColumns(),
columnPopoverVisible: false,
drawerVisible: false,
detail: null,
detailLoading: false,
};
},
mounted() {
// 支持 ?detailId=xxx 直接打开抽屉
if (this.$route && this.$route.query && this.$route.query.detailId) {
this.openDetail(this.$route.query.detailId);
}
this.getList();
},
methods: {
loadColumns() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
const arr = JSON.parse(raw);
if (Array.isArray(arr) && arr.length) {
const validProps = COLUMN_DEFS.map((c) => c.prop);
// 1) 与最新字段定义做交集2) 锁定列必须存在3) 新增的 defaultVisible 列自动并入
const next = arr.filter((p) => validProps.includes(p));
COLUMN_DEFS.forEach((c) => {
if ((c.fixed || c.defaultVisible) && !next.includes(c.prop)) {
next.push(c.prop);
}
});
return next;
}
}
} catch (e) { /* ignore */ }
return COLUMN_DEFS.filter((c) => c.defaultVisible || c.fixed).map((c) => c.prop);
},
persistColumns() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.visibleColumns));
} catch (e) { /* ignore */ }
},
selectAllColumns() {
this.visibleColumns = COLUMN_DEFS.map((c) => c.prop);
this.persistColumns();
},
invertColumns() {
const all = COLUMN_DEFS.map((c) => c.prop);
const next = all.filter((p) => !this.visibleColumns.includes(p));
// 锁定列必须保留
COLUMN_DEFS.filter((c) => c.fixed).forEach((c) => {
if (!next.includes(c.prop)) next.push(c.prop);
});
this.visibleColumns = next;
this.persistColumns();
},
resetColumns() {
this.visibleColumns = COLUMN_DEFS.filter((c) => c.defaultVisible || c.fixed).map((c) => c.prop);
this.persistColumns();
},
onStatusChange() {
this.applyUiStatus();
this.seachList();
},
applyUiStatus() {
// UI 标签 → status / isCancel 组合
switch (this.uiStatus) {
case 'unPaid':
this.tableFrom.status = 0;
this.tableFrom.isCancel = 0;
break;
case 'paid':
this.tableFrom.status = 1;
this.tableFrom.isCancel = 0;
break;
case 'complete':
this.tableFrom.status = 2;
this.tableFrom.isCancel = 0;
break;
case 'cancel':
this.tableFrom.status = null;
this.tableFrom.isCancel = 1;
break;
default:
this.tableFrom.status = null;
this.tableFrom.isCancel = null;
}
},
onTimeChange(val) {
// 清掉所有时间字段
this.tableFrom.buyTimeStart = '';
this.tableFrom.buyTimeEnd = '';
this.tableFrom.confirmTimeStart = '';
this.tableFrom.confirmTimeEnd = '';
if (val && val.length === 2) {
const startKey = `${this.timeField}Start`;
const endKey = `${this.timeField}End`;
// WaOrderSearchRequest 仅支持 buyTime / confirmTime其余维度回退到 buyTime
if (startKey in this.tableFrom) {
this.tableFrom[startKey] = `${val[0]} 00:00:00`;
this.tableFrom[endKey] = `${val[1]} 23:59:59`;
} else {
this.tableFrom.buyTimeStart = `${val[0]} 00:00:00`;
this.tableFrom.buyTimeEnd = `${val[1]} 23:59:59`;
}
}
this.seachList();
},
seachList() {
this.tableFrom.page = 1;
this.getList();
},
resetHandler() {
this.tableFrom = {
orderSn: '', buyerId: '', sellerId: '',
status: null, isCancel: null, isResell: null,
buyTimeStart: '', buyTimeEnd: '', confirmTimeStart: '', confirmTimeEnd: '',
page: 1, limit: 15,
};
this.uiStatus = 'all';
this.timeField = 'buyTime';
this.timeVal = [];
this.getList();
},
getList() {
this.listLoading = true;
const params = { ...this.tableFrom };
// null/空字段不发
Object.keys(params).forEach((k) => {
if (params[k] === '' || params[k] === null) delete params[k];
});
getExternalWaOrderList(params)
.then((res) => {
this.tableData.data = res.list || [];
this.tableData.total = res.total || 0;
})
.catch(() => {})
.finally(() => {
this.listLoading = false;
});
},
handleSizeChange(val) {
this.tableFrom.limit = val;
this.getList();
},
handleCurrentChange(val) {
this.tableFrom.page = val;
this.getList();
},
formatCell(prop, row) {
if (!row) return '-';
switch (prop) {
case 'totalMoney':
return row.totalMoney != null ? `${row.totalMoney}` : '-';
case 'statusStr': {
const status = this.statusLabel(row);
const type = this.statusTagType(row);
return `<span class="el-tag el-tag--${type} el-tag--mini">${status}</span>`;
}
case 'isResell':
return row.isResell === 1
? '<span style="color:#E6A23C">是</span>'
: '否';
case 'sellerId':
return row.sellerId === 0 ? '0平台' : row.sellerId;
case 'sellerName':
return row.sellerId === 0 ? '平台' : (row.sellerName || '-');
case 'areaText':
return this.joinArea(row);
case 'isShow':
return row.isShow === 1 ? '是' : '否';
case 'merchandiseTitle':
return row.merchandiseTitle || row.productName || '-';
default: {
const v = row[prop];
return v == null || v === '' ? '-' : v;
}
}
},
statusLabel(row) {
if (!row) return '-';
if (row.isCancel === 1) return '已取消';
if (row.isResell === 1 && row.status === 0) return '转拍中';
switch (row.status) {
case 0: return '待付款';
case 1: return '已支付';
case 2: return '交易完成';
default: return '-';
}
},
statusTagType(row) {
if (!row) return 'info';
if (row.isCancel === 1) return 'info';
switch (row.status) {
case 0: return 'warning';
case 1: return 'primary';
case 2: return 'success';
default: return 'info';
}
},
stepActive(row) {
if (!row) return 0;
if (row.confirmTime) return row.isResell === 1 ? 4 : 3;
if (row.payTime) return row.isResell === 1 ? 3 : 2;
if (row.isResell === 1 && row.createdAt) return 2;
if (row.buyTime) return 1;
return 0;
},
joinArea(row) {
if (!row) return '-';
const parts = [row.province, row.city, row.area].filter(Boolean);
return parts.length ? parts.join(' / ') : '-';
},
openDetail(id) {
if (!id) return;
this.drawerVisible = true;
this.detailLoading = true;
this.detail = null;
getExternalWaOrderInfo(id)
.then((res) => {
this.detail = res || null;
})
.catch(() => {
this.$message.error('订单详情加载失败');
this.drawerVisible = false;
})
.finally(() => {
this.detailLoading = false;
});
},
onDrawerClose() {
this.detail = null;
// 清掉 URL 上的 detailId
if (this.$route && this.$route.query && this.$route.query.detailId) {
const q = { ...this.$route.query };
delete q.detailId;
this.$router.replace({ path: this.$route.path, query: q });
}
},
copyOrderSn() {
if (!this.detail || !this.detail.orderSn) return;
const ta = document.createElement('textarea');
ta.value = this.detail.orderSn;
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); this.$message.success('已复制订单号'); }
catch (e) { this.$message.error('复制失败'); }
document.body.removeChild(ta);
},
},
watch: {
visibleColumns: {
deep: true,
handler() { this.persistColumns(); },
},
},
};
</script>
<style scoped lang="scss">
.mt10 { margin-top: 10px; }
.mt20 { margin-top: 20px; }
.selWidth { width: 180px; }
.header-actions {
text-align: right;
margin-bottom: 8px;
}
.column-setting {
.column-setting-actions {
margin-bottom: 8px;
border-bottom: 1px dashed #ebeef5;
padding-bottom: 4px;
}
}
.column-checkbox-group {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px 0;
}
.block { text-align: right; }
/* ===== 抽屉详情样式 ===== */
.drawer-body {
display: flex;
flex-direction: column;
height: 100%;
}
.drawer-header {
flex: 0 0 auto;
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px 20px;
background: #f5f7fa;
border-bottom: 1px solid #ebeef5;
.header-left { flex: 1; min-width: 0; }
.order-sn-row {
display: flex;
align-items: center;
gap: 8px;
.order-sn-label {
color: #909399;
font-size: 12px;
}
.order-sn {
font-size: 16px;
font-weight: 600;
color: #303133;
word-break: break-all;
}
}
.tag-row {
margin-top: 8px;
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.close-btn {
font-size: 18px;
color: #909399;
margin-left: 12px;
}
}
.drawer-content {
flex: 1 1 auto;
overflow-y: auto;
padding: 12px 16px;
}
.drawer-footer {
flex: 0 0 auto;
text-align: right;
padding: 12px 20px;
border-top: 1px solid #ebeef5;
background: #fafafa;
}
.block-card {
margin-bottom: 12px;
border: 1px solid #ebeef5;
::v-deep .el-card__header {
padding: 10px 14px;
background: #fafbfc;
}
::v-deep .el-card__body {
padding: 12px 14px;
}
}
.block-title {
font-size: 14px;
font-weight: 600;
color: #303133;
position: relative;
padding-left: 8px;
&::before {
content: '';
position: absolute;
left: 0; top: 3px;
width: 3px; height: 14px;
background: #409EFF;
border-radius: 2px;
}
}
.info-desc {
::v-deep .el-descriptions__label {
color: #909399;
font-weight: normal;
width: 90px;
}
::v-deep .el-descriptions__content {
color: #303133;
}
}
.price {
color: #f56c6c;
font-weight: 600;
}
::v-deep .wa-order-drawer .el-drawer__body {
padding: 0;
overflow: hidden;
}
</style>