2025-08-25 14:17:06 +08:00

667 lines
15 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>
<view class="invoice-detail-page">
<!-- 自定义导航栏 -->
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @click="goBack">
<uni-icons type="left" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">开票详情</view>
<view class="nav-right"></view>
</view>
</view>
<!-- 主内容区域 -->
<view class="main-content" :style="{ paddingTop: navBarHeight + 'px' }">
<!-- 加载状态 -->
<view v-if="isLoading" class="loading-container">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 错误状态 -->
<view v-else-if="hasError" class="error-container">
<text class="error-icon">!</text>
<text class="error-text">加载失败请重试</text>
<view class="retry-btn" @click="retryLoad">
<text class="retry-text">重新加载</text>
</view>
</view>
<!-- 正常内容 -->
<template v-else>
<!-- 发票状态指示器 -->
<view class="status-indicator" :class="getStatusClass(invoiceDetail.status)">
<view class="status-dot" :class="getStatusClass(invoiceDetail.status)"></view>
<text class="status-text">{{ getStatusText(invoiceDetail.status) }}</text>
</view>
<!-- 发票详情卡片 -->
<view class="detail-card">
<!-- 发票抬头 -->
<view class="detail-item">
<text class="item-label">发票抬头</text>
<text class="item-value">{{ invoiceDetail.title }}</text>
</view>
<!-- 单位税号 -->
<view class="detail-item">
<text class="item-label">单位税号</text>
<text class="item-value">{{ invoiceDetail.taxId }}</text>
</view>
<!-- 发票内容 -->
<view class="detail-item">
<text class="item-label">发票内容</text>
<text class="item-value">{{ invoiceDetail.content }}</text>
</view>
<!-- 发票金额 -->
<view class="detail-item">
<text class="item-label">发票金额</text>
<text class="item-value amount">{{ invoiceDetail.amount }}</text>
</view>
<!-- 电子邮箱 -->
<view class="detail-item">
<text class="item-label">电子邮箱</text>
<text class="item-value">{{ invoiceDetail.email }}</text>
</view>
<!-- 创建日期 -->
<view class="detail-item">
<text class="item-label">创建日期</text>
<text class="item-value">{{ invoiceDetail.createDate }}</text>
</view>
<!-- 备注 -->
<view class="detail-item" v-if="invoiceDetail.note">
<text class="item-label">备注</text>
<text class="item-value">{{ invoiceDetail.note }}</text>
</view>
</view>
<!-- 相关课程 -->
<view class="related-courses">
<view class="section-title">
<view class="title-icon"></view>
<text class="title-text">相关课程</text>
</view>
<view v-if="relatedCourses.length > 0" class="course-list">
<view
class="course-item"
v-for="(course, index) in relatedCourses"
:key="index"
>
<view class="course-bullet"></view>
<image class="course-image" :src="course.image" mode="aspectFill"></image>
<view class="course-info">
<text class="course-desc">{{ course.description }}</text>
<text class="course-price">实际支付: {{ course.price }}</text>
</view>
</view>
</view>
<view v-else class="empty-courses">
<text class="empty-text">暂无相关课程信息</text>
</view>
</view>
</template>
</view>
<!-- 底部下载按钮 -->
<view class="download-section" v-if="!isLoading && !hasError">
<view class="download-btn" @click="downloadInvoice">
<text class="download-text">下载发票</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import course_api from "@/api/course_api.js"
const invoice_id = ref('')
// 响应式数据
const statusBarHeight = ref(0)
const navBarHeight = ref(88)
const invoiceDetail = ref({
title: '',
taxId: '',
content: '',
amount: '',
email: '',
status: 1,
downUrl: '',
createDate: '',
note: ''
})
const relatedCourses = ref([])
const isLoading = ref(true)
const hasError = ref(false)
// 基础方法
const getSystemInfo = () => {
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight
// #ifdef MP-WEIXIN
navBarHeight.value = systemInfo.statusBarHeight + 44
// #endif
// #ifdef APP-PLUS
navBarHeight.value = systemInfo.statusBarHeight + 44
// #endif
}
const goBack = () => {
uni.navigateBack()
}
const downloadInvoice = () => {
// 检查是否有下载链接
if (invoiceDetail.value.downUrl) {
uni.showToast({
title: '正在打开下载链接...',
icon: 'loading'
})
// 使用uni.downloadFile下载文件
uni.downloadFile({
url: invoiceDetail.value.downUrl,
success: (res) => {
if (res.statusCode === 200) {
uni.showToast({
title: '下载成功',
icon: 'success'
})
// 保存文件到本地
uni.saveFile({
tempFilePath: res.tempFilePath,
success: (saveRes) => {
console.log('文件保存成功:', saveRes.savedFilePath)
},
fail: (err) => {
console.error('文件保存失败:', err)
}
})
}
},
fail: (err) => {
console.error('下载失败:', err)
uni.showToast({
title: '下载失败,请重试',
icon: 'none'
})
}
})
} else {
uni.showToast({
title: '暂无下载链接',
icon: 'none'
})
}
}
const retryLoad = () => {
isLoading.value = true
hasError.value = false
// 重新加载数据
setTimeout(() => {
loadInvoiceData()
}, 500)
}
// 获取状态样式类
const getStatusClass = (status) => {
switch (status) {
case 1:
return 'success'
case 0:
return 'processing'
case 2:
return 'failed'
default:
return 'success'
}
}
// 获取状态文本
const getStatusText = (status) => {
switch (status) {
case 1:
return '发票已开具'
case 0:
return '开票中'
case 2:
return '信息有误'
default:
return '发票已开具'
}
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return ''
try {
const date = new Date(dateStr)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
} catch (error) {
console.error('日期格式化失败:', error)
return dateStr
}
}
const loadInvoiceData = () => {
course_api.getExcellencourseOrderInvoice(invoice_id.value).then(res => {
console.log(res)
if (res.code === 200 && res.data) {
const data = res.data
// 更新发票详情数据
invoiceDetail.value = {
title: data.head || '欣欣相照科技股份有限公司',
taxId: data.tax_number || '22913u9u4923u293',
content: data.type || '培训费',
amount: `¥${data.amount?.toFixed(2) || '0.00'}`,
email: data.email || '153746774@qq.com',
status: data.status || 1,
downUrl: data.down_url || '',
createDate: formatDate(data.create_date) || '',
note: data.note || ''
}
// 更新相关课程数据
if (data.excellencourseList && data.excellencourseList.length > 0) {
relatedCourses.value = data.excellencourseList.map(course => ({
image: course.excellencourse_img || 'https://via.placeholder.com/120x80/ff6b35/ffffff?text=课程',
description: course.excellencourse_title || '找到已购买课程即可学习',
price: `¥${course.account?.toFixed(2) || '0.00'}`
}))
} else {
// 如果没有课程数据,使用默认数据
relatedCourses.value = [
{
image: 'https://via.placeholder.com/120x80/ff6b35/ffffff?text=课程',
description: '找到已购买课程即可学习',
price: '¥39'
}
]
}
isLoading.value = false
} else {
console.error('获取发票详情失败:', res.msg || '未知错误')
hasError.value = true
isLoading.value = false
}
}).catch(err => {
console.error('获取发票详情失败:', err)
hasError.value = true
isLoading.value = false
})
}
onMounted(() => {
getSystemInfo()
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
invoice_id.value = options.invoice_id
if (!invoice_id.value) {
hasError.value = true
isLoading.value = false
uni.showToast({
title: '缺少发票ID参数',
icon: 'none'
})
return
}
// 加载发票数据
loadInvoiceData()
// 设置页面标题
uni.setNavigationBarTitle({
title: '开票详情'
})
})
</script>
<style lang="scss" scoped>
.invoice-detail-page {
min-height: 100vh;
background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
position: relative;
}
.navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background-color: #fff;
border-bottom: 1rpx solid #e5e5e5;
.nav-content {
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32rpx;
.nav-left, .nav-right {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
.nav-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
}
.main-content {
flex: 1;
padding: 30rpx;
margin-bottom: 120rpx;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
.loading-spinner {
width: 80rpx;
height: 80rpx;
border: 6rpx solid #f3f3f3;
border-top: 6rpx solid #ff6b35;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 30rpx;
}
.loading-text {
font-size: 28rpx;
color: #666666;
}
}
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
.error-icon {
width: 120rpx;
height: 120rpx;
background: #ff6b35;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
font-size: 60rpx;
font-weight: bold;
margin-bottom: 30rpx;
}
.error-text {
font-size: 28rpx;
color: #666666;
margin-bottom: 40rpx;
}
.retry-btn {
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
border-radius: 12rpx;
padding: 20rpx 40rpx;
.retry-text {
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
}
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.status-indicator {
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
padding: 20rpx 30rpx;
margin-bottom: 30rpx;
&.success {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
box-shadow: 0 4rpx 16rpx rgba(82, 196, 26, 0.3);
}
&.processing {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
box-shadow: 0 4rpx 16rpx rgba(24, 144, 255, 0.3);
}
&.failed {
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
box-shadow: 0 4rpx 16rpx rgba(255, 77, 79, 0.3);
}
.status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 15rpx;
&.success {
background: #ffffff;
box-shadow: 0 0 0 4rpx rgba(255, 255, 255, 0.3);
}
&.processing {
background: #ffffff;
box-shadow: 0 0 0 4rpx rgba(255, 255, 255, 0.3);
}
&.failed {
background: #ffffff;
box-shadow: 0 0 0 4rpx rgba(255, 255, 255, 0.3);
}
}
.status-text {
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
}
}
.detail-card {
background: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.item-label {
font-size: 28rpx;
color: #666666;
font-weight: 500;
}
.item-value {
font-size: 28rpx;
color: #333333;
font-weight: 500;
max-width: 400rpx;
text-align: right;
&.amount {
color: #ff6b35;
font-weight: 600;
font-size: 32rpx;
}
}
}
.related-courses {
background: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.title-icon {
width: 8rpx;
height: 32rpx;
background: #ff6b35;
border-radius: 4rpx;
margin-right: 20rpx;
}
.title-text {
font-size: 32rpx;
font-weight: 600;
color: #333333;
}
}
.course-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.course-item {
display: flex;
align-items: center;
padding: 20rpx 0;
.course-bullet {
width: 16rpx;
height: 16rpx;
background: #ff6b35;
border-radius: 50%;
margin-right: 20rpx;
}
.course-image {
width: 120rpx;
height: 80rpx;
border-radius: 8rpx;
margin-right: 20rpx;
background-color: #f0f0f0;
}
.course-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 10rpx;
.course-desc {
font-size: 26rpx;
color: #333333;
line-height: 1.4;
}
.course-price {
font-size: 24rpx;
color: #ff6b35;
font-weight: 500;
}
}
}
.empty-courses {
display: flex;
align-items: center;
justify-content: center;
padding: 60rpx 0;
.empty-text {
font-size: 28rpx;
color: #999999;
}
}
.download-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 30rpx;
background: #ffffff;
border-top: 1rpx solid #e9ecef;
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.download-btn {
background: linear-gradient(135deg, #20c997 0%, #17a2b8 100%);
border-radius: 12rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
opacity: 0.9;
}
.download-text {
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
}
}
</style>