Files
cashier-web/src/views/order/index/detail.vue
2026-07-17 14:31:37 +08:00

744 lines
23 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 style="padding: 20px 0;width: 100%;flex: 1;">
<div class="header">
<div class="title">
<div class="back">
<el-icon :size="20" @click="router.back()">
<Back />
</el-icon>
</div>
订单详情
</div>
</div>
<div class="order_detail_container">
<div class="order_top">
<!-- <div class="info_content">
<div class="item">
<div class="label">会员信息</div>
<div class="row">
<div>会员昵称-{{ user.nickName || "" }}</div>
<div>联系电话-{{ user.phone || "" }}</div>
</div>
</div>
</div> -->
<div class="table">
<div class="item div1">
<el-text type="info">台桌信息</el-text>
<el-text size="large">{{ detail.tableName || '无台桌' }}</el-text>
<el-tag :type="detail.status == 'closed' ? 'success' : 'primary'" disable-transitions size="large"
style="margin-left: 14px;">
{{ statusFilter(detail.status) }}
</el-tag>
</div>
<div class="item div2">
<el-text type="info">订单类型</el-text>
<el-text>{{ sendTypeFilter(detail.dineMode) }}</el-text>
</div>
<div class="item div3">
<el-text type="info">下单方式</el-text>
<el-text>{{ platformTypeFilter(detail.platformType) }}</el-text>
</div>
<div class="item div4">
<el-text type="info">订单编号</el-text>
<el-text>{{ detail.orderNo }}</el-text>
</div>
<div class="item div5">
<el-text type="info">支付时间</el-text>
<el-text>{{ timeFilter(detail.paidTime) }}</el-text>
</div>
<div class="item div6">
<el-text type="info">下单时间</el-text>
<el-text>{{ timeFilter(detail.createTime) }}</el-text>
</div>
</div>
<!-- <div class="table">
<div class="item">
<div class="t">订单编号</div>
<div class="b">
{{ detail.orderNo }}
</div>
</div>
<div class="item">
<div class="t">下单时间</div>
<div class="b">{{ timeFilter(detail.createTime) }}</div>
</div>
<div class="item">
<div class="t">支付时间</div>
<div class="b">
{{ timeFilter(detail.paidTime) }}
</div>
</div>
</div> -->
</div>
<div class="info_content">
<div class="item">
<div class="title">订单金额</div>
<div class="row">
<div class="row_col">
<el-text type="info">实收金额</el-text>
<el-text size="large">{{ detail.payAmount }}</el-text>
<el-button v-if="detail.status != 'unpaid' && detail.refundAmount < detail.payAmount" size="small"
type="danger" class="u-m-l-10" @click="tuikuan()">
<span>退款</span>
</el-button>
</div>
<div class="row_col">
<el-text type="info">支付方式</el-text>
<el-text>{{ returnPayType(detail.payType) || '-' }}</el-text>
</div>
</div>
<div class="row">
<div class="row_col">
<el-text type="info">订单原价</el-text>
<el-text>{{ detail.originAmount }}</el-text>
</div>
<div class="row_col">
<el-text type="info">打包费</el-text>
<el-text>{{ detail.packFee || "-" }}</el-text>
</div>
<div class="row_col">
<el-text type="info">餐位费</el-text>
<el-text>{{ detail.seatAmount || "-" }}</el-text>
</div>
<div class="row_col">
<el-text type="info">商品券抵扣</el-text>
<el-text>{{ detail.productCouponDiscountAmount || '-' }}</el-text>
</div>
<div class="row_col">
<el-text type="info">优惠券抵扣</el-text>
<el-text>{{ detail.otherCouponDiscountAmount || '-' }}</el-text>
</div>
<div class="row_col"></div>
</div>
<div class="row">
<div class="row_col">
<el-text type="info">满减活动</el-text>
<el-text>{{ detail.discountActAmount || "-" }}</el-text>
</div>
<div class="row_col">
<el-text type="info">新客立减</el-text>
<el-text>{{ detail.newCustomerDiscountAmount || "-" }}</el-text>
</div>
<div class="row_col">
<el-text type="info">退单金额</el-text>
<el-text>{{ detail.refundAmount || "-" }}</el-text>
</div>
<div class="row_col">
<el-text type="info">整单改价</el-text>
<el-text>{{ detail.discountAmount || '-' }}</el-text>
</div>
<div class="row_col">
<el-text type="info">积分抵扣</el-text>
<el-text>{{ detail.pointsDiscountAmount || '-' }}</el-text>
</div>
<div class="row_col">
<el-text type="info">抹零</el-text>
<el-text>{{ detail.roundAmount || '-' }}</el-text>
</div>
</div>
<!-- <div class="row">
<div>满减活动{{ detail.discountActAmount || "-" }}</div>
<div>新客立减{{ detail.newCustomerDiscountAmount || "-" }}</div>
<div>会员折扣{{ detail.vipDiscountAmount || "-" }}</div>
</div>
<div class="row">
<div>退单金额{{ detail.refundAmount }}</div>
<div>整单改价{{ detail.discountAmount }}</div>
<div>积分抵扣{{ detail.pointsDiscountAmount }}</div>
<div>抹零{{ detail.roundAmount }}</div>
<div>
实收金额
<span style="color: red">{{ detail.payAmount }}</span>
</div>
</div> -->
<!-- <div class="row">
<div>支付方式{{ returnPayType(detail.payType) }}</div>
<div>
退款金额{{ detail.refundAmount }}
</div>
<div class="color-red">
<span v-if="detail.refundType">
退款方式{{ detail.refundType === "cash" ? "现金退款" : "原路退回" }}
</span>
</div>
</div> -->
</div>
<div class="item" v-if="detail.refundType">
<div class="title">退款详情</div>
<div class="row">
<div class="row_col">
<el-text type="info">退款金额</el-text>
<el-text type="danger">{{ detail.refundAmount }}</el-text>
</div>
<div class="row_col">
<el-text type="info">退款方式</el-text>
<el-text type="danger">{{ detail.refundType === "cash" ? "现金退款" : "原路退回" }}</el-text>
</div>
<div class="row_col">
<el-text type="info">退款原因</el-text>
<el-text type="danger">{{ detail.refundRemark }}</el-text>
</div>
</div>
</div>
</div>
<div class="info_content" v-if="user.id">
<div class="title">用户信息</div>
<div class="user_info_wrap">
<div class="user_info_wrap_item" style="width: 60px;flex: none;">
<el-avatar :size="40" :src="user.headImg">{{ user.nickName || '-' }}</el-avatar>
</div>
<div class="user_info_wrap_item">
<el-text type="info">用户名称</el-text>
<el-text>{{ user.nickName || '-' }}</el-text>
</div>
<div class="user_info_wrap_item">
<el-text type="info">ID</el-text>
<el-text>{{ user.id || '-' }}</el-text>
</div>
<div class="user_info_wrap_item">
<el-text type="info">当前余额</el-text>
<el-text>{{ user.amount || '-' }}</el-text>
</div>
<div class="user_info_wrap_item">
<el-text type="info">会员</el-text>
<el-text>{{ user.isVip ? user.memberLevelName : '暂未成为会员' }}</el-text>
</div>
<div class="user_info_wrap_item">
<el-text type="info">消费累计</el-text>
<el-text>{{ user.consumeAmount || '-' }}</el-text>
</div>
<div class="user_info_wrap_item">
<el-text type="info">注册时间</el-text>
<el-text>{{ user.createTime || '-' }}</el-text>
</div>
<div class="user_info_wrap_item" style="padding-left: 24px;">
<el-button type="primary"
@click="router.push({ path: '/order/index', query: { userId: user.userId } })">查看所有订单</el-button>
</div>
</div>
</div>
<div class="shop_info_wrap">
<div style="margin-bottom: 16px; font-size: 16px">商品详情</div>
<template v-for="(item, index) in detail.detailMap" :key="index">
<h4>{{ index }}次下单</h4>
<el-table :data="item" :ref="el => { if (el) tableRefs[`refTable${index}`] = el }"
@select-all="tableSelectAll($event, index)">
<el-table-column label="数量" type="selection">
<template #default="scope">
<div v-if="detail.status == 'unpaid'">
<el-checkbox v-if="scope.row.num - scope.row.returnNum > 0" v-model="scope.row.checked" />
</div>
<div v-else>
<el-checkbox v-if="scope.row.num - scope.row.refundNum > 0" v-model="scope.row.checked" />
</div>
</template>
</el-table-column>
<el-table-column label="商品" width="150">
<template #default="scope">
<div class="shop_info">
<el-image :src="scope.row.productImg" style="width: 40px; height: 40px"></el-image>
<div class="info">
<span :class="[scope.row.isVip == 1 ? 'colorStyle' : '']">
{{ scope.row.productName }}
</span>
<span style="color: #999">{{ scope.row.productSkuName }}</span>
<div>
<el-tag v-if="scope.row.isTimeDiscount" type="danger" disable-transitions>
限时折扣
</el-tag>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="下单数量">
<template #default="scope">x{{ scope.row.num }}</template>
</el-table-column>
<el-table-column label="退菜数量">
<template #default="scope">x{{ scope.row.returnNum }}</template>
</el-table-column>
<el-table-column label="单价">
<template #default="scope">{{ scope.row.unitPrice }}</template>
</el-table-column>
<el-table-column label="备注" prop="remark"></el-table-column>
<el-table-column label="出菜时间" prop="dishOutTime" width="180"></el-table-column>
<el-table-column label="上菜时间" prop="foodServeTime" width="180"></el-table-column>
<el-table-column label="小计">
<template #default="scope">{{ scope.row.payAmount }}</template>
</el-table-column>
<el-table-column label="实付">
<template #default="scope">{{ scope.row.payAmount }}</template>
</el-table-column>
<el-table-column v-if="detail.status == 'unpaid'" label="可退菜数量" align="center" width="130px">
<template #default="scope">
<el-input-number v-if="scope.row.checked" :min="0" style="width: 100px" v-model="scope.row.selNumber"
:max="scope.row.num - scope.row.returnNum"></el-input-number>
<span v-else>{{ scope.row.num - scope.row.returnNum }}</span>
</template>
</el-table-column>
<el-table-column v-else label="可退款数量" align="center" width="130px">
<template #default="scope">
<el-input-number v-if="scope.row.checked" :min="0" style="width: 100px" v-model="scope.row.selNumber"
:max="scope.row.num - scope.row.refundNum - scope.row.returnNum"></el-input-number>
<span v-else>
{{ scope.row.num - scope.row.refundNum - scope.row.returnNum }}
</span>
</template>
</el-table-column>
<el-table-column label="已退款数量" width="100" align="center">
<template #default="scope">
<span v-if="detail.status == 'unpaid'">{{ scope.row.returnNum }}</span>
<span v-else>{{ scope.row.refundNum }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right">
<template #default="scope">
<template v-if="detail.status != 'unpaid'">
<el-button v-if="canTuikuan(scope.row)" link size="small" @click="tuikuan(scope.row)">
<span>退款</span>
</el-button>
<span class="color-999" v-if="scope.row.status == 'refund'">已退款</span>
</template>
<template v-if="detail.status == 'unpaid'">
<el-button v-if="canTuicai(scope.row)" link size="small" @click="tuicai(scope.row)">
<span>退菜</span>
</el-button>
<span class="color-999" v-else>已退菜</span>
</template>
</template>
</el-table-column>
</el-table>
</template>
<div class="u-p-20 u-flex u-row-right"
v-if="detail.status !== 'refund' && detail.status !== 'unpaid' && detail.status !== 'cancelled'">
<el-checkbox v-model="allSelected" @change="allSelectedChange" label="全选"></el-checkbox>
<el-button type="danger" class="u-m-l-20" @click.stop="tuikuan('all')">退款</el-button>
</div>
<div class="u-p-20 u-flex u-row-right" v-if="detail.status == 'unpaid'">
<el-checkbox v-model="allSelected" @change="allSelectedChange" label="全选"></el-checkbox>
<el-button type="danger" class="u-m-l-20" @click.stop="tuicai('all')">退菜</el-button>
</div>
</div>
</div>
<return-money :modal="false" ref="refReturnMoney" :max="selGoods.num" :goods="selGoods"
@confirm="refReturnMoneyConfirm"></return-money>
<order-return-cart ref="refReturnCart" @confirm="refReturnCartConfirm"></order-return-cart>
</div>
</template>
<script setup>
import { ref, computed, watch, defineEmits, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router';
import returnMoney from "./components/return-money.vue";
import orderReturnCart from "./components/order-return-cart.vue";
import { ElMessage } from "element-plus";
import { returnOptionsLabel } from "./config/config";
import * as $util from "./order_goods_util.js";
import orderApi from "@/api/order/order";
import shopUserApi from "@/api/account/shopUser";
import orderEnum from "./components/orderEnum";
import dayjs from "dayjs";
import { use } from 'echarts';
const route = useRoute()
const router = useRouter()
const emit = defineEmits(['close'])
// 基础变量
const allSelected = ref(false)
const user = ref("")
const drawer = ref(false)
const type = ref("1")
const detail = ref("")
const loading = ref(false)
const refoundList = ref([])
const selGoods = ref({ num: 1 })
const tableRefs = ref({})
// 子组件ref
const refReturnMoney = ref(null)
const refReturnCart = ref(null)
// 计算属性
const returnOriginAmount = computed(() => {
let amount = 0;
if (detail.value && detail.value.originAmount) {
return detail.value.originAmount;
}
if (!detail.value) {
return 0;
}
for (const i in detail.value.detailMap) {
amount += detail.value.detailMap[i].reduce((pre, cur) => pre + cur.payAmount, 0);
}
return amount.toFixed(2);
})
// 监听关闭抽屉
watch(drawer, (newval) => {
if (!newval) {
reset();
}
})
// 方法
const tableSelectAll = (e, index) => {
const arr = detail.value.detailMap[index];
for (const i in arr) {
arr[i].checked = e.length ? true : false;
}
}
const allSelectedChange = (newval) => {
for (const i in detail.value.detailMap) {
for (const key in detail.value.detailMap[i]) {
detail.value.detailMap[i][key].checked = newval;
}
}
}
const reset = () => {
user.value = "";
allSelected.value = false;
emit("close");
}
const returnPayType = (payType) => {
if (!payType) return "";
return returnOptionsLabel("payType", payType);
}
const sendTypeFilter = (t) => {
if (!t) return t;
const item = orderEnum.sendType.find(item => item.key == t);
return item ? item.label : "-";
}
const platformTypeFilter = (t) => {
if (!t) return t;
const item = orderEnum.platformType.find(item => item.key == t);
return item ? item.label : "-";
}
const statusFilter = (t) => {
return returnOptionsLabel("status", t);
}
const timeFilter = (t) => {
return t ? dayjs(t).format("YYYY-MM-DD HH:mm:ss") : "-";
}
const canTuikuan = (item) => $util.canTuiKuan(detail.value, item)
const canTuicai = (item) => $util.canTuicai(detail.value, item)
const refReturnMoneyConfirm = async (e) => {
await orderApi.refundOrder({ ...e, orderId: detail.value.id });
ElMessage.success("退款成功");
update();
}
const update = () => {
tbOrderInfoDetail(detail.value.id);
}
const refReturnCartConfirm = async (e) => {
await orderApi.refundOrder({ ...e, orderId: detail.value.id });
ElMessage.success("退菜成功");
update();
}
const tuikuan = (item) => {
if (!item) {
refReturnMoney.value.open([], detail.value);
return;
}
const arr = [];
if (item === "all") {
for (const i in detail.value.detailMap) {
detail.value.detailMap[i].forEach(v => {
if (v.checked && v.selNumber) arr.push(v);
});
}
} else {
arr.push(item);
}
if (arr.length === 0) {
return ElMessage.error("请选择要退款的商品和数量");
}
refReturnMoney.value.open(arr, detail.value);
}
const tuicai = (item) => {
const arr = [];
if (item === "all") {
for (const i in detail.value.detailMap) {
detail.value.detailMap[i].forEach(v => {
if (v.checked && v.selNumber) arr.push(v);
});
}
} else {
arr.push(item);
}
if (arr.length === 0) {
return ElMessage.error("请选择要退菜的商品和数量");
}
refReturnCart.value.open(arr, detail.value);
}
// 拉取订单详情
const tbOrderInfoDetail = async (id) => {
try {
loading.value = true;
const res = await orderApi.get({ orderId: id });
if (res.userId) {
shopUserApi.get({ userId: res.userId }).then(res1 => {
user.value = res1;
});
}
for (const i in res.detailMap) {
res.detailMap[i] = res.detailMap[i].map(v => {
return {
...v,
checked: false,
selNumber: v.num - (res.status == "unpaid" ? v.returnNum : v.refundNum),
};
});
}
detail.value = res;
loading.value = false;
} catch (error) {
console.log(error);
}
}
const show = (obj) => {
drawer.value = true;
type.value = "1";
detail.value = "";
tbOrderInfoDetail(obj.id);
}
// 保留废弃方法防止外部调用报错
const tableSelect = () => { }
const tableSelectionChange = () => { }
const rowClick = () => { }
const isShowGoodsVipPrice = (item) => $util.isShowGoodsVipPrice(item)
const isUseCalcPrice = (item) => $util.isUseCalcPrice(detail.value, item)
const close = () => emit("close")
const isTui = (item) => $util.isTui(item)
const to2 = (n) => Number(n).toFixed(2)
const orderTypeFilter = (t) => {
if (!t) return t;
const row = orderEnum.orderType.find(item => item.key == t);
return row?.label;
}
onMounted(() => {
if (route.query.orderId) {
tbOrderInfoDetail(route.query.orderId);
}
})
</script>
<style scoped lang="scss">
.packeFee {
width: 40px;
box-sizing: border-box;
height: 40px;
background: #3f9eff;
color: #fff;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
white-space: nowrap;
}
.shop_info {
display: flex;
.info {
flex: 1;
display: flex;
flex-direction: column;
padding-left: 6px;
}
}
.header {
padding: 0 20px 0;
margin-bottom: 20px;
.title {
display: flex;
align-items: center;
gap: 10px;
.back {
display: flex;
align-items: center;
justify-content: center;
&:hover {
cursor: pointer;
}
}
}
}
.line-th {
text-decoration: line-through;
}
.order_detail_container {
width: 100%;
--gap: 20px;
padding: 0 var(--gap);
font-size: 14px;
.order_top {
background-color: #fff;
border-radius: 8px;
padding: 14px;
.table {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
grid-column-gap: 0px;
grid-row-gap: 10px;
.item {
display: flex;
align-items: center;
}
.div1 {
grid-area: 1 / 1 / 2 / 4;
}
.div2 {
grid-area: 2 / 1 / 3 / 2;
}
.div3 {
grid-area: 2 / 2 / 3 / 3;
}
.div4 {
grid-area: 3 / 1 / 4 / 2;
}
.div5 {
grid-area: 3 / 2 / 4 / 3;
}
.div6 {
grid-area: 3 / 3 / 4 / 4;
}
}
}
.info_content {
padding: var(--gap);
background-color: #fff;
border-radius: 8px;
margin-top: var(--gap);
.item {
// border-bottom: 1px dashed #ececec;
padding-bottom: 20px;
&:not(:first-child) {
margin-top: 20px;
}
.title {
font-size: 16px;
color: #333;
}
.row {
display: flex;
color: #555;
padding-top: 20px;
.row_col {
flex: 1;
display: flex;
align-items: center;
}
div {
width: 25%;
}
}
}
.user_info_wrap {
display: flex;
margin-top: var(--gap);
.user_info_wrap_item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
}
}
.shop_info_wrap {
background-color: #fff;
border-radius: 8px;
padding: var(--gap);
margin-top: var(--gap);
}
.refund_wrap {
.row {
border-bottom: 1px dashed #ececec;
padding-bottom: 20px;
&:not(:first-child) {
margin-top: 20px;
}
.time {
font-weight: bold;
color: #333;
}
.list {
.list_row {
display: flex;
padding-top: 10px;
.item {
flex: 1;
display: flex;
color: #555;
.name {
margin-left: 6px;
}
}
}
}
.foot {
color: #333;
display: flex;
justify-content: flex-end;
}
}
}
}
.colorStyle {
color: #ffc315;
}
</style>