9.19自定义消息,聊天相关页面

This commit is contained in:
zoujiandong 2025-09-19 17:37:19 +08:00
parent b884a8f6fc
commit ae08b222af
22 changed files with 4543 additions and 70 deletions

View File

@ -331,6 +331,10 @@ export default {
/** nim sdk 登录 */
nim.V2NIMLoginService.login(opts.account, opts.token).then(async () => {
console.log("登录成功");
nim.V2NIMLoginService.on('onKickedOffline', (res) => {
console.log("被踢下线", res);
});
// #ifdef APP-PLUS
/** 初始化音视频通话插件*/
nimCallKit.initConfig(
@ -345,6 +349,7 @@ export default {
if (ret.code != 200) {
// callkit init
} else {
nimCallKit.login(
{
account: opts.account,
@ -355,6 +360,7 @@ export default {
//
} else {
//
}
}
);
@ -396,7 +402,8 @@ export default {
console.log("音视频通话插件退出失败", error);
}
// 退
uni.$UIKitNIM.V2NIMLoginService.logout().then(() => {
uni.$UIKitNIM.V2NIMLoginService.logout().then((res) => {
console.log("退出登录", res);
uni.$UIKitStore.destroy();
customReLaunch({
url: "/pages/Login/index",

View File

@ -493,9 +493,21 @@ const api = {
meetingV2Video(data){
return request('/expertAPI/meetingV2Video', data, 'post', false);
},
listGroupSendMsg(data){
return request('/expertAPI/listGroupSendMsg', data, 'post', false);
},
addGroupSendMsg4YunXin(data){
return request('/expertAPI/addGroupSendMsg4YunXin', data, 'post', false);
},
quickReplyList(data){
return request('/expertAPI/QuickReplyList', data, 'post', false);
},
addQuickReply(data){
return request('/expertAPI/addQuickReply', data, 'post', false);
},
deleteQuickReply(data){
return request('/expertAPI/deleteQuickReply', data, 'post', false);
},
}

View File

@ -1047,6 +1047,66 @@
}
}
},
{
"path": "groupSend/groupSend",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "patientGroup/patientGroup",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "quickReply/quickReply",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "article/article",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "searchArticle/searchArticle",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "video/video",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "uni-app分页",
"app": {
"bounce": "none"
}
}
},
{
"path": "groupMessage/groupMessage",
"style": {

View File

@ -304,7 +304,7 @@
url="/pages_app/patientGroup/patientGroup"
break;
case 'groupMessage':
url="/pages_app/groupMessage/groupMessage"
url="/pages_chat/groupMessage/groupMessage"
break;
case 'qrcode':
url="/pages_app/myCode/myCode"

View File

@ -88,7 +88,7 @@
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 || '' }
return { uuid: id, realName: p?.realName || '', photo: p?.photo || '',nickName: p?.nickName || '' }
})
}
@ -103,7 +103,7 @@
//
selectedIds.value.push(id)
const p = patientList.value.find(x => x.uuid === id)
selectedDetail.value.push({ uuid: id, realName: p?.realName || '', photo: p?.photo || '' })
selectedDetail.value.push({ uuid: id, realName: p?.realName || '', photo: p?.photo || '',nickName: p?.nickName || '' })
}
}
@ -116,7 +116,9 @@
const pages = getCurrentPages()
const curr = pages[pages.length - 1]
const ec = curr?.getOpenerEventChannel?.()
ec?.emit && ec.emit('onPatientsSelected', payload)
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) {}

View File

@ -88,7 +88,7 @@
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 || '' }
return { uuid: id, realName: p?.realName || '', photo: p?.photo || '',nickname: p?.nickname || '' }
})
}
@ -105,7 +105,7 @@
selectedIds.value.push(id)
const p = patientList.value.find(x => x.uuid === id)
selectedDetail.value.push({ uuid: id, realName: p?.realName || '', photo: p?.photo || '' })
let payload = { uuid: id, realName: p?.realName || '', photo: p?.photo || '' }
let payload = { uuid: id, realName: p?.realName || '', photo: p?.photo || '',nickname: p?.nickname || '' }
const pages = getCurrentPages()
const curr = pages[pages.length - 1]
const ec = curr?.getOpenerEventChannel?.()

View File

