Compare commits

..

21 Commits

Author SHA1 Message Date
ba1200a650 Merge remote-tracking branch 'origin/dev-wdb' into dev-hjf 2026-02-10 11:07:54 +08:00
huxuejian
14149ef143 Update login.vue 2026-02-10 09:47:17 +08:00
huxuejian
a045a4ed32 Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-02-10 09:43:57 +08:00
huxuejian
c5e8e29afa fix:问题修复 2026-02-10 09:43:53 +08:00
89f460d5c1 Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-02-09 20:18:51 +08:00
7f7c901cb3 no message 2026-02-09 20:18:48 +08:00
huxuejian
4d6ad8489d Update .env.production 2026-02-09 19:59:54 +08:00
huxuejian
17c84c4780 Merge remote-tracking branch 'origin/dev-hjf' into dev-wdb 2026-02-09 19:59:17 +08:00
huxuejian
52fbf85b2a Update full-page.vue 2026-02-09 19:54:08 +08:00
75aad1306d Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-02-09 18:05:50 +08:00
600a8db449 no message 2026-02-09 18:05:46 +08:00
huxuejian
25a230ece7 Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-02-09 16:16:07 +08:00
huxuejian
3360f731eb fix: 问题修复 2026-02-09 16:16:06 +08:00
5c040e3f55 Merge commit '1018707e25082fb5b29b265ed55a9ea3499c44bf' into dev-wdb 2026-02-09 15:41:03 +08:00
2d10c5d7cb Merge commit 'f00383034d50a49e9128b20d15627da8893423bb' into dev-wdb
# Conflicts:
#	pages/message/message.vue
2026-02-09 15:40:51 +08:00
dc4d8f9b1b no message 2026-02-09 15:40:11 +08:00
c34602655f no message 2026-02-09 15:11:36 +08:00
53a53c7b6d no message 2026-02-09 14:55:29 +08:00
c836420191 no message 2026-02-09 14:06:35 +08:00
f1f148d8a1 no message 2026-02-09 12:01:03 +08:00
00478235e7 no message 2026-02-09 11:52:40 +08:00
23 changed files with 1397 additions and 1274 deletions

10
.env.production Normal file
View File

@ -0,0 +1,10 @@
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'

View File

@ -22,7 +22,7 @@
<slot name="footer"></slot> <slot name="footer"></slot>
</view> </view>
<!-- #ifdef MP--> <!-- #ifdef MP-->
<!-- <view v-if="showSafeArea" class="safeareaBottom"></view> --> <view v-if="showSafeArea" class="safeareaBottom"></view>
<!-- #endif --> <!-- #endif -->
</view> </view>
</template> </template>

View File

