IM 相关功能开发

This commit is contained in:
wangdongbo 2026-01-28 16:20:19 +08:00
parent 1b07a9bc2d
commit b33c4357e8
10 changed files with 285 additions and 2587 deletions

View File

@ -1,201 +0,0 @@
<template>
<view class="article-detail-page">
<view v-if="loading" class="loading-container">
<uni-icons type="spinner-cycle" size="40" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="error" class="error-container">
<text class="error-text">{{ error }}</text>
<button class="retry-btn" @click="loadArticle">重试</button>
</view>
<scroll-view v-else scroll-y class="article-content">
<view class="article-header">
<text class="article-title">{{ articleData.title }}</text>
<text class="article-date">{{ articleData.date }}</text>
</view>
<view class="article-body">
<view class="rich-text-wrapper">
<rich-text :nodes="articleData.content"></rich-text>
</view>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
import { getArticle } from "@/utils/api.js";
import { ref } from "vue";
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID;
const loading = ref(true);
const error = ref("");
const articleData = ref({
title: "",
content: "",
date: "",
});
let articleId = "";
// 使
const processRichTextContent = (html) => {
if (!html) return "";
// img
let processedHtml = html.replace(
/<img/gi,
'<img style="max-width:100%;height:auto;display:block;margin:10px 0;"'
);
//
processedHtml = processedHtml.replace(
/style="[^"]*width:\s*\d+px[^"]*"/gi,
(match) => {
return match.replace(/width:\s*\d+px;?/gi, "max-width:100%;");
}
);
//
processedHtml = processedHtml.replace(
/<table/gi,
'<table style="max-width:100%;overflow-x:auto;display:block;"'
);
//
processedHtml = `<div style="width:100%;overflow-x:hidden;word-wrap:break-word;word-break:break-all;">${processedHtml}</div>`;
return processedHtml;
};
//
const loadArticle = async () => {
loading.value = true;
error.value = "";
try {
const res = await getArticle({ id: articleId, corpId });
if (res.success && res.data) {
//
let date = "";
if (res.data.createTime) {
const d = new Date(res.data.createTime);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
date = `${year}-${month}-${day}`;
}
articleData.value = {
title: res.data.title || "宣教文章",
content: processRichTextContent(res.data.content || ""),
date: date,
};
} else {
error.value = res.message || "加载文章失败";
}
} catch (err) {
console.error("加载文章失败:", err);
error.value = "加载失败,请重试";
} finally {
loading.value = false;
}
};
onLoad((options) => {
if (options.id) {
articleId = options.id;
loadArticle();
} else {
error.value = "文章信息不完整";
loading.value = false;
}
});
</script>
<style scoped lang="scss">
.article-detail-page {
width: 100%;
height: 100vh;
background-color: #fff;
}
.loading-container,
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
padding: 40rpx;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
.error-text {
font-size: 28rpx;
color: #999;
margin-bottom: 30rpx;
text-align: center;
}
.retry-btn {
padding: 16rpx 60rpx;
background-color: #0877f1;
color: #fff;
border: none;
border-radius: 8rpx;
font-size: 28rpx;
}
.article-content {
height: 100vh;
}
.article-header {
padding: 40rpx 30rpx 20rpx;
border-bottom: 1px solid #eee;
}
.article-title {
display: block;
font-size: 36rpx;
font-weight: bold;
color: #333;
line-height: 1.6;
margin-bottom: 20rpx;
}
.article-date {
display: block;
font-size: 24rpx;
color: #999;
}
.article-body {
padding: 0;
}
.rich-text-wrapper {
padding: 30rpx;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
.rich-text-wrapper ::v-deep rich-text {
width: 100%;
}
.rich-text-wrapper ::v-deep rich-text img {
max-width: 100% !important;
height: auto !important;
display: block;
}
</style>

View File

@ -1,588 +0,0 @@
<template>
<view class="article-page">
<view class="header">
<view class="search-bar">
<uni-icons type="search" size="18" color="#999" />
<input
class="search-input"
v-model="searchTitle"
placeholder="输入内容名称搜索"
@input="handleSearch"
/>
</view>
</view>
<view class="content">
<view class="category-sidebar">
<scroll-view scroll-y class="category-scroll">
<view
v-for="cate in categoryList"
:key="cate._id || 'all'"
class="category-item"
:class="{ active: currentCateId === cate._id }"
@click="selectCategory(cate)"
>
{{ cate.label }}
</view>
</scroll-view>
</view>
<view class="article-list">
<scroll-view
scroll-y
class="article-scroll"
@scrolltolower="loadMore"
lower-threshold="50"
>
<view
v-if="loading && articleList.length === 0"
class="loading-container"
>
<uni-icons type="spinner-cycle" size="30" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="articleList.length === 0" class="empty-container">
<empty-data title="暂无文章" />
</view>
<view v-else>
<view
v-for="article in articleList"
:key="article._id"
class="article-item"
>
<view class="article-content" @click="previewArticle(article)">
<text class="article-title">{{ article.title }}</text>
<view class="article-footer">
<text class="article-date">{{ article.date }}</text>
<button
class="send-btn"
size="mini"
type="primary"
@click.stop="sendArticle(article)"
>
发送
</button>
</view>
</view>
</view>
<view v-if="loading && articleList.length > 0" class="loading-more">
<uni-icons type="spinner-cycle" size="20" color="#999" />
<text>加载中...</text>
</view>
<view
v-if="!loading && articleList.length >= total"
class="no-more"
>
没有更多了
</view>
</view>
</scroll-view>
</view>
</view>
<!-- 文章预览弹窗 -->
<uni-popup ref="previewPopup" type="bottom" :safe-area="false">
<view class="preview-container">
<view class="preview-header">
<text class="preview-title">{{ previewArticleData.title }}</text>
<view class="preview-close" @click="closePreview">
<uni-icons type="closeempty" size="24" color="#333" />
</view>
</view>
<scroll-view scroll-y class="preview-content">
<view class="rich-text-wrapper">
<rich-text :nodes="previewArticleData.content"></rich-text>
</view>
</scroll-view>
<view class="preview-footer">
<button class="preview-close-btn" @click="closePreview">关闭</button>
</view>
</view>
</uni-popup>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import {
getArticleCateList,
getArticleList,
getArticle,
sendArticleMessage,
} from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import EmptyData from "@/components/empty-data.vue";
const accountStore = useAccountStore();
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID;
//
const pageParams = ref({
groupId: "",
userId: "",
corpId: "",
});
//
const searchTitle = ref("");
let searchTimer = null;
//
const categoryList = ref([{ _id: "", label: "全部" }]);
const currentCateId = ref(""); // ""_id
//
const articleList = ref([]);
const loading = ref(false);
const page = ref(1);
const pageSize = 30;
const total = ref(0);
//
const previewArticleData = ref({
title: "",
content: "",
});
const previewPopup = ref(null);
//
const getCategoryList = async () => {
try {
const res = await getArticleCateList({ corpId: corpId });
if (res.success && res.list) {
const cates = res.list || [];
categoryList.value = [{ _id: "", label: "全部" }, ...cates];
}
} catch (error) {
console.error("获取分类列表失败:", error);
}
};
//
const selectCategory = (cate) => {
currentCateId.value = cate._id || "";
page.value = 1;
articleList.value = [];
loadArticleList();
};
//
const handleSearch = () => {
if (searchTimer) {
clearTimeout(searchTimer);
}
searchTimer = setTimeout(() => {
page.value = 1;
articleList.value = [];
loadArticleList();
}, 500);
};
//
const loadArticleList = async () => {
if (loading.value) return;
loading.value = true;
try {
const params = {
corpId: corpId,
page: page.value,
pageSize: pageSize,
enable: true,
title: searchTitle.value,
};
// ID
if (currentCateId.value) {
params.cateIds = [currentCateId.value];
}
const res = await getArticleList(params);
if (res.success && res.list) {
const { list = [], total: count = 0 } = res;
const formattedList = list.map((item) => {
//
let date = "";
if (item.createTime) {
const d = new Date(item.createTime);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
date = `${year}-${month}-${day}`;
}
return {
...item,
date,
};
});
if (page.value === 1) {
articleList.value = formattedList;
} else {
articleList.value = [...articleList.value, ...formattedList];
}
total.value = count;
} else {
uni.showToast({
title: res.message || "获取文章列表失败",
icon: "none",
});
}
} catch (error) {
console.error("加载文章列表失败:", error);
uni.showToast({
title: "加载失败,请重试",
icon: "none",
});
} finally {
loading.value = false;
}
};
//
const loadMore = () => {
if (loading.value || articleList.value.length >= total.value) return;
page.value += 1;
loadArticleList();
};
// 使
const processRichTextContent = (html) => {
if (!html) return "";
// img
let processedHtml = html.replace(
/<img/gi,
'<img style="max-width:100%;height:auto;display:block;margin:10px 0;"'
);
//
processedHtml = processedHtml.replace(
/style="[^"]*width:\s*\d+px[^"]*"/gi,
(match) => {
return match.replace(/width:\s*\d+px;?/gi, "max-width:100%;");
}
);
//
processedHtml = processedHtml.replace(
/<table/gi,
'<table style="max-width:100%;overflow-x:auto;display:block;"'
);
//
processedHtml = `<div style="width:100%;overflow-x:hidden;word-wrap:break-word;word-break:break-all;">${processedHtml}</div>`;
return processedHtml;
};
//
const previewArticle = async (article) => {
try {
uni.showLoading({ title: "加载中..." });
const res = await getArticle({ id: article._id, corpId: corpId });
uni.hideLoading();
if (res.success && res.data) {
previewArticleData.value = {
title: res.data.title || article.title,
content: processRichTextContent(res.data.content || ""),
};
previewPopup.value?.open();
} else {
uni.showToast({
title: res.message || "预览文章失败",
icon: "none",
});
}
} catch (error) {
uni.hideLoading();
console.error("预览文章失败:", error);
uni.showToast({
title: "预览失败,请重试",
icon: "none",
});
}
};
//
const closePreview = () => {
previewPopup.value?.close();
};
//
const sendArticle = async (article) => {
try {
const { doctorInfo } = useAccountStore();
const result = await sendArticleMessage({
groupId: pageParams.value.groupId,
fromAccount: doctorInfo.weChatOpenId,
articleId: article._id,
title: article.title || "宣教文章",
imgUrl: article.cover || "",
desc: "点击查看详情",
});
if (result.success) {
uni.navigateBack();
} else {
throw new Error(result.message || "发送失败");
}
} catch (error) {
console.error("发送文章失败:", error);
uni.showToast({
title: error.message || "发送失败",
icon: "none",
});
} finally {
loading.value = false;
}
};
//
const goBack = () => {
uni.navigateBack();
};
//
onLoad((options) => {
if (options.groupId) {
pageParams.value.groupId = options.groupId;
}
if (options.userId) {
pageParams.value.userId = options.userId;
}
if (options.corpId) {
pageParams.value.corpId = options.corpId;
}
});
onMounted(() => {
getCategoryList();
loadArticleList();
});
</script>
<style scoped lang="scss">
.article-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.header {
background-color: #fff;
padding: 20rpx;
border-bottom: 1px solid #eee;
}
.search-bar {
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 16rpx 24rpx;
}
.search-input {
flex: 1;
margin-left: 16rpx;
font-size: 28rpx;
}
.content {
flex: 1;
display: flex;
overflow: hidden;
}
.category-sidebar {
width: 200rpx;
background-color: #f8f8f8;
border-right: 1px solid #eee;
}
.category-scroll {
height: 100%;
}
.category-item {
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #333;
text-align: center;
border-bottom: 1px solid #eee;
transition: all 0.3s ease;
position: relative;
}
.category-item.active {
background-color: #fff;
color: #0877f1;
font-weight: bold;
border-left: 4rpx solid #0877f1;
}
.article-list {
flex: 1;
background-color: #fff;
}
.article-scroll {
height: 100%;
}
.loading-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
.article-item {
padding: 24rpx 30rpx;
border-bottom: 1px solid #eee;
transition: background-color 0.2s ease;
}
.article-item:active {
background-color: #f5f5f5;
}
.article-content {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.article-title {
font-size: 28rpx;
color: #333;
line-height: 1.6;
word-break: break-all;
font-weight: 500;
}
.article-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20rpx;
}
.article-date {
flex: 1;
font-size: 24rpx;
color: #999;
}
.send-btn {
flex-shrink: 0;
font-size: 26rpx;
padding: 8rpx 32rpx;
height: auto;
line-height: 1.4;
}
.loading-more,
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
font-size: 24rpx;
color: #999;
gap: 10rpx;
}
.footer {
background-color: #fff;
padding: 20rpx;
border-top: 1px solid #eee;
}
.cancel-btn {
width: 100%;
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
/* 预览弹窗样式 */
.preview-container {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
height: 80vh;
display: flex;
flex-direction: column;
}
.preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1px solid #eee;
}
.preview-title {
flex: 1;
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.preview-close {
padding: 10rpx;
}
.preview-content {
flex: 1;
padding: 0;
overflow-y: auto;
}
.rich-text-wrapper {
padding: 30rpx;
width: 100%;
box-sizing: border-box;
overflow: hidden;
}
/* rich-text 内部样式 */
.rich-text-wrapper ::v-deep rich-text {
width: 100%;
}
.rich-text-wrapper ::v-deep rich-text img {
max-width: 100% !important;
height: auto !important;
display: block;
}
.preview-footer {
padding: 20rpx;
border-top: 1px solid #eee;
}
.preview-close-btn {
width: 100%;
background-color: #1890ff;
color: #fff;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -79,7 +79,7 @@ const props = defineProps({
});
// Emits
const emit = defineEmits(["messageSent", "scrollToBottom", "endConsult"]);
const emit = defineEmits(["messageSent", "scrollToBottom"]);
//
const inputText = ref("");
@ -153,22 +153,7 @@ const sendTextMessage = async () => {
inputText.value = "";
};
//
const sendTextMessageFromPhrase = async (content) => {
if (!content.trim()) return;
await sendMessage("text", content);
//
nextTick(() => {
emit("scrollToBottom");
});
};
//
defineExpose({
sendTextMessageFromPhrase
});
//
const sendImageMessage = async (imageFile) => {
@ -338,91 +323,8 @@ const cancelRecord = () => {
stopRecordUtil(recorderManager);
};
//
const sendSurveyMessage = async () => {
const surveyMessage = createCustomMessage(
"survey",
{
content: "医生发送了问卷调查",
surveyTitle: "治疗效果评估",
surveyDescription: "您好,为了帮助了解您的病情变化,请您如实填写问卷。",
surveyMessage: "慢性病患者生活质量评估问卷",
estimatedTime: "约3-5分钟",
reward: "积分奖励10分",
note: "问卷内容涉及您的症状变化、用药情况等,请根据实际情况填写。",
},
props.formatTime
);
await sendCustomMessage(surveyMessage);
};
//
const goToCommonPhrases = () => {
uni.navigateTo({
url: '/pages/message/common-phrases'
});
};
//
const goToArticleList = () => {
uni.navigateTo({
url: `/pages/message/article-list?groupId=${props.groupId}&userId=${props.userId}&corpId=${props.corpId}`
});
};
//
const goToSurveyList = () => {
uni.navigateTo({
url: '/pages/message/survey-list'
});
};
//
const handleEndConsult = () => {
uni.showModal({
title: '确认结束问诊',
content: '确定要结束本次问诊吗?结束后将无法继续对话。',
confirmText: '确定结束',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
//
showMorePanel.value = false;
//
emit('endConsult');
}
}
});
};
const morePanelButtons = [
{ text: "照片", icon: "/static/icon/zhaopian.png", action: showImagePicker },
{
text: "回访任务",
icon: "/static/icon/zhaopian.png",
action: showImagePicker,
},
{
text: "常用语",
icon: "/static/icon/changyongyu.png",
action: goToCommonPhrases,
},
{
text: "宣教",
icon: "/static/icon/xuanjiaowenzhang.png",
action: goToArticleList,
},
{
text: "问卷",
icon: "/static/icon/wenjuan.png",
action: goToSurveyList,
},
{
text: "结束问诊",
icon: "/static/icon/jieshuzixun.png",
action: handleEndConsult,
},
];
function handleInputFocus() {

View File

@ -0,0 +1,118 @@
<template>
<view class="consult-apply-container">
<view class="apply-card">
<view class="apply-left">
<view class="icon-wrapper">
<image
class="icon-img"
src="/static/homepage/chat-icon.png"
mode="aspectFit"
/>
</view>
<view class="apply-content">
<text class="apply-title">聊天咨询</text>
<text class="apply-desc">团队协作高效沟通</text>
</view>
</view>
<view class="apply-right">
<button class="btn-apply" @click="handleApply">咨询申请</button>
</view>
</view>
</view>
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['apply']);
const handleApply = () => {
emit('apply');
};
</script>
<style scoped lang="scss">
.consult-apply-container {
width: 100%;
padding: 20rpx 32rpx;
background-color: #f5f5f5;
box-sizing: border-box;
}
.apply-card {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
display: flex;
align-items: center;
justify-content: space-between;
}
.apply-left {
display: flex;
align-items: center;
gap: 24rpx;
flex: 1;
}
.icon-wrapper {
width: 96rpx;
height: 96rpx;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.icon-img {
width: 56rpx;
height: 56rpx;
}
.apply-content {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.apply-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.apply-desc {
font-size: 24rpx;
color: #999;
}
.apply-right {
flex-shrink: 0;
}
.btn-apply {
padding: 0 32rpx;
background-color: #1890ff;
color: #fff;
border-radius: 8rpx;
font-size: 28rpx;
font-weight: 500;
border: none;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
}
.btn-apply:active {
opacity: 0.8;
transform: scale(0.98);
}
.btn-apply::after {
border: none;
}
</style>

View File

@ -51,10 +51,10 @@ const handleCancel = () => {
justify-content: center;
}
.btn-cancel {
.btn-cancel,
.btn-reapply {
width: 100%;
height: 80rpx;
border-radius: 8rpx;
heiger-radius: 8rpx;
font-size: 28rpx;
border: none;
display: flex;

View File

@ -114,9 +114,12 @@
<!-- 取消申请组件 -->
<ConsultCancel v-if="showConsultCancel" @cancel="handleCancelConsult" />
<!-- 咨询申请组件 -->
<ConsultApply v-if="showConsultApply" @apply="handleApplyConsult" />
<!-- 聊天输入组件 -->
<ChatInput
v-if="!isEvaluationPopupOpen && !showConsultCancel"
v-if="!isEvaluationPopupOpen && !showConsultCancel && !showConsultApply"
ref="chatInputRef"
:timChatManager="timChatManager"
:formatTime="formatTime"
@ -126,10 +129,9 @@
: ''
"
:userId="openid"
:corpId="corpId"
:corpId="corpId.value"
@scrollToBottom="() => scrollToBottom(true)"
@messageSent="() => scrollToBottom(true)"
@endConsult="handleEndConsult"
/>
</view>
</template>
@ -163,12 +165,12 @@ import MessageTypes from "./components/message-types.vue";
import ChatInput from "./components/chat-input.vue";
import SystemMessage from "./components/system-message.vue";
import ConsultCancel from "./components/consult-cancel.vue";
import ConsultApply from "./components/consult-apply.vue";
const timChatManager = globalTimChatManager;
//
const env = __VITE_ENV__;
const corpId = env.MP_CORP_ID || "";
// corpId
const corpId = ref("");
//
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
@ -197,8 +199,17 @@ const chatInfo = ref({
//
const isEvaluationPopupOpen = ref(false);
//
const showConsultCancel = ref(false);
//
const orderStatus = ref("");
//
const showConsultCancel = computed(() => orderStatus.value === "pending");
const showConsultApply = computed(
() =>
orderStatus.value === "finished" ||
orderStatus.value === "cancelled" ||
orderStatus.value === "rejected"
);
//
const messageList = ref([]);
@ -215,77 +226,75 @@ function isSystemMessage(message) {
if (message.type !== "TIMCustomElem") {
return false;
}
try {
// payload.data
if (message.payload?.data) {
const data =
typeof message.payload.data === "string"
? JSON.parse(message.payload.data)
: message.payload.data;
//
if (data.type === "system_message") {
return true;
}
}
// description
if (message.payload?.description === "系统消息标记") {
return true;
}
//
if (message.payload?.description === "SYSTEM_NOTIFICATION") {
return true;
}
} catch (error) {
console.error("判断系统消息失败:", error);
}
return false;
}
//
const fetchGroupOrderStatus = async () => {
try {
const result = await api("getGroupListByGroupId", {
groupId: groupId.value,
});
if (result.success && result.data) {
orderStatus.value = result.data.orderStatus || "";
corpId.value = result.data.corpId || "";
console.log("获取群组订单状态:", {
orderStatus: orderStatus.value,
corpId: corpId.value,
});
} else {
console.error("获取群组订单状态失败:", result.message);
}
} catch (error) {
console.error("获取群组订单状态异常:", error);
}
};
//
function handleSystemMessageReceived(message) {
try {
if (!message.payload?.data) return;
const data =
typeof message.payload.data === "string"
? JSON.parse(message.payload.data)
: message.payload.data;
if (data.type !== "system_message") return;
console.log("收到系统消息,类型:", data.messageType);
//
fetchGroupOrderStatus();
} catch (error) {
console.error("处理系统消息失败:", error);
}
}
//
function checkConsultPendingStatus() {
//
for (let i = messageList.value.length - 1; i >= 0; i--) {
const message = messageList.value[i];
if (message.type === "TIMCustomElem" && message.payload?.data) {
try {
const data =
typeof message.payload.data === "string"
? JSON.parse(message.payload.data)
: message.payload.data;
// consult_pending
if (
data.type === "system_message" &&
data.messageType === "consult_pending"
) {
showConsultCancel.value = true;
return;
}
//
if (
data.type === "system_message" &&
(data.messageType === "consult_accepted" ||
data.messageType === "consult_ended" ||
data.messageType === "consult_rejected")
) {
showConsultCancel.value = false;
return;
}
} catch (error) {
console.error("解析系统消息失败:", error);
}
}
}
//
showConsultCancel.value = false;
//
fetchGroupOrderStatus();
}
//
@ -324,7 +333,7 @@ const checkLoginAndInitTIM = async () => {
uni.showLoading({
title: "连接中...",
});
const success = await initIMAfterLogin(openid.value);
const success = await initIMAfterLogin();
uni.hideLoading();
if (!success) {
uni.showToast({
@ -379,6 +388,11 @@ const initTIMCallbacks = async () => {
messageList.value.push(message);
console.log("✓ 添加消息到列表,当前消息数量:", messageList.value.length);
//
if (isSystemMessage(message)) {
handleSystemMessageReceived(message);
}
//
checkConsultPendingStatus();
@ -502,7 +516,7 @@ const initTIMCallbacks = async () => {
};
//
const loadMessageList = () => {
const loadMessageList = async () => {
if (isLoading.value) {
console.log("正在加载中,跳过重复加载");
return;
@ -519,6 +533,9 @@ const loadMessageList = () => {
mask: false,
});
//
await fetchGroupOrderStatus();
timChatManager.enterConversation(chatInfo.value.conversationID || "test1");
//
@ -691,16 +708,6 @@ onHide(() => {
stopIMMonitoring();
});
const sendCommonPhrase = (content) => {
if (chatInputRef.value) {
chatInputRef.value.sendTextMessageFromPhrase(content);
}
};
//
defineExpose({
sendCommonPhrase,
});
//
const handleCancelConsult = async () => {
try {
@ -716,14 +723,22 @@ const handleCancelConsult = async () => {
//
const result = await api("cancelConsultApplication", {
groupId: groupId.value,
corpId: corpId,
corpId: corpId.value,
});
uni.hideLoading();
if (result.success) {
showConsultCancel.value = false;
uni.switchTab({
url: "/pages/home/home",
//
await fetchGroupOrderStatus();
uni.showToast({
title: "已取消申请",
icon: "success",
});
//
setTimeout(() => {
uni.switchTab({
url: "/pages/home/home",
});
}, 1500);
} else {
throw new Error(result.message || "取消申请失败");
}
@ -747,34 +762,84 @@ const handleCancelConsult = async () => {
}
};
//
const handleEndConsult = async () => {
//
const handleApplyConsult = async () => {
try {
uni.showLoading({
title: "处理中...",
});
// groupId
const result = await api("endConsultation", {
groupId: groupId.value,
adminAccount: account.value?.userId || "",
extraData: {
endBy: account.value?.userId || "",
endByName: account.value?.name || "医生",
endReason: "问诊完成",
uni.showModal({
title: "提示",
content: "确定要重新申请咨询吗?",
success: async (res) => {
if (res.confirm) {
uni.showLoading({
title: "申请中...",
});
try {
// IDcorpId
const groupInfo = await api("getGroupListByGroupId", {
groupId: groupId.value,
});
if (!groupInfo.success || !groupInfo.data) {
throw new Error("获取群组信息失败");
}
const customerId = groupInfo.data.customerId;
const groupCorpId = groupInfo.data.corpId;
if (!customerId) {
throw new Error("无法获取客户信息");
}
//
const result = await api("createConsultGroup", {
teamId: groupInfo.data.teamId || "",
corpId: groupCorpId || corpId.value,
customerId: customerId,
customerImUserId: openid.value,
});
uni.hideLoading();
if (result.success) {
const { groupId: newGroupId, isExisting } = result.data;
//
await fetchGroupOrderStatus();
uni.showToast({
title: isExisting ? "已重新发起申请" : "申请已发送",
icon: "success",
});
//
if (!isExisting && newGroupId !== groupId.value) {
setTimeout(() => {
uni.redirectTo({
url: `/pages/message/index?conversationID=GROUP${newGroupId}&groupID=${newGroupId}`,
});
}, 1500);
} else {
//
setTimeout(() => {
loadMessageList();
}, 1000);
}
} else {
throw new Error(result.message || "申请失败");
}
} catch (error) {
console.error("申请失败:", error);
uni.hideLoading();
uni.showToast({
title: error.message || "操作失败",
icon: "none",
});
}
}
},
});
uni.hideLoading();
if (result.success) {
uni.showToast({
title: "问诊已结束",
icon: "success",
});
} else {
throw new Error(result.message || "操作失败");
}
} catch (error) {
console.error("结束问诊失败:", error);
uni.hideLoading();
console.error("申请失败:", error);
uni.showToast({
title: error.message || "操作失败",
icon: "none",
@ -791,100 +856,6 @@ onUnmounted(() => {
timChatManager.setCallback("onMessageReceived", null);
timChatManager.setCallback("onMessageListLoaded", null);
timChatManager.setCallback("onError", null);
//
uni.$off("sendSurvey");
});
//
uni.$on("sendSurvey", async (data) => {
const { survey, corpId, userId, sendSurveyId } = data;
if (!survey || !survey._id) {
uni.showToast({
title: "问卷信息不完整",
icon: "none",
});
return;
}
try {
//
const env = __VITE_ENV__;
const baseUrl = env.VITE_PATIENT_PAGE_BASE_URL || "";
const surveyUrl = env.VITE_SURVEY_URL || "";
//
const customerId = chatInfo.value.userID || "";
const customerName = chatInfo.value.customerName || "";
//
const { createSurveyRecord } = await import("@/utils/api.js");
const recordRes = await createSurveyRecord({
corpId,
userId,
surveryId: survey._id,
memberId: customerId,
customer: customerName,
sendSurveyId,
});
if (!recordRes.success) {
throw new Error(recordRes.message || "创建问卷记录失败");
}
const answerId = recordRes.data?.id || "";
//
let surveyLink = "";
if (survey.createBy === "system") {
//
surveyLink = `${surveyUrl}?corpId=${corpId}&surveyId=${survey.surveyId}&memberId=${customerId}&sendSurveyId=${sendSurveyId}&userId=${userId}`;
} else {
//
surveyLink = `${baseUrl}pages/survery/fill?corpId=${corpId}&surveryId=${
survey._id
}&memberId=${customerId}&answerId=${answerId}&name=${encodeURIComponent(
customerName
)}`;
}
//
const customMessage = {
data: JSON.stringify({
type: "survey",
title: survey.name || "填写问卷",
desc: "请填写问卷",
url: surveyLink,
imgUrl:
"https://796f-youcan-clouddev-1-8ewcqf31dbb2b5-1317294507.tcb.qcloud.la/other/19-%E9%97%AE%E5%8D%B7.png?sign=55a4cd77c418b2c548b65792a2cf6bce&t=1701328694",
}),
description: "SURVEY",
extension: "",
};
//
const message = timChatManager.tim.createCustomMessage({
to: chatInfo.value.conversationID.replace("GROUP", ""),
conversationType: timChatManager.TIM.TYPES.CONV_GROUP,
payload: customMessage,
});
const sendResult = await timChatManager.tim.sendMessage(message);
if (sendResult.code === 0) {
uni.showToast({
title: "发送成功",
icon: "success",
});
} else {
throw new Error(sendResult.message || "发送失败");
}
} catch (error) {
console.error("发送问卷失败:", error);
uni.showToast({
title: error.message || "发送失败",
icon: "none",
});
}
});
</script>

View File

@ -95,7 +95,7 @@ const initIM = async () => {
uni.showLoading({
title: "连接中...",
});
const success = await initIMAfterLogin(openid.value);
const success = await initIMAfterLogin();
uni.hideLoading();
if (!success) {

View File

@ -1,446 +0,0 @@
<template>
<view class="survey-page">
<view class="header">
<view class="search-bar">
<uni-icons type="search" size="18" color="#999" />
<input
class="search-input"
v-model="searchName"
placeholder="输入问卷名称搜索"
@input="handleSearch"
/>
</view>
</view>
<view class="content">
<view class="category-sidebar">
<scroll-view scroll-y class="category-scroll">
<view
v-for="cate in categoryList"
:key="cate._id || 'all'"
class="category-item"
:class="{ active: currentCateId === cate._id }"
@click="selectCategory(cate)"
>
{{ cate.label }}
</view>
</scroll-view>
</view>
<view class="survey-list">
<scroll-view
scroll-y
class="survey-scroll"
@scrolltolower="loadMore"
lower-threshold="50"
>
<view
v-if="loading && surveyList.length === 0"
class="loading-container"
>
<uni-icons type="spinner-cycle" size="30" color="#999" />
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="surveyList.length === 0" class="empty-container">
<empty-data :title="emptyText || '暂无问卷'" />
</view>
<view v-else>
<view
v-for="survey in surveyList"
:key="survey._id"
class="survey-item"
>
<view class="survey-content" @click="previewSurvey(survey)">
<text class="survey-title">{{ survey.name }}</text>
<text class="survey-desc">{{
survey.description || "暂无问卷说明"
}}</text>
</view>
<view class="survey-action">
<button
class="send-btn"
size="mini"
type="primary"
@click="sendSurvey(survey)"
>
发送
</button>
</view>
</view>
<view v-if="loading && surveyList.length > 0" class="loading-more">
<uni-icons type="spinner-cycle" size="20" color="#999" />
<text>加载中...</text>
</view>
<view v-if="!loading && surveyList.length >= total" class="no-more">
没有更多了
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import {
getSurveyCateList,
getSurveyList,
createSurveyRecord,
} from "@/utils/api.js";
import useAccountStore from "@/store/account.js";
import EmptyData from "@/components/empty-data.vue";
const env = __VITE_ENV__;
const accountStore = useAccountStore();
const corpId = env.MP_CORP_ID;
const userId = ref("");
//
const searchName = ref("");
let searchTimer = null;
//
const categoryList = ref([{ _id: "", label: "全部" }]);
const currentCateId = ref("");
//
const surveyList = ref([]);
const loading = ref(false);
const page = ref(1);
const pageSize = 30;
const total = ref(0);
const emptyText = ref("");
//
const getCategoryList = async () => {
try {
const res = await getSurveyCateList({ corpId: corpId });
if (res.success && res.list) {
const cates = res.list || [];
categoryList.value = [{ _id: "", label: "全部" }, ...cates];
}
} catch (error) {
console.error("获取分类列表失败:", error);
}
};
//
const selectCategory = (cate) => {
currentCateId.value = cate._id || "";
page.value = 1;
surveyList.value = [];
loadSurveyList();
};
//
const handleSearch = () => {
if (searchTimer) {
clearTimeout(searchTimer);
}
searchTimer = setTimeout(() => {
page.value = 1;
surveyList.value = [];
loadSurveyList();
}, 500);
};
//
const loadSurveyList = async () => {
if (loading.value) return;
loading.value = true;
try {
const params = {
corpId: corpId,
page: page.value,
pageSize: pageSize,
name: searchName.value.trim(),
status: "enable",
showCount: false,
};
// ID
if (currentCateId.value) {
params.cateIds = [currentCateId.value];
}
const res = await getSurveyList(params);
if (res.success && res) {
const { list = [], total: count = 0 } = res;
if (page.value === 1) {
surveyList.value = list;
} else {
surveyList.value = [...surveyList.value, ...list];
}
total.value = count;
emptyText.value = "暂无问卷信息";
} else {
emptyText.value = res.message || "加载失败";
uni.showToast({
title: res.message || "获取问卷列表失败",
icon: "none",
});
}
} catch (error) {
console.error("加载问卷列表失败:", error);
emptyText.value = "加载失败";
uni.showToast({
title: "加载失败,请重试",
icon: "none",
});
} finally {
loading.value = false;
}
};
//
const loadMore = () => {
if (loading.value || surveyList.value.length >= total.value) return;
page.value += 1;
loadSurveyList();
};
//
const previewSurvey = (survey) => {
const timestamp = Date.now();
const previewUrl = `https://www.youcan365.com/surveyDev/#/pages/survey/survey?surveyId=${survey.surveyId}&t=${timestamp}`;
// #ifdef H5
window.open(previewUrl, '_blank');
// #endif
// #ifdef MP-WEIXIN
uni.navigateTo({
url: `/pages/webview/webview?url=${encodeURIComponent(previewUrl)}`
});
// #endif
// #ifdef APP-PLUS
plus.runtime.openURL(previewUrl);
// #endif
};
//
const generateRandomString = (length) => {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
//
const sendSurvey = async (survey) => {
if (loading.value) return;
try {
loading.value = true;
//
const doctorInfo = accountStore.doctorInfo;
userId.value = doctorInfo?.userid || accountStore.openid;
// ID
const sendSurveyId = generateRandomString(10);
//
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
//
// 使
uni.$emit("sendSurvey", {
survey: survey,
corpId: corpId,
userId: userId.value,
sendSurveyId: sendSurveyId,
});
uni.showToast({
title: "已选择问卷",
icon: "success",
});
//
setTimeout(() => {
uni.navigateBack();
}, 500);
} catch (error) {
console.error("发送问卷失败:", error);
uni.showToast({
title: error.message || "发送失败",
icon: "none",
});
} finally {
loading.value = false;
}
};
//
const goBack = () => {
uni.navigateBack();
};
onMounted(() => {
getCategoryList();
loadSurveyList();
});
</script>
<style scoped lang="scss">
.survey-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.header {
background-color: #fff;
padding: 20rpx;
border-bottom: 1px solid #eee;
}
.search-bar {
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 16rpx 24rpx;
}
.search-input {
flex: 1;
margin-left: 16rpx;
font-size: 28rpx;
}
.content {
flex: 1;
display: flex;
overflow: hidden;
}
.category-sidebar {
width: 200rpx;
background-color: #f8f8f8;
border-right: 1px solid #eee;
}
.category-scroll {
height: 100%;
}
.category-item {
padding: 20rpx 24rpx;
font-size: 28rpx;
color: #333;
text-align: center;
border-bottom: 1px solid #eee;
transition: all 0.3s ease;
position: relative;
}
.category-item.active {
background-color: #fff;
color: #0877f1;
font-weight: bold;
border-left: 4rpx solid #0877f1;
}
.survey-list {
flex: 1;
background-color: #fff;
}
.survey-scroll {
height: 100%;
}
.loading-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #999;
}
.survey-item {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
border-bottom: 1px solid #eee;
}
.survey-content {
flex: 1;
display: flex;
flex-direction: column;
margin-right: 20rpx;
}
.survey-title {
font-size: 28rpx;
color: #333;
font-weight: 500;
line-height: 1.5;
margin-bottom: 12rpx;
}
.survey-desc {
font-size: 24rpx;
color: #999;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.survey-action {
flex-shrink: 0;
}
.send-btn {
font-size: 26rpx;
}
.loading-more,
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
font-size: 24rpx;
color: #999;
gap: 10rpx;
}
.footer {
background-color: #fff;
padding: 20rpx;
border-top: 1px solid #eee;
}
.cancel-btn {
width: 100%;
background-color: #fff;
color: #333;
border: 1px solid #ddd;
}
</style>

View File

@ -43,12 +43,12 @@ export default defineStore("accountStore", () => {
loading.value = false
}
async function initIMAfterLogin(userID) {
async function initIMAfterLogin() {
if (isIMInitialized.value) {
return true;
}
try {
await initGlobalTIM(userID);
await initGlobalTIM();
isIMInitialized.value = true;
return true;
} catch (error) {