@ -0,0 +1,668 @@
<template>
<navBar title="患教学堂"></navBar>
<view class="page">
<view class="top">
<!-- 筛选栏 -->
<view class="filter-bar">
<view class="search-box" @click="goToSearch">
<uni-icons type="search" size="16" color="#999"></uni-icons>
<text class="search-text">搜索</text>
</view>
<view class="divider"></view>
<view class="filter-item active" @click="toggleSort">
<text>{{type==1?'最新':'最热'}}</text>
<up-image v-if="type==1" :src="upImg" width="20rpx" height="26rpx" ></up-image>
<up-image v-else :src="downImg" width="20rpx" height="26rpx" ></up-image>
</view>
<!-- <view class="divider"></view>
<view class="filter-item" :class="{ active: isFilterActive }" @click="showFilterPopup">
<text>筛选</text>
<up-image :src="isFilterActive ? filterOn : filter" width="30rpx" height="30rpx" ></up-image>
</view> -->
</view>
</view>
<!-- 筛选弹窗 -->
<view class="filter-popup" v-if="showFilter" @click="hideFilterPopup">
<view class="filter-content" @click.stop>
<!-- 筛选标签网格 -->
<view class="filter-tags">
<view
class="tag-item"
v-for="(tag, index) in filterTags"
:key="index"
:class="{ active: tag.selected }"
@click="toggleTag(index)"
>
{{ tag.DES }}
</view>
</view>
<!-- 底部按钮 -->
<view class="filter-buttons">
<view class="btn-reset" @click="resetFilter">重置</view>
<view class="btn-confirm" @click="confirmFilter">确定</view>
</view>
</view>
</view>
<!-- 文章列表 -->
<scroll-view
class="article-list"
scroll-y
refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
:lower-threshold="100"
>
<view class="article-item" v-for="(article, index) in articleList" :key="article.uuid || index" @click="goToDetail(article)">
<image class="article-image" :src="article.image" mode="aspectFill"></image>
<view class="article-content">
<view class="article-title">{{ article.title }}</view>
<view class="article-meta">
<view class="date-tag" v-if="article.isToday">今日</view>
<view class="date" v-else>{{ article.date }}</view>
<view class="stats">
<view class="stat-item">
<uni-icons type="eye" size="12" color="#999"></uni-icons>
<text>{{ article.views }}</text>
</view>
<view class="stat-item">
<uni-icons type="hand-up" size="13" color="#999"></uni-icons>
<text>{{ article.likes }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载更多状态 -->
<view class="load-more" v-if="loading">
<uni-icons type="spinner-cycle" size="20" color="#999"></uni-icons>
<text class="load-text">加载中...</text>
</view>
<!-- 没有更多数据 -->
<view class="no-more" v-if="!hasMore && articleList.length > 0">
<text class="no-more-text">没有更多数据了</text>
</view>
<!-- 空状态 -->
<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>
</view>
</scroll-view>
<!-- 底部导航栏 -->
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import upImg from "@/static/cb_up.png"
import downImg from "@/static/cb_up.png"
import filter from "@/static/cb_screen_no.png"
import filterOn from "@/static/cb_screen_yes.png"
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';
const type=ref(1)
//
const currentPage = ref(1);
const pageSize = ref(10);
const hasMore = ref(true);
const loading = ref(false);
const refreshing = ref(false);
//
const activeTab = ref(0);
const showFilter = ref(false);
const isFilterActive = ref(false);
const toggleSort = () => {
type.value = type.value === 1 ? 2 : 1;
//
currentPage.value = 1;
hasMore.value = true;
polularScienceArticleListByKeywordsNew(true);
};
const loadGuideTags = async () => {
try {
const res = await api.guideTag({
type:4
});
if(res && res.code === 200 && res.data) {
// API
filterTags.value = res.data.map(tag => ({
...tag,
selected: false
}));
console.log('指南标签加载成功:', filterTags.value);
}
} catch (e) {
console.error('加载指南标签失败:', e);
}
};
const polularScienceArticleListByKeywordsNew = async (isRefresh = false) => {
if (loading.value) return;
loading.value = true;
try {
const page = isRefresh ? 1 : currentPage.value;
const res = await api.polularScienceArticleListByKeywordsNew({
keywords: keywords.value,
page: page,
type: type.value
});
if (res && res.code === 200 && res.data) {
const newData = res.data.list || [];
if (isRefresh) {
//
articleList.value = newData.map(item => formatArticleData(item));
currentPage.value = 1;
} else {
//
const formattedData = newData.map(item => formatArticleData(item));
articleList.value = [...articleList.value, ...formattedData];
}
//
hasMore.value = newData.length >= pageSize.value;
if (!isRefresh) {
currentPage.value++;
}
console.log('文章数据加载成功:', articleList.value);
}
} catch (error) {
console.error('获取文章列表失败:', error);
uni.showToast({
title: '获取数据失败',
icon: 'error',
duration: 2000
});
} finally {
loading.value = false;
refreshing.value = false;
}
};
//
const formatArticleData = (item) => {
// item
if (!item) return null;
const submitDate = item.submitDate ? new Date(item.submitDate) : new Date();
const today = new Date();
const isToday = submitDate.toDateString() === today.toDateString();
return {
uuid: item.uuid || '',
title: item.topic || '无标题',
summary: item.summary || '',
image: item.imgPath ? docUrl + item.imgPath : '/static/liver_knowledge.png',
date: isToday ? '今日' : formatDate(submitDate),
isToday: isToday,
views: item.readnum || 0,
likes: item.agreenum || 0,
tags: item.tags || '',
path: item.path || '',
submitDate: item.submitDate || ''
};
};
//
const formatDate = (date) => {
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${month}-${day}`;
};
//
const filterTags = ref([]);
//
const articleList = ref([]);
//
const switchTab = (index) => {
if(index==1){
navTo({
url: '/pages_app/patientVideo/patientVideo'
})
}else if(index==2){
let url=encodeURIComponent('https://wx.igandan.com/wxPatient/index.htm#/problem?link=share&fromtype=doctor')
navTo({
url: '/pages_app/webview/webview?url='+url
})
}
};
const goToDetail = (article) => {
console.log('查看文章详情:', article);
//
uni.$emit('articelItem', article);
uni.navigateBack();
};
//
const goToSearch = () => {
navTo({
url: '/pages_chat/searchArticle/searchArticle'
});
};
//
const onRefresh = async () => {
refreshing.value = true;
await polularScienceArticleListByKeywordsNew(true);
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
};
//
const onLoadMore = async () => {
if (!hasMore.value || loading.value) return;
await polularScienceArticleListByKeywordsNew(false);
};
const goToPage = (page) => {
console.log('跳转到页面:', page);
//
if (page === 'index') {
uni.switchTab({
url: '/pages/index/index'
});
}
};
//
const showFilterPopup = () => {
showFilter.value = true;
};
const hideFilterPopup = () => {
showFilter.value = false;
};
const toggleTag = (index) => {
filterTags.value[index].selected = !filterTags.value[index].selected;
};
const resetFilter = () => {
filterTags.value.forEach(tag => {
tag.selected = false;
});
isFilterActive.value = false;
};
const keywords=ref('');
const confirmFilter = () => {
//
const hasSelected = filterTags.value.some(tag => tag.selected);
isFilterActive.value = hasSelected;
//
if (hasSelected) {
const selectedTags = filterTags.value.filter(tag => tag.selected).map(tag => tag.DES);
console.log('选中的标签:', selectedTags);
keywords.value = selectedTags.join(',');
} else {
keywords.value = '';
}
//
currentPage.value = 1;
hasMore.value = true;
showFilter.value = false;
polularScienceArticleListByKeywordsNew(true);
};
onShow(() => {
polularScienceArticleListByKeywordsNew(true);
loadGuideTags();
});
</script>
<style lang="scss" scoped>
//
$primary-color: #ff6b6b;
$theme-color: #8B2316;
$white: #fff;
$gray-bg: #f5f5f5;
$gray-light: #eee;
$gray-medium: #999;
$gray-dark: #666;
$text-color: #333;
//
$border-radius: 8px;
$border-radius-small: 6px;
$padding: 15px;
$padding-small: 10px;
.page {
background-color: $gray-bg;
height: calc(100vh - 140rpx);
display: flex;
overflow-y: hidden;
flex-direction: column;
//
}
.top{
width:100%;
position: fixed;
top:140rpx;
}
//
.tabs {
background-color: $white;
display: flex;
border-bottom: 1px solid $gray-light;
.tab-item {
flex: 1;
text-align: center;
padding: $padding 0;
font-size: 16px;
color: $gray-dark;
position: relative;
&.active {
color: $theme-color;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 2px;
}
}
}
}
//
.filter-bar {
background-color: $white;
padding: $padding-small $padding;
display: flex;
align-items: center;
border-bottom: 1px solid $gray-light;
.search-box {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
.search-text {
font-size: 14px;
color: $gray-medium;
}
}
.divider {
width: 1px;
height: 16px;
background-color: $gray-medium;
margin: 0 $padding;
}
.filter-item {
flex:1;
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
font-size: 14px;
color: #999;
transition: all 0.3s ease;
}
}
.filter-item.active{
color: $theme-color;
}
//
.article-list {
position: fixed;
bottom: 0;
top:240rpx;
padding-bottom: 0rpx;
.article-item {
background-color: $white;
margin-bottom: $padding-small;
border-radius: $border-radius;
padding: $padding;
display: flex;
gap: 12px;
.article-image {
width: 80px;
height: 80px;
border-radius: $border-radius-small;
flex-shrink: 0;
}
.article-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.article-title {
font-size: 16px;
color: $text-color;
line-height: 1.4;
margin-bottom: $padding-small;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.article-meta {
display: flex;
justify-content: space-between;
align-items: center;
.date-tag {
background-color: $primary-color;
color: $white;
font-size: 12px;
padding: 2px 6px;
border-radius: 3px;
}
.date {
font-size: 12px;
color: $gray-medium;
}
.stats {
display: flex;
gap: $padding;
.stat-item {
display: flex;
align-items: center;
gap: 3px;
font-size: 12px;
color: $gray-medium;
}
}
}
}
}
//
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx;
color: #999;
.load-text {
margin-left: 10rpx;
font-size: 28rpx;
}
}
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx;
.no-more-text {
font-size: 28rpx;
color: #999;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 30rpx;
.empty-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
}
}
//
.bottom-nav {
background-color: $white;
display: flex;
border-top: 1px solid $gray-light;
padding: 8px 0;
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
font-size: 12px;
color: $gray-medium;
&.active {
color: $primary-color;
}
text {
font-size: 10px;
}
}
}
//
.filter-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
// background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
z-index: 9999;
.filter-content {
margin-top:329rpx;
background-color: $white;
// border-radius: 20rpx 20rpx 0 0;
padding: 20rpx 30rpx 60rpx;
width: 100%;
max-height: 80vh;
.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 24rpx;
border-radius: 30rpx;
font-size: 26rpx;
border: 2rpx solid #efefef;
transition: all 0.3s ease;
&.active {
background-color: #fff;
color: $theme-color;
border-color: $theme-color;
}
}
}
.filter-buttons {
display: flex;
gap: 20rpx;
position: fixed;
bottom: 30rpx;
left: 30rpx;
right: 30rpx;
.btn-reset,
.btn-confirm {
flex: 1;
height: 70rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
}
.btn-reset {
background-color: $white;
color: $theme-color;
border: 2rpx solid $theme-color;
}
.btn-confirm {
border: 2rpx solid $theme-color;
background-color: $theme-color;
color: $white;
}
}
}
}
</style>

View File

@ -0,0 +1,234 @@
<template>
<div class="msg-custom" :style="{ fontSize: (fontSize || 16) + 'px' }">
<view
class="customCell cloumn"
v-if="JSON.parse(msg.attachment.raw).gdxz_ext_data == '[肝胆商城]'"
@click="goToDetail(JSON.parse(msg.attachment.raw).gdxz_url)"
>
<view class="title">{{ JSON.parse(msg.attachment.raw).gdxz_title }}</view>
<view class="row">
<view class="left"> 肝胆相照®肝胆病在线公共服务平台 </view>
<view class="right">
<image
src="http://doc.igandan.com/app/html/img/2016/20160714132557.png"
mode="widthFix"
></image>
</view>
</view>
</view>
<view
class="customCell cloumn"
v-else-if="JSON.parse(msg.attachment.raw).gdxz_ext_data == '[公益咨询]'"
@click="goDetail(JSON.parse(msg.attachment.raw).gdxz_id)"
>
<view class="title">{{ JSON.parse(msg.attachment.raw).gdxz_title }}</view>
<view class="row">
<view class="left"> 点击查看问题详情 </view>
<view class="right">
<image
src="http://doc.igandan.com/app/html/img/2016/20160714132557.png"
mode="widthFix"
></image>
</view>
</view>
</view>
<view
class="customCell cloumn"
v-else-if="JSON.parse(msg.attachment.raw).gdxz_ext_data == '[门诊公告]'"
@click="goToDetail(JSON.parse(msg.attachment.raw).gdxz_url)"
>
<view class="title">{{ JSON.parse(msg.attachment.raw).gdxz_title }}</view>
<view class="row">
<view class="left"> 肝胆相照®肝胆病在线公共服务平台 </view>
<view class="right">
<image
src="http://doc.igandan.com/app/html/img/2016/20160714132557.png"
mode="widthFix"
></image>
</view>
</view>
</view>
<view
class="customCell cloumn"
v-else-if="
JSON.parse(msg.attachment.raw).gdxz_ext_data == '[图文科普]' ||
JSON.parse(msg.attachment.raw).gdxz_ext_data == '[视频科普]'
"
@click="goToDetail(JSON.parse(msg.attachment.raw).gdxz_url)"
>
<view class="title">{{ JSON.parse(msg.attachment.raw).gdxz_title }}</view>
<view class="row">
<view class="left">
{{ JSON.parse(msg.attachment.raw).gdxz_content }}
</view>
<view class="right">
<image
src="http://doc.igandan.com/app/html/img/2016/20160714132557.png"
mode="widthFix"
></image>
</view>
</view>
</view>
<view
class="customCell cloumn"
v-else-if="JSON.parse(msg.attachment.raw).gdxz_ext_data == '[互联网医院]'"
@click="goToMiniProgram"
>
<view class="title">肝胆相照互联网医院</view>
<view class="row">
<view class="left">
{{ JSON.parse(msg.attachment.raw).gdxz_content }}
</view>
<view class="right">
<image
src="http://doc.igandan.com/app/html/img/2016/20160714132557.png"
mode="widthFix"
></image>
</view>
</view>
</view>
</div>
</template>
<script lang="ts" setup>
/**文本消息组件 */
import Icon from "@/components/Icon.vue";
// @ts-ignore
import UniLink from "@/components/uni-components/uni-link/components/uni-link/uni-link.vue";
import { parseText } from "@/utils/im/parseText";
import { EMOJI_ICON_MAP_CONFIG } from "@/utils/im/emoji";
import { V2NIMMessageForUI } from "@xkit-yx/im-store-v2/dist/types/types";
import { t } from "@/utils/im/i18n";
import { isHarmonyOs } from "@/utils/im/index";
import navTo from "@/utils/navTo";
const props = withDefaults(
defineProps<{
msg: V2NIMMessageForUI;
fontSize?: number;
}>(),
{}
);
const goToDetail = (url: string) => {
navTo({
url: `/pages_app/webview/webview?url=${encodeURIComponent(url)}`,
});
};
const goDetail = (id: string) => {
navTo({
url: `/pages_app/consultDetail/consultDetail?uuid=${id}`,
});
};
//
const goToMiniProgram = () => {
// App
// #ifdef APP-PLUS
// App
plus.share.getServices((res) => {
let sweixin = null;
for (let i in res) {
if (res[i].id == "weixin") {
sweixin = res[i];
}
}
//
if (sweixin) {
uni.hideLoading();
sweixin.launchMiniProgram({
id: "wxc83296720404aa7b", // ID
type: 0, // 0- 1- 2-
path: "Pages/index/index", //,
});
}
});
// #endif
// #ifdef MP-WEIXIN
//
uni.navigateToMiniProgram({
appId: "wxc83296720404aa7b",
path: "Pages/index/index",
extraData: {
//
},
envVersion: "release",
success: (res) => {
console.log("跳转小程序成功", res);
},
fail: (err) => {
console.log("跳转小程序失败", err);
uni.showToast({
title: "跳转失败",
icon: "none",
});
},
});
// #endif
};
/**解析文本 */
const textArr = parseText(props.msg?.text || "", props.msg?.serverExtension);
/**unilink 不支持鸿蒙 故提示在浏览器打开链接 */
const openInBrowser = (url: string) => {
uni.setClipboardData({
data: url,
showToast: false,
success: () => {
uni.showToast({
title: t("openUrlText"),
icon: "none",
});
},
});
};
</script>
<style lang="scss" scoped>
.msg-custom {
color: #333;
display: flex;
text-align: left;
overflow-y: auto;
word-break: break-all;
word-wrap: break-word;
white-space: break-spaces;
}
.customCell {
display: flex;
}
.customCell.cloumn {
flex-direction: column;
}
.customCell .title {
font-size: 30rpx;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /* 注意这不是标准的CSS属性 */
-webkit-box-orient: vertical;
}
.customCell .row {
margin-top: 30rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.left {
font-size: 26rpx;
color: #999;
margin-right: 20rpx;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2; /* 注意这不是标准的CSS属性 */
-webkit-box-orient: vertical;
}
.right image {
width: 100rpx;
height: auto;
}
</style>

View File

@ -78,9 +78,9 @@
<div class="msg-input-button">
<Icon @tap="handleSendMoreVisible" type="send-more" :size="20" />
</div>
<div class="msg-input-button">
<!-- <div class="msg-input-button">
<Icon @tap="handleSetting" type="icon-shezhi" :size="20" />
</div>
</div> -->
</div>
<div v-if="inputVisible" class="msg-input">
<!-- 当从表情面板切换到文字输入时直接唤起键盘会导致input框滚动消失故此处需要用EmojiInput兼容下保证先隐藏表情面板再弹出键盘 -->
@ -157,8 +157,54 @@
</div>
<div class="icon-text">{{ t('albumText') }}</div>
</div>
<div class="send-more-panel-item-wrapper">
<div
class="send-more-panel-item"
@tap="(event: any) => handleCustom('reply', event)"
>
<image :src="quickImg" mode="widthFix"></image>
</div>
<div class="icon-text">快捷回复</div>
</div>
<div class="send-more-panel-item-wrapper">
<div
class="send-more-panel-item"
@tap="(event: any) => handleCustom('hj', event)"
>
<image :src="quickImg" mode="widthFix"></image>
</div>
<div class="icon-text">患教</div>
</div>
<div class="send-more-panel-item-wrapper">
<div
class="send-more-panel-item"
@tap="(event: any) => handleCustom('outpatient', event)"
>
<image :src="chuzhenImg" mode="widthFix"></image>
</div>
<div class="icon-text">/停诊公告</div>
</div>
<div class="send-more-panel-item-wrapper">
<div
class="send-more-panel-item"
@tap="(event: any) => handleCustom('mall', event)"
>
<image :src="mallImg" mode="widthFix"></image>
</div>
<div class="icon-text">商城</div>
</div>
<!-- <div class="send-more-panel-item-wrapper">
<div
class="send-more-panel-item"
@tap="(event: any) => handleCustom('hospital', event)"
>
<image :src="hospitalImg" mode="widthFix"></image>
</div>
<div class="icon-text">互联网医院</div>
</div> -->
<!-- 音频呼叫 -->
<div
<!-- <div
class="send-more-panel-item-wrapper"
v-if="
isAndroidOrIosApp &&
@ -170,10 +216,10 @@
<Icon type="icon-audio-call" :size="30"></Icon>
</div>
<div class="icon-text">{{ t('voiceCallText') }}</div>
</div>
</div> -->
<!-- 视频呼叫 -->
<div
<!-- <div
class="send-more-panel-item-wrapper"
v-if="
isAndroidOrIosApp &&
@ -185,8 +231,8 @@
<Icon type="icon-video-call" :size="30"></Icon>
</div>
<div class="icon-text">{{ t('videoCallText') }}</div>
</div>
<div
</div> -->
<!-- <div
v-if="isWeb"
class="send-more-panel-item-wrapper"
@tap="handleSendFileMsg"
@ -195,7 +241,7 @@
<Icon type="icon-file" :size="30"></Icon>
</div>
<div class="icon-text">{{ t('fileText') }}</div>
</div>
</div> -->
</div>
</div>
<!-- @消息相关 popup -->
@ -208,12 +254,38 @@
>
<MentionMemberList :team-id="to"></MentionMemberList>
</UniPopup>
<!-- 复制自 groupSend 的遮罩与弹窗 -->
<view v-if="showModal" class="mask" @tap="closeModal"></view>
<view v-if="showModal" class="center-modal">
<view class="modal-title">温馨提示</view>
<view class="modal-divider"></view>
<view class="modal-item" @tap="onSelectSince('article')">
<text>图文科普</text>
</view>
<view class="modal-divider"></view>
<view class="modal-item" @tap="onSelectSince('video')">
<text>视频科普</text>
</view>
</view>
<!-- 商城弹窗 -->
<view v-if="showMallModal" class="mask" @tap="closeMallModal"></view>
<view v-if="showMallModal" class="center-modal">
<view class="modal-title">温馨提示</view>
<view class="modal-divider"></view>
<view class="modal-item" @tap="sendMallMsg">
<text>纽娃复合营养素固体饮料</text>
</view>
<view class="modal-divider"></view>
<view class="modal-item" >
<text>更多商品正在准备中</text>
</view>
</view>
</div>
</template>
<script lang="ts" setup>
/** 消息页面输入框组件 */
import docUrl from '@/utils/docUrl'
import Face from './face.vue'
import VoicePanel from './voice-panel.vue'
import Icon from '@/components/Icon.vue'
@ -242,6 +314,11 @@ import Appellation from '@/components/Appellation.vue'
import { AT_ALL_ACCOUNT } from '@/utils/im/constants'
import { replaceEmoji } from '@/utils/im/index'
import { autorun } from 'mobx'
import quickImg from '@/static/quck_message.png'
import chuzhenImg from '@/static/outpatient_true.png'
import mallImg from '@/static/ytx_chattingfooter_shopping.png'
import hospitalImg from '@/static/ytx_chatting_hospital.png'
import {onShow,onUnload} from '@dcloudio/uni-app'
import {
V2NIMTeam,
@ -257,7 +334,30 @@ import {
import { V2NIMConst } from '@/utils/im/nim'
import { V2NIMMessage } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMMessageService'
export type MentionedMember = { accountId: string; appellation: string }
import navTo from '@/utils/navTo'
import api from '@/api/api.js'
let userInfo=uni.getStorageSync('userInfo');
let expert_uuid=userInfo.uuid;
let expert_name=userInfo.realName;
let patient_uuid='';
const articleInfo=ref({});
const videoInfo=ref({});
const patientListByGBK = async () => {
const res = await api.patientListByGBK();
if(res.code == 1){
//patientList.value = res.data;
for (let i = 0; i < res.data.length; i++) {
if (res.data[i].uuid.toLowerCase() === props.to) {
patient_uuid = res.data[i].uuid;
break;
}
}
}
};
const props = withDefaults(
defineProps<{
conversationType: V2NIMConst.V2NIMConversationType
@ -278,6 +378,10 @@ const conversationId =
/** 输入框内容 */
const inputText = ref('')
const extVisible = ref(false)
/** 群发选择弹窗 */
const showModal = ref(false)
/** 商城弹窗 */
const showMallModal = ref(false)
/** 音频面板flag */
const audioPanelVisible = ref(false)
/** 发送更多面板flag */
@ -314,6 +418,39 @@ const handleCall = (type: number) => {
})
}
}
onShow(() => {
uni.$on('articelItem', (article) => {
console.log('article', article);
articleInfo.value = article;
senCustomMsg('article');
//handleSendTextMsg('article', article);
});
uni.$on('videoItem', (video) => {
videoInfo.value = video;
senCustomMsg('video');
//handleSendTextMsg('video', video);
});
uni.$on('quickReply', (reply) => {
inputText.value= reply;
});
});
onUnload(() => {
uni.$off('articelItem');
uni.$off('videoItem');
uni.$off('quickReply');
});
/** 商城弹窗 */
let rawStr="{\"gdxz_content\":\"我已入驻肝胆相照互联网医院,复诊购药一站式服务,快来看看吧\",\"gdxz_ext_data\":\"[互联网医院]\",\"gdxz_id\":\"1681174885629431808\",\"gdxz_img\":\"https://img.applets.igandanyiyuan.com/applet/admin/avatar/2023071813261420200708181049.png\",\"gdxz_title\":\"肝胆相照互联网医院\",\"gdxz_type\":\"[互联网医院]\",\"gdxz_url\":\"\"}";
const sendMallMsg = () => {
showMallModal.value = false;
handleSendTextMsg('mall');
}
/** 商城弹窗 */
const closeMallModal = () => {
showMallModal.value = false
}
/** 用于解决表情面板和键盘冲突,导致输入框滚动消失问题 */
const showEmojiInput = ref(false)
@ -326,6 +463,7 @@ const replyMsg = ref<V2NIMMessageForUI>()
/** @ 消息相关 */
const ctx = getCurrentInstance()
const popupRef = ref(null)
/** @ 成员列表 */
const selectedAtMembers = ref<MentionedMember[]>([])
@ -381,6 +519,26 @@ const onPopupChange = (e: any) => {
uni.$emit(events.HANDLE_MOVE_THROUGH, e.value)
}
/** 打开/关闭群发选择弹窗(与 groupSend 保持一致) */
const openModal = () => {
showModal.value = true;
}
const closeModal = () => {
showModal.value = false
}
const toggleModal = () => {
showModal.value = !showModal.value
}
const onSelectSince = (type: string) => {
showModal.value = false
if (type === 'article') {
navTo({ url: '/pages_chat/article/article' })
} else if (type === 'video') {
navTo({ url: '/pages_chat/video/video' })
}
}
/** 点击@ 群成员 */
const handleMentionItemClick = (member: MentionedMember) => {
//@ts-ignore
@ -455,19 +613,21 @@ const handleInput = (event: any) => {
}
/** 发送文本消息 */
const handleSendTextMsg = () => {
const handleSendTextMsg = (type:string) => {
if (inputText.value.trim() === '') return
const ext = onAtMembersExtHandler()
let text = replaceEmoji(inputText.value)
let mallText='纽娃复合营养素固体饮料主要成分是:蜂花粉、乳清蛋白粉、灰树花粉、低聚木糖、蚕蛹氨基酸、麦芽粉、薏苡仁粉、烟酸、磷脂,以及其他调味品、辅助原料。科学配比制成,含有丰富的蛋白质、氨基酸、维生素、微量元素及其他营养元素,点击链接了解详情。'
let text = type==='mall'?mallText:replaceEmoji(inputText.value)
const textMsg = uni.$UIKitNIM.V2NIMMessageCreator.createTextMessage(text)
let serverExtension={"gdxz_nickName":"测试","gdxz_sessionType":"general"};
uni.$UIKitStore.msgStore
.sendMessageActive({
msg: textMsg as unknown as V2NIMMessage,
conversationId,
serverExtension: selectedAtMembers.value.length && (ext as any),
serverExtension:selectedAtMembers.value.length && (ext as any),
sendBefore: () => {
scrollBottom()
scrollBottom();
},
})
.catch(() => {
@ -483,7 +643,10 @@ const handleSendTextMsg = () => {
inputText.value = ''
isReplyMsg.value = false
replyMsg.value = undefined
selectedAtMembers.value = []
selectedAtMembers.value = [];
if(type=='mall'){
senCustomMsg('mall');
}
}
/** 发送文件消息 */
@ -629,7 +792,75 @@ const handleSendImageMsg = () => {
complete: handleNoPermission,
})
}
const senCustomMsg = (type:string) => {
let rawStr="";
let text="";
if(type == 'hospital'){
text="[互联网医院]";
rawStr=`{\"gdxz_content\":\"我已入驻肝胆相照互联网医院,复诊购药一站式服务,快来看看吧\",\"gdxz_ext_data\":\"[互联网医院]\",\"gdxz_id\":\"1681174885629431808\",\"gdxz_img\":\"https://img.applets.igandanyiyuan.com/applet/admin/avatar/2023071813261420200708181049.png\",\"gdxz_title\":\"肝胆相照互联网医院\",\"gdxz_type\":\"[互联网医院]\",\"gdxz_url\":\"\"}`;
}else if(type == 'mall'){
text="[肝胆商城]";
rawStr=`{\"gdxz_content\":\"肝胆相照®肝胆病在线公共服务平台\",\"gdxz_ext_data\":\"[肝胆商城]\",\"gdxz_id\":\"\",\"gdxz_img\":\"\",\"gdxz_title\":\"纽娃复合营养素固体饮料\",\"gdxz_type\":\"[肝胆商城]\",\"gdxz_url\":\"https://wx.igandan.com/shop_notify/setInfo?patient_uuid=${patient_uuid}&expert_uuid=${expert_uuid}\"}`;
}else if(type == 'outpatient'){
text="[门诊公告]";
rawStr=`{\"gdxz_content\":\"门诊详情\",\"gdxz_ext_data\":\"[门诊公告]\",\"gdxz_id\":\"\",\"gdxz_img\":\"\",\"gdxz_title\":\"${expert_name}医生门诊详情\",\"gdxz_type\":\"[门诊公告]\",\"gdxz_url\":\"https://dev-wx.igandan.com/wxPatient/index.htm#/outPatient?link=share&expertUuid=${expert_uuid}\"}`
}else if(type == 'article'){
text="[图文科普]";
let content='"'+articleInfo.value.summary+'"';
let id='"'+articleInfo.value.uuid+'"';
let title='"'+articleInfo.value.title+'"';
let path='"'+docUrl+articleInfo.value.path+'"';
rawStr=`{\"gdxz_content\":${content},\"gdxz_ext_data\":\"[图文科普]\",\"gdxz_id\":${id},\"gdxz_img\":\"http://doc.igandan.org/app/book/pdf/2019/20190613152617.png\",\"gdxz_title\":${title},\"gdxz_type\":\"[图文科普]\",\"gdxz_url\":${path}}`
}else if(type == 'video'){
let content='"'+videoInfo.value.note+'"';
let id='"'+videoInfo.value.uuid+'"';
let title='"'+videoInfo.value.name+'"';
let path='"'+docUrl+videoInfo.value.path+'"';
text="[视频科普]";
rawStr=`{\"gdxz_content\":${content},\"gdxz_ext_data\":\"[视频科普]\",\"gdxz_id\":${id},\"gdxz_img\":\"http://doc.igandan.org/app/book/pdf/2019/20190613152617.png\",\"gdxz_title\":${title},\"gdxz_type\":\"[视频科普]\",\"gdxz_url\":${path}}`
}
const customMsg = uni.$UIKitNIM.V2NIMMessageCreator.createCustomMessage(text,rawStr)
uni.$UIKitStore.msgStore
.sendMessageActive({
msg: customMsg as unknown as V2NIMMessage,
conversationId,
progress: () => true,
sendBefore: () => {
scrollBottom()
},
})
.then(() => {
scrollBottom();
})
.catch(() => {
scrollBottom()
uni.showToast({
icon: 'error',
title: '发送失败',
})
})
}
const handleCustom = (type: string, event: any) => {
if (isTeamMute.value) return
if(type == 'reply'){
navTo({
url: '/pages_chat/quickReply/quickReply',
})
}else if(type == 'hj'){
showModal.value = true
}else if(type == 'mall'){
showMallModal.value = true
patientListByGBK();
}else if(type == 'outpatient'){
senCustomMsg('outpatient');
// navTo({
// url: '/pages_chat/outpatient/outpatient',
// })
}else if(type == 'hospital'){
senCustomMsg('hospital');
}
}
/** 发送视频消息(使用相机或者从相册选择) */
const handleSendVideoMsg = (type: string, event: any) => {
if (isTeamMute.value) return
@ -1073,12 +1304,18 @@ onUnmounted(() => {
.send-more-panel-item {
background-color: #fff;
border-radius: 8px;
width: 60px;
height: 60px;
width: 100rpx;
height: 100rpx;
display: flex;
overflow: hidden;
align-items: center;
margin: 0 15px;
justify-content: center;
image{
width: 100rpx;
height: 100rpx;
border-radius: 8px;
}
}
.icon-text {
font-size: 12px;
@ -1178,4 +1415,47 @@ onUnmounted(() => {
height: 60px;
}
}
/* 复制自 groupSend 的遮罩与弹窗样式 */
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 1000;
}
.center-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 650rpx;
background: #ffffff;
border-radius: 16rpx;
z-index: 1001;
overflow: hidden;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.15);
}
.modal-title {
text-align: center;
font-size: 34rpx;
color: #d32f2f;
padding: 28rpx 20rpx;
}
.modal-item {
padding: 36rpx 28rpx;
font-size: 30rpx;
color: #333333;
background: #ffffff;
}
.modal-divider {
height: 2rpx;
background: #eeeeee;
}
</style>

View File

@ -1,4 +1,5 @@
<template>
<!-- {{ props.msg }} -->
<div
:class="`msg-item-wrapper ${
props.msg.pinState &&
@ -351,6 +352,42 @@
</div>
<!-- <MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead> -->
</div>
<!-- 自定义消息-->
<div
class="msg-common"
v-else-if="
props.msg.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM
"
:style="{ flexDirection: !props.msg.isSelf ? 'row' : 'row-reverse' }"
>
<Avatar
:account="props.msg.senderId"
:teamId="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
? to
: ''
"
:goto-user-card="true"
/>
<div class="msg-content">
<div class="msg-name" v-if="!props.msg.isSelf">
{{ appellation }}
</div>
<MessageBubble
:msg="props.msg"
:tooltip-visible="true"
:bg-visible="true"
>
<ReplyMessage v-if="!!replyMsg" :replyMsg="replyMsg"></ReplyMessage>
<!-- <MessageText :msg="props.msg"></MessageText> -->
<MessageCustom :msg="props.msg"></MessageCustom>
</MessageBubble>
</div>
<MessageIsRead v-if="props.msg?.isSelf" :msg="props.msg"></MessageIsRead>
</div>
<!-- 通知消息-->
<MessageNotification
v-else-if="
@ -383,7 +420,7 @@
:tooltip-visible="true"
:bg-visible="true"
>
[{{ t('unknowMsgText') }}]
[{{ t('unknowMsgText')}}]
</MessageBubble>
</div>
</div>
@ -435,6 +472,7 @@ import MessageNotification from './message-notification.vue'
import MessageG2 from './message-g2.vue'
import { customNavigateTo } from '@/utils/im/customNavigate'
import MessageIsRead from './message-read.vue'
import MessageCustom from './message-custom.vue'
import Icon from '@/components/Icon.vue'
import Appellation from '@/components/Appellation.vue'
import { events, MSG_ID_FLAG } from '@/utils/im/constants'

View File

@ -1,67 +1,207 @@
<template>
<view class="page">
<!-- 顶部导航 -->
<view class="nav">
<view class="back" @click="onBack">
<uni-icons type="left" color="#8B2316" size="24"></uni-icons>
</view>
<view class="title">群发消息</view>
<view class="right"></view>
</view>
<!-- 时间展示 -->
<view class="time-row">{{ currentTime }}</view>
<!-- 卡片区域 -->
<view class="card">
<view class="card-header">
<text class="label">{{ patientCount }}位患者</text>
<view class="close" @click="onClear">
<text>×</text>
<navBar title="群发消息" />
<!-- 下拉刷新和上拉加载容器 -->
<scroll-view
class="scroll-container"
scroll-y
refresher-enabled
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
:lower-threshold="100"
>
<!-- 卡片区域 -->
<view class="card-cell" v-if="list.length > 0" v-for="(item, index) in list" :key="item.uuid">
<view class="time-row">{{ item.msg_send_date }}</view>
<view class="card" >
<view class="card-body">
<view class="card-header">
<text class="label">{{ item.realname.split(',').length }}位患者</text>
<view class="close" @click="onClear">
<text>×</text>
</view>
</view>
<view class="line" >
{{ item.realname }}
</view>
<view class="line phone" >
{{ item.msg_content }}
</view>
</view>
<view class="card-footer">
<view class="btn-outline" @click="onResend">再发一条</view>
</view>
</view>
</view>
<view class="card-body">
<view class="line">{{ patientName }}</view>
<view class="line phone">{{ patientPhone }}</view>
<!-- 空状态 -->
<view class="card" v-else>
<view class="card-header">
<text class="label">群发消息记录</text>
</view>
<view class="card-body">
<view class="line">暂无群发消息记录</view>
</view>
</view>
<view class="card-footer">
<view class="btn-outline" @click="onResend">再发一条</view>
</view>
</view>
<!-- 占位滚动区域 -->
<view class="spacer" />
<!-- 加载状态 -->
<view class="loading-state" v-if="loading">
<text>加载中...</text>
</view>
<!-- 没有更多数据 -->
<view class="no-more" v-if="!hasMore && list.length > 0">
<text>没有更多数据了</text>
</view>
<!-- 占位滚动区域 -->
<view class="spacer" />
</scroll-view>
<!-- 底部按钮 -->
<view class="bottom-bar">
<view class="btn-primary" @click="onSendGroup">群发消息</view>
<view class="btn-primary" @click="openModal">群发消息</view>
</view>
<!-- 弹窗与遮罩 -->
<view v-if="showModal" class="mask" @click="closeModal"></view>
<view v-if="showModal" class="center-modal">
<view class="modal-title">温馨提示</view>
<view class="modal-divider"></view>
<view class="modal-item" @click="onSelect('single')">
<text>单独选择</text>
</view>
<view class="modal-divider"></view>
<view class="modal-item" @click="onSelect('group')">
<text>分组选择</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import navBar from '@/components/navBar/navBar.vue'
import api from '@/api/api.js'
import { onShow } from '@dcloudio/uni-app'
const currentTime = ref('')
const patientCount = ref(1)
const patientName = ref('测试')
const patientPhone = ref('155555555')
const showModal=ref(false);
const page=ref(1);
const list=ref([]);
const refreshing=ref(false);
const loading=ref(false);
const hasMore=ref(true);
const pad = (n) => (n < 10 ? `0${n}` : `${n}`)
const updateTime = () => {
const d = new Date()
currentTime.value = `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
//
const formatTime = (dateStr) => {
if (!dateStr) return ''
const date = new Date(dateStr)
const now = new Date()
const diff = now - date
//
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
return `${pad(date.getHours())}:${pad(date.getMinutes())}`
}
//
else if (diff < 48 * 60 * 60 * 1000 && date.getDate() === now.getDate() - 1) {
return `昨天 ${pad(date.getHours())}:${pad(date.getMinutes())}`
}
//
else {
return `${date.getMonth() + 1}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}
}
onMounted(() => {
updateTime()
//
setInterval(updateTime, 1000)
})
//
const listGroupSendMsg=async(isRefresh = false)=>{
if(loading.value) return;
loading.value = true;
try {
const res=await api.listGroupSendMsg({
page: page.value,
});
if(res.code==200){
const newList = res.data.list || [];
if(isRefresh) {
//
list.value = newList;
} else {
//
list.value = [...list.value, ...newList];
}
//
hasMore.value = newList.length >= 10; // 10
// +1
if(!isRefresh) {
page.value++;
}
}
} catch (error) {
console.error('加载数据失败:', error);
uni.showToast({ title: '加载失败', icon: 'none' });
} finally {
loading.value = false;
}
};
//
const onRefresh = async () => {
refreshing.value = true;
page.value = 1;
hasMore.value = true;
await listGroupSendMsg(true);
refreshing.value = false;
uni.showToast({ title: '刷新成功', icon: 'success' });
};
//
const onLoadMore = async () => {
if(!hasMore.value || loading.value) return;
await listGroupSendMsg(false);
};
onMounted(() => {
})
onShow(() => {
listGroupSendMsg()
})
const onBack = () => {
uni.navigateBack()
}
const openModal = () => {
showModal.value = true;
};
const closeModal = () => {
showModal.value = false;
};
const onSelect = (type) => {
showModal.value = false;
if (type === 'single') {
uni.showToast({ title: '单独选择', icon: 'none' });
} else if (type === 'group') {
uni.showToast({ title: '分组选择', icon: 'none' });
}
};
const onClear = () => {
//
patientCount.value = 0
@ -82,7 +222,48 @@ $brand: #8B2316;
$brand-deep: #8B2316;
$primary: #00cbc0;
$red: #D32F2F;
/* 遮罩与弹窗 */
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.6);
z-index: 10;
}
.center-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 650rpx;
background: #ffffff;
border-radius: 16rpx;
z-index: 11;
overflow: hidden;
box-shadow: 0 10rpx 40rpx rgba(0,0,0,0.15);
}
.modal-title {
text-align: center;
font-size: 34rpx;
color: #8B2316;
padding: 28rpx 20rpx;
}
.modal-item {
padding: 36rpx 28rpx;
font-size: 30rpx;
color: #333333;
background: #ffffff;
}
.modal-divider {
height: 2rpx;
background: #eeeeee;
}
.page {
background: $page-bg;
min-height: 100vh;
@ -101,16 +282,45 @@ $red: #D32F2F;
.right { width: 80rpx; }
}
//
.scroll-container {
position: fixed;
top: 140rpx;
width:100%;
bottom: 200rpx;
}
.time-row {
text-align: center;
font-size: 32rpx;
padding: 32rpx 0;
color: #333;
background: #fff;
}
//
.loading-state {
text-align: center;
padding: 40rpx 0;
color: #999;
font-size: 28rpx;
}
.no-more {
text-align: center;
padding: 40rpx 0;
color: #ccc;
font-size: 24rpx;
}
.card-cell{
display: flex;
flex-direction: column;
margin: 0 30rpx;
box-sizing: border-box;
}
.card {
margin: 24rpx;
margin: 24rpx 0;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.06);
@ -122,8 +332,8 @@ $red: #D32F2F;
.label { font-size: 32rpx; color: #333; }
.close {
position: absolute;
right: 0;
top: 0;
right: -15rpx;
top: -18rpx;
width: 100rpx;
height: 100rpx;
border-bottom-left-radius: 100rpx;

View File

@ -0,0 +1,347 @@
<template>
<view class="page">
<!-- 顶部导航 -->
<uni-nav-bar
left-icon="left"
title="群发消息"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eee"
>
<template #right>
<view class="nav-right">
<uni-icons type="plus" size="24" color="#8B2316" @click="toggleModal"></uni-icons>
</view>
</template>
</uni-nav-bar>
<!-- 警告提示区域 -->
<view class="warning-section">
<view class="warning-icon">🔔</view>
<text class="warning-text"
>若群发消息选择患者过多(建议少于200人), 部分患者可能将延迟收到消息</text
>
</view>
<!-- 消息预览卡片 -->
<view class="message-preview-card" v-if="selectedPatients.length > 0">
<view class="card-header">
<text class="label"
>消息将发送给{{ selectedPatients.length }}位患者</text
>
<view class="close" @click="onClear">
<text>×</text>
</view>
</view>
<view class="card-body">
<view class="patient-list">
<text
class="patient-name"
v-for="(patient, index) in selectedPatients"
:key="patient.uuid"
>
{{ patient.nickname || patient.realName
}}{{ index < selectedPatients.length - 1 ? "、" : "" }}
</text>
</view>
</view>
</view>
<!-- 空状态区域 -->
<view class="empty-area" v-else>
<view class="empty-content">
<text class="empty-text">请选择要发送消息的患者</text>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-bar">
<view style="height: 'auto'">
<!-- <MessageInput
:reply-msgs-map="replyMsgsMap"
:conversation-type="conversationType"
:to="to"
/> -->
</view>
</view>
<!-- 弹窗与遮罩 -->
<view v-if="showModal" class="mask" @click="closeModal"></view>
<view v-if="showModal" class="center-modal">
<view class="modal-title">温馨提示</view>
<view class="modal-divider"></view>
<view class="modal-item" @click="onSelect('single')">
<text>单独选择</text>
</view>
<view class="modal-divider"></view>
<view class="modal-item" @click="onSelect('group')">
<text>分组选择</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import navBar from "@/components/navBar/navBar.vue";
import api from "@/api/api.js";
import { onShow,onLoad,onUnload } from "@dcloudio/uni-app";
import navTo from "@/utils/navTo.js";
const showModal = ref(false);
const selectedPatients = ref([]);
const replyMsgsMap = ref({});
const conversationType = ref("1");
const msg_content = ref("");
const msg_type = ref("1");
const to = ref("");
const toggleModal = () => {
showModal.value = !showModal.value;
}
const goBack = () => {
uni.navigateBack({
delta: 1,
fail: (err) => {
navTo({
url: "/pages/index/index"
})
}
});
}
const addGroupSendMsg4YunXin=async()=>{
const res=await api.addGroupSendMsg4YunXin({
patient_user_uuid: selectedPatients.value.map(item => item.uuid).join(','),
msg_content: msg_content.value,
msg_type: msg_type.value,
});
if(res.code==200){
uni.showToast({ title: "发送成功", icon: "none" });
navTo({
url: "/pages_chat/groupMessage/groupMessage"
})
}else{
uni.showToast({ title: res.msg, icon: "none" });
}
}
onMounted(() => {
//
});
onShow(() => {
try {
uni.$on('selectedChatPatients',(data)=>{
console.log(data)
selectedPatients.value = data.patients;
});
uni.$on('selectedChatPatientsSingle',(data)=>{
console.log(data)
selectedPatients.value = data.patients;
});
} catch (e) {
console.error('接收已选患者失败', e);
}
});
onUnload(() => {
uni.$off('selectedChatPatients');
uni.$off('selectedChatPatientsSingle');
});
const openModal = () => {
showModal.value = true;
};
const closeModal = () => {
showModal.value = false;
};
const onSelect = (type) => {
showModal.value = false;
if (type === "single") {
navTo({
url: "/pages_app/selectPatient/selectPatient"
})
//
} else if (type === "group") {
navTo({
url: "/pages_chat/patientGroup/patientGroup"
})
//
}
};
const onClear = () => {
selectedPatients.value = [];
uni.showToast({ title: "已清空选择", icon: "success" });
try { uni.removeStorageSync('GROUP_SEND_SELECTED_PATIENTS'); } catch (e) {}
};
</script>
<style lang="scss" scoped>
$page-bg: #f5f5f5;
$brand: #8b2316;
$brand-deep: #8b2316;
$primary: #00cbc0;
$red: #d32f2f;
.page {
background: $page-bg;
min-height: 100vh;
position: relative;
}
//
.warning-section {
display: flex;
align-items: flex-start;
padding: 24rpx 30rpx;
background: #fff8e1;
border-left: 6rpx solid #ff9800;
margin: 20rpx 30rpx;
border-radius: 8rpx;
.warning-icon {
font-size: 28rpx;
margin-right: 16rpx;
margin-top: 4rpx;
}
.warning-text {
font-size: 26rpx;
color: #e65100;
line-height: 1.5;
flex: 1;
}
}
//
.message-preview-card {
margin: 30rpx;
background: #fff;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
overflow: hidden;
border-top: 6rpx solid $red;
.card-header {
position: relative;
padding: 28rpx 28rpx 12rpx 28rpx;
.label {
font-size: 32rpx;
color: #333;
}
.close {
position: absolute;
right: -15rpx;
top: -18rpx;
width: 100rpx;
height: 100rpx;
border-bottom-left-radius: 100rpx;
background: $red;
display: flex;
align-items: center;
justify-content: center;
text {
color: #fff;
font-size: 40rpx;
font-weight: 500;
}
}
}
.card-body {
padding: 20rpx 28rpx 28rpx 28rpx;
.patient-list {
.patient-name {
font-size: 32rpx;
color: #333;
line-height: 1.6;
}
}
}
}
//
.empty-area {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 100rpx 30rpx;
.empty-content {
text-align: center;
.empty-text {
font-size: 28rpx;
color: #999;
}
}
}
//
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: $primary;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
.btn-primary {
color: #fff;
font-size: 34rpx;
font-weight: 600;
}
}
//
.mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
z-index: 10;
}
.center-modal {
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 650rpx;
background: #ffffff;
border-radius: 16rpx;
z-index: 11;
overflow: hidden;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.15);
}
.modal-title {
text-align: center;
font-size: 34rpx;
color: $red;
padding: 28rpx 20rpx;
}
.modal-item {
padding: 36rpx 28rpx;
font-size: 30rpx;
color: #333333;
background: #ffffff;
}
.modal-divider {
height: 2rpx;
background: #eeeeee;
}
</style>

