很多页面

This commit is contained in:
haomingming 2025-09-03 17:13:10 +08:00
parent 92bfc36d51
commit 84f59b20f6
26 changed files with 29536 additions and 616 deletions

117
README_MEETING_UPDATE.md Normal file
View File

@ -0,0 +1,117 @@
# 会议页面更新说明
## 更新内容
### 1. API集成
- 集成了真实的会议列表API `meetingListV2U`
- 替换了原有的模拟数据
- 添加了完整的错误处理机制
### 2. 数据处理
- 将API返回的数据转换为页面所需的格式
- 支持分页加载(下拉刷新和上拉加载更多)
- 根据会议状态动态生成标签颜色
- 支持按月份动态筛选会议数据
- 支持月份和地点组合筛选
- 统一的搜索筛选接口
### 3. 会议状态显示
- 预告 (status: 1) - 橙色标签
- 直播中 (status: 2) - 红色标签
- 已结束 (status: 3) - 灰色标签
### 4. 图片处理
- 支持相对路径和绝对URL的图片
- 自动添加基础URL前缀
- 提供默认占位符图片
### 5. 交互功能
- 会议已结束:点击跳转到详情页
- 会议进行中/预告:点击跳转到直播页面
- 支持webview页面跳转
## API数据结构
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"pageSize": 10,
"isFirstPage": true,
"isLastPage": true,
"pageNum": 1,
"pages": 1,
"list": [
{
"title": "会议标题",
"begin_date": "2023-09-26 18:30:00",
"end_date": "2025-09-26 22:30:00",
"liveimg": "live/img/2021/20211202152116.png",
"liveurl": "http://zhibo.igandan.com/watch/1633941",
"status": 3,
"location": "线上",
"id": 41,
"path": "https://dev-wx.igandan.com/conference/conference_info.htm?id=41"
}
],
"total": 2
}
}
```
## 主要函数
### `loadMeetingList()`
- 获取会议列表数据
- 处理分页逻辑
- 数据格式转换
### `loadMeetingListByMonth(monthValue)`
- 按月份筛选会议列表
- 支持时间范围查询
- 自动计算月份起止时间
### `meetingListBySearchU(filters)`
- 统一的搜索和筛选接口
- 支持月份和地点组合筛选
- 自动构建筛选参数
- 支持分页加载
- API参数
- `month`: 月份筛选格式9表示9月份选择"所有"时传空字符串)
- `location`: 地点筛选(省份名称,如:北京市、广东省;选择"全国"时传空字符串)
- `page`: 页码
### `getTagColor(status, beginDate)`
- 根据会议状态和日期生成标签颜色
- 支持动态颜色生成
### `generateMonthList()`
- 动态生成月份列表当前月份往后12个月
- 智能标签显示(本月、下月、年份月份)
- 支持跨年月份计算
### `getImageUrl(imagePath)`
- 处理图片URL
- 支持相对路径和绝对URL
### `playVideo(item)`
- 处理会议点击事件
- 根据状态跳转不同页面
## 注意事项
1. 需要确保 `api.meetingListV2U` 函数已正确定义
2. 图片基础URL可能需要根据实际环境调整
3. webview页面路径需要确保存在
4. 建议添加默认的会议海报占位符图片
5. 月份筛选功能使用 `month` 参数格式9表示9月份
6. 地点筛选功能使用 `location` 参数(省份名称,如:北京市、广东省等)
## 后续优化建议
1. 添加会议搜索功能
2. 支持按时间、地点筛选
3. 添加会议收藏功能
4. 优化图片加载性能
5. 添加会议提醒功能

View File

@ -138,6 +138,14 @@ const api = {
return request('/expertPay/getFlowerList', data, 'post', false);
},
getGandanfileMyDownload(data) {
return request('/expertAPI/gandanfile/myDownload', data, 'post', false);
},
getGandanfileMyShare(data) {
return request('/expertAPI/gandanfile/myShare', data, 'post', false);
},
// 课件详情
getGandanFileDetail(data) {
return request('/expertAPI/gandanFileDetail', data, 'post', false);
@ -242,7 +250,57 @@ const api = {
guideTag(data){
return request('/expertApp/tagList', data, 'post', false);
},
// 会议列表
meetingListV2U(data){
return request('/expertAPI/meetingListV2U', data, 'post', false);
},
// 会议列表
meetingListBySearchU(data){
return request('/expertAPI/meetingListBySearchU', data, 'post', false);
},
myWelfareCard(data){
return request('/expertAPI/myWelfareCard', data, 'post', false);
},
exchangeWelfareCard(data){
return request('/expertAPI/exchangeWelfareCard', data, 'post', false);
},
unReadList(data){
return request('/expertAPI/unReadList', data, 'post', false);
},
appMesageList(data){
return request('/expertAPI/appMesageList', data, 'post', false);
},
appMesageRead(data){
return request('/expertAPI/appMesageRead', data, 'post', false);
},
bankCardList(data){
return request('/pingExpertV2/bankCardList', data, 'post', false);
},
identificationBankCardNew(data){
return request('/pingExpertV2/identificationBankCardNew', data, 'post', false);
},
changePassword(data){
return request('/expertAPI/modifyPwd', data, 'post', false);
},
changeMobile(data){
return request('/expertAPI/modifyMobile', data, 'post', false);
},
smsSend(data){
return request('/expertAPI/smsSend', data, 'post', false);
},
// 肝胆新闻相关API
// 顶部轮播
newsRollNew(data){

33
api/goods_api.js Normal file
View File

@ -0,0 +1,33 @@
import {request} from '@/utils/request.js'
const goods_api = {
goodsNewsList() {
return request('/expertAPI/goodsNewsList', {}, 'post', true);
},
goodsList(data) {
return request('/expertAPI/goodsListV2', data, 'post', true);
},
goodsTagList(data) {
return request('/expertAPI/goodsTagList', data, 'post', true);
},
getGoodsDetail(data) {
return request('/expertAPI/getGoodsDetail', data, 'post', true);
},
getTotalPoints(data) {
return request('/expertAPI/getTotalPointsU', data, 'post', true);
},
createGoodsOrder(data) {
return request('/expertAPI/createGoodsOrder', data, 'post', true);
},
getGoodsOrderList(data) {
return request('/expertAPI/goodsOrderList', data, 'post', true);
}
}
export default goods_api;

View File

@ -1,301 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>生成占位图片</title>
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.image-item {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.image-preview {
margin-bottom: 10px;
}
canvas {
border: 1px solid #ccc;
border-radius: 5px;
}
.download-btn {
background: #007AFF;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
}
.download-btn:hover {
background: #0056CC;
}
.info {
color: #666;
font-size: 14px;
margin-top: 10px;
}
h1 {
color: #333;
text-align: center;
}
h3 {
color: #555;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>精品课页面占位图片生成器</h1>
<p style="text-align: center; color: #666;">点击下载按钮保存图片到 /static/ 目录</p>
<!-- 主横幅 -->
<div class="image-item">
<h3>1. HIV横幅图片 (hiv_banner.png)</h3>
<div class="image-preview">
<canvas id="hiv_banner" width="700" height="300"></canvas>
</div>
<button class="download-btn" onclick="downloadImage('hiv_banner', 'hiv_banner.png')">下载</button>
<div class="info">尺寸: 700x300px | 用途: 主横幅</div>
</div>
<!-- 肝病图标 -->
<div class="image-item">
<h3>2. 肝病图标 (liver_icon.png)</h3>
<div class="image-preview">
<canvas id="liver_icon" width="120" height="120"></canvas>
</div>
<button class="download-btn" onclick="downloadImage('liver_icon', 'liver_icon.png')">下载</button>
<div class="info">尺寸: 120x120px | 用途: 分类图标</div>
</div>
<!-- 肿瘤图标 -->
<div class="image-item">
<h3>3. 肿瘤图标 (tumor_icon.png)</h3>
<div class="image-preview">
<canvas id="tumor_icon" width="120" height="120"></canvas>
</div>
<button class="download-btn" onclick="downloadImage('tumor_icon', 'tumor_icon.png')">下载</button>
<div class="info">尺寸: 120x120px | 用途: 分类图标</div>
</div>
<!-- 感染图标 -->
<div class="image-item">
<h3>4. 感染图标 (infection_icon.png)</h3>
<div class="image-preview">
<canvas id="infection_icon" width="120" height="120"></canvas>
</div>
<button class="download-btn" onclick="downloadImage('infection_icon', 'infection_icon.png')">下载</button>
<div class="info">尺寸: 120x120px | 用途: 分类图标</div>
</div>
<!-- 课程缩略图 -->
<div class="image-item">
<h3>5. 课程缩略图 (course_thumbnail.png)</h3>
<div class="image-preview">
<canvas id="course_thumbnail" width="240" height="240"></canvas>
</div>
<button class="download-btn" onclick="downloadImage('course_thumbnail', 'course_thumbnail.png')">下载</button>
<div class="info">尺寸: 240x240px | 用途: 精品小课缩略图</div>
</div>
<!-- 返现卡片1 -->
<div class="image-item">
<h3>6. 返现卡片1 (reward_card1.png)</h3>
<div class="image-preview">
<canvas id="reward_card1" width="350" height="200"></canvas>
</div>
<button class="download-btn" onclick="downloadImage('reward_card1', 'reward_card1.png')">下载</button>
<div class="info">尺寸: 350x200px | 用途: 学克返现卡片</div>
</div>
<!-- 返现卡片2 -->
<div class="image-item">
<h3>7. 返现卡片2 (reward_card2.png)</h3>
<div class="image-preview">
<canvas id="reward_card2" width="350" height="200"></canvas>
</div>
<button class="download-btn" onclick="downloadImage('reward_card2', 'reward_card2.png')">下载</button>
<div class="info">尺寸: 350x200px | 用途: 学克返现卡片</div>
</div>
<!-- 福利横幅 -->
<div class="image-item">
<h3>8. 福利横幅 (welfare_banner.png)</h3>
<div class="image-preview">
<canvas id="welfare_banner" width="700" height="200"></canvas>
</div>
<button class="download-btn" onclick="downloadImage('welfare_banner', 'welfare_banner.png')">下载</button>
<div class="info">尺寸: 700x200px | 用途: 福利课堂横幅</div>
</div>
<!-- HIV课程图 -->
<div class="image-item">
<h3>9. HIV课程图 (hiv_course.png)</h3>
<div class="image-preview">
<canvas id="hiv_course" width="240" height="240"></canvas>
</div>
<button class="download-btn" onclick="downloadImage('hiv_course', 'hiv_course.png')">下载</button>
<div class="info">尺寸: 240x240px | 用途: 热门课程缩略图</div>
</div>
<div style="margin-top: 40px; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h3>使用说明:</h3>
<ol>
<li>点击每个图片下方的"下载"按钮</li>
<li>将下载的图片保存到项目的 <code>/static/</code> 目录中</li>
<li>确保文件名与页面代码中的引用名称一致</li>
<li>重新运行uniapp项目即可看到占位图片效果</li>
</ol>
</div>
</div>
<script>
// 绘制图片函数
function drawImage(canvasId, config) {
const canvas = document.getElementById(canvasId);
const ctx = canvas.getContext('2d');
// 创建渐变背景
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, config.startColor);
gradient.addColorStop(1, config.endColor);
// 填充背景
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 添加文字
ctx.fillStyle = '#ffffff';
ctx.font = `bold ${config.fontSize}px Arial, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// 绘制主标题
ctx.fillText(config.title, canvas.width / 2, canvas.height / 2 - 10);
// 绘制副标题(如果有)
if (config.subtitle) {
ctx.font = `${config.fontSize - 4}px Arial, sans-serif`;
ctx.fillText(config.subtitle, canvas.width / 2, canvas.height / 2 + 20);
}
// 添加图标/装饰元素
if (config.icon) {
ctx.font = `${config.fontSize + 10}px Arial, sans-serif`;
ctx.fillText(config.icon, canvas.width / 2, canvas.height / 2 - 40);
}
}
// 下载图片函数
function downloadImage(canvasId, filename) {
const canvas = document.getElementById(canvasId);
const link = document.createElement('a');
link.download = filename;
link.href = canvas.toDataURL();
link.click();
}
// 页面加载完成后绘制所有图片
window.onload = function() {
// HIV横幅
drawImage('hiv_banner', {
startColor: '#64B5F6',
endColor: '#2196F3',
title: '小懂医生讲HIV和感染',
subtitle: '专业医学知识分享',
fontSize: 24,
icon: '🏥'
});
// 肝病图标
drawImage('liver_icon', {
startColor: '#FF8A65',
endColor: '#FF5722',
title: '肝病',
fontSize: 16,
icon: '🫀'
});
// 肿瘤图标
drawImage('tumor_icon', {
startColor: '#FFD54F',
endColor: '#FF9800',
title: '肿瘤',
fontSize: 16,
icon: '🎯'
});
// 感染图标
drawImage('infection_icon', {
startColor: '#64B5F6',
endColor: '#2196F3',
title: '感染',
fontSize: 16,
icon: '🦠'
});
// 课程缩略图
drawImage('course_thumbnail', {
startColor: '#E1F5FE',
endColor: '#81D4FA',
title: '肝脏胖瘦临床',
subtitle: '影像学习交流',
fontSize: 16,
icon: '📚'
});
// 返现卡片1
drawImage('reward_card1', {
startColor: '#E8F5E8',
endColor: '#4CAF50',
title: '乙肝临床治疗肝肝',
subtitle: '肝炎及关并发作用',
fontSize: 14
});
// 返现卡片2
drawImage('reward_card2', {
startColor: '#FFF3E0',
endColor: '#FF9800',
title: '胆汁淤积性诊断与制',
subtitle: '定及临床手段',
fontSize: 14
});
// 福利横幅
drawImage('welfare_banner', {
startColor: '#F3E5F5',
endColor: '#9C27B0',
title: '福利课堂 - 免费学习医学知识',
fontSize: 20,
icon: '🎁'
});
// HIV课程图
drawImage('hiv_course', {
startColor: '#E3F2FD',
endColor: '#1976D2',
title: 'HIV感染',
subtitle: '抗菌药特性',
fontSize: 16,
icon: '💊'
});
};
</script>
</body>
</html>

View File

