38 changed files with 3619 additions and 2790 deletions
@ -0,0 +1,381 @@ |
|||
<template> |
|||
<el-dialog |
|||
v-model="showDialog" |
|||
:title="dialogTitle" |
|||
width="700px" |
|||
:close-on-click-modal="false" |
|||
> |
|||
<el-descriptions |
|||
v-if="orderDetail" |
|||
:column="2" |
|||
border |
|||
v-loading="loading" |
|||
> |
|||
<el-descriptions-item label="订单编号"> |
|||
{{ orderDetail.id }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="订单类型"> |
|||
{{ getOrderType(orderDetail.order_type) }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="订单状态"> |
|||
{{ getOrderStatus(orderDetail.order_status) }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="支付类型"> |
|||
{{ getPaymentType(orderDetail.payment_type) }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="订单金额"> |
|||
<span class="text-red-500 font-bold">¥{{ orderDetail.order_amount }}</span> |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="优惠金额" v-if="orderDetail.discount_amount"> |
|||
<span class="text-green-500">¥{{ orderDetail.discount_amount }}</span> |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="课程名称"> |
|||
{{ orderDetail.course_id_name || '-' }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="班级名称"> |
|||
{{ orderDetail.class_id_name || '-' }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="学员姓名"> |
|||
{{ orderDetail.student_name || '-' }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="所属校区"> |
|||
{{ orderDetail.campus_name || '-' }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="客户姓名"> |
|||
{{ orderDetail.resource_id_name || '-' }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="业务员"> |
|||
{{ orderDetail.staff_id_name || '-' }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="赠品信息" v-if="orderDetail.gift_info && orderDetail.gift_info.gift_name"> |
|||
{{ orderDetail.gift_info.gift_name }} |
|||
<el-tag size="small" :type="getGiftStatusType(orderDetail.gift_info.gift_status)" style="margin-left: 8px;"> |
|||
{{ getGiftStatus(orderDetail.gift_info.gift_status) }} |
|||
</el-tag> |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="支付时间" v-if="orderDetail.payment_time"> |
|||
{{ orderDetail.payment_time }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="创建时间"> |
|||
{{ orderDetail.created_at }} |
|||
</el-descriptions-item> |
|||
|
|||
<el-descriptions-item label="订单备注" :span="2" v-if="orderDetail.remark"> |
|||
{{ orderDetail.remark }} |
|||
</el-descriptions-item> |
|||
</el-descriptions> |
|||
|
|||
<!-- 确认支付时的支付凭证上传区域 --> |
|||
<div v-if="isPaymentAction && orderDetail?.order_status === 'pending'" class="payment-voucher-section"> |
|||
<el-divider content-position="left">支付凭证</el-divider> |
|||
|
|||
<el-form :model="paymentForm" label-width="100px" style="margin-top: 20px;"> |
|||
<el-form-item label="上传凭证"> |
|||
<div class="upload-wrapper"> |
|||
<el-upload |
|||
:action="uploadAction" |
|||
:headers="uploadHeaders" |
|||
:show-file-list="true" |
|||
:on-success="handleUploadSuccess" |
|||
:on-error="handleUploadError" |
|||
:before-upload="beforeUpload" |
|||
list-type="picture-card" |
|||
accept="image/*" |
|||
class="voucher-upload" |
|||
> |
|||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon> |
|||
</el-upload> |
|||
<div class="upload-tip"> |
|||
支持 jpg/png/jpeg 格式,单个文件不超过 5MB |
|||
</div> |
|||
</div> |
|||
</el-form-item> |
|||
<el-form-item label="支付备注"> |
|||
<el-input |
|||
v-model="paymentForm.remark" |
|||
type="textarea" |
|||
:rows="3" |
|||
placeholder="请输入支付备注信息(可选)" |
|||
/> |
|||
</el-form-item> |
|||
</el-form> |
|||
</div> |
|||
|
|||
<template #footer> |
|||
<span class="dialog-footer"> |
|||
<el-button @click="showDialog = false">关闭</el-button> |
|||
<el-button |
|||
v-if="isPaymentAction && orderDetail?.order_status === 'pending'" |
|||
type="primary" |
|||
@click="handleConfirmPayment" |
|||
:disabled="paymentForm.vouchers.length === 0" |
|||
> |
|||
确认支付 |
|||
</el-button> |
|||
</span> |
|||
</template> |
|||
</el-dialog> |
|||
</template> |
|||
|
|||
<script lang="ts" setup> |
|||
import { ref, computed } from 'vue' |
|||
import { getOrderDetail } from '@/app/api/order_table' |
|||
import { ElMessage } from 'element-plus' |
|||
import { Plus } from '@element-plus/icons-vue' |
|||
import { getToken } from '@/utils/common' |
|||
|
|||
const showDialog = ref(false) |
|||
const loading = ref(false) |
|||
const orderDetail = ref<any>(null) |
|||
const isPaymentAction = ref(false) |
|||
|
|||
// 支付表单数据 |
|||
const paymentForm = ref({ |
|||
vouchers: [] as string[], // 支付凭证URL数组 |
|||
remark: '' // 支付备注 |
|||
}) |
|||
|
|||
// 上传配置 |
|||
const uploadAction = ref(import.meta.env.VITE_APP_BASE_URL + 'sys/image') |
|||
const uploadHeaders = ref({ |
|||
token: getToken() |
|||
}) |
|||
|
|||
const dialogTitle = computed(() => { |
|||
return isPaymentAction.value ? '确认支付' : '订单详情' |
|||
}) |
|||
|
|||
// 订单类型映射 |
|||
const getOrderType = (type: number) => { |
|||
const typeMap: Record<number, string> = { |
|||
1: '新订单', |
|||
2: '续费订单', |
|||
3: '内部员工订单', |
|||
4: '转校', |
|||
5: '客户内转课订单' |
|||
} |
|||
return typeMap[type] || '-' |
|||
} |
|||
|
|||
// 订单状态映射 |
|||
const getOrderStatus = (status: string) => { |
|||
const statusMap: Record<string, string> = { |
|||
pending: '待支付', |
|||
paid: '已支付', |
|||
signed: '待签约', |
|||
completed: '已完成', |
|||
transfer: '转学' |
|||
} |
|||
return statusMap[status] || '-' |
|||
} |
|||
|
|||
// 支付类型映射 |
|||
const getPaymentType = (type: string) => { |
|||
const typeMap: Record<string, string> = { |
|||
cash: '现金支付', |
|||
scan_code: '扫码支付', |
|||
subscription: '订阅支付', |
|||
wxpay_online: '微信在线代付', |
|||
client_wxpay: '客户端微信支付', |
|||
deposit: '定金' |
|||
} |
|||
return typeMap[type] || '-' |
|||
} |
|||
|
|||
// 赠品状态映射 |
|||
const getGiftStatus = (status: number) => { |
|||
const statusMap: Record<number, string> = { |
|||
1: '未使用', |
|||
2: '已使用', |
|||
3: '已过期', |
|||
4: '已作废' |
|||
} |
|||
return statusMap[status] || '-' |
|||
} |
|||
|
|||
// 赠品状态标签类型 |
|||
const getGiftStatusType = (status: number) => { |
|||
const typeMap: Record<number, string> = { |
|||
1: 'success', |
|||
2: 'info', |
|||
3: 'warning', |
|||
4: 'danger' |
|||
} |
|||
return typeMap[status] || 'info' |
|||
} |
|||
|
|||
// 打开弹窗 |
|||
const openDialog = async (orderId: number, forPayment: boolean = false) => { |
|||
showDialog.value = true |
|||
loading.value = true |
|||
isPaymentAction.value = forPayment |
|||
|
|||
// 重置支付表单 |
|||
paymentForm.value = { |
|||
vouchers: [], |
|||
remark: '' |
|||
} |
|||
|
|||
try { |
|||
const res = await getOrderDetail(orderId) |
|||
orderDetail.value = res.data |
|||
} catch (error) { |
|||
ElMessage.error('获取订单详情失败') |
|||
showDialog.value = false |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
// 上传前校验 |
|||
const beforeUpload = (file: any) => { |
|||
const isImage = file.type.startsWith('image/') |
|||
const isLt5M = file.size / 1024 / 1024 < 5 |
|||
|
|||
if (!isImage) { |
|||
ElMessage.error('只能上传图片文件!') |
|||
return false |
|||
} |
|||
if (!isLt5M) { |
|||
ElMessage.error('图片大小不能超过 5MB!') |
|||
return false |
|||
} |
|||
return true |
|||
} |
|||
|
|||
// 上传成功回调 |
|||
const handleUploadSuccess = (response: any, file: any) => { |
|||
if (response.code === 1 && response.data && response.data.url) { |
|||
paymentForm.value.vouchers.push(response.data.url) |
|||
ElMessage.success('上传成功') |
|||
} else { |
|||
ElMessage.error(response.msg || '上传失败') |
|||
} |
|||
} |
|||
|
|||
// 上传失败回调 |
|||
const handleUploadError = () => { |
|||
ElMessage.error('上传失败,请重试') |
|||
} |
|||
|
|||
// 确认支付 |
|||
const handleConfirmPayment = () => { |
|||
if (paymentForm.value.vouchers.length === 0) { |
|||
ElMessage.warning('请上传支付凭证') |
|||
return |
|||
} |
|||
|
|||
// 这里可以调用支付接口 |
|||
console.log('订单ID:', orderDetail.value.id) |
|||
console.log('支付凭证:', paymentForm.value.vouchers) |
|||
console.log('支付备注:', paymentForm.value.remark) |
|||
|
|||
ElMessage.success('支付确认成功,待后端接口实现') |
|||
showDialog.value = false |
|||
} |
|||
|
|||
// 暴露方法供父组件调用 |
|||
defineExpose({ |
|||
openDialog |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.text-red-500 { |
|||
color: #ef4444; |
|||
} |
|||
.text-green-500 { |
|||
color: #10b981; |
|||
} |
|||
.font-bold { |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.payment-voucher-section { |
|||
margin-top: 20px; |
|||
padding: 20px; |
|||
background-color: #f9fafb; |
|||
border-radius: 8px; |
|||
} |
|||
|
|||
/* 上传容器布局 */ |
|||
.upload-wrapper { |
|||
width: 100%; |
|||
display: block; |
|||
overflow: visible; |
|||
} |
|||
|
|||
/* 上传提示文字 */ |
|||
.upload-tip { |
|||
margin-top: 10px; |
|||
padding-top: 10px; |
|||
display: block; |
|||
font-size: 12px; |
|||
color: #999; |
|||
line-height: 1.5; |
|||
clear: both; |
|||
} |
|||
|
|||
/* 上传组件样式 - 强制显示为块级元素 */ |
|||
.voucher-upload { |
|||
width: 100%; |
|||
display: block !important; |
|||
overflow: visible !important; |
|||
} |
|||
|
|||
/* el-upload 组件内部样式穿透 */ |
|||
:deep(.el-upload--picture-card) { |
|||
width: 120px !important; |
|||
height: 120px !important; |
|||
display: inline-block !important; |
|||
vertical-align: top !important; |
|||
margin: 0 8px 8px 0 !important; |
|||
} |
|||
|
|||
:deep(.el-upload-list) { |
|||
display: block !important; |
|||
overflow: visible !important; |
|||
position: relative !important; |
|||
} |
|||
|
|||
:deep(.el-upload-list--picture-card) { |
|||
display: block !important; |
|||
vertical-align: top !important; |
|||
width: 100% !important; |
|||
overflow: visible !important; |
|||
position: relative !important; |
|||
} |
|||
|
|||
:deep(.el-upload-list__item) { |
|||
width: 120px !important; |
|||
height: 120px !important; |
|||
display: inline-block !important; |
|||
margin: 0 8px 8px 0 !important; |
|||
vertical-align: top !important; |
|||
position: relative !important; |
|||
} |
|||
|
|||
.avatar-uploader-icon { |
|||
font-size: 28px; |
|||
color: #8c939d; |
|||
width: 120px; |
|||
height: 120px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
@ -0,0 +1,225 @@ |
|||
<!-- 体测记录列表弹窗组件 --> |
|||
<template> |
|||
<view class="fitness-record-list-popup" v-if="visible" @click.stop="handleMaskClick"> |
|||
<view class="popup-container" @click.stop> |
|||
<!-- 标题栏 --> |
|||
<view class="popup-header"> |
|||
<text class="popup-title">体测记录</text> |
|||
<view class="close-btn" @click.stop="handleClose"> |
|||
<text>✕</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 体测记录列表 --> |
|||
<view class="fitness-records-container"> |
|||
<!-- 空状态提示 --> |
|||
<view v-if="!records || records.length === 0" class="empty-state"> |
|||
<view class="empty-icon">📊</view> |
|||
<view class="empty-text">暂无体测记录</view> |
|||
<view class="empty-tip">点击下方"新增"按钮添加体测记录</view> |
|||
</view> |
|||
|
|||
<!-- 体测记录列表 --> |
|||
<FitnessRecordCard |
|||
v-for="record in records" |
|||
:key="record.id" |
|||
:record="record" |
|||
@edit="handleEdit" |
|||
/> |
|||
</view> |
|||
|
|||
<!-- 底部操作按钮 --> |
|||
<view class="popup-footer"> |
|||
<view class="footer-btn cancel-btn" @click.stop="handleClose">关闭</view> |
|||
<view class="footer-btn confirm-btn" @click.stop="handleAddNew">新增</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import FitnessRecordCard from '@/components/fitness-record-card/fitness-record-card.vue' |
|||
|
|||
export default { |
|||
name: 'FitnessRecordListPopup', |
|||
components: { |
|||
FitnessRecordCard |
|||
}, |
|||
props: { |
|||
// 是否显示弹窗 |
|||
visible: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
// 体测记录列表 |
|||
records: { |
|||
type: Array, |
|||
default: () => [] |
|||
} |
|||
}, |
|||
methods: { |
|||
// 关闭弹窗 |
|||
handleClose() { |
|||
this.$emit('close') |
|||
}, |
|||
|
|||
// 点击遮罩关闭 |
|||
handleMaskClick() { |
|||
this.handleClose() |
|||
}, |
|||
|
|||
// 新增体测记录 |
|||
handleAddNew() { |
|||
this.$emit('add') |
|||
}, |
|||
|
|||
// 编辑体测记录 |
|||
handleEdit(record) { |
|||
this.$emit('edit', record) |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="less" scoped> |
|||
.fitness-record-list-popup { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background-color: rgba(0, 0, 0, 0.5); |
|||
display: flex; |
|||
align-items: flex-end; |
|||
justify-content: center; |
|||
z-index: 999; /* 比FitnessRecordPopup的z-index低 */ |
|||
} |
|||
|
|||
.popup-container { |
|||
width: 100%; |
|||
max-height: 80vh; |
|||
background: #FFFFFF; |
|||
border-radius: 32rpx 32rpx 0 0; |
|||
display: flex; |
|||
flex-direction: column; |
|||
animation: slideUp 0.3s ease-out; |
|||
} |
|||
|
|||
@keyframes slideUp { |
|||
from { |
|||
transform: translateY(100%); |
|||
} |
|||
to { |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
.popup-header { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 32rpx 32rpx 24rpx; |
|||
border-bottom: 1rpx solid #F0F0F0; |
|||
} |
|||
|
|||
.popup-title { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
color: #333333; |
|||
} |
|||
|
|||
.close-btn { |
|||
width: 56rpx; |
|||
height: 56rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: #F5F5F5; |
|||
border-radius: 50%; |
|||
|
|||
text { |
|||
font-size: 40rpx; |
|||
color: #999999; |
|||
line-height: 1; |
|||
} |
|||
} |
|||
|
|||
.fitness-records-container { |
|||
flex: 1; |
|||
overflow-y: auto; |
|||
padding: 24rpx 32rpx; |
|||
max-height: 50vh; |
|||
} |
|||
|
|||
/* 滚动条样式 */ |
|||
.fitness-records-container::-webkit-scrollbar { |
|||
width: 6rpx; |
|||
} |
|||
|
|||
.fitness-records-container::-webkit-scrollbar-track { |
|||
background: transparent; |
|||
} |
|||
|
|||
.fitness-records-container::-webkit-scrollbar-thumb { |
|||
background: #29D3B4; |
|||
border-radius: 3rpx; |
|||
} |
|||
|
|||
.fitness-records-container::-webkit-scrollbar-thumb:hover { |
|||
background: #24B89E; |
|||
} |
|||
|
|||
.empty-state { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 80rpx 0; |
|||
} |
|||
|
|||
.empty-icon { |
|||
font-size: 120rpx; |
|||
line-height: 1; |
|||
margin-bottom: 24rpx; |
|||
} |
|||
|
|||
.empty-text { |
|||
font-size: 28rpx; |
|||
color: #999999; |
|||
margin-bottom: 16rpx; |
|||
} |
|||
|
|||
.empty-tip { |
|||
font-size: 24rpx; |
|||
color: #CCCCCC; |
|||
} |
|||
|
|||
.popup-footer { |
|||
display: flex; |
|||
padding: 24rpx 32rpx; |
|||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); |
|||
border-top: 1rpx solid #F0F0F0; |
|||
gap: 24rpx; |
|||
} |
|||
|
|||
.footer-btn { |
|||
flex: 1; |
|||
height: 88rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-radius: 16rpx; |
|||
font-size: 32rpx; |
|||
font-weight: 500; |
|||
} |
|||
|
|||
.cancel-btn { |
|||
background: #F5F5F5; |
|||
color: #666666; |
|||
} |
|||
|
|||
.confirm-btn { |
|||
background: linear-gradient(135deg, #29D3B4 0%, #24B89E 100%); |
|||
color: #FFFFFF; |
|||
} |
|||
</style> |
|||
File diff suppressed because it is too large
@ -0,0 +1,505 @@ |
|||
<!--支付凭证上传弹窗组件--> |
|||
<template> |
|||
<view class="payment-voucher-wrapper"> |
|||
<!-- 遮罩层 --> |
|||
<view class="popup-mask" @click="handleCancel"></view> |
|||
|
|||
<!-- 弹窗内容 --> |
|||
<view class="payment-voucher-popup"> |
|||
<view class="popup-content"> |
|||
<view class="popup-header"> |
|||
<text class="popup-title">上传支付凭证</text> |
|||
<view class="close-btn" @click="handleCancel"> |
|||
<text class="close-icon">✕</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="popup-body"> |
|||
<!-- 订单信息展示 --> |
|||
<view class="order-info-section"> |
|||
<view class="info-row"> |
|||
<text class="info-label">订单号:</text> |
|||
<text class="info-value">{{ orderInfo.order_no }}</text> |
|||
</view> |
|||
<view class="info-row"> |
|||
<text class="info-label">课程类型:</text> |
|||
<text class="info-value">{{ orderInfo.product_name }}</text> |
|||
</view> |
|||
<view class="info-row"> |
|||
<text class="info-label">支付金额:</text> |
|||
<text class="info-value amount">¥{{ orderInfo.total_amount }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 图片上传区域 --> |
|||
<view class="upload-section"> |
|||
<view class="section-title">支付凭证</view> |
|||
<view class="upload-tip">最多上传9张图片</view> |
|||
|
|||
<view class="image-list"> |
|||
<view |
|||
v-for="(image, index) in imageList" |
|||
:key="index" |
|||
class="image-item" |
|||
> |
|||
<image :src="image" class="preview-image" mode="aspectFill"></image> |
|||
<view class="delete-btn" @click="deleteImage(index)"> |
|||
<text class="delete-icon">×</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<view |
|||
v-if="imageList.length < 9" |
|||
class="upload-btn" |
|||
@click="chooseImage" |
|||
> |
|||
<text class="upload-icon">+</text> |
|||
<text class="upload-text">上传图片</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
|
|||
<view class="popup-footer"> |
|||
<view class="footer-btn cancel-btn" @click="handleCancel"> |
|||
<text>取消</text> |
|||
</view> |
|||
<view |
|||
class="footer-btn confirm-btn" |
|||
:class="{ disabled: !canSubmit }" |
|||
@click="handleConfirm" |
|||
> |
|||
<text>提交</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import { uploadFile } from '@/common/util.js' |
|||
|
|||
export default { |
|||
name: 'PaymentVoucherPopup', |
|||
|
|||
props: { |
|||
// 订单信息 |
|||
orderInfo: { |
|||
type: Object, |
|||
default: () => ({ |
|||
order_no: '', |
|||
product_name: '', |
|||
total_amount: '0.00' |
|||
}) |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
imageList: [], // 图片列表 |
|||
uploading: false |
|||
} |
|||
}, |
|||
|
|||
computed: { |
|||
// 是否可以提交 |
|||
canSubmit() { |
|||
return this.imageList.length > 0 && !this.uploading |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
/** |
|||
* 选择图片 |
|||
*/ |
|||
chooseImage() { |
|||
const remainCount = 9 - this.imageList.length |
|||
|
|||
uni.chooseImage({ |
|||
count: remainCount, |
|||
sizeType: ['compressed'], |
|||
sourceType: ['album', 'camera'], |
|||
success: (res) => { |
|||
const tempFilePaths = res.tempFilePaths |
|||
|
|||
// 上传图片 |
|||
this.uploadImages(tempFilePaths) |
|||
}, |
|||
fail: (err) => { |
|||
console.error('选择图片失败:', err) |
|||
uni.showToast({ title: '选择图片失败', icon: 'none' }) |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 上传图片 |
|||
*/ |
|||
async uploadImages(filePaths) { |
|||
this.uploading = true |
|||
uni.showLoading({ title: '上传中...' }) |
|||
|
|||
try { |
|||
const uploadPromises = filePaths.map(filePath => this.uploadSingleImage(filePath)) |
|||
const results = await Promise.all(uploadPromises) |
|||
|
|||
// 将上传成功的图片URL添加到列表 |
|||
results.forEach(url => { |
|||
if (url) { |
|||
this.imageList.push(url) |
|||
} |
|||
}) |
|||
|
|||
uni.hideLoading() |
|||
|
|||
if (results.some(url => url)) { |
|||
uni.showToast({ title: '上传成功', icon: 'success' }) |
|||
} |
|||
} catch (error) { |
|||
uni.hideLoading() |
|||
console.error('上传图片异常:', error) |
|||
uni.showToast({ title: '上传失败', icon: 'none' }) |
|||
} finally { |
|||
this.uploading = false |
|||
} |
|||
}, |
|||
|
|||
/** |
|||
* 上传单张图片 |
|||
*/ |
|||
uploadSingleImage(filePath) { |
|||
return new Promise((resolve, reject) => { |
|||
uploadFile( |
|||
filePath, |
|||
(fileData) => { |
|||
// 成功回调: fileData = { url, extname, name } |
|||
if (fileData && fileData.url) { |
|||
resolve(fileData.url) |
|||
} else { |
|||
console.error('上传成功但未返回URL') |
|||
resolve(null) |
|||
} |
|||
}, |
|||
(error) => { |
|||
// 失败回调 |
|||
console.error('上传请求失败:', error) |
|||
resolve(null) |
|||
} |
|||
) |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 删除图片 |
|||
*/ |
|||
deleteImage(index) { |
|||
uni.showModal({ |
|||
title: '确认删除', |
|||
content: '确定要删除这张图片吗?', |
|||
success: (res) => { |
|||
if (res.confirm) { |
|||
this.imageList.splice(index, 1) |
|||
} |
|||
} |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 取消 |
|||
*/ |
|||
handleCancel() { |
|||
this.$emit('cancel') |
|||
}, |
|||
|
|||
/** |
|||
* 确认提交 |
|||
*/ |
|||
handleConfirm() { |
|||
if (!this.canSubmit) { |
|||
uni.showToast({ title: '请先上传支付凭证', icon: 'none' }) |
|||
return |
|||
} |
|||
|
|||
// 将图片数组转为逗号分隔的字符串 |
|||
const payment_voucher = this.imageList.join(',') |
|||
|
|||
this.$emit('confirm', { |
|||
payment_voucher: payment_voucher, |
|||
order_id: this.orderInfo.id || this.orderInfo._raw?.id |
|||
}) |
|||
}, |
|||
|
|||
/** |
|||
* 重置数据 |
|||
*/ |
|||
reset() { |
|||
this.imageList = [] |
|||
this.uploading = false |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.payment-voucher-wrapper { |
|||
position: fixed; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
z-index: 9998; |
|||
display: flex; |
|||
align-items: flex-end; |
|||
} |
|||
|
|||
.popup-mask { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
background: rgba(0, 0, 0, 0.5); |
|||
z-index: 1; |
|||
} |
|||
|
|||
.payment-voucher-popup { |
|||
position: relative; |
|||
left: 0; |
|||
right: 0; |
|||
bottom: 0; |
|||
width: 100%; |
|||
background: #2A2A2A; |
|||
border-radius: 24rpx 24rpx 0 0; |
|||
overflow: hidden; |
|||
z-index: 2; |
|||
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.3); |
|||
animation: slideUp 0.3s ease; |
|||
} |
|||
|
|||
@keyframes slideUp { |
|||
from { |
|||
transform: translateY(100%); |
|||
} |
|||
to { |
|||
transform: translateY(0); |
|||
} |
|||
} |
|||
|
|||
.popup-content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
max-height: 90vh; |
|||
} |
|||
|
|||
.popup-header { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
padding: 32rpx 40rpx; |
|||
border-bottom: 1px solid #404040; |
|||
} |
|||
|
|||
.popup-title { |
|||
font-size: 32rpx; |
|||
font-weight: 600; |
|||
color: #ffffff; |
|||
} |
|||
|
|||
.close-btn { |
|||
width: 56rpx; |
|||
height: 56rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: rgba(255, 255, 255, 0.1); |
|||
border-radius: 50%; |
|||
transition: all 0.3s ease; |
|||
|
|||
&:active { |
|||
background: rgba(255, 255, 255, 0.15); |
|||
} |
|||
} |
|||
|
|||
.close-icon { |
|||
font-size: 40rpx; |
|||
color: #ffffff; |
|||
line-height: 1; |
|||
} |
|||
|
|||
.popup-body { |
|||
flex: 1; |
|||
padding: 32rpx 40rpx; |
|||
overflow-y: auto; |
|||
} |
|||
|
|||
.order-info-section { |
|||
background: rgba(41, 211, 180, 0.05); |
|||
border-radius: 16rpx; |
|||
padding: 24rpx; |
|||
margin-bottom: 32rpx; |
|||
border-left: 4rpx solid #29D3B4; |
|||
} |
|||
|
|||
.info-row { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
margin-bottom: 16rpx; |
|||
|
|||
&:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
} |
|||
|
|||
.info-label { |
|||
font-size: 28rpx; |
|||
color: #999999; |
|||
min-width: 140rpx; |
|||
} |
|||
|
|||
.info-value { |
|||
font-size: 28rpx; |
|||
color: #ffffff; |
|||
flex: 1; |
|||
text-align: right; |
|||
|
|||
&.amount { |
|||
color: #FFC107; |
|||
font-weight: 600; |
|||
font-size: 32rpx; |
|||
} |
|||
} |
|||
|
|||
.upload-section { |
|||
margin-bottom: 32rpx; |
|||
} |
|||
|
|||
.section-title { |
|||
font-size: 28rpx; |
|||
font-weight: 600; |
|||
color: #ffffff; |
|||
margin-bottom: 12rpx; |
|||
} |
|||
|
|||
.upload-tip { |
|||
font-size: 24rpx; |
|||
color: #999999; |
|||
margin-bottom: 24rpx; |
|||
} |
|||
|
|||
.image-list { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
gap: 16rpx; |
|||
} |
|||
|
|||
.image-item { |
|||
position: relative; |
|||
width: 200rpx; |
|||
height: 200rpx; |
|||
border-radius: 12rpx; |
|||
overflow: hidden; |
|||
} |
|||
|
|||
.preview-image { |
|||
width: 100%; |
|||
height: 100%; |
|||
} |
|||
|
|||
.delete-btn { |
|||
position: absolute; |
|||
top: 8rpx; |
|||
right: 8rpx; |
|||
width: 44rpx; |
|||
height: 44rpx; |
|||
background: rgba(0, 0, 0, 0.6); |
|||
border-radius: 50%; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
|
|||
&:active { |
|||
background: rgba(0, 0, 0, 0.8); |
|||
} |
|||
} |
|||
|
|||
.delete-icon { |
|||
font-size: 32rpx; |
|||
color: #ffffff; |
|||
line-height: 1; |
|||
} |
|||
|
|||
.upload-btn { |
|||
width: 200rpx; |
|||
height: 200rpx; |
|||
border: 2px dashed #404040; |
|||
border-radius: 12rpx; |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: rgba(255, 255, 255, 0.02); |
|||
transition: all 0.3s ease; |
|||
|
|||
&:active { |
|||
background: rgba(255, 255, 255, 0.05); |
|||
border-color: #29D3B4; |
|||
} |
|||
} |
|||
|
|||
.upload-icon { |
|||
font-size: 48rpx; |
|||
color: #666666; |
|||
line-height: 1; |
|||
margin-bottom: 8rpx; |
|||
} |
|||
|
|||
.upload-text { |
|||
font-size: 24rpx; |
|||
color: #999999; |
|||
} |
|||
|
|||
.popup-footer { |
|||
display: flex; |
|||
gap: 24rpx; |
|||
padding: 24rpx 40rpx; |
|||
border-top: 1px solid #404040; |
|||
background: #2A2A2A; |
|||
} |
|||
|
|||
.footer-btn { |
|||
flex: 1; |
|||
height: 88rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-radius: 16rpx; |
|||
font-size: 30rpx; |
|||
font-weight: 500; |
|||
transition: all 0.3s ease; |
|||
} |
|||
|
|||
.cancel-btn { |
|||
background: rgba(255, 255, 255, 0.1); |
|||
color: #ffffff; |
|||
|
|||
&:active { |
|||
background: rgba(255, 255, 255, 0.15); |
|||
} |
|||
} |
|||
|
|||
.confirm-btn { |
|||
background: #29D3B4; |
|||
color: #ffffff; |
|||
|
|||
&:active { |
|||
background: #1fb396; |
|||
} |
|||
|
|||
&.disabled { |
|||
background: #404040; |
|||
color: #666666; |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,207 @@ |
|||
<!--二维码支付弹窗组件--> |
|||
<template> |
|||
<view class="qrcode-payment-modal"> |
|||
<!-- 弹窗头部 --> |
|||
<view class="modal-header"> |
|||
<text class="modal-title">扫码支付</text> |
|||
<view class="close-btn" @click="handleClose"> |
|||
<text>✕</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 订单信息 --> |
|||
<view class="order-info"> |
|||
<view class="info-row"> |
|||
<text class="label">订单号:</text> |
|||
<text class="value">{{ paymentData.order.order_no }}</text> |
|||
</view> |
|||
<view class="info-row"> |
|||
<text class="label">支付金额:</text> |
|||
<text class="amount">¥{{ paymentData.order.total_amount }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 二维码区域 --> |
|||
<view class="qrcode-container"> |
|||
<image |
|||
v-if="paymentData.qrcodeImage" |
|||
:src="paymentData.qrcodeImage" |
|||
class="qrcode-image" |
|||
mode="aspectFit" |
|||
/> |
|||
<text v-else class="qrcode-placeholder">二维码加载中...</text> |
|||
<text class="qrcode-tip">请使用微信扫码完成支付</text> |
|||
</view> |
|||
|
|||
<!-- 操作按钮 --> |
|||
<view class="modal-buttons"> |
|||
<view class="btn secondary" @click="handleClose">取消支付</view> |
|||
<view class="btn primary" @click="handleConfirm">确认已支付</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'QRCodePaymentDialog', |
|||
props: { |
|||
paymentData: { |
|||
type: Object, |
|||
required: true |
|||
} |
|||
}, |
|||
|
|||
methods: { |
|||
handleClose() { |
|||
this.$emit('close') |
|||
}, |
|||
|
|||
handleConfirm() { |
|||
this.$emit('confirm') |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.qrcode-payment-modal { |
|||
background: #2A2A2A; |
|||
border-radius: 24rpx; |
|||
padding: 48rpx; |
|||
width: 600rpx; |
|||
max-width: 90vw; |
|||
} |
|||
|
|||
.modal-header { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 40rpx; |
|||
} |
|||
|
|||
.modal-title { |
|||
font-size: 36rpx; |
|||
font-weight: 600; |
|||
color: #ffffff; |
|||
} |
|||
|
|||
.close-btn { |
|||
width: 60rpx; |
|||
height: 60rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
background: rgba(255, 255, 255, 0.1); |
|||
border-radius: 50%; |
|||
font-size: 32rpx; |
|||
color: #ffffff; |
|||
transition: all 0.3s ease; |
|||
|
|||
&:active { |
|||
background: rgba(255, 255, 255, 0.2); |
|||
} |
|||
} |
|||
|
|||
.order-info { |
|||
background: rgba(255, 255, 255, 0.05); |
|||
border-radius: 16rpx; |
|||
padding: 32rpx; |
|||
margin-bottom: 40rpx; |
|||
} |
|||
|
|||
.info-row { |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
margin-bottom: 20rpx; |
|||
|
|||
&:last-child { |
|||
margin-bottom: 0; |
|||
} |
|||
} |
|||
|
|||
.label { |
|||
font-size: 28rpx; |
|||
color: #999999; |
|||
} |
|||
|
|||
.value { |
|||
font-size: 28rpx; |
|||
color: #ffffff; |
|||
} |
|||
|
|||
.amount { |
|||
font-size: 40rpx; |
|||
font-weight: 600; |
|||
color: #FFC107; |
|||
} |
|||
|
|||
.qrcode-container { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 40rpx; |
|||
background: #ffffff; |
|||
border-radius: 16rpx; |
|||
margin-bottom: 40rpx; |
|||
} |
|||
|
|||
.qrcode-image { |
|||
width: 400rpx; |
|||
height: 400rpx; |
|||
margin-bottom: 24rpx; |
|||
} |
|||
|
|||
.qrcode-placeholder { |
|||
width: 400rpx; |
|||
height: 400rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
font-size: 28rpx; |
|||
color: #999999; |
|||
margin-bottom: 24rpx; |
|||
} |
|||
|
|||
.qrcode-tip { |
|||
font-size: 26rpx; |
|||
color: #666666; |
|||
text-align: center; |
|||
} |
|||
|
|||
.modal-buttons { |
|||
display: flex; |
|||
gap: 24rpx; |
|||
} |
|||
|
|||
.btn { |
|||
flex: 1; |
|||
height: 88rpx; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
border-radius: 44rpx; |
|||
font-size: 30rpx; |
|||
font-weight: 500; |
|||
transition: all 0.3s ease; |
|||
|
|||
&.secondary { |
|||
background: rgba(255, 255, 255, 0.1); |
|||
color: #ffffff; |
|||
|
|||
&:active { |
|||
background: rgba(255, 255, 255, 0.15); |
|||
} |
|||
} |
|||
|
|||
&.primary { |
|||
background: #29D3B4; |
|||
color: #ffffff; |
|||
|
|||
&:active { |
|||
background: #1fb396; |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
Loading…
Reference in new issue