View File

@ -0,0 +1,714 @@
<template>
<view class="patient-group-page">
<!-- 头部 -->
<uni-nav-bar
left-icon="left"
title="患者分组"
@clickLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
>
<template #right>
<view class="save-text" @click="confirmGroup">确定({{ totalSelectedCount }})</view>
</template>
</uni-nav-bar>
<!-- 筛选排序栏 -->
<view class="filter-sort-bar">
<view class="sort-section" @click="toggleGroupSort">
<text class="sort-label">{{ groupSortTitle }}</text>
<view class="imgbox">
<up-image :src="upImg" width="26rpx" height="26rpx" ></up-image>
</view>
</view>
<view class="divider"></view>
<view class="current-sort" @click="toggleInnerSort">
<text class="sort-text">{{ innerSortTitle }}</text>
<view class="imgbox">
<up-image :src="upImg" width="26rpx" height="26rpx" ></up-image>
</view>
</view>
</view>
<!-- 分组排序弹窗 -->
<view v-if="showGroupSort" class="popup-panel">
<view class="popup-item" :class="{ active: group_sort==0 }" @click.stop="chooseGroupSort('letter')">
<text class="item-text">按首字母</text>
<uni-icons v-if="selectedGroupSort==='letter'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
<view class="popup-divider"></view>
<view class="popup-item" :class="{ active:group_sort==1}" @click.stop="chooseGroupSort('count')">
<text class="item-text">分组人数</text>
<uni-icons v-if="selectedGroupSort==='count'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
</view>
<view v-if="showGroupSort" class="popup-mask" @click="closeGroupSort"></view>
<!-- 组内排序弹窗 -->
<view v-if="showInnerSort" class="popup-panel">
<view class="popup-item" :class="{ active:list_sort==0 }" @click.stop="chooseInnerSort('letter')">
<text class="item-text">按首字母</text>
<uni-icons v-if="selectedInnerSort==='letter'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
<view class="popup-divider"></view>
<view class="popup-item" :class="{ active:list_sort==1 }" @click.stop="chooseInnerSort('count')">
<text class="item-text">随访时间</text>
<uni-icons v-if="selectedInnerSort==='count'" type="checkmarkempty" color="#8B2316" size="22"></uni-icons>
</view>
</view>
<view v-if="showInnerSort" class="popup-mask" @click="closeInnerSort"></view>
<!-- 患者列表 -->
<scroll-view class="patient-list-section" scroll-y="true" :style="{ height: scrollViewHeight }">
<!-- 分组循环渲染 -->
<view class="groupcell" v-for="(group, gi) in groups" :key="group.uuid || gi">
<view class="section-title" @click="toggleGroup(gi)">
<view class="left">
<view class="imgbox" :class="{ 'active':openGroups[gi] }">
<up-image v-if="!openGroups[gi]" :src="groupRightImg" width="19rpx" height="32rpx" ></up-image>
<up-image v-else :src="groupDownImg" width="32rpx" height="19rpx" ></up-image>
</view>
<view class="title">{{ group.name || '未命名分组' }} | {{ group.patientNum || (group.patientList ? group.patientList.length : 0) }}</view>
</view>
<view class="right-checkbox" @click.stop="toggleGroupSelection(gi)" v-if="group.patientList.length > 0">
<view class="checkbox" :class="{ 'checked': isGroupSelected(gi) }">
<uni-icons v-if="isGroupSelected(gi)" type="checkmarkempty" color="#8B2316" size="16"></uni-icons>
</view>
</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-avatar">
<up-image :src="docUrl + patient.photo" width="80rpx" height="80rpx" mode="aspectFill"></up-image>
</view>
<view class="patient-checkbox" @click.stop="togglePatientSelection(gi, patient, index)">
<view class="checkbox" :class="{ 'checked': isPatientSelected(gi, patient, index) }">
<uni-icons v-if="isPatientSelected(gi, patient, index)" type="checkmarkempty" color="#8B2316" size="16"></uni-icons>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="clear-btn" @click="clearSelection">清空选择</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { onShow,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 api from '@/api/api.js';
import docUrl from '@/utils/docUrl'
import dayjs from 'dayjs'
import navTo from '@/utils/navTo.js'
const list_sort = ref(0);
const group_sort = ref(0);
//
const groups = ref([]);
const openGroups = ref({});
//
const selectedGroups = ref({});
// keySetMap
const selectedPatients = ref({});
const formatYMD = (val) => {
if (!val) return '';
const d = dayjs(val);
return d.isValid() ? d.format('YYYY-MM-DD') : '';
};
//
const groupSortTitle = computed(() => group_sort.value === 0 ? '按首字母' : '分组人数');
const innerSortTitle = computed(() => list_sort.value === 0 ? '按首字母' : '随访时间');
//
const selectedGroupCount = computed(() => {
return Object.values(selectedGroups.value).filter(Boolean).length;
});
//
const getPatientUniqueId = (p) => {
return (p && (p.uuid || p.patientUuid || p.id || p.userId)) || '';
};
const selectedPatientCount = computed(() => {
const idSet = new Set();
groups.value.forEach((group, gi) => {
const map = selectedPatients.value[gi] || {};
(group.patientList || []).forEach((patient, pi) => {
if (map[pi]) {
const uid = getPatientUniqueId(patient);
if (uid) idSet.add(uid);
}
});
});
return idSet.size;
});
//
const totalSelectedCount = computed(() => selectedPatientCount.value);
onShow(() => {
fetchGroupList();
});
const fetchGroupList = async () => {
const res = await api.groupList({
list_sort:list_sort.value,
page:1,
pageSize:10,
group_sort:group_sort.value
});
if(res.code === 200){
groups.value = Array.isArray(res.data) ? res.data : [];
//
openGroups.value = {};
groups.value.forEach((group, idx) => {
openGroups.value[idx] = false;
// Map
if (!selectedPatients.value[idx]) selectedPatients.value[idx] = {};
//
updateGroupSelectionByPatients(idx);
});
}
};
const toggleGroup = (idx) => {
openGroups.value[idx] = !openGroups.value[idx];
};
const toggleGroupSelection = (idx) => {
const group = groups.value[idx] || {};
const patients = group.patientList || [];
const willSelect = !isGroupSelected(idx);
//
selectedGroups.value[idx] = willSelect;
//
if (!selectedPatients.value[idx]) selectedPatients.value[idx] = {};
patients.forEach((_, pi) => {
selectedPatients.value[idx][pi] = willSelect;
});
};
const isPatientSelected = (gi, patient, index) => {
const groupSelectedMap = selectedPatients.value[gi] || {};
return !!groupSelectedMap[index];
};
const togglePatientSelection = (gi, patient, index) => {
if (!selectedPatients.value[gi]) selectedPatients.value[gi] = {};
selectedPatients.value[gi][index] = !selectedPatients.value[gi][index];
//
updateGroupSelectionByPatients(gi);
};
const isGroupSelected = (gi) => {
// true
if (selectedGroups.value[gi]) return true;
//
const group = groups.value[gi] || {};
const patients = group.patientList || [];
if (patients.length === 0) return !!selectedGroups.value[gi];
const map = selectedPatients.value[gi] || {};
return patients.every((_, pi) => !!map[pi]);
};
const updateGroupSelectionByPatients = (gi) => {
const group = groups.value[gi] || {};
const patients = group.patientList || [];
const map = selectedPatients.value[gi] || {};
if (patients.length === 0) return;
selectedGroups.value[gi] = patients.every((_, pi) => !!map[pi]);
};
const editGroup = (group) => {
navTo({
url: `/pages_app/groupEdit/groupEdit?uuid=${group.uuid}`
})
};
//
const showGroupSort = ref(false);
const selectedGroupSort = ref('letter'); // letter | count
//
const showInnerSort = ref(false);
const selectedInnerSort = ref('letter');
// /
const showPending = ref(false);
const toggleGroupSort = () => {
showGroupSort.value = !showGroupSort.value;
if (showGroupSort.value) showInnerSort.value = false;
};
const closeGroupSort = () => {
showGroupSort.value = false;
};
const chooseGroupSort = (type) => {
selectedGroupSort.value = type;
// =0=1
group_sort.value = type === 'letter' ? 0 : 1;
closeGroupSort();
fetchGroupList();
};
const confirmGroup = () => {
//
const idSet = new Set();
const dedupPatients = [];
groups.value.forEach((group, gi) => {
const map = selectedPatients.value[gi] || {};
(group.patientList || []).forEach((patient, pi) => {
if (map[pi]) {
const uid = getPatientUniqueId(patient);
if (uid && !idSet.has(uid)) {
idSet.add(uid);
dedupPatients.push(patient);
}
}
});
});
if (dedupPatients.length === 0) {
uni.showToast({ title: '请至少选择1个患者', icon: 'none' });
return;
}
try {
uni.navigateTo({
url: '/pages_chat/groupSend/groupSend',
success: (res) => {
try {
uni.$emit('selectedChatPatients', { patients: dedupPatients });
} catch (emitErr) {
console.error('传递已选患者失败', emitErr);
uni.showToast({ title: '传递失败,请重试', icon: 'none' });
}
}
});
} catch (e) {
console.error('跳转失败', e);
uni.showToast({ title: '跳转失败,请重试', icon: 'none' });
}
}
const toggleInnerSort = () => {
showInnerSort.value = !showInnerSort.value;
if (showInnerSort.value) showGroupSort.value = false;
};
const closeInnerSort = () => {
showInnerSort.value = false;
};
const chooseInnerSort = (type) => {
selectedInnerSort.value = type;
// =0访=1
list_sort.value = type === 'letter' ? 0 : 1;
closeInnerSort();
fetchGroupList();
};
//
const goBack = () => {
uni.navigateBack();
};
const createNew = () => {
uni.showToast({
title: '跳转到新建分组页面',
icon: 'none'
});
//
};
//
const clearSelection = () => {
selectedGroups.value = {};
selectedPatients.value = {};
};
</script>
<style lang="scss" scoped>
.patient-group-page {
min-height: 100vh;
background-color: #f5f5f5;
}
/* 弹窗样式 */
.popup-mask {
position: fixed;
top: 220rpx; /* 位于筛选栏下方 */
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.3);
z-index: 8;
}
.popup-panel {
position: fixed;
top: 220rpx; /* 紧贴筛选栏 */
left: 0;
right: 0;
background: #fff;
z-index: 10;
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;
}
.save-text{
margin-top: -20rpx;
color:#8B2316;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10rpx 30rpx;
background-color: #ffffff;
font-size: 24rpx;
color: #333;
.status-left {
display: flex;
align-items: center;
gap: 20rpx;
.time {
font-weight: 500;
}
.app-icons {
display: flex;
gap: 10rpx;
.app-icon {
width: 24rpx;
height: 24rpx;
border-radius: 4rpx;
&.weibo {
background-color: #ff8200;
}
&.taobao {
background-color: #ff6a00;
}
&.xiaohongshu {
background-color: #ff2442;
}
}
}
}
.status-right {
display: flex;
align-items: center;
gap: 15rpx;
.network {
color: #666;
}
.signal-icon {
width: 32rpx;
height: 20rpx;
background-color: #333;
border-radius: 2rpx;
}
.battery-text {
font-size: 20rpx;
color: #333;
}
.battery {
width: 40rpx;
height: 20rpx;
border: 2rpx solid #333;
border-radius: 4rpx;
background-color: #4caf50;
position: relative;
&::after {
content: '';
position: absolute;
right: -6rpx;
top: 6rpx;
width: 4rpx;
height: 8rpx;
background-color: #333;
border-radius: 0 2rpx 2rpx 0;
}
}
}
}
.header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background-color: #ffffff;
border-bottom: 1rpx solid #f0f0f0;
.back-btn {
padding: 10rpx;
}
.title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: normal;
color: #ff0000;
margin-right: 60rpx; //
}
.new-btn {
padding: 10rpx;
.new-text {
font-size: 28rpx;
color: #ff0000;
}
}
}
.filter-sort-bar {
display: flex;
align-items: center;
justify-content:center;
padding: 20rpx 30rpx;
background-color: #ffff;
position: fixed;
top: 140rpx;
left: 0;
right: 0;
z-index: 9;
border-bottom: 1rpx solid #f0f0f0;
.imgbox{
width: 32rpx;
margin-top: -10rpx;
}
.sort-section {
display: flex;
align-items: center;
gap: 10rpx;
.sort-label {
font-size: 28rpx;
color: #333;
}
.sort-icon.down {
width: 0;
height: 0;
border-left: 8rpx solid transparent;
border-right: 8rpx solid transparent;
border-top: 12rpx solid #999;
}
}
.divider {
width: 2rpx;
height: 40rpx;
background-color: #e0e0e0;
margin: 0 80rpx;
}
.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;
}
}
}
}
.patient-list-section {
top: 240rpx;
width:100%;
bottom:120rpx; //
position: fixed;
background-color: #ffffff;
.section-title {
padding: 30rpx 30rpx 20rpx;
font-size: 32rpx;
width: 100%;
box-sizing: border-box;
font-weight: normal;
display: flex;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
align-items: center;
justify-content: space-between;
.left{
display:flex;
align-items:center;
}
.imgbox{
&.active{
margin-top: -20rpx;
}
margin-right: 8rpx;
}
.right-checkbox{
display:flex;
align-items:center;
.checkbox{
width: 40rpx;
height: 40rpx;
border: 2rpx solid #ddd;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
transition: all 0.3s ease;
&.checked{
border-color: #8B2316;
}
}
}
}
.patient-list {
//
&::-webkit-scrollbar {
display: none;
}
.patient-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
padding: 30rpx 40rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.patient-checkbox{
margin-right: 8rpx;
.checkbox{
width: 40rpx;
height: 40rpx;
border: 2rpx solid #ddd;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
transition: all 0.3s ease;
&.checked{ border-color: #8B2316; }
}
}
.patient-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 12rpx;
overflow: hidden;
}
.patient-info {
flex: 1;
.patient-name {
font-size: 32rpx;
color: #333;
margin-top: 24rpx;
}
.follow-up-time {
font-size: 24rpx;
color: #999;
display: flex;
justify-content: flex-end;
}
}
}
}
}
.bottom-bar{
position: fixed;
left: 0;
right: 0;
bottom: 0;
background: #fff;
box-shadow: 0 -6rpx 20rpx rgba(0,0,0,0.06);
padding: 20rpx 30rpx env(safe-area-inset-bottom);
display: flex;
justify-content: flex-end;
.align-right{justify-content:flex-end;}
.clear-btn{
width: 100%;
margin-bottom: 20rpx;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border-radius: 12rpx;
border: 2rpx solid #8B2316;
color: #8B2316;
font-size: 28rpx;
}
}
</style>

