consumer-app/pages/profileSub/serviceRecords.vue

1036 lines
26 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<view class="service-records-page">
<!-- 头部区域 -->
<NavHeader title="服务记录" />
<!-- Tab 切换 -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: currentTab === 'pending_payment' }"
@click="switchTab('pending_payment')"
>
<text class="tab-text">待支付</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'pending_verification' }"
@click="switchTab('pending_verification')"
>
<text class="tab-text">已完成</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'chargeback' }"
@click="switchTab('chargeback')"
>
<text class="tab-text">已退款</text>
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'cancelled' }"
@click="switchTab('cancelled')"
>
<text class="tab-text">已取消</text>
</view>
</view>
<!-- 列表内容 -->
<scroll-view
class="record-list"
scroll-y="true"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="handleRefresh"
@scrolltolower="handleLoadMore"
:lower-threshold="100"
>
<!-- 空数据提示 -->
<view class="empty-state" v-if="!loading && currentList.length === 0">
<image
class="empty-icon"
src="/static/home/entry_icon.png"
mode="aspectFit"
></image>
<text class="empty-text">暂无{{ getTabLabel() }}记录</text>
</view>
<!-- 记录列表项 -->
<view
class="record-item"
v-for="(item, index) in currentList"
:key="index"
>
<!-- 头部:门店 + 状态 -->
<view class="record-header">
<view class="record-shop-row">
<text class="shop-name">{{ item.shopName }}</text>
<view class="status-badge" :class="getStatusClass(item.status)">
<text class="status-text">{{ getStatusText(item.status) }}</text>
</view>
</view>
<text class="record-time">{{ formatTime(item.createTime) }}</text>
</view>
<!-- 中部:类似商品卡片 -->
<view class="record-body" @click="handleRecordClick(item)">
<view
class="record-goods"
v-for="(goods, gIndex) in getGoodsList(item)"
:key="goods.id || goods.couponId || gIndex"
>
<image
class="goods-image"
:src="
goods.coverUrl ||
goods.couponCoverUrl ||
goods.picUrl ||
'/static/home/entry_icon.png'
"
mode="aspectFill"
></image>
<view class="goods-info">
<text class="goods-title">{{
goods.couponName || goods.name
}}</text>
<!-- <text class="goods-subtitle">
订单号:{{ item.orderNumber }}
</text> -->
<view class="goods-meta-row">
<text class="goods-price"
>¥{{ formatFen(goods.salePrice) }}</text
>
<text
class="goods-count"
v-if="
(goods.num || goods.count || goods.quantity || goods.qty) >
1
"
>
×{{ goods.num || goods.count || goods.quantity || goods.qty }}
</text>
</view>
</view>
<view
class="qr-code-icon-wrapper"
@click.stop="handleQrCode(item, goods)"
>
<image
src="/static/home/qr_code_icon.png"
mode="aspectFill"
class="qr-code-icon"
></image>
</view>
</view>
</view>
<!-- 底部:合计 + 操作按钮 -->
<view class="record-footer">
<view class="total-info">
<text class="total-label">实付款:</text>
<text class="total-amount"
>¥{{ formatFen(item.payableAmount) }}</text
>
</view>
<!-- 操作按钮区域 -->
<view class="record-actions" v-if="item.status === 0">
<button
class="action-btn cancel-btn"
@click.stop="handleCancel(item)"
>
取消订单
</button>
<button class="action-btn pay-btn" @click.stop="handlePay(item)">
立即支付
</button>
</view>
<view class="record-actions" v-else-if="item.status === 1">
<!-- <button
class="action-btn detail-btn"
@click.stop="handleViewDetail(item)"
>
去核销
</button> -->
</view>
<view class="record-actions" v-else-if="item.status === 3">
<!-- <button
class="action-btn detail-btn"
@click.stop="handleViewDetail(item)"
>
查看详情
</button>
<button
class="action-btn review-btn"
@click.stop="handleReview(item)"
>
评价
</button> -->
</view>
<view class="record-actions" v-else-if="item.status === 4">
<button
class="action-btn detail-btn"
@click.stop="handleDelete(item)"
>
删除
</button>
</view>
</view>
</view>
<!-- 加载更多提示 -->
<view class="load-more" v-if="currentList.length > 0">
<text v-if="loadingMore" class="load-more-text">加载中...</text>
<text v-else-if="!hasMore" class="load-more-text">没有更多数据了</text>
<text v-else class="load-more-text">上拉加载更多</text>
</view>
</scroll-view>
<!-- 券码二维码弹窗 -->
<view
class="qr-modal-mask"
v-if="qrModalVisible"
@click="closeQrModal"
></view>
<view class="qr-modal-wrap" v-if="qrModalVisible">
<view class="qr-modal" @click.stop>
<view class="qr-modal-header">
<text class="qr-modal-title">核销码</text>
<view
class="qr-modal-status"
:class="getQrUseStatusClass(qrModalData.useStatus)"
>
{{ getQrUseStatusText(qrModalData.useStatus) }}
</view>
<view class="qr-modal-close" @click="closeQrModal">×</view>
</view>
<view class="qr-modal-body">
<view class="qr-code-box" v-if="qrModalData.couponCode">
<image
class="qr-code-image"
:src="qrCodeImageUrl"
mode="aspectFit"
></image>
<!-- <text class="qr-code-text">{{ qrModalData.couponCode }}</text> -->
</view>
<text v-else class="qr-code-empty"></text>
</view>
</view>
</view>
</view>
</template>
<script>
import NavHeader from "@/components/NavHeader/NavHeader.vue";
import { getLuMyOrderPage, cancelOrder, getPaySign,deleteOrder } from "@/api/service";
export default {
components: {
NavHeader,
},
data() {
return {
currentTab: "pending_payment", // 当前选中的 tab
refreshing: false,
loading: false,
loadingMore: false,
hasMore: true,
pageNo: 1,
pageSize: 10,
// 各状态对应的订单列表
recordsMap: {
pending_payment: [],
pending_verification: [],
completed: [],
cancelled: [],
},
qrModalVisible: false,
qrModalData: {
useStatus: 0,
couponCode: "",
},
};
},
computed: {
currentList() {
return this.recordsMap[this.currentTab] || [];
},
qrCodeImageUrl() {
const code = this.qrModalData.couponCode || "";
if (!code) return "";
return `https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=${encodeURIComponent(code)}`;
},
},
onLoad(options) {
// 如果通过参数传入 tab优先使用传入的 tab 值
if (options && options.tab) {
this.currentTab = options.tab;
}
this.loadData();
},
methods: {
// 一个订单可能包含多个商品couponPurchaseRespVOS
getGoodsList(order) {
const list = (order && order.couponPurchaseRespVOS) || [];
return Array.isArray(list) && list.length ? list : [order || {}];
},
// 金额分转元(去掉后两位)
formatFen(fen) {
const n = Number(fen);
if (!Number.isFinite(n)) return fen || "0.00";
return (n / 100).toFixed(2);
},
// 时间戳转可读时间(支持秒/毫秒)
formatTime(val) {
if (val == null || val === "") return "";
let ts = Number(val);
if (!Number.isFinite(ts)) return val;
if (String(ts).length <= 10) ts *= 1000;
const d = new Date(ts);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const h = String(d.getHours()).padStart(2, "0");
const min = String(d.getMinutes()).padStart(2, "0");
const s = String(d.getSeconds()).padStart(2, "0");
return `${y}-${m}-${day} ${h}:${min}:${s}`;
},
// 根据当前 tab 映射到接口所需的 status 值
getStatusValue() {
const map = {
pending_payment: 0, // 待支付
pending_verification: 1, // 已完成
chargeback: 3, // 已退款
cancelled: 4, // 已取消
};
return map[this.currentTab];
},
// 切换 Tab
switchTab(tab) {
if (this.currentTab === tab) return;
this.currentTab = tab;
this.pageNo = 1;
this.hasMore = true;
this.loadData();
},
// 获取 Tab 标签文本
getTabLabel() {
const labels = {
pending_payment: "待支付",
pending_verification: "待核销",
completed: "已完成",
cancelled: "已取消",
};
return labels[this.currentTab] || "";
},
// 获取状态文本
getStatusText(status) {
return status === 0
? "待支付"
: status === 1
? "已完成"
: status === 3
? "已退款"
: status === 4
? "已取消"
: "";
},
// 获取状态样式类
getStatusClass(status) {
return status === 0
? "status-pending"
: status === 1
? "status-verification"
: status === 3
? "status-completed"
: status === 4
? "status-cancelled"
: "";
},
// 加载数据(真机/预览无数据时请检查1. 小程序后台「服务器域名」已配置接口域名 2. 真机已登录)
async loadData(append = false) {
if (this.loading) return;
this.loading = true;
const status = this.getStatusValue();
try {
const res = await getLuMyOrderPage({
pageNo: this.pageNo,
pageSize: this.pageSize,
status,
});
// 兼容多种后端返回结构list / records / data.list
const rawList =
res.list ||
res.records ||
(res.data && (res.data.list || res.data.records)) ||
[];
const list = Array.isArray(rawList) ? rawList : [];
const currentList = this.recordsMap[this.currentTab] || [];
this.recordsMap = {
...this.recordsMap,
[this.currentTab]: append ? currentList.concat(list) : list,
};
// 是否还有更多
this.hasMore = list.length >= this.pageSize;
} catch (e) {
const msg = (e && e.message) ? String(e.message) : "";
console.error("加载服务记录失败:", e);
uni.showToast({
title: msg ? `加载失败:${msg}` : "加载服务记录失败",
icon: "none",
duration: 2500,
});
} finally {
this.loading = false;
this.refreshing = false;
this.loadingMore = false;
}
},
// 下拉刷新
handleRefresh() {
this.refreshing = true;
this.pageNo = 1;
this.hasMore = true;
this.loadData();
},
// 上拉加载更多
handleLoadMore() {
if (this.hasMore && !this.loadingMore && !this.loading) {
this.loadingMore = true;
this.pageNo += 1;
this.loadData(true);
}
},
// 点击记录项
handleRecordClick(item) {
// 可以跳转到详情页
console.log("点击记录:", item);
},
// 取消订单
handleCancel(item) {
uni.showModal({
title: "提示",
content: "确定要取消该订单吗?",
success: async (res) => {
if (res.confirm) {
// 这里应该调用接口取消订单,然后刷新列表
const res = await cancelOrder({
id: item.id,
});
if (res) {
uni.showToast({
title: "订单已取消",
icon: "success",
});
this.loadData();
}
}
},
});
},
// 删除订单
handleDelete(item) {
uni.showModal({
title: "提示",
content: "确定要删除该订单吗?",
success: async (res) => {
if (res.confirm) {
const resData = await deleteOrder({
id: item.id,
});
if (resData) {
uni.showToast({
title: "订单已删除",
icon: "success",
});
this.loadData();
}
}
},
});
},
// 立即支付(与店铺详情页 handlePay 逻辑一致,跳转收银台小程序)
async handlePay(item) {
if (!item || !item.orderNumber) {
uni.showToast({ title: "订单信息异常", icon: "none" });
return;
}
const couponArray = item.couponPurchaseRespVOS || [];
const couponMap = new Map();
couponArray.forEach((goods) => {
const key = goods.couponId;
if (!key) return;
const currentList = couponMap.get(key) || [];
currentList.push(goods);
couponMap.set(key, currentList);
});
let bodyStr = "";
couponMap.forEach((couponList) => {
bodyStr =
bodyStr +
(couponList[0] && couponList[0].couponName
? couponList[0].couponName
: "商品") +
"x" +
couponList.length +
";";
});
const randomstr = Math.floor(Math.random() * 10000000) + "";
const trxamt =
item.payableAmount != null && item.payableAmount !== ""
? String(item.payableAmount)
: "1";
const params = {
appid: "00390105",
body: bodyStr,
cusid: "56479107531MPMN",
notify_url:
"http://e989c692.natappfree.cc/admin-api/member/lu-order/tlNotice",
orgid: "56479107392N35H",
paytype: "W06",
randomstr: randomstr,
orderNumber: item.orderNumber,
remark: "1:" + item.orderNumber + ":" + bodyStr,
reqsn: item.orderNumber,
sign: "",
signtype: "RSA",
trxamt,
version: "12",
};
if (item.orderNumber) {
uni.setStorageSync("lastOrderNumber", item.orderNumber);
}
try {
const sign = await getPaySign(params);
params["sign"] = sign;
uni.navigateToMiniProgram({
appId: "wxef277996acc166c3",
extraData: params,
success(res) {
console.log("小程序跳转成功", res);
},
fail(err) {
console.error("小程序跳转失败", err);
uni.showToast({ title: "跳转失败,请稍后重试", icon: "none" });
},
});
} catch (e) {
console.error("获取支付签名失败:", e);
uni.showToast({ title: "支付准备失败,请稍后重试", icon: "none" });
}
},
// 查看详情
handleViewDetail(item) {
uni.showToast({
title: "该功能正在开发中",
icon: "none",
});
},
// 评价
handleReview(item) {
uni.showToast({
title: "该功能正在开发中",
icon: "none",
});
},
// 点击二维码图标:打开弹窗,展示券状态 + 根据 couponCode 生成二维码
handleQrCode(item, goods) {
const useStatus = goods?.useStatus ?? item?.useStatus ?? 0;
const couponCode =
(goods && (goods.couponCode || goods.couponNo)) ||
item?.couponCode ||
item?.couponNo ||
"";
this.qrModalData = { useStatus, couponCode };
this.qrModalVisible = true;
},
getQrUseStatusText(useStatus) {
const map = { 0: "未使用", 1: "已使用", 3: "已退款", 4: "已取消" };
return map[useStatus] ?? "未使用";
},
getQrUseStatusClass(useStatus) {
const map = {
0: "status-unused",
1: "status-used",
3: "status-refund",
4: "status-cancelled",
};
return map[useStatus] ?? "status-unused";
},
closeQrModal() {
this.qrModalVisible = false;
},
},
};
</script>
<style lang="scss" scoped>
.service-records-page {
min-height: 100vh;
background: #e2e8f1;
display: flex;
flex-direction: column;
}
/* Tab 切换栏 */
.tab-bar {
display: flex;
padding: 0 20rpx;
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
position: relative;
.tab-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 28rpx;
color: #999999;
}
&.active {
.tab-text {
color: #004294;
font-weight: 600;
}
&::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: #004294;
border-radius: 2rpx;
}
}
}
}
/* 列表区域 */
.record-list {
flex: 1;
padding: 20rpx;
height: 0; // 配合 flex: 1 使用
box-sizing: border-box;
/* 空数据提示 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 200rpx 0;
min-height: 500rpx;
.empty-icon {
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.5;
}
.empty-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 28rpx;
color: #999999;
}
}
/* 加载更多提示 */
.load-more {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0;
min-height: 80rpx;
.load-more-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 400;
font-size: 24rpx;
color: #999999;
}
}
/* 记录项 */
.record-item {
background-color: #ffffff;
border-radius: 20rpx;
margin-bottom: 20rpx;
padding: 24rpx 24rpx 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.record-header {
margin-bottom: 16rpx;
.record-shop-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8rpx;
.shop-name {
flex: 1;
font-family: PingFang-SC, PingFang-SC;
font-weight: 600;
font-size: 28rpx;
color: #1a1819;
}
.status-badge {
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
.status-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
}
&.status-pending {
background-color: rgba(255, 107, 0, 0.1);
.status-text {
color: #ff6b00;
}
}
&.status-verification {
background-color: rgba(0, 66, 148, 0.1);
.status-text {
color: #004294;
}
}
&.status-completed {
background-color: rgba(76, 175, 80, 0.1);
.status-text {
color: #4caf50;
}
}
&.status-cancelled {
background-color: rgba(158, 158, 158, 0.1);
.status-text {
color: #9e9e9e;
}
}
}
}
.record-time {
font-family: PingFang-SC, PingFang-SC;
font-weight: 400;
font-size: 22rpx;
color: #999999;
}
}
.record-content {
margin-bottom: 24rpx;
.record-info-row {
display: flex;
align-items: flex-start;
margin-bottom: 16rpx;
line-height: 1.5;
&:last-child {
margin-bottom: 0;
}
.info-label {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 24rpx;
color: #888888;
margin-right: 8rpx;
flex-shrink: 0;
}
.info-value {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 24rpx;
color: #333333;
flex: 1;
&.price {
color: #d51c3c;
font-weight: 600;
font-size: 28rpx;
}
}
}
}
.record-body {
background-color: #f8f9fb;
border-radius: 16rpx;
padding: 18rpx;
.record-goods {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.goods-image {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
margin-right: 16rpx;
flex-shrink: 0;
}
.goods-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 8rpx;
.goods-title {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 26rpx;
color: #1a1819;
}
.goods-subtitle {
font-family: PingFang-SC, PingFang-SC;
font-weight: 400;
font-size: 22rpx;
color: #888888;
}
.goods-meta-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4rpx;
.goods-price {
font-family: PingFang-SC, PingFang-SC;
font-weight: 600;
font-size: 28rpx;
color: #d51c3c;
}
.goods-count {
font-family: PingFang-SC, PingFang-SC;
font-weight: 400;
font-size: 22rpx;
color: #666666;
}
}
}
.qr-code-icon-wrapper {
flex-shrink: 0;
padding: 12rpx;
margin-left: 8rpx;
}
.qr-code-icon {
width: 44rpx;
height: 44rpx;
}
}
}
.record-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 4rpx;
.total-info {
display: flex;
align-items: baseline;
.total-label {
font-family: PingFang-SC, PingFang-SC;
font-weight: 400;
font-size: 22rpx;
color: #666666;
}
.total-amount {
margin-left: 4rpx;
font-family: PingFang-SC, PingFang-SC;
font-weight: 600;
font-size: 30rpx;
color: #1a1819;
}
}
.record-actions {
display: flex;
align-items: center;
gap: 16rpx;
button {
margin: 0;
padding: 0;
}
.action-btn {
min-width: 150rpx;
height: 56rpx;
padding: 0 20rpx;
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 24rpx;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.pay-btn {
background-color: #004294;
color: #ffffff;
}
.detail-btn {
background-color: #f5f5f5;
color: #666666;
}
.review-btn {
background-color: #004294;
color: #ffffff;
}
}
}
}
}
/* 券码二维码弹窗 */
.qr-modal-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 900;
}
.qr-modal-wrap {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 901;
display: flex;
align-items: center;
justify-content: center;
padding: 36rpx;
box-sizing: border-box;
}
.qr-modal {
width: 100%;
max-width: 720rpx;
background: #ffffff;
border-radius: 28rpx;
overflow: hidden;
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.15);
}
.qr-modal-header {
position: relative;
padding: 40rpx 32rpx 28rpx;
border-bottom: 1rpx solid #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 16rpx;
}
.qr-modal-title {
font-family: PingFang-SC, PingFang-SC;
font-weight: 600;
font-size: 36rpx;
color: #1a1819;
}
.qr-modal-status {
padding: 8rpx 20rpx;
border-radius: 24rpx;
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 24rpx;
&.status-unused {
background: rgba(0, 66, 148, 0.1);
color: #004294;
}
&.status-used {
background: rgba(158, 158, 158, 0.15);
color: #9e9e9e;
}
&.status-refund {
background: rgba(76, 175, 80, 0.1);
color: #4caf50;
}
&.status-cancelled {
background: rgba(158, 158, 158, 0.15);
color: #9e9e9e;
}
}
.qr-modal-close {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 44rpx;
color: #999999;
line-height: 1;
}
.qr-modal-body {
padding: 48rpx 32rpx 56rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.qr-code-box {
display: flex;
flex-direction: column;
align-items: center;
}
.qr-code-image {
width: 520rpx;
height: 520rpx;
background: #fff;
border: 2rpx solid #e8e8e8;
border-radius: 20rpx;
margin-bottom: 28rpx;
}
.qr-code-text {
font-family: PingFang-SC, PingFang-SC;
font-weight: 500;
font-size: 26rpx;
color: #333333;
word-break: break-all;
text-align: center;
padding: 0 20rpx;
}
.qr-code-empty {
font-family: PingFang-SC, PingFang-SC;
font-size: 28rpx;
color: #999999;
}
</style>