Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb
This commit is contained in:
commit
b6bc5a24c7
25
App.vue
25
App.vue
@ -29,13 +29,36 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
$primary-color: #0074ff;
|
||||
$primary-color: #0877F1;
|
||||
|
||||
page {
|
||||
height: 100%;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 全局按钮样式 - 使用项目主题色 */
|
||||
button[type="primary"],
|
||||
.button-primary,
|
||||
uni-button[type="primary"] {
|
||||
background-color: $primary-color !important;
|
||||
border-color: $primary-color !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
button[type="primary"]:not([disabled]):active,
|
||||
.button-primary:active,
|
||||
uni-button[type="primary"]:not([disabled]):active {
|
||||
background-color: darken($primary-color, 10%) !important;
|
||||
border-color: darken($primary-color, 10%) !important;
|
||||
}
|
||||
|
||||
/* 微信小程序按钮样式覆盖 */
|
||||
.wx-button[type="primary"] {
|
||||
background-color: $primary-color !important;
|
||||
border-color: $primary-color !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.shadow-up {
|
||||
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
||||
rgba(0, 0, 0, 0.1) 0px -10px 15px -3px, rgba(0, 0, 0, 0.1) 0px -4px 6px -4px;
|
||||
|
||||
30
pages.json
30
pages.json
@ -6,6 +6,36 @@
|
||||
"navigationBarTitleText": "消息"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/common-phrases",
|
||||
"style": {
|
||||
"navigationBarTitleText": "常用语"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/article-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "宣教文章"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/article-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "宣教文章"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/survey-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "问卷列表"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/webview/webview",
|
||||
"style": {
|
||||
"navigationBarTitleText": "预览"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/index",
|
||||
"style": {
|
||||
|
||||
@ -1,95 +0,0 @@
|
||||
# 常用语功能说明
|
||||
|
||||
## 功能概述
|
||||
在聊天页面添加了常用语功能,医生可以快速选择并发送预设的常用语内容。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 常用语列表页面 (`common-phrases.vue`)
|
||||
- 支持分类管理(文字随访、语音随访、常用回复等)
|
||||
- 支持添加、编辑、删除常用语
|
||||
- 支持添加自定义分类
|
||||
- 点击常用语直接发送到聊天
|
||||
|
||||
### 2. 聊天输入框集成
|
||||
- 在聊天输入框的"更多"面板中添加"常用语"入口
|
||||
- 点击后跳转到常用语列表页面
|
||||
- 选择常用语后自动返回并发送
|
||||
|
||||
### 3. 数据持久化
|
||||
- 常用语数据存储在MongoDB的`common-words`集合中
|
||||
- 支持机构级别和个人级别的常用语
|
||||
- 与PC端管理后台数据互通
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
ykt-wx-app/pages/message/
|
||||
├── common-phrases.vue # 常用语列表页面
|
||||
├── components/
|
||||
│ └── chat-input.vue # 聊天输入框组件(已更新)
|
||||
├── index.vue # 聊天主页面(已更新)
|
||||
└── README.md # 本文档
|
||||
|
||||
ykt-wx-app/utils/
|
||||
└── api.js # API配置(已添加常用语接口)
|
||||
|
||||
ytk-customer-service/knowledgeBase/common-words/
|
||||
└── index.js # 后端常用语接口(已添加小程序接口)
|
||||
```
|
||||
|
||||
## API接口
|
||||
|
||||
### 1. 获取常用语列表
|
||||
```javascript
|
||||
api('getCommonPhrases', { corpId })
|
||||
```
|
||||
|
||||
### 2. 保存常用语
|
||||
```javascript
|
||||
api('saveCommonPhrase', {
|
||||
id, // 可选,更新时传入
|
||||
corpId,
|
||||
userId,
|
||||
categoryId,
|
||||
content
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 删除常用语
|
||||
```javascript
|
||||
api('deleteCommonPhrase', {
|
||||
id,
|
||||
corpId
|
||||
})
|
||||
```
|
||||
|
||||
### 4. 获取分类列表
|
||||
```javascript
|
||||
api('getCommonPhraseCategories', { corpId })
|
||||
```
|
||||
|
||||
### 5. 保存分类
|
||||
```javascript
|
||||
api('saveCommonPhraseCategory', {
|
||||
corpId,
|
||||
userId,
|
||||
name
|
||||
})
|
||||
```
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. 用户在聊天页面点击输入框右侧的"+"按钮
|
||||
2. 在弹出的功能面板中点击"常用语"
|
||||
3. 跳转到常用语列表页面
|
||||
4. 可以切换分类查看不同类型的常用语
|
||||
5. 点击任意常用语,自动返回聊天页面并发送该内容
|
||||
6. 在编辑模式下可以添加、编辑、删除常用语
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 常用语功能需要用户已登录并有有效的corpId
|
||||
2. 如果后端API调用失败,会使用本地模拟数据
|
||||
3. 常用语内容最多500字
|
||||
4. 分类名称最多6个字
|
||||
201
pages/message/article-detail.vue
Normal file
201
pages/message/article-detail.vue
Normal file
@ -0,0 +1,201 @@
|
||||
<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>
|
||||
588
pages/message/article-list.vue
Normal file
588
pages/message/article-list.vue
Normal file
@ -0,0 +1,588 @@
|
||||
<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>
|
||||
@ -136,7 +136,7 @@ $primary-color: #0877F1;
|
||||
|
||||
.message-list {
|
||||
padding: 0 16rpx;
|
||||
padding-bottom: 20rpx;
|
||||
padding-bottom: 60rpx; /* 增加底部内边距,防止被小程序底部横线遮挡 */
|
||||
}
|
||||
|
||||
.message-item {
|
||||
@ -350,10 +350,10 @@ $primary-color: #0877F1;
|
||||
flex: 1;
|
||||
padding: 0 46rpx;
|
||||
background-color: #f3f5fa;
|
||||
border-radius: 50rpx;
|
||||
border-radius: 20rpx;
|
||||
margin: 0 16rpx;
|
||||
font-size: 28rpx;
|
||||
height: 96rpx;
|
||||
height: 80rpx;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
@ -372,8 +372,8 @@ $primary-color: #0877F1;
|
||||
justify-content: flex-start;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
padding: 20rpx 0 8rpx 60rpx;
|
||||
gap: 40rpx;
|
||||
padding: 20rpx 0 40rpx 60rpx;
|
||||
gap: 40rpx 50rpx;
|
||||
flex-wrap: wrap;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
@ -1248,4 +1248,113 @@ $primary-color: #0877F1;
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-10rpx); }
|
||||
}
|
||||
}
|
||||
|
||||
/* 文章卡片样式 */
|
||||
.article-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
max-width: 500rpx;
|
||||
box-shadow: none;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 20rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.article-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.article-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.article-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 文章卡片在不同消息流中的样式 */
|
||||
.message-right .article-card {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.message-left .article-card {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 问卷卡片样式 */
|
||||
.survey-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
max-width: 500rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.survey-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 20rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.survey-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.survey-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.survey-image {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 问卷卡片在不同消息流中的样式 */
|
||||
.message-right .survey-card {
|
||||
background-color: #e8f4ff;
|
||||
}
|
||||
|
||||
.message-left .survey-card {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
@ -5,11 +5,11 @@
|
||||
<view class="category-sidebar">
|
||||
<scroll-view class="category-list" scroll-y>
|
||||
<view
|
||||
v-for="category in categories"
|
||||
v-for="category in allCategories"
|
||||
:key="category.id"
|
||||
class="category-item"
|
||||
:class="{ active: currentCategory === category.id }"
|
||||
@click="switchCategory(category.id)"
|
||||
@click="handleCategoryClick(category)"
|
||||
@longpress="handleCategoryLongPress(category)"
|
||||
>
|
||||
<view class="category-content">
|
||||
@ -23,26 +23,16 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="add-category-item" @click="showAddCategoryDialog">
|
||||
<text class="plus-icon">+</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 新增分类按钮(固定在底部) -->
|
||||
<view class="add-category-footer" @click="showAddCategoryDialog">
|
||||
<text class="add-category-text">新增分类</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右侧常用语列表 -->
|
||||
<view class="phrases-container">
|
||||
<!-- 顶部操作栏 -->
|
||||
<view class="action-bar">
|
||||
<view class="add-phrase-btn" @click="showAddPhraseDialog">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加快捷回复</text>
|
||||
</view>
|
||||
<view class="edit-btn" @click="toggleEditMode">
|
||||
<text class="edit-text">{{ isEditMode ? "完成" : "编辑" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view class="phrases-list" scroll-y>
|
||||
<view
|
||||
v-for="phrase in currentPhrases"
|
||||
@ -66,9 +56,20 @@
|
||||
|
||||
<view v-if="currentPhrases.length === 0" class="empty-state">
|
||||
<text class="empty-text">暂无常用语</text>
|
||||
<text class="empty-hint">点击上方按钮添加常用语</text>
|
||||
<text class="empty-hint">点击下方按钮添加常用语</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部操作栏(页脚) -->
|
||||
<view class="footer-action-bar">
|
||||
<view class="add-phrase-btn" @click="showAddPhraseDialog">
|
||||
<text class="add-icon">+</text>
|
||||
<text class="add-text">添加快捷回复</text>
|
||||
</view>
|
||||
<view class="edit-btn" @click="toggleEditMode">
|
||||
<text class="edit-text">{{ isEditMode ? "完成" : "编辑" }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@ -108,7 +109,9 @@
|
||||
>
|
||||
<view class="popup-content category-popup" @click.stop>
|
||||
<view class="popup-header">
|
||||
<text class="popup-title">新建分类</text>
|
||||
<text class="popup-title">{{
|
||||
editingCategory ? "编辑分类" : "新建分类"
|
||||
}}</text>
|
||||
<view class="popup-close" @click="closeCategoryPopup">
|
||||
<text class="close-icon">×</text>
|
||||
</view>
|
||||
@ -117,11 +120,12 @@
|
||||
v-model="categoryForm.name"
|
||||
class="category-input"
|
||||
placeholder="请输入分类名,最多6个字"
|
||||
maxlength="6"
|
||||
/>
|
||||
<view class="popup-actions">
|
||||
<button class="cancel-btn" @click="closeCategoryPopup">取消</button>
|
||||
<button class="confirm-btn" @click="saveCategory">确认添加</button>
|
||||
<button class="confirm-btn" @click="saveCategory">
|
||||
{{ editingCategory ? "保存" : "确认添加" }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -130,7 +134,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import api from "@/utils/api";
|
||||
import useAccountStore from "@/store/account";
|
||||
|
||||
// 获取 store
|
||||
const { doctorInfo } = storeToRefs(useAccountStore());
|
||||
|
||||
// 获取系统状态栏高度
|
||||
const statusBarHeight = ref(0);
|
||||
@ -138,21 +147,64 @@ const statusBarHeight = ref(0);
|
||||
// 编辑模式
|
||||
const isEditMode = ref(false);
|
||||
|
||||
// 分类数据
|
||||
const categories = ref([
|
||||
{ id: "visit", name: "文字随访", deletable: false },
|
||||
{ id: "voice", name: "语音随访", deletable: false },
|
||||
{ id: "common", name: "常用回复", deletable: false },
|
||||
]);
|
||||
// 默认分类(虚拟分类,不存在于数据库)
|
||||
const DEFAULT_CATEGORY = {
|
||||
id: "__default__",
|
||||
name: "默认",
|
||||
sort: -1,
|
||||
deletable: false,
|
||||
type: "personal",
|
||||
};
|
||||
|
||||
const currentCategory = ref("common");
|
||||
// 分类数据(初始为空,从后台加载)
|
||||
const categories = ref([]);
|
||||
|
||||
// 机构分类数据
|
||||
const corpCategories = ref([]);
|
||||
|
||||
// 所有分类(包含默认分类 + 个人分类 + 机构分类)
|
||||
const allCategories = computed(() => {
|
||||
// 个人分类
|
||||
const personalCats = [DEFAULT_CATEGORY, ...categories.value];
|
||||
|
||||
// 机构分类(标记为只读)
|
||||
const corpCats = corpCategories.value.map(cat => ({
|
||||
...cat,
|
||||
type: "corp",
|
||||
deletable: false,
|
||||
isCorpCategory: true, // 标记为机构分类
|
||||
}));
|
||||
|
||||
return [...personalCats, ...corpCats];
|
||||
});
|
||||
|
||||
const currentCategory = ref(DEFAULT_CATEGORY.id);
|
||||
|
||||
// 常用语数据
|
||||
const phrases = ref([]);
|
||||
|
||||
// 机构常用语数据
|
||||
const corpPhrases = ref([]);
|
||||
|
||||
// 当前分类的常用语
|
||||
const currentPhrases = computed(() => {
|
||||
return phrases.value.filter((p) => p.categoryId === currentCategory.value);
|
||||
// 获取当前分类信息
|
||||
const currentCat = allCategories.value.find(c => c.id === currentCategory.value);
|
||||
|
||||
if (currentCategory.value === DEFAULT_CATEGORY.id) {
|
||||
// 默认分类显示没有 categoryId 或 categoryId 为空的个人常用语
|
||||
return phrases.value.filter((p) => !p.categoryId);
|
||||
}
|
||||
|
||||
// 如果是机构分类,返回机构常用语
|
||||
if (currentCat?.isCorpCategory) {
|
||||
return corpPhrases.value.filter((p) => p.categoryId === currentCategory.value);
|
||||
}
|
||||
|
||||
// 个人分类,返回个人常用语
|
||||
const filtered = phrases.value.filter((p) => p.categoryId === currentCategory.value);
|
||||
console.log("当前分类:", currentCategory.value);
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// 弹窗显示状态
|
||||
@ -169,6 +221,7 @@ const categoryForm = ref({
|
||||
});
|
||||
|
||||
const editingPhrase = ref(null);
|
||||
const editingCategory = ref(null);
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
@ -185,6 +238,24 @@ const switchCategory = (categoryId) => {
|
||||
currentCategory.value = categoryId;
|
||||
};
|
||||
|
||||
// 处理分类点击(编辑模式下编辑分类名称)
|
||||
const handleCategoryClick = (category) => {
|
||||
if (isEditMode.value && category.deletable) {
|
||||
// 编辑模式下,点击可编辑的分类,弹出编辑框
|
||||
editCategory(category);
|
||||
} else {
|
||||
// 非编辑模式,切换分类
|
||||
switchCategory(category.id);
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑分类
|
||||
const editCategory = (category) => {
|
||||
editingCategory.value = category;
|
||||
categoryForm.value.name = category.name;
|
||||
showCategoryPopup.value = true;
|
||||
};
|
||||
|
||||
// 处理常用语点击
|
||||
const handlePhraseClick = (phrase) => {
|
||||
if (isEditMode.value) {
|
||||
@ -220,63 +291,79 @@ const editPhrase = (phrase) => {
|
||||
const savePhrase = async () => {
|
||||
if (!phraseForm.value.content.trim()) {
|
||||
uni.showToast({
|
||||
title: '请输入内容',
|
||||
icon: 'none'
|
||||
title: "请输入内容",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const corpId = uni.getStorageSync('corpId');
|
||||
const userId = uni.getStorageSync('userId');
|
||||
|
||||
if (!corpId || !userId) {
|
||||
if (!doctorInfo.value) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
title: "请先登录",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api('saveCommonPhrase', {
|
||||
const corpId = doctorInfo.value.corpId;
|
||||
const userId = doctorInfo.value.userid;
|
||||
|
||||
// 如果是默认分类,categoryId 设置为 null
|
||||
let categoryId = editingPhrase.value?.categoryId || currentCategory.value;
|
||||
if (categoryId === DEFAULT_CATEGORY.id) {
|
||||
categoryId = null;
|
||||
}
|
||||
|
||||
const result = await api("savePersonalPhrase", {
|
||||
id: editingPhrase.value?.id,
|
||||
categoryId: editingPhrase.value?.categoryId || currentCategory.value,
|
||||
categoryId: categoryId,
|
||||
content: phraseForm.value.content,
|
||||
corpId,
|
||||
userId
|
||||
userId,
|
||||
});
|
||||
|
||||
if (result.code === 200) {
|
||||
console.log("保存常用语返回结果:", result);
|
||||
|
||||
if (result.success) {
|
||||
if (editingPhrase.value) {
|
||||
// 更新
|
||||
const index = phrases.value.findIndex(p => p.id === editingPhrase.value.id);
|
||||
const index = phrases.value.findIndex(
|
||||
(p) => p.id === editingPhrase.value.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
phrases.value[index].content = phraseForm.value.content;
|
||||
// 如果是默认分类,categoryId 设置为 null
|
||||
const newCategoryId = editingPhrase.value.categoryId || currentCategory.value;
|
||||
phrases.value[index].categoryId = newCategoryId === DEFAULT_CATEGORY.id ? null : newCategoryId;
|
||||
}
|
||||
} else {
|
||||
// 新增
|
||||
console.log("新增常用语,返回数据:", result.data);
|
||||
console.log("当前分类ID:", currentCategory.value);
|
||||
if (result.data) {
|
||||
phrases.value.push(result.data);
|
||||
console.log("添加后的 phrases:", phrases.value);
|
||||
}
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: editingPhrase.value ? '保存成功' : '添加成功',
|
||||
icon: 'success'
|
||||
title: editingPhrase.value ? "保存成功" : "添加成功",
|
||||
icon: "success",
|
||||
});
|
||||
|
||||
closePopup();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || '操作失败',
|
||||
icon: 'none'
|
||||
title: result.message || "操作失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存常用语失败:', error);
|
||||
console.error("保存常用语失败:", error);
|
||||
uni.showToast({
|
||||
title: '操作失败',
|
||||
icon: 'none'
|
||||
title: "操作失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -289,20 +376,31 @@ const deletePhrase = (phrase) => {
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await api("deleteCommonPhrase", {
|
||||
const corpId = doctorInfo.value.corpId;
|
||||
const userId = doctorInfo.value.userid;
|
||||
|
||||
const result = await api("deletePersonalPhrase", {
|
||||
id: phrase.id,
|
||||
corpId: uni.getStorageSync("corpId"),
|
||||
corpId,
|
||||
userId,
|
||||
});
|
||||
|
||||
const index = phrases.value.findIndex((p) => p.id === phrase.id);
|
||||
if (index !== -1) {
|
||||
phrases.value.splice(index, 1);
|
||||
if (result.success) {
|
||||
const index = phrases.value.findIndex((p) => p.id === phrase.id);
|
||||
if (index !== -1) {
|
||||
phrases.value.splice(index, 1);
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: "删除成功",
|
||||
icon: "success",
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || "删除失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: "删除成功",
|
||||
icon: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("删除常用语失败:", error);
|
||||
uni.showToast({
|
||||
@ -317,12 +415,13 @@ const deletePhrase = (phrase) => {
|
||||
|
||||
// 显示添加分类弹窗
|
||||
const showAddCategoryDialog = () => {
|
||||
editingCategory.value = null;
|
||||
categoryForm.value.name = "";
|
||||
showCategoryPopup.value = true;
|
||||
};
|
||||
|
||||
// 保存分类
|
||||
const saveCategory = () => {
|
||||
const saveCategory = async () => {
|
||||
if (!categoryForm.value.name.trim()) {
|
||||
uni.showToast({
|
||||
title: "请输入分类名",
|
||||
@ -331,18 +430,66 @@ const saveCategory = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
categories.value.push({
|
||||
id: `category_${Date.now()}`,
|
||||
name: categoryForm.value.name,
|
||||
deletable: true, // 用户添加的分类可删除
|
||||
});
|
||||
if (categoryForm.value.name.length > 6) {
|
||||
uni.showToast({
|
||||
title: "输入内容超过6个字",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: "添加成功",
|
||||
icon: "success",
|
||||
});
|
||||
try {
|
||||
const corpId = doctorInfo.value.corpId;
|
||||
const userId = doctorInfo.value.userid;
|
||||
|
||||
closeCategoryPopup();
|
||||
const result = await api("savePersonalPhraseCategory", {
|
||||
id: editingCategory.value?.id, // 如果是编辑,传入 id
|
||||
name: categoryForm.value.name,
|
||||
corpId,
|
||||
userId,
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
if (editingCategory.value) {
|
||||
// 更新分类
|
||||
const index = categories.value.findIndex(
|
||||
(c) => c.id === editingCategory.value.id
|
||||
);
|
||||
if (index !== -1) {
|
||||
categories.value[index].name = result.data.name;
|
||||
}
|
||||
uni.showToast({
|
||||
title: "修改成功",
|
||||
icon: "success",
|
||||
});
|
||||
} else {
|
||||
// 新增分类
|
||||
categories.value.push({
|
||||
id: result.data.id,
|
||||
name: result.data.name,
|
||||
sort: result.data.sort,
|
||||
deletable: result.data.deletable,
|
||||
});
|
||||
uni.showToast({
|
||||
title: "添加成功",
|
||||
icon: "success",
|
||||
});
|
||||
}
|
||||
|
||||
closeCategoryPopup();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || "操作失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("保存分类失败:", error);
|
||||
uni.showToast({
|
||||
title: "操作失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 长按分类
|
||||
@ -361,33 +508,54 @@ const handleCategoryLongPress = (category) => {
|
||||
};
|
||||
|
||||
// 删除分类
|
||||
const deleteCategory = (category) => {
|
||||
const deleteCategory = async (category) => {
|
||||
if (!category.deletable) {
|
||||
uni.showToast({
|
||||
title: "默认分类不可删除",
|
||||
title: "该分类不可删除",
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 删除分类下的所有常用语
|
||||
phrases.value = phrases.value.filter((p) => p.categoryId !== category.id);
|
||||
try {
|
||||
const corpId = doctorInfo.value.corpId;
|
||||
const userId = doctorInfo.value.userid;
|
||||
|
||||
// 删除分类
|
||||
const index = categories.value.findIndex((c) => c.id === category.id);
|
||||
if (index !== -1) {
|
||||
categories.value.splice(index, 1);
|
||||
const result = await api("deletePersonalPhraseCategory", {
|
||||
id: category.id,
|
||||
corpId,
|
||||
userId,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// 删除分类
|
||||
const index = categories.value.findIndex((c) => c.id === category.id);
|
||||
if (index !== -1) {
|
||||
categories.value.splice(index, 1);
|
||||
}
|
||||
|
||||
// 如果删除的是当前分类,切换到默认分类
|
||||
if (currentCategory.value === category.id) {
|
||||
currentCategory.value = DEFAULT_CATEGORY.id;
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: "删除成功",
|
||||
icon: "success",
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || "删除失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("删除分类失败:", error);
|
||||
uni.showToast({
|
||||
title: "删除失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
|
||||
// 如果删除的是当前分类,切换到第一个分类
|
||||
if (currentCategory.value === category.id && categories.value.length > 0) {
|
||||
currentCategory.value = categories.value[0].id;
|
||||
}
|
||||
|
||||
uni.showToast({
|
||||
title: "删除成功",
|
||||
icon: "success",
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
@ -402,104 +570,46 @@ const closeCategoryPopup = () => {
|
||||
// 加载常用语数据
|
||||
const loadPhrases = async () => {
|
||||
try {
|
||||
const corpId = uni.getStorageSync('corpId');
|
||||
const userId = uni.getStorageSync('userId');
|
||||
|
||||
if (!corpId) {
|
||||
uni.showToast({
|
||||
title: '请先登录',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api('getCommonPhrases', {
|
||||
const corpId = doctorInfo.value.corpId;
|
||||
const userId = doctorInfo.value.userid;
|
||||
|
||||
const result = await api("getPersonalPhrases", {
|
||||
corpId,
|
||||
userId
|
||||
userId,
|
||||
});
|
||||
|
||||
if (result.code === 200) {
|
||||
|
||||
if (result.success && result.data) {
|
||||
// 更新常用语列表
|
||||
if (Array.isArray(result.data)) {
|
||||
phrases.value = result.data;
|
||||
if (Array.isArray(result.data.phrases)) {
|
||||
phrases.value = result.data.phrases;
|
||||
}
|
||||
|
||||
// 更新分类列表(合并默认分类和后台分类)
|
||||
if (Array.isArray(result.categories)) {
|
||||
const backendCategories = result.categories.map(cat => ({
|
||||
id: cat.id,
|
||||
name: cat.name,
|
||||
deletable: true // 后台分类都可以删除
|
||||
}));
|
||||
|
||||
// 如果后台有分类,使用后台分类,否则使用默认分类
|
||||
if (backendCategories.length > 0) {
|
||||
categories.value = backendCategories;
|
||||
// 设置当前分类为第一个
|
||||
if (!currentCategory.value || !categories.value.find(c => c.id === currentCategory.value)) {
|
||||
currentCategory.value = categories.value[0].id;
|
||||
// 更新分类列表
|
||||
if (Array.isArray(result.data.categories)) {
|
||||
categories.value = result.data.categories;
|
||||
|
||||
// 如果当前分类不存在,保持默认分类
|
||||
if (currentCategory.value && currentCategory.value !== DEFAULT_CATEGORY.id) {
|
||||
const categoryExists = categories.value.find((c) => c.id === currentCategory.value);
|
||||
if (!categoryExists) {
|
||||
currentCategory.value = DEFAULT_CATEGORY.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || "加载失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载常用语失败:', error);
|
||||
console.error("加载常用语失败:", error);
|
||||
uni.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
title: "加载失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟数据
|
||||
const getMockPhrases = () => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
categoryId: "common",
|
||||
content:
|
||||
"想要买草药自己熬还是配方颗粒直接开水冲着喝?线上购买全国包邮,一二线城市一般1-2天送到,其他城市可能慢一些。",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
categoryId: "common",
|
||||
content: "以前得过什么病吗?做过什么检查吗?有检查结果吗?详细说一下。",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
categoryId: "common",
|
||||
content: "把我之前给您开的处方,拍个照上传给我看一下,拍招清楚一点。",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
categoryId: "common",
|
||||
content: "服药期间要慎炸身体,少吃油炸等辛辣食物,按时作息,精神放松。",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
categoryId: "common",
|
||||
content:
|
||||
"煎药最好用砂锅,避免用铁锅,煎药前用冷水浸泡30分钟,水量超过药材2-5厘米,一般中药煎2次,第1次煮沸后再煎20分钟就可以倒出来了,再次加水煮沸后再煎30分钟后倒出,将2次倒出的药混合在一起,就可以按照医嘱用药了。",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
categoryId: "common",
|
||||
content:
|
||||
"如果在线上买药,点击整体治疗方案,选择首选处方,划价取药,填写地址后去付款即可。",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
categoryId: "voice",
|
||||
content: "想要买草药自己熬还是配方颗粒直接开水冲着喝?",
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
categoryId: "visit",
|
||||
content: "您好,我是您的健康管理师,请问有什么可以帮助您的吗?",
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// 获取系统信息
|
||||
const systemInfo = uni.getSystemInfoSync();
|
||||
@ -511,7 +621,7 @@ onMounted(() => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
// 使用项目主题色
|
||||
$primary-color: #0877f1;
|
||||
$primary-color: #0877F1;
|
||||
$primary-light: #e8f3ff;
|
||||
$primary-gradient-start: #1b5cc8;
|
||||
$primary-gradient-end: #0877f1;
|
||||
@ -600,34 +710,29 @@ $primary-gradient-end: #0877f1;
|
||||
}
|
||||
}
|
||||
|
||||
// 标题栏
|
||||
.action-bar {
|
||||
// 底部操作栏(页脚)
|
||||
.footer-action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin: 24rpx 24rpx 0;
|
||||
.edit-btn {
|
||||
padding: 16rpx 32rpx;
|
||||
.edit-text {
|
||||
font-size: 28rpx;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
padding: 24rpx;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
box-shadow: 0 -2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
|
||||
.add-phrase-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 16rpx 24rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
$primary-gradient-end 0%,
|
||||
$primary-gradient-start 100%
|
||||
);
|
||||
border-radius: 40rpx;
|
||||
border-radius: 30rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(8, 119, 241, 0.2);
|
||||
|
||||
.add-icon {
|
||||
@ -643,6 +748,19 @@ $primary-gradient-end: #0877f1;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
padding: 20rpx 32rpx;
|
||||
// background-color: $primary-light;
|
||||
border-radius: 20rpx;
|
||||
// box-shadow: 0 2rpx 8rpx rgba(8, 119, 241, 0.15);
|
||||
|
||||
.edit-text {
|
||||
font-size: 28rpx;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
@ -653,17 +771,21 @@ $primary-gradient-end: #0877f1;
|
||||
|
||||
// 左侧分类栏
|
||||
.category-sidebar {
|
||||
width: 180rpx;
|
||||
width: 210rpx;
|
||||
background-color: #fff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.category-list {
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
height: 0; // 配合 flex: 1 实现固定高度
|
||||
overflow-y: auto; // 支持垂直滚动
|
||||
}
|
||||
|
||||
.category-item {
|
||||
position: relative;
|
||||
padding: 40rpx 16rpx;
|
||||
padding: 20rpx 16rpx;
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
transition: all 0.3s;
|
||||
@ -690,10 +812,10 @@ $primary-gradient-end: #0877f1;
|
||||
|
||||
.delete-badge {
|
||||
position: absolute;
|
||||
top: -12rpx;
|
||||
right: 8rpx;
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
top: -20rpx;
|
||||
right: 2rpx;
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
background-color: #ff3b30;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
@ -702,7 +824,7 @@ $primary-gradient-end: #0877f1;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 59, 48, 0.3);
|
||||
|
||||
.delete-icon {
|
||||
font-size: 28rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
font-weight: 300;
|
||||
@ -710,27 +832,25 @@ $primary-gradient-end: #0877f1;
|
||||
}
|
||||
}
|
||||
|
||||
.add-category-item {
|
||||
// 新增分类按钮(固定在底部)
|
||||
.add-category-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40rpx 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: $primary-color;
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
box-shadow: 0 -2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 18rpx;
|
||||
|
||||
.plus-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
font-weight: 300;
|
||||
.add-category-text {
|
||||
font-size: 26rpx;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $primary-light;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -745,7 +865,7 @@ $primary-gradient-end: #0877f1;
|
||||
|
||||
.phrases-list {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
padding: 10rpx;
|
||||
height: 0; // 配合 flex: 1 实现固定高度
|
||||
overflow-y: auto; // 支持垂直滚动
|
||||
|
||||
@ -894,7 +1014,7 @@ $primary-gradient-end: #0877f1;
|
||||
|
||||
.category-input {
|
||||
width: 100%;
|
||||
padding: 20rpx;
|
||||
padding: 0 20rpx; // 只设置左右 padding,去掉上下 padding
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
background-color: #f8f9fa;
|
||||
@ -903,6 +1023,7 @@ $primary-gradient-end: #0877f1;
|
||||
margin-bottom: 32rpx;
|
||||
box-sizing: border-box;
|
||||
height: 100rpx;
|
||||
line-height: 100rpx; // 行高等于高度,实现垂直居中
|
||||
}
|
||||
|
||||
.popup-actions {
|
||||
|
||||
@ -73,10 +73,13 @@ const props = defineProps({
|
||||
patientInfo: { type: Object, default: () => ({}) },
|
||||
chatRoomBusiness: { type: Object, default: () => ({}) },
|
||||
formatTime: { type: Function, required: true },
|
||||
groupId: { type: String, default: '' },
|
||||
userId: { type: String, default: '' },
|
||||
corpId: { type: String, default: '' },
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["messageSent", "scrollToBottom"]);
|
||||
const emit = defineEmits(["messageSent", "scrollToBottom", "endConsult"]);
|
||||
|
||||
// 输入相关状态
|
||||
const inputText = ref("");
|
||||
@ -361,6 +364,38 @@ const goToCommonPhrases = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到宣教文章页面
|
||||
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 },
|
||||
{
|
||||
@ -376,17 +411,17 @@ const morePanelButtons = [
|
||||
{
|
||||
text: "宣教",
|
||||
icon: "/static/icon/xuanjiaowenzhang.png",
|
||||
action: showImagePicker,
|
||||
action: goToArticleList,
|
||||
},
|
||||
{
|
||||
text: "问卷",
|
||||
icon: "/static/icon/zhaopian.png",
|
||||
action: showImagePicker,
|
||||
icon: "/static/icon/wenjuan.png",
|
||||
action: goToSurveyList,
|
||||
},
|
||||
{
|
||||
text: "结束问诊",
|
||||
icon: "/static/icon/jieshuzixun.png",
|
||||
action: showImagePicker,
|
||||
action: handleEndConsult,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
88
pages/message/components/consult-accept.vue
Normal file
88
pages/message/components/consult-accept.vue
Normal file
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<view class="consult-accept-container">
|
||||
<view class="accept-card">
|
||||
<view class="accept-content">
|
||||
<text class="accept-text">患者已发起咨询申请,请及时接诊</text>
|
||||
</view>
|
||||
<view class="accept-actions">
|
||||
<button class="btn-cancel" @click="handleReject">暂不接受</button>
|
||||
<button class="btn-confirm" @click="handleAccept">接受咨询</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from 'vue';
|
||||
|
||||
const emit = defineEmits(['accept', 'reject']);
|
||||
|
||||
const handleAccept = () => {
|
||||
emit('accept');
|
||||
};
|
||||
|
||||
const handleReject = () => {
|
||||
emit('reject');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.consult-accept-container {
|
||||
width: 100%;
|
||||
padding: 20rpx 32rpx;
|
||||
background-color: #f5f5f5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.accept-card {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.accept-content {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.accept-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.accept-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.btn-cancel,
|
||||
.btn-confirm {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-cancel::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background-color: #1677ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-confirm::after {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@ -63,8 +63,46 @@
|
||||
</view>
|
||||
|
||||
<!-- 自定义消息卡片 -->
|
||||
<!-- <template v-else-if="message.type === 'TIMCustomElem'">
|
||||
<template v-else-if="message.type === 'TIMCustomElem'">
|
||||
<!-- 文章消息 -->
|
||||
<view
|
||||
v-if="getCustomMessageType(message) === 'article'"
|
||||
class="article-card"
|
||||
@click="handleArticleClick(message)"
|
||||
>
|
||||
<view class="article-content">
|
||||
<view class="article-title">{{ getArticleData(message).title }}</view>
|
||||
<view class="article-desc">{{ getArticleData(message).desc }}</view>
|
||||
</view>
|
||||
<image
|
||||
v-if="getArticleData(message).imgUrl"
|
||||
class="article-image"
|
||||
:src="getArticleData(message).imgUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 问卷消息 -->
|
||||
<view
|
||||
v-else-if="getCustomMessageType(message) === 'survey'"
|
||||
class="survey-card"
|
||||
@click="handleSurveyClick(message)"
|
||||
>
|
||||
<view class="survey-content">
|
||||
<view class="survey-title">{{ getSurveyData(message).title }}</view>
|
||||
<view class="survey-desc">{{ getSurveyData(message).desc }}</view>
|
||||
</view>
|
||||
<image
|
||||
v-if="getSurveyData(message).imgUrl"
|
||||
class="survey-image"
|
||||
:src="getSurveyData(message).imgUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 其他自定义消息 -->
|
||||
<!-- <view
|
||||
v-else
|
||||
class="card-avatar-row"
|
||||
@click="() => console.log('点击头像', message)"
|
||||
>
|
||||
@ -74,8 +112,8 @@
|
||||
:flow="message.flow"
|
||||
@viewDetail="$emit('viewDetail', $event)"
|
||||
/>
|
||||
</view>
|
||||
</template> -->
|
||||
</view> -->
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -163,6 +201,80 @@ const getVoiceStyle = (duration) => {
|
||||
width: width + "rpx",
|
||||
};
|
||||
};
|
||||
|
||||
// 获取自定义消息类型
|
||||
const getCustomMessageType = (message) => {
|
||||
try {
|
||||
if (message.payload && message.payload.data) {
|
||||
const data = JSON.parse(message.payload.data);
|
||||
return data.type || "";
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析自定义消息失败:", error);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// 获取文章数据
|
||||
const getArticleData = (message) => {
|
||||
try {
|
||||
if (message.payload && message.payload.data) {
|
||||
const data = JSON.parse(message.payload.data);
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析文章数据失败:", error);
|
||||
}
|
||||
return {
|
||||
title: "宣教文章",
|
||||
desc: "宣教文章",
|
||||
url: "",
|
||||
imgUrl: "",
|
||||
};
|
||||
};
|
||||
|
||||
// 处理文章点击
|
||||
const handleArticleClick = (message) => {
|
||||
const { articleId } = getArticleData(message);
|
||||
uni.navigateTo({
|
||||
url: `/pages/message/article-detail?id=${articleId}`,
|
||||
});
|
||||
};
|
||||
|
||||
// 获取问卷数据
|
||||
const getSurveyData = (message) => {
|
||||
try {
|
||||
if (message.payload && message.payload.data) {
|
||||
const data = JSON.parse(message.payload.data);
|
||||
return {
|
||||
title: data.title || "填写问卷",
|
||||
desc: data.desc || "请填写问卷",
|
||||
url: data.url || "",
|
||||
imgUrl: data.imgUrl || "",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析问卷数据失败:", error);
|
||||
}
|
||||
return {
|
||||
title: "填写问卷",
|
||||
desc: "请填写问卷",
|
||||
url: "",
|
||||
imgUrl: "",
|
||||
};
|
||||
};
|
||||
|
||||
// 处理问卷点击
|
||||
const handleSurveyClick = (message) => {
|
||||
const surveyData = getSurveyData(message);
|
||||
if (surveyData.url) {
|
||||
// 跳转到问卷填写页面或打开外部链接
|
||||
console.log("打开问卷:", surveyData.url);
|
||||
// uni.navigateTo({
|
||||
// url: `/pages/survey/fill?url=${encodeURIComponent(surveyData.url)}`
|
||||
// });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
297
pages/message/components/reject-reason-modal.vue
Normal file
297
pages/message/components/reject-reason-modal.vue
Normal file
@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<view v-if="visible" class="modal-overlay" @click="handleCancel">
|
||||
<view class="modal-container" @click.stop>
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">选择暂不接受咨询原因,供患者知晓</text>
|
||||
</view>
|
||||
|
||||
<view class="modal-content">
|
||||
<!-- 预设原因选项 -->
|
||||
<view class="reason-options">
|
||||
<view
|
||||
v-for="(option, index) in reasonOptions"
|
||||
:key="index"
|
||||
class="reason-option"
|
||||
:class="{ active: selectedReason === option }"
|
||||
@click="selectReason(option)"
|
||||
>
|
||||
<text class="option-text">{{ option }}</text>
|
||||
<view class="option-icon" :class="{ checked: selectedReason === option }">
|
||||
<text v-if="selectedReason === option" class="check-mark">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义输入选项 -->
|
||||
<view
|
||||
class="reason-option custom-option"
|
||||
:class="{ active: isCustomInput }"
|
||||
@click="selectCustomInput"
|
||||
>
|
||||
<text class="option-text">填写拒诊理由</text>
|
||||
<view class="option-icon arrow">
|
||||
<text class="arrow-icon">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义输入框 -->
|
||||
<view v-if="isCustomInput" class="custom-input-container">
|
||||
<textarea
|
||||
class="custom-textarea"
|
||||
v-model="customReason"
|
||||
placeholder="请输入理由,供患者知晓"
|
||||
maxlength="200"
|
||||
:auto-height="true"
|
||||
/>
|
||||
<text class="char-count">{{ customReason.length }}/200</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="modal-footer">
|
||||
<button class="btn-cancel" @click="handleCancel">取消</button>
|
||||
<button class="btn-confirm" @click="handleConfirm">确定</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirm', 'cancel']);
|
||||
|
||||
// 预设的拒绝原因选项
|
||||
const reasonOptions = ref([
|
||||
'临时有紧急事务',
|
||||
'患者病情复杂,需线下就诊',
|
||||
]);
|
||||
|
||||
const selectedReason = ref('');
|
||||
const isCustomInput = ref(false);
|
||||
const customReason = ref('');
|
||||
|
||||
// 选择预设原因
|
||||
const selectReason = (option) => {
|
||||
selectedReason.value = option;
|
||||
isCustomInput.value = false;
|
||||
customReason.value = '';
|
||||
};
|
||||
|
||||
// 选择自定义输入
|
||||
const selectCustomInput = () => {
|
||||
selectedReason.value = '';
|
||||
isCustomInput.value = true;
|
||||
};
|
||||
|
||||
// 取消
|
||||
const handleCancel = () => {
|
||||
// 重置状态
|
||||
selectedReason.value = '';
|
||||
isCustomInput.value = false;
|
||||
customReason.value = '';
|
||||
emit('cancel');
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handleConfirm = () => {
|
||||
let reason = '';
|
||||
|
||||
if (isCustomInput.value) {
|
||||
reason = customReason.value.trim();
|
||||
if (!reason) {
|
||||
uni.showToast({
|
||||
title: '请输入拒绝原因',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
reason = selectedReason.value;
|
||||
if (!reason) {
|
||||
uni.showToast({
|
||||
title: '请选择拒绝原因',
|
||||
icon: 'none',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
emit('confirm', reason);
|
||||
|
||||
// 重置状态
|
||||
selectedReason.value = '';
|
||||
isCustomInput.value = false;
|
||||
customReason.value = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 600rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 32rpx 32rpx 24rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 24rpx 32rpx;
|
||||
max-height: 600rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.reason-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.reason-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
background-color: #e6f4ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
}
|
||||
|
||||
.option-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.option-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #d9d9d9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.checked {
|
||||
background-color: #1677ff;
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
&.arrow {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.check-mark {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.custom-input-container {
|
||||
margin-top: 16rpx;
|
||||
padding: 16rpx;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.custom-textarea {
|
||||
width: 100%;
|
||||
min-height: 120rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
display: block;
|
||||
text-align: right;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx 32rpx 32rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.btn-cancel,
|
||||
.btn-confirm {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background-color: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-cancel::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background-color: #1677ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-confirm::after {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<view v-if="notifyText" class="notify-bar">
|
||||
<!-- <view v-if="notifyText" class="notify-bar">
|
||||
<view class="notify-line"></view>
|
||||
<view class="notify-text">{{ notifyText }}</view>
|
||||
<view class="notify-line"></view>
|
||||
</view>
|
||||
</view> -->
|
||||
<view class="system-message">
|
||||
<view class="system-text">{{ text }}</view>
|
||||
</view>
|
||||
@ -21,17 +21,111 @@ const props = defineProps({
|
||||
|
||||
const payload = computed(() => props.message?.payload || {});
|
||||
|
||||
const extension = computed(() => {
|
||||
// 解析系统消息内容
|
||||
const systemMessageData = computed(() => {
|
||||
try {
|
||||
return JSON.parse(payload.value.extension);
|
||||
// 尝试从 payload.data 解析系统消息
|
||||
if (payload.value.data) {
|
||||
const data = typeof payload.value.data === 'string'
|
||||
? JSON.parse(payload.value.data)
|
||||
: payload.value.data;
|
||||
|
||||
if (data.type === 'system_message') {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return {};
|
||||
console.error('解析系统消息失败:', e);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const text = computed(() => extension.value.patient || payload.value.data || '')
|
||||
// 解析扩展信息
|
||||
const extension = computed(() => {
|
||||
try {
|
||||
if (payload.value.extension) {
|
||||
return typeof payload.value.extension === 'string'
|
||||
? JSON.parse(payload.value.extension)
|
||||
: payload.value.extension;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析扩展信息失败:', e);
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const notifyText = computed(() => extension.value.notifyText || '')
|
||||
// 显示的文本内容
|
||||
const text = computed(() => {
|
||||
// 优先从系统消息数据中获取文本
|
||||
if (systemMessageData.value?.text) {
|
||||
return systemMessageData.value.text;
|
||||
}
|
||||
|
||||
// 根据消息类型返回默认文本
|
||||
if (systemMessageData.value?.messageType) {
|
||||
const messageType = systemMessageData.value.messageType;
|
||||
switch (messageType) {
|
||||
case 'consult_pending':
|
||||
return '患者已发起咨询申请,请及时接诊';
|
||||
case 'consult_accepted':
|
||||
return '医生已接诊';
|
||||
case 'consult_rejected':
|
||||
return '医生暂时无法接诊';
|
||||
case 'consult_ended':
|
||||
return '问诊已结束';
|
||||
case 'consult_timeout':
|
||||
return '问诊已超时';
|
||||
default:
|
||||
return systemMessageData.value.content || '[系统消息]';
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容旧格式:从 extension 中获取
|
||||
if (extension.value.patient) {
|
||||
return extension.value.patient;
|
||||
}
|
||||
|
||||
// 兼容旧格式:直接从 payload.data 获取
|
||||
if (payload.value.data && typeof payload.value.data === 'string') {
|
||||
// 如果 data 不是 JSON 格式,直接显示
|
||||
try {
|
||||
JSON.parse(payload.value.data);
|
||||
} catch {
|
||||
return payload.value.data;
|
||||
}
|
||||
}
|
||||
|
||||
return '[系统消息]';
|
||||
});
|
||||
|
||||
// 通知文本(红色提示)
|
||||
const notifyText = computed(() => {
|
||||
// 从扩展信息中获取通知文本
|
||||
if (extension.value.notifyText) {
|
||||
return extension.value.notifyText;
|
||||
}
|
||||
|
||||
// 根据系统消息类型显示不同的通知
|
||||
if (systemMessageData.value) {
|
||||
const messageType = systemMessageData.value.messageType;
|
||||
switch (messageType) {
|
||||
case 'consult_pending':
|
||||
return '待接诊';
|
||||
case 'consult_rejected':
|
||||
return '已拒绝';
|
||||
case 'consult_timeout':
|
||||
return '已超时';
|
||||
case 'consult_accepted':
|
||||
return '已接诊';
|
||||
case 'consult_ended':
|
||||
return '已结束';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@ -54,7 +54,9 @@
|
||||
<image
|
||||
v-if="message.flow === 'in'"
|
||||
class="doctor-msg-avatar"
|
||||
:src="chatMember[message.from]?.avatar || '/static/default-avatar.png'"
|
||||
:src="
|
||||
chatMember[message.from]?.avatar || '/static/default-avatar.png'
|
||||
"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
|
||||
@ -62,7 +64,9 @@
|
||||
<image
|
||||
v-if="message.flow === 'out'"
|
||||
class="user-msg-avatar"
|
||||
:src="chatMember[message.from]?.avatar || '/static/home/avatar.svg'"
|
||||
:src="
|
||||
chatMember[message.from]?.avatar || '/static/home/avatar.svg'
|
||||
"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
|
||||
@ -81,7 +85,7 @@
|
||||
}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 消息气泡 -->
|
||||
|
||||
<view class="message-bubble" :class="getBubbleClass(message)">
|
||||
<!-- 消息内容 -->
|
||||
<MessageTypes
|
||||
@ -108,14 +112,32 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 接受问诊组件 -->
|
||||
<ConsultAccept
|
||||
v-if="showConsultAccept"
|
||||
@accept="handleAcceptConsult"
|
||||
@reject="handleRejectConsult"
|
||||
/>
|
||||
|
||||
<!-- 拒绝原因对话框 -->
|
||||
<RejectReasonModal
|
||||
:visible="showRejectReasonModal"
|
||||
@confirm="handleRejectReasonConfirm"
|
||||
@cancel="handleRejectReasonCancel"
|
||||
/>
|
||||
|
||||
<!-- 聊天输入组件 -->
|
||||
<ChatInput
|
||||
v-if="!isEvaluationPopupOpen"
|
||||
v-if="!isEvaluationPopupOpen && !showConsultAccept"
|
||||
ref="chatInputRef"
|
||||
:timChatManager="timChatManager"
|
||||
:formatTime="formatTime"
|
||||
:groupId="chatInfo.conversationID ? chatInfo.conversationID.replace('GROUP', '') : ''"
|
||||
:userId="openid"
|
||||
:corpId="corpId"
|
||||
@scrollToBottom="() => scrollToBottom(true)"
|
||||
@messageSent="() => scrollToBottom(true)"
|
||||
@endConsult="handleEndConsult"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
@ -143,13 +165,20 @@ import {
|
||||
handleViewDetail,
|
||||
checkIMConnectionStatus,
|
||||
} from "@/utils/chat-utils.js";
|
||||
import { sendConsultRejectedMessage, endConsultation, sendArticleMessage } from "@/utils/api.js";
|
||||
import useGroupChat from "./hooks/use-group-chat";
|
||||
import MessageTypes from "./components/message-types.vue";
|
||||
import ChatInput from "./components/chat-input.vue";
|
||||
import SystemMessage from "./components/system-message.vue";
|
||||
import ConsultAccept from "./components/consult-accept.vue";
|
||||
import RejectReasonModal from "./components/reject-reason-modal.vue";
|
||||
|
||||
const timChatManager = globalTimChatManager;
|
||||
|
||||
// 获取环境变量
|
||||
const env = __VITE_ENV__;
|
||||
const corpId = env.MP_CORP_ID || '';
|
||||
|
||||
// 获取登录状态
|
||||
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
|
||||
const { initIMAfterLogin } = useAccountStore();
|
||||
@ -158,15 +187,12 @@ const { initIMAfterLogin } = useAccountStore();
|
||||
const chatInputRef = ref(null);
|
||||
|
||||
const groupId = ref("");
|
||||
const {
|
||||
chatMember,
|
||||
getGroupInfo
|
||||
} = useGroupChat(groupId);
|
||||
const { chatMember, getGroupInfo } = useGroupChat(groupId);
|
||||
|
||||
// 动态设置导航栏标题
|
||||
const updateNavigationTitle = () => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: "群聊"
|
||||
title: "群聊",
|
||||
});
|
||||
};
|
||||
|
||||
@ -180,6 +206,12 @@ const chatInfo = ref({
|
||||
// 评价弹窗状态
|
||||
const isEvaluationPopupOpen = ref(false);
|
||||
|
||||
// 接受问诊状态
|
||||
const showConsultAccept = ref(false);
|
||||
|
||||
// 拒绝原因对话框状态
|
||||
const showRejectReasonModal = ref(false);
|
||||
|
||||
// 消息列表相关状态
|
||||
const messageList = ref([]);
|
||||
const isLoading = ref(false);
|
||||
@ -192,10 +224,80 @@ const lastFirstMessageId = ref("");
|
||||
|
||||
// 判断是否为系统消息
|
||||
function isSystemMessage(message) {
|
||||
const description = message.payload?.description;
|
||||
return (
|
||||
message.type === "TIMCustomElem" && description === "SYSTEM_NOTIFICATION"
|
||||
);
|
||||
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;
|
||||
}
|
||||
|
||||
// 检查是否有待接诊的系统消息
|
||||
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"
|
||||
) {
|
||||
showConsultAccept.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是其他系统消息类型(如已接诊、已结束、已拒绝等),隐藏接受组件
|
||||
if (
|
||||
data.type === "system_message" &&
|
||||
(data.messageType === "consult_accepted" ||
|
||||
data.messageType === "consult_ended" ||
|
||||
data.messageType === "consult_rejected")
|
||||
) {
|
||||
showConsultAccept.value = false;
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("解析系统消息失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到相关系统消息,隐藏接受组件
|
||||
showConsultAccept.value = false;
|
||||
}
|
||||
|
||||
// 获取消息气泡样式类
|
||||
@ -204,9 +306,8 @@ function getBubbleClass(message) {
|
||||
if (message.type === "TIMImageElem") {
|
||||
return "image-bubble";
|
||||
}
|
||||
|
||||
if (message.type === "TIMCustomElem") {
|
||||
return message.flow === "out" ? "user-bubble" : "doctor-bubble-blue";
|
||||
return message.flow === "out" ? "" : "";
|
||||
}
|
||||
return message.flow === "out" ? "user-bubble" : "doctor-bubble";
|
||||
}
|
||||
@ -224,6 +325,7 @@ onLoad((options) => {
|
||||
if (options.userID) {
|
||||
chatInfo.value.userID = options.userID;
|
||||
}
|
||||
|
||||
checkLoginAndInitTIM();
|
||||
updateNavigationTitle();
|
||||
});
|
||||
@ -288,6 +390,10 @@ const initTIMCallbacks = async () => {
|
||||
if (!existingMessage) {
|
||||
messageList.value.push(message);
|
||||
console.log("✓ 添加消息到列表,当前消息数量:", messageList.value.length);
|
||||
|
||||
// 检查是否有待接诊的系统消息
|
||||
checkConsultPendingStatus();
|
||||
|
||||
// 立即滚动到底部,不使用延迟
|
||||
nextTick(() => {
|
||||
scrollToBottom(true);
|
||||
@ -346,6 +452,9 @@ const initTIMCallbacks = async () => {
|
||||
isCompleted.value = data.isCompleted || false;
|
||||
isLoadingMore.value = false;
|
||||
|
||||
// 检查是否有待接诊的系统消息
|
||||
checkConsultPendingStatus();
|
||||
|
||||
nextTick(() => {
|
||||
if (data.isRefresh) {
|
||||
console.log("后台刷新完成,保持当前滚动位置");
|
||||
@ -485,16 +594,16 @@ const scrollToBottom = (immediate = false) => {
|
||||
if (messageList.value.length > 0) {
|
||||
const lastMessage = messageList.value[messageList.value.length - 1];
|
||||
const targetId = `msg-${lastMessage.ID}`;
|
||||
|
||||
|
||||
if (immediate) {
|
||||
// 立即滚动:先清空再设置,触发滚动
|
||||
scrollIntoView.value = '';
|
||||
scrollIntoView.value = "";
|
||||
nextTick(() => {
|
||||
scrollIntoView.value = targetId;
|
||||
});
|
||||
} else {
|
||||
// 正常滚动,使用短延迟确保DOM更新
|
||||
scrollIntoView.value = '';
|
||||
scrollIntoView.value = "";
|
||||
setTimeout(() => {
|
||||
scrollIntoView.value = targetId;
|
||||
}, 50);
|
||||
@ -594,18 +703,152 @@ onHide(() => {
|
||||
stopIMMonitoring();
|
||||
});
|
||||
|
||||
// 发送常用语(从常用语页面返回时调用)
|
||||
|
||||
const sendCommonPhrase = (content) => {
|
||||
if (chatInputRef.value) {
|
||||
chatInputRef.value.sendTextMessageFromPhrase(content);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给常用语页面调用
|
||||
defineExpose({
|
||||
sendCommonPhrase
|
||||
sendCommonPhrase,
|
||||
});
|
||||
|
||||
// 处理接受问诊
|
||||
const handleAcceptConsult = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: "处理中...",
|
||||
});
|
||||
|
||||
// 发送接受问诊的系统消息
|
||||
const customMessage = {
|
||||
data: JSON.stringify({
|
||||
type: "system_message",
|
||||
messageType: "consult_accepted",
|
||||
content: "医生已接诊",
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
description: "系统消息标记",
|
||||
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) {
|
||||
showConsultAccept.value = false;
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: "已接受问诊",
|
||||
icon: "success",
|
||||
});
|
||||
} else {
|
||||
throw new Error(sendResult.message || "发送失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("接受问诊失败:", error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || "操作失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理拒绝问诊
|
||||
const handleRejectConsult = () => {
|
||||
// 显示拒绝原因选择对话框
|
||||
showRejectReasonModal.value = true;
|
||||
};
|
||||
|
||||
// 处理拒绝原因确认
|
||||
const handleRejectReasonConfirm = async (reason) => {
|
||||
try {
|
||||
showRejectReasonModal.value = false;
|
||||
|
||||
uni.showLoading({
|
||||
title: "处理中...",
|
||||
});
|
||||
|
||||
// 获取医生信息
|
||||
const memberName = account.value?.name || "医生";
|
||||
|
||||
// 获取群组ID
|
||||
const groupId = chatInfo.value.conversationID.replace("GROUP", "");
|
||||
|
||||
// 调用后端接口发送拒绝消息
|
||||
const result = await sendConsultRejectedMessage({
|
||||
groupId,
|
||||
memberName,
|
||||
reason,
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
if (result.success) {
|
||||
showConsultAccept.value = false;
|
||||
uni.showToast({
|
||||
title: "已拒绝问诊",
|
||||
icon: "success",
|
||||
});
|
||||
} else {
|
||||
throw new Error(result.message || "发送失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("拒绝问诊失败:", error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || "操作失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 处理拒绝原因取消
|
||||
const handleRejectReasonCancel = () => {
|
||||
showRejectReasonModal.value = false;
|
||||
};
|
||||
// 处理结束问诊
|
||||
const handleEndConsult = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: "处理中...",
|
||||
});
|
||||
// 调用结束问诊接口,直接传入 groupId
|
||||
const result = await endConsultation({
|
||||
groupId: groupId.value,
|
||||
adminAccount: account.value?.userId || "",
|
||||
extraData: {
|
||||
endBy: account.value?.userId || "",
|
||||
endByName: account.value?.name || "医生",
|
||||
endReason: "问诊完成",
|
||||
},
|
||||
});
|
||||
uni.hideLoading();
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: "问诊已结束",
|
||||
icon: "success",
|
||||
});
|
||||
} else {
|
||||
throw new Error(result.message || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("结束问诊失败:", error);
|
||||
uni.hideLoading();
|
||||
uni.showToast({
|
||||
title: error.message || "操作失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 页面卸载
|
||||
onUnmounted(() => {
|
||||
clearMessageCache();
|
||||
@ -615,6 +858,101 @@ 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>
|
||||
|
||||
|
||||
446
pages/message/survey-list.vue
Normal file
446
pages/message/survey-list.vue
Normal file
@ -0,0 +1,446 @@
|
||||
<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>
|
||||
24
pages/webview/webview.vue
Normal file
24
pages/webview/webview.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<view class="webview-container">
|
||||
<web-view :src="url"></web-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { ref } from "vue";
|
||||
const url = ref("");
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.url) {
|
||||
url.value = decodeURIComponent(options.url);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.webview-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
58
styles/theme.scss
Normal file
58
styles/theme.scss
Normal file
@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 项目主题色配置
|
||||
* 统一管理项目中使用的主题色
|
||||
*/
|
||||
|
||||
// 主题色
|
||||
$primary-color: #0877F1;
|
||||
$primary-light: #e8f3ff;
|
||||
$primary-dark: #0660c9;
|
||||
$primary-gradient-start: #1b5cc8;
|
||||
$primary-gradient-end: #0877F1;
|
||||
|
||||
// 辅助色
|
||||
$success-color: #4cd964;
|
||||
$warning-color: #f0ad4e;
|
||||
$error-color: #dd524d;
|
||||
$info-color: #909399;
|
||||
|
||||
// 文字颜色
|
||||
$text-color-primary: #333;
|
||||
$text-color-regular: #666;
|
||||
$text-color-secondary: #999;
|
||||
$text-color-placeholder: #c0c4cc;
|
||||
$text-color-white: #fff;
|
||||
|
||||
// 背景颜色
|
||||
$bg-color-white: #ffffff;
|
||||
$bg-color-page: #f5f5f5;
|
||||
$bg-color-hover: #f1f1f1;
|
||||
$bg-color-mask: rgba(0, 0, 0, 0.4);
|
||||
|
||||
// 边框颜色
|
||||
$border-color-base: #e4e7ed;
|
||||
$border-color-light: #ebeef5;
|
||||
$border-color-lighter: #f2f6fc;
|
||||
|
||||
// 圆角
|
||||
$border-radius-small: 4rpx;
|
||||
$border-radius-base: 8rpx;
|
||||
$border-radius-large: 16rpx;
|
||||
$border-radius-round: 999rpx;
|
||||
|
||||
// 间距
|
||||
$spacing-small: 16rpx;
|
||||
$spacing-base: 24rpx;
|
||||
$spacing-large: 32rpx;
|
||||
|
||||
// 字体大小
|
||||
$font-size-small: 24rpx;
|
||||
$font-size-base: 28rpx;
|
||||
$font-size-medium: 32rpx;
|
||||
$font-size-large: 36rpx;
|
||||
$font-size-xlarge: 40rpx;
|
||||
|
||||
// 阴影
|
||||
$box-shadow-light: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
$box-shadow-base: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
$box-shadow-dark: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
|
||||
2
uni.scss
2
uni.scss
@ -15,7 +15,7 @@
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-primary: #0877F1;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
99
utils/api.js
99
utils/api.js
@ -26,7 +26,26 @@ const urlsConfig = {
|
||||
saveCommonPhrase: 'saveCommonPhrase',
|
||||
deleteCommonPhrase: 'deleteCommonPhrase',
|
||||
getCommonPhraseCategories: 'getCommonPhraseCategories',
|
||||
saveCommonPhraseCategory: 'saveCommonPhraseCategory'
|
||||
saveCommonPhraseCategory: 'saveCommonPhraseCategory',
|
||||
// 个人常用语接口
|
||||
getPersonalPhrases: 'getPersonalPhrases',
|
||||
savePersonalPhrase: 'savePersonalPhrase',
|
||||
deletePersonalPhrase: 'deletePersonalPhrase',
|
||||
getPersonalPhraseCategories: 'getPersonalPhraseCategories',
|
||||
savePersonalPhraseCategory: 'savePersonalPhraseCategory',
|
||||
deletePersonalPhraseCategory: 'deletePersonalPhraseCategory',
|
||||
// 宣教文章接口
|
||||
getArticleCateList: 'getArticleCateList',
|
||||
getArticleList: 'getArticleList',
|
||||
getArticle: 'getArticle',
|
||||
addArticleSendRecord: 'addArticleSendRecord'
|
||||
},
|
||||
|
||||
survery: {
|
||||
getSurveyCateList: 'getSurveryCateList',
|
||||
getSurveyList: 'getList',
|
||||
createSurveyRecord: 'createRecord',
|
||||
getSurveyDetail: 'getDetail'
|
||||
},
|
||||
member: {
|
||||
addCustomer: 'add',
|
||||
@ -43,7 +62,10 @@ const urlsConfig = {
|
||||
im: {
|
||||
getUserSig: 'getUserSig',
|
||||
sendSystemMessage: "sendSystemMessage",
|
||||
getChatRecordsByGroupId: "getChatRecordsByGroupId"
|
||||
getChatRecordsByGroupId: "getChatRecordsByGroupId",
|
||||
sendConsultRejectedMessage: "sendConsultRejectedMessage",
|
||||
endConsultation: "endConsultation",
|
||||
getGroupListByGroupId: "getGroupListByGroupId"
|
||||
}
|
||||
|
||||
}
|
||||
@ -77,3 +99,76 @@ export default async function api(urlId, data) {
|
||||
})
|
||||
}
|
||||
|
||||
// 宣教文章相关 API
|
||||
export async function getArticleCateList(data) {
|
||||
return api('getArticleCateList', data);
|
||||
}
|
||||
|
||||
export async function getArticleList(data) {
|
||||
return api('getArticleList', data);
|
||||
}
|
||||
|
||||
export async function getArticle(data) {
|
||||
return api('getArticle', data);
|
||||
}
|
||||
|
||||
export async function addArticleSendRecord(data) {
|
||||
return api('addArticleSendRecord', data);
|
||||
}
|
||||
|
||||
// 问卷相关 API
|
||||
export async function getSurveyCateList(data) {
|
||||
return api('getSurveyCateList', data);
|
||||
}
|
||||
|
||||
export async function getSurveyList(data) {
|
||||
return api('getSurveyList', data);
|
||||
}
|
||||
|
||||
export async function createSurveyRecord(data) {
|
||||
return api('createSurveyRecord', data);
|
||||
}
|
||||
|
||||
export async function getSurveyDetail(data) {
|
||||
return api('getSurveyDetail', data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// IM 系统消息相关 API
|
||||
export async function sendConsultRejectedMessage(data) {
|
||||
return request({
|
||||
url: '/getYoucanData/im',
|
||||
data: {
|
||||
type: 'sendConsultRejectedMessage',
|
||||
...data
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 发送宣教文章消息
|
||||
export async function sendArticleMessage(data) {
|
||||
return request({
|
||||
url: '/getYoucanData/im',
|
||||
data: {
|
||||
type: 'sendArticleMessage',
|
||||
...data
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 结束问诊接口
|
||||
export async function endConsultation(data) {
|
||||
return request({
|
||||
url: '/getYoucanData/im',
|
||||
data: {
|
||||
type: 'endConsultation',
|
||||
...data
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 根据群组ID获取群组记录
|
||||
export async function getGroupListByGroupId(data) {
|
||||
return api('getGroupListByGroupId', data);
|
||||
}
|
||||
|
||||
@ -363,7 +363,6 @@ class TimChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户信息并登录
|
||||
async getUserInfoAndLogin(userID) {
|
||||
try {
|
||||
if (userID) {
|
||||
@ -376,7 +375,6 @@ class TimChatManager {
|
||||
}
|
||||
this.currentUserID = userInfo.userID
|
||||
}
|
||||
|
||||
this.currentUserSig = await this.getUserSig(this.currentUserID)
|
||||
await this.loginTIM()
|
||||
} catch (error) {
|
||||
@ -711,26 +709,37 @@ class TimChatManager {
|
||||
// 获取消息所属的会话ID
|
||||
const messageConversationID = convertedMessage.conversationID
|
||||
|
||||
// 检查是否为系统消息
|
||||
const isSystemMsg = this.isSystemMessage(convertedMessage)
|
||||
|
||||
console.log('收到新消息:', {
|
||||
messageID: convertedMessage.ID,
|
||||
messageConversationID: messageConversationID,
|
||||
currentConversationID: this.currentConversationID,
|
||||
messageType: convertedMessage.type,
|
||||
from: convertedMessage.from
|
||||
from: convertedMessage.from,
|
||||
isSystemMessage: isSystemMsg
|
||||
})
|
||||
// 判断是否为当前会话的消息(必须有currentConversationID且匹配才显示)
|
||||
const isCurrentConversation = this.currentConversationID &&
|
||||
messageConversationID === this.currentConversationID
|
||||
|
||||
// 判断是否为当前会话的消息
|
||||
// 系统消息:只要会话ID匹配就显示(不要求必须有currentConversationID)
|
||||
// 普通消息:必须有currentConversationID且匹配才显示
|
||||
const isCurrentConversation = isSystemMsg
|
||||
? messageConversationID === this.currentConversationID
|
||||
: (this.currentConversationID && messageConversationID === this.currentConversationID)
|
||||
|
||||
console.log('消息会话匹配检查:', {
|
||||
isCurrentConversation,
|
||||
isSystemMessage: isSystemMsg,
|
||||
hasCurrentConversationID: !!this.currentConversationID,
|
||||
conversationIDMatch: messageConversationID === this.currentConversationID
|
||||
})
|
||||
|
||||
if (isCurrentConversation) {
|
||||
// 当前会话的消息,触发回调
|
||||
console.log('✓ 消息属于当前会话,触发显示')
|
||||
this.triggerCallback('onMessageReceived', convertedMessage)
|
||||
// 处理已读状态
|
||||
// 处理已读状态(系统消息也标记为已读)
|
||||
if (this.currentConversationID) {
|
||||
this.markConversationAsRead(this.currentConversationID)
|
||||
}
|
||||
@ -1093,7 +1102,52 @@ class TimChatManager {
|
||||
} else if (lastMessage.type === 'TIMSoundElem') {
|
||||
lastMessageText = '[语音]'
|
||||
} else if (lastMessage.type === 'TIMCustomElem') {
|
||||
lastMessageText = lastMessage.payload.data || '[自定义消息]'
|
||||
// 解析自定义消息
|
||||
try {
|
||||
const customData = JSON.parse(lastMessage.payload.data)
|
||||
const messageType = customData.messageType
|
||||
// 根据消息类型返回不同的预览文本
|
||||
switch (messageType) {
|
||||
case 'system_message':
|
||||
lastMessageText = '[系统消息]'
|
||||
break
|
||||
case 'symptom':
|
||||
lastMessageText = '[病情描述]'
|
||||
break
|
||||
case 'prescription':
|
||||
lastMessageText = '[处方单]'
|
||||
break
|
||||
case 'refill':
|
||||
lastMessageText = '[续方申请]'
|
||||
break
|
||||
case 'survey':
|
||||
lastMessageText = '[问卷调查]'
|
||||
break
|
||||
case 'article':
|
||||
lastMessageText = '[文章]'
|
||||
break
|
||||
case "consult_pending":
|
||||
lastMessageText = '患者向团队发起咨询,请在1小时内接诊,超时将自动关闭会话'
|
||||
break
|
||||
case "consult_rejected":
|
||||
lastMessageText = '患者向团队发起咨询,由于有紧急事务要处理暂时无法接受咨询.本次会话丿关闭'
|
||||
break
|
||||
case "consult_timeout":
|
||||
lastMessageText = '患者向团队发起咨询,团队成员均未接受咨询,本次会话已自动关闭'
|
||||
break
|
||||
case "consult_accepted":
|
||||
lastMessageText = '已接诊,会话已开始'
|
||||
break
|
||||
case "consult_ended":
|
||||
lastMessageText = '已结束当前会话'
|
||||
break
|
||||
default:
|
||||
lastMessageText = '[自定义消息]'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析自定义消息失败:', error)
|
||||
lastMessageText = '[自定义消息]'
|
||||
}
|
||||
} else {
|
||||
lastMessageText = '[未知消息类型]'
|
||||
}
|
||||
@ -2362,6 +2416,41 @@ class TimChatManager {
|
||||
}
|
||||
|
||||
// 工具方法
|
||||
// 判断是否为系统消息
|
||||
isSystemMessage(message) {
|
||||
if (message.type !== 'TIMCustomElem') {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查 payload.data 是否包含系统消息标记
|
||||
try {
|
||||
if (message.payload && 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 && message.payload.description === '系统消息标记') {
|
||||
return true
|
||||
}
|
||||
|
||||
// 兼容旧的系统消息格式
|
||||
if (message.payload && message.payload.description === 'SYSTEM_NOTIFICATION') {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('判断系统消息失败:', error)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
filterMessage(message) {
|
||||
if (message.type === 'TIMCustomElem' && message.payload && message.payload.data) {
|
||||
if (message.payload.data === 'group_create' || message.payload.data === 'purchased') {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user