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

713 lines
16 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-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 class="info-btn" @click="showInfo">
<uni-icons type="info" size="20" color="#666"></uni-icons>
</view>
</view>
</view>
</view>
<!-- 标签页 -->
<view class="tabs" :style="{ paddingTop: navBarHeight + 'px' }">
<view
class="tab-item"
:class="{ active: activeTab === 'pending' }"
@click="switchTab('pending')"
>
<text>待开发票</text>
</view>
<view class="tab-divider"></view>
<view
class="tab-item"
:class="{ active: activeTab === 'history' }"
@click="switchTab('history')"
>
<text>开票历史</text>
</view>
</view>
<!-- 待开发票内容 -->
<view v-if="activeTab === 'pending'" class="pending-content">
<!-- 课程列表 -->
<scroll-view class="course-list" scroll-y>
<view
class="course-item"
v-for="(course, index) in courseList"
:key="index"
@click="toggleSelect(index)"
>
<view class="checkbox" :class="{ selected: course.selected }">
<text v-if="course.selected" class="checkmark"></text>
</view>
<image class="course-image" :src="course.image" mode="aspectFill"></image>
<view class="course-info">
<text class="course-desc">{{ course.description }}</text>
<view class="course-meta">
<text class="course-type">{{ course.typeName }}</text>
<text class="course-price">实际支付: ¥{{ course.price }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="select-all" @click="toggleSelectAll">
<view class="checkbox" :class="{ selected: isAllSelected }">
<text v-if="isAllSelected" class="checkmark"></text>
</view>
<text class="select-all-text">全选</text>
</view>
<view class="invoice-info">
<text class="invoice-amount">开票金额: ¥{{ totalAmount }}</text>
<text class="selected-count">(已选{{ selectedCount }}个课程)</text>
</view>
<view class="invoice-btn" @click="issueInvoice">
<text>开具发票</text>
</view>
</view>
</view>
<!-- 开票历史内容 -->
<view v-if="activeTab === 'history'" class="history-content">
<scroll-view class="history-list" scroll-y>
<view
class="history-item"
v-for="(item, index) in historyList"
:key="index"
@click="viewHistoryDetail(item)"
>
<view class="history-left">
<text class="history-date">{{ item.date }}</text>
<text class="history-count">{{ item.courseCount }}</text>
</view>
<view class="history-right">
<text class="history-status" :class="item.status">{{ item.statusText }}</text>
<view class="history-arrow"></view>
<text class="history-amount">{{ item.amount }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import course_api from "@/api/course_api.js"
// 响应式数据
const statusBarHeight = ref(0)
const navBarHeight = ref(88)
const activeTab = ref('pending')
const courseList = ref([])
// 开票历史数据
const historyList = ref([])
// 计算属性
const selectedCount = computed(() => {
return courseList.value.filter(course => course.selected).length
})
const totalAmount = computed(() => {
return courseList.value
.filter(course => course.selected)
.reduce((sum, course) => sum + course.price, 0)
.toFixed(2)
})
const isAllSelected = computed(() => {
return courseList.value.length > 0 && courseList.value.every(course => course.selected)
})
// 方法
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 showInfo = () => {
uni.showToast({
title: '发票说明',
icon: 'none'
})
}
const switchTab = (tab) => {
activeTab.value = tab
if (tab === 'history') {
course_api.listExcellencourseOrderInvoiceHistory(1).then(res => {
console.log(res)
if (res.code === 200 && res.data && res.data.list) {
// 将API返回的开票历史数据映射到组件使用的数据结构
historyList.value = res.data.list.map(item => ({
id: item.id,
date: formatDate(item.create_date),
courseCount: `共计${item.num}个课程`,
status: getStatusClass(item.status),
statusText: getStatusText(item.status),
amount: `¥${item.amount.toFixed(2)}`
}))
}
}).catch(err => {
console.error('获取开票历史失败:', err)
uni.showToast({
title: '获取开票历史失败',
icon: 'none'
})
})
}
}
// 格式化日期
const formatDate = (dateStr) => {
const date = new Date(dateStr)
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
return `${year}${month}${day}`
}
// 获取状态样式类
const getStatusClass = (status) => {
switch (status) {
case 1:
return 'success'
case 0:
return 'processing'
case 2:
return 'failed'
default:
return 'failed'
}
}
// 获取状态文本
const getStatusText = (status) => {
switch (status) {
case 1:
return '已完成'
case 0:
return '开票中'
case 2:
return '信息有误'
default:
return '开票失败'
}
}
const toggleSelect = (index) => {
courseList.value[index].selected = !courseList.value[index].selected
}
const toggleSelectAll = () => {
const newState = !isAllSelected.value
courseList.value.forEach(course => {
course.selected = newState
})
}
const issueInvoice = () => {
if (selectedCount.value === 0) {
uni.showToast({
title: '请选择要开票的课程',
icon: 'none'
})
return
}
// 跳转到填写发票信息页面
uni.navigateTo({
url: '/pages_course/invoice_info/invoice_info'
})
}
// 查看开票历史详情
const viewHistoryDetail = (item) => {
console.log('查看开票历史详情:', item)
// 跳转到发票详情页面
uni.navigateTo({
url: `/pages_course/invoice_detail/invoice_detail?invoice_id=${item.id}`
})
}
onMounted(() => {
getSystemInfo()
course_api.listExcellencourseOrderNoInvoice().then(res => {
console.log(res)
if (res.code === 200 && res.data) {
// 将API返回的数据映射到组件使用的数据结构
courseList.value = res.data.map(item => ({
id: item.uuid,
description: item.excellencourse_title,
price: item.account,
image: item.excellencourse_img,
selected: false,
typeName: item.type_name
}))
}
}).catch(err => {
console.error('获取课程列表失败:', err)
uni.showToast({
title: '获取课程列表失败',
icon: 'none'
})
})
})
</script>
<style lang="scss" scoped>
.invoice-page {
min-height: 100vh;
background: linear-gradient(180deg, #f8f9fa 0%, #ffffff 100%);
position: relative;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx 0;
height: 88rpx;
.time {
font-size: 32rpx;
font-weight: 600;
color: #000000;
}
.status-icons {
display: flex;
align-items: center;
gap: 8rpx;
.signal, .wifi, .battery {
width: 24rpx;
height: 24rpx;
background-color: #000000;
border-radius: 2rpx;
}
}
}
.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;
}
.info-btn {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
}
}
}
.tabs {
display: flex;
align-items: center;
padding: 0 30rpx;
height: 88rpx;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-bottom: 1rpx solid #e9ecef;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
position: relative;
cursor: pointer;
transition: all 0.3s ease;
text {
font-size: 32rpx;
color: #6c757d;
font-weight: 500;
transition: all 0.3s ease;
position: relative;
z-index: 2;
}
&.active {
text {
color: #ff6b35;
font-weight: 600;
}
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 120rpx;
height: 60rpx;
// background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
// border-radius: 30rpx;
z-index: 1;
// box-shadow: 0 4rpx 16rpx rgba(255, 107, 53, 0.3);
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 4rpx;
background: linear-gradient(90deg, #ff6b35 0%, #ff8c42 100%);
border-radius: 2rpx;
z-index: 1;
}
}
&:not(.active):hover {
text {
color: #ff6b35;
}
}
}
.tab-divider {
width: 2rpx;
height: 40rpx;
background: linear-gradient(180deg, #dee2e6 0%, #adb5bd 100%);
margin: 0 20rpx;
border-radius: 1rpx;
}
}
.course-list {
flex: 1;
padding: 0 30rpx;
margin-bottom: 120rpx;
}
.course-item {
display: flex;
align-items: center;
padding: 30rpx 0;
border-bottom: 1rpx solid #f5f5f5;
.checkbox {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #e0e0e0;
border-radius: 50%;
margin-right: 20rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffffff;
transition: all 0.3s;
&.selected {
background-color: #ff6b35;
border-color: #ff6b35;
.checkmark {
color: #ffffff;
font-size: 24rpx;
font-weight: bold;
}
}
}
.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: 28rpx;
color: #333333;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.course-meta {
display: flex;
align-items: center;
gap: 20rpx;
margin-top: 5rpx;
.course-type {
font-size: 24rpx;
color: #6c757d;
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-weight: 500;
}
.course-price {
font-size: 28rpx;
color: #ff0000;
font-weight: 600;
}
}
}
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 120rpx;
background-color: #ffffff;
border-top: 1rpx solid #f0f0f0;
display: flex;
align-items: center;
padding: 0 30rpx;
gap: 30rpx;
.select-all {
display: flex;
align-items: center;
gap: 15rpx;
.checkbox {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #e0e0e0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffffff;
transition: all 0.3s;
&.selected {
background-color: #ff6b35;
border-color: #ff6b35;
.checkmark {
color: #ffffff;
font-size: 24rpx;
font-weight: bold;
}
}
}
.select-all-text {
font-size: 28rpx;
color: #333333;
}
}
.invoice-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 5rpx;
.invoice-amount {
font-size: 32rpx;
color: #ff0000;
font-weight: 600;
}
.selected-count {
font-size: 24rpx;
color: #999999;
}
}
.invoice-btn {
background-color: #ff0000;
border-radius: 8rpx;
padding: 20rpx 40rpx;
text {
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
}
}
}
// 开票历史样式
.history-content {
flex: 1;
padding: 30rpx;
margin-bottom: 120rpx;
}
.history-list {
height: 100%;
}
.history-item {
display: flex;
justify-content: space-between;
align-items: center;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border-radius: 16rpx;
margin-bottom: 24rpx;
padding: 32rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
border: 1rpx solid #f0f0f0;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
background: linear-gradient(180deg, #ff6b35 0%, #ff8c42 100%);
border-radius: 0 3rpx 3rpx 0;
}
&:active {
transform: translateY(2rpx);
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.12);
}
.history-left {
display: flex;
flex-direction: column;
gap: 12rpx;
flex: 1;
.history-date {
font-size: 30rpx;
color: #2c3e50;
font-weight: 600;
letter-spacing: 0.5rpx;
}
.history-count {
font-size: 26rpx;
color: #7f8c8d;
font-weight: 400;
}
}
.history-right {
display: flex;
align-items: center;
gap: 20rpx;
.history-status {
font-size: 26rpx;
font-weight: 500;
padding: 8rpx 16rpx;
border-radius: 20rpx;
min-width: 80rpx;
text-align: center;
&.success {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #ffffff;
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
}
&.failed {
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
color: #ffffff;
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
}
&.processing {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
color: #ffffff;
box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.3);
}
}
.history-arrow {
font-size: 28rpx;
color: #bdc3c7;
font-weight: 300;
margin: 0 8rpx;
}
.history-amount {
font-size: 28rpx;
color: #e74c3c;
font-weight: 700;
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
}
</style>