精品课

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

View File

@ -0,0 +1,106 @@
# 订单记录页面悬浮发票按钮
## 功能说明
在订单记录页面添加了一个悬浮的开具发票按钮,用户可以随时点击进入发票开具流程,无需返回首页。
## 功能特性
### 1. 悬浮按钮设计
- **位置**: 固定在页面右下角距离右边距30rpx距离底部120rpx
- **尺寸**: 120rpx × 120rpx 的圆形按钮
- **样式**: 渐变背景色(红色到橙色),带有阴影效果
- **层级**: z-index: 999确保按钮始终显示在最上层
### 2. 按钮内容
- **图标**: 使用 `paperplane` 图标,表示发票功能
- **文字**: "开具发票" 标签,清晰说明按钮功能
- **布局**: 图标在上,文字在下,垂直居中对齐
### 3. 交互效果
- **点击反馈**: 点击时有缩放和阴影变化效果
- **过渡动画**: 所有状态变化都有平滑的过渡效果
- **悬停效果**: 按钮具有现代化的视觉反馈
## 使用方法
### 1. 访问订单记录页面
- 在课程相关页面中进入"订单记录"
- 页面右下角会显示悬浮的发票按钮
### 2. 点击开具发票
- 点击悬浮按钮
- 自动跳转到发票选择页面
- 选择要开票的课程后进入发票信息填写页面
### 3. 完成开票流程
- 填写发票信息
- 提交表单
- 返回订单记录页面
## 技术实现
### 1. 按钮结构
```vue
<!-- 悬浮发票按钮 -->
<view class="floating-invoice-btn" @click="goToInvoice">
<uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
<text class="btn-text">开具发票</text>
</view>
```
### 2. 跳转方法
```javascript
// 跳转到开具发票页面
const goToInvoice = () => {
uni.navigateTo({
url: '/pages_course/invoice/invoice'
})
}
```
### 3. 样式特点
- 使用 `position: fixed` 实现悬浮定位
- 渐变背景色增强视觉效果
- 阴影效果提升层次感
- 响应式设计适配不同设备
## 页面流程
1. **订单记录页面** → 显示悬浮发票按钮
2. **点击按钮** → 跳转到发票选择页面
3. **选择课程** → 进入发票信息填写页面
4. **填写信息** → 提交发票申请
5. **返回页面** → 回到订单记录页面
## 设计优势
### 1. 用户体验
- **便捷访问**: 无需返回首页,直接在订单页面即可开票
- **视觉突出**: 悬浮按钮醒目,用户容易发现
- **操作简单**: 一键点击即可进入开票流程
### 2. 界面设计
- **现代化**: 渐变色彩和阴影效果符合现代UI设计趋势
- **一致性**: 与整体页面风格保持一致
- **响应性**: 支持触摸反馈和动画效果
### 3. 功能完整
- **流程完整**: 从订单记录到发票开具的完整流程
- **页面跳转**: 正确的路由配置和页面跳转
- **状态管理**: 按钮状态和页面状态的有效管理
## 注意事项
1. 悬浮按钮使用固定定位,不会随页面滚动而移动
2. 按钮层级较高,确保在各种情况下都能正常显示
3. 支持触摸反馈,提供良好的移动端体验
4. 按钮位置经过精心设计,不会遮挡页面内容
## 后续优化建议
1. 可以添加按钮的显示/隐藏控制
2. 可以增加按钮的动画效果
3. 可以添加按钮的权限控制
4. 可以优化按钮在不同设备上的显示效果
5. 可以添加按钮的使用统计功能

83
README_INVOICE.md Normal file
View File