View File

@ -0,0 +1,279 @@
<template>
<view class="quick-reply-page">
<uni-nav-bar
left-icon="left"
@clickLeft="goBack"
title="快捷回复"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#f5f5f5"
/>
<!-- 顶部添加快捷回复入口 -->
<view class="add-entry" @click="onAdd">
<view class="row">
<view class="add-icon">
<uni-icons type="plusempty" color="#8B2316" size="22" />
</view>
<text class="add-text">添加快捷回复</text>
</view>
<view class="bar">
</view>
</view>
<!-- 列表 -->
<scroll-view class="list" scroll-y>
<view
class="item"
v-for="(text, index) in replies"
:key="index"
@click="onSelect(text.replystr)"
@longpress="onLongPress(text.uuid)"
>
<text class="item-text">{{text.replystr }}</text>
</view>
</scroll-view>
<!-- 输入内容弹窗 -->
<UniPopup
ref="popupRef"
type="center"
background-color="#ffffff"
mask-background-color="rgba(0,0,0,0.4)"
>
<view class="input-popup">
<view class="popup-title">新增快捷回复</view>
<textarea
v-model="inputContent"
class="popup-textarea"
:maxlength="-1"
placeholder="请输入回复内容"
auto-height
/>
<view class="popup-actions">
<view class="btn cancel" @click="onCancel">取消</view>
<view class="btn confirm" @click="onConfirm">确定</view>
</view>
</view>
</UniPopup>
</view>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance } from 'vue'
// @ts-ignore
const delUuid=ref('');
import UniPopup from '@/components/uni-components/uni-popup/components/uni-popup/uni-popup.vue'
import api from '@/api/api'
//
const replies = ref([])
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({ url: '/pages/index/index' })
},
})
}
const popupRef = ref(null)
const inputContent = ref('')
const onAdd = () => {
// @ts-ignore
popupRef.value && popupRef.value.open('center')
}
const onSelect = (text) => {
uni.$emit('quickReply', text);
uni.navigateBack()
}
const getQuickReplyList = async () => {
const res = await api.quickReplyList({})
if(res.code==200){
replies.value = res.data
}
}
const addQuickReply = async () => {
const res = await api.addQuickReply({
replystr:inputContent.value
})
if(res.code==200){
uni.showToast({title: '添加成功', icon: 'none'})
getQuickReplyList()
}
}
const deleteQuickReply = async () => {
const res = await api.deleteQuickReply({
uuid:delUuid.value
})
if(res.code==200){
uni.showToast({title: '删除成功', icon: 'none'})
getQuickReplyList()
}
};
//
const onLongPress = (index) => {
delUuid.value = index;
uni.showActionSheet({
itemList: ['删除该条快捷回复'],
success: (res) => {
if (res.tapIndex === 0) {
uni.showModal({
title: '确认删除',
content: '删除后不可恢复,是否删除?',
confirmColor: '#8B2316',
success: (modalRes) => {
if (modalRes.confirm) {
deleteQuickReply()
}
},
})
}
},
})
}
const onCancel = () => {
// @ts-ignore
popupRef.value && popupRef.value.close()
inputContent.value = ''
}
const onConfirm = () => {
const text = inputContent.value.trim()
if (!text) {
uni.showToast({ title: '请输入内容', icon: 'none' })
return
}
replies.value = [text, ...replies.value]
uni.setClipboardData({ data: text, showToast: false })
uni.showToast({ title: '已添加并复制', icon: 'none' })
onCancel()
}
onMounted(() => {})
</script>
<style scoped lang="scss">
.bar{
width:100%;
height:20rpx;
background-color: #e3e3e3;
}
.quick-reply-page {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #ffffff;
}
/* 顶部添加入口 */
.add-entry {
position: fixed;
top: 140rpx; /* 与导航栏高度一致 */
left: 0;
right: 0;
z-index: 2;
}
.row{
display: flex;
align-items: center;
padding: 28rpx 30rpx;
background-color: #ffffff;
}
.add-icon {
width: 56rpx;
height: 56rpx;
border-radius: 50%;
border: 2rpx solid #8b2316;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.add-text {
font-size: 30rpx;
color: #8b2316;
}
/* 列表区域 */
.list {
position: fixed;
top: 272rpx; /* 导航栏 + 添加入口高度(约) */
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
}
.item {
padding: 28rpx 30rpx;
border-bottom: 1rpx solid #eeeeee;
}
.item-text {
font-size: 30rpx;
color: #333333;
line-height: 1.6;
word-break: break-all;
}
/* 弹窗样式 */
.input-popup{
width: 640rpx;
background-color: #ffffff;
border-radius: 16rpx;
padding: 30rpx;
box-sizing: border-box;
}
.popup-title{
font-size: 32rpx;
color: #333333;
font-weight: 500;
margin-bottom: 20rpx;
}
.popup-textarea{
width: 100%;
min-height: 160rpx;
background: #f7f7f7;
border-radius: 12rpx;
padding: 20rpx;
box-sizing: border-box;
font-size: 28rpx;
color: #333333;
}
.popup-actions{
display: flex;
justify-content: space-between;
margin-top: 24rpx;
}
.btn{
flex: 1;
height: 72rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
}
.btn.cancel{
background-color: #ffffff;
border: 2rpx solid #8b2316;
color: #8b2316;
margin-right: 20rpx;
}
.btn.confirm{
background-color: #8b2316;
color: #ffffff;
}
</style>