@ -39,6 +39,16 @@
}
}
},
{
"path": "pages/meeting/meeting",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "直播",
"app": {
"bounce": "none"
}
}
},
{
"path": "pages/education/education",
"style": {
@ -178,6 +188,16 @@
{
"root": "pages_app",
"pages": [
{
"path": "changePassword/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "修改登录密码",
"app": {
"bounce": "none"
}
}
},
{
"path": "search/search",
"style": {
@ -259,6 +279,14 @@
}
}
},
{
"path": "myWelfareCard/exchange",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "兑换福利卡",
"app": { "bounce": "none" }
}
},
{
"path": "ppt/ppt",
"style": {
@ -521,6 +549,16 @@
}
}
},
{
"path": "idcardAuth/bankCardList",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "常用银行卡",
"app": {
"bounce": "none"
}
}
},
{
"path": "myVideo/myVideo",
"style": {
@ -664,6 +702,71 @@
}
]
},
{
"root": "pages_goods",
"pages": [
{
"path": "pointMall/pointMall",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "积分商城",
"app": {
"bounce": "none"
}
}
},
{
"path": "myRedemption/myRedemption",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "我的兑换",
"app": {
"bounce": "none"
}
}
},
{
"path": "productDetail/productDetail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "商品详情",
"app": {
"bounce": "none"
}
}
},
{
"path": "exchange/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "在线兑换",
"app": {
"bounce": "none"
}
}
},
{
"path": "exchange/address_list",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "地址管理",
"app": {
"bounce": "none"
}
}
},
{
"path": "exchange/address",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "收货地址",
"app": {
"bounce": "none"
}
}
}
]
}],
"globalStyle": {

1206
pages/meeting/meeting.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -110,7 +110,7 @@
<up-image :src="kjmxImg" width="48rpx" height="48rpx"></up-image>
<text>课件明细</text>
</view>
<view class="account-item" @click="goToPage('courseDetail')">
<view class="account-item" @click="goToPage('couseDetail')">
<up-image :src="kcmxImg" width="48rpx" height="48rpx"></up-image>
<text>课程明细</text>
</view>
@ -331,13 +331,13 @@
url="/pages_app/myWelfare/myWelfare"
break;
case 'myFlower':
url="/pages_app/myFlower/myFlower"
url="/pages_app/myFlower/myFlower"
break;
case 'pptDetail':
url="/pages_app/myFlower/myFlower"
url="/pages_app/myCourseware/myCourseware"
break;
case 'couseDetail':
url="/pages_app/myFlower/myFlower"
url="/pages_course/order_record/order_record"
break;
case 'wechatUnbind':
url="/pages_app/wechatContact/wechatContact"
@ -349,7 +349,7 @@
url="/pages_app/myWelfareCard/myWelfareCard"
break;
case 'bankCard':
url="/pages_app/bindCard/bindCard"
url="/pages_app/idcardAuth/bankCardList"
break;
case 'settings':
url="/pages_app/setting/setting"
@ -587,7 +587,7 @@
border-radius: 20rpx;
overflow: hidden;
background-color: #fff;
margin-bottom: 30rpx;
margin-bottom: 230rpx;
}
//

View File