@ -0,0 +1,83 @@
# 精品课开具发票页面
## 功能说明
这是一个精品课开具发票的页面完全按照设计图100%还原实现。
## 页面特性
### 1. 状态栏
- 显示时间 "9:41"
- 显示信号、WiFi、电池等状态图标
### 2. 导航栏
- 左侧:红色返回按钮
- 中间:红色标题 "开具发票"
- 右侧:红色圆形信息按钮
### 3. 标签页
- "待开发票" - 当前激活状态
- "开票历史" - 可切换查看
- 标签页之间有灰色分隔线
### 4. 课程列表
- 显示5个课程项目
- 每个项目包含:
- 圆形复选框(可选中/取消)
- 课程缩略图(使用医生头像占位图)
- 课程描述文字
- 实际支付价格(红色显示)
- 默认第3个课程被选中
### 5. 底部操作栏
- 左侧:全选复选框和"全选"文字
- 中间:显示开票金额和已选课程数量
- 右侧:红色"开具发票"按钮
## 使用方法
### 1. 访问页面
在首页功能网格中点击"开具发票"按钮,即可跳转到发票页面。
### 2. 选择课程
- 点击单个课程的复选框来选中/取消选中
- 点击底部的"全选"复选框来全选/取消全选所有课程
### 3. 开具发票
- 选择要开票的课程后,底部会显示总金额
- 点击"开具发票"按钮进行开票操作
## 技术实现
### 文件结构
```
pages_course/
└── invoice/
└── invoice.vue # 发票页面主文件
```
### 主要功能
- 响应式布局,适配不同屏幕尺寸
- 复选框状态管理
- 动态计算选中课程数量和总金额
- 页面路由配置已添加到 pages.json
### 样式特点
- 使用红色作为主色调(#ff0000
- 橙色作为选中状态颜色(#ff6b35
- 现代化的UI设计包含圆角、阴影等效果
- 响应式设计,支持不同设备
## 注意事项
1. 页面使用了自定义导航栏样式
2. 课程图片使用了占位图 `/static/placeholder_doctor.png`
3. 页面已配置为无导航栏模式navigationStyle: "custom"
4. 支持返回上一页功能
## 后续优化建议
1. 可以添加真实的课程数据接口
2. 可以增加发票类型选择功能
3. 可以添加发票信息填写表单
4. 可以增加开票记录查询功能

View File

@ -0,0 +1,119 @@
# 发票页面设计优化
## 优化概述
对发票页面进行了全面的视觉设计优化,提升了用户体验和界面美观度。
## 主要优化内容
### 1. 整体视觉风格
- **渐变背景**: 从浅灰色到白色的渐变背景,增加层次感
- **现代化设计**: 采用卡片式设计,增加阴影和圆角
- **色彩统一**: 使用橙色系主色调(#ff6b35#ff8c42
### 2. 导航栏优化
- **渐变背景**: 添加微妙的渐变背景
- **阴影效果**: 增加底部阴影,提升层次感
- **交互反馈**: 按钮点击时有缩放和背景色变化
- **标题样式**: 使用渐变文字效果,更加醒目
### 3. 标签页优化
- **活跃状态**: 活跃标签使用圆角胶囊背景
- **渐变效果**: 橙色渐变背景,白色文字
- **底部指示器**: 渐变色的底部指示条
- **悬停效果**: 非活跃标签悬停时显示橙色
### 4. 开票历史记录优化
- **卡片设计**: 每个记录项都是独立的卡片
- **左侧装饰条**: 橙色渐变装饰条,增加视觉层次
- **渐变背景**: 微妙的渐变背景
- **阴影效果**: 柔和的阴影,提升立体感
- **状态标签**: 状态标签使用渐变背景和阴影
- **交互反馈**: 点击时有轻微的下移和阴影变化
## 设计特点
### 1. 色彩搭配
- **主色调**: 橙色系(#ff6b35, #ff8c42
- **辅助色**: 绿色(成功)、红色(失败)、蓝色(处理中)
- **中性色**: 灰色系用于文字和边框
### 2. 视觉效果
- **渐变**: 多处使用渐变效果,增加现代感
- **阴影**: 不同层级的阴影,营造立体感
- **圆角**: 统一的圆角设计,更加友好
- **过渡**: 所有交互都有平滑的过渡动画
### 3. 交互体验
- **点击反馈**: 按钮和卡片都有点击反馈
- **悬停效果**: 标签页有悬停状态
- **动画过渡**: 平滑的状态切换动画
## 技术实现
### 1. CSS特性
```scss
// 渐变背景
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
// 渐变文字
background: linear-gradient(135deg, #ff6b35 0%, #ff8c42 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
// 阴影效果
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
// 过渡动画
transition: all 0.3s ease;
```
### 2. 伪元素装饰
```scss
// 左侧装饰条
&::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;
}
```
### 3. 状态样式
```scss
// 成功状态
&.success {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #ffffff;
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
}
```
## 用户体验提升
### 1. 视觉层次
- 清晰的信息层级
- 合理的间距和留白
- 突出的重要信息
### 2. 交互反馈
- 即时的视觉反馈
- 平滑的动画过渡
- 直观的操作提示
### 3. 美观度
- 现代化的设计风格
- 协调的色彩搭配
- 精致的细节处理
## 后续优化建议
1. **深色模式**: 可以添加深色主题支持
2. **动画效果**: 可以增加更多的微交互动画
3. **响应式**: 可以优化不同屏幕尺寸的显示
4. **无障碍**: 可以增加无障碍访问支持
5. **主题定制**: 可以支持用户自定义主题色彩

185
README_INVOICE_HISTORY.md Normal file
View File

@ -0,0 +1,185 @@
# 开票历史功能
## 功能说明
在发票页面添加了"开票历史"标签页,用户可以查看历史开票记录,包括开票状态、课程数量、开票金额等信息。
## 功能特性
### 1. 标签页切换
- **待开发票**: 显示可选择的课程列表,支持批量选择和开票
- **开票历史**: 显示历史开票记录,包含详细的开票信息
### 2. 开票历史记录
- **日期显示**: 显示开票日期2021年6月11日
- **课程数量**: 显示该次开票包含的课程数量共计3个课程
- **开票状态**: 显示开票的当前状态
- **开票金额**: 显示开票的金额信息
### 3. 开票状态类型
- **已开票**: 绿色显示,表示开票成功
- **开票失败**: 红色显示,表示开票失败
- **开票中**: 蓝色显示,表示正在处理中
## 页面结构
### 1. 标签页导航
```vue
<view class="tabs">
<view class="tab-item" :class="{ active: activeTab === 'pending' }">
<text>待开发票</text>
</view>
<view class="tab-divider"></view>
<view class="tab-item" :class="{ active: activeTab === 'history' }">
<text>开票历史</text>
</view>
</view>
```
### 2. 内容区域
- **待开发票**: 显示课程列表和底部操作栏
- **开票历史**: 显示历史记录列表
### 3. 历史记录项
```vue
<view class="history-item" v-for="item in historyList">
<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">{{ item.statusText }}</text>
<view class="history-arrow"></view>
<text class="history-amount">{{ item.amount }}</text>
</view>
</view>
```
## 数据结构
### 1. 历史记录数据
```javascript
historyList: [
{
id: 1,
date: '2021年6月11日',
courseCount: '共计3个课程',
status: 'success',
statusText: '已开票',
amount: '¥0.00'
},
{
id: 2,
date: '2021年6月11日',
courseCount: '共计3个课程',
status: 'failed',
statusText: '开票失败',
amount: '¥0.00'
},
{
id: 3,
date: '2021年6月11日',
courseCount: '共计3个课程',
status: 'processing',
statusText: '开票中',
amount: '¥0.00'
}
]
```
### 2. 状态枚举
- `success`: 已开票(绿色)
- `failed`: 开票失败(红色)
- `processing`: 开票中(蓝色)
## 交互功能
### 1. 标签页切换
- 点击"待开发票"标签显示课程选择界面
- 点击"开票历史"标签显示历史记录界面
- 标签页之间有视觉分隔线
### 2. 历史记录查看
- 点击任意历史记录项可查看详情
- 显示相应的状态提示信息
- 支持后续扩展详情页面
### 3. 状态颜色区分
- 不同状态使用不同颜色显示
- 提供清晰的视觉反馈
- 符合用户认知习惯
## 样式设计
### 1. 历史记录项样式
- 浅灰色背景(#f8f8f8
- 圆角设计8rpx
- 卡片式布局,带有间距
- 底部边框分隔线
### 2. 状态颜色
- 成功状态:绿色(#52c41a
- 失败状态:红色(#ff4d4f
- 处理中状态:蓝色(#1890ff
- 默认状态:灰色(#666666
### 3. 布局特点
- 左右分布,信息清晰
- 响应式设计,适配不同屏幕
- 触摸友好的交互区域
## 使用方法
### 1. 查看开票历史
- 在发票页面点击"开票历史"标签
- 浏览历史开票记录
- 查看开票状态和详细信息
### 2. 切换标签页
- 点击"待开发票"返回课程选择界面
- 点击"开票历史"查看历史记录
- 标签页状态实时更新
### 3. 查看记录详情
- 点击任意历史记录项
- 系统显示相应的状态信息
- 支持后续功能扩展
## 技术实现
### 1. 条件渲染
```vue
<view v-if="activeTab === 'pending'" class="pending-content">
<!-- 待开发票内容 -->
</view>
<view v-if="activeTab === 'history'" class="history-content">
<!-- 开票历史内容 -->
</view>
```
### 2. 数据绑定
- 使用Vue的响应式数据绑定
- 支持动态数据更新
- 状态切换实时响应
### 3. 事件处理
```javascript
viewHistoryDetail(item) {
console.log('查看开票历史详情:', item)
uni.showToast({
title: `查看${item.statusText}详情`,
icon: 'none'
})
}
```
## 后续优化建议
1. **详情页面**: 可以添加开票历史详情页面
2. **状态筛选**: 可以添加按状态筛选历史记录
3. **时间筛选**: 可以添加按时间范围筛选
4. **搜索功能**: 可以添加搜索特定开票记录
5. **导出功能**: 可以添加导出开票历史记录
6. **分页加载**: 可以添加分页加载更多历史记录
7. **实时更新**: 可以添加开票状态的实时更新
8. **通知功能**: 可以添加开票完成的通知提醒

95
README_INVOICE_INFO.md Normal file
View File

@ -0,0 +1,95 @@
# 填写发票信息页面
## 功能说明
这是一个填写发票信息的页面完全按照设计图100%还原实现。用户在选择课程后,通过此页面填写详细的发票信息。
## 页面特性
### 1. 状态栏
- 显示时间 "9:41"
- 显示信号、WiFi、电池等状态图标
### 2. 导航栏
- 左侧:红色返回按钮
- 中间:红色标题 "填写发票信息"
- 右侧:红色圆形信息按钮
### 3. 表单内容
- **发票抬头**: 输入框,必填项
- **单位税号**: 输入框,必填项
- **发票内容**: 显示固定值 "培训费"
- **发票金额**: 显示固定值 "¥59.00"(红色显示)
- **电子邮箱**: 输入框,必填项
### 4. 提交按钮
- 底部固定位置的青色按钮
- 白色文字 "提交"
## 使用方法
### 1. 访问页面
在发票页面选择课程后,点击"开具发票"按钮即可跳转到此页面。
### 2. 填写信息
- 发票抬头:输入公司或个人名称
- 单位税号:输入税务登记号
- 电子邮箱:输入接收发票的邮箱地址
### 3. 提交表单
- 点击"提交"按钮
- 系统会验证必填字段和邮箱格式
- 提交成功后自动返回上一页
## 技术实现
### 文件结构
```
pages_course/
└── invoice_info/
└── invoice_info.vue # 发票信息填写页面
```
### 主要功能
- 表单验证(必填字段、邮箱格式)
- 响应式布局设计
- 自定义导航栏
- 表单数据双向绑定
### 样式特点
- 使用红色作为主色调(#ff0000
- 青色作为提交按钮颜色(#20b2aa
- 白色卡片式表单设计
- 浅灰色分隔线
- 现代化UI设计包含圆角、阴影等效果
## 表单验证规则
1. **发票抬头**: 必填,不能为空
2. **单位税号**: 必填,不能为空
3. **电子邮箱**: 必填,必须符合邮箱格式
## 页面流程
1. 用户在发票页面选择课程
2. 点击"开具发票"按钮
3. 跳转到填写发票信息页面
4. 填写必要信息
5. 点击提交
6. 验证通过后返回上一页
## 注意事项
1. 页面使用了自定义导航栏样式
2. 发票内容和金额为固定值,不可编辑
3. 页面已配置为无导航栏模式navigationStyle: "custom"
4. 支持返回上一页功能
5. 表单验证失败会显示相应的提示信息
## 后续优化建议
1. 可以添加发票类型选择功能
2. 可以增加发票抬头历史记录
3. 可以添加发票预览功能
4. 可以增加发票模板选择
5. 可以添加发票状态查询功能

View File

@ -1,5 +1,9 @@
import {request} from '@/utils/request.js'
const api = {
expertWxLogin(jscode){
return request('/miniprogram/expertLogin', {jscode: jscode}, 'get', true);
},
// 微信登录
wxLogin(data) {
return request('/login/wechat/mobile', data, 'post', true);

View File

@ -22,8 +22,8 @@ const course_api = {
},
// 创建订单
createExcellencourseMixedOrder(id, order_pay_type) {
return request('/expertPay/createExcellencourseOrder', {appid:"wx061c1f4e16a5f20f", openid:"", excellencourse_id: id, order_pay_type: order_pay_type}, 'post', true);
createExcellencourseMixedOrder(id, order_pay_type, openid) {
return request('/expertPay/createExcellencourseOrder', {appid:"wx061c1f4e16a5f20f", openid: openid, excellencourse_id: id, order_pay_type: order_pay_type}, 'post', true);
},
// 订单列表
@ -31,6 +31,46 @@ const course_api = {
return request('/expertAPI/listExcellencourseOrder', {order_status: order_status, page:page}, 'post', true);
},
// 我的课程
listMyExcellencourse(state, page) {
return request('/expertAPI/listMyExcellencourse', {state:state,page:page}, 'post', true);
},
// 添加评论
addExcellencourseComment(data) {
return request('/expertAPI/addExcellencourseComment', data, 'post', true);
},
// 一级分类
listExcellencourseFirstType() {
return request('/expertAPI/listExcellencourseFirstType', {}, 'post', true);
},
// 二级分类
listExcellencourseSecondType(type_id) {
return request('/expertAPI/listExcellencourseSecondType', {first_id: type_id}, 'post', true);
},
// 筛选列表
excellencourseList(data) {
return request('/expertAPI/excellencourseScreen', data, 'post', true);
},
// 未开票订单列表
listExcellencourseOrderNoInvoice() {
return request('/expertAPI/listExcellencourseOrderNoInvoice', {}, 'post', true);
},
// 开票历史列表
listExcellencourseOrderInvoiceHistory(page) {
return request('/expertAPI/listExcellencourseOrderInvoice', {page:page}, 'post', true);
},
// 获取订单发票
getExcellencourseOrderInvoice(order_id) {
return request('/expertAPI/getExcellencourseOrderInvoice', {id:order_id}, 'post', true);
},
// 短信验证码登录
smsLogin(data,header){
return request('/expertAPI/smsLogin', data, 'post', true,'application/json',header);

View File

@ -17,7 +17,8 @@
"delay" : 0
},
"modules" : {
"OAuth" : {}
"OAuth" : {},
"Payment" : {}
},
/* */
"distribute" : {
@ -50,7 +51,15 @@
"univerify" : {},
"weixin" : {
"appid" : "wxbf3658f5e674667c",
"UniversalLinks" : ""
"UniversalLinks" : "https://doc.igandan.com/gdxzExpert/"
}
},
"payment" : {
"appleiap" : {},
"weixin" : {
"__platform__" : [ "ios", "android" ],
"appid" : "wxbf3658f5e674667c",
"UniversalLinks" : "https://doc.igandan.com/gdxzExpert/"
}
}
}

View File

@ -142,6 +142,36 @@
"bounce": "none"
}
}
},
{
"path": "invoice/invoice",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "开具发票",
"app": {
"bounce": "none"
}
}
},
{
"path": "invoice_info/invoice_info",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "填写发票信息",
"app": {
"bounce": "none"
}
}
},
{
"path": "invoice_detail/invoice_detail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "开票详情",
"app": {
"bounce": "none"
}
}
}
]
},
@ -329,6 +359,7 @@
}
}
}
]
}],

