youdas/components/youdas-container/order-list-item.vue
2025-06-24 00:34:39 +08:00

877 lines
25 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.

<!-- 在这个文件对每个tab对应的列表进行渲染 -->
<template>
<view class="content">
<!-- :enable-back-to-top="currentIndex===tabIndex" 在微信小程序上可以多加这一句因为默认是允许点击返回顶部的但是这个页面有多个scroll-view会全部返回顶部所以需要控制是当前index才允许点击返回顶部 -->
<!-- 如果当前页已经加载过数据或者当前切换到的tab是当前页才展示当前页数据懒加载 -->
<z-paging v-if="firstLoaded || isCurrentPage" ref="paging" v-model="dataList" @query="queryList" :fixed="false">
<!-- 搜索框 -->
<text-search v-model="searchKeyword" placeholder="搜索订单" @search="onSearch" @clear="onClear" />
<!-- 如果希望其他view跟着页面滚动可以放在z-paging标签内 -->
<view class="order-item" v-for="(item, index) in dataList" :key="index" @click="itemClick(item)">
<view class="order-header">
<text class="order-no" @click.stop="copyOrderNo(item.order_no)">订单号:{{ item.order_no }}</text>
<text class="order-status">{{ item.order_status_text }}</text>
</view>
<view class="order-content">
<image class="product-cover" :src="item.product_cover" mode="aspectFill"></image>
<view class="product-info">
<text class="product-title">{{ item.product_title }}</text>
<text class="payment-amount">¥{{ item.payment_amount }}</text>
</view>
</view>
<view class="order-footer">
<text class="order-time">{{ item.payment_time }}</text>
<view class="order-actions">
<!-- 已发货状态 -->
<template v-if="item.order_status === 2">
<view class="btn default" @click.stop="applyAfterSale(item)">申请售后</view>
<view class="btn primary" @click.stop="confirmReceive(item)">确认收货</view>
</template>
<!-- 已收货状态 -->
<template v-else-if="item.order_status === 3">
<view class="btn default" v-if="item.is_invoice !== 1" @click.stop="applyInvoice(item)">申请发票
</view>
<view class="btn default" v-else>已申请发票</view>
<view class="btn default"
v-if="item.is_invoice !== 1 && isWithinSevenDays(item.receive_time)"
@click.stop="applyAfterSale(item)">
申请售后</view>
<view class="btn default" @click.stop="deleteOrder(item)">删除订单</view>
<view class="btn default" @click.stop="buyAgain(item)">再次购买</view>
</template>
<!-- 已取消状态 -->
<template v-else-if="item.order_status === 5">
<view class="btn default" @click.stop="buyAgain(item)">再次购买</view>
</template>
<!-- 申请售后状态 -->
<template v-else-if="item.order_status === 6">
<view class="btn default" @click.stop="cancelAfterSale(item)">取消售后</view>
</template>
</view>
</view>
</view>
<template #empty>
<no-data />
</template>
<template #loading>
<loading-data />
</template>
</z-paging>
<!-- 申请发票弹窗 -->
<view class="invoice-popup" v-if="showInvoicePopup">
<view class="popup-mask" @click="closeInvoicePopup"></view>
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">申请发票</text>
<text class="popup-close" @click="closeInvoicePopup">×</text>
</view>
<view class="product-info-box">
<image class="product-popup-cover" :src="currentProduct.product_cover" mode="aspectFill"></image>
<view class="product-popup-info">
<text class="product-popup-title">{{ currentProduct.product_title }}</text>
<text class="product-popup-price">¥{{ currentProduct.payment_amount }}</text>
</view>
</view>
<view class="form-item">
<view class="form-label">发票类型</view>
<view class="form-content radio-group">
<view v-for="(type, index) in invoiceTypes" :key="index" class="radio-item"
:class="{ active: invoiceForm.invoice_type === type.value }"
@click="invoiceForm.invoice_type = type.value">
<text>{{ type.label }}</text>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label">发票抬头<text class="required">*</text></view>
<view class="form-content">
<input type="text" class="input" v-model="invoiceForm.invoice_title" placeholder="请输入发票抬头" />
</view>
</view>
<view class="form-item">
<view class="form-label">发票内容<text class="required">*</text></view>
<view class="form-content">
<input type="text" class="input" v-model="invoiceForm.invoice_content" placeholder="请输入发票内容" />
</view>
</view>
<view class="form-item">
<view class="form-label">申请邮箱<text class="required">*</text></view>
<view class="form-content">
<input type="text" class="input" v-model="invoiceForm.user_email" placeholder="请输入接收发票的邮箱" />
</view>
</view>
<view class="popup-footer">
<view class="btn default" @click="closeInvoicePopup">取消</view>
<view class="btn primary" @click="submitInvoice">提交申请</view>
</view>
</view>
</view>
<!-- 申请售后弹窗 -->
<view class="after-sale-popup" v-if="showAfterSalePopup">
<view class="popup-mask" @click="closeAfterSalePopup"></view>
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">申请售后</text>
<text class="popup-close" @click="closeAfterSalePopup">×</text>
</view>
<view class="product-info-box">
<image class="product-popup-cover" :src="currentProduct.product_cover" mode="aspectFill"></image>
<view class="product-popup-info">
<text class="product-popup-title">{{ currentProduct.product_title }}</text>
<text class="product-popup-price">¥{{ currentProduct.payment_amount }}</text>
</view>
</view>
<view class="form-item">
<view class="form-label">售后类型</view>
<view class="form-content radio-group">
<view v-for="(type, index) in afterSaleTypes" :key="index" class="radio-item"
:class="{ active: afterSaleForm.type === type.value }"
@click="afterSaleForm.type = type.value">
<text>{{ type.label }}</text>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label">申请原因</view>
<view class="form-content radio-group">
<view v-for="(reason, index) in afterSaleReasons" :key="index" class="radio-item"
:class="{ active: afterSaleForm.reason === reason }" @click="afterSaleForm.reason = reason">
<text>{{ reason }}</text>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label">申请说明<text class="required">*</text></view>
<view class="form-content">
<textarea class="input-textarea" v-model="afterSaleForm.remark" placeholder="请详细描述您的问题" />
</view>
</view>
<view class="form-item">
<view class="form-label">联系电话</view>
<view class="form-content">
<input type="text" class="input" v-model="afterSaleForm.phone" placeholder="请输入联系电话" />
</view>
</view>
<view class="popup-footer">
<view class="btn default" @click="closeAfterSalePopup">取消</view>
<view class="btn primary" @click="submitAfterSale">提交申请</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, watch, nextTick, onMounted, reactive } from 'vue';
import { order_receive, apply_refund, cancel_refund, delete_order, apply_invoice } from '@/common/server/order';
import TextSearch from '@/components/youdas-container/text-search.vue';
// props定义
const props = defineProps({
// 当前组件的index也就是当前组件是swiper中的第几个
tabIndex: {
type: Number,
default: 0
},
// 当前swiper切换到第几个index
currentIndex: {
type: Number,
default: 0
},
responseCallback: {
type: Function,
default: null
}
});
// tabIndex ['全部', '待发货', '待收货', '已收货', '售后']
// 数据
// v-model绑定的这个变量不要在分页请求结束中自己赋值
const dataList = ref([]);
// 当前组件是否已经加载过了
const firstLoaded = ref(false);
// 是否滚动到当前页
const isCurrentPage = ref(false);
// ref引用
const paging = ref(null);
// 搜索关键词
const searchKeyword = ref('');
// 售后弹窗数据
const showAfterSalePopup = ref(false);
const currentProduct = ref({});
const afterSaleTypes = [
{ label: '退货退款', value: 1 },
{ label: '换货', value: 2 },
{ label: '其它', value: 3 }
];
const afterSaleReasons = ['不想要了', '材质与商品不符', '质量问题', '发错货', '其他原因'];
const afterSaleForm = reactive({
type: 1,
reason: '不想要了',
remark: '',
phone: ''
});
// 发票弹窗数据
const showInvoicePopup = ref(false);
const invoiceTypes = [
{ label: '个人', value: 1 },
{ label: '企业', value: 2 }
];
const invoiceForm = reactive({
invoice_type: 1,
invoice_title: '',
invoice_content: '',
user_email: ''
});
// 监听currentIndex变化
watch(
() => props.currentIndex,
(newVal) => {
if (newVal === props.tabIndex) {
// 懒加载当滑动到当前的item时才去加载
if (!firstLoaded.value) {
// 这里需要延迟渲染z-paging的原因是为了避免在一些平台上立即渲染可能引发的底层报错问题
nextTick(() => {
setTimeout(() => {
isCurrentPage.value = true;
}, 100);
});
}
}
},
{ immediate: true }
);
// 接收父组件传过来的刷新列表要求
const reload = () => {
nextTick(() => {
// 刷新列表数据(如果不希望列表pageNo被重置可以用refresh代替reload方法)
paging.value && paging.value.reload();
});
};
const queryList = (pageNo, pageSize) => {
if (!props.responseCallback) { paging.value.complete(false); return; };
const params = {
pageNo: pageNo,
pageSize: pageSize,
type: props.tabIndex,
keyword: searchKeyword.value // 添加搜索关键词参数
};
try {
props.responseCallback(params).then(res => {
if (paging.value) {
console.log(res);
paging.value.complete(res.list || []);
firstLoaded.value = true;
}
}).catch(err => {
if (paging.value) {
paging.value.complete(false);
}
console.error('获取新闻列表失败', err);
});
} catch (e) {
console.error('调用responseCallback异常', e);
if (paging.value) {
paging.value.complete(false);
}
}
};
// 搜索处理函数
const onSearch = (value) => {
searchKeyword.value = value;
// 刷新列表,重新搜索
reload();
};
// 清空搜索
const onClear = () => {
searchKeyword.value = '';
// 刷新列表,显示所有数据
reload();
};
// 点击项目
const itemClick = (item) => {
yds.navigateTo("/pages/mall/order-detail?order_no=" + item.order_no);
};
const applyInvoice = async (item) => {
currentProduct.value = item;
// 尝试从本地缓存获取上次的发票信息
try {
const cachedInvoiceData = uni.getStorageSync('lastInvoiceData');
if (cachedInvoiceData) {
// 使用缓存数据预填充表单
invoiceForm.invoice_type = cachedInvoiceData.invoice_type || 1;
invoiceForm.invoice_title = cachedInvoiceData.invoice_title || '';
invoiceForm.invoice_content = cachedInvoiceData.invoice_content || '';
invoiceForm.user_email = cachedInvoiceData.user_email || '';
} else {
// 无缓存数据时重置表单
invoiceForm.invoice_type = 1;
invoiceForm.invoice_title = '';
invoiceForm.invoice_content = '';
invoiceForm.user_email = '';
}
} catch (e) {
console.error('获取缓存发票数据失败', e);
// 发生错误时重置表单
invoiceForm.invoice_type = 1;
invoiceForm.invoice_title = '';
invoiceForm.invoice_content = '';
invoiceForm.user_email = '';
}
// 显示弹窗
showInvoicePopup.value = true;
};
// 复制订单号
const copyOrderNo = (orderNo) => {
uni.setClipboardData({
data: orderNo,
success: function () {
uni.showToast({
title: '订单号已复制',
icon: 'none'
});
}
});
};
// 通用确认弹窗方法
const showConfirmDialog = (title, content) => {
return new Promise((resolve) => {
uni.showModal({
title: title,
content: content,
confirmText: '取消',
cancelText: '确定',
success: function (res) {
resolve(!res.confirm);
}
});
});
};
// 通用API调用方法
const callApiWithLoading = async (apiFunc, successMsg) => {
try {
uni.showLoading({
title: '正在处理'
});
const res = await apiFunc();
uni.hideLoading();
if (res) {
uni.showToast({
title: successMsg,
icon: 'none'
});
// 刷新列表
reload();
return true;
} else {
uni.showToast({
title: '操作失败,请重试',
icon: 'none'
});
return false;
}
} catch (error) {
uni.hideLoading();
uni.showToast({
title: '网络异常,请重试',
icon: 'none'
});
return false;
}
};
// 确认收货
const confirmReceive = async (item) => {
const confirmed = await showConfirmDialog('确认提示', '您确定要确认收货吗?');
if (confirmed) {
await callApiWithLoading(() => order_receive(item.order_no), '确认收货成功');
}
};
// 申请售后
const applyAfterSale = (item) => {
currentProduct.value = item;
// 重置表单
afterSaleForm.type = 1;
afterSaleForm.reason = '不想要了';
afterSaleForm.remark = '';
afterSaleForm.phone = '';
// 显示弹窗
showAfterSalePopup.value = true;
};
// 删除订单
const deleteOrder = async (item) => {
const confirmed = await showConfirmDialog('删除提示', '您确定要删除此订单吗?');
if (confirmed) {
await callApiWithLoading(() => delete_order(item.order_no), '删除成功');
}
};
// 再次购买
const buyAgain = (item) => {
yds.navigateTo("/pages/mall/product-detail?id=" + item.product_id);
};
// 判断是否在收货后7天内
const isWithinSevenDays = (receiveTime) => {
if (!receiveTime) return false;
const receiveDate = new Date(receiveTime);
const currentDate = new Date();
// 计算日期差值(毫秒)
const diffTime = currentDate - receiveDate;
// 转换为天数
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
// 如果天数差值小于等于7返回true
return diffDays <= 7;
};
// 取消售后
const cancelAfterSale = async (item) => {
const confirmed = await showConfirmDialog('取消提示', '您确定要取消售后申请吗?');
if (confirmed) {
await callApiWithLoading(() => cancel_refund(item.order_no), '取消售后成功');
}
};
// 关闭售后弹窗
const closeAfterSalePopup = () => {
showAfterSalePopup.value = false;
};
// 关闭发票弹窗
const closeInvoicePopup = () => {
showInvoicePopup.value = false;
};
// 提交发票申请
const submitInvoice = async () => {
// 表单验证
if (!invoiceForm.invoice_title) {
uni.showToast({
title: '请填写发票抬头',
icon: 'none'
});
return;
}
if (!invoiceForm.invoice_content) {
uni.showToast({
title: '请填写发票内容',
icon: 'none'
});
return;
}
if (!invoiceForm.user_email) {
uni.showToast({
title: '请填写申请邮箱',
icon: 'none'
});
return;
}
// 验证邮箱格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(invoiceForm.user_email)) {
uni.showToast({
title: '请输入正确的邮箱格式',
icon: 'none'
});
return;
}
const result = await callApiWithLoading(
() => apply_invoice(
currentProduct.value.order_no,
invoiceForm.invoice_title,
invoiceForm.invoice_content,
invoiceForm.invoice_type,
invoiceForm.user_email
),
'申请发票成功,发票抬头文件会在48小时内发送到您的邮箱。'
);
// 如果申请成功,保存发票数据到本地存储
if (result) {
try {
// 保存发票信息到本地缓存,下次使用
uni.setStorageSync('lastInvoiceData', {
invoice_type: invoiceForm.invoice_type,
invoice_title: invoiceForm.invoice_title,
invoice_content: invoiceForm.invoice_content,
user_email: invoiceForm.user_email
});
} catch (e) {
console.error('保存发票数据到缓存失败', e);
}
}
// 关闭弹窗
closeInvoicePopup();
};
// 提交售后申请
const submitAfterSale = async () => {
// 表单验证
if (!afterSaleForm.remark) {
uni.showToast({
title: '请填写申请说明',
icon: 'none'
});
return;
}
// 准备提交信息
const extendInfo = JSON.stringify({
type: afterSaleForm.type,
reason: afterSaleForm.reason,
remark: afterSaleForm.remark,
phone: afterSaleForm.phone
});
await callApiWithLoading(
() => apply_refund(currentProduct.value.order_no, extendInfo),
'申请售后成功客服会在24小时内联系您处理。'
);
// 关闭弹窗
closeAfterSalePopup();
};
// 暴露方法给父组件调用
defineExpose({
reload
});
</script>
<style lang="scss" scoped>
/* 注意:父节点需要固定高度z-paging的height:100%才会生效 */
.content {
height: 100%;
}
.order-item {
background-color: #ffffff;
border-radius: 12rpx;
margin: 20rpx 30rpx;
padding: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 20rpx;
border-bottom: 1px solid #f5f5f5;
}
.order-no {
font-size: 24rpx;
color: #999999;
position: relative;
&::after {
content: "";
position: absolute;
left: 0;
bottom: -2rpx;
width: 100%;
height: 1rpx;
background-color: #999999;
opacity: 0.5;
}
}
.order-status {
font-size: 28rpx;
color: #ff6700;
font-weight: 500;
}
.order-content {
display: flex;
padding: 20rpx 0;
}
.product-cover {
width: 160rpx;
height: 160rpx;
border-radius: 8rpx;
}
.product-info {
flex: 1;
padding-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-title {
font-size: 28rpx;
color: #333333;
font-weight: 500;
line-height: 40rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.payment-amount {
font-size: 32rpx;
color: #ff6700;
font-weight: 500;
}
.order-footer {
display: flex;
flex-direction: column;
padding-top: 15rpx;
border-top: 1px solid #f5f5f5;
}
.order-time {
font-size: 24rpx;
color: #999999;
}
.order-actions {
display: flex;
justify-content: flex-end;
margin-top: 20rpx;
flex-wrap: wrap;
}
/* 按钮样式 */
.btn {
display: inline-block;
font-size: 24rpx;
padding: 10rpx 24rpx;
margin-left: 16rpx;
border-radius: 8rpx;
text-align: center;
transition: all 0.15s ease;
-webkit-tap-highlight-color: transparent;
&:active {
transform: scale(0.95);
}
&.default {
background-color: #ffffff;
color: #333333;
border: 1px solid #dddddd;
}
&.primary {
background-color: #e94e42;
color: #ffffff;
border: 1px solid #e94e42;
}
}
/* 售后弹窗样式 */
.after-sale-popup,
.invoice-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.popup-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.popup-content {
position: relative;
width: 92%;
max-height: 90vh;
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
box-sizing: border-box;
overflow-y: auto;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.popup-close {
font-size: 40rpx;
color: #999;
padding: 0 10rpx;
}
.product-info-box {
display: flex;
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
margin-bottom: 20rpx;
}
.product-popup-cover {
width: 120rpx;
height: 120rpx;
border-radius: 8rpx;
}
.product-popup-info {
flex: 1;
padding-left: 20rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.product-popup-title {
font-size: 26rpx;
color: #333;
line-height: 36rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.product-popup-price {
font-size: 28rpx;
color: #ff6700;
font-weight: 500;
}
.form-item {
margin-bottom: 20rpx;
}
.form-label {
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
.required {
color: #ff4d4f;
margin-left: 4rpx;
}
}
.form-content {
width: 100%;
}
.radio-group {
display: flex;
flex-wrap: wrap;
margin: 0 -10rpx;
}
.radio-item {
margin: 10rpx;
padding: 10rpx 20rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
font-size: 26rpx;
color: #666;
&.active {
border-color: #ff6700;
color: #ff6700;
background-color: rgba(255, 103, 0, 0.05);
}
}
.input {
width: 100%;
height: 80rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.input-textarea {
width: 100%;
height: 180rpx;
border: 1px solid #ddd;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
box-sizing: border-box;
}
.popup-footer {
display: flex;
justify-content: flex-end;
margin-top: 30rpx;
.btn {
margin-left: 20rpx;
min-width: 160rpx;
height: 70rpx;
line-height: 70rpx;
font-size: 28rpx;
}
}
</style>