精品课

This commit is contained in:
haomingming
2025-08-25 14:17:06 +08:00
parent 979c332454
commit 0de0b6732b
22 changed files with 3392 additions and 716 deletions
+34 -241
View File
@@ -28,7 +28,6 @@
<image :src="banner.sroll_img || banner.image || '/static/lunbo_bg.png'" mode="aspectFill" class="banner-image"></image>
<view class="banner-content">
<text class="banner-title">{{ banner.title || '课程标题' }}</text>
<text class="banner-subtitle">{{ banner.subtitle || '课程描述' }}</text>
</view>
</swiper-item>
</swiper>
@@ -54,7 +53,7 @@
:key="typeItem.id"
@click="goVideoType(typeItem.id, typeItem.name)">
<view class="function-icon">
<image :src="typeItem.img" mode="aspectFit"></image>
<image style="height: 50rpx;" :src="typeItem.img" mode="aspectFit"></image>
<!-- <uni-icons :type="getFunctionIconType(index)" size="24" :color="getFunctionIconColor(index)"></uni-icons> -->
</view>
<text class="function-text">{{ typeItem.name }}</text>
@@ -65,7 +64,7 @@
<view class="section">
<view class="section-header">
<text class="section-title">精品小课</text>
<view class="more-btn" @click="goMorePremium">
<view class="more-btn" @click="goMorePremium('精品小课')">
<text>更多</text>
<uni-icons type="arrowright" size="14" color="#999"></uni-icons>
</view>
@@ -80,7 +79,7 @@
<text class="course-title">{{ course.title }}</text>
<view class="course-meta">
<view class="participant-count">{{ course.study_num }}人</view>
<text class="course-price" v-if="course.discount_price > 0">¥{{ course.discount_price }}</text>
<text class="course-price" v-if="course.discount_price > 0">¥{{ (course.discount_price / 100).toFixed(2) }}</text>
<text class="course-price free" v-else>免费</text>
</view>
</view>
@@ -95,7 +94,7 @@
<view class="section">
<view class="section-header">
<text class="section-title">学完返现</text>
<view class="more-btn" @click="goMoreRewards">
<view class="more-btn" @click="goMoreRewards('学完返现')">
<text>更多</text>
<uni-icons type="arrowright" size="14" color="#999"></uni-icons>
</view>
@@ -114,7 +113,7 @@
<view class="section">
<view class="section-header">
<text class="section-title">福利课堂</text>
<view class="more-btn" @click="goMoreWelfare">
<view class="more-btn" @click="goMoreWelfare('福利课堂')">
<text>更多</text>
<uni-icons type="arrowright" size="14" color="#999"></uni-icons>
</view>
@@ -139,11 +138,11 @@
</scroll-view>
</view>
<!-- 热门课程 -->
<!-- 课程推荐 -->
<view class="section">
<view class="section-header">
<text class="section-title">热门课程</text>
<view class="more-btn" @click="goMoreHot">
<text class="section-title">课程推荐</text>
<view class="more-btn" @click="goMoreHot('课程推荐')">
<text>更多</text>
<uni-icons type="arrowright" size="14" color="#999"></uni-icons>
</view>
@@ -154,7 +153,7 @@
<view class="course-content">
<text class="course-name">{{ course.title }}</text>
<text class="course-desc">{{ course.tags || '' }}</text>
<text class="course-cost">¥{{ course.discount_price }}</text>
<text class="course-cost">¥{{ (course.discount_price / 100).toFixed(2) }}</text>
</view>
</view>
</view>
@@ -188,115 +187,7 @@ const premiumCourseList = ref([]) // 新增:精品小课数据
const rewardCourseList = ref([]) // 新增:学完返现数据
const welfareCourseList = ref([]) // 新增:福利课堂数据
const recommendedCourseList = ref([]) // 新增:课程推荐数据
const bannerList = ref([
{
id: 1,
title: "小懂医生讲HIV和感染",
subtitle: "授课专家: 黄湛镰 副主任医师 | 中山大学附属第三医院感染科",
image: "/static/lunbo_bg.png",
link: "/pages_course/hiv_detail"
},
{
id: 2,
title: "肝脏肿瘤临床影像学习",
subtitle: "授课专家: 王学浩 教授 | 南京医科大学第一附属医院",
image: "/static/paper_bg.png",
link: "/pages_course/tumor_detail"
},
{
id: 3,
title: "慢性肝病营养治疗指南",
subtitle: "授课专家: 段钟平 教授 | 首都医科大学附属北京佑安医院",
image: "/static/pap_bg.png",
link: "/pages_course/nutrition_detail"
},
{
id: 4,
title: "肝移植术后管理要点",
subtitle: "授课专家: 郑树森 院士 | 浙江大学医学院附属第一医院",
image: "/static/bo_bg.png",
link: "/pages_course/transplant_detail"
},
{
id: 5,
title: "肝癌早期筛查与预防",
subtitle: "授课专家: 陈孝平 院士 | 华中科技大学同济医学院",
image: "/static/livebg.png",
link: "/pages_course/screening_detail"
}
])
const hotCourses = ref([
{
id: 1,
title: "小懂医生讲HIV和感染-各类抗菌药特性解读III",
description: "小懂医生讲HIV和感染-各类抗菌药特性解读III",
price: "20.00",
thumbnail: "/static/jingpinkecheng.png"
},
{
id: 2,
title: "小懂医生讲HIV和感染-各类抗菌药特性解读III",
description: "小懂医生讲HIV和感染-各类抗菌药特性解读III",
price: "20.00",
thumbnail: "/static/jingpinkecheng.png"
},
{
id: 3,
title: "小懂医生讲HIV和感染-各类抗菌药特性解读III",
description: "小懂医生讲HIV和感染-各类抗菌药特性解读III",
price: "20.00",
thumbnail: "/static/jingpinkecheng.png"
},
{
id: 4,
title: "小懂医生讲HIV和感染物",
description: "",
price: "50.00",
thumbnail: "/static/jingpinkecheng.png"
}
])
const welfareList = ref([
{
id: 1,
title: "《中生肝脏病学》云解读系列会七",
subtitle: "汪晖 院士 北京大学医学院",
teacher: "汪晖 院士",
price: "免费",
image: "/static/fulicard.png"
},
{
id: 2,
title: "肝病诊疗新进展研讨会",
subtitle: "王学浩 教授 南京医科大学",
teacher: "王学浩 教授",
price: "免费",
image: "/static/paper_bg.png"
},
{
id: 3,
title: "肝癌早期筛查与预防",
subtitle: "陈孝平 院士 华中科技大学",
teacher: "陈孝平 院士",
price: "免费",
image: "/static/pap_bg.png"
},
{
id: 4,
title: "慢性肝病营养治疗指南",
subtitle: "段钟平 教授 首都医科大学",
teacher: "段钟平 教授",
price: "免费",
image: "/static/bo_bg.png"
},
{
id: 5,
title: "肝移植术后管理要点",
subtitle: "郑树森 院士 浙江大学医学院",
teacher: "郑树森 院士",
price: "免费",
image: "/static/livebg.png"
}
])
const bannerList = ref([])
// 方法
const getSystemInfo = () => {
@@ -311,7 +202,9 @@ const getSystemInfo = () => {
}
const goBack = () => {
uni.navigateBack()
uni.navigateTo({
url: '/pages/index/index'
})
}
const goSearch = () => {
@@ -322,6 +215,9 @@ const goSearch = () => {
const goCategory = (type, name) => {
console.log('跳转分类:', type, name)
uni.navigateTo({
url: `/pages_course/course_filter/course_filter?type=${type}`
})
}
const getCategoryIconClass = (index) => {
@@ -337,29 +233,6 @@ const getCategoryIconClass = (index) => {
}
}
const getFunctionIconClass = (index) => {
switch (index) {
case 0:
return 'fire-icon';
case 1:
return 'star-icon';
case 2:
return 'gift-icon';
case 3:
return 'heart-icon';
default:
return '';
}
}
const goHotCourses = () => {
console.log('跳转热门课程')
}
const goPremiumCourses = () => {
console.log('跳转精品小课')
}
const goPremiumCourse = (course) => {
console.log('跳转精品小课详情:', course)
@@ -369,9 +242,6 @@ const goPremiumCourse = (course) => {
})
}
const goStudyRewards = () => {
console.log('跳转学克返现')
}
const goRewardCourse = (course) => {
console.log('跳转返现课程:', course)
@@ -381,24 +251,32 @@ const goRewardCourse = (course) => {
})
}
const goWelfareCourses = () => {
console.log('跳转福利课堂')
}
const goMorePremium = () => {
const goMorePremium = (name) => {
console.log('查看更多精品课程')
uni.navigateTo({
url: `/pages_course/course_filter/course_filter?special_type=${specialTypeList.value.find(item => item.name == name).id}&special_name=${name}`
})
}
const goMoreRewards = () => {
const goMoreRewards = (name) => {
console.log('查看更多返现')
uni.navigateTo({
url: `/pages_course/course_filter/course_filter?special_type=${specialTypeList.value.find(item => item.name == name).id}&special_name=${name}`
})
}
const goMoreWelfare = () => {
const goMoreWelfare = (name) => {
console.log('查看更多福利')
uni.navigateTo({
url: `/pages_course/course_filter/course_filter?special_type=${specialTypeList.value.find(item => item.name == name).id}&special_name=${name}`
})
}
const goMoreHot = () => {
const goMoreHot = (name) => {
console.log('查看更多热门')
uni.navigateTo({
url: `/pages_course/course_filter/course_filter?special_type=${specialTypeList.value.find(item => item.name == name).id}&special_name=${name}`
})
}
const goCourseDetail = (course) => {
@@ -421,7 +299,7 @@ const goBannerDetail = (banner) => {
console.log('横幅详情:', banner)
// 跳转到课程详情页面
uni.navigateTo({
url: '/pages_course/course_detail/course_detail'
url: '/pages_course/course_detail/course_detail?id=' + banner.id
})
}
@@ -487,104 +365,19 @@ const getIndex = async () => {
}
} else {
console.warn('API返回数据格式异常:', res)
setDefaultData()
}
} catch (error) {
console.error('获取分类数据失败:', error)
setDefaultData()
}
}
// 设置默认数据
const setDefaultData = () => {
// 设置默认分类数据作为备用
firstTypeList.value = [
{ id: 185, name: "肝病", img: "/static/icon_home_my_patient.png" },
{ id: 188, name: "肿瘤", img: "/static/icon_home_my_library.png" },
{ id: 195, name: "感染", img: "/static/icon_home_video.png" }
]
// 设置默认功能按钮数据作为备用
specialTypeList.value = [
{ type_id: 189, type_name: "课程推荐" },
{ type_id: 190, type_name: "精品小课" },
{ type_id: 191, type_name: "学完返现" },
{ type_id: 192, type_name: "福利课堂" }
]
// 设置默认精品小课数据作为备用
premiumCourseList.value = [
{
id: 1,
title: "肝脏胖瘦临床、影像学习交流",
study_num: 28,
discount_price: 20,
index_img: "/static/jingpingke.png"
}
]
// 设置默认学完返现数据作为备用
rewardCourseList.value = [
{
id: 1,
title: "重症肝病的抗凝治疗",
discount_price: 2,
back_bon: 2,
index_img: "/static/paper_bg.png"
},
{
id: 2,
title: "重症肝病的抗凝治疗2",
discount_price: 2,
back_bon: 2,
index_img: "/static/pap_bg.png"
}
]
// 设置默认福利课堂数据作为备用
welfareCourseList.value = [
{
id: 1,
title: "《中生肝脏病学》云解读系列会七",
special_type_name: "福利课堂",
study_num: 8,
fuli_bon: 100,
index_img: "/static/fulicard.png"
},
{
id: 2,
title: "肝病诊疗新进展研讨会",
special_type_name: "福利课堂",
study_num: 5,
fuli_bon: 88,
index_img: "/static/paper_bg.png"
}
]
// 设置默认课程推荐数据作为备用
recommendedCourseList.value = [
{
id: 1,
title: "测试精品课上报02",
tags: "测试,肝胆相照",
discount_price: 100,
index_img: "/static/jingpinkecheng.png"
},
{
id: 2,
title: "测试精品课上报01",
tags: "肝胆相照,健康中国",
discount_price: 100,
index_img: "/static/jingpinkecheng.png"
}
]
}
const goVideoType = (typeId, typeName) => {
console.log('跳转视频类型:', typeId, typeName)
// 跳转到视频类型详情页面
uni.navigateTo({
url: `/pages_course/video_type_detail/video_type_detail?type_id=${typeId}`
url: `/pages_course/course_filter/course_filter?special_type=${typeId}&special_name=${typeName}`
})
}
// 生命周期
+75 -41
View File
@@ -6,7 +6,7 @@
<view class="nav-left" @click="goBack">
<uni-icons type="left" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">课程详情02</view>
<view class="nav-title">{{ courseDetail.title || '肝硬化与重症肝病' }}</view>
<view class="nav-right" @click="goShare">
<uni-icons type="redo" size="24" color="#333"></uni-icons>
</view>
@@ -106,7 +106,7 @@
<!-- 课程目录内容 -->
<view class="course-catalog" v-if="activeTab === 'catalog'">
<!-- 课程概览 -->
<view class="catalog-overview">
<!-- <view class="catalog-overview">
<view class="overview-item">
<text class="overview-label">总课时</text>
<text class="overview-value">{{ courseDetail.video_num || 0 }}节课</text>
@@ -119,7 +119,7 @@
<text class="overview-label">学习进度</text>
<text class="overview-value">{{ courseDetail.study_status || 0 }}%</text>
</view>
</view>
</view> -->
<!-- 课程章节列表 -->
<view class="chapter-list">
@@ -130,14 +130,14 @@
<uni-icons :type="chapter.expanded ? 'down' : 'right'" size="16" color="#666"></uni-icons>
<text class="chapter-name">{{ chapter.title }}</text>
</view>
<view class="chapter-meta">
<!-- <view class="chapter-meta">
<text class="lesson-count">{{ chapter.lessons.length }}节课</text>
<text class="duration">{{ chapter.duration }}</text>
</view>
</view> -->
</view>
<view class="chapter-status">
<!-- <view class="chapter-status">
<text class="status-text" :class="chapter.status">{{ chapter.statusText }}</text>
</view>
</view> -->
</view>
<!-- 课时列表 -->
@@ -150,7 +150,7 @@
</view>
<view class="lesson-meta">
<text class="lesson-duration">{{ lesson.duration }}</text>
<text class="lesson-teacher">{{ lesson.teacher }}</text>
<!-- <text class="lesson-teacher">{{ lesson.teacher }}</text> -->
</view>
</view>
<view class="lesson-status">
@@ -168,7 +168,9 @@
<text class="review-title">课程评价</text>
<view class="review-actions-header">
<text class="review-count">共{{ reviewTotal || courseDetail.comment_num || 0 }}条评价</text>
<button class="write-review-btn" @click="goToReview" v-if="courseDetail.is_buy === 1">写评价</button>
<button class="write-review-btn" @click="goToReview" v-if="courseDetail.is_buy === 1">
{{ courseDetail.is_commented === 1 ? '追加评论' : '写评价' }}
</button>
</view>
</view>
@@ -254,7 +256,15 @@
<uni-icons type="chat" size="80" color="#E0E0E0"></uni-icons>
</view>
<text class="empty-text">暂无评价</text>
<text class="empty-desc">成为第一个评价的人吧</text>
<text class="empty-desc" v-if="courseDetail.is_buy === 1 && courseDetail.is_commented === 1">
您已经评价过此课程,可以追加评论
</text>
<text class="empty-desc" v-else-if="courseDetail.is_buy === 1">
成为第一个评价的人吧
</text>
<text class="empty-desc" v-else>
购买课程后即可评价
</text>
</view>
<!-- 加载中状态 -->
@@ -268,15 +278,17 @@
</view>
<!-- 底部购买栏 -->
<view class="bottom-bar">
<view class="bottom-bar" v-if="courseDetail.is_buy === 0">
<view class="price-section">
<text class="price">¥{{ currentPriceYuan.toFixed(2) }}</text>
<text class="price" >¥{{ currentPriceYuan.toFixed(2) }}</text>
</view>
<view class="buy-section">
<button class="buy-btn" @click="goBuy" v-if="courseDetail.is_buy === 0">{{ courseDetail.is_buy === 0 ? '立即购买' : '已购买' }}</button>
<button class="buy-btn" @click="goStudy" v-else style="background-color: #4CAF50;">开始学习</button>
<button class="buy-btn" @click="goBuy" >立即购买</button>
</view>
</view>
<view class="bottom-bar" v-else>
<button class="buy-btn" @click="goStudy" style=" width: 100%;background-color: #4CAF50;color: #fff;">开始学习</button>
</view>
</view>
</template>
@@ -569,48 +581,70 @@ const submitReply = (reviewId) => {
return
}
// 找到对应的评价
const review = reviewList.value.find(item => item.id === reviewId)
if (review) {
// 添加新回复
const newReply = {
id: Date.now(),
userName: '我',
avatar: '/static/icon_home_my_patient.png',
time: new Date().toLocaleDateString(),
content: replyText.value.trim(),
isAuthor: false
}
if (!review.replies) {
review.replies = []
}
review.replies.push(newReply)
// 重置状态
activeReplyId.value = null
replyText.value = ''
uni.showToast({
title: '回复成功',
icon: 'success'
course_api.addExcellencourseComment({
excellentcourse_id: courseId.value,
comment: replyText.value.trim(),
p_id: reviewId,
star:0,
type:0
}).then(res => {
console.log('评价提交结果:', res)
if(res.code == 200){
uni.hideLoading()
// 添加新回复
const newReply = {
id: Date.now(),
userName: '我',
avatar: '/static/icon_home_my_patient.png',
time: new Date().toLocaleDateString(),
content: replyText.value.trim(),
isAuthor: false
}
if (!review.replies) {
review.replies = []
}
review.replies.push(newReply)
// 重置状态
activeReplyId.value = null
replyText.value = ''
uni.showToast({
title: '回复成功',
icon: 'success'
})
}
})
}
}
const goToReview = () => {
uni.navigateTo({
url: '/pages_course/course_review/course_review'
url: '/pages_course/course_review/course_review?id=' + courseId.value + '&is_commented=' + courseDetail.value.is_commented
})
}
const getCourseDetail = () => {
course_api.excellencourseDetail(courseId.value).then(res => {
course_api.excellencourseDetail(courseId.value).then(res => {
console.log('课程详情数据:', res)
if(res.code == 200){
const data = res.data
courseDetail.value = data
// 调试 is_commented 字段
console.log('is_commented 字段值:', data.is_commented)
console.log('is_buy 字段值:', data.is_buy)
// 更新页面数据
updatePageData(data)
// 优惠价格与倒计时
@@ -694,8 +728,8 @@ const updatePageData = (data) => {
title: lesson.title,
duration: lesson.time || '未知时长',
teacher: teacherName,
status: (data.is_buy === 1 || data.type === 0) ? 'unlocked' : 'locked',
statusText: (data.is_buy === 1 || data.type === 0) ? (data.type === 0 ? '试播' : '已解锁') : '未解锁',
status: (data.is_buy === 1 || lesson.type === 0) ? 'unlocked' : 'locked',
statusText: (data.is_buy === 1 || lesson.type === 0) ? (lesson.type === 0 ? '试播' : '已解锁') : '未解锁',
vid: lesson.vid,
type: lesson.type
}
@@ -1570,7 +1604,7 @@ onUnmounted(() => {
border-top: 1rpx solid #e5e5e5;
display: flex;
align-items: center;
padding: 0 24rpx;
padding: 0 24rpx 20rpx 24rpx;
z-index: 999;
.price-section {
+406 -302
View File
@@ -4,9 +4,9 @@
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#FF4757"></uni-icons>
<uni-icons type="left" size="24" color="#FF4757"></uni-icons>
</view>
<view class="nav-title">肝病精品课</view>
<view class="nav-title">{{ special_name?special_name:'肝病精品课' }}</view>
<view class="nav-right" @click="goSearch">
<uni-icons type="search" size="24" color="#FF4757"></uni-icons>
</view>
@@ -27,11 +27,11 @@
indicator-color="#d8d8d8"
indicator-active-color="#FF4757"
>
<swiper-item v-for="(banner, idx) in bannerList" :key="idx">
<view class="banner-content" :style="{ background: banner.background }">
<view class="main-logo">{{ banner.title }}</view>
<view class="sub-title">{{ banner.subTitle }}</view>
<view class="description">{{ banner.description }}</view>
<swiper-item v-for="(banner, idx) in bannerList" :key="banner.id || idx">
<image :src="banner.sroll_img || banner.image || '/static/lunbo_bg.png'" mode="aspectFill" class="banner-image"></image>
<view class="banner-content">
<text class="banner-title">{{ banner.title || '' }}</text>
<text class="banner-subtitle">{{ banner.subtitle || '' }}</text>
</view>
</swiper-item>
</swiper>
@@ -39,30 +39,63 @@
<!-- 筛选栏 -->
<view class="filter-bar">
<view class="filter-item" @click="showCategoryFilter">
<text>全部课程</text>
<uni-icons type="arrowdown" size="16" color="#666"></uni-icons>
<view class="filter-item">
<uni-data-select
v-model="selectedCategory"
:localdata="categoryOptions"
:clear="false"
placeholder="全部课程"
@change="onCategoryChange"
>
<template v-slot:selected="{selectedItems}">
<view class="filter-display">
<text>{{ selectedItems.length > 0 ? selectedItems[0].text : '全部课程' }}</text>
<uni-icons type="arrowdown" size="16" color="#666"></uni-icons>
</view>
</template>
</uni-data-select>
</view>
<view class="filter-item" @click="showSortFilter">
<text>默认排序</text>
<uni-icons type="arrowdown" size="16" color="#666"></uni-icons>
<view class="filter-item">
<uni-data-select
v-model="selectedSort"
:localdata="sortOptions"
:clear="false"
placeholder="默认排序"
@change="onSortChange"
>
<template v-slot:selected="{selectedItems}">
<view class="filter-display">
<text>{{ selectedItems.length > 0 ? selectedItems[0].text : '默认排序' }}</text>
<uni-icons type="arrowdown" size="16" color="#666"></uni-icons>
</view>
</template>
</uni-data-select>
</view>
<view class="separator"></view>
<view class="filter-item" @click="toggleLimitedOffer">
<text :class="{ active: filters.limitedOffer }">限时优惠</text>
<view v-if="special_type == 0" class="separator"></view>
<view v-if="special_type == 0" class="filter-item" :class="{ active: filters.limitedOffer }" @click="toggleLimitedOffer">
<text>限时优惠</text>
</view>
<view class="separator"></view>
<view class="filter-item" @click="toggleFreeCourses">
<text :class="{ active: filters.freeCourses }">免费课程</text>
<view v-if="special_type == 0" class="separator"></view>
<view v-if="special_type == 0" class="filter-item" :class="{ active: filters.freeCourses }" @click="toggleFreeCourses">
<text>免费课程</text>
</view>
</view>
<!-- 课程列表 -->
<view class="course-list">
<!-- 空状态 -->
<view v-if="courseList.length === 0" class="empty-state">
<image src="/static/empty_course.png" mode="aspectFit" class="empty-image"></image>
<text class="empty-text">暂无相关课程</text>
<text class="empty-tip">试试调整筛选条件吧</text>
</view>
<!-- 课程列表 -->
<view
v-else
class="course-item"
v-for="(course, index) in courseList"
:key="index"
:key="course.id"
@click="goToCourseDetail(course)"
>
<view class="course-image">
@@ -70,16 +103,26 @@
<view class="learner-count" v-if="course.learnerCount">
{{ course.learnerCount }}人学
</view>
<!-- <view class="video-count" v-if="course.videoNum">
{{ course.videoNum }}课时
</view> -->
</view>
<view class="course-info">
<view class="course-title">{{ course.title }}</view>
<view class="course-instructor" v-if="course.instructor">
{{ course.instructor }}
</view>
<view class="course-tags" v-if="course.tags && course.tags.length > 0">
<text class="tag" v-for="tag in course.tags.slice(0, 2)" :key="tag">{{ tag }}</text>
</view>
<view class="course-price">
<text class="current-price">¥{{ course.price }}</text>
<text class="original-price" v-if="course.originalPrice">¥{{ course.originalPrice }}</text>
</view>
<view class="course-bonuses" v-if="course.backBonus > 0 || course.welfareBonus > 0">
<text class="bonus back-bonus" v-if="course.backBonus > 0">返现{{ course.backBonus }}积分</text>
<text class="bonus welfare-bonus" v-if="course.welfareBonus > 0">福利{{ course.welfareBonus }}积分</text>
</view>
<view class="limited-offer" v-if="course.limitedOffer">限时优惠</view>
</view>
</view>
@@ -97,148 +140,52 @@
<text>我的课程</text>
</view>
</view>
<!-- 分类筛选弹窗 -->
<uni-popup ref="categoryPopup" type="bottom">
<view class="popup-content">
<view class="popup-header">
<text>选择课程分类</text>
<view class="close-btn" @click="closeCategoryPopup">
<uni-icons type="close" size="20" color="#666"></uni-icons>
</view>
</view>
<view class="popup-options">
<view
class="popup-option"
:class="{ active: selectedCategory === option.value }"
v-for="option in categoryOptions"
:key="option.value"
@click="selectCategory(option.value)"
>
{{ option.label }}
</view>
</view>
</view>
</uni-popup>
<!-- 排序筛选弹窗 -->
<uni-popup ref="sortPopup" type="bottom">
<view class="popup-content">
<view class="popup-header">
<text>选择排序方式</text>
<view class="close-btn" @click="closeSortPopup">
<uni-icons type="close" size="20" color="#666"></uni-icons>
</view>
</view>
<view class="popup-options">
<view
class="popup-option"
:class="{ active: selectedSort === option.value }"
v-for="option in sortOptions"
:key="option.value"
@click="selectSort(option.value)"
>
{{ option.label }}
</view>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import course_api from '@/api/course_api'
// 响应式数据
const statusBarHeight = ref(0)
const navBarHeight = ref(88)
// 轮播横幅
const bannerList = ref([
{
title: '辩以明思',
subTitle: '慢乙肝治疗新起点',
description: '肝胆相照·全国中青年医师 学术争鸣辩论赛',
background: 'linear-gradient(135deg, #87CEEB 0%, #E0F6FF 100%)'
},
{
title: '名医大讲堂',
subTitle: '临床思维系统课程',
description: '顶尖专家带你系统化掌握诊疗要点',
background: 'linear-gradient(135deg, #FFE8E8 0%, #FFF5F5 100%)'
},
{
title: '专科提升',
subTitle: '循证与实践结合',
description: '以真实病例训练临床路径与决策',
background: 'linear-gradient(135deg, #E6F7FF 0%, #F2FFFC 100%)'
const bannerList = ref([])
const getIndexBanners = async () => {
try {
const res = await course_api.index()
if (res.code === 200 && res.data && res.data.scroll_list) {
bannerList.value = res.data.scroll_list
}
} catch (e) {
console.error('获取横幅失败:', e)
}
])
}
// 筛选条件
const filters = ref({
limitedOffer: false,
freeCourses: false
})
// 选中的分类和排序
const selectedCategory = ref('all')
const selectedSort = ref('default')
const selectedCategory = ref('')
const selectedSort = ref('0')
// 分类选项
const categoryOptions = ref([
{ value: 'all', label: '全部课程' },
{ value: 'liver', label: '肝病相关' },
{ value: 'hepatitis', label: '肝炎治疗' },
{ value: 'cirrhosis', label: '肝硬化' },
{ value: 'cancer', label: '肝癌' }
{ value: '', text: '全部课程' }
])
// 排序选项
const sortOptions = ref([
{ value: 'default', label: '默认排序' },
{ value: 'newest', label: '最新发布' },
{ value: 'popular', label: '最受欢迎' },
{ value: 'price', label: '价格排序' }
{ value: '0', text: '默认排序' },
{ value: '0', text: '最新上架' },
{ value: '1', text: '人气最高' },
{ value: '2', text: '课时数' },
{ value: '3', text: '价格升序' },
{ value: '4', text: '价格降序' }
])
// 课程列表
const courseList = ref([
{
id: 1,
title: '找到已购买课程即学习找到已购买课程即可学习',
instructor: '佑安医院 临床思维 陈煜',
price: 39,
image: '/static/placeholder_doctor.png',
learnerCount: 123
},
{
id: 2,
title: '找到已购买课程即学习找到已购买课程即可学习',
price: 39,
image: '/static/placeholder_doctor.png',
learnerCount: 89
},
{
id: 3,
title: '找到已购买课程即学习找到已购买课程即可学习',
price: 29,
originalPrice: 39,
image: '/static/placeholder_doctor.png',
limitedOffer: true,
learnerCount: 156
},
{
id: 4,
title: '找到已购买课程即学习找到已购买课程即可学习',
price: 29,
originalPrice: 39,
image: '/static/placeholder_doctor.png',
limitedOffer: true,
learnerCount: 78
},
{
id: 5,
title: '找到已购买课程即学习找到已购买课程即可学习',
price: 39,
image: '/static/placeholder_doctor.png',
learnerCount: 234
}
])
const courseList = ref([])
// 方法
const getSystemInfo = () => {
@@ -274,44 +221,7 @@ const goToCourseDetail = (course) => {
})
}
// 显示分类筛选
const showCategoryFilter = () => {
// 这里需要获取弹窗引用,暂时注释
// this.$refs.categoryPopup.open()
console.log('显示分类筛选')
}
// 关闭分类筛选
const closeCategoryPopup = () => {
// this.$refs.categoryPopup.close()
console.log('关闭分类筛选')
}
// 选择分类
const selectCategory = (value) => {
selectedCategory.value = value
closeCategoryPopup()
filterCourses()
}
// 显示排序筛选
const showSortFilter = () => {
// this.$refs.sortPopup.open()
console.log('显示排序筛选')
}
// 关闭排序筛选
const closeSortPopup = () => {
// this.$refs.sortPopup.close()
console.log('关闭排序筛选')
}
// 选择排序
const selectSort = (value) => {
selectedSort.value = value
closeSortPopup()
sortCourses()
}
// 分类选择已通过uni-data-select组件处理
// 切换限时优惠
const toggleLimitedOffer = () => {
@@ -328,18 +238,119 @@ const toggleFreeCourses = () => {
// 筛选课程
const filterCourses = () => {
// 这里实现课程筛选逻辑
console.log('筛选条件:', filters.value, selectedCategory.value)
console.log('筛选条件:', filters.value, selectedCategory.value, selectedSort.value)
let data = {
first_type: type_id.value,
second_type: selectedCategory.value,
sort: selectedSort.value,
page: 1
}
if(filters.value.limitedOffer){
data.discount_type = 1
}
if(filters.value.freeCourses){
data.free = 1
}
course_api.excellencourseList(data).then(res => {
console.log(res)
if (res.code === 200 && res.data && res.data.list) {
// 转换API数据为课程列表格式
courseList.value = res.data.list.map(item => ({
id: item.id,
title: item.title,
instructor: item.special_type_name || '专业讲师',
price: ((item.discount_price || item.account || 0) / 100).toFixed(2), // 分转元
originalPrice: item.discount_price && item.account && item.discount_price !== item.account ? (item.account / 100).toFixed(2) : null, // 分转元
image: item.index_img || '/static/placeholder_doctor.png',
learnerCount: item.study_num || 0,
limitedOffer: item.discount_type === 1,
videoNum: item.video_num || 0,
backBonus: item.back_bon || 0,
welfareBonus: item.fuli_bon || 0,
tags: item.tags ? item.tags.split(',') : []
}))
}
})
}
// 排序课程
const sortCourses = () => {
// 这里实现课程排序逻辑
console.log('排序方式:', selectedSort.value)
const special_type = ref(0)
const special_name = ref('')
const type_id = ref(0)
const getFirstType = () => {
course_api.listExcellencourseFirstType().then(res => {
console.log(res)
if (res.code === 200 && res.data) {
// 转换数据结构为uni-data-select需要的格式
const options = res.data.map(item => ({
value: item.id.toString(),
text: item.name
}))
// 添加"全部课程"选项
categoryOptions.value = [
{ value: '', text: '全部课程' },
...options
]
}
})
}
const getSecondType = () => {
course_api.listExcellencourseSecondType(type_id.value).then(res => {
console.log(res)
if (res.code === 200 && res.data) {
// 转换数据结构为uni-data-select需要的格式
const options = res.data.map(item => ({
value: item.id.toString(),
text: item.name
}))
// 添加"全部课程"选项
categoryOptions.value = [
{ value: '', text: '全部课程' },
...options
]
}
})
}
const onCategoryChange = (e) => {
selectedCategory.value = e
console.log('选择的分类:', e)
// 自动触发课程筛选
filterCourses()
}
const onSortChange = (e) => {
selectedSort.value = e
console.log('选择的排序:', e)
filterCourses()
}
// 生命周期
onMounted(() => {
// 获取页面参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
type_id.value = options.type
special_type.value = options.special_type
special_name.value = options.special_name
console.log('特殊类型ID:', options.special_type)
console.log('类型ID:', options.type)
getSystemInfo()
if(type_id.value > 0){
getSecondType()
}
if(special_type.value > 0){
getFirstType()
}
// 获取顶部横幅,与course.vue一致
getIndexBanners()
// 初始加载课程列表
filterCourses()
})
</script>
@@ -389,42 +400,46 @@ onMounted(() => {
// 主横幅
.main-banner {
margin: 0rpx;
margin: 0;
overflow: hidden;
position: relative;
.banner-swiper {
height: 300rpx;
height: 380rpx;
overflow: hidden;
}
.banner-content {
padding: 48rpx 32rpx;
text-align: center;
position: relative;
.main-logo {
font-size: 48rpx;
font-weight: bold;
color: #FF4757;
margin-bottom: 16rpx;
}
.sub-title {
background-color: #FF4757;
color: #fff;
padding: 8rpx 24rpx;
border-radius: 20rpx;
font-size: 24rpx;
display: inline-block;
margin-bottom: 16rpx;
}
.description {
color: #0066CC;
font-size: 26rpx;
margin-bottom: 8rpx;
}
}
.banner-image {
width: 100%;
height: 100%;
}
.banner-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 32rpx 24rpx;
background: linear-gradient(transparent, rgba(0,0,0,0.6));
box-sizing: border-box;
}
.banner-title {
display: block;
font-size: 30rpx;
color: #fff;
font-weight: 600;
margin-bottom: 6rpx;
text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.4);
}
.banner-subtitle {
display: block;
font-size: 22rpx;
color: rgba(255,255,255,0.9);
line-height: 1.4;
text-shadow: 0 1rpx 2rpx rgba(0,0,0,0.4);
}
}
@@ -433,70 +448,133 @@ onMounted(() => {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
background: #ffffff;
border-bottom: 1rpx solid #f1f3f4;
gap: 8rpx; // 减少间距
flex-wrap: nowrap; // 不换行
position: relative; // 添加相对定位
z-index: 100; // 提高层级
.filter-item {
display: flex;
align-items: center;
gap: 8rpx;
padding: 16rpx 24rpx;
border-radius: 20rpx;
transition: all 0.3s;
padding: 12rpx 0rpx; // 恢复左右内边距
cursor: pointer;
transition: all 0.2s ease;
font-size: 24rpx; // 稍微减小字体
color: #495057;
font-weight: 500;
flex: 1; // 平均分配空间
min-width: 0; // 允许压缩
justify-content: center; // 居中显示
position: relative; // 添加相对定位
z-index: 101; // 提高层级
text {
font-size: 26rpx;
color: #666;
&.active {
color: #FF4757;
}
// 让选择器组件自适应宽度
uni-data-select {
width: 100%;
display: flex;
z-index: 102; // 最高层级
}
&:active {
background-color: #f5f5f5;
&:hover {
background: #e9ecef;
border-color: #dee2e6;
}
&.active {
background: #FF4757;
color: #ffffff;
border-color: #FF4757;
text { color: #ffffff; }
}
text {
color: inherit;
font-weight: 500;
white-space: nowrap; // 文字不换行
overflow: hidden; // 超出隐藏
text-overflow: ellipsis; // 显示省略号
}
.filter-display {
display: flex;
align-items: center;
justify-content: center;
gap: 4rpx; // 减少间距
width: 100%;
text {
font-size: 24rpx;
color: inherit;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.separator {
width: 2rpx;
height: 32rpx;
background-color: #e0e0e0;
margin: 0 16rpx;
width: 1rpx;
height: 24rpx;
background: #dee2e6;
margin: 0 2rpx; // 减少间距
flex: 0 0 auto; // 分隔符不压缩
}
}
// 课程列表
.course-list {
padding: 24rpx;
background: #f8f9fa;
position: relative; // 添加相对定位
z-index: 1; // 降低层级,确保在筛选栏下方
.course-item {
display: flex;
padding: 24rpx;
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
background: #ffffff;
border-radius: 12rpx;
margin-bottom: 16rpx;
box-shadow: 0 1rpx 3rpx rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
&:hover {
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
transform: translateY(-2rpx);
}
.course-image {
position: relative;
margin-right: 24rpx;
margin-right: 20rpx;
image {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
border-radius: 8rpx;
}
.learner-count {
position: absolute;
bottom: 8rpx;
left: 8rpx;
background-color: #FF4757;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 8rpx;
border-radius: 4rpx;
}
.video-count {
position: absolute;
bottom: 8rpx;
right: 8rpx;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 20rpx;
padding: 4rpx 8rpx;
border-radius: 4rpx;
}
}
@@ -508,50 +586,123 @@ onMounted(() => {
.course-title {
font-size: 28rpx;
color: #333;
color: #212529;
line-height: 1.4;
margin-bottom: 8rpx;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 600;
}
.course-instructor {
font-size: 24rpx;
color: #999;
margin-bottom: 16rpx;
color: #6c757d;
margin-bottom: 12rpx;
}
.course-tags {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-bottom: 12rpx;
.tag {
font-size: 22rpx;
color: #FF4757;
background: #FFF5F5;
padding: 4rpx 12rpx;
border-radius: 20rpx;
border: 1rpx solid #FF4757;
}
}
.course-price {
display: flex;
align-items: center;
gap: 16rpx;
gap: 12rpx;
margin-bottom: 8rpx;
.current-price {
font-size: 32rpx;
color: #FF4757;
font-weight: bold;
font-weight: 700;
}
.original-price {
font-size: 24rpx;
color: #999;
color: #adb5bd;
text-decoration: line-through;
}
}
.course-bonuses {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-bottom: 12rpx;
.bonus {
font-size: 22rpx;
padding: 4rpx 12rpx;
border-radius: 20rpx;
border: 1rpx solid;
}
.back-bonus {
color: #FF4757;
border-color: #FF4757;
background: #FFF5F5;
}
.welfare-bonus {
color: #28a745;
border-color: #28a745;
background: #F2FFFC;
}
}
.limited-offer {
background-color: #FF4757;
background: #FF4757;
color: #fff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
border-radius: 4rpx;
align-self: flex-start;
}
}
}
// 空状态
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
text-align: center;
.empty-image {
width: 200rpx;
height: 200rpx;
margin-bottom: 32rpx;
opacity: 0.6;
}
.empty-text {
font-size: 32rpx;
color: #666;
margin-bottom: 16rpx;
font-weight: 500;
}
.empty-tip {
font-size: 26rpx;
color: #999;
line-height: 1.4;
}
}
}
// 底部导航栏
@@ -560,9 +711,9 @@ onMounted(() => {
bottom: 0;
left: 0;
right: 0;
height: 120rpx;
background-color: #fff;
border-top: 1rpx solid #f0f0f0;
height: 100rpx;
background: #ffffff;
border-top: 1rpx solid #e9ecef;
display: flex;
z-index: 999;
@@ -572,11 +723,13 @@ onMounted(() => {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
gap: 4rpx;
transition: all 0.2s ease;
text {
font-size: 24rpx;
color: #999;
font-size: 22rpx;
color: #6c757d;
font-weight: 500;
}
&.active {
@@ -586,53 +739,4 @@ onMounted(() => {
}
}
}
// 弹窗样式
.popup-content {
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
padding: 32rpx;
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 32rpx;
text {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.close-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
}
}
.popup-options {
display: flex;
flex-direction: column;
gap: 24rpx;
.popup-option {
padding: 24rpx;
text-align: center;
border-radius: 16rpx;
background-color: #f8f9fa;
font-size: 28rpx;
color: #666;
transition: all 0.3s;
&.active {
background-color: #FF4757;
color: #fff;
}
}
}
}
</style>
+29 -5
View File
@@ -4,7 +4,7 @@
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
<uni-icons type="left" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">课程支付</view>
<view class="nav-right"></view>
@@ -88,6 +88,7 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import course_api from "@/api/course_api.js"
import { requestPayment } from '@/utils/payment.js'
// 响应式数据
const statusBarHeight = ref(0)
@@ -203,12 +204,35 @@ const confirmPayment = () => {
order_pay_type.value = computeOrderPayType()
console.log('确认支付,支付方式:', { usePoints: usePoints.value, useBalance: useBalance.value, order_pay_type: order_pay_type.value })
course_api.createExcellencourseMixedOrder(courseId.value, order_pay_type.value).then(res => {
let openid = ""
if (process.env.NODE_ENV === 'development') {
openid = uni.getStorageSync('DEV_APPID');
} else {
openid = uni.getStorageSync('AUTH_APPID');
}
course_api.createExcellencourseMixedOrder(courseId.value, order_pay_type.value, openid).then(res => {
console.log('创建订单:', res)
if (res.code == 200) {
uni.redirectTo({
url: '/pages_course/course_detail/course_detail?id='+courseId.value
})
const payParams = res.data.order
console.log(payParams)
requestPayment(
payParams,
// 支付成功
(res) => {
uni.showToast({ title: '支付成功', icon: 'success' })
// 可以跳转到支付成功页,或者查询订单状态
},
// 支付失败
(err) => {
console.error('支付失败', err)
uni.showToast({ title: '支付失败或取消', icon: 'none' })
}
)
// uni.redirectTo({
// url: '/pages_course/course_detail/course_detail?id='+courseId.value
// })
}else{
uni.showToast({
title: res.msg,
+67 -41
View File
@@ -4,7 +4,7 @@
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
<uni-icons type="left" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">评价课程</view>
<view class="nav-right"></view>
@@ -15,22 +15,22 @@
<view class="page-content" :style="{ paddingTop: navBarHeight + 'px' }">
<!-- 星级评分区域 -->
<view class="rating-section">
<text class="rating-title">点击星星评分</text>
<view class="stars-container">
<view
class="star-item"
v-for="star in 5"
:key="star"
@click="setRating(star)"
>
<uni-icons
:type="star <= currentRating ? 'star-filled' : 'star'"
size="48"
:color="star <= currentRating ? '#FFB800' : '#E0E0E0'"
></uni-icons>
</view>
<uni-rate
v-model="currentRating"
:max="5"
:value="currentRating"
:size="48"
:margin="16"
:allow-half="true"
:touchable="true"
@change="onRatingChange"
/>
</view>
<view class="rating-display">
<text class="rating-score">{{ currentRating * 2 }}分</text>
<text class="rating-feedback">{{ ratingFeedback }}</text>
</view>
<text class="rating-feedback">{{ ratingFeedback }}</text>
</view>
<!-- 评价输入区域 -->
@@ -62,6 +62,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import course_api from "@/api/course_api.js"
// 响应式数据
const statusBarHeight = ref(0)
@@ -82,22 +83,29 @@ const getSystemInfo = () => {
// #endif
}
const setRating = (rating) => {
currentRating.value = rating
const onRatingChange = (e) => {
currentRating.value = e.value
updateRatingFeedback()
}
const updateRatingFeedback = () => {
const feedbacks = {
0.5: '很差,课程几乎没有帮助',
1: '差评,课程没有任何帮助',
1.5: '较差,课程帮助很小',
2: '一般,课程内容一般',
2.5: '一般偏上,课程有一定帮助',
3: '还行,课程有一定帮助',
3.5: '不错,课程内容较好',
4: '不错,课程内容很好',
4.5: '很好,课程内容很棒',
5: '很好,课程非常棒'
}
ratingFeedback.value = feedbacks[currentRating.value] || ''
}
const courseId = ref(0)
const is_commented = ref(0)
const submitReview = () => {
if (currentRating.value === 0) {
uni.showToast({
@@ -119,20 +127,23 @@ const submitReview = () => {
uni.showLoading({
title: '提交中...'
})
course_api.addExcellencourseComment({
excellentcourse_id: courseId.value,
comment: reviewContent.value,
star: currentRating.value,
type: is_commented.value
}).then(res => {
console.log('评价提交结果:', res)
if(res.code == 200){
uni.hideLoading()
uni.showToast({
title: '评价提交成功',
icon: 'success'
})
}
uni.navigateBack()
})
// 模拟提交
setTimeout(() => {
uni.hideLoading()
uni.showToast({
title: '评价提交成功',
icon: 'success'
})
// 延迟返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
}, 1000)
}
const goBack = () => {
@@ -142,6 +153,14 @@ const goBack = () => {
// 生命周期
onMounted(() => {
getSystemInfo()
// 获取页面参数
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
is_commented.value = options.is_commented
courseId.value = options.id
console.log('课程ID:', options.id)
})
</script>
@@ -203,19 +222,26 @@ onMounted(() => {
.stars-container {
display: flex;
justify-content: center;
gap: 16rpx;
margin-bottom: 24rpx;
.star-item {
cursor: pointer;
}
margin: 24rpx 0;
}
.rating-feedback {
display: block;
font-size: 24rpx;
color: #333;
font-weight: 500;
.rating-display {
display: flex;
flex-direction: column;
align-items: center;
gap: 16rpx;
.rating-score {
font-size: 32rpx;
color: #FFB800;
font-weight: 600;
}
.rating-feedback {
font-size: 24rpx;
color: #333;
font-weight: 500;
}
}
}
+712
View File
@@ -0,0 +1,712 @@
<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>
@@ -0,0 +1,666 @@
<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>
+362
View File
@@ -0,0 +1,362 @@
<template>
<view class="invoice-info-page">
<!-- 状态栏 -->
<view class="status-bar">
<text class="time">9:41</text>
<view class="status-icons">
<view class="signal"></view>
<view class="wifi"></view>
<view class="battery"></view>
</view>
</view>
<!-- 导航栏 -->
<view class="nav-bar">
<view class="back-btn" @click="goBack">
<text class="back-icon">‹</text>
</view>
<text class="title">填写发票信息</text>
<view class="info-btn" @click="showInfo">
<text class="info-icon">!</text>
</view>
</view>
<!-- 主内容区域 -->
<view class="main-content">
<!-- 表单卡片 -->
<view class="form-card">
<!-- 发票抬头 -->
<view class="form-field">
<text class="field-label">发票抬头:</text>
<input
class="field-input"
placeholder="请输入抬头名称(必填)"
placeholder-class="placeholder"
v-model="formData.title"
/>
</view>
<!-- 分隔线 -->
<view class="field-divider"></view>
<!-- 单位税号 -->
<view class="form-field">
<text class="field-label">单位税号:</text>
<input
class="field-input"
placeholder="请输入单位税号(必填)"
placeholder-class="placeholder"
v-model="formData.taxId"
/>
</view>
<!-- 分隔线 -->
<view class="field-divider"></view>
<!-- 发票内容 -->
<view class="form-field">
<text class="field-label">发票内容:</text>
<text class="field-value">{{ formData.content }}</text>
</view>
<!-- 分隔线 -->
<view class="field-divider"></view>
<!-- 发票金额 -->
<view class="form-field">
<text class="field-label">发票金额:</text>
<text class="field-value amount">{{ formData.amount }}</text>
</view>
<!-- 分隔线 -->
<view class="field-divider"></view>
<!-- 电子邮箱 -->
<view class="form-field">
<text class="field-label">电子邮箱:</text>
<input
class="field-input"
placeholder="请输入您的邮箱(必填)"
placeholder-class="placeholder"
v-model="formData.email"
type="email"
/>
</view>
</view>
</view>
<!-- 底部提交按钮 -->
<view class="submit-section">
<view class="submit-btn" @click="submitForm">
<text class="submit-text">提交</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'InvoiceInfo',
data() {
return {
formData: {
title: '',
taxId: '',
content: '培训费',
amount: '¥59.00',
email: ''
}
}
},
methods: {
goBack() {
uni.navigateBack()
},
showInfo() {
uni.showToast({
title: '发票信息说明',
icon: 'none'
})
},
submitForm() {
// 验证必填字段
if (!this.formData.title.trim()) {
uni.showToast({
title: '请输入发票抬头',
icon: 'none'
})
return
}
if (!this.formData.taxId.trim()) {
uni.showToast({
title: '请输入单位税号',
icon: 'none'
})
return
}
if (!this.formData.email.trim()) {
uni.showToast({
title: '请输入电子邮箱',
icon: 'none'
})
return
}
// 验证邮箱格式
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(this.formData.email)) {
uni.showToast({
title: '请输入正确的邮箱格式',
icon: 'none'
})
return
}
// 提交表单
uni.showModal({
title: '确认提交',
content: '确认提交发票信息吗?',
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '提交成功',
icon: 'success'
})
// 延迟返回上一页
setTimeout(() => {
uni.navigateBack()
}, 1500)
}
}
})
}
}
}
</script>
<style lang="scss" scoped>
.invoice-info-page {
min-height: 100vh;
background-color: #f5f5f5;
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;
}
}
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
height: 88rpx;
.back-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 48rpx;
color: #ff0000;
font-weight: bold;
}
}
.title {
font-size: 36rpx;
font-weight: 600;
color: #ff0000;
}
.info-btn {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background-color: #ff0000;
display: flex;
align-items: center;
justify-content: center;
.info-icon {
color: #ffffff;
font-size: 32rpx;
font-weight: bold;
}
}
}
.main-content {
padding: 30rpx;
flex: 1;
}
.form-card {
background-color: #ffffff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.form-field {
display: flex;
align-items: center;
padding: 30rpx;
min-height: 80rpx;
.field-label {
font-size: 32rpx;
color: #000000;
font-weight: 500;
min-width: 160rpx;
margin-right: 20rpx;
}
.field-input {
flex: 1;
font-size: 32rpx;
color: #000000;
border: none;
outline: none;
background: transparent;
}
.field-value {
flex: 1;
font-size: 32rpx;
color: #000000;
&.amount {
color: #ff0000;
font-weight: 600;
}
}
.placeholder {
color: #cccccc;
}
}
.field-divider {
height: 1rpx;
background-color: #f0f0f0;
margin: 0 30rpx;
}
.submit-section {
padding: 30rpx;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #f5f5f5;
}
.submit-btn {
background-color: #20b2aa;
border-radius: 8rpx;
padding: 25rpx;
display: flex;
align-items: center;
justify-content: center;
.submit-text {
color: #ffffff;
font-size: 36rpx;
font-weight: 600;
}
}
/* 响应式设计 */
@media (max-width: 750rpx) {
.form-field {
padding: 25rpx;
.field-label {
font-size: 28rpx;
min-width: 140rpx;
}
.field-input, .field-value {
font-size: 28rpx;
}
}
.submit-btn {
padding: 20rpx;
.submit-text {
font-size: 32rpx;
}
}
}
</style>
+121 -17
View File
@@ -4,7 +4,7 @@
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#FF4757"></uni-icons>
<uni-icons type="left" size="24" color="#FF4757"></uni-icons>
</view>
<view class="nav-title">我的课程</view>
<view class="nav-right" @click="goSearch">
@@ -28,26 +28,38 @@
<!-- 课程列表 -->
<view class="course-list">
<view class="course-item" v-for="(course, index) in currentCourseList" :key="course.id" @click="goCourseDetail(course)">
<view class="course-left">
<image :src="course.image" mode="aspectFill" class="course-image"></image>
</view>
<view class="course-right">
<text class="course-title">{{ course.title }}</text>
<view class="course-meta">
<text class="meta-text">{{ course.lessonCount }}节</text>
<text class="meta-separator">•</text>
<text class="meta-text">{{ course.status }}</text>
<text class="meta-separator">•</text>
<text class="meta-text">已学{{ course.learnedCount }}节</text>
<!-- 有课程数据时显示课程列表 -->
<view v-if="currentCourseList.length > 0">
<view class="course-item" v-for="(course, index) in currentCourseList" :key="course.id" @click="goCourseDetail(course)">
<view class="course-left">
<image :src="course.image" mode="aspectFill" class="course-image"></image>
</view>
<view class="course-tags" v-if="course.tags && course.tags.length > 0">
<view class="tag-item" v-for="tag in course.tags" :key="tag.id">
<text class="tag-text">{{ tag.text }}</text>
<view class="course-right">
<text class="course-title">{{ course.title }}</text>
<view class="course-meta">
<text class="meta-text">{{ course.lessonCount }}节</text>
<text class="meta-separator">•</text>
<text class="meta-text">{{ course.status }}</text>
<text class="meta-separator">•</text>
<text class="meta-text">已学{{ course.learnedCount }}节</text>
</view>
<view class="course-tags" v-if="course.tags && course.tags.length > 0">
<view class="tag-item" v-for="tag in course.tags" :key="tag.id">
<text class="tag-text">{{ tag.text }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 空状态显示 -->
<view v-else class="empty-state">
<view class="empty-icon">
<uni-icons type="book" size="80" color="#E0E0E0"></uni-icons>
</view>
<text class="empty-title">{{ getEmptyTitle() }}</text>
<text class="empty-desc">{{ getEmptyDesc() }}</text>
</view>
</view>
<!-- 订单记录浮动按钮 -->
@@ -77,6 +89,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import course_api from "@/api/course_api.js"
// 响应式数据
const statusBarHeight = ref(0)
@@ -170,12 +183,14 @@ const getSystemInfo = () => {
const switchTab = (tab) => {
activeTab.value = tab
console.log('切换到标签:', tab)
// 切换标签页后重新获取对应的课程数据
getMyCourses()
}
const goCourseDetail = (course) => {
console.log('进入课程详情:', course.title)
uni.navigateTo({
url: '/pages_course/course_detail/course_detail'
url: '/pages_course/course_detail/course_detail?id=' + course.id
})
}
@@ -208,9 +223,73 @@ const goMyCourses = () => {
console.log('当前已在我的课程页面')
}
// 获取空状态标题
const getEmptyTitle = () => {
if (activeTab.value === 'learning') {
return '暂无学习中的课程'
} else if (activeTab.value === 'completed') {
return '暂无已完成的课程'
}
return '暂无课程'
}
// 获取空状态描述
const getEmptyDesc = () => {
if (activeTab.value === 'learning') {
return '快去购买课程开始学习吧'
} else if (activeTab.value === 'completed') {
return '继续学习,完成更多课程'
}
return '快去探索更多课程吧'
}
const getMyCourses = () => {
// 根据activeTab确定state参数
let state = 1 // 默认学习中
if (activeTab.value === 'completed') {
state = 2 // 已学完
}
course_api.listMyExcellencourse(state, 1).then(res => {
console.log('API返回数据:', res)
if (res.code === 200 && res.data && res.data.list) {
// 将API数据转换为课程列表格式
const apiCourseList = res.data.list.map(item => ({
id: item.excellencourse_id,
title: item.excellencourse_title,
lessonCount: item.excellencourse_video_num,
status: item.excellencourse_upload_num === item.excellencourse_video_num ? '已完结' : '更新中',
learnedCount: item.study_num,
image: item.excellencourse_index_img || '/static/icon_home_my_patient.png',
tags: item.special_type_name ? [{ id: Date.now(), text: item.special_type_name }] : []
}))
// 根据当前标签页更新对应的课程列表
if (activeTab.value === 'learning') {
courseList.value.learning = apiCourseList
} else if (activeTab.value === 'completed') {
courseList.value.completed = apiCourseList
}
} else {
console.error('获取课程列表失败:', res.msg)
uni.showToast({
title: res.msg || '获取课程列表失败',
icon: 'none'
})
}
}).catch(err => {
console.error('API调用失败:', err)
uni.showToast({
title: '获取课程列表失败',
icon: 'none'
})
})
}
// 生命周期
onMounted(() => {
getSystemInfo()
getMyCourses()
})
</script>
@@ -369,6 +448,31 @@ onMounted(() => {
}
}
// 空状态样式
.empty-state {
padding: 120rpx 0;
text-align: center;
.empty-icon {
margin-bottom: 32rpx;
}
.empty-title {
display: block;
font-size: 32rpx;
color: #999;
font-weight: 500;
margin-bottom: 16rpx;
}
.empty-desc {
display: block;
font-size: 26rpx;
color: #ccc;
line-height: 1.4;
}
}
// 订单记录浮动按钮
.floating-order {
position: fixed;
+92 -61
View File
@@ -4,7 +4,7 @@
<view class="navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-content">
<view class="nav-left" @click="goBack">
<uni-icons type="arrowleft" size="24" color="#333"></uni-icons>
<uni-icons type="left" size="24" color="#333"></uni-icons>
</view>
<view class="nav-title">订单记录</view>
<view class="nav-right"></view>
@@ -90,6 +90,17 @@
</view>
</view>
</view>
<!-- 悬浮发票按钮 -->
<view class="floating-invoice-btn" @click="goToInvoice">
<view class="btn-content">
<view class="icon-wrapper">
<uni-icons type="receipt" size="20" color="#fff"></uni-icons>
</view>
<text class="btn-text">开具发票1</text>
</view>
<view class="btn-shadow"></view>
</view>
</view>
</template>
@@ -102,65 +113,7 @@ const statusBarHeight = ref(0)
const navBarHeight = ref(88)
const activeTab = ref('paid') // 默认激活"已支付"标签
// 订单数据
const orderList = ref({
paid: [
{
id: 1,
orderNumber: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX',
courseName: '找到已购买课程即可学习找到学习找到到到已到已购买',
orderTime: '2020.30.21 12:23:00',
courseAmount: '62.00',
balancePayment: '50.00',
actualPayment: '12.00',
status: 'paid'
},
{
id: 2,
orderNumber: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX',
courseName: '找到已购买课程即可学习找到学习找到到到已到已购买',
orderTime: '2020.30.21 12:23:00',
courseAmount: '62.00',
balancePayment: '50.00',
actualPayment: '12.00',
status: 'paid'
}
],
unpaid: [
{
id: 3,
orderNumber: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX',
courseName: '找到已购买课程即可学习找到学习找到到到已到已购买',
orderTime: '2020.30.21 12:23:00',
courseAmount: '62.00',
balancePayment: '50.00',
actualPayment: '12.00',
remainingTime: '12:23:00',
status: 'pending'
},
{
id: 4,
orderNumber: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX',
courseName: '找到已购买课程即可学习找到学习找到到到已到已购买',
orderTime: '2020.30.21 12:23:00',
courseAmount: '62.00',
balancePayment: '50.00',
actualPayment: '12.00',
remainingTime: '12:23:00',
status: 'timeout'
},
{
id: 5,
orderNumber: 'XXXXXXXXXXXXXXXXXXXXXXXXXXX',
courseName: '找到已购买课程即可学习找到学习找到到到已到已购买',
orderTime: '2020.30.21 12:23:00',
courseAmount: '62.00',
pointsPayment: '200',
balancePayment: '42.00',
actualPayment: '0.00',
status: 'failed'
}
]
})
const orderList = ref([])
// 计算属性
const currentOrderList = computed(() => {
@@ -292,6 +245,13 @@ const enterLearning = (order) => {
})
}
// 跳转到开具发票页面
const goToInvoice = () => {
uni.navigateTo({
url: '/pages_course/invoice/invoice'
})
}
const viewOrder = (order) => {
console.log('查看订单详情:', order.id)
uni.showToast({
@@ -323,7 +283,7 @@ const getOrderList = () => {
return {
id: item.id,
orderNumber: item.order_id || item.uuid || '-',
orderNumber: item.uuid || '-',
courseName: item.excellencourse_title || '-',
orderTime: formatTs(item.create_date),
courseAmount: toYuan(item.account),
@@ -577,4 +537,75 @@ onMounted(() => {
}
}
}
// 悬浮发票按钮
.floating-invoice-btn {
position: fixed;
right: 40rpx;
bottom: 140rpx;
width: 140rpx;
height: 140rpx;
z-index: 999;
cursor: pointer;
.btn-content {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 3rpx solid rgba(255, 255, 255, 0.2);
&:active {
transform: scale(0.92);
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.icon-wrapper {
width: 48rpx;
height: 48rpx;
background: rgba(255, 255, 255, 0.15);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
backdrop-filter: blur(10rpx);
}
.btn-text {
color: #ffffff;
font-size: 22rpx;
font-weight: 600;
text-align: center;
line-height: 1.2;
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.1);
letter-spacing: 1rpx;
}
}
.btn-shadow {
position: absolute;
top: 8rpx;
left: 8rpx;
right: -8rpx;
bottom: -8rpx;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.3), rgba(118, 75, 162, 0.3));
border-radius: 50%;
z-index: -1;
filter: blur(8rpx);
opacity: 0.8;
transition: all 0.3s ease;
}
&:hover .btn-shadow {
opacity: 1;
transform: scale(1.05);
}
}
</style>