View File

@ -354,6 +354,10 @@
icon: course,
text: '精品课'
},
{
icon: '',
text: '开具发票'
},
{
icon: more,
text: '更多'
@ -582,8 +586,18 @@
//
const onClick = (e) => {
console.log('点击了第' + e.detail.index + '个');
const clickedItem = gridList[e.detail.index];
//
if (clickedItem.text === '开具发票') {
uni.navigateTo({
url: '/pages_course/invoice/invoice'
});
return;
}
uni.showToast({
title: `点击了${gridList[e.detail.index].text}`,
title: `点击了${clickedItem.text}`,
icon: 'none'
});
};

View File

@ -63,6 +63,15 @@
<view class="sms-login" @click="onSmsLogin" style="margin-top: 20rpx;">
<text class="sms-text">短信验证码登录</text>
</view>
<!-- 微信登录 -->
<view class="wechat-login" @click="onWechatLogin">
<view class="wechat-icon">
<up-image :src="wxImg" width="100rpx" height="100rpx" ></up-image>
</view>
<text class="wechat-text">微信登录</text>
</view>
<!-- #endif -->
</view>
@ -98,6 +107,7 @@
import wxImg from "@/static/weixin_login.png"
import checkImg from "@/static/login_new_unselect.png"
import checkOnImg from "@/static/login_new_select.png"
import api from "@/api/api.js"
const customStyle = reactive({
height: "100rpx",
@ -164,10 +174,35 @@
}
console.log('微信登录');
uni.showToast({
title: '微信登录功能开发中',
icon: 'none'
wx.login({
success(res) {
if (res.code) {
// res.code openId, sessionKey, unionId
api.expertWxLogin(res.code).then((data) => {
console.log(data)
if (data.data.openid) {
if (process.env.NODE_ENV === 'development') {
uni.setStorageSync('DEV_APPID', data.data.openid);
} else {
uni.setStorageSync('AUTH_APPID', data.data.openid);
}
}
let openid = ""
if (process.env.NODE_ENV === 'development') {
openid = uni.getStorageSync('DEV_APPID');
} else {
openid = uni.getStorageSync('AUTH_APPID');
}
console.log("openid: ", openid)
})
} else {
console.log('登录失败!' + res.errMsg);
}
}
});
};
//

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}`
})
}
//

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 {

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>

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,

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;
}
}
}

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>

View File

@ -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>

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>

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 = () => {
// activeTabstate
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;

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>

99
utils/payment.js Normal file
View File

@ -0,0 +1,99 @@
// utils/payment.js
// 统一的支付方法
export function requestPayment(orderInfo, successCallback, failCallback) {
// orderInfo 一般由后端返回,包括平台所需的支付参数
// 比如 { "timeStamp": "", "nonceStr": "", "package": "", "signType": "RSA", "paySign": "" } (微信)
// 或者 { "orderInfo": "xxx" } (支付宝)
// #ifdef MP-WEIXIN
console.log("MP-WEIXIN == MP-WEIXIN == MP-WEIXIN == MP-WEIXIN")
// 微信小程序支付
uni.requestPayment({
provider: 'wxpay',
timeStamp: orderInfo.timeStamp,
nonceStr: orderInfo.nonceStr,
package: 'prepay_id='+orderInfo.prepayId,
signType: orderInfo.signType || 'RSA',
paySign: orderInfo.paySign,
success: (res) => {
console.log('微信支付成功', res);
successCallback && successCallback(res);
},
fail: (err) => {
console.error('微信支付失败', err);
failCallback && failCallback(err);
}
});
// #endif
// #ifdef MP-ALIPAY
console.log("MP-ALIPAY == MP-ALIPAY == MP-ALIPAY == MP-ALIPAY")
// 支付宝小程序支付
uni.requestPayment({
provider: 'alipay',
orderInfo: orderInfo.orderInfo, // 需要的是支付宝的 orderString
success: (res) => {
console.log('支付宝支付成功', res);
successCallback && successCallback(res);
},
fail: (err) => {
console.error('支付宝支付失败', err);
failCallback && failCallback(err);
}
});
// #endif
// #ifdef APP-PLUS
console.log("APP-PLUS == APP-PLUS==APP-PLUS== APP-PLUS")
// App 端支付(支持微信/支付宝/ApplePay等
// 通常也是调用 uni.requestPayment通过 provider 区分
const platform = uni.getSystemInfoSync().platform; // iOS or Android
let provider = '';
if (platform === 'ios') {
// 可以根据业务需求选择微信或 Apple Pay
provider = 'wxpay'; // 或者 'wxpay',但 App 微信支付一般用 'wxpay'
} else {
// Android 通常用支付宝或微信
// 你可以根据后端返回的 provider 决定,比如 orderInfo.provider
provider = orderInfo.provider || 'wxpay'; // 或 alipay
}
uni.requestPayment({
provider: provider, // 可选值wxpay、alipay、appleiap 等
orderInfo: orderInfo.orderInfo, // 支付宝传 orderInfo微信可能传其它参数
timeStamp: orderInfo.timestamp,
nonceStr: orderInfo.noncestr,
package: orderInfo.package_str,
signType: orderInfo.signType || 'RSA',
paySign: orderInfo.sign,
success: (res) => {
console.log(`${provider} 支付成功`, res);
successCallback && successCallback(res);
},
fail: (err) => {
console.error(`${provider} 支付失败`, err);
failCallback && failCallback(err);
}
});
// #endif
// #ifdef H5
console.log("H5 == H5==H5==H5==H5")
// H5 支付:通常跳转到第三方支付页面(如微信扫码、支付宝跳转)
// 由于 H5 无法直接调起微信支付(除非在微信内置浏览器),所以一般后端会返回支付链接,前端跳转
if (orderInfo.payUrl) {
// 比如支付宝的支付链接,或者微信的二维码链接
window.location.href = orderInfo.payUrl;
} else {
uni.showToast({
title: 'H5暂不支持直接支付请使用其他端',
icon: 'none'
});
}
// #endif
// 其它平台(如 QuickApp、钉钉等可根据需要扩展
}