Compare commits

...
59 Commits
Author SHA1 Message Date
zoujiandong c7dbbfda16 4.7提交 2026-04-07 14:32:00 +08:00
zoujiandong fd78b504a6 4.3提交 2026-04-03 17:32:25 +08:00
zoujiandong a26fc1a4a7 4.2 2026-04-02 14:49:21 +08:00
zoujiandong 5d31c3658b 4.2提交 2026-04-02 11:30:39 +08:00
zoujiandong 5f9a346960 3.31画像提交 2026-03-31 13:07:16 +08:00
zoujiandong 7180e176eb 3.30解决bug 2026-03-30 16:12:38 +08:00
zoujiandong e3a852cb43 3.30提交 2026-03-30 09:45:55 +08:00
zoujiandong 0a51901807 3.26 2026-03-26 17:34:15 +08:00
zoujiandong 8a879df4b4 3.25提交 2026-03-25 17:33:54 +08:00
zoujiandong 18c7f7386d 3.18提交 2026-03-18 17:19:05 +08:00
zoujiandong b5cc11ddd3 3.17 2026-03-17 18:59:46 +08:00
haomingming 4a22470f38 Merge branch 'master' of https://gitea.igandanyiyuan.com/gdxz/uniapp-app
# Conflicts:
#	pages_app/patientMsg/patientMsg.vue
2026-03-17 14:53:44 +08:00
haomingming 6cdd8b90db patientList 2026-03-17 14:49:50 +08:00
zoujiandong cbc427e41c 3.17上午提交 2026-03-17 14:02:39 +08:00
zoujiandong 45a9478790 3.16 2026-03-16 14:35:45 +08:00
haomingming c6ce198c31 Merge branch 'master' of https://gitea.igandanyiyuan.com/gdxz/uniapp-app 2026-03-16 14:34:53 +08:00
haomingming f4fe516224 webview 2026-03-16 14:34:46 +08:00
zoujiandong 80772749b2 3.14提交 2026-03-14 17:32:11 +08:00
zoujiandong 0fa310e422 地址高度修改 2026-03-13 17:50:47 +08:00
zoujiandong 761a8fdaa4 3.13下班提交 2026-03-13 17:42:34 +08:00
zoujiandong ca69dfbe4b 3.13 五点更新 2026-03-13 17:13:09 +08:00
zoujiandong 4042df6762 3.13提交 2026-03-13 16:18:33 +08:00
zoujiandong 4e3a30b863 3.12晚上 2026-03-12 18:57:28 +08:00
zoujiandong 2831de027c 3.12提交 2026-03-12 14:38:11 +08:00
zoujiandong 08e3ba22c5 3.11提交 2026-03-11 13:13:10 +08:00
zoujiandong 40f5ddf4a5 3.9提交 2026-03-09 18:59:27 +08:00
zoujiandong 1fa822ed72 3.6修改 2026-03-06 19:01:31 +08:00
haomingming 89848ba321 我的账户 2026-03-06 14:40:09 +08:00
haomingming 7fb1200528 我的鲜花 2026-03-06 10:45:44 +08:00
haomingming b833a00d65 我的鲜花 2026-03-06 10:44:49 +08:00
zoujiandong e8e9c84cdf 分组数据较多时 2026-03-05 19:00:51 +08:00
haomingming 1e99b3a082 Merge branch 'master' of https://gitea.igandanyiyuan.com/gdxz/uniapp-app 2026-03-05 17:08:47 +08:00
haomingming efcc7d9603 常用银行卡 2026-03-05 17:08:18 +08:00
zoujiandong 54f8132cee ppt页面修改 2026-03-05 14:50:50 +08:00
zoujiandong ddbfac11c3 请求注释 2026-03-05 13:14:03 +08:00
zoujiandong 1de18aa228 群发消息输入 2026-03-05 10:10:58 +08:00
zoujiandong 799471090f 3.5提交代码 2026-03-05 09:57:10 +08:00
zoujiandong ae9a961a42 2.6提交 2026-02-06 17:30:16 +08:00
zoujiandong db9ca707ae 保利视频 2026-02-04 10:12:59 +08:00
zoujiandong a7b2a673a9 2.2提交 2026-02-02 17:44:10 +08:00
zoujiandong d06ee397cf 1.16提交 2026-01-16 17:36:04 +08:00
zoujiandong 034744959f 1.16更新 2026-01-16 10:58:35 +08:00
zoujiandong cf6df28e8c 1.15提交 2026-01-15 18:38:21 +08:00
zoujiandong 00d0ec8a9b 1.14提交 2026-01-14 17:41:42 +08:00
zoujiandong 56c645a75c 1.6提交 2026-01-06 08:36:02 +08:00
zoujiandong 8025b2abab 12.16提交 2025-12-16 17:42:25 +08:00
zoujiandong c86aa03f01 12.10提交 2025-12-10 17:33:39 +08:00
zoujiandong 22104881f1 12.3 2025-12-03 17:31:08 +08:00
zoujiandong f4c8df1950 111 2025-12-03 10:20:33 +08:00
zoujiandong 1785e9309e 1111 2025-12-03 10:06:59 +08:00
zoujiandong da3659412d 11111 2025-12-03 09:58:56 +08:00
zoujiandong 51367c48c9 pdf方法 2025-12-03 09:13:24 +08:00
zoujiandong 41079458dc zhinanlist报错 2025-12-03 08:40:39 +08:00
zoujiandong 60531b339e 12.2提交 2025-12-02 15:24:51 +08:00
zoujiandong 9788e723a2 11.28提交 2025-11-28 10:43:13 +08:00
zoujiandong 91726b87c5 11.21提交 2025-11-21 17:49:39 +08:00
zoujiandong 6b37f0899d 去掉toast的引入 2025-11-20 11:33:55 +08:00
zoujiandong f4b1ef932d 11.20 2025-11-20 10:29:35 +08:00
zoujiandong 76e619b85d video宽度 2025-11-17 16:08:45 +08:00
238 changed files with 45176 additions and 10278 deletions
+135 -94
View File
@@ -1,45 +1,85 @@
<script>
export default {
import version from '@/utils/version.js'
export default {
globalData: {
plAd: true,
dotNumber: {
}
dotNumber: {},
apiHost: "https://dev-app.igandan.com/app",
//BASE_URL='https://dev-app.igandan.com/app'
},
onLaunch(options) {
console.log('打印options');
//uni.setStorageSync('apiHost', 'value')
console.log("打印options");
console.log(options);
if (options && options.referrerInfo.extraData && options.referrerInfo.extraData.token) {
uni.setStorageSync("AUTH_TOKEN_App", options.referrerInfo.extraData.token);
if (
options &&
options.referrerInfo.extraData &&
options.referrerInfo.extraData.token
) {
uni.setStorageSync(
"AUTH_TOKEN_App",
options.referrerInfo.extraData.token
);
uni.setStorageSync("userInfo", options.referrerInfo.extraData.userInfo);
};
if (options && options.referrerInfo.extraData && options.referrerInfo.extraData.targetPath) {
uni.navigateTo({
url: options.referrerInfo.extraData.targetPath
});
if(process.env.UNI_PLATFORM =="mp-weixin"){
uni.setStorageSync("app_version",version);
}else{
let app_version=options.referrerInfo.extraData.app_version?options.referrerInfo.extraData.app_version:version;
uni.setStorageSync("app_version",app_version);
};
//getApp({allowDefault: true}).globalData.apiHost = options.referrerInfo.extraData.apiHost;
}
if (
options &&
options.referrerInfo.extraData &&
options.referrerInfo.extraData.targetPath
) {
let src=options.referrerInfo.extraData.targetPath
if(src.indexOf('downLoadVideo') > -1 ){
uni.navigateTo({
url: src+'?from=mine',
});
}else{
uni.navigateTo({
url: src,
});
}
}
console.log("app传递登录信息onLaunch-------------------");
try {
// #ifdef APP
// uni.onNativeEventReceive((event, data) => {
// console.log("app传递登录信息-------------------");
// console.log("event数据");
// console.log(event);
// console.log("data数据");
// console.log(data);
// if (event.indexOf("loginInfo") > -1) {
// uni.setStorageSync("DEV_AUTH_TOKEN_App", data.token);
// uni.setStorageSync("userInfo", data.userInfo);
// console.log("获取token数据");
// console.log(uni.getStorageSync("DEV_AUTH_TOKEN_App"));
// }
// });
uni.onNativeEventReceive((event, data) => {
console.log("event", event);
console.log("data", data);
if (event.indexOf("clearToken") > -1) {
uni.setStorageSync("AUTH_TOKEN_App", "");
plus.runtime.quit();
}
if(event.indexOf("goPage") > -1){
if(data.indexOf('downLoadVideo') > -1){
uni.navigateTo({
url: data+'?from=mine'
});
}else{
uni.navigateTo({
url: data,
});
}
}
if(event.indexOf('paySuccess') > -1){
console.log('app.vue监听到成功支付');
if(data.type=='GandanFile'){
uni.$emit('paySuccess',{
type:'GandanFile',
});
}else if(data.type=='BuyPoint'){
uni.navigateBack();
}
}
});
// #endif
} catch (error) {
console.log("app传递登录信息error-------------------");
@@ -49,35 +89,43 @@
onShow() {
try {
// #ifdef APP
let checkTokenCallBackFlag = false
console.log("执行onshow")
let checkTokenCallBackFlag = false;
console.log("执行onshow");
uni.sendNativeEvent('checkToken', {
msg: 'checkToken'
}, ret => {
checkTokenCallBackFlag = true
console.log('check回调')
console.log(ret)
uni.sendNativeEvent(
"checkToken",
{
msg: "checkToken",
},
(ret) => {
checkTokenCallBackFlag = true;
console.log("check回调");
console.log(ret);
if (ret.code == 0) {
uni.showModal({
title: '提示',
title: "提示",
showCancel: false,
content: '您已退出登录!',
content: "您已退出登录!",
success: function (res) {
if (res.confirm) {
uni.sendNativeEvent('goTabbarPage', {
msg: 'home'
}, ret => {
uni.sendNativeEvent(
"goTabbarPage",
{
msg: "home",
},
(ret) => {
console.log(ret);
})
plus.runtime.quit()
}
);
plus.runtime.quit();
} else if (res.cancel) {
console.log('用户点击取消');
}
console.log("用户点击取消");
}
},
});
}
})
}
);
// setTimeout(function(){
// console.log("执行 setTimeout")
@@ -101,31 +149,23 @@
// }
// }, 1000)
uni.onNativeEventReceive((event, data) => {
console.log("event", event);
console.log("data", data);
if (event.indexOf("clearToken") > -1) {
uni.setStorageSync('AUTH_TOKEN_App', '')
plus.runtime.quit()
}
})
let main = plus.android.runtimeMainActivity();
//为了防止快速点按返回键导致程序退出重写quit方法改为隐藏至后台
plus.runtime.quit = function() {
plus.runtime.quit = function () {
main.moveTaskToBack(false);
};
//重写toast方法如果内容为 ‘再按一次退出应用’ 就隐藏应用,其他正常toast
plus.nativeUI.toast = (function(str) {
if (str == '再按一次退出应用') {
plus.nativeUI.toast = function (str) {
if (str == "再按一次退出应用") {
main.moveTaskToBack(false);
} else {
uni.showToast({
title: str,
icon: 'none',
})
}
icon: "none",
});
}
};
// uni.onNativeEventReceive((event, data) => {
// console.log("app传递登录信息onshow-------------------");
@@ -141,16 +181,14 @@
// }
// });
// #endif
} catch (error) {
console.log('error');
console.log(error)
console.log("error");
console.log(error);
}
},
onHide() {},
methods: {},
};
};
</script>
<template>
@@ -158,66 +196,69 @@
</template>
<style lang="scss">
/*每个页面公共css */
@import "@/uni_modules/uni-scss/index.scss";
/* #ifndef APP-NVUE */
@import "@/static/customicons.css";
/*每个页面公共css */
@import "@/uni_modules/uni-scss/index.scss";
/* #ifndef APP-NVUE */
@import "@/static/customicons.css";
// 设置整个项目的背景色
page {
// 设置整个项目的背景色
page {
background-color: #f5f5f5;
}
}
.uni-navbar__header-btns-right {
.uni-navbar__header-btns-right {
min-width: 120rpx;
width: auto !important;
}
}
/* #endif */
.uni-nav-bar-text {
font-weight: bold;
font-size: 36rpx !important;
}
/* #endif */
.uni-nav-bar-text {
.example-info {
font-size: 40rpx !important;
}
.example-info {
font-size: 14px;
color: #333;
padding: 10px;
}
}
.twoline {
.twoline {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
}
.oneline {
.oneline {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.uni-navbar .uniui-left {
.uni-navbar .uniui-left {
font-weight: bold;
font-size: 50rpx !important;
}
}
::-webkit-scrollbar {
::-webkit-scrollbar {
display: none;
}
}
.status_bar {
.status_bar {
height: var(--status-bar-height);
background-color: #eeeeee;
}
}
.navbox {
.navbox {
position: fixed;
top: 0;
left: 0;
right: 0;
height: calc(var(--status-bar-height) + 44px);
z-index: 9999;
}
}
.share-content .share-item:last-child{
display: none;
}
</style>
+87 -6
View File
@@ -120,11 +120,11 @@ const api = {
},
// 积分支付列表
bonusPointsPayList(data) {
return request('/expertAPI/bonusPointsPayList', data, 'post', false);
return request('/expertAPI/bonusPointsPayList', data, 'post', true);
},
// 积分列表
bonusPointsList(data) {
return request('/expertAPI/bonusPointsList', data, 'post', false);
return request('/expertAPI/bonusPointsList', data, 'post', true);
},
getWelfarePage(data) {
@@ -336,6 +336,9 @@ const api = {
applyList(data){
return request('/expertAPI/applyList', data, 'post', false);
},
patientListByGBKPage(data){
return request('/expertAPI/patientListByGBKPage', data, 'post', false);
},
patientListByGBK(data){
return request('/expertAPI/patientListByGBK', data, 'post', false);
},
@@ -412,6 +415,10 @@ const api = {
bankCardList(data){
return request('/expertPay/bankCardList', data, 'post', false);
},
deleteBankCard(data){
return request('/expertPay/deleteBankCard', data, 'post', false);
},
getIncomeTax(data){
return request('/expertPay/getIncomeTax', data, 'post', false);
},
@@ -447,10 +454,10 @@ const api = {
return request('/expertAPI/consultListHis', data, 'post', false);
},
listNewInterrogation(data){
return request('/expertAPI/listNewInterrogation', data, 'post', false);
return request('/expertAPI/listNewInterrogation', data, 'post', true);
},
listMyAnsweredInterrogation(data){
return request('/expertAPI/listMyAnsweredInterrogation', data, 'post', false);
return request('/expertAPI/listMyAnsweredInterrogation', data, 'post', true);
},
getInterrogation(data){
return request('/expertAPI/getInterrogation', data, 'post', false);
@@ -459,10 +466,10 @@ const api = {
return request('/expertAPI/InterrogationPatientInfo', data, 'post', false);
},
updateInterrogationAnswer(data){
return request('/expertAPI/updateInterrogationAnswerU', data, 'post', false);
return request('/expertAPI/updateInterrogationAnswerU', data, 'post', true);
},
addInterrogationAnswer(data){
return request('/expertAPI/addInterrogationAnswerU', data, 'post', false);
return request('/expertAPI/addInterrogationAnswerU', data, 'post', true);
},
videoDetail(data){
return request('/expertAPI/videoDetail', data, 'post', false);
@@ -599,6 +606,7 @@ const api = {
getOrderStatus(data){
return request('/expertPay/getOrderStatus', data, 'post', false);
},
getOrderStatus(data){
return request('/expertPay/getOrderStatus', data, 'post', false);
},
@@ -651,6 +659,79 @@ const api = {
patientVideoByJingHuaNew(data){
return request('/expertAPI/patientVideoByJingHuaNew', data, 'post', false);
},
useWelfareNum(data){
return request('/expertAPI/useWelfareNum', data, 'post', false);
},
deleteComment(data){
return request('/expertAPI/deleteComment', data, 'post', false);
},
downloadGanDanFile(data){
return request('/expertAPI/downloadGanDanFile', data, 'post', false,'application/json',{},'arraybuffer');
},
downloadGanDanFileV2(data){
return request('/expertAPI/downloadGanDanFileV2', data, 'post', false);
},
feedBack(data){
return request('/expertAPI/addFeedBack', data, 'post', false);
},
getPointUnitPrice(data){
return request('/expertAPI/getPointUnitPrice', data, 'post', false);
},
welfareList(data){
return request('/expertAPI/welfareList', data, 'post', false);
},
getUGoods(data){
return request('/expertAPI/getGoodsDetailU', data, 'post', false);
},
createGoodsOrder(data){
return request('/expertAPI/createGoodsOrder', data, 'post', false);
},
useWelfareNum(data){
return request('/expertAPI/useWelfareNum', data, 'post', false);
},
createBointsOrder(data){
return request('/expertPay/createBointsOrder', data, 'post', false);
},
receiveWelfare(data){
return request('/expertAPI/receiveWelfare', data, 'post', false);
},
getKePuCollection(data){
return request('/expertAPI/getKePuCollection', data, 'post', false);
},
getRead(data){
return request('/expertAPI/read', data, 'post', false);
},
getOutRead(data){
return request('/expertAPI/outread', data, 'post', false);
},
getAgree(data){
return request('/expertAPI/agree', data, 'post', false);
},
getDisagree(data){
return request('/expertAPI/disagree', data, 'post', false);
},
getCollect(data){
return request('/expertAPI/collection', data, 'post', false);
},
getDiscollect(data){
return request('/expertAPI/discollection', data, 'post', false);
},
getGanDanFileOrder(data){
return request('/expertPay/getGanDanFileOrder', data, 'post', false);
},
publishOutPatient(data){
return request('/expertAPI/addGroupSendMsg4YunXin', data, 'post', false);
},
polularScienceArticleListIndexNew(data){
return request('/expertAPI/polularScienceArticleListIndexNew', data, 'post', false);
},
getNewsDetail(data){
return request('/expertAPI/newsDetial', data, 'post', false);
},
//https://dev-app.igandan.com/app/expertPay/getGanDanFileOrder
}
export default api
+15 -1
View File
@@ -23,9 +23,23 @@ const goods_api = {
createGoodsOrder(data) {
return request('/expertAPI/createGoodsOrder', data, 'post', true);
},
payGoodsOrder(data) {
return request('/expertAPI/payGoodsOrder', data, 'post', true);
},
getGoodsOrderList(data) {
return request('/expertAPI/goodsOrderList', data, 'post', true);
},
getUpanWelfareCount(data){
return request('/expertAPI/getUpanWelfareCount', data, 'post', false);
},
getGoodsOrderDetail(data) {
return request('/expertAPI/getGoodsOrder', data, 'post', true);
},
getOrderTrack(data) {
return request('/expertAPI/getOrderTrack', data, 'post', true);
},
cancelGoodsOrder(data) {
return request('/expertAPI/cancleGoodsOrder', data, 'post', true);
}
}
+94
View File
@@ -0,0 +1,94 @@
<template>
<!-- domId 是要截图的容器 id -->
<view class="my-html2canvas" :prop="domId" :change:prop="h2cRender.watchDomId">
<!-- 用于触发 renderjs 截图 -->
<text :prop="expOver" :change:prop="h2cRender.watchExpOver"></text>
</view>
</template>
<script>
export default {
props: {
domId: {
type: String,
required: true
}
},
data() {
return {
expOver: 0
}
},
methods: {
// 业务层调用这个方法开始截图
h2cRenderDom() {
// #ifdef H5
// H5 直接在当前环境(有 DOM)里跑 renderDom
this.$emit('renderDomInH5')
// #endif
// #ifndef H5
// 非 H5(APP 等)通过 expOver 触发 renderjs 中的截图逻辑
this.expOver++
// #endif
},
// renderjs 截图完成后会 callMethod 回调到这里
renderOver(base64) {
this.$emit('renderOver', base64)
}
}
}
</script>
<!-- renderjs 只能内联 -->
<script module="h2cRender" lang="renderjs">
import html2canvas from 'html2canvas'
export default {
data() {
return {
domIdValue: ''
}
},
methods: {
async renderDom() {
try {
const el = document.getElementById(this.domIdValue)
if (!el) {
console.error('[MyHtml2canvas] dom 未找到,请检查 domId:', this.domIdValue)
return
}
const canvas = await html2canvas(el, {
width: el.offsetWidth,
height: el.offsetHeight,
x: 0,
y: 0,
useCORS: true,
scale: 2
})
const base64 = canvas.toDataURL('image/png', 1)
this.$ownerInstance.callMethod('renderOver', base64)
} catch (err) {
console.error('[MyHtml2canvas] html2canvas 失败:', err && err.message)
}
},
// script 部分通过 :prop 传入 domId
watchDomId(newVal) {
this.domIdValue = newVal
},
// expOver 变化时触发渲染
watchExpOver(newVal) {
if (newVal !== 0) {
this.renderDom()
}
}
}
}
</script>
<style>
.my-html2canvas {
position: relative;
}
</style>
+91
View File
@@ -0,0 +1,91 @@
<template>
<view>
<!-- 当父组件传递的prop属性数据发生变化的时候 ,生成海报图片-->
<view :prop="domId" :change:prop="html2canvas.emitData" class="html2canvas">
<slot></slot>
</view>
</view>
</template>
<script>
import { base64ToPath } from '@/utils/image-tools.js';
export default {
props: {
domId: {
type: String,
required: true
}
},
mounted:function(){
console.log("组件加载完成");
},
methods: {
// 接收renderjs发回的数据
async receiveRenderData(imgData) {
let posterHeight=imgData.height;
let posterWidth=imgData.width;
let imgPath =await base64ToPath(imgData.imgVal, '.jpeg');
this.$emit('renderFinish', {"posterPath":imgPath,"posterHeight":posterHeight,"posterWidth":posterWidth});
uni.hideLoading();
},
showLoading() {
uni.showLoading({
title: "正在生成导出",
icon: "none",
mask: true,
duration: 3000
})
},
hideLoading() {
uni.hideLoading();
}
}
}
</script>
<script module="html2canvas" lang="renderjs">
import html2canvas from 'html2canvas';
export default {
data() {
return {
}
},
mounted:function(){
console.log("视图层渲染完成");
},
methods: {
// 发送数据到逻辑层,下面这里最好是加一个settimeout延迟,因为主界面数据还没加载渲染完成
emitData(domId) {
this.$ownerInstance.callMethod('showLoading', true);
const dom = document.getElementById('poster');
console.log("dom",dom);
let width= dom.offsetWidth;
let height=dom.offsetHeight;
let windowHeight= uni.getSystemInfoSync().windowHeight;
let windowWidth=uni.getSystemInfoSync().windowWidth;
html2canvas(dom, {
width:width,
height:height,
//scale:2.5, // 缩放倍数
scrollY: 0, // html2canvas默认绘制视图内的页面,需要把scrollY,scrollX设置为0
scrollX: 0,
x:0,
y:0,
windowWidth:windowWidth,
windowHeight:windowHeight,
useCORS: true, //支持跨域,但好像没什么用
}).then((canvas) => {
let imgVal= canvas.toDataURL('image/png');
this.$ownerInstance.callMethod('receiveRenderData',{imgVal:imgVal,height:height,width:width});
});
}
}
};
</script>
<style lang="scss">
</style>
@@ -0,0 +1,91 @@
<template>
<view>
<!-- 当父组件传递的prop属性数据发生变化的时候 ,生成海报图片-->
<view :prop="domId" :change:prop="html2canvas.emitData" class="html2canvas">
<slot></slot>
</view>
</view>
</template>
<script>
import { base64ToPath } from '@/utils/image-tools.js';
export default {
props: {
domId: {
type: String,
required: true
}
},
mounted:function(){
console.log("组件加载完成");
},
methods: {
// 接收renderjs发回的数据
async receiveRenderData(imgData) {
let posterHeight=imgData.height;
let posterWidth=imgData.width;
let imgPath =await base64ToPath(imgData.imgVal, '.jpeg');
this.$emit('renderFinish', {"posterPath":imgPath,"posterHeight":posterHeight,"posterWidth":posterWidth});
uni.hideLoading();
},
showLoading() {
uni.showLoading({
title: "正在生成导出",
icon: "none",
mask: true,
duration: 3000
})
},
hideLoading() {
uni.hideLoading();
}
}
}
</script>
<script module="html2canvas" lang="renderjs">
import html2canvas from 'html2canvas';
export default {
data() {
return {
}
},
mounted:function(){
console.log("视图层渲染完成");
},
methods: {
// 发送数据到逻辑层,下面这里最好是加一个settimeout延迟,因为主界面数据还没加载渲染完成
emitData(domId) {
this.$ownerInstance.callMethod('showLoading', true);
const dom = document.getElementById('poster');
console.log("dom",dom);
let width= dom.offsetWidth;
let height=dom.offsetHeight;
let windowHeight= uni.getSystemInfoSync().windowHeight;
let windowWidth=uni.getSystemInfoSync().windowWidth;
html2canvas(dom, {
width:width,
height:height,
//scale:2.5, // 缩放倍数
scrollY: 0, // html2canvas默认绘制视图内的页面,需要把scrollY,scrollX设置为0
scrollX: 0,
x:0,
y:0,
windowWidth:windowWidth,
windowHeight:windowHeight,
useCORS: true, //支持跨域,但好像没什么用
}).then((canvas) => {
let imgVal= canvas.toDataURL('image/png');
this.$ownerInstance.callMethod('receiveRenderData',{imgVal:imgVal,height:height,width:width});
});
}
}
};
</script>
<style lang="scss">
</style>
+9 -3
View File
@@ -3,7 +3,7 @@
<view class="dialog-container" @click.stop>
<!-- 弹窗标题 -->
<view class="dialog-header">
<text class="dialog-title">{{ title }}</text>
<text class="dialog-title" :style="{ color: titleColor }">{{ title }}</text>
</view>
<!-- 弹窗内容 -->
@@ -42,13 +42,13 @@
@click="oncancel"
>
<text class="btn-text">取消</text>
<text class="btn-text">{{ cancelText }}</text>
</view>
<view
class="dialog-btn confirm"
@click="onconfirm"
>
<text class="btn-text">确定</text>
<text class="btn-text">{{ confirmText }}</text>
</view>
</view>
</view>
@@ -63,6 +63,10 @@
type: Boolean,
default: false
},
titleColor: {
type: String,
default: '#8B2316'
},
title: {
type: String,
default: '温馨提示'
@@ -130,6 +134,8 @@
}
.dialog-container {
position: relative;
z-index:100;
background-color: #ffffff;
border-radius: 20rpx;
width: 92%;
+63 -4
View File
@@ -1,14 +1,68 @@
<template>
<view class="emptybox">
<up-image :src="emptyImg" width="176rpx" height="204rpx" ></up-image>
<text class="empty-text">暂无数据</text>
<view class="empty-image-wrap" :style="imageWrapStyle">
<up-image
:src="emptyImage || emptyImg"
:width="renderWidth"
:height="renderHeight"
:mode="imgMode"
></up-image>
</view>
<text class="empty-text">{{ emptyDesc }}</text>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import { computed } from 'vue';
import emptyImg from "@/static/icon_empty.png"
const props = defineProps({
emptyImage: {
type: String,
default: ''
},
imgWidth: {
type: [String, Number],
default: ''
},
imgHeight: {
type: [String, Number],
default: ''
},
emptyDesc: {
type: String,
default: '暂无数据'
}
})
const normalizeSize = (size) => {
if (size === '' || size === null || size === undefined) return '';
// 直传字符串(如 50%、206rpx、120px),数字交给 up-image 按组件规则处理
return typeof size === 'string' ? size.trim() : size;
};
const isPercentSize = (size) => typeof size === 'string' && size.endsWith('%');
const imgWidth = computed(() => {
const width = normalizeSize(props.imgWidth);
const height = normalizeSize(props.imgHeight);
if (width !== '') return width;
if (height === '') return '206rpx';
return '';
});
const imgHeight = computed(() => normalizeSize(props.imgHeight));
const renderWidth = computed(() => (isPercentSize(imgWidth.value) ? '100%' : imgWidth.value));
const renderHeight = computed(() => (isPercentSize(imgHeight.value) ? '100%' : imgHeight.value));
const imageWrapStyle = computed(() => {
const style = {};
if (isPercentSize(imgWidth.value)) style.width = imgWidth.value;
if (isPercentSize(imgHeight.value)) style.height = imgHeight.value;
return style;
});
const imgMode = computed(() => {
// 只传宽/高时按比例缩放,避免图片被拉伸
if (imgWidth.value && !imgHeight.value) return 'widthFix';
if (!imgWidth.value && imgHeight.value) return 'heightFix';
return 'aspectFit';
});
</script>
<style scoped lang="scss">
@@ -21,6 +75,11 @@
align-items: center;
justify-content: center;
flex-direction: column;
.empty-image-wrap{
width: 100%;
display: flex;
justify-content: center;
}
.empty-text{
font-size: 30rpx;
color:#999;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,97 @@
/**
* @file 拖动进度提示组件
*/
<template>
<view class="plv-player-drag-tips">
<view class="plv-player-drag-tips__box">
<image class="plv-player-drag-tips__direction-img" :src="imgSrc"
@error="imageError">
</image>
<text class="plv-player-drag-tips__time">
{{changeTimeFormat}} / {{durationFormat}}
</text>
</view>
</view>
</template>
<script>
import utils from '@/common/util.js';
import backImg from '@/static/skin/back.png';
import forwardImg from '@/static/skin/forward.png'
export default {
props: {
currentTime: {
type: Number
},
duration: {
type: Number
},
changeTime: {
type: Number
}
},
data() {
return {
isForward: false
}
},
watch: {
changeTime(newValue, oldValue){
this.isForward = newValue > oldValue;
}
},
computed: {
timeFormatType() {
this.duration >= 3600 ? 2 : 1;
},
changeTimeFormat() {
return utils.formatTimeByDuration(this.changeTime, this.timeFormatType);
},
durationFormat() {
return utils.formatTimeByDuration(this.duration, this.timeFormatType)
},
imgSrc() {
return this.isForward ? forwardImg : backImg;
}
}
}
</script>
<style>
.plv-player-drag-tips {
width: 250rpx;
height: 160rpx;
background: rgba(0,0,0,.7);
border-radius: 10rpx;
}
.plv-player-drag-tips__box {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
justify-content: center;
align-items: center;
}
.plv-player-drag-tips__direction-img {
width: 100rpx;
height: 100rpx;
}
.plv-player-drag-tips__time {
font-size: 24rpx;
color: #FFFFFF;
}
</style>
+271
View File
@@ -0,0 +1,271 @@
<template>
<view class="plv-player-gesture"
@touchstart="touchstart"
@touchmove="touchmove"
@longpress="handleLongPress"
@touchcancel="handleTouchCancel"
@touchend="touchend">
</view>
</template>
<script>
export default {
props: {
// 屏幕宽度(物理像素)
holeWidth: Number,
// 视频总时长
duration: Number,
// 视频当前播放时间
currentTime: Number
},
computed: {
halfWidth() {
return parseInt(this.holeWidth / 2);
}
},
data() {
return {
// 是否在左区域按下手势
downLeft: false,
// 滑动方向
swipeDir: null
}
},
mounted() {
this.addHandler();
},
methods:{
addHandler() {
// 增加父容器左右滑动监听
let finalTime = -1;
// 计算左右滑动seek进度
const countTime = (position) => {
const { holeWidth, duration, currentTime } = this;
const precent = Math.abs(position) / holeWidth;
const nowTime = currentTime || 0;
const constSecond = duration < 60 ? duration : 60;
let plusTime = parseInt(Math.abs(precent * constSecond));
if (position < 0) plusTime = plusTime * -1;
finalTime = nowTime + plusTime;
finalTime = this.limit(finalTime, 0, duration);
return finalTime;
};
this.addGesture((type, position) => {
switch (type) {
// 下滑
case -1:
// console.log('//下滑');
this.$emit('onGestureEvent', {
type: this.downLeft ? 'LEFT_DOWN' : 'RIGHT_DOWN',
position
});
break;
// 上滑
case -2:
// console.log('//上滑');
this.$emit('onGestureEvent', {
type: this.downLeft ? 'LEFT_UP' : 'RIGHT_UP',
position
});
break;
// 左滑
case 0:
// console.log('//左滑');
this.swipeDir = type;
countTime(position);
this.$emit('onGestureEvent', {
type: 'SEEK_TIME_UPDATE',
finalTime
});
break;
// 右滑
case 1:
// console.log('//右滑');
this.swipeDir = type;
countTime(position);
this.$emit('onGestureEvent', {
type: 'SEEK_TIME_UPDATE',
finalTime
});
break;
// touchend for swipe
case 3:
if (this.swipeDir > -1) {
this.$emit('onGestureEvent', {
type: this.swipeDir ? 'SWIPE_RIGHT' : 'SWIPE_LEFT'
});
this.swipeDir = -1;
}
if (finalTime > -1) {
this.$emit('onGestureSeekTo', finalTime);
finalTime = -1;
}
break;
// doubleTap
case 4:
// console.log('>>>> 双击');
this.$emit('onGestureEvent', {
type: 'DOUBLE_CLICK'
});
break;
// tap
case 5:
// console.log('>>>> 单击');
this.handleClick();
break;
// longTap
case 6:
break;
}
});
},
addGesture(callback) {
const { options } = this;
let startX, startY, moveEndX, moveEndY, X, Y, position, hasType;
let T, lastTap, isJustTouch = true;
const excuteCb = (type) => {
// type 手势类型 -2 上滑 -1 下滑 0 左滑 1 右滑 2 just touch 3 touchend 4 doubleTap 5 tap 6 longTap
if (callback && typeof callback === 'function')
callback(type, position);
};
const YPlus = 20;
const YReduce = YPlus * -1;
const XPlus = 50;
const XReduce = XPlus * -1;
const isXchange = () => {
if (X > XPlus || X < XReduce) {
position = X;
hasType = 'X';
isJustTouch = false;
}
if (X > XPlus) excuteCb(1);
// 左滑
else if (X < XReduce) excuteCb(0);
};
const isYchange = () => {
if (Y > YPlus || Y < YReduce) {
position = Y;
hasType = 'Y';
isJustTouch = false;
}
if (Y > YPlus) excuteCb(-1);
// 上滑
else if (Y < YReduce) excuteCb(-2);
};
const countDirect = () => {
if (hasType) {
hasType === 'X' ? isXchange() : isYchange();
return;
}
isXchange();
isYchange();
};
this.handleTouch = (type, e) => {
switch(type) {
case 'start':
startX = e.changedTouches[0].pageX;
startY = e.changedTouches[0].pageY;
T = Date.now();
hasType = '';
isJustTouch = true;
this.downLeft = startX < this.halfWidth ? true : false;
break;
case 'move':
moveEndX = e.changedTouches[0].pageX;
moveEndY = e.changedTouches[0].pageY;
X = moveEndX - startX;
Y = moveEndY - startY;
countDirect();
break;
case 'end':
if (isJustTouch)
if (lastTap && Date.now() - lastTap <= 300)
excuteCb(4);
else if (Date.now() - T < 1000)
excuteCb(5);
else
excuteCb(6);
else
excuteCb(3);
lastTap = Date.now();
isJustTouch = true;
break;
}
};
},
handleClick() {
this.$emit('onGestureClick');
},
touchstart(e) {
this.handleTouch('start', e);
},
touchmove(e) {
this.handleTouch('move', e);
},
touchend(e) {
this.handleTouch('end', e);
this.$emit('onTouchEnd');
},
handleTouchCancel() {
this.$emit('onTouchCancel');
},
handleLongPress() {
this.$emit('onLongPress');
},
/**
* 控制最小值,最大值
* @param num
* @param min
* @param max
* @returns {*}
*/
limit(num, min, max) {
if (num < min) return min;
if (num > max) return max;
return num;
}
}
}
</script>
<style>
.plv-player-gesture {
flex: 1;
flex-direction: row;
}
.plv-player-gesture__left {
flex: 5;
}
.plv-player-gesture__right {
flex: 5;
}
</style>
+68
View File
@@ -0,0 +1,68 @@
<template>
<view v-if="isShow" class="plv-player-icon">
<image class="plv-player-icon__img" :src="imgUrl"></image>
<text class="plv-player-icon__text">{{ precent + '%' }}</text>
</view>
</template>
<script>
export default {
props: {
imgUrl: {
type: 'string',
required: true
},
precent: {
type: 'Number',
default: 100,
required: true
}
},
watch: {
precent(num) {
if (!this.isShow) this.isShow = true;
this.startClock();
}
},
data() {
return {
isShow: false
}
},
methods: {
startClock() {
this.clearClock();
this.clock = setTimeout(() => {
this.isShow = false;
}, 1500);
},
clearClock() {
if (this.clock) clearTimeout(this.clock);
this.clock = null;
}
}
}
</script>
<style>
.plv-player-icon {
align-items: center;
justify-content: center;
}
.plv-player-icon__img {
width: 80rpx;
height: 80rpx;
}
.plv-player-icon__text {
color: #FFFFFF;
text-align: center;
}
</style>
File diff suppressed because it is too large Load Diff
+92
View File
@@ -0,0 +1,92 @@
<template>
<view v-if="isShow" class="plv-player-volume-wrap">
<image class="plv-player-volume__img" :src="value === 0 ? muteImgUrl : imgUrl "></image>
<view class="plv-player-volume__progress">
<view class="plv-player-volume__progress__played" :style="'flex:' + value"></view>
<view class="plv-player-volume__progress_surplus" :style="'flex:' + valueLeft"></view>
</view>
</view>
</template>
<script>
export default {
props: {
imgUrl: {
type: 'string',
required: true
},
muteImgUrl: {
type: 'string',
required: true
},
value: {
type: 'Number',
default: 0.5,
required: true
}
},
watch: {
value(num) {
if (!this.isShow) this.isShow = true;
this.startClock();
}
},
computed: {
valueLeft() {
return 1 - this.value;
}
},
data() {
return {
isShow: false
}
},
methods: {
startClock() {
this.clearClock();
this.clock = setTimeout(() => {
this.isShow = false;
}, 1500);
},
clearClock() {
if (this.clock) clearTimeout(this.clock);
this.clock = null;
}
}
}
</script>
<style>
.plv-player-volume-wrap {
flex-direction: row;
align-items: center;
justify-content: center;
}
.plv-player-volume__img {
width: 40rpx;
height: 40rpx;
margin-right: 10rpx;
}
.plv-player-volume__progress {
width: 260rpx;
height: 8rpx;
flex-direction: row;
background-color: #09BB07;
}
.plv-player-volume__progress__played {
background-color: #007AFF;
}
.plv-player-volume__progress_surplus {
background-color: #FFFFFF;
}
</style>
+40 -5
View File
@@ -2,8 +2,8 @@
"name" : "肝胆相照专家版",
"appid" : "__UNI__89F511F",
"description" : "",
"versionName" : "4.1.5",
"versionCode" : 100,
"versionName" : "4.2.7",
"versionCode" : 427,
"transformPx" : false,
"app-plus" : {
/* 5+App特有相关 */
@@ -31,7 +31,8 @@
"VideoPlayer" : {},
"OAuth" : {},
"Barcode" : {},
"Camera" : {}
"Camera" : {},
"Payment" : {}
},
/* 模块配置 */
"distribute" : {
@@ -57,11 +58,17 @@
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
],
"abiFilters" : [ "armeabi-v7a", "arm64-v8a", "x86" ],
"minSdkVersion" : 21
"minSdkVersion" : 21,
"targetSdkVersion" : 30
},
"ios" : {
"dSYMs" : false,
"idfa" : false
"idfa" : false,
"urlschemewhitelist" : [ "weixin", "wechat" ],
"permission" : {
"NSPhotoLibraryUsageDescription" : "需要访问相册以选择图片",
"NSCameraUsageDescription" : "需要访问相机以拍照"
}
},
/* ios打包配置 */
"sdkConfigs" : {
@@ -159,6 +166,34 @@
}
}
}
},
"PLV-MediaPlayer" : {
"__plugin_info__" : {
"name" : "Polyv播放器插件",
"description" : "该插件封装了保利威底层播放器,是云直播和云点播插件必须的附属插件",
"platforms" : "Android,iOS",
"url" : "https://ext.dcloud.net.cn/plugin?id=4798",
"android_package_name" : "cn.shangyu.gdxzExpert",
"ios_bundle_id" : "cn.shangyu.gdxzExport",
"isCloud" : true,
"bought" : 1,
"pid" : "4798",
"parameters" : {}
}
},
"PLV-VodUniPlugin" : {
"__plugin_info__" : {
"name" : "polyv保利威视频云点播插件",
"description" : "polyv保利威视频云点播软件组件,支持聊天弹幕、视频加密、多码率、去广告等,还支持视频多维数据统计,适用于教学录播、产品介绍片、视频门户等",
"platforms" : "Android,iOS",
"url" : "https://ext.dcloud.net.cn/plugin?id=2802",
"android_package_name" : "cn.shangyu.gdxzExpert",
"ios_bundle_id" : "cn.shangyu.gdxzExport",
"isCloud" : true,
"bought" : 1,
"pid" : "2802",
"parameters" : {}
}
}
}
},
+2
View File
@@ -12,12 +12,14 @@
"@xkit-yx/utils": "^0.7.2",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.18",
"html2canvas": "^1.4.1",
"image-tools": "^1.4.0",
"js-base64": "^3.7.8",
"js-md5": "^0.8.3",
"mobx": "^6.6.1",
"nim-web-sdk-ng": "^10.9.50",
"pinyin": "^4.0.0",
"uni-packing-wgt": "^1.2.3",
"uniapp-video-player": "^1.3.0",
"uview-plus": "^3.4.73",
"vconsole": "^3.15.1"
+983 -634
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -107,18 +107,18 @@
// 方法
// 方法
const goToCourse = (courseType) => {
console.log('进入课程:', courseType);
// 根据课程类型进行不同的处理
switch(courseType) {
case 'course':
navTo({
url: '/pages_course/course/course'
uni.sendNativeEvent('goCourse',{
msg: 'course'
})
break;
case 'video':
navTo({
url: '/pages_app/video/video'
url: '/pages_app/video/video?from=education'
})
break;
case 'ppt':
+13 -11
View File
@@ -703,31 +703,31 @@
let url = "";
if (name == "我的患者") {
url = "/pages_app/patientMsg/patientMsg";
sendAppMsgEvent('goPatientMsg', name);
//sendAppMsgEvent('goPatientMsg', name);
} else if (name == "肝胆视频") {
url = "/pages_app/video/video";
sendAppMsgEvent('goGdVideo', name);
//sendAppMsgEvent('goGdVideo', name);
} else if (name == "公益咨询") {
url = "/pages_app/consult/consult";
sendAppMsgEvent('goFreeConsult', name);
//sendAppMsgEvent('goFreeConsult', name);
} else if (name == "指南杂志") {
url = "/pages_app/zhinan/zhinan";
sendAppMsgEvent('goGuide', name);
//sendAppMsgEvent('goGuide', name);
} else if (name == "肝胆新闻") {
url = "/pages_app/newsList/newsList";
sendAppMsgEvent('goGdNews', name);
//sendAppMsgEvent('goGdNews', name);
} else if (name == "肝胆课件") {
url = "/pages_app/ppt/ppt";
sendAppMsgEvent('goGdPpt', name);
//sendAppMsgEvent('goGdPpt', name);
} else if (name == "精品课") {
url = "/pages_course/course/course";
sendAppMsgEvent('goCourse', name);
//sendAppMsgEvent('goCourse', name);
} else if (name == "积分商城") {
url = "/pages_goods/pointMall/pointMall";
sendAppMsgEvent('goPointMall', name);
//sendAppMsgEvent('goPointMall', name);
} else if (name == "我的福利") {
url = "/pages_app/myWelfare/myWelfare";
sendAppMsgEvent('goMyWelfare', name);
//sendAppMsgEvent('goMyWelfare', name);
} else if (name == "专题e站") {
const encoded = encodeURIComponent(
"https://wx.igandan.com/Esite/index.htm#/home?fromtype=doctor"
@@ -738,9 +738,11 @@
});
} else {
url = "/pages_app/myApplication/myApplication";
sendAppMsgEvent('goMyApplication', name);
// sendAppMsgEvent('goMyApplication', name);
}
navTo({
url: url,
});
};
const sendAppMsg = () => {
uni.sendNativeEvent('openPage', {
+57 -26
View File
@@ -3,6 +3,7 @@
<view class="status_bar"></view>
<uni-nav-bar
title="肝胆会议"
:left-icon="from?'left':''"
@clickLeft="goBack"
color="#8B2316"
:border="false"
@@ -66,6 +67,7 @@
<scroll-view
ref="scrollView"
class="scroll-content"
:class="{'active':from}"
scroll-y="true"
refresher-enabled="true"
:refresher-triggered="isRefreshing"
@@ -245,7 +247,7 @@
</view>
<!-- 底部导航栏 -->
<CustomTabbar></CustomTabbar>
<CustomTabbar v-if="!from"></CustomTabbar>
<topTip ref="topTipRef" title="相机权限使用说明" content="“肝胆相照”想使用您的相机和访问您的媒体,用于帮助您进行后续的扫码活动"></topTip>
</view>
</template>
@@ -269,6 +271,7 @@ import { encryptAesEcb, decryptAesEcb } from "@/utils/aesEcb.js";
import { getCameraPermission } from "@/utils/permission.js";
import topTip from "@/components/topTip/topTip.vue";
import lazyImg from "@/static/default_news_iv.png";
import isAndroid from "@/utils/platform.js";
// 弹窗状态
const isTimePopupShow = ref(false);
const isLocationPopupShow = ref(false);
@@ -287,6 +290,7 @@ const pageSize = ref(10);
const scrollTop = ref(0);
const permissionListener=ref(null);
const topTipRef=ref(null);
const from=ref('');
const goToMeetHistroy = () => {
navTo({
url: "/pages_app/meetHistroy/meetHistroy",
@@ -296,8 +300,20 @@ const goToMeetHistroy = () => {
onUnload (() => {
permissionListener.value?.stop();
});
onLoad(()=>{
onLoad((options)=>{
if(options.from){
from.value=options.from;
}
// #ifdef APP-PLUS
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "会议直播",
},
},
(ret) => {
console.log(ret);
});
permissionListener.value=uni.createRequestPermissionListener();
console.log('放在onLoad执行');
console.log(permissionListener.value);
@@ -372,6 +388,7 @@ const generateMonthList = () => {
console.log("生成的月份列表:", monthList.value);
};
const goDetail = (item) => {
console.log(item);
// uni.sendNativeEvent('noticeBack', {
// msg: 'go'
// }, ret => {
@@ -381,10 +398,16 @@ const goDetail = (item) => {
const encoded = encodeURIComponent(item.path);
let imgPath=encodeURIComponent('https://doc.igandan.com/app/html/img/2016/20160714132557.png');
uni.navigateTo({
url: `/pages_app/webview/webview?url=${encoded}&sharetitle=${item.title}&bg=1&type=live_yugao&imgPath=${imgPath}&share=1&title=会议详情`,
url: `/pages_app/webview/webview?url=${encoded}&sharetitle=${item.title}&bg=1&type=live_yugao&imgPath=${imgPath}&share=1&title=会议详情&tags=${item.tags}`,
});
};
const goBack = () => {
if(from.value){
uni.navigateBack();
}
};
onBackPress(() => {
if(!from.value){
try{
uni.sendNativeEvent('goTabbarPage', {
msg: 'home'
@@ -396,6 +419,7 @@ const goDetail = (item) => {
}
plus.runtime.quit()
return true;
}
})
const sendWebsocketMsg = (code) => {
let userInfo = uni.getStorageSync("userInfo");
@@ -438,23 +462,25 @@ const sendWebsocketMsg = (code) => {
// });
};
const handleScanCode = () => {
if(isAndroid){
getCameraPermission(scanCode);
// uni.sendNativeEvent('getScanCodeAuth', {
// msg: 'getScanCodeAuth'
// }, ret => {
// console.log('getScanCodeAuth回调数据');
// console.log(ret);
// if(ret.code==200){
// scanCode()
// }else{
// uni.showToast({
// title: '相机权限授权失败',
// icon: 'none'
// });
// }
// })
}else{
console.log('ios执行');
uni.sendNativeEvent('getScanCodeAuth', {
msg: 'getScanCodeAuth'
}, ret => {
console.log('getScanCodeAuth回调数据');
console.log(ret);
if(ret.code==200){
scanCode()
}else{
uni.showToast({
title: '请在iPhone的"设置-隐私-相机"中允许肝胆相照专家端访问相机',
icon: 'none'
});
}
})
}
}
const scanCode = () => {
uni.scanCode({
@@ -615,6 +641,7 @@ const meetingListBySearch = async (isRefresh = false) => {
poster: item.liveimg, // 使用 liveimg 作为海报
time: timeStr,
location: item.location || "线上",
tags: item.tags || '',
status: getStatusText(item.status),
// 保留原始数据用于其他功能
originalData: item,
@@ -728,6 +755,7 @@ const getMeetingList = async (isRefresh = false) => {
liveurl: item.liveurl,
spareurl: item.spareurl,
path: item.path,
tags: item.tags || '',
status_code: item.status,
};
});
@@ -921,12 +949,12 @@ const selectProvince = (province) => {
// 播放视频
const playVideo = (item) => {
uni.sendNativeEvent('noticeBack', {
msg: 'go'
}, ret => {
console.log('ret数据');
console.log(ret);
})
// uni.sendNativeEvent('noticeBack', {
// msg: 'go'
// }, ret => {
// console.log('ret数据');
// console.log(ret);
// })
if(item.status_code==1 || item.status_code==2 || item.status_code==4){
let zhiboUrl=OTHER_HOST+'/hcp/setInfo';
let userInfo=uni.getStorageSync('userInfo');
@@ -937,7 +965,7 @@ const playVideo = (item) => {
const encoded = encodeURIComponent(tempUrl);
let imgPath=encodeURIComponent(docUrl+item.liveimg);
uni.navigateTo({
url: `/pages_app/webview/webview?url=${encoded}&sharetitle=${item.title}&bg=1&type=live&imgPath=${imgPath}&share=1&title=会议直播`,
url: `/pages_app/webview/webview?url=${encoded}&sharetitle=${item.title}&bg=1&type=live&imgPath=${imgPath}&share=1&title=会议直播&tags=${item.tags}`,
});
}else{
goDetail(item);
@@ -1065,6 +1093,9 @@ $shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
right: 0;
bottom:120rpx; // 底部导航栏高度
width: 100%;
&.active{
bottom:10rpx;
}
}
// 筛选标签栏
+22 -1
View File
@@ -4,21 +4,42 @@
</template>
<script setup>
import { onMounted } from 'vue';
import { onMounted,ref} from 'vue';
import { onLoad,onUnload,onHide,onShow } from '@dcloudio/uni-app';
import navBar from '@/components/navBar/navBar.vue';
const timer=ref(null);
const count=ref(0);
const countDown=()=>{
if(timer.value){
return
}
timer.value=setInterval(()=>{
count.value++;
if(count.value>=8){
clearInterval(timer.value);
plus.runtime.quit();
}
},1000);
}
onShow(() => {
uni.showLoading({
title: '加载中...',
mask: true
});
countDown()
});
onUnload(() => {
uni.hideLoading();
if(timer.value){
clearInterval(timer.value);
}
});
onHide(() => {
uni.hideLoading();
if(timer.value){
clearInterval(timer.value);
}
});
</script>
+1 -1
View File
@@ -523,7 +523,7 @@
url="/pages_app/changeMobile/changeMobile"
break;
case 'benefitExchange':
url="/pages_app/myWelfareCard/myWelfareCard"
url="/pages_app/myWelfareCard/myWelfareCard?from=my"
break;
case 'bankCard':
url="/pages_app/idcardAuth/bankCardList"
+43 -23
View File
@@ -108,8 +108,9 @@
<!-- 空状态 -->
<view class="empty-state" v-if="articleList.length === 0 && !loading">
<uni-icons type="article" size="80" color="#cccccc"></uni-icons>
<text class="empty-text">暂无文章</text>
<empty :emptyDesc="'暂无患教信息'"></empty>
<!-- <uni-icons type="article" size="80" color="#cccccc"></uni-icons>
<text class="empty-text">暂无文章</text> -->
</view>
</scroll-view>
@@ -121,7 +122,7 @@
<script setup>
import { ref } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import {onShow,onLoad,onBackPress } from "@dcloudio/uni-app";
import CustomTabbar from '@/components/tabBar/tabBar.vue';
import upImg from "@/static/cb_up.png"
import downImg from "@/static/cb_up.png"
@@ -133,7 +134,21 @@
import navBar from "@/components/navBar/navBar.vue"
import formatNumber from '@/utils/formatNumber.js';
const type=ref(1)
onBackPress(() => {
plus.runtime.quit();
return true;
});
onLoad(() => {
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "患教文库",
},
},
(ret) => {
console.log(ret);
});
})
// 分页相关状态
const currentPage = ref(1);
const pageSize = ref(10);
@@ -150,7 +165,7 @@
currentPage.value = 1;
hasMore.value = true;
articleList.value = [];
polularScienceArticleListByKeywordsNew(true);
polularScienceArticleListByKeywordsNew(false);
};
const loadGuideTags = async () => {
try {
@@ -198,9 +213,9 @@
// 判断是否还有更多数据
hasMore.value = newData.length >= pageSize.value;
if (!isRefresh) {
currentPage.value++;
}
// if (!isRefresh) {
// currentPage.value++;
// }
console.log('文章数据加载成功:', articleList.value);
}
@@ -267,7 +282,7 @@
if(index==1){
navTo({
url: '/pages_app/patientVideo/patientVideo'
url: '/pages_app/patientVideo/patientVideo?from=patientClass'
})
}else if(index==2){
let url=encodeURIComponent('https://wx.igandan.com/wxPatient/index.htm#/problem?link=share&fromtype=doctor')
@@ -282,15 +297,15 @@
// 跳转到详情页
if (article.path) {
uni.navigateTo({
url: `/pages_app/webview/webview?url=${encodeURIComponent(docUrl+article.path)}&title=患教详情`
url: `/pages_app/webview/webviewClass?url=${encodeURIComponent(docUrl+article.path+'?fromtype=doctor')}&title=患教详情&uuid=${article.uuid}&sharetitle=${article.title}&summary=${article.summary}&type=huanjiao&imgPath=${encodeURIComponent(article.image)}&tags=${article.tags}`
});
}
};
// 跳转到搜索页面
const goToSearch = () => {
uni.navigateTo({
url: '/pages_app/search/search'
uni.sendNativeEvent('goHomeSearch',{
msg: 'patientClass'
});
};
@@ -308,6 +323,7 @@
// 上拉加载更多
const onLoadMore = async () => {
if (!hasMore.value || loading.value) return;
currentPage.value++
await polularScienceArticleListByKeywordsNew(false);
};
@@ -357,25 +373,26 @@
// 检查是否有选中的标签
const hasSelected = filterTags.value.some(tag => tag.selected);
isFilterActive.value = hasSelected;
let words='';
// 执行筛选逻辑
if (hasSelected) {
const selectedTags = filterTags.value.filter(tag => tag.selected).map(tag => tag.NAME);
console.log('选中的标签:', selectedTags);
keywords.value = selectedTags.join(',');
words = selectedTags.join(',');
} else {
keywords.value = '';
words = '';
}
keywords.value = words;
// 重置分页状态
currentPage.value = 1;
hasMore.value = true;
articleList.value = [];
showFilter.value = false;
polularScienceArticleListByKeywordsNew(true);
polularScienceArticleListByKeywordsNew(false);
};
onShow(() => {
polularScienceArticleListByKeywordsNew(true);
loadGuideTags();
});
@@ -496,23 +513,24 @@ $padding-small: 20rpx;
// 文章列表
.article-list {
position: fixed;
height:calc(100vh - var(--status-bar-height) - 44px - 188rpx);
// height:calc(100vh - var(--status-bar-height) - 44px - 207rpx);
top: calc(var(--status-bar-height) + 44px + 188rpx);
padding-bottom: 130rpx;
bottom: 128rpx;
.article-item {
background-color: $white;
margin-bottom: $padding-small;
border-radius: $border-radius;
padding: $padding;
display: flex;
gap: 24rpx;
.article-image {
width: 260rpx;
height: 160rpx;
border-radius: $border-radius-small;
flex-shrink: 0;
margin-right: 24rpx;
}
.article-content {
@@ -567,6 +585,9 @@ $padding-small: 20rpx;
font-size: 24rpx;
color: $gray-medium;
text{
white-space: nowrap;
}
}
}
}
@@ -665,16 +686,15 @@ $padding-small: 20rpx;
.filter-tags {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 60rpx;
max-height: 65vh;
overflow-y: auto;
.tag-item {
background-color: #f8f8f8;
color: $gray-dark;
padding: 8rpx 0rpx;
width:150rpx;
width:152rpx;
margin:8rpx;
text-align:center;
border-radius: 30rpx;
font-size: 26rpx;
+299
View File
@@ -0,0 +1,299 @@
<template>
<view class="wrap">
<view :class="isFull ? 'player-full' : 'player'">
<plv-player
ref="vodPlayer"
class="vod-player"
seekType=1
autoPlay=true
disableScreenCAP=false
rememberLastPosition=false
@onPlayStatus="onPlayStatus"
@onPlayError="onPlayError"
@positionChange="positionChange"
@onEnableSubtitle="onEnableSubtitle"
@onSRTTextConfig="onSRTTextConfig"
@onSRTTitle="onSRTTitle">
</plv-player>
<skin ref="skinRef"
class="skin-control"
:defaultVolume="defaultVolume"
@onPlayBtnClick="onPlayBtnClick"
@onToSeek="onToSeek"
@onFullBtnClick="onFullBtnClick"
@onHdBtnClick="onHdBtnClick"
@onRateBtnClick="onRateBtnClick"
@onVolumeChanged="onVolumeChanged"
@onScalingBtnClick="onScalingBtnClick"
@onScreenShot="onScreenShot"
@onChangeSRTBtnClick="onChangeSRTBtnClick"
@onChangeSRTSingleMode="onChangeSRTSingleMode"
@onOpenSRT="onOpenSRT">
</skin>
</view>
<view v-if="!isFull" style="padding: 20rpx;">
<input class="uni-input" style="min-height: 35px; height: 35px; border: 1px solid #cccccc;font-size: 14px;" :value="videoVid"
@input="onVidInput" placeholder="请输入视频vid" />
<button type="primary" @click="setVid()">播放</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
//import Head from '../../components/page-head.nvue';
//import skin from '@/components/plv-player-skin/skin.nvue';
//import DragTips from '@/components/plv-player-skin/drag-tips.nvue';
const dom = weex.requireModule('dom');
var configModule = uni.requireNativePlugin("PLV-VodUniPlugin-ConfigModule")
const vod=ref(null);
const title='播放器Demo';
// 播放器组件
const vodPlayer=ref(null);
const skinRef=ref(null);
const isFull=ref(false);
const defaultVolume=ref(0.5);
const videoVid=ref('4ae3fe3ab5a2009a8d6605d81b2e3c9b_4');
onShow(() => {
configModule.setConfig({
'config': "dDzXiPJ8psA/7LvtlPgpNfdOjzKVVEYvca1QBOp2MqCVKC93YiligqqfzOzETkDmolLNBUF4vAykBuUnn+U4V9NoK9vaofwESluIMO9WSB0eWhrw+fBkn+h+OZAmwmYgHQ6fCtsggZKN6cW/FhjWAQ==", //加密串配置
},
(ret) => {
if (ret.isSuccess == true) {
uni.showToast({
title: '设置加密串成功',
icon: "none"
})
}
})
})
const onVidInput=(event)=> {
videoVid.value = event.detail.value;
};
const setVid=()=> {
if(!videoVid.value) {
uni.showToast({
title: "请输入视频Vid",
icon: "none"
})
return;
}
vodPlayer.value.setVid({
vid:videoVid.value,
level:0
}, (ret) => {
console.log(12)
console.log(ret)
text.value = JSON.stringify(ret);
if (ret.errMsg != null) {
uni.showToast({
title: ret.errMsg,
icon: "none"
})
}
});
};
const onPlayError=(e)=>{
console.log(e);
if (e.detail.errEvent != null) {
uni.showToast({
title:'playErrorEvent - '+e.detail.errEvent,
icon: "none"
})
}
};
const positionChange=(e)=>{
skinRef.value.timeUpdate(e.detail.currentPosition);
};
const onEnableSubtitle=(e)=>{
console.log("===1" + e.detail);
skinRef.value.setEnableSubtitle(e.detail.isEnableSubtitle, e.detail.isEnableDoubleSubtitle);
};
const onSRTTextConfig=(e)=>{
console.log("===1 fontColor: " + e.detail.fontColor);
skinRef.value.setSRTTextConfig(e.detail);
};
const onSRTTitle=(e)=>{
console.log("===1 onSRTTitle" + e.detail);
};
const onPlayStatus=(e)=>{
//const { skin, vodPlayer } = this;
const state = e.detail.playbackState;
const preparedToPlay = e.detail.preparedToPlay;
if (state != null) {
skinRef.value.changePlayStatus(state === 'start');
} else if (preparedToPlay != null) {
updateDuration();
updateLevels();
updateScaling();
}
};
const updateDuration=()=> {
if (vodPlayer.value) return;
vodPlayer.value.getDuration(null, ret => {
skinRef.value.updateDuration(ret.duration);
});
};
const updateLevels=()=> {
if (vodPlayer.value) return;
vodPlayer.value.getLevelNum(null, ret => {
skinRef.value.updateLevels(ret.levelNum);
});
vodPlayer.value.getCurrentLevel(null, ret => {
skinRef.value.updateCurrentLevel(ret.currentLevel);
});
};
const updateScaling=()=> {
if (vodPlayer.value) return;
vodPlayer.value.getScalingMode({}, ret => {
skinRef.value.updateScaling(ret.scalingMode);
});
};
const onPlayBtnClick=(isPlaying)=> {
if (vodPlayer.value) return;
isPlaying ? vodPlayer.value.start() : vodPlayer.value.pause();
};
const onHdBtnClick=(level)=> {
if (vodPlayer.value) return;
vodPlayer.value.changeLevel({level});
};
const onRateBtnClick=(speed)=> {
if (vodPlayer.value) return;
vodPlayer.value.setSpeed({speed});
};
const onScalingBtnClick=(scalingMode)=> {
if (vodPlayer.value) return;
vodPlayer.value.setScalingMode({scalingMode});
};
const onChangeSRTBtnClick=(srt)=> {
console.log("===1 " + srt);
if (vodPlayer.value) return;
vodPlayer.value.changeSRT(srt);
};
const onChangeSRTSingleMode=(isSingle)=> {
if (vodPlayer.value) return;
vodPlayer.value.changeSRTSingleMode(isSingle);
};
const onOpenSRT=(isOpen)=> {
if (vodPlayer.value) return;
vodPlayer.value.setOpenSRT({
openSrt: isOpen
});
};
const onFullBtnClick=(isFull)=> {
// plus.screen.unlockOrientation();
plus.navigator.setFullscreen(isFull);
if (vodPlayer.value) return;
isFull ? vodPlayer.value.changeToLandscape() : vodPlayer.value.changeToPortrait();
isFull.value = isFull;
};
const onToSeek=(time)=> {
if (vodPlayer.value) return;
vodPlayer.value.seekTo({seconds: time});
};
const onVolumeChanged=(value)=> {
if (vodPlayer.value) return;
vodPlayer.value.getVolume(null, ret => {
const changedValue = ret.volume + value;
const realValue =limit(changedValue, 0, 1);
vodPlayer.value.setVolume({volume: realValue});
skinRef.value.updateVolumeValue(realValue);
});
};
const onScreenShot=()=> {
if (vodPlayer.value) return;
vodPlayer.value.snapshot(null, result =>{
if(result.errMsg != null){
console.log(result.errMsg)
}
});
};
/**
* 控制最小值,最大值
* @param num
* @param min
* @param max
* @returns {*}
*/
const limit=(num, min, max)=> {
if (num < min) return min;
if (num > max) return max;
return num;
}
</script>
<style>
.wrap {
flex: 1;
}
.title {
height: 140rpx;
}
.player {
height: 400rpx;
}
.player-full {
flex: 1;
}
.vod-player {
flex: 1;
}
.skin-control {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.hide {
height: 0;
}
</style>
+337
View File
@@ -0,0 +1,337 @@
<template>
<view class="wrap">
<view :class="isFull ? 'player-full' : 'player'">
<plv-player
ref="vod"
class="vod-player"
seekType=1
autoPlay=true
disableScreenCAP=false
rememberLastPosition=false
@onPlayStatus="onPlayStatus"
@onPlayError="onPlayError"
@positionChange="positionChange"
@onEnableSubtitle="onEnableSubtitle"
@onSRTTextConfig="onSRTTextConfig"
@onSRTTitle="onSRTTitle">
</plv-player>
<Skin ref="skin"
class="skin-control"
:defaultVolume="defaultVolume"
@onPlayBtnClick="onPlayBtnClick"
@onToSeek="onToSeek"
@onFullBtnClick="onFullBtnClick"
@onHdBtnClick="onHdBtnClick"
@onRateBtnClick="onRateBtnClick"
@onVolumeChanged="onVolumeChanged"
@onScalingBtnClick="onScalingBtnClick"
@onScreenShot="onScreenShot"
@onChangeSRTBtnClick="onChangeSRTBtnClick"
@onChangeSRTSingleMode="onChangeSRTSingleMode"
@onOpenSRT="onOpenSRT">
</Skin>
</view>
<view v-if="!isFull" style="padding: 20rpx;">
<input class="uni-input" style="min-height: 35px; height: 35px; border: 1px solid #cccccc;font-size: 14px;" :value="videoVid"
@input="onVidInput" placeholder="请输入视频vid" />
<button type="primary" @click="setVid()">播放</button>
</view>
</view>
</template>
<script>
//import Head from '../../components/page-head.nvue';
import Skin from '@/components/plv-player-skin2/skin.nvue';
//import DragTips from '@/components/plv-player-skin/drag-tips.nvue';
const dom = weex.requireModule('dom');
var configModule = uni.requireNativePlugin("PLV-VodUniPlugin-ConfigModule")
export default {
components: {
Skin,
//Head
},
onReady() {
configModule.setConfig({
'config': "dDzXiPJ8psA/7LvtlPgpNfdOjzKVVEYvca1QBOp2MqCVKC93YiligqqfzOzETkDmolLNBUF4vAykBuUnn+U4V9NoK9vaofwESluIMO9WSB0eWhrw+fBkn+h+OZAmwmYgHQ6fCtsggZKN6cW/FhjWAQ==", //加密串配置
},
(ret) => {
if (ret.isSuccess == true) {
uni.showToast({
title: '设置加密串成功',
icon: "none"
})
} else {
let errMsg = ret.errMsg;
uni.showToast({
title: '设置加密串失败:' + errMsg,
icon: "none"
})
}
})
this.vodPlayer = this.$refs.vod;
this.skin = this.$refs.skin;
// plus.screen.lockOrientation('portrait-primary');
},
data() {
return {
title: '播放器Demo',
// 播放器组件
vodPlayer: null,
// 控制栏组件
skin: null,
//是否全屏
isFull: false,
// 默认音量
defaultVolume: 0.5,
videoVid: '4ae3fe3ab5a2009a8d6605d81b2e3c9b_4'
};
},
methods: {
//输入vid
onVidInput: function(event) {
this.videoVid = event.detail.value;
},
setVid() {
if(!this.videoVid) {
uni.showToast({
title: "请输入视频Vid",
icon: "none"
})
return;
}
const { vodPlayer } = this;
vodPlayer.setVid({
vid:this.videoVid,
level:0
}, (ret) => {
console.log(12)
console.log(ret)
this.text = JSON.stringify(ret);
if (ret.errMsg != null) {
uni.showToast({
title: ret.errMsg,
icon: "none"
})
}
});
},
onPlayError(e){
console.log(e);
if (e.detail.errEvent != null) {
uni.showToast({
title:'playErrorEvent - '+e.detail.errEvent,
icon: "none"
})
}
},
positionChange(e){
this.skin.timeUpdate(e.detail.currentPosition);
},
onEnableSubtitle(e){
console.log("===1" + e.detail);
this.skin.setEnableSubtitle(e.detail.isEnableSubtitle, e.detail.isEnableDoubleSubtitle);
},
onSRTTextConfig(e) {
console.log("===1 fontColor: " + e.detail.fontColor);
this.skit.setSRTTextConfig(e.detail);
},
onSRTTitle(e) {
console.log("===1 onSRTTitle" + e.detail);
},
onPlayStatus(e){
const { skin, vodPlayer } = this;
const state = e.detail.playbackState;
const preparedToPlay = e.detail.preparedToPlay;
if (state != null) {
this.skin.changePlayStatus(state === 'start');
} else if (preparedToPlay != null) {
this.updateDuration();
this.updateLevels();
this.updateScaling();
}
},
updateDuration() {
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.getDuration(null, ret => {
this.skin.updateDuration(ret.duration);
});
},
updateLevels() {
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.getLevelNum(null, ret => {
this.skin.updateLevels(ret.levelNum);
});
vodPlayer.getCurrentLevel(null, ret => {
this.skin.updateCurrentLevel(ret.currentLevel);
});
},
updateScaling() {
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.getScalingMode({}, ret => {
this.skin.updateScaling(ret.scalingMode);
});
},
onPlayBtnClick(isPlaying) {
const { vodPlayer } = this;
if (!vodPlayer) return;
isPlaying ? vodPlayer.start() : vodPlayer.pause();
},
onHdBtnClick(level) {
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.changeLevel({level});
},
onRateBtnClick(speed) {
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.setSpeed({speed});
},
onScalingBtnClick(scalingMode) {
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.setScalingMode({scalingMode});
},
onChangeSRTBtnClick(srt) {
console.log("===1 " + srt);
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.changeSRT(srt);
},
onChangeSRTSingleMode(isSingle) {
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.changeSRTSingleMode(isSingle);
},
onOpenSRT(isOpen) {
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.setOpenSRT({
openSrt: isOpen
});
},
onFullBtnClick(isFull) {
// plus.screen.unlockOrientation();
plus.navigator.setFullscreen(isFull);
//通过h5+实现横竖屏
// isFull ? plus.screen.lockOrientation('landscape-primary') : plus.screen.lockOrientation('portrait-primary');
//通过原生实现横竖屏切换
const { vodPlayer } = this;
if (!vodPlayer) return;
isFull ? vodPlayer.changeToLandscape() : vodPlayer.changeToPortrait();
this.isFull = isFull;
},
onToSeek(time) {
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.seekTo({seconds: time});
},
onVolumeChanged(value) {
const { vodPlayer, skin } = this;
if (!vodPlayer) return;
vodPlayer.getVolume(null, ret => {
const changedValue = ret.volume + value;
const realValue = this.limit(changedValue, 0, 1);
vodPlayer.setVolume({volume: realValue});
skin.updateVolumeValue(realValue);
});
},
onScreenShot() {
const { vodPlayer } = this;
if (!vodPlayer) return;
vodPlayer.snapshot(null, result =>{
if(result.errMsg != null){
console.log(result.errMsg)
}
});
},
/**
* 控制最小值,最大值
* @param num
* @param min
* @param max
* @returns {*}
*/
limit(num, min, max) {
if (num < min) return min;
if (num > max) return max;
return num;
}
}
}
</script>
<style>
.wrap {
flex: 1;
}
.title {
height: 140rpx;
}
.player {
height: 400rpx;
}
.player-full {
flex: 1;
}
.vod-player {
flex: 1;
}
.skin-control {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.hide {
height: 0;
}
</style>
@@ -0,0 +1,843 @@
<template>
<view class="zhinan-list-page">
<!-- 头部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="文献期刊"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eeeeee"
>
</uni-nav-bar>
</view>
<!-- 使用scroll-view实现列表 -->
<scroll-view
class="guidelines-scroll-view"
scroll-y="true"
>
<!-- 指南列表 -->
<view class="guidelines-list">
<view
class="guideline-item"
v-for="(item, index) in downLoadtaskList"
:key="item.id || index"
>
<!-- 指南信息 -->
<view class="item-content">
<view class="item-title">{{ item.title }}</view>
<view class="item-bottom">
<view class="item-date">{{ formatDate(item.releaseTime) }}</view>
<!-- 操作按钮 -->
<view class="item-action">
<view
class="view-btn"
@click.stop="viewGuideline(item)"
>查看</view>
</view>
</view>
</view>
</view>
</view>
<!-- 无数据提示 -->
<view class="no-data" v-if="downLoadtaskList.length === 0 ">
<uni-icons type="info" size="60" color="#999"></uni-icons>
<text>暂无数据</text>
</view>
</scroll-view>
</view>
<!-- 组内排序弹窗 -->
<view v-if="showInnerSort" class="popup-panel">
<view
class="popup-item"
:class="{ active: sort == 1 }"
@click.stop="chooseInnerSort(1)"
>
<text class="item-text">下载量</text>
<uni-icons
v-if="sort == 1"
type="checkmarkempty"
color="#8B2316"
size="22"
></uni-icons>
</view>
<view class="popup-divider"></view>
<view
class="popup-item"
:class="{ active: sort == 2 }"
@click.stop="chooseInnerSort(2)"
>
<text class="item-text">上传时间</text>
<uni-icons
v-if="sort == 2"
type="checkmarkempty"
color="#8B2316"
size="22"
></uni-icons>
</view>
<view class="popup-divider"></view>
<view
class="popup-item"
:class="{ active: sort == 3 }"
@click.stop="chooseInnerSort(3)"
>
<text class="item-text">标题</text>
<uni-icons
v-if="sort == 3"
type="checkmarkempty"
color="#8B2316"
size="22"
></uni-icons>
</view>
</view>
</template>
<script setup>
import { ref, onMounted,computed,nextTick} from "vue";
import api from "@/api/api.js";
import { onShow, onLoad } from "@dcloudio/uni-app";
import docUrl from "@/utils/docUrl.js";
import navTo from "@/utils/navTo.js";
import hotImg from "@/static/hot_booklist.png";
const guidelinesList = ref([]);
const isLoading = ref(false);
const hasMoreData = ref(true);
const currentPage = ref(1);
const pageSize = ref(10);
const isRefreshing = ref(false);
const keywords = ref("");
import upImg from "@/static/triangle_green_theme.png";
import downImg from "@/static/triangle_normal.png";
const loadMoreStatus = ref("more"); // 'loading', 'more', 'noMore'
const typeUuid = ref("");
const title = ref("");
const sort = ref(2);
const showInnerSort = ref(false);
const innerSortTitle = ref("上传时间");
import downloadStore from "@/store/downloadStoreFile.js";
const downloadTasks=ref([]);
const downLoadtaskList=computed(()=>{
return downloadTasks.value.filter((item)=>item.status == "completed" && item.type == "article");
});
const chooseInnerSort = (index) => {
sort.value = index;
if (index == 1) {
innerSortTitle.value = "下载量";
} else if (index == 2) {
innerSortTitle.value = "上传时间";
} else if (index == 3) {
innerSortTitle.value = "标题";
}
showInnerSort.value = false;
loadGuidelinesList(true);
};
onLoad((options) => {
});
const goGot = () => {
navTo({
url: "/pages_app/hot/hot?from=hot&typeUuid=" + typeUuid.value,
});
};
const toggleInnerSort = () => {
showInnerSort.value = !showInnerSort.value;
};
onShow(() => {
syncTasksFromStore();
// 监听store变化
downloadStore.addListener((tasks) => {
downloadTasks.value = tasks;
});
// 恢复正在下载的任务
resumeDownloadingTasks();
// 页面显示时,如果列表为空则加载数据
});
// 跳转到搜索页
const goToSearch = () => {
const q = keywords.value ? encodeURIComponent(keywords.value) : "";
const t = title.value ? encodeURIComponent(title.value) : "";
const type = typeUuid.value
? `&typeUuid=${encodeURIComponent(typeUuid.value)}`
: "";
uni.navigateTo({
url: `/pages_app/search/search?keywords=${q}&title=${t}${type}`,
});
// const goSearch=()=>{
// uni.sendNativeEvent('goHomeSearch', {
// msg: 'goHomeSearch'
// },ret => {
// console.log(ret);
// })
// }
};
// scroll-view 下拉刷新
const onRefresh = async () => {
console.log("scroll-view 下拉刷新触发");
isRefreshing.value = true;
await refreshData();
isRefreshing.value = false;
};
// scroll-view 滚动到底部
const onScrollToLower = () => {
console.log("=== onScrollToLower 触发 ===");
console.log("当前状态:", {
loadMoreStatus: loadMoreStatus.value,
isRefreshing: isRefreshing.value,
currentPage: currentPage.value,
listLength: guidelinesList.value.length,
hasMoreData: hasMoreData.value,
isLoading: isLoading.value,
});
if (
loadMoreStatus.value === "more" &&
!isRefreshing.value &&
!isLoading.value
) {
console.log("条件满足,开始加载更多数据");
loadMoreData();
} else {
console.log("条件不满足,跳过加载:", {
loadMoreStatus: loadMoreStatus.value,
isRefreshing: isRefreshing.value,
isLoading: isLoading.value,
});
}
};
// 刷新数据
const refreshData = async () => {
try {
currentPage.value = 1;
hasMoreData.value = true;
loadMoreStatus.value = "more";
await loadGuidelinesList(true);
uni.showToast({
title: "刷新成功",
icon: "none",
});
} catch (error) {
console.error("刷新失败:", error);
uni.showToast({
title: "刷新失败",
icon: "none",
});
}
};
// 加载指南列表
const loadGuidelinesList = async (isRefresh = false) => {
console.log("=== loadGuidelinesList 开始 ===");
console.log("当前参数:", {
isRefresh,
page: currentPage.value,
keywords: keywords.value,
});
if (isLoading.value) return;
try {
isLoading.value = true;
loadMoreStatus.value = "loading";
const params = {
page: currentPage.value,
pageSize: pageSize.value,
type: 1, // 诊疗指南类型
sort: 2,
typeUuid: typeUuid.value,
keywords: keywords.value,
};
const res = await api.searchLibraryU(params);
console.log("指南列表响应:", res);
if (res.code === 200 && res.data) {
const newData = res.data.list || res.data;
if (isRefresh) {
guidelinesList.value = newData;
console.log("刷新模式:替换列表数据");
} else {
guidelinesList.value = [...guidelinesList.value, ...newData];
console.log("加载更多:追加数据到列表");
}
// 判断是否还有更多数据
if (newData.length < pageSize.value) {
loadMoreStatus.value = "noMore";
hasMoreData.value = false;
console.log("没有更多数据了");
} else {
loadMoreStatus.value = "more";
hasMoreData.value = true;
// 只有在加载更多模式下才递增页码
if (!isRefresh) {
currentPage.value++;
console.log(`还有更多数据,下一页: ${currentPage.value}`);
}
}
} else {
console.error("加载指南列表失败:", res.message);
loadMoreStatus.value = "noMore";
uni.showToast({
title: res.message || "加载失败",
icon: "none",
});
}
} catch (error) {
console.error("加载指南列表异常:", error);
loadMoreStatus.value = "noMore";
uni.showToast({
title: "网络异常,请重试",
icon: "none",
});
} finally {
isLoading.value = false;
console.log("=== loadGuidelinesList 结束 ===");
console.log("最终状态:", {
loadMoreStatus: loadMoreStatus.value,
page: currentPage.value,
listLength: guidelinesList.value.length,
});
}
};
// 加载更多数据
const loadMoreData = () => {
console.log("=== loadMoreData 被调用 ===");
console.log("检查条件:", {
hasMoreData: hasMoreData.value,
isLoading: isLoading.value,
currentPage: currentPage.value,
});
if (hasMoreData.value && !isLoading.value) {
console.log("条件满足,开始加载更多数据");
loadGuidelinesList(false);
} else {
console.log("条件不满足,跳过加载更多");
}
};
// 下载指南
const downloadGuideline = (item) => {
console.log("下载指南:", item);
uni.showToast({
title: "开始下载...",
icon: "none",
});
// 这里可以调用下载API
// 示例:下载PDF文件
if (item.file_path) {
uni.downloadFile({
url: item.file_path,
success: (res) => {
if (res.statusCode === 200) {
uni.showToast({
title: "下载成功",
icon: "none",
});
}
},
fail: (err) => {
console.error("下载失败:", err);
uni.showToast({
title: "下载失败1",
icon: "none",
});
},
});
}
};
const downLoad = (item) => {
if(!item.path){
uni.showToast({
title: "暂无下载链接",
icon: "none",
});
return;
}
addDownloadTask({
url: docUrl + item.path,
title: item.title,
id: item.uuid,
});
};
// 查看指南(支持常用文档直链:pdf/doc/docx/xls/xlsx/ppt/pptx/txt)
const viewGuideline = (cell) => {
console.log(downloadTasks.value);
downloadTasks.value.forEach(item => {
if(item.id == cell.id) {
console.log(2222222222);
console.log(item.localPath);
plus.runtime.openFile(item.localPath, function(e) {
console.log('打开成功');
}, function(e) {
uni.showToast({
title: '打开失败:' + e.message,
icon: "none",
duration: 2000,
});
console.log('打开失败:' + e.message);
});
// uni.openDocument({
// filePath: item.localPath,
// fileType: 'pdf',
// success: () => console.log('打开成功'),
// fail: (err) => {
// uni.showModal({
// title: '提示',
// content: JSON.stringify(err),
// showCancel: false,
// confirmText: '确定',
// success: (res) => {
// if(res.confirm){
// }
// }
// });
// }
// });
// let url = "https://view.xdocin.com/view?src=" + encodeURIComponent(pdfUrl);
// uni.downloadFile({
// url: pdfUrl,
// success: (res) => {
// if (res.statusCode === 200) {
// uni.openDocument({
// filePath: res.tempFilePath, // 临时文件路径
// fileType: 'pdf',
// success: () => console.log('打开成功'),
// fail: (err) => console.error('打开失败', err)
// });
// }
// },
// });
}
});
}
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 添加下载任务
const addDownloadTask = (item) => {
// 使用store添加任务
const taskIndex = downloadStore.addTask({
...item,
type: 'zhinan',
});
// 同步任务列表
syncTasksFromStore();
// 开始下载
startDownload(taskIndex);
};
// 开始下载
const startDownload = (index) => {
const taskItem = downloadStore.getTask(index);
if (!taskItem) return;
if(taskItem.status == "loading"){
return false;
};
uni.showToast({
title: "正在下载中",
icon: "none",
});
downloadStore.updateTask(index, {
status: "loading"
});
const task = uni.downloadFile({
url: taskItem.url,
success: (res1) => {
console.log("res1:"+JSON.stringify(res1));
if (res1.statusCode === 200) {
downloadStore.updateTask(index, {
status: "completed",
filePath: res1.tempFilePath,
});
uni.showToast({
title: "下载成功",
icon: "none",
});
uni.saveFile({
tempFilePath: res1.tempFilePath,
success: function (res2) {
console.log("res2:"+JSON.stringify(res2));
uni.getSavedFileInfo({
filePath: res2.savedFilePath,
success: function (res) {
console.log("res:"+JSON.stringify(res));
console.log("size:"+res.size);
downloadStore.updateTask(index, {
status: "completed",
localPath: res2.savedFilePath,
});
},
});
},
});
} else {
downloadStore.updateTask(index, {
status: "failed",
});
removeTask(index);
uni.showToast({
title: "下载失败2",
icon: "none",
});
}
},
fail: (err) => {
downloadStore.updateTask(index, {
status: "failed",
});
removeTask(index);
uni.showToast({
title: "下载失败3: " + (err.errMsg || "未知错误"),
icon: "none",
duration: 2000,
});
},
});
// 监听下载进度
task.onProgressUpdate((update) => {
const currentTask = downloadStore.getTask(index);
if (currentTask && currentTask.status === "downloading") {
downloadStore.updateTask(index, {
progress: update.progress
});
}
});
// 保存task对象到store(用于暂停/继续)
downloadStore.updateTask(index, {
task: task,
});
};
const syncTasksFromStore = () => {
downloadTasks.value = downloadStore.getTasks();
};
const removeTask = (index) => {
downloadStore.removeTask(index);
syncTasksFromStore();
};
// 恢复正在下载的任务
const resumeDownloadingTasks = () => {
// 使用 nextTick 确保在页面渲染后再恢复下载任务
nextTick(() => {
downloadStore.resumeDownloadingTasks((index) => {
// 重新开始下载(uni.downloadFile不支持断点续传,所以从0开始)
console.log("恢复下载任务:", downloadStore.getTask(index)?.url);
startDownload(index);
});
});
};
// 测试加载更多功能
const testLoadMore = () => {
console.log("=== 手动测试加载更多 ===");
console.log("当前状态:", {
loadMoreStatus: loadMoreStatus.value,
hasMoreData: hasMoreData.value,
isLoading: isLoading.value,
currentPage: currentPage.value,
listLength: guidelinesList.value.length,
});
if (loadMoreStatus.value === "more" && !isLoading.value) {
loadMoreData();
} else {
uni.showToast({
title: `状态: ${loadMoreStatus.value}, 加载中: ${isLoading.value}`,
icon: "none",
duration: 2000,
});
}
};
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return "";
try {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
} catch (error) {
return dateString;
}
};
</script>
<style lang="scss" scoped>
// 颜色变量
$primary-color: #ff4757;
$text-primary: #333;
$text-secondary: #666;
$text-light: #999;
$border-color: #e0e0e0;
$bg-color: #f6f6f6;
$white: #ffffff;
.imgbox {
width: 32rpx;
margin-top: -10rpx;
}
.popup-panel {
position: fixed;
top:285rpx;
left: 0;
right: 0;
background: #fff;
z-index: 100;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
}
.popup-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 30rpx;
font-size: 30rpx;
color: #333;
}
.popup-divider {
height: 2rpx;
background: #eaeaea;
}
.popup-item.active .item-text {
color: #8b2316;
}
.current-sort {
display: flex;
align-items: center;
gap: 10rpx;
.sort-text {
font-size: 28rpx;
color: #333;
}
.sort-icon.up {
width: 0;
height: 0;
border-left: 8rpx solid transparent;
border-right: 8rpx solid transparent;
border-bottom: 12rpx solid #999;
&.active {
border-bottom-color: #ff0000;
}
}
}
.zhinan-list-page {
background-color: $bg-color;
height: 100vh;
}
.guidelines-scroll-view {
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
bottom: 0;
z-index: 99;
height: calc(100vh - var(--status-bar-height) - 44px);
background-color: $bg-color;
}
// 固定搜索容器
.search-container-fixed {
position: fixed;
top: calc(var(--status-bar-height) + 44px); // 导航栏高度
left: 0;
right: 0;
z-index: 90;
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 30rpx;
background-color: $white;
gap: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
// 筛选栏
.filter-bar {
background-color: $white;
display: flex;
align-items: center;
border-bottom: 2px solid #eee;
.search-box {
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
.search-text {
font-size: 14px;
color: #999;
}
}
.divider {
width: 1px;
height: 16px;
background-color: #999;
margin: 0 60rpx;
}
.filter-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
font-size: 14px;
color: #999;
}
}
// 指南列表
.guidelines-list {
padding: 20rpx 30rpx;
.guideline-item {
background-color: $white;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
.item-content {
flex: 1;
margin-right: 30rpx;
.item-title {
font-size: 32rpx;
color: $text-primary;
line-height: 1.5;
margin-bottom: 16rpx;
font-weight: 500;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
.item-bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 10rpx;
.item-date {
font-size: 26rpx;
color: $text-secondary;
}
.item-action {
display: flex;
align-items: center;
.download-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(255, 71, 87, 0.1);
// transition: all 0.3s ease;
// &:active {
// transform: scale(0.95);
// background-color: rgba(255, 71, 87, 0.2);
// }
}
.view-btn {
color: #8b2316;
font-size: 28rpx;
font-weight: 500;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
background-color: darken($primary-color, 10%);
}
}
}
}
}
}
}
// 加载更多状态
.load-more-status {
padding: 20rpx 0;
text-align: center;
.loading,
.more,
.no-more {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
padding: 20rpx 0;
font-size: 26rpx;
color: $text-light;
}
}
// 无数据提示
.no-data {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 0;
text {
margin-top: 30rpx;
font-size: 28rpx;
color: $text-light;
}
}
</style>
+72 -21
View File
@@ -1,5 +1,5 @@
<template>
<uni-nav-bar
<!-- <uni-nav-bar
left-icon="left"
title="购买积分"
@clickLeft="goBack"
@@ -8,7 +8,8 @@
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
></uni-nav-bar> -->
<navBar :title="'购买积分'" ></navBar>
<view class="buy-point-page">
@@ -46,20 +47,80 @@
</view>
</view>
</view>
<unidialog
:visible="freeVisible"
:content="freeContent"
@close="freeClose"
@confirm="freeConfirm"
></unidialog>
</template>
<script setup>
import { ref, computed } from 'vue';
import api from '@/api/api';
import { onLoad } from '@dcloudio/uni-app';
import unidialog from '@/components/dialog/dialog.vue';
import navBar from '@/components/navBar/navBar.vue';
const freeVisible = ref(false);
const freeContent = ref('您确定是否购买?');
const product_id = ref('');
const type = ref('');
const freeClose = () => {
freeVisible.value = false;
};
const freeConfirm = () => {
freeVisible.value = false;
api.createBointsOrder({
point: selectedPackageData.value.points
}).then(res => {
if(res.code==200){
console.log('price:'+res.data.amount);
console.log('order_id:'+res.data.order_id);
console.log('trade_no:'+res.data.trade_no);
console.log('jifen:'+res.data.point);
uni.sendNativeEvent('payPonitPage', {
msg: {
"price":res.data.amount,
"order_id":res.data.order_id,
"trade_no":res.data.trade_no,
'jifen':res.data.point,
'product_id':product_id.value,
'type':type.value,
},
});
}
});
};
onLoad((options) => {
if(options.product_id){
product_id.value = options.product_id;
}
if(options.type){
type.value = options.type;
}
getPointUnitPrice();
});
const pointUnitPrice = ref(1);
// 积分套餐数据 - 根据图片显示5个选项
const packages = ref([
{ id: 1, points: 500, price: '45.00' },
{ id: 2, points: 1000, price: '88.00' },
{ id: 3, points: 2500, price: '198.00' },
{ id: 4, points: 5000, price: '398.00' },
{ id: 5, points: 10000, price: '758.00' }
{ id: 1, points: 500, price: 500*pointUnitPrice.value/100 },
{ id: 2, points: 1000, price: 1000*pointUnitPrice.value/100 },
{ id: 3, points: 2500, price: 2500*pointUnitPrice.value/100 },
{ id: 4, points: 5000, price: 5000*pointUnitPrice.value/100 },
{ id: 5, points: 10000, price: 10000*pointUnitPrice.value/100 }
]);
const getPointUnitPrice = () => {
api.getPointUnitPrice().then(res => {
if(res.code==200){
pointUnitPrice.value=Number(res.data);
console.log(pointUnitPrice.value);
packages.value.forEach(pkg => {
pkg.price = pkg.points * pointUnitPrice.value / 100;
});
}
});
};
// 选择状态
const selectedPackage = ref(0); // 默认未选择
@@ -96,18 +157,7 @@
}
const packageData = selectedPackageData.value;
uni.showModal({
title: '确认购买',
content: `确认购买${packageData.points}积分,支付金额¥${packageData.price}?`,
success: (res) => {
if (res.confirm) {
uni.showToast({
title: '购买成功',
icon: 'none'
});
}
}
});
freeVisible.value=true;
};
</script>
@@ -138,7 +188,7 @@
}
.buy-point-page {
height: calc(100vh - 180rpx);
height: 100vh;
background-color: $bg-color;
overflow: hidden; // 隐藏滚动条
@@ -224,6 +274,7 @@
}
.main-content {
margin-top: calc(var(--status-bar-height) + 44px);
padding: 40rpx 0rpx;
.page-title {
+323
View File
@@ -0,0 +1,323 @@
<template>
<view class="address-page">
<navBar :title="'收货地址'" />
<!-- <uni-nav-bar left-icon="left" title="收货地址" fixed color="#8B2316" height="180rpx" :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="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>
<view class="divider-line"></view>
</scroll-view>
<view class="footer-bar" @click="submit">
<view class="confirm-btn" >确定兑换</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>
<unidialog
:title="'确认兑换信息'"
:visible="freeVisible"
:content="freeContent"
:titleColor="'#000'"
@close="freeClose"
@confirm="freeConfirm"
>
<template #content>
<view>
<view class="row">
<view class="left">收件人:</view>
<view class="right">{{ receiver }}</view>
</view>
<view class="row">
<view class="left">手机号:</view>
<view class="right">{{ mobile }}</view>
</view>
<view class="row">
<view class="left">地址:</view>
<view class="right">{{ regionText+detail }}</view>
</view>
<view class="row">
<view class="left">兑换积分:</view>
<view class="right"><text class="price">0积分</text>(包邮)</view>
</view>
<view class="row">请确认此次兑换,物品不退不换</view>
</view>
</template>
</unidialog>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import areaList from '@/utils/areaList.js'
import api from '@/api/api.js'
import unidialog from '@/components/dialog/dialog.vue';
const freeVisible = ref(false)
const freeContent = ref('')
const goods_order_id = ref('')
const goodsName = ref('');
import navTo from '@/utils/navTo';
const freeClose = () => {
freeVisible.value = false
}
const freeConfirm = () => {
freeVisible.value = false;
api.useWelfareNum({
other_uuid: goods_order_id.value,
type: 5
}).then(res => {
if(res.code == 1 || res.code == 200){
uni.showToast({
title: '兑换成功',
icon: 'none'
});
setTimeout(() => {
navTo({
url: '/pages_goods/myRedemption/myRedemption?from=upan'
});
}, 1000);
// uni.sendNativeEvent('goTabbarPage', {
// msg: 'mine'
// },ret => {
// console.log(ret);
// })
}
})
}
const receiver = ref('')
const mobile = ref('')
const regionText = ref('')
const detail = ref('')
const email = ref('')
const editingId = ref(null)
const goodsUuid = ref('')
const goodsNum = ref(0)
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) => {
console.log(22);
console.log(opts);
goodsUuid.value = opts.goodsUuid
goodsNum.value = opts.goodsNum;
goodsName.value = opts.goodsName;
getAddress()
})
const saveAddress = () => {
uni.setStorageSync('goods_address', {
receiver: receiver.value,
mobile: mobile.value,
regionText: regionText.value,
detail: detail.value,
email: email.value
})
}
const getAddress = () => {
const address = uni.getStorageSync('goods_address')
if(address){
receiver.value = address.receiver
mobile.value = address.mobile
regionText.value = address.regionText
detail.value = address.detail
email.value = address.email
}
}
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' })
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "boint_jifengood",
page_type: "积分商城",
resource_name:goodsName.value,
event_val:0,
event_unit:'积分',
number:goodsNum.value,
belong:'U盘',
},
},
(ret) => {
console.log(ret);
});
saveAddress();
api.createGoodsOrder({
goodsUuid: goodsUuid.value,
goodsNum: goodsNum.value,
user_name: receiver.value,
mobile: mobile.value,
email:'',
address: regionText.value+detail.value
}).then(res => {
if(res.code == 1 || res.code == 200){
freeVisible.value = true
goods_order_id.value = res.data.goods_order_id
}
})
}
</script>
<style scoped lang="scss">
.address-page { min-height: 100vh; background: #fff; }
.content { position: absolute; top: calc(var(--status-bar-height) + 44px); 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: #808080;}
.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; }
.row{
display: flex;
align-items: flex-start;
font-size: 28rpx;
padding: 12rpx 0;
max-width: 800rpx;
.left{
text-align: left;
max-width: 142rpx;
white-space: nowrap;
width:auto!important;
}
.price{
color: #FF4D4F;
}
}
.left{
width: 160rpx;
}
</style>
+297
View File
@@ -0,0 +1,297 @@
<template>
<navBar :title="'U盘在线兑换'" />
<view class="buy-upan-page">
<!-- 主要内容区域 -->
<view class="main-content">
<!-- 商品信息卡片 -->
<view class="product-card">
<!-- 商品描述 -->
<view class="product-title">{{ goodsInfo.name }}</view>
<!-- 兑换信息 -->
<view class="exchange-info">
<text class="info-left">免费兑换剩余{{ remainingCount }}个</text>
<text class="info-right">已兑换{{ exchangedCount }}件</text>
</view>
</view>
<!-- 兑换数量选择器 -->
<view class="quantity-section">
<text class="quantity-label">兑换数量</text>
<view class="quantity-selector">
<view
class="quantity-btn minus-btn"
:class="{ disabled: quantity <= 1 }"
@click="decreaseQuantity"
>
<text class="btn-text">—</text>
</view>
<view class="quantity-display">
<text class="quantity-value">{{ quantity }}</text>
</view>
<view
class="quantity-btn plus-btn"
:class="{ disabled: quantity >= remainingCount }"
@click="increaseQuantity"
>
<text class="btn-text">+</text>
</view>
</view>
</view>
</view>
<!-- 底部兑换按钮 -->
<view class="bottom-actions">
<view class="exchange-btn" @click="handleExchange">
<text class="btn-text">我要兑换</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import navBar from '@/components/navBar/navBar.vue';
import api from '@/api/api';
import goods_api from '@/api/goods_api';
import navTo from '@/utils/navTo';
const goodsUuid = ref('');
// 商品信息
const remainingCount = ref(0); // 剩余数量
const exchangedCount = ref(1129); // 已兑换数量
const quantity = ref(1); // 当前选择的数量
const goodsInfo = ref({});
// 增加数量
const increaseQuantity = () => {
if (quantity.value < remainingCount.value) {
quantity.value += 1;
}
};
const getUpanCount = async () => {
const res = await goods_api.getUpanWelfareCount()
if (res.code == 200) {
remainingCount.value=res.data;
};
}
// 减少数量
const decreaseQuantity = () => {
if (quantity.value > 1) {
quantity.value -= 1;
}
};
// 处理兑换
const handleExchange = () => {
if (quantity.value <= 0) {
uni.showToast({
title: '请选择兑换数量',
icon: 'none'
});
return;
}
// if (quantity.value > remainingCount.value) {
// uni.showToast({
// title: '兑换数量不能超过剩余数量',
// icon: 'none'
// });
// return;
// }
// TODO: 调用兑换接口
navTo({
url:'/pages_app/buyUpan/address?goodsUuid='+goodsUuid.value+'&goodsNum='+quantity.value+'&goodsName='+goodsInfo.value.name
})
};
// 获取U盘福利剩余数量
const getUpanWelfareCount = () => {
api.getUGoods().then(res => {
console.log(res);
if (res.code == 1 || res.code == 200) {
// 根据实际接口返回的数据结构更新
console.log(33);
console.log(res.data);
goodsInfo.value = res.data;
if (res.data) {
//remainingCount.value = res.data.remainingCount;
exchangedCount.value = res.data.times;
goodsUuid.value = res.data.uuid;
}
}
}).catch(err => {
console.error('获取U盘福利数量失败:', err);
});
};
onShow((options) => {
//remainingCount.value = options.restNum;
// 可选:获取实时剩余数量
//getUGoods();
getUpanWelfareCount();
getUpanCount();
});
</script>
<style scoped lang="scss">
// 变量定义
$bg-color: #ffffff;
$text-primary: #333333;
$text-secondary: #666666;
$text-light: #999999;
$border-color: #e5e5e5;
$white: #ffffff;
$teal-color: #00cbc0;
$light-teal: #e6f7f6;
$page-bg: #f5f6f7;
// 混合器
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
@mixin flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
.buy-upan-page {
overflow: hidden;
background-color: $page-bg;
margin-top: calc(var(--status-bar-height) + 44px);
// 为底部按钮留出空间
}
// 商品信息卡片
.product-card {
background-color: $white;
padding: 40rpx 30rpx;
margin-bottom: 40rpx;
.product-title {
font-size: 32rpx;
color: $text-primary;
font-weight: 500;
margin-bottom: 30rpx;
line-height: 1.5;
}
.exchange-info {
@include flex-between;
font-size: 28rpx;
.info-left {
color: red;
}
.info-right {
color: $text-secondary;
}
}
}
// 数量选择器区域
.quantity-section {
background-color: $white;
border-radius: 16rpx;
padding: 40rpx 30rpx;
.quantity-label {
font-size: 32rpx;
color: $text-primary;
margin-bottom: 30rpx;
display: block;
}
.quantity-selector {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 20rpx;
.quantity-btn {
width: 60rpx;
height: 60rpx;
border: 2rpx solid $teal-color;
border-radius: 8rpx;
@include flex-center;
background-color: $white;
cursor: pointer;
transition: all 0.3s ease;
.btn-text {
font-size: 36rpx;
color: $teal-color;
font-weight: 500;
line-height: 1;
}
&.disabled {
opacity: 0.5;
border-color: $border-color;
cursor: not-allowed;
.btn-text {
color: $text-light;
}
}
&:not(.disabled):active {
background-color: $light-teal;
}
}
.quantity-display {
min-width: 120rpx;
height: 80rpx;
background-color: $teal-color;
border-radius: 8rpx;
@include flex-center;
margin: 0 10rpx;
.quantity-value {
font-size: 40rpx;
color: $white;
font-weight: 500;
}
}
}
}
// 底部操作按钮
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
.exchange-btn {
background-color: $teal-color;
height: 88rpx;
@include flex-center;
cursor: pointer;
transition: all 0.3s ease;
&:active {
opacity: 0.8;
}
.btn-text {
color: $white;
font-size: 32rpx;
font-weight: 500;
}
}
}
</style>
+15 -7
View File
@@ -1,13 +1,15 @@
<template>
<view class="container">
<!-- 头部导航 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="病情记录"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -17,6 +19,7 @@
</view>
</template>
</uni-nav-bar>
</view>
<!-- 主要内容区域 -->
@@ -30,10 +33,11 @@
:lower-threshold="100"
>
<!-- 空状态 -->
<view class="empty-state" v-if="recordList.length === 0 && !loading">
<empty v-if="recordList.length === 0 && !loading" :emptyDesc="'您暂未添加病情记录'"></empty>
<!-- <view class="empty-state" v-if="recordList.length === 0 && !loading">
<text class="empty-text">暂无病情记录</text>
<button class="add-first-btn" @click="addRecord">添加第一条记录</button>
</view>
</view> -->
<!-- 时间线容器 -->
<view class="timeline-container" v-else>
@@ -105,6 +109,7 @@ import { onShow, onLoad } from '@dcloudio/uni-app'
import navBar from '@/components/navBar/navBar.vue'
import api from '@/api/api.js'
import docUrl from '@/utils/docUrl.js'
import empty from '@/components/empty/empty.vue'
const patientUuid = ref('')
const getRecordList = (isRefresh = false) => {
@@ -197,7 +202,7 @@ const goBack = () => {
const addRecord = () => {
// 跳转到添加记录页面
uni.navigateTo({
url: '/pages_app/addRecord/addRecord?patientUuid=' + patientUuid.value
url: '/pages_app/caseRecord/caseRecord?patientUuid=' + patientUuid.value
})
}
@@ -301,7 +306,10 @@ onMounted(() => {
/* 主要内容区域 */
.main-content {
height: calc(100vh - 180rpx);
top: calc(var(--status-bar-height) + 44px);
width: 100%;
position: fixed;
bottom:0;
padding: 40rpx 30rpx;
box-sizing: border-box;
}
@@ -390,7 +398,7 @@ onMounted(() => {
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.content-image {
+15 -4
View File
@@ -1,13 +1,13 @@
<template>
<view class="container">
<!-- 头部导航 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="病情记录"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -16,10 +16,11 @@
<text class="modify-btn" @click="saveRecord" v-if="recordUuid"
>修改</text
>
<text class="save-btn" @click="saveRecord" v-else>保存</text>
<text class="save-btn" @click="saveRecord" v-else>提交</text>
</view>
</template>
</uni-nav-bar>
</view>
<!-- 主要内容区域 -->
<view class="main-content">
@@ -40,6 +41,7 @@
class="description-input"
v-model="des"
placeholder="请输入患者病情"
:adjust-position="false"
></textarea>
</view>
</view>
@@ -337,7 +339,16 @@ const saveRecord = () => {
/* 主要内容区域 */
.main-content {
padding: 0rpx 30rpx;
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
width: 100%;
box-sizing: border-box;
bottom:150rpx;
z-index:1;
overflow-y: scroll;
padding:0 30rpx;
}
.form-item {
+28 -15
View File
@@ -6,15 +6,13 @@
<!-- 主要内容区域 -->
<view class="main-content">
<!-- 疾病诊断部分 -->
<view class="header">
<view class="section">
<view class="section-header">
<view class="red-bar"></view>
<text class="section-title">疾病诊断</text>
</view>
<view class="section-content">
<text class="diagnosis-text">疾病诊断: {{ pageData.diseaseName }}</text>
</view>
</view>
</view>
<!-- 化验报告部分 -->
<view class="section">
@@ -153,7 +151,7 @@ onMounted(() => {
})
</script>
<style scoped>
<style scoped lang="scss">
.container {
background-color: #ffffff;
min-height: 100vh;
@@ -205,13 +203,7 @@ onMounted(() => {
}
/* 头部导航样式 */
.header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
}
.back-btn {
width: 60rpx;
@@ -237,7 +229,13 @@ onMounted(() => {
/* 主要内容区域 */
.main-content {
padding: 0 30rpx;
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right:0;
bottom: 0rpx;
overflow-y: scroll;
}
.section {
@@ -247,6 +245,7 @@ onMounted(() => {
.section-header {
margin-top: 20rpx;
position: relative;
margin-bottom: 20rpx;
}
@@ -277,7 +276,21 @@ onMounted(() => {
color: #333333;
line-height: 1.5;
}
.header{
background: #8B2316;
color: #fff;
margin-bottom: 20rpx;
.section{
padding:20rpx;
margin-bottom: 10rpx;
}
.section-content{
padding-left:0rpx;
}
.diagnosis-text{
color: #fff;
}
}
.no-report-text {
font-size: 28rpx;
color: #999999;
@@ -306,7 +319,7 @@ onMounted(() => {
height: 200rpx;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
}
.report-image {
+129 -46
View File
@@ -4,6 +4,7 @@
<navBar title="公益咨询" />
<view class="tabs">
<view :class="['tab', activeTab==='new' ? 'active' : '']" @tap="switchTab('new')">新的咨询</view>
<view class="bar"></view>
<view :class="['tab', activeTab==='mine' ? 'active' : '']" @tap="switchTab('mine')">我已回答</view>
</view>
<view class="tabs-spacer"></view>
@@ -20,14 +21,18 @@
>
<view v-for="(item, idx) in displayList" :key="idx" class="consult-card" @click="goDetail(item.id,item.patientUuid)">
<view class="card-head">
<view class="namebox">
<text class="user-name">{{ item.maskName }}</text>
<text class="user-avatar" v-if="bottomActive!=='quick'">({{item.sex==1?'女':'男' }}<text decode>&nbsp;</text>{{ calcAge(item.birthDate) }}岁)</text>
</view>
<text class="date">{{ item.date }}</text>
</view>
<view class="card-body">
<text class="content">{{ item.content }}</text>
<text class="content twoline">{{ item.disease_describe }}</text>
</view>
<view class="card-foot" v-if="bottomActive==='multi'">
<text class="reply-count">{{ item.answer_num }}位医生已回答</text>
<text class="reply-count" v-if="item.answer_num>0">{{ item.answer_num }}位医生已回答</text>
<text class="reply-count-none" v-else>暂未有医生回答</text>
<view v-if="item.tag" class="tag">{{ item.tag }}</view>
</view>
<view class="card-foot" v-else>
@@ -52,7 +57,7 @@
<script setup>
import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { onShow,onBackPress,onLoad } from '@dcloudio/uni-app'
import navBar from '@/components/navBar/navBar.vue'
import empty from '@/components/empty/empty.vue'
import api from '@/api/api.js'
@@ -65,6 +70,10 @@ const pageSize=ref(10)
const hasMore = ref(true)
const isLoading = ref(false)
const isRefreshing = ref(false)
onBackPress(()=>{
plus.runtime.quit();
return true;
})
const goDetail=async(uuid,patientUuid)=>{
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
@@ -73,11 +82,16 @@ const goDetail=async(uuid,patientUuid)=>{
})
}else{
let userId=uni.getStorageSync('userInfo').uuid.toLowerCase();
let conversationId=userId+'|1|'+patientUuid.toLowerCase();
await uni.$UIKitStore.uiStore.selectConversation(conversationId)
navTo({
url:'/pages_chat/chat/index?from=consult&&patientUuid='+patientUuid+'&&uuid='+uuid
uni.sendNativeEvent('goConsultPage', {
msg: 'chat'
},ret => {
console.log(ret);
})
//let conversationId=userId+'|1|'+patientUuid.toLowerCase();
//await uni.$UIKitStore.uiStore.selectConversation(conversationId)
// navTo({
// url:'/pages_chat/chat/index?from=consult&&patientUuid='+patientUuid+'&&uuid='+uuid
// })
}
}else{
@@ -97,6 +111,22 @@ function maskName(name){
const first = name.slice(0,1)
return `${first}**`
}
// 根据出生日期计算年龄(birth 可以是 'YYYY-MM-DD' 或可被 Date 解析的字符串 / 时间戳)
function calcAge(birth) {
if (!birth) return ''
const birthDate = new Date(birth)
if (isNaN(birthDate.getTime())) {
return ''
}
const today = new Date()
let age = today.getFullYear() - birthDate.getFullYear()
const m = today.getMonth() - birthDate.getMonth()
if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) {
age--
}
// 年龄小于 0 视为无效
return age >= 0 ? age : ''
}
const newConsultList=async(isRefresh=false)=>{
if(isLoading.value) return
isLoading.value = true
@@ -113,10 +143,12 @@ const newConsultList=async(isRefresh=false)=>{
if(res && res.code===200 && res.data && res.data.consult_list){
const list = Array.isArray(res.data.consult_list.list) ? res.data.consult_list.list : []
const mapped = list.map(item=>({
maskName: maskName(item.realName || ''),
date: (item.createDate || '').slice(0,10),
maskName: item.realName,
date: (item.createDate || '').slice(0,16),
content: item.content || '',
replyCount: 0,
sex: item.sex || 0,
birthDate: item.birthDate || item.birthday,
tag: item.diseaseName || '',
id:item.uuid || '',
patientUuid:item.patientUuid || ''
@@ -150,10 +182,12 @@ const consultListHis=async(isRefresh=false)=>{
if(res.code==200){
const list = Array.isArray(res.data.list) ? res.data.list : []
const mapped = list.map(item=>({
maskName: maskName(item.realName || ''),
date: (item.createDate || '').slice(0,10),
maskName: item.realName,
date: (item.createDate || '').slice(0,16),
content: item.content || '',
replyCount: 0,
sex: item.sex || 0,
birthDate: item.birthDate || item.birthday,
tag: item.diseaseName || '',
id:item.uuid || '',
patientUuid:item.patientUuid || ''
@@ -191,7 +225,9 @@ const listNewInterrogation=async(isRefresh=false)=>{
maskName: maskName(item.name || ''),
date: (item.create_date || '').slice(0,10),
content: item.your_question || '',
disease_describe:item.your_question || '',
disease_describe:item.disease_describe || '',
sex: item.sex || 0,
birthDate: item.birthday || '',
answer_num: item.answer_num || 0,
tag: item.disease_name || '',
id:item.step1_uuid || ''
@@ -229,6 +265,8 @@ const listMyAnsweredInterrogation=async(isRefresh=false)=>{
date: (item.create_date || '').slice(0,10),
content: item.your_question || '',
disease_describe:item.disease_describe || '',
sex: item.sex || 0,
birthDate: item.birthday || '',
answer_num: item.answer_num || 0,
tag: item.disease_name || '',
id:item.step1_uuid || ''
@@ -246,20 +284,22 @@ const listMyAnsweredInterrogation=async(isRefresh=false)=>{
isRefreshing.value = false
}
};
onShow(()=>{
onLoad(()=>{
page.value = 1
hasMore.value = true;
listNew.value = [];
listMine.value=[];
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(true)
newConsultList(false)
}else{
consultListHis(true)
consultListHis(false)
}
}else{
if(activeTab.value==='new'){
listNewInterrogation(true)
listNewInterrogation(false)
}else{
listMyAnsweredInterrogation(true)
listMyAnsweredInterrogation(false)
}
}
@@ -276,18 +316,20 @@ function switchTab(key) {
isLoading.value = false;
listNew.value = [];
listMine.value=[];
page.value = 1
hasMore.value = true;
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(true)
newConsultList(false)
}else{
consultListHis(true)
consultListHis(false)
}
}else{
if(activeTab.value==='new'){
listNewInterrogation(true)
listNewInterrogation(false)
}else{
listMyAnsweredInterrogation(true)
listMyAnsweredInterrogation(false)
}
}
@@ -303,25 +345,32 @@ function goBack() {
}
// 底部tab交互
const bottomActive = ref('quick')
const bottomActive = ref('multi')
const switchBottomTab=(key)=>{
isLoading.value = false;
bottomActive.value = key;
page.value = 1
hasMore.value = true;
listNew.value = [];
listMine.value=[];
if(key=='quick'){
try {
plus.runtime.quit();
} catch (error) {
}
if(activeTab.value==='new'){
newConsultList(true)
newConsultList(false)
}else{
consultListHis(true)
consultListHis(false)
}
}else{
if(activeTab.value==='new'){
console.log('listNewInterrogation')
listNewInterrogation(true);
listNewInterrogation(false);
}else{
listMyAnsweredInterrogation(true)
listMyAnsweredInterrogation(false)
}
}
}
@@ -331,6 +380,8 @@ function onRefresh(){
isRefreshing.value = true
page.value = 1
hasMore.value = true
listNew.value = [];
listMine.value=[];
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(true)
@@ -381,32 +432,46 @@ function onReachBottom(){
display: flex;
justify-content: space-around;
align-items: center;
height: 44px;
height: 48px;
padding: 0 16px;
position: fixed;
top: 180rpx;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
z-index: 10;
background-color: #ffffff;
box-shadow: 0 1px 0 rgba(0,0,0,0.06);
border-bottom: 1px solid #e5e5e5;
.bar{
width:1px;
height: 100%;
background-color: #e5e5e5;
}
.tab {
flex: 1;
text-align: center;
font-size: 16px;
font-size: 36rpx;
color: #7a7a7a;
padding: 10px 0;
&.active { color: #8B2316; position: relative; }
&.active::after { content: ''; position: absolute; left: 25%; right: 25%; bottom: 2px; height: 3px; background-color: #8B2316; border-radius: 2px; }
}
}
.tabs-spacer { height: 44px; }
.namebox{
display: flex;
align-items: center;
justify-content: center;
.user-avatar{
margin-left: 10rpx;
font-size:32rpx;
color: #666;
}
}
.list-scroll {
flex: 1;
position: fixed;
top: 268rpx;
bottom: 136rpx;
padding: 8px 0px 0 0px;
top: calc(var(--status-bar-height) + 44px + 48px);
bottom:56px;
margin: 20rpx rpx 0;
box-sizing: border-box;
width:100%;
@@ -421,8 +486,8 @@ function onReachBottom(){
display: flex;
justify-content: space-between;
align-items: center;
.user-name { font-size: 16px; color: #a31712; }
.date { font-size: 14px; color: #9aa0a6; }
.user-name { font-size: 36rpx; color: #a31712; }
.date { font-size: 36rpx; color: #9aa0a6; }
}
.card-body {
background: #efefef;
@@ -452,31 +517,49 @@ function onReachBottom(){
}
display: flex; align-items: center; justify-content: space-between; margin-top: 8px;
.reply-count { font-size:28rpx; color: #8B2316; }
.reply-count-none{
font-size:28rpx;
color: #999;
position: relative;
padding-left: 25rpx;
}
.reply-count-none::after{
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
width: 20rpx;
height: 20rpx;
background: red;
border-radius: 50%;
}
.tag {
display: flex;
align-items: center;
justify-content: center;
padding: 8rpx 16rpx; background: #fff5f5; color: #a31712; border-radius: 28rpx; font-size: 24rpx; border:2rpx solid #a31712;}
padding: 0rpx 16rpx; background: #fff; color: #a31712; border-radius: 28rpx; font-size: 24rpx; border:2rpx solid #a31712;}
}
}
.bottom-bar {
border-top: 1px solid #e5e5e5;
position: fixed;
left: 0; right: 0; bottom: 0;
background-color: #ffffff;
box-shadow: 0 -2px 8px rgba(0,0,0,0.06);
display: flex; align-items: center; justify-content: center;
padding: 8px 12px;
gap: 12px;
display: flex;
align-items: center;
justify-content: center;
.bottom-tab {
flex: 1;
height: 40px;
line-height: 40px;
height: 56px;
line-height: 56px;
text-align: center;
border-radius: 6px;
font-size: 16px;
color: #a31712;
background-color: #fff5f5;
font-size: 36rpx;
color: #999;
background-color: #fff;
}
.bottom-tab.active {
background-color: #a31712;
+466 -158
View File
@@ -1,7 +1,8 @@
<template>
<navBar title="问题详情" />
<view class="consult-detail-page">
<!-- 导航栏 -->
<navBar title="问题详情" />
<!-- 内容区域 -->
<scroll-view scroll-y class="content-scroll">
@@ -9,11 +10,13 @@
<view class="user-section">
<view class="user-info">
<view class="user-name">
<text class="name">{{ userInfo.name }}</text>
<text class="gender-age">({{ userInfo.gender }} {{ userInfo.age }}岁)</text>
<text class="name">{{ maskName(userInfo.name) }}</text>
<text class="gender-age"
>({{ userInfo.gender }} {{ userInfo.age }}岁)</text
>
</view>
<view class="detail-btn" @click="goInfo">
<up-image :src="detailImg" width="183rpx" height="34rpx" ></up-image>
<up-image :src="detailImg" width="183rpx" height="34rpx"></up-image>
</view>
</view>
</view>
@@ -28,7 +31,12 @@
<!-- 问题内容 -->
<view class="question-content">
<text class="content-text">{{ questionInfo.diseaseDescribe }}</text>
<view class="content-text">
{{ questionInfo.diseaseDescribe }}
</view>
<view class="content-text">
{{ questionInfo.your_question }}
</view>
</view>
<!-- 疾病描述 -->
@@ -40,29 +48,121 @@
<!-- 图片网格 -->
<view class="image-grid" v-if="questionInfo.images">
<image
v-if="questionInfo.images && questionInfo.images.split(',').length>0"
v-for="(img, index) in questionInfo.images.split(',')"
:key="index"
:src="docUrl+img"
:src="docUrl + img"
class="grid-image"
mode="aspectFill"
@click="previewImage(docUrl+img, index)"
@click="previewImageInfo(questionInfo.images,docUrl + img, index)"
/>
</view>
<view class="bar"></view>
<view
class="lookmore"
v-if="
questionInfo.SupplementList &&
questionInfo.SupplementList.length > 1 && questionInfo.AnswerList.length > 0
"
@click="supplementShowAll = !supplementShowAll"
>
<view class="lookmore-text">
{{ supplementShowAll ? "收起" : "查看更多" }}
</view>
<uni-icons :type="supplementShowAll?'up':'down'" size="34rpx" color="#8B2316"></uni-icons>
</view>
<view
class="doctor-reply-section"
style="margin-bottom: 10rpx;"
v-for="(item,index) in displaySupplementList"
:key="index"
v-if="questionInfo.SupplementList && questionInfo.SupplementList.length > 0"
>
<view class="addbox">
<view class="addwrap">
<image :src="bgImg" class="addimg"></image>
<view class="addtext">信息补充</view>
</view>
<view class="date" > {{formatDate(item.create_date) }} </view>
</view>
<view class="question-content" style="margin-top:25rpx;" >
<text class="content-text" >
{{item.disease_describe }}
</text>
</view>
<!-- 图片网格 -->
<view class="image-grid" v-if="item.imgs">
<image
v-for="(img, index) in item.imgs.split(',')"
:key="index"
:src="docUrl + img"
class="grid-image"
mode="aspectFill"
@click="previewImageSupplement(item.imgs,docUrl + img, index)"
/>
</view>
</view>
<!-- 补充信息:查看更多 / 收起 -->
<view class="topbar"></view>
<!-- 医生回答区域 -->
<view class="doctor-reply-section">
<view
class="doctor-reply-section"
v-if="questionInfo.AnswerList.length > 0"
>
<view class="section-title">医生回答</view>
<view class="doctor-cell" v-for="item in questionInfo.AnswerList" :key="item.answer_uuid">
<view
class="doctor-cell"
v-for="item in questionInfo.AnswerList"
:key="item.answer_uuid"
>
<view class="doctor-card">
<view class="doctor-info">
<image :src="doctorReply.avatar" class="doctor-avatar" />
<up-image
:src="docUrl + item.photo"
mode="aspectFill"
:loadingIcon="lazyImg"
:errorIcon="lazyImg"
:lazy-load="true"
class="doctor-avatar"
width="90rpx"
height="90rpx"
radius="10rpx"
>
<template #error>
<image
:src="lazyImg"
mode="widthFix"
height="90rpx"
width="90rpx"
radius="10rpx"
></image>
</template>
<template v-slot:loading>
<image
:src="lazyImg"
mode="widthFix"
height="90rpx"
width="90rpx"
radius="10rpx"
></image>
</template>
</up-image>
<view class="doctor-details">
<view class="namebox">
<view class="namecontet">
<view class="doctor-name">{{ item.realname }}</view>
<view class="bar" v-if="item.realname!='账号已注销'"></view>
<view class="doctor-hospital" v-if="item.realname!='账号已注销'" style="margin-left: 0rpx;color:#999">{{ item.name }}</view>
</view>
</view>
<view class="hospital-time-row">
<view class="doctor-hospital" >{{ item.hospital }}</view>
<view class="doctor-hospital">{{ item.hospital_name }}</view>
<view class="reply-time">{{ item.create_date }}</view>
</view>
</view>
@@ -70,26 +170,22 @@
<view class="reply-content">
<text class="reply-text">{{ item.note }}</text>
</view>
<view class="reply-content" style="background:none;pading:0">
<view class="reply-content" style="background: none; padding: 0">
<view v-if="item.imgs" class="reply-images">
<image
v-for="(img, idx) in item.imgs.split(',')"
:key="idx"
:src="docUrl+img"
:src="docUrl + img"
class="reply-image"
@click="previewReplyImages(item, idx)"
/>
</view>
</view>
</view>
<view class="smallbar"></view>
</view>
</view>
</scroll-view>
<!-- 底部固定区域 -->
@@ -97,174 +193,325 @@
<!-- 特别声明 -->
<view class="disclaimer">
<text class="disclaimer-title">特别声明:</text>
<text class="disclaimer-text">答案仅为医生个人经验或建议分享,不能视为诊断依据,如有诊疗需求,请务必前往正规医院就诊。</text>
<text class="disclaimer-text"
>答案仅为医生个人经验或建议分享,不能视为诊断依据,如有诊疗需求,请务必前往正规医院就诊。</text
>
</view>
<!-- 编辑按钮 -->
<view class="edit-btn" @click="editQuestion(status)">
{{status==1?'我要编辑':'我要回答'}}
{{ status == 1 ? "我要编辑" : "我要回答" }}
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import navBar from '@/components/navBar/navBar.vue'
import detailImg from "@/static/iv_jiwangshi.png"
import navTo from '@/utils/navTo.js'
import { onLoad,onShow } from '@dcloudio/uni-app'
import docUrl from '@/utils/docUrl'
import api from "@/api/api.js"
const uuid = ref('');
const step1_uuid = ref('');
const answer_uuid = ref('');
const status = ref(0)
import { ref, computed } from "vue";
import navBar from "@/components/navBar/navBar.vue";
import detailImg from "@/static/iv_jiwangshi.png";
import navTo from "@/utils/navTo.js";
import { onLoad, onShow,onUnload } from "@dcloudio/uni-app";
import docUrl from "@/utils/docUrl";
import api from "@/api/api.js";
import lazyImg from "@/static/avastar.png";
import bgImg from "@/static/complete_question.png";
const uuid = ref("");
const step1_uuid = ref("");
const answer_uuid = ref("");
const status = ref(0);
onLoad((options) => {
uuid.value = options.uuid
status.value = options.status || 0
console.log(uuid.value)
})
const goInfo=()=>{
uuid.value = options.uuid;
status.value = options.status || 0;
console.log(uuid.value);
});
const goInfo = () => {
navTo({
url:'/pages_app/patientInfo/patientInfo?step1_uuid='+step1_uuid.value
url: "/pages_app/patientInfo/patientInfo?step1_uuid=" + step1_uuid.value,
});
};
const getInterrogation = () => {
api
.getInterrogation({
uuid: uuid.value,
})
}
const getInterrogation=()=>{
api.getInterrogation({
uuid:uuid.value
}).then(res=>{
console.log(res)
if(res.code === '200' && res.data) {
step1_uuid.value = res.data.step1_uuid || ''
.then((res) => {
console.log(res);
if (res.code === "200" && res.data) {
step1_uuid.value = res.data.step1_uuid || "";
// 更新用户信息
userInfo.value = {
name: res.data.name || '提**',
gender: res.data.sex === 1 ? '男' : '女',
age: res.data.birthday ? calculateAge(res.data.birthday) : '未知'
}
name: res.data.name || "提**",
gender: res.data.sex === 1 ? "女" : "男",
age: res.data.birthday ? calculateAge(res.data.birthday) : "未知",
};
// 更新问题信息
questionInfo.value = {
date: res.data.create_date ? formatDate(res.data.create_date) : '未知',
diseaseTag: res.data.disease_name || '未知疾病',
content: res.data.your_question || '暂无问题描述',
images: res.data.imgs || '',
date: res.data.create_date
? formatDate(res.data.create_date)
: "未知",
diseaseTag: res.data.disease_name || "未知疾病",
content: res.data.your_question || "暂无问题描述",
images: res.data.imgs || "",
AnswerList: res.data.AnswerList || [],
your_question: res.data.your_question || ''
SupplementList: res.data.SupplementList || [],
your_question: res.data.your_question || "",
};
if(res.data.AnswerList.length == 0 && res.data.SupplementList.length > 0){
supplementShowAll.value = true;
}else{
supplementShowAll.value = false;
}
// 更新疾病描述
if(res.data.disease_describe) {
questionInfo.value.diseaseDescribe = res.data.disease_describe
if (res.data.disease_describe) {
questionInfo.value.diseaseDescribe = res.data.disease_describe;
}
let user=uni.getStorageSync('userInfo');
let arr=res.data.AnswerList.filter(item=>{
return user.uuid == item.expert_uuid
})
if(arr.length>0){
let user = uni.getStorageSync("userInfo");
let arr = res.data.AnswerList.filter((item) => {
return user.uuid == item.expert_uuid;
});
if (arr.length > 0) {
status.value = 1;
answer_uuid.value = arr[0].answer_uuid;
}
}
})
}
onShow(()=>{
getInterrogation()
});
};
onUnload(() => {
uni.$off('updateStatus');
})
onShow(() => {
getInterrogation();
uni.$on('updateStatus',() => {
status.value = 1;
})
});
const maskName = (name) => {
if (!name) return "**";
const first = name.slice(0, 1);
return `${first}**`;
};
// 计算年龄
function calculateAge(birthday) {
const birthDate = new Date(birthday)
const today = new Date()
let age = today.getFullYear() - birthDate.getFullYear()
const monthDiff = today.getMonth() - birthDate.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--
const birthDate = new Date(birthday);
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (
monthDiff < 0 ||
(monthDiff === 0 && today.getDate() < birthDate.getDate())
) {
age--;
}
return age.toString()
return age.toString();
}
// 格式化日期
function formatDate(dateString) {
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
// 用户信息
const userInfo = ref({
name: '提**',
gender: '男',
age: '15'
})
name: "提**",
gender: "男",
age: "15",
});
// 问题信息
const questionInfo = ref({
date: '2022-11-09',
diseaseTag: '甲型肝炎',
content: '为什么程序员总是分不清万圣节和圣诞节?因为Oct31==Dec25。\n任何我写的代码,超过6个月不去看它,当我再看时,都像是别人写的。',
diseaseDescribe: '', // 疾病描述
images: [
'/static/images/placeholder1.jpg',
'/static/images/placeholder2.jpg',
'/static/images/placeholder3.jpg',
'/static/images/placeholder4.jpg',
'/static/images/placeholder5.jpg',
'/static/images/placeholder6.jpg',
'/static/images/placeholder7.jpg',
'/static/images/placeholder8.jpg'
]
})
date: "2022-11-09",
diseaseTag: "甲型肝炎",
content:
"为什么程序员总是分不清万圣节和圣诞节?因为Oct31==Dec25。\n任何我写的代码,超过6个月不去看它,当我再看时,都像是别人写的。",
diseaseDescribe: "", // 疾病描述
images: "",
SupplementList: [],
AnswerList: [],
your_question: "",
});
// 补充信息:查看更多 / 收起
const supplementShowAll = ref(false);
const displaySupplementList = computed(() => {
if (
!questionInfo.value.SupplementList ||
questionInfo.value.SupplementList.length === 0
) {
return [];
}else if(!questionInfo.value.SupplementList || questionInfo.value.SupplementList.length == 1){
return questionInfo.value.SupplementList;
}
return supplementShowAll.value
? questionInfo.value.SupplementList
: [];
});
// 医生回答
const doctorReply = ref({
avatar: '/static/images/doctor-avatar.jpg',
name: 'los测试',
hospital: '隆福医院',
time: '28天前',
content: '徐徐,喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵',
image: '/static/images/reply-image.jpg'
})
avatar: "/static/images/doctor-avatar.jpg",
name: "los测试",
hospital: "隆福医院",
time: "28天前",
content:
"徐徐,喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵",
image: "/static/images/reply-image.jpg",
});
// 返回上一页
function goBack() {
uni.navigateBack()
uni.navigateBack();
}
// 预览图片
function previewImage(current, index) {
let urls=questionInfo.value.images
? questionInfo.value.images.split(",").map((path) => docUrl + path)
: [];
uni.previewImage({
urls: questionInfo.value.images?questionInfo.value.images.split(',').map(path=> docUrl + path):[],
current: index
showmenu:true,
urls:urls,
current: index,
longPressActions: {
itemList:['保存图片'],
success:()=>{
uni.closePreviewImage();
uni.saveImageToPhotosAlbum({
filePath: urls[index],
success:()=>{
uni.showToast({
title: '图片保存成功',
icon: 'none'
})
},
fail:(err)=>{
uni.showToast({
title: '图片保存失败:'+err.errMsg,
icon: 'none'
})
}
})
},
},
});
}
const previewImageSupplement = (imgsStr,current, index) => {
let urls=imgsStr.split(",").map((path) => docUrl + path);
uni.previewImage({
showmenu:true,
urls: urls,
current: index,
longPressActions: {
itemList:['保存图片'],
success:()=>{
uni.closePreviewImage();
uni.saveImageToPhotosAlbum({
filePath: urls[index],
success:()=>{
uni.showToast({
title: '图片保存成功',
icon: 'none'
})
},
fail:(err)=>{
uni.showToast({
title: '图片保存失败:'+err.errMsg,
icon: 'none'
})
}
})
},
},
});
}
const previewImageInfo = (imgsStr,current, index) => {
let urls=imgsStr.split(",").map((path) => docUrl + path);
uni.previewImage({
showmenu:true,
urls: urls,
current: index,
longPressActions: {
itemList:['保存图片'],
success:()=>{
uni.closePreviewImage();
uni.saveImageToPhotosAlbum({
filePath: urls[index],
success:()=>{
uni.showToast({
title: '图片保存成功',
icon: 'none'
})
},
fail:(err)=>{
uni.showToast({
title: '图片保存失败:'+err.errMsg,
icon: 'none'
})
}
})
},
},
});
}
// 预览医生回复图片
function previewReplyImages(item, index){
if(!item || !item.imgs){
return
function previewReplyImages(item, index) {
if (!item || !item.imgs) {
return;
}
const urls = item.imgs.split(',').map(path=> docUrl + path)
const urls = item.imgs.split(",").map((path) => docUrl + path);
uni.previewImage({
showmenu:true,
urls,
current: index
current: index,
longPressActions: {
itemList:['保存图片'],
success:()=>{
uni.closePreviewImage();
uni.saveImageToPhotosAlbum({
filePath: urls[index],
success:()=>{
uni.showToast({
title: '图片保存成功',
icon: 'none'
})
},
fail:(err)=>{
uni.showToast({
title: '图片保存失败:'+err.errMsg,
icon: 'none'
})
}
})
},
},
});
}
// 编辑问题
function editQuestion() {
if(status.value == 1){
if (status.value == 1) {
navTo({
url:'/pages_app/myAnswer/myAnswer?answer_uuid='+answer_uuid.value+'&uuid='+uuid.value
})
}else{
url:
"/pages_app/myAnswer/myAnswer?answer_uuid=" +
answer_uuid.value +
"&uuid=" +
uuid.value,
});
} else {
navTo({
url:'/pages_app/myAnswer/myAnswer?uuid='+uuid.value
})
url: "/pages_app/myAnswer/myAnswer?uuid=" + uuid.value,
});
}
}
</script>
@@ -298,7 +545,7 @@ function editQuestion() {
.back-icon {
font-size: 48rpx;
color: #8B2316;
color: #8b2316;
font-weight: bold;
}
}
@@ -306,29 +553,83 @@ function editQuestion() {
.nav-title {
font-size: 36rpx;
font-weight: 500;
color: #8B2316;
color: #8b2316;
}
.nav-right {
width: 80rpx;
}
}
.bar{
width:100%;
height:20rpx;
.topbar{
width: 100%;
height: 20rpx;
background-color: #efefef;
}
.smallbar{
width:100%;
height:10rpx;
background-color: #efefef;
.bar {
width: 2rpx;
height: 30rpx;
margin:0 20rpx;
background-color: #999;
}
.doctor-hospital{
margin-left: 20rpx;
}
.namecontet{
display: flex;
margin-left: 20rpx;
align-items: center;
}
.lookmore{
display: flex;
justify-content: center;
align-items: center;
font-size: 28rpx;
margin: 20rpx 30rpx 20rpx;
.lookmore-text{
font-size: 28rpx;
color: #8B2316;
}
}
.addbox {
display: flex;
align-items: center;
justify-content: space-between;
margin: 20rpx 30rpx 0;
.addwrap {
display: flex;
align-items: center;
position: relative;
.addimg {
width: 110rpx;
height: 60rpx;
}
}
.addtext {
position: absolute;
top: 9rpx;
left: 6rpx;
font-size: 24rpx;
color: #fff;
font-weight: 500;
}
.date {
font-size: 28rpx;
color: #999;
}
}
// .smallbar {
// width: 100%;
// height: 10rpx;
// background-color: #efefef;
// }
.content-scroll {
flex: 1;
position: fixed;
top: 135rpx;
width:auto;
top: calc(var(--status-bar-height) + 44px);
width: auto;
box-sizing: border-box;
width: 100%;
padding: 30rpx 0;
bottom: 313rpx;
}
@@ -337,7 +638,7 @@ function editQuestion() {
background-color: #ffffff;
border-radius: 16rpx;
margin-bottom: 24rpx;
padding: 0 30rpx;
margin: 0 30rpx;
.user-info {
display: flex;
@@ -351,7 +652,7 @@ function editQuestion() {
.name {
font-size: 32rpx;
color: #8B2316;
color: #8b2316;
font-weight: 500;
}
@@ -366,7 +667,6 @@ function editQuestion() {
display: flex;
margin-left: 10rpx;
.detail-text {
color: #ffffff;
font-size: 24rpx;
@@ -379,7 +679,6 @@ function editQuestion() {
}
}
}
}
.tag-date-row {
@@ -394,8 +693,8 @@ function editQuestion() {
.tag-text {
display: inline-block;
background-color: #ffffff;
color: #8B2316;
border: 2rpx solid #8B2316;
color: #8b2316;
border: 2rpx solid #8b2316;
border-radius: 40rpx;
padding: 7rpx 22rpx;
font-size: 24rpx;
@@ -419,6 +718,7 @@ function editQuestion() {
font-size: 28rpx;
color: #333;
line-height: 1.6;
word-break: break-all;
}
}
@@ -427,11 +727,11 @@ function editQuestion() {
border-radius: 16rpx;
padding: 32rpx;
margin: 0 30rpx 24rpx;
border-left: 6rpx solid #8B2316;
border-left: 6rpx solid #8b2316;
.describe-title {
font-size: 30rpx;
color: #8B2316;
color: #8b2316;
font-weight: 500;
margin-bottom: 16rpx;
}
@@ -449,7 +749,6 @@ function editQuestion() {
gap: 8rpx;
margin: 0 30rpx 40rpx;
.grid-image {
width: 100%;
height: 160rpx;
@@ -461,14 +760,14 @@ function editQuestion() {
margin: 0 0rpx 40rpx;
.section-title {
font-size: 32rpx;
padding:24rpx 30rpx;
border-bottom:1rpx solid #efefef;
color: #8B2316;
padding: 24rpx 30rpx;
border-bottom: 1rpx solid #efefef;
color: #8b2316;
font-weight: 500;
}
.doctor-card {
margin:20rpx 30rpx 0;
margin: 20rpx 30rpx 0;
background-color: #ffffff;
border-radius: 16rpx;
.doctor-info {
@@ -477,9 +776,9 @@ function editQuestion() {
margin-bottom: 24rpx;
.doctor-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
width: 90rpx;
height: 90rpx;
border-radius: 10rpx;
margin-right: 24rpx;
}
@@ -487,13 +786,17 @@ function editQuestion() {
flex: 1;
.doctor-name {
font-size: 32rpx;
color: #333;
font-size: 34rpx;
color: #8B2316;
font-weight: 500;
margin-bottom: 8rpx;
max-width: 203rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.hospital-time-row {
margin-top: 20rpx;
display: flex;
align-items: center;
justify-content: space-between;
@@ -522,7 +825,7 @@ function editQuestion() {
color: #333;
line-height: 1.6;
}
.reply-images{
.reply-images {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8rpx;
@@ -532,7 +835,7 @@ function editQuestion() {
.reply-image {
width: 100%;
height:150rpx;
height: 150rpx;
border-radius: 8rpx;
object-fit: cover;
}
@@ -550,21 +853,20 @@ function editQuestion() {
}
.disclaimer {
background-color: #fff5f5;
background-color: #f6f6f6;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
.disclaimer-title {
font-size: 28rpx;
color: #8B2316;
font-weight: 500;
color: #892b1b;
}
.disclaimer-text {
font-size: 28rpx;
color: #666;
line-height: 1.5;
color: #9c9492;
}
}
@@ -573,10 +875,16 @@ function editQuestion() {
height: 88rpx;
line-height: 88rpx;
text-align: center;
background-color: #00bcd4;
background-color: #3cc7c0;
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border-radius: 16rpx;
}
.doctor-cell{
border-bottom: 12rpx solid #efefef;
}
.doctor-cell:last-child{
border-bottom: none;
}
</style>
+935
View File
@@ -0,0 +1,935 @@
<template>
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="视频缓存"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eeeeee"
>
<template #right>
<view class="nav-actions">
<view class="nav-edit" @click="toggleEdit">
<text>{{ isEditMode ? "取消" : "编辑" }}</text>
</view>
</view>
</template>
</uni-nav-bar>
</view>
<view class="download-video-page">
<!-- 标签页 -->
<view class="tabs-container">
<view
class="tab-item"
:class="{ active: activeTab === 'completed' }"
@click="switchTab('completed')"
>
已完成
</view>
<view class="tab-divider"></view>
<view
class="tab-item"
:class="{ active: activeTab === 'downloading' }"
@click="switchTab('downloading')"
>
缓存中
</view>
</view>
<!-- 内容列表 -->
<scroll-view class="content-scroll" scroll-y :scroll-top="scrollTop">
<view v-if="currentList.length === 0" class="empty-state">
<text>暂无数据</text>
</view>
<view
v-for="(item, index) in currentList"
:key="item.id || index"
class="video-item"
@click="handleItemClick(item)"
>
<!-- 编辑模式下的多选框 -->
<view
v-if="isEditMode"
class="checkbox-wrapper"
@click.stop="toggleSelectItem(item)"
>
<view
class="checkbox"
:class="{ checked: isItemSelected(item) }"
>
<uni-icons
v-if="isItemSelected(item)"
type="checkmarkempty"
size="16"
color="#ffffff"
></uni-icons>
</view>
</view>
<!-- 缩略图 -->
<image
class="video-thumbnail"
:src="docUrl + item.imgpath"
mode="aspectFill"
/>
<!-- 内容信息 -->
<view class="video-info">
<view class="video-title">{{ item.name || "视频标题" }}</view>
<view class="video-author">{{ item.author || "作者" }}</view>
<!-- 时长和大小 -->
<view class="video-meta" v-if="item.status === 'completed'">
<uni-icons
type="clock"
size="12"
color="#999999"
class="meta-icon"
></uni-icons>
<text class="meta-text"><text v-if="item.duration">{{ formatDuration(item.duration) }}</text></text>
<text class="meta-size">{{ formatSize(item.size) }}</text>
</view>
<view class="video-meta video-status" v-if="item.status == 'downloading' || item.status == 'paused'">
<view class="status-icon">
<image :src="item.status == 'paused' ? playImg : pauseImg" mode="aspectFill"></image>
<text>{{ item.status == 'paused' ? '继续' : '暂停' }}</text>
</view>
<view class="box">
<!-- <view class="video-progress-bar">
<view class="video-progress-bar-inner" :style="{ width: item.progress + '%' }"></view>
</view> -->
<!-- <text>{{ item.progress + '%' }}</text> -->
<view class="size-box">{{ formatSize(item.downloadSize) }}/{{ formatSize(item.totalSize) }}</view>
</view>
</view>
</view>
<!-- 编辑模式下的删除按钮 -->
<!-- <view
v-if="isEditMode"
class="delete-btn"
@click.stop="deleteItem(item, index)"
>
<text>删除</text>
</view> -->
</view>
</scroll-view>
<!-- 底部存储信息 -->
<view class="bottom-box">
<view class="storage-info">
<text
>手机存储:总空间{{ storageInfo.total }}/剩余{{
storageInfo.available
}}可用</text
>
</view>
<view class="footer-btn" v-if="isEditMode">
<view class="footer-btn-item select" @click="clearAll">
<text v-if="!isAllSelect">全选</text>
<text v-else>取消全选</text>
</view>
<view class="bar"></view>
<view class="footer-btn-item del" @click="delTask">
删除<text v-if="selectedItems.length > 0">({{ selectedItems.length }})</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, nextTick } from "vue";
import downloadStore from "@/store/downloadStoreVideo.js";
import { onLoad, onShow,onUnload,onBackPress} from "@dcloudio/uni-app";
import playImg from "@/static/down_true.png";
import pauseImg from "@/static/down_false.png";
import navTo from "@/utils/navTo";
import docUrl from "@/utils/docUrl";
const downloadTasks = ref([]);
const unsubscribe = ref(null);
const isAllSelect = ref(false);
const selectedItems = ref([]);
// 响应式数据
const activeTab = ref("completed"); // completed: 已完成, downloading: 缓存中
const isEditMode = ref(false);
const scrollTop = ref(0);
const storageInfo = ref({
total: "461.31G",
available: "312.92G",
});
const delTask=()=>{
if(selectedItems.value.length === 0){
uni.showToast({
title: "请选择要删除的缓存",
icon: "none",
});
return;
}
uni.showModal({
title: "提示",
content: "确定要删除选中的缓存吗?",
success: (res) => {
if (res.confirm) {
// 从后往前删除,避免索引变化问题
const idsToDelete = [...selectedItems.value];
idsToDelete.forEach(id => {
const taskIndex = downloadTasks.value.findIndex(t => t.id === id);
if (taskIndex !== -1) {
removeTask(taskIndex);
}
});
// 清空选择
selectedItems.value = [];
isAllSelect.value = false;
}
}
});
}
const clearAll=()=>{
if(!isAllSelect.value){
// 全选当前列表的所有项
selectedItems.value = currentList.value.map(item => item.id);
isAllSelect.value = true;
}else{
// 取消全选
selectedItems.value = [];
isAllSelect.value = false;
}
}
// 判断项是否被选中
const isItemSelected = (item) => {
return selectedItems.value.includes(item.id);
}
onBackPress(() => {
if(from.value){
plus.runtime.quit();
return true;
}
});
// 切换项的选中状态
const toggleSelectItem = (item) => {
const index = selectedItems.value.indexOf(item.id);
if (index > -1) {
selectedItems.value.splice(index, 1);
} else {
selectedItems.value.push(item.id);
}
// 更新全选状态
isAllSelect.value = selectedItems.value.length === currentList.value.length && currentList.value.length > 0;
}
// 计算当前显示的列表
const currentList = computed(() => {
let list = [];
if (activeTab.value === "completed") {
list = downloadTasks.value.filter((task) => task.status === "completed");
} else {
list = downloadTasks.value.filter(
(task) => task.status === "downloading" || task.status === "paused"
);
}
// 当列表变化时,更新全选状态
nextTick(() => {
if (isAllSelect.value && selectedItems.value.length !== list.length) {
isAllSelect.value = false;
}
});
return list;
});
// 方法
const goBack = () => {
if(from.value){
plus.runtime.quit();
}else{
uni.navigateBack()
};
};
const switchTab = (tab) => {
activeTab.value = tab;
isEditMode.value = false;
// 切换标签时清空选择
selectedItems.value = [];
isAllSelect.value = false;
// 切换到缓存中时,滚动到顶部
if (tab === 'downloading') {
// 先设置为一个非0值,然后在下一个tick设置为0,确保滚动触发
scrollTop.value = 0.01;
nextTick(() => {
scrollTop.value = 0;
});
}
};
const toggleEdit = () => {
isEditMode.value = !isEditMode.value;
// 退出编辑模式时清空选择
if (!isEditMode.value) {
selectedItems.value = [];
isAllSelect.value = false;
}
};
const handleItemClick = (item) => {
if (isEditMode.value) {
// 编辑模式下,点击项切换选中状态
toggleSelectItem(item);
return;
}
// 跳转到视频详情或播放页面
if (item.status === "completed") {
uni.navigateTo({
url: `/pages_app/videoDetail/videoDetail?id=${item.id}&from=download`,
});
}else if(item.status === "downloading"){
let index=downloadTasks.value.findIndex(t => t.id === item.id);
if(index !== -1){
pauseTask(index);
}
}else if(item.status === "paused"){
let index=downloadTasks.value.findIndex(t => t.id === item.id);
if(index !== -1){
resumeTask(index);
}
}
};
const deleteItem = (item, index) => {
uni.showModal({
title: "提示",
content: "确定要删除这个缓存吗?",
success: (res) => {
if (res.confirm) {
// 从store中删除
const taskIndex = downloadTasks.value.findIndex((t) => t.id === item.id);
if (taskIndex !== -1) {
removeTask(taskIndex);
}
}
},
});
};
const formatDuration = (duration) => {
if (!duration) return "00:00:00";
// 如果duration是秒数
if (typeof duration === "number") {
const hours = Math.floor(duration / 3600);
const minutes = Math.floor((duration % 3600) / 60);
const seconds = duration % 60;
return `${String(hours).padStart(2, "0")}:${String(minutes).padStart(
2,
"0"
)}:${String(seconds).padStart(2, "0")}`;
}
// 如果已经是格式化字符串
return duration;
};
const formatSize = (size) => {
if (!size) return "0M";
// 如果size是字节数
const mb = (Number(size) / 1024 / 1024).toFixed(2);
return `${mb}M`;
// 如果已经是格式化字符串
return size;
};
const from = ref('');
onLoad((options) => {
if(options.from){
from.value = options.from;
}
if(options.type){
switchTab('downloading')
}
// 从store同步任务列表
syncTasksFromStore();
// 监听store变化
unsubscribe.value = downloadStore.addListener((tasks) => {
downloadTasks.value = tasks;
});
console.log(11111);
console.log(downloadTasks.value);
// 恢复正在下载的任务
resumeDownloadingTasks();
}),
onShow(() => {
// 页面显示时同步最新任务列表
syncTasksFromStore();
// 检查并恢复下载任务
resumeDownloadingTasks();
}),
onUnload(() => {
// 页面 卸载时取消监听
if (unsubscribe.value) {
unsubscribe.value();
}
});
const getStorageInfo = () => {
// 获取手机存储信息
// #ifdef APP-PLUS
if (uni.getSystemInfoSync().platform !== 'android') {
uni.sendNativeEvent('getSysSpace',{
msg: 'getSysSpace'
},(res) => {
storageInfo.value = {
total: res.total,
available: res.available,
};
});
}else{
// 1. 导入所需的Android类
const Environment = plus.android.importClass("android.os.Environment");
const StatFs = plus.android.importClass("android.os.StatFs");
try {
// 2. 获取内部存储的数据目录(File对象)
const dataDirectory = Environment.getDataDirectory(); // 这是一个Java File对象
// 3. 关键修正:使用 plus.android.invoke 调用 File 对象的 getPath 方法
const dataDirectoryPath = plus.android.invoke(dataDirectory, "getPath");
// 现在 dataDirectoryPath 是一个字符串,表示路径
// 4. 创建 StatFs 对象,传入路径字符串
const statFs = new StatFs(dataDirectoryPath);
// 5. 使用 invoke 调用 StatFs 的方法获取存储块信息
const blockSize = plus.android.invoke(statFs, "getBlockSize");
const availableBlocks = plus.android.invoke(statFs, "getAvailableBlocks");
const totalBlocks = plus.android.invoke(statFs, "getBlockCount");
// 6. 计算可用空间和总空间(单位:字节)
const availableSizeInBytes = blockSize * availableBlocks;
const totalSizeInBytes = blockSize * totalBlocks;
// 7. 转换为常用单位
const availableSizeInGB = (availableSizeInBytes / (1024 * 1024 * 1024)).toFixed(2);
const availableSizeInMB = (availableSizeInBytes / (1024 * 1024)).toFixed(2);
const totalSizeInGB = (totalSizeInBytes / (1024 * 1024 * 1024)).toFixed(2);
// console.log(`可用存储空间:${availableSizeInBytes} 字节`);
// console.log(`可用存储空间:${availableSizeInMB} MB`);
// console.log(`可用存储空间:${availableSizeInGB} GB`);
// 可以根据需要返回不同单位的值
storageInfo.value = {
total: totalSizeInGB+"G",
available: availableSizeInGB+"G",
};
} catch (error) {
console.error('获取存储信息时发生错误:', error);
return null;
}
}
// #endif
};
const syncTasksFromStore=()=> {
downloadTasks.value = downloadStore.getTasks();
};
// 添加下载任务
const addDownloadTask = (item) => {
// 使用store添加任务
const taskIndex = downloadStore.addTask(item);
// 同步任务列表
syncTasksFromStore();
// 开始下载
startDownload(taskIndex);
};
// 开始下载
const startDownload = (index) => {
const taskItem = downloadStore.getTask(index);
if (!taskItem) return;
const task = uni.downloadFile({
url: taskItem.url,
success: (res) => {
if (res.statusCode === 200) {
downloadStore.updateTask(index, {
status: "completed",
filePath: res.tempFilePath,
});
uni.showToast({
title: "下载成功",
icon: "success",
});
console.log(res);
uni.saveFile({
tempFilePath: res.tempFilePath,
success: function (res) {
uni.getSavedFileInfo({
filePath: res.savedFilePath,
success: function (res) {
downloadStore.updateTask(index, {
status: "completed",
realyPath: res.savedFilePath,
size: res.size,
});
},
});
},
});
} else {
downloadStore.updateTask(index, {
status: "failed",
});
uni.showToast({
title: "下载失败",
icon: "none",
});
}
},
fail: (err) => {
downloadStore.updateTask(index, {
status: "failed",
});
console.error("下载失败:", err);
uni.showToast({
title: "下载失败: " + (err.errMsg || "未知错误"),
icon: "none",
duration: 2000,
});
},
});
// 监听下载进度
task.onProgressUpdate((update) => {
const currentTask = downloadStore.getTask(index);
if (currentTask && currentTask.status === "downloading") {
console.log(update.progress);
downloadStore.updateTask(index, {
progress: update.progress,
downloadSize: update.totalBytesWritten,
totalSize: update.totalBytesExpectedToWrite,
});
}
});
// 保存task对象到store(用于暂停/继续)
downloadStore.updateTask(index, {
task: task,
});
};
// 暂停任务
const pauseTask = (index) => {
const taskItem = downloadStore.getTask(index);
if (taskItem && taskItem.task) {
taskItem.task.abort();
downloadStore.updateTask(index, {
status: "paused",
task: null,
});
uni.showToast({
title: "已暂停",
icon: "none",
});
}
};
// 继续任务
const resumeTask = (index) => {
downloadStore.updateTask(index, {
status: "downloading",
progress: 0, // 重新开始(uni.downloadFile不支持断点续传)
});
startDownload(index);
};
// 删除任务
const removeTask = (index) => {
downloadStore.removeTask(index);
syncTasksFromStore();
};
// 打开文件
const openFile = (filePath) => {
// #ifdef APP-PLUS
plus.runtime.openFile(filePath);
// #endif
// #ifndef APP-PLUS
uni.showToast({
title: "请在APP中打开",
icon: "none",
});
// #endif
};
// 获取文件名
const getFileName = (url) => {
if (!url) return "未知文件";
try {
const urlObj = new URL(url);
const pathname = urlObj.pathname;
const fileName = pathname.split("/").pop();
return fileName || "下载文件";
} catch (e) {
// 如果不是完整URL,尝试从路径中提取
const parts = url.split("/");
return parts[parts.length - 1] || "下载文件";
}
};
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
downloading: "下载中",
paused: "已暂停",
completed: "已完成",
failed: "下载失败",
};
return statusMap[status] || "未知";
};
// 恢复正在下载的任务
const resumeDownloadingTasks = () => {
// 使用 nextTick 确保在页面渲染后再恢复下载任务
nextTick(() => {
downloadStore.resumeDownloadingTasks((index) => {
// 重新开始下载(uni.downloadFile不支持断点续传,所以从0开始)
console.log("恢复下载任务:", downloadStore.getTask(index)?.url);
startDownload(index);
});
});
};
onMounted(() => {
getStorageInfo();
});
</script>
<style lang="scss" scoped>
page{
background-color: #ffffff;
}
.size-box{
display: flex;
white-space: nowrap;
justify-content: flex-end;
white-space: nowrap;
font-size: 24rpx;
color:#8b2316
}
.box{
display: flex;
white-space: nowrap;
justify-content: flex-end;
margin-left: 30rpx;
text{
width: 40rpx;
display: flex;
white-space: nowrap;
font-size: 24rpx;
color:#8b2316
}
}
.download-video-page {
width: 100%;
margin-top: calc(var(--status-bar-height) + 44px);
height: calc(100vh - var(--status-bar-height) - 44px);
background-color: #ffffff;
display: flex;
flex-direction: column;
}
.footer-btn{
display: flex;
align-items: center;
justify-content: space-between;
height: 100rpx;
background-color: #ffffff;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: center;
.select{
flex:1;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
color: #333;
}
.del{
height: 100rpx;
flex:1;
color:red;
display: flex;
align-items: center;
justify-content: center;
}
.bar{
width: 1px;
height: 40rpx;
background-color: #999;
}
}
.video-progress-bar {
flex:1;
height: 10rpx;
background-color: #f0f0f0;
border-radius: 5px;
}
.video-progress-bar-inner {
height: 10rpx;
background-color: #8b2316;
border-radius: 5px;
}
.nav-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 44px;
padding: 0 15px;
background-color: #ffffff;
position: relative;
}
.nav-back {
width: 40px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
}
.back-icon {
font-size: 28px;
color: #8b2316;
font-weight: bold;
line-height: 1;
}
.nav-title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 18px;
font-weight: 500;
color: #8b2316;
}
.nav-edit {
width: 40px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #333333;
}
.tabs-container {
display: flex;
align-items: center;
height: 50px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
position: sticky;
top: calc(var(--status-bar-height) + 44px);
z-index: 100;
}
.tab-item {
flex: 1;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #999999;
position: relative;
&.active {
color: #8b2316;
font-weight: 500;
}
}
.tab-divider {
width: 1px;
height: 20px;
background-color: #e0e0e0;
}
.content-scroll {
flex: 1;
overflow-y: auto;
padding-bottom: 200rpx;
background-color: #ffffff;
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
padding: 100px 0;
color: #999999;
font-size: 14px;
}
.video-item {
display: flex;
align-items: center;
padding: 15px;
border-bottom: 1px solid #f0f0f0;
position: relative;
}
.video-thumbnail {
width: 120px;
height: 80px;
border-radius: 4px;
background-color: #f5f5f5;
margin-right: 12px;
flex-shrink: 0;
}
.video-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 80px;
}
.video-title {
font-size: 15px;
color: #333333;
line-height: 1.4;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.video-author {
font-size: 13px;
color: #666666;
margin-bottom: 8px;
}
.video-meta {
display: flex;
align-items: center;
font-size: 12px;
color: #999999;
}
.video-status{
display: flex;
justify-content: space-between;
.status-icon{
display: flex;
align-items: center;
white-space: nowrap;
justify-content: center;
image{
width:30rpx;
height: 29rpx;
margin-right: 10rpx;
}
text{
white-space: nowrap;
font-size: 24rpx;
color: #999999;
}
}
}
.meta-icon {
margin-right: 4px;
display: inline-flex;
align-items: center;
}
.meta-text {
margin-right: 12px;
}
.meta-size {
margin-left: auto;
}
.checkbox-wrapper {
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.checkbox {
width: 20px;
height: 20px;
border: 1px solid #d0d0d0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background-color: #ffffff;
transition: all 0.2s;
&.checked {
background-color: #8b2316;
border-color: #8b2316;
}
}
.delete-btn {
position: absolute;
right: 15px;
top: 50%;
transform: translateY(-50%);
padding: 8px 16px;
background-color: #ff4444;
color: #ffffff;
border-radius: 4px;
font-size: 14px;
}
.bottom-box {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
background-color: #ffffff;
z-index: 100;
}
.storage-info {
padding: 12px 15px;
background-color: #f8f8f8;
border-top: 1px solid #f0f0f0;
font-size: 12px;
color: #666666;
text-align: center;
}
</style>
+16 -8
View File
@@ -3,17 +3,18 @@
<!-- 顶部导航栏 -->
<navBar title="意见反馈"></navBar>
<navBar :title="'投诉反馈'"></navBar>
<!-- 反馈输入区域 -->
<view class="feedback-input-container">
<view class="input-area">
<text class="input-placeholder" v-show="!feedbackText">欢迎对我们的软件提供建议,帮助我们更好更快的改进"肝胆相照"APP (200字以内)</text>
<!-- <text class="input-placeholder" v-show="!feedbackText">欢迎对我们的软件提供建议,帮助我们更好更快的改进"肝胆相照"APP (200字以内)</text> -->
<textarea
class="feedback-textarea"
v-model="feedbackText"
:maxlength="200"
placeholder=""
placeholder="尊敬的医生您好,若在交流过程中发现患者发布不适当内容,请您及时填写相关信息,我们将第一时间处理,谢谢您的支持。"
:adjust-position="false"
@input="onInput"
></textarea>
<!-- <view class="voice-button" @click="toggleVoiceInput">
@@ -24,7 +25,7 @@
<!-- 底部提交按钮 -->
<view class="submit-container">
<button class="submit-btn" @click="submitFeedback" :disabled="!feedbackText.trim()">
<button class="submit-btn" @click="submitFeedback" >
提交
</button>
</view>
@@ -126,8 +127,15 @@
/* 反馈输入区域 */
.feedback-input-container {
top: calc(var(--status-bar-height) + 44px);
position: fixed;
left: 0;
right: 0;
bottom: 160rpx;
overflow-y: scroll;
z-index: 1;
padding: 30rpx;
flex: 1;
box-sizing: border-box;
}
.input-area {
@@ -199,10 +207,10 @@
.submit-btn {
width: 100%;
height: 88rpx;
background-color: #ffffff;
border: 2rpx solid #e0e0e0;
background-color: #fff;
border: 2rpx solid #8B2316;
border-radius: 8rpx;
color: #ff0000;
color: #8B2316;
font-size: 32rpx;
font-weight: 500;
display: flex;
+20 -6
View File
@@ -1,6 +1,7 @@
<template>
<view class="visit-note-page">
<uni-nav-bar
<navBar :title="'随访详情'" />
<!-- <uni-nav-bar
left-icon="left"
title="随访详情"
@clickLeft="goBack"
@@ -11,9 +12,10 @@
backgroundColor="#eee"
>
</uni-nav-bar>
</uni-nav-bar> -->
<!-- 患者行 -->
<view class="contentbox">
<view class="row" @click="noop">
<text class="row-label">患者</text>
<text class="row-value">{{ patientName }}</text>
@@ -27,7 +29,8 @@
<!-- 输入框区域 -->
<view class="note-box">
<textarea class="note-textarea" v-model.trim="note" placeholder="请填写随访内容" auto-height :maxlength="200" />
<textarea class="note-textarea" v-model.trim="note" placeholder="请填写随访内容" auto-height :maxlength="200" :adjust-position="false"/>
</view>
</view>
<!-- 底部删除按钮 -->
<view class="fixed-footer">
@@ -41,6 +44,7 @@
import { onLoad,onShow } from '@dcloudio/uni-app'
import api from '@/api/api.js'
const patientName = ref('')
import navBar from '@/components/navBar/navBar.vue'
const note = ref('请近日来医院复诊、复查')
const datetime = ref('')
const followUpUuid = ref('')
@@ -91,19 +95,29 @@
</script>
<style lang="scss" scoped>
.contentbox{
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
overflow-y: scroll;
bottom:128rpx;
z-index:1;
}
.visit-note-page{
min-height: 100vh; background:#f5f5f5; padding-bottom: 120rpx;
background:#f5f5f5; padding-bottom: 120rpx;
}
.nav-right{ display:flex; align-items:center; }
.submit-text{ color:#8B2316; font-size: 30rpx; }
.row{
margin-top: 20rpx; background:#fff; padding: 26rpx 30rpx; display:flex; align-items:center; justify-content:space-between; border-bottom: 1rpx solid #eee;
background:#fff; padding: 26rpx 30rpx; display:flex; align-items:center; justify-content:space-between; border-bottom: 1rpx solid #eee;
.row-label{ font-size: 32rpx; color:#8B2316; font-weight: 500; }
.row-value{ font-size: 32rpx; color:#333; }
}
.section-title{ background:#fff; padding: 22rpx 30rpx; color:#8B2316; font-size: 30rpx; border-top: 1rpx solid #eee; border-bottom: 1rpx solid #eee; }
.section-title{ background:#fff; padding: 22rpx 30rpx; color:#8B2316; font-size: 30rpx;border-bottom: 1rpx solid #eee; }
.note-box{ background:#fff; padding: 20rpx 24rpx; }
.note-textarea{
+10 -6
View File
@@ -158,12 +158,17 @@ const countConsult=()=>{
}
const answerConsult=async()=>{
if(hasAnswer.value){
let userId=uni.getStorageSync('userInfo').uuid.toLowerCase();
let conversationId=userId+'|1|'+patientUuid.value.toLowerCase();
await uni.$UIKitStore.uiStore.selectConversation(conversationId)
navTo({
url:'/pages_chat/chat/index?from=consult&patientUuid='+patientUuid.value
uni.sendNativeEvent('goConsultPage', {
msg: 'chat'
},ret => {
console.log(ret);
})
// let userId=uni.getStorageSync('userInfo').uuid.toLowerCase();
// let conversationId=userId+'|1|'+patientUuid.value.toLowerCase();
// await uni.$UIKitStore.uiStore.selectConversation(conversationId)
// navTo({
// url:'/pages_chat/chat/index?from=consult&patientUuid='+patientUuid.value
// })
}else{
countConsult()
}
@@ -324,7 +329,6 @@ function editQuestion() {
border-radius: 16rpx;
margin-bottom: 24rpx;
padding: 0 30rpx;
margin: 0 30rpx;
.user-info {
display: flex;
align-items: center;
+39 -9
View File
@@ -1,12 +1,14 @@
<template>
<view class="group-edit-page">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
:title="groupUuid ? '编辑分组' : '新建分组'"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -15,6 +17,8 @@
</template>
</uni-nav-bar>
</view>
<view class="contentbox">
<!-- 分组名称 -->
<view class="section-header">分组名称</view>
<view class="name-row">
@@ -28,16 +32,14 @@
<view class="section-header">分组成员</view>
<view class="add-member" @click="addMember">
<view class="add-circle">
<up-icon name="plus" size="34" color="#bfbfbf" />
</view>
<up-image :src="addImg" width="120rpx" height="120rpx" />
<text class="add-text">添加组患者</text>
</view>
<!-- 已选中的成员 -->
<view class="selected-members" v-if="members.length > 0">
<view class="selected-item" v-for="(m, idx) in members" :key="m.uuid || idx">
<image class="selected-avatar" :src="docUrl + (m.photo || '')" mode="aspectFill" />
<text class="selected-name">{{ m.realName || '未知' }}</text>
<image class="selected-avatar" :src="getMemberAvatar(m)" mode="aspectFill" @error="handleMemberAvatarError(m)" />
<text class="selected-name">{{ m.nickname || m.realName }}</text>
<view class="remove-selected" @click="removeMember(idx)">
<text class="remove-text">——</text>
<!-- <up-icon name="minus" size="43rpx" color="#fff" bold /> -->
@@ -46,6 +48,7 @@
</view>
</view>
<!-- 底部删除按钮 -->
@@ -63,12 +66,35 @@
import navTo from '@/utils/navTo.js'
import docUrl from '@/utils/docUrl.js'
import delImg from "@/static/iv_delete.png"
import addImg from "@/static/addpatient.png"
import api from '@/api/api.js'
import defaultImg from "@/static/default.png"
import unidialog from '@/components/dialog/dialog.vue'
const groupUuid = ref('')
const groupName = ref('')
const members = ref([])
const visible = ref(false)
const avatarErrorMap = ref({})
const getAvatarKey = (member = {}) => String(member.uuid || member.id || member.realName || '')
const normalizeAvatarUrl = (photo) => {
const raw = String(photo || '').trim()
if (!raw) return defaultImg
if (/^https?:\/\//i.test(raw)) return raw
return `${docUrl}${raw}`
}
const getMemberAvatar = (member = {}) => {
const key = getAvatarKey(member)
if (key && avatarErrorMap.value[key]) return defaultImg
return normalizeAvatarUrl(member.photo)
}
const handleMemberAvatarError = (member) => {
const key = getAvatarKey(member)
if (!key) return
avatarErrorMap.value = {
...avatarErrorMap.value,
[key]: true
}
}
const confirmDelete = () => {
deleteGroup();
}
@@ -182,6 +208,7 @@
})
}
const patientListByGroup = async () => {
avatarErrorMap.value = {}
const res = await api.patientListByGroup({
group_uuid: groupUuid.value,
list_sort:0
@@ -194,19 +221,22 @@
</script>
<style lang="scss" scoped>
.contentbox{
margin-top: calc(var(--status-bar-height) + 44px);
}
.group-edit-page{
min-height: 100vh;
background: #f5f5f5;
padding-bottom: 160rpx;
}
.save-text{
background:#8B2316;
color:#8B2316;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 15rpx 25rpx;
color:#fff;
border-radius: 12rpx;
}
+26 -8
View File
@@ -1,12 +1,12 @@
<template>
<view class="group-manage-page">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="分组管理"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -14,7 +14,9 @@
<view class="save-text" @click="saveGroups">保存</view>
</template>
</uni-nav-bar>
</view>
<view class="content-box">
<!-- 已选中的分组 -->
<view class="selected-section">
<view class="tag-list">
@@ -30,7 +32,7 @@
<!-- 所有分组 -->
<view class="all-groups-section">
<view class="section-title">所有分组(选择分组)</view>
<view class="section-title"><text calss="red">所有分组</text>(选择分组)</view>
<view class="tag-list">
<view
class="tag-item"
@@ -43,6 +45,7 @@
</view>
</view>
</view>
</view>
<!-- 底部添加按钮 -->
<view class="bottom-bar">
@@ -80,7 +83,9 @@ const loadAllGroups = async () => {
try {
const res = await api.groupList();
if (res.code === 200) {
allGroups.value = res.data || [];
const hiddenGroupNames = new Set(['待分组患者', '肝系一生非患教组', '肝系一生患教组']);
const arr = (res.data || []).filter((item) => !hiddenGroupNames.has(String(item?.name || '').trim()));
allGroups.value = arr;
}
} catch (e) {
console.error("加载分组列表失败:", e);
@@ -165,10 +170,20 @@ const saveGroups = () => {
font-size: 30rpx;
color: #333;
}
.content-box{
position: fixed;
left: 0;
right: 0;
top: calc(var(--status-bar-height) + 44px);
bottom: 140rpx;
overflow-y: scroll;
z-index: 1;
}
.selected-section {
min-height: 300rpx;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
border-bottom: 20rpx solid #f0f0f0;
}
.all-groups-section {
@@ -176,9 +191,12 @@ const saveGroups = () => {
}
.section-title {
font-size: 28rpx;
color: #666;
font-size: 30rpx;
color: #999;
margin-bottom: 20rpx;
.red{
color: #8b2316;
}
}
.tag-list {
@@ -233,7 +251,7 @@ const saveGroups = () => {
bottom: 0;
background: #fff;
border-top: 1rpx solid #f0f0f0;
padding: 20rpx 30rpx env(safe-area-inset-bottom);
padding: 20rpx 30rpx 20rpx;
}
.add-btn {
+229 -74
View File
@@ -1,7 +1,7 @@
<template>
<view class="zhinan-list-page">
<!-- 头部导航栏 -->
<uni-nav-bar
<!-- <uni-nav-bar
left-icon="left"
:title="title"
@clickLeft="goBack"
@@ -12,7 +12,8 @@
backgroundColor="#eeeeee"
>
</uni-nav-bar>
</uni-nav-bar> -->
<navBar :title="title" />
<!-- 使用scroll-view实现列表 -->
<scroll-view
@@ -39,19 +40,16 @@
<view class="item-date">{{ formatDate(item.releaseTime) }}</view>
<!-- 操作按钮 -->
<view class="item-action">
<view
class="download-btn"
@click.stop="viewGuideline(item)"
>
<up-icon name="download" color="#8D2316" size="28"></up-icon>
<view class="download-btn" @click.stop="downLoad(item)" v-if="!downLoadtaskList.includes(item.uuid)">
<!-- <up-icon name="download" color="#8D2316" size="28" ></up-icon> -->
<up-image :src="downIng" width="35rpx" height="35rpx"></up-image>
<!-- <uni-icons type="arrow-down" size="26" color="#8D2316"></uni-icons> -->
</view>
<!-- <view
<view
v-else
class="view-btn"
@click.stop="viewGuideline(item)"
>
查看
</view> -->
>查看</view>
</view>
</view>
</view>
@@ -73,22 +71,25 @@
</view>
<!-- 无数据提示 -->
<view class="no-data" v-if="guidelinesList.length === 0 && !isLoading">
<empty v-if="guidelinesList.length === 0 && !isLoading"></empty>
<!-- <view class="no-data" v-if="guidelinesList.length === 0 && !isLoading">
<uni-icons type="info" size="60" color="#999"></uni-icons>
<text>暂无诊疗指南数据</text>
</view>
</view> -->
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted} from 'vue'
import { ref, onMounted,computed,nextTick} from 'vue'
import api from '@/api/api.js'
import { onShow,onLoad } from "@dcloudio/uni-app";
import docUrl from "@/utils/docUrl.js"
import navTo from "@/utils/navTo.js"
import hotImg from "@/static/hot_booklist.png"
import navBar from "@/components/navBar/navBar.vue"
import empty from "@/components/empty/empty.vue"
// 响应式数据
const guidelinesList = ref([])
const isLoading = ref(false)
@@ -99,6 +100,7 @@ const isRefreshing = ref(false)
const keywords=ref('');
import upImg from "@/static/triangle_green_theme.png"
import downImg from "@/static/triangle_normal.png"
import downIng from "@/static/icon_library_down.png";
const loadMoreStatus = ref('more') // 'loading', 'more', 'noMore'
const typeUuid=ref('');
const title=ref('');
@@ -107,6 +109,12 @@ const from=ref('');
const name=ref('');
const showInnerSort=ref(false);
const innerSortTitle=ref('上传时间');
import downloadStore from "@/store/downloadStoreFile.js";
import isAndroid from '@/utils/platform';
const downloadTasks=ref([]);
const downLoadtaskList=computed(()=>{
return downloadTasks.value.filter((item)=>item.status == "completed" && item.type == "zhinan").map((item)=>item.id);
});
const chooseInnerSort=(index)=>{
sort.value=index;
if(index==1){
@@ -144,12 +152,20 @@ const toggleInnerSort=()=>{
showInnerSort.value=!showInnerSort.value;
}
onShow(() => {
syncTasksFromStore();
// 监听store变化
downloadStore.addListener((tasks) => {
downloadTasks.value = tasks;
});
// 恢复正在下载的任务
resumeDownloadingTasks();
// 页面显示时,如果列表为空则加载数据
if (guidelinesList.value.length === 0) {
if(from.value){
top10ByType();
}else{
loadGuidelinesList(true)
loadGuidelinesList(false)
}
}
})
@@ -246,15 +262,16 @@ const loadGuidelinesList = async (isRefresh = false) => {
try {
isLoading.value = true
loadMoreStatus.value = 'loading'
let expertUuid=uni.getStorageSync('userInfo').uuid;
const params = {
page: currentPage.value,
pageSize: pageSize.value,
expertUUid:expertUuid,
type: 1, // 诊疗指南类型
sort:2,
name:name.value,
typeUuid:typeUuid.value,
keywords:keywords.value
keyWords:keywords.value
}
const res = await api.searchLibraryU(params)
@@ -272,7 +289,7 @@ const loadGuidelinesList = async (isRefresh = false) => {
}
// 判断是否还有更多数据
if (newData.length < pageSize.value) {
if (newData.length < res.data.pageSize) {
loadMoreStatus.value = 'noMore'
hasMoreData.value = false
console.log('没有更多数据了');
@@ -359,66 +376,94 @@ const downloadGuideline = (item) => {
})
}
}
const downLoad = (item) => {
if(!item.path){
uni.showToast({
title: "暂无下载链接",
icon: "none",
});
return;
}
// 查看指南(支持常用文档直链:pdf/doc/docx/xls/xlsx/ppt/pptx/txt)
const viewGuideline = (item) => {
const url = docUrl+item.path
if (url) {
const isPDF = /\.(pdf)(\?|$)/i.test(url)
const isOffice = /\.(docx?|xlsx?|pptx?)(\?|$)/i.test(url)
const isText = /\.(txt)(\?|$)/i.test(url)
// H5 端
// #ifdef H5
if (isPDF) {
try { window.open(url, '_blank') } catch (e) {
uni.navigateTo({ url: `/pages_app/webview/webview?url=${encodeURIComponent(url)}` })
}
return
}
if (isOffice) {
const officeUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`
try { window.open(officeUrl, '_blank') } catch (e) {
uni.navigateTo({ url: `/pages_app/webview/webview?url=${encodeURIComponent(officeUrl)}` })
}
return
}
if (isText) {
uni.navigateTo({ url: `/pages_app/webview/webview?url=${encodeURIComponent(url)}` })
return
}
// 其他格式统一用 webview 打开
uni.navigateTo({ url: `/pages_app/webview/webview?url=${encodeURIComponent(url)}` })
return
// #endif
// 非 H5 端:下载后尝试用 openDocument,失败回退 webview
// #ifndef H5
uni.showToast({ title: '正在打开...', icon: 'none' })
uni.downloadFile({
url,
success: (res) => {
if (res.statusCode === 200 && res.tempFilePath) {
uni.openDocument({
filePath: res.tempFilePath,
showMenu: true,
success: () => {},
fail: () => {
uni.navigateTo({ url: `/pages_app/webview/webview?url=${encodeURIComponent(url)}` })
}
})
} else {
uni.navigateTo({ url: `/pages_app/webview/webview?url=${encodeURIComponent(url)}` })
}
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "magazine_donwload",
page_type: "诊疗指南",
resource_name:item.title,
belong:title.value,
number:1,
resource_option:item.tags
},
fail: () => {
uni.navigateTo({ url: `/pages_app/webview/webview?url=${encodeURIComponent(url)}` })
},
(ret) => {
console.log(ret);
});
addDownloadTask({
url: docUrl + item.path,
title: item.title,
id: item.uuid,
type: 'zhinan',
releaseTime: item.releaseTime,
});
};
// 查看指南(支持常用文档直链:pdf/doc/docx/xls/xlsx/ppt/pptx/txt)
const viewGuideline = (cell) => {
console.log(cell);
if (!cell.path) {
uni.showToast({
title: "暂无下载链接",
icon: "none",
});
return;
}
})
return
// #endif
console.log('下载列表');
console.log(downloadTasks.value);
downloadTasks.value.forEach(item => {
if(item.id == cell.uuid) {
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "detail_page",
page_type: "诊疗指南",
resource_name:cell.title,
resource_option:cell.tags
},
},
(ret) => {
console.log(ret);
});
// if(isAndroid){
plus.runtime.openFile(item.localPath, function(e) {
console.log('打开成功');
}, function(e) {
uni.showToast({
title: '打开失败:' + e.message,
icon: "none",
duration: 2000,
});
console.log('打开失败:' + e.message);
});
// }else{
// uni.sendNativeEvent('openDocument', {
// msg: item.localPath.replaceAll('_doc/uniapp_save/',''),
// }, ret => {
// console.log(ret);
// });
// }
// uni.openDocument({
// filePath: item.localPath,
// fileType: 'pdf',
// success: () => console.log('打开成功'),
// fail: (err) => {
// uni.showToast({
// title: err,
// icon: "none",
// });
// }
// });
}
// 无直链,回退到详情页
});
}
@@ -463,6 +508,116 @@ const formatDate = (dateString) => {
return dateString
}
}
// 添加下载任务
const addDownloadTask = (item) => {
// 使用store添加任务
const taskIndex = downloadStore.addTask(item);
// 同步任务列表
syncTasksFromStore();
// 开始下载
startDownload(taskIndex);
};
// 开始下载
const startDownload = (index) => {
const taskItem = downloadStore.getTask(index);
if (!taskItem) return;
if(taskItem.status == "loading"){
return false;
};
uni.showToast({
title: "正在下载中",
icon: "none",
});
downloadStore.updateTask(index, {
status: "loading"
});
const task = uni.downloadFile({
url: taskItem.url,
success: (res1) => {
//console.log("res1:"+JSON.stringify(res1));
if (res1.statusCode === 200) {
downloadStore.updateTask(index, {
status: "completed",
filePath: res1.tempFilePath,
});
uni.showToast({
title: "下载成功",
icon: "none",
});
uni.saveFile({
tempFilePath: res1.tempFilePath,
success: function (res2) {
console.log("res2:"+JSON.stringify(res2));
uni.getSavedFileInfo({
filePath: res2.savedFilePath,
success: function (res) {
console.log("res:"+JSON.stringify(res));
console.log("size:"+res.size);
downloadStore.updateTask(index, {
status: "completed",
localPath: res2.savedFilePath,
});
},
});
},
});
} else {
downloadStore.updateTask(index, {
status: "failed",
});
uni.showToast({
title: "下载失败",
icon: "none",
});
}
},
fail: (err) => {
downloadStore.updateTask(index, {
status: "failed",
});
uni.showToast({
title: "下载失败: " + (err.errMsg || "未知错误"),
icon: "none",
duration: 2000,
});
},
});
// 监听下载进度
task.onProgressUpdate((update) => {
const currentTask = downloadStore.getTask(index);
if (currentTask && currentTask.status === "downloading") {
downloadStore.updateTask(index, {
progress: update.progress
});
}
});
// 保存task对象到store(用于暂停/继续)
downloadStore.updateTask(index, {
task: task,
});
};
const syncTasksFromStore = () => {
downloadTasks.value = downloadStore.getTasks();
};
// 恢复正在下载的任务
const resumeDownloadingTasks = () => {
// 使用 nextTick 确保在页面渲染后再恢复下载任务
nextTick(() => {
downloadStore.resumeDownloadingTasks((index) => {
// 重新开始下载(uni.downloadFile不支持断点续传,所以从0开始)
console.log("恢复下载任务:", downloadStore.getTask(index)?.url);
startDownload(index);
});
});
};
</script>
<style lang="scss" scoped>
+206 -15
View File
@@ -1,25 +1,47 @@
<template>
<view class="bank-card-page">
<!-- 顶部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="常用银行卡"
@clickLeft="goBack()"
right-text="添加"
@clickLeft="goBack"
right-icon="plusempty"
right-icon-color="#8B2316"
@clickRight="addBankCard"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
>
</uni-nav-bar>
</view>
<!-- 银行卡列表 -->
<view class="card-list">
<view class="card-item" v-for="(card, index) in bankCards" :key="card.uuid">
<uni-swipe-action>
<uni-swipe-action-item
v-for="(card, index) in bankCards"
:key="card.uuid"
@change="onSwipeChange($event, card)"
>
<template v-slot:right>
<view class="swipe-right">
<view class="card-delete-btn" @click.stop="handleDeleteBankCard(card)">删除</view>
</view>
</template>
<view class="card-item" :class="{ 'is-swiping': openedSwipeUuid === card.uuid }">
<view class="card-logo">
<view class="logo-bg">
<image
v-if="getBankLogoImg(card.open_bank)"
class="bank-logo-img"
:src="getBankLogoImg(card.open_bank)"
mode="aspectFit"
></image>
<view v-else class="logo-bg">
<text class="logo-text">{{ getBankLogo(card.open_bank) }}</text>
</view>
</view>
@@ -28,8 +50,14 @@
<view class="bank-name">{{ card.open_bank }}</view>
</view>
</view>
</uni-swipe-action-item>
</uni-swipe-action>
</view>
<empty v-if="bankCards.length === 0"></empty>
<view class="empty-container" v-if="bankCards.length === 0">
<image :src="emptyBankImg" class="empty-image"></image>
<text class="empty-text">使用常用卡,支付更快捷</text>
</view>
<!-- <empty v-if="bankCards.length === 0" :imgWidth="'218rpx'" :imgHeight="'164rpx'" :emptyImage="emptyBankImg" emptyDesc="使用常用卡,支付更快捷"></empty> -->
<!-- 底部导航指示器 -->
<view class="bottom-indicator"></view>
@@ -41,27 +69,127 @@ import { ref, onMounted } from 'vue';
import navTo from '@/utils/navTo';
import api from '@/api/api';
import empty from "@/components/empty/empty.vue"
import emptyBankImg from "@/static/empty_bank.png"
const bankCards = ref([]);
const openedSwipeUuid = ref('');
import { onLoad,onBackPress,onShow } from '@dcloudio/uni-app';
const from = ref('');
const returnTo = ref('');
onLoad((options) => {
if(options.from){
from.value = options.from;
}
if (options.returnTo) {
returnTo.value = decodeURIComponent(options.returnTo);
}
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "常用银行卡",
},
});
});
onShow(() => {
getBankCardList();
});
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
// 银行卡数据
const getBankCardList = () => {
api.bankCardList({}).then(res => {
if (res.code === 200 && res.data.bankList) {
bankCards.value = res.data.bankList;
if (res.code === 200 && res.data) {
bankCards.value = res.data;
}
});
};
const goBack = () => {
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
};
const addBankCard = () => {
const target = returnTo.value
? `/pages_app/idcardAuth/idcardAuth?returnTo=${encodeURIComponent(returnTo.value)}`
: '/pages_app/idcardAuth/idcardAuth';
navTo({
url: '/pages_app/idcardAuth/idcardAuth'
url: target
});
};
const bankLogoMap = {
'工商银行': '/static/icon_bankcard_gongshang.png',
'中国银行': '/static/icon_bankcard_zhongguo.png',
'建设银行': '/static/icon_bankcard_jianseyinhang.png',
'农业银行': '/static/icon_bankcard_nongyeyinhang.png',
'交通银行': '/static/icon_bankcard_jiatong.png',
'招商银行': '/static/icon_bankcard_zhaoshangyinhang.png',
'民生银行': '/static/icon_bankcard_minshneg.png',
'兴业银行': '/static/icon_bankcard_xingye.png',
'浦发银行': '/static/icon_bankcard_shanghaipudongfazhanyinhang.png',
'光大银行': '/static/icon_bankcard_guangda.png',
'华夏银行': '/static/icon_bankcard_huaxiayinhnag.png',
'中信银行': '/static/icon_bankcard_zhongxinshiye.png',
'平安银行': '/static/icon_bankcard_pingan.png',
'广发银行': '/static/icon_bankcard_guangfa.png',
'邮储银行': '/static/icon_bankcard_youzheng.png',
'上海银行': '/static/icon_bankcard_shanghai.png',
'北京银行': '/static/icon_bankcard_beijingyinhang.png'
};
const getBankLogoImg = (bankName) => {
let name1=bankName.split(',')[0];
let name2=bankName.split(',')[1];
return bankLogoMap[name1] || bankLogoMap[name2];
};
const handleDeleteBankCard = (card) => {
uni.showModal({
title: '确认删除',
content: `确定删除尾号${card.card_number}的银行卡吗?`,
success: async (modalRes) => {
if (!modalRes.confirm) return;
try {
const res = await api.deleteBankCard({
uuid: card.uuid
});
if (res.code === 200) {
uni.showToast({
title: '删除成功',
icon: 'none'
});
getBankCardList();
return;
}
uni.showToast({
title: res.msg || '删除失败',
icon: 'none'
});
} catch (error) {
uni.showToast({
title: '网络错误,请重试',
icon: 'none'
});
}
}
});
};
const onSwipeChange = (e, card) => {
const state = typeof e === 'string'
? e
: e?.position || e?.show || e?.detail?.position || e?.detail?.show || 'none';
openedSwipeUuid.value = state === 'right' ? card.uuid : '';
};
// 获取银行logo文字
const getBankLogo = (bankName) => {
const bankLogos = {
@@ -84,34 +212,96 @@ const getBankLogo = (bankName) => {
return bankLogos[bankName] || bankName.charAt(0);
};
onMounted(() => {
getBankCardList();
});
</script>
<style lang="scss" scoped>
.empty-container {
height:800rpx;
min-width:600rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.empty-image {
width: 218rpx;
height: 164rpx;
}
.empty-text {
margin-top: 20rpx;
font-size: 30rpx;
color: #999;
}
}
.bank-card-page {
min-height: 100vh;
background: #F5F5F5;
position: relative;
:deep(.uni-nav-bar-right-text) {
font-size: 32rpx;
}
:deep(.uni-swipe){
margin-bottom: 20rpx!important;
}
}
/* 银行卡列表样式 */
.card-list {
top: calc(var(--status-bar-height) + 44px);
position: fixed;
left: 0;
right: 0;
bottom: 0rpx;
overflow-y: scroll;
z-index: 1;
padding: 30rpx;
box-sizing: border-box;
.swipe-right {
height: 100%;
display: flex;
align-items: stretch;
padding-right: 0;
box-sizing: border-box;
}
.card-delete-btn {
min-width: 112rpx;
height: 100%;
padding: 0 24rpx;
border-radius: 0 16rpx 16rpx 0;
background: #E60012;
color: #ffffff;
font-size: 24rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.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);
&.is-swiping {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.card-logo {
margin-right: 30rpx;
.bank-logo-img {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
.logo-bg {
width: 80rpx;
height: 80rpx;
@@ -145,6 +335,7 @@ onMounted(() => {
color: #666666;
}
}
}
}
+139 -46
View File
@@ -1,17 +1,20 @@
<template>
<view class="idcard-auth-page">
<!-- 顶部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
:title="currentStep === 1 ? '身份验证' : '添加银行卡'"
@clickLeft="goBack()"
fixed
@clickLeft="goBack"
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
>
</uni-nav-bar>
</view>
<!-- 内容区域 -->
<view class="content-area">
<!-- 进度指示器 -->
@@ -65,6 +68,7 @@
class="form-input"
placeholder="请输入您的姓名"
v-model="formData.name"
:adjust-position="false"
placeholder-style="color: #cccccc"
/>
</view>
@@ -73,6 +77,7 @@
<input
class="form-input"
placeholder="请输入您的身份证号"
:adjust-position="false"
v-model="formData.idNumber"
placeholder-style="color: #cccccc"
/>
@@ -85,28 +90,34 @@
<text class="form-label">银行卡号</text>
<view class="input-container">
<input
class="form-input"
class="form-input card-number-input"
:class="{ 'is-filled': !!formData.cardNumber }"
placeholder="仅限借记卡"
v-model="formData.cardNumber"
:value="cardNumberDisplay"
:adjust-position="false"
@input="onCardNumberInput"
placeholder-style="color: #cccccc"
/>
<view class="info-icon">
<text class="icon-text">!</text>
</view>
<view class="info-icon" @click="showInfo('card')">
<text class="icon-text">!</text>
</view>
</view>
<view class="form-item">
<text class="form-label">手机号</text>
<view class="input-container">
<input
class="form-input"
class="form-input card-number-input"
placeholder="银行预留手机号"
v-model="formData.mobile"
:adjust-position="false"
placeholder-style="color: #cccccc"
/>
<view class="info-icon">
<text class="icon-text">!</text>
</view>
<view class="info-icon" @click="showInfo('phone')">
<text class="icon-text">!</text>
</view>
</view>
</view>
@@ -145,6 +156,18 @@
</view>
</view>
</view>
<unidialog
:visible="cardVisible"
:showCancel="false"
:title="cardTitle"
:content="cardContent"
:showConfirm="true"
:confirmText="'知道了'"
@close="cardVisible=false"
@confirm="cardVisible=false"
>
</unidialog>
</template>
<script setup>
@@ -153,7 +176,11 @@ 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';
import unidialog from '@/components/dialog/dialog.vue';
import { onLoad } from '@dcloudio/uni-app';
const cardContent = ref('暂支持以下银行:农业银行、建设银行、光大银行、平安银行、兴业银行、中信银行、邮政储蓄银行、民生银行、中国银行、工商银行、交通银行、浦发银行、广发银行、华夏银行、招商银行、北京银行、上海银行。');
const cardTitle = ref('银行卡说明');
const cardVisible = ref(false);
const formData = ref({
name: '',
idNumber: '',
@@ -163,13 +190,29 @@ const formData = ref({
const isLoading = ref(false);
const currentStep = ref(1); // 当前步骤,1表示身份信息,2表示添加银行卡
const showInfo = (type) => {
if (type === 'card') {
cardTitle.value = '银行卡说明';
cardContent.value = '暂支持以下银行:农业银行、建设银行、光大银行、平安银行、兴业银行、中信银行、邮政储蓄银行、民生银行、中国银行、工商银行、交通银行、浦发银行、广发银行、华夏银行、招商银行、北京银行、上海银行。';
} else if (type === 'phone') {
cardTitle.value = '手机号说明';
cardContent.value = '银行预留的手机号码是办理银行卡时所填写的手机号码。没有预留,手机号码已经忘记或者已经停用,请联系银行客服更新处理';
}
cardVisible.value = true;
};
// 短信弹框相关
const showSmsDialog = ref(false);
const smsCode = ref('');
const sendingCode = ref(false);
const countdown = ref(0);
let countdownTimer = null;
const returnTo = ref('');
onLoad((options) => {
if (options.returnTo) {
returnTo.value = decodeURIComponent(options.returnTo);
}
});
const maskedMobile = computed(() => {
const m = formData.value.mobile || '';
@@ -178,6 +221,18 @@ const maskedMobile = computed(() => {
}
return m || '***********';
});
const formatCardNumber = (value) => {
const digits = String(value || '').replace(/\D/g, '').slice(0, 19);
return digits.replace(/(\d{4})(?=\d)/g, '$1 ');
};
const cardNumberDisplay = computed(() => formatCardNumber(formData.value.cardNumber));
const onCardNumberInput = (e) => {
const value = e?.detail?.value || '';
formData.value.cardNumber = String(value).replace(/\D/g, '').slice(0, 19);
};
const goBack = () => {
uni.navigateBack();
};
@@ -246,7 +301,7 @@ const onNextStep = async () => {
}
} catch (error) {
console.error('身份验证失败:', error);
uni.showToast({ title: '网络错误,请重试', icon: 'none' });
uni.showToast({ title: error.data.message || error.message, icon: 'none' });
isLoading.value = false;
}
} else if (currentStep.value === 2) {
@@ -296,7 +351,9 @@ const onGetSmsCode = async () => {
}
}, 1000);
} else {
uni.showToast({ title: res.msg || '短信验证码发送失败', icon: 'none' });
uni.showToast({
title: res.msg || res.data.message, icon: 'none'
});
}
};
@@ -315,9 +372,16 @@ const onConfirmSms = async () => {
});
if (res.code === 200) {
uni.showToast({ title: '银行卡添加成功', icon: 'none' });
navTo({
url: '/pages_app/idcardAuth/bankCardList'
});
uni.navigateBack();
// if (returnTo.value) {
// uni.redirectTo({
// url: returnTo.value
// });
// } else {
// navTo({
// url: '/pages_app/idcardAuth/bankCardList'
// });
// }
} else {
uni.showToast({ title: res.msg || '银行卡添加失败', icon: 'none' });
}
@@ -332,12 +396,17 @@ const onCancelSms = () => {
<style lang="scss" scoped>
.idcard-auth-page {
min-height: 100vh;
background: #f5f5f5;
}
.content-area {
padding-top: 160rpx;
top: calc(var(--status-bar-height) + 44px);
position: fixed;
left: 0;
right: 0;
overflow-y: scroll;
z-index: 1;
padding: 0rpx 0rpx 0;
}
@@ -455,78 +524,98 @@ const onCancelSms = () => {
.form-item {
margin-bottom: 30rpx;
display: flex;
align-items: center;
position: relative;
border-bottom: 2rpx solid #eee;
padding: 0 30rpx 20rpx;
height: 92rpx;
padding: 0 30rpx;
box-sizing: border-box;
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
.form-label {
display: block;
display: flex;
align-items: center;
justify-content: flex-end;
height: 80rpx;
font-size: 28rpx;
color: #000000;
width: 120rpx;
font-weight: 500;
text-align: right;
text-align-last: auto;
letter-spacing: 0;
white-space: nowrap;
margin-right: 10rpx;
}
.form-input {
flex: 1;
height: 80rpx;
min-height: 80rpx;
line-height: 80rpx;
padding: 0 20rpx;
font-size: 28rpx;
background: #ffffff;
border: none;
outline: none;
box-sizing: border-box;
&:focus {
border-color: #8B2316;
}
}
.info-icon {
position: absolute;
right: 30rpx;
top: 50%;
transform: translateY(-50%);
width: 40rpx;
height: 40rpx;
border-radius: 50%;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid #8B2316;
.icon-text {
font-size: 20rpx;
color: #8B2316;
font-weight: bold;
}
}
.input-container {
display: flex;
align-items: center;
position: relative;
width: 80%;
height: 80rpx;
flex: 1;
padding-right: 56rpx;
box-sizing: border-box;
.form-input {
flex: 9;
height: 80rpx;
padding: 0 20rpx;
flex: 1;
height: 100%;
min-height: 100%;
line-height: 80rpx;
padding: 0;
font-size: 28rpx;
background: #ffffff;
border: none;
border-radius: 0;
outline: none;
box-sizing: border-box;
&:focus {
/* 去除边框后无需变更边框颜色 */
&.card-number-input.is-filled {
font-weight: normal;
}
}
.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;
}
}
}
}
}
@@ -644,6 +733,10 @@ const onCancelSms = () => {
background: #eeeeee;
color: #8B2316;
font-size: 26rpx;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.sms-actions {
+8 -2
View File
@@ -1,5 +1,5 @@
<template>
<navBar title="肝胆回放" />
<navBar :title="from == 'video' ? '肝胆视频' : '肝胆回放'" />
<view class="page">
<!-- 筛选标签栏 -->
<view class="filter-bar">
@@ -221,7 +221,7 @@
<script setup>
import { ref, nextTick, computed } from "vue";
import { onShow, onHide } from "@dcloudio/uni-app";
import { onShow, onHide,onLoad } from "@dcloudio/uni-app";
import CustomTabbar from "@/components/tabBar/tabBar.vue";
import api from "@/api/api.js";
import select from "@/static/triangle_normal.png";
@@ -234,6 +234,12 @@ import navTo from "@/utils/navTo";
import navBar from "@/components/navBar/navBar.vue";
import empty from "@/components/empty/empty.vue";
import lazyImg from "@/static/default_news_iv.png";
const from=ref('');
onLoad((options)=>{
if(options.from){
from.value=options.from;
}
});
// 弹窗状态
const isTimePopupShow = ref(false);
const isLocationPopupShow = ref(false);
@@ -272,7 +272,7 @@ const goDetail=(item)=>{
const encoded = encodeURIComponent(item.path);
let imgPath=encodeURIComponent('https://doc.igandan.com/app/html/img/2016/20160714132557.png');
uni.navigateTo({
url: `/pages_app/webview/webview?url=${encoded}&sharetitle=${item.title}&bg=1&type=live_yugao&imgPath=${imgPath}&share=1&title=会议详情`
url: `/pages_app/webview/webview?url=${encoded}&sharetitle=${item.title}&bg=1&type=live_old&imgPath=${imgPath}&share=1&title=会议详情`
});
}
</script>
+277 -82
View File
@@ -1,47 +1,53 @@
<template>
<!-- 顶部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="消息"
right-text="清除"
@clickRight="clearMsg"
:title="'消息'"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
>
<template v-slot:right>
<view class="nav-right" >
<view class="collect-img" @click="clearMsg" >
<image class="img-icon" :src="clearImg" />
</view>
</view>
</template>
</uni-nav-bar>
</view>
<view class="msg-page">
<!-- 四宫格入口 -->
<view class="shortcut-grid">
<view class="grid-item" @click="goBenefit">
<view class="icon-wrap gift">
<uni-icons type="gift" size="34" color="#fff"></uni-icons>
<view class="icon-wrap">
<image :src="fuliImg" class="imgcell"/>
<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="icon-wrap ">
<image :src="orderImg" class="imgcell"/>
<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="icon-wrap">
<image :src="followImg" class="imgcell"/>
<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="icon-wrap">
<image :src="replyImg" class="imgcell"/>
<view class="badge" v-if="badgeData.Module_Comment > 0">{{ badgeData.Module_Comment }}</view>
</view>
<text class="label">回复我的</text>
@@ -53,11 +59,11 @@
class="msg-list"
scroll-y="true"
:show-scrollbar="false"
refresher-enabled="true"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
lower-threshold="80"
:lower-threshold="100"
>
<!-- 回复我的模块 - 聊天列表样式 -->
<view v-if="currentModule === 4" class="chat-list">
@@ -77,24 +83,28 @@
<!-- 其他模块 - 卡片样式 -->
<view v-else>
<view class="card" v-for="(msg, idx) in msgList" :key="msg.id" @click="goMsgDetail(msg)">
<view class="card-title">{{ msg.title }}</view>
<view class="card-content">{{ msg.content }}</view>
<view class="card-cell" v-for="(msg, idx) in msgList" :key="msg.id" @click="goMsgDetail(msg)">
<view class="card-time">{{ msg.create_date }}</view>
<view class="card" >
<view class="card-title" :class="{'active':msg.is_read==0}">{{ msg.title }}</view>
<view class="card-content">{{ msg.content }}</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-if="msgList.length === 0" class="empty-state">
<text class="empty-text">暂无消息</text>
<view v-if="msgList.length === 0 && !loading && !refreshing" class="empty-state">
<empty></empty>
</view>
<!-- 加载更多提示 -->
<view v-if="loading" class="loading-more">
<view v-if="loading && msgList.length > 0" class="loading-more">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 没有更多数据提示 -->
<view v-if="noMore" class="no-more">
<text class="no-more-text">没有更多数据了</text>
<view v-if="noMore && msgList.length > 0 && !loading" class="no-more">
<text class="no-more-text">— 没有更多数据了 —</text>
</view>
</scroll-view>
</view>
@@ -105,6 +115,17 @@
import api from '@/api/api.js';
import docUrl from '@/utils/docUrl';
import navTo from '@/utils/navTo';
import clearImg from '@/static/clear_message.png';
import empty from '@/components/empty/empty.vue';
import fuliImg from '@/static/fu_message_big.png';
import orderImg from '@/static/ding_message_big.png';
import followImg from '@/static/sui_message_big.png';
import replyImg from '@/static/hui_message_big.png';
import {onBackPress} from '@dcloudio/uni-app';
onBackPress(() => {
plus.runtime.quit();
return true;
});
// 角标数据
const badgeData = ref({
@@ -123,93 +144,183 @@
const loading = ref(false);
const noMore = ref(false);
const page = ref(1);
const pageSize = ref(10);
const pageSize = 10;
// 重置分页状态
const resetPagination = () => {
page.value = 1;
noMore.value = false;
msgList.value = [];
};
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({
url: '/pages/index/index'
plus.runtime.quit();
};
// 切换模块
const switchModule = (module) => {
if (currentModule.value === module && msgList.value.length > 0) return; // 避免重复加载
currentModule.value = module;
resetPagination();
getAppMesageList(true);
};
const goBenefit = () => switchModule(1);
const goOrder = () => switchModule(2);
const goFollow = () => switchModule(3);
const goReply = () => switchModule(4);
const readMsg = (id) => {
api.appMesageRead({
id: id
}).then(res => {
if(res.code==200){
msgList.value.forEach(item => {
if(item.id==id){
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "read_message",
},
},
(ret) => {
console.log(ret);
});
item.is_read=1;
}
});
}
});
};
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 => {
api.appMesageRead().then(res => {
console.log(res);
});
getUnReadList();
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "clear_message",
},
},
(ret) => {
console.log(ret);
});
});
}
}
});
}
const goMsgDetail = (msg) => {
console.log(msg);
let url="";
if(msg.extra.module==1 && msg.extra.type==36){
url=`/pages_app/myPoint/myPoint`
}else if(msg.extra.module==3 && msg.extra.type==12){
url=`/pages_app/myPatient/myPatient`
if(msg.is_read==0){
readMsg(msg.id);
};
//readMsg(msg.id);
if(msg.extra.module==4){
if(msg.extra.type==17 || msg.extra.type==40){
uni.showToast({
title: '暂停中',
icon: 'none'
});
}else{
uni.sendNativeEvent('goCourseDetail', {
msg: {
id:msg.extra.id
}
});
}
}else if(msg.extra.module==2){
if(msg.extra.type==19){
let url=`/pages_app/myCourseware/myCourseware?from=msg`
navTo({
url: url
})
}else if(msg.extra.type==18){
let url=`/pages_app/myFlower/myFlower?from=msg`
navTo({
url: url
})
}
// 下拉刷新=
}else{
let url="";
if(msg.extra.module==1 && msg.extra.type==36){
url=`/pages_app/myPoint/myPoint?from=msg`
navTo({
url: url
})
}else if(msg.extra.module==1 && msg.extra.type==31){
url=`/pages_app/myPoint/myPoint?from=msg`
navTo({
url: url
})
}else if(msg.extra.module==1 && msg.extra.type==35){
//uni.setStorageSync('lookWelfare', 'useWelfare');
url=`/pages_app/myWelfareCard/myWelfareCard?from=msg`
navTo({
url: url
})
}else if(msg.extra.module==3 && msg.extra.type==12){
url=`/pages_app/myPatient/myPatient?from=msg`
navTo({
url: url
})
}
}
}
// 下拉刷新
const onRefresh = async () => {
if (refreshing.value) return;
refreshing.value = true;
try {
// 重新获取当前模块的消息列表
getAppMesageList(currentModule.value);
// 重新获取未读消息数量
getUnReadList();
// 重置分页并重新获取数据
resetPagination();
await Promise.all([
getAppMesageList(true),
getUnReadList()
]);
} catch (err) {
console.error('刷新失败:', err);
uni.showToast({
title: '刷新失败',
icon: 'none'
});
} finally {
refreshing.value = false;
}
};
// 上拉加载
// 上拉加载更多
const onLoadMore = async () => {
if (loading.value || noMore.value) return;
if (loading.value || noMore.value || refreshing.value) return;
loading.value = true;
try {
// 这里可以根据需要实现分页加载
// 目前接口返回的数据已经包含了分页信息
// 如果需要加载更多,可以调用 getAppMesageList 并传入页码参数
noMore.value = true; // 暂时设置为没有更多数据
page.value++;
await getAppMesageList(false);
} catch (err) {
// 加载失败时回退页码
page.value = Math.max(1, page.value - 1);
console.error('加载更多失败:', err);
uni.showToast({
title: '加载失败',
icon: 'none'
});
} finally {
loading.value = false;
}
};
const getUnReadList = () => {
api.unReadList({}).then(res => {
return api.unReadList({}).then(res => {
console.log(res);
if (res.code === 200 && res.data) {
// 更新角标数据
@@ -220,17 +331,24 @@
Module_Relation: parseInt(res.data.Module_Relation) || 0
};
}
return res;
}).catch(err => {
console.error('获取未读消息列表失败:', err);
throw err;
});
};
const getAppMesageList = (module) => {
api.appMesageList({module: module}).then(res => {
// 获取消息列表
// isRefresh: true 表示刷新,false 表示加载更多
const getAppMesageList = (isRefresh = false) => {
return api.appMesageList({
module: currentModule.value,
page: page.value,
pageSize: pageSize
}).then(res => {
console.log(res);
if (res.code === 200 && res.data && res.data.list) {
// 更新消息列表数据
msgList.value = res.data.list.map(item => ({
const newList = res.data.list.map(item => ({
id: item.id,
title: item.title,
content: item.content,
@@ -245,9 +363,31 @@
`${getBaseUrl()}${item.extra.user_photo}`) :
'/static/default-avatar.png'
}));
// 刷新时替换数据,加载更多时追加数据
if (isRefresh || page.value === 1) {
msgList.value = newList;
} else {
msgList.value = [...msgList.value, ...newList];
}
// 判断是否还有更多数据
if (res.isLastPage || newList.length < pageSize) {
noMore.value = true;
} else {
noMore.value = false;
}
} else if (res.code === 200 && (!res.data || !res.data.list || res.data.list.length === 0)) {
// 没有数据的情况
if (isRefresh || page.value === 1) {
msgList.value = [];
}
noMore.value = true;
}
return res;
}).catch(err => {
console.error('获取消息列表失败:', err);
throw err;
});
};
@@ -264,7 +404,7 @@
onMounted(() => {
getUnReadList();
getAppMesageList(1);
getAppMesageList(true); // 初始加载视为刷新
});
</script>
@@ -283,16 +423,31 @@
align-items: center;
justify-content: center;
}
.imgcell{
width:74rpx;
height: 74rpx;
}
.nav-right{
display: flex;
align-items: center;
.collect-img {
width: 50rpx;
height: 50rpx;
.img-icon{
width: 50rpx;
height: 50rpx;
}
}
}
.msg-page {
min-height: 100vh;
background: $bg;
padding-top: $nav-h; // 与导航栏高度一致
}
.shortcut-grid {
position: fixed;
top: $nav-h;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
z-index: 9;
@@ -346,7 +501,7 @@
.msg-list {
position: fixed;
// 减去导航与宫格的高度
top: 360rpx; // 让列表起始位置在宫格下方
top: calc(var(--status-bar-height) + 44px + 230rpx);
padding: 20rpx 24rpx 40rpx;
box-sizing: border-box;
bottom: 0rpx;
@@ -360,6 +515,47 @@
}
}
// 空状态样式
.empty-state {
flex-direction: column;
padding: 100rpx 0;
gap: 20rpx;
}
// 加载动画
.loading-more {
gap: 16rpx;
.loading-spinner {
width: 32rpx;
height: 32rpx;
border: 3rpx solid #eee;
border-top-color: $theme;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.card-cell{
display: flex;
flex-direction: column;
margin-bottom: 30rpx;
box-sizing: border-box;
}
.card-time {
width:100%;
margin:10rpx 0;
font-size: 30rpx;
color: #666;
text-align: center;
}
.card {
background: #fff;
border-radius: 16rpx;
@@ -372,6 +568,9 @@
color: $text-primary;
font-weight: 600;
margin-bottom: 14rpx;
&.active{
color: red;
}
}
.card-content {
@@ -381,11 +580,7 @@
margin-bottom: 14rpx;
}
.card-time {
font-size: 24rpx;
color: $muted;
text-align: right;
}
}
// 聊天列表样式
+141
View File
@@ -0,0 +1,141 @@
<template>
<view class="bill-detail-page">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="详情"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
</view>
<view class="detail-content">
<view class="detail-row">
<text class="label">交易号:</text>
<text class="value">{{ detail.tradeNo || '-' }}</text>
</view>
<view class="detail-row">
<text class="label">类型:</text>
<text class="value">{{ detail.typeName || '-' }}</text>
</view>
<view class="detail-row">
<text class="label">金额:</text>
<text class="value amount" :class="amountClass">{{ detail.accountStr || '-' }}元</text>
</view>
<view class="detail-row">
<text class="label">用户:</text>
<text class="value">{{ detail.userName || '-' }}</text>
</view>
<view class="detail-row">
<text class="label">时间:</text>
<text class="value">{{ formatDateTime(detail.createTime) }}</text>
</view>
<view class="detail-row">
<text class="label">备注:</text>
<text class="value">{{ detail.remark || '' }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const detail = ref({})
const from = ref('')
onLoad((options) => {
if (options.from) {
from.value = decodeURIComponent(options.from)
}
})
const amountClass = computed(() => {
const amount = Number(detail.value?.accountStr || 0)
return amount < 0 ? 'negative' : 'positive'
})
const formatDateTime = (value) => {
if (!value) return '-'
const text = String(value).trim()
const date = new Date(text)
if (Number.isNaN(date.getTime())) return text
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const hh = String(date.getHours()).padStart(2, '0')
const mm = String(date.getMinutes()).padStart(2, '0')
const ss = String(date.getSeconds()).padStart(2, '0')
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
}
const goBack = () => {
if (from.value === 'billDetails') {
uni.navigateBack()
return
}
uni.redirectTo({
url: '/pages_app/myAccount/billDetails?from=myAccount'
})
}
onMounted(() => {
const cache = uni.getStorageSync('billDetailItem')
detail.value = cache || {}
})
</script>
<style lang="scss" scoped>
.bill-detail-page {
min-height: 100vh;
background: #f3f3f3;
}
.detail-content {
position: fixed;
left: 0;
right: 0;
top: calc(var(--status-bar-height) + 44px);
bottom: 0;
overflow-y: auto;
box-sizing: border-box;
background: #ffffff;
padding: 0 24rpx;
}
.detail-row {
min-height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1rpx solid #efefef;
}
.label {
font-size: 30rpx;
color: #2c2c2c;
}
.value {
flex: 1;
text-align: right;
font-size: 30rpx;
color: #666666;
margin-left: 20rpx;
word-break: break-all;
}
.amount {
&.positive {
color: #8B2316;
}
&.negative {
color: #3f3f3f;
}
}
</style>
+64 -17
View File
@@ -1,16 +1,19 @@
<template>
<view class="bill-details-page">
<!-- 顶部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="账单明细"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
</view>
<!-- 表格头部 -->
<view class="table-header">
@@ -32,7 +35,7 @@
@scrolltolower="onLoadMore"
:lower-threshold="100"
>
<view class="transaction-item" v-for="(item, index) in orderList" :key="item.id">
<view class="transaction-item" v-for="(item, index) in orderList" :key="item.id" @click="goToDetail(item)">
<view class="cell business-type">{{ item.typeName }}</view>
<view class="cell user">{{ item.userName }}</view>
<view class="cell date-amount">
@@ -62,6 +65,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import api from '@/api/api.js'
import { onLoad } from '@dcloudio/uni-app'
// 响应式数据
const orderList = ref([])
@@ -71,10 +75,24 @@ const page = ref(1)
const total = ref(0)
const isFirstPage = ref(true)
const isLastPage = ref(false)
const from = ref('')
onLoad((options) => {
if (options.from) {
from.value = decodeURIComponent(options.from)
}
})
const stopRefresh = () => {
refreshing.value = false
}
// 获取订单列表
const getOrderList = async (isRefresh = false) => {
if (loading.value) return
if (loading.value) {
if (isRefresh) stopRefresh()
return
}
loading.value = true
@@ -93,15 +111,18 @@ const getOrderList = async (isRefresh = false) => {
}
// 处理订单数据
const processedList = list.map(item => ({
const safeList = Array.isArray(list) ? list : []
const processedList = safeList.map(item => ({
id: item.trade_no,
tradeNo: item.trade_no,
typeName: item.type_name,
userName: item.user_name,
createTime: item.create_time,
accountStr: item.accountstr,
payChannel: item.pay_channel,
orderType: item.order_type,
status: item.status
status: item.status,
remark: item.remark || item.message || ''
}))
orderList.value.push(...processedList)
@@ -117,12 +138,16 @@ const getOrderList = async (isRefresh = false) => {
})
} finally {
loading.value = false
refreshing.value = false
stopRefresh()
}
}
// 刷新数据
const onRefresh = () => {
if (loading.value) {
stopRefresh()
return
}
refreshing.value = true
page.value = 1
isLastPage.value = false
@@ -149,9 +174,23 @@ const formatDate = (dateStr) => {
// 返回上一页
const goBack = () => {
if (from.value === 'myAccount') {
uni.redirectTo({
url: '/pages_app/myAccount/myAccount?from=billDetails'
})
return
}
uni.navigateBack()
}
const goToDetail = (item) => {
uni.setStorageSync('billDetailItem', item || {})
const fromPage = encodeURIComponent('billDetails')
uni.navigateTo({
url: `/pages_app/myAccount/billDetail?from=${fromPage}`
})
}
onMounted(() => {
getOrderList()
})
@@ -182,14 +221,17 @@ onMounted(() => {
// 表格头部样式
.table-header {
background-color: #e0e0e0;
background-color: #fff;
position: fixed;
display: flex;
width: 100%;
padding: 20rpx 30rpx;
margin-top: 170rpx; /* 为固定导航栏留出空间 */
box-sizing: border-box;
top:calc(var(--status-bar-height) + 44px); /* 为固定导航栏留出空间 */
border-bottom: 1rpx solid #ccc;
.header-cell {
font-size: 28rpx;
font-size: 30rpx;
color: #333;
font-weight: 500;
@@ -224,17 +266,22 @@ onMounted(() => {
// 交易记录列表样式
.transaction-list {
background-color: #f5f5f5;
top:calc(var(--status-bar-height) + 44px + 130rpx);
position: fixed;
left: 0;
right: 0;
bottom: 0rpx;
background-color: #fff;
}
.transaction-item {
background-color: #f8f8f8;
display: flex;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #e5e5e5;
.cell {
font-size: 26rpx;
font-size: 30rpx;
&.business-type {
flex: 1;
@@ -245,7 +292,7 @@ onMounted(() => {
&.user {
flex: 1;
text-align: center;
color: #ff0000;
color: #8B2316;
}
&.date-amount {
@@ -257,7 +304,7 @@ onMounted(() => {
.date {
font-size: 24rpx;
color: #666;
color: #999;
margin-bottom: 4rpx;
}
@@ -265,11 +312,11 @@ onMounted(() => {
font-size: 24rpx;
&.positive {
color: #ff0000;
color: #000;
}
&.negative {
color: #666;
color: #000;
}
}
}
+69 -27
View File
@@ -1,17 +1,19 @@
<template>
<view class="my-account-page">
<!-- 顶部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="我的账户"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
</view>
<view class="contentbox">
<!-- 账户余额区域 -->
<view class="account-summary">
<view class="profile-section">
@@ -21,7 +23,7 @@
<text class="balance-amount">{{ (accountBalance/100).toFixed(2) }}</text>
<!-- 账户信息详情 -->
<view class="account-details">
<!-- <view class="account-details">
<view class="detail-item">
<text class="detail-label">单次提现限额</text>
<text class="detail-value">{{ (withdrawalBalanceMaxOnce/100).toFixed(2) }}元</text>
@@ -34,35 +36,36 @@
<text class="detail-label">最低提现金额</text>
<text class="detail-value">{{ (lessWithdrawalBalance/100).toFixed(2) }}元</text>
</view>
</view>
</view> -->
</view>
<!-- 操作选项 -->
<view class="action-items">
<view class="action-item" @click="handleWithdrawal">
<view class="action-icon withdrawal-icon">
<text class="icon-text">💰</text>
<image class="icon-img" :src="withdrawalsIcon" mode="aspectFit"></image>
</view>
<view class="action-content">
<text class="action-title">提现</text>
<text class="action-subtitle">提现金额不得少于{{ (lessWithdrawalBalance/100).toFixed(2) }}元</text>
</view>
<text class="action-arrow">></text>
<uni-icons type="right" size="25" color="#999"></uni-icons>
</view>
<view class="action-item" @click="handleBillDetails">
<view class="action-icon bill-icon">
<text class="icon-text">📋</text>
<image class="icon-img" :src="accountIcon" mode="aspectFit"></image>
</view>
<view class="action-content">
<text class="action-title">账单明细</text>
</view>
<text class="action-arrow">></text>
<uni-icons type="right" size="25" color="#999"></uni-icons>
</view>
</view>
</view>
<!-- 底部指示器 -->
<view class="bottom-indicator"></view>
<!-- <view class="bottom-indicator"></view> -->
</view>
</template>
@@ -70,6 +73,27 @@
import { ref, onMounted } from 'vue'
import api from '@/api/api.js'
import docUrl from '@/utils/docUrl'
import withdrawalsIcon from '@/static/withdrawals.png'
import accountIcon from '@/static/account_icon.png'
import { onLoad,onBackPress,onShow } from '@dcloudio/uni-app';
const from = ref('');
onLoad((options) => {
if(options.from){
from.value = options.from;
}
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "我的账户",
},
});
});
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
// 响应式数据
const accountBalance = ref(0)
const withdrawalBalanceMaxOnce = ref(0)
@@ -77,7 +101,7 @@ const withdrawalBalanceFee = ref(0)
const lessWithdrawalBalance = ref(0)
const avatar = ref('')
onMounted(() => {
onShow(() => {
getMyAccount()
let userInfo= uni.getStorageSync('userInfo')
@@ -86,13 +110,17 @@ onMounted(() => {
// 方法
const goBack = () => {
uni.navigateBack()
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
}
const handleWithdrawal = () => {
if(accountBalance.value < lessWithdrawalBalance.value){
uni.showToast({
title: `提现金额不得少于${(lessWithdrawalBalance/100).toFixed(2)}元`,
title: `提现金额不得少于${(lessWithdrawalBalance.value/100).toFixed(2)}元`,
icon: 'none'
})
return
@@ -111,8 +139,9 @@ const handleWithdrawal = () => {
cancelText: '取消',
success: (res) => {
if(res.confirm){
const returnTo = encodeURIComponent('/pages_app/myAccount/myAccount')
uni.navigateTo({
url: '/pages_app/idcardAuth/bankCardList'
url: `/pages_app/idcardAuth/idcardAuth?returnTo=${returnTo}`
})
}
}
@@ -123,8 +152,9 @@ const handleWithdrawal = () => {
}
const handleBillDetails = () => {
const from = encodeURIComponent('myAccount')
uni.navigateTo({
url: '/pages_app/myAccount/billDetails'
url: `/pages_app/myAccount/billDetails?from=${from}`
})
}
@@ -142,6 +172,14 @@ const getMyAccount = async () => {
</script>
<style lang="scss" scoped>
.contentbox{
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
bottom: 0;
}
.my-account-page {
background-color: #f5f5f5;
min-height: 100vh;
@@ -151,20 +189,19 @@ const getMyAccount = async () => {
// 账户余额区域样式
.account-summary {
background: linear-gradient(135deg, #8B2316, #A0522D);
margin: 30rpx;
border-radius: 20rpx;
padding: 60rpx 40rpx;
// margin: 30rpx;
// border-radius: 20rpx;
padding: 30rpx 40rpx 10rpx;
text-align: center;
box-shadow: 0 8rpx 24rpx rgba(139, 35, 22, 0.3);
position: relative;
.profile-section {
margin-bottom: 40rpx;
margin-bottom: 10rpx;
.profile-avatar {
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
border-radius: 20rpx;
border: 4rpx solid rgba(255, 255, 255, 0.3);
object-fit: cover;
}
@@ -174,18 +211,18 @@ const getMyAccount = async () => {
display: block;
color: rgba(255, 255, 255, 0.9);
font-size: 28rpx;
margin-bottom: 20rpx;
margin-bottom: 10rpx;
font-weight: 500;
}
.balance-amount {
display: block;
color: #ffffff;
font-size: 72rpx;
font-size: 50rpx;
font-weight: bold;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.2);
letter-spacing: 2rpx;
margin-bottom: 40rpx;
margin-bottom: 0rpx;
}
// 账户信息详情样式
@@ -223,15 +260,15 @@ const getMyAccount = async () => {
// 操作选项样式
.action-items {
background-color: #ffffff;
margin: 0 30rpx;
border-radius: 20rpx;
margin-top: 0rpx;
// border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
// box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
.action-item {
display: flex;
align-items: center;
padding: 40rpx 30rpx;
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
transition: background-color 0.2s ease;
@@ -256,6 +293,11 @@ const getMyAccount = async () => {
.icon-text {
font-size: 32rpx;
}
.icon-img {
width: 34rpx;
height: 34rpx;
}
}
.withdrawal-icon {
+129 -47
View File
@@ -1,16 +1,17 @@
<template>
<view class="withdrawal-page">
<!-- 顶部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="提现"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
</view>
<!-- 主内容区域 -->
<view class="main-content">
@@ -25,20 +26,27 @@
<text class="section-title">结算银行卡</text>
<text class="withdrawal-rules" @click="showWithdrawalRules">提现规则</text>
</view>
<view class="divider"></view>
<view class="bank-card-info" @click="selectBankCard" v-if="selectedBankCard">
<view class="bank-logo">
<text class="bank-icon">{{ selectedBankCard.bankName ? selectedBankCard.bankName.charAt(0) : '工' }}</text>
<image
v-if="getBankLogoImg(getBankName(selectedBankCard))"
class="bank-logo-img"
:src="getBankLogoImg(getBankName(selectedBankCard))"
mode="aspectFit"
></image>
<view v-else class="logo-bg">
<text class="bank-icon">{{ getBankLogo(getBankName(selectedBankCard)) }}</text>
</view>
</view>
<view class="bank-details">
<text class="bank-name">{{ selectedBankCard.bankName || '工商银行' }}</text>
<text class="bank-card-number">尾号为{{ selectedBankCard.cardNumber ? selectedBankCard.cardNumber.slice(-4) : '8937' }}储蓄卡</text>
<text class="bank-name">{{ getBankName(selectedBankCard) || '工商银行' }}</text>
<text class="bank-card-number">尾号为{{ getCardTail(selectedBankCard) }}储蓄卡</text>
</view>
<text class="arrow-icon">›</text>
<uni-icons type="right" size="25" color="#999"></uni-icons>
</view>
<view class="no-bank-card" v-else>
<text class="no-card-text">请先添加银行卡</text>
<text class="arrow-icon">›</text>
<uni-icons type="right" size="25" color="#999"></uni-icons>
</view>
<view class="divider"></view>
</view>
@@ -48,12 +56,12 @@
<text class="amount-title">提现金额</text>
<view class="amount-display">
<text class="currency-symbol">¥</text>
<text class="amount-value">{{ withdrawalAmount }}</text>
<text class="amount-value">{{ withdrawalAmount}}</text>
</view>
<view class="amount-details">
<text class="tax-info">扣除个人所得税<text class="highlight-red">{{ (incomeTax / 100).toFixed(2) }}元</text></text>
<text class="fee-info">扣除提现手续费<text class="highlight-red">{{ (withdrawalBalanceFee / 100).toFixed(2) }}元</text></text>
<text class="actual-amount">此次提现实际到账<text class="highlight-red">{{ actualAmount }}元</text></text>
<text class="tax-info">扣除个人所得税<text class="highlight-red">{{ incomeTax}}元</text></text>
<!-- <text class="fee-info">扣除提现手续费<text class="highlight-red">{{ (withdrawalBalanceFee / 100).toFixed(2) }}元</text></text> -->
<text class="actual-amount">此次提现实际到账<text class="highlight-red">{{ actualAmount}}元</text></text>
</view>
</view>
</view>
@@ -132,10 +140,11 @@ onMounted(() => {
// 计算提现金额
const calculateAmounts = () => {
// 提现金额 = 账户余额 - 个人所得税 - 提现手续费
const totalDeduction = incomeTax.value + withdrawalBalanceFee.value
const amount = accountBalance.value - totalDeduction
withdrawalAmount.value = accountBalance.value > 0 ? (accountBalance.value/100).toFixed(2) : '0.00'
actualAmount.value = amount > 0 ? (amount/100).toFixed(2) : '0.00'
// const totalDeduction = incomeTax.value + withdrawalBalanceFee.value
// const amount = accountBalance.value - totalDeduction
// withdrawalAmount.value = accountBalance.value > 0 ? (accountBalance.value/100).toFixed(2) : '0.00'
// actualAmount.value = amount > 0 ? (amount/100).toFixed(2) : '0.00'
}
// 初始化数据
@@ -162,6 +171,63 @@ const goBack = () => {
uni.navigateBack()
}
const bankLogoMap = {
'工商银行': '/static/icon_bankcard_gongshang.png',
'中国银行': '/static/icon_bankcard_zhongguo.png',
'建设银行': '/static/icon_bankcard_jianseyinhang.png',
'农业银行': '/static/icon_bankcard_nongyeyinhang.png',
'交通银行': '/static/icon_bankcard_jiatong.png',
'招商银行': '/static/icon_bankcard_zhaoshangyinhang.png',
'民生银行': '/static/icon_bankcard_minshneg.png',
'兴业银行': '/static/icon_bankcard_xingye.png',
'浦发银行': '/static/icon_bankcard_shanghaipudongfazhanyinhang.png',
'光大银行': '/static/icon_bankcard_guangda.png',
'华夏银行': '/static/icon_bankcard_huaxiayinhnag.png',
'中信银行': '/static/icon_bankcard_zhongxinshiye.png',
'平安银行': '/static/icon_bankcard_pingan.png',
'广发银行': '/static/icon_bankcard_guangfa.png',
'邮储银行': '/static/icon_bankcard_youzheng.png',
'上海银行': '/static/icon_bankcard_shanghai.png',
'北京银行': '/static/icon_bankcard_beijingyinhang.png'
}
const getBankName = (card) => {
if (!card) return ''
return card.bankName || card.open_bank || ''
}
const getCardTail = (card) => {
if (!card) return '8937'
const raw = card.cardNumber || card.card_number || ''
return raw ? String(raw).slice(-4) : '8937'
}
const getBankLogoImg = (bankName) => {
return bankLogoMap[bankName] || ''
}
const getBankLogo = (bankName) => {
const bankLogos = {
'工商银行': '工',
'中国银行': '中',
'建设银行': '建',
'农业银行': '农',
'交通银行': '交',
'招商银行': '招',
'民生银行': '民',
'兴业银行': '兴',
'浦发银行': '浦',
'光大银行': '光',
'华夏银行': '华',
'中信银行': '信',
'平安银行': '平',
'广发银行': '广',
'邮储银行': '邮'
}
if (!bankName) return '工'
return bankLogos[bankName] || bankName.charAt(0)
}
// 获取我的账户信息
const getMyAccount = async () => {
try {
@@ -173,7 +239,7 @@ const getMyAccount = async () => {
withdrawalBalanceFee.value = res.data.withdrawalBalanceFee
lessWithdrawalBalance.value = res.data.lessWithdrawalBalance
// 获取账户信息后计算金额
calculateAmounts()
//calculateAmounts()
// 然后获取个人所得税
getIncomeTax(accountBalance.value)
}
@@ -187,10 +253,13 @@ const getIncomeTax = async (amount) => {
try {
const res = await api.getIncomeTax({amount: amount})
console.log('个人所得税:', res)
if (res && res.code === 200 && res.data) {
incomeTax.value = res.data.incomeTax || 0
if (res.code ==200) {
incomeTax.value = (res.data.tax/100).toFixed(2) || 0
withdrawalAmount.value = (amount/100).toFixed(2) || 0
actualAmount.value = (res.data.actual_amount/100).toFixed(2) || 0
// 获取税费后重新计算金额
calculateAmounts()
//calculateAmounts()
}
} catch (error) {
console.error('获取个人所得税失败:', error)
@@ -214,8 +283,9 @@ const getBankCardList = async () => {
confirmText: '去添加',
success: (modalRes) => {
if (modalRes.confirm) {
const returnTo = encodeURIComponent('/pages_app/myAccount/withdrawal?from=bankCardList')
uni.navigateTo({
url: '/pages_app/idcardAuth/bankCardList'
url: `/pages_app/idcardAuth/bankCardList?from=withdrawal&returnTo=${returnTo}`
})
}
}
@@ -238,7 +308,7 @@ const selectBankCard = () => {
// 这里可以实现银行卡选择逻辑
uni.showActionSheet({
itemList: bankCardList.value.map(card => `${card.bankName} 尾号${card.cardNumber.slice(-4)}`),
itemList: bankCardList.value.map(card => `${getBankName(card)} 尾号${getCardTail(card)}`),
success: (res) => {
selectedBankCard.value = bankCardList.value[res.tapIndex]
}
@@ -252,8 +322,8 @@ const showWithdrawalRules = () => {
const feeAmount = (withdrawalBalanceFee.value / 100).toFixed(2)
uni.showModal({
title: '提现规则',
content: `1. 提现金额不得少于${minAmount}元\n2. 单次提现限额为${maxAmount}元\n3. 提现手续费为${feeAmount}元\n4. 工作日24小时内到账`,
title: '温馨提示',
content: `提交成功后会有工作人员与您联系`,
showCancel: false,
confirmText: '我知道了'
})
@@ -278,15 +348,7 @@ const confirmWithdrawal = () => {
return
}
// 检查单次提现限额
const maxAmount = withdrawalBalanceMaxOnce.value / 100
if (parseFloat(withdrawalAmount.value) > maxAmount) {
uni.showToast({
title: `单次提现限额为${maxAmount.toFixed(2)}元`,
icon: 'none'
})
return
}
// 弹出短信验证码弹框
showSmsDialog.value = true
@@ -337,7 +399,8 @@ const onGetSmsCode = async () => {
} catch (error) {
console.error('发送短信验证码失败:', error)
uni.showToast({
title: '网络错误,请重试',
title:error.data.message || error.message || error.data.msg || error.msg || '频繁发送短信,请稍后再试'
,
icon: 'none'
})
} finally {
@@ -405,7 +468,9 @@ const onCancelSms = () => {
// 主内容区域
.main-content {
padding: 0 30rpx;
padding-top: calc(var(--status-bar-height) + 44px);
padding-bottom: 180rpx;
box-sizing: border-box;
}
// 加载状态
@@ -433,7 +498,7 @@ const onCancelSms = () => {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 0;
padding: 30rpx;
.section-title {
font-size: 32rpx;
@@ -448,32 +513,45 @@ const onCancelSms = () => {
}
.divider {
height: 1rpx;
background-color: #f0f0f0;
height: 2rpx;
background-color: #bab7b7;
margin: 0;
}
.bank-card-info {
display: flex;
align-items: center;
padding: 30rpx 0;
padding: 30rpx;
background-color: #ffffff;
.bank-logo {
width: 60rpx;
height: 60rpx;
background-color: #8B2316;
border-radius: 8rpx;
margin-right: 24rpx;
.bank-logo-img {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
}
.logo-bg {
width: 60rpx;
height: 60rpx;
background: #E60012;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
border: 2rpx solid #E60012;
.bank-icon {
font-size: 32rpx;
font-size: 28rpx;
color: #ffffff;
font-weight: bold;
}
}
}
.bank-details {
flex: 1;
@@ -493,7 +571,7 @@ const onCancelSms = () => {
}
.arrow-icon {
font-size: 32rpx;
font-size: 62rpx;
color: #cccccc;
font-weight: bold;
}
@@ -520,8 +598,8 @@ const onCancelSms = () => {
// 金额区域
.amount-section {
padding: 40rpx 0;
padding: 40rpx 30rpx;
background-color: #ffffff;
.amount-title {
display: block;
font-size: 32rpx;
@@ -556,7 +634,7 @@ const onCancelSms = () => {
margin-bottom: 12rpx;
.highlight-red {
color: #8B2316;
color: red;
}
}
}
@@ -668,6 +746,10 @@ const onCancelSms = () => {
color: #8B2316;
font-size: 26rpx;
border: none;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
&:disabled {
background: #f5f5f5;
+288 -130
View File
@@ -14,8 +14,8 @@
placeholder="请依据患者的个人信息、疾病资料及患者所咨询的问题详细解答患者的问题(信息仅提问患者及医生可见,最多输入300个字)"
placeholder-class="ph"
auto-height
:adjust-position="false"
/>
</view>
</view>
@@ -23,9 +23,12 @@
<view class="card">
<view class="card-title">相关图片</view>
<view class="sub-tip">可以用部分科普或文献来协助回答问题,最多6张</view>
<!-- <view class="imgbox">
<uni-file-picker limit="6" v-model="tempImageList" file-mediatype="image" :auto-upload="false" @select="selectImages" @delete="deleteImages" ></uni-file-picker>
</view> -->
<view class="img-grid">
<view
v-if="tempImageList.length>0"
v-if="tempImageList.length > 0"
v-for="(img, index) in tempImageList"
:key="index"
class="img-item"
@@ -34,13 +37,17 @@
<image :src="img" mode="aspectFill" class="img" />
<view class="del" @click.stop="remove(index)">×</view>
</view>
<view v-if="tempImageList.length < maxImages" class="img-item add" @click="addImages">
<view
v-if="tempImageList.length < maxImages"
class="img-item add"
@click="beforeAddImages"
>
<view class="plus">+</view>
</view>
</view>
</view>
</scroll-view>
<!-- <button style="position: fixed;bottom: 200rpx;left: 50%;transform: translateX(-50%);z-index: 99999999999;" @click="test" >测试</button> -->
<!-- 底部提交 -->
<view class="bottom-fixed">
<view class="submit-btn" @click="submit">提交</view>
@@ -49,38 +56,56 @@
</template>
<script setup>
import { ref } from 'vue'
import navBar from '@/components/navBar/navBar.vue'
import api from '@/api/api'
import docUrl from '@/utils/docUrl'
import { onLoad,onShow } from '@dcloudio/uni-app'
import { pathToBase64} from "image-tools";
const uuid=ref('');
const answer_uuid=ref('');
import { ref } from "vue";
import navBar from "@/components/navBar/navBar.vue";
import api from "@/api/api";
import docUrl from "@/utils/docUrl";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { pathToBase64 } from "image-tools";
import { getCameraPermission } from "@/utils/permission.js";
import isAndroid from "@/utils/platform.js";
const uuid = ref("");
const answer_uuid = ref("");
const maxImages = 6;
const imgList = ref([]);
const isLock = ref(false);
const tempImageList = ref([]);
const note = ref('');
const note = ref("");
onLoad((options) => {
uuid.value = options.uuid || ''
answer_uuid.value = options.answer_uuid || ''
getInterrogation()
})
const getInterrogation=()=>{
api.getInterrogation({
uuid:uuid.value
}).then(res=>{
console.log(res)
if(res.code === '200' && res.data) {
let user=uni.getStorageSync('userInfo');
let arr=res.data.AnswerList.filter(item=>{
return user.uuid == item.expert_uuid
uuid.value = options.uuid || "";
answer_uuid.value = options.answer_uuid || "";
getInterrogation();
});
const beforeAddImages = () => {
if (isAndroid) {
getCameraPermission(addImages);
} else {
addImages();
}
};
const getInterrogation = () => {
api
.getInterrogation({
uuid: uuid.value,
})
imgList.value=[];
if(arr && arr.length>0){
note.value= arr[0].note || '';
let imgs= arr[0].imgs?arr[0].imgs.split(','):[];
tempImageList.value= imgs.map(item=>docUrl+item);
.then((res) => {
console.log(res);
if (res.code === "200" && res.data) {
let user = uni.getStorageSync("userInfo");
let arr = res.data.AnswerList.filter((item) => {
return user.uuid == item.expert_uuid;
});
imgList.value = [];
if (arr && arr.length > 0) {
note.value = arr[0].note || "";
let imgs = arr[0].imgs ? arr[0].imgs.split(",") : [];
tempImageList.value = imgs.map((item, index) => {
return {
name: "图片" + index + 1,
url: docUrl + item,
};
});
tempImageList.value = imgs.map((item) => docUrl + item);
for (let i = 0; i < imgs.length; i++) {
uni.request({
url: docUrl + imgs[i],
@@ -97,115 +122,231 @@ const getInterrogation=()=>{
}
}
}
})
}
const updateInterrogationAnswer=()=>{
if(note.value.trim().length<5){
uni.showToast({title: '输入意见至少5个字', icon: 'none'})
return
});
};
const updateInterrogationAnswer = () => {
if (note.value.trim().length < 5) {
uni.showToast({ title: "输入意见至少5个字", icon: "none" });
return;
}
let imgobj={};
if(imgList.value.length>0){
let count=0;
imgList.value.forEach((item,index)=>{
imgobj['img'+(index+1)]=item;
})
if (isLock.value) {
return;
}
api.updateInterrogationAnswer({
isLock.value = true;
let imgobj = {};
if (imgList.value.length > 0) {
let count = 0;
imgList.value.forEach((item, index) => {
imgobj["img" + (index + 1)] = item;
});
}
api
.updateInterrogationAnswer({
uuid: answer_uuid.value,
note:note.value,
imgsBean: imgobj
}).then(res=>{
if(res.code == 200){
uni.showToast({title: '提交成功', icon: 'none'})
uni.navigateBack()
}
})
}
const addInterrogationAnswer=()=>{
if(note.value.trim().length<5){
uni.showToast({title: '输入意见至少5个字', icon: 'none'})
return
}
let imgobj={};
if(imgList.value.length>0){
imgList.value.forEach((item,index)=>{
imgobj['img'+(index+1)]=item;
})
}
api.addInterrogationAnswer({
step1_uuid:uuid.value,
note: note.value,
imgsBean: imgobj
}).then(res=>{
if(res.code == 200){
uni.showToast({title: '提交成功', icon: 'none'})
uni.navigateBack()
}
imgsBean: imgobj,
})
}
function addImages(){
const remain = maxImages - imgList.value.length
if(remain <= 0){
uni.showToast({title: '最多6张', icon: 'none'})
return
.then((res) => {
isLock.value = false;
if (res.code == 200) {
uni.showToast({ title: "提交成功", icon: "none" });
uni.$emit("updateStatus", 1);
uni.navigateBack();
}
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album','camera'],
success: (res)=>{
console.log(res.tempFilePaths)
tempImageList.value = [...tempImageList.value, ...res.tempFilePaths];
for (let i = 0; i < res.tempFilePaths.length; i++) {
pathToBase64(res.tempFilePaths[i])
});
};
const test = () => {
uni.navigateTo({
url: "/pages/upload/upload",
});
};
const addInterrogationAnswer = () => {
if (note.value.trim().length < 5) {
uni.showToast({ title: "输入意见至少5个字", icon: "none" });
return;
}
if (isLock.value) {
return;
}
isLock.value = true;
let imgobj = {};
if (imgList.value.length > 0) {
imgList.value.forEach((item, index) => {
imgobj["img" + (index + 1)] = item;
});
}
api
.addInterrogationAnswer({
step1_uuid: uuid.value,
note: note.value,
imgsBean: imgobj,
})
.then((res) => {
isLock.value = false;
if (res.code == 200) {
uni.$emit("updateStatus", 1);
uni.showToast({ title: "提交成功", icon: "none" });
uni.navigateBack();
}
});
};
const selectImages = (e) => {
let tempFilePaths = e.tempFiles;
console.log(e);
tempImageList.value = [...tempImageList.value, ...tempFilePaths];
for (let i = 0; i < tempFilePaths.length; i++) {
pathToBase64(tempFilePaths[i].url)
.then((base64) => {
imgList.value.push(base64.split(',')[1]);
console.log("转base64成功");
imgList.value.push(base64.split(",")[1]);
})
.catch((error) => {
console.log("转base64成功");
console.error(error);
});
}
console.log(tempImageList.value)
console.log(imgList.value)
console.log(tempImageList.value);
console.log(imgList.value);
};
const addImages = () => {
const remain = maxImages - imgList.value.length;
if (remain <= 0) {
uni.showToast({ title: "最多6张", icon: "none" });
return;
}
console.log("执行选择图片");
console.log('isAndroid:'+isAndroid)
if (isAndroid) {
uni.chooseImage({
count: remain,
sizeType: ["original", "compressed"],
sourceType: ["album", "camera"],
success: (res) => {
console.log("获取到照片");
console.log(res.tempFilePaths);
tempImageList.value = [...tempImageList.value, ...res.tempFilePaths];
for (let i = 0; i < res.tempFilePaths.length; i++) {
uni.saveFile({
tempFilePath: res.tempFilePaths[i],
success: (res1) => {
pathToBase64(res1.savedFilePath)
.then((base64) => {
console.log("转base64成功");
imgList.value.push(base64.split(",")[1]);
})
.catch((error) => {
console.log("转base64失败");
console.error(error);
});
}
})
}
function preview(index){
uni.previewImage({
urls:tempImageList.value,
current: index
}
},
});
} else {
uni.chooseImage({
count: remain,
sourceType: ["album", "camera"],
crop: {
width: 375,
height: 500,
resize: true,
quality:100,
},
success: (res) => {
console.log("获取到照片");
console.log(res.tempFilePaths);
tempImageList.value = [...tempImageList.value, ...res.tempFilePaths];
for (let i = 0; i < res.tempFilePaths.length; i++) {
uni.getImageInfo({
src: res.tempFilePaths[i],
success: (result) => {
console.log(result);
if (result.width > 1000) {
uni.compressImage({
quality:100,
src: res.tempFilePaths[i],
success: (result2) => {
console.log("压缩成功");
console.log(result2.tempFilePath);
pathToBase64(result2.tempFilePath)
.then((base64) => {
console.log("转base64成功");
imgList.value.push(base64.split(",")[1]);
})
.catch((error) => {
console.log("转base64失败");
console.error(error);
});
},
fail: (err) => {
console.log("压缩失败:" + err.errMsg);
},
});
} else {
pathToBase64(res.tempFilePaths[i])
.then((base64) => {
console.log("转base64成功");
imgList.value.push(base64.split(",")[1]);
})
.catch((error) => {
console.log("转base64失败");
console.error(error);
});
}
},
});
}
},
fail: (err) => {
console.log("获取图片信息失败:");
console.error(err.errMsg);
},
});
}
};
function preview(index) {
uni.previewImage({
urls: tempImageList.value,
current: index,
});
}
function remove(index){
imgList.value.splice(index, 1)
tempImageList.value.splice(index, 1)
function remove(index) {
imgList.value.splice(index, 1);
tempImageList.value.splice(index, 1);
}
const deleteImages = (e) => {
let index = e.index;
imgList.value.splice(index, 1);
tempImageList.value.splice(index, 1);
};
function submit(){
if(answer_uuid.value){
updateInterrogationAnswer()
}else{
addInterrogationAnswer()
function submit() {
if (answer_uuid.value) {
updateInterrogationAnswer();
} else {
addInterrogationAnswer();
}
}
</script>
<style lang="scss" scoped>
.my-answer-page{
.my-answer-page {
background-color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
}
.content-scroll{
.content-scroll {
flex: 1;
position: fixed;
top: 135rpx;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
bottom: 160rpx;
@@ -213,34 +354,38 @@ function submit(){
box-sizing: border-box;
}
.card{
.card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.card-title{
.card-title {
font-size: 32rpx;
color: #8B2316;
color: #8b2316;
font-weight: 500;
margin-bottom: 16rpx;
}
.required{ color: #ff4d4f; }
.required {
color: #ff4d4f;
}
.textarea-wrap{
.textarea-wrap {
position: relative;
background: #f7f7f7;
border-radius: 12rpx;
padding: 16rpx 88rpx 16rpx 16rpx;
padding: 16rpx 16rpx 16rpx 16rpx;
}
.textarea{
.textarea {
min-height: 180rpx;
width: 100%;
font-size: 28rpx;
color: #333;
}
.ph{ color:#999; }
.voice-btn{
.ph {
color: #999;
}
.voice-btn {
position: absolute;
right: 16rpx;
bottom: 16rpx;
@@ -255,55 +400,68 @@ function submit(){
font-size: 28rpx;
}
.sub-tip{
.sub-tip {
font-size: 26rpx;
color: #999;
margin-bottom: 16rpx;
}
.img-grid{
.img-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.img-item{
.img-item {
position: relative;
width:150rpx;
width: 150rpx;
height: 150rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f0f0f0;
}
.img{ width: 100%; height: 100%; }
.add{ display:flex; align-items:center; justify-content:center; }
.plus{ font-size: 80rpx; color:#c0c0c0; line-height: 1; }
.del{
.img {
width: 100%;
height: 100%;
}
.add {
display: flex;
align-items: center;
justify-content: center;
}
.plus {
font-size: 80rpx;
color: #c0c0c0;
line-height: 1;
}
.del {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
background: rgba(0,0,0,.5);
background: rgba(0, 0, 0, 0.5);
color: #fff;
text-align: center;
line-height: 44rpx;
font-size: 28rpx;
}
.bottom-fixed{
.bottom-fixed {
position: fixed;
left: 0; right: 0; bottom: 0;
left: 0;
right: 0;
bottom: 0;
background: #fff;
padding: 24rpx;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,.06);
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.06);
}
.submit-btn{
.submit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background: #00bcd4;
background: #3cc7c0;
color: #fff;
font-size: 32rpx;
border-radius: 16rpx;
+649 -65
View File
@@ -1,13 +1,45 @@
<template>
<view class="my-code-page">
<view class="navbox" v-if="!jieping">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="我的二维码"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eeeeee"
>
<template #right>
<view class="nav-actions">
<view class="collect-img" @click="shareToggle" >
<image
class="share-img-icon"
:src="shareIcon"
mode="aspectFill"
/>
</view>
</view>
</template>
</uni-nav-bar>
</view>
<!-- <navBar title="我的二维码" v-if="!jieping"/> -->
<!-- 自己实现的 html2canvas 截图组件 -->
<MyHtml2canvas
ref="shotRef"
domId="render-dom"
@renderOver="onRenderOver"
/>
<view class="my-code-page renderDom" id="render-dom" :class="{'active':jieping}">
<!-- <webview src="https://dev-wx.igandan.com/expert/expertcodeimg?expert_uuid=9UFkll2Xo57km6224XE&fromtype=doctor"></webview> -->
<!-- 顶部导航栏 -->
<navBar title="我的二维码" />
<!-- 内容 -->
<scroll-view scroll-y class="page-scroll">
<scroll-view scroll-y class="page-scroll" id="pageContent">
<!-- 顶部蓝色横幅 -->
<view class="blue-banner">
<up-image :src="bgImg" width="100%" mode="widthFix" ></up-image>
<up-image :src="bgBase64 || bgImg" width="100%" mode="widthFix" ></up-image>
</view>
<!-- 白色信息卡 -->
@@ -16,10 +48,17 @@
<view class="leftCircle"></view>
<view class="rightCircle"></view>
<view class="halfCircle"></view>
<view class="avatar-wrapper">
<image class="avatar" :src="docUrl+userInfo.photo" mode="aspectFill" />
<view class="star" v-if="userInfo.isStar==1">
咨询专家
</view>
<view class="name-viewne">{{ userInfo.realName }} {{ userInfo.positionName }}</view>
<view class="avatar-wrapper">
<image
class="avatar"
:src="userInfo.avatarBase64 || userInfo.localPhoto || (docUrl+userInfo.photo)"
mode="aspectFill"
/>
</view>
<view class="name-viewne">{{ userInfo.realName }} <text class="position-name">{{ userInfo.positionName }}</text></view>
<view class="org-viewne">{{ userInfo.hospitalName }}</view>
<view class="dash-viewne"></view>
<view class="slogan">
@@ -28,10 +67,10 @@
</view>
<view class="contact-qr">
<view class="contact-btn">
<up-image :src="viewnkImg" width="430rpx" height="131rpx" ></up-image>
<up-image :src="viewnkBase64 || viewnkImg" width="430rpx" height="131rpx" ></up-image>
</view>
<image class="qr-img" :src="docUrl+userInfo.qrcode" mode="aspectFit" />
<image class="qr-img" :src="userInfo.localQrcode || (docUrl+userInfo.qrcode)" mode="aspectFit" />
</view>
</view>
</view>
@@ -44,8 +83,8 @@
<view class="s-bottom">
<view>
<view class="descrow"><text>1</text>微信扫一扫上方二维码,关注“肝胆相照一家人”公众号</view>
<view class="descrow"><text>2</text>点击“邹建东专家工作室”,选择微信登录,注册后直接发送随访申请</view>
<view class="descrow"><text>3</text>若未弹出“随访申请发送成功”提示,请再次点击“邹建东专家工作室”发送随访申请</view>
<view class="descrow"><text>2</text>点击“{{ userInfo.realName }}专家工作室”,选择微信登录,注册后直接发送随访申请</view>
<view class="descrow"><text>3</text>若未弹出“随访申请发送成功”提示,请再次点击“{{ userInfo.realName }}专家工作室”发送随访申请</view>
<view class="descrow"><text>4</text>审核通过后,点击“就医服务-随访交流”,与专家进行图文交流</view>
</view>
</view>
@@ -58,61 +97,160 @@
</scroll-view>
<!-- 底部保存按钮 -->
<view class="save-bar">
<button class="save-btn" @click="onSave">保存二维码到手机</button>
<view class="save-bar" v-if="!jieping">
<button class="save-btn" @click="onSave">保存到手机相册</button>
</view>
</view>
<!-- 分享弹窗 -->
<uni-popup ref="shareRef" type="bottom" safeArea backgroundColor="#fff">
<view class="share-popup">
<view class="share-title">分享到</view>
<view class="share-content">
<view class="share-item" @click="shareToWechat">
<view class="share-icon wechat-icon">
<image class="share-img" :src="wxImg" mode="aspectFill" />
</view>
<text class="share-text">微信</text>
</view>
<view class="share-item" @click="shareToMoments">
<view class="share-icon moments-icon">
<image class="share-img" :src="friendImg" mode="aspectFill" />
</view>
<text class="share-text">朋友圈</text>
</view>
<view class="share-item" @click="shareToWeibo">
<view class="share-icon weibo-icon">
<image class="share-img" :src="sinaImg" mode="aspectFill" />
</view>
<text class="share-text">新浪微博</text>
</view>
</view>
<view class="share-cancel" @click="closeShare">
<text>取消</text>
</view>
</view>
</uni-popup>
</template>
<script setup>
<script setup >
import { base64ToPath } from 'image-tools'
import { urlToBase64 } from '@/uni_modules/sp-html2canvas-render/utils/index.js'
import navBar from '@/components/navBar/navBar.vue'
import { onShow } from "@dcloudio/uni-app";
import { onShow,onBackPress,onLoad} from "@dcloudio/uni-app";
import { ref } from 'vue';
import MyHtml2canvas from '@/components/MyHtml2canvas.vue'
import docUrl from '@/utils/docUrl'
import bgImg from "@/static/background.jpg"
import otherHost from '@/utils/otherHost'
import shareIcon from "@/static/icon_share.png";
import sinaImg from "@/static/share_sina.png";
import wxImg from "@/static/share_weixin.png";
import friendImg from "@/static/share_wxc.png";
import logoImg from "@/static/weiboShare.png";
import isAndroid from "@/utils/platform.js";
// 背景图使用线上地址
const bgImg = 'https://oss.igandan.com/images/expertcode_background.jpg?v=1'
import viewnkImg from "@/static/arr.png"
const userInfo = ref({})
const jieping = ref(false)
const shotRef = ref(null)
const bgBase64 = ref('')
const viewnkBase64 = ref('')
const from=ref('');
const title=ref('');
const summary=ref('参与爱肝行动,共创美好前程');
const link=ref('');
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
const goBack = () => {
if(!from.value){
plus.runtime.quit();
return true;
}else{
uni.navigateBack();
};
onShow(()=>{
userInfo.value = uni.getStorageSync('userInfo')
})
const onSave = () => {
// 检查是否有二维码图片
if (!userInfo.value.qrcode) {
uni.showToast({ title: '二维码不存在', icon: 'none' });
return;
}
// 显示加载提示
uni.showLoading({ title: '保存中...' });
// 下载二维码图片
uni.downloadFile({
url: docUrl + userInfo.value.qrcode,
success: (res) => {
if (res.statusCode === 200) {
// 保存到相册
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.hideLoading();
uni.showToast({
title: '保存成功',
icon: 'none',
duration: 2000
});
};
onLoad((options) => {
from.value = options.from;
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "我的二维码",
},
fail: (err) => {
uni.hideLoading();
console.error('保存失败:', err);
});
})
// 根据错误类型给出不同提示
if (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize')) {
// 页面显示时拉取用户信息,并尽量把图片转成 base64,避免 html2canvas 跨域污染
onShow(async () => {
const info = uni.getStorageSync('userInfo') || {}
console.log('[myCode] onShow userInfo:', info)
title.value = '不方便到医院就诊,'+info.realName+'医生在肝胆相照平台帮助您!';
link.value = otherHost+'/expert/expertcodeimg?expert_uuid='+info.uuid+'&fromtype=doctor'
// 背景图转 base64
if (!bgBase64.value) {
try {
console.log('[myCode] 准备将背景图转为 base64:', bgImg)
const bg = await urlToBase64(bgImg, 'jpg')
bgBase64.value = bg
console.log('[myCode] 背景图转 base64 成功,长度:', bg && bg.length)
} catch (e) {
console.error('[myCode] 背景图转 base64 失败:', e)
}
}
// 按钮图转 base64
if (!viewnkBase64.value) {
try {
console.log('[myCode] 准备将按钮图转为 base64:', viewnkImg)
const btn = await urlToBase64(viewnkImg, 'png')
viewnkBase64.value = btn
console.log('[myCode] 按钮图转 base64 成功,长度:', btn && btn.length)
} catch (e) {
console.error('[myCode] 按钮图转 base64 失败:', e)
}
}
// 头像:远程地址转 base64,写入 avatarBase64,优先被使用
if (info.photo && !info.avatarBase64) {
const avatarUrl = docUrl + info.photo
try {
console.log('[myCode] 准备将头像转为 base64:', avatarUrl)
const avatarBase64 = await urlToBase64(avatarUrl, 'jpg')
info.avatarBase64 = avatarBase64
console.log('[myCode] 头像转 base64 成功,长度:', avatarBase64 && avatarBase64.length)
} catch (e) {
console.error('[myCode] 头像转 base64 失败:', e)
}
}
// 二维码:远程地址转 base64,写入 localQrcode,模板已优先使用该字段
if (info.qrcode && !info.localQrcode) {
const qrUrl = docUrl + info.qrcode
try {
console.log('[myCode] 准备将二维码转为 base64:', qrUrl)
const qrBase64 = await urlToBase64(qrUrl, 'png')
info.localQrcode = qrBase64
console.log('[myCode] 二维码转 base64 成功,长度:', qrBase64 && qrBase64.length)
} catch (e) {
console.error('[myCode] 二维码转 base64 失败:', e)
}
}
userInfo.value = info
})
// 保存失败时统一处理
const handleSaveError = (err) => {
if (err && err.errMsg && (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize'))) {
uni.showModal({
title: '权限提示',
content: '需要相册权限才能保存图片,请在设置中开启权限',
content: '需要相册权限才能保存图片,请在系统设置中开启相册权限',
showCancel: false,
confirmText: '知道了'
});
@@ -123,35 +261,306 @@ const onSave = () => {
duration: 2000
});
}
};
// 点击“保存到手机相册”
const onSave = () => {
console.log('[myCode] onSave 点击,准备开始截图')
// 使用自定义组件触发 html2canvas(内部通过 renderjs 在视图层执行)
jieping.value = true
// 调用自定义组件方法开始渲染
if (shotRef.value && shotRef.value.h2cRenderDom) {
shotRef.value.h2cRenderDom()
} else {
console.error('[myCode] shotRef 或 h2cRenderDom 不存在')
jieping.value = false
uni.showToast({
title: '截图组件未准备好',
icon: 'none'
})
}
};
// 自定义组件截图完成回调(拿到 base64)
const onRenderOver = (base64) => {
console.log('[myCode] onRenderOver base64 长度:', base64 && base64.length)
if (!base64) {
jieping.value = false
uni.showToast({
title: '生成图片失败',
icon: 'none'
})
return
}
base64ToPath(base64)
.then((res) => {
const path = res.path || res
console.log('[myCode] base64ToPath 转换成功,path:', path)
uni.saveImageToPhotosAlbum({
filePath: path,
success() {
uni.showToast({
title: '保存图片成功',
mask: false,
duration: 1500
})
},
fail(err) {
console.error('[myCode] 保存到相册失败:', err)
handleSaveError(err)
},
complete() {
jieping.value = false
}
})
})
.catch((err) => {
console.error('[myCode] base64ToPath 失败:', err)
handleSaveError(err)
jieping.value = false
})
}
const shareRef = ref();
// 分享APP
const shareToggle = () => {
shareRef.value.open();
};
// 关闭分享弹窗
const closeShare = () => {
shareRef.value.close();
};
// 分享到微信
const shareToWechat = () => {
// #ifdef APP-PLUS
// 使用系统分享
uni.downloadFile({
url:'https://doc.igandan.com/app/html/img/2016/20160714132557.png',
success: function (res) {
uni.compressImage({
src: res.tempFilePath,
quality: 60,
success: function (res2) {
uni.share({
provider: "weixin",
scene: "WXSceneSession",
type: 0,
title: title.value,
summary: summary.value,
href: link.value,
imageUrl: res2.tempFilePath,
success: function (res) {
console.log("success:" + JSON.stringify(res));
},
fail: function (err) {
console.log("fail:" + JSON.stringify(err));
},
});
},
});
},
});
// uni.share({
// provider: "weixin",
// scene: "WXSceneSession",
// type: 0,
// title: videoInfo.value.name,
// summary: videoInfo.value.note,
// href: shareLink.value,
// imageUrl: docUrl + videoInfo.value.imgpath,
// success: function (res) {
// //console.log("success:" + JSON.stringify(res));
// },
// fail: function (err) {
// //console.log("fail:" + JSON.stringify(err));
// },
// });
// #endif
// #ifdef H5
// H5环境下的分享
if (navigator.share) {
navigator
.share({
title: "肝胆相照APP",
text: "专业的医疗健康平台,快来下载体验吧!",
url: shareLink.value,
})
.then(() => {
uni.showToast({
title: "分享成功",
icon: "success",
});
})
.catch(() => {
uni.showToast({
title: "分享失败",
icon: "none",
});
});
} else {
uni.hideLoading();
// 复制到剪贴板
uni.setClipboardData({
data:
"肝胆相照APP - 专业的医疗健康平台,快来下载体验吧!\n下载链接:" +
shareLink.value,
success: () => {
uni.showToast({
title: '下载失败,请检查网络',
icon: 'none',
duration: 2000
title: "已复制到剪贴板",
icon: "success",
});
}
},
fail: (err) => {
uni.hideLoading();
console.error('下载失败:', err);
uni.showToast({
title: '下载失败,请检查网络',
icon: 'none',
duration: 2000
});
}
// #endif
// #ifdef MP-WEIXIN
// 微信小程序分享
uni.showShareMenu({
withShareTicket: true,
menus: ["shareAppMessage", "shareTimeline"],
});
// #endif
closeShare();
};
// 分享到朋友圈
const shareToMoments = () => {
// #ifdef APP-PLUS
uni.downloadFile({
url: 'https://doc.igandan.com/app/html/img/2016/20160714132557.png',
success: function (res) {
uni.compressImage({
src: res.tempFilePath,
quality: 60,
success: function (res2) {
uni.share({
provider: "weixin",
scene: "WXSceneTimeline",
type: 0,
title: title.value,
summary: summary.value,
href: link.value,
imageUrl: res2.tempFilePath,
success: function (res) {
//console.log("success:" + JSON.stringify(res));
},
fail: function (err) {
//console.log("fail:" + JSON.stringify(err));
},
});
},
});
},
});
// #endif
// #ifdef H5
// 复制到剪贴板
uni.setClipboardData({
data: "肝胆相照APP - 专业的医疗健康平台,快来下载体验吧!\n下载链接:https://www.igandan.com",
success: () => {
uni.showToast({
title: "已复制到剪贴板,可分享到朋友圈",
icon: "success",
});
},
});
// #endif
// #ifdef MP-WEIXIN
uni.showShareMenu({
withShareTicket: true,
menus: ["shareAppMessage", "shareTimeline"],
});
// #endif
closeShare();
};
// 分享到新浪微博
const shareToWeibo = () => {
// #ifdef APP-PLUS
uni.share({
provider: "sinaweibo",
type: 0,
title: title.value,
summary: summary.value,
href: link.value,
imageUrl: logoImg,
success: function (res) {
//console.log("分享成功");
},
fail: function (err) {
//console.log("fail:" + JSON.stringify(err));
},
});
// plus.share.sendWithSystem({
// type: 'text',
// content: '肝胆相照APP - 专业的医疗健康平台,快来下载体验吧!\n下载链接:https://www.igandan.com'
// }, () => {
// uni.showToast({
// title: '分享成功',
// icon: 'success'
// })
// }, (err) => {
// console.log('分享失败:', err)
// uni.showToast({
// title: '分享失败',
// icon: 'none'
// })
// })
// #endif
// #ifdef H5
// 复制到剪贴板
uni.setClipboardData({
data: "肝胆相照APP - 专业的医疗健康平台,快来下载体验吧!\n下载链接:https://www.igandan.com",
success: () => {
uni.showToast({
title: "已复制到剪贴板,可分享到微博",
icon: "success",
});
},
});
// #endif
closeShare();
};
</script>
<style lang="scss" scoped>
.share-img {
width: 100rpx;
height: 100rpx;
}
.share-img-icon {
width: 40rpx;
height:40rpx;
}
.nav-actions {
display: flex;
align-items: center;
justify-content: center;
}
.my-code-page {
min-height: 100vh;
background-color: #0d7dfd;
.active{
transform: scale(0.8);
margin-top: -calc(var(--status-bar-height) + 44px);
transition: all 0.3s ease-in-out;
}
}
.squrebox{
margin:0 30rpx;
@@ -226,7 +635,8 @@ const onSave = () => {
}
}
.page-scroll {
height: calc(100vh - 100rpx);
margin-top: calc(var(--status-bar-height) + 44px);
// height: calc(100vh - var(--status-bar-height) - 44px);
padding-bottom: 140rpx;
}
@@ -251,7 +661,7 @@ const onSave = () => {
.qr-card {
position: relative;
margin: -120rpx 30rpx 30rpx;
margin: -80rpx 30rpx 30rpx;
background: #ffffff;
border-radius: 20rpx;
padding: 140rpx 30rpx 30rpx;
@@ -275,6 +685,22 @@ const onSave = () => {
border-radius: 50%;
background-color: #0d7dfd;
}
.star{
position: absolute;
top: 48rpx;
height: 40rpx;
padding:0 16rpx;
left: 50%;
transform: translateX(-50%);
background-color:#ff9f1a;
color: #fff;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
z-index:10;
font-size: 24rpx;
}
.halfCircle{
border-radius: 50%;
position: absolute;
@@ -312,13 +738,17 @@ const onSave = () => {
.name-viewne {
text-align: center;
font-size: 34rpx;
font-size: 36rpx;
color: #1a76d2;
font-weight: bold;
margin-top: 10rpx;
.position-name{
font-weight: normal;
}
}
.org-viewne {
text-align: center;
font-size: 28rpx;
font-size: 36rpx;
color: #4a90e2;
margin-top: 16rpx;
}
@@ -400,4 +830,158 @@ const onSave = () => {
font-weight: normal;
}
}
/* 分享弹窗样式 */
.share-popup {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
padding: 40rpx 0 0;
}
.share-title {
text-align: center;
font-size: 32rpx;
color: #333;
margin-bottom: 40rpx;
font-weight: 500;
}
.share-content {
display: flex;
justify-content: space-around;
padding: 0 40rpx 40rpx;
}
.share-item {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.share-icon {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.wechat-icon {
background-color: #07c160;
}
.moments-icon {
background-color: #07c160;
}
.weibo-icon {
background-color: #e6162d;
}
.qq-icon {
background-color: #12b7f5;
}
.share-icon-text {
font-size: 50rpx;
color: #fff;
font-weight: bold;
}
.share-text {
font-size: 24rpx;
color: #666;
}
.share-cancel {
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
border-top: 1rpx solid #f0f0f0;
font-size: 32rpx;
color: #333;
}
.share-cancel:active {
background-color: #f5f5f5;
}
// 删除确认弹窗
.delete-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
}
.delete-modal-content {
width: 600rpx;
background-color: #fff;
border-radius: 24rpx;
padding: 60rpx 40rpx 40rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
}
.delete-modal-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
text-align: center;
margin-bottom: 30rpx;
}
.delete-modal-text {
font-size: 28rpx;
color: #666;
text-align: center;
line-height: 1.6;
margin-bottom: 50rpx;
word-break: break-all;
}
.delete-modal-buttons {
display: flex;
gap: 20rpx;
}
.delete-modal-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 500;
transition: all 0.3s ease;
}
.cancel-btn {
background-color: #f5f5f5;
color: #333;
&:active {
background-color: #e0e0e0;
}
}
.confirm-btn {
background-color: #8b2316;
color: #fff;
&:active {
background-color: #6b1a0f;
}
}
</style>
+218
View File
@@ -0,0 +1,218 @@
<template>
<view class="detail-page">
<navBar title="订单详情" />
<view class="order-card">
<view class="order-top">
<text class="download-no">下载课件号: {{ detail.downloadNo || "--" }}</text>
<text class="pay-status">{{ detail.statusText || "--" }}</text>
</view>
<view class="section-divider"></view>
<view class="course-title">{{ detail.title || "--" }}</view>
<view class="info-list">
<view class="info-row">
<text class="info-label">课件作者:</text>
<text class="info-value">{{ detail.author || "--" }}</text>
</view>
<view class="info-row">
<text class="info-label">金额:</text>
<text class="info-value amount">{{ detail.amountText || "--" }}</text>
</view>
<view class="info-row">
<text class="info-label">创建时间:</text>
<text class="info-value">{{ detail.createTime || "--" }}</text>
</view>
<view class="info-row">
<text class="info-label">支付方式:</text>
<text class="info-value">{{ detail.payType}}</text>
</view>
<view class="info-row">
<text class="info-label">支付时间:</text>
<text class="info-value">{{ detail.payTime || "--" }}</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import api from "@/api/api.js";
const coursewareId = ref("");
const detail = ref({
downloadNo: "",
statusText: "",
title: "",
author: "",
amountText: "",
createTime: "",
payType: "",
payTime: "",
});
const formatDateTime = (value) => {
if (!value) return "";
if (typeof value === "string" && value.includes("-")) return value;
const timeNum = Number(value);
if (!timeNum) return "";
const ms = timeNum < 10000000000 ? timeNum * 1000 : timeNum;
const d = new Date(ms);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const h = String(d.getHours()).padStart(2, "0");
const min = String(d.getMinutes()).padStart(2, "0");
const s = String(d.getSeconds()).padStart(2, "0");
return `${y}-${m}-${day} ${h}:${min}:${s}`;
};
const formatAmount = (value) => {
if (value === undefined || value === null || value === "") return "";
const num = Number(value);
if (Number.isNaN(num)) return "";
const amount = num / 100;
return `${amount.toFixed(2)}元`;
};
const formatStatus = (raw) => {
if (raw === undefined || raw === null) return "";
const text = String(raw).toLowerCase();
if (text === "paid" || text === "success" || text === "1" || text === "2") return "已支付";
if (text === "unpaid" || text === "0" || text === "created") return "未支付";
return String(raw);
};
const formatPayType = (raw) => {
if(raw=='balance'){
return '余额支付';
}else if(raw=='FREE'){
return '免费'
}else if(raw=='USEWELFARENUM'){
return '免费次数';
}else if(raw=='wx'){
return '微信支付';
}
};
const fetchCoursewareDetail = async () => {
if (!coursewareId.value) return;
try {
const res = await api.getGanDanFileOrder({
order_id: coursewareId.value,
});
if (res.code !== 200 || !res.data) return;
const data = res.data;
detail.value = {
downloadNo: data.order_id || data.download_no || data.gandanfile_order_id || data.id || "",
statusText: formatStatus(data.order_status || data.status || data.pay_status || ""),
title: data.title || data.name || data.file_name || "",
author: data.provider_name || data.author || data.nickname || "",
amountText: formatAmount(data.pay_amount || data.total_fee || data.amount || data.price || 0),
createTime: formatDateTime(data.create_date || data.create_time || data.order_time || ""),
payType: formatPayType(data.pay_channel),
payTime: formatDateTime(data.pay_date || data.pay_time || ""),
};
} catch (error) {
uni.showToast({
title: "获取详情失败",
icon: "none",
});
}
};
onLoad((options) => {
coursewareId.value = options.id;
fetchCoursewareDetail();
});
</script>
<style lang="scss" scoped>
.detail-page {
background: #f5f5f5;
}
.order-card {
background: #fff;
top: calc(var(--status-bar-height) + 44px);
position: fixed;
left: 0;
right:0;
z-index: 10;
overflow-y: scroll;
}
.order-top {
min-height: 88rpx;
padding: 0 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.download-no {
font-size: 30rpx;
color: #000;
}
.pay-status {
font-size: 32rpx;
color: #8b2316;
}
.section-divider {
height: 16rpx;
background: #f5f5f5;
}
.course-title {
padding: 20rpx 30rpx;
color: #8b2316;
font-size: 32rpx;
line-height: 1.5;
word-break: break-all;
border-bottom: 2rpx solid #ccc;
}
.info-list {
padding: 24rpx 30rpx;
}
.info-row {
min-height: 56rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10rpx;
}
.info-row:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 30rpx;
color: #000;
}
.info-value {
max-width: 68%;
font-size: 28rpx;
color: #666;
text-align: right;
word-break: break-all;
}
.amount {
color:red;
}
</style>
+97 -65
View File
@@ -1,18 +1,18 @@
<template>
<view class="my-courseware-page">
<!-- 顶部导航栏 -->
<!-- <navBar title="我的课件" /> -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="我的课件"
@cviewckLeft="goBack"
fixed
@clickLeft="goBack"
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
>
</uni-nav-bar>
/>
</view>
<view class="my-courseware-page">
<!-- 顶部导航栏 -->
<!-- 标签页导航 -->
<view class="tab-nav">
@@ -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">{{ activeTab === 'download' ? '下载账户' : '分享账户' }}: {{ downloadCount }}</text>
<text class="summary-text">{{ totalAmount}}</text>
</view>
<view class="summary-item">
<up-image :src="moneyImg" width="36rpx" height="36rpx" ></up-image>
<text class="summary-text">{{ activeTab === 'download' ? '文件数量' : '分享文件' }}: {{ totalAmount }}</text>
<text class="summary-text">{{ downloadCount }}</text>
</view>
</view>
@@ -49,12 +49,15 @@
<scroll-view
scroll-y
class="courseware-list"
:scroll-top="listScrollTop"
:scroll-into-view="scrollIntoViewId"
refresher-enabled
@refresherrefresh="onRefresh"
:refresher-triggered="refreshing"
@scrolltolower="onLoadMore"
:lower-threshold="100"
>
<view id="courseware-top"></view>
<!-- 空状态 -->
<view v-if="coursewareList.length === 0 && !loading" class="empty-state">
<text>{{ activeTab === 'download' ? '暂无下载数据' : '暂无分享数据' }}</text>
@@ -62,21 +65,22 @@
<view class="courseware-item" v-for="(item, index) in coursewareList" :key="index" @click="onItemClick(item)">
<view class="item-content">
<view class="courseware-provider" v-if="activeTab === 'share'">
<text class="label">下载医生</text>
<text class="value">:{{ item.providername }}</text>
</view>
<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>
<text class="label">课件名称</text>
<text class="value">:{{ item.name }}</text>
</view>
<view class="courseware-time">
<text class="label">时间:</text>
<text class="value">{{ item.time }}</text>
<text class="label">时间</text>
<text class="value">:{{ item.time }}</text>
</view>
<view class="courseware-status">
<text class="label">状态:</text>
<text class="value status-paid">{{ item.status }}</text>
<view class="courseware-status" v-if="activeTab === 'download'">
<text class="label">状态</text>
<text class="value status-paid">:{{item.status}}</text>
</view>
</view>
</view>
@@ -95,31 +99,76 @@
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, nextTick } from 'vue';
import api from '@/api/api';
import navTo from '@/utils/navTo.js';
import { onLoad,onBackPress } from '@dcloudio/uni-app';
import downLoadImg from "@/static/course_download.png"
import moneyImg from "@/static/course_yuan.png"
const activeTab = ref('download');
const listScrollTop = ref(0);
const scrollIntoViewId = ref('');
const refreshing = ref(false);
const loading = ref(false);
const noMore = ref(false);
const page = ref(1);
const pageSize = ref(10);
const downloadCount = ref(4);
const totalAmount = ref('20.00');
const downloadCount = ref(0);
const totalAmount = ref(0);
const coursewareList = ref([]);
const from = ref('');
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
onLoad((options) => {
if(options.from){
from.value = options.from;
}
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "我的课件",
},
});
});
onMounted(() => {
onRefresh();
});
const getStatusText = (status) => {
console.log(status);
if(status === 'paid'){
return '已支付';
}else if(status === 'unpaid'){
return '未支付';
}else{
return '已分享';
}
};
const goBack = () => {
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
};
onLoad((options) => {
if(options.from){
//from.value = options.from;
switchTab('share')
}
})
const switchTab = (tab) => {
activeTab.value = tab;
listScrollTop.value = 0;
scrollIntoViewId.value = '';
nextTick(() => {
scrollIntoViewId.value = 'courseware-top';
});
// 切换标签时重置数据
page.value = 1;
noMore.value = false;
@@ -132,36 +181,9 @@ const onItemClick = (item) => {
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' });
// 这里可以添加分享相关的操作,比如查看分享详情、重新分享等
navTo({
url: `/pages_app/myCourseware/coursewareDetail?id=${item.order_id}`
})
}
};
@@ -175,7 +197,7 @@ const onRefresh = () => {
if (res.code === 200 && res.data) {
const data = res.data.list;
// 更新摘要信息
downloadCount.value = res.data.downloadTotalAccount || 0;
downloadCount.value = (res.data.downloadTotalAccount/100).toFixed(2) || 0;
totalAmount.value = res.data.downloadFileCount || 0;
// 处理课件列表数据
@@ -186,7 +208,7 @@ const onRefresh = () => {
uuid: item.uuid,
order_id: item.order_id,
type: item.type,
providername: item.providername
providername: item.downloadername
}));
// 判断是否还有更多数据
@@ -203,7 +225,7 @@ const onRefresh = () => {
if (res.code === 200 && res.data) {
const data = res.data.list;
// 更新摘要信息 - 分享数据使用不同的字段
downloadCount.value = res.data.shareTotalAccount || 0;
downloadCount.value = (res.data.shareTotalAccount/100).toFixed(2) || 0;
totalAmount.value = res.data.shareloadFileCount || 0;
// 处理分享列表数据
@@ -305,7 +327,7 @@ const onLoadMore = () => {
background: #ffffff;
border-bottom: 2rpx solid #f0f0f0;
position: fixed;
top: 180rpx;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
z-index: 10;
@@ -332,20 +354,21 @@ const onLoadMore = () => {
.summary-bar {
display: flex;
justify-content: space-between;
align-items: center;
background: #8B2316;
padding: 20rpx 30rpx;
position: fixed;
top: 260rpx; // 标签页导航下方
top: calc(var(--status-bar-height) + 44px + 124rpx); // 标签页导航下方
left: 0;
right: 0;
z-index: 10;
.summary-item {
flex:1;
display: flex;
align-items: center;
justify-content: flex-start;
.summary-text {
color: #ffffff;
font-size: 28rpx;
@@ -355,9 +378,9 @@ const onLoadMore = () => {
}
.courseware-list {
height: calc(100vh - 180rpx - 116rpx - 80rpx);
// height: calc(100vh - 180rpx - 116rpx - 80rpx);
position: fixed;
top: 336rpx; // 调整顶部位置,为固定的摘要栏腾出空间
top: calc(var(--status-bar-height) + 44px + 124rpx + 80rpx); // 调整顶部位置,为固定的摘要栏腾出空间
bottom: 0;
width: 100%;
}
@@ -383,6 +406,15 @@ const onLoadMore = () => {
width: 120rpx;
color: #8B2316;
font-size: 28rpx;
display: inline-block;
text-align: justify;
text-align-last: justify;
text-justify: inter-ideograph;
}
.label::after {
content: '';
display: inline-block;
width: 100%;
}
.value {
+231 -40
View File
@@ -2,17 +2,21 @@
<view class="flower-page">
<!-- 顶部统计栏(与截图一致:两项) -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="我的鲜花"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
/>
</view>
<view class="fixed-top-block">
<view class="stats-bar">
<view class="stat">
<up-image :src="flowerImg" width="36rpx" height="36rpx" ></up-image>
@@ -30,6 +34,7 @@
<text class="col time">时间</text>
<text class="col qty">数量</text>
</view>
</view>
<!-- 表体/列表或空状态 -->
<scroll-view
@@ -43,6 +48,7 @@
lower-threshold="100"
:scroll-top="scrollTop"
>
<view class="list-content">
<view v-if="records.length === 0" class="empty-wrap">
<up-image :src="emptyImg" width="176rpx" height="204rpx" ></up-image>
<text class="empty-text">您暂未收到鲜花</text>
@@ -60,20 +66,44 @@
<view v-if="noMore && records.length > 0" class="no-more">
<text>没有更多数据了</text>
</view>
<view 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> -->
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, onMounted, nextTick } from 'vue';
import flowerImg from "@/static/flowers.png"
import moneyImg from "@/static/mind_totle_money.png"
import emptyImg from "@/static/icon_empty.png"
import api from '@/api/api.js';
import { onBackPress,onLoad } from '@dcloudio/uni-app';
const from=ref('');
const user_uuid=ref('');
onLoad((options) => {
if(options.from){
from.value = options.from;
}
let userInfo= uni.getStorageSync('userInfo')
user_uuid.value = userInfo.uuid;
loadFirstTwoPages();
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "我的鲜花",
},
});
});
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
// 与截图一致的顶部两项统计:总鲜花数与累计金额
const stat = ref({ totalCount: 0, totalAmount: 0.0 });
@@ -87,19 +117,44 @@ const noMore = ref(false);
const page = ref(1);
const pageSize = ref(10);
const scrollTop = ref(0);
const autoLoading = ref(false);
const formatDateToYDM = (value) => {
if (!value) return '-';
const raw = String(value).trim();
const directMatch = raw.match(/^(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})/);
if (directMatch) {
const [, year, month, day] = directMatch;
return `${year}-${day.padStart(2, '0')}-${month.padStart(2, '0')}`;
}
const parsedDate = new Date(raw);
if (Number.isNaN(parsedDate.getTime())) return '-';
const year = parsedDate.getFullYear();
const month = String(parsedDate.getMonth() + 1).padStart(2, '0');
const day = String(parsedDate.getDate()).padStart(2, '0');
return `${year}-${day}-${month}`;
};
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({ url: '/pages/index/index' });
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
});
};
const getFlowerList = () => {
return new Promise((resolve, reject) => {
console.log('[myFlower] getFlowerList:start', {
page: page.value,
loading: loading.value,
refreshing: refreshing.value,
noMore: noMore.value
});
// 只在第一页时设置loading状态,避免与onLoadMore冲突
if (page.value === 1) {
loading.value = true;
@@ -107,20 +162,30 @@ const getFlowerList = () => {
console.log('正在获取第', page.value, '页数据...');
api.getFlowerList({page: page.value}).then(res => {
api.getFlowerList({
page: page.value,
user_uuid: user_uuid.value,
}).then(res => {
console.log('接口返回数据:', res);
if (res.code === 200 && res.data) {
const { flower_data, total_amount, total_num } = res.data;
console.log('[myFlower] getFlowerList:response', {
reqPage: page.value,
pageNum: flower_data?.pageNum,
pages: flower_data?.pages,
isLastPage: flower_data?.isLastPage,
listLength: flower_data?.list?.length || 0
});
// 更新统计数据
stat.value.totalCount = total_num || 0;
stat.value.totalAmount = total_amount || 0;
// 处理列表数据
if (flower_data && flower_data.list) {
if (flower_data && Array.isArray(flower_data.list)) {
const newRecords = flower_data.list.map(item => ({
name: item.patient_name || '未知',
time: item.create_date || '',
name: item.patient_name || '账号已注销',
time: formatDateToYDM(item.create_date),
amount: item.num || 0,
// 保留原始数据以备后用
original: item
@@ -135,12 +200,28 @@ const getFlowerList = () => {
console.log('第', page.value, '页数据,追加到列表,新增', newRecords.length, '条');
}
// 判断是否还有更多数据
// 判断是否还有更多数据,兼容不同接口字段
if (typeof flower_data.isLastPage === 'boolean') {
noMore.value = flower_data.isLastPage;
console.log('是否最后一页:', flower_data.isLastPage, ',总页数:', flower_data.pages);
} else if (Number(flower_data.pages) > 0) {
noMore.value = page.value >= Number(flower_data.pages);
} else {
// 未返回分页字段时,仅在本页无数据时判定到底
noMore.value = newRecords.length === 0;
}
console.log('是否最后一页:', noMore.value, ',总页数:', flower_data.pages);
}
console.log('[myFlower] getFlowerList:end', {
page: page.value,
records: records.value.length,
noMore: noMore.value
});
resolve(res);
} else {
console.log('[myFlower] getFlowerList:invalid', {
code: res?.code,
msg: res?.msg
});
reject(new Error('接口返回错误'));
}
}).catch(err => {
@@ -156,18 +237,11 @@ const getFlowerList = () => {
});
});
};
onMounted(() => {
getFlowerList();
});
const onRefresh = async () => {
if (refreshing.value) return;
if (refreshing.value || loading.value) return;
refreshing.value = true;
try {
page.value = 1;
noMore.value = false;
records.value = []; // 清空数据
getFlowerList(); // 重新获取第一页数据
await loadFirstTwoPages();
uni.showToast({ title: '刷新成功', icon: 'none' });
} catch (error) {
console.error('刷新失败:', error);
@@ -177,39 +251,136 @@ const onRefresh = async () => {
const onLoadMore = async () => {
console.log('触发上拉加载更多事件');
console.log('[myFlower] onLoadMore:enter', {
page: page.value,
loading: loading.value,
refreshing: refreshing.value,
noMore: noMore.value
});
// 检查各种状态
if (loading.value) {
if (loading.value || refreshing.value) {
console.log('正在加载中,忽略重复请求');
console.log('[myFlower] onLoadMore:blockByState');
return;
}
if (noMore.value) {
console.log('已经没有更多数据了');
console.log('[myFlower] onLoadMore:blockByNoMore');
return;
}
console.log('开始加载下一页,当前页码:', page.value);
loading.value = true;
const currentPage = page.value;
try {
page.value += 1;
page.value = currentPage + 1;
console.log('[myFlower] onLoadMore:request', {
fromPage: currentPage,
toPage: page.value
});
await getFlowerList(); // 等待数据加载完成
console.log('加载完成,当前页码:', page.value);
console.log('[myFlower] onLoadMore:done', {
page: page.value,
records: records.value.length,
noMore: noMore.value
});
} catch (error) {
console.error('加载更多失败:', error);
page.value -= 1; // 恢复页码
page.value = currentPage; // 恢复页码
console.log('[myFlower] onLoadMore:rollback', { page: page.value });
uni.showToast({ title: '加载失败', icon: 'error' });
} finally {
loading.value = false;
}
};
const getListLayoutInfo = () => {
return new Promise((resolve) => {
const query = uni.createSelectorQuery();
query.select('.table-body').boundingClientRect();
query.select('.list-content').boundingClientRect();
query.exec((res = []) => {
const bodyHeight = Number(res?.[0]?.height || 0);
const contentHeight = Number(res?.[1]?.height || 0);
resolve({ bodyHeight, contentHeight });
});
});
};
const ensureListCanScroll = async () => {
if (autoLoading.value || loading.value || refreshing.value || noMore.value) return;
autoLoading.value = true;
try {
while (!noMore.value) {
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 50));
const { bodyHeight, contentHeight } = await getListLayoutInfo();
if (!bodyHeight || !contentHeight) break;
if (contentHeight > bodyHeight) break;
await onLoadMore();
if (loading.value) break;
}
} finally {
autoLoading.value = false;
}
};
const loadFirstTwoPages = async () => {
console.log('[myFlower] loadFirstTwoPages:start');
page.value = 1;
noMore.value = false;
records.value = [];
console.log('[myFlower] loadFirstTwoPages:reset', {
page: page.value,
records: records.value.length,
noMore: noMore.value
});
await getFlowerList();
// 第1页 Promise resolve 早于 finally 复位 loading,这里手动兜底复位
loading.value = false;
console.log('[myFlower] loadFirstTwoPages:afterPage1', {
page: page.value,
records: records.value.length,
noMore: noMore.value
});
if (!noMore.value) {
console.log('[myFlower] loadFirstTwoPages:triggerPage2');
const firstPage = page.value;
try {
page.value = firstPage + 1;
console.log('[myFlower] loadFirstTwoPages:requestPage2', {
fromPage: firstPage,
toPage: page.value,
refreshing: refreshing.value
});
await getFlowerList();
loading.value = false;
} catch (error) {
page.value = firstPage;
console.log('[myFlower] loadFirstTwoPages:page2FailedRollback', { page: page.value });
throw error;
}
} else {
console.log('[myFlower] loadFirstTwoPages:skipPage2(noMore=true)');
}
console.log('[myFlower] loadFirstTwoPages:end', {
page: page.value,
records: records.value.length,
noMore: noMore.value
});
};
// 测试加载更多功能
const testLoadMore = () => {
console.log('手动测试加载更多');
onLoadMore();
};
</script>
<style lang="scss" scoped>
@@ -221,13 +392,27 @@ $card: #ffffff;
.flower-page {
background: $bg;
--nav-offset: calc(var(--status-bar-height) + 44px);
--stats-height: 86rpx;
--header-height: 74rpx;
/* 导航栏下方固定区域总高度:统计栏 + 表头 */
--fixed-block-height: calc(var(--stats-height) + var(--header-height));
}
.fixed-top-block {
position: fixed;
top: var(--nav-offset);
left: 0;
right: 0;
z-index: 20;
}
.stats-bar {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
height: var(--stats-height);
padding: 0 30rpx;
box-sizing: border-box;
background: #fff;
border-bottom: 2rpx solid #eee;
@@ -244,20 +429,25 @@ $card: #ffffff;
color: #fff;
display: flex;
justify-content: space-between;
padding: 22rpx 30rpx;
align-items: center;
height: var(--header-height);
padding: 0 30rpx;
box-sizing: border-box;
font-size: 30rpx;
position: sticky;
top: 180rpx; /* nav 高度 */
z-index: 5;
.col { width: 33%; }
.name { text-align: left; }
.time { text-align: center; }
.qty { text-align: right; }
.qty { text-align: center; }
}
.table-body {
height: calc(100vh - 180rpx - 88rpx - 70rpx);
position: fixed;
top: calc(var(--nav-offset) + var(--fixed-block-height));
left: 0;
right: 0;
bottom: 0;
box-sizing: border-box;
/* 充满剩余高度,避免滚动穿透 */
}
@@ -266,16 +456,17 @@ $card: #ffffff;
justify-content: space-between;
align-items: center;
padding: 26rpx 30rpx;
border-bottom: 2rpx solid #f2f2f2;
background: #fff;
border-bottom: 2rpx solid #ccc;
font-size: 28rpx;
color: #333;
.cell { width: 33%; }
.name { text-align: left; }
.time { text-align: center; color: #666; }
.qty { text-align: right; }
.qty.plus { color: #2dbd85; }
.qty.minus { color: #e34d4d; }
.qty { text-align: center; }
.qty.plus { color: #333; }
.qty.minus { color: #333; }
}
.empty-wrap { padding-top: 200rpx; display: flex; flex-direction: column; align-items: center; color: #bdbdbd; }
+153
View File
@@ -0,0 +1,153 @@
<template>
<view class="page">
<view class="header">
<text class="title">scroll-view 上拉加载 Demo</text>
<text class="sub">下拉刷新 + 触底加载更多</text>
</view>
<scroll-view
class="list-scroll"
scroll-y
lower-threshold="80"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
>
<view v-for="item in list" :key="item.id" class="list-item">
<text class="item-title">{{ item.title }}</text>
<text class="item-time">{{ item.time }}</text>
</view>
<view v-if="loading" class="tip">加载中...</view>
<view v-else-if="noMore && list.length > 0" class="tip">没有更多数据了</view>
<view v-else-if="list.length === 0" class="tip">暂无数据</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const page = ref(1);
const pageSize = ref(10);
const list = ref([]);
const loading = ref(false);
const refreshing = ref(false);
const noMore = ref(false);
const totalMockCount = 37;
const fullData = Array.from({ length: totalMockCount }, (_, i) => {
const index = i + 1;
return {
id: index,
title: `第 ${index} 条数据`,
time: `2026-03-${String((index % 28) + 1).padStart(2, '0')}`
};
});
const getPageData = (pageNum, size) => {
return new Promise((resolve) => {
setTimeout(() => {
const start = (pageNum - 1) * size;
const end = start + size;
resolve(fullData.slice(start, end));
}, 350);
});
};
const fetchList = async (isRefresh = false) => {
if (loading.value) return;
loading.value = true;
try {
if (isRefresh) {
page.value = 1;
noMore.value = false;
}
const rows = await getPageData(page.value, pageSize.value);
if (isRefresh) {
list.value = rows;
} else {
list.value = [...list.value, ...rows];
}
noMore.value = rows.length < pageSize.value;
} finally {
loading.value = false;
refreshing.value = false;
}
};
const onRefresh = async () => {
if (refreshing.value) return;
refreshing.value = true;
await fetchList(true);
};
const onLoadMore = async () => {
if (loading.value || refreshing.value || noMore.value) return;
page.value += 1;
await fetchList(false);
};
onMounted(() => {
fetchList(true);
});
</script>
<style scoped>
.page {
height: 100vh;
background: #f7f8fa;
}
.header {
padding: 30rpx;
background: #ffffff;
border-bottom: 1rpx solid #ededed;
}
.title {
display: block;
font-size: 34rpx;
color: #222;
font-weight: 600;
}
.sub {
display: block;
margin-top: 8rpx;
font-size: 24rpx;
color: #888;
}
.list-scroll {
height: calc(100vh - 130rpx);
}
.list-item {
margin: 20rpx 24rpx 0;
padding: 24rpx;
background: #ffffff;
border-radius: 12rpx;
}
.item-title {
display: block;
font-size: 30rpx;
color: #222;
}
.item-time {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #999;
}
.tip {
padding: 28rpx 0 40rpx;
text-align: center;
color: #9aa0a6;
font-size: 24rpx;
}
</style>
+99 -48
View File
@@ -1,24 +1,11 @@
<template>
<navBar :title="'新的患者'" />
<view class="new-patient-page">
<uni-nav-bar
left-icon="left"
title="我的患者"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
/>
<!-- 提醒区域 -->
<view class="contentbox">
<view class="reminder-section">
<view class="reminder-icon">
<uni-icons type="notification" size="16" color="#ff9500"></uni-icons>
<up-image :src="tipImg" width="35rpx" height="42rpx"></up-image>
</view>
<text class="reminder-text">提醒: 为了避免不必要的纠纷,请您务必选择线下就诊过的患者</text>
</view>
@@ -29,15 +16,15 @@
<view class="pending-request" v-for="(item, index) in applyList" :key="index">
<view class="request-item">
<view class="avatar">
<up-image :src="docUrl+item.photo" radius="10rpx" width="80rpx" height="80rpx" ></up-image>
<up-image :src="docUrl+item.photo" radius="10rpx" width="120rpx" height="120rpx" ></up-image>
</view>
<view class="request-content">
<view class="request-time">{{ item.createDate }}</view>
<view class="request-text">{{ item.content }}</view>
<view class="action-buttons">
<button class="reject-btn" @click="applyListOperate(item.uuid,3)">拒绝</button>
<button class="agree-btn" @click="applyListOperate(item.uuid,2)">同意</button>
<button class="reject-btn" @click="applyListOperate(item.uuid,3,item.content,item.patientUuid)">拒绝</button>
<button class="agree-btn" @click="applyListOperate(item.uuid,2,item.content,item.patientUuid)">同意</button>
</view>
</view>
</view>
@@ -48,14 +35,14 @@
<view class="history-section" v-if="historyList.length > 0">
<view class="section-title">申请记录(近一月)</view>
<view class="history-list">
<view class="history-item" v-for="(item, index) in historyList" :key="index">
<view class="history-item" v-for="(item, index) in historyList" :key="index" @click="goPatientDetail(item.patient_uuid,item.status)">
<view class="avatar">
<up-image v-if="docUrl+item.patient_photo" :src="docUrl + item.patient_photo" radius="10rpx" width="80rpx" height="80rpx"></up-image>
<up-image v-if="docUrl+item.patient_photo" :src="docUrl + item.patient_photo" radius="10rpx" width="120rpx" height="120rpx"></up-image>
</view>
<view class="history-content">
<view class="history-time">{{ formatDate(item.createDate) }}</view>
<view class="nickname">{{ item.nickname || item.patient_name }}</view>
<view class="history-time">{{ formatDate2(item.create_date) }}</view>
<view class="nickname">昵称:{{ item.patient_name || item.nickname }}</view>
<view class="history-text">{{ item.content}}</view>
<view class="status-info">
<up-image :src="goImg" width="30rpx" height="30rpx" v-if="item.status==2"></up-image>
@@ -66,6 +53,9 @@
</view>
</view>
<empty v-if="applyList.length == 0 && historyList.length == 0" :emptyDesc="'暂无新随访申请患者'" />
</view>
<!-- 底部按钮 -->
<view class="bottom-button">
<button class="add-patient-btn" @click="addPatient">加患者</button>
@@ -75,11 +65,27 @@
<script setup>
import { ref } from 'vue';
import { onLoad,onShow} from '@dcloudio/uni-app';
import { onLoad,onShow,onBackPress} from '@dcloudio/uni-app';
import goImg from "@/static/go_big.png"
import api from '@/api/api.js'
import docUrl from "@/utils/docUrl"
import dayjs from 'dayjs'
import navTo from "@/utils/navTo.js"
import empty from "@/components/empty/empty.vue"
import navBar from "@/components/navBar/navBar.vue"
import tipImg from "@/static/sendgroup_tishi_big.png"
const from=ref('');
onLoad((options) => {
if(options.from){
from.value = options.from;
}
});
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
// 响应式数据
const pendingRequest = ref({
name: '陈新华',
@@ -91,7 +97,11 @@ const historyList = ref([]);
// 方法
const goBack = () => {
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
};
const rejectRequest = () => {
@@ -162,20 +172,27 @@ const getApplyList = async () => { // 申请列表
});
}
};
const applyListOperate = async (uuid,status) => {
const applyListOperate = async (uuid,status,content,patientUuid) => {
let data = {
uuid: uuid,
status: status
}
const res = await api.applyListOperate(data);
if(res.code === 200){
uni.showToast({
title: '操作成功',
icon: 'none',
duration: 1500
});
let nickname = content.split(',')[0].replace('我是', '');
getApplyList();
getRelationRecordLately();
if(status == 2){
navTo({
url: `/pages_app/patientRemark/patientRemark?uuid=${patientUuid}&&nickname=${nickname}`
})
}
}
};
onShow(() => {
@@ -184,6 +201,15 @@ const getApplyList = async () => { // 申请列表
});
const goPatientDetail = (uuid,status) => {
if(status == 2){
navTo({
url: `/pages_app/patientDetail/patientDetail?uuid=${uuid}&from=myPatient`
})
}
}
// 辅助方法
// 格式化日期
const formatDate = (dateString) => {
@@ -196,6 +222,9 @@ const formatDate = (dateString) => {
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
const formatDate2 = (dateString) => {
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
}
// 获取状态文本
const getStatusText = (status) => {
@@ -208,7 +237,7 @@ const getStatusText = (status) => {
const addPatient = () => {
navTo({
url:'/pages_app/myCode/myCode'
url:'/pages_app/myCode/myCode?from=myPatient'
});
// 这里可以跳转到添加患者页面
};
@@ -216,7 +245,7 @@ const addPatient = () => {
<style lang="scss" scoped>
.new-patient-page {
min-height: 100vh;
background-color: #f5f5f5;
}
@@ -311,34 +340,47 @@ const addPatient = () => {
margin-right: 60rpx; // 为了保持标题居中
}
}
.contentbox{
top: calc(var(--status-bar-height) + 44px);
position: fixed;
left: 0;
right: 0;
bottom: 100rpx;
overflow-y: scroll;
z-index: 1;
box-sizing: border-box;
}
.reminder-section {
display: flex;
align-items: flex-start;
align-items:center;
padding: 30rpx;
background-color: #ffffff;
margin-bottom: 20rpx;
.reminder-icon {
width: 35rpx;
height: 42rpx;
margin-right: 20rpx;
margin-top: 4rpx;
}
.reminder-text {
flex: 1;
font-size: 28rpx;
color: #333;
font-size: 32rpx;
color: #999;
line-height: 1.5;
}
}
.follow-up-section {
background-color: #ffffff;
margin-bottom: 20rpx;
.section-title {
padding: 30rpx 30rpx 20rpx;
padding: 30rpx 30rpx 30rpx;
font-size: 32rpx;
background-color: #f0f0f0;
font-weight: normal;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
@@ -349,11 +391,12 @@ const addPatient = () => {
.request-item {
display: flex;
gap: 20rpx;
.avatar {
width: 80rpx;
height: 80rpx;
width: 120rpx;
height: 120rpx;
margin-top: 30rpx;
background-color: #ffb6c1;
border-radius: 12rpx;
display: flex;
@@ -361,8 +404,8 @@ const addPatient = () => {
justify-content: center;
.avatar-icon {
width: 40rpx;
height: 40rpx;
width: 120rpx;
height: 120rpx;
background-color: #ffffff;
border-radius: 50%;
}
@@ -370,7 +413,7 @@ const addPatient = () => {
.request-content {
flex: 1;
margin-left: 20rpx;
.request-text {
font-size: 28rpx;
color: #333;
@@ -432,18 +475,19 @@ const addPatient = () => {
margin-bottom: 120rpx; // 为底部按钮留出空间
.section-title {
padding: 30rpx 30rpx 20rpx;
padding: 30rpx 30rpx 30rpx;
font-size: 32rpx;
font-weight: normal;
background-color: #f0f0f0;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
}
.history-list {
margin-bottom: 100rpx;
margin-bottom: 30rpx;
.history-item {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
@@ -452,8 +496,9 @@ const addPatient = () => {
}
.avatar {
width: 80rpx;
height: 80rpx;
width: 120rpx;
height: 120rpx;
margin-top: 30rpx;
background-color: #ffb6c1;
border-radius: 12rpx;
display: flex;
@@ -470,18 +515,24 @@ const addPatient = () => {
.history-content {
flex: 1;
margin-left: 20rpx;
min-width: 0;
.nickname {
font-size: 28rpx;
font-size: 30rpx;
font-weight: normal;
color: #8B2316;
margin-bottom: 10rpx;
}
.history-text {
font-size: 28rpx;
display: block;
width: 100%;
font-size: 30rpx;
color: #333;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 20rpx;
}
File diff suppressed because it is too large Load Diff
+393 -96
View File
@@ -1,15 +1,24 @@
<template>
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="我的福利"
:title="'我的福利'"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
>
<template v-slot:right>
<view class="nav-right" @click="goRules" >
规则
</view>
</template>
</uni-nav-bar>
</view>
<view class="benefits-page">
@@ -19,7 +28,7 @@
<view class="segmented-control">
<view
class="tab-item"
:class="{ active: activeTab === 0 }"
:class="{ active: activeTab == 0 }"
@click="switchTab(0)"
>
<text class="tab-text">领取福利</text>
@@ -27,7 +36,7 @@
<view class="divider"></view>
<view
class="tab-item"
:class="{ active: activeTab === 1 }"
:class="{ active: activeTab == 1 }"
@click="switchTab(1)"
>
<text class="tab-text">使用福利</text>
@@ -35,61 +44,106 @@
<view class="divider"></view>
<view
class="tab-item"
:class="{ active: activeTab === 2 }"
:class="{ active: activeTab == 2 }"
@click="switchTab(2)"
>
<text class="tab-text">兑福利卡</text>
</view>
</view>
<!-- v-if="(benefit.name != 'upan') || (getFirstNum(benefit.left_result) > 0 && benefit.name == 'upan')" -->
<!-- 福利卡片列表 -->
<view class="scrollbox">
<scroll-view class="benefits-list" scroll-y="true" :show-scrollbar="false">
<scroll-view class="scrollbox" scroll-y="true" :show-scrollbar="false">
<view
class="benefit-card"
v-show="(benefit.name != 'upan') || (getFirstNum(benefit.left_result) > 0 && benefit.name == 'upan')"
v-for="(benefit, index) in benefitsList"
:key="index"
:class="[benefit.type, { 'disabled': !benefit.flag }]"
@click="claimBenefit(benefit)"
:class="fotmatType(benefit)"
>
<view class="card-title">{{ benefit.title }}</view>
<view class="card-title" :class="fotmatType(benefit)">{{ benefit.title }}</view>
<!-- <view class="card-bg">
</view> -->
<view class="card-content">
<view class="card-content" >
<image :src="jifenBgImg" mode="widthFix" class="card-img" v-if="benefit.name == 'jifen'"></image>
<image :src="videoImg" mode="widthFix" class="card-img" v-if="benefit.name == 'video'"></image>
<image :src="kejianImg" mode="widthFix" class="card-img" v-if="benefit.name == 'kejian'"></image>
<image :src="upanImg" mode="widthFix" class="card-img" v-if="benefit.name == 'upan' && (!(benefit.right_tip =='已领取' || benefit.right_result =='已领取') || activeTab == 1)"></image>
<image :src="upanNoimg" mode="widthFix" class="card-img" v-if="benefit.name == 'upan' && ((benefit.right_tip =='已领取' || benefit.right_result =='已领取') && activeTab == 0)"></image>
<image :src="wanfangImg" mode="widthFix" class="card-img" v-if="benefit.name == 'wanfang'"></image>
<image :src="wenxianImg" mode="widthFix" class="card-img" v-if="benefit.name == 'daicha'"></image>
<view class="card-details">
<view class="left-section">
<text class="condition">{{ benefit.left_tip }}</text>
<text class="requirement">{{ benefit.left_result }}</text>
</view>
<view class="right-section">
<view class="right-section" @click="claimBenefit(benefit)">
<text class="reward-type">{{ benefit.right_tip }}</text>
<text class="reward-value">{{ benefit.right_result }}</text>
<text class="reward-value" v-if="activeTab == 0 && !benefit.flag && benefit.name != 'upan'">{{ benefit.right_result }}</text>
<text class="reward-value" v-else-if="activeTab == 0 && benefit.name == 'upan' && !(benefit.right_tip =='已领取' || benefit.right_result =='已领取') && benefit.right_result !='立即领取'">{{ benefit.right_result }}</text>
<!-- <text class="reward-value" v-if="activeTab == 1">{{ benefit.right_result }}个</text> -->
</view>
</view>
<view class="panbox" :class="{'disabled': ((benefit.right_tip =='已领取' || benefit.right_result =='已领取') && activeTab== 0)}" v-if="benefit.name == 'upan'">知识U盘</view>
</view>
</view>
</scroll-view>
</view>
<!-- 底部导航栏 -->
<view class="bottom-nav">
<view class="nav-item" @click="goPointsDetail">
<!-- <up-image :src="jifenImg" width="34rpx" height="34rpx" ></up-image> -->
<text class="nav-text">兑换福利卡</text>
<up-image :src="jifenImg" width="34rpx" height="34rpx" ></up-image>
<text class="nav-text">积分详情</text>
</view>
<view class="nav-item" @click="gofuliDetail">
<up-image :src="fuliImg" width="34rpx" height="34rpx" ></up-image>
<text class="nav-text">福利详情</text>
</view>
<view class="nav-item" @click="goAddPatient">
<up-image :src="addImg" width="34rpx" height="34rpx" ></up-image>
<text class="nav-text">添加患者</text>
</view>
</view>
</view>
<unidialog :visible="showDialog" :content="content" @close="showDialog=false" :showCancel="false" @confirm="confirmDialog"></unidialog>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import jifenImg from "@/static/duihuan.png"
import {onShow,onBackPress,onLoad,onHide} from '@dcloudio/uni-app';
import docUrl from '@/utils/docUrl.js';
const from = ref('');
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
//import jifenImg from "@/static/duihuan.png"
import api from '@/api/api';
import unidialog from '@/components/dialog/dialog.vue'
import navTo from "@/utils/navTo.js";
import navBar from '@/components/navBar/navBar.vue'
import fuliImg from "@/static/fulidetail.png"
import jifenImg from "@/static/jifendetail.png"
import addImg from "@/static/tianjiapatient.png"
import jifenBgImg from "@/static/fljifen_big.png"
import videoImg from "@/static/flshipin_big.png"
import kejianImg from "@/static/flkejian_big.png"
import upanImg from "@/static/flupan_big2.png"
import wanfangImg from "@/static/flwanfang_big.png"
import wenxianImg from "@/static/flwen_big.png"
import upanNoimg from "@/static/flupannull_big.png"
const showDialog = ref(false);
const content = ref('肝胆相照平台协助查找文献次数已经用完,谢谢您的支持!');
const confirmDialog = () => {
showDialog.value = false;
}
// 当前选中的标签页
const activeTab = ref(0);
@@ -139,13 +193,11 @@
// 方法
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({
url: '/pages/index/index'
});
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
});
};
const showRules = () => {
@@ -154,63 +206,213 @@
icon: 'none'
});
};
const fotmatType = (benefit) => {
let name = benefit.name;
let name1 = benefit.right_tip;
let name2 = benefit.right_result;
if(name == 'video'){
return 'video';
}else if(name == 'kejian'){
return 'kejian';
}else if(name == 'upan'){
if((name1 == '已领取' && name2 == '已领取') && activeTab.value == 0){
return 'upan_disabled';
}else{
return 'upan';
}
}else if(name == 'wanfang'){
return 'wanfang';
}else if(name=='jifen'){
return 'jifen';
}else if(name=='daicha'){
return 'wenxian';
}
}
const switchTab = (index) => {
activeTab.value = index;
if (index === 0) {
if (index == 0) {
activeTab.value = index;
getWelfarePage();
} else if (index === 1) {
} else if (index == 1) {
activeTab.value = index;
useWelfarePage();
} else if (index === 2) {
} else if (index == 2) {
uni.setStorageSync('lookWelfare', '');
navTo({
url: '/pages_app/myWelfareCard/myWelfareCard'
url: '/pages_app/myWelfareCard/myWelfareCard?from=myWelfare'
});
}
};
onLoad((options) => {
const claimBenefit = (benefit) => {
if (!benefit.flag) {
uni.showToast({
title: `需${benefit.num}个新随访`,
icon: 'none'
if(options.from){
from.value = options.from;
}
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "我的福利",
},
});
return;
getWelfarePage();
});
onHide(() => {
uni.setStorageSync('lookWelfare', '');
});
onShow(() => {
let lookWelfare = uni.getStorageSync('lookWelfare');
if(lookWelfare){
activeTab.value =1;
switchTab(activeTab.value);
}
});
const claimBenefit = (benefit) => {
console.log(benefit);
console.log(activeTab.value);
if(activeTab.value == 1){
if(benefit.name == 'jifen'){
navTo({
url: '/pages_goods/pointMall/pointMall'
});
}else if(benefit.name == 'video'){
navTo({
url: '/pages_app/video/video?from=myWelfare'
});
}else if(benefit.name == 'kejian'){
navTo({
url: '/pages_app/ppt/ppt'
});
}else if(benefit.name == 'upan'){
let num = getFirstNum(benefit.left_result) || benefit.num;
navTo({
url: '/pages_app/buyUpan/buyUpan?restNum='+num
});
}else if(benefit.name == 'wanfang'){
navTo({
url: '/pages_app/zhinan/zhinan'
});
}else if(benefit.name == 'daicha'){
// if(benefit.num == 0){
// showDialog.value = true;
// }else{
plus.share.getServices(res => {
const wechat = res.find(i => i.id === 'weixin')
if (wechat) {
wechat.openCustomerServiceChat({
corpid: 'wwc72af5440141e84e',//第三步获取
url: 'https://work.weixin.qq.com/kfid/kfceb116f7032dbbb91',
}, src => {
console.log("success:")
}, err => {
console.log("error:")
})
} else {
uni.showToast({
title: '当前环境不支持',
icon: "error"
});
}
});
//}
}
}else if(activeTab.value == 0){
if(benefit.flag){
receiveWelfare(benefit.name);
}
uni.showToast({
title: `领取${benefit.title}`,
icon: 'none'
});
}
};
const goPointsDetail = () => {
uni.showToast({
title: '积分详情',
icon: 'none'
});
navTo({
url: '/pages_app/myPoint/myPoint'
})
};
const goBenefitDetail = () => {
uni.showToast({
title: '福利详情',
icon: 'none'
});
const gofuliDetail = () => {
navTo({
url: '/pages_app/myWelfare/welfareDetail'
})
};
const addPatient = () => {
uni.showToast({
title: '添加患者',
icon: 'none'
});
const goAddPatient = () => {
// uni.sendNativeEvent('goExpertQrcodePage', {
// msg: "expertQrcodePage"
// },ret => {
// console.log(ret);
// })
navTo({
url: '/pages_app/myCode/myCode?from=myWelfare'
})
};
const goRules = () => {
navTo({
url: '/pages_app/webview/webview?url='+encodeURIComponent('https://doc.igandan.com/app/integral/integral_welfare.html')+'&title=福利规则'
})
};
const receiveWelfare = (name) => {
let type = '';
if(name == 'jifen'){
type = 0;
}else if(name == 'video'){
type = 1;
}else if(name == 'kejian'){
type = 2;
}else if(name == 'upan'){
type = 5;
}else if(name == 'wanfang'){
return false;
}else if(name == 'daicha'){
return false;
}
api.receiveWelfare({
type: type
}).then(res => {
if(res.code == 200){
uni.showToast({
title:'领取成功',
icon: 'success'
});
getWelfarePage();
}
})
}
const getFirstNum = (str) => {
if(!str) return '';
let num = str.substring(0,1);
return Number(num);
}
const useWelfarePage = () => {
api.useWelfarePage().then(res => {
if (res.code === '200' && res.data) {
// 更新福利列表数据
benefitsList.value = res.data;
res.data.forEach(item => {
item.name = item.name.replace(' ', '');
});
// 定义排序顺序:肝胆积分、肝胆视频、肝胆课件、查找文献、知识U盘
const sortOrder = ['jifen', 'video', 'kejian', 'daicha', 'upan'];
// 对列表进行排序
benefitsList.value = res.data.sort((a, b) => {
const indexA = sortOrder.indexOf(a.name);
const indexB = sortOrder.indexOf(b.name);
// 如果找不到对应的排序索引,放在最后
if (indexA === -1 && indexB === -1) return 0;
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
});
}
});
};
@@ -233,9 +435,7 @@
});
};
onMounted(() => {
getWelfarePage();
});
</script>
<style lang="scss" scoped>
@@ -254,9 +454,9 @@
.benefits-page {
min-height: 100vh;
height: 100vh;
background-color: $bg-color;
padding-top: $nav-height; // 为固定导航栏预留空间
}
.status-bar {
@@ -332,7 +532,7 @@
.segmented-control {
position: fixed;
top: $nav-height;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
z-index: 10;
@@ -341,7 +541,7 @@
display: flex;
align-items: center;
padding: 0 30rpx;
border-bottom: 1rpx solid $border-color;
border-bottom: 1rpx solid #eee;
.tab-item {
flex: 1;
@@ -369,32 +569,56 @@
.divider {
width: 2rpx;
height: 40rpx;
background-color: $divider-color;
background-color:#eee;
}
}
.scrollbox{
position: fixed;
top: calc(#{$nav-height} + #{$segmented-height});
left: 30rpx;
right: 30rpx;
width:100%;
top: calc(var(--status-bar-height) + 44px + 82rpx);
height:calc(100vh - var(--status-bar-height) - 44px - 82rpx - 100rpx);
box-sizing: border-box;
bottom: $bottom-nav-height;
margin: 30rpx 0 ;
overflow-y: scroll;
// bottom: 100rpx;
padding-bottom: 50rpx;
margin: 30rpx 0 0;
}
.benefits-list {
.benefit-card {
background: $white;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
margin:0 30rpx 30rpx;
position: relative;
overflow: hidden;
border:2rpx solid #fff;
&.jifen{
border:2rpx solid #ef828f;
background:#feeae9;
}
&.wenxian{
border:2rpx solid #33c966;
background:#efffe9;
}
&.video{
border:2rpx solid #ff951d;
background:#fefbea;
}
&.kejian{
border:2rpx solid #f17dff;
background:#fdeafe;
}
&.upan{
border:2rpx solid #40cdc5;
background:#eafffa;
}
&.wanfang{
border:2rpx solid #4aa2fe;
background:#fff;
}
.disabled-text {
color: $text-light !important;
}
@@ -406,44 +630,111 @@
left:20rpx;
z-index:0;
}
.card-content {
margin-top: 30rpx;
background:url("@/static/fljifen_big.png") 0 0 no-repeat;
background-size: 100% 100%;
position: relative;
height: 220rpx;
z-index: 1;
.card-title {
font-size: 32rpx;
color: $white;
font-weight: 600;
margin-bottom: 30rpx;
line-height: 1.4;
&.jifen{
color: #ef828f;
}
&.video{
color: #ff951d;
}
&.kejian{
color: #f17dff;
}
&.upan{
color: #40cdc5;
}
&.upan_disabled{
color: #cccccc;
}
&.wanfang{
color: #4aa2fe;
}
&.wenxian{
color: #33c966;
}
}
.card-content {
margin-top: 30rpx;
position: relative;
min-height: 220rpx;
z-index: 1;
.card-img{
position: absolute;
top: 0;
left: 0;
z-index: 0;
width: 100%;
height: 100%;
}
.panbox{
margin-top: 20rpx;
font-size: 32rpx;
color: #fff;
font-weight: 600;
height: 84rpx;
line-height: 84rpx;
background: #40cdc5;
border-radius: 10rpx;
padding: 0 20rpx;
text-align: center;
&.disabled{
background: #cccccc;
}
}
.card-details {
position: relative;
z-index: 2;
display: flex;
justify-content: space-between;
align-items: center;
padding:0 40rpx;
height: 220rpx;
.left-section,
.right-section {
.left-section{
display: flex;
flex-direction: column;
gap: 8rpx;
justify-content: center;
align-items: flex-start;
.condition,
.reward-type {
font-size: 24rpx;
font-size: 32rpx;
color: rgba(255, 255, 255, 11);
}
.requirement,
.reward-value {
font-size: 36rpx;
margin-top: 8rpx;
font-size: 42rpx;
color: $white;
font-weight: bold;
}
.requirement{
margin-top: 40rpx;
}
}
.right-section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center!important;
.condition,
.reward-type {
font-size: 32rpx;
color: rgba(255, 255, 255, 11);
}
.requirement,
.reward-value {
margin-top: 8rpx;
font-size: 42rpx;
color: $white;
font-weight: bold;
}
@@ -474,7 +765,7 @@
background-color: #FF9800;
}
}
}
.bottom-nav {
position: fixed;
@@ -491,10 +782,11 @@
padding: 0 20rpx;
.nav-item {
display: flex;
align-items: center;
justify-content: center;
flex-direction: flex;
gap: 8rpx;
.nav-icon {
font-size: 40rpx;
@@ -502,8 +794,13 @@
.nav-text {
font-size: 32rpx;
margin-left: 8rpx;
color: #fff;
}
}
}
.nav-right{
font-size: 28rpx;
color: #8B2316;
}
</style>
+392
View File
@@ -0,0 +1,392 @@
<template>
<navBar :title="'福利明细'" />
<view class="welfare-detail-page">
<!-- Tab 切换栏 -->
<view class="segmented-control">
<view
class="tab-item"
:class="{ active: activeTab === 1 }"
@click="switchTab(1)"
>
<text class="tab-text">视频下载</text>
</view>
<view class="divider"></view>
<view
class="tab-item"
:class="{ active: activeTab === 2 }"
@click="switchTab(2)"
>
<text class="tab-text">课件下载</text>
</view>
<view class="divider"></view>
<view
class="tab-item"
:class="{ active: activeTab === 6 }"
@click="switchTab(6)"
>
<text class="tab-text">查找文献</text>
</view>
</view>
<!-- 表格头部 -->
<view class="table-header">
<text class="col reason">原因</text>
<text class="col time">时间</text>
<text class="col count">次数</text>
</view>
<!-- 列表数据区域 -->
<scroll-view
class="table-body"
scroll-y
:show-scrollbar="false"
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
:lower-threshold="100"
>
<view
class="row"
v-for="(item, index) in filteredList"
:key="index"
>
<text class="cell reason">{{ item.reason }}</text>
<text class="cell time">{{ item.time }}</text>
<text class="cell count" >{{ item.count }}</text>
</view>
<!-- 空数据状态 -->
<view v-if="!loading && filteredList.length === 0" class="empty-wrap">
<empty />
</view>
<!-- 加载更多提示 -->
<view v-if="loading" class="loading">
<text>加载中...</text>
</view>
<!-- 没有更多数据提示 -->
<!-- <view v-if="isLastPage && filteredList.length > 0" class="no-more">
<text>没有更多数据了</text>
</view> -->
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import navBar from '@/components/navBar/navBar.vue';
import api from '@/api/api';
import dayjs from 'dayjs';
import empty from '@/components/empty/empty.vue';
// 当前选中的 Tab(0: 视频下载, 1: 课件下载, 2: 查找文献)
const activeTab = ref(1);
const page = ref(1);
const pageSize = ref(10);
// 福利明细列表数据
const welfareList = ref([]);
// 加载状态
const loading = ref(false);
const refreshing = ref(false);
const isLastPage = ref(false);
// 根据 activeTab 过滤后的列表数据
const filteredList = computed(() => {
if (!welfareList.value || welfareList.value.length === 0) {
return [];
}
const typeMap = {
0: '视频下载',
1: '课件下载',
2: '查找文献'
};
const currentType = typeMap[activeTab.value];
return welfareList.value;
});
// Tab 切换
const switchTab = (index) => {
if (activeTab.value === index) return;
activeTab.value = index;
page.value = 1;
pageSize.value = 10;
welfareList.value = [];
isLastPage.value = false;
loading.value = false;
refreshing.value = false;
// 切换 Tab 时重新加载数据
loadWelfareDetail();
};
// 加载福利明细数据
const loadWelfareDetail = () => {
if (loading.value) return;
loading.value = true;
console.log(api);
// 调用 API 获取数据
api.welfareList({
type: activeTab.value,
page: page.value,
pageSize: pageSize.value
}).then(res => {
console.log(res);
if (res.code ==1) {
// 兼容两种数据结构:直接数组或包含 list 的对象
let rawList = [];
if (Array.isArray(res.data)) {
rawList = res.data;
isLastPage.value = false;
} else if (res.data.list && Array.isArray(res.data.list)) {
rawList = res.data.list;
isLastPage.value = !!res.data.isLastPage;
} else {
if (page.value === 1) {
welfareList.value = [];
}
isLastPage.value = false;
return;
}
// 将接口返回的数据映射到页面需要的格式
const formattedList = rawList.map(item => {
let timeStr = item.create_date || item.time || '';
// 使用 dayjs 格式化日期,只显示年月日
if (timeStr) {
timeStr = dayjs(timeStr).format('YYYY-MM-DD');
}
return {
reason: item.name || item.reason || '',
time: timeStr,
count: item.count_num ? parseInt(item.count_num) : (item.count || 0)
};
});
// 如果是第一页或刷新,替换数据;否则追加数据
if (page.value === 1 || refreshing.value) {
welfareList.value = formattedList;
} else {
welfareList.value = [...welfareList.value, ...formattedList];
}
} else {
if (page.value === 1) {
welfareList.value = [];
}
isLastPage.value = false;
}
}).catch(err => {
console.error('获取福利明细失败:', err);
if (page.value === 1) {
welfareList.value = [];
}
uni.showToast({
title: '获取福利明细失败',
icon: 'none'
});
}).finally(() => {
loading.value = false;
refreshing.value = false;
});
};
// 下拉刷新
const onRefresh = () => {
refreshing.value = true;
page.value = 1;
isLastPage.value = false;
loadWelfareDetail();
};
// 上拉加载更多
const onLoadMore = () => {
if (isLastPage.value || loading.value) return;
page.value += 1;
loadWelfareDetail();
};
onMounted(() => {
loadWelfareDetail();
});
</script>
<style scoped lang="scss">
// 变量定义
$bg-color: #f5f6f7;
$text-primary: #333333;
$text-secondary: #666666;
$text-light: #999999;
$border-color: #e5e5e5;
$white: #ffffff;
$theme-color: #8B2316;
$table-header-bg: #8B2316;
$divider-color: #cccccc;
$nav-height: 180rpx;
$segmented-height: 80rpx;
$table-header-height: 70rpx;
.welfare-detail-page {
min-height: 100vh;
background-color: $bg-color;
padding-top: $nav-height;
}
// Tab 切换栏
.segmented-control {
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
z-index: 10;
height: $segmented-height;
background-color: $white;
display: flex;
align-items: center;
padding: 0 30rpx;
border-bottom: 1rpx solid $border-color;
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
position: relative;
.tab-text {
font-size: 28rpx;
color: $text-secondary;
transition: all 0.3s ease;
}
&.active {
.tab-text {
color: $theme-color;
font-size: 30rpx;
font-weight: 500;
}
}
}
.divider {
width: 2rpx;
height: 40rpx;
background-color: $divider-color;
}
}
// 表格头部
.table-header {
position: fixed;
top: calc(var(--status-bar-height) + 44px + $segmented-height);
left: 0;
right: 0;
z-index: 5;
background: $table-header-bg;
color: $white;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
font-size: 30rpx;
font-weight: 500;
height: $table-header-height;
box-sizing: border-box;
.col {
flex: 1;
text-align: center;
&.reason {
text-align: left;
}
&.time {
text-align: center;
}
&.count {
text-align: right;
}
}
}
// 列表数据区域
.table-body {
position: fixed;
left: 0;
right: 0;
top: calc(var(--status-bar-height) + 44px + $segmented-height + $table-header-height);
height: calc(100vh - var(--status-bar-height) - 44px - $segmented-height - $table-header-height);
background-color: #fff;
}
// 列表行
.row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 2rpx solid #eee;
// background-color: #f5f5f5;
font-size: 28rpx;
color: $text-primary;
.cell {
flex: 1;
text-align: center;
&.reason {
text-align: left;
color: $text-primary;
}
&.time {
text-align: center;
color: $text-secondary;
}
&.count {
text-align: right;
color: $text-primary;
&.negative {
color: #e34d4d;
}
}
}
}
// 空数据状态
.empty-wrap {
padding-top: 200rpx;
display: flex;
flex-direction: column;
align-items: center;
color: #bdbdbd;
.empty-text {
margin-top: 20rpx;
font-size: 30rpx;
}
}
// 加载状态
.loading, .no-more {
text-align: center;
color: $text-light;
padding: 30rpx 0;
font-size: 26rpx;
text {
display: inline-block;
padding: 10rpx 20rpx;
border-radius: 20rpx;
}
}
</style>
+78 -36
View File
@@ -1,5 +1,5 @@
<template>
<uni-nav-bar
<!-- <uni-nav-bar
left-icon="left"
title="兑换福利卡"
@clickLeft="goBack"
@@ -8,16 +8,18 @@
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
></uni-nav-bar> -->
<navBar :title="'兑换福利卡'" />
<view class="exchange-page">
<!-- 顶部红色横幅 -->
<view class="top-banner">
<view class="top-banner" @click="goWelfare">
<image :src="bgImg" mode="widthFix" class="bg-img"></image>
<view class="banner-text">
<view class="line1">已兑换{{ exchangedCount }}张</view>
<view class="line2" @click="goMyWelfare">查看现有权益</view>
<view class="line2" @click.stop="goWelfare">查看现有权益</view>
</view>
<view class="help-btn" @click="showHelp">帮助说明</view>
<view class="help-btn" @click.stop="showHelp">帮助说明</view>
</view>
<!-- 使用统一的自定义居中模态框 -->
@@ -38,19 +40,20 @@
</view>
<!-- 输入提示 -->
<view class="tips">请输入16位福利卡密码(不区分大小写)<text class="paste-action" @click="pasteFromClipboard">粘贴</text></view>
<view class="tips">请输入16位福利卡密码(不区分大小写)<text class="paste-action" @click="fillByText" v-if="hasPaste">粘贴</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)"/>
<input :adjust-position="false" class="code-box" type="text" v-model="code1" maxlength="4" placeholder="" :focus="f1" @input="handleInput(1, $event)" @paste="handlePaste"/>
<input :adjust-position="false" class="code-box" type="text" v-model="code2" maxlength="4" placeholder="" :focus="f2" @input="handleInput(2, $event)" />
<input :adjust-position="false" class="code-box" type="text" v-model="code3" maxlength="4" placeholder="" :focus="f3" @input="handleInput(3, $event)"/>
<input :adjust-position="false" 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>
<button class="submit-btn" @click="submit">立即兑换</button>
</view>
</view>
</template>
@@ -58,7 +61,11 @@
<script setup>
import { ref, computed, nextTick } from 'vue'
import api from '@/api/api';
const exchangedCount = ref(5)
import navBar from '@/components/navBar/navBar.vue'
import bgImg from '@/static/fulicard_bg.png'
const exchangedCount = ref(0)
const hasPaste = ref(false)
const pasteText = ref('')
const code1 = ref('')
const code2 = ref('')
const code3 = ref('')
@@ -66,18 +73,23 @@
const f1 = ref(true)
const f2 = ref(false)
const f3 = ref(false)
const f4 = ref(false)
const f4 = ref(false);
import { onLoad } from '@dcloudio/uni-app'
const helpVisible = ref(false)
const centerVisible = ref(false)
const centerText = ref('')
const centerHelp = ref(false)
const centerHelp = ref(false);
const isabled = ref(true);
const isFull = computed(() => (code1.value+code2.value+code3.value+code4.value).length === 16)
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({ url: '/pages/index/index' })
}
onLoad((opts) => {
exchangedCount.value = opts.num
pasteFromClipboard()
})
const goWelfare = () => {
uni.setStorageSync('lookWelfare', 'useWelfare');
uni.redirectTo({
url:
'/pages_app/myWelfare/myWelfare?from=useWelfare'
})
}
const showHelp = () => {
@@ -111,9 +123,19 @@
if (sanitized.length === 4 && idx < 4) {
nextTick(() => setFocus(idx + 1))
}
if(code1.value.length+code2.value.length+code3.value.length+code4.value.length==16){
isFull.value = true
console.log(1111)
}else{
isFull.value = false
console.log(2222)
}
}
const submit = () => {
if (!isFull.value) return
if(code1.value.length+code2.value.length+code3.value.length+code4.value.length!=16){
uni.showToast({ title: '请输入16位福利卡密码', icon: 'none' })
return
}
const code = (code1.value+code2.value+code3.value+code4.value).toUpperCase()
uni.showToast({ title: '兑换中: '+ code, icon: 'none' })
api.exchangeWelfareCard({password: code}).then(res => {
@@ -133,6 +155,7 @@
const handlePaste = (e) => {
const text = (e.clipboardData && e.clipboardData.getData('text')) || ''
fillByText(text)
// 阻止默认粘贴到单个输入框
e && e.preventDefault && e.preventDefault()
}
@@ -141,18 +164,30 @@
const pasteFromClipboard = () => {
uni.getClipboardData({
success: (res) => {
fillByText(res.data || '')
if(res.data.length>0){
hasPaste.value = true
pasteText.value = res.data
}
}
})
}
const fillByText = (raw) => {
const v = String(raw || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 16)
const v = pasteText.value.replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 16);
console.log(v)
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))
if(code1.value.length+code2.value.length+code3.value.length+code4.value.length==16){
isFull.value = true
console.log(1111)
}else{
isFull.value = false
console.log(2222)
}
}
</script>
@@ -164,22 +199,28 @@
}
.top-banner{
position: relative;
height: 280rpx;
background: linear-gradient(180deg,#ff6a4a 0%, #e93b2d 100%);
border-bottom-left-radius: 40rpx;
border-bottom-right-radius: 40rpx;
height: 286rpx;
margin-top: calc(var(--status-bar-height) + 44px);
.bg-img{
position: absolute;
width: 100%;
z-index:0;
height: 286rpx;
}
.banner-text{
position: absolute;
left: 48rpx;
top: 120rpx;
left: 50%;
transform: translateX(-50%);
top: 80rpx;
z-index:1;
color: #fff;
.line1{font-size: 44rpx;font-weight: 600;}
.line1{font-size: 44rpx;}
.line2{font-size: 36rpx;margin-top: 12rpx;}
}
.help-btn{
position: absolute;
right: 40rpx;
top: 90rpx;
right: 30rpx;
top: 20rpx;
background: rgba(255,255,255,.95);
color: #e04835;
border-radius: 999rpx;
@@ -189,20 +230,21 @@
}
.tips{
margin: 48rpx;
color: #333;
font-size: 32rpx;
color: #000;
font-size: 30rpx;
.paste-action{
color: #007aff;
color: #8B2316;
text-decoration: underline;
}
}
.code-inputs{
display: flex;
gap: 30rpx;
justify-content: space-between;
padding: 0 48rpx;
.code-box{
flex: 1;
height: 96rpx;
max-width: 142rpx;
background: #fff;
border-radius: 16rpx;
text-align: center;
+247 -63
View File
@@ -1,23 +1,28 @@
<template>
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="我的福利卡"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
>
</uni-nav-bar>
</view>
<!-- <navBar :title="'我的福利卡'" /> -->
<view class="benefits-page">
<!-- 顶部红色横幅 -->
<view class="top-banner">
<view class="top-banner" @click="goWelfare" v-if="hasCard">
<image :src="bgImg" mode="widthFix" class="bg-img"></image>
<view class="banner-text">
<view class="line1">已兑换{{ cardInfo.length }}张</view>
<view class="line2" @click="goMyWelfare">查看现有权益</view>
<view class="line1">已兑换{{ totalCount}}张</view>
<view class="line2" @click.stop="goWelfare">查看现有权益</view>
</view>
<view class="help-btn" @click="showRules">帮助说明</view>
<view class="help-btn" @click.stop="showRules">帮助说明</view>
</view>
<!-- 帮助说明模态框(与兑换页一致的居中弹层) -->
@@ -37,35 +42,76 @@
</view>
<!-- 有卡展示(多张) -->
<view v-if="hasCard" >
<scroll-view
v-if="hasCard"
class="scroll-container"
scroll-y
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
:lower-threshold="50"
>
<view class="card-wrapper" v-for="(card, cIdx) in cardInfo" :key="card.id">
<image :src="cardImg" mode="widthFix" class="card-img"></image>
<view class="cardbox">
<view class="card-header">
<text>卡号:{{ card.idcard }}</text>
<text class="time">兑换时间:{{ card.exchange_date }}</text>
<image :src="logoImg" mode="widthFix" class="logo-img"></image>
<view class="headerbox">
<view>卡号:{{ card.idcard }}</view>
<view class="time">兑换时间:{{ card.exchange_date }}</view>
</view>
</view>
<view class="card-body">
<image v-if="card.welfare_list && card.welfare_list.length > 3 && (swiperCurrent[cIdx] || 0) > 0"
:src="rightImg" mode="widthFix" class="left-img" @click="swipeLeft(cIdx)"></image>
<view v-else class="left-img-placeholder"></view>
<!-- 超过3条使用swiper,否则直接显示 -->
<swiper v-if="card.welfare_list && card.welfare_list.length > 3"
:class="'welfare-swiper-' + cIdx"
class="welfare-swiper"
:indicator-dots="false"
:autoplay="false"
:circular="false"
:display-multiple-items="1"
:current="swiperCurrent[cIdx] || 0"
@change="onSwiperChange($event, cIdx)">
<swiper-item v-for="(page, pageIdx) in getWelfarePages(card.welfare_list)" :key="pageIdx">
<view class="linebox">
<view class="benefit-line" v-for="(w, idx) in page" :key="idx">
<text class="index">{{ pageIdx * 3 + idx + 1 }}、</text>
<text class="text">{{ w.type_name }}{{ w.num }}{{ w.type_unit }}</text>
</view>
</view>
</swiper-item>
</swiper>
<view v-else class="linebox">
<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>
<image v-if="card.welfare_list && card.welfare_list.length > 3 && (swiperCurrent[cIdx] || 0) < getWelfarePages(card.welfare_list).length - 1"
:src="rightImg" mode="widthFix" class="right-img" @click="swipeRight(cIdx)"></image>
<view v-else class="right-img-placeholder"></view>
</view>
</view>
</view>
<!-- 加载提示 -->
<view class="loadmore-tip" v-if="isLoading || isLastPage">
<text>{{ isLoading ? '加载中...' : (isLastPage ? '没有更多了' : '') }}</text>
</view>
</view>
</scroll-view>
<!-- 无卡空状态 -->
<view v-else class="emptybox">
<up-image :src="emptyImg" width="176rpx" height="204rpx"></up-image>
<view class="empty_desc">暂无福利卡</view>
<empty />
</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>
@@ -76,8 +122,37 @@
import { ref, onMounted } from 'vue';
import jifenImg from "@/static/duihuan.png"
import emptyImg from "@/static/icon_empty.png"
import bgImg from '@/static/fulicard_bg.png'
import cardImg from '@/static/carditem_bg.png'
import api from '@/api/api';
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
import logoImg from '@/static/logo_gdxz.png'
import rightImg from '@/static/right_selsect_big.png'
import empty from '@/components/empty/empty.vue'
import { onLoad,onBackPress } from '@dcloudio/uni-app'
const totalCount = ref(0);
const from=ref('');
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
onLoad((options) => {
from.value = options.from;
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "福利卡兑换",
},
});
})
const goWelfare = () => {
uni.setStorageSync('lookWelfare', 'useWelfare');
console.log('6789');
uni.redirectTo({
url: '/pages_app/myWelfare/myWelfare'
});
}
// 是否有福利卡以及示例卡信息
const hasCard = ref(true)
const cardInfo = ref([]);
@@ -86,6 +161,9 @@
const pageSize = ref(10)
const isLastPage = ref(false)
const isLoading = ref(false)
const refreshing = ref(false)
// 存储每个卡片的swiper当前索引
const swiperCurrent = ref({})
const getMyWelfareCard = (opts = { isRefresh: false }) => {
if (isLoading.value) return
@@ -99,44 +177,46 @@
} else {
cardInfo.value = pageNum.value === 1 ? list : cardInfo.value.concat(list)
}
totalCount.value = res.data.total;
hasCard.value = list.length > 0 || cardInfo.value.length > 0
isLastPage.value = !!res.data.isLastPage
}
})
.finally(() => {
isLoading.value = false
uni.stopPullDownRefresh()
refreshing.value = false
})
}
const goBack = () => {
console.log('我的福利卡goBack');
console.log(from.value)
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
}
onMounted(() => {
getMyWelfareCard()
})
// 下拉刷新
onPullDownRefresh(() => {
// scroll-view 下拉刷新
const onRefresh = () => {
refreshing.value = true
pageNum.value = 1
isLastPage.value = false
getMyWelfareCard({ isRefresh: true })
})
}
// 上拉加载更多
onReachBottom(() => {
// scroll-view 上拉加载更多
const onLoadMore = () => {
if (isLastPage.value || isLoading.value) return
pageNum.value += 1
getMyWelfareCard()
})
}
// 方法
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({
url: '/pages/index/index'
});
}
});
};
const centerVisible = ref(false)
const showRules = () => {
@@ -147,12 +227,48 @@
};
const goPointsDetail = () => {
uni.navigateTo({ url: '/pages_app/myWelfareCard/exchange' })
console.log('goPointsDetail')
uni.navigateTo({ url: '/pages_app/myWelfareCard/exchange?num='+totalCount.value })
};
const goMyWelfare = () => {
uni.navigateTo({ url: '/pages_app/myWelfare/myWelfare' })
}
// 将福利列表分组,每组最多3条
const getWelfarePages = (welfareList) => {
if (!welfareList || welfareList.length === 0) return []
const pages = []
for (let i = 0; i < welfareList.length; i += 3) {
pages.push(welfareList.slice(i, i + 3))
}
return pages
}
// swiper切换事件
const onSwiperChange = (e, cardIndex) => {
swiperCurrent.value[cardIndex] = e.detail.current
}
// 向左滑动
const swipeLeft = (cardIndex) => {
const current = swiperCurrent.value[cardIndex] || 0
if (current > 0) {
swiperCurrent.value[cardIndex] = current - 1
}
}
// 向右滑动
const swipeRight = (cardIndex) => {
const current = swiperCurrent.value[cardIndex] || 0
const card = cardInfo.value[cardIndex]
if (card && card.welfare_list) {
const pages = getWelfarePages(card.welfare_list)
if (current < pages.length - 1) {
swiperCurrent.value[cardIndex] = current + 1
}
}
}
</script>
<style lang="scss" scoped>
@@ -187,8 +303,16 @@
}
.benefits-page {
min-height: 100vh;
background-color: $bg-color;
height: 100vh;
.scroll-container {
position: fixed;
top: calc(var(--status-bar-height) + 44px + 286rpx);
left: 0;
right: 0;
bottom: 100rpx;
height: calc(100vh - var(--status-bar-height) - 44px - 286rpx - 100rpx);
box-sizing: border-box;
}
.emptybox{
display: flex;
height: 100vh;
@@ -310,16 +434,7 @@
background-color: $divider-color;
}
}
.scrollbox{
position: fixed;
top: calc(#{$nav-height} + #{$segmented-height});
left: 30rpx;
right: 30rpx;
box-sizing: border-box;
bottom: $bottom-nav-height;
margin: 30rpx 0 ;
overflow-y: auto;
}
.benefits-list {
@@ -397,24 +512,30 @@
/* 顶部横幅与卡片样式 */
.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;
position: fixed;
height: 286rpx;
width:100%;
top: calc(var(--status-bar-height) + 44px);
.bg-img{
position: absolute;
width: 100%;
z-index:0;
height: 286rpx;
}
.banner-text{
position: absolute;
left: 48rpx;
top: 120rpx;
left: 50%;
transform: translateX(-50%);
top: 80rpx;
z-index:1;
color: #fff;
.line1{font-size: 44rpx;font-weight: 600;}
.line1{font-size: 44rpx;}
.line2{font-size: 36rpx;margin-top: 12rpx;}
}
.help-btn{
position: absolute;
right: 40rpx;
top: 90rpx;
right: 30rpx;
top: 20rpx;
background: rgba(255,255,255,.95);
color: #e04835;
border-radius: 999rpx;
@@ -424,27 +545,90 @@
}
.card-wrapper{
position: relative;
margin: 24rpx 30rpx;
background: #fff;
border-radius: 16rpx;
@include shadow;
overflow: hidden;
.cardbox{
position: relative;
z-index:2;
}
.card-img{
position: absolute;
width: 100%;
z-index:0;
height: 336rpx;
}
.card-header{
background: linear-gradient(90deg,#ff7e4a,#ff4d2e);
color: #fff;
padding: 24rpx 28rpx;
padding: 4rpx 28rpx;
font-size: 26rpx;
display: flex;
justify-content: space-between;
align-items: center;
.headerbox{
display: flex;
flex-direction: column;
justify-content: center;
}
.time{opacity:.95}
.logo-img{
width:94rpx;
height: 94rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 20rpx;
}
}
.card-body{
padding: 34rpx 28rpx 40rpx;
height: 240rpx;
display: flex;
justify-content: center;
align-items: center;
padding:0rpx 28rpx;
.left-img{
width:28rpx;
height: 50rpx;
transform: rotate(180deg);
cursor: pointer;
}
.right-img{
width:28rpx;
height: 50rpx;
cursor: pointer;
}
.left-img-placeholder,
.right-img-placeholder{
width:28rpx;
height: 50rpx;
}
.welfare-swiper{
flex: 1;
height: 100%;
}
.linebox{
min-width: 500rpx;
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
padding:0 20rpx;
height: 100%;
}
.benefit-line{
font-size: 30rpx;
color: #333;
width:355rpx;
color: #8B2316;
line-height: 56rpx;
.index{color:#333}
text-align: left;
.index{color:#8B2316;}
.text{
text-align: left;
}
}
}
}
+33 -17
View File
@@ -1,6 +1,18 @@
<template>
<navBar title="肝胆新闻" />
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="肝胆新闻"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eeeeee"
>
</uni-nav-bar>
</view>
<view class="news-page">
<!-- Fixed Banner Swiper -->
<view class="banner-container">
@@ -45,7 +57,7 @@
<!-- 肝胆新闻 -->
<view class="news-item" @click="goToNews('news')">
<view class="news-icon blue">
<uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
<image :src="newImg" width="94rpx" height="94rpx" ></image>
</view>
<view class="news-content">
<view class="news-title">肝胆新闻</view>
@@ -101,7 +113,7 @@
<!-- 肝胆会议 -->
<view class="news-item" @click="goToCourse('hepatoBiliaryConference')">
<view class="news-icon yellow">
<uni-icons type="staff" size="24" color="#fff"></uni-icons>
<image :src="meetingImg" width="94rpx" height="94rpx" ></image>
</view>
<view class="news-content">
<view class="news-title">肝胆会议</view>
@@ -136,6 +148,9 @@
import docUrl from "@/utils/docUrl.js";
import navTo from '../../utils/navTo';
import navBar from '@/components/navBar/navBar.vue';
import newImg from '@/static/gandanxinwen.png';
import meetingImg from '@/static/gandanhuiyi.png';
import {onBackPress} from '@dcloudio/uni-app';
onMounted(() => {
loadNewsBanner();
@@ -145,6 +160,10 @@
loadNewsBanner();
});
onBackPress(() => {
plus.runtime.quit();
return true;
});
// 加载新闻轮播数据
const loadNewsBanner = async () => {
try {
@@ -170,16 +189,6 @@
// Banner相关数据
const bannerList = ref([]);
const currentBannerIndex = ref(0);
// 返回
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({
url: '/pages/index/index'
});
}
});
};
// Banner相关方法
const getMockBannerData = () => {
return [
@@ -212,11 +221,14 @@
};
const playBannerVideo = (banner) => {
uni.navigateTo({
url: `/pages/videoDetail/videoDetail?id=${banner.id}`
const encoded = encodeURIComponent(docUrl+banner.path);
navTo({
url: `/pages_app/webview/webview?url=${encoded}&type=news&share=1&sharetitle=${banner.title}`
});
};
const goBack = () => {
plus.runtime.quit();
};
// 新闻相关方法
const goToNews = (type) => {
if(type=="news"){
@@ -228,7 +240,7 @@
const goToCourse = (type) => {
navTo({
url:'/pages/live/live'
url:'/pages/live/live?from=news'
})
};
</script>
@@ -332,6 +344,10 @@
align-items: center;
justify-content: center;
margin-right: 30rpx;
image{
width: 100%;
height: 100%;
}
&.blue {
background-color: $blue-color;
+87 -16
View File
@@ -1,16 +1,27 @@
<template>
<view class="news-list-page">
<!-- 顶部导航栏 -->
<!-- <navBar title="肝胆新闻" /> -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="肝胆新闻"
:title="'肝胆新闻'"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
>
<template v-slot:right>
<view class="nav-right" >
<view class="collect-img" @click="goSearch" >
<image class="img-icon" :src="searchImg" mode="aspectFill" />
</view>
</view>
</template>
</uni-nav-bar>
</view>
<!-- 导航标签 -->
<view class="nav-tabs">
@@ -64,7 +75,7 @@
<swiper-item
v-for="(banner, index) in bannerList"
:key="banner.id"
@click="playBannerVideo(banner)"
@click="goToNewsDetail(banner)"
>
<view class="banner-item">
<image
@@ -97,11 +108,14 @@
<view class="item-title">{{ news.title }}</view>
<view class="item-meta">
<text class="item-date">{{ formatDate(news.createDate) }}</text>
<view class="item-stats">
<view class="item-stats middel" >
<uni-icons type="eye" size="14" color="#999"></uni-icons>
<text>{{ news.readnum || 0 }}</text>
</view>
<view class="item-stats">
<uni-icons type="hand-up" size="14" color="#999" style="margin-left: 20rpx;"></uni-icons>
<text>{{ news.likenum || 0 }}</text>
<text>{{ news.agreenum || 0 }}</text>
</view>
</view>
</view>
@@ -127,10 +141,34 @@
<script setup>
import { ref, onMounted, reactive } from 'vue';
import { onShow, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app';
import { onShow, onPullDownRefresh, onReachBottom,onLoad,onBackPress} from '@dcloudio/uni-app';
import api from '@/api/api.js';
import docUrl from '@/utils/docUrl.js';
import navTo from "@/utils/navTo.js";
import navBar from '@/components/navBar/navBar.vue';
import dayjs from 'dayjs';
import searchImg from "@/static/search.png";
const from=ref('');
onLoad((options) => {
if(options.from){
from.value = options.from;
}
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "肝胆新闻",
},
},
(ret) => {
console.log(ret);
});
});
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true
}
});
// Banner相关数据
const bannerList = ref([]);
const currentBannerIndex = ref(0);
@@ -167,9 +205,9 @@
currentBannerIndex.value = e.detail.current;
};
const playBannerVideo = (banner) => {
let url=encodeURIComponent(docUrl+banner.path)
uni.navigateTo({
url: `/pages_app/webview/webview?url=${url}&title=${banner.title}`
const encoded = encodeURIComponent(docUrl+banner.path);
navTo({
url: `/pages_app/webview/webview?url=${encoded}&type=news&share=1&sharetitle=${banner.title}`
});
};
// scroll-view 下拉刷新
@@ -208,7 +246,11 @@
// 方法
const goBack = () => {
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
};
const goToSearch = () => {
@@ -477,10 +519,22 @@
};
const goToNewsDetail = (item) => {
// Implement navigation to news detail page
console.log('Go to news detail:', item);
uni.navigateTo({
url: `/pages_app/newsDetail/newsDetail?id=${item.uuid || item.id}`
// Implement navigation to news detail page
//const encoded = encodeURIComponent(docUrl+item.path);
// navTo({
// url: `/pages_app/webview/webview?url=${encoded}&type=news&share=1&sharetitle=${item.title}`
// });
navTo({
url: `/pages_app/webview/webviewClass?url=${encodeURIComponent(docUrl+item.path+'?fromtype=doctor')}&title=新闻详情&uuid=${item.uuid}&sharetitle=${item.title}&summary=${item.summary}&type=huanjiao_news&imgPath=${encodeURIComponent(item.headImg)}`
});
// uni.navigateTo({
// url: `/pages_app/newsDetail/newsDetail?id=${item.uuid || item.id}`
// });
};
const goSearch = () => {
uni.sendNativeEvent('goHomeSearch',{
msg: 'news'
});
};
@@ -499,6 +553,16 @@
</script>
<style lang="scss" scoped>
.img-icon {
width: 34rpx;
height: 34rpx;
}
.nav-actions {
display: flex;
align-items: center;
justify-content: center;
}
.news-list-page {
background-color: #f8f8f8;
height: 100vh;
@@ -544,7 +608,7 @@
}
&.active {
color: #ff4757;
color: #8B2316;
font-weight: bold;
&::after {
@@ -610,7 +674,7 @@
bottom:0;
width:100%;
top:240rpx;
height: calc(100vh - 240rpx); // 减去导航栏、标签栏和banner的高度
height: calc(100vh - var(--status-bar-height) - 44px - 100rpx); // 减去导航栏、标签栏和banner的高度
background-color: #f8f8f8;
}
@@ -684,6 +748,8 @@
color: #999;
.item-date {
width:125rpx;
white-space: nowrap;
flex-shrink: 0;
}
@@ -691,10 +757,15 @@
display: flex;
align-items: center;
text {
margin-left: 8rpx;
}
}
.middel {
width:95rpx;
text-align: left;
}
}
}
}
+115 -31
View File
@@ -1,32 +1,34 @@
<template>
<view class="content">
<!-- 顶部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="患者详情"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
>
<template #right>
<view class="nav-right" @click.stop="editPatient">
<uni-icons type="compose" size="22" color="#8B2316"></uni-icons>
<up-image :src="editImg" width="40rpx" height="40rpx"></up-image>
<!-- <uni-icons type="compose" size="22" color="#8B2316"></uni-icons> -->
</view>
</template>
</uni-nav-bar>
</view>
<!-- 头部信息卡片 -->
<view class="card header-card">
<image class="avatar" :src="docUrl+patientInfo.photo" mode="aspectFill"></image>
<image class="avatar" :src="avatarSrc" mode="aspectFill" @error="handleAvatarError"></image>
<view class="base-info">
<view class="name-row">
<text class="name">{{ patientInfo.realName || '未知' }}</text>
<text class="name">{{nickname || patientInfo.realName }}</text>
<up-image :src="genderIcon" width="36rpx" height="36rpx"></up-image>
</view>
<view class="line"><text class="label">昵称:</text><text class="value">{{nickname }}</text></view>
<view class="line" v-if="nickname"><text class="label">昵称:</text><text class="value">{{ patientInfo.realName || '未知' }}</text></view>
<view class="line">
<text class="label">年龄:</text><text class="value">{{ patientDetail.age || '未知' }}</text>
<text class="sep">|</text>
@@ -41,9 +43,16 @@
<!-- 功能分组条目 -->
<view class="cell" @click="openGroup" style="margin-top: 20rpx;">
<text class="cell-label">备注</text>
<view class="cell-right">
<text class="hint">{{nickname || '给患者添加备注名'}}</text>
<uni-icons type="right" color="#999" size="18"></uni-icons>
</view>
</view>
<view class="cell" @click="openGroup" >
<text class="cell-label">分组</text>
<view class="cell-right">
<text class="hint">{{ group.name }}</text>
<text class="hint">{{ group.name || '通过分组给患者分类' }}</text>
<uni-icons type="right" color="#999" size="18"></uni-icons>
</view>
</view>
@@ -57,27 +66,32 @@
<view class="cell">
<text class="cell-label">电话号码</text>
<view class="cell-right">
<text class="phone">{{ patientInfo.mobile || '未设置' }}</text>
<text class="phone" @click="alertDialog">{{ patientInfo.mobile || '未设置' }}</text>
</view>
</view>
<!-- 患者病史 -->
<view class="history-section" v-if="patientDetail.medicalHistoryContent">
<view class="history-section">
<view class="section-title">患者病史</view>
<view class="card history-card">
<view class="card history-card" v-if="patientDetail.medicalHistoryContent">
<text class="history-text" :class="{ fold: !showAllHistory }">{{ patientDetail.medicalHistoryContent }}</text>
<text class="toggle" @click="toggleHistory" v-if="patientDetail.medicalHistoryContent">{{ showAllHistory ? '收起' : '展开全部' }}</text>
</view>
<view class="card history-card" v-else>
<text class="history-text">暂无</text>
</view>
</view>
<!-- 检查报告 -->
<view class="history-section" v-if="patientDetail.patientCase && patientDetail.patientCase.length>0">
<view class="section-title">检查报告</view>
<view class="card report-card">
<scroll-view class="card report-card-scroll" scroll-x>
<view class="report-card">
<view class="report-item" v-for="item in patientDetail.patientCase" :key="item.diseaseUuid" @click="openCase(item.uuid)">
<view class="report-item-title">{{ $u.timeFormat(item.createDate, 'yyyy-mm-dd') }}</view>
<view class="report-item-content">{{ item.diseaseName }}</view>
</view>
</view>
</scroll-view>
</view>
<!-- 底部操作 -->
@@ -87,20 +101,29 @@
<text class="action-text">发消息</text>
</view>
<view class="action" @click="goMakePlan">
<up-image :src="planImg" width="44rpx" height="44rpx" ></up-image>
<up-image :src="recordImg" width="44rpx" height="44rpx" ></up-image>
<text class="action-text">制定随访计划</text>
</view>
<view class="action" @click="recordIllness">
<up-image :src="recordImg" width="44rpx" height="44rpx" ></up-image>
<up-image :src="planImg" width="44rpx" height="44rpx" ></up-image>
<text class="action-text">记录病情</text>
</view>
</view>
</view>
<unidialog
:visible="showDialog"
@close="showDialog=false"
@confirm="confirmDialog"
:content="'复制该患者手机号码'"
:confirmText="'确定'"
:cancelText="'取消'"
>
</unidialog>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onShow,onLoad } from '@dcloudio/uni-app'
import { onShow,onLoad,onBackPress} from '@dcloudio/uni-app'
import api from '@/api/api.js'
import docUrl from '@/utils/docUrl.js'
import navTo from '@/utils/navTo.js'
@@ -109,15 +132,42 @@
import recordImg from "@/static/image_backgrond_red_big.png"
import manImg from "@/static/new_man_big.png"
import womanImg from "@/static/new_woman_big.png"
import defaultImg from "@/static/default.png"
import unidialog from '@/components/dialog/dialog.vue'
import editImg from "@/static/edit_patitent.png"
const showAllHistory = ref(false);
const showDialog = ref(false);
const patientDetail = ref({});
const patient_uuid = ref('');
const note = ref('');
const avatarLoadError = ref(false);
const from = ref('');
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
const confirmDialog = ()=>{
showDialog.value = false;
uni.setClipboardData({
data: patientInfo.value.mobile,
success: () => {
uni.showToast({ title: '复制成功', icon: 'none' });
}
})
}
// 计算属性:患者基本信息
const patientInfo = computed(() => {
return patientDetail.value.patient || {};
});
const avatarSrc = computed(() => {
if (avatarLoadError.value) return defaultImg;
return patientInfo.value.photo ? (docUrl + patientInfo.value.photo) : defaultImg;
});
const handleAvatarError = () => {
avatarLoadError.value = true;
};
@@ -136,9 +186,19 @@
// 计算属性:患者病史文本
onLoad((options) => {
console.log(options);
patient_uuid.value = options.uuid;
if(options.from){
from.value=options.from
}
});
const goBack = ()=> uni.navigateBack()
const goBack = ()=> {
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
}
const openCase = (uuid)=>{
navTo({
url:'/pages_app/checkRecord/checkRecord?uuid='+uuid
@@ -154,13 +214,31 @@
url:'/pages_app/patientRemark/patientRemark?uuid=' + patientInfo.value.uuid
})
}
const alertDialog = ()=>{
if(patientInfo.value.mobile){
showDialog.value = true;
}
}
const editDesc = ()=> uni.showToast({ title:'编辑描述', icon:'none' })
const sendMessage = async()=>{
let userId=uni.getStorageSync('userInfo').uuid.toLowerCase();
let conversationId=userId+'|1|'+patientInfo.value.uuid.toLowerCase();
await uni.$UIKitStore.uiStore.selectConversation(conversationId)
navTo({
url:'/pages_chat/chat/index'
// let conversationId=userId+'|1|'+patientInfo.value.uuid.toLowerCase();
// await uni.$UIKitStore.uiStore.selectConversation(conversationId)
// navTo({
// url:'/pages_chat/chat/index'
// })
console.log(patientInfo.value.realName);
console.log(nickname.value);
uni.sendNativeEvent('goSendMsg', {
msg: {
patientuuid: patientInfo.value.uuid,
nickname:nickname.value,
realName:patientInfo.value.realName,
photo:patientInfo.value.photo,
}
},ret => {
console.log(ret);
})
};
const nickname = ref('');
@@ -194,6 +272,7 @@
}).then(res=>{
if(res.code == 200){
patientDetail.value = res.data;
avatarLoadError.value = false;
console.log('患者详情数据:', res.data);
} else {
uni.showToast({
@@ -216,7 +295,7 @@
});
const goMakePlan = ()=> {
navTo({
url:'/pages_app/visitPlan/visitPlan'
url:'/pages_app/visitPlan/visitPlan?uuid='+patient_uuid.value+'&patient_name='+patientInfo.value.realName+'&type=patientDetail'
})
}
const recordIllness = ()=> {
@@ -228,15 +307,18 @@
</script>
<style lang="scss" scoped>
.report-card-scroll{
white-space: nowrap;
}
.report-card{
display: flex;
gap: 55rpx;
flex-wrap: wrap;
display: inline-flex;
gap: 24rpx;
}
.report-item{
display: flex;
flex-direction: column;
gap: 10rpx;
min-width: 260rpx;
background: #f5f5f5;
padding: 20rpx;
@@ -255,15 +337,17 @@
.nav-right { padding-right: 20rpx; }
.card { background:#ffffff; padding: 20rpx; padding-top: 0;}
.header-card { display:flex; padding-top: 20rpx;}
.header-card {
margin-top: calc(var(--status-bar-height) + 44px);
display:flex; padding-top: 20rpx;}
.avatar { width: 140rpx; height: 140rpx; border-radius: 12rpx; margin-right: 20rpx; }
.base-info { flex:1; }
.name-row { display:flex; align-items:center; margin-bottom: 10rpx; }
.name { font-size: 40rpx; color:#222; margin-right: 10rpx;}
.name { font-size: 36rpx; color:#222; margin-right: 10rpx;}
.line { font-size: 28rpx; color:#666; margin-top: 6rpx; display:flex; align-items:center; }
.label { color:#999;white-space: nowrap; }
.value { color:#333; }
.sep { color:#ddd; margin: 0 10rpx; }
.label { color:#666;white-space: nowrap; }
.value { color:#666; }
.sep { color:#666; margin: 0 10rpx; }
.multi{
display: inline;
}
@@ -274,9 +358,9 @@
.hint { font-size: 28rpx; color:#999; margin-right: 12rpx; }
.phone { font-size: 32rpx; color:#b10000; }
.section-title { padding: 16rpx 30rpx; color:#8B2316; font-size: 30rpx; margin-top: 20rpx; background: #fff;}
.section-title { padding: 16rpx 30rpx; color:#333; font-size: 28rpx; margin-top: 20rpx; background: #fff;}
.history-card { position: relative; }
.history-text { font-size: 28rpx; color:#666; line-height: 1.7; display:block; }
.history-text { font-size: 28rpx; color:#999; line-height: 1.7; display:block; }
.history-text.fold { display:-webkit-box; -webkit-line-clamp: 2; line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.toggle { position:absolute; right: 20rpx; bottom: 25rpx; color:#b10000; font-size: 28rpx;background: #fff; z-index: 10;}
+155 -27
View File
@@ -2,13 +2,15 @@
<view class="patient-group-page">
<!-- 头部 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="患者分组"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
>
@@ -16,19 +18,20 @@
<view class="save-text" @click="addGroup">新建</view>
</template>
</uni-nav-bar>
</view>
<!-- 筛选排序栏 -->
<view class="filter-sort-bar">
<view class="sort-section" @click="toggleGroupSort">
<text class="sort-label">{{ groupSortTitle }}</text>
<text class="sort-label" :class="{ 'is-selected': groupSortSelected }">{{ groupSortTitle }}</text>
<view class="imgbox">
<up-image :src="upImg" width="26rpx" height="26rpx" ></up-image>
<up-image :src="groupSortIcon" width="26rpx" height="26rpx" ></up-image>
</view>
</view>
<view class="divider"></view>
<view class="current-sort" @click="toggleInnerSort">
<text class="sort-text">{{ innerSortTitle }}</text>
<text class="sort-text" :class="{ 'is-selected': innerSortSelected }">{{ innerSortTitle }}</text>
<view class="imgbox">
<up-image :src="upImg" width="26rpx" height="26rpx" ></up-image>
<up-image :src="innerSortIcon" width="26rpx" height="26rpx" ></up-image>
</view>
</view>
</view>
@@ -62,9 +65,9 @@
<view v-if="showInnerSort" class="popup-mask" @click="closeInnerSort"></view>
<!-- 患者列表 -->
<scroll-view class="patient-list-section" scroll-y="true" :style="{ height: scrollViewHeight }">
<scroll-view class="patient-list-section" scroll-y="true" >
<!-- 分组循环渲染 -->
<view class="groupcell" v-for="(group, gi) in groups" :key="group.uuid || gi">
<view class="groupcell" v-for="(group, gi) in groups" :key="group.uuid || gi" style="flex-direction: row;">
<view class="section-title" @click="toggleGroup(gi)">
<view class="left">
<view class="imgbox" :class="{ 'active':openGroups[gi] }">
@@ -79,38 +82,71 @@
</view>
</view>
<view class="patient-list" v-if="openGroups[gi]">
<view class="patient-item" v-for="(patient, index) in (group.patientList || [])" :key="patient.uuid || index">
<view class="patient-item" v-for="(patient, index) in getVisiblePatients(group, gi)" :key="patient.uuid || index" @click="goPatientDetail(patient.uuid)">
<view class="patient-avatar">
<up-image :src="docUrl + patient.photo" width="80rpx" height="80rpx" mode="aspectFill"></up-image>
<up-image :src="patient._avatarLoadError ? defaultImg : patient._photoUrl" width="80rpx" height="80rpx" mode="aspectFill" @error="handlePatientAvatarError(patient)"></up-image>
</view>
<view class="patient-info">
<view class="patient-name">{{ patient.realName || patient.nickname || '-' }}</view>
<view class="follow-up-time">随访于{{ formatYMD(patient.join_date) }}</view>
<view class="patient-name">{{ patient._displayName }}</view>
<view class="follow-up-time">随访于{{ patient._formattedJoinDate }}</view>
</view>
</view>
<view v-if="isGroupRendering(group, gi)" class="rendering-tip">加载中...</view>
</view>
</view>
<empty v-if="groups.length == 0" :emptyDesc="'暂无分组'" />
</scroll-view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import { ref, computed, onMounted,nextTick } from 'vue';
import { onShow,onBackPress,onLoad } from "@dcloudio/uni-app";
import upImg from "@/static/triangle_green_theme.png"
import downImg from "@/static/triangle_normal.png"
import groupRightImg from "@/static/groupright_big.png"
import groupDownImg from "@/static/groupup_big.png"
import normalImg from "@/static/triangle_normal.png"
import api from '@/api/api.js';
import docUrl from '@/utils/docUrl'
import empty from "@/components/empty/empty.vue"
import dayjs from 'dayjs'
import navTo from '@/utils/navTo.js'
import defaultImg from "@/static/default.png"
const list_sort = ref(0);
const group_sort = ref(0);
const groupSortSelected = ref(false);
const innerSortSelected = ref(false);
const from = ref('');
onLoad((options) => {
if(options.from){
from.value = options.from;
}
});
onShow(() => {
uni.showLoading({
title: '加载中...',
mask: true
});
fetchGroupList();
});
const goPatientDetail = (uuid) => {
navTo({
url: `/pages_app/patientDetail/patientDetail?uuid=${uuid}`
})
}
onBackPress(() => {
if(!from.value){
uni.navigateBack();
return true;
}
});
// 分组数据与展开状态
const groups = ref([]);
const openGroups = ref({});
const groupRenderCount = ref({});
const INITIAL_RENDER_COUNT = 20;
const BATCH_RENDER_COUNT = 20;
const formatYMD = (val) => {
if (!val) return '';
@@ -118,13 +154,18 @@ const formatYMD = (val) => {
return d.isValid() ? d.format('YYYY-MM-DD') : '';
};
// 排序栏标题
const groupSortTitle = computed(() => group_sort.value === 0 ? '按首字母' : '分组人数');
const innerSortTitle = computed(() => list_sort.value === 0 ? '按首字母' : '随访时间');
onShow(() => {
fetchGroupList();
// 排序栏标题/图标(初始化为默认文案+灰色图标,选择后展示具体文案+主题图标)
const groupSortTitle = computed(() => {
if (!groupSortSelected.value) return '分组排序';
return group_sort.value === 0 ? '按首字母' : '分组人数';
});
const innerSortTitle = computed(() => {
if (!innerSortSelected.value) return '组内排序';
return list_sort.value === 0 ? '按首字母' : '随访时间';
});
const groupSortIcon = computed(() => groupSortSelected.value ? upImg : normalImg);
const innerSortIcon = computed(() => innerSortSelected.value ? upImg : normalImg);
const fetchGroupList = async () => {
const res = await api.groupList({
@@ -134,15 +175,80 @@ const fetchGroupList = async () => {
group_sort:group_sort.value
});
if(res.code === 200){
groups.value = Array.isArray(res.data) ? res.data : [];
groups.value = Array.isArray(res.data) ? preprocessGroups(res.data) : [];
// 初始化展开状态:默认收起
openGroups.value = {};
groupRenderCount.value = {};
groups.value.forEach((_, idx) => openGroups.value[idx] = false);
nextTick(() => {
uni.hideLoading();
});
}
};
const toggleGroup = (idx) => {
openGroups.value[idx] = !openGroups.value[idx];
if (openGroups.value[idx]) {
startProgressiveRender(idx);
}
};
const preprocessGroups = (rawGroups) => {
return rawGroups.map((group) => {
const list = Array.isArray(group.patientList) ? group.patientList : [];
list.forEach((patient) => {
patient._formattedJoinDate = formatYMD(patient.join_date);
patient._displayName = patient.nickname || patient.realName ;
patient._photoUrl = patient.photo ? `${docUrl}${patient.photo}` : defaultImg;
});
return {
...group,
patientList: list
};
});
};
const handlePatientAvatarError = (patient) => {
if (!patient) return;
patient._avatarLoadError = true;
};
const startProgressiveRender = (idx) => {
const group = groups.value[idx];
if (!group) return;
const total = (group.patientList || []).length;
if (!total) {
groupRenderCount.value[idx] = 0;
return;
}
groupRenderCount.value[idx] = Math.min(INITIAL_RENDER_COUNT, total);
if (total <= INITIAL_RENDER_COUNT) return;
const loadNextBatch = () => {
if (!openGroups.value[idx]) return;
const current = groupRenderCount.value[idx] || 0;
if (current >= total) return;
groupRenderCount.value[idx] = Math.min(current + BATCH_RENDER_COUNT, total);
if (groupRenderCount.value[idx] < total) {
setTimeout(loadNextBatch, 16);
}
};
setTimeout(loadNextBatch, 16);
};
const getVisiblePatients = (group, idx) => {
const list = group?.patientList || [];
const renderCount = groupRenderCount.value[idx] || 0;
if (renderCount >= list.length) return list;
return list.slice(0, renderCount);
};
const isGroupRendering = (group, idx) => {
const total = (group?.patientList || []).length;
const rendered = groupRenderCount.value[idx] || 0;
return total > 0 && rendered < total;
};
const editGroup = (group) => {
@@ -174,6 +280,7 @@ const chooseGroupSort = (type) => {
selectedGroupSort.value = type;
// 修改接口字段:按首字母=0,分组人数=1
group_sort.value = type === 'letter' ? 0 : 1;
groupSortSelected.value = true;
closeGroupSort();
fetchGroupList();
};
@@ -195,13 +302,18 @@ const chooseInnerSort = (type) => {
selectedInnerSort.value = type;
// 组内排序:按首字母=0,随访时间=1
list_sort.value = type === 'letter' ? 0 : 1;
innerSortSelected.value = true;
closeInnerSort();
fetchGroupList();
};
// 方法
const goBack = () => {
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
};
const createNew = () => {
@@ -218,6 +330,9 @@ const createNew = () => {
.patient-group-page {
min-height: 100vh;
background-color: #f5f5f5;
.groupcell{
flex-direction: row!important;
}
}
/* 弹窗样式 */
@@ -233,7 +348,7 @@ const createNew = () => {
.popup-panel {
position: fixed;
top: 220rpx; /* 紧贴筛选栏 */
top: calc(var(--status-bar-height) + 44px + 82rpx); /* 紧贴筛选栏 */
left: 0;
right: 0;
background: #fff;
@@ -388,7 +503,7 @@ const createNew = () => {
padding: 20rpx 30rpx;
background-color: #ffff;
position: fixed;
top: 180rpx;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
z-index: 9;
@@ -404,7 +519,10 @@ const createNew = () => {
.sort-label {
font-size: 28rpx;
color: #333;
color: #999;
&.is-selected {
color: #8B2316;
}
}
.sort-icon.down {
@@ -430,7 +548,10 @@ const createNew = () => {
.sort-text {
font-size: 28rpx;
color: #333;
color: #999;
&.is-selected {
color: #8B2316;
}
}
.sort-icon.up {
@@ -448,7 +569,7 @@ const createNew = () => {
}
.patient-list-section {
top: 240rpx;
top: calc(var(--status-bar-height) + 44px + 100rpx);
width:100%;
bottom:0;
position: fixed;
@@ -525,5 +646,12 @@ const createNew = () => {
}
}
}
.rendering-tip {
padding: 16rpx 30rpx 24rpx;
font-size: 24rpx;
color: #999;
text-align: center;
}
}
</style>
+116 -27
View File
@@ -28,43 +28,61 @@
<view v-if="activeTab === 'basic'" class="basic-info">
<view class="info-item">
<text class="info-label">姓名</text>
<text class="info-value">{{ patientInfo.name || '提**' }}</text>
<text class="info-value">{{ maskName(patientInfo.name) }}</text>
</view>
<view class="info-item">
<text class="info-label">性别</text>
<text class="info-value">{{ patientInfo.gender || '男' }}</text>
<text class="info-value">{{ patientInfo.gender }}</text>
</view>
<view class="info-item">
<text class="info-label">年龄</text>
<text class="info-value">{{ patientInfo.age || '15' }}</text>
<text class="info-value">{{ patientInfo.age }}</text>
</view>
<view class="info-item">
<text class="info-label">地址</text>
<text class="info-value">{{ patientInfo.address || '北京市东城区' }}</text>
<text class="info-value twoline" style="max-width:380rpx;flex:none">{{ patientInfo.address }}</text>
</view>
<view class="info-item" v-if="patientInfo.gender=='女'">
<text class="info-label">是否怀孕</text>
<text class="info-value">{{ formatwhetherpregnant(patientInfo.whether_pregnant) }}</text>
</view>
<view class="info-item" v-if="patientInfo.gender=='女' && patientInfo.expected_date_of_childbirth" >
<text class="info-label">预产期</text>
<text class="info-value">{{ patientInfo.expected_date_of_childbirth }}</text>
</view>
<view class="info-item">
<text class="info-label">肝硬化或肝癌家族史</text>
<text class="info-value">{{ patientInfo.familyHistory || '无' }}</text>
<text class="info-value">{{ whetherHbv(patientInfo.whether_hbv) }}</text>
</view>
</view>
<!-- 病史信息内容 -->
<view v-if="activeTab === 'history'" class="history-info">
<view class="info-item">
<text class="info-label">既往病史</text>
<text class="info-value">暂无</text>
<text class="info-label">前往医院就诊该疾病情况</text>
<text class="info-value">{{ goHospital(patientInfo.go_hospital) }}</text>
</view>
<view class="info-item">
<text class="info-label">过敏史</text>
<text class="info-value">暂无</text>
<text class="info-label">患病时间</text>
<text class="info-value">{{ patientInfo.disease_date }}</text>
</view>
<view class="info-item">
<text class="info-label">手术史</text>
<text class="info-value">暂无</text>
<text class="info-label">目前肝脏状态</text>
<text class="info-value">{{ patientInfo.liver_status }}</text>
</view>
<view class="info-item">
<text class="info-label">用药史</text>
<text class="info-value">暂无</text>
<text class="info-label">当前服用肝病用药情况</text>
<text class="info-value">{{ patientInfo.boolean_medication }}</text>
</view>
<view class="info-item column" v-if="patientInfo.medication_info">
<text class="info-label" style="padding-top: 20rpx;">当前服用的肝病药物以及服用时长</text>
<text class="info-content">{{ patientInfo.medication_info }}</text>
</view>
<view class="info-item column" >
<text class="info-label" style="padding-top: 20rpx;">是否合并其他慢性疾病</text>
<view class="otherbox" v-if="patientInfo.other_disease">
<view class="cell" v-for="item in patientInfo.other_disease.split(',')" :key="item">{{ item}}</view>
</view>
</view>
</view>
</scroll-view>
@@ -96,17 +114,22 @@ const interrogationPatientInfo=()=>{
step1_uuid: step1_uuid.value
}).then(res=>{
if(res.code == 200 && res.data){
const d = res.data
patientInfo.name = d.name || '提**'
patientInfo.gender = d.sex === 1 ? '男' : '女'
const d = res.data;
Object.assign(patientInfo, d);
// patientInfo.name = d.name || '提**'
patientInfo.gender = d.sex === 1 ? '女' : '男'
patientInfo.age = d.birthday ? calculateAge(d.birthday) : '未知'
patientInfo.address = d.address || ''
// 家族史字段接口未提供,设为未知
patientInfo.familyHistory = '未知'
// patientInfo.address = d.address || ''
// // 家族史字段接口未提供,设为未知
// patientInfo.familyHistory = '未知'
}
})
}
const maskName = (name)=>{
if(!name) return '**'
const first = name.slice(0,1)
return `${first}**`
}
function calculateAge(birthday){
const birth = new Date(birthday)
const today = new Date()
@@ -126,7 +149,33 @@ const statusBarStyle = computed(() => ({
const goBack = () => {
uni.navigateBack()
}
const formatwhetherpregnant = (value)=>{
if(value==1){
return '无计划'
}else if(value==2){
return '计划中'
}else if(value==3){
return '已怀孕'
}else if(value==4){
return '家有宝宝'
}
}
const goHospital = (val)=>{
if(val==1){
return '是'
}else if(val==0){
return '否'
}
}
const whetherHbv = (value)=>{
if(value==0){
return '无'
}else if(value==1){
return '有'
}else{
return '未知'
}
}
const switchTab = (tab) => {
activeTab.value = tab
}
@@ -141,7 +190,7 @@ onMounted(() => {
<style lang="scss" scoped>
// 变量定义
$primary-color: #ff0000;
$primary-color: #8B2316;
$text-color: #333333;
$text-color-light: #666666;
$border-color: #f0f0f0;
@@ -152,7 +201,22 @@ $tab-height: 88rpx;
$info-item-height: 100rpx;
$padding-horizontal: 30rpx;
$padding-small: 10rpx;
.info-content{
font-size: 28rpx;
color:#999;
width: 100%;
background-color: #f0f0f0;
border-radius: 16rpx;
padding:20rpx;
box-sizing: border-box;
margin-top: 20rpx;
margin-bottom: 20rpx;
}
.column{
flex-direction: column;
justify-content: flex-start!important;
align-items: flex-start!important;
}
.patient-info-container {
background-color: $background-color;
min-height: 100vh;
@@ -161,7 +225,21 @@ $padding-small: 10rpx;
.status-bar {
background-color: $background-color;
}
.otherbox{
display: flex;
flex-wrap: wrap;
margin-top: 20rpx;
.cell{
font-size: 24rpx;
padding: 10rpx 12rpx;
border-radius: 20rpx;
margin-bottom: 20rpx;
background-color: #fff;
color:#8B2316;
border: 1rpx solid #8B2316;
margin-right: 10rpx;
}
}
.nav-bar {
display: flex;
align-items: center;
@@ -199,6 +277,10 @@ $padding-small: 10rpx;
}
.tab-bar {
top:calc(var(--status-bar-height) + 44px);
position: fixed;
left: 0;
right: 0;
display: flex;
background-color: $background-color;
border-bottom: 2rpx solid $border-color-light;
@@ -232,6 +314,10 @@ $padding-small: 10rpx;
}
.content-area {
top:calc(var(--status-bar-height) + 44px + 90rpx);
position: fixed;
left: 0;
right: 0;
flex: 1;
background-color: $background-color;
}
@@ -244,7 +330,7 @@ $padding-small: 10rpx;
display: flex;
align-items: center;
justify-content: space-between;
height: $info-item-height;
min-height: $info-item-height;
border-bottom: 2rpx solid $border-color;
padding: 0 $padding-small;
@@ -255,14 +341,17 @@ $padding-small: 10rpx;
.info-label {
font-size: 32rpx;
color: $text-color;
flex: 1;
text-align: left;
flex:1.5;
}
.info-value {
font-size: 32rpx;
color: $text-color-light;
color:#999;
text-align: right;
flex: 1;
display: flex;
justify-content: flex-end;
}
}
}
+825
View File
@@ -0,0 +1,825 @@
<template>
<view class="content">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="患者列表"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right">
<uni-icons type="search" size="24" color="#8B2316" @click="searchPatients"></uni-icons>
<uni-icons type="plusempty" size="24" color="#8B2316" @click="goCode" style="margin-left: 30rpx;"></uni-icons>
</view>
</template>
</uni-nav-bar>
</view>
<view class="patient-list">
<view class="listbox alertContent" v-if="showAlert">
<image :src="delImg" class="del-img" mode="widthFix" @click="hideAlert"></image>
<image :src="alertImg" class="alert-img" mode="widthFix"></image>
</view>
<view class="listbox patient-building" v-if="patientDataLoading && !loadFinish">
<text class="patient-building-text">正在整理患者数据,请稍候...</text>
</view>
<view class="listbox" v-show="patientList.length > 0">
<scroll-view
class="groups-scroll"
scroll-y="true"
:scroll-into-view="scrollIntoViewId"
:scroll-with-animation="true"
@scroll="onGroupsScroll"
@scrolltolower="onGroupsScrollToLower"
:lower-threshold="120"
v-show="loadFinish"
>
<view class="special-actions">
<view class="action-item" @click="addNewPatient">
<view class="action-icon new-patient">
<up-image :src="newPatientImg" width="60rpx" height="60rpx"></up-image>
</view>
<text class="action-text">新的患者<text class="new-patient-count" v-if="applyList.length > 0">(待审核{{ applyList.length }}人)</text></text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
<view class="action-item" @click="managePatientGroups">
<view class="action-icon group-icon">
<view class="grid-icon">
<view class="grid-item"></view>
<view class="grid-item"></view>
<view class="grid-item"></view>
<view class="grid-item"></view>
</view>
</view>
<text class="action-text">患者分组 <text class="new-patient-count" v-if="patientList.length > 0">(随访{{ patientListTotal }}人)</text></text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="group-section" v-for="group in patientGroups" :key="group.letter" :id="`anchor-${group.letter}`">
<view class="group-header">{{ group.letter }}</view>
<view class="patient-item" v-for="item in group.items" :key="item.uuid || item.id" @longpress="onPatientItemLongPress(item)" @click="goPatientDetail(item.uuid)">
<image
v-if="hasPatientAvatar(item)"
class="patient-avatar"
:src="getPatientAvatar(item)"
mode="aspectFill"
lazy-load="true"
@error="handlePatientAvatarError(item)"
@click.stop="goPatientDetail(item.uuid)"
></image>
<view v-else class="patient-avatar avatar-placeholder"></view>
<view class="patient-info">
<text class="patient-name">{{ item.nickname || item.realName }}</text>
</view>
<view class="patient-status">
<view class="edit-icon" @click.stop="editPatient(item.uuid)">
<view class="edit-pen"></view>
</view>
<text class="follow-date">随访于{{ item.joinDateYMD }}</text>
</view>
</view>
</view>
<view class="load-more" v-if="patientListLoadingMore">
<text class="load-more-text">加载中...</text>
</view>
<view class="load-more" v-else-if="!patientListHasMore && patientList.length > 0">
<text class="load-more-text">没有更多了</text>
</view>
</scroll-view>
<view class="letter-index" v-if="loadFinish && indexList.length > 0">
<view class="letter-item" v-for="letter in indexList" :key="letter" :class="{ active: activeLetter === letter }" @click="onLetterTap(letter)">
{{ letter }}
</view>
</view>
</view>
<view class="listbox" v-show="patientList.length === 0 && !patientDataLoading">
<view class="special-actions">
<view class="action-item" @click="addNewPatient">
<view class="action-icon new-patient">
<up-image :src="newPatientImg" width="60rpx" height="60rpx"></up-image>
</view>
<text class="action-text">新的患者<text class="new-patient-count" v-if="applyList.length > 0">(待审核{{ applyList.length }}人)</text></text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
<view class="action-item" @click="managePatientGroups">
<view class="action-icon group-icon">
<view class="grid-icon">
<view class="grid-item"></view>
<view class="grid-item"></view>
<view class="grid-item"></view>
<view class="grid-item"></view>
</view>
</view>
<text class="action-text">患者分组</text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
<empty></empty>
</view>
</view>
<view class="tab-bar">
<view class="tab-item" @click="goMessageTab">
<text class="tab-text">患者消息</text>
</view>
<view class="tab-item active">
<text class="tab-text">患者列表</text>
<view class="tab-dot" v-if="hasNewPatient"><uni-badge class="uni-badge-left-margin" :text="applyList.length" :offset="[-3, -3]" size="small"></uni-badge></view>
</view>
<view class="tab-item" @click="goPlanTab">
<text class="tab-text">随访计划</text>
</view>
</view>
</view>
<unidialog :visible="visible" :content="message" @close="visible=false" @confirm="deleteFollowUpFromList"></unidialog>
</template>
<script setup>
import { computed, nextTick, ref, shallowRef } from 'vue';
import { onBackPress, onLoad, onShow } from '@dcloudio/uni-app';
import dayjs from 'dayjs';
import pinyin from 'pinyin';
import api from '@/api/api.js';
import navTo from '@/utils/navTo.js';
import docUrl from '@/utils/docUrl.js';
import unidialog from '@/components/dialog/dialog.vue';
import newPatientImg from '@/static/new_patient.png';
import delImg from '@/static/iv_delete.png';
import alertImg from '@/static/patientgif.png';
const patientList = ref([]);
const patientDataLoaded = ref(false);
const patientDataLoading = ref(false);
const loadFinish = ref(false);
const showAlert = ref(false);
const applyList = ref([]);
const visible = ref(false);
const message = ref('');
const selectedPatientUuid = ref('');
const selectedPatientName = ref('');
const firstLetterCache = new Map();
const LETTER_ORDER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const GROUP_BUILD_CHUNK_SIZE = 180;
const PERF_TRACE = true;
const SCROLL_LOG_MIN_INTERVAL = 180;
const activeLetter = ref('');
const scrollIntoViewId = ref('');
const groupScrollRanges = ref([]);
const indexList = ref([]);
const avatarErrorMap = ref({});
const patientGroups = shallowRef([]);
const allPatientGroups = shallowRef([]);
const lastScrollTop = ref(0);
const lastScrollLogAt = ref(0);
const buildStartedAt = ref(0);
const fetchStartedAt = ref(0);
const patientListPage = ref(1);
const patientListPageSize = ref(300);
const patientListHasMore = ref(true);
const patientListLoadingMore = ref(false);
const canTriggerLowerLoad = ref(true);
const lastLowerTriggerTop = ref(0);
const LOWER_RESET_DISTANCE = 120;
const patientListTotal = ref(0);
const hasNewPatient = computed(() => applyList.value.length > 0);
const perfLog = (...args) => {
if (!PERF_TRACE) return;
console.log('[patientList/perf]', ...args);
};
const sortLetter = (a, b) => {
if (a === '#') return 1;
if (b === '#') return -1;
const ai = LETTER_ORDER.indexOf(a);
const bi = LETTER_ORDER.indexOf(b);
if (ai === -1 && bi === -1) return String(a).localeCompare(String(b));
if (ai === -1) return 1;
if (bi === -1) return -1;
return ai - bi;
};
const formatYMD = (input) => {
if (!input) return '';
const d = dayjs(input);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
};
const getFirstLetter = (name = '') => {
const firstChar = String(name).trim().charAt(0);
if (!firstChar) return '#';
if (/^[A-Za-z]$/.test(firstChar)) return firstChar.toUpperCase();
if (firstLetterCache.has(firstChar)) return firstLetterCache.get(firstChar);
try {
const py = pinyin(firstChar, { style: pinyin.STYLE_NORMAL })?.[0]?.[0] || '';
const letter = py ? py.charAt(0).toUpperCase() : '#';
firstLetterCache.set(firstChar, letter);
return letter;
} catch (error) {
return '#';
}
};
const getAvatarKey = (item = {}) => String(item.uuid || item.id || item.realName || item.nickname || '');
const normalizeAvatarUrl = (photo) => {
const raw = String(photo || '').trim();
if (!raw) return '';
if (/^https?:\/\//i.test(raw)) return raw;
return `${docUrl}${raw}`;
};
const hasPatientAvatar = (item = {}) => {
const key = getAvatarKey(item);
if (!item.avatarUrl) return false;
if (key && avatarErrorMap.value[key]) return false;
return true;
};
const getPatientAvatar = (item = {}) => {
return item.avatarUrl || '';
};
const handlePatientAvatarError = (item) => {
if (!item) return;
const key = getAvatarKey(item);
if (!key) return;
avatarErrorMap.value = { ...avatarErrorMap.value, [key]: true };
};
const rebuildIndexList = () => {
indexList.value = allPatientGroups.value
.map(g => String(g?.letter || '').toUpperCase())
.filter((letter, index, arr) => (letter === '#' || /^[A-Z]$/.test(letter)) && arr.indexOf(letter) === index)
.sort(sortLetter);
activeLetter.value = indexList.value[0] || '';
};
const rpxToPx = (rpx) => {
const { screenWidth } = uni.getSystemInfoSync();
return Math.round((screenWidth / 750) * rpx);
};
const buildGroupScrollRanges = () => {
const actionHeight = rpxToPx(120);
const headerHeight = rpxToPx(80);
const itemHeight = rpxToPx(150);
let cursor = actionHeight * 2;
groupScrollRanges.value = patientGroups.value.map(group => {
const groupHeight = headerHeight + (group.items?.length || 0) * itemHeight;
const range = { letter: group.letter, start: cursor, end: cursor + groupHeight };
cursor += groupHeight;
return range;
});
};
const onGroupsScroll = (e) => {
const top = Number(e?.detail?.scrollTop || 0);
if (!canTriggerLowerLoad.value && top < lastLowerTriggerTop.value - LOWER_RESET_DISTANCE) {
canTriggerLowerLoad.value = true;
}
if (!groupScrollRanges.value.length) return;
let current = groupScrollRanges.value[0].letter;
for (let i = 0; i < groupScrollRanges.value.length; i++) {
const range = groupScrollRanges.value[i];
if (top >= range.start) {
current = range.letter;
} else {
break;
}
}
activeLetter.value = current;
const now = Date.now();
if (now - lastScrollLogAt.value >= SCROLL_LOG_MIN_INTERVAL) {
perfLog('scroll', {
top,
deltaTop: top - lastScrollTop.value,
activeLetter: current,
groupCount: patientGroups.value.length,
patientCount: patientList.value.length
});
lastScrollLogAt.value = now;
lastScrollTop.value = top;
}
};
const onLetterTap = (letter) => {
if (!letter) return;
activeLetter.value = letter;
const targetId = `anchor-${letter}`;
if (scrollIntoViewId.value === targetId) {
scrollIntoViewId.value = '';
nextTick(() => {
scrollIntoViewId.value = targetId;
});
return;
}
scrollIntoViewId.value = targetId;
};
const waitForNextFrame = () => new Promise(resolve => setTimeout(resolve, 0));
const buildGroupsFromPatients = async () => {
buildStartedAt.value = Date.now();
perfLog('build:start', { count: patientList.value.length });
avatarErrorMap.value = {};
const map = new Map();
const source = patientList.value || [];
for (let i = 0; i < source.length; i++) {
const p = source[i] || {};
const normalized = { ...p, avatarUrl: normalizeAvatarUrl(p.photo), joinDateYMD: formatYMD(p.join_date) };
const first = getFirstLetter(normalized.nickname || normalized.realName);
const letter = /^[A-Z]$/.test(first) ? first : '#';
if (!map.has(letter)) map.set(letter, []);
map.get(letter).push(normalized);
if (i > 0 && i % GROUP_BUILD_CHUNK_SIZE === 0) {
await waitForNextFrame();
}
}
const letters = Array.from(map.keys()).sort(sortLetter);
allPatientGroups.value = letters.map(l => ({ letter: l, items: map.get(l) }));
patientGroups.value = allPatientGroups.value;
rebuildIndexList();
buildGroupScrollRanges();
perfLog('build:done', {
costMs: Date.now() - buildStartedAt.value,
groupCount: patientGroups.value.length,
patientCount: patientList.value.length
});
};
const getApplyList = async () => {
try {
const res = await api.applyList();
if (res && res.code === 200) applyList.value = res.data || [];
} catch (error) {}
};
const isApiSuccess = (res) => Number(res?.code) === 1 || Number(res?.code) === 200;
const parsePagedPatients = (res) => {
const data = res?.data;
if (Array.isArray(data)) {
return { list: data, total: undefined, pageNum: undefined, pages: undefined, isLastPage: undefined };
}
const list =
(Array.isArray(data?.list) && data.list) ||
(Array.isArray(data?.records) && data.records) ||
(Array.isArray(data?.rows) && data.rows) ||
(Array.isArray(data?.data) && data.data) ||
[];
const total = Number(
data?.total ??
data?.count ??
data?.totalCount ??
undefined
);
const pageNum = Number(data?.pageNum ?? data?.page ?? data?.current ?? undefined);
const pages = Number(data?.pages ?? data?.pageCount ?? undefined);
const isLastPage =
typeof data?.isLastPage === 'boolean'
? data.isLastPage
: undefined;
return {
list,
total: Number.isFinite(total) ? total : undefined
,
pageNum: Number.isFinite(pageNum) ? pageNum : undefined,
pages: Number.isFinite(pages) ? pages : undefined,
isLastPage
};
};
const mergePatientListUnique = (base = [], incoming = []) => {
if (!incoming.length) return base;
const seen = new Set();
const merged = [];
const pushUnique = (item) => {
const key = String(item?.uuid || item?.id || item?.mobile || '');
if (!key) {
merged.push(item);
return;
}
if (seen.has(key)) return;
seen.add(key);
merged.push(item);
};
for (let i = 0; i < base.length; i++) pushUnique(base[i]);
for (let i = 0; i < incoming.length; i++) pushUnique(incoming[i]);
return merged;
};
const applyPatientListData = async (list, total) => {
patientList.value = list || [];
patientListTotal.value = total || 0;
await buildGroupsFromPatients();
loadFinish.value = true;
patientDataLoaded.value = true;
if (patientListTotal.value > 1000) {
const hasAlert = uni.getStorageSync('hasAlert');
showAlert.value = !hasAlert;
}
};
const patientListByGBKPage = async ({ showLoading = true, reset = false } = {}) => {
if (patientDataLoading.value || patientListLoadingMore.value) return;
if (!reset && !patientListHasMore.value) return;
if (reset) {
patientDataLoading.value = true;
loadFinish.value = false;
patientListPage.value = 1;
patientListHasMore.value = true;
canTriggerLowerLoad.value = true;
lastLowerTriggerTop.value = 0;
} else {
patientListLoadingMore.value = true;
}
fetchStartedAt.value = Date.now();
perfLog('api:start', { showLoading, reset, page: patientListPage.value, pageSize: patientListPageSize.value });
if (showLoading) uni.showLoading({ title: '加载中...', mask: true });
try {
const requestPage = patientListPage.value;
const res = await api.patientListByGBKPage({
page: requestPage,
pageSize: patientListPageSize.value,
});
if (isApiSuccess(res)) {
const { list, total, pageNum, pages, isLastPage } = parsePagedPatients(res);
const merged = reset ? list : mergePatientListUnique(patientList.value, list);
await applyPatientListData(merged, total);
if (typeof isLastPage === 'boolean') {
patientListHasMore.value = !isLastPage;
} else if (typeof pages === 'number' && typeof pageNum === 'number') {
patientListHasMore.value = pageNum < pages;
} else if (typeof total === 'number') {
patientListHasMore.value = merged.length < total;
} else {
patientListHasMore.value = list.length >= patientListPageSize.value;
}
if (patientListHasMore.value) {
if (typeof pageNum === 'number') {
patientListPage.value = pageNum + 1;
} else {
patientListPage.value = requestPage + 1;
}
}
perfLog('api:done', {
totalMs: Date.now() - fetchStartedAt.value,
requestPage,
page: typeof pageNum === 'number' ? pageNum : (reset ? 1 : patientListPage.value - 1),
pageDataCount: list.length,
mergedCount: merged.length,
hasMore: patientListHasMore.value
});
} else {
perfLog('api:bad-code', { code: res.code });
}
} catch (error) {
perfLog('api:error', error?.message || error);
throw error;
} finally {
patientDataLoading.value = false;
patientListLoadingMore.value = false;
if (showLoading) uni.hideLoading();
}
};
const onGroupsScrollToLower = () => {
void patientListByGBKPage({ showLoading: false, reset: false });
};
const goPatientDetail = (uuid) => {
navTo({ url: `/pages_app/patientDetail/patientDetail?uuid=${uuid}` });
};
const editPatient = (uuid) => {
navTo({ url: `/pages_app/patientSetting/patientSetting?uuid=${uuid}` });
};
const searchPatients = () => {
navTo({ url: '/pages_app/searchPatient/searchPatient' });
};
const addNewPatient = () => {
navTo({ url: '/pages_app/myPatient/myPatient?from=patientMsg' });
};
const managePatientGroups = () => {
navTo({ url: '/pages_app/patientGroup/patientGroup?from=patientMsg' });
};
const goCode = () => {
navTo({ url: '/pages_app/myCode/myCode' });
};
const hideAlert = () => {
showAlert.value = false;
uni.setStorageSync('hasAlert', true);
};
const onPatientItemLongPress = (item) => {
if (!item?.uuid) return;
selectedPatientUuid.value = item.uuid;
selectedPatientName.value = item.nickname || item.realName;
message.value = `确认解除[${selectedPatientName.value || '该患者'}]的随访关系?`;
visible.value = true;
};
const deleteFollowUpFromList = async () => {
if (!selectedPatientUuid.value) {
visible.value = false;
return;
}
const res = await api.cancelRes({ patientUuid: selectedPatientUuid.value });
if (isApiSuccess(res)) {
uni.showToast({ title: '解除成功', icon: 'none' });
await patientListByGBKPage({ showLoading: true, reset: true });
}
visible.value = false;
};
const goBack = () => {
plus.runtime.quit();
};
const goMessageTab = () => {
navTo({ url: '/pages_app/patientMsg/patientMsg' });
};
const goPlanTab = () => {
navTo({ url: '/pages_app/patientMsg/patientPlan' });
};
onBackPress(() => {
plus.runtime.quit();
return true;
});
onLoad(async () => {
perfLog('lifecycle:onLoad');
getApplyList();
void patientListByGBKPage({ showLoading: true, reset: true });
});
onShow(async () => {
perfLog('lifecycle:onShow', { patientDataLoaded: patientDataLoaded.value });
getApplyList();
if (!patientDataLoaded.value) {
void patientListByGBKPage({ showLoading: true, reset: true });
}
});
</script>
<style lang="scss" scoped>
.content {
background-color: #f5f5f5;
height: 100vh;
overflow-y: hidden;
}
.nav-right {
display: flex;
align-items: center;
}
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #f8f8f8;
display: flex;
align-items: center;
border-top: 1rpx solid #e0e0e0;
z-index: 999;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
height: 100%;
}
.tab-item:nth-child(2) {
border: 2rpx solid #ccc;
border-top: none;
border-bottom: none;
}
.tab-item .tab-text {
font-size: 32rpx;
color: #999;
}
.tab-item.active .tab-text {
color: #8B2316;
font-weight: 500;
}
.tab-dot {
position: absolute;
top: 18rpx;
right: 28rpx;
}
.patient-list {
width: 100%;
margin-top: calc(var(--status-bar-height) + 44px);
height: calc(100vh - var(--status-bar-height) - 44px - 103rpx);
overflow: hidden;
background-color: #fff;
display: flex;
flex-direction: column;
}
.listbox {
flex: 1;
min-height: 0;
}
.patient-building {
display: flex;
align-items: center;
justify-content: center;
}
.patient-building-text {
font-size: 28rpx;
color: #999;
}
.groups-scroll {
height: 100%;
}
.load-more {
display: flex;
align-items: center;
justify-content: center;
height: 80rpx;
}
.load-more-text {
font-size: 24rpx;
color: #999;
}
.special-actions {
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.action-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.action-icon {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.new-patient,
.group-icon {
background-color: #8B2316;
}
.grid-icon {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 2rpx;
width: 32rpx;
height: 32rpx;
}
.grid-item {
background-color: #fff;
border-radius: 2rpx;
}
.action-text {
flex: 1;
font-size: 32rpx;
color: #333;
}
.new-patient-count {
color: red;
font-size: 32rpx;
margin-left: 10rpx;
}
.group-header {
padding: 20rpx 30rpx;
background-color: #e4e4e4;
font-size: 36rpx;
color: #666;
}
.patient-item {
display: flex;
align-items: center;
padding: 30rpx 80rpx 20rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
background-color: #fff;
}
.patient-avatar {
width: 90rpx;
height: 90rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.avatar-placeholder {
position: relative;
background: #e0e0e0;
border-radius: 50%;
}
.avatar-placeholder::before {
content: '';
position: absolute;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: #ffffff;
top: 16rpx;
left: 31rpx;
}
.avatar-placeholder::after {
content: '';
position: absolute;
width: 56rpx;
height: 30rpx;
border-radius: 28rpx 28rpx 12rpx 12rpx;
background: #ffffff;
left: 17rpx;
bottom: 14rpx;
}
.patient-info {
flex: 1;
}
.patient-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
.patient-status {
display: flex;
align-items: flex-end;
flex-direction: column;
}
.edit-icon {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
}
.edit-pen {
position: relative;
width: 24rpx;
height: 6rpx;
background: #8B2316;
border-radius: 3rpx;
transform: rotate(-35deg);
}
.edit-pen::before {
content: '';
position: absolute;
left: -8rpx;
top: 0;
width: 0;
height: 0;
border-top: 3rpx solid transparent;
border-bottom: 3rpx solid transparent;
border-right: 8rpx solid #8B2316;
}
.edit-pen::after {
content: '';
position: absolute;
right: -5rpx;
top: 0;
width: 5rpx;
height: 6rpx;
background: #d9d9d9;
border-radius: 0 3rpx 3rpx 0;
}
.follow-date {
margin-top: 8rpx;
font-size: 24rpx;
color: #666;
}
.letter-index {
position: fixed;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
background-color: rgba(255, 255, 255, 0.9);
border-radius: 20rpx;
padding: 10rpx 5rpx;
z-index: 999;
}
.letter-item {
padding: 8rpx 12rpx;
font-size: 24rpx;
color: #666;
text-align: center;
border-radius: 10rpx;
}
.letter-item.active {
color: #8B2316;
font-weight: 600;
}
.alertContent {
z-index: 99;
position: fixed;
bottom: 100rpx;
width: 100%;
top: calc(var(--status-bar-height) + 44px);
pointer-events: none;
}
.alert-img {
width: 100%;
}
.del-img {
position: absolute;
top: 280rpx;
z-index: 999;
right: 100rpx;
width: 48rpx;
height: 48rpx;
pointer-events: auto;
}
</style>
+430
View File
@@ -0,0 +1,430 @@
<template>
<view class="content">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="患者列表压测"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right">
<uni-icons type="search" size="24" color="#8B2316"></uni-icons>
</view>
</template>
</uni-nav-bar>
</view>
<view class="patient-list">
<view class="perf-toolbar">
<view class="perf-btn primary" @click="loadRealData">加载真实数据</view>
<view class="perf-btn" @click="loadRealData(true)">强制刷新</view>
<view class="perf-btn" @click="clearData">清空</view>
</view>
<view class="perf-tip">当前数据量: {{ patientList.length }},分组: {{ patientGroups.length }},当前字母: {{ activeLetter || '-' }}</view>
<view class="listbox patient-building" v-if="patientDataLoading && !loadFinish">
<text class="patient-building-text">正在生成并整理模拟数据...</text>
</view>
<view class="listbox" v-show="patientList.length > 0">
<scroll-view
class="groups-scroll"
scroll-y="true"
:scroll-into-view="scrollIntoViewId"
:scroll-with-animation="true"
@scroll="onGroupsScroll"
v-show="loadFinish"
>
<view class="special-actions">
<view class="action-item">
<view class="action-icon new-patient"></view>
<text class="action-text">新的患者(模拟)</text>
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="group-section" v-for="group in patientGroups" :key="group.letter" :id="`anchor-${group.letter}`">
<view class="group-header">{{ group.letter }}</view>
<view class="patient-item" v-for="item in group.items" :key="item.uuid || item.id">
<view class="patient-avatar avatar-placeholder"></view>
<view class="patient-info">
<text class="patient-name">{{ item.nickname || item.realName }}</text>
</view>
<view class="patient-status">
<text class="follow-date">随访于{{ item.joinDateYMD }}</text>
</view>
</view>
</view>
</scroll-view>
<view class="letter-index" v-if="loadFinish && indexList.length > 0">
<view class="letter-item" v-for="letter in indexList" :key="letter" :class="{ active: activeLetter === letter }" @click="onLetterTap(letter)">
{{ letter }}
</view>
</view>
</view>
<view class="listbox" v-show="patientList.length === 0 && !patientDataLoading">
<empty></empty>
</view>
</view>
</view>
</template>
<script setup>
import { nextTick, ref, shallowRef } from 'vue';
import { onBackPress } from '@dcloudio/uni-app';
import dayjs from 'dayjs';
import pinyin from 'pinyin';
import api from '@/api/api.js';
const patientList = ref([]);
const patientDataLoading = ref(false);
const loadFinish = ref(false);
const firstLetterCache = new Map();
const LETTER_ORDER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const GROUP_BUILD_CHUNK_SIZE = 180;
const indexList = ref([]);
const patientGroups = shallowRef([]);
const allPatientGroups = shallowRef([]);
const activeLetter = ref('');
const scrollIntoViewId = ref('');
const groupScrollRanges = ref([]);
const sortLetter = (a, b) => {
if (a === '#') return 1;
if (b === '#') return -1;
const ai = LETTER_ORDER.indexOf(a);
const bi = LETTER_ORDER.indexOf(b);
if (ai === -1 && bi === -1) return String(a).localeCompare(String(b));
if (ai === -1) return 1;
if (bi === -1) return -1;
return ai - bi;
};
const formatYMD = (input) => {
if (!input) return '';
const d = dayjs(input);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
};
const getFirstLetter = (name = '') => {
const firstChar = String(name).trim().charAt(0);
if (!firstChar) return '#';
if (/^[A-Za-z]$/.test(firstChar)) return firstChar.toUpperCase();
if (firstLetterCache.has(firstChar)) return firstLetterCache.get(firstChar);
try {
const py = pinyin(firstChar, { style: pinyin.STYLE_NORMAL })?.[0]?.[0] || '';
const letter = py ? py.charAt(0).toUpperCase() : '#';
firstLetterCache.set(firstChar, letter);
return letter;
} catch (error) {
return '#';
}
};
const rebuildIndexList = () => {
indexList.value = allPatientGroups.value
.map(g => String(g?.letter || '').toUpperCase())
.filter((letter, index, arr) => (letter === '#' || /^[A-Z]$/.test(letter)) && arr.indexOf(letter) === index)
.sort(sortLetter);
activeLetter.value = indexList.value[0] || '';
};
const onLetterTap = (letter) => {
if (!letter) return;
activeLetter.value = letter;
const targetId = `anchor-${letter}`;
if (scrollIntoViewId.value === targetId) {
scrollIntoViewId.value = '';
nextTick(() => {
scrollIntoViewId.value = targetId;
});
return;
}
scrollIntoViewId.value = targetId;
};
const rpxToPx = (rpx) => {
const { screenWidth } = uni.getSystemInfoSync();
return Math.round((screenWidth / 750) * rpx);
};
const buildGroupScrollRanges = () => {
const actionHeight = rpxToPx(120); // action-item 高度近似
const headerHeight = rpxToPx(80); // group-header 高度近似
const itemHeight = rpxToPx(150); // patient-item 高度近似
let cursor = actionHeight;
groupScrollRanges.value = patientGroups.value.map(group => {
const groupHeight = headerHeight + (group.items?.length || 0) * itemHeight;
const range = {
letter: group.letter,
start: cursor,
end: cursor + groupHeight
};
cursor += groupHeight;
return range;
});
};
const onGroupsScroll = (e) => {
const top = Number(e?.detail?.scrollTop || 0);
if (!groupScrollRanges.value.length) return;
let current = groupScrollRanges.value[0].letter;
for (let i = 0; i < groupScrollRanges.value.length; i++) {
const range = groupScrollRanges.value[i];
if (top >= range.start) {
current = range.letter;
} else {
break;
}
}
activeLetter.value = current;
};
const waitForNextFrame = () => new Promise(resolve => setTimeout(resolve, 0));
const buildGroupsFromPatients = async () => {
const map = new Map();
const source = patientList.value || [];
for (let i = 0; i < source.length; i++) {
const p = source[i] || {};
const normalized = {
...p,
joinDateYMD: formatYMD(p.join_date)
};
const first = getFirstLetter(normalized.nickname || normalized.realName);
const letter = /^[A-Z]$/.test(first) ? first : '#';
if (!map.has(letter)) map.set(letter, []);
map.get(letter).push(normalized);
if (i > 0 && i % GROUP_BUILD_CHUNK_SIZE === 0) {
await waitForNextFrame();
}
}
const letters = Array.from(map.keys()).sort(sortLetter);
allPatientGroups.value = letters.map(l => ({ letter: l, items: map.get(l) }));
patientGroups.value = allPatientGroups.value;
rebuildIndexList();
buildGroupScrollRanges();
};
const loadRealData = async (force = false) => {
if (patientDataLoading.value) return;
if (!force && patientList.value.length > 0) return;
patientDataLoading.value = true;
loadFinish.value = false;
try {
const t1 = Date.now();
const res = await api.patientListByGBK();
if (res?.code !== 1) {
uni.showToast({ title: '获取数据失败', icon: 'none' });
return;
}
patientList.value = res.data || [];
await buildGroupsFromPatients();
loadFinish.value = true;
const t2 = Date.now();
uni.showToast({
title: `加载完成 ${patientList.value.length}条/${t2 - t1}ms`,
icon: 'none',
duration: 1500
});
} finally {
patientDataLoading.value = false;
}
};
const clearData = () => {
patientList.value = [];
patientGroups.value = [];
allPatientGroups.value = [];
indexList.value = [];
groupScrollRanges.value = [];
activeLetter.value = '';
scrollIntoViewId.value = '';
loadFinish.value = false;
};
const goBack = () => {
plus.runtime.quit();
};
onBackPress(() => {
plus.runtime.quit();
return true;
});
</script>
<style lang="scss" scoped>
.content {
background-color: #f5f5f5;
height: 100vh;
overflow-y: hidden;
}
.nav-right {
display: flex;
align-items: center;
}
.patient-list {
width: 100%;
margin-top: calc(var(--status-bar-height) + 44px);
height: calc(100vh - var(--status-bar-height) - 44px);
overflow: hidden;
background-color: #fff;
display: flex;
flex-direction: column;
}
.perf-toolbar {
display: flex;
align-items: center;
padding: 16rpx 20rpx;
border-bottom: 1rpx solid #f0f0f0;
background: #fff;
position: sticky;
top: 0;
z-index: 5;
}
.perf-btn {
padding: 10rpx 16rpx;
background: #f2f2f2;
border-radius: 8rpx;
font-size: 24rpx;
color: #555;
margin-right: 12rpx;
}
.perf-btn.primary {
background: #8B2316;
color: #fff;
}
.perf-tip {
font-size: 24rpx;
color: #999;
padding: 8rpx 20rpx 14rpx;
}
.listbox {
flex: 1;
min-height: 0;
}
.patient-building {
display: flex;
align-items: center;
justify-content: center;
}
.patient-building-text {
font-size: 28rpx;
color: #999;
margin-top: 60rpx;
}
.groups-scroll {
height: 100%;
}
.special-actions {
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.action-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.action-icon {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.new-patient {
background-color: #8B2316;
}
.action-text {
flex: 1;
font-size: 32rpx;
color: #333;
}
.group-header {
padding: 20rpx 30rpx;
background-color: #e4e4e4;
font-size: 36rpx;
color: #666;
}
.patient-item {
display: flex;
align-items: center;
padding: 30rpx 80rpx 20rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
background-color: #fff;
}
.patient-avatar {
width: 90rpx;
height: 90rpx;
border-radius: 10rpx;
margin-right: 20rpx;
}
.avatar-placeholder {
position: relative;
background: #e0e0e0;
border-radius: 50%;
}
.avatar-placeholder::before {
content: '';
position: absolute;
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: #ffffff;
top: 16rpx;
left: 31rpx;
}
.avatar-placeholder::after {
content: '';
position: absolute;
width: 56rpx;
height: 30rpx;
border-radius: 28rpx 28rpx 12rpx 12rpx;
background: #ffffff;
left: 17rpx;
bottom: 14rpx;
}
.patient-info {
flex: 1;
}
.patient-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
}
.patient-status {
display: flex;
align-items: flex-end;
flex-direction: column;
}
.follow-date {
margin-top: 8rpx;
font-size: 24rpx;
color: #666;
}
.letter-index {
position: fixed;
right: 10rpx;
top: 50%;
transform: translateY(-50%);
background-color: rgba(255, 255, 255, 0.9);
border-radius: 20rpx;
padding: 10rpx 5rpx;
z-index: 999;
}
.letter-item {
padding: 8rpx 12rpx;
font-size: 24rpx;
color: #666;
text-align: center;
border-radius: 10rpx;
}
.letter-item.active {
color: #8B2316;
font-weight: 600;
}
</style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,481 @@
<template>
<view class="content">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="患者消息"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right">
<uni-icons type="search" size="24" color="#8B2316" @click="searchPatients"></uni-icons>
<uni-icons type="staff" size="24" color="#8B2316" @click="managePatients" style="margin-left: 30rpx;"></uni-icons>
</view>
</template>
</uni-nav-bar>
</view>
<scroll-view
class="message-list"
scroll-y="true"
refresher-enabled="true"
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
>
<empty></empty>
</scroll-view>
<view class="tab-bar">
<view class="tab-item active">
<text class="tab-text">患者消息</text>
</view>
<view class="tab-item" @click="goListTab">
<text class="tab-text">患者列表</text>
<view class="tab-dot" v-if="hasNewPatient">
<uni-badge class="uni-badge-left-margin" :text="applyList.length" :offset="[-3, -3]" size="small"></uni-badge>
</view>
</view>
<view class="tab-item" @click="goPlanTab">
<text class="tab-text">随访计划</text>
</view>
</view>
</view>
</template>
<script setup>
<<<<<<< HEAD
import { computed, ref } from 'vue';
import { onBackPress, onLoad, onShow } from '@dcloudio/uni-app';
import api from '@/api/api.js';
import navTo from '@/utils/navTo.js';
=======
import { ref, getCurrentInstance, computed, nextTick, shallowRef } from 'vue';
import { onShow,onLoad,onBackPress,onHide} from "@dcloudio/uni-app";
import dayImg from "@/static/visit_data11.png"
import planImg from "@/static/visitplan.png"
import api from '@/api/api.js';
import navTo from '@/utils/navTo.js';
import docUrl from '@/utils/docUrl.js';
import unidialog from '@/components/dialog/dialog.vue'
import defaultImg from "@/static/default.png"
import newPatientImg from "@/static/new_patient.png"
import delImg from "@/static/iv_delete.png"
import editImg from "@/static/edit_patitent.png"
const patientList = ref([]);
const visible = ref(false)
const message = ref('')
const selectedPatientUuid = ref('')
const selectedPatientName = ref('')
const patientDataLoaded = ref(false);
const patientDataLoading = ref(false);
const firstLetterCache = new Map();
const showAlert = ref(false);
const navHeight = ref(40);
onBackPress(() => {
plus.runtime.quit();
return true;
});
onHide(() => {
patientDataLoaded.value = false;
});
import alertImg from "@/static/patientgif.png"
const getNavHeight = () => {
const systemInfo = uni.getSystemInfoSync();
console.log(2223);
console.log(systemInfo);
navHeight.value = systemInfo.statusBarHeight + 44;
console.log(navHeight.value);
}
import pinyin from 'pinyin';
import dayjs from 'dayjs'
import lineImg from "@/static/item_visitplan_fg.png"
const loadFinish = ref(false);
//import ConversationList from './conversation-list/index.vue'
const title = ref('患者消息');
const goPatientDetail = (uuid) => {
navTo({
url: `/pages_app/patientDetail/patientDetail?uuid=${uuid}&from=patientMsg`
})
}
const editPatient = (uuid) => {
console.log(uuid)
navTo({
url: `/pages_app/patientSetting/patientSetting?uuid=${uuid}?from=patientMsg`
})
}
const onPatientItemLongPress = (item) => {
if (!item || !item.uuid) return
selectedPatientUuid.value = item.uuid
selectedPatientName.value = item.nickname || item.realName
message.value = `确认解除[${selectedPatientName.value || '该患者'}]的随访关系?`
visible.value = true
}
const deleteFollowUpFromList = async () => {
if (!selectedPatientUuid.value) {
visible.value = false
return
}
const res = await api.cancelRes({
patientUuid: selectedPatientUuid.value
})
if (res.code == 1) {
uni.showToast({
title: '解除成功',
icon: 'none'
})
patientListByGBK()
}
visible.value = false
}
const avatarErrorMap = ref({});
const getAvatarKey = (item = {}) => String(item.uuid || item.id || item.realName || item.nickname || '');
const normalizeAvatarUrl = (photo) => {
const raw = String(photo || '').trim();
if (!raw) return defaultImg;
if (/^https?:\/\//i.test(raw)) return raw;
return `${docUrl}${raw}`;
};
const getPatientAvatar = (item = {}) => {
const key = getAvatarKey(item);
if (key && avatarErrorMap.value[key]) return defaultImg;
return item.avatarUrl || defaultImg;
};
const handlePatientAvatarError = (item) => {
if (!item) return;
const key = getAvatarKey(item);
if (!key) return;
avatarErrorMap.value = {
...avatarErrorMap.value,
[key]: true
};
}
const goCode = () => {
navTo({
url: `/pages_app/myCode/myCode?from=patientMsg`
})
}
// 仅保留年月日
const formatYMD = (input) => {
if (!input) return '';
const d = dayjs(input);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
}
const formatYearMonth = (input) => {
if (!input) return '';
const d = dayjs(input);
return d.isValid() ? d.format('YYYY年MM月') : '';
}
// 格式化显示日(如:03)
const formatDay = (input) => {
if (!input) return '';
const d = dayjs(input);
return d.isValid() ? d.format('DD日') : '';
}
// 申请列表数据
const messageList = ref([
{
id: 1,
patientName: '测试',
messagePreview: '[图片]',
time: '2025-08-11',
avatar: ''
}
]);
// 当前激活的标签
const activeTab = ref('message');
const activeLetter = ref('A');
const scrollIntoViewId = ref('');
// 控制添加菜单弹窗显示
const showAddMenuFlag = ref(false);
// 组件实例(用于 selectorQuery 作用域)
const { proxy } = getCurrentInstance();
const LETTER_ORDER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const sortLetter = (a, b) => {
if (a === '#') return 1;
if (b === '#') return -1;
const ai = LETTER_ORDER.indexOf(a);
const bi = LETTER_ORDER.indexOf(b);
if (ai === -1 && bi === -1) return String(a).localeCompare(String(b));
if (ai === -1) return 1;
if (bi === -1) return -1;
return ai - bi;
};
// up-index-list 索引使用实际分组字母,避免索引与锚点数量不一致
const indexList = ref([]);
// 列表高度(像素):窗口高度 - 顶部导航(rpx转px) - 底部tab(约100rpx->px) - 操作区(约200rpx->px)
const groupsListHeight = ref(0);
const rpxToPx = (rpx) => {
const { screenWidth } = uni.getSystemInfoSync();
return Math.round((screenWidth / 750) * rpx);
};
const computeListHeight = () => {
const { windowHeight } = uni.getSystemInfoSync();
const reserved = rpxToPx(240); // 140rpx(nav) + 100rpx(tab) 近似
groupsListHeight.value = Math.max(0, windowHeight - reserved);
};
>>>>>>> cbc427e41ce2bf41f4bd01d9206215c015e257ae
const applyList = ref([]);
const isRefreshing = ref(false);
const hasNewPatient = computed(() => applyList.value.length > 0);
const getApplyList = async () => {
try {
const res = await api.applyList();
if (res && res.code === 200) {
applyList.value = res.data || [];
}
} catch (error) {}
};
const onRefresh = async () => {
isRefreshing.value = true;
try {
await getApplyList();
uni.showToast({
title: '刷新成功',
icon: 'none',
duration: 1200
});
<<<<<<< HEAD
} finally {
isRefreshing.value = false;
=======
};
// 随访计划上拉加载
const onFollowUpLoadMore = async () => {
if (!followUpHasMore.value || followUpLoading.value) return;
await getFollowUpList(false);
};
// 搜索患者
const searchPatients = () => {
navTo({
url: `/pages_app/searchPatient/searchPatient?from=patientMsg`
})
};
// 管理患者
const managePatients = () => {
uni.showToast({
title: '患者管理',
icon: 'none'
});
};
// 打开消息
const openMessage = (item) => {
uni.showToast({
title: `打开患者 ${item?.patientName || '未知'} 的消息`,
icon: 'none'
});
// 这里可以跳转到消息详情页面
// uni.navigateTo({
// url: `/pages_app/messageDetail/messageDetail?id=${item?.id}`
// });
};
// 切换标签
const switchTab = (tab) => {
activeTab.value = tab;
switch(tab) {
case 'message':
// 患者消息页面逻辑 - 刷新申请列表
uni.sendNativeEvent('goPatientMsg', {
msg: 'goPatientMsg'
},ret => {
console.log(ret);
})
// getApplyList();
// title.value = '患者消息';
break;
case 'list':
title.value = '患者列表';
if (patientDataLoaded.value) {
loadFinish.value = true;
break;
}
uni.showLoading({
title: '加载中...',
mask: true
});
patientListByGBK();
// 显示患者列表
break;
case 'plan':
title.value = '随访计划';
// 随访计划页面 - 加载随访计划数据
if (followUpList.value.length === 0) {
getFollowUpList(true);
}
break;
}
};
// 添加新患者
const addNewPatient = () => {
navTo({
url: '/pages_app/myPatient/myPatient?from=patientMsg'
})
};
// 管理患者分组
const managePatientGroups = () => {
navTo({
url: '/pages_app/patientGroup/patientGroup?from=patientMsg'
})
};
// 打开患者详情
const openPatient = (patientName) => {
uni.showToast({
title: `打开患者: ${patientName}`,
icon: 'none'
});
};
// 添加随访计划
const addFollowUpPlan = () => {
showAddMenuFlag.value = false;
uni.navigateTo({
url: '/pages_app/visit/visit?from=patientMsg'
});
};
const goBack = () => {
plus.runtime.quit();
>>>>>>> cbc427e41ce2bf41f4bd01d9206215c015e257ae
}
};
const searchPatients = () => {
navTo({
url: '/pages_app/searchPatient/searchPatient'
});
};
const managePatients = () => {
uni.showToast({
title: '患者管理',
icon: 'none'
});
};
const goListTab = () => {
navTo({
url: '/pages_app/patientMsg/patientList'
});
};
const goPlanTab = () => {
navTo({
url: '/pages_app/patientMsg/patientPlan'
});
};
const goBack = () => {
plus.runtime.quit();
};
const notifyNativePatientMsg = () => {
uni.sendNativeEvent('goPatientMsg', { msg: 'goPatientMsg' }, () => {});
};
onBackPress(() => {
plus.runtime.quit();
return true;
});
onLoad((options) => {
if (options.type === 'list') {
goListTab();
return;
}
if (options.type === 'plan') {
goPlanTab();
return;
}
getApplyList();
notifyNativePatientMsg();
});
onShow(() => {
getApplyList();
notifyNativePatientMsg();
});
</script>
<style lang="scss" scoped>
.content {
background-color: #f5f5f5;
height: 100vh;
overflow-y: hidden;
}
.nav-right {
display: flex;
align-items: center;
}
.message-list {
margin-top: calc(var(--status-bar-height) + 44px);
height: calc(100vh - var(--status-bar-height) - 44px - 103rpx);
background: #fff;
}
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #f8f8f8;
display: flex;
align-items: center;
border-top: 1rpx solid #e0e0e0;
z-index: 999;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
height: 100%;
}
.tab-item:nth-child(2) {
border: 2rpx solid #ccc;
border-top: none;
border-bottom: none;
}
.tab-text {
font-size: 32rpx;
color: #999;
}
.tab-item.active .tab-text {
color: #8B2316;
font-weight: 500;
}
.tab-dot {
position: absolute;
top: 18rpx;
right: 28rpx;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,201 @@
<template>
<view class="content">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="患者消息"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right">
<uni-icons type="search" size="24" color="#8B2316" @click="searchPatients"></uni-icons>
<uni-icons type="staff" size="24" color="#8B2316" @click="managePatients" style="margin-left: 30rpx;"></uni-icons>
</view>
</template>
</uni-nav-bar>
</view>
<scroll-view
class="message-list"
scroll-y="true"
refresher-enabled="true"
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
>
<empty></empty>
</scroll-view>
<view class="tab-bar">
<view class="tab-item active">
<text class="tab-text">患者消息</text>
</view>
<view class="tab-item" @click="goListTab">
<text class="tab-text">患者列表</text>
<view class="tab-dot" v-if="hasNewPatient">
<uni-badge class="uni-badge-left-margin" :text="applyList.length" :offset="[-3, -3]" size="small"></uni-badge>
</view>
</view>
<view class="tab-item" @click="goPlanTab">
<text class="tab-text">随访计划</text>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue';
import { onBackPress, onLoad, onShow } from '@dcloudio/uni-app';
import api from '@/api/api.js';
import navTo from '@/utils/navTo.js';
const applyList = ref([]);
const isRefreshing = ref(false);
const hasNewPatient = computed(() => applyList.value.length > 0);
const getApplyList = async () => {
try {
const res = await api.applyList();
if (res && res.code === 200) {
applyList.value = res.data || [];
}
} catch (error) {}
};
const onRefresh = async () => {
isRefreshing.value = true;
try {
await getApplyList();
uni.showToast({
title: '刷新成功',
icon: 'none',
duration: 1200
});
} finally {
isRefreshing.value = false;
}
};
const searchPatients = () => {
navTo({
url: '/pages_app/searchPatient/searchPatient'
});
};
const managePatients = () => {
uni.showToast({
title: '患者管理',
icon: 'none'
});
};
const goListTab = () => {
navTo({
url: '/pages_app/patientMsg/patientList'
});
};
const goPlanTab = () => {
navTo({
url: '/pages_app/patientMsg/patientPlan'
});
};
const goBack = () => {
plus.runtime.quit();
};
const notifyNativePatientMsg = () => {
uni.sendNativeEvent('goPatientMsg', { msg: 'goPatientMsg' }, () => {});
};
onBackPress(() => {
plus.runtime.quit();
return true;
});
onLoad((options) => {
if (options.type === 'list') {
goListTab();
return;
}
if (options.type === 'plan') {
goPlanTab();
return;
}
getApplyList();
notifyNativePatientMsg();
});
onShow(() => {
getApplyList();
notifyNativePatientMsg();
});
</script>
<style lang="scss" scoped>
.content {
background-color: #f5f5f5;
height: 100vh;
overflow-y: hidden;
}
.nav-right {
display: flex;
align-items: center;
}
.message-list {
margin-top: calc(var(--status-bar-height) + 44px);
height: calc(100vh - var(--status-bar-height) - 44px - 103rpx);
background: #fff;
}
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #f8f8f8;
display: flex;
align-items: center;
border-top: 1rpx solid #e0e0e0;
z-index: 999;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
height: 100%;
}
.tab-item:nth-child(2) {
border: 2rpx solid #ccc;
border-top: none;
border-bottom: none;
}
.tab-text {
font-size: 32rpx;
color: #999;
}
.tab-item.active .tab-text {
color: #8B2316;
font-weight: 500;
}
.tab-dot {
position: absolute;
top: 18rpx;
right: 28rpx;
}
</style>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+378
View File
@@ -0,0 +1,378 @@
<template>
<view class="content">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="随访计划"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right" @click="showAddMenu">
<view class="save-btn">添加</view>
</view>
</template>
</uni-nav-bar>
</view>
<view class="plan">
<scroll-view
class="plan-scroll"
scroll-y="true"
refresher-enabled="true"
:refresher-triggered="followUpRefreshing"
@refresherrefresh="onFollowUpRefresh"
@scrolltolower="onFollowUpLoadMore"
:lower-threshold="100"
>
<view class="plan-list" v-if="groupedFollowUpList.length > 0">
<view class="plan-group" v-for="group in groupedFollowUpList" :key="group.yearMonth">
<view class="group-header">{{ group.yearMonth }}</view>
<view class="plan-card" v-for="item in group.items" :key="item.uuid" @click="goFollowDetail(item)">
<view class="left-rail">
<text class="day">{{ formatDay(item.datetime) }}</text>
</view>
<view class="linebox">
<up-image :src="lineImg" width="14rpx" height="180rpx"></up-image>
</view>
<view class="right-content">
<view class="leftcontent">
<view class="note">{{ item.note }}</view>
<view class="name">{{ item.patientname }}</view>
</view>
<uni-icons type="forward" size="20" color="#999"></uni-icons>
</view>
</view>
</view>
<view class="load-more" v-if="followUpLoading">
<uni-icons type="spinner-cycle" size="20" color="#999"></uni-icons>
<text class="load-text">加载中...</text>
</view>
<view class="no-more" v-if="!followUpHasMore && followUpList.length > 0">
<text class="no-more-text">没有更多数据了</text>
</view>
</view>
<empty v-else></empty>
</scroll-view>
<view class="add-menu-popup" v-if="showAddMenuFlag" @click="hideAddMenu">
<view class="menu-content" @click.stop>
<view class="menu-item" @click="addSchedule">
<up-image :src="dayImg" width="34rpx" height="34rpx"></up-image>
<text class="menu-text">添加日程</text>
</view>
<view class="menu-divider"></view>
<view class="menu-item" @click="addFollowUpPlan">
<up-image :src="planImg" width="34rpx" height="34rpx"></up-image>
<text class="menu-text">添加随访计划</text>
</view>
</view>
</view>
</view>
<view class="tab-bar">
<view class="tab-item" @click="goMessageTab">
<text class="tab-text">患者消息</text>
</view>
<view class="tab-item" @click="goListTab">
<text class="tab-text">患者列表</text>
<view class="tab-dot" v-if="hasNewPatient"><uni-badge class="uni-badge-left-margin" :text="applyList.length" :offset="[-3, -3]" size="small" /></view>
</view>
<view class="tab-item active">
<text class="tab-text">随访计划</text>
</view>
</view>
</view>
</template>
<script setup>
import { computed, ref } from 'vue';
import { onBackPress, onLoad, onShow } from '@dcloudio/uni-app';
import dayjs from 'dayjs';
import api from '@/api/api.js';
import navTo from '@/utils/navTo.js';
import dayImg from '@/static/visit_data11.png';
import planImg from '@/static/visitplan.png';
import lineImg from '@/static/item_visitplan_fg.png';
const applyList = ref([]);
const page = ref(1);
const followUpList = ref([]);
const followUpLoading = ref(false);
const followUpRefreshing = ref(false);
const followUpHasMore = ref(true);
const followUpPageSize = ref(10);
const showAddMenuFlag = ref(false);
const hasNewPatient = computed(() => applyList.value.length > 0);
const formatYearMonth = (input) => {
if (!input) return '';
const d = dayjs(input);
return d.isValid() ? d.format('YYYY年MM月') : '';
};
const formatDay = (input) => {
if (!input) return '';
const d = dayjs(input);
return d.isValid() ? d.format('DD日') : '';
};
const groupedFollowUpList = computed(() => {
if (!followUpList.value.length) return [];
const groups = new Map();
followUpList.value.forEach(item => {
const yearMonth = formatYearMonth(item.datetime);
if (!groups.has(yearMonth)) groups.set(yearMonth, []);
groups.get(yearMonth).push(item);
});
return Array.from(groups.entries())
.map(([yearMonth, items]) => ({
yearMonth,
items: items.sort((a, b) => new Date(a.datetime) - new Date(b.datetime))
}))
.sort((a, b) => new Date(a.items[0].datetime) - new Date(b.items[0].datetime));
});
const getApplyList = async () => {
try {
const res = await api.applyList();
if (res && res.code === 200) {
applyList.value = res.data || [];
}
} catch (error) {}
};
const getFollowUpList = async (isRefresh = false) => {
if (followUpLoading.value) return;
followUpLoading.value = true;
try {
const currentPage = isRefresh ? 1 : page.value;
const res = await api.followUpList({
page: currentPage,
pageSize: followUpPageSize.value
});
if (res.code === 200) {
const newData = res.data.list || [];
if (isRefresh) {
followUpList.value = newData;
page.value = 1;
} else {
followUpList.value = [...followUpList.value, ...newData];
}
followUpHasMore.value = newData.length >= followUpPageSize.value;
if (!isRefresh) page.value++;
}
} finally {
followUpLoading.value = false;
followUpRefreshing.value = false;
}
};
const onFollowUpRefresh = async () => {
followUpRefreshing.value = true;
await getFollowUpList(true);
uni.showToast({ title: '刷新成功', icon: 'none', duration: 1200 });
};
const onFollowUpLoadMore = async () => {
if (!followUpHasMore.value || followUpLoading.value) return;
await getFollowUpList(false);
};
const addFollowUpPlan = () => {
showAddMenuFlag.value = false;
navTo({ url: '/pages_app/visit/visit?from=patientMsg' });
};
const addSchedule = () => {
showAddMenuFlag.value = false;
navTo({ url: '/pages_app/schedule/schedule' });
};
const showAddMenu = () => {
showAddMenuFlag.value = true;
};
const hideAddMenu = () => {
showAddMenuFlag.value = false;
};
const goFollowDetail = (raw) => {
if (!raw) return;
navTo({
url: `/pages_app/followDetail/followDetail?followUpUuid=${encodeURIComponent(raw.uuid || '')}&patient_name=${raw.patientname}`
});
};
const goBack = () => {
plus.runtime.quit();
};
const goMessageTab = () => {
navTo({ url: '/pages_app/patientMsg/patientMsg' });
};
const goListTab = () => {
navTo({ url: '/pages_app/patientMsg/patientList' });
};
onBackPress(() => {
plus.runtime.quit();
return true;
});
onLoad(() => {
getApplyList();
getFollowUpList(true);
});
onShow(() => {
getApplyList();
});
</script>
<style lang="scss" scoped>
.content {
background-color: #f5f5f5;
height: 100vh;
overflow-y: hidden;
}
.save-btn {
font-size: 32rpx;
color: #8b2316;
font-weight: 500;
}
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #f8f8f8;
display: flex;
align-items: center;
border-top: 1rpx solid #e0e0e0;
z-index: 999;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
height: 100%;
}
.tab-item:nth-child(2) {
border: 2rpx solid #ccc;
border-top: none;
border-bottom: none;
}
.tab-item .tab-text {
font-size: 32rpx;
color: #999;
}
.tab-item.active .tab-text {
color: #8B2316;
font-weight: 500;
}
.tab-dot {
position: absolute;
top: 18rpx;
right: 28rpx;
}
.plan {
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
bottom: 110rpx;
}
.plan-scroll {
height: 100%;
}
.plan-list {
padding-bottom: 20rpx;
}
.plan-group {
background: #f5f5f5;
}
.group-header {
text-align: left;
background: #eee;
color: #333;
font-size: 30rpx;
padding: 20rpx 30rpx;
}
.plan-card {
display: flex;
align-items: center;
background: #fff;
padding: 26rpx 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.linebox {
margin: 0 20rpx;
}
.left-rail .day {
font-size: 36rpx;
color: #8B2316;
}
.right-content {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
}
.note {
font-size: 30rpx;
color: #333;
}
.name {
margin-top: 30rpx;
font-size: 28rpx;
color: #8B2316;
}
.load-more,
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx;
}
.load-text,
.no-more-text {
font-size: 28rpx;
color: #999;
}
.add-menu-popup {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.menu-content {
background-color: #fff;
border-radius: 20rpx;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.2);
width: 80%;
max-width: 400rpx;
overflow: hidden;
position: fixed;
top: calc(var(--status-bar-height) + 48px);
right: 20rpx;
}
.menu-item {
display: flex;
align-items: center;
padding: 30rpx 40rpx;
}
.menu-text {
font-size: 32rpx;
color: #333;
margin-left: 20rpx;
}
.menu-divider {
height: 1rpx;
background-color: #f0f0f0;
margin: 0 40rpx;
}
</style>
+86 -17
View File
@@ -1,39 +1,53 @@
<template>
<view class="remark-page">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="设置备注和分组"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
/>
</view>
<view class="contentbox">
<view class="form-block">
<view class="label">备注</view>
<input class="input" v-model.trim="remark" :placeholder="groupInfo.nickname || '给患者添加备注名'" placeholder-class="ph" maxlength="20"/>
<input class="input" v-model.trim="remark" :placeholder="groupInfo.nickname || '给患者添加备注名'" placeholder-class="ph" maxlength="20" />
</view>
<view class="row" v-if="nickname">
<view class="left">申请消息为:我是{{ nickname }}</view>
<view class="right" @click="fillNickname">
填入
</view>
</view>
<view class="form-block" @click="goGroup">
<view class="label">分组</view>
<view class="label" @click.stop="goGroup">分组</view>
<view class="iptbox readonly">
<input class="input readonly-input" v-model.trim="groupName" :placeholder="groupInfo.name || '通过分组给患者分类'" placeholder-class="ph" maxlength="20" readonly disabled/>
<uni-icons type="right" size="20" color="#666"></uni-icons>
<view class="input readonly-input" :class="{'ph':!groupInfo.name}" @click.stop="goGroup">{{groupInfo.name || '通过分组给患者分类'}}</view>
<uni-icons type="right" size="20" color="#666" @click.stop="goGroup"></uni-icons>
</view>
</view>
<view class="form-block">
<view class="label">描述</view>
<textarea class="textarea" v-model.trim="note" :placeholder="groupInfo.note || '补充患者关键信息,方便随访患者'" placeholder-class="ph" auto-height maxlength="140"/>
<view class="textarea-wrap">
<textarea class="textarea" v-model.trim="note" :placeholder="groupInfo.note || '补充患者关键信息,方便随访患者'" placeholder-class="ph" auto-height maxlength="100" :adjust-position="false"/>
<view class="textarea-count">已输入{{ noteLength }}/100</view>
</view>
</view>
</view>
<view class="bottom-bar">
<button class="save-btn" @click="updateNicknameNote">保存</button>
<button class="save-btn" :class="{ disabled: isRemarkEmpty }" :disabled="isRemarkEmpty" @click="updateNicknameNote">完成</button>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { ref, computed } from 'vue'
import api from '@/api/api.js'
import navTo from '@/utils/navTo.js'
import { onLoad, onShow } from '@dcloudio/uni-app'
@@ -43,6 +57,10 @@
const patientUuid = ref('')
const patientCardData = ref({})
const groupInfo = ref({})
const noteLength = computed(() => (note.value || '').length)
const isRemarkEmpty = computed(() => !(remark.value || '').trim())
const nickname = ref('')
const tempName=ref('')
const goBack = () => {
uni.navigateBack()
@@ -55,6 +73,9 @@
onLoad((options) => {
patientUuid.value = options.uuid || ''
if(options.nickname){
nickname.value = options.nickname
}
})
onShow(() => {
@@ -68,13 +89,14 @@
patientCardData.value = res
groupInfo.value = res.group || {}
// 初始化表单数据
remark.value = res.group?.nickname || ''
remark.value = res.group?.nickname || remark.value
note.value = res.group?.note || ''
groupName.value = res.group?.name || ''
}
})
}
const updateNicknameNote = async() => {
if (isRemarkEmpty.value) return
const res = await api.updateNicknameNote({
patient_uuid: patientUuid.value,
nickname: remark.value,
@@ -95,13 +117,41 @@
uni.showToast({ title: '保存成功', icon: 'none' })
setTimeout(() => goBack(), 700)
}
const fillNickname = () => {
remark.value = nickname.value
}
</script>
<style lang="scss" scoped>
.row{
margin-top: -20rpx;
display: flex;
background: #fff;
padding:0 30rpx;
align-items: center;
justify-content: space-between;
.left{
font-size: 28rpx;
color: #666;
}
.right{
font-size: 32rpx;
color: #3ec7c0;
}
}
.contentbox{
top: calc(var(--status-bar-height) + 44px);
position: fixed;
left: 0;
right: 0;
bottom: 140rpx;
overflow-y: scroll;
z-index: 1;
}
.remark-page{
min-height: 100vh;
height: 100vh;
background: #f7f7f7;
padding-bottom: 140rpx;
}
.form-block{
.iptbox{
@@ -130,18 +180,33 @@
color: #333;
&.readonly-input {
background: transparent;
color: #999;
color: #333;
}
&.ph{
color: #808080;
}
}
.textarea{
background: #f8f8f8;
border-radius: 12rpx;
padding: 24rpx;
padding: 24rpx 24rpx 58rpx;
font-size: 28rpx;
color: #333;
min-height: 180rpx;
min-height: 250rpx;
box-sizing: border-box;
width: 100%;
line-height: 1.6;
}
.textarea-wrap{
position: relative;
}
.textarea-count{
position: absolute;
right: 20rpx;
bottom: 16rpx;
font-size: 24rpx;
color: #999;
}
.ph{
color: #bfbfbf;
}
@@ -151,18 +216,22 @@
left: 0; right: 0; bottom: 0;
background: #ffffff;
border-top: 1rpx solid #f0f0f0;
padding: 20rpx 24rpx env(safe-area-inset-bottom);
padding: 20rpx 24rpx 20rpx;
.save-btn{
width: 100%;
display: flex;
align-items: center;
justify-content: center;
height: 92rpx;
background: #8B2316;
background: #3ec7c0;
color: #fff;
border: none;
border-radius: 12rpx;
font-size: 32rpx;
&.disabled{
background: #d9d9d9;
color: #ffffff;
}
}
}
</style>
+60 -18
View File
@@ -1,6 +1,7 @@
<template>
<view class="setting-page">
<uni-nav-bar
<navBar :title="'常用设置'"></navBar>
<!-- <uni-nav-bar
left-icon="left"
title="常用设置"
@clickLeft="goBack"
@@ -11,7 +12,7 @@
backgroundColor="#eee"
>
</uni-nav-bar>
</uni-nav-bar> -->
<view class="list-block">
<view class="cell" @click="goRemark">
@@ -22,7 +23,7 @@
</view>
</view>
<view class="cell" @click="goGroup">
<view class="cell" @click="goRemark">
<text class="cell-left">设置分组</text>
<view class="cell-right">
<text class="cell-desc">{{ groupInfo.name || '通过分组给患者分类' }}</text>
@@ -52,11 +53,12 @@
<uni-icons type="right" size="20" color="#999"></uni-icons>
</view>
</view>
<view class="danger-action">
<button class="danger-btn" @click="visible=true">解除随访</button>
</view>
</view>
<view class="danger-block">
<text class="danger-text" @click="visible=true">解除随访</text>
</view>
</view>
<unidialog :visible="visible" :content="message" @close="visible=false" @confirm="deleteFollowUp" ></unidialog>
</template>
@@ -64,7 +66,8 @@
<script setup>
import { ref } from 'vue'
import navTo from '@/utils/navTo.js'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { onLoad, onShow,onBackPress} from '@dcloudio/uni-app'
import navBar from '@/components/navBar/navBar.vue'
import api from '@/api/api.js'
import unidialog from '@/components/dialog/dialog.vue'
const patientUuid = ref('')
@@ -73,9 +76,20 @@
const groupInfo = ref({})
const visible = ref(false)
const message = ref('')
const from = ref('');
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
const goBack = () => {
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
}
const goRemark = () => {
navTo({ url: '/pages_app/patientRemark/patientRemark?uuid=' + patientUuid.value })
}
@@ -85,6 +99,9 @@
}
onLoad((options) => {
patientUuid.value = options.uuid;
if(options.from){
from.value = options.from;
}
console.log(patientUuid.value)
})
onShow(() => {
@@ -106,8 +123,10 @@
patient_uuid: patientUuid.value
}).then(res => {
if(res.code == 200){
patientName.value = res.group.nickname || res.patient.realName
message.value = `解除[${patientName.value}]的随访关系?`
console.log(res.patient)
patientName.value = res.patient.nickname?res.patient.nickname:res.patient.realname;
console.log(patientName.value)
message.value = `确认解除[${patientName.value}]的随访关系?`
patientCardData.value = res
groupInfo.value = res.group || {}
}
@@ -122,9 +141,9 @@
title: '解除成功',
icon: 'none'
})
uni.redirectTo({
url: '/pages_app/patientMsg/patientMsg'
})
setTimeout(() => {
uni.navigateBack()
}, 1000)
}
visible.value = false
})
@@ -137,7 +156,11 @@
background:#f7f7f7;
}
.list-block{
margin-top: 20rpx;
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
bottom: 0rpx;
background: #fff;
.cell{
display: flex;
@@ -154,22 +177,41 @@
.cell-right{
display: flex;
align-items: center;
gap: 16rpx;
justify-content: flex-end;
flex: 1;
margin-left: 20rpx;
min-width: 0;
.cell-desc{
flex: 1;
min-width: 0;
display: block;
font-size: 26rpx;
color: #999;
overflow: hidden;
margin-right: 16rpx;
text-overflow: ellipsis;
white-space: nowrap;
text-align: right;
}
}
}
}
.danger-block{
.danger-action{
margin-top: 30rpx;
background: #fff;
padding: 40rpx 0;
padding: 20rpx 30rpx calc(20rpx + env(safe-area-inset-bottom));
display: flex;
justify-content: center;
.align-center{ align-items: center; }
.danger-text{
.danger-btn{
width: 100%;
height: 92rpx;
line-height: 92rpx;
border: 1rpx solid #8B2316;
background: #fff;
border-radius: 12rpx;
color: #8B2316;
font-size: 32rpx;
}
+163 -33
View File
@@ -1,5 +1,25 @@
<template>
<navBar title="患教视频" />
<!-- <navBar title="患教视频" /> -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="患教视频"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eeeeee"
>
<template #right>
<view class="nav-actions">
<view class="collect-img" @click="goSearch">
<image class="img-icon" :src="searchImg" mode="aspectFill" />
</view>
</view>
</template>
</uni-nav-bar>
</view>
<view class="video-page">
<!-- Header -->
@@ -12,7 +32,7 @@
@click="showAllVideoPopup=!showAllVideoPopup"
>
<up-image :src="isAllActive?allOnImg:allImg" width="30rpx" height="30rpx" ></up-image>
<text class="tab-text" :class="{active:isAllActive}">{{typeName}}</text>
<text class="tab-text" :class="{active:isAllActive}" style="margin-top:4rpx;">{{formatName(typeName)}}</text>
<up-image :src="isAllActive?selectOnImg:selectImg" width="30rpx" height="30rpx" ></up-image>
</view>
<view class="right">
@@ -25,6 +45,7 @@
<up-image :src="sort==2?upImg:downImg" width="20rpx" height="26rpx" ></up-image>
</view>
</view>
<view class="bar"></view>
<view
class="tab-item"
@click="showFilterPopup"
@@ -58,7 +79,32 @@
@click="playVideo(video)"
>
<view class="video-thumbnail">
<image :src="docUrl + video.imgpath" mode="aspectFill"></image>
<up-image
:src="docUrl + video.imgpath"
class="poster-image"
mode="aspectFill"
:loadingIcon="lazyImg"
:errorIcon="lazyImg"
:lazy-load="true"
width="100%"
height="220rpx"
>
<template #error>
<image
:src="lazyImg"
class="video-thumbnail"
mode="aspectFill"
></image>
</template>
<template v-slot:loading>
<image
:src="lazyImg"
class="video-thumbnail"
mode="aspectFill"
></image>
</template>
</up-image>
<!-- <image :src="docUrl + video.imgpath" mode="aspectFill"></image> -->
</view>
<view class="video-info">
<view class="video-title">{{video.title || video.name}}</view>
@@ -126,7 +172,7 @@
:key="item.uuid"
@click="selectCategory(item.uuid)"
>
{{ item.name }}
{{ formatName(item.name) }}
</view>
<view v-if="videoTypeList.length === 0" class="empty-content">
<text>暂无数据</text>
@@ -139,7 +185,7 @@
<script setup>
import { ref, onMounted, computed,reactive } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import { onShow,onLoad,onBackPress} from "@dcloudio/uni-app";
import api from '@/api/api.js';
import allImg from "@/static/video_all.png"
import allOnImg from "@/static/video_select.png"
@@ -154,6 +200,7 @@
import empty from '@/components/empty/empty.vue';
import formatNumber from '@/utils/formatNumber.js';
import navBar from '@/components/navBar/navBar.vue';
import lazyImg from "@/static/default_video.png";
const isAllActive=ref(false)
// 响应式数据
const videoList = ref([]);
@@ -165,8 +212,10 @@
const pageSize = ref(10);
const hasMoreData = ref(true);
const filteredContent=ref([])
import searchImg from "@/static/search.png";
const sort =ref(2);
const keywords=ref('');
const from=ref('');
const typeUuid=ref('0e5fa3d76b8047528fdd3c452b77e9dd');
const typeName=ref('乙肝')
const isFilterActive=ref(false)
@@ -179,6 +228,18 @@
// 全部视频弹窗相关数据
const showAllVideoPopup = ref(false);
const selectedCategory = ref('全部');
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
onLoad((options) => {
if(options.from){
from.value = options.from;
}
});
const selectCategory=(uuid)=>{
console.log(222)
console.log(uuid)
@@ -187,15 +248,19 @@
isAllActive.value=true;
closeAllVideoPopup();
currentPage.value = 1;
videoList.value = [];
hasMoreData.value = true;
loadVideoData();
}
const videoTypeList=ref([]);
const videoTypeList=ref([{
name:'全部视频',
uuid:''
}]);
const patientVideoNew=async ()=>{
const res=await api.patientVideoNew();
if(res.code==200){
videoTypeList.value=res.data;
videoTypeList.value=videoTypeList.value.concat(res.data).filter(item=>!item.name.includes('BMS患者端'));
}
}
// 页面加载
@@ -203,7 +268,7 @@
});
onShow(() => {
onLoad(() => {
// 调用真实API获取数据
currentPage.value = 1;
hasMoreData.value = true;
@@ -211,7 +276,18 @@
loadVideoData();
patientVideoNew();
});
const formatName = (name) => {
let nameStr=name.substring(0,4);
let lateStr=name.substring(4,name.length);
let index = nameStr.lastIndexOf("-");
if (index > -1) {
return name.substring(index+1,4)+lateStr;
}
return name;
};
// 加载轮播数据
// 加载视频标签
@@ -244,7 +320,7 @@
page: currentPage.value,
pageSize: pageSize.value,
keywords: keywords.value,
sort:sort.value,
sort:sort.value.toString(),
typeUuid:typeUuid.value,
});
@@ -323,18 +399,12 @@
const videoId = video.id || video.uuid;
navTo({
url: `/pages_app/videoDetail/videoDetail?id=${videoId}`
url: `/pages_app/videoDetail/videoDetail?id=${videoId}&type=patientVideo`
});
};
// 搜索
const goSearch = () => {
uni.navigateTo({
url: '/pages_app/search/search'
});
};
// 跳转到患教视频列表
@@ -410,8 +480,29 @@
};
const toggleTag = (index) => {
filterTags.value[index].selected = !filterTags.value[index].selected;
const currentTag = filterTags.value[index];
// 如果当前标签已选中,直接取消选中
if (currentTag.selected) {
currentTag.selected = false;
return;
}
// 如果当前标签未选中,检查已选中的标签数量
const selectedCount = filterTags.value.filter(tag => tag.selected).length;
// 如果已选中3个或更多,不允许再选中
if (selectedCount >= 3) {
uni.showToast({
title: '最多只能选择3个标签',
icon: 'none',
duration: 2000
});
return;
}
// 允许选中
currentTag.selected = true;
};
const resetFilter = () => {
@@ -430,35 +521,50 @@
const confirmFilter = () => {
const selectedTags = filterTags.value.filter(tag => tag.selected);
console.log('选中的筛选标签:', selectedTags);
let words='';
for (var i = 0; i < selectedTags.length; i++) {
if(keywords.value){
keywords.value+=","+selectedTags[i].NAME
if(words){
words+=","+selectedTags[i].NAME
}else{
keywords.value=selectedTags[i].NAME
words=selectedTags[i].NAME
}
}
keywords.value=words;
isFilterActive.value=true;
hideFilterPopup();
// 根据选中的标签重新加载数据
currentPage.value = 1;
hasMoreData.value = true;
videoList.value = [];
loadVideoData(true);
};
// 返回
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({
url: '/pages/index/index'
});
if(!from.value){
plus.runtime.quit();
return true;
}else{
uni.navigateBack();
}
});
}
const goSearch = () => {
uni.sendNativeEvent(
"goHomeSearch",
{
msg: "patientVideo",
},
(ret) => {
console.log(ret);
}
);
}
const toggleSort = () => {
sort.value = sort.value === 1 ? 2 : 1; // 1=最新, 2=最热(与后端约定)
currentPage.value = 1;
hasMoreData.value = true;
videoList.value = [];
loadVideoData();
};
@@ -505,7 +611,16 @@
display: flex;
align-items: center;
}
.img-icon {
width: 34rpx;
height: 34rpx;
}
.nav-actions {
display: flex;
align-items: center;
justify-content: center;
}
.header-title {
font-size: 36rpx;
font-weight: 600;
@@ -516,7 +631,7 @@
.scroll-view {
position: fixed;
bottom: 0rpx;
height: calc(100vh - var(--status-bar-height) - 44px); /* 固定高度,减去导航栏高度 */
height: calc(100vh - var(--status-bar-height) - 65px); /* 固定高度,减去导航栏高度 */
background-color: #f5f5f5;
top: calc(var(--status-bar-height) + 65px); /* 为导航栏留出空间 */
}
@@ -527,8 +642,9 @@
.filter-tabs {
background-color: #fff;
display: flex;
justify-content: space-between;
padding: 30rpx 30rpx;
padding: 15rpx 30rpx;
}
.tab-item {
@@ -543,6 +659,7 @@
}
.right{
display: flex;
align-items: center;
}
.tab-item.active .tab-text {
@@ -556,7 +673,7 @@
color: #666;
}
.tab-item:first-child .tab-text{
max-width:295rpx;
max-width:250rpx;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
@@ -663,6 +780,10 @@
.author {
font-size: 24rpx;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 20rpx;
}
.stats {
@@ -715,7 +836,7 @@
.popup-content {
width: 100%;
background-color: #fff;
height:calc(100vh - 272rpx);
height:calc(100vh - var(--status-bar-height) - 44px - 92rpx);
overflow-y: scroll;
}
@@ -780,18 +901,18 @@
z-index: 9999;
.filter-content {
margin-top:272rpx;
margin-top:calc(var(--status-bar-height) + 44px + 90rpx);
overflow-y:scroll;
background-color: $white;
// border-radius: 20rpx 20rpx 0 0;
padding: 20rpx 30rpx 60rpx;
width: 100%;
height: calc(100vh - 272rpx);
height: calc(100vh - calc(var(--status-bar-height) + 44px + 90rpx));
.filter-tags {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 60rpx;
max-height: 65vh;
overflow-y: auto;
@@ -801,6 +922,7 @@
color: $gray-dark;
padding: 8rpx 0; /* 去掉左右padding,仅保留上下 */
border-radius: 30rpx;
margin:8rpx;
font-size: 26rpx;
border: 2rpx solid #efefef;
transition: all 0.3s ease;
@@ -853,4 +975,12 @@
}
}
}
.bar {
width: 2rpx;
margin: 0 43rpx 0 0rpx;
height: 32rpx;
background-color: #666;
}
</style>
+65 -20
View File
@@ -1,14 +1,5 @@
<template>
<uni-nav-bar
left-icon="left"
title="在线支付"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
></uni-nav-bar>
<navBar :showLeft="true" title="在线支付" />
<view class="pay-page">
<!-- 主要内容区域 -->
<view class="main-content">
@@ -26,7 +17,7 @@
</view>
<radio-group @change="radioChange">
<!-- 余额支付选项 -->
<label class="payment-option" @tap="selectPayment('balance')">
<label class="payment-option" @click="selectPayment('balance')">
<view class="option-left">
<up-image :src="moneyImg" width="56rpx" height="56rpx" ></up-image>
</view>
@@ -35,8 +26,9 @@
<text class="option-desc" v-if="balance<paymentInfo.amount/100">余额不足</text>
</view>
<view class="option-right">
{{ selectedPayment === 'balance' }}
<radio
@click.stop="selectPayment('balance')"
:checked="selectedPayment === 'balance'"
value="balance"
:disabled="balance<paymentInfo.amount/100"
@@ -51,7 +43,7 @@
</view>
<!-- 微信支付选项 -->
<label class="payment-option" @tap="selectPayment('wechat')">
<label class="payment-option" @click="selectPayment('wechat')">
<view class="option-left">
<up-image :src="wxImg" width="56rpx" height="56rpx" ></up-image>
</view>
@@ -59,8 +51,9 @@
<text class="option-title">微信支付</text>
</view>
<view class="option-right">
{{ selectedPayment === 'balance' }}
<radio
@click.stop="selectPayment('wechat')"
:checked="selectedPayment === 'wechat'"
value="wechat"
color="#8B2316"
@@ -77,6 +70,11 @@
</view>
</view>
</view>
<up-modal :show="showPwdModal" showCancelButton :title="'提示'" @confirm="confirmPwd" @cancel="showPwdModal=false" confirmColor="#8B2316">
<view class="slot-content">
<input type="text" password v-model="pwd" placeholder="请输入您的登录密码" />
</view>
</up-modal>
</template>
<script setup>
@@ -90,6 +88,8 @@
const type=ref('');
const trade_no=ref('');
const balance=ref(0);
const pwd=ref('');
const showPwdModal=ref(false);
onLoad((options) => {
if(options.type=='ganDanFile'){
file_uuid.value=options.uuid;
@@ -121,11 +121,13 @@
if(res.code==200){
const payParams = res.data
requestPayment(payParams,()=>{
console.log('支付成功回调')
uni.showToast({
title: '支付成功',
icon: 'none'
icon: 'none',
duration:10000
});
uni.navigateBack();
//uni.navigateBack();
},(err)=>{
console.log(err)
uni.showToast({
@@ -139,7 +141,7 @@
const getBalance=()=>{
api.getBalance().then(res=>{
if(res.code==200){
balance.value=res.data.toFixed(2);
balance.value=(res.data/100).toFixed(2);
}
})
};
@@ -153,6 +155,21 @@
}
})
};
const payGanDanFileOrder=()=>{
api.payGanDanFile({
trade_no:trade_no.value,
channel:'balance',
pwd:pwd.value
}).then(res=>{
if(res.code==200){
uni.showToast({
title: '支付成功',
icon: 'none'
});
uni.navigateBack();
}
})
};
// .png支付信息
const paymentInfo = ref({
amount:0,
@@ -187,11 +204,17 @@
}else{
selectedPayment.value = paymentType;
}
console.log('selectedPayment22222222222')
console.log(selectedPayment.value)
};
const confirmPwd=()=>{
showPwdModal.value=false;
payGanDanFileOrder();
}
const confirmPayment = () => {
console.log(selectedPayment.value)
if (!selectedPayment.value) {
uni.showToast({
title: '请选择支付方式',
@@ -199,8 +222,12 @@
});
return;
}
payGanDanFile()
if(selectedPayment.value=='balance'){
//payGanDanFileOrder();
showPwdModal.value=true;
}else{
payGanDanFile();
}
};
</script>
@@ -228,9 +255,27 @@
align-items: center;
justify-content: space-between;
}
.slot-content{
width: 100%;
display: flex;
align-items: center;
input{
flex:1;
height: 90rpx;
background:#f5f5f5;
border-radius: 10rpx;
padding:0 20rpx;
font-size: 28rpx;
}
}
.pay-page {
height: calc(100vh - 180rpx);
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
height: calc(100vh - var(--status-bar-height) - 44px - 130rpx);
background-color: $bg-color;
overflow: hidden;
}
+20 -6
View File
@@ -138,6 +138,20 @@
<script setup>
import { ref } from 'vue';
import {onBackPress,onLoad} from '@dcloudio/uni-app';
const from = ref('');
onBackPress(() => {
if(!from.vlaue){
plus.runtime.quit();
return true;
}
});
onLoad((options) => {
if(options.from){
from.value = options.from;
}
});
// 轮播横幅数据
const bannerList = ref([
@@ -207,13 +221,12 @@
// 方法
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({
url: '/pages/index/index'
});
if(!from.vlaue){
plus.runtime.quit();
return true;
}else{
uni.navigateBack();
}
});
};
const showSearch = () => {
@@ -334,6 +347,7 @@
min-height: 100vh;
background-color: $bg-color;
padding-top: $nav-height; // 为固定导航栏预留空间
overflow: hidden;
}
// 固定在顶部的促销横幅
+213 -48
View File
@@ -1,21 +1,22 @@
<template>
<!-- 头部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="肝胆课件"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eeeeee"
>
<template v-slot:right>
<view class="nav-right" @click="testLoadMore">
<text style="font-size: 24rpx; color: #8B2316;">测试加载</text>
<view class="collect-img" @click="goSearch" style="margin-right: 20rpx;">
<image class="img-icon" :src="searchImg" mode="aspectFill" />
</view>
</template>
</uni-nav-bar>
</view>
<view class="courseware-container">
@@ -51,14 +52,16 @@
>
<view class="item-left">
<view class="pdf-icon">
<up-image :src="pptImg" width="114rpx" height="114rpx" ></up-image>
<up-image :src="item.type=='pdf'?pptImg:wordImg" width="114rpx" height="114rpx" ></up-image>
</view>
</view>
<view class="item-content">
<view class="item-title">{{item.title}}</view>
<view class="item-title twoline">{{item.title}}</view>
<view class="info">
<view >{{ item.providername }}</view>
<view class="item-author">{{ item.hospitalname }}</view>
<view class="author-name">{{ item.providername }}</view>
<view class="item-author">
<view>{{ item.hospitalname }}</view>
</view>
</view>
<view class="item-stats">
<view class="views">
@@ -66,13 +69,13 @@
<text class="view-count">{{ item.readnum }}人阅读</text>
</view>
<view class="price">
<view class="priceImg">
<view class="price" v-if="item.price>=0">
<view class="priceImg" style="margin-top: -4rpx;">
<up-image :src="downLoadImg" width="32rpx" height="32rpx" ></up-image>
</view>
<text class="price-value" v-if="item.price>0"><text class="money-unit">¥</text>{{ item.price>item.discount?fromatPrice(item.discount):fromatPrice(item.price) }}</text>
<text class="yuanjia" v-if="item.price>0 && item.price>item.discount">原价<text class="jiaprice">{{fromatPrice(item.price/100)}}</text></text>
<text v-else class="free">免费</text>
<text class="price-value" v-if="item.price>0 && item.discount>0"><text class="money-unit">¥</text>{{fromatPrice(item.price/100*item.discount)}}</text>
<text class="yuanjia" v-if="item.price>0 && item.discount<1 && item.discount>0">原价<text class="jiaprice">{{fromatPrice(item.price/100)}}</text></text>
<text v-else-if="item.price<=0 || item.discount===0 || item.price*item.discount==0 " class="free">免费</text>
</view>
</view>
</view>
@@ -105,24 +108,48 @@
<!-- 投稿按钮 -->
<view class="submit-btn" @click="submitCourseware">
<up-image :src="tougaoImg" width="152rpx" height="80rpx" ></up-image>
<up-image :src="getPptImg" width="152rpx" height="80rpx" ></up-image>
</view>
</view>
<unidialog
:visible="pptVisible"
:cancelText="'取消'"
:confirmText="'保存二维码'"
@close="pptVisible=false"
@confirm="pptConfirm"
>
<template v-slot:content>
<view class="ppt-content">
<image :src="contactImg" width="80rpx" height="80rpx" class="contact-img"></image>
<view class="ppt-content-title">
<text style="color:#666">APP下载课件为pdf格式,扫码联系平台文献助手获取原版PPT。</text>
</view>
</view>
</template>
</unidialog>
</template>
<script setup>
import { ref } from 'vue';
import { onShow,onLoad} from "@dcloudio/uni-app";
import unidialog from "@/components/dialog/dialog.vue";
import { onShow,onLoad,onBackPress} from "@dcloudio/uni-app";
import searchImg from "@/static/search.png";
import filter from "@/static/cb_screen_no.png"
import filterOn from "@/static/cb_screen_yes.png"
import upImg from "@/static/cb_up.png"
import downImg from "@/static/cb_up.png"
import tougaoImg from "@/static/kejiantougao.png"
import getPptImg from "@/static/getPpt.png"
import downLoadImg from "@/static/wdxz.png"
import pptImg from "@/static/pdf.png"
import wordImg from "@/static/word.png"
import contactImg from "@/static/contact.png"
import api from '@/api/api.js';
import navTo from '@/utils/navTo.js';
const isFilterActive=ref(false)
const pptVisible=ref(false)
const pptContent=ref('肝胆相照将稍后与您沟通课件分享,谢谢您对平台的支持!')
const hcp_token=ref('')
const keywords=ref('')
const sort = ref(0); // 默认最新排序
@@ -132,9 +159,61 @@
const noMore = ref(false);
const page = ref(1);
const pageSize = ref(10);
const pptConfirm=()=>{
saveContactImgToAlbum();
pptVisible.value=false;
//feedBack();
}
const saveContactImgToAlbum = () => {
uni.getImageInfo({
src: contactImg,
success: (imgRes) => {
uni.saveImageToPhotosAlbum({
filePath: imgRes.path,
success: () => {
uni.showToast({
title: '图片已保存到相册',
icon: 'none'
});
},
fail: (err) => {
const errMsg = (err && err.errMsg) || '';
if (errMsg.includes('auth deny') || errMsg.includes('authorize no response') || errMsg.includes('denied')) {
uni.showModal({
title: '提示',
content: '请先在系统设置中允许访问相册',
showCancel: true,
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
uni.openSetting();
}
}
});
return;
}
uni.showToast({
title: '保存失败,请稍后重试',
icon: 'none'
});
}
});
},
fail: () => {
uni.showToast({
title: '图片读取失败',
icon: 'none'
});
}
});
}
// 响应式数据
const coursewareList = ref([]);
onBackPress(()=>{
plus.runtime.quit();
return true;
})
const checkUser= async()=>{
const res = await api.checkUser();
if(res.code==200){
@@ -143,9 +222,30 @@
}
}
onLoad(()=>{
checkUser();
})
checkUser();
loadData(false);
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "list_page",
page_type: "肝胆课件",
},
},
(ret) => {
console.log(ret);
});
})
const goSearch = () => {
uni.sendNativeEvent(
"goHomeSearch",
{
msg: "ppt",
},
(ret) => {
console.log(ret);
}
);
};
// 方法
const goBack = () => {
uni.navigateBack({
@@ -167,8 +267,11 @@
const toggleSort = () => {
sort.value = sort.value === 0 ? 1 : 0;
console.log('切换排序:', sort.value);
page.value=1;
coursewareList.value = [];
noMore.value = false;
// 切换排序后重新加载数据
loadData(true);
loadData(false);
};
@@ -188,11 +291,21 @@
};
const submitCourseware = () => {
pptVisible.value=true;
};
const feedBack=()=>{
const expertUuid=uni.getStorageSync('userInfo').uuid
api.feedBack({
content: '我要共享课件',
expertUuid:expertUuid
}).then((res)=>{
uni.showToast({
title: '课件投稿功能',
title: '谢谢您的支持',
icon: 'none'
});
};
})
}
const loadGuideTags = async () => {
try {
const res = await api.guideTag({
@@ -220,9 +333,9 @@
// 重置数据
page.value = 1;
noMore.value = false;
coursewareList.value = [];
// 重新加载第一页数据
await loadData(true);
await loadData(false);
uni.showToast({
title: '刷新成功',
@@ -377,6 +490,18 @@
};
const toggleTag = (index) => {
// 已选中的标签数量
const selectedCount = filterTags.value.filter(tag => tag.selected).length;
// 当前是未选中 -> 试图选中,且已达到上限
if (!filterTags.value[index].selected && selectedCount >= 3) {
uni.showToast({
title: '最多选择3个标签',
icon: 'none'
});
return;
}
filterTags.value[index].selected = !filterTags.value[index].selected;
//isFilterActive.value = filterTags.value.some(tag => tag.selected);
};
@@ -389,16 +514,21 @@
const confirmFilter = () => {
const selectedTags = filterTags.value.filter(tag => tag.selected);
console.log('选中的筛选标签:', selectedTags);
let words='';
for (var i = 0; i < selectedTags.length; i++) {
if(keywords.value){
keywords.value+=","+selectedTags[i].NAME
if(words){
words+=","+selectedTags[i].NAME
}else{
keywords.value=selectedTags[i].NAME
words=selectedTags[i].NAME
}
}
keywords.value=words;
isFilterActive.value =true;
hideFilterPopup();
loadData(true);
page.value=1;
coursewareList.value = [];
noMore.value = false;
loadData(false);
// 这里可以根据选中的标签进行数据筛选
};
const fromatPrice=(price)=>{
@@ -411,7 +541,7 @@
onShow(() => {
// 页面显示时加载数据
console.log('页面显示,开始加载课件数据');
loadData(true);
loadGuideTags()
});
@@ -448,13 +578,22 @@
$submit-btn-color: #20b2aa;
$theme-color: #8B2316;
$white: #fff;
.contact-img{
width: 280rpx;
height: 280rpx;
margin: 0 auto;
display: block;
}
// 混合器
@mixin flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.img-icon{
width: 34rpx;
height: 34rpx;
}
@mixin flex-between {
display: flex;
align-items: center;
@@ -513,20 +652,26 @@
height: 100rpx;
background-color: $white;
@include flex-center;
gap: 100rpx;
border-bottom: 1rpx solid $border-color;
position: sticky;
top: 180rpx;
top: calc(var(--status-bar-height) + 44px);
z-index: 99;
.divider{
width: 2rpx;
height: 30rpx;
margin:0 80rpx;
background-color: #999;
}
.filter-item{
display:flex;
align-items: center;
.filterbox{
margin-top: 5rpx;
margin-top: 0rpx;
}
.newbox{
margin-top: -11rpx;
}
.filter-text{
color:#666;
@@ -539,23 +684,22 @@
.courseware-list {
position: fixed;
top:280rpx;
top:calc(var(--status-bar-height) + 44px + 100rpx);
flex: 1;
bottom:0;
left:30rpx;
right:30rpx;
left:0rpx;
right:0rpx;
padding-top: 20rpx;
width:auto;
.courseware-item {
margin:0 30rpx 20rpx;
background-color: $white;
border-radius: 16rpx;
padding: 30rpx 0;
margin-bottom: 20rpx;
padding: 30rpx 20rpx;
@include shadow;
display: flex;
gap: 20rpx;
.item-left {
.pdf-icon {
@@ -565,10 +709,11 @@
}
.item-content {
flex: 1;
width:500rpx;
display: flex;
margin-left: 10px;
flex-direction: column;
gap: 16rpx;
margin-right: 30rpx;
.item-title{
font-size: 32rpx;
color:#333;
@@ -577,15 +722,29 @@
display: flex;
font-size: 26rpx;
color:#666;
width:100%;
min-width: 0;
margin:10rpx 0;
overflow: hidden;
}
.author-name{
white-space: nowrap;
}
.item-author {
flex:1;
min-width: 0;
max-width: 100%;
margin-left: 10rpx;
margin-right: 30rpx;
overflow: hidden;
view{
width:100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx; /* 限制最大宽度 */
}
}
.item-stats {
@@ -619,7 +778,7 @@
.price {
display: flex;
margin-left: 30rpx;
align-items: baseline;
align-items: center;
gap: 4rpx;
.price-symbol {
@@ -690,7 +849,7 @@
z-index: 9999;
.filter-content {
margin-top:245rpx;
margin-top:calc(var(--status-bar-height) + 44px + 102rpx);
background-color: $white;
// border-radius: 20rpx 20rpx 0 0;
@@ -701,7 +860,7 @@
.filter-tags {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 60rpx;
max-height: 65vh;
overflow-y: auto;
@@ -709,7 +868,13 @@
.tag-item {
background-color: #f8f8f8;
color: #666;
padding: 8rpx 24rpx;
padding: 6rpx 0rpx;
width:150rpx;
margin:8rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: center;
border-radius: 30rpx;
font-size: 26rpx;
border: 2rpx solid #efefef;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -87,6 +87,7 @@
<textarea
class="form-textarea"
type="text"
:adjust-position="false"
placeholder="不支持关键字/广泛查询"
v-model="literature.literatureName"
/>
+6 -2
View File
@@ -5,9 +5,9 @@
@clickLeft="goBack" :title="name" color="#8B2316" :border="false" backgroundColor="#eeeeee">
<template #right>
<!-- <view class="nav-right" @click="goSelect">
<view class="nav-right" @click="goSelect">
精选
</view> -->
</view>
</template>
</uni-nav-bar>
</view>
@@ -450,6 +450,10 @@ const getVideoType=async () => {
.author {
font-size: 24rpx;
color: #999;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 20rpx;
}
.stats {
+13 -7
View File
@@ -11,7 +11,7 @@
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
lower-threshold="1000"
lower-threshold="300"
>
<view class="meeting-list">
<view
@@ -42,11 +42,12 @@
></uni-load-more>
<!-- 空状态 -->
<view class="empty-state" v-if="meetingList.length === 0 && !loading">
<empty v-if="meetingList.length === 0 && !loading"></empty>
<!-- <view class="empty-state" v-if="meetingList.length === 0 && !loading">
<view class="empty-content">
<text class="empty-text">暂无数据</text>
</view>
</view>
</view> -->
</scroll-view>
</view>
</template>
@@ -57,6 +58,7 @@
import navBar from '@/components/navBar/navBar.vue';
import api from '@/api/api.js';
import navTo from '@/utils/navTo';
import empty from "@/components/empty/empty.vue";
// 会议列表数据
const typeUuid=ref('');
const typeName=ref('');
@@ -101,11 +103,9 @@
};
const loadVideoData = async (isRefresh = false) => {
if (loading.value && !isRefresh) return;
loading.value = true;
try {
const response = await api.videoByTypeNew({
const response = await api.patientVideoByJingHuaNew({
page: currentPage.value,
pageSize: pageSize.value,
typeUuid:typeUuid.value,
@@ -128,7 +128,7 @@
if (videoData && videoData.list) {
const { list, totalPage,pageNumber } = videoData;
console.log('视频列表数据:', list);
loading.value=false;
if (isRefresh) {
meetingList.value = list || [];
currentPage.value = 1;
@@ -138,6 +138,10 @@
hasMoreData.value = totalPage>pageNumber;
loadMoreStatus.value = hasMoreData.value ? 'more' : 'noMore';
if(hasMoreData.value ){
onLoadMore();
}
} else {
throw new Error(response?.message || '获取数据失败');
}
@@ -162,6 +166,8 @@
// 上拉加载更多
const onLoadMore = () => {
console.log('onLoadMore');
console.log('hasMoreData',hasMoreData.value);
console.log('loading',loading.value);
if (!hasMoreData.value || loading.value) return;
loadMoreStatus.value = 'loading';
+13 -6
View File
@@ -1,13 +1,14 @@
<template>
<view class="reply-page">
<!-- 导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="回复"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -17,6 +18,7 @@
</view>
</template>
</uni-nav-bar>
</view>
<!-- 主内容区域 -->
<view class="main-content">
@@ -24,6 +26,7 @@
<textarea
class="reply-input"
v-model="replyText"
:adjust-position="false"
:placeholder="placeholder"
:auto-height="true"
:maxlength="maxLength"
@@ -73,20 +76,23 @@ const addCommentV2=async()=>{
comment:replyText.value+'||'+name.value+':'+comment_partent.value
})
if(res.code==200){
uni.$emit('replySuccess',true);
console.log('replySuccess')
uni.showToast({ title: '回复成功', icon: 'none' })
replyText.value = '';
uni.navigateBack();
}
}
onUnmounted(() => {
// 清理工作
console.log('回复页面已卸载')
//console.log('回复页面已卸载')
})
// 方法
const goBack = () => {
uni.navigateBack()
uni.navigateBack();
}
const confirmReply = async () => {
@@ -300,8 +306,9 @@ $blue: #007aff;
// 主内容区域
.main-content {
flex: 1;
padding: 20px 16px;
padding: 10px 16px;
background-color: #fff;
margin-top: calc(var(--status-bar-height) + 44px);
.input-container {
position: relative;
background-color: $white;
+45 -9
View File
@@ -1,13 +1,15 @@
<template>
<view class="content">
<!-- 顶部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="添加日程"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -17,6 +19,7 @@
</view>
</template>
</uni-nav-bar>
</view>
<!-- 主要内容区域 -->
<view class="main-content">
@@ -27,7 +30,7 @@
</view>
<view class="section-content">
<text class="content-text">{{ selectedPatient || '请选择患者' }}</text>
<uni-icons type="right" class ="icon" size="20" color="#999"></uni-icons>
<uni-icons type="right" class ="icon" size="20" color="#999" v-if="type != 'patientDetail'"></uni-icons>
</view>
</view>
@@ -53,6 +56,7 @@
v-model="followUpContent"
placeholder="请输入随访内容"
:maxlength="500"
:adjust-position="false"
auto-height
></textarea>
</view>
@@ -77,7 +81,7 @@
</view>
</view>
</view>
<u-overlay :show="show">
<up-overlay :show="show">
<view class="warp">
<view class="calendarbox">
@@ -91,17 +95,17 @@
<uni-calendar class="uni-calendar--hook" @change="change" @monthSwitch="monthSwitch" />
<view class="calendarbottom">
<view class="cancel" @click="show=false">取消</view>
<view class="ok">确定</view>
<view class="ok" @click="confirmDate">确定</view>
</view>
</view>
</view>
</u-overlay>
</up-overlay>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import { onShow,onLoad } from "@dcloudio/uni-app";
import navTo from '@/utils/navTo.js';
import api from '@/api/api.js';
// 表单数据
@@ -115,10 +119,25 @@
const headerYear = ref('');
const headerDay = ref('');
const patientUuid = ref('');
let tempDate=('');
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
const patientName = ref('')
const type = ref('')
onLoad((options) => {
if(options.uuid){
patientUuid.value = options.uuid
}
if(options.patient_name){
selectedPatient.value = options.patient_name
}
if(options.type){
type.value = options.type
}
})
const addFollowUps=()=>{
api.addFollowUps({
patient_uuid: patientUuid.value,
@@ -135,6 +154,12 @@
}
})
}
const confirmDate=()=>{
selectedDate.value = tempDate;
//visits.value[dealIndex.value].date = tempDate.value;
show.value=false;
}
// 提交日程
const submitSchedule = () => {
if (!selectedPatient.value) {
@@ -149,6 +174,9 @@
// 选择患者
const selectPatient = () => {
if(type.value == 'patientDetail'){
return
}else{
navTo({
url:'/pages_app/selectPatientSingle/selectPatientSingle',
events: {
@@ -158,6 +186,8 @@
}
}
})
}
};
// 选择日期
@@ -193,7 +223,8 @@
headerYear.value = `${y}年`;
headerDay.value = `${m}月${dd}日周${w}`;
datetime.value = `${y}-${m}-${dd}`;
selectedDate.value = `${y}年${m}月${dd}日(星期${w})`;
//selectedDate.value = `${y}年${m}月${dd}日(星期${w})`;
tempDate= `${y}年${m}月${dd}日(星期${w})`;
}
};
@@ -296,8 +327,13 @@
/* 主要内容区域 */
.main-content {
position: fixed;
top: calc(var(--status-bar-height) + 44px);
left: 0;
right: 0;
bottom: 0;
margin-top: 20rpx;
// margin-top: 20rpx;
}
/* 表单区域样式 */
+54 -13
View File
@@ -1,7 +1,21 @@
<template>
<view class="select-page">
<navBar title="搜索患者" />
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="搜索患者"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eee"
>
</uni-nav-bar>
</view>
<!-- 搜索框 -->
<view class="search-bar">
@@ -19,6 +33,7 @@
<image class="avatar" :src="docUrl + (p.photo || '')" mode="aspectFill" />
<view class="name">{{ p.nickname || p.realName }}</view>
</view>
<empty v-if="availablePatientList.length == 0" :emptyDesc="'暂无搜索结果'"></empty>
</scroll-view>
</view>
</template>
@@ -26,29 +41,38 @@
<script setup>
import { ref, computed } from 'vue'
import docUrl from '@/utils/docUrl.js'
import { onShow,onLoad} from "@dcloudio/uni-app";
import { onShow,onLoad,onBackPress} from "@dcloudio/uni-app";
import empty from "@/components/empty/empty.vue"
import api from '@/api/api.js'
import navTo from '@/utils/navTo.js'
import navBar from '@/components/navBar/navBar.vue'
const from = ref('');
const keyword = ref('')
const patientList = ref([])
const availablePatientList = ref([])
// 计算属性:显示所有患者,但标记已选中的状态
const availablePatientList = computed(() => {
return patientList.value
})
// const availablePatientList = computed(() => {
// return patientList.value
// })
onBackPress(() => {
if(!from.value){
plus.runtime.quit();
return true;
}
});
const patientListByGBK = async () => {
const res = await api.patientListByGBK();
if(res.code == 1){
patientList.value = res.data;
availablePatientList.value = res.data;
}
};
onLoad((options) => {
if(options.from == 'chatMsg'){
from.value = 'chatMsg';
if(options.from){
from.value = options.from;
}
})
onShow(() => {
patientListByGBK();
@@ -58,13 +82,19 @@
const goDetail = (uuid) => {
navTo({ url: `/pages_app/patientDetail/patientDetail?uuid=${uuid}` })
navTo({ url: `/pages_app/patientDetail/patientDetail?uuid=${uuid}&from=searchPatient`})
}
const onSearch = () => {
patientList.value = patientList.value.filter(p => p.realName.indexOf(keyword.value) !== -1 || (p.nickname && p.nickname.indexOf(keyword.value) !== -1) || p.mobile.indexOf(keyword.value) !== -1)
availablePatientList.value = patientList.value.filter(p => p.realName.indexOf(keyword.value) !== -1 || (p.nickname && p.nickname.indexOf(keyword.value) !== -1) || p.mobile.indexOf(keyword.value) !== -1)
}
const goBack = () => {
if(!from.value){
plus.runtime.quit();
}else{
uni.navigateBack();
}
}
const goBack = () => uni.navigateBack()
</script>
<style lang="scss" scoped>
@@ -75,8 +105,12 @@ const goDetail = (uuid) => {
.confirm-btn{ background:#7f7f7f; padding: 10rpx 18rpx; border-radius: 26rpx; }
.confirm-btn.active{ background:#8B2316; }
.search-bar{
position: fixed;
left:30rpx;
right:30rpx;
top:calc(var(--status-bar-height) + 44px);
border: 2rpx solid #eee;
margin: 20rpx 30rpx; display:flex; align-items:center; gap: 16rpx;
margin: 20rpx 0rpx; display:flex; align-items:center; gap: 16rpx;
.input-wrap{ flex:1; background:#fff; border-radius: 12rpx; padding: 16rpx 20rpx; }
.search-input{ font-size: 28rpx; color:#333; }
.ph{ color:#bfbfbf; }
@@ -87,7 +121,14 @@ const goDetail = (uuid) => {
width: 88rpx; height: 72rpx; background:#fff;
}
}
.list{ border-radius: 12rpx; }
.list{
border-radius: 12rpx;
position: fixed;
left: 0;
right: 0;
top: calc(var(--status-bar-height) + 44px + 115rpx);
bottom: 0rpx;
}
.item{background:#fff; display:flex; align-items:center;padding: 24rpx 30rpx; border-bottom: 2rpx solid #eee; }
.avatar{ width: 96rpx; height:96rpx; border-radius: 16rpx; background:#ffe; }
.name{ flex:1; margin-left: 20rpx; font-size: 32rpx; color:#333; }
@@ -0,0 +1,352 @@
<template>
<view class="select-page">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="选择患者"
@clickLeft="goBack"
color="#8B2316"
:border="false"
backgroundColor="#eee"
>
<template #right>
<view class="confirm-btn" :class="{ active: selectedIds.length > 0 }" @click="confirmSelect">
<text class="confirm-text">确定({{ selectedIds.length }})</text>
</view>
</template>
</uni-nav-bar>
</view>
<!-- 搜索框 -->
<view class="search-bar">
<view class="input-wrap">
<input class="search-input" v-model.trim="keyword" placeholder="搜索患者的备注名、昵称或手机号" placeholder-class="ph" @input="$u.debounce(onSearch, 500)" />
</view>
<view class="search-btn" @click="onSearch">
<uni-icons type="search" size="50rpx" color="#999" />
</view>
</view>
<!-- 列表 -->
<scroll-view class="list" scroll-y @scrolltolower="onScrollToLower" :lower-threshold="100">
<view class="item" @click="toggle(p.uuid)" v-for="p in availablePatientList" :key="p.uuid">
<image class="avatar" :src="getAvatarSrc(p)" mode="aspectFill" @error="handleAvatarError(p)" />
<view class="name">{{ p.realName || p.nickname}}</view>
<view class="check" >
<view class="circle" :class="{ active: selectedIds.includes(p.uuid) }"></view>
</view>
</view>
</scroll-view>
<view class="bottom-actions">
<view class="select-all-btn" @click="toggleSelectAll">
{{ isAllSelected ? '全不选' : '全选' }}
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed,nextTick} from 'vue'
import docUrl from '@/utils/docUrl.js'
import { onShow,onLoad} from "@dcloudio/uni-app";
import api from '@/api/api.js'
import navTo from '@/utils/navTo.js'
import defaultImg from "@/static/default.png"
const from = ref('');
const keyword = ref('')
const selectedIds = ref([])
const patientList = ref([])
const selectedDetail = ref([])
const availablePatientList = ref([])
const patientListPage = ref(1)
const patientListPageSize = ref(200)
const patientListHasMore = ref(true)
const patientListLoading = ref(false)
const patientListLoadingMore = ref(false)
const isAllSelected = computed(() => {
const total = patientList.value.length
return total > 0 && selectedIds.value.length === total
})
// 计算属性:显示所有患者,但标记已选中的状态
// const availablePatientList = computed(() => {
// return patientList.value
// })
const isApiSuccess = (res) => Number(res?.code) === 1 || Number(res?.code) === 200
const parsePagedPatients = (res) => {
const data = res?.data
if (Array.isArray(data)) {
return { list: data, total: undefined, pageNum: undefined, pages: undefined, isLastPage: undefined }
}
const list =
(Array.isArray(data?.list) && data.list) ||
(Array.isArray(data?.records) && data.records) ||
(Array.isArray(data?.rows) && data.rows) ||
(Array.isArray(data?.data) && data.data) ||
[]
const total = Number(data?.total ?? data?.count ?? data?.totalCount ?? undefined)
const pageNum = Number(data?.pageNum ?? data?.page ?? data?.current ?? undefined)
const pages = Number(data?.pages ?? data?.pageCount ?? undefined)
const isLastPage = typeof data?.isLastPage === 'boolean' ? data.isLastPage : undefined
return {
list,
total: Number.isFinite(total) ? total : undefined,
pageNum: Number.isFinite(pageNum) ? pageNum : undefined,
pages: Number.isFinite(pages) ? pages : undefined,
isLastPage
}
}
const mergePatientListUnique = (base = [], incoming = []) => {
if (!incoming.length) return base
const seen = new Set()
const merged = []
const pushUnique = (item) => {
const key = String(item?.uuid || item?.id || item?.mobile || '')
if (!key) {
merged.push(item)
return
}
if (seen.has(key)) return
seen.add(key)
merged.push(item)
}
base.forEach(pushUnique)
incoming.forEach(pushUnique)
return merged
}
const applyPatientList = (list = []) => {
patientList.value = list
onSearch()
}
const patientListByGBKPage = async ({ reset = false, showLoading = false } = {}) => {
if (patientListLoading.value || patientListLoadingMore.value) return
if (!reset && !patientListHasMore.value) return
if (reset) {
patientListLoading.value = true
patientListPage.value = 1
patientListHasMore.value = true
} else {
patientListLoadingMore.value = true
}
if (showLoading) {
uni.showLoading({ title: '加载中...', mask: true })
}
try {
const requestPage = patientListPage.value
const res = await api.patientListByGBKPage({
page: requestPage,
//pageNum: requestPage,
//current: requestPage,
pageSize: patientListPageSize.value,
//size: patientListPageSize.value
})
if (!isApiSuccess(res)) return
const { list, total, pageNum, pages, isLastPage } = parsePagedPatients(res)
const merged = reset ? list : mergePatientListUnique(patientList.value, list)
applyPatientList(merged)
if (typeof isLastPage === 'boolean') {
patientListHasMore.value = !isLastPage
} else if (typeof pages === 'number' && typeof pageNum === 'number') {
patientListHasMore.value = pageNum < pages
} else if (typeof total === 'number') {
patientListHasMore.value = merged.length < total
} else {
patientListHasMore.value = list.length >= patientListPageSize.value
}
if (patientListHasMore.value) {
patientListPage.value = typeof pageNum === 'number' ? pageNum + 1 : requestPage + 1
}
} finally {
patientListLoading.value = false
patientListLoadingMore.value = false
if (showLoading) {
nextTick(() => {
uni.hideLoading()
})
}
}
}
const onScrollToLower = () => {
void patientListByGBKPage({ reset: false, showLoading: false })
}
onLoad((options) => {
uni.showLoading({
title: '加载中...',
mask: true
})
if(options.from ){
from.value =options.from;
}
// 读取已选中的成员ID
try {
const preSelected = uni.getStorageSync('preSelectedIds')
if (Array.isArray(preSelected)) {
selectedIds.value = [...preSelected]
// 清理缓存
uni.removeStorageSync('preSelectedIds')
}
} catch (e) {}
void patientListByGBKPage({ reset: true, showLoading: true });
})
onShow(() => {
// 根据已选中的ID更新selectedDetail
updateSelectedDetail();
});
// 根据已选中的ID更新selectedDetail
const updateSelectedDetail = () => {
selectedDetail.value = selectedIds.value.map(id => {
const p = patientList.value.find(x => x.uuid === id)
return { uuid: id, realName: p?.realName || '', photo: p?.photo || '',nickName: p?.nickName || '' }
})
}
const toggle = (id) => {
const i = selectedIds.value.indexOf(id)
if (i > -1) {
// 如果已选中,则取消选中
selectedIds.value.splice(i, 1)
const di = selectedDetail.value.findIndex(it => it.uuid === id)
if (di > -1) selectedDetail.value.splice(di, 1)
} else {
// 如果未选中,则选中
selectedIds.value.push(id)
const p = patientList.value.find(x => x.uuid === id)
selectedDetail.value.push({ uuid: id, realName: p?.realName || '', photo: p?.photo || '',nickName: p?.nickName || '' })
}
}
const getAvatarSrc = (patient) => {
if (patient?._avatarLoadError) return defaultImg
return patient?.photo ? (docUrl + patient.photo) : defaultImg
}
const handleAvatarError = (patient) => {
if (!patient) return
patient._avatarLoadError = true
}
const toggleSelectAll = () => {
if (!patientList.value.length) return
if (isAllSelected.value) {
selectedIds.value = []
selectedDetail.value = []
return
}
selectedIds.value = patientList.value.map(item => item.uuid)
selectedDetail.value = patientList.value.map(item => ({
uuid: item.uuid,
realName: item?.realName || '',
photo: item?.photo || '',
nickName: item?.nickName || ''
}))
}
const onSearch = () => {
if(keyword.value.replace(/\s/g, "")){
availablePatientList.value = patientList.value.filter(p => p.realName.indexOf(keyword.value) !== -1 || (p.nickname && p.nickname.indexOf(keyword.value) !== -1) || p.mobile.indexOf(keyword.value) !== -1)
}else{
availablePatientList.value = patientList.value
}
}
const goBack = () => uni.navigateBack()
const confirmSelect = () => {
if(from.value == 'chatMsg' || from.value == 'videoDetail'){
if(selectedIds.value.length >200){
uni.showToast({
title: '选择人数不能超过200',
icon: 'none'
})
return;
}
}
const payload = { ids: selectedIds.value, list: selectedDetail.value }
// 通过事件通道回传
try {
const pages = getCurrentPages()
const curr = pages[pages.length - 1]
const ec = curr?.getOpenerEventChannel?.()
ec?.emit && ec.emit('onPatientsSelected', payload);
console.log(selectedDetail.value)
uni.$emit('selectedChatPatientsSingle', {patients: selectedDetail.value });
} catch (e) {}
// 兜底:使用本地存储
try {
uni.setStorageSync('patientsSelectedPayload', payload) } catch (e) {}
uni.setStorageSync('selectedChatPatientsSingle', {patients: selectedDetail.value })
if(from.value == 'chatMsg'){
navTo({
url: '/pages_chat/groupSend/groupSend?from=chatMsg'
})
}else{
uni.navigateBack()
}
}
</script>
<style lang="scss" scoped>
.select-page{
min-height: 100vh; background:#fefefe;overflow: hidden;
}
.confirm-text{ color:#fff; font-size: 28rpx;white-space: nowrap; }
.confirm-btn{ background:#7f7f7f; padding: 10rpx 18rpx; border-radius: 26rpx; }
.confirm-btn.active{ background:#8B2316; }
.search-bar{
border: 2rpx solid #eee;
margin: 0rpx 30rpx; display:flex;
margin-top: calc(var(--status-bar-height) + 64px);
align-items:center; gap: 16rpx;
.input-wrap{ flex:1; background:#fff; border-radius: 12rpx; padding: 16rpx 20rpx; }
.search-input{ font-size: 28rpx; color:#333; }
.ph{ color:#bfbfbf; }
.search-btn{
display: flex;
align-items: center;
justify-content: center;
width: 88rpx; height: 72rpx; background:#fff;
}
}
.list{
border-radius: 12rpx;
position: fixed;
left: 0;
right: 0;
top: calc(var(--status-bar-height) + 64px + 94rpx);
bottom: 130rpx;
}
.item{background:#fff; display:flex; align-items:center;padding: 24rpx 30rpx; border-bottom: 2rpx solid #eee; }
.avatar{ width: 96rpx; height:96rpx; border-radius: 16rpx; background:#ffe; }
.name{ flex:1; margin-left: 20rpx; font-size: 32rpx; color:#333; }
.check{ padding-left: 12rpx; }
.circle{ width: 40rpx; height: 40rpx; border-radius: 50%; border: 2rpx solid #cfcfcf; }
.circle.active{ background:#8B2316; border-color:#8B2316; position: relative; }
.circle.active::after{ content:''; position:absolute; left: 14rpx; top: 6rpx; width: 10rpx; height: 18rpx; border: 4rpx solid #fff; border-top: 0; border-left: 0; transform: rotate(45deg); }
.bottom-actions{
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-top: 1rpx solid #f0f0f0;
padding: 20rpx 30rpx 20rpx;
}
.select-all-btn{
height: 88rpx;
border-radius: 12rpx;
color: #8B2316;
background: #fff;
border: 2rpx solid #8B2316;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
</style>
+206 -26
View File
@@ -1,12 +1,12 @@
<template>
<view class="select-page">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="选择患者"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -16,6 +16,7 @@
</view>
</template>
</uni-nav-bar>
</view>
<!-- 搜索框 -->
<view class="search-bar">
@@ -28,47 +29,154 @@
</view>
<!-- 列表 -->
<scroll-view class="list" scroll-y>
<scroll-view class="list" scroll-y @scrolltolower="onScrollToLower" :lower-threshold="100">
<view class="item" @click="toggle(p.uuid)" v-for="p in availablePatientList" :key="p.uuid">
<image class="avatar" :src="docUrl + (p.photo || '')" mode="aspectFill" />
<view class="name">{{ p.nickname || p.realName }}</view>
<image class="avatar" :src="getAvatarSrc(p)" mode="aspectFill" @error="handleAvatarError(p)" />
<view class="name">{{ p.realName || p.nickname}}</view>
<view class="check" >
<view class="circle" :class="{ active: selectedIds.includes(p.uuid) }"></view>
</view>
</view>
</scroll-view>
<view class="bottom-actions">
<view class="select-all-btn" @click="toggleSelectAll">
{{ isAllSelected ? '全不选' : '全选' }}
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed,nextTick} from 'vue'
import docUrl from '@/utils/docUrl.js'
import { onShow,onLoad} from "@dcloudio/uni-app";
import api from '@/api/api.js'
import navTo from '@/utils/navTo.js'
import defaultImg from "@/static/default.png"
const from = ref('');
const keyword = ref('')
const selectedIds = ref([])
const patientList = ref([])
const selectedDetail = ref([])
// 计算属性:显示所有患者,但标记已选中的状态
const availablePatientList = computed(() => {
return patientList.value
const availablePatientList = ref([])
const patientListPage = ref(1)
const patientListPageSize = ref(200)
const patientListHasMore = ref(true)
const patientListLoading = ref(false)
const patientListLoadingMore = ref(false)
const isAllSelected = computed(() => {
const total = patientList.value.length
return total > 0 && selectedIds.value.length === total
})
const patientListByGBK = async () => {
const res = await api.patientListByGBK();
if(res.code == 1){
patientList.value = res.data;
// 计算属性:显示所有患者,但标记已选中的状态
// const availablePatientList = computed(() => {
// return patientList.value
// })
const isApiSuccess = (res) => Number(res?.code) === 1 || Number(res?.code) === 200
const parsePagedPatients = (res) => {
const data = res?.data
if (Array.isArray(data)) {
return { list: data, total: undefined, pageNum: undefined, pages: undefined, isLastPage: undefined }
}
const list =
(Array.isArray(data?.list) && data.list) ||
(Array.isArray(data?.records) && data.records) ||
(Array.isArray(data?.rows) && data.rows) ||
(Array.isArray(data?.data) && data.data) ||
[]
const total = Number(data?.total ?? data?.count ?? data?.totalCount ?? undefined)
const pageNum = Number(data?.pageNum ?? data?.page ?? data?.current ?? undefined)
const pages = Number(data?.pages ?? data?.pageCount ?? undefined)
const isLastPage = typeof data?.isLastPage === 'boolean' ? data.isLastPage : undefined
return {
list,
total: Number.isFinite(total) ? total : undefined,
pageNum: Number.isFinite(pageNum) ? pageNum : undefined,
pages: Number.isFinite(pages) ? pages : undefined,
isLastPage
}
}
const mergePatientListUnique = (base = [], incoming = []) => {
if (!incoming.length) return base
const seen = new Set()
const merged = []
const pushUnique = (item) => {
const key = String(item?.uuid || item?.id || item?.mobile || '')
if (!key) {
merged.push(item)
return
}
if (seen.has(key)) return
seen.add(key)
merged.push(item)
}
base.forEach(pushUnique)
incoming.forEach(pushUnique)
return merged
}
const applyPatientList = (list = []) => {
patientList.value = list
onSearch()
}
const patientListByGBKPage = async ({ reset = false, showLoading = false } = {}) => {
if (patientListLoading.value || patientListLoadingMore.value) return
if (!reset && !patientListHasMore.value) return
if (reset) {
patientListLoading.value = true
patientListPage.value = 1
patientListHasMore.value = true
} else {
patientListLoadingMore.value = true
}
if (showLoading) {
uni.showLoading({ title: '加载中...', mask: true })
}
try {
const requestPage = patientListPage.value
const res = await api.patientListByGBKPage({
page: requestPage,
//pageNum: requestPage,
//current: requestPage,
pageSize: patientListPageSize.value,
//size: patientListPageSize.value
})
if (!isApiSuccess(res)) return
const { list, total, pageNum, pages, isLastPage } = parsePagedPatients(res)
const merged = reset ? list : mergePatientListUnique(patientList.value, list)
applyPatientList(merged)
if (typeof isLastPage === 'boolean') {
patientListHasMore.value = !isLastPage
} else if (typeof pages === 'number' && typeof pageNum === 'number') {
patientListHasMore.value = pageNum < pages
} else if (typeof total === 'number') {
patientListHasMore.value = merged.length < total
} else {
patientListHasMore.value = list.length >= patientListPageSize.value
}
if (patientListHasMore.value) {
patientListPage.value = typeof pageNum === 'number' ? pageNum + 1 : requestPage + 1
}
} finally {
patientListLoading.value = false
patientListLoadingMore.value = false
if (showLoading) {
nextTick(() => {
uni.hideLoading()
})
}
}
}
const onScrollToLower = () => {
void patientListByGBKPage({ reset: false, showLoading: false })
}
};
onLoad((options) => {
if(options.from == 'chatMsg'){
from.value = 'chatMsg';
uni.showLoading({
title: '加载中...',
mask: true
})
if(options.from ){
from.value =options.from;
}
// 读取已选中的成员ID
try {
@@ -79,9 +187,11 @@
uni.removeStorageSync('preSelectedIds')
}
} catch (e) {}
void patientListByGBKPage({ reset: true, showLoading: true });
})
onShow(() => {
patientListByGBK();
// 根据已选中的ID更新selectedDetail
updateSelectedDetail();
});
@@ -111,11 +221,51 @@
}
}
const getAvatarSrc = (patient) => {
if (patient?._avatarLoadError) return defaultImg
return patient?.photo ? (docUrl + patient.photo) : defaultImg
}
const handleAvatarError = (patient) => {
if (!patient) return
patient._avatarLoadError = true
}
const toggleSelectAll = () => {
if (!patientList.value.length) return
if (isAllSelected.value) {
selectedIds.value = []
selectedDetail.value = []
return
}
selectedIds.value = patientList.value.map(item => item.uuid)
selectedDetail.value = patientList.value.map(item => ({
uuid: item.uuid,
realName: item?.realName || '',
photo: item?.photo || '',
nickName: item?.nickName || ''
}))
}
const onSearch = () => {
patientList.value = patientList.value.filter(p => p.realName.indexOf(keyword.value) !== -1 || (p.nickname && p.nickname.indexOf(keyword.value) !== -1) || p.mobile.indexOf(keyword.value) !== -1)
if(keyword.value.replace(/\s/g, "")){
availablePatientList.value = patientList.value.filter(p => p.realName.indexOf(keyword.value) !== -1 || (p.nickname && p.nickname.indexOf(keyword.value) !== -1) || p.mobile.indexOf(keyword.value) !== -1)
}else{
availablePatientList.value = patientList.value
}
}
const goBack = () => uni.navigateBack()
const confirmSelect = () => {
if(from.value == 'chatMsg' || from.value == 'videoDetail'){
if(selectedIds.value.length >200){
uni.showToast({
title: '选择人数不能超过200',
icon: 'none'
})
return;
}
}
const payload = { ids: selectedIds.value, list: selectedDetail.value }
// 通过事件通道回传
try {
@@ -143,14 +293,17 @@
<style lang="scss" scoped>
.select-page{
min-height: 100vh; background:#fefefe;
min-height: 100vh; background:#fefefe;overflow: hidden;
}
.confirm-text{ color:#fff; font-size: 28rpx;white-space: nowrap; }
.confirm-btn{ background:#7f7f7f; padding: 10rpx 18rpx; border-radius: 26rpx; }
.confirm-btn.active{ background:#8B2316; }
.search-bar{
border: 2rpx solid #eee;
margin: 20rpx 30rpx; display:flex; align-items:center; gap: 16rpx;
margin: 0rpx 30rpx; display:flex;
margin-top: calc(var(--status-bar-height) + 64px);
align-items:center; gap: 16rpx;
.input-wrap{ flex:1; background:#fff; border-radius: 12rpx; padding: 16rpx 20rpx; }
.search-input{ font-size: 28rpx; color:#333; }
.ph{ color:#bfbfbf; }
@@ -161,7 +314,14 @@
width: 88rpx; height: 72rpx; background:#fff;
}
}
.list{ border-radius: 12rpx; }
.list{
border-radius: 12rpx;
position: fixed;
left: 0;
right: 0;
top: calc(var(--status-bar-height) + 64px + 94rpx);
bottom: 130rpx;
}
.item{background:#fff; display:flex; align-items:center;padding: 24rpx 30rpx; border-bottom: 2rpx solid #eee; }
.avatar{ width: 96rpx; height:96rpx; border-radius: 16rpx; background:#ffe; }
.name{ flex:1; margin-left: 20rpx; font-size: 32rpx; color:#333; }
@@ -169,4 +329,24 @@
.circle{ width: 40rpx; height: 40rpx; border-radius: 50%; border: 2rpx solid #cfcfcf; }
.circle.active{ background:#8B2316; border-color:#8B2316; position: relative; }
.circle.active::after{ content:''; position:absolute; left: 14rpx; top: 6rpx; width: 10rpx; height: 18rpx; border: 4rpx solid #fff; border-top: 0; border-left: 0; transform: rotate(45deg); }
.bottom-actions{
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
border-top: 1rpx solid #f0f0f0;
padding: 20rpx 30rpx 20rpx;
}
.select-all-btn{
height: 88rpx;
border-radius: 12rpx;
color: #8B2316;
background: #fff;
border: 2rpx solid #8B2316;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
</style>
@@ -1,12 +1,12 @@
<template>
<view class="select-page">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="选择患者"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -16,6 +16,7 @@
</view>
</template> -->
</uni-nav-bar>
</view>
<!-- 搜索框 -->
<view class="search-bar">
@@ -30,8 +31,8 @@
<!-- 列表 -->
<scroll-view class="list" scroll-y>
<view class="item" @click="toggle(p.uuid)" v-for="p in availablePatientList" :key="p.uuid">
<image class="avatar" :src="docUrl + (p.photo || '')" mode="aspectFill" />
<view class="name">{{ p.nickname || p.realName }}</view>
<image class="avatar" :src="getAvatarSrc(p)" mode="aspectFill" @error="handleAvatarError(p)" />
<view class="name">{{ p.realName || p.nickname }}</view>
<view class="check" >
<view class="circle" :class="{ active: selectedIds.includes(p.uuid) }"></view>
</view>
@@ -41,31 +42,44 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed,nextTick } from 'vue'
import docUrl from '@/utils/docUrl.js'
import { onShow,onLoad} from "@dcloudio/uni-app";
import api from '@/api/api.js'
import defaultImg from "@/static/default.png"
const keyword = ref('')
const selectedIds = ref([])
const patientList = ref([])
const selectedDetail = ref([])
const availablePatientList = ref([])
// 计算属性:显示所有患者,但标记已选中的状态
const availablePatientList = computed(() => {
return patientList.value
})
// const availablePatientList = computed(() => {
// return patientList.value
// })
const patientListByGBK = async () => {
const res = await api.patientListByGBK();
if(res.code == 1){
patientList.value = res.data;
availablePatientList.value = res.data;
nextTick(() => {
uni.hideLoading();
});
// if(res.data.length > 1000){
// setTimeout(() => {
// uni.hideLoading();
// }, 5000);
// }else{
// uni.hideLoading();
// }
}
};
onLoad(() => {
uni.showLoading({
title: '加载中...',
mask: true
})
// 读取已选中的成员ID
try {
const preSelected = uni.getStorageSync('preSelectedIds')
@@ -75,9 +89,10 @@
uni.removeStorageSync('preSelectedIds')
}
} catch (e) {}
patientListByGBK();
})
onShow(() => {
patientListByGBK();
// 根据已选中的ID更新selectedDetail
updateSelectedDetail();
});
@@ -114,8 +129,23 @@
}
}
const getAvatarSrc = (patient) => {
if (patient?._avatarLoadError) return defaultImg
return patient?.photo ? (docUrl + patient.photo) : defaultImg
}
const handleAvatarError = (patient) => {
if (!patient) return
patient._avatarLoadError = true
}
const onSearch = () => {
patientList.value = patientList.value.filter(p => p.realName.indexOf(keyword.value) !== -1 || (p.nickname && p.nickname.indexOf(keyword.value) !== -1) || p.mobile.indexOf(keyword.value) !== -1)
if(keyword.value.replace(/\s/g, "")){
availablePatientList.value = patientList.value.filter(p => p.realName.indexOf(keyword.value) !== -1 || (p.nickname && p.nickname.indexOf(keyword.value) !== -1) || p.mobile.indexOf(keyword.value) !== -1)
}else{
availablePatientList.value = patientList.value
}
//patientList.value = patientList.value.filter(p => p.realName.indexOf(keyword.value) !== -1 || (p.nickname && p.nickname.indexOf(keyword.value) !== -1) || p.mobile.indexOf(keyword.value) !== -1)
}
const goBack = () => uni.navigateBack()
const confirmSelect = (id) => {
@@ -135,14 +165,16 @@
<style lang="scss" scoped>
.select-page{
min-height: 100vh; background:#fefefe;
min-height: 100vh; background:#fefefe;overflow: hidden;
}
.confirm-text{ color:#fff; font-size: 28rpx;white-space: nowrap; }
.confirm-btn{ background:#7f7f7f; padding: 10rpx 18rpx; border-radius: 26rpx; }
.confirm-btn.active{ background:#8B2316; }
.search-bar{
border: 2rpx solid #eee;
margin: 20rpx 30rpx; display:flex; align-items:center; gap: 16rpx;
margin: 20rpx 30rpx;
margin-top: calc(var(--status-bar-height) + 64px);
display:flex; align-items:center; gap: 16rpx;
.input-wrap{ flex:1; background:#fff; border-radius: 12rpx; padding: 16rpx 20rpx; }
.search-input{ font-size: 28rpx; color:#333; }
.ph{ color:#bfbfbf; }
+580 -392
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+23 -13
View File
@@ -1,13 +1,13 @@
<template>
<view class="content">
<!-- 顶部导航栏 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="添加随访计划"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -17,6 +17,8 @@
</view>
</template>
</uni-nav-bar>
</view>
<!-- -->
<!-- 患者选择 -->
<view class="form-section" @click="selectPatient">
@@ -102,10 +104,17 @@
const dealIndex=ref(0);
import api from '@/api/api.js'
const from = ref('');
const type = ref('');
onLoad((options) => {
console.log(options)
patientUuid.value = options.uuid;
patientName.value = options.patient_name;
if(options.from){
from.value = options.from;
}
if(options.type){
type.value = options.type;
}
});
const selectedPatient = ref('');
const defaultContent = '请近日来医院复诊、复查';
@@ -140,8 +149,9 @@
show.value=false;
}
const goVisitNote=(idx,note)=>{
let name=patientName.value?encodeURIComponent(patientName.value):'';
navTo({
url: '/pages_app/visitNote/visitNote?idx='+idx+'&note='+encodeURIComponent(note)+'&patient_name='+encodeURIComponent(patientName.value),
url: '/pages_app/visitNote/visitNote?idx='+idx+'&note='+encodeURIComponent(note)+'&patient_name='+encodeURIComponent(name),
events: {
onVisitNoteSubmit: ({ note, idx }) => {
console.log('onVisitNoteSubmit', note, idx)
@@ -186,11 +196,13 @@
const goBack=()=>uni.navigateBack();
const submitPlan=()=>{
addFollowUps();
uni.showToast({ title: '已提交', icon: 'none' });
setTimeout(()=>uni.navigateBack(),1000);
//uni.showToast({ title: '已提交', icon: 'none' });
//setTimeout(()=>uni.navigateBack(),1000);
};
const selectPatient=()=>{
if(from.value=='patientMsg'){
if(type.value == 'patientDetail'){
return false
}else if(from.value=='patientMsg'){
navTo({
url:'/pages_app/selectPatient/selectPatient',
events: {
@@ -216,7 +228,7 @@
if (!isNaN(parsed.getTime())) baseDate = parsed;
}
const nextDate = addMonths(baseDate, 3);
visits.value.push({ id, date: formatDate(nextDate) });
visits.value.push({ id, date: formatDate(nextDate),note:defaultContent });
// 下一帧滚动到新项
nextTick(()=>{ scrollIntoViewId.value = `visit-${id}`; });
};
@@ -305,19 +317,17 @@
}
.content {
background-color: #f5f5f5;
min-height: 100vh;
}
.content {
background-color: #f5f5f5;
min-height: 100vh;
}
.nav-right { display: flex; align-items: center; }
.submit-text { font-size: 32rpx; color: #8B2316; font-weight: 500; }
.form-section {
margin-top: 20rpx;
margin-top: calc(var(--status-bar-height) + 44px);
// margin-top: 20rpx;
display: flex; align-items: center; justify-content: space-between;
padding: 30rpx; background: #ffffff; border-bottom: 1rpx solid #f0f0f0;
.section-label { width: 160rpx; flex-shrink: 0; }
+19 -7
View File
@@ -1,12 +1,12 @@
<template>
<view class="visit-note-page">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="添加随访计划"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -16,11 +16,13 @@
</view>
</template>
</uni-nav-bar>
</view>
<view class="contentbox">
<!-- 患者行 -->
<view class="row" @click="noop">
<text class="row-label">患者</text>
<text class="row-value">{{ patientName }}</text>
<text class="row-value">{{ patientName?patientName:'' }}</text>
</view>
<!-- 标题栏 -->
@@ -28,7 +30,8 @@
<!-- 输入框区域 -->
<view class="note-box">
<textarea class="note-textarea" v-model.trim="note" placeholder="请填写随访内容" auto-height :maxlength="200" />
<textarea class="note-textarea" v-model.trim="note" placeholder="请填写随访内容" auto-height :maxlength="200" :adjust-position="false"/>
</view>
</view>
</view>
</template>
@@ -41,7 +44,9 @@
const note = ref('请近日来医院复诊、复查')
const idx = ref(0)
onLoad((options) => {
patientName.value =decodeURIComponent( options?.patient_name )
console.log(options.patient_name)
patientName.value =options.patient_name ?decodeURIComponent(options.patient_name):'';
console.log(patientName.value)
if (options?.note) note.value = decodeURIComponent(options.note)
if (options?.idx) idx.value = options.idx
@@ -62,8 +67,15 @@
</script>
<style lang="scss" scoped>
.contentbox{
position: fixed;
left: 0;
right:0;
overflow-y: scroll;
top: calc(var(--status-bar-height) + 44px);
}
.visit-note-page{
min-height: 100vh; background:#f5f5f5;
background:#f5f5f5;
}
.nav-right{ display:flex; align-items:center; }
.submit-text{ color:#8B2316; font-size: 30rpx; }
@@ -78,7 +90,7 @@
.note-box{ background:#fff; padding: 20rpx 24rpx; }
.note-textarea{
width: 100%; min-height: 260rpx; background:#e5e5e5; border-radius: 0; padding: 20rpx; font-size: 32rpx; color:#111;
box-sizing: border-box;width: 100%; min-height: 260rpx; background:#e5e5e5; border-radius: 0; padding: 20rpx; font-size: 32rpx; color:#111;
}
</style>
+53 -11
View File
@@ -1,12 +1,14 @@
<template>
<view class="content">
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
left-icon="left"
title="随访计划"
@clickLeft="goBack"
fixed
color="#8B2316"
height="180rpx"
:border="false"
backgroundColor="#eee"
>
@@ -17,7 +19,8 @@
</view>
</template>
</uni-nav-bar>
<view class="plan">
</view>
<scroll-view
class="plan-scroll"
scroll-y="true"
@@ -75,20 +78,33 @@
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { onShow,onLoad } from '@dcloudio/uni-app'
import dayjs from 'dayjs'
import api from '@/api/api.js'
import navTo from '@/utils/navTo.js'
import dayImg from '@/static/visit_data11.png'
import planImg from '@/static/visitplan.png'
import lineImg from '@/static/item_visitplan_fg.png'
const patientUuid = ref('')
const patientName = ref('')
const type = ref('')
onLoad((options) => {
if(options.uuid){
patientUuid.value = options.uuid
}
if(options.patient_name){
patientName.value = options.patient_name
}
if(options.type){
type.value = options.type
}
})
const formatYearMonth = (input) => {
if (!input) return ''
const d = dayjs(input)
@@ -159,8 +175,31 @@ const goBack = () => uni.navigateBack()
const showAddMenuFlag = ref(false)
const showAddMenu = () => { showAddMenuFlag.value = true }
const hideAddMenu = () => { showAddMenuFlag.value = false }
const addFollowUpPlan = () => { showAddMenuFlag.value = false; uni.navigateTo({ url: '/pages_app/visit/visit?from=visitPlan' }) }
const addSchedule = () => { showAddMenuFlag.value = false; uni.navigateTo({ url: '/pages_app/schedule/schedule' }) }
const addFollowUpPlan = () => {
showAddMenuFlag.value = false;
if(type.value == 'patientDetail'){
uni.navigateTo({
url: '/pages_app/visit/visit?uuid='+patientUuid.value+'&patient_name='+patientName.value+'&type=patientDetail'
})
}else{
uni.navigateTo({
url: '/pages_app/visit/visit?from=visitPlan'
})
}
}
const addSchedule = () => {
showAddMenuFlag.value = false;
if(type.value == 'patientDetail'){
uni.navigateTo({
url: '/pages_app/schedule/schedule?uuid='+patientUuid.value+'&patient_name='+patientName.value+'&type=patientDetail'
})
}else{
uni.navigateTo({
url: '/pages_app/schedule/schedule'
})
}
}
const goFollowDetail = (raw) => { if (!raw) return; navTo({ url: `/pages_app/followDetail/followDetail?followUpUuid=${encodeURIComponent(raw.uuid || '')}&patient_name=${raw.patientname}` }) }
onShow(() => {
@@ -182,8 +221,8 @@ onShow(() => {
.content { background-color: #f5f5f5; height: 100vh; overflow-y: hidden; }
.linebox{ margin:0 20rpx; }
.plan{ position: fixed; top: 180rpx; left: 0; right: 0; bottom:0rpx; }
.plan-scroll { height: 100%; }
.plan-scroll{ position: fixed; top: calc(var(--status-bar-height) + 44px); left: 0; right: 0; bottom:0rpx;z-index:1; }
.plan-list{ padding-bottom: 20rpx; }
.plan-group{ background:#f5f5f5; }
.group-header{ text-align: center; background:#e4e4e4; color:#333; font-size: 30rpx; padding: 20rpx 30rpx; }
@@ -200,7 +239,10 @@ onShow(() => {
.floating-add-btn { position: fixed; bottom: 140rpx; right: 30rpx; background-color: #8B2316; border-radius: 50%; width: 100rpx; height: 100rpx; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:999; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1); }
.floating-add-btn .btn-text{ font-size:20rpx; color:#fff; margin-top:4rpx; }
.add-menu-popup { position: fixed; top:0; left:0; right:0; bottom:0; background-color: rgba(0,0,0,0); display:flex; align-items:center; justify-content:center; z-index:1000; }
.menu-content { background:#fff; border-radius:20rpx; box-shadow:0 8rpx 24rpx rgba(0,0,0,0.2); width:80%; max-width:400rpx; overflow:hidden; }
.menu-content { background:#fff; border-radius:20rpx; box-shadow:0 8rpx 24rpx rgba(0,0,0,0.2); width:80%; max-width:400rpx; overflow:hidden; position: fixed;
top: calc(var(--status-bar-height) + 48px);
z-index: 9999;
right: 20rpx;}
.menu-item { display:flex; align-items:center; padding:30rpx 40rpx; border-bottom:1rpx solid #f0f0f0; }
.menu-item:last-child{ border-bottom:none; }
.menu-text{ font-size:32rpx; color:#333; margin-left:20rpx; }
+396 -207
View File
@@ -2,7 +2,6 @@
<view class="container" :style="{ backgroundColor: backgroundColor }">
<!-- 分享弹窗 -->
<view class="navbox">
<view class="status_bar"></view>
<uni-nav-bar
@@ -16,14 +15,18 @@
<template #right>
<view class="nav-actions">
<view class="collect-img" @click="shareToggle" v-if="canShare">
<image class="share-img-icon" :src="shareIcon" mode="aspectFill" />
<image
class="share-img-icon"
:src="shareIcon"
mode="aspectFill"
/>
</view>
</view>
</template>
</uni-nav-bar>
</view>
</view>
<uni-popup ref="shareRef" type="bottom" safeArea backgroundColor="#fff">
</view>
<uni-popup ref="shareRef" type="bottom" safeArea backgroundColor="#fff">
<view class="share-popup">
<view class="share-title">分享到</view>
<view class="share-content">
@@ -51,12 +54,11 @@
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref } from "vue";
import { onLoad,onHide,onShow,onUnload} from "@dcloudio/uni-app";
import { onLoad, onHide, onShow, onUnload } from "@dcloudio/uni-app";
import navBar from "@/components/navBar/navBar.vue";
import shareIcon from "@/static/icon_share.png";
import sinaImg from "@/static/share_sina.png";
@@ -70,23 +72,24 @@ const hasBg = ref(false);
const canShare = ref(false);
const summary = ref("");
const shareLink = ref("");
const shareImg=ref("");
const shareImg = ref("");
const shareTitle = ref("");
const type = ref("");
const tags = ref("");
const reportTitle = ref("");
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({
url: '/pages/index/index'
url: "/pages/index/index",
});
}
},
});
};
onShow(() => {
uni.hideLoading();
// console.log('show');
// plus.screen.lockOrientation('portrait-primary');
});
onUnload(() => {
// console.log('onUnload');
@@ -95,17 +98,22 @@ onUnload(() => {
onHide(() => {
// console.log('onHide');
// plus.screen.lockOrientation('landscape-primary');
});
onLoad((query) => {
if(query.title){
console.log(query);
if (query.title) {
title.value = query.title;
}
if(query.share == 1){
if (query.tags) {
tags.value = query.tags;
}
if (query.type) {
type.value = query.type;
}
if (query.share == 1) {
canShare.value = true;
}
if(query.bg == 1){
if (query.bg == 1) {
hasBg.value = true;
backgroundColor.value = "linear-gradient(to bottom,'#fff' , '#398775')";
@@ -116,30 +124,59 @@ onLoad((query) => {
success: (sysinfo) => {
height = sysinfo.windowHeight; //自行修改
},
complete: () => {}
complete: () => {},
});
const raw = query && (query.url || "");
try {
safeUrl.value = decodeURIComponent(raw);
shareImg.value = decodeURIComponent(query.imgPath);
if(query.type == "live_yugao"){
shareImg.value = query.imgPath?decodeURIComponent(query.imgPath):'https://doc.igandan.com/app/html/img/2016/20160714132557.png';
if (query.type == "live_yugao") {
shareTitle.value = query.sharetitle;
summary.value = "分享一篇来自“肝胆相照”的会议预告:"+shareTitle.value;
}else if(query.type == "live_old"){
reportTitle.value = shareTitle.value;
summary.value = "分享一篇来自“肝胆相照”的会议预告:" + shareTitle.value;
console.log(reportTitle.value);
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "detail_page",
page_type: "会议直播",
resource_name:reportTitle.value,
resource_option:tags.value,
},
},
(ret) => {
console.log(ret);
});
} else if (query.type == "live_old") {
shareTitle.value = query.sharetitle;
summary.value = "分享一篇来自“肝胆相照”的历史会议:"+shareTitle.value;
}else if(query.type == "live"){
shareTitle.value = '肝胆相照直播:'+query.sharetitle;
summary.value = "分享一篇来自“肝胆相照”的历史会议:" + shareTitle.value;
} else if (query.type == "live") {
shareTitle.value = "肝胆相照直播:" + query.sharetitle;
reportTitle.value = query.sharetitle;
summary.value = shareTitle.value;
console.log(reportTitle.value);
// uni.sendNativeEvent("portraitReport",{
// msg:{
// event_nickname: "detail_page",
// page_type: "会议直播",
// resource_name:reportTitle.value,
// resource_option:tags.value,
// },
// },
// (ret) => {
// console.log(ret);
// });
}else if(query.type=='news'){
shareTitle.value = query.sharetitle;
summary.value ='';
}
// #ifdef APP-PLUS
const resInfo = uni.getSystemInfoSync(); // 或者使用 uni.getSystemInfo({...}).then(res => {...})
let statusBarHeight = resInfo.statusBarHeight;
let wv = plus.webview.create("", "custom-webview", {
top: "140rpx",
bottom:"0rpx",
top: statusBarHeight + 44 + "px",
bottom: "0rpx",
});
wv.loadURL( safeUrl.value);
wv.loadURL(safeUrl.value);
let pages = getCurrentPages();
let page = pages[pages.length - 1];
@@ -148,44 +185,49 @@ onLoad((query) => {
currentWebview.append(wv);
setTimeout(function () {
wv = currentWebview.children()[0];
wv.setStyle({ top: 80,height: height- 80,bottom:0,scalable:true, videoFullscreen: 'landscape',zIndex:-1});
wv.setStyle({
top: statusBarHeight + 44 + "px",
height: height - 80,
bottom: 0,
scalable: true,
videoFullscreen: "landscape",
zIndex: -1,
});
}, 300); //如果是页面初始化调用时,需要延时一下
wv.onloaded = (e) => {
wv.show();
};
uni.onWindowResize((res) => {
if (res.size.windowHeight < height) {
console.log(res.size.windowHeight);
console.log(height)
console.log('键盘高度',res.height);
setTimeout(function() {
wv.setStyle({ //设置web-view距离顶部的距离以及自己的高度,单位为px
top:80,
videoFullscreen: 'landscape',
height: height-res.size.windowHeight+100,
console.log(height);
console.log("键盘高度", res.height);
setTimeout(function () {
wv.setStyle({
//设置web-view距离顶部的距离以及自己的高度,单位为px
top: statusBarHeight + 44 + "px",
videoFullscreen: "landscape",
height: height - res.size.windowHeight + 100,
scalable: true, //webview的页面是否可以缩放,双指放大缩小
})
},0);
});
}, 0);
//高度缩小
//如页面初始化调用需要写延迟
} else {
setTimeout(function() {
wv.setStyle({ //设置web-view距离顶部的距离以及自己的高度,单位为px
top:80,
height:height-80,
videoFullscreen: 'landscape',
setTimeout(function () {
wv.setStyle({
//设置web-view距离顶部的距离以及自己的高度,单位为px
top: statusBarHeight + 44 + "px",
height: height - 80,
videoFullscreen: "landscape",
scalable: true, //webview的页面是否可以缩放,双指放大缩小
})
},0);
});
}, 0);
}
})
});
// 创建并展示原生弹窗
createNativePopup();
@@ -193,8 +235,6 @@ onLoad((query) => {
} catch (e) {
safeUrl.value = raw;
}
});
const shareRef = ref(null);
shareRef.value?.open();
@@ -227,6 +267,15 @@ const closeShare = () => {
const nativeMaskView = ref(null);
const nativePopupView = ref(null);
const popupShowing = ref(false);
/** 原生分享面板:防止同一次手势内重复触发(兜底) */
let nativePanelLastActionAt = 0;
const NATIVE_PANEL_ACTION_GAP_MS = 400;
function tryCommitNativePanelAction(fn) {
const t = Date.now();
if (t - nativePanelLastActionAt < NATIVE_PANEL_ACTION_GAP_MS) return;
nativePanelLastActionAt = t;
fn();
}
// 保存弹窗尺寸信息,供事件处理使用
const popupInfo = ref({
screenW: 0,
@@ -235,7 +284,7 @@ const popupInfo = ref({
cancelH: 0,
contentTop: 0,
iconSize: 0,
textSize: 0
textSize: 0,
});
function createNativePopup() {
@@ -260,78 +309,102 @@ function createNativePopup() {
cancelH,
contentTop,
iconSize,
textSize
textSize,
};
// 遮罩
nativeMaskView.value = new plus.nativeObj.View('native-share-mask', {
left: '0px',
top: '0px',
width: screenW + 'px',
height: screenH + 'px',
nativeMaskView.value = new plus.nativeObj.View(
"native-share-mask",
{
left: "0px",
top: "0px",
width: screenW + "px",
height: screenH + "px",
zindex: 99998,
touchable: true,
interceptTouchEvent: true
}, [
{ tag: 'rect', id: 'mask', position: { left: '0px', top: '0px', width: screenW + 'px', height: screenH + 'px' }, color: 'rgba(0,0,0,0.5)' }
]);
interceptTouchEvent: true,
},
[
{
tag: "rect",
id: "mask",
position: {
left: "0px",
top: "0px",
width: screenW + "px",
height: screenH + "px",
},
color: "rgba(0,0,0,0.5)",
},
]
);
// 面板
nativePopupView.value = new plus.nativeObj.View('native-share-panel', {
left: '0px',
top: (screenH - panelH) + 'px',
width: screenW + 'px',
height: panelH + 'px',
nativePopupView.value = new plus.nativeObj.View(
"native-share-panel",
{
left: "0px",
top: screenH - panelH + "px",
width: screenW + "px",
height: panelH + "px",
zindex: 99999,
touchable: true,
interceptTouchEvent: true
}, []);
interceptTouchEvent: true,
},
[]
);
// 背景+圆角
nativePopupView.value.drawRect({
color: '#FFFFFF',
nativePopupView.value.drawRect(
{
color: "#FFFFFF",
radius: radius,
rectStyles: {}
}, {
left: uni.upx2px(0) + 'px',
top: uni.upx2px(0) + 'px',
width: screenW + 'px',
height: panelH + 'px'
});
rectStyles: {},
},
{
left: uni.upx2px(0) + "px",
top: uni.upx2px(0) + "px",
width: screenW + "px",
height: panelH + "px",
}
);
// 标题
nativePopupView.value.drawText('分享到', {
left: '0px',
top: uni.upx2px(20) + 'px',
width: screenW + 'px',
height: titleH + 'px'
}, {
size: uni.upx2px(32) + 'px',
color: '#333333',
align: 'center',
verticalAlign: 'middle',
weight: '500'
});
nativePopupView.value.drawText(
"分享到",
{
left: "0px",
top: uni.upx2px(20) + "px",
width: screenW + "px",
height: titleH + "px",
},
{
size: uni.upx2px(32) + "px",
color: "#333333",
align: "center",
verticalAlign: "middle",
weight: "500",
}
);
const items = [
{ id: "it-wechat", text: "微信" },
{ id: "it-moments", text: "朋友圈" },
// { id: 'it-weibo', text: '微博' }
];
// 三个分享项区域(使用图标)
const itemW = screenW / 3;
const itemW = screenW / items.length;
const iconTop = contentTop;
const textTop = iconTop + iconSize + uni.upx2px(16);
console.log('图标布局信息:', {
console.log("图标布局信息:", {
screenW,
itemW,
iconSize,
iconTop,
contentTop
contentTop,
});
// 图标文件名映射(直接使用文件名,避免路径转换问题)
const iconFiles = [
'share_weixin.png', // 微信
'share_wxc.png', // 朋友圈
'share_sina.png' // 微博
];
const items = [
{ id: 'it-wechat', text: '微信' },
{ id: 'it-moments', text: '朋友圈' },
{ id: 'it-weibo', text: '微博' }
"share_weixin.png", // 微信
"share_wxc.png", // 朋友圈
"share_sina.png", // 微博
];
items.forEach((it, idx) => {
@@ -343,116 +416,148 @@ function createNativePopup() {
const iconTopPx = Math.round(iconTop);
const iconSizePx = iconSize;
console.log(`图标 ${idx} (${it.text}): left=${left}, itemW=${itemW.toFixed(2)}, iconLeft=${iconLeftPx}, iconTop=${iconTopPx}, iconSize=${iconSizePx}`);
console.log(
`图标 ${idx} (${it.text}): left=${left}, itemW=${itemW.toFixed(
2
)}, iconLeft=${iconLeftPx}, iconTop=${iconTopPx}, iconSize=${iconSizePx}`
);
// 构建图标本地路径
// 说明:
// - 在 Android 上使用 convertLocalFileSystemURL 可以得到 file:// 路径;
// - 但在 iOS 上,plus.nativeObj.View.drawBitmap 对 file:// 路径兼容性较差,
// 更推荐直接使用 "_www" 相对路径。
// 这里统一使用 "_www/static/xxx.png" 相对路径,兼容 Android 和 iOS。
let iconPath = null;
try {
// 直接使用静态资源路径
const iconFile = iconFiles[idx];
iconPath = plus.io.convertLocalFileSystemURL('_www/static/' + iconFile);
// 使用 _www 相对路径,避免 iOS 下 file:// 路径导致图片不显示
iconPath = "_www/static/" + iconFile;
console.log(`图标 ${it.text} 路径: ${iconPath}`);
} catch (e) {
console.log('构建图标路径失败:', e);
console.log("构建图标路径失败:", e);
iconPath = null;
}
// 绘制图标
if (iconPath) {
try {
console.log(`绘制图标 ${it.text}: 位置=(${iconLeftPx}, ${iconTopPx}), 大小=${iconSizePx}x${iconSizePx}`);
console.log(
`绘制图标 ${it.text}: 位置=(${iconLeftPx}, ${iconTopPx}), 大小=${iconSizePx}x${iconSizePx}`
);
// 使用 drawBitmap 绘制图标
// 尝试不同的参数格式,确保图标不变形且正确显示
// 格式1: drawBitmap(src, position, options)
try {
nativePopupView.value.drawBitmap(iconPath, {}, {
left: iconLeftPx + 'px',
top: iconTopPx + 'px',
width: iconSizePx + 'px',
height: iconSizePx + 'px'
});
nativePopupView.value.drawBitmap(
iconPath,
{},
{
left: iconLeftPx + "px",
top: iconTopPx + "px",
width: iconSizePx + "px",
height: iconSizePx + "px",
}
);
} catch (e1) {
// 如果格式1失败,尝试格式2: drawBitmap(src, position)
try {
nativePopupView.value.drawBitmap(iconPath, {
left: iconLeftPx + 'px',
top: iconTopPx + 'px',
width: iconSizePx + 'px',
height: iconSizePx + 'px'
left: iconLeftPx + "px",
top: iconTopPx + "px",
width: iconSizePx + "px",
height: iconSizePx + "px",
});
} catch (e2) {
console.log('drawBitmap 两种格式都失败:', e1, e2);
console.log("drawBitmap 两种格式都失败:", e1, e2);
throw e2;
}
}
} catch (e) {
console.log('绘制图标失败,使用圆形色块代替:', e);
console.log("绘制图标失败,使用圆形色块代替:", e);
// 如果图标加载失败,使用圆形色块作为后备
const bgColor = idx === 2 ? '#E6162D' : '#07C160';
nativePopupView.value.drawRect({
const bgColor = idx === 2 ? "#E6162D" : "#07C160";
nativePopupView.value.drawRect(
{
color: bgColor,
radius: Math.round(iconSize / 2)
}, {
left: Math.round(iconLeft) + 'px',
top: Math.round(iconTop) + 'px',
width: Math.round(iconSize) + 'px',
height: Math.round(iconSize) + 'px'
});
radius: Math.round(iconSize / 2),
},
{
left: Math.round(iconLeft) + "px",
top: Math.round(iconTop) + "px",
width: Math.round(iconSize) + "px",
height: Math.round(iconSize) + "px",
}
);
}
} else {
// 如果没有图标路径,使用圆形色块
const bgColor = idx === 2 ? '#E6162D' : '#07C160';
nativePopupView.value.drawRect({
const bgColor = idx === 2 ? "#E6162D" : "#07C160";
nativePopupView.value.drawRect(
{
color: bgColor,
radius: Math.round(iconSize / 2)
}, {
left: Math.round(iconLeft) + 'px',
top: Math.round(iconTop) + 'px',
width: Math.round(iconSize) + 'px',
height: Math.round(iconSize) + 'px'
});
radius: Math.round(iconSize / 2),
},
{
left: Math.round(iconLeft) + "px",
top: Math.round(iconTop) + "px",
width: Math.round(iconSize) + "px",
height: Math.round(iconSize) + "px",
}
);
}
// 绘制文字
nativePopupView.value.drawText(it.text, {
left: left + 'px',
top: textTop + 'px',
width: itemW + 'px',
height: textSize + uni.upx2px(20) + 'px'
}, {
size: textSize + 'px',
color: '#666666',
align: 'center',
verticalAlign: 'top'
});
nativePopupView.value.drawText(
it.text,
{
left: left + "px",
top: textTop + "px",
width: itemW + "px",
height: textSize + uni.upx2px(20) + "px",
},
{
size: textSize + "px",
color: "#666666",
align: "center",
verticalAlign: "top",
}
);
});
// 取消按钮分割线
const cancelTop = panelH - cancelH;
nativePopupView.value.drawRect({ color: '#F0F0F0' }, {
left: '0px',
top: (cancelTop - 1) + 'px',
width: screenW + 'px',
height: '1px'
});
nativePopupView.value.drawText('取消', {
left: '0px',
top: cancelTop + 'px',
width: screenW + 'px',
height: cancelH + 'px'
}, {
size: uni.upx2px(32) + 'px',
color: '#333333',
align: 'center',
verticalAlign: 'middle'
});
nativePopupView.value.drawRect(
{ color: "#F0F0F0" },
{
left: "0px",
top: cancelTop - 1 + "px",
width: screenW + "px",
height: "1px",
}
);
nativePopupView.value.drawText(
"取消",
{
left: "0px",
top: cancelTop + "px",
width: screenW + "px",
height: cancelH + "px",
},
{
size: uni.upx2px(32) + "px",
color: "#333333",
align: "center",
verticalAlign: "middle",
}
);
// 事件处理函数
const handleMaskClick = () => {
console.log('遮罩被点击');
console.log("遮罩被点击");
closeNativePopup();
};
const handlePanelClick = (e) => {
console.log('面板被点击', e);
console.log("面板被点击", e);
const info = popupInfo.value;
// 获取点击坐标
@@ -474,11 +579,11 @@ function createNativePopup() {
y = e.changedTouches[0].clientY || e.changedTouches[0].pageY || 0;
}
console.log('原始坐标:', x, y);
console.log('屏幕信息:', info);
console.log("原始坐标:", x, y);
console.log("屏幕信息:", info);
if (!x || !y) {
console.log('坐标无效');
console.log("坐标无效");
return;
}
@@ -492,70 +597,103 @@ function createNativePopup() {
// 相对于视图的坐标,直接使用
relativeY = y;
relativeX = x;
console.log('使用相对于视图的坐标:', relativeX, relativeY);
} else if (y >= (info.screenH - info.panelH)) {
console.log("使用相对于视图的坐标:", relativeX, relativeY);
} else if (y >= info.screenH - info.panelH) {
// 相对于屏幕的坐标,转换为相对于视图的坐标
relativeY = y - (info.screenH - info.panelH);
relativeX = x;
console.log('使用相对于屏幕的坐标,转换为相对视图:', relativeX, relativeY);
console.log(
"使用相对于屏幕的坐标,转换为相对视图:",
relativeX,
relativeY
);
} else {
// 可能是其他情况,尝试直接使用
console.log('坐标类型不确定,尝试直接使用');
console.log("坐标类型不确定,尝试直接使用");
}
// 取消区域(底部取消按钮)- 相对于视图
const cancelTop = info.panelH - info.cancelH;
console.log('取消区域判断: relativeY =', relativeY, ', cancelTop =', cancelTop);
console.log(
"取消区域判断: relativeY =",
relativeY,
", cancelTop =",
cancelTop
);
if (relativeY >= cancelTop) {
console.log('点击了取消按钮');
closeNativePopup();
console.log("点击了取消按钮");
tryCommitNativePanelAction(() => closeNativePopup());
return;
}
// 分享项区域(图标+文字区域)- 相对于视图
const shareAreaTop = info.contentTop;
const shareAreaBottom = shareAreaTop + info.iconSize + info.textSize + uni.upx2px(20);
const shareAreaBottom =
shareAreaTop + info.iconSize + info.textSize + uni.upx2px(20);
console.log('分享区域: relativeY =', relativeY, ', 范围 =', shareAreaTop, '到', shareAreaBottom);
console.log(
"分享区域: relativeY =",
relativeY,
", 范围 =",
shareAreaTop,
"到",
shareAreaBottom
);
if (relativeY >= shareAreaTop && relativeY <= shareAreaBottom) {
// 计算点击的是第几个分享项(0:微信, 1:朋友圈, 2:微博)
const itemWidth = info.screenW / 3;
const itemWidth = info.screenW / items.length;
const idx = Math.min(2, Math.max(0, Math.floor(relativeX / itemWidth)));
console.log('点击了分享项:', idx, ', x =', relativeX, ', itemWidth =', itemWidth);
console.log(
"点击了分享项:",
idx,
", x =",
relativeX,
", itemWidth =",
itemWidth
);
if (idx === 0) {
console.log('执行微信分享');
console.log("执行微信分享");
tryCommitNativePanelAction(() => {
shareToWechat();
closeNativePopup();
});
} else if (idx === 1) {
console.log('执行朋友圈分享');
console.log("执行朋友圈分享");
tryCommitNativePanelAction(() => {
shareToMoments();
closeNativePopup();
});
} else if (idx === 2) {
console.log('执行微博分享');
shareToWeibo();
closeNativePopup();
console.log("执行微博分享");
// shareToWeibo();
// closeNativePopup();
}
} else {
console.log('点击位置不在分享区域内, relativeY =', relativeY, ', 需要范围:', shareAreaTop, '到', shareAreaBottom);
console.log(
"点击位置不在分享区域内, relativeY =",
relativeY,
", 需要范围:",
shareAreaTop,
"到",
shareAreaBottom
);
}
};
// 同时监听 click 和 touchstart 事件
nativeMaskView.value.addEventListener('click', handleMaskClick, false);
nativeMaskView.value.addEventListener('touchstart', handleMaskClick, false);
// 仅监听 click:若同时绑定 touchstart/touchend,一次点击会触发多次,导致分享重复执行
nativeMaskView.value.addEventListener("click", handleMaskClick, false);
nativePopupView.value.addEventListener('click', handlePanelClick, false);
nativePopupView.value.addEventListener('touchstart', handlePanelClick, false);
nativePopupView.value.addEventListener('touchend', handlePanelClick, false);
nativePopupView.value.addEventListener("click", handlePanelClick, false);
// #endif
}
function showNativePopup() {
// #ifdef APP-PLUS
if (popupShowing.value) return;
nativePanelLastActionAt = 0;
if (!nativeMaskView.value || !nativePopupView.value) {
createNativePopup();
}
@@ -577,15 +715,32 @@ function closeNativePopup() {
// 分享到微信
const shareToWechat = () => {
// #ifdef APP-PLUS
if(type.value == "live" || type.value == "live_yugao"){
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "share",
page_type: "会议直播",
resource_name:reportTitle.value,
resource_option:tags.value,
},
});
}
// 使用系统分享
uni.downloadFile({
url: shareImg.value,
success: function (res) {
uni.compressImage({
src: res.tempFilePath,
quality: 60,
success: function (res2) {
uni.share({
provider: "weixin",
scene: "WXSceneSession",
type: 0,
title: shareTitle.value,
summary:summary.value,
summary: summary.value,
href: safeUrl.value,
imageUrl: shareImg.value,
imageUrl: res2.tempFilePath,
success: function (res) {
console.log("success:" + JSON.stringify(res));
},
@@ -593,6 +748,11 @@ const shareToWechat = () => {
console.log("fail:" + JSON.stringify(err));
},
});
},
});
},
});
// #endif
// #ifdef H5
@@ -651,10 +811,14 @@ onHide(() => {
});
onUnload(() => {
// #ifdef APP-PLUS
try{
nativeMaskView.value && nativeMaskView.value.close && nativeMaskView.value.close();
nativePopupView.value && nativePopupView.value.close && nativePopupView.value.close();
}catch(e){}
try {
nativeMaskView.value &&
nativeMaskView.value.close &&
nativeMaskView.value.close();
nativePopupView.value &&
nativePopupView.value.close &&
nativePopupView.value.close();
} catch (e) {}
nativeMaskView.value = null;
nativePopupView.value = null;
popupShowing.value = false;
@@ -663,14 +827,31 @@ onUnload(() => {
// 分享到朋友圈
const shareToMoments = () => {
// #ifdef APP-PLUS
if(type.value == "live" || type.value == "live_yugao"){
uni.sendNativeEvent("portraitReport",{
msg:{
event_nickname: "share",
page_type: "会议直播",
resource_name:reportTitle.value,
resource_option:tags.value,
},
});
}
uni.downloadFile({
url: shareImg.value,
success: function (res) {
uni.compressImage({
src: res.tempFilePath,
quality: 60,
success: function (res2) {
uni.share({
provider: "weixin",
scene: "WXSceneTimeline",
type: 0,
title: shareTitle.value,
summary:summary.value,
summary: summary.value,
href: safeUrl.value,
imageUrl: shareImg.value,
imageUrl: res2.tempFilePath,
success: function (res) {
console.log("success:" + JSON.stringify(res));
},
@@ -678,6 +859,14 @@ const shareToMoments = () => {
console.log("fail:" + JSON.stringify(err));
},
});
},
fail: function (err) {
console.log("fail:" + JSON.stringify(err));
},
});
},
});
// #endif
// #ifdef H5
@@ -709,8 +898,8 @@ const shareToWeibo = () => {
uni.share({
provider: "sinaweibo",
type: 0,
title: shareTitle.value ,
summary:summary.value,
title: shareTitle.value,
summary: summary.value,
href: safeUrl.value,
imageUrl: logoImg,
success: function (res) {
@@ -755,7 +944,7 @@ const shareToWeibo = () => {
</script>
<style>
.mask{
.mask {
position: fixed;
top: 0;
left: 0;
@@ -861,7 +1050,7 @@ const shareToWeibo = () => {
.container {
height: 100vh;
position: relative;
z-index:0;
z-index: 0;
overflow: scroll;
}
.tip {

Some files were not shown because too many files have changed in this diff Show More