9.16下午更新 包含im

This commit is contained in:
zoujiandong
2025-09-16 16:19:29 +08:00
parent 215b3298d0
commit b884a8f6fc
127 changed files with 59972 additions and 182 deletions
+479
View File
@@ -0,0 +1,479 @@
<template>
<view class="consult-page">
<!-- 顶部导航与标签 -->
<navBar title="公益咨询" />
<view class="tabs">
<view :class="['tab', activeTab==='new' ? 'active' : '']" @tap="switchTab('new')">新的咨询</view>
<view :class="['tab', activeTab==='mine' ? 'active' : '']" @tap="switchTab('mine')">我已回答</view>
</view>
<view class="tabs-spacer"></view>
<!-- 列表 -->
<scroll-view
scroll-y
class="list-scroll"
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onReachBottom"
lower-threshold="80"
>
<view v-for="(item, idx) in displayList" :key="idx" class="consult-card" @click="goDetail(item.id)">
<view class="card-head">
<text class="user-name">{{ item.maskName }}</text>
<text class="date">{{ item.date }}</text>
</view>
<view class="card-body">
<text class="content">{{ item.content }}</text>
</view>
<view class="card-foot" v-if="bottomActive==='multi'">
<text class="reply-count">{{ item.answer_num }}位医生已回答</text>
<view v-if="item.tag" class="tag">{{ item.tag }}</view>
</view>
<view class="card-foot" v-else>
<view class="left">
<view class="detail">问题详情</view>
<view v-if="item.tag" class="tag">{{ item.tag }}</view>
</view>
</view>
</view>
<empty v-if="displayList.length===0" />
<view v-if="isLoading" style="text-align:center;color:#9aa0a6;padding:10px 0;">加载中...</view>
<view v-else-if="!hasMore && displayList.length>0" style="text-align:center;color:#9aa0a6;padding:10px 0;">没有更多了</view>
</scroll-view>
<!-- 底部操作条双按钮 Tab -->
<view class="bottom-bar" :style="{height: bottomBarHeight+'px'}">
<view :class="['bottom-tab', bottomActive==='quick' ? 'active' : '']" @click="switchBottomTab('quick')">快速问医生</view>
<view :class="['bottom-tab', bottomActive==='multi' ? 'active' : '']" @click="switchBottomTab('multi')">多对一解惑</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import navBar from '@/components/navBar/navBar.vue'
import empty from '@/components/empty/empty.vue'
import api from '@/api/api.js'
import navTo from '@/utils/navTo.js'
const activeTab = ref('new');
const bottomBarHeight = 56
const page=ref(1)
const pageSize=ref(10)
const hasMore = ref(true)
const isLoading = ref(false)
const isRefreshing = ref(false)
const goDetail=async(uuid)=>{
if(bottomActive.value==='quick'){
let userId=uni.getStorageSync('userInfo').uuid.toLowerCase();
let conversationId=userId+'|1|'+uuid.toLowerCase();
await uni.$UIKitStore.uiStore.selectConversation(conversationId)
navTo({
url:'/pages_chat/chat/index?from=consult'
})
}else{
let status=0;
if(activeTab.value==='new'){
status=0;
}else{
status=1;
}
navTo({
url:'/pages_app/consultDetail/consultDetail?uuid='+uuid+'&status='+status
})
}
};
function maskName(name){
if(!name) return '**'
const first = name.slice(0,1)
return `${first}**`
}
const newConsultList=async(isRefresh=false)=>{
if(isLoading.value) return
isLoading.value = true
if(isRefresh){
page.value = 1
hasMore.value = true
listNew.value = []
}
const res=await api.newConsultList({
page:page.value,
pageSize:pageSize.value
})
console.log(res)
if(res && res.code===200 && res.data && res.data.consult_list){
const list = Array.isArray(res.data.consult_list.list) ? res.data.consult_list.list : []
const mapped = list.map(item=>({
maskName: maskName(item.realName || ''),
date: (item.createDate || '').slice(0,10),
content: item.content || '',
replyCount: 0,
tag: item.diseaseName || '',
id:item.patientUuid || ''
}))
if(isRefresh){
listNew.value = mapped
}else{
listNew.value = page.value===1 ? mapped : [...listNew.value, ...mapped]
}
const totalPage = Number(res.data.consult_list.totalPage || 1)
hasMore.value = page.value < totalPage
}
isLoading.value = false
if(isRefresh){
isRefreshing.value = false
}
}
const consultListHis=async(isRefresh=false)=>{
if(isLoading.value) return
isLoading.value = true
if(isRefresh){
page.value = 1
hasMore.value = true
listMine.value = []
}
const res=await api.consultListHis({
page:page.value,
pageSize:pageSize.value
})
console.log(res)
if(res.code==200){
const list = Array.isArray(res.data.list) ? res.data.list : []
const mapped = list.map(item=>({
maskName: maskName(item.realName || ''),
date: (item.createDate || '').slice(0,10),
content: item.content || '',
replyCount: 0,
tag: item.diseaseName || '',
id:item.patientUuid || ''
}))
if(isRefresh){
listMine.value = mapped
}else{
listMine.value = page.value===1 ? mapped : [...listMine.value, ...mapped]
}
const totalPage = Number(res.data.totalPage || 1)
hasMore.value = page.value < totalPage
}
isLoading.value = false
if(isRefresh){
isRefreshing.value = false
}
}
const listNewInterrogation=async(isRefresh=false)=>{
if(isLoading.value) return
isLoading.value = true
if(isRefresh){
page.value = 1
hasMore.value = true
listNew.value = []
}
const res=await api.listNewInterrogation({
page:page.value,
pageSize:pageSize.value
})
if(res.code ==200){
const list =res.data.list;
console.log(1111)
console.log(list)
const mapped = list.map(item=>({
maskName: maskName(item.name || ''),
date: (item.create_date || '').slice(0,10),
content: item.your_question || '',
disease_describe:item.your_question || '',
answer_num: item.answer_num || 0,
tag: item.disease_name || '',
id:item.step1_uuid || ''
}))
if(isRefresh){
listNew.value = mapped
}else{
listNew.value = page.value===1 ? mapped : [...listNew.value, ...mapped]
}
const totalPage = Number(res.data.pages || 1)
hasMore.value = page.value < totalPage
}
isLoading.value = false
if(isRefresh){
isRefreshing.value = false
}
}
const listMyAnsweredInterrogation=async(isRefresh=false)=>{
if(isLoading.value) return
isLoading.value = true
if(isRefresh){
page.value = 1
hasMore.value = true
listMine.value = []
}
const res=await api.listMyAnsweredInterrogation({
page:page.value,
pageSize:pageSize.value
})
console.log(res)
if(res.code==200){
const list = Array.isArray(res.data.list) ? res.data.list : []
const mapped = list.map(item=>({
maskName: maskName(item.name || ''),
date: (item.create_date || '').slice(0,10),
content: item.your_question || '',
disease_describe:item.disease_describe || '',
answer_num: item.answer_num || 0,
tag: item.disease_name || '',
id:item.step1_uuid || ''
}))
if(isRefresh){
listMine.value = mapped
}else{
listMine.value = page.value===1 ? mapped : [...listMine.value, ...mapped]
}
const totalPage = Number(res.data.pages || 1)
hasMore.value = page.value < totalPage
}
isLoading.value = false
if(isRefresh){
isRefreshing.value = false
}
};
onShow(()=>{
page.value = 1
hasMore.value = true;
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(true)
}else{
consultListHis(true)
}
}else{
if(activeTab.value==='new'){
listNewInterrogation(true)
}else{
listMyAnsweredInterrogation(true)
}
}
})
const listNew = ref([])
const listMine = ref([])
const displayList = computed(() => (activeTab.value === 'new' ? listNew.value : listMine.value))
function switchTab(key) {
activeTab.value = key;
isLoading.value = false;
listNew.value = [];
listMine.value=[];
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(true)
}else{
consultListHis(true)
}
}else{
if(activeTab.value==='new'){
listNewInterrogation(true)
}else{
listMyAnsweredInterrogation(true)
}
}
}
function goBack() {
// #ifdef H5
history.back()
// #endif
// #ifndef H5
uni.navigateBack({ delta: 1 })
// #endif
}
// 底部tab交互
const bottomActive = ref('quick')
const switchBottomTab=(key)=>{
isLoading.value = false;
bottomActive.value = key;
listNew.value = [];
listMine.value=[];
if(key=='quick'){
if(activeTab.value==='new'){
newConsultList(true)
}else{
consultListHis(true)
}
}else{
if(activeTab.value==='new'){
console.log('listNewInterrogation')
listNewInterrogation(true);
}else{
listMyAnsweredInterrogation(true)
}
}
}
// 下拉刷新
function onRefresh(){
if(isRefreshing.value) return
isRefreshing.value = true
page.value = 1
hasMore.value = true
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(true)
}else{
consultListHis(true)
}
}else{
if(activeTab.value==='new'){
listNewInterrogation(true)
}else{
listMyAnsweredInterrogation(true)
}
}
}
// 触底加载
function onReachBottom(){
if(!hasMore.value || isLoading.value) return
page.value += 1
if(bottomActive.value==='quick'){
if(activeTab.value==='new'){
newConsultList(false)
}else{
consultListHis(false)
}
}else{
if(activeTab.value==='new'){
listNewInterrogation(false)
}else{
listMyAnsweredInterrogation(false)
}
}
}
</script>
<style lang="scss" scoped>
.consult-page {
background-color: #f7f7f7;
height: 100vh;
display: flex;
flex-direction: column;
}
.tabs {
display: flex;
justify-content: space-around;
align-items: center;
height: 44px;
padding: 0 16px;
position: fixed;
top: 140rpx;
left: 0;
right: 0;
z-index: 10;
background-color: #ffffff;
box-shadow: 0 1px 0 rgba(0,0,0,0.06);
.tab {
flex: 1;
text-align: center;
font-size: 16px;
color: #7a7a7a;
padding: 10px 0;
&.active { color: #8B2316; position: relative; }
&.active::after { content: ''; position: absolute; left: 25%; right: 25%; bottom: 2px; height: 3px; background-color: #8B2316; border-radius: 2px; }
}
}
.tabs-spacer { height: 44px; }
.list-scroll {
flex: 1;
position: fixed;
top: 228rpx;
bottom: 136rpx;
padding: 8px 0px 0 0px;
margin: 20rpx 30rpx 0;
box-sizing: border-box;
width:auto;
}
.consult-card {
background-color: #ffffff;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
.card-head {
display: flex;
justify-content: space-between;
align-items: center;
.user-name { font-size: 16px; color: #a31712; }
.date { font-size: 14px; color: #9aa0a6; }
}
.card-body {
background: #efefef;
padding: 20rpx;
margin: 10px 0;
.content {
word-break: break-all;
font-size: 15px; color: #2b2f33; line-height: 1.6;
} }
.card-foot {
.left{
display: flex;
.detail{
font-size: 28rpx;
background: #a31712;
color:#fff;
border-radius: 14rpx;
margin-right: 10rpx;
padding: 8rpx 16rpx;
border-radius: 28rpx;
font-size: 28rpx;
display: flex;
align-items: center;
justify-content: center;
border:2rpx solid #a31712;
}
}
display: flex; align-items: center; justify-content: space-between; margin-top: 8px;
.reply-count { font-size:28rpx; color: #8B2316; }
.tag {
display: flex;
align-items: center;
justify-content: center;
padding: 8rpx 16rpx; background: #fff5f5; color: #a31712; border-radius: 28rpx; font-size: 24rpx; border:2rpx solid #a31712;}
}
}
.bottom-bar {
position: fixed;
left: 0; right: 0; bottom: 0;
background-color: #ffffff;
box-shadow: 0 -2px 8px rgba(0,0,0,0.06);
display: flex; align-items: center; justify-content: center;
padding: 8px 12px;
gap: 12px;
.bottom-tab {
flex: 1;
height: 40px;
line-height: 40px;
text-align: center;
border-radius: 6px;
font-size: 16px;
color: #a31712;
background-color: #fff5f5;
}
.bottom-tab.active {
background-color: #a31712;
color: #ffffff;
}
}
</style>
+575
View File
@@ -0,0 +1,575 @@
<template>
<view class="consult-detail-page">
<!-- 导航栏 -->
<navBar title="问题详情" />
<!-- 内容区域 -->
<scroll-view scroll-y class="content-scroll">
<!-- 用户信息区域 -->
<view class="user-section">
<view class="user-info">
<view class="user-name">
<text class="name">{{ userInfo.name }}</text>
<text class="gender-age">{{ userInfo.gender }} {{ userInfo.age }}</text>
</view>
<view class="detail-btn" @click="goInfo">
<up-image :src="detailImg" width="183rpx" height="34rpx" ></up-image>
</view>
</view>
</view>
<!-- 疾病标签和日期 -->
<view class="tag-date-row">
<view class="disease-tag">
<text class="tag-text">{{ questionInfo.diseaseTag }}</text>
</view>
<view class="date">{{ questionInfo.date }}</view>
</view>
<!-- 问题内容 -->
<view class="question-content">
<text class="content-text">{{ questionInfo.diseaseDescribe }}</text>
</view>
<!-- 疾病描述 -->
<!-- <view v-if="questionInfo.diseaseDescribe" class="disease-describe">
<view class="describe-title">疾病描述</view>
<text class="describe-text">{{ questionInfo.diseaseDescribe }}</text>
</view> -->
<!-- 图片网格 -->
<view class="image-grid" v-if="questionInfo.images">
<image
v-if="questionInfo.images && questionInfo.images.split(',').length>0"
v-for="(img, index) in questionInfo.images.split(',')"
:key="index"
:src="docUrl+img"
class="grid-image"
mode="aspectFill"
@click="previewImage(docUrl+img, index)"
/>
</view>
<view class="bar"></view>
<!-- 医生回答区域 -->
<view class="doctor-reply-section">
<view class="section-title">医生回答</view>
<view class="doctor-cell" v-for="item in questionInfo.AnswerList" :key="item.answer_uuid">
<view class="doctor-card">
<view class="doctor-info">
<image :src="doctorReply.avatar" class="doctor-avatar" />
<view class="doctor-details">
<view class="doctor-name">{{ item.realname }}</view>
<view class="hospital-time-row">
<view class="doctor-hospital" >{{ item.hospital }}</view>
<view class="reply-time">{{ item.create_date }}</view>
</view>
</view>
</view>
<view class="reply-content">
<text class="reply-text">{{ item.note }}</text>
</view>
<view class="reply-content" style="background:none;pading:0">
<view v-if="item.imgs" class="reply-images">
<image
v-for="(img, idx) in item.imgs.split(',')"
:key="idx"
:src="docUrl+img"
class="reply-image"
@click="previewReplyImages(item, idx)"
/>
</view>
</view>
</view>
<view class="smallbar"></view>
</view>
</view>
</scroll-view>
<!-- 底部固定区域 -->
<view class="bottom-fixed">
<!-- 特别声明 -->
<view class="disclaimer">
<text class="disclaimer-title">特别声明</text>
<text class="disclaimer-text">答案仅为医生个人经验或建议分享不能视为诊断依据如有诊疗需求请务必前往正规医院就诊</text>
</view>
<!-- 编辑按钮 -->
<view class="edit-btn" @click="editQuestion">
{{status==1?'我要编辑':'我要回答'}}
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import navBar from '@/components/navBar/navBar.vue'
import detailImg from "@/static/iv_jiwangshi.png"
import navTo from '@/utils/navTo.js'
import { onLoad,onShow } from '@dcloudio/uni-app'
import docUrl from '@/utils/docUrl'
import api from "@/api/api.js"
const uuid = ref('');
const step1_uuid = ref('');
const answer_uuid = ref('');
const status = ref(0)
onLoad((options) => {
uuid.value = options.uuid || '125891f8c12145a99de01e29729978fb'
status.value = options.status || 0
console.log(uuid.value)
})
const goInfo=()=>{
navTo({
url:'/pages_app/patientInfo/patientInfo?step1_uuid='+step1_uuid.value
})
}
const getInterrogation=()=>{
api.getInterrogation({
uuid:uuid.value
}).then(res=>{
console.log(res)
if(res.code === '200' && res.data) {
step1_uuid.value = res.data.step1_uuid || ''
// 更新用户信息
userInfo.value = {
name: res.data.name || '提**',
gender: res.data.sex === 1 ? '男' : '女',
age: res.data.birthday ? calculateAge(res.data.birthday) : '未知'
}
// 更新问题信息
questionInfo.value = {
date: res.data.create_date ? formatDate(res.data.create_date) : '未知',
diseaseTag: res.data.disease_name || '未知疾病',
content: res.data.your_question || '暂无问题描述',
images: res.data.imgs || '',
AnswerList: res.data.AnswerList || [],
your_question: res.data.your_question || ''
}
// 更新疾病描述
if(res.data.disease_describe) {
questionInfo.value.diseaseDescribe = res.data.disease_describe
}
let user=uni.getStorageSync('userInfo');
let arr=res.data.AnswerList.filter(item=>{
return user.uuid == item.expert_uuid
})
if(arr.length>0){
answer_uuid.value = arr[0].answer_uuid;
}
}
})
}
onShow(()=>{
getInterrogation()
})
// 计算年龄
function calculateAge(birthday) {
const birthDate = new Date(birthday)
const today = new Date()
let age = today.getFullYear() - birthDate.getFullYear()
const monthDiff = today.getMonth() - birthDate.getMonth()
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--
}
return age.toString()
}
// 格式化日期
function formatDate(dateString) {
const date = new Date(dateString)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 用户信息
const userInfo = ref({
name: '提**',
gender: '男',
age: '15'
})
// 问题信息
const questionInfo = ref({
date: '2022-11-09',
diseaseTag: '甲型肝炎',
content: '为什么程序员总是分不清万圣节和圣诞节?因为Oct31==Dec25。\n任何我写的代码,超过6个月不去看它,当我再看时,都像是别人写的。',
diseaseDescribe: '', // 疾病描述
images: [
'/static/images/placeholder1.jpg',
'/static/images/placeholder2.jpg',
'/static/images/placeholder3.jpg',
'/static/images/placeholder4.jpg',
'/static/images/placeholder5.jpg',
'/static/images/placeholder6.jpg',
'/static/images/placeholder7.jpg',
'/static/images/placeholder8.jpg'
]
})
// 医生回答
const doctorReply = ref({
avatar: '/static/images/doctor-avatar.jpg',
name: 'los测试',
hospital: '隆福医院',
time: '28天前',
content: '徐徐,喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵喵',
image: '/static/images/reply-image.jpg'
})
// 返回上一页
function goBack() {
uni.navigateBack()
}
// 预览图片
function previewImage(current, index) {
uni.previewImage({
urls: questionInfo.value.images?questionInfo.value.images.split(',').map(path=> docUrl + path):[],
current: index
})
}
// 预览医生回复图片
function previewReplyImages(item, index){
if(!item || !item.imgs){
return
}
const urls = item.imgs.split(',').map(path=> docUrl + path)
uni.previewImage({
urls,
current: index
})
}
// 编辑问题
function editQuestion() {
// 编辑逻辑
navTo({
url:'/pages_app/myAnswer/myAnswer?answer_uuid='+answer_uuid.value+'&uuid='+uuid.value
})
}
</script>
<style lang="scss" scoped>
.consult-detail-page {
background-color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: 88rpx;
padding: 0 32rpx;
background-color: #ffffff;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
.nav-left {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 48rpx;
color: #8B2316;
font-weight: bold;
}
}
.nav-title {
font-size: 36rpx;
font-weight: 500;
color: #8B2316;
}
.nav-right {
width: 80rpx;
}
}
.bar{
width:100%;
height:20rpx;
background-color: #efefef;
}
.smallbar{
width:100%;
height:10rpx;
background-color: #efefef;
}
.content-scroll {
flex: 1;
position: fixed;
top: 135rpx;
width:auto;
box-sizing: border-box;
padding: 30rpx 0;
bottom: 313rpx;
}
.user-section {
background-color: #ffffff;
border-radius: 16rpx;
margin-bottom: 24rpx;
padding: 0 30rpx;
margin: 0 30rpx;
.user-info {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.user-name {
display: flex;
align-items: center;
.name {
font-size: 32rpx;
color: #8B2316;
font-weight: 500;
}
.gender-age {
font-size: 28rpx;
color: #666;
margin-left: 8rpx;
}
}
.detail-btn {
display: flex;
margin-left: 10rpx;
.detail-text {
color: #ffffff;
font-size: 24rpx;
margin-right: 8rpx;
}
.detail-icon {
color: #ffffff;
font-size: 24rpx;
}
}
}
}
.tag-date-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
margin: 0 30rpx 30rpx;
.disease-tag {
margin-bottom: 0;
.tag-text {
display: inline-block;
background-color: #ffffff;
color: #8B2316;
border: 2rpx solid #8B2316;
border-radius: 40rpx;
padding: 7rpx 22rpx;
font-size: 24rpx;
}
}
.date {
font-size: 28rpx;
color: #999;
}
}
.question-content {
background-color: #f0f0f0;
border-radius: 16rpx;
padding: 32rpx;
margin: 0 30rpx;
margin-bottom: 24rpx;
.content-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
}
.disease-describe {
background-color: #f8f9fa;
border-radius: 16rpx;
padding: 32rpx;
margin: 0 30rpx 24rpx;
border-left: 6rpx solid #8B2316;
.describe-title {
font-size: 30rpx;
color: #8B2316;
font-weight: 500;
margin-bottom: 16rpx;
}
.describe-text {
font-size: 28rpx;
color: #333;
line-height: 1.6;
}
}
.image-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8rpx;
margin: 0 30rpx 40rpx;
.grid-image {
width: 100%;
height: 160rpx;
border-radius: 8rpx;
}
}
.doctor-reply-section {
margin: 0 0rpx 40rpx;
.section-title {
font-size: 32rpx;
padding:24rpx 30rpx;
border-bottom:1rpx solid #efefef;
color: #8B2316;
font-weight: 500;
}
.doctor-card {
margin:20rpx 30rpx 0;
background-color: #ffffff;
border-radius: 16rpx;
.doctor-info {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.doctor-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 40rpx;
margin-right: 24rpx;
}
.doctor-details {
flex: 1;
.doctor-name {
font-size: 32rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.hospital-time-row {
display: flex;
align-items: center;
justify-content: space-between;
.doctor-hospital {
font-size: 28rpx;
color: #999;
}
.reply-time {
font-size: 28rpx;
color: #999;
}
}
}
}
.reply-content {
margin-bottom: 24rpx;
background-color: #f0f0f0;
border-radius: 16rpx;
font-size: 28rpx;
padding: 32rpx;
.reply-text {
font-size: 30rpx;
color: #333;
line-height: 1.6;
}
.reply-images{
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8rpx;
margin-top: 16rpx;
}
}
.reply-image {
width: 100%;
height:150rpx;
border-radius: 8rpx;
object-fit: cover;
}
}
}
.bottom-fixed {
position: fixed;
left: 0;
right: 0;
bottom: 0;
background-color: #ffffff;
padding: 24rpx 32rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.disclaimer {
background-color: #fff5f5;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
.disclaimer-title {
font-size: 28rpx;
color: #8B2316;
font-weight: 500;
}
.disclaimer-text {
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
}
.edit-btn {
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background-color: #00bcd4;
color: #ffffff;
font-size: 32rpx;
font-weight: 500;
border-radius: 16rpx;
}
</style>
+3 -9
View File
@@ -1,15 +1,9 @@
<template>
<view class="feedback-container">
<!-- 状态栏占位 -->
<view class="status-bar"></view>
<!-- 顶部导航栏 -->
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="back-arrow"></text>
</view>
<text class="nav-title">意见反馈</text>
</view>
<navBar title="意见反馈"></navBar>
<!-- 反馈输入区域 -->
<view class="feedback-input-container">
@@ -41,7 +35,7 @@
import { ref } from 'vue';
import api from '@/api/api';
const feedbackText = ref('');
import navBar from "@/components/navBar/navBar.vue"
// 返回上一页
const goBack = () => {
uni.navigateBack();
+2
View File
@@ -29,6 +29,7 @@
</view>
</view>
</view>
<empty v-if="bankCards.length === 0"></empty>
<!-- 底部导航指示器 -->
<view class="bottom-indicator"></view>
@@ -39,6 +40,7 @@
import { ref, onMounted } from 'vue';
import navTo from '@/utils/navTo';
import api from '@/api/api';
import empty from "@/components/empty/empty.vue"
const bankCards = ref([]);
// 银行卡数据
File diff suppressed because it is too large Load Diff
+273
View File
@@ -0,0 +1,273 @@
<template>
<view class="my-answer-page">
<navBar title="我的意见" />
<scroll-view scroll-y class="content-scroll">
<!-- 文本输入 -->
<view class="card">
<view class="card-title">我的意见 <text class="required">*</text></view>
<view class="textarea-wrap">
<textarea
v-model="form.note"
class="textarea"
:maxlength="300"
placeholder="请依据患者的个人信息、疾病资料及患者所咨询的问题详细解答患者的问题(信息仅提问患者及医生可见,最多输入300个字)"
placeholder-class="ph"
auto-height
/>
</view>
</view>
<!-- 图片上传 -->
<view class="card">
<view class="card-title">相关图片</view>
<view class="sub-tip">可以用部分科普或文献来协助回答问题最多6张</view>
<view class="img-grid">
<view
v-if="imgList.length>0"
v-for="(img, index) in imgList"
:key="index"
class="img-item"
@click="preview(index)"
>
<image :src="docUrl+img" mode="aspectFill" class="img" />
<view class="del" @click.stop="remove(index)">×</view>
</view>
<view v-if="imgList.length < maxImages" class="img-item add" @click="addImages">
<view class="plus"></view>
</view>
</view>
</view>
</scroll-view>
<!-- 底部提交 -->
<view class="bottom-fixed">
<view class="submit-btn" @click="submit">提交</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import navBar from '@/components/navBar/navBar.vue'
import api from '@/api/api'
import docUrl from '@/utils/docUrl'
import { onLoad,onShow } from '@dcloudio/uni-app'
const uuid=ref('');
const maxImages = 6;
const imgList = ref([]);
const form = ref({
note: '',
images: []
})
onLoad((options) => {
uuid.value = options.uuid || ''
})
const getInterrogation=()=>{
api.getInterrogation({
uuid:uuid.value
}).then(res=>{
console.log(res)
if(res.code === '200' && res.data) {
let user=uni.getStorageSync('userInfo');
let arr=res.data.AnswerList.filter(item=>{
return user.uuid == item.expert_uuid
})
form.value= arr[0];
imgList.value= form.value.imgs?form.value.imgs.split(','):[];
}
})
}
const updateInterrogationAnswer=()=>{
let imgobj={};
if(imgList.value.length>0){
let count=0;
imgList.value.forEach(item=>{
imgobj['img'+count]=docUrl+item;
})
}
api.updateInterrogationAnswer({
answer_uuid: answer_uuid.value,
note: form.value.note,
imgsBean: imgobj
}).then(res=>{
if(res.code == 200){
uni.showToast({title: '提交成功', icon: 'none'})
uni.navigateBack()
}
})
}
const addInterrogationAnswer=()=>{
api.addInterrogationAnswer({
note: form.value.note,
imgsBean: imgobj
}).then(res=>{
if(res.code == 200){
uni.showToast({title: '提交成功', icon: 'none'})
uni.navigateBack()
}
})
}
onShow(()=>{
getInterrogation()
})
function addImages(){
const remain = maxImages - form.value.images.length
if(remain <= 0){
uni.showToast({title: '最多6张', icon: 'none'})
return
}
uni.chooseImage({
count: remain,
sizeType: ['compressed'],
sourceType: ['album','camera'],
success: (res)=>{
const paths = (res.tempFilePaths || res.tempFiles?.map(f=>f.path) || [])
imgList.value = imgList.value.concat(paths).slice(0, maxImages)
}
})
}
function preview(index){
uni.previewImage({
urls:imgList.map(path=> docUrl + path),
current: index
})
}
function remove(index){
imgList.valuesplice(index, 1)
}
function submit(){
if(!form.value.note.trim()){
uni.showToast({title:'请输入意见', icon:'none'})
return
}
if(answer_uuid.value){
updateInterrogationAnswer()
}else{
addInterrogationAnswer()
}
}
</script>
<style lang="scss" scoped>
.my-answer-page{
background-color: #fff;
height: 100vh;
display: flex;
flex-direction: column;
}
.content-scroll{
flex: 1;
position: fixed;
top: 135rpx;
left: 0;
right: 0;
bottom: 160rpx;
padding: 24rpx 24rpx 0;
box-sizing: border-box;
}
.card{
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.card-title{
font-size: 32rpx;
color: #8B2316;
font-weight: 500;
margin-bottom: 16rpx;
}
.required{ color: #ff4d4f; }
.textarea-wrap{
position: relative;
background: #f7f7f7;
border-radius: 12rpx;
padding: 16rpx 88rpx 16rpx 16rpx;
}
.textarea{
min-height: 180rpx;
width: 100%;
font-size: 28rpx;
color: #333;
}
.ph{ color:#999; }
.voice-btn{
position: absolute;
right: 16rpx;
bottom: 16rpx;
width: 64rpx;
height: 64rpx;
border-radius: 32rpx;
background: #b90f0f;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 28rpx;
}
.sub-tip{
font-size: 26rpx;
color: #999;
margin-bottom: 16rpx;
}
.img-grid{
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.img-item{
position: relative;
width:150rpx;
height: 150rpx;
border-radius: 12rpx;
overflow: hidden;
background: #f0f0f0;
}
.img{ width: 100%; height: 100%; }
.add{ display:flex; align-items:center; justify-content:center; }
.plus{ font-size: 80rpx; color:#c0c0c0; line-height: 1; }
.del{
position: absolute;
top: 8rpx;
right: 8rpx;
width: 44rpx;
height: 44rpx;
border-radius: 22rpx;
background: rgba(0,0,0,.5);
color: #fff;
text-align: center;
line-height: 44rpx;
font-size: 28rpx;
}
.bottom-fixed{
position: fixed;
left: 0; right: 0; bottom: 0;
background: #fff;
padding: 24rpx;
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,.06);
}
.submit-btn{
width: 100%;
height: 88rpx;
line-height: 88rpx;
text-align: center;
background: #00bcd4;
color: #fff;
font-size: 32rpx;
border-radius: 16rpx;
}
</style>
+11 -13
View File
@@ -83,8 +83,9 @@
<script setup>
import { ref } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import api from "@/api/api";
import docUrl from '@/utils/docUrl';
import api from "@/api/api";
import docUrl from '@/utils/docUrl';
import navTo from '@/utils/navTo';
// 编辑模式状态
const isEditMode = ref(false);
@@ -124,9 +125,7 @@
const allApps = sourceList.map(item => ({
id: item.id,
name: item.name,
icon: docUrl + item.img, // 使用docUrl拼接图片路径
bgColor: bgColorMap[item.id] || '#999999', // 使用预设颜色
url: urlMap[item.id] || '', // 使用预设路由
icon: docUrl + item.img, // 使用docUrl拼接图片路径 // 使用预设颜色// 使用预设路由
selected: item.selected
}));
@@ -322,9 +321,7 @@
.app-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
border: 1rpx solid #e0e0e0;
border-left: none;
border-right: none;
}
.app-item {
@@ -333,8 +330,9 @@
flex-direction: column;
align-items: center;
padding: 20rpx 10rpx;
border-right: 1rpx solid #e0e0e0;
border-bottom: 1rpx solid #e0e0e0;
border: 1rpx solid #e0e0e0;
border-right:none;
transition: all 0.3s ease;
.iconbox {
position: absolute;
@@ -348,12 +346,12 @@
// 右边框处理
&:nth-child(3n) {
border-right: none;
}
// 下边框处理(最后一行)
&:nth-last-child(-n + 3) {
border-bottom: none;
&:nth-last-child(1){
border-right: 1rpx solid #e0e0e0;
}
}
+84 -31
View File
@@ -1,20 +1,7 @@
<template>
<view class="my-code-page">
<!-- 顶部导航栏 -->
<uni-nav-bar
left-icon="left"
title="我的二维码"
@cviewckLeft="goBack"
fixed
color="#8B2316"
height="140rpx"
:border="false"
backgroundColor="#eeeeee"
>
<template v-slot:right>
<uni-icons type="redo" color="#8B2316" size="22"></uni-icons>
</template>
</uni-nav-bar>
<navBar title="我的二维码" />
<!-- 内容 -->
<scroll-view scroll-y class="page-scroll">
@@ -30,10 +17,10 @@
<view class="rightCircle"></view>
<view class="halfCircle"></view>
<view class="avatar-wrapper">
<image class="avatar" :src="avatarImg" mode="aspectFill" />
<image class="avatar" :src="docUrl+userInfo.photo" mode="aspectFill" />
</view>
<view class="name-viewne">邹建东 主任医师</view>
<view class="org-viewne">北京肝胆相照公益基金会</view>
<view class="name-viewne">{{ userInfo.realName }} {{ userInfo.positionName }}</view>
<view class="org-viewne">{{ userInfo.hospitalName }}</view>
<view class="dash-viewne"></view>
<view class="slogan">
<text class="h1">不方便到医院就诊</text>
@@ -44,7 +31,7 @@
<up-image :src="viewnkImg" width="430rpx" height="131rpx" ></up-image>
</view>
<image class="qr-img" :src="qrImg" mode="aspectFit" />
<image class="qr-img" :src="docUrl+userInfo.qrcode" mode="aspectFit" />
</view>
</view>
</view>
@@ -72,25 +59,91 @@
<!-- 底部保存按钮 -->
<view class="save-bar">
<button class="save-btn" @cviewck="onSave">保存二维码到手机</button>
<button class="save-btn" @click="onSave">保存二维码到手机</button>
</view>
</view>
</template>
<script setup>
import navBar from '@/components/navBar/navBar.vue'
import { onShow } from "@dcloudio/uni-app";
import { ref } from 'vue';
const avatarImg = '/static/xxtx.png';
const qrImg = '/static/sfewm.png';
import docUrl from '@/utils/docUrl'
import bgImg from "@/static/background.jpg"
import viewnkImg from "@/static/arr.png"
const userInfo = ref({})
const goBack = () => {
uni.navigateBack();
};
onShow(()=>{
userInfo.value = uni.getStorageSync('userInfo')
})
const onSave = () => {
uni.showToast({ title: '已保存(示例)', icon: 'none' });
// 检查是否有二维码图片
if (!userInfo.value.qrcode) {
uni.showToast({ title: '二维码不存在', icon: 'none' });
return;
}
// 显示加载提示
uni.showLoading({ title: '保存中...' });
// 下载二维码图片
uni.downloadFile({
url: docUrl + userInfo.value.qrcode,
success: (res) => {
if (res.statusCode === 200) {
// 保存到相册
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.hideLoading();
uni.showToast({
title: '保存成功',
icon: 'success',
duration: 2000
});
},
fail: (err) => {
uni.hideLoading();
console.error('保存失败:', err);
// 根据错误类型给出不同提示
if (err.errMsg.includes('auth deny') || err.errMsg.includes('authorize')) {
uni.showModal({
title: '权限提示',
content: '需要相册权限才能保存图片,请在设置中开启权限',
showCancel: false,
confirmText: '知道了'
});
} else {
uni.showToast({
title: '保存失败,请重试',
icon: 'none',
duration: 2000
});
}
}
});
} else {
uni.hideLoading();
uni.showToast({
title: '下载失败,请检查网络',
icon: 'none',
duration: 2000
});
}
},
fail: (err) => {
uni.hideLoading();
console.error('下载失败:', err);
uni.showToast({
title: '下载失败,请检查网络',
icon: 'none',
duration: 2000
});
}
});
};
</script>
@@ -182,7 +235,7 @@ const onSave = () => {
width:100%;
color: #ffffff;
text-aviewgn: center;
text-align: center;
.banner-title-small {
font-size: 26rpx;
@@ -279,10 +332,10 @@ const onSave = () => {
font-weight:bold;
letter-spacing: 8rpx;
flex-direction: column;
text-aviewgn: center;
text-align: center;
font-size: 40rpx;
color: #1e88e5;
viewne-height: 1.6;
line-height: 1.6;
text{
text-align: center;
}
@@ -293,7 +346,7 @@ const onSave = () => {
}
.contact-qr {
display: flex;
aviewgn-items: center;
align-items: center;
justify-content: space-between;
gap: 20rpx;
margin-top: 30rpx;
@@ -305,12 +358,12 @@ const onSave = () => {
color: #ffffff;
display: flex;
aviewgn-items: center;
align-items: center;
justify-content: space-between;
.contact-text {
white-space: pre-viewne;
font-size: 26rpx;
viewne-height: 1.5;
line-height: 1.5;
}
.arrow {
font-size: 36rpx;
+269
View File
@@ -0,0 +1,269 @@
<template>
<view class="patient-info-container">
<!-- 导航栏 -->
<navBar title="患者信息" />
<!-- 标签页 -->
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: activeTab === 'basic' }"
@click="switchTab('basic')"
>
基本资料
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'history' }"
@click="switchTab('history')"
>
病史信息
</view>
</view>
<!-- 内容区域 -->
<scroll-view class="content-area" scroll-y>
<!-- 基本资料内容 -->
<view v-if="activeTab === 'basic'" class="basic-info">
<view class="info-item">
<text class="info-label">姓名</text>
<text class="info-value">{{ patientInfo.name || '提**' }}</text>
</view>
<view class="info-item">
<text class="info-label">性别</text>
<text class="info-value">{{ patientInfo.gender || '男' }}</text>
</view>
<view class="info-item">
<text class="info-label">年龄</text>
<text class="info-value">{{ patientInfo.age || '15' }}</text>
</view>
<view class="info-item">
<text class="info-label">地址</text>
<text class="info-value">{{ patientInfo.address || '北京市东城区' }}</text>
</view>
<view class="info-item">
<text class="info-label">肝硬化或肝癌家族史</text>
<text class="info-value">{{ patientInfo.familyHistory || '无' }}</text>
</view>
</view>
<!-- 病史信息内容 -->
<view v-if="activeTab === 'history'" class="history-info">
<view class="info-item">
<text class="info-label">既往病史</text>
<text class="info-value">暂无</text>
</view>
<view class="info-item">
<text class="info-label">过敏史</text>
<text class="info-value">暂无</text>
</view>
<view class="info-item">
<text class="info-label">手术史</text>
<text class="info-value">暂无</text>
</view>
<view class="info-item">
<text class="info-label">用药史</text>
<text class="info-value">暂无</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted, computed } from 'vue'
import navBar from '@/components/navBar/navBar.vue'
import { onLoad } from '@dcloudio/uni-app'
import api from '@/api/api.js'
// 响应式数据
const statusBarHeight = ref(0)
const activeTab = ref('basic');
const step1_uuid = ref('');
const patientInfo = reactive({
name: '提**',
gender: '男',
age: '15',
address: '北京市东城区',
familyHistory: '未知'
})
onLoad((options) => {
step1_uuid.value = options.step1_uuid || ''
interrogationPatientInfo()
})
const interrogationPatientInfo=()=>{
api.interrogationPatientInfo({
step1_uuid: step1_uuid.value
}).then(res=>{
if(res.code == 200 && res.data){
const d = res.data
patientInfo.name = d.name || '提**'
patientInfo.gender = d.sex === 1 ? '男' : '女'
patientInfo.age = d.birthday ? calculateAge(d.birthday) : '未知'
patientInfo.address = d.address || ''
// 家族史字段接口未提供,设为未知
patientInfo.familyHistory = '未知'
}
})
}
function calculateAge(birthday){
const birth = new Date(birthday)
const today = new Date()
let age = today.getFullYear() - birth.getFullYear()
const m = today.getMonth() - birth.getMonth()
if(m < 0 || (m === 0 && today.getDate() < birth.getDate())){
age--
}
return String(age)
}
// 计算属性
const statusBarStyle = computed(() => ({
height: statusBarHeight.value + 'px'
}))
// 方法
const goBack = () => {
uni.navigateBack()
}
const switchTab = (tab) => {
activeTab.value = tab
}
// 生命周期
onMounted(() => {
// 获取状态栏高度
const systemInfo = uni.getSystemInfoSync()
statusBarHeight.value = systemInfo.statusBarHeight || 0
})
</script>
<style lang="scss" scoped>
// 变量定义
$primary-color: #ff0000;
$text-color: #333333;
$text-color-light: #666666;
$border-color: #f0f0f0;
$border-color-light: #e0e0e0;
$background-color: #ffffff;
$nav-height: 88rpx;
$tab-height: 88rpx;
$info-item-height: 100rpx;
$padding-horizontal: 30rpx;
$padding-small: 10rpx;
.patient-info-container {
background-color: $background-color;
min-height: 100vh;
}
.status-bar {
background-color: $background-color;
}
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
height: $nav-height;
padding: 0 $padding-horizontal;
background-color: $background-color;
border-bottom: 2rpx solid $border-color;
.nav-left {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.back-arrow {
font-size: 48rpx;
color: $primary-color;
font-weight: bold;
}
}
.nav-title {
flex: 1;
text-align: center;
font-size: 36rpx;
font-weight: 500;
color: $primary-color;
}
.nav-right {
width: 60rpx;
}
}
.tab-bar {
display: flex;
background-color: $background-color;
border-bottom: 2rpx solid $border-color-light;
.tab-item {
flex: 1;
height: $tab-height;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: $text-color;
position: relative;
&.active {
color: $primary-color;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: $primary-color;
}
}
}
}
.content-area {
flex: 1;
background-color: $background-color;
}
.basic-info,
.history-info {
padding: 0 $padding-horizontal;
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
height: $info-item-height;
border-bottom: 2rpx solid $border-color;
padding: 0 $padding-small;
&:last-child {
border-bottom: none;
}
.info-label {
font-size: 32rpx;
color: $text-color;
flex: 1;
}
.info-value {
font-size: 32rpx;
color: $text-color-light;
text-align: right;
flex: 1;
}
}
}
</style>
@@ -0,0 +1,112 @@
<template>
<div
class="p2p-msg-receipt-wrapper"
v-if="
conversationType ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
p2pMsgReceiptVisible
"
>
<div v-if="p2pMsgRotateDeg == 360" class="icon-read-wrapper">
<Icon type="icon-read" :size="16"></Icon>
</div>
<div v-else class="sector">
<span
class="cover-1"
:style="`transform: rotate(${p2pMsgRotateDeg}deg)`"
></span>
<span
:class="p2pMsgRotateDeg >= 180 ? 'cover-2 cover-3' : 'cover-2'"
></span>
</div>
</div>
</template>
<script lang="ts" setup>
/** 会话列表已读未读组件 */
import { computed } from 'vue'
import Icon from '@/components/Icon.vue'
import { V2NIMConst } from '@/utils/im/nim'
import {
V2NIMConversationForUI,
V2NIMLocalConversationForUI,
} from '@xkit-yx/im-store-v2/dist/types/types'
import { V2NIMConversationType } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMConversationService'
const props = withDefaults(
defineProps<{
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
}>(),
{}
)
/** 是否需要显示 p2p 消息、p2p会话列表消息已读未读,默认 false*/
const p2pMsgReceiptVisible = uni.$UIKitStore.localOptions.p2pMsgReceiptVisible
/** 会话类型 */
const conversationType =
uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationType(
props.conversation.conversationId
) as unknown as V2NIMConversationType
const p2pMsgRotateDeg = computed(() => {
return (props?.conversation?.msgReceiptTime || 0) >=
(props?.conversation?.lastMessage?.messageRefer?.createTime || 0)
? 360
: 0
})
</script>
<style scoped lang="scss">
.p2p-msg-receipt-wrapper {
width: 22px;
height: 22px;
overflow: hidden;
line-height: 18px;
vertical-align: bottom;
display: flex;
align-items: center;
justify-content: center;
}
.icon-read-wrapper {
margin: 0px 3px 0px 0;
width: 18px;
height: 18px;
overflow: hidden;
display: flex;
align-items: center;
}
.sector {
display: inline-block;
position: relative;
overflow: hidden;
border: 2px solid #4c84ff;
width: 12px;
height: 12px;
background-color: #ffffff;
border-radius: 50%;
margin: 0px 3px 0 0;
.cover-1,
.cover-2 {
position: absolute;
top: 0;
width: 50%;
height: 100%;
background-color: #ffffff;
}
.cover-1 {
background-color: #4c84ff;
transform-origin: right;
}
.cover-3 {
right: 0;
background-color: #4c84ff;
}
}
</style>
@@ -0,0 +1,266 @@
<template>
<div>
<div
v-if="
props.lastMessage.lastMessageState ===
V2NIMConst.V2NIMLastMessageState.V2NIM_MESSAGE_STATUS_REVOKE
"
>
{{ t('recall') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION
"
>
{{ t('conversationNotificationText') }}
</div>
<div
v-else-if="
props.lastMessage.sendingState ===
V2NIMConst.V2NIMMessageSendingState.V2NIM_MESSAGE_SENDING_STATE_FAILED
"
>
{{ t('conversationSendFailText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_FILE
"
>
{{ translateMsg('fileMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_IMAGE
"
>
{{ translateMsg('imgMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CUSTOM
"
>
{{ props.lastMessage.text || translateMsg('customMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_AUDIO
"
>
{{ translateMsg('audioMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL
"
>
{{ translateMsg('callMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_LOCATION
"
>
{{ translateMsg('geoMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_ROBOT
"
>
{{ translateMsg('robotMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TIPS
"
>
{{ translateMsg('tipMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_VIDEO
"
>
{{ translateMsg('videoMsgText') }}
</div>
<div
v-else-if="
props.lastMessage.messageType ===
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_TEXT
"
class="msg-conversation-text-wrap"
>
<template v-for="item in textArr" :key="item.key">
<template v-if="item.type === 'text'">
<span class="msg-conversation-text">{{ item.value }}</span>
</template>
<template v-else-if="item.type === 'emoji'">
<span
:class="
isWxApp
? 'msg-conversation-text-emoji-wx'
: 'msg-conversation-text-emoji'
"
>
<Icon :type="EMOJI_ICON_MAP_CONFIG[item.value]" :size="16" />
</span>
</template>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
/** 会话列表Item 外漏消息组件 */
import { computed } from 'vue'
import Icon from '@/components/Icon.vue'
import { V2NIMConst } from '@/utils/im/nim'
import { t } from '@/utils/im/i18n'
import { V2NIMLastMessage } from 'nim-web-sdk-ng/dist/esm/nim/src/V2NIMConversationService'
import { EMOJI_ICON_MAP_CONFIG, emojiRegExp } from '@/utils/im/emoji'
import { isWxApp } from '@/utils/im/index'
const props = withDefaults(
defineProps<{
lastMessage: V2NIMLastMessage
}>(),
{}
)
/** 筛选出文本和表情 */
const parseTextWithEmoji = (text: string) => {
if (!text) return []
const matches: {
type: 'emoji' | 'text'
value: string
index: number
}[] = []
let match
const regexEmoji = emojiRegExp
while ((match = regexEmoji.exec(text)) !== null) {
matches.push({
type: 'emoji',
value: match[0],
index: match.index,
})
const fillText = ' '.repeat(match[0].length)
text = text.replace(match[0], fillText)
}
text = text.replace(regexEmoji, ' ')
if (text) {
text
.split(' ')
.filter((item) => item.trim())
.map((item) => {
const index = text?.indexOf(item)
matches.push({
type: 'text',
value: item,
index,
})
const fillText = ' '.repeat(item.length)
text = text.replace(item, fillText)
})
}
return matches
.sort((a, b) => a.index - b.index)
.map((item, index) => {
return {
...item,
key: index + item.type,
}
})
}
/** 解析的消息数组 */
const textArr = computed(() => {
return parseTextWithEmoji(props.lastMessage.text as string)
})
/** 消息映射 */
const translateMsg = (key: string): string => {
const text =
{
textMsgText: t('textMsgText'),
customMsgText: t('customMsgText'),
audioMsgText: t('audioMsgText'),
videoMsgText: t('videoMsgText'),
fileMsgText: t('fileMsgText'),
callMsgText: t('callMsgText'),
geoMsgText: t('geoMsgText'),
imgMsgText: t('imgMsgText'),
notiMsgText: t('notiMsgText'),
robotMsgText: t('robotMsgText'),
tipMsgText: t('tipMsgText'),
unknowMsgText: t('unknowMsgText'),
}[key] || ''
return `[${text}]`
}
</script>
<style scoped lang="scss">
.wrapper {
flex: 1;
font-size: 13px;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.msg-conversation-text {
font-size: 13px !important;
height: 22px;
line-height: 22px;
width: 100%;
display: inline;
}
.msg-conversation-text-wrap {
width: 80%;
line-height: 22px;
height: 22px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 14px;
overflow: hidden;
}
.msg-conversation-text-emoji {
display: inline-flex;
width: 18px;
height: 18px;
box-sizing: border-box;
align-items: center;
justify-content: center;
}
.msg-conversation-text-emoji-wx {
display: inline-flex;
width: 18px;
height: 18px;
box-sizing: border-box;
align-items: center;
justify-content: center;
position: relative;
bottom: 4px;
}
</style>
@@ -0,0 +1,496 @@
<template>
<view
:class="[
'conversation-item-container',
{
'show-action-list': showMoreActions,
'stick-on-top': conversation.stickTop,
},
]"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@click="handleConversationItemClick()"
>
<view class="conversation-item-content">
<view class="conversation-item-left">
<!-- 会话Item未读数 -->
<view class="unread" v-if="unread">
<view class="dot" v-if="isMute"></view>
<view class="badge" v-else>{{ unread }}</view>
</view>
<!-- 会话头像 -->
<Avatar :account="to" :avatar="teamAvatar" />
<!-- 用户在线离线状态 -->
<view
class="login-state-icon"
v-if="
loginStateVisible &&
!isAiUser &&
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
status
"
></view>
<view
class="unlogin-state-icon"
v-if="
loginStateVisible &&
!isAiUser &&
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P &&
!status
"
></view>
</view>
<view class="conversation-item-right">
<!-- 会话名称 -->
<view class="conversation-item-top">
<Appellation
class="conversation-item-title"
v-if="
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
"
:account="to"
/>
<span v-else class="conversation-item-title">
{{ sessionName }}
</span>
<span class="conversation-item-time">{{ date }}</span>
</view>
<view class="conversation-item-desc">
<!-- 是否有人@ -->
<span v-if="beMentioned" class="beMentioned">
{{ '[' + t('someoneText') + '@' + t('meText') + ']' }}
</span>
<!-- 会话最后一条消息是否已读 -->
<!-- <ConversationItemIsRead
v-if="showConversationUnread"
:conversation="props.conversation"
></ConversationItemIsRead> -->
<!-- 会话最后一条消息外露 -->
<span
v-if="props.conversation.lastMessage"
class="conversation-item-desc-content"
>
<LastMsgContent :lastMessage="props.conversation.lastMessage" />
</span>
<!-- 消息免打扰 -->
<span class="conversation-item-desc-ait">
<Icon
v-if="isMute"
iconClassName="conversation-item-desc-state"
type="icon-xiaoximiandarao"
color="#ccc"
/>
</span>
</view>
</view>
</view>
<!-- 消息右键操作列表 -->
<view class="right-action-list">
<view
v-for="action in moreActions"
:key="action.type"
:class="['right-action-item', action.class]"
@click="() => handleClick(action.type)"
>
{{ action.name }}
</view>
</view>
</view>
</template>
<script lang="ts" setup>
/** 会话列表Item组件 */
import Avatar from '@/components/Avatar.vue'
import Appellation from '@/components/Appellation.vue'
import Icon from '@/components/Icon.vue'
import { computed, onUnmounted, withDefaults } from 'vue'
import dayjs from 'dayjs'
import { t } from '@/utils/im/i18n'
import { V2NIMConst } from '@/utils/im/nim'
import {
V2NIMConversationForUI,
V2NIMLocalConversationForUI,
} from '@xkit-yx/im-store-v2/dist/types/types'
import ConversationItemIsRead from './conversation-item-isRead.vue'
import LastMsgContent from './conversation-item-last-msg-content.vue'
import { ref } from 'vue'
import { autorun } from 'mobx'
const props = withDefaults(
defineProps<{
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
showMoreActions?: boolean
}>(),
{ showMoreActions: false }
)
const emit = defineEmits(['click', 'delete', 'stickyToTop', 'leftSlide'])
/** 右滑操作列表 */
const moreActions = computed(() => {
return [
{
name: props.conversation.stickTop
? t('deleteStickTopText')
: t('addStickTopText'),
class: 'action-top',
type: 'action-top',
},
{
name: t('deleteSessionText'),
class: 'action-delete',
type: 'action-delete',
},
]
})
/** 对话方 */
const to = computed(() => {
const res = uni.$UIKitNIM.V2NIMConversationIdUtil.parseConversationTargetId(
props.conversation.conversationId
)
return res
})
/** 全局配置的是否需要展示在线离线状态 */
const loginStateVisible = uni.$UIKitStore.localOptions.loginStateVisible
/** 当前会话方在线离线状态 */
const status = ref<boolean>(false)
/** 右滑操作点击 */
const handleClick = (type: string) => {
if (type === 'action-top') {
emit('stickyToTop', props.conversation)
} else {
emit('delete', props.conversation)
}
}
/** 群头像 */
const teamAvatar = computed(() => {
if (
props.conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM
) {
const { avatar } = props.conversation
return avatar
}
})
/** 会话昵称 */
const sessionName = computed(() => {
if (props.conversation.name) {
return props.conversation.name
}
return props.conversation.conversationId
})
/** 是否是机器人 */
const isAiUser = ref(false)
/** 时间 */
const date = computed(() => {
const time =
props.conversation.lastMessage?.messageRefer.createTime ||
props.conversation.updateTime
// 如果最后一条消息时间戳不存在,则会话列表不显示
if (!time) {
return ''
}
const _d = dayjs(time)
const isCurrentDay = _d.isSame(dayjs(), 'day')
const isCurrentYear = _d.isSame(dayjs(), 'year')
return _d.format(
isCurrentDay ? 'HH:mm' : isCurrentYear ? 'MM-DD HH:mm' : 'YYYY-MM-DD HH:mm'
)
})
const max = 99
/** 未读数 */
const unread = computed(() => {
return props.conversation.unreadCount > 0
? props.conversation.unreadCount > max
? `${max}+`
: props.conversation.unreadCount + ''
: ''
})
/** 是否免打扰 */
const isMute = computed(() => {
return !!props.conversation.mute
})
/** 是否被@ */
const beMentioned = computed(() => {
return !!props.conversation.aitMsgs?.length
})
/** 是否展示 未读数 */
const showConversationUnread = computed(() => {
const myUserAccountId = uni.$UIKitNIM.V2NIMLoginService.getLoginUser()
if (
props.conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
return (
props?.conversation?.lastMessage?.messageRefer.senderId ===
myUserAccountId &&
props?.conversation?.lastMessage?.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_CALL &&
props?.conversation?.lastMessage?.messageType !==
V2NIMConst.V2NIMMessageType.V2NIM_MESSAGE_TYPE_NOTIFICATION &&
props?.conversation?.lastMessage?.sendingState ===
V2NIMConst.V2NIMMessageSendingState
.V2NIM_MESSAGE_SENDING_STATE_SUCCEEDED &&
props?.conversation?.lastMessage?.lastMessageState !==
V2NIMConst.V2NIMLastMessageState.V2NIM_MESSAGE_STATUS_REVOKE
)
} else {
return false
}
})
// 左滑显示 action 动画
let startX = 0,
startY = 0
// 开始左滑
function handleTouchStart(event: TouchEvent) {
startX = event.changedTouches[0].pageX
startY = event.changedTouches[0].pageY
}
// 左滑
function handleTouchMove(event: TouchEvent) {
const moveEndX = event.changedTouches[0].pageX
const moveEndY = event.changedTouches[0].pageY
const X = moveEndX - startX + 20
const Y = moveEndY - startY
if (Math.abs(X) > Math.abs(Y) && X > 0) {
emit('leftSlide', null)
} else if (Math.abs(X) > Math.abs(Y) && X < 0) {
emit('leftSlide', props.conversation)
}
}
/** 会话列表点击事件 */
function handleConversationItemClick() {
if (props.showMoreActions) {
emit('leftSlide', null)
return
}
emit('click', props.conversation)
}
/** 监听是否是Ai 数字人 */
const isAiUserWatch = autorun(() => {
isAiUser.value = uni.$UIKitStore.aiUserStore.isAIUser(to.value)
})
/** 监听会话方在线离线状态 */
const statusWatch = autorun(() => {
if (
props.conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
) {
const stateMap = uni.$UIKitStore?.subscriptionStore.stateMap
if (
stateMap.get(to.value) &&
uni.$UIKitStore.localOptions.loginStateVisible
) {
status.value =
stateMap.get(to.value)?.statusType ===
V2NIMConst.V2NIMUserStatusType.V2NIM_USER_STATUS_TYPE_LOGIN
} else {
status.value = false
}
}
})
onUnmounted(() => {
isAiUserWatch()
statusWatch()
})
</script>
<style lang="scss" scoped>
$cellHeight: 72px;
.conversation-item-container {
position: relative;
transition: transform 0.3s;
&.show-action-list {
transform: translateX(-200px);
}
&.stick-on-top {
background: #f3f5f7;
}
.beMentioned {
color: #ff4d4f;
}
.content {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.right-action-list {
position: absolute;
top: 0;
right: -200px;
bottom: 0;
width: 200px;
white-space: nowrap;
.right-action-item {
width: 100px;
display: inline-block;
color: #fff;
text-align: center;
height: $cellHeight;
line-height: $cellHeight;
}
.action-top {
background: #337eff;
}
.action-delete {
background: #a8abb6;
}
}
.conversation-item-content {
display: flex;
align-items: center;
padding: 10px 20px;
height: $cellHeight;
box-sizing: border-box;
}
.conversation-item-left {
position: relative;
.conversation-item-badge {
position: absolute;
top: 0px;
right: 0px;
z-index: 10;
}
}
.conversation-item-right {
flex: 1;
width: 0;
margin-left: 10px;
}
.conversation-item-top {
display: flex;
justify-content: space-between;
align-items: center;
.conversation-item-title {
overflow: hidden; //超出的文本隐藏
text-overflow: ellipsis; //溢出用省略号显示
white-space: nowrap; //溢出不换行
}
.conversation-item-time {
font-size: 12px;
color: #cccccc;
text-align: right;
width: 90px;
flex-shrink: 0;
}
}
.conversation-item-desc {
font-size: 13px;
color: #999999;
display: flex;
align-items: center;
justify-content: space-between;
height: 22px;
.conversation-item-desc-content {
overflow: hidden; //超出的文本隐藏
text-overflow: ellipsis; //溢出用省略号显示
white-space: nowrap; //溢出不换行
flex: 1;
}
.conversation-item-desc-state {
margin-left: 10px;
}
}
.dot {
background-color: #ff4d4f;
color: #fff;
width: 10px;
height: 10px;
border-radius: 5px;
box-sizing: border-box;
z-index: 99;
}
.badge {
background-color: #ff4d4f;
color: #fff;
font-size: 12px;
min-width: 20px;
height: 20px;
line-height: 19px;
border-radius: 10px;
padding: 0 5px;
box-sizing: border-box;
text-align: center;
z-index: 99;
position: relative;
}
.unread {
position: absolute;
right: -4px;
top: -2px;
z-index: 99;
}
.conversation-item-desc-ait {
display: inline-block;
}
.login-state-icon {
width: 8px;
height: 8px;
box-sizing: content-box;
background-color: #84ed85;
border: 2px solid #fff;
position: absolute;
right: -2px;
bottom: -2px;
border-radius: 50%;
}
.unlogin-state-icon {
width: 8px;
height: 8px;
box-sizing: content-box;
background-color: #d4d9da;
border: 2px solid #fff;
position: absolute;
right: -2px;
bottom: -2px;
border-radius: 50%;
}
</style>
@@ -0,0 +1,570 @@
<template>
<div class="conversation-wrapper">
<div
v-if="addDropdownVisible"
class="dropdown-mark"
@touchstart="hideAddDropdown"
></div>
<div class="navigation-bar">
<div :class="isWxApp ? 'button-box-mp' : 'button-box'">
<!-- #ifdef MP -->
<image
src="https://yx-web-nosdn.netease.im/common/9ae07d276ba2833b678a4077960e2d1e/Group 1899.png"
class="button-icon"
@tap="showAddDropdown"
/>
<!-- #endif -->
<!-- #ifndef MP -->
<div class="button-icon-add" @tap="showAddDropdown">
<Icon type="icon-More" />
</div>
<!-- #endif -->
<div v-if="addDropdownVisible" class="dropdown-container">
<div class="add-menu-list">
<div class="add-menu-item" @tap="onDropdownClick('addFriend')">
<Icon type="icon-tianjiahaoyou" :style="{ marginRight: '5px' }" />
{{ t('addFriendText') }}
</div>
<div class="add-menu-item" @tap="onDropdownClick('createGroup')">
<Icon
type="icon-chuangjianqunzu"
:style="{ marginRight: '5px' }"
/>
{{ t('createTeamText') }}
</div>
<div
class="add-menu-item"
@tap="onDropdownClick('createDiscussion')"
>
<Icon
type="icon-chuangjianqunzu"
:style="{ marginRight: '5px' }"
/>
{{ t('createDiscussionText') }}
</div>
</div>
</div>
</div>
</div>
<div class="block"></div>
<NetworkAlert />
<div v-if="!conversationList || conversationList.length === 0">
<div class="conversation-search" @tap="goToSearchPage">
<div class="search-input-wrapper">
<div class="search-icon-wrapper">
<Icon
iconClassName="search-icon"
:size="16"
color="#A6ADB6"
type="icon-sousuo"
></Icon>
</div>
<div class="search-input">{{ t('searchText') }}</div>
</div>
</div>
<!-- 页面初始化的过程中sessionList编译到小程序和h5出现sessionList为undefined的情况即使给了默认值为空数组故在此处进行判断 -->
<Empty
v-if="!conversationList || conversationList.length === 0"
:text="t('conversationEmptyText')"
/>
</div>
<div v-else class="conversation-list-wrapper">
<div class="security-tip">
<div>
{{ t('securityTipText') }}
</div>
</div>
<div class="conversation-search" @click="goToSearchPage">
<div class="search-input-wrapper">
<div class="search-icon-wrapper">
<Icon
iconClassName="search-icon"
:size="16"
color="#A6ADB6"
type="icon-sousuo"
></Icon>
</div>
<div class="search-input">{{ t('searchText') }}</div>
</div>
</div>
<!-- 此处的key如果用conversationId会在ios上渲染存在问题会出现会话列表显示undefined -->
<div
v-for="conversation in conversationList"
:key="conversation.renderKey"
>
<ConversationItem
:key="conversation.renderKey"
:showMoreActions="
currentMoveSessionId === conversation.conversationId
"
:conversation="conversation"
@delete="handleSessionItemDeleteClick"
@stickyToTop="handleSessionItemStickTopChange"
@click="handleSessionItemClick"
@leftSlide="handleSessionItemLeftSlide"
/>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
/** 会话列表主界面 */
import { onUnmounted, ref, watch } from 'vue'
import { autorun } from 'mobx'
import { onShow, onHide } from '@dcloudio/uni-app'
import Icon from '@/components/Icon.vue'
import NetworkAlert from '@/components/NetworkAlert.vue'
import Empty from '@/components/Empty.vue'
import ConversationItem from './conversation-item.vue'
import { setContactTabUnread, setTabUnread } from '@/utils/im/msg'
import { t } from '@/utils/im/i18n'
import { customNavigateTo } from '@/utils/im/customNavigate'
import { V2NIMConst } from '@/utils/im/nim'
import { isWxApp } from '@/utils/im/index'
import { trackInit } from '@/utils/im/reporter'
import {
V2NIMConversationForUI,
V2NIMLocalConversationForUI,
} from '@xkit-yx/im-store-v2/dist/types/types'
/**会话列表 */
const conversationList = ref<
(
| (V2NIMConversationForUI & { renderKey: string })
| (V2NIMLocalConversationForUI & { renderKey: string })
)[]
>([])
/** 右上角更多 */
const addDropdownVisible = ref(false)
/** 当前左滑会话ID */
const currentMoveSessionId = ref('')
/**是否是云端会话 */
const enableV2CloudConversation =
uni.$UIKitStore?.sdkOptions?.enableV2CloudConversation
/** 会话左滑 */
const handleSessionItemLeftSlide = (
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI | null
) => {
// 微信小程序点击也会触发左滑事件,但此时 conversation 为 null
if (conversation) {
currentMoveSessionId.value = conversation.conversationId
} else {
currentMoveSessionId.value = ''
}
}
let flag = false
// 点击会话
const handleSessionItemClick = async (
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
) => {
console.log(conversation)
if (flag) return
currentMoveSessionId.value = ''
try {
flag = true
// 处理@消息相关
if (
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_TEAM ||
conversation.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_SUPER_TEAM
) {
if (enableV2CloudConversation) {
await uni.$UIKitStore.conversationStore?.markConversationReadActive(
conversation.conversationId
)
} else {
await uni.$UIKitStore.localConversationStore?.markConversationReadActive(
conversation.conversationId
)
}
}
await uni.$UIKitStore.uiStore.selectConversation(
conversation.conversationId
)
customNavigateTo({
url: '/pages_chat/chat/index',
})
} catch {
uni.showToast({
title: t('selectSessionFailText'),
icon: 'error',
})
} finally {
flag = false
}
}
// 删除会话
const handleSessionItemDeleteClick = async (
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
) => {
try {
if (enableV2CloudConversation) {
await uni.$UIKitStore.conversationStore?.deleteConversationActive(
conversation.conversationId
)
} else {
await uni.$UIKitStore.localConversationStore?.deleteConversationActive(
conversation.conversationId
)
}
currentMoveSessionId.value = ''
} catch {
uni.showToast({
title: t('deleteSessionFailText'),
icon: 'error',
})
}
}
// 置顶会话
const handleSessionItemStickTopChange = async (
conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI
) => {
if (conversation.stickTop) {
try {
if (enableV2CloudConversation) {
await uni.$UIKitStore?.conversationStore?.stickTopConversationActive(
conversation.conversationId,
false
)
} else {
await uni.$UIKitStore?.localConversationStore?.stickTopConversationActive(
conversation.conversationId,
false
)
}
} catch {
uni.showToast({
title: t('deleteStickTopFailText'),
icon: 'error',
})
}
} else {
try {
if (enableV2CloudConversation) {
await uni.$UIKitStore?.conversationStore?.stickTopConversationActive(
conversation.conversationId,
true
)
} else {
await uni.$UIKitStore?.localConversationStore?.stickTopConversationActive(
conversation.conversationId,
true
)
}
} catch {
uni.showToast({
title: t('addStickTopFailText'),
icon: 'error',
})
}
}
}
/** 显示添加好友、群聊 Dropdown */
const showAddDropdown = () => {
addDropdownVisible.value = true
}
/** 隐藏添加好友、群聊 Dropdown */
const hideAddDropdown = () => {
addDropdownVisible.value = false
}
/** 点击Dropdown */
const onDropdownClick = (
urlType: 'addFriend' | 'createGroup' | 'createDiscussion'
) => {
const urlMap = {
// 添加好友
addFriend: '/pages/User/friend/add-friend',
// 创建群聊
createGroup: '/pages/Team/team-create/index',
// 创建讨论组和创建群聊复用一个页面,仅在创建群接口时,群扩展字段添加im_ui_kit_group参数区分,讨论组本质也是群,只是少了群的一些能力,旨在于快速创建讨论
createDiscussion: `/pages/Team/team-create/index?createDiscussion=${true}`,
}
addDropdownVisible.value = false
customNavigateTo({
url: urlMap[urlType],
})
}
/** 跳转至搜索页面 */
const goToSearchPage = () => {
customNavigateTo({
url: '/pages/Conversation/conversation-search/index',
})
}
/** 订阅当前会话方在线离线状态 */
const subscribeUserStatus = (
conversations: (V2NIMConversationForUI | V2NIMLocalConversationForUI)[]
) => {
const loginStateVisible = uni.$UIKitStore.localOptions.loginStateVisible
if (loginStateVisible) {
// 订阅会话列表中 单聊的在线离线状态
const accounts = conversations
.filter(
(item) =>
item.type ===
V2NIMConst.V2NIMConversationType.V2NIM_CONVERSATION_TYPE_P2P
)
.map((item) => {
return uni.$UIKitNIM?.V2NIMConversationIdUtil.parseConversationTargetId(
item.conversationId
)
})
// 将 accounts 拆分成多个长度不超过 100 的子数组
const chunkSize = 100
const length = accounts.length
for (let i = 0; i < length; i += chunkSize) {
const chunk = accounts.slice(i, i + chunkSize)
if (chunk.length > 0) {
uni.$UIKitStore.subscriptionStore.subscribeUserStatusActive(chunk)
}
}
}
}
trackInit('ConversationUIKit')
/** 监听会话列表数据变更,实时更新 conversationList */
const conversationListWatch = autorun(() => {
const _conversationList = enableV2CloudConversation
? uni.$UIKitStore?.uiStore?.conversations
: uni.$UIKitStore?.uiStore?.localConversations
conversationList.value = _conversationList
?.map(
(conversation: V2NIMConversationForUI | V2NIMLocalConversationForUI) => {
return {
...conversation,
// 为什么要加一个renderKey 直接在渲染的时候写 :key = conversation.conversationId 不就行了吗?
// 如果不加,在渲染的时候,就会出现会话列表显示undefined,uniapp 很奇怪的问题
renderKey: conversation.conversationId,
}
}
)
.sort(
(
a: V2NIMConversationForUI | V2NIMLocalConversationForUI,
b: V2NIMConversationForUI | V2NIMLocalConversationForUI
) => b.sortOrder - a.sortOrder
)
setTabUnread()
})
/** 连接状态监听 断网重连后重新订阅 */
const connectWatch = autorun(() => {
if (
uni.$UIKitStore?.connectStore.loginStatus ===
V2NIMConst.V2NIMLoginStatus.V2NIM_LOGIN_STATUS_LOGINED &&
uni.$UIKitStore?.connectStore.connectStatus ===
V2NIMConst.V2NIMConnectStatus.V2NIM_CONNECT_STATUS_CONNECTED
) {
subscribeUserStatus(conversationList?.value)
}
})
/** 监听系统消息未读 */
const getTotalUnreadMsgsCountWatch = autorun(() => {
// 为了监听会触发
uni.$UIKitStore?.sysMsgStore?.getTotalUnreadMsgsCount()
setContactTabUnread()
})
// 监听数组长度变化
watch(
() => conversationList?.value?.length, // 监听 length 属性
() => {
subscribeUserStatus(conversationList?.value)
}
)
// 监听会话列表数据变更,实时订阅在线离线状态
onShow(() => {
if (conversationList.value?.length) {
subscribeUserStatus(conversationList?.value)
}
})
onHide(() => {
addDropdownVisible.value = false
currentMoveSessionId.value = ''
})
/**卸载监听 */
onUnmounted(() => {
conversationListWatch()
getTotalUnreadMsgsCountWatch()
connectWatch()
})
</script>
<style lang="scss" scoped>
@import '@/styles/common.scss';
.conversation-wrapper {
height: 100vh;
overflow: hidden;
}
.navigation-bar {
position: fixed;
height: 60px;
border-bottom: 1rpx solid #e9eff5;
padding: 0 20px;
display: none;
align-items: center;
justify-content: space-between;
padding-top: var(--status-bar-height);
background-color: #fff;
width: 100%;
opacity: 1;
z-index: 999;
}
.conversation-search {
display: none;
align-items: center;
height: 54px;
box-sizing: border-box;
overflow: hidden;
padding: 10px;
}
.security-tip {
padding: 0 10px;
background: #fff5e1;
height: 50px;
width: 100%;
box-sizing: border-box;
font-size: 14px;
text-align: center;
white-space: wrap;
color: #eb9718;
text-align: left;
display: none;
align-items: center;
}
.search-input-wrapper {
flex: 1;
display: flex;
align-items: center;
height: 34px;
overflow: hidden;
box-sizing: border-box;
padding: 8px 10px;
background: #f3f5f7;
border-radius: 5px;
}
.search-input {
margin-left: 5px;
color: #999999;
font-size: 14px;
}
.search-icon-wrapper {
height: 22px;
display: flex;
align-items: center;
}
.block {
height: 60px;
width: 100%;
display: none;
padding-top: var(--status-bar-height);
}
.conversation-list-wrapper {
height: calc(100% - 60px - var(--status-bar-height));
box-sizing: border-box;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.logo-box {
display: flex;
align-items: center;
font-size: 20px;
font-weight: 500;
.logo-img {
width: 32px;
height: 32px;
margin-right: 10px;
}
}
.button-icon-add {
position: relative;
right: 20px;
}
.dropdown-mark {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.dropdown-container {
position: absolute;
// #ifdef MP
top: -105px;
// #endif
// #ifndef MP
top: 100%;
// #endif
right: 30px;
min-width: 122px;
min-height: 40px;
background-color: #fff;
border: 1px solid #e6e6e6;
box-shadow: 0px 4px 7px rgba(133, 136, 140, 0.25);
border-radius: 8px;
z-index: 99;
}
.add-menu-list {
padding: 15px 10px;
.add-menu-item {
white-space: nowrap;
font-size: 16px;
padding-left: 5px;
margin-bottom: 10px;
height: 30px;
line-height: 30px;
display: flex;
align-items: center;
&:last-child {
margin-bottom: 0;
}
}
}
.conversation-block {
width: 100%;
height: 72px;
}
</style>
+27 -10
View File
@@ -28,8 +28,9 @@
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
>
<ConversationList />
<!-- 消息项 -->
<view class="message-item" v-for="(item, index) in messageList" :key="item.id || index" @click="openMessage(item)">
<!-- <view class="message-item" v-for="(item, index) in messageList" :key="item.id || index" @click="openMessage(item)">
<view class="message-avatar">
<view class="avatar-placeholder" v-if="!item.avatar">
<uni-icons type="person" size="32" color="#ffffff"></uni-icons>
@@ -48,14 +49,14 @@
</view>
<!-- 空状态提示 -->
<view class="empty-state" v-if="messageList.length === 0 && !isRefreshing">
<!-- <view class="empty-state" v-if="messageList.length === 0 && !isRefreshing">
<uni-icons type="chat" size="80" color="#cccccc"></uni-icons>
<text class="empty-text">暂无患者消息</text>
<text class="empty-subtext">下拉刷新获取最新申请</text>
<view class="debug-actions">
<button class="debug-btn" @click="getApplyList">测试API调用</button>
</view>
</view>
</view> -->
</scroll-view>
<!-- 患者列表区域 -->
@@ -141,7 +142,10 @@
<up-image :src="lineImg" width="14rpx" height="140rpx" ></up-image>
</view>
<view class="right-content">
<view class="note">{{ item.note }}</view>
<view class="leftcontent">
<view class="note">{{ item.note }}</view>
<view class="name">{{ item.patientname }}</view>
</view>
<uni-icons type="forward" size="20" color="#999"></uni-icons>
</view>
</view>
@@ -207,7 +211,7 @@
<script setup>
import { ref, getCurrentInstance, computed } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import { onShow,onLoad} from "@dcloudio/uni-app";
import dayImg from "@/static/visit_data11.png"
import planImg from "@/static/visitplan.png"
import api from '@/api/api.js';
@@ -217,6 +221,7 @@
import pinyin from 'pinyin';
import dayjs from 'dayjs'
import lineImg from "@/static/item_visitplan_fg.png"
import ConversationList from './conversation-list/index.vue'
const goPatientDetail = (uuid) => {
navTo({
@@ -581,13 +586,21 @@
// 使用 up-index-list 后不再需要手动滚动联动逻辑
// 页面显示时加载数据
onShow(() => {
activeTab.value='message';
onLoad(() => {
loadMessageList();
computeListHeight();
getApplyList();
patientListByGBK();
getFollowUpList();
});
onShow(() => {
followUpList.value = [];
page.value = 1;
followUpHasMore.value = true;
followUpLoading.value = false;
followUpRefreshing.value = false;
getFollowUpList(true);
});
// 加载消息列表
@@ -598,7 +611,7 @@
const goFollowDetail = (raw) => {
if(!raw) return;
navTo({
url: `/pages_app/followDetail/followDetail?followUpUuid=${encodeURIComponent(raw.uuid || '')}&patient_name=${encodeURIComponent(raw.patient_name || '')}`
url: `/pages_app/followDetail/followDetail?followUpUuid=${encodeURIComponent(raw.uuid || '')}&patient_name=${raw.patientname}`
});
};
</script>
@@ -1081,7 +1094,11 @@
font-size: 30rpx;
color:#333;
}
.right-content .name{
margin-top: 30rpx;
font-size: 28rpx;
color:#8B2316;
}
/* 加载状态样式 */
.load-more {
display: flex;
@@ -148,6 +148,7 @@
&:last-child{ border-bottom: none; }
.cell-left{
font-size: 32rpx;
white-space:nowrap;
color: #333;
}
.cell-right{
+397
View File
@@ -0,0 +1,397 @@
<template>
<view class="reply-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" @click="confirmReply">
<view class="btn-confirm" >确定</view>
</view>
</template>
</uni-nav-bar>
<!-- 主内容区域 -->
<view class="main-content">
<view class="input-container">
<textarea
class="reply-input"
v-model="replyText"
:placeholder="placeholder"
:auto-height="true"
:maxlength="maxLength"
:disabled="isSubmitting"
></textarea>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue'
import api from '@/api/api'
import { onLoad } from "@dcloudio/uni-app";
const video_uuid=ref('');
const name=ref('');
const comment_partent=ref('');
onLoad((options) => {
video_uuid.value=options.video_uuid;
placeholder.value='回复 '+options.name+':';
comment_partent.value=options.comment_partent;
name.value=options.name;
})
// 响应式数据
const statusBarHeight = ref(0)
const replyText = ref('')
const isSubmitting = ref(false)
const maxLength = 500
const placeholder = ref('')
// 计算属性
const replyLength = computed(() => replyText.value.length)
const canSubmit = computed(() => replyText.value.trim().length > 0 && !isSubmitting.value)
const remainingChars = computed(() => maxLength - replyLength.value)
// 监听器
watch(replyText, (newValue) => {
if (newValue.length > maxLength) {
replyText.value = newValue.slice(0, maxLength)
}
})
const addCommentV2=async()=>{
const res=await api.addCommentV2({
article_uuid:video_uuid.value,
type: "8",
comment:replyText.value+'||'+name.value+''+comment_partent.value
})
if(res.code==200){
uni.showToast({ title: '回复成功', icon: 'none' })
replyText.value = '';
uni.navigateBack();
}
}
onUnmounted(() => {
// 清理工作
console.log('回复页面已卸载')
})
// 方法
const goBack = () => {
uni.navigateBack()
}
const confirmReply = async () => {
console.log(1111)
if (!canSubmit.value) return
isSubmitting.value = true
try {
addCommentV2();
} catch (error) {
console.error('回复失败:', error)
uni.showToast({
title: '回复失败,请重试',
icon: 'error'
})
} finally {
isSubmitting.value = false
}
}
const startVoiceInput = () => {
// 语音输入功能
uni.showToast({
title: '语音输入功能',
icon: 'none'
})
}
const clearText = () => {
replyText.value = ''
}
</script>
<style lang="scss" scoped>
// 变量定义
$primary-color: #ff2442;
$text-color: #000000;
$text-light: #999999;
$text-medium: #333333;
$border-color: #e0e0e0;
$border-light: #f0f0f0;
$white: #ffffff;
$green: #00a86b;
$orange: #ff6900;
$blue: #007aff;
.reply-page {
background-color: $white;
min-height: 100vh;
}
// 状态栏样式
.status-bar {
background-color: $white;
.status-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
height: 20px;
.time {
font-size: 14px;
font-weight: 600;
color: $text-color;
}
.status-icons {
display: flex;
align-items: center;
gap: 8px;
.icon-group {
display: flex;
gap: 4px;
.app-icon {
width: 16px;
height: 16px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
color: $white;
&.green {
background-color: $green;
}
&.orange {
background-color: $orange;
}
&.red {
background-color: $primary-color;
}
&.blue {
background-color: $blue;
}
}
}
.network-info {
display: flex;
align-items: center;
gap: 4px;
.bluetooth-icon {
width: 12px;
height: 8px;
background-color: $text-color;
border-radius: 2px;
}
.speed {
font-size: 10px;
color: $text-color;
}
.signal-bars {
display: flex;
gap: 1px;
align-items: end;
.bar {
width: 2px;
background-color: $text-color;
&:nth-child(1) { height: 3px; }
&:nth-child(2) { height: 5px; }
&:nth-child(3) { height: 7px; }
&:nth-child(4) { height: 9px; }
}
}
.network-type {
font-size: 10px;
color: $text-color;
font-weight: 600;
}
.wifi-icon {
width: 12px;
height: 8px;
background-color: $text-color;
border-radius: 2px;
}
}
.battery {
font-size: 12px;
color: $text-color;
font-weight: 600;
}
}
}
}
.btn-confirm {
font-size: 28rpx;
color: #8B2316;
transition: opacity 0.3s ease;
}
// 导航栏样式
.nav-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: $white;
border-bottom: 1px solid $border-light;
.nav-left, .nav-right {
width: 60px;
display: flex;
align-items: center;
}
.nav-right {
justify-content: flex-end;
}
.back-arrow {
font-size: 24px;
color: $primary-color;
font-weight: bold;
}
.nav-title {
font-size: 18px;
color: $primary-color;
font-weight: 600;
}
.confirm-btn {
font-size: 16px;
color: $primary-color;
font-weight: 600;
transition: opacity 0.3s ease;
&.disabled {
opacity: 0.5;
color: $text-light;
}
}
}
// 主内容区域
.main-content {
flex: 1;
padding: 20px 16px;
.input-container {
position: relative;
background-color: $white;
border: 1px solid $border-color;
border-radius: 8px;
min-height: 200px;
padding: 16px;
.input-placeholder {
font-size: 14px;
color: $text-light;
margin-bottom: 8px;
}
.reply-input {
width: 100%;
min-height: 120px;
font-size: 16px;
color: $text-medium;
line-height: 1.5;
border: none;
outline: none;
resize: none;
}
.input-footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid $border-light;
.char-count {
font-size: 12px;
color: $text-light;
transition: color 0.3s ease;
&.warning {
color: $primary-color;
}
}
.action-buttons {
display: flex;
align-items: center;
gap: 8px;
.clear-btn {
width: 32px;
height: 32px;
background-color: $border-light;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
&:active {
background-color: $border-color;
}
.clear-icon {
font-size: 14px;
color: $text-light;
}
}
.voice-btn {
width: 40px;
height: 40px;
background-color: $primary-color;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(255, 36, 66, 0.3);
transition: transform 0.2s ease;
&:active {
transform: scale(0.95);
}
.voice-icon {
font-size: 18px;
color: $white;
}
}
}
}
}
}
</style>
+22 -12
View File
@@ -102,11 +102,13 @@
<script setup>
import { ref, onMounted } from 'vue';
import { onShow } from "@dcloudio/uni-app";
import navTo from '@/utils/navTo.js';
import api from '@/api/api.js';
// 表单数据
const show=ref(false);
const selectedPatient = ref('');
const selectedDate = ref('');
const datetime = ref('');
const followUpContent = ref('请于近日来院复诊、复查');
const remindMe = ref(false);
const remindPatient = ref(true);
@@ -117,7 +119,22 @@
const goBack = () => {
uni.navigateBack();
};
const addFollowUps=()=>{
api.addFollowUps({
patient_uuid: patientUuid.value,
note: followUpContent.value,
datetime: datetime.value,
isremindpatient: remindPatient.value?1:0,
isremindme:remindMe.value?1:0,
type:1
}).then(res=>{
console.log(res)
if(res.code==200){
uni.showToast({ title: '提交成功', icon: 'success' });
setTimeout(()=>uni.navigateBack(),700);
}
})
}
// 提交日程
const submitSchedule = () => {
if (!selectedPatient.value) {
@@ -127,16 +144,7 @@
});
return;
}
uni.showToast({
title: '日程添加成功',
icon: 'success'
});
// 延迟返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1500);
addFollowUps();
};
// 选择患者
@@ -184,6 +192,7 @@
const w = weekdays[d.getDay()];
headerYear.value = `${y}`;
headerDay.value = `${m}${dd}日周${w}`;
datetime.value = `${y}-${m}-${dd}`;
selectedDate.value = `${y}${m}${dd}日(星期${w})`;
}
};
@@ -201,6 +210,7 @@
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
const weekday = weekdays[today.getDay()];
selectedDate.value = `${year}${month}${day}日(星期${weekday})`;
datetime.value = `${year}-${month}-${day}`;
headerYear.value = `${year}`;
headerDay.value = `${month}${day}日周${weekday}`;
};
+2 -1
View File
@@ -66,6 +66,7 @@
import { ref } from 'vue'
import { onLoad,onShow } from '@dcloudio/uni-app'
import api from '@/api/api.js'
import navTo from '@/utils/navTo.js'
const keywords=ref('')
const title = ref('肝胆视频')
const activeTab = ref(0)
@@ -187,7 +188,7 @@ const switchTab = (index) => {
const openDetail = (item) => {
// 打开视频详情/播放页
navTo({
url: `/pages_app/videoDetail/videoDetail?uuid=${item.uuid}`
url: `/pages_app/videoDetail/videoDetail?id=${item.uuid}`
})
}
@@ -95,6 +95,7 @@
const toggle = (id) => {
const i = selectedIds.value.indexOf(id)
if (i > -1) {
return false;
// 如果已选中,则取消选中
selectedIds.value.splice(i, 1)
const di = selectedDetail.value.findIndex(it => it.uuid === id)
@@ -104,6 +105,12 @@
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 || '' }
const pages = getCurrentPages()
const curr = pages[pages.length - 1]
const ec = curr?.getOpenerEventChannel?.()
ec?.emit && ec.emit('onPatientsSelected', payload)
uni.navigateBack()
}
}
+13
View File
@@ -344,6 +344,19 @@
uni.setStorageSync('DEV_AUTH_YX_TOKEN_App', result.YX_token);
uni.setStorageSync('userInfo', result.data);
}
}else{
if (BASE_URL.indexOf('dev') == -1) {
uni.setStorageSync('AUTH_TOKEN_App',result.access_token);
uni.setStorageSync('AUTH_YX_ACCID_App', result.YX_accid);
uni.setStorageSync('AUTH_YX_TOKEN_App', result.YX_token);
uni.setStorageSync('userInfo', result.data);
} else {
uni.setStorageSync('DEV_AUTH_TOKEN_App', result.access_token);
uni.setStorageSync('DEV_AUTH_YX_ACCID_App', result.YX_accid);
uni.setStorageSync('DEV_AUTH_YX_TOKEN_App', result.YX_token);
uni.setStorageSync('userInfo', result.data);
}
}
+222 -71
View File
@@ -12,7 +12,10 @@
<template #right>
<view class="nav-actions">
<uni-icons type="share" size="22" color="#8B2316" />
<uni-icons type="heart" size="22" color="#8B2316" />
<view class="collect-img" @click="toCollection">
<image class="collect-img-icon" :src="videoInfo.isCollection?collectImg:discollectImg" mode="aspectFill" />
</view>
</view>
</template>
</uni-nav-bar>
@@ -42,8 +45,8 @@
<view class="video-title">{{ decodeURIComponent(pageParams.title) }}</view>
<view v-if="pageParams.author" class="video-author">{{ decodeURIComponent(pageParams.author) }}</view>
</view>
<view class="speaker">陈煜 教授</view>
<text class="intro-text">{{ introText }}</text>
<view class="speaker">{{videoInfo.public_name}}</view>
<text class="intro-text">{{ videoInfo.note }}</text>
</view>
<view v-else class="comments">
<view v-if="commentList.length === 0" class="empty">暂无评论</view>
@@ -54,6 +57,16 @@
<view class="name">{{ c.name }}</view>
<view class="content">{{ c.content }}</view>
<view class="time">{{ c.time }}</view>
<view v-if="c.children && c.children.length" class="child-list">
<view class="child-item" v-for="(r,i) in c.children" :key="i">
<image class="avatar small" :src="r.avatar" mode="aspectFill" />
<view class="meta">
<view class="name">{{ r.name }}</view>
<view class="content">{{ r.content }}</view>
<view class="time">{{ r.time }}</view>
</view>
</view>
</view>
</view>
<view class="reply-btn" @click="onReply(c)">回复</view>
</view>
@@ -70,38 +83,142 @@
<!-- 底部区域信息页为下载条评论页为上传图片+输入+发送 -->
<view v-if="activeTab === 'info'" class="bottom-download" @click="onDownload">
<text class="download-text">点击下载(限时<text class="discount">5</text>仅需50积分)</text>
<text class="download-text">点击下载(限时<text class="discount">5</text>仅需{{videoInfo.point-welfareNum}}积分)</text>
</view>
<view v-else class="bottom-comment">
<input class="comment-input" v-model="commentText" placeholder="我也说一句" confirm-type="send" @confirm="sendComment" />
<view class="send-btn" @click="sendComment">发送</view>
</view>
<unidialog :visible="networkVisible" :content="networkContent" @close="networkVisible=false" @confirm="networkConfirm"></unidialog>
<unidialog :visible="pointVisible" :content="pointContent" @close="pointVisible=false" @confirm="pointConfirm"></unidialog>
<unidialog :visible="notEnoughVisible" :content="notEnoughContent" @close="notEnoughVisible=false" @confirm="notEnoughConfirm"></unidialog>
</template>
<script setup>
import { ref } from 'vue';
import uniVideo from '@/components/uniVideo/uniVideo.vue';
import { onLoad,onShow } from "@dcloudio/uni-app";
import unidialog from '@/components/dialog/dialog.vue';
import collectImg from '@/static/icon_book_collect_sel.png';
import discollectImg from '@/static/icon_book_collect_nor.png';
import api from '@/api/api';
import docUrl from '@/utils/docUrl'
import navTo from '@/utils/navTo'
const video_uuid=ref('');
const videoInfo=ref({});
const networkVisible=ref(false);
const networkContent=ref('');
const pointVisible=ref(false);
const pointContent=ref('');
const notEnoughVisible=ref(false);
const notEnoughContent=ref('');
const welfareNum=ref(0);
const notEnoughConfirm=()=>{
notEnoughVisible.value=false;
navTo({
url:'/pages_app/buyPoint/buyPoint'
})
}
const pointConfirm=()=>{
pointVisible.value=false;
payVideoDownload();
}
const toCollection=()=>{
if(videoInfo.value.isCollection==1){
discollection();
}else{
collection();
}
}
const networkConfirm=()=>{
networkVisible.value=false;
pointVisible.value=true;
}
const addVideoWatchRecord=async()=>{
const res=await api.addVideoWatchRecord({
video_uuid:video_uuid.value
})
if(res.code==200){
}
}
// 接收页面参数
const pageParams = ref({});
// 使用uni-app的onLoad生命周期
const onLoad = (options) => {
pageParams.value = options;
console.log('接收到的参数:', pageParams.value);
// 如果有标题参数,可以在这里进行相应处理
if (options.title) {
// 可以更新页面标题或进行其他操作
console.log('视频标题:', decodeURIComponent(options.title));
const videoDetail=async()=>{
const res=await api.videoDetail({video_uuid:video_uuid.value})
if(res.code==200){
videoInfo.value=res.video;
}
if (options.author) {
console.log('视频作者:', decodeURIComponent(options.author));
}
const collection=async()=>{
const res=await api.collection({
other_uuid:video_uuid.value,
type:5
})
if(res.code==200){
uni.showToast({ title: '收藏成功', icon: 'none' })
videoDetail()
}
if (options.id) {
console.log('视频ID:', options.id);
}
const discollection=async()=>{
const res=await api.discollection({
other_uuid:video_uuid.value,
type:5
})
if(res.code==200){
uni.showToast({ title: '取消收藏成功', icon: 'none' })
videoDetail()
}
};
const isVideoDownloadRecord=ref(false);
const VideoDownloadRecord=async()=>{
const res=await api.isVideoDownloadRecord({video_uuid:video_uuid.value})
if(res.code==200){
isVideoDownloadRecord.value=res.result==0?false:true;
}
}
const getWelfareNum=async()=>{
const res=await api.getWelfareNum({
type:1
})
if(res.code==1){
welfareNum.value=res.WelfareNum;
}
}
const videoCommentListV2= async()=>{
loading.value = true;
try{
const res = await api.videoCommentListV2({
uuid: video_uuid.value
});
if(res && res.code==200 && Array.isArray(res.data)){
const mapped = res.data.map(mapComment);
commentList.value = mapped.reverse();
noMore.value = true;
}else{
noMore.value = true;
}
}finally{
loading.value = false;
}
}
onShow(()=>{
if(activeTab.value=='comment'){
videoCommentListV2();
}
videoDetail()
})
// 使用uni-app的onLoad生命周期
onLoad((options) => {
console.log(options)
video_uuid.value=options.id;
addVideoWatchRecord();
VideoDownloadRecord();
getWelfareNum();
});
const videoSrc = ref('');
const poster = ref('/static/livebg.png');
@@ -112,41 +229,68 @@ const introText = ref(
);
// 示例评论数据
const commentList = ref([
{
avatar: '/static/icon_home_my_public.png',
name: '肝胆相照测试号4',
content: 'hhjh',
time: '2025-07-01 17:17:13'
},
{
avatar: '/static/icon_home_my_public.png',
name: '肝胆相照测试号4',
content: 'kkkk',
time: '2025-07-01 17:17:18'
},
{
avatar: '/static/icon_home_my_public.png',
name: '肝胆相照测试号4',
content: 'nnj',
time: '2025-07-01 17:17:22'
}
]);
const commentList = ref([]);
const switchTab = (tab) => {
activeTab.value = tab;
if(tab=='comment'){
videoCommentListV2();
}
};
const payVideoDownload=async()=>{
const res=await api.payVideoDownload({video_uuid:video_uuid.value})
if(res.code==200){
navTo({
url:'/pages_app/myDownLoad/myDownLoad'
})
}else if(res.code==106){
notEnoughVisible.value=true;
notEnoughContent.value=`您的积分不足,是否购买积分?`
}
};
const onDownload = () => {
uni.showToast({ title: '前往下载页', icon: 'none' });
console.log(isVideoDownloadRecord.value);
if(!isVideoDownloadRecord.value){
pointContent.value=`当前需要${videoInfo.value.point-welfareNum.value}积分兑换,若删除可以再次缓存`
uni.getNetworkType({
success: function (res) {
console.log(res);
if(res.networkType!='none'){
networkVisible.value=true;
networkContent.value=`当前为${res.networkType}网络,确定下载?`
console.log(11111)
}else{
uni.showToast({title:'当前未联网,请检查网络',icon:'none'})
}
}
});
}else{
navTo({
url:'/pages_app/myDownLoad/myDownLoad'
})
}
};
const goBack = () => {
uni.navigateBack();
};
const addCommentV2=async()=>{
const res=await api.addCommentV2({
article_uuid:video_uuid.value,
type: "8",
comment:commentText.value
})
if(res.code==200){
uni.showToast({ title: '评论成功', icon: 'none' })
commentText.value = '';
videoCommentListV2();
}
}
const onReply = (c) => {
uni.showToast({ title: `回复:${c.name}`, icon: 'none' });
navTo({
url:'/pages_app/reply/reply?comment_partent='+c.content+'&name='+c.name+'&video_uuid='+video_uuid.value
})
};
// 评论输入
@@ -156,13 +300,7 @@ const sendComment = () => {
uni.showToast({ title: '请输入内容', icon: 'none' });
return;
}
commentList.value.unshift({
avatar: '/static/icon_home_my_public.png',
name: '我',
content: commentText.value,
time: new Date().toISOString().slice(0, 19).replace('T', ' ')
});
commentText.value = '';
addCommentV2();
};
// 上拉加载
@@ -171,30 +309,25 @@ const pageSize = ref(10);
const loading = ref(false);
const noMore = ref(false);
const mockMore = Array.from({ length: 30 }).map((_, i) => ({
avatar: '/static/icon_home_my_public.png',
name: '肝胆相照测试号4',
content: `更多评论 ${i + 1}`,
time: `2025-07-01 17:${20 + Math.floor(i / 2)}:${10 + (i % 2) * 5}`
}));
const onScrollToLower = async () => {
if (activeTab.value !== 'comment' || loading.value || noMore.value) return;
loading.value = true;
try {
await new Promise(r => setTimeout(r, 600));
const start = (page.value - 1) * pageSize.value;
const chunk = mockMore.slice(start, start + pageSize.value);
if (!chunk.length) {
noMore.value = true;
} else {
commentList.value.push(...chunk);
page.value += 1;
}
} finally {
loading.value = false;
}
// 如需分页,可在此按页码调用接口并 push 结果
};
const toFullUrl=(p)=>{
if(!p) return '/static/icon_home_my_public.png';
return p.startsWith('http')?p:(docUrl+p);
}
const mapComment=(item)=>{
return {
avatar: toFullUrl(item.photo||''),
name: item.name || '匿名',
content: item.content || '',
time: item.create_date || '',
children: Array.isArray(item.childs)? item.childs.map(mapComment): []
}
}
</script>
<style lang="scss" scoped>
@@ -204,7 +337,10 @@ $bg-color: #f7f7f7;
$text-primary: #333;
$text-secondary: #666;
$theme-color: #8B2316;
.collect-img-icon{
width: 40rpx;
height: 40rpx;
}
.nav-actions {
display: flex;
align-items: center;
@@ -371,6 +507,21 @@ $theme-color: #8B2316;
padding: 20rpx 0;
font-size: 26rpx;
}
.child-list {
margin-top: 8rpx;
margin-left: -10rpx;
}
.child-item {
display: flex;
align-items: flex-start;
margin-top: 16rpx;
}
.avatar.small {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
margin-right: 16rpx;
}
}
.bottom-spacer {