@ -28,6 +28,12 @@
"UNI_PLATFORM": "mp-weixin" "UNI_PLATFORM": "mp-weixin"
} }
}, },
"pro": {
"title": "线上",
"env": {
"UNI_PLATFORM": "mp-weixin"
}
},
"ip": { "ip": {
"title": "本机ip", "title": "本机ip",
"env": { "env": {

View File

@ -12,6 +12,12 @@
"navigationBarTitleText": "登录" "navigationBarTitleText": "登录"
} }
}, },
{
"path": "pages/login/agreement",
"style": {
"navigationBarTitleText": ""
}
},
{ {
"path": "pages/home/work-home", "path": "pages/home/work-home",
"style": { "style": {
@ -238,6 +244,12 @@
"navigationBarTitleText": "添加病历" "navigationBarTitleText": "添加病历"
} }
}, },
{
"path": "ai-medical-case-form",
"style": {
"navigationBarTitleText": "添加病历"
}
},
{ {
"path": "service-record-detail", "path": "service-record-detail",
"style": { "style": {

View File

@ -0,0 +1,480 @@
<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>

View File

@ -91,11 +91,13 @@
</view> </view>
</view> </view>
<button <button
v-if="canShowSendButton(i)" v-if="fromChat && isExecutor(i)"
class="action-btn send-btn" class="action-btn send-btn"
@click.stop="goChatAndSend(i)" :class="{ loading: sendingFollowUp }"
:disabled="sendingFollowUp"
@click.stop="sendFollowUp(i)"
> >
发送 {{ sendingFollowUp ? "发送中..." : "发送" }}
</button> </button>
</view> </view>
<view v-if="i.status === 'treated'" class="result" <view v-if="i.status === 'treated'" class="result"
@ -240,6 +242,12 @@ function getUserId() {
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || "") || ""; 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() { function getCorpId() {
const team = uni.getStorageSync("ykt_case_current_team") || {}; const team = uni.getStorageSync("ykt_case_current_team") || {};
const d = doctorInfo.value || {}; const d = doctorInfo.value || {};
@ -296,6 +304,7 @@ const pages = ref(1);
const loading = ref(false); const loading = ref(false);
const userNameMap = ref({}); const userNameMap = ref({});
const sendingFollowUp = ref(false);
const moreStatus = computed(() => { const moreStatus = computed(() => {
if (loading.value) return "loading"; if (loading.value) return "loading";
@ -367,7 +376,6 @@ function eventTypeLabel(eventType) {
} }
function resolveUserName(userId) { function resolveUserName(userId) {
const id = String(userId || "");
if (!id) return ""; if (!id) return "";
const map = userNameMap.value || {}; const map = userNameMap.value || {};
return String(map[id] || "") || id; return String(map[id] || "") || id;
@ -531,163 +539,90 @@ function toDetail(todo) {
}); });
} }
function hasSendContent(todo) { async function sendFollowUp(todo) {
return Boolean(todo?.sendContent) || (Array.isArray(todo?.fileList) && todo.fileList.length > 0); if (sendingFollowUp.value) {
toast("正在发送中,请稍候...");
return;
} }
function isExecutorMe(todo) { if (!todo.sendContent && (!todo.fileList || todo.fileList.length === 0)) {
const me = String(getUserId() || ""); toast("没有发送内容");
const executor = String(todo?.executorUserId || ""); return;
if (!me || !executor) return false;
return me === executor;
} }
function canShowSendButton(todo) { sendingFollowUp.value = true;
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 (Array.isArray(todo?.fileList)) {
for (const file of todo.fileList) {
const outerType = String(file?.type || "");
let innerFile = file?.file;
if (typeof innerFile === "string") {
try { try {
innerFile = JSON.parse(innerFile); const messages = [];
} catch {
// ignore // 1.
if (todo.sendContent) {
messages.push({
type: "text",
content: todo.sendContent,
});
} }
} console.log("==============>fileList", todo.fileList);
innerFile = innerFile && typeof innerFile === "object" ? innerFile : null;
const innerType = String(innerFile?.type || ""); // 2.
const outerUrl = String(file?.URL || file?.url || ""); if (Array.isArray(todo.fileList)) {
const innerUrl = String(innerFile?.url || ""); for (const file of todo.fileList) {
if (file.type === "image" && file.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({ messages.push({
type: "image", type: "image",
content: url, content: file.URL,
name: innerFile?.name || file?.name || "图片", name: file.file?.name || file.name || "图片",
}); });
continue; } else if (file.file.type === "article" && file.file?.url) {
} // - URL id
const articleId = extractIdFromUrl(file.file.url);
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({ messages.push({
type: "article", type: "article",
content: { content: {
_id: articleId, _id: articleId,
title: innerFile?.name || file?.name || "宣教文章", title: file.file?.name || "宣教文章",
url: articleUrl, url: file.file?.url || file.URL,
subtitle: innerFile?.subtitle || "", subtitle: file.file?.subtitle || "",
cover: innerFile?.cover || file?.URL || "", cover: file.file?.cover || "",
articleId: articleId, articleId: articleId,
}, },
}); });
continue; } else if (
} file.file.type === "questionnaire" &&
(file.file?.url || file.URL)
if (fileType === "questionnaire") { ) {
const surveryId = innerFile?.surveryId || file?.surveryId; // - URL surveryId
if (!surveryId) continue; const surveryUrl = file.file?.url || file.URL;
const surveyId = String(innerFile?._id || file?._id || surveryId || ""); const surveryId = extractSurveryIdFromUrl(surveryUrl);
messages.push({ messages.push({
type: "questionnaire", type: "questionnaire",
content: { content: {
_id: surveyId, _id: surveryId,
name: innerFile?.name || file?.name || "问卷", name: file.file?.name || file.name || "问卷",
surveryId, surveryId: surveryId,
url: String(url || ""), url: surveryUrl,
createBy: innerFile?.createBy,
}, },
}); });
} }
} }
} }
return messages; //
} const success = await handleFollowUpMessages(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(), userId: getUserId(),
customerId: props.archiveId, customerId: props.archiveId,
customerName: props.data?.name || "", customerName: props.data?.name || "",
corpId: getCorpId(), corpId: getCorpId(),
env: __VITE_ENV__, env: __VITE_ENV__,
},
}); });
uni.navigateTo({ if (success) {
url: `/pages/message/index?conversationID=${encodeURIComponent( toast("消息已发送");
conversationID uni.navigateBack();
)}&groupID=${encodeURIComponent(gid)}&fromCase=true&pendingFollowUpSend=1`, }
}); } finally {
sendingFollowUp.value = false;
}
} }
/** /**
@ -714,77 +649,21 @@ function extractIdFromUrl(url) {
} }
} }
const isRefreshingChatRoom = ref(false); /**
let lastRefreshChatRoomAt = 0; * 从问卷 URL 中提取 surveryId 参数
* @param {string} url - 完整的 URL格式如: https://www.youcan365.com/patientDeploy/#/pages/survery/fill?corpId=wwe3fb2faa52cf9dfb&surveryId=9ji5kg2oa9x52oyg9w4rj5k81769510562099
function parseAnyTimeMs(v) { * @returns {string} 提取出的 surveryId
if (v === null || v === undefined) return 0; */
if (typeof v === "number") return v; function extractSurveryIdFromUrl(url) {
const s = String(v).trim(); if (!url) return "";
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 { try {
await ensureDoctor(); // 使 surveryId
const corpId = getCorpId(); // : ?surveryId=xxx &surveryId=xxx
const teamId = getCurrentTeamId(); const match = url.match(/[?&]surveryId=([^&]+)/);
return match ? decodeURIComponent(match[1]) : "";
const baseQuery = { } catch (error) {
corpId, console.error("解析问卷 URL 失败:", error);
customerId, return "";
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;
} }
} }
@ -1166,6 +1045,14 @@ watch(
color: #fff; color: #fff;
} }
.send-btn.loading {
opacity: 0.6;
}
.send-btn:disabled {
opacity: 0.6;
}
.empty { .empty {
padding: 120px 0; padding: 120px 0;
text-align: center; text-align: center;

View File

@ -6,7 +6,6 @@
@team-change="handleTeamChange" @team-change="handleTeamChange"
@add-patient="handleAddPatient" @add-patient="handleAddPatient"
/> />
<!-- 消息列表 --> <!-- 消息列表 -->
<scroll-view <scroll-view
class="message-list" class="message-list"
@ -26,7 +25,7 @@
<view class="avatar-container"> <view class="avatar-container">
<image <image
class="avatar" class="avatar"
:src="conversation.avatar || '/static/default-avatar.png'" :src="conversation.avatar || '/static/default-patient-avatar.png'"
mode="aspectFill" mode="aspectFill"
/> />
<view v-if="conversation.unreadCount > 0" class="unread-badge"> <view v-if="conversation.unreadCount > 0" class="unread-badge">
@ -53,7 +52,7 @@
</view> </view>
<view class="message-preview"> <view class="message-preview">
<text class="preview-text">{{ <text class="preview-text">{{
conversation.lastMessage || "暂无消息" cleanMessageText(conversation.lastMessage) || "暂无消息"
}}</text> }}</text>
</view> </view>
</view> </view>
@ -61,7 +60,14 @@
<!-- 空状态 --> <!-- 空状态 -->
<view <view
v-if="filteredConversationList.length === 0" 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" class="empty-container"
> >
<image class="empty-image" src="/static/empty.svg" mode="aspectFit" /> <image class="empty-image" src="/static/empty.svg" mode="aspectFit" />
@ -92,10 +98,12 @@ import useTeamStore from "@/store/team.js";
import useInfoCheck from "@/hooks/useInfoCheck.js"; import useInfoCheck from "@/hooks/useInfoCheck.js";
import { globalTimChatManager } from "@/utils/tim-chat.js"; import { globalTimChatManager } from "@/utils/tim-chat.js";
import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.js"; import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.js";
import MessageHeader from "./components/message-header.vue"; import MessageHeader from "../home/components/message-header.vue";
// //
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore()); const { account, openid, isIMInitialized, doctorInfo } = storeToRefs(
useAccountStore()
);
const { initIMAfterLogin } = useAccountStore(); const { initIMAfterLogin } = useAccountStore();
// //
@ -156,46 +164,184 @@ 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 // IM
const initIM = async () => { const initIM = async () => {
if (!isIMInitialized.value) { // isIMInitialized
const needsInit =
!isIMInitialized.value ||
!globalTimChatManager ||
!globalTimChatManager.isLoggedIn;
if (needsInit) {
uni.showLoading({ uni.showLoading({
title: "连接中...", title: "连接中...",
}); });
//
if (
isIMInitialized.value &&
globalTimChatManager &&
!globalTimChatManager.isLoggedIn
) {
console.log("IM已初始化但连接已断开清理旧实例后重新初始化");
await globalTimChatManager.cleanupOldInstance();
}
const success = await initIMAfterLogin(); const success = await initIMAfterLogin();
uni.hideLoading(); uni.hideLoading();
// if (!success) {
// uni.showToast({ if (!success) {
// title: "IM", handleReloginIM();
// icon: "none", // //
// uni.showModal({
// title: "IM",
// content:
// "IMIM",
// confirmText: "",
// cancelText: "",
// success: (res) => {
// if (res.confirm) {
// //
// handleReloginIM();
// }
// },
// }); // });
// return false; // return false;
// }
} else if (globalTimChatManager && !globalTimChatManager.isLoggedIn) {
uni.showLoading({
title: "重连中...",
});
const reconnected = await globalTimChatManager.ensureIMConnection();
uni.hideLoading();
if (!reconnected) {
return false;
} }
} }
return true; 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 () => { const loadConversationList = async () => {
if (loading.value) return; if (loading.value) return;
loading.value = true; loading.value = true;
try {
console.log("开始加载群聊列表"); const accountStore = useAccountStore();
if (!globalTimChatManager || !globalTimChatManager.getGroupList) { if (!doctorInfo.value?.userid) {
console.warn("userId 未获取,跳过加载会话列表");
loading.value = false; loading.value = false;
return; return;
} }
const result = await globalTimChatManager.getGroupList(); 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 timeoutPromise = new Promise((_, reject) => {
setTimeout(
() => reject(new Error("加载会话列表超时,请检查网络连接")),
35000
);
});
const result = await Promise.race([
globalTimChatManager.getGroupList(),
timeoutPromise,
]);
if (result && result.success && result.groupList) { if (result && result.success && result.groupList) {
// //
conversationList.value = await mergeConversationWithGroupDetails( conversationList.value = await mergeConversationWithGroupDetails(
@ -218,17 +364,26 @@ const loadConversationList = async () => {
); );
} else { } else {
console.error("加载群聊列表失败:", result); console.error("加载群聊列表失败:", result);
uni.showToast({ throw new Error(result?.message || "加载失败,请重试");
title: "加载失败,请重试",
icon: "none",
});
} }
} catch (error) { } catch (error) {
console.error("加载会话列表失败:", error); console.error("加载会话列表失败:", error);
//
if (
error.message &&
(error.message.includes("超时") || error.message.includes("连接"))
) {
uni.showToast({
title: "网络连接不稳定,请重试",
icon: "none",
});
} else {
uni.showToast({ uni.showToast({
title: error.message || "加载失败,请重试", title: error.message || "加载失败,请重试",
icon: "none", icon: "none",
}); });
}
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -389,6 +544,12 @@ const setupConversationListener = () => {
}); });
}; };
//
const cleanMessageText = (text) => {
if (!text) return "";
return text.replace(/[\r\n]+/g, " ").trim();
};
// //
const formatPatientName = (conversation) => { const formatPatientName = (conversation) => {
return conversation.patientName || "未知患者"; return conversation.patientName || "未知患者";
@ -470,7 +631,9 @@ const handleClickConversation = (conversation) => {
// //
uni.navigateTo({ 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)}`,
}); });
}; };
@ -506,7 +669,7 @@ onShow(async () => {
// //
await getTeams(); await getTeams();
// IM // IM - IM
const imReady = await initIM(); const imReady = await initIM();
if (!imReady) { if (!imReady) {
console.error("IM初始化失败"); console.error("IM初始化失败");
@ -682,7 +845,10 @@ onHide(() => {
color: #999; color: #999;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-break: break-all;
} }
.load-more { .load-more {

View File

@ -19,13 +19,24 @@
</text> </text>
<text v-else class="user-name text-black text-lg font-semibold" @click="editProfile()">请完善信息</text> <text v-else class="user-name text-black text-lg font-semibold" @click="editProfile()">请完善信息</text>
<view class="flex items-center mt-5"> <view class="flex items-center mt-5">
<view v-if="!doctorInfo || !doctorInfo.anotherName" class="status-tag tag-orange mr-10" <view v-if="!doctorInfo || !doctorInfo.anotherName"
class="mr-10 flex items-center bg-warning text-white text-center px-10 rounded-full"
@click="editProfile()"> @click="editProfile()">
<text class="tag-text text-white">信息待完善</text> <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> </view>
<view v-if="certStatus" class="px-10 py-3 text-sm rounded-full" :class="certStatus.classnames" </view>
<view v-if="certStatus" class="flex items-center px-10 rounded-full" :class="certStatus.classnames"
@click.stop="handleCert()"> @click.stop="handleCert()">
{{ certStatus.text }} <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>
</view> </view>
</view> </view>
</view> </view>
@ -382,4 +393,12 @@ useShow(async () => {
padding-top: 6rpx; padding-top: 6rpx;
padding-bottom: 6rpx; padding-bottom: 6rpx;
} }
.pb-2 {
padding-bottom: 4rpx;
}
.translate-y--1 {
transform: translateY(-2rpx);
}
</style> </style>

30
pages/login/agreement.vue Normal file
View File

@ -0,0 +1,30 @@
<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>

View File

@ -27,8 +27,8 @@
> >
<checkbox :checked="checked" style="transform: scale(0.7)" /> <checkbox :checked="checked" style="transform: scale(0.7)" />
<view class="text-sm text-gray">我已阅读并同意</view> <view class="text-sm text-gray">我已阅读并同意</view>
<view class="text-sm text-primary">用户协议</view> <view class="text-sm text-primary" @click.stop="toAggreement('userAgreement')">用户协议</view>
<view class="text-sm text-primary">隐私政策</view> <view class="text-sm text-primary" @click.stop="toAggreement('privacyPolicy')">隐私政策</view>
</view> </view>
</template> </template>
@ -76,6 +76,12 @@ function toHome() {
}); });
} }
function toAggreement(type) {
uni.navigateTo({
url: `/pages/login/agreement?type=${type}`
})
}
async function getPhoneNumber(e) { async function getPhoneNumber(e) {
const phoneCode = e && e.detail && e.detail.code; const phoneCode = e && e.detail && e.detail.code;
// if (e && !phoneCode) return; // if (e && !phoneCode) return;

View File

@ -0,0 +1,166 @@
export default `
隐私政策
欢迎您访问健康柚平台
下称患者用户在健康柚平台使用我们的服务或产品时我们可能会收集您的相关个人信息健康柚深知个人信息对您的重要性并会尽全力保护您的个人信息安全可靠我们致力于维持您对我们的信任恪守以下原则保护您的个人信息权责一致原则目的明确原则选择同意原则最少够用原则确保安全原则主体参与原则公开透明原则等同时健康柚承诺我们将按业界成熟的安全标准采取相应的安全保护措施来保护您的个人信息
为此我们制定本隐私政策适用于我们为患者提供的产品和服务包括但不限于健康柚微信小程序柚助手微信小程序
在使用健康柚平台提供的产品或服务前请您务必认真仔细阅读并确认充分理解本隐私政策在确认充分理解并同意后再开始使用一旦您主动选择确认本隐私政策并继续使用的即视为同意本隐私政策的全部内容如您不同意相关协议或其中的任何条款的您应停止访问健康柚平台或使用健康柚产品和服务
如您是未成年人请您和您的监护人仔细阅读本政策并在征得您的监护人授权同意的前提下使用我们的服务或向我们提供个人信息
本隐私政策将帮助您了解以下内容
1我们如何收集和使用您的个人信息
2我们如何使用Cookie和同类技术
3我们如何共享转移公开披露您的个人信息
4我们如何保存和保护您的个人信息
5您的权利
6我们如何处理未成年人的个人信息
7您的个人信息如何在全球范围转移
8本隐私政策更新及通知
9如何联系我们
10争议解决
我们如何收集和使用您的个人信息
个人信息是指以电子或其他方式记录的与已识别或者可识别的自然人有关的各种信息不包括匿名化处理后的信息
我们仅会出于本政策所述的以下目的收集和使用您的个人信息当我们要将信息用于本政策未载明的其他用途时会事先征求您的同意
() 注册成为用户
健康柚平台提供的服务或产品是基于注册用户使用的如您希望使用健康柚平台提供的服务或产品则需要通过以下步骤完成账号注册
创建健康柚账户我们将提供手机号码授权登陆方式您需要提供您的手机号码如不提供上述注册信息您无法使用需注册成为健康柚平台用户方可使用的服务
() 成员档案管理服务
用户在使用成员档案管理服务时需先添加成员信息包括您的姓名性别年龄与成员关系本人/子女/父母/其他等目的是协助医生对患者进行管理
() 我的服务团队服务
用户在成员档案管理中添加成员信息后使用团队服务时我们可能会收集在健康柚其他平台已与患者建立关系的服务团队信息目的是与服务团队建立联系以确保成员聊天咨询的连续性和准确性我们还可能收集您的登记信息姓名身份证号码性别年龄检查检验报告用药记录过敏史等个人健康生理信息以及与个人身体健康状况相关的身高体重信息用于了解您的健康状况和咨询需求如不收集这类信息我们将无法为您提供健康咨询相关的服务但不影响您使用其他服务
您与团队建立服务关系后您理解并同意将添加的个人信息病历信息就诊记录将向该团队展示
() 回访服务
在您的服务团队人员对您进行回访的过程中我们可能收集您与团队人员的沟通聊天记录为您制定的回访计划向您发送的文章以及您填写的问卷信息以支持您在健康柚平台上获得持续可追溯的回访服务
() 客户服务
当您向我们提出问题投诉或建议时我们需要收集您的通信/通话记录您提供的联系方式信息您为了证明相关事实提供的信息以及您参与问卷调查时向我们发送的问卷答复信息我们收集上述信息的法律依据是基于向您提供健康柚平台服务所必需为您解决您在使用平台及享受服务过程中所遇到的问题以及向您提供相关问题的处理方案和结果如不收集这类信息您的投诉建议和反馈将无法得到及时有效处理但不影响您使用其它服务
() 保障功能运行和风控服务
为保障您正常使用我们及我们关联公司合作伙伴提供的服务维护我们系统基础功能的正常运行拦截钓鱼网站欺诈防止网络漏洞计算机病毒网络攻击网络侵入改进及优化我们的服务体验以及保障您的账号安全我们需要整合我们已根据本隐私政策合法收集的您的个人基本信息姓名身份证号码手机号码性别年龄个人生理健康信息既往病史用药记录体重并收集使用或整合您的网络身份标识信息BSSIDDNS地址IP地址SSID代理信息网络类型网络名称掩码信息个人常用设备信息IMEIIMSI设备IDMAC地址IDFAIDFVAndroidIdMCCMNCUUID标准国家码操作系统信息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楼
争议解决
因本政策以及我们处理您个人信息事宜引起的任何争议您可随时联系我公司个人信息保护相关负责人要求给出回复如果您对我们的回复不满意的认为我们的个人信息处理行为严重损害了您的合法权益的您还可以通过向本隐私政策服务提供商健康柚所在地杭州市有管辖权的人民法院提起诉讼来寻求解决方案
感谢您对健康柚平台以及健康柚产品和服务的信任和使用
`

View File

@ -0,0 +1,79 @@
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楼
`

View File

@ -11,16 +11,23 @@ $primary-color: #0877F1;
height: 100vh; height: 100vh;
background-color: #f5f5f5; background-color: #f5f5f5;
overflow: hidden; overflow: hidden;
position: relative;
width: 100%;
} }
/* 患者信息栏样式 */ /* 患者信息栏样式 - 固定在顶部 */
.patient-info-bar { .patient-info-bar {
position: relative; position: fixed;
top: 0;
left: 0;
right: 0;
background: #fff; background: #fff;
border-bottom: 1rpx solid #f0f0f0; border-bottom: 1rpx solid #f0f0f0;
padding: 20rpx 32rpx; padding: 20rpx 32rpx;
z-index: 10; z-index: 100;
flex-shrink: 0; /* 防止被压缩 */ flex-shrink: 0; /* 防止被压缩 */
width: 100%;
box-sizing: border-box;
} }
.patient-info-content { .patient-info-content {
@ -87,7 +94,12 @@ $primary-color: #0877F1;
flex: 1; flex: 1;
box-sizing: border-box; box-sizing: border-box;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto;
min-height: 0; min-height: 0;
margin-top: 120rpx;
margin-bottom: 0;
position: relative;
z-index: 1;
} }
.chat-content-compressed { .chat-content-compressed {

View File

@ -348,7 +348,7 @@ const handleNextFromProgress = (data) => {
// //
uni.navigateTo({ uni.navigateTo({
url: `/pages/case/medical-case-form?caseType=${data.caseType}&patientId=${ url: `/pages/case/ai-medical-case-form?caseType=${data.caseType}&patientId=${
props.patientId props.patientId
}&groupId=${props.groupId}&formData=${encodeURIComponent( }&groupId=${props.groupId}&formData=${encodeURIComponent(
JSON.stringify(extractedData) JSON.stringify(extractedData)

View File

@ -93,6 +93,7 @@ const FIELD_LABELS = {
inspectSummary: "体检小结", inspectSummary: "体检小结",
positiveFind: "阳性发现及处理意见", positiveFind: "阳性发现及处理意见",
// //
consultationDate: "问诊日期",
presentIllnessHistory: "现病史", presentIllnessHistory: "现病史",
pastMedicalHistory: "既往史", pastMedicalHistory: "既往史",
}; };

View File

@ -181,7 +181,7 @@ import { ref, onUnmounted, nextTick, watch, computed } from "vue";
import { onLoad, onShow, onHide } from "@dcloudio/uni-app"; import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import useAccountStore from "@/store/account.js"; import useAccountStore from "@/store/account.js";
import { globalTimChatManager, TIM } from "@/utils/tim-chat.js"; import { globalTimChatManager } from "@/utils/tim-chat.js";
import { handleFollowUpMessages } from "@/utils/send-message-helper"; import { handleFollowUpMessages } from "@/utils/send-message-helper";
import { import {
startIMMonitoring, startIMMonitoring,
@ -872,12 +872,29 @@ onShow(() => {
checkLoginAndInitTIM(); checkLoginAndInitTIM();
} else if (timChatManager.tim && !timChatManager.isLoggedIn) { } else if (timChatManager.tim && !timChatManager.isLoggedIn) {
timChatManager.ensureIMConnection(); timChatManager.ensureIMConnection();
} else if (timChatManager.tim && timChatManager.isLoggedIn && chatInfo.value.conversationID) {
//
console.log("页面从后台返回,重新加载消息列表");
messageList.value = [];
isCompleted.value = false;
lastFirstMessageId.value = "";
loadMessageList();
} }
startIMMonitoring(30000); startIMMonitoring(30000);
// 访 // 访
uni.$on("send-followup-message", handleSendFollowUpMessage); uni.$on("send-followup-message", handleSendFollowUpMessage);
//
uni.onKeyboardHeightChange((res) => {
if (res.height > 0) {
//
setTimeout(() => {
scrollToBottom(true);
}, 100);
}
});
}); });
// 访 // 访

View File

@ -1,796 +0,0 @@
<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>

View File

@ -111,11 +111,11 @@ const rule = computed(() => {
data.job.name = "岗位 (不可修改)"; data.job.name = "岗位 (不可修改)";
data.job.disabled = true; data.job.disabled = true;
data.title.name = doctorInfo.value.verifyStatus === 'verified' ? "职称 (不可修改)" : "职称"; // data.title.name = doctorInfo.value.verifyStatus === 'verified' ? " ()" : "";
data.title.disabled = doctorInfo.value.verifyStatus === 'verified'; // data.title.disabled = doctorInfo.value.verifyStatus === 'verified';
data.dept.name = doctorInfo.value.verifyStatus === 'verified' ? "科室 (不可修改)" : "科室"; // data.dept.name = doctorInfo.value.verifyStatus === 'verified' ? " ()" : "";
data.dept.disabled = doctorInfo.value.verifyStatus === 'verified'; // data.dept.disabled = doctorInfo.value.verifyStatus === 'verified';
} }
return data return data
}); });

View File

@ -17,7 +17,8 @@
加入我的团队协同开展患者管理服务 加入我的团队协同开展患者管理服务
</view> </view>
<view class="mt-10 flex px-15 leading-normal text-center"> <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 class="mr-10 border-auto rounded py-5 text-base text-primary flex-grow"
@click="saveImage('save')">
保存图片 保存图片
</button> </button>
<button class="bg-primary rounded py-5 text-base text-white flex-grow" open-type="share">分享微信</button> <button class="bg-primary rounded py-5 text-base text-white flex-grow" open-type="share">分享微信</button>
@ -29,31 +30,53 @@
</view> </view>
</template> </template>
<script setup> <script setup>
import { computed, ref } from "vue"; import {
import { storeToRefs } from "pinia"; computed,
import { onLoad, onShareAppMessage } from "@dcloudio/uni-app"; ref
} from "vue";
import {
storeToRefs
} from "pinia";
import {
onLoad,
onShareAppMessage
} from "@dcloudio/uni-app";
import useGuard from "@/hooks/useGuard.js"; import useGuard from "@/hooks/useGuard.js";
import useAccountStore from "@/store/account.js"; import useAccountStore from "@/store/account.js";
import api from '@/utils/api'; import api from '@/utils/api';
import { toast } from "@/utils/widget"; import {
import { getInviteMatePoster } from './base-poster-data'; toast
} from "@/utils/widget";
import {
getInviteMatePoster
} from './base-poster-data';
const env = __VITE_ENV__; const env = __VITE_ENV__;
const inviteQrcode = env.MP_INVITE_TEAMMATE_QRCODE; const inviteQrcode = env.MP_INVITE_TEAMMATE_QRCODE;
const options = { margin: 10 }; const options = {
margin: 10
};
const team = ref(null); const team = ref(null);
const teamId = ref(''); const teamId = ref('');
const painterRef = ref() const painterRef = ref()
const poster = ref({}) const poster = ref({})
const { useLoad, useShow } = useGuard(); const {
const { account } = storeToRefs(useAccountStore()); 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() { async function getTeam() {
const res = await api('getTeamData', { teamId: teamId.value, corpId: account.value.corpId }); const res = await api('getTeamData', {
teamId: teamId.value,
corpId: account.value.corpId
});
if (res && res.data) { if (res && res.data) {
team.value = res.data; team.value = res.data;
} else { } else {

View File

@ -82,6 +82,9 @@ export default defineStore("accountStore", () => {
async function getDoctorInfo(data = {}) { async function getDoctorInfo(data = {}) {
try { try {
if (!account.value?.openid) {
return
}
const res = await api('getCorpMemberData', { const res = await api('getCorpMemberData', {
...data, ...data,
weChatOpenId: account.value.openid, weChatOpenId: account.value.openid,
@ -109,16 +112,12 @@ export default defineStore("accountStore", () => {
async function initIMAfterLogin() { async function initIMAfterLogin() {
if (isIMInitialized.value) return true; if (isIMInitialized.value) return true;
if (!doctorInfo.value) { if (!doctorInfo.value) {
console.error('医生信息未获取无法初始化IM'); await getDoctorInfo();
return false;
} }
try { try {
const userID = doctorInfo.value.userid; const userID = doctorInfo.value?.userid;
if (!userID) { if (!userID) {
await getDoctorInfo(); return
if (!doctorInfo.value?.userid) {
throw new Error('无法获取用户ID');
}
} }
const success = await initGlobalTIM(userID); const success = await initGlobalTIM(userID);

View File

@ -424,6 +424,7 @@ export async function handleFollowUpMessages(messages, context = {}) {
corpId: context.corpId, corpId: context.corpId,
}); });
} else if (msg.type === 'questionnaire') { } else if (msg.type === 'questionnaire') {
success = await sendSurveyMessage(msg.content, { success = await sendSurveyMessage(msg.content, {
userId: context.userId, userId: context.userId,
customerId: context.customerId, customerId: context.customerId,

View File

@ -1032,6 +1032,8 @@ class TimChatManager {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// 检查userId是否存在不存在则不需要初始化 // 检查userId是否存在不存在则不需要初始化
if (!this.currentUserID) { if (!this.currentUserID) {
console.error('currentUserID不存在无法获取群聊列表')
reject()
return return
} }
@ -1076,12 +1078,13 @@ class TimChatManager {
if (timeoutHandle) clearTimeout(timeoutHandle) if (timeoutHandle) clearTimeout(timeoutHandle)
this.getGroupListInternal().then(resolve).catch(reject) this.getGroupListInternal().then(resolve).catch(reject)
} else if (waitTime >= maxWaitTime) { } else if (waitTime >= maxWaitTime) {
console.error('等待SDK就绪超时') console.error('等待SDK就绪超时当前isLoggedIn:', this.isLoggedIn)
if (timeoutHandle) clearTimeout(timeoutHandle) if (timeoutHandle) clearTimeout(timeoutHandle)
// 超时时返回错误而不是继续等待
reject(new Error('SDK初始化超时请检查网络连接')) reject(new Error('SDK初始化超时请检查网络连接'))
} else { } else {
waitTime += checkInterval waitTime += checkInterval
console.log(`等待SDK就绪... (${Math.floor(waitTime / 1000)}/${Math.floor(maxWaitTime / 1000)})`) console.log(`等待SDK就绪... (${Math.floor(waitTime / 1000)}/${Math.floor(maxWaitTime / 1000)}, isLoggedIn: ${this.isLoggedIn})`)
timeoutHandle = setTimeout(checkSDKReady, checkInterval) timeoutHandle = setTimeout(checkSDKReady, checkInterval)
} }
} }
@ -2758,6 +2761,7 @@ class TimChatManager {
// 标记会话为已读 // 标记会话为已读
markConversationAsRead(conversationID) { markConversationAsRead(conversationID) {
if (!this.tim || !this.isLoggedIn) { if (!this.tim || !this.isLoggedIn) {
console.log('⚠️ TIM未初始化或未登录无法标记会话已读'); console.log('⚠️ TIM未初始化或未登录无法标记会话已读');
return; return;
@ -2777,6 +2781,7 @@ class TimChatManager {
} }
} }
// 更新会话列表 // 更新会话列表
updateConversationListOnNewMessage(message) { updateConversationListOnNewMessage(message) {
try { try {