@ -61,8 +61,9 @@
<script setup>
import { ref, onUnmounted,computed } from 'vue';
import phoneImg from "@/static/phone.png"
import smsImg from "@/static/sms.png"
import phoneImg from "@/static/phone.png"
import smsImg from "@/static/sms.png"
import api from "@/api/api.js"
const mobile = ref('');
const code = ref('');
const sending = ref(false);
@ -94,8 +95,17 @@ const sendCode = () => {
return;
}
if (sending.value) return;
api.smsSend({
mobile: mobile.value,
type: 6
}).then(res => {
if (res.code === 200) {
uni.showToast({ title: '验证码已发送', icon: 'success' });
} else {
uni.showToast({ title: res.msg || '验证码发送失败', icon: 'none' });
}
});
// TODO:
uni.showToast({ title: '验证码已发送', icon: 'success' });
startTimer();
};
@ -108,8 +118,18 @@ const onConfirm = () => {
uni.showToast({ title: '请输入正确的验证码', icon: 'none' });
return;
}
// TODO:
uni.showToast({ title: '已提交', icon: 'success' });
//String oldMobile,newMobile, sms = params.get("sms");
api.changeMobile({
newMobile: mobile.value,
sms: code.value,
oldMobile: uni.getStorageSync('userInfo').mobile
}).then(res => {
if (res.code === 200) {
uni.showToast({ title: '更换手机号成功', icon: 'success' });
} else {
uni.showToast({ title: res.msg || '更换手机号失败', icon: 'none' });
}
});
};
onUnmounted(() => {

View File

@ -0,0 +1,142 @@
<template>
<view class="change-pwd-page">
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
title="修改登录密码"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
<!-- 表单区域 -->
<view class="form-wrapper">
<view class="input-row">
<input class="text-input" :password="true" v-model="oldPwd" placeholder="请输入原密码" placeholder-style="color:#c7c7c7" />
</view>
<view class="input-row">
<input class="text-input" :password="!showNewPwd" v-model="newPwd" placeholder="新密码(6~16位数字字母组合)" placeholder-style="color:#c7c7c7" />
</view>
<view class="input-row">
<input class="text-input" :password="!showConfirmPwd" v-model="confirmPwd" placeholder="确认新密码" placeholder-style="color:#c7c7c7" />
</view>
<view class="forgot" @click="goForgot">忘记密码?</view>
</view>
<!-- 底部确定按钮 -->
<view class="bottom-bar">
<button class="confirm-btn" @click="onConfirm"> </button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import api from "@/api/api.js"
const oldPwd = ref('');
const newPwd = ref('');
const confirmPwd = ref('');
const showNewPwd = ref(false);
const showConfirmPwd = ref(false);
const goBack = () => {
uni.navigateBack();
};
const goForgot = () => {
//
uni.navigateTo({ url: '/pages_app/smsLogin/smsLogin' });
};
const isStrong = (pwd) => {
return /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6,16}$/.test(pwd);
};
const onConfirm = () => {
if (!oldPwd.value) {
return uni.showToast({ title: '请输入原密码', icon: 'none' });
}
if (!newPwd.value) {
return uni.showToast({ title: '请输入新密码', icon: 'none' });
}
if (!isStrong(newPwd.value)) {
return uni.showToast({ title: '密码需6-16位数字字母组合', icon: 'none' });
}
if (confirmPwd.value !== newPwd.value) {
return uni.showToast({ title: '两次输入的密码不一致', icon: 'none' });
}
api.changePassword({
old_password: oldPwd.value,
password: newPwd.value
}).then(res => {
if (res.code === 200) {
uni.showToast({ title: '修改密码成功', icon: 'success' });
//
uni.clearStorageSync();
setTimeout(() => {
uni.reLaunch({
url: '/pages_app/login/login'
});
}, 2000);
} else {
uni.showToast({ title: res.msg || '修改密码失败', icon: 'none' });
}
});
};
</script>
<style scoped>
.change-pwd-page {
background: #ffffff;
min-height: 100vh;
}
.form-wrapper {
padding: 40rpx 30rpx 0;
}
.input-row {
margin-bottom: 30rpx;
background: #ffffff;
border: 2rpx solid #eeeeee;
border-radius: 12rpx;
padding: 0 24rpx;
}
.text-input {
height: 96rpx;
font-size: 30rpx;
}
.forgot {
text-align: center;
color: #8b8b8b;
font-size: 28rpx;
margin-top: 20rpx;
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 40rpx;
padding: 0 30rpx;
}
.confirm-btn {
height: 100rpx;
line-height: 100rpx;
border-radius: 50rpx;
background-color: #ffffff;
border: 2rpx solid #8B2316;
color: #8B2316;
font-size: 36rpx;
}
</style>

View File

@ -0,0 +1,160 @@
<template>
<view class="bank-card-page">
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
title="常用银行卡"
@clickLeft="goBack()"
right-text="添加"
@clickRight="addBankCard"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
>
</uni-nav-bar>
<!-- 银行卡列表 -->
<view class="card-list">
<view class="card-item" v-for="(card, index) in bankCards" :key="card.uuid">
<view class="card-logo">
<view class="logo-bg">
<text class="logo-text">{{ getBankLogo(card.open_bank) }}</text>
</view>
</view>
<view class="card-info">
<view class="card-number">尾号{{ card.card_number }}储蓄卡</view>
<view class="bank-name">{{ card.open_bank }}</view>
</view>
</view>
</view>
<!-- 底部导航指示器 -->
<view class="bottom-indicator"></view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import navTo from '@/utils/navTo';
import api from '@/api/api';
const bankCards = ref([]);
//
const getBankCardList = () => {
api.bankCardList({}).then(res => {
if (res.code === 200 && res.data.bankList) {
bankCards.value = res.data.bankList;
}
});
};
const goBack = () => {
uni.navigateBack();
};
const addBankCard = () => {
navTo({
url: '/pages_app/idcardAuth/idcardAuth'
});
};
// logo
const getBankLogo = (bankName) => {
const bankLogos = {
'工商银行': '工',
'中国银行': '中',
'建设银行': '建',
'农业银行': '农',
'交通银行': '交',
'招商银行': '招',
'民生银行': '民',
'兴业银行': '兴',
'浦发银行': '浦',
'光大银行': '光',
'华夏银行': '华',
'中信银行': '信',
'平安银行': '平',
'广发银行': '广',
'邮储银行': '邮'
};
return bankLogos[bankName] || bankName.charAt(0);
};
onMounted(() => {
getBankCardList();
});
</script>
<style lang="scss" scoped>
.bank-card-page {
min-height: 100vh;
background: #F5F5F5;
position: relative;
}
/* 银行卡列表样式 */
.card-list {
padding: 30rpx;
.card-item {
background: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.card-logo {
margin-right: 30rpx;
.logo-bg {
width: 80rpx;
height: 80rpx;
background: #E60012;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #E60012;
.logo-text {
color: #ffffff;
font-size: 32rpx;
font-weight: bold;
}
}
}
.card-info {
flex: 1;
.card-number {
font-size: 28rpx;
color: #000000;
margin-bottom: 8rpx;
font-weight: 500;
}
.bank-name {
font-size: 24rpx;
color: #666666;
}
}
}
}
/* 底部指示器 */
.bottom-indicator {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 120rpx;
height: 6rpx;
background: #333333;
border-radius: 3rpx;
}
</style>

View File

@ -3,8 +3,8 @@
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
title="身份验证"
@cviewckLeft="goBack"
:title="currentStep === 1 ? '身份验证' : '添加银行卡'"
@clickLeft="goBack()"
fixed
color="#8B2316"
height="140rpx"
@ -18,18 +18,18 @@
<view class="progress-bar">
<view class="barbox">
<view class="imgbox">
<up-image :src="stepImg" width="46rpx" height="46rpx" ></up-image>
<view class="desc ">身份信息</view>
<up-image :src="currentStep >= 1 ? stepActiveImg : stepImg" width="46rpx" height="46rpx"></up-image>
<view class="desc" :class="{ active: currentStep >= 1 }">身份信息</view>
</view>
<view class="line"></view>
<view class="line" :class="{ active: currentStep >= 2 }"></view>
<view class="imgbox">
<up-image :src="stepImg" width="46rpx" height="46rpx" ></up-image>
<view class="desc">添加银行卡</view>
<up-image :src="currentStep >= 2 ? stepActiveImg : stepImg" width="46rpx" height="46rpx"></up-image>
<view class="desc" :class="{ active: currentStep >= 2 }">添加银行卡</view>
</view>
<view class="line"></view>
<view class="line" :class="{ active: currentStep >= 3 }"></view>
<view class="imgbox">
<up-image :src="stepImg" width="46rpx" height="46rpx" ></up-image>
<view class="desc">完成</view>
<up-image :src="currentStep >= 3 ? stepActiveImg : stepImg" width="46rpx" height="46rpx"></up-image>
<view class="desc" :class="{ active: currentStep >= 3 }">完成</view>
</view>
</view>
@ -57,23 +57,58 @@
<!-- 输入表单 -->
<view class="form-section">
<view class="form-item">
<text class="form-label">姓名</text>
<input
class="form-input"
placeholder="请输入您的姓名"
v-model="formData.name"
placeholder-style="color: #cccccc"
/>
<!-- 第一步身份信息 -->
<view v-if="currentStep === 1">
<view class="form-item">
<text class="form-label">姓名</text>
<input
class="form-input"
placeholder="请输入您的姓名"
v-model="formData.name"
placeholder-style="color: #cccccc"
/>
</view>
<view class="form-item">
<text class="form-label">身份证号</text>
<input
class="form-input"
placeholder="请输入您的身份证号"
v-model="formData.idNumber"
placeholder-style="color: #cccccc"
/>
</view>
</view>
<view class="form-item">
<text class="form-label">身份证号</text>
<input
class="form-input"
placeholder="请输入您的身份证号"
v-model="formData.idNumber"
placeholder-style="color: #cccccc"
/>
<!-- 第二步添加银行卡 -->
<view v-if="currentStep === 2">
<view class="form-item">
<text class="form-label">银行卡号</text>
<view class="input-container">
<input
class="form-input"
placeholder="仅限借记卡"
v-model="formData.cardNumber"
placeholder-style="color: #cccccc"
/>
<view class="info-icon">
<text class="icon-text">!</text>
</view>
</view>
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<view class="input-container">
<input
class="form-input"
placeholder="银行预留手机号"
v-model="formData.mobile"
placeholder-style="color: #cccccc"
/>
<view class="info-icon">
<text class="icon-text">!</text>
</view>
</view>
</view>
</view>
</view>
@ -84,38 +119,215 @@
<!-- 下一步按钮 -->
<view class="bottom-actions">
<button class="next-btn" @click="onNextStep">下一步</button>
<button class="next-btn" :class="{ loading: isLoading }" @click="onNextStep" :disabled="isLoading">
<text v-if="!isLoading">下一步</text>
<text v-else>验证中...</text>
</button>
</view>
<!-- 短信验证码弹框 -->
<view v-if="showSmsDialog" class="sms-mask">
<view class="sms-dialog">
<view class="sms-title">短信验证码</view>
<view class="sms-subtitle">请输入手机{{ maskedMobile }}收到的验证码</view>
<view class="sms-input-row">
<input class="sms-input" v-model="smsCode" placeholder="请输入验证码" placeholder-style="color: #cccccc" />
<button class="sms-code-btn" :disabled="countdown > 0 || sendingCode" @click="onGetSmsCode">
<text v-if="countdown === 0">获取验证码</text>
<text v-else>{{ countdown }}s</text>
</button>
</view>
<view class="sms-actions">
<button class="sms-cancel" @click="onCancelSms">取消</button>
<button class="sms-confirm" @click="onConfirmSms">确定</button>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import stepImg from "@/static/add_card_no.png"
import stepActiveImg from "@/static/add_card_yes.png"
import navTo from '@/utils/navTo';
import api from '@/api/api';
const formData = ref({
name: '',
idNumber: ''
idNumber: '',
cardNumber: '',
mobile: ''
});
const isLoading = ref(false);
const currentStep = ref(1); // 12
//
const showSmsDialog = ref(false);
const smsCode = ref('');
const sendingCode = ref(false);
const countdown = ref(0);
let countdownTimer = null;
const maskedMobile = computed(() => {
const m = formData.value.mobile || '';
if (m && m.length >= 7) {
return `${m.slice(0,3)}****${m.slice(-4)}`;
}
return m || '***********';
});
const goBack = () => {
uni.navigateBack();
};
const onNextStep = () => {
if (!formData.value.name.trim()) {
uni.showToast({ title: '请输入姓名', icon: 'none' });
return;
}
if (!formData.value.idNumber.trim()) {
uni.showToast({ title: '请输入身份证号', icon: 'none' });
return;
}
uni.showToast({ title: '验证通过,跳转下一步', icon: 'success' });
//
//
const validateIdNumber = (idNumber) => {
const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
return reg.test(idNumber);
};
//
const validateName = (name) => {
const reg = /^[\u4e00-\u9fa5]{2,10}$/;
return reg.test(name);
};
//
const validateCardNumber = (cardNumber) => {
const reg = /^\d{16,19}$/;
return reg.test(cardNumber);
};
//
const validateMobile = (mobile) => {
const reg = /^1[3-9]\d{9}$/;
return reg.test(mobile);
};
const onNextStep = async () => {
if (currentStep.value === 1) {
//
if (!formData.value.name.trim()) {
uni.showToast({ title: '请输入姓名', icon: 'none' });
return;
}
if (!validateName(formData.value.name)) {
uni.showToast({ title: '请输入正确的姓名2-10个汉字', icon: 'none' });
return;
}
if (!formData.value.idNumber.trim()) {
uni.showToast({ title: '请输入身份证号', icon: 'none' });
return;
}
if (!validateIdNumber(formData.value.idNumber)) {
uni.showToast({ title: '请输入正确的身份证号', icon: 'none' });
return;
}
//
isLoading.value = true;
try {
// API
const res = {code:200}
if (res.code === 200) {
uni.showToast({ title: '身份验证成功', icon: 'success' });
currentStep.value = 2;
isLoading.value = false;
} else {
uni.showToast({ title: res.msg || '身份验证失败', icon: 'none' });
isLoading.value = false;
}
} catch (error) {
console.error('身份验证失败:', error);
uni.showToast({ title: '网络错误,请重试', icon: 'none' });
isLoading.value = false;
}
} else if (currentStep.value === 2) {
//
if (!formData.value.cardNumber.trim()) {
uni.showToast({ title: '请输入银行卡号', icon: 'none' });
return;
}
if (!validateCardNumber(formData.value.cardNumber)) {
uni.showToast({ title: '请输入正确的银行卡号', icon: 'none' });
return;
}
if (!formData.value.mobile.trim()) {
uni.showToast({ title: '请输入手机号', icon: 'none' });
return;
}
if (!validateMobile(formData.value.mobile)) {
uni.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
//
showSmsDialog.value = true;
}
};
const onGetSmsCode = async () => {
if (countdown.value > 0) return;
const res = await api.smsSend({
mobile: formData.value.mobile,
type: 3
});
if (res.code === 200) {
uni.showToast({ title: '短信验证码发送成功', icon: 'success' });
// 60
countdown.value = 60;
if (countdownTimer) clearInterval(countdownTimer);
countdownTimer = setInterval(() => {
if (countdown.value > 0) {
countdown.value -= 1;
} else {
clearInterval(countdownTimer);
countdownTimer = null;
}
}, 1000);
} else {
uni.showToast({ title: res.msg || '短信验证码发送失败', icon: 'none' });
}
};
const onConfirmSms = async () => {
if (!smsCode.value) {
uni.showToast({ title: '请输入短信验证码', icon: 'none' });
return;
}
const res = await api.identificationBankCardNew({
phone_number: formData.value.mobile,
sms: smsCode.value,
id_number: formData.value.idNumber,
card_number: formData.value.cardNumber,
id_name: formData.value.name
});
if (res.code === 200) {
uni.showToast({ title: '银行卡添加成功', icon: 'success' });
navTo({
url: '/pages_app/idcardAuth/bankCardList'
});
} else {
uni.showToast({ title: res.msg || '银行卡添加失败', icon: 'none' });
}
};
const onCancelSms = () => {
showSmsDialog.value = false;
};
</script>
<style lang="scss" scoped>
@ -235,10 +447,11 @@ const onNextStep = () => {
}
}
.form-section {
background: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
padding: 0;
.form-item {
@ -246,58 +459,207 @@ const onNextStep = () => {
display: flex;
align-items: center;
border-bottom: 2rpx solid #eee;
padding: 0 30rpx 20rpx;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
color: #000000;
width:120rpx;
width: 120rpx;
font-weight: 500;
}
.form-input {
flex:1;
flex: 1;
height: 80rpx;
padding: 0 20rpx;
font-size: 28rpx;
background: #ffffff;
border: none;
outline: none;
&:focus {
border-color: #ff0000;
border-color: #8B2316;
}
}
.input-container {
display: flex;
align-items: center;
position: relative;
width: 80%;
.form-input {
flex: 9;
height: 80rpx;
padding: 0 20rpx;
font-size: 28rpx;
background: #ffffff;
border: none;
border-radius: 0;
outline: none;
&:focus {
/* 去除边框后无需变更边框颜色 */
}
}
.info-icon {
flex:1;
position: absolute;
right: 20rpx;
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
.icon-text {
font-size: 20rpx;
color: #cccccc;
font-weight: bold;
}
}
}
}
}
.warning-text {
background: #ffffff;
background: #fff3cd;
border: 2rpx solid #ffeaa7;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 60rpx;
font-size: 26rpx;
color: #000000;
color: #856404;
line-height: 1.6;
position: relative;
&::before {
content: "⚠️";
position: absolute;
left: 30rpx;
top: 30rpx;
font-size: 32rpx;
}
padding-left: 80rpx;
}
.bottom-actions {
padding: 0 30rox 40rpx;
padding: 0 30rpx 40rpx;
.next-btn {
margin:0 30rpx;
width: 100%;
height: 88rpx;
background: #cccccc;
background: #8B2316;
color: #ffffff;
border: none;
border-radius: 8rpx;
font-size: 32rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:active {
background: #8B2316;
background: #6B1A0F;
}
&.loading {
background: #cccccc;
opacity: 0.7;
}
&:disabled {
background: #cccccc;
opacity: 0.7;
}
}
}
/* 短信弹框样式 */
.sms-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.sms-dialog {
width: 680rpx;
background: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
}
.sms-title {
text-align: center;
font-size: 34rpx;
color: #000000;
font-weight: 600;
margin-bottom: 20rpx;
}
.sms-subtitle {
text-align: center;
font-size: 28rpx;
color: #333333;
margin-bottom: 24rpx;
}
.sms-input-row {
display: flex;
align-items: center;
gap: 16rpx;
margin: 10rpx 0 30rpx;
}
.sms-input {
flex: 1;
height: 88rpx;
border: 2rpx solid #eeeeee;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
.sms-code-btn {
width: 200rpx;
height: 88rpx;
border-radius: 8rpx;
background: #eeeeee;
color: #8B2316;
font-size: 26rpx;
}
.sms-actions {
display: flex;
justify-content: space-between;
margin-top: 10rpx;
}
.sms-cancel,
.sms-confirm {
flex: 1;
height: 88rpx;
border-radius: 8rpx;
font-size: 32rpx;
}
.sms-cancel { background: #f5f5f5; color: #000000; margin-right: 20rpx; }
.sms-confirm { background: #8B2316; color: #ffffff; margin-left: 20rpx; }
</style>

View File

@ -4,6 +4,8 @@
<uni-nav-bar
left-icon="left"
title="消息"
right-text="清除"
@clickRight="clearMsg"
@clickLeft="goBack"
fixed
color="#8B2316"
@ -19,25 +21,28 @@
<view class="grid-item" @click="goBenefit">
<view class="icon-wrap gift">
<uni-icons type="gift" size="34" color="#fff"></uni-icons>
<view class="badge" v-if="badgeData.Module_Welfare > 0">{{ badgeData.Module_Welfare }}</view>
</view>
<text class="label">福利</text>
</view>
<view class="grid-item" @click="goOrder">
<view class="icon-wrap order">
<uni-icons type="list" size="34" color="#fff"></uni-icons>
<view class="badge" v-if="badgeData.Module_Order > 0">{{ badgeData.Module_Order }}</view>
</view>
<text class="label">订单</text>
</view>
<view class="grid-item" @click="goFollow">
<view class="icon-wrap visit">
<uni-icons type="heart" size="34" color="#fff"></uni-icons>
<view class="badge" v-if="followBadge > 0">{{ followBadge }}</view>
<view class="badge" v-if="badgeData.Module_Relation > 0">{{ badgeData.Module_Relation }}</view>
</view>
<text class="label">随访</text>
</view>
<view class="grid-item" @click="goReply">
<view class="icon-wrap reply">
<uni-icons type="chatbubble" size="34" color="#fff"></uni-icons>
<view class="badge" v-if="badgeData.Module_Comment > 0">{{ badgeData.Module_Comment }}</view>
</view>
<text class="label">回复我的</text>
</view>
@ -54,13 +59,35 @@
@scrolltolower="onLoadMore"
lower-threshold="80"
>
<view class="group" v-for="(group, gIdx) in msgGroups" :key="gIdx">
<view class="group-time">{{ group.time }}</view>
<view class="card" v-for="(msg, idx) in group.items" :key="idx">
<view class="card-title">系统消息</view>
<view class="card-content">{{ msg }}</view>
<!-- 回复我的模块 - 聊天列表样式 -->
<view v-if="currentModule === 4" class="chat-list">
<view class="chat-item" v-for="(msg, idx) in msgList" :key="msg.id">
<view class="avatar">
<image :src="msg.avatar || '/static/default-avatar.png'" mode="aspectFill" @error="handleImageError"></image>
</view>
<view class="chat-content">
<view class="chat-header">
<text class="sender-name">{{ msg.sender_name || msg.title }}</text>
<text class="chat-time">{{ msg.create_date }}</text>
</view>
<view class="message-preview">{{ msg.content }}</view>
</view>
</view>
</view>
<!-- 其他模块 - 卡片样式 -->
<view v-else>
<view class="card" v-for="(msg, idx) in msgList" :key="msg.id">
<view class="card-title">{{ msg.title }}</view>
<view class="card-content">{{ msg.content }}</view>
<view class="card-time">{{ msg.create_date }}</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="msgList.length === 0" class="empty-state">
<text class="empty-text">暂无消息</text>
</view>
<!-- 加载更多提示 -->
<view v-if="loading" class="loading-more">
<text class="loading-text">加载中...</text>
@ -74,24 +101,21 @@
</template>
<script setup>
import { ref } from 'vue';
import { onMounted, ref } from 'vue';
import api from '@/api/api.js';
import docUrl from '@/utils/docUrl';
const followBadge = ref(5);
//
const badgeData = ref({
Module_Order: 0, //
Module_Comment: 0, //
Module_Welfare: 0, //
Module_Relation: 0 // 访
});
const msgGroups = ref([
{
time: '2022-04-22 16:32:54',
items: ['恭喜您,您的肝胆积分又增加了,快去查看吧']
},
{
time: '2022-04-20 13:33:23',
items: ['恭喜您,您的肝胆积分又增加了,快去查看吧']
},
{
time: '2022-04-15 13:36:23',
items: ['恭喜您,您的肝胆积分又增加了,快去查看吧']
}
]);
//
const msgList = ref([]);
const currentModule = ref(1); // 1
// /
const refreshing = ref(false);
@ -100,13 +124,7 @@
const page = ref(1);
const pageSize = ref(10);
//
const mockMore = [
{ time: '2022-04-10 09:20:11', items: ['系统保养完成,服务更稳定~'] },
{ time: '2022-04-05 18:06:42', items: ['积分到账提醒,请注意查收。'] },
{ time: '2022-03-30 08:15:00', items: ['本周学术会议日程已发布。'] },
{ time: '2022-03-25 10:05:16', items: ['您有新的随访任务待处理。'] }
];
const goBack = () => {
uni.navigateBack({
@ -118,23 +136,46 @@
});
};
const goBenefit = () => uni.showToast({ title: '福利', icon: 'none' });
const goOrder = () => uni.showToast({ title: '订单', icon: 'none' });
const goFollow = () => uni.showToast({ title: '随访', icon: 'none' });
const goReply = () => uni.showToast({ title: '回复我的', icon: 'none' });
const goBenefit = () => {
currentModule.value = 1;
getAppMesageList(1);
};
const goOrder = () => {
currentModule.value = 2;
getAppMesageList(2);
};
const goFollow = () => {
currentModule.value = 3;
getAppMesageList(3);
};
const goReply = () => {
currentModule.value = 4;
getAppMesageList(4);
};
const clearMsg = () => {
uni.showModal({
title: '提醒',
content: '是否要清除所有未读消息?',
success: (res) => {
if (res.confirm) {
api.appMesageRead({}).then(res => {
console.log(res);
});
getUnReadList();
}
}
});
}
//
const onRefresh = async () => {
if (refreshing.value) return;
refreshing.value = true;
try {
await new Promise(r => setTimeout(r, 800));
page.value = 1;
noMore.value = false;
msgGroups.value.unshift({
time: new Date().toISOString().slice(0, 19).replace('T', ' '),
items: ['您有一条新的系统通知,欢迎查看。']
});
//
getAppMesageList(currentModule.value);
//
getUnReadList();
} finally {
refreshing.value = false;
}
@ -145,19 +186,73 @@
if (loading.value || noMore.value) return;
loading.value = true;
try {
await new Promise(r => setTimeout(r, 800));
const start = (page.value - 1) * 2;
const chunk = mockMore.slice(start, start + 2);
if (chunk.length === 0) {
noMore.value = true;
} else {
msgGroups.value.push(...chunk);
page.value += 1;
}
//
//
// getAppMesageList
noMore.value = true; //
} finally {
loading.value = false;
}
};
const getUnReadList = () => {
api.unReadList({}).then(res => {
console.log(res);
if (res.code === 200 && res.data) {
//
badgeData.value = {
Module_Order: parseInt(res.data.Module_Order) || 0,
Module_Comment: parseInt(res.data.Module_Comment) || 0,
Module_Welfare: parseInt(res.data.Module_Welfare) || 0,
Module_Relation: parseInt(res.data.Module_Relation) || 0
};
}
}).catch(err => {
console.error('获取未读消息列表失败:', err);
});
};
const getAppMesageList = (module) => {
api.appMesageList({module: module}).then(res => {
console.log(res);
if (res.code === 200 && res.data && res.data.list) {
//
msgList.value = res.data.list.map(item => ({
id: item.id,
title: item.title,
content: item.content,
create_date: item.create_date,
is_read: item.is_read,
extra: item.extra,
// extra
sender_name: item.extra?.user_name || item.title,
avatar: item.extra?.user_photo ?
(item.extra.user_photo.startsWith('http') ?
item.extra.user_photo :
`${getBaseUrl()}${item.extra.user_photo}`) :
'/static/default-avatar.png'
}));
}
}).catch(err => {
console.error('获取消息列表失败:', err);
});
};
// URL
const getBaseUrl = () => {
return docUrl;
};
//
const handleImageError = (e) => {
//
e.target.src = '/static/default-avatar.png';
};
onMounted(() => {
getUnReadList();
getAppMesageList(1);
});
</script>
<style lang="scss" scoped>
@ -243,41 +338,107 @@
box-sizing: border-box;
bottom: 0rpx;
//
.loading-more, .no-more {
.loading-more, .no-more, .empty-state {
@include flex-center;
padding: 28rpx 0;
.loading-text, .no-more-text {
.loading-text, .no-more-text, .empty-text {
font-size: 26rpx;
color: $muted;
}
}
.group {
.group-time {
text-align: center;
color: $muted;
font-size: 24rpx;
margin: 26rpx 0 16rpx;
.card {
background: #fff;
border-radius: 16rpx;
padding: 26rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
margin-top: 26rpx;
.card-title {
font-size: 30rpx;
color: $text-primary;
font-weight: 600;
margin-bottom: 14rpx;
}
.card {
background: #fff;
border-radius: 16rpx;
padding: 26rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.04);
margin-bottom: 26rpx;
.card-content {
font-size: 28rpx;
color: $text-secondary;
line-height: 1.6;
margin-bottom: 14rpx;
}
.card-title {
font-size: 30rpx;
color: $text-primary;
font-weight: 600;
margin-bottom: 14rpx;
.card-time {
font-size: 24rpx;
color: $muted;
text-align: right;
}
}
//
.chat-list {
.chat-item {
display: flex;
align-items: center;
padding: 24rpx;
border-bottom: 1rpx solid #f0f0f0;
background: #fff;
margin-top: 24rpx;
&:last-child {
border-bottom: none;
}
.card-content {
font-size: 28rpx;
color: $text-secondary;
line-height: 1.6;
.avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 24rpx;
flex-shrink: 0;
image {
width: 100%;
height: 100%;
}
}
.chat-content {
flex: 1;
min-width: 0;
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
.sender-name {
font-size: 30rpx;
color: $text-primary;
font-weight: 500;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-time {
font-size: 24rpx;
color: $muted;
flex-shrink: 0;
margin-left: 16rpx;
}
}
.message-preview {
font-size: 28rpx;
color: $text-secondary;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}

View File

@ -37,11 +37,11 @@
<view class="summary-bar">
<view class="summary-item">
<up-image :src="downLoadImg" width="36rpx" height="36rpx" ></up-image>
<text class="summary-text">: {{ downloadCount }}</text>
<text class="summary-text">{{ activeTab === 'download' ? '下载账户' : '分享账户' }}: {{ downloadCount }}</text>
</view>
<view class="summary-item">
<up-image :src="moneyImg" width="36rpx" height="36rpx" ></up-image>
<text class="summary-text">: {{ totalAmount }}</text>
<text class="summary-text">{{ activeTab === 'download' ? '文件数量' : '分享文件' }}: {{ totalAmount }}</text>
</view>
</view>
@ -55,12 +55,21 @@
@scrolltolower="onLoadMore"
:lower-threshold="100"
>
<!-- 空状态 -->
<view v-if="coursewareList.length === 0 && !loading" class="empty-state">
<text>{{ activeTab === 'download' ? '暂无下载数据' : '暂无分享数据' }}</text>
</view>
<view class="courseware-item" v-for="(item, index) in coursewareList" :key="index" @click="onItemClick(item)">
<view class="item-content">
<view class="courseware-name">
<text class="label">课件名称:</text>
<text class="value">{{ item.name }}</text>
</view>
<view class="courseware-provider" v-if="item.providername">
<text class="label">{{ activeTab === 'download' ? '提供者' : '下载者' }}:</text>
<text class="value">{{ item.providername }}</text>
</view>
<view class="courseware-time">
<text class="label">时间:</text>
<text class="value">{{ item.time }}</text>
@ -78,7 +87,7 @@
</view>
<!-- 没有更多数据提示 -->
<view v-if="noMore" class="no-more">
<view v-if="noMore && coursewareList.length > 0" class="no-more">
<text>没有更多数据了</text>
</view>
</scroll-view>
@ -86,7 +95,8 @@
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import api from '@/api/api';
import downLoadImg from "@/static/course_download.png"
import moneyImg from "@/static/course_yuan.png"
const activeTab = ref('download');
@ -98,28 +108,11 @@ const pageSize = ref(10);
const downloadCount = ref(4);
const totalAmount = ref('20.00');
const coursewareList = ref([
{
name: '慢性病毒性肝炎患者干扰素治疗不良反应临床处理专家共识',
time: '2025-02-21',
status: '已支付'
},
{
name: '慢乙肝抗病毒治疗-把握时机正确选择',
time: '2024-11-27',
status: '已支付'
},
{
name: '肝病相关血小板减少症临床管理中国专家共识(2023)解读',
time: '2024-10-06',
status: '已支付'
},
{
name: '俞云松:耐药阳性菌感染诊疗思路(CHINET数据云)',
time: '2022-10-24',
status: '已支付'
}
]);
const coursewareList = ref([]);
onMounted(() => {
onRefresh();
});
const goBack = () => {
uni.navigateBack();
@ -131,37 +124,173 @@ const switchTab = (tab) => {
page.value = 1;
noMore.value = false;
//
onRefresh();
};
const onItemClick = (item) => {
uni.showToast({ title: `点击了: ${item.name}`, icon: 'none' });
//
console.log('点击课件:', item);
if (activeTab.value === 'download') {
//
if (item.download_path) {
uni.downloadFile({
url: item.download_path,
success: (res) => {
if (res.statusCode === 200) {
uni.openDocument({
filePath: res.tempFilePath,
success: () => {
console.log('打开文档成功');
},
fail: (err) => {
console.error('打开文档失败:', err);
uni.showToast({ title: '无法打开此文件', icon: 'none' });
}
});
}
},
fail: (err) => {
console.error('下载失败:', err);
uni.showToast({ title: '下载失败', icon: 'none' });
}
});
} else {
uni.showToast({ title: `点击了下载课件: ${item.name}`, icon: 'none' });
}
} else {
//
uni.showToast({ title: `点击了分享课件: ${item.name}`, icon: 'none' });
//
}
};
const onRefresh = () => {
refreshing.value = true;
page.value = 1;
noMore.value = false;
setTimeout(() => {
refreshing.value = false;
uni.showToast({ title: '刷新完成', icon: 'success' });
}, 1000);
if (activeTab.value === 'download') {
api.getGandanfileMyDownload({ page: page.value }).then(res => {
console.log(res);
if (res.code === 200 && res.data) {
const data = res.data.list;
//
downloadCount.value = res.data.downloadTotalAccount || 0;
totalAmount.value = res.data.downloadFileCount || 0;
//
coursewareList.value = data.list.map(item => ({
name: item.title || '未知课件',
time: item.create_date ? item.create_date.split(' ')[0] : '',
status: item.order_status === 'paid' ? '已支付' : '未支付',
uuid: item.uuid,
order_id: item.order_id,
type: item.type,
providername: item.providername
}));
//
noMore.value = data.pageNumber >= data.totalPage;
}
refreshing.value = false;
}).catch(err => {
console.error('获取下载列表失败:', err);
refreshing.value = false;
});
} else {
api.getGandanfileMyShare({ page: page.value }).then(res => {
console.log(res);
if (res.code === 200 && res.data) {
const data = res.data.list;
// - 使
downloadCount.value = res.data.shareTotalAccount || 0;
totalAmount.value = res.data.shareloadFileCount || 0;
//
coursewareList.value = data.list.map(item => ({
name: item.title || '未知课件',
time: item.create_date ? item.create_date.split(' ')[0] : '',
status: '已分享', //
uuid: item.uuid,
order_id: item.order_id,
type: item.type,
providername: item.downloadername, //
downloadername: item.downloadername
}));
//
noMore.value = data.pageNumber >= data.totalPage;
}
refreshing.value = false;
}).catch(err => {
console.error('获取分享列表失败:', err);
refreshing.value = false;
});
}
};
const onLoadMore = () => {
if (loading.value || noMore.value) return;
loading.value = true;
page.value++;
setTimeout(() => {
if (page.value < 3) {
page.value++;
uni.showToast({ title: '加载完成', icon: 'success' });
} else {
noMore.value = true;
}
loading.value = false;
}, 1000);
if (activeTab.value === 'download') {
api.getGandanfileMyDownload({ page: page.value }).then(res => {
console.log(res);
if (res.code === 200 && res.data) {
const data = res.data.list;
//
const newList = data.list.map(item => ({
name: item.title || '未知课件',
time: item.create_date ? item.create_date.split(' ')[0] : '',
status: item.order_status === 'paid' ? '已支付' : '未支付',
uuid: item.uuid,
order_id: item.order_id,
type: item.type,
providername: item.providername
}));
coursewareList.value = [...coursewareList.value, ...newList];
//
noMore.value = data.pageNumber >= data.totalPage;
}
loading.value = false;
}).catch(err => {
console.error('加载更多下载列表失败:', err);
page.value--; //
loading.value = false;
});
} else {
api.getGandanfileMyShare({ page: page.value }).then(res => {
console.log(res);
if (res.code === 200 && res.data) {
const data = res.data.list;
//
const newList = data.list.map(item => ({
name: item.title || '未知课件',
time: item.create_date ? item.create_date.split(' ')[0] : '',
status: '已分享', //
uuid: item.uuid,
order_id: item.order_id,
type: item.type,
providername: item.downloadername, //
downloadername: item.downloadername
}));
coursewareList.value = [...coursewareList.value, ...newList];
//
noMore.value = data.pageNumber >= data.totalPage;
}
loading.value = false;
}).catch(err => {
console.error('加载更多分享列表失败:', err);
page.value--; //
loading.value = false;
});
}
};
</script>
@ -240,6 +369,7 @@ const onLoadMore = () => {
border-bottom:2rpx solid #eee;
.item-content {
.courseware-name,
.courseware-provider,
.courseware-time,
.courseware-status {
display: flex;
@ -269,10 +399,15 @@ const onLoadMore = () => {
}
}
.loading-more, .no-more {
.loading-more, .no-more, .empty-state {
text-align: center;
padding: 30rpx;
color: #999999;
font-size: 26rpx;
}
.empty-state {
padding: 100rpx 30rpx;
font-size: 28rpx;
}
</style>

View File

@ -60,11 +60,9 @@
<view v-if="noMore && records.length > 0" class="no-more">
<text>没有更多数据了</text>
</view>
<!-- 调试按钮 -->
<view v-if="records.length > 0" class="debug-actions">
<button @click="testLoadMore" size="mini" type="primary">测试加载更多</button>
<text class="debug-info">当前页: {{ page }}, 加载中: {{ loading }}, 无更多: {{ noMore }}</text>
<view class="debug-actions">
<!-- <button @click="testLoadMore" size="mini" type="primary">测试加载更多</button>
<text class="debug-info">当前页: {{ page }}, 加载中: {{ loading }}, 无更多: {{ noMore }}</text> -->
</view>
</scroll-view>
</view>
@ -221,7 +219,9 @@ $text: #333;
$muted: #999;
$card: #ffffff;
.flower-page { height: calc(100vh - 140rpx); background: $bg; }
.flower-page {
background: $bg;
}
.stats-bar {
display: flex;
@ -297,6 +297,7 @@ $card: #ffffff;
}
.debug-actions {
height: 50rpx;
text-align: center;
padding: 20rpx;
border-top: 1rpx solid #eee;

View File

@ -472,7 +472,7 @@
const formattedList = listData.map(item => ({
type: item.score_type_name,
time: item.create_date,
amount: -item.score //
amount: item.score //
}));
//

View File

@ -0,0 +1,238 @@
<template>
<uni-nav-bar
left-icon="left"
title="兑换福利卡"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
<view class="exchange-page">
<!-- 顶部红色横幅 -->
<view class="top-banner">
<view class="banner-text">
<view class="line1">已兑换{{ exchangedCount }}</view>
<view class="line2" @click="goMyWelfare">查看现有权益</view>
</view>
<view class="help-btn" @click="showHelp">帮助说明</view>
</view>
<!-- 使用统一的自定义居中模态框 -->
<view v-if="centerVisible" class="center-modal" @click.self="closeCenter">
<view class="center-modal-content">
<view class="center-title">{{ centerHelp ? '帮助说明' : '提示' }}</view>
<view v-if="centerHelp" class="help-content center-help">
<text>1点击兑换福利卡输入密码即可兑换相应权益</text>
<text>2每张福利卡仅限兑换一次兑换后权益可在我的福利-使用福利中查看</text>
<text>3福利卡长期有效福利卡不能退换或者折现</text>
<text>4查找文献权益不限文献类型如指南共识论文电子书课件或者视频</text>
</view>
<view v-else class="center-body">{{ centerText }}</view>
<view class="center-actions">
<button class="center-btn" @click="closeCenter">知道了</button>
</view>
</view>
</view>
<!-- 输入提示 -->
<view class="tips">请输入16位福利卡密码不区分大小写<text class="paste-action" @click="pasteFromClipboard">粘贴</text></view>
<!-- 四段输入框 -->
<view class="code-inputs">
<input class="code-box" type="text" v-model="code1" maxlength="4" placeholder="" :focus="f1" @input="handleInput(1, $event)" @paste="handlePaste"/>
<input class="code-box" type="text" v-model="code2" maxlength="4" placeholder="" :focus="f2" @input="handleInput(2, $event)"/>
<input class="code-box" type="text" v-model="code3" maxlength="4" placeholder="" :focus="f3" @input="handleInput(3, $event)"/>
<input class="code-box" type="text" v-model="code4" maxlength="4" placeholder="" :focus="f4" @input="handleInput(4, $event)"/>
</view>
<!-- 按钮 -->
<view class="btn-wrapper">
<button class="submit-btn" :disabled="!isFull" @click="submit">立即兑换</button>
</view>
</view>
</template>
<script setup>
import { ref, computed, nextTick } from 'vue'
import api from '@/api/api';
const exchangedCount = ref(5)
const code1 = ref('')
const code2 = ref('')
const code3 = ref('')
const code4 = ref('')
const f1 = ref(true)
const f2 = ref(false)
const f3 = ref(false)
const f4 = ref(false)
const helpVisible = ref(false)
const centerVisible = ref(false)
const centerText = ref('')
const centerHelp = ref(false)
const isFull = computed(() => (code1.value+code2.value+code3.value+code4.value).length === 16)
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({ url: '/pages/index/index' })
}
})
}
const showHelp = () => {
centerHelp.value = true
centerVisible.value = true
}
//
const openCenter = (text) => {
centerText.value = text
centerHelp.value = false
centerVisible.value = true
}
const closeCenter = () => {
centerVisible.value = false
centerHelp.value = false
}
const setFocus = (idx) => {
f1.value = idx === 1
f2.value = idx === 2
f3.value = idx === 3
f4.value = idx === 4
}
const handleInput = (idx, e) => {
const raw = (e.detail && e.detail.value) || ''
const sanitized = raw.replace(/[^a-zA-Z0-9]/g, '')
if (idx === 1) code1.value = sanitized
if (idx === 2) code2.value = sanitized
if (idx === 3) code3.value = sanitized
if (idx === 4) code4.value = sanitized
if (sanitized.length === 4 && idx < 4) {
nextTick(() => setFocus(idx + 1))
}
}
const submit = () => {
if (!isFull.value) return
const code = (code1.value+code2.value+code3.value+code4.value).toUpperCase()
uni.showToast({ title: '兑换中: '+ code, icon: 'none' })
api.exchangeWelfareCard({password: code}).then(res => {
console.log(res)
if (res.code == 200) {
uni.showToast({ title: '兑换成功', icon: 'success' })
uni.navigateBack()
}
})
}
const goMyWelfare = () => {
uni.navigateTo({ url: '/pages_app/myWelfare/myWelfare' })
}
// H5paste
const handlePaste = (e) => {
const text = (e.clipboardData && e.clipboardData.getData('text')) || ''
fillByText(text)
//
e && e.preventDefault && e.preventDefault()
}
// App
const pasteFromClipboard = () => {
uni.getClipboardData({
success: (res) => {
fillByText(res.data || '')
}
})
}
const fillByText = (raw) => {
const v = String(raw || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 16)
code1.value = v.slice(0, 4)
code2.value = v.slice(4, 8)
code3.value = v.slice(8, 12)
code4.value = v.slice(12, 16)
nextTick(() => setFocus(v.length >= 16 ? 4 : Math.floor((v.length)/4) + 1))
}
</script>
<style lang="scss" scoped>
$nav-height: 140rpx;
.exchange-page{
min-height: 100vh;
background: #f5f6f7;
}
.top-banner{
position: relative;
height: 280rpx;
background: linear-gradient(180deg,#ff6a4a 0%, #e93b2d 100%);
border-bottom-left-radius: 40rpx;
border-bottom-right-radius: 40rpx;
.banner-text{
position: absolute;
left: 48rpx;
top: 120rpx;
color: #fff;
.line1{font-size: 44rpx;font-weight: 600;}
.line2{font-size: 36rpx;margin-top: 12rpx;}
}
.help-btn{
position: absolute;
right: 40rpx;
top: 90rpx;
background: rgba(255,255,255,.95);
color: #e04835;
border-radius: 999rpx;
padding: 10rpx 24rpx;
font-size: 24rpx;
}
}
.tips{
margin: 48rpx;
color: #333;
font-size: 32rpx;
.paste-action{
color: #007aff;
text-decoration: underline;
}
}
.code-inputs{
display: flex;
gap: 30rpx;
padding: 0 48rpx;
.code-box{
flex: 1;
height: 96rpx;
background: #fff;
border-radius: 16rpx;
text-align: center;
font-size: 36rpx;
}
}
.btn-wrapper{
padding: 80rpx 48rpx 0;
.submit-btn{
width: 100%;
height: 100rpx;
background: linear-gradient(90deg,#ff4d2e,#e93b2d);
border-radius: 60rpx;
color: #fff;
font-size: 36rpx;
}
.submit-btn[disabled]{
opacity: .6;
}
}
/* 帮助弹层样式 */
.center-help{padding: 0 32rpx;display:flex;flex-direction:column;align-items:center;gap: 16rpx;color:#333;font-size:28rpx;line-height:1.6;}
/* 自定义通用模态框 */
.center-modal{position: fixed;left:0;right:0;top:0;bottom:0;background: rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index: 9999;}
.center-modal-content{width: 640rpx;background:#fff;border-radius:24rpx;overflow:hidden;}
.center-title{font-size: 32rpx;color:#333;text-align:center;padding:28rpx 24rpx 12rpx;font-weight:600;}
.center-body{padding:0 32rpx 12rpx;color:#333;font-size:28rpx;line-height:1.7;text-align:center;}
.center-actions{padding: 24rpx 24rpx 28rpx;}
.center-btn{width:100%;height:88rpx;border-radius:999rpx;background:#e93b2d;color:#fff;font-size:30rpx;}
</style>

View File

@ -11,100 +11,121 @@
></uni-nav-bar>
<view class="benefits-page">
<!-- 顶部红色横幅 -->
<view class="top-banner">
<view class="banner-text">
<view class="line1">已兑换{{ cardInfo.length }}</view>
<view class="line2" @click="goMyWelfare">查看现有权益</view>
</view>
<view class="help-btn" @click="showRules">帮助说明</view>
</view>
<!-- 头部导航栏 -->
<!-- 帮助说明模态框与兑换页一致的居中弹层 -->
<view v-if="centerVisible" class="center-modal" @click.self="closeCenter">
<view class="center-modal-content">
<view class="center-title">帮助说明</view>
<view class="center-help">
<text>1点击兑换福利卡输入密码即可兑换相应权益</text>
<text>2每张福利卡仅限兑换一次兑换后权益可在我的福利-使用福利中查看</text>
<text>3福利卡长期有效福利卡不能退换或者折现</text>
<text>4查找文献权益不限文献类型如指南共识论文电子书课件或者视频</text>
</view>
<view class="center-actions">
<button class="center-btn" @click="closeCenter">知道了</button>
</view>
</view>
</view>
<!-- 福利卡片列表 -->
<view class="scrollbox">
<scroll-view class="benefits-list" scroll-y="true" :show-scrollbar="false">
<view
class="benefit-card"
v-for="(benefit, index) in benefitsList"
:key="index"
:class="benefit.type"
@click="claimBenefit(benefit)"
>
<view class="card-title">{{ benefit.title }}</view>
<!-- <view class="card-bg">
</view> -->
<view class="card-content">
<view class="card-details">
<view class="left-section">
<text class="condition">{{ benefit.condition }}</text>
<text class="requirement">{{ benefit.requirement }}</text>
</view>
<view class="right-section">
<text class="reward-type">{{ benefit.rewardType }}</text>
<text class="reward-value">{{ benefit.rewardValue }}</text>
</view>
</view>
<!-- 有卡展示多张 -->
<view v-if="hasCard" >
<view class="card-wrapper" v-for="(card, cIdx) in cardInfo" :key="card.id">
<view class="card-header">
<text>卡号{{ card.idcard }}</text>
<text class="time">兑换时间{{ card.exchange_date }}</text>
</view>
<view class="card-body">
<view class="benefit-line" v-for="(w, idx) in card.welfare_list" :key="idx">
<text class="index">{{ idx + 1 }}</text>
<text class="text">{{ w.type_name }}{{ w.num }}{{ w.type_unit }}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 加载提示 -->
<view class="loadmore-tip" v-if="isLoading || isLastPage">
<text>{{ isLoading ? '加载中...' : (isLastPage ? '没有更多了' : '') }}</text>
</view>
</view>
<!-- 兑换福利卡 -->
<view class="emptybox">
<up-image :src="emptyImg" width="176rpx" height="204rpx" ></up-image>
<!-- 无卡空状态 -->
<view v-else class="emptybox">
<up-image :src="emptyImg" width="176rpx" height="204rpx"></up-image>
<view class="empty_desc">暂无福利卡</view>
</view>
<!-- 底部导航栏 -->
<view class="bottom-nav">
<view class="nav-item" @click="goPointsDetail">
<up-image :src="jifenImg" width="34rpx" height="34rpx" ></up-image>
<up-image :src="jifenImg" width="34rpx" height="34rpx"></up-image>
<text class="nav-text">兑换福利卡</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { ref, onMounted } from 'vue';
import jifenImg from "@/static/duihuan.png"
import emptyImg from "@/static/icon_empty.png"
//
const activeTab = ref(0);
import api from '@/api/api';
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
//
const hasCard = ref(true)
const cardInfo = ref([]);
//
const pageNum = ref(1)
const pageSize = ref(10)
const isLastPage = ref(false)
const isLoading = ref(false)
const getMyWelfareCard = (opts = { isRefresh: false }) => {
if (isLoading.value) return
isLoading.value = true
api.myWelfareCard({page: pageNum.value}).then(res => {
console.log(res)
if (res.code == 200) {
const list = Array.isArray(res.data.list) ? res.data.list : []
if (opts.isRefresh) {
cardInfo.value = list
} else {
cardInfo.value = pageNum.value === 1 ? list : cardInfo.value.concat(list)
}
hasCard.value = list.length > 0 || cardInfo.value.length > 0
isLastPage.value = !!res.data.isLastPage
}
})
.finally(() => {
isLoading.value = false
uni.stopPullDownRefresh()
})
}
onMounted(() => {
getMyWelfareCard()
})
//
const benefitsList = ref([
{
type: 'points',
title: '肝胆积分 (5个新随访)',
condition: '赠送积分',
requirement: '200积分',
rewardType: '立即领取',
rewardValue: ''
},
{
type: 'video',
title: '肝胆视频',
condition: '再新增随访',
requirement: '1个',
rewardType: '赠送下载',
rewardValue: '2集'
},
{
type: 'courseware',
title: '肝胆课件',
condition: '再新增随访',
requirement: '6个',
rewardType: '赠送下载',
rewardValue: '1篇'
},
{
type: 'usb',
title: '知识U盘',
condition: '再新增随访 (年度计算)',
requirement: '96个',
rewardType: '赠送U盘',
rewardValue: '1个'
}
]);
//
onPullDownRefresh(() => {
pageNum.value = 1
isLastPage.value = false
getMyWelfareCard({ isRefresh: true })
})
//
onReachBottom(() => {
if (isLastPage.value || isLoading.value) return
pageNum.value += 1
getMyWelfareCard()
})
//
const goBack = () => {
@ -117,49 +138,21 @@
});
};
const centerVisible = ref(false)
const showRules = () => {
uni.showToast({
title: '福利规则',
icon: 'none'
});
centerVisible.value = true
};
const switchTab = (index) => {
activeTab.value = index;
//
uni.showToast({
title: `切换到${['领取福利', '使用福利', '兑福利卡'][index]}`,
icon: 'none'
});
};
const claimBenefit = (benefit) => {
uni.showToast({
title: `领取${benefit.title}`,
icon: 'none'
});
const closeCenter = () => {
centerVisible.value = false
};
const goPointsDetail = () => {
uni.showToast({
title: '积分详情',
icon: 'none'
});
uni.navigateTo({ url: '/pages_app/myWelfareCard/exchange' })
};
const goBenefitDetail = () => {
uni.showToast({
title: '福利详情',
icon: 'none'
});
};
const addPatient = () => {
uni.showToast({
title: '添加患者',
icon: 'none'
});
};
const goMyWelfare = () => {
uni.navigateTo({ url: '/pages_app/myWelfare/myWelfare' })
}
</script>
<style lang="scss" scoped>
@ -196,7 +189,6 @@
.benefits-page {
min-height: 100vh;
background-color: $bg-color;
padding-top: $nav-height; //
.emptybox{
display: flex;
height: 100vh;
@ -330,7 +322,7 @@
}
.benefits-list {
.benefit-card {
background: $white;
border-radius: 20rpx;
@ -340,22 +332,6 @@
overflow: hidden;
border:2rpx solid #fff;
&.points {
}
&.video {
}
&.courseware {
}
&.usb {
}
.card-bg {
position: absolute;
top: 0;
@ -364,7 +340,6 @@
z-index:0;
}
.card-content {
@ -388,7 +363,7 @@
justify-content: space-between;
align-items: center;
padding:0 40rpx;
height: 220rpx;
height: 220rpx;
.left-section,
.right-section {
display: flex;
@ -420,6 +395,60 @@
}
}
/* 顶部横幅与卡片样式 */
.top-banner{
position: relative;
height: 280rpx;
background: linear-gradient(180deg,#ff6a4a 0%, #e93b2d 100%);
border-bottom-left-radius: 40rpx;
border-bottom-right-radius: 40rpx;
@include shadow;
.banner-text{
position: absolute;
left: 48rpx;
top: 120rpx;
color: #fff;
.line1{font-size: 44rpx;font-weight: 600;}
.line2{font-size: 36rpx;margin-top: 12rpx;}
}
.help-btn{
position: absolute;
right: 40rpx;
top: 90rpx;
background: rgba(255,255,255,.95);
color: #e04835;
border-radius: 999rpx;
padding: 10rpx 24rpx;
font-size: 24rpx;
}
}
.card-wrapper{
margin: 24rpx 30rpx;
background: #fff;
border-radius: 16rpx;
@include shadow;
overflow: hidden;
.card-header{
background: linear-gradient(90deg,#ff7e4a,#ff4d2e);
color: #fff;
padding: 24rpx 28rpx;
font-size: 26rpx;
display: flex;
justify-content: space-between;
.time{opacity:.95}
}
.card-body{
padding: 34rpx 28rpx 40rpx;
.benefit-line{
font-size: 30rpx;
color: #333;
line-height: 56rpx;
.index{color:#333}
}
}
}
.bottom-nav {
position: fixed;
bottom: 0;
@ -449,4 +478,20 @@
}
}
}
/* 底部加载提示 */
.loadmore-tip{
text-align: center;
color: #999;
font-size: 24rpx;
padding: 20rpx 0 120rpx; //
}
/* 居中模态框样式(复用兑换页风格) */
.center-modal{position: fixed;left:0;right:0;top:0;bottom:0;background: rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;z-index: 9999;}
.center-modal-content{width: 640rpx;background:#fff;border-radius:24rpx;overflow:hidden;}
.center-title{font-size: 32rpx;color:#333;text-align:center;padding:28rpx 24rpx 12rpx;font-weight:600;}
.center-help{padding: 0 32rpx;display:flex;flex-direction:column;align-items:center;gap: 16rpx;color:#333;font-size:28rpx;line-height:1.6;}
.center-actions{padding: 24rpx 24rpx 28rpx;}
.center-btn{width:100%;height:88rpx;border-radius:999rpx;background:#e93b2d;color:#fff;font-size:30rpx;}
</style>

View File

@ -130,7 +130,7 @@
//
const goToChangePassword = () => {
uni.navigateTo({
url: '/pages_app/pwdLogin/pwdLogin'
url: '/pages_app/changePassword/index'
});
};

99
pages_goods/README.md Normal file
View File

@ -0,0 +1,99 @@
# 积分商城页面
## 页面结构
### 1. 积分商城首页 (`pointMall/pointMall.vue`)
- **状态栏**: 显示时间、网络状态、电池等信息
- **顶部导航栏**: 包含返回按钮、标题"积分商城"、搜索按钮
- **轮播图横幅**: 显示活动信息,包含渐变背景和文字覆盖
- **筛选排序栏**: 提供筛选和排序功能
- **商品网格**: 两列布局显示商品列表
- **底部导航栏**: 包含"我的兑换"和"购买积分"两个选项
### 2. 商品详情页 (`productDetail/productDetail.vue`)
- **状态栏**: 与首页相同的状态栏
- **顶部导航栏**: 包含返回按钮、标题"商品详情"、分享按钮
- **商品信息**: 显示商品图片、标题、价格和描述
- **兑换按钮**: 提供商品兑换功能
## 功能特性
### 积分商城首页
- ✅ 100%还原设计稿的UI布局
- ✅ 响应式设计,适配不同屏幕尺寸
- ✅ 轮播图自动播放
- ✅ 商品网格布局
- ✅ 点击商品跳转到详情页
- ✅ 筛选和排序功能(待实现具体逻辑)
- ✅ 底部导航栏交互
### 商品详情页
- ✅ 商品信息展示
- ✅ 兑换确认功能
- ✅ 返回导航
## 路由配置
页面已添加到 `pages.json` 中:
```json
{
"root": "pages_goods",
"pages": [
{
"path": "pointMall/pointMall",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "积分商城"
}
},
{
"path": "productDetail/productDetail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "商品详情"
}
}
]
}
```
## 使用方法
### 访问积分商城首页
```javascript
uni.navigateTo({
url: '/pages_goods/pointMall/pointMall'
})
```
### 访问商品详情页
```javascript
uni.navigateTo({
url: '/pages_goods/productDetail/productDetail?id=1'
})
```
## 样式特点
- 使用自定义状态栏和导航栏
- 渐变色背景的轮播图
- 卡片式商品布局
- 固定底部导航栏
- 响应式网格布局
- 统一的颜色主题(红色 #e74c3c,青色 #20b2aa
## 待完善功能
1. 筛选和排序的具体实现
2. 商品数据的动态加载
3. 搜索功能的具体实现
4. 我的兑换页面
5. 购买积分页面
6. 商品图片的实际加载
## 注意事项
- 页面使用了自定义导航栏,确保在 `pages.json` 中设置了 `"navigationStyle": "custom"`
- 商品图片路径需要根据实际情况调整
- 可以根据需要添加更多的商品数据和功能

View File

@ -0,0 +1,230 @@
<template>
<view class="address-page">
<uni-nav-bar left-icon="left" title="收货地址" fixed color="#8B2316" height="140rpx" :border="false" backgroundColor="#ffffff" @clickLeft="goBack" />
<scroll-view scroll-y class="content">
<view class="form-item">
<view class="label">收件人</view>
<input class="input" v-model="receiver" placeholder="请输入收货人名字" />
</view>
<view class="divider-line"></view>
<view class="form-item">
<view class="label">手机号</view>
<input class="input" v-model="mobile" type="number" placeholder="收货人的电话,方便联系" />
</view>
<view class="divider-line"></view>
<view class="form-item">
<view class="label">邮箱</view>
<input class="input" v-model="email" type="text" placeholder="用于接收电子卡等信息(可选)" />
</view>
<view class="divider-line"></view>
<view class="form-item select-item" @click="openAreaPicker">
<view class="label">地址</view>
<view class="input placeholder" v-if="!regionText">请选择地址</view>
<view class="input" v-else>{{ regionText }}</view>
<text class="arrow"></text>
</view>
<view class="divider-line"></view>
<view class="form-item">
<view class="label">详细地址</view>
<input class="input" v-model="detail" placeholder="请输入街道、门牌等详细地址信息" />
</view>
</scroll-view>
<view class="footer-bar">
<view class="confirm-btn" @click="submit">确定</view>
</view>
<!-- 省市区选择器 -->
<view v-if="showAreaPicker" class="picker-mask" @click="closeAreaPicker"></view>
<view v-if="showAreaPicker" class="picker-panel">
<view class="picker-header">
<text class="picker-btn" @click="closeAreaPicker">取消</text>
<text class="picker-title">选择地区</text>
<text class="picker-btn ok" @click="confirmArea">确定</text>
</view>
<picker-view v-if="provinces.length && cities.length && areas.length" class="picker-view" :indicator-style="indicatorStyle" :value="pickerIndex" @change="onAreaChange">
<picker-view-column>
<view v-for="(p,pi) in provinces" :key="pi" class="picker-item">{{ p.name }}</view>
</picker-view-column>
<picker-view-column>
<view v-for="(c,ci) in cities" :key="ci" class="picker-item">{{ c.name }}</view>
</picker-view-column>
<picker-view-column>
<view v-for="(a,ai) in areas" :key="ai" class="picker-item">{{ a.name }}</view>
</picker-view-column>
</picker-view>
<view v-else class="picker-empty">地区数据加载中...</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import areaList from '@/utils/areaList.js'
const receiver = ref('')
const mobile = ref('')
const regionText = ref('')
const detail = ref('')
const email = ref('')
const editingId = ref(null)
const goBack = () => uni.navigateBack()
// [{code,label,value,children:[...] }...]
const provinces = ref([])
const cities = ref([])
const areas = ref([])
const pickerIndex = ref([0,0,0])
const showAreaPicker = ref(false)
const indicatorStyle = `height: 80rpx;`
const normalizeNode = (node) => ({
code: node?.code || '',
name: node?.label || node?.value || node?.name || '',
children: Array.isArray(node?.children) ? node.children : []
})
const getAreaTree = () => {
const raw = areaList && (areaList.default || areaList)
return Array.isArray(raw) ? raw.map(normalizeNode) : []
}
const buildData = () => {
const tree = getAreaTree()
provinces.value = tree.length ? tree : [{ code: '', name: '', children: [] }]
const pIdx = Math.min(pickerIndex.value[0], Math.max(provinces.value.length - 1, 0))
const pNode = provinces.value[pIdx]
cities.value = (pNode?.children || []).map(normalizeNode)
if (!cities.value.length) cities.value = [{ code: '', name: '', children: [] }]
const cIdx = Math.min(pickerIndex.value[1], Math.max(cities.value.length - 1, 0))
const cNode = cities.value[cIdx]
areas.value = (cNode?.children || []).map(normalizeNode)
if (!areas.value.length) areas.value = [{ code: '', name: '' }]
}
const openAreaPicker = () => {
showAreaPicker.value = true
pickerIndex.value = [0,0,0]
buildData()
}
const closeAreaPicker = () => { showAreaPicker.value = false }
const onAreaChange = (e) => {
const val = (e && e.detail && e.detail.value) ? e.detail.value : [0,0,0]
const [pi, ci, ai] = val
if (pi !== pickerIndex.value[0]) {
pickerIndex.value = [pi, 0, 0]
buildData()
return
}
if (ci !== pickerIndex.value[1]) {
pickerIndex.value = [pi, ci, 0]
buildData()
return
}
pickerIndex.value = [pi, ci, ai]
}
const confirmArea = () => {
const p = provinces.value[pickerIndex.value[0]]
const c = cities.value[pickerIndex.value[1]]
const a = areas.value[pickerIndex.value[2]]
regionText.value = [p?.name, c?.name, a?.name].filter(Boolean).join(' ')
closeAreaPicker()
}
onLoad((opts) => {
if (opts && opts.id) {
//
editingId.value = Number(opts.id)
try {
const list = uni.getStorageSync('goods_addresses') || []
const target = Array.isArray(list) ? list.find(a => a.id === editingId.value) : null
if (target) {
receiver.value = target.receiver || ''
mobile.value = target.mobile || ''
regionText.value = target.region || ''
detail.value = target.detail || ''
email.value = target.email || ''
}
} catch (e) {}
}
})
const submit = () => {
if (!receiver.value) return uni.showToast({ title: '请输入收件人', icon: 'none' })
if (!/^1\d{10}$/.test(mobile.value)) return uni.showToast({ title: '请输入正确手机号', icon: 'none' })
if (!regionText.value) return uni.showToast({ title: '请选择地址', icon: 'none' })
if (!detail.value) return uni.showToast({ title: '请输入详细地址', icon: 'none' })
if (email.value && !/^([a-zA-Z0-9_\.-]+)@([a-zA-Z0-9\.-]+)\.([a-zA-Z]{2,})$/.test(email.value)) return uni.showToast({ title: '邮箱格式不正确', icon: 'none' })
const STORAGE_KEY = 'goods_addresses'
let list = []
try {
const cached = uni.getStorageSync(STORAGE_KEY)
list = Array.isArray(cached) ? cached : []
} catch (e) { list = [] }
if (editingId.value) {
//
list = list.map(a => a.id === editingId.value ? {
...a,
receiver: receiver.value,
mobile: mobile.value,
region: regionText.value,
detail: detail.value,
fullAddress: `${regionText.value} ${detail.value}`,
email: email.value
} : a)
} else {
//
const address = {
id: Date.now(),
receiver: receiver.value,
mobile: mobile.value,
region: regionText.value,
detail: detail.value,
fullAddress: `${regionText.value} ${detail.value}`,
email: email.value,
createdAt: Date.now()
}
list.unshift(address)
}
uni.setStorageSync(STORAGE_KEY, list)
uni.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => { uni.navigateBack() }, 500)
}
</script>
<style scoped>
.address-page { min-height: 100vh; background: #fff; }
.content { position: absolute; top: 140rpx; bottom: 120rpx; left: 0; right: 0; background: #fff; }
.form-item { display: flex; align-items: center; padding: 24rpx; }
.label { width: 160rpx; color: #333; font-size: 30rpx; }
.input { flex: 1; color: #333; font-size: 30rpx; }
.placeholder { color: #bbb; }
.divider-line { height: 2rpx; background: #eee; margin: 0 24rpx; }
.select-item { position: relative; }
.arrow { position: absolute; right: 24rpx; color: #999; font-size: 48rpx; }
.footer-bar { position: fixed; left: 0; right: 0; bottom: 0; height: 120rpx; background: #27c5b8; display: flex; align-items: center; justify-content: center; }
.confirm-btn { color: #fff; font-size: 34rpx; font-weight: 700; }
/* 地区选择器 */
.picker-mask { position: fixed; left: 0; right: 0; top: 0; bottom: 0; background: rgba(0,0,0,0.4); }
.picker-panel { position: fixed; left: 0; right: 0; bottom: 0; background: #fff; }
.picker-header { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; border-bottom: 1rpx solid #eee; }
.picker-title { font-size: 30rpx; color: #333; }
.picker-btn { color: #666; font-size: 28rpx; }
.picker-btn.ok { color: #38c1b1; }
.picker-view { height: 480rpx; }
.picker-item { height: 80rpx; line-height: 80rpx; text-align: center; color: #333; }
.picker-empty { padding: 40rpx; text-align: center; color: #999; }
</style>

View File

@ -0,0 +1,91 @@
<template>
<view class="addr-list-page">
<uni-nav-bar left-icon="left" title="地址管理" fixed color="#8B2316" height="140rpx" :border="false" backgroundColor="#ffffff" @clickLeft="goBack" />
<scroll-view scroll-y class="list-wrap">
<view v-if="!list.length" class="empty">暂无收货地址</view>
<view v-for="item in list" :key="item.id" class="addr-item" @click="select(item)">
<view class="addr-main">
<view class="row">
<text class="name">{{ item.receiver }}</text>
<text class="mobile">{{ item.mobile }}</text>
</view>
<view class="row small">{{ item.fullAddress || (item.region + ' ' + item.detail) }}</view>
</view>
<view class="addr-actions">
<text class="act" @click.stop="edit(item)">编辑</text>
<text class="act danger" @click.stop="remove(item)">删除</text>
</view>
</view>
</scroll-view>
<view class="footer-bar">
<view class="add-btn" @click="addNew">新增地址</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
const STORAGE_KEY = 'goods_addresses'
const SELECTED_KEY = 'goods_selected_address'
const list = ref([])
const goBack = () => uni.navigateBack()
const load = () => {
try {
const cached = uni.getStorageSync(STORAGE_KEY)
list.value = Array.isArray(cached) ? cached : []
} catch (e) {
list.value = []
}
}
onShow(() => load())
const addNew = () => uni.navigateTo({ url: '/pages_goods/exchange/address' })
const edit = (item) => {
uni.navigateTo({ url: `/pages_goods/exchange/address?id=${item.id}` })
}
const remove = (item) => {
uni.showModal({
title: '删除地址',
content: '确定删除该地址吗?',
success: (r) => {
if (r.confirm) {
list.value = list.value.filter(a => a.id !== item.id)
uni.setStorageSync(STORAGE_KEY, list.value)
}
}
})
}
const select = (item) => {
uni.setStorageSync(SELECTED_KEY, item)
uni.showToast({ title: '已选择该地址', icon: 'success' })
setTimeout(() => uni.navigateBack(), 300)
}
</script>
<style scoped>
.addr-list-page { min-height: 100vh; background: #fff; }
.list-wrap { position: absolute; top: 140rpx; bottom: 120rpx; left: 0; right: 0; }
.empty { text-align: center; color: #999; padding: 80rpx 24rpx; }
.addr-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx; border-bottom: 1rpx solid #f0f0f0; }
.addr-main { flex: 1; }
.row { display: flex; align-items: center; gap: 20rpx; margin-bottom: 8rpx; }
.row.small { color: #666; font-size: 26rpx; }
.name { color: #333; font-size: 30rpx; }
.mobile { color: #333; font-size: 30rpx; }
.addr-actions { display: flex; align-items: center; gap: 24rpx; margin-left: 24rpx; }
.act { color: #38c1b1; font-size: 28rpx; }
.act.danger { color: #ff4d4f; }
.footer-bar { position: fixed; left: 0; right: 0; bottom: 0; height: 120rpx; background: #27c5b8; display: flex; align-items: center; justify-content: center; }
.add-btn { color: #fff; font-size: 34rpx; font-weight: 700; }
</style>

View File

@ -0,0 +1,192 @@
<template>
<view class="exchange-page">
<uni-nav-bar left-icon="left" title="在线兑换" fixed color="#8B2316" height="140rpx" :border="false" backgroundColor="#ffffff" @clickLeft="goBack" />
<scroll-view scroll-y class="content">
<!-- 收货地址 -->
<view class="addr-card" @click="changeAddress">
<view class="addr-header">
<text class="addr-title">收货信息</text>
<text class="addr-change">{{ selectedAddress ? '更换' : '添加' }}</text>
</view>
<view v-if="selectedAddress" class="addr-info">
<text class="addr-row">{{ selectedAddress.receiver }} {{ selectedAddress.mobile }}</text>
<text class="addr-row small">{{ selectedAddress.fullAddress || (selectedAddress.region + ' ' + selectedAddress.detail) }}</text>
</view>
<view v-else class="addr-empty">请添加收货地址</view>
</view>
<view class="goods-title">{{ title }}</view>
<view class="price">{{ price }}积分</view>
<view class="divider"></view>
<view class="section">
<view class="section-label">兑换数量</view>
<view class="qty-row">
<view class="qty-btn" :class="{ active: qty === 1 }" @click="setQty(1)">1</view>
<view class="qty-btn" :class="{ active: qty === 2 }" @click="setQty(2)">2</view>
<view class="qty-btn" :class="{ active: qty === 3 }" @click="setQty(3)">3</view>
</view>
</view>
<view class="section">
<view class="section-label">所需积分</view>
<view class="need-points">{{ needPoints }}积分</view>
</view>
</scroll-view>
<view class="footer-bar">
<view class="confirm-btn" @click="goAddress">我要兑换</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import goods_api from '@/api/goods_api'
const title = ref('')
const price = ref(0)
const qty = ref(1)
const needPoints = ref(0)
const totalPoints = ref(0)
const selectedAddress = ref(null)
const id = ref('')
const goBack = () => uni.navigateBack()
const setQty = (n) => {
qty.value = n
needPoints.value = n * Number(price.value || 0)
}
onLoad((opts) => {
if (opts) {
title.value = decodeURIComponent(opts.title || '')
price.value = Number(opts.price || 0)
id.value = opts.id || ''
setQty(1)
getTotalPoints()
}
})
onShow(() => {
loadAddress()
})
const STORAGE_KEY = 'goods_addresses'
const SELECTED_KEY = 'goods_selected_address'
const loadAddress = () => {
try {
const sel = uni.getStorageSync(SELECTED_KEY)
if (sel && sel.id) {
selectedAddress.value = sel
return
}
const list = uni.getStorageSync(STORAGE_KEY)
if (Array.isArray(list) && list.length) {
selectedAddress.value = list[0]
} else {
selectedAddress.value = null
}
} catch (e) {
selectedAddress.value = null
}
}
const changeAddress = () => {
let list = []
try {
const cached = uni.getStorageSync(STORAGE_KEY)
list = Array.isArray(cached) ? cached : []
} catch (e) { list = [] }
if (!list.length) {
uni.navigateTo({ url: '/pages_goods/exchange/address' })
return
}
uni.navigateTo({ url: '/pages_goods/exchange/address_list' })
}
const goAddress = () => {
if (needPoints.value > totalPoints.value) {
uni.showToast({ title: '积分不足', icon: 'none' })
return
}
if (!selectedAddress.value) {
changeAddress()
return
}
const data = {
goodsUuid: id.value,
address: selectedAddress.value.fullAddress,
user_name: selectedAddress.value.receiver,
mobile: selectedAddress.value.mobile,
goodsNum: qty.value,
email: selectedAddress.value.email
}
console.log(data)
//{"goodsUuid":"e6ca84edc8e64242a379dd0b895ea812",
// "address":"",
// "user_name":"",
// "mobile":"13825244552",
// "goodsNum":"1",
// "email":""}
//
uni.showModal({
title: '确认兑换',
content: `兑换“${title.value}”x${qty.value},共需 ${needPoints.value} 积分`,
success: (r) => {
if (r.confirm) {
//
totalPoints.value = Math.max(0, totalPoints.value - needPoints.value)
uni.removeStorageSync(SELECTED_KEY)
goods_api.createGoodsOrder(data).then(res => {
console.log(res)
if (res.code == 200) {
uni.showToast({ title: '兑换成功', icon: 'success' })
uni.navigateBack()
} else {
uni.showToast({ title: res.msg, icon: 'none' })
}
})
}
}
})
}
const getTotalPoints = () => {
goods_api.getTotalPoints && goods_api.getTotalPoints().then(res => {
if (res.code == 200) {
totalPoints.value = res.data || 0
}
}).catch(() => {})
}
</script>
<style scoped>
.exchange-page { min-height: 100vh; background: #fff; }
.content { position: absolute; top: 140rpx; bottom: 120rpx; left: 0; right: 0; background: #fff; }
.addr-card { background: #fff; padding: 24rpx; border-bottom: 1rpx solid #f0f0f0; }
.addr-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12rpx; }
.addr-title { color: #333; font-size: 30rpx; }
.addr-change { color: #38c1b1; font-size: 28rpx; }
.addr-info { display: flex; flex-direction: column; gap: 8rpx; }
.addr-row { color: #333; font-size: 28rpx; }
.addr-row.small { color: #666; font-size: 26rpx; }
.addr-empty { color: #bbb; font-size: 28rpx; }
.goods-title { padding: 24rpx; font-size: 32rpx; color: #333; line-height: 1.6; }
.price { padding: 0 24rpx 24rpx; color: #e74c3c; font-size: 30rpx; font-weight: 700; }
.divider { height: 16rpx; background: #f5f5f5; }
.section { padding: 24rpx; }
.section-label { color: #333; font-size: 30rpx; margin-bottom: 20rpx; }
.qty-row { display: flex; gap: 24rpx; }
.qty-btn { flex: 1; text-align: center; height: 96rpx; line-height: 96rpx; border: 2rpx solid #38c1b1; color: #38c1b1; border-radius: 12rpx; font-size: 36rpx; }
.qty-btn.active { background: #38c1b1; color: #fff; }
.need-points { color: #333; font-size: 30rpx; text-align: right; }
.footer-bar { position: fixed; left: 0; right: 0; bottom: 0; height: 120rpx; background: #27c5b8; display: flex; align-items: center; justify-content: center; }
.confirm-btn { color: #fff; font-size: 34rpx; font-weight: 700; }
</style>

View File

@ -0,0 +1,113 @@
<template>
<view class="page">
<uni-nav-bar left-icon="left" title="我的兑换" fixed color="#8B2316" height="140rpx" :border="false" backgroundColor="#ffffff" @clickLeft="goBack" />
<scroll-view
scroll-y
class="content"
refresher-enabled
:refresher-triggered="refresherTriggered"
@refresherrefresh="onRefresh"
@scrolltolower="onReachBottom"
:lower-threshold="80"
>
<view v-if="!orderList.length" class="empty">暂无兑换记录</view>
<view v-for="item in orderList" :key="item.uuid" class="card">
<view class="row">
<text class="label">兑换物品</text>
<text class="value">{{ item.goods_name }}</text>
</view>
<view class="row">
<text class="label">兑换时间</text>
<text class="value">{{ formatDate(item.create_date) }}</text>
</view>
<view class="row">
<text class="label">状态</text>
<text class="status" :class="statusClass(item.status)">{{ statusText(item.status) }}</text>
</view>
</view>
<view class="list-footer" v-if="isLoading">加载中...</view>
<view class="list-footer" v-else-if="noMore && orderList.length">没有更多了</view>
</scroll-view>
</view>
</template>
<script setup>
import goods_api from '@/api/goods_api'
import { ref, onMounted } from 'vue'
const orderList = ref([])
const page = ref(1)
const pageSize = ref(10)
const isLoading = ref(false)
const noMore = ref(false)
const refresherTriggered = ref(false)
const fetchList = () => {
if (isLoading.value || noMore.value) return
isLoading.value = true
goods_api.getGoodsOrderList({ page: page.value, pageSize: pageSize.value }).then(res => {
if (res.code == 200 && res.data) {
const list = Array.isArray(res.data.list) ? res.data.list : []
orderList.value = page.value === 1 ? list : orderList.value.concat(list)
noMore.value = !!res.data.isLastPage
page.value = (res.data.pageNum || page.value) + 1
}
}).finally(() => {
isLoading.value = false
refresherTriggered.value = false
})
}
const onRefresh = () => {
refresherTriggered.value = true
noMore.value = false
page.value = 1
fetchList()
}
const onReachBottom = () => {
fetchList()
}
const formatDate = (sec) => {
if (!sec) return ''
const d = new Date(Number(sec) * 1000)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}-${m}-${day}`
}
const statusText = (s) => {
if (s === 1 || s === 2) return '已支付'
if (s === 3) return '待支付'
return ''
}
const statusClass = (s) => {
return (s === 1 || s === 2) ? 'paid' : 'unpaid'
}
onMounted(() => {
page.value = 1
noMore.value = false
orderList.value = []
fetchList()
})
const goBack = () => uni.navigateBack()
</script>
<style scoped>
.page { min-height: 100vh; background: #fff; }
.content { position: absolute; top: 140rpx; bottom: 0; left: 0; right: 0; background: #fff; }
.card { background: #fff; padding: 24rpx; border-bottom: 16rpx solid #f5f5f5; }
.row { display: flex; align-items: center; padding: 10rpx 0; }
.label { color: #333; font-size: 30rpx; width: 180rpx; }
.value { color: #333; font-size: 30rpx; }
.status { font-size: 30rpx; }
.status.paid { color: #333; }
.status.unpaid { color: #333; }
.list-footer { text-align: center; color: #999; padding: 24rpx 0; }
.empty { text-align: center; color: #999; padding: 40rpx 0; }
</style>

View File

@ -0,0 +1,555 @@
<template>
<view class="point-mall-container">
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
title="积分商城"
@clickLeft="goBack"
fixed
color="#e74c3c"
height="140rpx"
:border="false"
backgroundColor="#ffffff"
>
<template v-slot:right>
<text class="search-btn" @click="goToSearch">搜索</text>
</template>
</uni-nav-bar>
<!-- 轮播图横幅 -->
<view class="banner-section">
<swiper class="banner-swiper" :indicator-dots="true" :autoplay="true" :interval="3000" :duration="500">
<swiper-item v-for="(banner, index) in bannerList" :key="banner.uuid">
<view class="banner-item" @click="goToBannerDetail(banner)">
<image class="banner-image" :src="banner.headImg" mode="aspectFill"></image>
<view class="banner-content">
<view class="banner-text green">{{ banner.title }}</view>
<view class="banner-text orange">{{ banner.create_date }}</view>
</view>
</view>
</swiper-item>
<!-- 如果没有数据显示默认轮播图 -->
<swiper-item v-if="bannerList.length === 0">
<view class="banner-item">
<image class="banner-image" src="/static/banner-bg.jpg" mode="aspectFill"></image>
<view class="banner-content">
<view class="banner-text green">第二届京津冀感染肝病高峰论坛&</view>
<view class="banner-text green">第九届河北省感染科医师培训班</view>
<view class="banner-text orange">2016.10.14-2016.10.16</view>
<view class="banner-text orange">河北石家庄</view>
</view>
</view>
</swiper-item>
</swiper>
</view>
<!-- 筛选排序栏 -->
<view class="filter-bar">
<view class="filter-item" @click="showFilter">
<text class="filter-text">{{ selectedTagName || '筛选' }}</text>
<text class="filter-icon"></text>
</view>
<view class="filter-divider"></view>
<view class="filter-item" @click="showSort">
<text class="filter-text">{{ selectedSortLabel || '排序' }}</text>
<text class="filter-icon"></text>
</view>
</view>
<!-- 筛选下拉 -->
<view v-if="showFilterDropdown" class="dropdown">
<view
class="dropdown-item"
:class="{ active: selectedTagId === null }"
@click="onTagSelect({ id: null, name: '全部' })">
全部
</view>
<view
v-for="tag in tagList"
:key="tag.id"
class="dropdown-item"
:class="{ active: selectedTagId === tag.id }"
@click="onTagSelect(tag)">
{{ tag.name }}
</view>
</view>
<!-- 排序下拉 -->
<view v-if="showSortDropdown" class="dropdown">
<view
v-for="opt in sortOptions"
:key="opt.value"
class="dropdown-item"
:class="{ active: selectedSortValue === opt.value }"
@click="onSortSelect(opt)">
{{ opt.label }}
</view>
</view>
<!-- 商品网格上拉加载 -->
<scroll-view class="product-grid" scroll-y @scrolltolower="getGoodsList" :lower-threshold="100">
<view class="grid-wrap">
<view class="product-item" v-for="(product, index) in products" :key="product.uuid || index" @click="goToProductDetail(product)">
<view class="product-image-container">
<image class="product-image" :src="product.image" mode="aspectFill"></image>
</view>
<view class="product-info">
<text class="product-title">{{ product.title }}</text>
<text class="product-price">{{ product.price }}积分</text>
</view>
</view>
</view>
</scroll-view>
<!-- 加载与无更多提示 -->
<view class="list-footer" v-if="isLoadingMore">
<text>加载中...</text>
</view>
<view class="list-footer" v-else-if="noMore && products.length > 0">
<text>没有更多了</text>
</view>
<!-- 底部导航栏 -->
<view class="bottom-nav">
<view class="nav-item active" @click="goToMyRedemption">
<view class="nav-icon">💰</view>
<text class="nav-text">我的兑换</text>
</view>
<view class="nav-divider"></view>
<view class="nav-item" @click="goToBuyPoints">
<view class="nav-icon">🛒</view>
<text class="nav-text">购买积分</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import goods_api from '@/api/goods_api'
import docUrl from '@/utils/docUrl'
//
const bannerList = ref([])
//
const products = ref([])
const page = ref(1)
const pageSize = ref(10)
const isLoadingMore = ref(false)
const noMore = ref(false)
//
const tagList = ref([])
const selectedTagId = ref(null)
const selectedTagName = ref('')
const showFilterDropdown = ref(false)
//
const showSortDropdown = ref(false)
const selectedSortValue = ref(null)
const selectedSortLabel = ref('')
const sortOptions = ref([
{ label: '积分从小到大', value: 4 },
{ label: '积分从大到小', value: 3 },
{ label: '兑换从多到少', value: 5 },
{ label: '兑换从少到多', value: 6 }
])
//
const goBack = () => {
uni.navigateBack()
}
const goToSearch = () => {
uni.navigateTo({
url: '/pages_app/search/search'
})
}
const showFilter = () => {
showFilterDropdown.value = !showFilterDropdown.value
}
const showSort = () => {
showSortDropdown.value = !showSortDropdown.value
}
const onTagSelect = (tag) => {
selectedTagId.value = tag.id
selectedTagName.value = tag.name
showFilterDropdown.value = false
//
products.value = []
page.value = 1
noMore.value = false
getGoodsList()
}
const onSortSelect = (opt) => {
selectedSortValue.value = opt.value
selectedSortLabel.value = opt.label
showSortDropdown.value = false
products.value = []
page.value = 1
noMore.value = false
getGoodsList()
}
const goToProductDetail = (product) => {
const id = product.uuid || product.id
uni.navigateTo({
url: `/pages_goods/productDetail/productDetail?id=${id}`
})
}
const goToMyRedemption = () => {
uni.navigateTo({
url: '/pages_goods/myRedemption/myRedemption'
})
}
const goToBuyPoints = () => {
uni.navigateTo({
url: '/pages_app/buyPoint/buyPoint'
})
}
const goToBannerDetail = (banner) => {
//
uni.navigateTo({
url: `/pages_app/webview/webview?url=${encodeURIComponent(banner.path)}&title=${encodeURIComponent(banner.title)}`
})
}
const getGoodsNewsList = () => {
goods_api.goodsNewsList().then(res => {
console.log('轮播图数据:', res)
if (res.code === 200 && res.data && res.data.length > 0) {
//
bannerList.value = res.data.map(item => ({
uuid: item.uuid,
title: item.title,
headImg: docUrl + item.headImg,
create_date: item.create_date,
path: item.path,
agreenum: item.agreenum,
readnum: item.readnum
}))
} else {
console.log('轮播图数据为空或请求失败')
}
}).catch(err => {
console.error('获取轮播图数据失败:', err)
uni.showToast({
title: '获取数据失败',
icon: 'none'
})
})
}
const getGoodsList = () => {
if (noMore.value || isLoadingMore.value) return
isLoadingMore.value = true
const tagParam = selectedTagId.value ? selectedTagId.value : ""
const sortParam = selectedSortValue.value ? selectedSortValue.value : 1
goods_api.goodsList({ name: '', page: page.value, sort: sortParam, tag_type: tagParam }).then(res => {
console.log('商品数据:', res)
if ((res.code === 200 || res.code === '200') && res.data && res.data.list) {
const list = res.data.list.map(item => ({
uuid: item.uuid,
title: item.name,
price: item.bonuspoints,
image: docUrl + item.img,
times: item.times,
type: item.type,
upan: item.upan
}))
products.value = products.value.concat(list)
const { isLastPage, pageNum } = res.data
page.value = pageNum + 1
noMore.value = !!isLastPage
} else {
console.log('商品列表为空或请求失败')
}
}).catch(err => {
console.error('获取商品列表失败:', err)
uni.showToast({ title: '获取商品失败', icon: 'none' })
}).finally(() => {
isLoadingMore.value = false
})
}
const getGoodsTagList = () => {
goods_api.goodsTagList({}).then(res => {
console.log('商品类型数据:', res)
if (res.code === 200 && res.data) {
tagList.value = res.data.map(i => ({ id: i.id, name: i.name }))
}
})
}
onMounted(() => {
getGoodsNewsList()
//
page.value = 1
noMore.value = false
products.value = []
getGoodsList()
getGoodsTagList()
})
</script>
<style scoped>
.point-mall-container {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
position: relative;
padding-bottom: 80px;
}
/* 搜索按钮样式 */
.search-btn {
color: #e74c3c;
font-size: 28rpx;
padding: 10rpx;
}
/* 轮播图样式 */
.banner-section {
margin: 0;
position: relative;
}
.banner-swiper {
height: 200px;
}
.banner-item {
position: relative;
width: 100%;
height: 100%;
}
.banner-image {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #87CEEB 0%, #98FB98 50%, #F0E68C 100%);
}
.banner-content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20px;
}
.banner-text {
background-color: rgba(255, 255, 255, 0.95);
padding: 6px 12px;
margin: 3px 0;
border-radius: 6px;
font-size: 13px;
font-weight: bold;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.banner-text.green {
color: #27ae60;
}
.banner-text.orange {
color: #e67e22;
}
/* 轮播图指示器样式 */
.banner-swiper ::v-deep .uni-swiper-dots {
bottom: 10px;
}
.banner-swiper ::v-deep .uni-swiper-dot {
width: 8px;
height: 8px;
background-color: rgba(255, 255, 255, 0.5);
border-radius: 50%;
margin: 0 4px;
}
.banner-swiper ::v-deep .uni-swiper-dot-active {
background-color: #fff;
}
/* 筛选排序栏样式 */
.filter-bar {
display: flex;
align-items: center;
background-color: #fff;
padding: 12px 16px;
border-bottom: 1px solid #eee;
}
.dropdown {
background: #fff;
border-top: 1px solid #eee;
border-bottom: 1px solid #eee;
padding: 8px 0;
}
.dropdown-item {
padding: 12px 16px;
font-size: 14px;
color: #333;
}
.dropdown-item.active {
color: #e74c3c;
font-weight: bold;
background: #fff7f7;
}
.filter-item {
display: flex;
align-items: center;
flex: 1;
justify-content: center;
padding: 8px;
}
.filter-text {
font-size: 14px;
color: #333;
margin-right: 4px;
}
.filter-icon {
font-size: 10px;
color: #666;
}
.filter-divider {
width: 1px;
height: 20px;
background-color: #ddd;
}
/* 商品网格样式 */
.product-grid {
height: calc(100vh - 320rpx); /* 预留顶部导航/筛选及底部栏 */
background-color: #f5f5f5;
}
.grid-wrap {
display: flex;
flex-wrap: wrap;
padding: 16px;
gap: 16px;
}
.list-footer {
text-align: center;
color: #999;
padding: 12px 0 80px; /* 避开底部吸底栏 */
}
.product-item {
width: calc(50% - 8px);
background-color: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s ease;
}
.product-item:active {
transform: scale(0.98);
}
.product-image-container {
width: 100%;
height: 120px;
overflow: hidden;
position: relative;
}
.product-image {
width: 100%;
height: 100%;
background-color: #f0f0f0;
}
.product-info {
padding: 12px;
}
.product-title {
font-size: 12px;
color: #333;
line-height: 1.4;
margin-bottom: 8px;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-price {
font-size: 14px;
color: #e74c3c;
font-weight: bold;
}
/* 底部导航栏样式 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 60px;
background-color: #20b2aa;
display: flex;
align-items: center;
z-index: 1000;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
transition: background-color 0.2s ease;
}
.nav-item.active {
background-color: rgba(255, 255, 255, 0.15);
border-radius: 8px;
margin: 4px;
}
.nav-icon {
font-size: 20px;
margin-bottom: 2px;
}
.nav-text {
font-size: 12px;
color: #fff;
font-weight: 500;
}
.nav-divider {
width: 1px;
height: 40px;
background-color: rgba(255, 255, 255, 0.3);
}
</style>

View File

@ -0,0 +1,162 @@
<template>
<view class="product-detail-container">
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
:title="navTitle"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#ffffff"
/>
<!-- 内容区 -->
<scroll-view scroll-y class="detail-scroll">
<!-- 商品主图轮播 -->
<view class="swiper-box">
<swiper class="detail-swiper" :indicator-dots="true" :autoplay="false" :interval="3000" :duration="400">
<swiper-item v-for="(img, idx) in images" :key="idx">
<image class="swiper-image" :src="img" mode="aspectFit"></image>
</swiper-item>
</swiper>
</view>
<!-- 标题与价格销量 -->
<view class="summary">
<view class="title">{{ product.title }}</view>
<view class="price-row">
<text class="price">{{ product.price }}积分</text>
<text class="exchanged">已兑换{{ product.times }}</text>
</view>
</view>
<view class="divider"></view>
<!-- 温馨提示 -->
<view class="tips-section">
<view class="tips-title">温馨提示</view>
<view class="tip-item"><text class="num">1</text><text class="tip-text">商品一经兑换消耗的积分恕不退还请各位用户兑换前仔细阅读商品描述详情</text></view>
<view class="tip-item"><text class="num">2</text><text class="tip-text">商品兑换后不接受退换货申请虚拟商品请在商品有效期内及时兑换</text></view>
<view class="tip-item"><text class="num">3</text><text class="tip-text">所有实物商品包邮虚拟商品将发送相关信息到联系邮箱或手机</text></view>
<view class="tip-item"><text class="num">4</text><text class="tip-text">实物商品兑换后2-4工作日内发放虚拟商品兑换后1-3工作日内发送</text></view>
</view>
<!-- 物品展示 -->
<view class="section-title">物品展示</view>
<view class="rich-wrapper">
<rich-text :nodes="product.content"></rich-text>
</view>
</scroll-view>
<!-- 底部兑换按钮 -->
<view class="bottom-bar">
<view class="redeem-btn" @click="goExchange">在线兑换</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import goods_api from '@/api/goods_api'
import docUrl from '@/utils/docUrl'
const product = ref({ id: '', title: '', price: 0, times: 0, content: '' })
const images = ref([])
const navTitle = ref('商品详情')
const goBack = () => uni.navigateBack()
const fetchDetail = async (id) => {
try {
const res = await goods_api.getGoodsDetail({ uuid: id })
if ((res.code === 200 || res.code === '200') && res.data) {
product.value = {
id,
title: res.data.name || '',
price: res.data.bonuspoints || 0,
times: res.data.times || 0,
content: res.data.content || ''
}
navTitle.value = (product.value.title || '').slice(0, 18)
const list = []
if (res.data.detial_imgpath) {
res.data.detial_imgpath.split(',').forEach(u => u && list.push(docUrl + u))
}
// if (res.data.img) list.push(docUrl + res.data.img)
images.value = list.length ? list : ['/static/product2.jpg']
} else {
uni.showToast({ title: res.message || '获取详情失败', icon: 'none' })
}
} catch (e) {
console.error(e)
uni.showToast({ title: '网络错误', icon: 'none' })
}
}
onLoad((opts) => {
if (opts && opts.id) fetchDetail(opts.id)
})
const goExchange = () => {
uni.navigateTo({
url: `/pages_goods/exchange/index?id=${product.value.id}&title=${encodeURIComponent(product.value.title)}&price=${product.value.price}`
})
}
</script>
<style scoped>
.product-detail-container {
width: 100%;
min-height: 100vh;
background-color: #f5f5f5;
}
.detail-scroll {
position: absolute;
top: 140rpx;
left: 0;
right: 0;
bottom: 100rpx;
background: #fff;
}
/* 主图轮播 */
.swiper-box { background: #fff; }
.detail-swiper { height: 480rpx; }
.swiper-image { width: 100%; height: 100%; }
/* 概要 */
.summary { background: #fff; padding: 24rpx; }
.title { font-size: 34rpx; color: #333; line-height: 1.6; }
.price-row { display: flex; justify-content: space-between; align-items: center; margin-top: 16rpx; }
.price { color: #e74c3c; font-size: 34rpx; font-weight: 700; }
.exchanged { color: #999; font-size: 24rpx; }
.divider { height: 16rpx; background: #f5f5f5; }
/* 温馨提示 */
.tips-section { background: #fff; padding: 24rpx; }
.tips-title { text-align: center; font-size: 36rpx; font-weight: 700; color: #333; margin-bottom: 16rpx; }
.tip-item { display: flex; align-items: flex-start; background: #f7f7f7; border-radius: 8rpx; padding: 20rpx; margin: 12rpx 0; }
.num { width: 36rpx; height: 36rpx; line-height: 36rpx; text-align: center; color: #888; background: #fff; border-radius: 50%; margin-right: 16rpx; border: 1rpx solid #ddd; }
.tip-text { color: #666; font-size: 26rpx; line-height: 1.6; flex: 1; }
.section-title { background: #fff; padding: 24rpx; font-size: 34rpx; font-weight: 700; color: #333; text-align: center; }
.rich-wrapper { background: #fff; padding: 24rpx; }
/* 底部按钮 */
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #27c5b8;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
}
.redeem-btn { color: #fff; font-size: 32rpx; font-weight: 700; }
</style>

24998
utils/areaList.js Normal file

File diff suppressed because it is too large Load Diff