Compare commits
No commits in common. "ba1200a6508a8eae533ab0e38ac3b30374e54244" and "414a32410b68fdb40fcdefd7b573b691725485df" have entirely different histories.
ba1200a650
...
414a32410b
@ -1,10 +0,0 @@
|
||||
MP_API_BASE_URL=https://ykt.youcan365.com
|
||||
MP_IMAGE_URL=https://ykt.youcan365.com
|
||||
MP_CACHE_PREFIX=production
|
||||
MP_WX_APP_ID=wx1d8337a40c11d66c
|
||||
MP_CORP_ID=wpLgjyawAA8N0gWmXgyJq8wpjGcOT7fg
|
||||
MP_TIM_SDK_APP_ID=1600123876
|
||||
MP_INVITE_TEAMMATE_QRCODE=https://www.youcan365.com/invite-teammate
|
||||
MP_INVITE_PATIENT_QRCODE=https://www.youcan365.com/invite-patient
|
||||
MP_PATIENT_PAGE_BASE_URL= 'https://www.youcan365.com/h5/#/'
|
||||
MP_SURVEY_URL= 'https://www.youcan365.com/surveyDev/#/pages/survey/survey'
|
||||
@ -22,7 +22,7 @@
|
||||
<slot name="footer"></slot>
|
||||
</view>
|
||||
<!-- #ifdef MP-->
|
||||
<view v-if="showSafeArea" class="safeareaBottom"></view>
|
||||
<!-- <view v-if="showSafeArea" class="safeareaBottom"></view> -->
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -28,12 +28,6 @@
|
||||
"UNI_PLATFORM": "mp-weixin"
|
||||
}
|
||||
},
|
||||
"pro": {
|
||||
"title": "线上",
|
||||
"env": {
|
||||
"UNI_PLATFORM": "mp-weixin"
|
||||
}
|
||||
},
|
||||
"ip": {
|
||||
"title": "本机ip",
|
||||
"env": {
|
||||
|
||||
12
pages.json
12
pages.json
@ -12,12 +12,6 @@
|
||||
"navigationBarTitleText": "登录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/agreement",
|
||||
"style": {
|
||||
"navigationBarTitleText": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/home/work-home",
|
||||
"style": {
|
||||
@ -244,12 +238,6 @@
|
||||
"navigationBarTitleText": "添加病历"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "ai-medical-case-form",
|
||||
"style": {
|
||||
"navigationBarTitleText": "添加病历"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "service-record-detail",
|
||||
"style": {
|
||||
|
||||
@ -1,480 +0,0 @@
|
||||
<template>
|
||||
<view class="medical-case-form">
|
||||
<view class="form-container">
|
||||
<!-- 动态渲染表单字段 -->
|
||||
<view
|
||||
v-for="field in currentFields"
|
||||
:key="field.key"
|
||||
class="form-item"
|
||||
:class="{ required: field.required }"
|
||||
>
|
||||
<view class="item-label">{{ field.label }}</view>
|
||||
|
||||
<!-- 日期选择器 -->
|
||||
<picker
|
||||
v-if="field.type === 'date'"
|
||||
mode="date"
|
||||
:value="formData[field.key]"
|
||||
@change="onDateChange(field.key, $event)"
|
||||
:disabled="!isEditing"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{ formData[field.key] || "暂无" }}
|
||||
</view>
|
||||
</picker>
|
||||
|
||||
<!-- 多行文本 -->
|
||||
<textarea
|
||||
v-else-if="field.type === 'textarea'"
|
||||
class="item-textarea"
|
||||
v-model="formData[field.key]"
|
||||
placeholder="请输入"
|
||||
:disabled="!isEditing"
|
||||
/>
|
||||
|
||||
<!-- 单行文本 -->
|
||||
<input
|
||||
v-else
|
||||
class="item-input"
|
||||
v-model="formData[field.key]"
|
||||
placeholder="暂无"
|
||||
:disabled="!isEditing"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="footer-buttons">
|
||||
<view class="btn-regenerate" @click="handleRegenerate">
|
||||
<text class="btn-text">重新生成</text>
|
||||
</view>
|
||||
<view class="btn-save" @click="handleSave">
|
||||
<text class="btn-text">保存至档案</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import useAccountStore from "@/store/account";
|
||||
import api from "@/utils/api.js";
|
||||
const caseType = ref("");
|
||||
const formData = ref({});
|
||||
const isEditing = ref(true);
|
||||
const customerId = ref("");
|
||||
const groupId = ref("");
|
||||
const accountStore = useAccountStore();
|
||||
const { doctorInfo } = storeToRefs(accountStore);
|
||||
// 病历类型名称
|
||||
const CASE_TYPE_NAMES = {
|
||||
outpatient: "门诊病历",
|
||||
inhospital: "住院病历",
|
||||
physicalExaminationTemplate: "体检记录",
|
||||
preConsultation: "预问诊记录",
|
||||
};
|
||||
|
||||
// 字段标签
|
||||
const FIELD_LABELS = {
|
||||
// 门诊病历
|
||||
visitTime: "就诊日期",
|
||||
chiefComplaint: "主诉",
|
||||
medicalHistorySummary: "病史概要",
|
||||
examination: "检查",
|
||||
diagnosisName: "门诊诊断",
|
||||
// 住院病历
|
||||
inhosDate: "入院日期",
|
||||
operation: "手术记录",
|
||||
operationDate: "手术日期",
|
||||
treatmentPlan: "治疗方案",
|
||||
// 体检记录
|
||||
inspectTime: "体检日期",
|
||||
inspectSummary: "体检小结",
|
||||
positiveFind: "阳性发现及处理意见",
|
||||
// 预问诊记录
|
||||
consultationDate: "问诊日期",
|
||||
presentIllnessHistory: "现病史",
|
||||
pastMedicalHistory: "既往史",
|
||||
};
|
||||
|
||||
// 字段配置:根据病历类型定义字段
|
||||
const FIELD_CONFIG = {
|
||||
outpatient: [
|
||||
{
|
||||
key: "visitTime",
|
||||
label: FIELD_LABELS.visitTime,
|
||||
type: "date",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "diagnosisName",
|
||||
label: FIELD_LABELS.diagnosisName,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "chiefComplaint",
|
||||
label: FIELD_LABELS.chiefComplaint,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "medicalHistorySummary",
|
||||
label: FIELD_LABELS.medicalHistorySummary,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "examination",
|
||||
label: FIELD_LABELS.examination,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "treatmentPlan",
|
||||
label: "治疗方案",
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
inhospital: [
|
||||
{
|
||||
key: "inhosDate",
|
||||
label: FIELD_LABELS.inhosDate,
|
||||
type: "date",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "diagnosisName",
|
||||
label: "住院主诊断",
|
||||
type: "textarea",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "operation",
|
||||
label: FIELD_LABELS.operation,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "operationDate",
|
||||
label: FIELD_LABELS.operationDate,
|
||||
type: "date",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "treatmentPlan",
|
||||
label: FIELD_LABELS.treatmentPlan,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "chiefComplaint",
|
||||
label: FIELD_LABELS.chiefComplaint,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "medicalHistorySummary",
|
||||
label: FIELD_LABELS.medicalHistorySummary,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "examination",
|
||||
label: FIELD_LABELS.examination,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "treatmentPlan",
|
||||
label: "治疗方案",
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
physicalExaminationTemplate: [
|
||||
{
|
||||
key: "inspectTime",
|
||||
label: FIELD_LABELS.inspectTime,
|
||||
type: "date",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "inspectSummary",
|
||||
label: FIELD_LABELS.inspectSummary,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
key: "positiveFind",
|
||||
label: FIELD_LABELS.positiveFind,
|
||||
type: "textarea",
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
preConsultation: [
|
||||
{
|
||||
key: "consultationDate",
|
||||
label: FIELD_LABELS.consultationDate,
|
||||
type: "date",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "chiefComplaint",
|
||||
label: FIELD_LABELS.chiefComplaint,
|
||||
type: "textarea",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "presentIllnessHistory",
|
||||
label: FIELD_LABELS.presentIllnessHistory,
|
||||
type: "textarea",
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "pastMedicalHistory",
|
||||
label: FIELD_LABELS.pastMedicalHistory,
|
||||
type: "textarea",
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 当前病历类型的字段配置
|
||||
const currentFields = computed(() => {
|
||||
return FIELD_CONFIG[caseType.value] || [];
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
const options = currentPage.options;
|
||||
|
||||
caseType.value = options.caseType || "";
|
||||
customerId.value = options.patientId || "";
|
||||
groupId.value = options.groupId || "";
|
||||
// 从 options 中解析表单数据
|
||||
if (options.formData) {
|
||||
try {
|
||||
formData.value = JSON.parse(decodeURIComponent(options.formData));
|
||||
} catch (e) {
|
||||
console.error("解析表单数据失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置页面标题
|
||||
const title = CASE_TYPE_NAMES[caseType.value]
|
||||
? `添加${CASE_TYPE_NAMES[caseType.value]}`
|
||||
: "添加病历";
|
||||
uni.setNavigationBarTitle({ title });
|
||||
});
|
||||
|
||||
const onDateChange = (field, event) => {
|
||||
formData.value[field] = event.detail.value;
|
||||
};
|
||||
|
||||
const handleRegenerate = () => {
|
||||
uni.showModal({
|
||||
title: "提示",
|
||||
content: "确定要重新生成吗?当前编辑的内容将被覆盖",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 返回上一页并触发重新生成
|
||||
uni.navigateBack({
|
||||
success: () => {
|
||||
uni.$emit("regenerateMedicalCase", {
|
||||
caseType: caseType.value,
|
||||
customerId: customerId.value,
|
||||
groupId: groupId.value,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
// 验证必填项
|
||||
const requiredFields = getRequiredFields();
|
||||
const missingFields = requiredFields.filter(
|
||||
(field) => !formData.value[field.key]
|
||||
);
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
uni.showToast({
|
||||
title: `请填写${missingFields[0].label}`,
|
||||
icon: "none",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
uni.showLoading({ title: "保存中..." });
|
||||
const result = await api("addMedicalRecord", {
|
||||
medicalType: caseType.value,
|
||||
memberId: customerId.value,
|
||||
creator: doctorInfo.value.userid,
|
||||
...formData.value,
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
|
||||
if (result.success) {
|
||||
uni.showToast({
|
||||
title: "保存成功",
|
||||
icon: "success",
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateBack();
|
||||
}, 1500);
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: result.message || "保存失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error("保存病历失败:", error);
|
||||
uni.showToast({
|
||||
title: "保存失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getRequiredFields = () => {
|
||||
return currentFields.value.filter((field) => field.required);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.medical-case-form {
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
padding-bottom: 120rpx;
|
||||
|
||||
.form-container {
|
||||
background-color: #ffffff;
|
||||
padding: 32rpx;
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
&.required .item-label::before {
|
||||
content: "*";
|
||||
color: #ff4d4f;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.item-input,
|
||||
.picker-value {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
padding: 0 24rpx;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
|
||||
&[disabled] {
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.item-textarea {
|
||||
width: 100%;
|
||||
min-height: 100rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333333;
|
||||
height: 100px;
|
||||
&[disabled] {
|
||||
color: #999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tips-box {
|
||||
margin-top: 32rpx;
|
||||
padding: 24rpx;
|
||||
background-color: #fffbe6;
|
||||
border-radius: 8rpx;
|
||||
|
||||
.tips-text {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer-buttons {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
|
||||
.btn-regenerate,
|
||||
.btn-save {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 44rpx;
|
||||
|
||||
.btn-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-regenerate {
|
||||
background-color: #ffffff;
|
||||
border: 2rpx solid #1890ff;
|
||||
|
||||
.btn-text {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background-color: #1890ff;
|
||||
|
||||
.btn-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -91,13 +91,11 @@
|
||||
</view>
|
||||
</view>
|
||||
<button
|
||||
v-if="fromChat && isExecutor(i)"
|
||||
v-if="canShowSendButton(i)"
|
||||
class="action-btn send-btn"
|
||||
:class="{ loading: sendingFollowUp }"
|
||||
:disabled="sendingFollowUp"
|
||||
@click.stop="sendFollowUp(i)"
|
||||
@click.stop="goChatAndSend(i)"
|
||||
>
|
||||
{{ sendingFollowUp ? "发送中..." : "发送" }}
|
||||
发送
|
||||
</button>
|
||||
</view>
|
||||
<view v-if="i.status === 'treated'" class="result"
|
||||
@ -242,12 +240,6 @@ function getUserId() {
|
||||
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || "") || "";
|
||||
}
|
||||
|
||||
function isExecutor(todo) {
|
||||
const currentUserId = getUserId();
|
||||
const executorUserId = String(todo?.executorUserId || "");
|
||||
return currentUserId && executorUserId && currentUserId === executorUserId;
|
||||
}
|
||||
|
||||
function getCorpId() {
|
||||
const team = uni.getStorageSync("ykt_case_current_team") || {};
|
||||
const d = doctorInfo.value || {};
|
||||
@ -304,7 +296,6 @@ const pages = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const userNameMap = ref({});
|
||||
const sendingFollowUp = ref(false);
|
||||
|
||||
const moreStatus = computed(() => {
|
||||
if (loading.value) return "loading";
|
||||
@ -376,6 +367,7 @@ function eventTypeLabel(eventType) {
|
||||
}
|
||||
|
||||
function resolveUserName(userId) {
|
||||
const id = String(userId || "");
|
||||
if (!id) return "";
|
||||
const map = userNameMap.value || {};
|
||||
return String(map[id] || "") || id;
|
||||
@ -539,90 +531,163 @@ function toDetail(todo) {
|
||||
});
|
||||
}
|
||||
|
||||
async function sendFollowUp(todo) {
|
||||
if (sendingFollowUp.value) {
|
||||
toast("正在发送中,请稍候...");
|
||||
return;
|
||||
function hasSendContent(todo) {
|
||||
return Boolean(todo?.sendContent) || (Array.isArray(todo?.fileList) && todo.fileList.length > 0);
|
||||
}
|
||||
|
||||
function isExecutorMe(todo) {
|
||||
const me = String(getUserId() || "");
|
||||
const executor = String(todo?.executorUserId || "");
|
||||
if (!me || !executor) return false;
|
||||
return me === executor;
|
||||
}
|
||||
|
||||
function canShowSendButton(todo) {
|
||||
if (!hasSendContent(todo)) return false;
|
||||
if (!isExecutorMe(todo)) return false;
|
||||
// 当前患者无会话则不展示
|
||||
return Boolean(currentChatGroupId.value);
|
||||
}
|
||||
|
||||
function buildFollowUpMessages(todo) {
|
||||
const messages = [];
|
||||
if (todo?.sendContent) {
|
||||
messages.push({ type: "text", content: String(todo.sendContent) });
|
||||
}
|
||||
|
||||
if (!todo.sendContent && (!todo.fileList || todo.fileList.length === 0)) {
|
||||
toast("没有发送内容");
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(todo?.fileList)) {
|
||||
for (const file of todo.fileList) {
|
||||
const outerType = String(file?.type || "");
|
||||
|
||||
sendingFollowUp.value = true;
|
||||
try {
|
||||
const messages = [];
|
||||
|
||||
// 1. 发送文字内容
|
||||
if (todo.sendContent) {
|
||||
messages.push({
|
||||
type: "text",
|
||||
content: todo.sendContent,
|
||||
});
|
||||
}
|
||||
console.log("==============>fileList", todo.fileList);
|
||||
|
||||
// 2. 处理文件列表(图片、宣教文章、问卷)
|
||||
if (Array.isArray(todo.fileList)) {
|
||||
for (const file of todo.fileList) {
|
||||
if (file.type === "image" && file.URL) {
|
||||
// 发送图片
|
||||
messages.push({
|
||||
type: "image",
|
||||
content: file.URL,
|
||||
name: file.file?.name || file.name || "图片",
|
||||
});
|
||||
} else if (file.file.type === "article" && file.file?.url) {
|
||||
// 发送宣教文章 - 从 URL 中解析 id
|
||||
const articleId = extractIdFromUrl(file.file.url);
|
||||
messages.push({
|
||||
type: "article",
|
||||
content: {
|
||||
_id: articleId,
|
||||
title: file.file?.name || "宣教文章",
|
||||
url: file.file?.url || file.URL,
|
||||
subtitle: file.file?.subtitle || "",
|
||||
cover: file.file?.cover || "",
|
||||
articleId: articleId,
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
file.file.type === "questionnaire" &&
|
||||
(file.file?.url || file.URL)
|
||||
) {
|
||||
// 发送问卷 - 从 URL 中解析 surveryId
|
||||
const surveryUrl = file.file?.url || file.URL;
|
||||
const surveryId = extractSurveryIdFromUrl(surveryUrl);
|
||||
messages.push({
|
||||
type: "questionnaire",
|
||||
content: {
|
||||
_id: surveryId,
|
||||
name: file.file?.name || file.name || "问卷",
|
||||
surveryId: surveryId,
|
||||
url: surveryUrl,
|
||||
},
|
||||
});
|
||||
let innerFile = file?.file;
|
||||
if (typeof innerFile === "string") {
|
||||
try {
|
||||
innerFile = JSON.parse(innerFile);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
innerFile = innerFile && typeof innerFile === "object" ? innerFile : null;
|
||||
|
||||
// 调用统一的消息发送处理函数
|
||||
const success = await handleFollowUpMessages(messages, {
|
||||
const innerType = String(innerFile?.type || "");
|
||||
const outerUrl = String(file?.URL || file?.url || "");
|
||||
const innerUrl = String(innerFile?.url || "");
|
||||
|
||||
// 兼容 followup-detail.vue 的判定方式
|
||||
let fileType = "";
|
||||
if (outerType === "image" || innerType.includes("image")) fileType = "image";
|
||||
else if (innerType === "article") fileType = "article";
|
||||
else if (innerType === "questionnaire") fileType = "questionnaire";
|
||||
else fileType = outerType;
|
||||
|
||||
const url = fileType === "article" || fileType === "questionnaire" ? (innerUrl || outerUrl) : (outerUrl || innerUrl);
|
||||
|
||||
if (fileType === "image" && url) {
|
||||
messages.push({
|
||||
type: "image",
|
||||
content: url,
|
||||
name: innerFile?.name || file?.name || "图片",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fileType === "article") {
|
||||
const fallbackArticleId = String(innerFile?._id || file?._id || innerFile?.articleId || file?.articleId || "") || "";
|
||||
const extractedId = extractIdFromUrl(url);
|
||||
const articleId = String(extractedId || fallbackArticleId || "");
|
||||
|
||||
// url 兜底:如果后端没给文章跳转链接,则用 articleId+corpId 拼接
|
||||
let articleUrl = String(url || "");
|
||||
if (!articleUrl && articleId) {
|
||||
const corpId = getCorpId();
|
||||
articleUrl = `${__VITE_ENV__?.MP_PATIENT_PAGE_BASE_URL || ""}pages/article/index?id=${encodeURIComponent(
|
||||
articleId
|
||||
)}&corpId=${encodeURIComponent(corpId || "")}`;
|
||||
}
|
||||
|
||||
// 没有 id 和 url 时,跳过
|
||||
if (!articleId && !articleUrl) continue;
|
||||
|
||||
messages.push({
|
||||
type: "article",
|
||||
content: {
|
||||
_id: articleId,
|
||||
title: innerFile?.name || file?.name || "宣教文章",
|
||||
url: articleUrl,
|
||||
subtitle: innerFile?.subtitle || "",
|
||||
cover: innerFile?.cover || file?.URL || "",
|
||||
articleId: articleId,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (fileType === "questionnaire") {
|
||||
const surveryId = innerFile?.surveryId || file?.surveryId;
|
||||
if (!surveryId) continue;
|
||||
const surveyId = String(innerFile?._id || file?._id || surveryId || "");
|
||||
messages.push({
|
||||
type: "questionnaire",
|
||||
content: {
|
||||
_id: surveyId,
|
||||
name: innerFile?.name || file?.name || "问卷",
|
||||
surveryId,
|
||||
url: String(url || ""),
|
||||
createBy: innerFile?.createBy,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
async function goChatAndSend(todo) {
|
||||
if (!canShowSendButton(todo)) return;
|
||||
if (!props.archiveId) return;
|
||||
|
||||
let gid = normalizeGroupId(currentChatGroupId.value || "");
|
||||
if (!gid) {
|
||||
await refreshChatRoom();
|
||||
gid = normalizeGroupId(currentChatGroupId.value || "");
|
||||
}
|
||||
if (!gid) {
|
||||
toast("暂无可进入的会话");
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = buildFollowUpMessages(todo);
|
||||
if (!messages.length) {
|
||||
console.warn("[followup] buildFollowUpMessages empty:", {
|
||||
sendContent: todo?.sendContent,
|
||||
fileList: todo?.fileList,
|
||||
});
|
||||
toast("发送内容解析失败");
|
||||
return;
|
||||
}
|
||||
|
||||
const conversationID = `GROUP${gid}`;
|
||||
|
||||
uni.setStorageSync(PENDING_FOLLOWUP_SEND_STORAGE_KEY, {
|
||||
createdAt: Date.now(),
|
||||
groupId: gid,
|
||||
conversationID,
|
||||
messages,
|
||||
context: {
|
||||
userId: getUserId(),
|
||||
customerId: props.archiveId,
|
||||
customerName: props.data?.name || "",
|
||||
corpId: getCorpId(),
|
||||
env: __VITE_ENV__,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (success) {
|
||||
toast("消息已发送");
|
||||
uni.navigateBack();
|
||||
}
|
||||
} finally {
|
||||
sendingFollowUp.value = false;
|
||||
}
|
||||
uni.navigateTo({
|
||||
url: `/pages/message/index?conversationID=${encodeURIComponent(
|
||||
conversationID
|
||||
)}&groupID=${encodeURIComponent(gid)}&fromCase=true&pendingFollowUpSend=1`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -649,21 +714,77 @@ function extractIdFromUrl(url) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从问卷 URL 中提取 surveryId 参数
|
||||
* @param {string} url - 完整的 URL,格式如: https://www.youcan365.com/patientDeploy/#/pages/survery/fill?corpId=wwe3fb2faa52cf9dfb&surveryId=9ji5kg2oa9x52oyg9w4rj5k81769510562099
|
||||
* @returns {string} 提取出的 surveryId 值
|
||||
*/
|
||||
function extractSurveryIdFromUrl(url) {
|
||||
if (!url) return "";
|
||||
const isRefreshingChatRoom = ref(false);
|
||||
let lastRefreshChatRoomAt = 0;
|
||||
|
||||
function parseAnyTimeMs(v) {
|
||||
if (v === null || v === undefined) return 0;
|
||||
if (typeof v === "number") return v;
|
||||
const s = String(v).trim();
|
||||
if (!s) return 0;
|
||||
if (/^\d{10,13}$/.test(s)) return Number(s.length === 10 ? `${s}000` : s);
|
||||
const d = dayjs(s);
|
||||
return d.isValid() ? d.valueOf() : 0;
|
||||
}
|
||||
|
||||
async function refreshChatRoom() {
|
||||
const customerId = String(props.archiveId || "");
|
||||
if (!customerId) return;
|
||||
if (isRefreshingChatRoom.value) return;
|
||||
const now = Date.now();
|
||||
if (now - lastRefreshChatRoomAt < 5000) return;
|
||||
lastRefreshChatRoomAt = now;
|
||||
|
||||
isRefreshingChatRoom.value = true;
|
||||
try {
|
||||
// 使用正则表达式提取 surveryId 参数
|
||||
// 处理格式: ?surveryId=xxx 或 &surveryId=xxx
|
||||
const match = url.match(/[?&]surveryId=([^&]+)/);
|
||||
return match ? decodeURIComponent(match[1]) : "";
|
||||
} catch (error) {
|
||||
console.error("解析问卷 URL 失败:", error);
|
||||
return "";
|
||||
await ensureDoctor();
|
||||
const corpId = getCorpId();
|
||||
const teamId = getCurrentTeamId();
|
||||
|
||||
const baseQuery = {
|
||||
corpId,
|
||||
customerId,
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
};
|
||||
|
||||
const queryWithTeam = teamId ? { ...baseQuery, teamId } : baseQuery;
|
||||
let detailRes = await api("getGroupList", queryWithTeam, false);
|
||||
let details = Array.isArray(detailRes?.data?.list) ? detailRes.data.list : [];
|
||||
|
||||
if (!details.length && teamId) {
|
||||
detailRes = await api("getGroupList", baseQuery, false);
|
||||
details = Array.isArray(detailRes?.data?.list) ? detailRes.data.list : [];
|
||||
}
|
||||
|
||||
if (!detailRes?.success || !details.length) {
|
||||
chatGroupId.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTeamId = getCurrentTeamId();
|
||||
const detailsForCurrentTeam = currentTeamId
|
||||
? details.filter((g) => String(g?.teamId || g?.team?._id || g?.team?.teamId || "") === currentTeamId)
|
||||
: [];
|
||||
const candidates = detailsForCurrentTeam.length ? detailsForCurrentTeam : details;
|
||||
|
||||
const statusRank = (s) => (s === "processing" ? 3 : s === "pending" ? 2 : 1);
|
||||
candidates.sort((a, b) => {
|
||||
const ra = statusRank(String(a?.orderStatus || ""));
|
||||
const rb = statusRank(String(b?.orderStatus || ""));
|
||||
if (rb !== ra) return rb - ra;
|
||||
const ta = parseAnyTimeMs(a?.updatedAt) || parseAnyTimeMs(a?.createdAt);
|
||||
const tb = parseAnyTimeMs(b?.updatedAt) || parseAnyTimeMs(b?.createdAt);
|
||||
return tb - ta;
|
||||
});
|
||||
|
||||
const best = candidates[0] || {};
|
||||
const gid = normalizeGroupId(best.groupId || best.groupID || best.group_id || "");
|
||||
chatGroupId.value = gid ? String(gid) : "";
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
isRefreshingChatRoom.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1045,14 +1166,6 @@ watch(
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.send-btn.loading {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 120px 0;
|
||||
text-align: center;
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
@team-change="handleTeamChange"
|
||||
@add-patient="handleAddPatient"
|
||||
/>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view
|
||||
class="message-list"
|
||||
@ -25,7 +26,7 @@
|
||||
<view class="avatar-container">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="conversation.avatar || '/static/default-patient-avatar.png'"
|
||||
:src="conversation.avatar || '/static/default-avatar.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-if="conversation.unreadCount > 0" class="unread-badge">
|
||||
@ -52,7 +53,7 @@
|
||||
</view>
|
||||
<view class="message-preview">
|
||||
<text class="preview-text">{{
|
||||
cleanMessageText(conversation.lastMessage) || "暂无消息"
|
||||
conversation.lastMessage || "暂无消息"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
@ -60,14 +61,7 @@
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view
|
||||
v-if="!loading && conversationList.length === 0"
|
||||
class="empty-container"
|
||||
>
|
||||
<image class="empty-image" src="/static/empty.svg" mode="aspectFit" />
|
||||
<text class="empty-text">暂无会话</text>
|
||||
</view>
|
||||
<view
|
||||
v-else-if="!loading && filteredConversationList.length === 0"
|
||||
v-if="filteredConversationList.length === 0"
|
||||
class="empty-container"
|
||||
>
|
||||
<image class="empty-image" src="/static/empty.svg" mode="aspectFit" />
|
||||
@ -98,12 +92,10 @@ import useTeamStore from "@/store/team.js";
|
||||
import useInfoCheck from "@/hooks/useInfoCheck.js";
|
||||
import { globalTimChatManager } from "@/utils/tim-chat.js";
|
||||
import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.js";
|
||||
import MessageHeader from "../home/components/message-header.vue";
|
||||
import MessageHeader from "./components/message-header.vue";
|
||||
|
||||
// 获取登录状态
|
||||
const { account, openid, isIMInitialized, doctorInfo } = storeToRefs(
|
||||
useAccountStore()
|
||||
);
|
||||
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
|
||||
const { initIMAfterLogin } = useAccountStore();
|
||||
|
||||
// 获取团队信息
|
||||
@ -164,184 +156,46 @@ const handleAddPatient = withInfo(() => {
|
||||
});
|
||||
});
|
||||
|
||||
// 立即更新未读徽章
|
||||
const updateUnreadBadgeImmediately = async () => {
|
||||
try {
|
||||
if (!globalTimChatManager || !globalTimChatManager.tim) {
|
||||
console.warn("TIM实例不存在,无法更新徽章");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await globalTimChatManager.tim.getConversationList();
|
||||
|
||||
if (!response || !response.data || !response.data.conversationList) {
|
||||
console.warn("获取会话列表返回数据异常");
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算群聊总未读数
|
||||
const totalUnreadCount = response.data.conversationList
|
||||
.filter(
|
||||
(conv) => conv.conversationID && conv.conversationID.startsWith("GROUP")
|
||||
)
|
||||
.reduce((sum, conv) => sum + (conv.unreadCount || 0), 0);
|
||||
|
||||
// 更新 tabBar 徽章 - 添加错误处理,防止在非TabBar页面调用时出错
|
||||
try {
|
||||
if (totalUnreadCount > 0) {
|
||||
uni.setTabBarBadge({
|
||||
index: 1,
|
||||
text: totalUnreadCount > 99 ? "99+" : String(totalUnreadCount),
|
||||
});
|
||||
console.log("已更新 tabBar 徽章:", totalUnreadCount);
|
||||
} else {
|
||||
uni.removeTabBarBadge({
|
||||
index: 1,
|
||||
});
|
||||
console.log("已移除 tabBar 徽章");
|
||||
}
|
||||
} catch (badgeError) {
|
||||
// 在非TabBar页面上调用时会出错,这是正常的,不需要处理
|
||||
if (badgeError.errMsg && badgeError.errMsg.includes("not TabBar page")) {
|
||||
console.log("当前不是TabBar页面,跳过徽章更新");
|
||||
} else {
|
||||
console.error("更新TabBar徽章失败:", badgeError);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("更新未读徽章失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化IM
|
||||
const initIM = async () => {
|
||||
// 关键修复:不仅检查 isIMInitialized,还要检查实际连接状态
|
||||
const needsInit =
|
||||
!isIMInitialized.value ||
|
||||
!globalTimChatManager ||
|
||||
!globalTimChatManager.isLoggedIn;
|
||||
|
||||
if (needsInit) {
|
||||
if (!isIMInitialized.value) {
|
||||
uni.showLoading({
|
||||
title: "连接中...",
|
||||
});
|
||||
|
||||
// 如果已初始化但连接断开,先清理旧实例
|
||||
if (
|
||||
isIMInitialized.value &&
|
||||
globalTimChatManager &&
|
||||
!globalTimChatManager.isLoggedIn
|
||||
) {
|
||||
console.log("IM已初始化但连接已断开,清理旧实例后重新初始化");
|
||||
await globalTimChatManager.cleanupOldInstance();
|
||||
}
|
||||
|
||||
const success = await initIMAfterLogin();
|
||||
uni.hideLoading();
|
||||
// if (!success) {
|
||||
// uni.showToast({
|
||||
// title: "IM连接失败,请重试",
|
||||
// icon: "none",
|
||||
// });
|
||||
// return false;
|
||||
// }
|
||||
} else if (globalTimChatManager && !globalTimChatManager.isLoggedIn) {
|
||||
uni.showLoading({
|
||||
title: "重连中...",
|
||||
});
|
||||
const reconnected = await globalTimChatManager.ensureIMConnection();
|
||||
uni.hideLoading();
|
||||
|
||||
if (!success) {
|
||||
handleReloginIM();
|
||||
// // 显示重试提示
|
||||
// uni.showModal({
|
||||
// title: "IM连接失败",
|
||||
// content:
|
||||
// "连接失败,请检查网络后重试。如果IM连接失败,请重新登陆IM再连接",
|
||||
// confirmText: "重新登陆",
|
||||
// cancelText: "取消",
|
||||
// success: (res) => {
|
||||
// if (res.confirm) {
|
||||
// // 重新登陆
|
||||
// handleReloginIM();
|
||||
// }
|
||||
// },
|
||||
// });
|
||||
// return false;
|
||||
if (!reconnected) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 重新登陆IM
|
||||
const handleReloginIM = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: "重新登陆中...",
|
||||
});
|
||||
|
||||
// 清理旧的IM实例
|
||||
if (globalTimChatManager) {
|
||||
await globalTimChatManager.cleanupOldInstance();
|
||||
}
|
||||
|
||||
// 重新初始化IM
|
||||
const { initIMAfterLogin } = useAccountStore();
|
||||
const success = await initIMAfterLogin();
|
||||
uni.hideLoading();
|
||||
|
||||
if (success) {
|
||||
await loadConversationList();
|
||||
setupConversationListener();
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error("重新登陆IM失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载会话列表
|
||||
const loadConversationList = async () => {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
if (!doctorInfo.value?.userid) {
|
||||
console.warn("userId 未获取,跳过加载会话列表");
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
console.log("开始加载群聊列表");
|
||||
|
||||
// 确保 IM 已连接
|
||||
if (!globalTimChatManager) {
|
||||
throw new Error("IM管理器未初始化");
|
||||
if (!globalTimChatManager || !globalTimChatManager.getGroupList) {
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 TIM 实例是否存在
|
||||
if (!globalTimChatManager.tim) {
|
||||
console.warn("TIM实例不存在,尝试重新初始化IM");
|
||||
const reinitialized = await initIMAfterLogin();
|
||||
if (!reinitialized) {
|
||||
// throw new Error("IM重新初始化失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已登录 - 这是关键检查
|
||||
if (!globalTimChatManager.isLoggedIn) {
|
||||
console.warn("IM未登录,尝试重新连接");
|
||||
const reconnected = await globalTimChatManager.ensureIMConnection();
|
||||
if (!reconnected) {
|
||||
throw new Error("IM重新连接失败");
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalTimChatManager.getGroupList) {
|
||||
throw new Error("IM管理器方法不可用");
|
||||
}
|
||||
|
||||
// 添加超时控制,防止永久等待
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(
|
||||
() => reject(new Error("加载会话列表超时,请检查网络连接")),
|
||||
35000
|
||||
);
|
||||
});
|
||||
|
||||
const result = await Promise.race([
|
||||
globalTimChatManager.getGroupList(),
|
||||
timeoutPromise,
|
||||
]);
|
||||
|
||||
const result = await globalTimChatManager.getGroupList();
|
||||
if (result && result.success && result.groupList) {
|
||||
// 合并后端群组详细信息(已包含格式化和排序)
|
||||
conversationList.value = await mergeConversationWithGroupDetails(
|
||||
@ -364,26 +218,17 @@ const loadConversationList = async () => {
|
||||
);
|
||||
} else {
|
||||
console.error("加载群聊列表失败:", result);
|
||||
throw new Error(result?.message || "加载失败,请重试");
|
||||
uni.showToast({
|
||||
title: "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载会话列表失败:", error);
|
||||
|
||||
// 如果是超时或连接错误,提示用户重试
|
||||
if (
|
||||
error.message &&
|
||||
(error.message.includes("超时") || error.message.includes("连接"))
|
||||
) {
|
||||
uni.showToast({
|
||||
title: "网络连接不稳定,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: error.message || "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
uni.showToast({
|
||||
title: error.message || "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -544,12 +389,6 @@ const setupConversationListener = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 清理消息文本(移除换行符)
|
||||
const cleanMessageText = (text) => {
|
||||
if (!text) return "";
|
||||
return text.replace(/[\r\n]+/g, " ").trim();
|
||||
};
|
||||
|
||||
// 格式化患者姓名
|
||||
const formatPatientName = (conversation) => {
|
||||
return conversation.patientName || "未知患者";
|
||||
@ -631,9 +470,7 @@ const handleClickConversation = (conversation) => {
|
||||
|
||||
// 跳转到聊天页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/message/index?conversationID=${encodeURIComponent(
|
||||
conversation.conversationID
|
||||
)}&groupID=${encodeURIComponent(conversation.groupID)}`,
|
||||
url: `/pages/message/index?conversationID=${encodeURIComponent(conversation.conversationID)}&groupID=${encodeURIComponent(conversation.groupID)}`,
|
||||
});
|
||||
};
|
||||
|
||||
@ -669,7 +506,7 @@ onShow(async () => {
|
||||
// 加载团队列表
|
||||
await getTeams();
|
||||
|
||||
// 初始化IM - 关键修复:确保IM连接状态正确
|
||||
// 初始化IM
|
||||
const imReady = await initIM();
|
||||
if (!imReady) {
|
||||
console.error("IM初始化失败");
|
||||
@ -845,10 +682,7 @@ onHide(() => {
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
|
||||
@ -19,24 +19,13 @@
|
||||
</text>
|
||||
<text v-else class="user-name text-black text-lg font-semibold" @click="editProfile()">请完善信息</text>
|
||||
<view class="flex items-center mt-5">
|
||||
<view v-if="!doctorInfo || !doctorInfo.anotherName"
|
||||
class="mr-10 flex items-center bg-warning text-white text-center px-10 rounded-full"
|
||||
<view v-if="!doctorInfo || !doctorInfo.anotherName" class="status-tag tag-orange mr-10"
|
||||
@click="editProfile()">
|
||||
<view class="mr-5 pb-2 text-sm leading-normal text-white">信息待完善</view>
|
||||
<view class="translate-y--1">
|
||||
<uni-icons type="right" size="12" color="#fff" />
|
||||
</view>
|
||||
<text class="tag-text text-white">信息待完善</text>
|
||||
</view>
|
||||
<view v-if="certStatus" class="flex items-center px-10 rounded-full" :class="certStatus.classnames"
|
||||
<view v-if="certStatus" class="px-10 py-3 text-sm rounded-full" :class="certStatus.classnames"
|
||||
@click.stop="handleCert()">
|
||||
<view class="text-sm leading-normal">{{ certStatus.text }}</view>
|
||||
<view v-if="certStatus.text === '未认证'" class="translate-y--1">
|
||||
<uni-icons type="right" size="12" color="#999" />
|
||||
</view>
|
||||
<view v-else-if="certStatus.text === '认证失败'" class="translate-y--1">
|
||||
<uni-icons type="right" size="12" color="#fff" />
|
||||
</view>
|
||||
|
||||
{{ certStatus.text }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -393,12 +382,4 @@ useShow(async () => {
|
||||
padding-top: 6rpx;
|
||||
padding-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.pb-2 {
|
||||
padding-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.translate-y--1 {
|
||||
transform: translateY(-2rpx);
|
||||
}
|
||||
</style>
|
||||
@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<scroll-view class="h-full bg-white" scroll-y="true">
|
||||
<view class="p-15 text-base text-dark leading-normal" style="white-space: pre-wrap;">
|
||||
{{ content }}
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
|
||||
import privacy from './privacy-policy.js';
|
||||
import userAgreement from './user-agreement.js';
|
||||
|
||||
const content = ref('');
|
||||
|
||||
onLoad((options) => {
|
||||
if (options.type === 'privacyPolicy') {
|
||||
content.value = privacy;
|
||||
uni.setNavigationBarTitle({
|
||||
title: '隐私政策'
|
||||
})
|
||||
} else if (options.type === 'userAgreement') {
|
||||
content.value = userAgreement;
|
||||
uni.setNavigationBarTitle({
|
||||
title: '用户协议'
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@ -27,8 +27,8 @@
|
||||
>
|
||||
<checkbox :checked="checked" style="transform: scale(0.7)" />
|
||||
<view class="text-sm text-gray">我已阅读并同意</view>
|
||||
<view class="text-sm text-primary" @click.stop="toAggreement('userAgreement')">《用户协议》、</view>
|
||||
<view class="text-sm text-primary" @click.stop="toAggreement('privacyPolicy')">《隐私政策》</view>
|
||||
<view class="text-sm text-primary">《用户协议》、</view>
|
||||
<view class="text-sm text-primary">《隐私政策》</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -76,12 +76,6 @@ function toHome() {
|
||||
});
|
||||
}
|
||||
|
||||
function toAggreement(type) {
|
||||
uni.navigateTo({
|
||||
url: `/pages/login/agreement?type=${type}`
|
||||
})
|
||||
}
|
||||
|
||||
async function getPhoneNumber(e) {
|
||||
const phoneCode = e && e.detail && e.detail.code;
|
||||
// if (e && !phoneCode) return;
|
||||
|
||||
@ -1,166 +0,0 @@
|
||||
export default `
|
||||
隐私政策
|
||||
|
||||
欢迎您访问健康柚平台!
|
||||
您(下称“患者”、“用户”)在健康柚平台使用我们的服务或产品时,我们可能会收集您的相关个人信息。健康柚深知个人信息对您的重要性,并会尽全力保护您的个人信息安全可靠。我们致力于维持您对我们的信任,恪守以下原则,保护您的个人信息:权责一致原则、目的明确原则、选择同意原则、最少够用原则、确保安全原则、主体参与原则、公开透明原则等。同时,健康柚承诺,我们将按业界成熟的安全标准,采取相应的安全保护措施来保护您的个人信息。
|
||||
为此,我们制定本隐私政策,适用于我们为患者提供的产品和服务,包括但不限于健康柚微信小程序、柚助手微信小程序。
|
||||
在使用健康柚平台提供的产品或服务前,请您务必认真仔细阅读并确认充分理解本隐私政策,在确认充分理解并同意后再开始使用。一旦您主动选择确认本隐私政策并继续使用的,即视为同意本隐私政策的全部内容;如您不同意相关协议或其中的任何条款的,您应停止访问健康柚平台或使用健康柚产品和服务。
|
||||
如您是未成年人,请您和您的监护人仔细阅读本政策,并在征得您的监护人授权同意的前提下使用我们的服务或向我们提供个人信息。
|
||||
|
||||
本隐私政策将帮助您了解以下内容:
|
||||
1、我们如何收集和使用您的个人信息
|
||||
2、我们如何使用Cookie和同类技术
|
||||
3、我们如何共享、转移、公开披露您的个人信息
|
||||
4、我们如何保存和保护您的个人信息
|
||||
5、您的权利
|
||||
6、我们如何处理未成年人的个人信息
|
||||
7、您的个人信息如何在全球范围转移
|
||||
8、本隐私政策更新及通知
|
||||
9、如何联系我们
|
||||
10、争议解决
|
||||
一、我们如何收集和使用您的个人信息
|
||||
个人信息是指以电子或其他方式记录的与已识别或者可识别的自然人有关的各种信息,不包括匿名化处理后的信息。
|
||||
我们仅会出于本政策所述的以下目的,收集和使用您的个人信息,当我们要将信息用于本政策未载明的其他用途时,会事先征求您的同意。
|
||||
(一) 注册成为用户
|
||||
健康柚平台提供的服务或产品是基于注册用户使用的,如您希望使用健康柚平台提供的服务或产品,则需要通过以下步骤完成账号注册。
|
||||
创建健康柚账户,我们将提供手机号码授权登陆方式,您需要提供您的手机号码。如不提供上述注册信息,您无法使用需注册成为健康柚平台用户方可使用的服务。
|
||||
(二) “成员档案管理”服务
|
||||
用户在使用“成员档案管理”服务时,需先添加成员信息,包括您的姓名、性别、年龄、与成员关系(本人/子女/父母/其他等),目的是协助医生对患者进行管理。
|
||||
(三) “我的服务团队”服务
|
||||
用户在“成员档案管理”中添加成员信息后,使用“团队”服务时,我们可能会收集在健康柚其他平台已与患者建立关系的服务团队信息,目的是与服务团队建立联系,以确保成员聊天咨询的连续性和准确性。我们还可能收集您的登记信息(姓名、身份证号码、性别、年龄),检查检验报告、用药记录、过敏史等个人健康生理信息,以及与个人身体健康状况相关的身高、体重信息,用于了解您的健康状况和咨询需求。如不收集这类信息,我们将无法为您提供健康咨询相关的服务,但不影响您使用其他服务。
|
||||
您与团队建立服务关系后,您理解并同意将添加的个人信息、病历信息、就诊记录将向该团队展示;
|
||||
(四) “回访”服务
|
||||
在您的服务团队人员对您进行回访的过程中,我们可能收集您与团队人员的沟通聊天记录、为您制定的回访计划、向您发送的文章,以及您填写的问卷信息。以支持您在健康柚平台上获得持续、可追溯的回访服务。
|
||||
(五) 客户服务
|
||||
当您向我们提出问题、投诉或建议时,我们需要收集您的通信/通话记录、您提供的联系方式信息、您为了证明相关事实提供的信息以及您参与问卷调查时向我们发送的问卷答复信息。我们收集上述信息的法律依据是基于向您提供健康柚平台服务所必需,为您解决您在使用平台及享受服务过程中所遇到的问题,以及向您提供相关问题的处理方案和结果。如不收集这类信息,您的投诉、建议和反馈将无法得到及时、有效处理,但不影响您使用其它服务。
|
||||
(六) 保障功能运行和风控服务
|
||||
为保障您正常使用我们及我们关联公司、合作伙伴提供的服务,维护我们系统基础功能的正常运行,拦截钓鱼网站、欺诈,防止网络漏洞、计算机病毒、网络攻击、网络侵入,改进及优化我们的服务体验以及保障您的账号安全,我们需要整合我们已根据本隐私政策合法收集的您的个人基本信息(姓名、身份证号码、手机号码、性别、年龄)、个人生理健康信息(既往病史、用药记录、体重),并收集、使用或整合您的网络身份标识信息(BSSID、DNS地址、IP地址、SSID、代理信息、网络类型、网络名称、掩码信息)、个人常用设备信息(IMEI、IMSI、设备ID、MAC地址、IDFA、IDFV、AndroidId、MCC、MNC、UUID、标准国家码、操作系统信息、Cookie启用状态、重力传感、陀螺仪传感、加速度传感、已安装应用列表)、位置信息(经纬度)、人脸识别信息以及我们关联公司、合作伙伴取得您授权或依据法律共享的信息。我们收集上述信息的法律依据是基于法定义务及向您提供健康柚平台服务所必需,以综合判断您账户及交易风险、进行身份验证、检测及防范账户安全事件,并依法采取必要的记录、审计、分析、处置措施。如不收集这类信息,您将无法使用健康柚平台及健康柚平台提供的相应服务。
|
||||
(七) 我们如何使用您的信息
|
||||
1、我们会对我们提供的服务使用情况进行统计,并可能会与公众或第三方共享这些统计信息,以用于产品开发、服务优化、安全保障、数据分析等目的。但这些统计信息不包含您的任何身份识别信息。
|
||||
2、根据相关法律法规规定,以下情形中收集、使用您的个人信息无需征得您的授权同意:
|
||||
(1)为订立、履行您作为一方当事人的合同所必需;
|
||||
(2)为履行法定职责或者法定义务所必需;
|
||||
(3)为应对突发公共卫生事件,或者紧急情况下为保护自然人的生命健康和财产安全所必需;
|
||||
(4)为公共利益实施新闻报道、舆论监督等行为,在合理的范围内处理您的个人信息;
|
||||
(5)依照法律规定在合理的范围内处理您自行公开或者其他已经合法公开的个人信息;
|
||||
(6)法律、行政法规规定的其他情形。
|
||||
二、我们如何使用Cookies和同类技术
|
||||
(一) Cookies
|
||||
为确保网站正常运转、为您获得更轻松的访问体验,我们会在您的计算机或移动设备上存储名为Cookies的小数据文件。Cookies通常包含标识符、站点名称以及一些号码和字符。借助于Cookies,网站能够记住您的选择,存储您的偏好等数据。
|
||||
我们不会将Cookies用于本政策所述目的之外的任何用途。您可根据自己的偏好管理或删除Cookies。您可以清除计算机或手机上保存的所有Cookies,大部分网络浏览器都设有阻止Cookies的功能。如果您这么做,则需要在每一次访问我们的网站时亲自更改用户设置。
|
||||
第三方合作伙伴通过Cookies收集和使用您的信息不受本政策约束,而是受到其自身的信息保护声明约束,我们不对第三方的Cookies或同类技术承担责任。
|
||||
三、我们如何共享、转移、公开披露您的个人信息
|
||||
(一) 对外提供
|
||||
如您主动、自愿要求我们向第三方提供您的个人信息的,我们将基于您同意的目的,在相应页面中以适当方式告知您个人信息接收方的名称和联系方式。例如,您主动要求使用健康柚平台账户登录第三方产品或服务的,我们或第三方将在关联登录页面告知您为此目的,健康柚平台需向第三方提供的个人信息以及第三方的名称和联系方式。
|
||||
我们基于以下情况,可能会对外共享您的个人信息:
|
||||
1、在法定情形下的共享:我们可能会根据法律法规规定,或按政府主管部门的强制性要求,对外共享您的个人信息。我们为履行法定义务而向第三方提供您的个人信息的,我们将在相应页面中以适当方式告知您个人信息接收方的名称和联系信息。
|
||||
2、与关联公司间共享:我们只会共享必要的个人信息(如为便于您通过统一账号使用我们关联公司产品或服务,我们会向关联公司共享您必要的账户信息),如果我们共享您的个人敏感信息或关联公司改变个人信息的使用及处理目的,将在此就分享目的、范围、形式等必要内容征求您的授权统一。
|
||||
3、基于向您提供健康柚平台服务所必需。部分服务可能是我们的关联公司和合作机构(“授权合作伙伴”)或我们与第三方共同向您提供。因此,为向您提供健康柚平台服务,我们必需将您的个人信息提供给我们的关联公司及业务合作伙伴。例如,在某些情况下,我们必须与物流服务提供商共享您的收货信息才能安排配送。我们仅会出于合法、正当、必要、特定、明确的目的共享您的个人信息,并且只会共享提供服务所必要的个人信息。我们的合作伙伴无权将共享的个人信息用于任何其他用途。
|
||||
目前,我们的授权合作伙伴包含以下类型:
|
||||
1)技术服务供应商。我们可能会将您的个人信息共享给支持我们功能的第三方。这些支持包括为我们提供基础设施技术服务、安全保障服务、代表我们发出短信的通讯服务供应商、物流配送服务、数据处理等。我们共享这些信息的目的是可以实现我们产品或服务的功能,比如我们必须与物流服务提供商共享您的收货信息才能安排送货。
|
||||
2)分析服务类的授权合作伙伴。在征得您的许可后,我们可能将不能识别您的个人身份信息的统计或匿名信息共享给提供分析服务的合作伙伴。对于分析数据的伙伴,我们仅会向这些合作伙伴提供不能识别个人身份的统计或匿名信息。
|
||||
3)委托我们进行推广的合作伙伴。有时我们会代表其他企业向使用我们产品或服务的用户群提供促销推广的服务。我们可能会使用您的个人信息以及您的非个人信息集合形成的间接用户画像与委托我们进行推广的合作伙伴(“委托方”)共享,但我们仅会向这些委托方提供推广的覆盖面和有效性的信息,而不会提供您的个人身份信息,或者我们将这些信息进行汇总,以便它不会识别您个人。比如我们可以告知该委托方有多少人看了他们的推广信息,或者向他们提供不能识别个人身份的统计信息,帮助他们了解其受众或顾客。对我们与之共享个人信息的公司、组织和个人,我们会与其签署严格的保密协定,要求他们按照我们的说明、本隐私政策以及其他任何相关的保密和安全措施来处理个人信息。
|
||||
4)医疗技术与药物研发合作伙伴。在对您的个人信息进行去标识化处理、统计后,我们可能会向开展医疗技术与药物研发的合作伙伴提供相关去标识化之后的信息。我们将与我们的合作伙伴签署严格的保密协议,要求他们采取严格的保密和安全措施,仅为医疗技术与药物研发目的处理该等去标识化后的信息,并禁止其采取任何技术手段尝试利用该等信息重新识别您的身份。
|
||||
4、设备权限调用及SDK
|
||||
我们将审慎评估关联方、第三方数据使用共享信息的目的,对这些合作方的安全保障能力进行综合评估,并要求其遵循合作法律协议。我们会对合作方获取信息的软件工具开发包(SDK)、应用程序接口(API)进行严格的安全监测,以保护数据安全。
|
||||
(二) 转移
|
||||
1、您如果需要将您的个人信息转移至您指定的第三方的,您可以通过本政策载明的方式联系我们,在符合法律法规规定的条件下,我们将逐一处理和响应;
|
||||
2、在涉及合并、分立、清算、资产或业务的收购或出售等交易原因需要转移您的个人信息,我们将向您告知接收方的名称或者姓名和联系方式,并促使接收方继续履行个人信息保护义务。接收方变更原先的处理目的、处理方式的,应当依法规定重新取得您的同意,或具备其他合法事由。
|
||||
(三) 公开披露
|
||||
我们原则上不会公开披露您的个人信息,以下情况除外:
|
||||
1、获得您的单独同意后;
|
||||
2、基于法律的披露:在法律、法律程序、诉讼或政府主管部门强制性要求的情况下,我们可能会公开披露您的个人信息。
|
||||
3、在符合法律法规的前提下,当我们收到上述披露信息的请求时,我们会要求必须出具与之相应的法律文件,如传票或调查函。我们坚信,对于要求我们提供的信息,应该在法律允许的范围内尽可能保持透明。
|
||||
(四) 共享、转移、公开披露个人信息时事先征得授权同意的例外
|
||||
在以下情形中,共享、转移、公开披露您的个人信息无需事先征得您的授权同意:
|
||||
1、为履行法定职责或者法定义务所必需;
|
||||
2、为应对突发公共卫生事件,或者紧急情况下为保护自然人的生命健康和财产安全所必需;
|
||||
3、为公共利益实施新闻报道、舆论监督等行为,在合理的范围内处理个人信息;
|
||||
4、依照本法规定在合理的范围内处理个人自行公开或者其他已经合法公开的个人信息;
|
||||
5、法律、行政法规规定的其他情形。
|
||||
6、已经匿名化处理的您的个人信息,指经过处理无法识别特定自然人且不能复原。
|
||||
四、我们如何保存和保护您的个人信息
|
||||
(一) 个人信息的保存
|
||||
1、保存期限:如您删除或通过系统设置拒绝我们对您的个人信息进行收集,或者在您申请注销账号经核实身份注销后,我们将停止使用并删除或匿名化处理您的个人信息。我们的个人信息保存期限为实现目的所需及法律法规要求的最短时间,但法律法规另有规定或者您另行授权同意的除外。
|
||||
2、保存地域:上述信息将存储于中华人民共和国境内。如需跨境传输,我们将会在符合国家对于信息出境的相关法律规定情况下,另行单独征得您的授权同意。
|
||||
(二) 个人信息的保护
|
||||
1、安全措施
|
||||
1)我们已使用符合业界标准的安全防护措施保护您提供的个人信息,防止数据遭到未经授权访问、公开披露、使用、修改、损坏或丢失。我们会采取一切合理可行的措施,保护您的个人信息。
|
||||
2)我们会使用加密技术确保数据的安全;我们会使用受信赖的保护机制防止数据遭到恶意攻击。
|
||||
3)我们已部署访问控制机制,确保只有授权人员才可访问个人信息;我们会与接触您个人信息的员工、合作伙伴签署保密协议,明确岗位职责及行为准则,确保只有授权人员才可访问个人信息,并对此进行审查。若有违反保密协议的行为,会被追究相关责任。
|
||||
4)我们会举办安全和隐私保护培训课程,加强员工对于保护个人信息重要性的认识。
|
||||
2、安全提醒
|
||||
1)互联网并非绝对安全的环境,我们强烈建议您不要通过电子邮件、即使通讯及与其他用户交流等未加密的方式发送个人信息。请登陆时使用手机验证码,协助我们保证您的账号安全。
|
||||
2)请使用复杂密码,协助我们保证您的账号安全。我们将尽力保障您发送给我们的任何信息的安全性。如果我们的物理、技术、或管理防护设施遭到破坏,导致信息被非授权访问、公开披露、篡改、或毁坏,导致您的合法权益受损,我们将承担相应的法律责任。
|
||||
3)您在使用健康柚平台及服务时,请谨慎发表、上传可能会涉及您或他人隐私的信息,也勿将该等信息通过健康柚平台的服务传播给他人,若因您该等行为引起您或他人的隐私泄露,由您自行承担责任。
|
||||
4)请勿在使用健康柚平台服务时公开透露自己的各类财产账户、银行卡、信用卡、第三方支付账户及对应密码等重要资料,否则由此带来的损失由您自行承担责任。
|
||||
5)健康柚平台一旦发现假冒、仿冒、盗用他人名义进行平台认证的,健康柚有权立即删除用户信息并有权在用户提供充分证据前禁止其使用平台服务。
|
||||
3、安全事件通知
|
||||
1)我们会制定相应的网络安全事件应急预案,及时处置系统漏洞、计算机病毒、网络攻击、网络侵入等安全风险,在发生危害网络安全的事件时,我们会立即启动应急预案,采取相应的补救措施。
|
||||
2)在不幸发生个人信息安全事件后,我们将按照法律法规的要求,及时向您告知:安全事件的基本情况和可能的影响、我们已采取或将要采取的处置措施、您可自主防范和降低风险的建议、对您的补救措施等。我们将及时将事件相关情况以邮件、信函、电话、推送通知等方式告知您,难以逐一告知个人信息主体时,我们会采取合理、有效的方式发布公告。
|
||||
同时,我们还将按照监督部门要求,主动上报个人信息安全事件的处置情况。
|
||||
请您理解,根据法律法规的规定,如果我们采取的措施能够有效避免信息泄露、篡改、丢失造成危害的,除非监管部门要求向您通知,我们可以选择不向您通知该个人信息安全事件。
|
||||
3)如您发现自己的个人信息泄密,尤其是您的账户及密码发生泄露,请您立即通过健康柚平台或本隐私政策提供的联系方式联络我们,以便我们采取相应措施。
|
||||
五、您的权利
|
||||
按照中国相关的法律、法规、标准,以及其他国家、地区的通行做法,我们保障您对自己的个人信息行使以下权利:
|
||||
(一) 访问您的个人信息
|
||||
您有权访问您的个人信息,法律法规规定的例外情况除外。如果您想行使数据访问权,可以通过以下方式自行访问:
|
||||
档案信息:小程序中,您可以通过【档案管理】中新增、查阅、删除您的档案信息。
|
||||
咨询记录:小程序中,您可以通过【咨询】列表查阅历史咨询记录;
|
||||
问卷信息:小程序中,您可以通过【我的问卷】查阅历史填写的问卷记录;
|
||||
(二) 更正您的个人信息
|
||||
当您发现我们处理的关于您的个人信息有错误时,您有权通过客服提出更正申请。
|
||||
(三) 删除您的个人信息
|
||||
如果您决定不再使用我们平台,需要注销账户请联系客服,进入个人中心扫描客户二维码。
|
||||
(四) 改变您授权同意的范围
|
||||
您可以通过解除绑定、删除信息、关闭设备功能、修改个人设置、联系客服等方式改变您授权我们继续收集个人信息的范围或随时撤回您的授权(包括对第三方共享信息授权)。
|
||||
(五) 注销帐号
|
||||
您随时可注销此前注册的账户,您可以通过客服向我们申请注销和删除您的信息。
|
||||
在注销账户之后,我们将停止为您提供产品或服务,并依据您的要求,删除您的个人信息,或进行匿名化处理,法律法规另有规定的除外。
|
||||
(六) 个人信息主体获取个人信息副本
|
||||
您有权复制我们收集的您的个人信息。在法律法规规定的条件下,如果技术可行,您也可以要求我们将您的个人信息转移至您指定的其他主体。您可以通过以下方式自行操作:通过客服与我们联系,我们将在15个工作日内对您的请求进行处理。
|
||||
(七) 约束信息系统自动决策
|
||||
在某些业务功能中,我们可能仅依据信息系统、算法等在内的非人工自动决策机制做出决定。如果这些决定显著影响您的合法权益,您有权拒绝并要求我们做出解释,我们将提供适当的救济方式。
|
||||
(八) 响应您的上述请求
|
||||
为保障安全,我们可能会先验证您的身份,然后再处理您的请求。您可能需要提供书面请求,或以其他方式证明您的身份。验证通过后,对于您的请求,我们原则上将于15个工作日内做出答复。
|
||||
对于您合理的请求,我们原则上不收取费用,但对多次重复、超出合理限度的请求,我们将视情收取一定成本费用。对于那些无端重复、需要过多技术手段(例如,需要开发新系统或从根本上改变现行惯例)、给他人合法权益带来风险或者非常不切实际(例如,涉及备份磁带上存放的信息)的请求,我们可能会予以拒绝。
|
||||
在以下情形中,按照法律法规要求,我们将无法响应您的请求:
|
||||
1、与我们履行法律法规规定的义务相关的;
|
||||
2、与国家安全、国防安全直接相关的;
|
||||
3、与公共安全、公共卫生、重大公共利益直接相关的;
|
||||
4、与犯罪侦查、起诉、审判和判决执行等直接相关的;
|
||||
5、有充分证据表明您存在主观恶意或滥用权利的;
|
||||
6、响应您的请求将导致您或其他个人、组织的合法权益受到严重损害的;
|
||||
7、涉及商业秘密的。
|
||||
(九) 获得解释的权利
|
||||
您有权要求我们就个人信息处理规则作出解释说明。您可以通过第九部分中的联系方式与我们取得联系。
|
||||
六、我们如何处理未成年人的个人信息
|
||||
6.1如果没有父母或其他监护人的统一,儿童不得创建自己的用户账户。如您为儿童的,我们要求您请您的父母或其他监护人仔细阅读本政策,并在征得您的父母或其他监护人同意的前提下使用我们的服务或产品或向我们提供信息。
|
||||
6.2对于经父母或其他监护人同意使用我们的服务或产品而收集儿童个人信息的情况,我们只会在法律法规允许、父母或其他监护人明确同意或者保护儿童所必要的情况下使用、共享、转让或披露此信息。
|
||||
七、您的个人信息如何在全球范围转移
|
||||
我们在中华人民共和国境内运营中收集和产生的个人信息,储存在中国境内,一下情形除外:
|
||||
1. 法律法规有明确规定。
|
||||
2. 获得您的明确授权且经过国家安全相关审查的。
|
||||
针对以上情形,我们会确保依据本政策对您的个人信息提供足够的保护。
|
||||
八、本隐私政策更新及通知
|
||||
我们的隐私政策可能变更。
|
||||
未经您明确同意,我们不会削减您按照本隐私政策所应享有的权利。我们会在本页面上发布对本政策所做的任何变更并取得您的同意。
|
||||
对于重大变更,我们可能还会提供更为显著的通知(包括对于某些服务,我们会通过电子邮件、站内信、短信、小程序服务通知、公众号通知、弹窗等方式发送通知,说明隐私政策的具体变更内容)。
|
||||
本政策重大变更包括但不限于:
|
||||
1、我们的服务模式发生重大变化。如处理个人信息的目的、处理的个人信息类型、个人信息的使用方式等;
|
||||
2、我们在所有权结构、组织架构等方面发生重大变化。如业务调整、破产并购等引起的所有者变更等;
|
||||
3、个人信息共享、转移或公开披露的主要对象发生变化;
|
||||
4、您参与个人信息处理方面的权利及其行使方式发生重大变化;
|
||||
5、我们负责处理个人信息安全的责任部门、联络方式及投诉渠道发生变化;
|
||||
6、个人信息安全影响评估报告表明存在高风险时。
|
||||
我们还会将本政策的旧版本存档,供您查阅。
|
||||
九、如何联系我们
|
||||
如您对本政策内容有任何疑问、意见或建议,或发现个人信息可能被泄露的,您可以通过以下方式与我们联系,一般情况下我们将在15个工作日内回复您的请求。可以通过“我的-联系客服”联系我们。或邮寄至下列地址:
|
||||
公司名称:杭州柚康科技有限公司
|
||||
法定代表人:王荣波
|
||||
联系地址:中国浙江省杭州西湖区塘苗路1号,2号楼4楼。
|
||||
十、争议解决
|
||||
因本政策以及我们处理您个人信息事宜引起的任何争议,您可随时联系我公司个人信息保护相关负责人要求给出回复,如果您对我们的回复不满意的,认为我们的个人信息处理行为严重损害了您的合法权益的,您还可以通过向本隐私政策服务提供商健康柚所在地【杭州市】有管辖权的人民法院提起诉讼来寻求解决方案。
|
||||
感谢您对健康柚平台以及健康柚产品和服务的信任和使用!
|
||||
|
||||
`
|
||||
@ -1,79 +0,0 @@
|
||||
export default `
|
||||
用户注册服务协议
|
||||
|
||||
尊敬的用户:
|
||||
为了保障您的权益,请您在使用健康柚平台各项服务(下称"本服务")前,详细阅读此用户注册服务协议(下称"本协议")。如您不同意本协议中的任何条款或对本协议存在质疑,请您停止使用本服务;如您已经开始或正在使用本服务,即表示您已阅读并同意本协议全部内容。本协议对您与健康柚平台服务提供者(下称"我们")具有同等法律效力。
|
||||
提示条款:
|
||||
请您务必审慎阅读、充分理解各条款内容,特别是免除或限制责任的相应条款,以及开通或使用某项服务的单独协议,限制或免除责任条款将以加粗形式提示您注意。当您点击【已阅读并同意《用户注册服务协议》,即表示您已充分阅读、理解并接受本协议全部内容。
|
||||
您确认,在您开始使用本服务前,您应具备中华人民共和国法律规定的与您行为相适应的民事行为能力。如您未满18周岁或是其他限制行为能力人或无民事行为能力人,请您在监护人的监护下阅读并遵守本协议,并在监护人的指导下使用本服务。如您不具备前述与您行为相适应的民事行为能力,则您及您的监护人应依照法律规定承担因此而导致的一切责任。
|
||||
一、注册和登录
|
||||
1.1您可通过手机号码注册成为健康柚平台的正式用户。在注册过程中,您的用户名注册与使用应符合网络道德,遵守中华人民共和国法律法规。您的用户名和昵称中不能含有威胁、淫秽、谩骂、非法、侵害他人正当权益等有争议性的文字。如发现您的账号中含有不雅文字或不恰当等名称,我们有权要求您更改、不予注册或收回账号的权利。
|
||||
1.2您应对自己的账户和密码的安全负责。您利用本账户和密码所进行的一切活动引起的任何损失,均由您自行承担全部责任。如您发现账号遭到未授权的使用或发生任何其他安全问题,应立即修改账号密码并妥善保管,如有必要,请立即联系我们。除非有法律规定或司法裁定,否则,您的账户不得以任何方式转让、赠与、继承(符合《个人信息保护法》等相关法律规定的除外)、借用,否则您应自行承担由此产生的全部责任。
|
||||
1.3您应保证:提供详尽、真实、准确和完整的个人资料以符合实名认证的要求。如果资料发生变动,您应及时更改。若您提供任何错误、不实、过时或不完整的资料,并为我们所确知,或者我们有合理理由怀疑前述资料为错误、不实、过时或不完整的资料,我们有权暂停或终止对您的账号提供服务,并拒绝现在或将来申请使用本服务的全部或一部分的请求。在此情况下,您可通过我们的申诉途径与我们取得联系并修正个人资料,经我们核实后恢复账号使用。
|
||||
二、用户责任
|
||||
2.1我们运用自己的操作系统通过互联网为您提供互联网电子服务或商品,并承担本协议和其它服务协议中对您的责任和义务。为使用本服务,您必须能够自行通过有法律资格的第三方对您提供互联网接入服务,并自行承担以下内容:
|
||||
(1)自行配备上网所需的设备,包括个人电脑,调制解调器及其他必要的设备装置。
|
||||
(2)自行承担上网所需的相关必要费用,如:电话费用、网络费用等。
|
||||
(3)本协议中规定的您的其他责任和义务。
|
||||
2.2您在使用本服务过程中,必须遵循以下原则,如因您违反相关法律、法规或本协议的规定给我们、医师或第三方造成任何损失的,您统一承担由此产生的损害赔偿责任,其中包括但不限于我们为此而支付的律师费用、公证费用、公告费用、检测费用、鉴定费用、诉讼费用。
|
||||
(1)遵守中华人民共和国有关的法律法规、社会道德规范及公序良俗;
|
||||
(2)遵守所有与本服务有关的网络协议、规定、程序和惯例;
|
||||
(3)不得因任何非法目的而使用本服务;不得利用本服务进行任何可能对互联网的正常运转造成不利影响的行为;
|
||||
(4)不得填写、发布、传输任何非法的、违反公序良俗的、虚假的、骚扰性的、中伤他人的信息资料;不得发布介绍个人、科室等广告性质的内容;不得利用本服务进行任何损害我们或第三方合法权益的行为;
|
||||
(5)不得侵犯任何第三方的合法知识产权;
|
||||
(6)不得擅自更改医师处方、隐瞒过敏史;
|
||||
(7)不得利用健康柚平台从事洗钱、窃取商业秘密、窃取个人信息等违法犯罪活动;
|
||||
(8)不得干扰健康柚平台的正常运转(如恶意投诉医师或平台),不得侵入健康柚平台及国家计算机信息系统;
|
||||
(9)不得教唆他人从事本条所禁止的行为。
|
||||
您单独承担在健康柚平台上发布内容的一切相关责任。如您违反以上原则,我们有权随时中断或终止向您提供本协议项下的服务而无需对您或患者承担任何责任。
|
||||
2.3我们不接受用户线上咨询包括但不限于以下问题:
|
||||
(1)非健康类问题,如社会意识形态问题等;
|
||||
(2)医疗司法举证或询证问题;
|
||||
(3)胎儿性别鉴定问题;
|
||||
(4)未按提问要求提问,如提问时未指定医师,却要求具体医师回复;
|
||||
(5)有危害他人/自己的问题;
|
||||
(6)涉及医师个人信息问题;
|
||||
(7)故意挑逗、侮辱医师的提问;
|
||||
(8)其他可能危害国家公共安全、违反社会公共秩序、违背公序良俗、侵犯他人合法权益或者损害公共利益的问题。
|
||||
2.4您从中国境内向外传输技术性资料时必须符合中国有关法律法规的规定。
|
||||
2.5您的授权行为:对我们而言,您的帐号和密码是唯一验证您真实性的依据,只要使用了正确的您的账号和密码,无论是谁登录均视为已经得到您本人的授权。
|
||||
2.6您同意您勾选知情同意书选项或采纳医师建议即视为风险提示已告知并获得您的知情同意。
|
||||
2.7您的授权行为:用户同意授权我们获取患者数据,并为患者服务的目的按照最小影响原则使用就诊数据,包括用户在其他实体医疗机构的数据,请您慎重考虑。
|
||||
三、用户管理
|
||||
3.1我们保留在中华人民共和国大陆地区施行之法律允许的范围内独自决定拒绝服务、关闭用户账户、清除或编辑内容或取消订单的权利。
|
||||
3.2本服务不会提供给被暂时中止或永久终止资格的健康柚平台用户。
|
||||
3.3鉴于移动互联网服务的特殊性,我们有权随时变更、中止或终止部分或全部的服务。如变更、中止或终止的服务属于免费服务,我们无需通知您,也无需对您或任何第三方承担任何责任。
|
||||
3.4您理解,我们需要定期或不定期地对提供本服务的平台或相关的设备进行检修或者维护,如因此类情况而造成本服务在合理时间内的中断,我们无需为此承担任何责任,但我们将通过平台提前发布通知。
|
||||
3.5我们不对您所发布信息的删除或储存失败负责。我们积极采用数据备份加密等措施保障您数据的安全,但不对由于因意外因素导致的数据损失和泄漏负责。我们有权审查和监督您的行为是否符合本协议的要求,如果您违背了本协议的约定,则我们有权中断您的服务。
|
||||
4.6若您的行为不符合本协议的规定,我们有权做出独立判断,并立即停止向您的帐号提供服务。您需对自己在网上的行为承担法律责任。您若在健康柚平台上散布和传播反动、色情或其他违反国家法律、法规的信息,我们的系统记录有可能作为您违反法律的证据。
|
||||
四、责任限制
|
||||
4.1您同意因下列情形之一的,我们不承担任何责任:
|
||||
(1)用户或其近亲属不配合进行符合诊疗规范的诊疗;或提供信息不完整、不真实、不准确,对医师诊断产生误导影响;或未按要求披露过敏史等;
|
||||
(2)医务人员在紧急情况下已经尽到合理诊疗义务;或限于当时的医疗水平难以诊疗;
|
||||
(3)因不可抗力、病毒、木马、黑客攻击、系统不稳定、第三方服务瑕疵、政府行为等原因可能导致的服务中断、数据丢失以及其他的损失和风险;
|
||||
(4)因用户不正当使用网络服务、私自在网上进行交易、非法使用网络服务或传送的信息有所变动而受到的损害;
|
||||
(5)其他法律法规规定应当免责的情形。
|
||||
4.2健康柚所有健康资讯仅供参考。健康柚致力于提供正确、完整的健康资讯,但不保证信息的绝对正确性和完整性,且不对因信息的不正确或遗漏导致的任何损失或损害承担责任。健康柚所提供的任何健康资讯不能替代医师和其他医务人员的建议,如自行使用健康柚资料发生偏差,健康柚不承担任何法律责任。
|
||||
4.3用户知晓并同意自开始使用健康柚服务时起,其就相同或类似服务将不与健康柚签约医师在健康柚外达成任何形式的约定/协议。如果用户与健康柚签约的医师在健康柚外进行咨询或者相关交易,产生的纠纷,健康柚将不予受理、不承担任何法律责任。
|
||||
五、用户特别授权
|
||||
您授权我们使用您注册、使用本服务过程中形成的信息,并允许我们通过邮件、微信、短信、电话等形式向您传送我们的服务。您同意接受我们通过短信、邮件、电话或其他形式向您发送活动、服务或其他相关商业信息。如果您不需要我们提供的部分或全部服务的活动、服务或其他相关商业信息的服务,在您向客服提出申请后将予以中止、终止对您提供的该部分或全部服务。
|
||||
六、知识产权条款
|
||||
6.1您一旦接受本协议,即表明您主动将您在任何时间段在健康柚发表的任何形式的信息内容的财产性权利及任何可转让的权利,如著作权财产权,全部独家且不可撤销地转让给健康柚所有。
|
||||
6.2杭州柚康科技有限公司拥有健康柚平台内容及资源的著作权等合法权利,受国家法律保护,有权不时地对本协议及健康柚平台的内容进行修改,并在健康柚平台公告,无须另行通知您。在法律允许的最大限度范围内,杭州柚康科技有限公司对本协议及健康柚平台的内容拥有解释权。
|
||||
6.3除法律另有强制性规定外,未经健康柚明确的特别书面许可,任何单位或个人不得以任何方式非法地全部或部分复制、转载、引用、链接、抓取或以其他方式使用健康柚平台的信息内容,否则,健康柚有权追究其法律责任。
|
||||
七、个人信息保护
|
||||
保护您隐私是我们的基本政策。您的信任对我们非常重要,我们深知个人信息安全的重要性,并将按照法律法规要求,采取安全保护措施,保护您的个人信息安全。具体详见《隐私政策》。
|
||||
八、协议内容及修改
|
||||
8.1我们在此特别提醒您。本协议内容包括协议正文、隐私政策及所有我们已经发布或将来可能发布的各类规则、规范、通知、公告等。您确认;本协议是处理双方权利义务的契约,始终有效,法律另有强制性规定或双方另有特别约定的,依其规定。
|
||||
8.2根据国家法律法规变化及网络运营需要,我们有权不定时修订协议,如本协议有任何变更,您再次登录的时候,系统会提醒您条款变更,请您重新确定是否接受。确认接受后,条款即生效,如您不接受的,您有权终止使用本服务,如您继续使用本服务,即视为同意更新后的协议。
|
||||
九、法律管辖和适用
|
||||
9.1本协议的订立、执行和解释及争议的解决均应适用中华人民共和国大陆地区之有效法律(但不包括其冲突法规则)。
|
||||
9.2如缔约方就本协议内容或其执行发生任何争议,应首先协商解决;协商不成时,任何一方均可向被告所在地有管辖权的人民法院提起诉讼。
|
||||
十、如何联系我们
|
||||
如您对本政策内容有任何疑问、意见或建议,或发现个人信息可能被泄露的,您可以通过以下方式与我们联系,一般情况下我们将在15个工作日内回复您的请求。可以通过“我的-联系客服”联系我们。或邮寄至下列地址:
|
||||
公司名称:杭州柚康科技有限公司
|
||||
法定代表人:王荣波
|
||||
联系地址:中国浙江省杭州西湖区塘苗路1号,2号楼4楼。
|
||||
|
||||
|
||||
`
|
||||
@ -11,23 +11,16 @@ $primary-color: #0877F1;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 患者信息栏样式 - 固定在顶部 */
|
||||
/* 患者信息栏样式 */
|
||||
.patient-info-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
padding: 20rpx 32rpx;
|
||||
z-index: 100;
|
||||
z-index: 10;
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.patient-info-content {
|
||||
@ -94,12 +87,7 @@ $primary-color: #0877F1;
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
margin-top: 120rpx;
|
||||
margin-bottom: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chat-content-compressed {
|
||||
|
||||
@ -348,7 +348,7 @@ const handleNextFromProgress = (data) => {
|
||||
|
||||
// 跳转到病历填写页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/case/ai-medical-case-form?caseType=${data.caseType}&patientId=${
|
||||
url: `/pages/case/medical-case-form?caseType=${data.caseType}&patientId=${
|
||||
props.patientId
|
||||
}&groupId=${props.groupId}&formData=${encodeURIComponent(
|
||||
JSON.stringify(extractedData)
|
||||
|
||||
@ -93,7 +93,6 @@ const FIELD_LABELS = {
|
||||
inspectSummary: "体检小结",
|
||||
positiveFind: "阳性发现及处理意见",
|
||||
// 预问诊记录
|
||||
consultationDate: "问诊日期",
|
||||
presentIllnessHistory: "现病史",
|
||||
pastMedicalHistory: "既往史",
|
||||
};
|
||||
|
||||
@ -181,7 +181,7 @@ import { ref, onUnmounted, nextTick, watch, computed } from "vue";
|
||||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||||
import { storeToRefs } from "pinia";
|
||||
import useAccountStore from "@/store/account.js";
|
||||
import { globalTimChatManager } from "@/utils/tim-chat.js";
|
||||
import { globalTimChatManager, TIM } from "@/utils/tim-chat.js";
|
||||
import { handleFollowUpMessages } from "@/utils/send-message-helper";
|
||||
import {
|
||||
startIMMonitoring,
|
||||
@ -872,29 +872,12 @@ onShow(() => {
|
||||
checkLoginAndInitTIM();
|
||||
} else if (timChatManager.tim && !timChatManager.isLoggedIn) {
|
||||
timChatManager.ensureIMConnection();
|
||||
} else if (timChatManager.tim && timChatManager.isLoggedIn && chatInfo.value.conversationID) {
|
||||
// 页面从后台返回时,重新加载消息列表
|
||||
console.log("页面从后台返回,重新加载消息列表");
|
||||
messageList.value = [];
|
||||
isCompleted.value = false;
|
||||
lastFirstMessageId.value = "";
|
||||
loadMessageList();
|
||||
}
|
||||
|
||||
startIMMonitoring(30000);
|
||||
|
||||
// 监听回访任务发送事件
|
||||
uni.$on("send-followup-message", handleSendFollowUpMessage);
|
||||
|
||||
// 监听键盘高度变化,自动滚动到底部
|
||||
uni.onKeyboardHeightChange((res) => {
|
||||
if (res.height > 0) {
|
||||
// 键盘弹出,延迟滚动到底部
|
||||
setTimeout(() => {
|
||||
scrollToBottom(true);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 处理发送回访任务消息
|
||||
|
||||
796
pages/message/message.vue
Normal file
796
pages/message/message.vue
Normal file
@ -0,0 +1,796 @@
|
||||
<template>
|
||||
<view class="message-page">
|
||||
<!-- 头部组件 -->
|
||||
<message-header
|
||||
v-model:activeTab="activeTab"
|
||||
@team-change="handleTeamChange"
|
||||
@add-patient="handleAddPatient"
|
||||
/>
|
||||
<!-- 消息列表 -->
|
||||
<scroll-view
|
||||
class="message-list"
|
||||
scroll-y="true"
|
||||
refresher-enabled
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="handleRefresh"
|
||||
@scrolltolower="handleLoadMore"
|
||||
>
|
||||
<!-- 消息列表项 -->
|
||||
<view
|
||||
v-for="conversation in filteredConversationList"
|
||||
:key="conversation.groupID || conversation.conversationID"
|
||||
class="message-item"
|
||||
@click="handleClickConversation(conversation)"
|
||||
>
|
||||
<view class="avatar-container">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="conversation.avatar || '/static/default-patient-avatar.png'"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-if="conversation.unreadCount > 0" class="unread-badge">
|
||||
<text class="unread-text">{{
|
||||
conversation.unreadCount > 99 ? "99+" : conversation.unreadCount
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content">
|
||||
<view class="header">
|
||||
<view class="name-info">
|
||||
<text class="name">{{ formatPatientName(conversation) }}</text>
|
||||
<text
|
||||
v-if="conversation.patientSex || conversation.patientAge"
|
||||
class="patient-info"
|
||||
>
|
||||
{{ formatPatientInfo(conversation) }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="time">{{
|
||||
formatMessageTime(conversation.lastMessageTime)
|
||||
}}</text>
|
||||
</view>
|
||||
<view class="message-preview">
|
||||
<text class="preview-text">{{
|
||||
conversation.lastMessage || "暂无消息"
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view
|
||||
v-if="!loading && conversationList.length === 0"
|
||||
class="empty-container"
|
||||
>
|
||||
<image class="empty-image" src="/static/empty.svg" mode="aspectFit" />
|
||||
<text class="empty-text">医生信息未获取,请稍后重试</text>
|
||||
</view>
|
||||
<view
|
||||
v-else-if="
|
||||
!loading &&
|
||||
filteredConversationList.length === 0
|
||||
"
|
||||
class="empty-container"
|
||||
>
|
||||
<image class="empty-image" src="/static/empty.svg" mode="aspectFit" />
|
||||
<text class="empty-text">{{
|
||||
activeTab === "processing" ? "暂无处理中的会话" : "暂无已结束的会话"
|
||||
}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载更多 -->
|
||||
<view
|
||||
v-if="hasMore && filteredConversationList.length > 0"
|
||||
class="load-more"
|
||||
>
|
||||
<text class="load-more-text">{{
|
||||
loadingMore ? "加载中..." : "上拉加载更多"
|
||||
}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
|
||||
import { storeToRefs } from "pinia";
|
||||
import useAccountStore from "@/store/account.js";
|
||||
import useTeamStore from "@/store/team.js";
|
||||
import useInfoCheck from "@/hooks/useInfoCheck.js";
|
||||
import { globalTimChatManager } from "@/utils/tim-chat.js";
|
||||
import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.js";
|
||||
import MessageHeader from "../home/components/message-header.vue";
|
||||
|
||||
// 获取登录状态
|
||||
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
|
||||
const { initIMAfterLogin } = useAccountStore();
|
||||
|
||||
// 获取团队信息
|
||||
const teamStore = useTeamStore();
|
||||
const { getTeams } = teamStore;
|
||||
|
||||
// 信息完善检查
|
||||
const { withInfo } = useInfoCheck();
|
||||
|
||||
// 团队相关状态
|
||||
const currentTeamId = ref(""); // 空字符串表示"全部会话消息"
|
||||
|
||||
// 状态
|
||||
const conversationList = ref([]);
|
||||
const loading = ref(false);
|
||||
const loadingMore = ref(false);
|
||||
const hasMore = ref(false);
|
||||
const refreshing = ref(false);
|
||||
const activeTab = ref("processing");
|
||||
|
||||
// 根据 orderStatus 过滤会话列表
|
||||
const filteredConversationList = computed(() => {
|
||||
let filtered = [];
|
||||
|
||||
if (activeTab.value === "processing") {
|
||||
// 处理中:pending(待处理) 和 processing(处理中)
|
||||
filtered = conversationList.value.filter(
|
||||
(conv) =>
|
||||
conv.orderStatus === "pending" || conv.orderStatus === "processing"
|
||||
);
|
||||
} else {
|
||||
// 已结束:cancelled(已取消)、completed(已完成)、finished(已结束)
|
||||
filtered = conversationList.value.filter(
|
||||
(conv) =>
|
||||
conv.orderStatus === "cancelled" ||
|
||||
conv.orderStatus === "completed" ||
|
||||
conv.orderStatus === "finished"
|
||||
);
|
||||
}
|
||||
|
||||
// 如果选择了团队,进一步过滤
|
||||
if (currentTeamId.value) {
|
||||
filtered = filtered.filter((conv) => conv.teamId === currentTeamId.value);
|
||||
}
|
||||
return filtered;
|
||||
});
|
||||
|
||||
// 处理团队切换
|
||||
const handleTeamChange = (teamId) => {
|
||||
currentTeamId.value = teamId;
|
||||
console.log("切换到团队ID:", teamId);
|
||||
};
|
||||
|
||||
// 邀请患者 - 使用 withInfo 包装,确保信息完善后才能使用
|
||||
const handleAddPatient = withInfo(() => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/work/team/invite/invite-patient",
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化IM
|
||||
const initIM = async () => {
|
||||
if (!isIMInitialized.value) {
|
||||
uni.showLoading({
|
||||
title: "连接中...",
|
||||
});
|
||||
const success = await initIMAfterLogin();
|
||||
uni.hideLoading();
|
||||
|
||||
if (!success) {
|
||||
// 显示重试提示
|
||||
uni.showModal({
|
||||
title: "IM连接失败",
|
||||
content:
|
||||
"连接失败,请检查网络后重试。如果IM连接失败,请重新登陆IM再连接",
|
||||
confirmText: "重新登陆",
|
||||
cancelText: "取消",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 重新登陆
|
||||
handleReloginIM();
|
||||
}
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
} else if (globalTimChatManager && !globalTimChatManager.isLoggedIn) {
|
||||
uni.showLoading({
|
||||
title: "重连中...",
|
||||
});
|
||||
const reconnected = await globalTimChatManager.ensureIMConnection();
|
||||
uni.hideLoading();
|
||||
|
||||
if (!reconnected) {
|
||||
// 显示重试提示
|
||||
uni.showModal({
|
||||
title: "IM连接失败",
|
||||
content:
|
||||
"连接失败,请检查网络后重试。如果IM连接失败,请重新登陆IM再连接",
|
||||
confirmText: "重新登陆",
|
||||
cancelText: "取消",
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 重新登陆
|
||||
handleReloginIM();
|
||||
}
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 重新登陆IM
|
||||
const handleReloginIM = async () => {
|
||||
try {
|
||||
uni.showLoading({
|
||||
title: "重新登陆中...",
|
||||
});
|
||||
|
||||
// 清理旧的IM实例
|
||||
if (globalTimChatManager) {
|
||||
await globalTimChatManager.cleanupOldInstance();
|
||||
}
|
||||
|
||||
// 重新初始化IM
|
||||
const { initIMAfterLogin } = useAccountStore();
|
||||
const success = await initIMAfterLogin();
|
||||
uni.hideLoading();
|
||||
|
||||
if (success) {
|
||||
uni.showToast({
|
||||
title: "IM连接成功",
|
||||
icon: "success",
|
||||
});
|
||||
// 重新加载会话列表
|
||||
await loadConversationList();
|
||||
setupConversationListener();
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: "IM连接失败,请检查网络",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
uni.hideLoading();
|
||||
console.error("重新登陆IM失败:", error);
|
||||
uni.showToast({
|
||||
title: "重新登陆失败",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 加载会话列表
|
||||
const loadConversationList = async () => {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
console.log("开始加载群聊列表");
|
||||
|
||||
// 确保 IM 已连接
|
||||
if (!globalTimChatManager) {
|
||||
throw new Error("IM管理器未初始化");
|
||||
}
|
||||
|
||||
// 检查 TIM 实例是否存在
|
||||
if (!globalTimChatManager.tim) {
|
||||
console.warn("TIM实例不存在,尝试重新初始化IM");
|
||||
const reinitialized = await initIMAfterLogin();
|
||||
if (!reinitialized) {
|
||||
throw new Error("IM重新初始化失败");
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
if (!globalTimChatManager.isLoggedIn) {
|
||||
console.warn("IM未登录,尝试重新连接");
|
||||
const reconnected = await globalTimChatManager.ensureIMConnection();
|
||||
if (!reconnected) {
|
||||
throw new Error("IM重新连接失败");
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalTimChatManager.getGroupList) {
|
||||
throw new Error("IM管理器方法不可用");
|
||||
}
|
||||
|
||||
const result = await globalTimChatManager.getGroupList();
|
||||
if (result && result.success && result.groupList) {
|
||||
// 合并后端群组详细信息(已包含格式化和排序)
|
||||
conversationList.value = await mergeConversationWithGroupDetails(
|
||||
result.groupList
|
||||
);
|
||||
|
||||
console.log("=== 会话列表加载完成 ===");
|
||||
console.log("总会话数:", conversationList.value.length);
|
||||
// 打印前3个会话的 orderStatus
|
||||
conversationList.value.slice(0, 3).forEach((conv, index) => {
|
||||
console.log(
|
||||
`会话 ${index} - orderStatus: ${conv.orderStatus}, 名称: ${conv.name}`
|
||||
);
|
||||
});
|
||||
|
||||
console.log(
|
||||
"群聊列表加载成功,共",
|
||||
conversationList.value.length,
|
||||
"个会话"
|
||||
);
|
||||
} else {
|
||||
console.error("加载群聊列表失败:", result);
|
||||
uni.showToast({
|
||||
title: "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载会话列表失败:", error);
|
||||
uni.showToast({
|
||||
title: error.message || "加载失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖更新定时器
|
||||
let updateTimer = null;
|
||||
|
||||
// 设置会话列表监听,实时更新列表
|
||||
const setupConversationListener = () => {
|
||||
if (!globalTimChatManager) return;
|
||||
|
||||
// 监听会话列表更新事件
|
||||
globalTimChatManager.setCallback("onConversationListUpdated", (eventData) => {
|
||||
console.log("会话列表更新事件:", eventData);
|
||||
|
||||
// 处理单个会话更新(标记已读的情况)
|
||||
if (eventData && !Array.isArray(eventData) && eventData.conversationID) {
|
||||
const conversationID = eventData.conversationID;
|
||||
const existingIndex = conversationList.value.findIndex(
|
||||
(conv) => conv.conversationID === conversationID
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// 直接更新未读数,避免触发整个对象的响应式更新
|
||||
if (eventData.unreadCount !== undefined) {
|
||||
conversationList.value[existingIndex].unreadCount =
|
||||
eventData.unreadCount;
|
||||
console.log(
|
||||
`已清空会话未读数: ${conversationList.value[existingIndex].name}, unreadCount: ${eventData.unreadCount}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eventData || !Array.isArray(eventData)) {
|
||||
console.warn("会话列表更新事件数据格式错误");
|
||||
return;
|
||||
}
|
||||
|
||||
// 防抖处理:避免频繁更新导致闪动
|
||||
if (updateTimer) {
|
||||
clearTimeout(updateTimer);
|
||||
}
|
||||
|
||||
updateTimer = setTimeout(async () => {
|
||||
// 过滤出群聊会话
|
||||
const groupConversations = eventData.filter(
|
||||
(conv) => conv.conversationID && conv.conversationID.startsWith("GROUP")
|
||||
);
|
||||
|
||||
console.log(`收到 ${groupConversations.length} 个群聊会话更新`);
|
||||
|
||||
// 使用 TimChatManager 的格式化方法转换为标准格式
|
||||
const formattedConversations = groupConversations.map((conv) =>
|
||||
globalTimChatManager.formatConversationData(conv)
|
||||
);
|
||||
|
||||
// 合并后端群组详细信息(已包含格式化和排序)
|
||||
const mergedConversations = await mergeConversationWithGroupDetails(
|
||||
formattedConversations
|
||||
);
|
||||
|
||||
if (!mergedConversations || mergedConversations.length === 0) {
|
||||
console.log("合并后的会话数据为空,跳过更新");
|
||||
return;
|
||||
}
|
||||
let needSort = false;
|
||||
// 更新会话列表
|
||||
mergedConversations.forEach((conversationData) => {
|
||||
const conversationID = conversationData.conversationID;
|
||||
const existingIndex = conversationList.value.findIndex(
|
||||
(conv) => conv.conversationID === conversationID
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
const existing = conversationList.value[existingIndex];
|
||||
if (
|
||||
existing.lastMessage !== conversationData.lastMessage ||
|
||||
existing.lastMessageTime !== conversationData.lastMessageTime ||
|
||||
existing.unreadCount !== conversationData.unreadCount ||
|
||||
existing.patientName !== conversationData.patientName ||
|
||||
existing.patientSex !== conversationData.patientSex ||
|
||||
existing.patientAge !== conversationData.patientAge
|
||||
) {
|
||||
// 只更新变化的字段,保持头像和未读数稳定
|
||||
conversationList.value[existingIndex] = {
|
||||
...conversationData,
|
||||
// 保持原有头像,避免闪动
|
||||
avatar: existing.avatar || conversationData.avatar,
|
||||
// 保留较大的未读数(避免被后端数据覆盖)
|
||||
unreadCount: Math.max(
|
||||
existing.unreadCount || 0,
|
||||
conversationData.unreadCount || 0
|
||||
),
|
||||
};
|
||||
needSort = true;
|
||||
console.log(
|
||||
`已更新会话: ${conversationData.name}, unreadCount: ${conversationList.value[existingIndex].unreadCount}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 添加新会话
|
||||
conversationList.value.push(conversationData);
|
||||
needSort = true;
|
||||
console.log(`已添加新会话: ${conversationData.name}`);
|
||||
}
|
||||
});
|
||||
|
||||
// 只在需要时才排序
|
||||
if (needSort) {
|
||||
conversationList.value.sort(
|
||||
(a, b) => b.lastMessageTime - a.lastMessageTime
|
||||
);
|
||||
}
|
||||
}, 100); // 100ms 防抖延迟
|
||||
});
|
||||
|
||||
// 监听消息接收事件(用于更新未读数)
|
||||
globalTimChatManager.setCallback("onMessageReceived", (message) => {
|
||||
console.log("消息列表页面收到新消息:", message);
|
||||
|
||||
// 找到对应的会话
|
||||
const conversationID = message.conversationID;
|
||||
const conversationIndex = conversationList.value.findIndex(
|
||||
(conv) => conv.conversationID === conversationID
|
||||
);
|
||||
|
||||
if (conversationIndex !== -1) {
|
||||
const conversation = conversationList.value[conversationIndex];
|
||||
|
||||
// 检查当前页面栈,判断用户是否正在查看该会话的聊天详情页
|
||||
const pages = getCurrentPages();
|
||||
const currentPage = pages[pages.length - 1];
|
||||
|
||||
// 获取当前页面的 groupID 参数(如果在聊天详情页)
|
||||
const currentGroupID = currentPage?.options?.groupID;
|
||||
const isViewingThisConversation =
|
||||
currentPage?.route === "pages/message/index" &&
|
||||
currentGroupID === conversation.groupID;
|
||||
|
||||
// 如果用户正在查看这个具体的会话,不增加未读数
|
||||
if (isViewingThisConversation) {
|
||||
console.log("用户正在查看该会话,不增加未读数");
|
||||
return;
|
||||
}
|
||||
|
||||
// 只在用户不在该会话的聊天页面时才增加未读数
|
||||
conversation.unreadCount = (conversation.unreadCount || 0) + 1;
|
||||
console.log(
|
||||
"已更新会话未读数:",
|
||||
conversation.name,
|
||||
"unreadCount:",
|
||||
conversation.unreadCount
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 格式化患者姓名
|
||||
const formatPatientName = (conversation) => {
|
||||
return conversation.patientName || "未知患者";
|
||||
};
|
||||
|
||||
// 格式化患者信息(性别 + 年龄)
|
||||
const formatPatientInfo = (conversation) => {
|
||||
const parts = [];
|
||||
|
||||
// 性别
|
||||
if (conversation.patientSex === "男") {
|
||||
parts.push("男");
|
||||
} else if (conversation.patientSex === "女") {
|
||||
parts.push("女");
|
||||
}
|
||||
|
||||
// 年龄
|
||||
if (conversation.patientAge) {
|
||||
parts.push(`${conversation.patientAge}岁`);
|
||||
}
|
||||
|
||||
return parts.join(" ");
|
||||
};
|
||||
|
||||
// 格式化消息时间
|
||||
const formatMessageTime = (timestamp) => {
|
||||
if (!timestamp) return "";
|
||||
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const date = new Date(timestamp);
|
||||
|
||||
// 1分钟内
|
||||
if (diff < 60 * 1000) {
|
||||
return "刚刚";
|
||||
}
|
||||
|
||||
// 1小时内
|
||||
if (diff < 60 * 60 * 1000) {
|
||||
return `${Math.floor(diff / (60 * 1000))}分钟前`;
|
||||
}
|
||||
|
||||
// 今天
|
||||
const today = new Date();
|
||||
if (date.toDateString() === today.toDateString()) {
|
||||
return `${String(date.getHours()).padStart(2, "0")}:${String(
|
||||
date.getMinutes()
|
||||
).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// 昨天
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
if (date.toDateString() === yesterday.toDateString()) {
|
||||
return "昨天";
|
||||
}
|
||||
|
||||
// 今年
|
||||
if (date.getFullYear() === today.getFullYear()) {
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
}
|
||||
|
||||
// 其他
|
||||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
};
|
||||
|
||||
// 点击会话
|
||||
const handleClickConversation = (conversation) => {
|
||||
console.log("点击会话:", conversation);
|
||||
|
||||
// 立即清空本地未读数(优化用户体验)
|
||||
const conversationIndex = conversationList.value.findIndex(
|
||||
(conv) => conv.conversationID === conversation.conversationID
|
||||
);
|
||||
if (conversationIndex !== -1) {
|
||||
conversationList.value[conversationIndex].unreadCount = 0;
|
||||
console.log("已清空本地未读数:", conversation.name);
|
||||
}
|
||||
|
||||
// 跳转到聊天页面
|
||||
uni.navigateTo({
|
||||
url: `/pages/message/index?conversationID=${encodeURIComponent(conversation.conversationID)}&groupID=${encodeURIComponent(conversation.groupID)}`,
|
||||
});
|
||||
};
|
||||
|
||||
// 加载更多
|
||||
const handleLoadMore = () => {
|
||||
if (loadingMore.value || !hasMore.value) return;
|
||||
|
||||
loadingMore.value = true;
|
||||
// TODO: 实现分页加载
|
||||
setTimeout(() => {
|
||||
loadingMore.value = false;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
refreshing.value = true;
|
||||
try {
|
||||
await loadConversationList();
|
||||
} finally {
|
||||
refreshing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 页面加载
|
||||
onLoad(() => {
|
||||
console.log("消息列表页面加载");
|
||||
});
|
||||
|
||||
// 页面显示
|
||||
onShow(async () => {
|
||||
try {
|
||||
// 加载团队列表
|
||||
await getTeams();
|
||||
|
||||
// 初始化IM
|
||||
const imReady = await initIM();
|
||||
if (!imReady) {
|
||||
console.error("IM初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载会话列表
|
||||
await loadConversationList();
|
||||
|
||||
// 设置监听器,后续通过事件更新列表
|
||||
setupConversationListener();
|
||||
} catch (error) {
|
||||
console.error("页面初始化失败:", error);
|
||||
uni.showToast({
|
||||
title: "初始化失败,请重试",
|
||||
icon: "none",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 页面隐藏
|
||||
onHide(() => {
|
||||
// 清除防抖定时器
|
||||
if (updateTimer) {
|
||||
clearTimeout(updateTimer);
|
||||
updateTimer = null;
|
||||
}
|
||||
|
||||
// 移除消息监听
|
||||
if (globalTimChatManager) {
|
||||
globalTimChatManager.setCallback("onConversationListUpdated", null);
|
||||
globalTimChatManager.setCallback("onMessageReceived", null);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.message-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
|
||||
&:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
margin-right: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 8rpx;
|
||||
background-color: #ff4d4f;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.unread-text {
|
||||
font-size: 20rpx;
|
||||
color: #fff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.name-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.patient-info {
|
||||
font-size: 26rpx;
|
||||
padding-left: 12rpx;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-left: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
padding: 20rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.load-more-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@ -111,11 +111,11 @@ const rule = computed(() => {
|
||||
data.job.name = "岗位 (不可修改)";
|
||||
data.job.disabled = true;
|
||||
|
||||
// data.title.name = doctorInfo.value.verifyStatus === 'verified' ? "职称 (不可修改)" : "职称";
|
||||
// data.title.disabled = doctorInfo.value.verifyStatus === 'verified';
|
||||
data.title.name = doctorInfo.value.verifyStatus === 'verified' ? "职称 (不可修改)" : "职称";
|
||||
data.title.disabled = doctorInfo.value.verifyStatus === 'verified';
|
||||
|
||||
// data.dept.name = doctorInfo.value.verifyStatus === 'verified' ? "科室 (不可修改)" : "科室";
|
||||
// data.dept.disabled = doctorInfo.value.verifyStatus === 'verified';
|
||||
data.dept.name = doctorInfo.value.verifyStatus === 'verified' ? "科室 (不可修改)" : "科室";
|
||||
data.dept.disabled = doctorInfo.value.verifyStatus === 'verified';
|
||||
}
|
||||
return data
|
||||
});
|
||||
|
||||
@ -1,141 +1,118 @@
|
||||
<template>
|
||||
<view v-if="team" class="flex flex-col justify-center h-full bg-white">
|
||||
<view>
|
||||
<view class="text-dark text-lg font-semibold text-center mb-10">
|
||||
{{ team.name }}
|
||||
</view>
|
||||
<view class="mb-10 text-dark text-lg font-semibold text-center mb-10">
|
||||
成员邀请码
|
||||
</view>
|
||||
<view class="flex justify-center overflow-hidden">
|
||||
<uqrcode canvas-id="qrcode" :value="qrcode" :options="options"></uqrcode>
|
||||
</view>
|
||||
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
|
||||
微信扫一扫上面的二维码
|
||||
</view>
|
||||
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
|
||||
加入我的团队,协同开展患者管理服务
|
||||
</view>
|
||||
<view class="mt-10 flex px-15 leading-normal text-center">
|
||||
<button class="mr-10 border-auto rounded py-5 text-base text-primary flex-grow"
|
||||
@click="saveImage('save')">
|
||||
保存图片
|
||||
</button>
|
||||
<button class="bg-primary rounded py-5 text-base text-white flex-grow" open-type="share">分享微信</button>
|
||||
</view>
|
||||
<view class="canvas-box">
|
||||
<l-painter ref="painterRef" :board="poster" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="team" class="flex flex-col justify-center h-full bg-white">
|
||||
<view>
|
||||
<view class="text-dark text-lg font-semibold text-center mb-10">
|
||||
{{ team.name }}
|
||||
</view>
|
||||
<view class="mb-10 text-dark text-lg font-semibold text-center mb-10">
|
||||
成员邀请码
|
||||
</view>
|
||||
<view class="flex justify-center overflow-hidden">
|
||||
<uqrcode canvas-id="qrcode" :value="qrcode" :options="options"></uqrcode>
|
||||
</view>
|
||||
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
|
||||
微信扫一扫上面的二维码
|
||||
</view>
|
||||
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
|
||||
加入我的团队,协同开展患者管理服务
|
||||
</view>
|
||||
<view class="mt-10 flex px-15 leading-normal text-center">
|
||||
<button class="mr-10 border-auto rounded py-5 text-base text-primary flex-grow" @click="saveImage('save')">
|
||||
保存图片
|
||||
</button>
|
||||
<button class="bg-primary rounded py-5 text-base text-white flex-grow" open-type="share">分享微信</button>
|
||||
</view>
|
||||
<view class="canvas-box">
|
||||
<l-painter ref="painterRef" :board="poster" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
computed,
|
||||
ref
|
||||
} from "vue";
|
||||
import {
|
||||
storeToRefs
|
||||
} from "pinia";
|
||||
import {
|
||||
onLoad,
|
||||
onShareAppMessage
|
||||
} from "@dcloudio/uni-app";
|
||||
import useGuard from "@/hooks/useGuard.js";
|
||||
import useAccountStore from "@/store/account.js";
|
||||
import api from '@/utils/api';
|
||||
import {
|
||||
toast
|
||||
} from "@/utils/widget";
|
||||
import {
|
||||
getInviteMatePoster
|
||||
} from './base-poster-data';
|
||||
import { computed, ref } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { onLoad, onShareAppMessage } from "@dcloudio/uni-app";
|
||||
import useGuard from "@/hooks/useGuard.js";
|
||||
import useAccountStore from "@/store/account.js";
|
||||
import api from '@/utils/api';
|
||||
import { toast } from "@/utils/widget";
|
||||
import { getInviteMatePoster } from './base-poster-data';
|
||||
|
||||
const env = __VITE_ENV__;
|
||||
const inviteQrcode = env.MP_INVITE_TEAMMATE_QRCODE;
|
||||
const env = __VITE_ENV__;
|
||||
const inviteQrcode = env.MP_INVITE_TEAMMATE_QRCODE;
|
||||
|
||||
const options = {
|
||||
margin: 10
|
||||
};
|
||||
const team = ref(null);
|
||||
const teamId = ref('');
|
||||
const painterRef = ref()
|
||||
const poster = ref({})
|
||||
const options = { margin: 10 };
|
||||
const team = ref(null);
|
||||
const teamId = ref('');
|
||||
const painterRef = ref()
|
||||
const poster = ref({})
|
||||
|
||||
const {
|
||||
useLoad,
|
||||
useShow
|
||||
} = useGuard();
|
||||
const {
|
||||
account
|
||||
} = storeToRefs(useAccountStore());
|
||||
const { useLoad, useShow } = useGuard();
|
||||
const { account } = storeToRefs(useAccountStore());
|
||||
|
||||
const qrcode = computed(() => `${inviteQrcode}?type=inviteTeam&teamId=${teamId.value}`)
|
||||
const qrcode = computed(() => `${inviteQrcode}?type=inviteTeam&teamId=${teamId.value}`)
|
||||
|
||||
async function getTeam() {
|
||||
const res = await api('getTeamData', {
|
||||
teamId: teamId.value,
|
||||
corpId: account.value.corpId
|
||||
});
|
||||
if (res && res.data) {
|
||||
team.value = res.data;
|
||||
} else {
|
||||
toast(res?.message || '获取团队信息失败')
|
||||
}
|
||||
}
|
||||
async function getTeam() {
|
||||
const res = await api('getTeamData', { teamId: teamId.value, corpId: account.value.corpId });
|
||||
if (res && res.data) {
|
||||
team.value = res.data;
|
||||
} else {
|
||||
toast(res?.message || '获取团队信息失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function saveImage(action = 'save') {
|
||||
const data = getInviteMatePoster(team.value.name, qrcode.value)
|
||||
try {
|
||||
await painterRef.value.render(data);
|
||||
painterRef.value.canvasToTempFilePathSync({
|
||||
fileType: "jpg",
|
||||
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum,需要设置 pathType为url
|
||||
pathType: 'url',
|
||||
quality: 1,
|
||||
success: (res) => {
|
||||
console.log(res.tempFilePath);
|
||||
if (action === 'save') {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: function() {
|
||||
console.log('save success');
|
||||
}
|
||||
});
|
||||
} else if (action === 'share') {
|
||||
wx.showShareImageMenu({
|
||||
path: res.tempFilePath,
|
||||
needShowEntrance: false
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
toast(e?.message)
|
||||
}
|
||||
}
|
||||
useLoad(options => {
|
||||
teamId.value = options.teamId;
|
||||
})
|
||||
async function saveImage(action = 'save') {
|
||||
const data = getInviteMatePoster(team.value.name, qrcode.value)
|
||||
try {
|
||||
await painterRef.value.render(data);
|
||||
painterRef.value.canvasToTempFilePathSync({
|
||||
fileType: "jpg",
|
||||
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum,需要设置 pathType为url
|
||||
pathType: 'url',
|
||||
quality: 1,
|
||||
success: (res) => {
|
||||
console.log(res.tempFilePath);
|
||||
if (action === 'save') {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: function () {
|
||||
console.log('save success');
|
||||
}
|
||||
});
|
||||
} else if (action === 'share') {
|
||||
wx.showShareImageMenu({
|
||||
path: res.tempFilePath,
|
||||
needShowEntrance: false
|
||||
})
|
||||
}
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
toast(e?.message)
|
||||
}
|
||||
}
|
||||
useLoad(options => {
|
||||
teamId.value = options.teamId;
|
||||
})
|
||||
|
||||
useShow(() => {
|
||||
getTeam()
|
||||
});
|
||||
useShow(() => {
|
||||
getTeam()
|
||||
});
|
||||
|
||||
onShareAppMessage(() => {
|
||||
return {
|
||||
title: '邀请团队成员',
|
||||
path: `pages/login/redirect-page?type=inviteTeam&teamId=${teamId.value}`
|
||||
}
|
||||
})
|
||||
onShareAppMessage(() => {
|
||||
return {
|
||||
title: '邀请团队成员',
|
||||
path: `pages/login/redirect-page?type=inviteTeam&teamId=${teamId.value}`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.canvas-box {
|
||||
top: 10000rpx;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.canvas-box {
|
||||
top: 10000rpx;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@ -82,9 +82,6 @@ export default defineStore("accountStore", () => {
|
||||
|
||||
async function getDoctorInfo(data = {}) {
|
||||
try {
|
||||
if (!account.value?.openid) {
|
||||
return
|
||||
}
|
||||
const res = await api('getCorpMemberData', {
|
||||
...data,
|
||||
weChatOpenId: account.value.openid,
|
||||
@ -112,12 +109,16 @@ export default defineStore("accountStore", () => {
|
||||
async function initIMAfterLogin() {
|
||||
if (isIMInitialized.value) return true;
|
||||
if (!doctorInfo.value) {
|
||||
await getDoctorInfo();
|
||||
console.error('医生信息未获取,无法初始化IM');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const userID = doctorInfo.value?.userid;
|
||||
const userID = doctorInfo.value.userid;
|
||||
if (!userID) {
|
||||
return
|
||||
await getDoctorInfo();
|
||||
if (!doctorInfo.value?.userid) {
|
||||
throw new Error('无法获取用户ID');
|
||||
}
|
||||
}
|
||||
|
||||
const success = await initGlobalTIM(userID);
|
||||
|
||||
@ -424,7 +424,6 @@ export async function handleFollowUpMessages(messages, context = {}) {
|
||||
corpId: context.corpId,
|
||||
});
|
||||
} else if (msg.type === 'questionnaire') {
|
||||
|
||||
success = await sendSurveyMessage(msg.content, {
|
||||
userId: context.userId,
|
||||
customerId: context.customerId,
|
||||
|
||||
@ -1032,8 +1032,6 @@ class TimChatManager {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 检查userId是否存在,不存在则不需要初始化
|
||||
if (!this.currentUserID) {
|
||||
console.error('currentUserID不存在,无法获取群聊列表')
|
||||
reject()
|
||||
return
|
||||
}
|
||||
|
||||
@ -1078,13 +1076,12 @@ class TimChatManager {
|
||||
if (timeoutHandle) clearTimeout(timeoutHandle)
|
||||
this.getGroupListInternal().then(resolve).catch(reject)
|
||||
} else if (waitTime >= maxWaitTime) {
|
||||
console.error('等待SDK就绪超时,当前isLoggedIn:', this.isLoggedIn)
|
||||
console.error('等待SDK就绪超时')
|
||||
if (timeoutHandle) clearTimeout(timeoutHandle)
|
||||
// 超时时返回错误而不是继续等待
|
||||
reject(new Error('SDK初始化超时,请检查网络连接'))
|
||||
} else {
|
||||
waitTime += checkInterval
|
||||
console.log(`等待SDK就绪... (${Math.floor(waitTime / 1000)}/${Math.floor(maxWaitTime / 1000)}秒, isLoggedIn: ${this.isLoggedIn})`)
|
||||
console.log(`等待SDK就绪... (${Math.floor(waitTime / 1000)}/${Math.floor(maxWaitTime / 1000)}秒)`)
|
||||
timeoutHandle = setTimeout(checkSDKReady, checkInterval)
|
||||
}
|
||||
}
|
||||
@ -2761,7 +2758,6 @@ class TimChatManager {
|
||||
|
||||
// 标记会话为已读
|
||||
markConversationAsRead(conversationID) {
|
||||
|
||||
if (!this.tim || !this.isLoggedIn) {
|
||||
console.log('⚠️ TIM未初始化或未登录,无法标记会话已读');
|
||||
return;
|
||||
@ -2781,7 +2777,6 @@ class TimChatManager {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 更新会话列表
|
||||
updateConversationListOnNewMessage(message) {
|
||||
try {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user