View File

@ -0,0 +1,762 @@
<template>
<view class="search-header">
<view class="back-icon" @click="goBack">
<uni-icons type="left" size="30" color="#8B2316"></uni-icons>
</view>
<view class="search-input-wrapper">
<view class="search-icon">
<uni-icons type="search" size="20" color="#999"></uni-icons>
</view>
<input
class="search-input"
placeholder="请输入搜索内容"
v-model="keywords"
@input="onSearchInput"
@confirm="onSearchConfirm"
confirm-type="search"
/>
</view>
<view class="cancel-btn" @click="goSearch">
<text class="cancel-text">搜索</text>
</view>
</view>
<view class="page">
<!-- <view class="top">
<view class="filter-bar">
<view class="search-box" @click="goToSearch">
<uni-icons type="search" size="16" color="#999"></uni-icons>
<text class="search-text">搜索</text>
</view>
<view class="divider"></view>
<view class="filter-item active" @click="toggleSort">
<text>{{type==1?'最新':'最热'}}</text>
<up-image v-if="type==1" :src="upImg" width="20rpx" height="26rpx" ></up-image>
<up-image v-else :src="downImg" width="20rpx" height="26rpx" ></up-image>
</view>
<view class="divider"></view>
<view class="filter-item" :class="{ active: isFilterActive }" @click="showFilterPopup">
<text>筛选</text>
<up-image :src="isFilterActive ? filterOn : filter" width="30rpx" height="30rpx" ></up-image>
</view>
</view>
</view> -->
<!-- 筛选弹窗 -->
<view class="filter-popup" v-if="showFilter" @click="hideFilterPopup">
<view class="filter-content" @click.stop>
<!-- 筛选标签网格 -->
<view class="filter-tags">
<view
class="tag-item"
v-for="(tag, index) in filterTags"
:key="index"
:class="{ active: tag.selected }"
@click="toggleTag(index)"
>
{{ tag.DES }}
</view>
</view>
<!-- 底部按钮 -->
<view class="filter-buttons">
<view class="btn-reset" @click="resetFilter">重置</view>
<view class="btn-confirm" @click="confirmFilter">确定</view>
</view>
</view>
</view>
<!-- 文章列表 -->
<scroll-view
class="article-list"
scroll-y
refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
:lower-threshold="100"
>
<view class="article-item" v-for="(article, index) in articleList" :key="article.uuid || index" @click="goToDetail(article)">
<image class="article-image" :src="article.image" mode="aspectFill"></image>
<view class="article-content">
<view class="article-title">{{ article.title }}</view>
<view class="article-meta">
<view class="date-tag" v-if="article.isToday">今日</view>
<view class="date" v-else>{{ article.date }}</view>
<view class="stats">
<view class="stat-item">
<uni-icons type="eye" size="12" color="#999"></uni-icons>
<text>{{ article.views }}</text>
</view>
<view class="stat-item">
<uni-icons type="hand-up" size="13" color="#999"></uni-icons>
<text>{{ article.likes }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 加载更多状态 -->
<view class="load-more" v-if="loading">
<uni-icons type="spinner-cycle" size="20" color="#999"></uni-icons>
<text class="load-text">加载中...</text>
</view>
<!-- 没有更多数据 -->
<view class="no-more" v-if="!hasMore && articleList.length > 0">
<text class="no-more-text">没有更多数据了</text>
</view>
<!-- 空状态 -->
<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>
</view>
</scroll-view>
<!-- 底部导航栏 -->
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import upImg from "@/static/cb_up.png"
import downImg from "@/static/cb_up.png"
import filter from "@/static/cb_screen_no.png"
import filterOn from "@/static/cb_screen_yes.png"
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';
const type=ref(1)
//
const currentPage = ref(1);
const pageSize = ref(10);
const hasMore = ref(true);
const loading = ref(false);
const refreshing = ref(false);
//
const activeTab = ref(0);
const showFilter = ref(false);
const isFilterActive = ref(false);
const toggleSort = () => {
type.value = type.value === 1 ? 2 : 1;
//
currentPage.value = 1;
hasMore.value = true;
polularScienceArticleListByKeywordsNew(true);
};
const goSearch = () => {
if(!keywords.value){
uni.showToast({
title: '请输入搜索内容',
icon: 'none'
});
return;
}
polularScienceArticleListByKeywordsNew(true);
};
const loadGuideTags = async () => {
try {
const res = await api.guideTag({
type:4
});
if(res && res.code === 200 && res.data) {
// API
filterTags.value = res.data.map(tag => ({
...tag,
selected: false
}));
console.log('指南标签加载成功:', filterTags.value);
}
} catch (e) {
console.error('加载指南标签失败:', e);
}
};
const polularScienceArticleListByKeywordsNew = async (isRefresh = false) => {
if (loading.value) return;
loading.value = true;
try {
const page = isRefresh ? 1 : currentPage.value;
const res = await api.polularScienceArticleListByKeywordsNew({
keywords: keywords.value,
page: page,
type: type.value
});
if (res && res.code === 200 && res.data) {
const newData = res.data.list || [];
if (isRefresh) {
//
articleList.value = newData.map(item => formatArticleData(item));
currentPage.value = 1;
} else {
//
const formattedData = newData.map(item => formatArticleData(item));
articleList.value = [...articleList.value, ...formattedData];
}
//
hasMore.value = newData.length >= pageSize.value;
if (!isRefresh) {
currentPage.value++;
}
console.log('文章数据加载成功:', articleList.value);
}
} catch (error) {
console.error('获取文章列表失败:', error);
uni.showToast({
title: '获取数据失败',
icon: 'error',
duration: 2000
});
} finally {
loading.value = false;
refreshing.value = false;
}
};
//
const formatArticleData = (item) => {
// item
if (!item) return null;
const submitDate = item.submitDate ? new Date(item.submitDate) : new Date();
const today = new Date();
const isToday = submitDate.toDateString() === today.toDateString();
return {
uuid: item.uuid || '',
title: item.topic || '无标题',
summary: item.summary || '',
image: item.imgPath ? docUrl + item.imgPath : '/static/liver_knowledge.png',
date: isToday ? '今日' : formatDate(submitDate),
isToday: isToday,
views: item.readnum || 0,
likes: item.agreenum || 0,
tags: item.tags || '',
path: item.path || '',
submitDate: item.submitDate || ''
};
};
//
const formatDate = (date) => {
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${month}-${day}`;
};
//
const filterTags = ref([]);
//
const articleList = ref([]);
//
const switchTab = (index) => {
if(index==1){
navTo({
url: '/pages_app/patientVideo/patientVideo'
})
}else if(index==2){
let url=encodeURIComponent('https://wx.igandan.com/wxPatient/index.htm#/problem?link=share&fromtype=doctor')
navTo({
url: '/pages_app/webview/webview?url='+url
})
}
};
const goToDetail = (article) => {
console.log('查看文章详情:', article);
//
uni.$emit('articelItem', article);
uni.navigateBack();
};
//
const goToSearch = () => {
uni.navigateTo({
url: '/pages_app/search/search'
});
};
//
const onRefresh = async () => {
refreshing.value = true;
await polularScienceArticleListByKeywordsNew(true);
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
};
//
const onLoadMore = async () => {
if (!hasMore.value || loading.value) return;
await polularScienceArticleListByKeywordsNew(false);
};
const goToPage = (page) => {
console.log('跳转到页面:', page);
//
if (page === 'index') {
uni.switchTab({
url: '/pages/index/index'
});
}
};
//
const showFilterPopup = () => {
showFilter.value = true;
};
const goBack = () => {
uni.navigateBack({
delta:1,
fail(){
uni.redirectTo({
url:'/pages/index/index'
})
}
});
};
const hideFilterPopup = () => {
showFilter.value = false;
};
const toggleTag = (index) => {
filterTags.value[index].selected = !filterTags.value[index].selected;
};
const resetFilter = () => {
filterTags.value.forEach(tag => {
tag.selected = false;
});
isFilterActive.value = false;
};
const keywords=ref('');
const confirmFilter = () => {
//
const hasSelected = filterTags.value.some(tag => tag.selected);
isFilterActive.value = hasSelected;
//
if (hasSelected) {
const selectedTags = filterTags.value.filter(tag => tag.selected).map(tag => tag.DES);
console.log('选中的标签:', selectedTags);
keywords.value = selectedTags.join(',');
} else {
keywords.value = '';
}
//
currentPage.value = 1;
hasMore.value = true;
showFilter.value = false;
polularScienceArticleListByKeywordsNew(true);
};
onShow(() => {
polularScienceArticleListByKeywordsNew(true);
loadGuideTags();
});
</script>
<style lang="scss" scoped>
//
$primary-color: #ff6b6b;
$theme-color: #8B2316;
$white: #fff;
$gray-bg: #f5f5f5;
$gray-light: #eee;
$gray-medium: #999;
$gray-dark: #666;
$text-color: #333;
//
$border-radius: 8px;
$border-radius-small: 6px;
$padding: 15px;
$padding-small: 10px;
.page {
background-color: $gray-bg;
height: calc(100vh - 140rpx);
display: flex;
overflow-y: hidden;
flex-direction: column;
//
}
.top{
width:100%;
position: fixed;
top:140rpx;
}
//
.tabs {
background-color: $white;
display: flex;
border-bottom: 1px solid $gray-light;
.tab-item {
flex: 1;
text-align: center;
padding: $padding 0;
font-size: 16px;
color: $gray-dark;
position: relative;
&.active {
color: $theme-color;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 30px;
height: 2px;
}
}
}
}
//
.search-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background-color: #eeeeee;
display: flex;
height:140rpx;
align-items: center;
padding:0 30rpx;
gap: 20rpx;
.search-input-wrapper {
flex: 1;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 20rpx;
padding: 0 20rpx;
height:70rpx;
.search-icon {
margin-right: 15rpx;
}
.search-input {
flex: 1;
height: 80rpx;
font-size: 28rpx;
color: #333;
}
.clear-icon {
margin-left: 15rpx;
padding: 10rpx;
}
}
.cancel-btn {
padding: 10rpx 20rpx;
background:#8B2316;
border-radius: 10rpx;
.cancel-text {
font-size: 28rpx;
color: #fff;
}
}
}
//
.filter-bar {
background-color: $white;
padding: $padding-small $padding;
display: flex;
align-items: center;
border-bottom: 1px solid $gray-light;
.search-box {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
.search-text {
font-size: 14px;
color: $gray-medium;
}
}
.divider {
width: 1px;
height: 16px;
background-color: $gray-medium;
margin: 0 $padding;
}
.filter-item {
flex:1;
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
font-size: 14px;
color: #999;
transition: all 0.3s ease;
}
}
.filter-item.active{
color: $theme-color;
}
//
.article-list {
position: fixed;
bottom: 0;
top:140rpx;
padding-bottom: 0rpx;
.article-item {
background-color: $white;
margin-bottom: $padding-small;
border-radius: $border-radius;
padding: $padding;
display: flex;
gap: 12px;
.article-image {
width: 80px;
height: 80px;
border-radius: $border-radius-small;
flex-shrink: 0;
}
.article-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
.article-title {
font-size: 16px;
color: $text-color;
line-height: 1.4;
margin-bottom: $padding-small;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
}
.article-meta {
display: flex;
justify-content: space-between;
align-items: center;
.date-tag {
background-color: $primary-color;
color: $white;
font-size: 12px;
padding: 2px 6px;
border-radius: 3px;
}
.date {
font-size: 12px;
color: $gray-medium;
}
.stats {
display: flex;
gap: $padding;
.stat-item {
display: flex;
align-items: center;
gap: 3px;
font-size: 12px;
color: $gray-medium;
}
}
}
}
}
//
.load-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx;
color: #999;
.load-text {
margin-left: 10rpx;
font-size: 28rpx;
}
}
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx;
.no-more-text {
font-size: 28rpx;
color: #999;
}
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 30rpx;
.empty-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
}
}
//
.bottom-nav {
background-color: $white;
display: flex;
border-top: 1px solid $gray-light;
padding: 8px 0;
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
font-size: 12px;
color: $gray-medium;
&.active {
color: $primary-color;
}
text {
font-size: 10px;
}
}
}
//
.filter-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
// background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
z-index: 9999;
.filter-content {
margin-top:329rpx;
background-color: $white;
// border-radius: 20rpx 20rpx 0 0;
padding: 20rpx 30rpx 60rpx;
width: 100%;
max-height: 80vh;
.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 24rpx;
border-radius: 30rpx;
font-size: 26rpx;
border: 2rpx solid #efefef;
transition: all 0.3s ease;
&.active {
background-color: #fff;
color: $theme-color;
border-color: $theme-color;
}
}
}
.filter-buttons {
display: flex;
gap: 20rpx;
position: fixed;
bottom: 30rpx;
left: 30rpx;
right: 30rpx;
.btn-reset,
.btn-confirm {
flex: 1;
height: 70rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
}
.btn-reset {
background-color: $white;
color: $theme-color;
border: 2rpx solid $theme-color;
}
.btn-confirm {
border: 2rpx solid $theme-color;
background-color: $theme-color;
color: $white;
}
}
}
}
</style>

860
pages_chat/video/video.vue Normal file
View File

@ -0,0 +1,860 @@
<template>
<uni-nav-bar left-icon="left"
@clickLeft="goBack" title="患教视频" fixed color="#8B2316" height="140rpx" :border="false" backgroundColor="#eeeeee"></uni-nav-bar>
<view class="video-page">
<!-- Header -->
<!-- Fixed Filter Tabs -->
<view class="filter-tabs-container">
<view class="filter-tabs">
<view
class="tab-item"
@click="showAllVideoPopup=!showAllVideoPopup"
>
<up-image :src="isAllActive?allOnImg:allImg" width="30rpx" height="30rpx" ></up-image>
<text class="tab-text" :class="{active:isAllActive}">{{typeName}}</text>
<up-image :src="isAllActive?selectOnImg:selectImg" width="30rpx" height="30rpx" ></up-image>
</view>
<view class="divider"></view>
<view class="right">
<view
class="tab-item"
@click="toggleSort"
>
<text class="tab-text active">{{sort==2?'最新':'最热'}}</text>
<view class="newbox">
<up-image :src="sort==2?upImg:downImg" width="20rpx" height="26rpx" ></up-image>
</view>
</view>
<!-- <view
class="tab-item"
@click="showFilterPopup"
>
<text class="tab-text " :class="{active:isFilterActive}">筛选</text>
<view class="filterbox">
<up-image :src="isFilterActive ? filterOn : filter" width="30rpx" height="30rpx" ></up-image>
</view>
</view> -->
</view>
</view>
</view>
<!-- Main Content -->
<scroll-view
class="scroll-view"
scroll-y="true"
refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
lower-threshold="100"
>
<!-- Video List -->
<view class="video-list">
<view
class="video-item"
v-for="(video, index) in videoList"
:key="video.id || video.uuid"
@click="playVideo(video)"
>
<view class="video-thumbnail">
<image :src="docUrl + video.imgpath" mode="aspectFill"></image>
</view>
<view class="video-info">
<view class="video-title">{{video.title || video.name}}</view>
<view class="video-meta">
<text class="author">{{video.public_name}}</text>
<view class="stats">
<uni-icons type="eye" size="14" color="#999"></uni-icons>
<text class="view-count">{{video.readnum}}</text>
</view>
</view>
</view>
</view>
</view>
<!-- Loading More -->
<uni-load-more
v-if="videoList.length > 0"
:status="loadMoreStatus"
:content-text="{
contentdown: '上拉加载更多',
contentrefresh: '正在加载...',
contentnomore: '没有更多数据了'
}"
></uni-load-more>
<!-- Empty State -->
<view class="empty-state" v-if="videoList.length === 0 && !loading">
<empty></empty>
</view>
</scroll-view>
<!-- 筛选弹窗 -->
<view class="filter-popup" v-if="showFilter" @click="hideFilterPopup">
<view class="filter-content" @click.stop>
<!-- 筛选标签网格 -->
<view class="filter-tags">
<view
class="tag-item"
v-for="(tag, index) in filterTags"
:key="index"
:class="{ active: tag.selected }"
@click="toggleTag(index)"
>
{{ tag.NAME }}
</view>
</view>
<!-- 底部按钮 -->
<view class="filter-buttons">
<view class="btn-reset" @click="resetFilter">重置</view>
<view class="btn-confirm" @click="confirmFilter">确定</view>
</view>
</view>
</view>
<!-- 全部视频弹窗 -->
<view class="all-video-popup" v-if="showAllVideoPopup" @click="closeAllVideoPopup">
<view class="popup-content" @click.stop>
<view class="popup-body">
<view
class="category-item"
:class="{ active: typeUuid === item.uuid }"
v-for="item in videoTypeList"
:key="item.uuid"
@click="selectCategory(item.uuid)"
>
{{ item.name }}
</view>
<view v-if="videoTypeList.length === 0" class="empty-content">
<text>暂无数据</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, computed,reactive } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import api from '@/api/api.js';
import allImg from "@/static/video_all.png"
import allOnImg from "@/static/video_select.png"
import selectImg from "@/static/all_video.png"
import selectOnImg from "@/static/select_video.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_down.png"
import docUrl from '@/utils/docUrl';
import navTo from '@/utils/navTo';
import empty from '@/components/empty/empty.vue';
const isAllActive=ref(false)
//
const videoList = ref([]);
const activeTab = ref(1); // ""
const loading = ref(false);
const refreshing = ref(false);
const loadMoreStatus = ref('more'); // more, loading, noMore
const currentPage = ref(1);
const pageSize = ref(10);
const hasMoreData = ref(true);
const filteredContent=ref([])
const sort =ref(2);
const keywords=ref('');
const typeUuid=ref('0e5fa3d76b8047528fdd3c452b77e9dd');
const typeName=ref('乙肝')
const isFilterActive=ref(false)
const showFilter = ref(false);
const yearList=ref([]);
const selectYearContent=reactive({})
//
const filterTags = ref([]);
//
const showAllVideoPopup = ref(false);
const selectedCategory = ref('全部');
const selectCategory=(uuid)=>{
console.log(222)
console.log(uuid)
typeUuid.value=uuid;
typeName.value=videoTypeList.value.find(item=>item.uuid==uuid).name;
isAllActive.value=true;
closeAllVideoPopup();
currentPage.value = 1;
hasMoreData.value = true;
loadVideoData();
}
const videoTypeList=ref([]);
const patientVideoNew=async ()=>{
const res=await api.patientVideoNew();
if(res.code==200){
videoTypeList.value=res.data;
}
}
//
onMounted(() => {
});
onShow(() => {
// API
currentPage.value = 1;
hasMoreData.value = true;
loadGuideTags();
loadVideoData();
patientVideoNew();
});
//
//
const loadGuideTags = async () => {
try {
const res = await api.guideTag({
type:5
});
console.log('指南标签API响应:', res);
if(res && res.code === 200 && res.data) {
// API
filterTags.value = res.data.map(tag => ({
...tag,
selected: false
}));
console.log('指南标签加载成功:', filterTags.value);
}
} catch (e) {
console.error('加载指南标签失败:', e);
}
};
//
const loadVideoData = async (isRefresh = false) => {
if (loading.value && !isRefresh) return;
loading.value = true;
try {
const response = await api.patientVideoByKeyWordsNew({
page: currentPage.value,
pageSize: pageSize.value,
keywords: keywords.value,
sort:sort.value,
typeUuid:typeUuid.value,
});
console.log('视频列表API原始响应:', response);
//
let videoData = {};
if (response && response.data) {
if (response.data.code === 200) {
videoData = response.data.data || {};
} else if (response.code === 200) {
videoData = response.data || {};
}
} else if (response && response.code === 200) {
videoData = response.data || {};
}
console.log('解析后的视频数据:', videoData);
if (videoData && videoData.list) {
const { list, totalPage,pageNumber } = videoData;
console.log('视频列表数据:', list);
if (isRefresh) {
videoList.value = list || [];
currentPage.value = 1;
} else {
videoList.value = [...videoList.value, ...(list || [])];
}
hasMoreData.value = totalPage>pageNumber;
loadMoreStatus.value = hasMoreData.value ? 'more' : 'noMore';
} else {
throw new Error(response?.message || '获取数据失败');
}
} catch (error) {
console.error('加载视频失败:', error);
} finally {
loading.value = false;
refreshing.value = false;
}
};
//
const onRefresh = () => {
refreshing.value = true;
currentPage.value = 1;
hasMoreData.value = true;
loadVideoData(true);
};
//
const onLoadMore = () => {
if (!hasMoreData.value || loading.value) return;
loadMoreStatus.value = 'loading';
currentPage.value++;
loadVideoData();
};
//
const switchTab = (tabIndex) => {
activeTab.value = tabIndex;
//
currentPage.value = 1;
hasMoreData.value = true;
loadVideoData();
};
//
const playVideo = (video) => {
uni.$emit('videoItem', video);
uni.navigateBack();
};
//
const goSearch = () => {
uni.navigateTo({
url: '/pages_app/search/search'
});
};
//
//
const searchVideos = async (keywords) => {
if (!keywords || keywords.trim() === '') {
loadVideoData();
return;
}
try {
console.log('搜索关键词:', keywords.trim());
const response = await api.patientVideoByKeyWordsNew({
keywords: keywords.trim(),
page: 1,
pageSize: pageSize.value
});
console.log('搜索API原始响应:', response);
//
let searchData = {};
if (response && response.data) {
if (response.data.code === 200) {
searchData = response.data.data || {};
} else if (response.code === 200) {
searchData = response.data || {};
}
} else if (response && response.code === 200) {
searchData = response.data || {};
}
console.log('解析后的搜索数据:', searchData);
if (searchData && searchData.list) {
const { list, hasMore } = searchData;
console.log('搜索结果:', list);
videoList.value = list || [];
hasMoreData.value = hasMore !== false;
loadMoreStatus.value = hasMoreData.value ? 'more' : 'noMore';
currentPage.value = 1;
}
} catch (error) {
console.error('搜索视频失败:', error);
uni.showToast({
title: '搜索失败,请重试',
icon: 'none'
});
}
};
//
const openAllVideoPopup = () => {
showAllVideoPopup.value = true;
};
const closeAllVideoPopup = () => {
showAllVideoPopup.value = false;
};
//
const showFilterPopup = () => {
showFilter.value = true;
};
const hideFilterPopup = () => {
showFilter.value = false;
};
const toggleTag = (index) => {
filterTags.value[index].selected = !filterTags.value[index].selected;
};
const resetFilter = () => {
//
filterTags.value.forEach(tag => tag.selected = false);
//
//
keywords.value = '';
//
currentPage.value = 1;
hasMoreData.value = true;
loadVideoData(true);
};
const confirmFilter = () => {
const selectedTags = filterTags.value.filter(tag => tag.selected);
console.log('选中的筛选标签:', selectedTags);
for (var i = 0; i < selectedTags.length; i++) {
if(keywords.value){
keywords.value+=","+selectedTags[i].NAME
}else{
keywords.value=selectedTags[i].NAME
}
}
isFilterActive.value=true;
hideFilterPopup();
//
currentPage.value = 1;
hasMoreData.value = true;
loadVideoData(true);
};
//
const goBack = () => {
uni.navigateBack({
fail() {
uni.redirectTo({
url: '/pages/index/index'
});
}
});
}
const toggleSort = () => {
sort.value = sort.value === 1 ? 2 : 1; // 1=, 2=
currentPage.value = 1;
hasMoreData.value = true;
loadVideoData();
};
</script>
<style scoped lang="scss">
$primary-color: #ff6b6b;
$theme-color: #8B2316;
$white: #fff;
$gray-bg: #f5f5f5;
$gray-light: #eee;
$gray-medium: #999;
$gray-dark: #666;
$text-color: #333;
//
$border-radius: 8px;
$border-radius-small: 6px;
$padding: 15px;
$padding-small: 10px;
.video-page {
background-color: #f5f5f5;
height: 100vh;
display: flex;
overflow: hidden;
flex-direction: column;
}
.divider {
width: 1px;
height: 16px;
background-color: $gray-medium;
margin: 0 $padding;
}
/* Header Styles */
.header {
background-color: #fff;
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1);
position: relative;
z-index: 100;
}
.header-left, .header-right {
display: flex;
align-items: center;
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #8B4513;
}
/* Scroll View */
.scroll-view {
position: fixed;
bottom: 0rpx;
height: calc(100vh - 140rpx); /* 固定高度,减去导航栏高度 */
background-color: #f5f5f5;
top: 172rpx; /* 为导航栏留出空间 */
}
/* Filter Tabs */
.filter-tabs {
background-color: #fff;
display: flex;
align-items: center;
justify-content:center;
padding: 30rpx 30rpx;
}
.tab-item {
display: flex;
align-items: center;
justify-content: center;
flex:1;
padding: 10rpx 0;
}
.tab-item:last-child{
margin-right: 0;
}
.right{
flex:1;
display: flex;
}
.tab-item.active .tab-text {
color: #E74C3C;
font-weight: 600;
}
.tab-text {
margin: 0 8rpx;
font-size: 28rpx;
color: #666;
}
.tab-item:first-child .tab-text{
max-width:155rpx;
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tab-item .tab-text.active {
color: #8B2316;
}
/* Fixed Filter Tabs Container */
.filter-tabs-container {
position: fixed;
top: 140rpx; /* uni-nav-bar height */
left: 0;
right: 0;
z-index: 40;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
}
.doctor-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
overflow: hidden;
}
.doctor-avatar image {
width: 100%;
height: 100%;
}
/* Video List */
.video-list {
padding: 0 20rpx;
margin-top: 100rpx; /* 为固定的Filter Tabs留出空间 */
display: flex;
flex-wrap: wrap;
gap: 16rpx; /* 卡片间距 */
}
.video-item {
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 0;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
width: calc(50% - 8rpx); /* 两列布局减半gap */
min-width: 300rpx; /* 确保最小宽度 */
flex-shrink: 0; /* 防止收缩 */
}
.video-thumbnail {
position: relative;
height: 200rpx;
}
.video-thumbnail image {
width: 100%;
height: 100%;
}
.video-duration {
margin-top: 8rpx;
}
.video-duration text {
font-size: 22rpx;
color: #999;
}
.video-info {
padding: 24rpx;
}
.video-title {
font-size: 28rpx;
font-weight: 400;
color: #333;
line-height: 1.4;
margin-bottom: 16rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
height: 2.8em; /* 固定2行高度1.4 * 2 = 2.8em */
}
.video-meta {
display: flex;
justify-content: space-between;
align-items: center;
}
.author {
font-size: 24rpx;
color: #999;
}
.stats {
display: flex;
align-items: center;
}
.view-count {
font-size: 24rpx;
color: #999;
margin-left: 8rpx;
}
/* Empty State */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.empty-state image {
width: 200rpx;
height: 200rpx;
margin-bottom: 30rpx;
opacity: 0.5;
}
.empty-state text {
color: #999;
font-size: 28rpx;
}
.newbox{
margin-top: -12rpx;
}
/* 全部视频弹窗样式 */
.all-video-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: flex-end;
}
.popup-content {
width: 100%;
background-color: #fff;
height:calc(100vh - 272rpx);
overflow-y: scroll;
}
.popup-header {
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
background-color: #fff;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.popup-body {
height:100%;
}
.category-item {
padding: 30rpx;
font-size: 28rpx;
color: #333;
border-bottom: 1rpx solid #f0f0f0;
background-color: #fff;
transition: background-color 0.2s ease;
}
.category-item:last-child {
border-bottom: none;
}
.category-item.active {
background-color: #f8f8f8;
color: #8B2316;
}
.category-item:active {
background-color: #e8f4fd;
}
.empty-content {
display: flex;
justify-content: center;
align-items: center;
height: 200rpx;
color: #999;
font-size: 28rpx;
}
//
.filter-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
// background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
z-index: 9999;
.filter-content {
margin-top:272rpx;
overflow-y:scroll;
background-color: $white;
// border-radius: 20rpx 20rpx 0 0;
padding: 20rpx 30rpx 60rpx;
width: 100%;
height: calc(100vh - 272rpx);
.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 0; /* 去掉左右padding仅保留上下 */
border-radius: 30rpx;
font-size: 26rpx;
border: 2rpx solid #efefef;
transition: all 0.3s ease;
width: 152rpx; /* 固定宽度 */
text-align: center; /* 文本居中 */
white-space: nowrap; /* 单行显示 */
overflow: hidden; /* 隐藏溢出 */
text-overflow: ellipsis; /* 超出显示省略号 */
&.active {
background-color: #fff;
color: $theme-color;
border-color: $theme-color;
}
}
}
.filter-buttons {
display: flex;
gap: 20rpx;
position: fixed;
bottom: 30rpx;
left: 30rpx;
right: 30rpx;
background-color: #fff; /* 背景色为白色 */
.btn-reset,
.btn-confirm {
flex: 1;
height: 70rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
}
.btn-reset {
background-color: $white;
color: $theme-color;
border: 2rpx solid $theme-color;
}
.btn-confirm {
border: 2rpx solid $theme-color;
background-color: $theme-color;
color: $white;
}
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
static/outpatient_true.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
static/quck_message.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB