Compare commits

..

24 Commits

Author SHA1 Message Date
4160f737e7 Merge remote-tracking branch 'origin/dev-wdb' into dev-hjf 2026-02-02 14:51:54 +08:00
65c9a3efe5 头像调整 2026-02-02 14:12:46 +08:00
3ccdc954c2 no message 2026-02-02 13:27:48 +08:00
a7d3eeae3a Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-02-02 08:53:29 +08:00
dcc84cf449 fix: 提交 2026-02-02 08:53:26 +08:00
huxuejian
b08830c7ad Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-02-02 08:52:43 +08:00
huxuejian
3328435be1 feat: 页面提交 2026-02-02 08:52:42 +08:00
68e4f01bd2 新增 问卷预览 2026-01-30 18:19:39 +08:00
b48a00b58a 生成二维码页面优化 2026-01-30 17:25:25 +08:00
18675de48b no message 2026-01-30 16:41:12 +08:00
e26785bf4b no message 2026-01-30 14:47:52 +08:00
a83f92023e Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-01-30 14:25:13 +08:00
01575ef51a no message 2026-01-30 14:25:09 +08:00
huxuejian
38553df861 Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-01-30 14:14:14 +08:00
huxuejian
ceccd59b65 fix:修改appId 2026-01-30 14:14:13 +08:00
7b222b6143 Merge commit '4e0bbbf3e36ad79e0865a365f94c1c06e8f59577' into dev-wdb 2026-01-30 13:52:59 +08:00
d6851d72f2 IM 2026-01-30 13:52:35 +08:00
3316e1bfa0 Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-01-30 10:28:38 +08:00
3ad6140829 提交 2026-01-30 10:28:34 +08:00
huxuejian
a66bfce901 Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-01-30 10:26:13 +08:00
huxuejian
ca5ad25854 Update team.js 2026-01-30 10:26:11 +08:00
7f737dfd8a IM 2026-01-29 18:44:34 +08:00
963b4cc780 Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb 2026-01-29 18:03:43 +08:00
4fc7a8f6f2 no message 2026-01-29 18:03:40 +08:00
37 changed files with 3967 additions and 932 deletions

View File

@ -1,7 +1,7 @@
MP_API_BASE_URL=http://localhost:8080 MP_API_BASE_URL=http://localhost:8080
MP_IMAGE_URL=https://patient.youcan365.com MP_IMAGE_URL=https://patient.youcan365.com
MP_CACHE_PREFIX=development MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e MP_WX_APP_ID=wx1d8337a40c11d66c
MP_CORP_ID=wwe3fb2faa52cf9dfb MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600123876 MP_TIM_SDK_APP_ID=1600123876
MP_INVITE_TEAMMATE_QRCODE=https://patient.youcan365.com/invite-teammate MP_INVITE_TEAMMATE_QRCODE=https://patient.youcan365.com/invite-teammate

View File

@ -1,7 +1,7 @@
MP_API_BASE_URL=http://localhost:8080 MP_API_BASE_URL=http://localhost:8080
MP_IMAGE_URL=https://patient.youcan365.com MP_IMAGE_URL=https://patient.youcan365.com
MP_CACHE_PREFIX=development MP_CACHE_PREFIX=development
MP_WX_APP_ID=wx93af55767423938e MP_WX_APP_ID=wx1d8337a40c11d66c
MP_CORP_ID=wwe3fb2faa52cf9dfb MP_CORP_ID=wwe3fb2faa52cf9dfb
MP_TIM_SDK_APP_ID=1600123876 MP_TIM_SDK_APP_ID=1600123876
MP_INVITE_TEAMMATE_QRCODE=https://patient.youcan365.com/invite-teammate MP_INVITE_TEAMMATE_QRCODE=https://patient.youcan365.com/invite-teammate

39
baseData/index.js Normal file
View File

@ -0,0 +1,39 @@
export const ToDoEventType = {
followUpNoShow: "未到院回访",
followUpNoDeal: "未成交回访",
followUp: "诊后回访",
followUpPostSurgery: "术后回访",
followUpPostTreatment: "治疗后回访",
appointmentReminder: "就诊提醒",
followUpReminder: "复诊提醒",
medicationReminder: "用药提醒",
serviceSummary: "咨询服务",
eventNotification: "活动通知",
ContentReminder: "宣教发送",
questionnaire: "问卷调查",
followUpComplaint: "投诉回访",
followUpActivity: "活动回访",
other: "其他",
Feedback: "意见反馈",
// 预约相关服务类型
treatmentAppointment: "治疗预约",
followupAppointment: "复诊预约",
confirmArrival: "确认到院",
prenatalFollowUp: "孕期回访",
};
export const statusNames = {
notStart: "未开始",
treated: "已完成",
processing: "待处理",
cancelled: "已取消",
expired: "已过期",
};
export const statusClassNames = {
notStart: "text-primary",
treated: "text-success",
processing: "text-danger",
cancelled: "text-gray",
expired: "text-gray",
}

View File

@ -7,8 +7,14 @@
<view v-if="customScroll" class="page-scroll"> <view v-if="customScroll" class="page-scroll">
<slot></slot> <slot></slot>
</view> </view>
<scroll-view v-else scroll-y="true" :scroll-top="scrollTop" class="page-scroll" @scrolltolower="scrolltolower" <scroll-view
@scroll="onScroll"> v-else
scroll-y="true"
:scroll-top="scrollTop"
class="page-scroll"
@scrolltolower="scrolltolower"
@scroll="onScroll"
>
<slot></slot> <slot></slot>
</scroll-view> </scroll-view>
</view> </view>
@ -16,22 +22,22 @@
<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>
<script setup> <script setup>
import { computed, useSlots, ref } from 'vue'; import { computed, useSlots, ref } from "vue";
import useDebounce from '@/utils/useDebounce'; import useDebounce from "@/utils/useDebounce";
const emits = defineEmits(['reachBottom']); const emits = defineEmits(["reachBottom"]);
const props = defineProps({ const props = defineProps({
customScroll: { type: Boolean, default: false }, customScroll: { type: Boolean, default: false },
mainClass: { type: String, default: '' }, mainClass: { type: String, default: "" },
mainStyle: { default: '' }, mainStyle: { default: "" },
pageClass: { type: String, default: '' }, pageClass: { type: String, default: "" },
pageStyle: { default: '' }, pageStyle: { default: "" },
showSafeArea: { type: Boolean, default: true } showSafeArea: { type: Boolean, default: true },
}); });
const slots = useSlots(); const slots = useSlots();
const hasHeader = computed(() => !!slots.header); const hasHeader = computed(() => !!slots.header);
@ -40,7 +46,7 @@ const hasFooter = computed(() => !!slots.footer);
const scrollTop = ref(0); const scrollTop = ref(0);
const scrolltolower = useDebounce(() => { const scrolltolower = useDebounce(() => {
emits('reachBottom'); emits("reachBottom");
}); });
const onScroll = useDebounce((e) => { const onScroll = useDebounce((e) => {
@ -52,9 +58,8 @@ function scrollToBottom() {
} }
defineExpose({ defineExpose({
scrollToBottom scrollToBottom,
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.full-page { .full-page {

View File

@ -0,0 +1,89 @@
<template>
<view class="share-actions">
<view v-if="showSave" class="action-btn save-btn" @click="handleSave">
<text class="btn-text">{{ saveText }}</text>
</view>
<button
v-if="showShare"
class="action-btn share-btn"
open-type="share"
>
<text class="btn-text">{{ shareText }}</text>
</button>
</view>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
//
showSave: {
type: Boolean,
default: true
},
//
showShare: {
type: Boolean,
default: true
},
//
saveText: {
type: String,
default: '保存图片'
},
//
shareText: {
type: String,
default: '分享微信'
}
})
const emit = defineEmits(['save', 'share'])
function handleSave() {
emit('save')
}
</script>
<style scoped>
.share-actions {
display: flex;
gap: 20rpx;
padding: 0 30rpx;
}
.action-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 12rpx;
font-size: 28rpx;
}
.save-btn {
border: 2rpx solid #0074ff;
background: transparent;
}
.save-btn .btn-text {
color: #0074ff;
}
.share-btn {
background: #0074ff;
border: none;
padding: 0;
line-height: normal;
}
.share-btn::after {
border: none;
}
.share-btn .btn-text {
color: #ffffff;
}
</style>

33
hooks/usePageList.js Normal file
View File

@ -0,0 +1,33 @@
import { computed, ref, watch } from "vue";
import useDebounce from '@/utils/useDebounce'
export default function usePageList(callback, options = {}) {
const keyword = ref('')
const list = ref([])
const page = ref(1)
const pageSize = ref(options.pageSize || 20)
const pages = ref(0);
const loading = ref(false)
const total = ref(0)
const hasMore = computed(() => page.value < pages.value)
const handleKeywordChange = useDebounce(() => {
getList()
}, options.debounce || 1000)
function changePage(p) {
if (loading.value) return
page.value = p
getList()
}
function getList() {
typeof callback === 'function' && callback()
}
watch(keyword, handleKeywordChange);
return { total, page, pageSize, keyword, list, pages, changePage, loading, hasMore }
}

View File

@ -50,7 +50,7 @@
"quickapp" : {}, "quickapp" : {},
/* */ /* */
"mp-weixin" : { "mp-weixin" : {
"appid" : "wx93af55767423938e", "appid" : "wx1d8337a40c11d66c",
"setting" : { "setting" : {
"urlCheck" : false "urlCheck" : false
}, },

View File

@ -115,6 +115,12 @@
"navigationBarTitleText": "病历详情" "navigationBarTitleText": "病历详情"
} }
}, },
{
"path": "pages/case/medical-case-form",
"style": {
"navigationBarTitleText": "添加病历"
}
},
{ {
"path": "pages/case/service-record-detail", "path": "pages/case/service-record-detail",
"style": { "style": {

View File

@ -0,0 +1,473 @@
<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: "阳性发现及处理意见",
//
presentIllnessHistory: "现病史",
pastMedicalHistory: "既往史",
};
//
const FIELD_CONFIG = {
outpatient: [
{
key: "visitTime",
label: FIELD_LABELS.visitTime,
type: "date",
required: false,
},
{
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: false,
},
{
key: "diagnosisName",
label: "住院主诊断",
type: "textarea",
required: false,
},
{
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: false,
},
{
key: "inspectSummary",
label: FIELD_LABELS.inspectSummary,
type: "textarea",
required: false,
},
{
key: "positiveFind",
label: FIELD_LABELS.positiveFind,
type: "textarea",
required: false,
},
],
preConsultation: [
{
key: "chiefComplaint",
label: FIELD_LABELS.chiefComplaint,
type: "textarea",
required: false,
},
{
key: "presentIllnessHistory",
label: FIELD_LABELS.presentIllnessHistory,
type: "textarea",
required: false,
},
{
key: "pastMedicalHistory",
label: FIELD_LABELS.pastMedicalHistory,
type: "textarea",
required: false,
},
],
};
//
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

@ -5,12 +5,12 @@
<view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view> <view class="mt-12 text-base text-dark">生命全周期健康管理伙伴</view>
</view> </view>
<view class="login-btn-wrap"> <view class="login-btn-wrap">
<!-- <button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber"> <button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
手机号快捷登录
</button> -->
<button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()">
手机号快捷登录 手机号快捷登录
</button> </button>
<!-- <button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()">
手机号快捷登录
</button> -->
<button v-else class="login-btn" type="primary" @click="remind()"> <button v-else class="login-btn" type="primary" @click="remind()">
手机号快捷登录 手机号快捷登录
</button> </button>

View File

@ -120,7 +120,7 @@ const corpId = env.MP_CORP_ID;
// //
const pageParams = ref({ const pageParams = ref({
groupId: "", groupId: "",
userId: "", patientId: "",
corpId: "", corpId: "",
}); });
@ -323,8 +323,19 @@ const sendArticle = async (article) => {
imgUrl: article.cover || "", imgUrl: article.cover || "",
desc: "点击查看详情", desc: "点击查看详情",
}); });
if (result.success) { if (result.success) {
//
try {
await api("addArticleSendRecord", {
articleId: article._id,
userId: doctorInfo.userid,
customerId: pageParams.value.patientId,
corpId: corpId,
});
} catch (recordError) {
console.error("记录文章发送失败:", recordError);
}
uni.navigateBack(); uni.navigateBack();
} else { } else {
throw new Error(result.message || "发送失败"); throw new Error(result.message || "发送失败");
@ -350,8 +361,8 @@ onLoad((options) => {
if (options.groupId) { if (options.groupId) {
pageParams.value.groupId = options.groupId; pageParams.value.groupId = options.groupId;
} }
if (options.userId) { if (options.patientId) {
pageParams.value.userId = options.userId; pageParams.value.patientId = options.patientId;
} }
if (options.corpId) { if (options.corpId) {
pageParams.value.corpId = options.corpId; pageParams.value.corpId = options.corpId;

View File

@ -16,6 +16,76 @@ $primary-color: #0877F1;
background-color: #f5f5f5; background-color: #f5f5f5;
} }
/* 患者信息栏样式 */
.patient-info-bar {
position: relative;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
padding: 20rpx 32rpx;
z-index: 10;
flex-shrink: 0; /* 防止被压缩 */
}
.patient-info-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.patient-basic-info {
display: flex;
align-items: center;
gap: 16rpx;
flex: 1;
min-width: 0; /* 允许文字截断 */
}
.patient-name {
font-size: 32rpx;
color: #333;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx;
}
.patient-detail {
font-size: 28rpx;
color: #999;
white-space: nowrap;
flex-shrink: 0;
}
.patient-detail-btn {
display: flex;
align-items: center;
gap: 4rpx;
padding: 12rpx 24rpx;
background: linear-gradient(270deg, #1b5cc8 2.26%, #0877f1 94.33%);
border-radius: 40rpx;
transition: all 0.2s;
flex-shrink: 0; /* 防止按钮被压缩 */
}
.patient-detail-btn:active {
opacity: 0.8;
transform: scale(0.98);
}
.detail-btn-text {
font-size: 26rpx;
color: #fff;
font-weight: 500;
}
.arrow-icon {
font-size: 32rpx;
color: #fff;
font-weight: 600;
line-height: 1;
}
.chat-content { .chat-content {
flex: 1; flex: 1;
box-sizing: border-box; box-sizing: border-box;
@ -348,23 +418,28 @@ $primary-color: #0877F1;
.text-input, .text-input,
.voice-input-btn { .voice-input-btn {
flex: 1; flex: 1;
padding: 0 46rpx; padding: 16rpx 46rpx;
background-color: #f3f5fa; background-color: #f3f5fa;
border-radius: 20rpx; border-radius: 20rpx;
margin: 0 16rpx; margin: 0 16rpx;
font-size: 28rpx; font-size: 28rpx;
height: 80rpx; min-height: 80rpx;
max-height: 200rpx;
border: none; border: none;
outline: none; outline: none;
box-sizing: border-box; box-sizing: border-box;
display: flex; line-height: 1.5;
align-items: center;
line-height: 96rpx;
color: #333; color: #333;
} }
.voice-input-btn { .voice-input-btn {
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 46rpx;
text-align: center; text-align: center;
line-height: 80rpx;
} }
.more-panel { .more-panel {
@ -926,7 +1001,15 @@ $primary-color: #0877F1;
.text-input::-moz-placeholder, .text-input::-moz-placeholder,
.text-input:-ms-input-placeholder, .text-input:-ms-input-placeholder,
.text-input::placeholder { .text-input::placeholder {
line-height: 96rpx; line-height: normal;
}
.voice-input-btn::-webkit-input-placeholder,
.voice-input-btn:-moz-placeholder,
.voice-input-btn::-moz-placeholder,
.voice-input-btn:-ms-input-placeholder,
.voice-input-btn::placeholder {
line-height: 80rpx;
} }
/* 时间分割线 */ /* 时间分割线 */

View File

@ -0,0 +1,427 @@
<template>
<view class="ai-assistant-buttons">
<view
v-for="button in buttons"
:key="button.id"
class="ai-button"
:class="{ loading: button.loading }"
@click="handleButtonClick(button)"
>
<image class="button-icon" :src="button.icon" mode="aspectFit" />
<text class="button-text">{{
button.loading && button.loadingText ? button.loadingText : button.text
}}</text>
</view>
<!-- 病历类型选择弹窗 -->
<medical-case-type-selector
ref="typeSelectorRef"
@select="handleCaseTypeSelect"
/>
<!-- 进度显示弹窗 -->
<medical-case-progress
ref="progressRef"
@regenerate="handleRegenerateFromProgress"
@next="handleNextFromProgress"
/>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import request from "@/utils/http.js";
import api from "@/utils/api.js";
import MedicalCaseTypeSelector from "./medical-case-type-selector.vue";
import MedicalCaseProgress from "./medical-case-progress.vue";
const props = defineProps({
groupId: {
type: String,
required: true,
},
patientAccountId: {
type: String,
default: "",
},
patientId: {
type: String,
default: "",
},
corpId: {
type: String,
default: "",
},
customerId: {
type: String,
default: "",
},
});
const emit = defineEmits(["streamText", "clearInput"]);
const typeSelectorRef = ref(null);
const progressRef = ref(null);
const buttons = ref([
{
id: "followUp",
text: "追问病情",
loadingText: "AI分析中正在为您生成追问建议…",
icon: "/static/icon/zhuiwen.png",
loading: false,
},
// {
// id: "aiAssistant",
// text: "AI",
// icon: "/static/icon/kaiqiAI.png",
// loading: false,
// },
{
id: "supplementRecord",
text: "补充病历",
icon: "/static/icon/buchong.png",
loading: false,
},
]);
//
const handleButtonClick = async (button) => {
if (button.loading) return;
switch (button.id) {
case "followUp":
await handleFollowUpInquiry(button);
break;
case "aiAssistant":
handleAIAssistant(button);
break;
case "supplementRecord":
handleSupplementRecord(button);
break;
}
};
//
const handleFollowUpInquiry = async (button) => {
try {
button.loading = true;
// ID
let finalPatientAccountId = props.patientAccountId;
if (!finalPatientAccountId) {
// ID
const chatRecordsResult = await api("getChatRecordsByGroupId", {
groupID: props.groupId,
count: 5,
});
if (
chatRecordsResult.success &&
chatRecordsResult.data &&
chatRecordsResult.data.records
) {
const records = chatRecordsResult.data.records;
//
const currentUserId = uni.getStorageSync("openid") || "";
const patientMessage = records.find(
(record) =>
record.From_Account && record.From_Account !== currentUserId
);
if (patientMessage) {
finalPatientAccountId = patientMessage.From_Account;
}
}
}
if (!finalPatientAccountId) {
uni.showToast({
title: "无法获取患者信息",
icon: "none",
});
button.loading = false;
return;
}
// loading
const result = await request(
{
url: "/getYoucanData/im",
data: {
type: "followUpInquiry",
groupId: props.groupId,
patientAccountId: finalPatientAccountId,
corpId: props.corpId,
},
},
false
); // falseloading
if (result.success && result.data && result.data.suggestion) {
//
streamTextToInput(result.data.suggestion);
} else {
uni.showToast({
title: result.message || "获取追问建议失败",
icon: "none",
});
}
} catch (error) {
console.error("追问病情失败:", error);
uni.showToast({
title: "操作失败,请重试",
icon: "none",
});
} finally {
button.loading = false;
}
};
//
const streamTextToInput = (text) => {
if (!text) return;
//
emit("clearInput");
let currentIndex = 0;
const speed = 50; //
//
setTimeout(() => {
const streamInterval = setInterval(() => {
if (currentIndex < text.length) {
const char = text[currentIndex];
emit("streamText", char);
currentIndex++;
} else {
clearInterval(streamInterval);
}
}, speed);
}, 100);
};
// AI
const handleAIAssistant = (button) => {
uni.showToast({
title: "AI助手功能开发中",
icon: "none",
});
};
//
const handleSupplementRecord = (button) => {
typeSelectorRef.value?.open();
};
//
const handleCaseTypeSelect = async (type) => {
try {
//
progressRef.value?.open(type.id);
progressRef.value?.updateProgress(10);
//
await requestWithStream({
url: "/getYoucanData/im",
data: {
type: "supplementMedicalCase",
groupId: props.groupId,
patientAccountId: props.patientAccountId || props.customerId,
corpId: props.corpId,
caseType: type.id,
},
onProgress: (data) => {
//
handleStreamData(data, type.id);
},
onComplete: (finalData) => {
//
handleComplete(finalData, type.id);
},
onError: (error) => {
progressRef.value?.close();
uni.showToast({
title: error.message || "生成病历失败",
icon: "none",
});
},
});
} catch (error) {
console.error("补充病历失败:", error);
progressRef.value?.close();
uni.showToast({
title: "操作失败,请重试",
icon: "none",
});
}
};
//
const requestWithStream = async ({
url,
data,
onProgress,
onComplete,
onError,
}) => {
try {
// loading false
const result = await request(
{
url,
data,
},
false
);
if (result.success && result.data) {
//
const extractedData = result.data.extractedData || {};
//
let progressValue = 20;
const fields = Object.entries(extractedData);
const delay = 300; //
for (let i = 0; i < fields.length; i++) {
const [key, value] = fields[i];
// ""
await new Promise((resolve) => setTimeout(resolve, delay));
onProgress({ key, value });
progressValue += Math.floor(60 / fields.length);
progressRef.value?.updateProgress(Math.min(progressValue, 80));
}
//
onComplete(result.data);
} else {
onError(new Error(result.message || "请求失败"));
}
} catch (error) {
onError(error);
}
};
//
const handleStreamData = (data, caseType) => {
const { key, value } = data;
//
progressRef.value?.addDetectedInfo(key, value);
};
//
const handleComplete = (finalData, caseType) => {
progressRef.value?.updateProgress(90);
progressRef.value?.setGenerating(true);
//
setTimeout(() => {
progressRef.value?.updateProgress(100);
progressRef.value?.setGenerating(false);
//
setTimeout(() => {
progressRef.value?.setCompleted(true, finalData);
}, 500);
}, 800);
};
//
const handleRegenerateFromProgress = (data) => {
const type = { id: data.caseType };
handleCaseTypeSelect(type);
};
//
const handleNextFromProgress = (data) => {
//
const extractedData = data.data?.extractedData || {};
//
uni.navigateTo({
url: `/pages/case/medical-case-form?caseType=${data.caseType}&patientId=${
props.patientId
}&groupId=${props.groupId}&formData=${encodeURIComponent(
JSON.stringify(extractedData)
)}`,
});
};
//
onMounted(() => {
uni.$on("regenerateMedicalCase", handleRegenerateMedicalCase);
});
onUnmounted(() => {
uni.$off("regenerateMedicalCase", handleRegenerateMedicalCase);
});
const handleRegenerateMedicalCase = (data) => {
const type = { id: data.caseType };
handleCaseTypeSelect(type);
};
</script>
<style scoped lang="scss">
.ai-assistant-buttons {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx 24rpx;
background-color: #f8f9fa;
border-bottom: 1rpx solid #e5e5e5;
.ai-button {
display: flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 20rpx;
background-color: #ffffff;
border: 1rpx solid #e0e0e0;
border-radius: 40rpx;
transition: all 0.3s ease;
&.loading {
opacity: 0.8;
pointer-events: none;
.loading-icon {
width: 32rpx;
height: 32rpx;
border: 3rpx solid #e0e0e0;
border-top-color: #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
}
&:active {
background-color: #f0f0f0;
transform: scale(0.98);
}
.button-icon {
width: 32rpx;
height: 32rpx;
}
.button-text {
font-size: 28rpx;
color: #333333;
white-space: nowrap;
}
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@ -6,8 +6,9 @@
<uni-icons v-else type="mic" size="28" color="#666" /> <uni-icons v-else type="mic" size="28" color="#666" />
</view> </view>
<view class="input-area"> <view class="input-area">
<input v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..." <textarea v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..."
@confirm="sendTextMessage" @focus="handleInputFocus" /> @confirm="sendTextMessage" @focus="handleInputFocus" @input="handleInput"
:auto-height="true" :show-confirm-bar="false" :adjust-position="true" />
<input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord" <input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord"
@touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled> @touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled>
</input> </input>
@ -25,7 +26,6 @@
<text>{{ btn.text }}</text> <text>{{ btn.text }}</text>
</view> </view>
</view> </view>
<!-- 录音遮罩层 --> <!-- 录音遮罩层 -->
<view v-if="isRecording" class="recording-overlay"> <view v-if="isRecording" class="recording-overlay">
<view class="recording-modal" :class="{ 'cancel-mode': isCancelMode }"> <view class="recording-modal" :class="{ 'cancel-mode': isCancelMode }">
@ -75,6 +75,7 @@ const props = defineProps({
formatTime: { type: Function, required: true }, formatTime: { type: Function, required: true },
groupId: { type: String, default: '' }, groupId: { type: String, default: '' },
userId: { type: String, default: '' }, userId: { type: String, default: '' },
patientId: { type: String, default: '' },
corpId: { type: String, default: '' }, corpId: { type: String, default: '' },
}); });
@ -95,6 +96,11 @@ const cloudCustomData = computed(() => {
return arr.filter(Boolean).join("|"); return arr.filter(Boolean).join("|");
}); });
//
const appendStreamText = (char) => {
inputText.value += char;
};
// + // +
const recordingDuration = ref(0); const recordingDuration = ref(0);
let recordingTimer = null; let recordingTimer = null;
@ -165,9 +171,22 @@ const sendTextMessageFromPhrase = async (content) => {
}); });
}; };
//
const setInputText = (text) => {
inputText.value = text;
};
//
const clearInputText = () => {
inputText.value = '';
};
// //
defineExpose({ defineExpose({
sendTextMessageFromPhrase sendTextMessageFromPhrase,
appendStreamText,
setInputText,
clearInputText
}); });
// //
@ -367,7 +386,7 @@ const goToCommonPhrases = () => {
// //
const goToArticleList = () => { const goToArticleList = () => {
uni.navigateTo({ uni.navigateTo({
url: `/pages/message/article-list?groupId=${props.groupId}&userId=${props.userId}&corpId=${props.corpId}` url: `/pages/message/article-list?groupId=${props.groupId}&patientId=${props.patientId}&corpId=${props.corpId}`
}); });
}; };
@ -432,6 +451,13 @@ function handleInputFocus() {
}); });
} }
function handleInput(e) {
// textarea
nextTick().then(() => {
emit("scrollToBottom");
});
}
onMounted(() => { onMounted(() => {
// //
initRecorderManager(); initRecorderManager();

View File

@ -0,0 +1,378 @@
<template>
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="progress-modal">
<view class="close-btn" @click="close">
<text class="close-icon"></text>
</view>
<view class="progress-content">
<view class="progress-title">{{ progressTitle }}</view>
<view class="progress-bar-wrapper">
<view class="progress-bar">
<view
class="progress-fill"
:style="{ width: progress + '%' }"
></view>
</view>
<text class="progress-text">{{ progress }}%</text>
</view>
<view class="detected-info">
<text class="detected-title">检测到以下{{ caseTypeName }}信息</text>
<view class="info-list">
<view
v-for="(item, index) in detectedInfo"
:key="index"
class="info-item"
:class="{ 'fade-in': item.animated }"
>
<text class="check-icon"></text>
<text
class="info-text"
:class="{ 'empty-value': item.value === '暂无' }"
>
{{ item.label }}{{ item.value }}
</text>
</view>
</view>
</view>
<view v-if="isGenerating" class="generating-text">
<text class="dot-animation">正在生成结构化{{ caseTypeName }}</text>
</view>
<!-- 完成后的操作按钮 -->
<view v-if="isCompleted" class="action-buttons">
<view class="action-button secondary" @click="handleRegenerate">
<text class="button-text">重新生成</text>
</view>
<view class="action-button primary" @click="handleNext">
<text class="button-text">下一步</text>
</view>
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, computed } from "vue";
const emit = defineEmits(["regenerate", "next"]);
const popup = ref(null);
const progress = ref(0);
const detectedInfo = ref([]);
const isGenerating = ref(false);
const isCompleted = ref(false);
const caseType = ref("");
const finalData = ref(null);
const CASE_TYPE_NAMES = {
outpatient: "门诊病历",
inhospital: "住院病历",
physicalExaminationTemplate: "体检记录",
preConsultation: "预问诊记录",
};
const FIELD_LABELS = {
//
visitTime: "就诊日期",
chiefComplaint: "主诉",
medicalHistorySummary: "病史概要",
examination: "检查",
diagnosisName: "门诊诊断",
//
inhosDate: "入院日期",
operation: "手术名称",
operationDate: "手术日期",
treatmentPlan: "治疗方案",
//
inspectTime: "体检日期",
inspectSummary: "体检小结",
positiveFind: "阳性发现及处理意见",
//
presentIllnessHistory: "现病史",
pastMedicalHistory: "既往史",
};
const caseTypeName = computed(() => CASE_TYPE_NAMES[caseType.value] || "病历");
const progressTitle = computed(() => {
if (progress.value < 100) {
return `正在智能整理${caseTypeName.value}...`;
}
return `${caseTypeName.value}生成完成`;
});
const open = (type) => {
caseType.value = type;
progress.value = 0;
detectedInfo.value = [];
isGenerating.value = false;
isCompleted.value = false;
finalData.value = null;
popup.value?.open();
};
const close = () => {
popup.value?.close();
};
const updateProgress = (value) => {
progress.value = value;
};
const addDetectedInfo = (fieldKey, fieldValue) => {
const label = FIELD_LABELS[fieldKey] || fieldKey;
// ""
const displayValue = fieldValue && fieldValue.trim() ? fieldValue : "暂无";
detectedInfo.value.push({
label,
value: displayValue,
animated: true,
});
};
const setGenerating = (value) => {
isGenerating.value = value;
};
const setCompleted = (value, data = null) => {
isCompleted.value = value;
finalData.value = data;
};
const reset = () => {
progress.value = 0;
detectedInfo.value = [];
isGenerating.value = false;
isCompleted.value = false;
finalData.value = null;
};
const handleRegenerate = () => {
emit("regenerate", { caseType: caseType.value });
close();
};
const handleNext = () => {
emit("next", {
caseType: caseType.value,
data: finalData.value,
});
close();
};
defineExpose({
open,
close,
updateProgress,
addDetectedInfo,
setGenerating,
setCompleted,
reset,
});
</script>
<style scoped lang="scss">
.progress-modal {
width: 600rpx;
background-color: #ffffff;
border-radius: 24rpx;
padding: 48rpx 40rpx;
position: relative;
max-height: 80vh;
overflow-y: auto;
.close-btn {
position: absolute;
top: 20rpx;
right: 20rpx;
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
.close-icon {
font-size: 40rpx;
color: #999999;
}
}
.progress-content {
.progress-title {
font-size: 32rpx;
font-weight: 600;
color: #333333;
margin-bottom: 32rpx;
text-align: center;
}
.progress-bar-wrapper {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 32rpx;
.progress-bar {
flex: 1;
height: 16rpx;
background-color: #e5e5e5;
border-radius: 8rpx;
overflow: hidden;
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #1890ff 0%, #40a9ff 100%);
transition: width 0.5s ease;
}
}
.progress-text {
font-size: 28rpx;
color: #1890ff;
font-weight: 600;
min-width: 80rpx;
text-align: right;
}
}
.detected-info {
margin-bottom: 24rpx;
.detected-title {
font-size: 28rpx;
color: #666666;
display: block;
margin-bottom: 16rpx;
}
.info-list {
max-height: 400rpx;
overflow-y: auto;
.info-item {
display: flex;
align-items: flex-start;
gap: 12rpx;
margin-bottom: 12rpx;
opacity: 0;
transform: translateX(-20rpx);
&.fade-in {
animation: fadeInSlide 0.4s ease forwards;
}
.check-icon {
font-size: 28rpx;
color: #52c41a;
font-weight: bold;
margin-top: 2rpx;
}
.info-text {
flex: 1;
font-size: 26rpx;
color: #333333;
line-height: 1.6;
word-break: break-all;
//
&.empty-value {
color: #999999;
}
}
}
}
}
.generating-text {
font-size: 28rpx;
color: #1890ff;
text-align: center;
padding: 16rpx 0;
.dot-animation::after {
content: "...";
animation: dots 1.5s steps(4, end) infinite;
}
}
.action-buttons {
display: flex;
gap: 16rpx;
margin-top: 32rpx;
.action-button {
flex: 1;
height: 80rpx;
border-radius: 40rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&.primary {
background: linear-gradient(90deg, #1890ff 0%, #40a9ff 100%);
.button-text {
color: #ffffff;
}
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
&.secondary {
background-color: #ffffff;
border: 2rpx solid #d9d9d9;
.button-text {
color: #666666;
}
&:active {
background-color: #f5f5f5;
transform: scale(0.98);
}
}
.button-text {
font-size: 30rpx;
font-weight: 500;
}
}
}
}
}
@keyframes fadeInSlide {
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes dots {
0%,
20% {
content: "";
}
40% {
content: ".";
}
60% {
content: "..";
}
80%,
100% {
content: "...";
}
}
</style>

View File

@ -0,0 +1,134 @@
<template>
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="medical-case-selector">
<view class="selector-header">
<text class="header-title">快速生成:</text>
<view class="close-btn" @click="close">
<text class="close-icon"></text>
</view>
</view>
<view class="case-type-grid">
<view
v-for="type in caseTypes"
:key="type.id"
class="case-type-item"
@click="selectType(type)"
>
<text class="type-name">{{ type.name }}</text>
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref } from "vue";
const emit = defineEmits(["select"]);
const popup = ref(null);
const caseTypes = [
{
id: "outpatient",
name: "门诊病历",
},
{
id: "inhospital",
name: "住院病历",
},
{
id: "physicalExaminationTemplate",
name: "体检记录",
},
{
id: "preConsultation",
name: "预问诊记录",
},
];
const open = () => {
popup.value?.open();
};
const close = () => {
popup.value?.close();
};
const selectType = (type) => {
emit("select", type);
close();
};
defineExpose({
open,
close,
});
</script>
<style scoped lang="scss">
.medical-case-selector {
width: 600rpx;
background-color: #ffffff;
border-radius: 24rpx;
padding: 40rpx;
.selector-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #333333;
}
.close-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
.close-icon {
font-size: 40rpx;
color: #999999;
}
}
}
.case-type-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24rpx;
.case-type-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx 20rpx;
background-color: #f8f9fa;
border: 2rpx solid #e5e5e5;
border-radius: 16rpx;
transition: all 0.3s ease;
&:active {
background-color: #e8f4ff;
border-color: #1890ff;
transform: scale(0.98);
}
.type-name {
font-size: 30rpx;
color: #333333;
text-align: center;
font-weight: 500;
}
}
}
}
</style>

View File

@ -0,0 +1,317 @@
<template>
<view class="header-container">
<view class="header-content">
<!-- 团队选择器 -->
<view class="team-selector" @click="showTeamPicker = true">
<text class="team-name">{{ currentTeamName }}</text>
<image class="arrow-icon" src="/static/zhuanhua.svg" mode="aspectFit" />
</view>
<!-- 右侧操作按钮 -->
<view class="header-actions">
<view class="action-btn" @click="handleAddPatient">
<image
class="invite-icon"
src="/static/work/qrcode.svg"
mode="aspectFit"
/>
<text class="action-text">邀请患者</text>
</view>
</view>
</view>
<!-- 标签页切换 -->
<view class="tabs-container">
<view
class="tab-item"
:class="{ active: activeTab === 'processing' }"
@click="handleTabChange('processing')"
>
<text class="tab-text">处理中</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'finished' }"
@click="handleTabChange('finished')"
>
<text class="tab-text">已结束</text>
</view>
</view>
<!-- 团队选择弹窗 -->
<view
v-if="showTeamPicker"
class="team-picker-overlay"
@click="showTeamPicker = false"
>
<view class="team-picker-content" @click.stop>
<view class="team-picker-header">
<text class="picker-title">选择团队</text>
</view>
<scroll-view class="team-list" scroll-y>
<view
v-for="team in teamList"
:key="team.teamId"
class="team-item"
:class="{ active: currentTeamId === team.teamId }"
@click="selectTeam(team)"
>
<text class="team-item-name">{{ team.name }}</text>
<text v-if="currentTeamId === team.teamId" class="check-icon"></text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { storeToRefs } from "pinia";
import useTeamStore from "@/store/team.js";
// Props
const props = defineProps({
activeTab: {
type: String,
default: "processing",
},
});
// Emits
const emit = defineEmits(["update:activeTab", "teamChange", "addPatient"]);
//
const teamStore = useTeamStore();
const { teams } = storeToRefs(teamStore);
//
const showTeamPicker = ref(false);
const currentTeamId = ref(""); // ""
// ""
const teamList = computed(() => {
const allOption = { teamId: "", name: "全部会话消息" };
return [allOption, ...(teams.value || [])];
});
//
const currentTeamName = computed(() => {
if (!currentTeamId.value) return "全部会话消息";
const team = teams.value.find((t) => t.teamId === currentTeamId.value);
return team ? team.name : "全部会话消息";
});
//
const selectTeam = (team) => {
currentTeamId.value = team.teamId;
showTeamPicker.value = false;
console.log("切换到团队:", team.name);
emit("teamChange", team.teamId);
};
//
const handleTabChange = (tab) => {
if (props.activeTab === tab) return;
emit("update:activeTab", tab);
};
//
const handleAddPatient = () => {
emit("addPatient");
};
</script>
<style scoped lang="scss">
.header-container {
background-color: #fff;
flex-shrink: 0;
}
.header-content {
display: flex;
align-items: center;
padding: 16rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.team-selector {
flex: 1;
display: flex;
align-items: center;
min-width: 0;
cursor: pointer;
&:active {
opacity: 0.7;
}
}
.team-name {
font-size: 32rpx;
font-weight: 500;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow-icon {
width: 24rpx;
height: 24rpx;
margin-left: 8rpx;
flex-shrink: 0;
opacity: 0.6;
filter: brightness(0.5);
}
.header-actions {
display: flex;
align-items: center;
gap: 16rpx;
margin-left: 16rpx;
flex-shrink: 0;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6rpx 12rpx;
// background-color: #f5f5f5;
border-radius: 8rpx;
&:active {
opacity: 0.7;
}
}
.invite-icon {
width: 32rpx;
height: 32rpx;
margin-bottom: 4rpx;
}
.action-text {
font-size: 20rpx;
color: #666;
line-height: 1.4;
text-align: center;
}
.tabs-container {
display: flex;
padding: 0 32rpx;
gap: 48rpx;
}
.tab-item {
position: relative;
padding: 20rpx 0;
cursor: pointer;
&.active {
.tab-text {
color: #1890ff;
font-weight: 500;
}
&::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 4rpx;
background-color: #1890ff;
border-radius: 2rpx;
}
}
}
.tab-text {
font-size: 28rpx;
color: #666;
transition: color 0.3s;
}
.team-picker-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 200rpx;
}
.team-picker-content {
width: 600rpx;
max-height: 800rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.team-picker-header {
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
text-align: center;
}
.picker-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.team-list {
flex: 1;
overflow-y: auto;
}
.team-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
cursor: pointer;
&:active {
background-color: #f5f5f5;
}
&.active {
background-color: #e6f7ff;
.team-item-name {
color: #1890ff;
}
}
&:last-child {
border-bottom: none;
}
}
.team-item-name {
font-size: 30rpx;
color: #333;
flex: 1;
}
.check-icon {
font-size: 32rpx;
color: #1890ff;
font-weight: bold;
}
</style>

View File

@ -1,5 +1,7 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { onShow, onUnload } from '@dcloudio/uni-app' import { onShow, onUnload } from '@dcloudio/uni-app'
import api from '@/utils/api.js'
import useTeamStore from '@/store/team.js'
/** /**
* 简单的群聊hook * 简单的群聊hook
@ -8,6 +10,9 @@ import { onShow, onUnload } from '@dcloudio/uni-app'
export default function useGroupChat(groupID) { export default function useGroupChat(groupID) {
const groupInfo = ref({}) const groupInfo = ref({})
const members = ref([]) const members = ref([])
const teamMemberIds = ref([]) // 存储团队成员的userId列表
const patientId = ref('') // 存储患者ID
const teamStore = useTeamStore()
// 群聊成员映射 // 群聊成员映射
const chatMember = computed(() => { const chatMember = computed(() => {
@ -15,30 +20,79 @@ export default function useGroupChat(groupID) {
members.value.forEach(member => { members.value.forEach(member => {
res[member.id] = { res[member.id] = {
name: member.name, name: member.name,
avatar: member.avatar || '/static/default-avatar.png' avatar: member.avatar,
isTeamMember: member.isTeamMember // 标记是否为团队成员
} }
}) })
return res return res
}) })
// 获取群聊信息 // 判断某个userId是否为团队成员
const isTeamMember = (userId) => {
return teamMemberIds.value.includes(userId)
}
// 获取用户头像(根据是否为团队成员返回不同的默认头像)
const getUserAvatar = (userId) => {
const member = chatMember.value[userId]
if (!member) {
// 如果找不到成员信息,根据是否为团队成员返回默认头像
return isTeamMember(userId) ? '/static/home/avatar.svg' : '/static/default-patient-avatar.png'
}
// 如果有头像且不为空字符串,返回头像
if (member.avatar && member.avatar.trim() !== '') {
return member.avatar
}
// 否则根据是否为团队成员返回默认头像
return member.isTeamMember ? '/static/home/avatar.svg' : '/static/default-patient-avatar.png'
}
// 获取群聊信息和成员头像
async function getGroupInfo() { async function getGroupInfo() {
const gid = typeof groupID === 'string' ? groupID : groupID.value const gid = typeof groupID === 'string' ? groupID : groupID.value
if (!gid) return if (!gid) return
try { try {
// 这里可以调用API获取群聊信息 // 1. 获取群聊基本信息
// const res = await getGroupDetail(gid) const groupResult = await api('getGroupListByGroupId', { groupId: gid })
// if (res && res.success) {
// groupInfo.value = res.data
// members.value = res.data.members || []
// }
// 暂时使用本地数据 if (groupResult && groupResult.success && groupResult.data) {
groupInfo.value = { groupInfo.value = {
groupID: gid, groupID: gid,
name: '群聊', name: groupResult.data.team?.name || '群聊',
status: 'active' status: groupResult.data.orderStatus || 'active',
teamId: groupResult.data.teamId
}
// 2. 如果有teamId获取团队成员头像
if (groupResult.data.teamId) {
const avatarMap = await teamStore.getTeamMemberAvatars(groupResult.data.teamId)
// 3. 存储团队成员ID列表
teamMemberIds.value = Object.keys(avatarMap)
// 4. 构建团队成员列表
members.value = teamMemberIds.value.map(userId => ({
id: userId,
name: userId, // 这里可以从其他地方获取真实姓名
avatar: avatarMap[userId] || '',
isTeamMember: true
}))
// 5. 添加患者信息(使用默认患者头像)
if (groupResult.data.patient) {
const pid = groupResult.data.patientId?.toString() || ''
patientId.value = pid
members.value.push({
id: pid,
name: groupResult.data.patient.name || '患者',
avatar: '', // 患者不设置头像,使用默认
isTeamMember: false
})
}
}
} }
} catch (error) { } catch (error) {
console.error('获取群聊信息失败:', error) console.error('获取群聊信息失败:', error)
@ -57,6 +111,8 @@ export default function useGroupChat(groupID) {
groupInfo, groupInfo,
members, members,
chatMember, chatMember,
getGroupInfo getGroupInfo,
isTeamMember,
getUserAvatar
} }
} }

View File

@ -1,5 +1,20 @@
<template> <template>
<view class="chat-page"> <view class="chat-page">
<!-- 患者信息栏 -->
<view class="patient-info-bar" v-if="patientInfo.name">
<view class="patient-info-content">
<view class="patient-basic-info">
<text class="patient-name">{{ patientInfo.name }}</text>
<text class="patient-detail"
>{{ patientInfo.sex }} · {{ patientInfo.age }}</text
>
</view>
<view class="patient-detail-btn" @click="handleViewPatientDetail">
<text class="detail-btn-text">查看档案</text>
</view>
</view>
</view>
<!-- 聊天消息区域 --> <!-- 聊天消息区域 -->
<scroll-view <scroll-view
class="chat-content" class="chat-content"
@ -50,23 +65,16 @@
<!-- 消息内容 --> <!-- 消息内容 -->
<view v-else class="message-content"> <view v-else class="message-content">
<!-- 医生头像左侧 -->
<image <image
v-if="message.flow === 'in'" v-if="message.flow === 'in'"
class="doctor-msg-avatar" class="doctor-msg-avatar"
:src=" :src="getUserAvatar(message.from)"
chatMember[message.from]?.avatar || '/static/default-avatar.png'
"
mode="aspectFill" mode="aspectFill"
/> />
<!-- 患者头像右侧 -->
<image <image
v-if="message.flow === 'out'" v-if="message.flow === 'out'"
class="user-msg-avatar" class="user-msg-avatar"
:src=" :src="getUserAvatar(message.from)"
chatMember[message.from]?.avatar || '/static/home/avatar.svg'
"
mode="aspectFill" mode="aspectFill"
/> />
@ -125,6 +133,21 @@
@cancel="handleRejectReasonCancel" @cancel="handleRejectReasonCancel"
/> />
<!-- AI助手按钮组 -->
<AIAssistantButtons
v-if="
!isEvaluationPopupOpen &&
!showConsultAccept &&
orderStatus === 'processing'
"
:groupId="groupId"
:patientAccountId="chatInfo.userID || ''"
:patientId="patientId"
:corpId="corpId"
@streamText="handleStreamText"
@clearInput="handleClearInput"
/>
<!-- 聊天输入组件 --> <!-- 聊天输入组件 -->
<ChatInput <ChatInput
v-if="!isEvaluationPopupOpen && !showConsultAccept" v-if="!isEvaluationPopupOpen && !showConsultAccept"
@ -137,6 +160,7 @@
: '' : ''
" "
:userId="openid" :userId="openid"
:patientId="patientId"
:corpId="corpId" :corpId="corpId"
@scrollToBottom="() => scrollToBottom(true)" @scrollToBottom="() => scrollToBottom(true)"
@messageSent="() => scrollToBottom(true)" @messageSent="() => scrollToBottom(true)"
@ -175,6 +199,7 @@ import ChatInput from "./components/chat-input.vue";
import SystemMessage from "./components/system-message.vue"; import SystemMessage from "./components/system-message.vue";
import ConsultAccept from "./components/consult-accept.vue"; import ConsultAccept from "./components/consult-accept.vue";
import RejectReasonModal from "./components/reject-reason-modal.vue"; import RejectReasonModal from "./components/reject-reason-modal.vue";
import AIAssistantButtons from "./components/ai-assistant-buttons.vue";
const timChatManager = globalTimChatManager; const timChatManager = globalTimChatManager;
@ -190,12 +215,12 @@ const { initIMAfterLogin } = useAccountStore();
const chatInputRef = ref(null); const chatInputRef = ref(null);
const groupId = ref(""); const groupId = ref("");
const { chatMember, getGroupInfo } = useGroupChat(groupId); const { chatMember, getGroupInfo, getUserAvatar } = useGroupChat(groupId);
// //
const updateNavigationTitle = () => { const updateNavigationTitle = (title = "群聊") => {
uni.setNavigationBarTitle({ uni.setNavigationBarTitle({
title: "群聊", title: title,
}); });
}; };
@ -211,6 +236,17 @@ const isEvaluationPopupOpen = ref(false);
// //
const orderStatus = ref(""); const orderStatus = ref("");
//
const patientInfo = ref({
name: "",
sex: "",
age: "",
mobile: "",
});
// ID
const patientId = ref("");
// - pending // - pending
const showConsultAccept = computed(() => orderStatus.value === "pending"); const showConsultAccept = computed(() => orderStatus.value === "pending");
@ -270,8 +306,29 @@ const fetchGroupOrderStatus = async () => {
if (result.success && result.data) { if (result.success && result.data) {
orderStatus.value = result.data.orderStatus || ""; orderStatus.value = result.data.orderStatus || "";
//
const teamName = result.data.team?.name || "群聊";
updateNavigationTitle(teamName);
//
if (result.data.patient) {
patientInfo.value = {
name: result.data.patient.name || "",
sex: result.data.patient.sex || "",
age: result.data.patient.age || "",
mobile: result.data.patient.mobile || "",
};
}
// ID
if (result.data.patientId) {
patientId.value = result.data.patientId.toString();
}
console.log("获取群组订单状态:", { console.log("获取群组订单状态:", {
orderStatus: orderStatus.value, orderStatus: orderStatus.value,
teamName: teamName,
patientInfo: patientInfo.value,
groupId: groupId.value, groupId: groupId.value,
}); });
} else { } else {
@ -282,12 +339,6 @@ const fetchGroupOrderStatus = async () => {
} }
}; };
//
function checkConsultPendingStatus() {
//
fetchGroupOrderStatus();
}
// //
function getBubbleClass(message) { function getBubbleClass(message) {
// //
@ -390,18 +441,17 @@ const initTIMCallbacks = async () => {
}); });
// 0 // 0
if (timChatManager.tim && timChatManager.isLoggedIn && chatInfo.value.conversationID) { if (
timChatManager.tim &&
timChatManager.isLoggedIn &&
chatInfo.value.conversationID
) {
timChatManager.tim timChatManager.tim
.setMessageRead({ .setMessageRead({
conversationID: chatInfo.value.conversationID, conversationID: chatInfo.value.conversationID,
}) })
.then(() => { .then(() => {
console.log("✓ 收到新消息后已标记为已读"); console.log("✓ 收到新消息后已标记为已读");
// 0
timChatManager.triggerCallback('onConversationListUpdated', {
conversationID: chatInfo.value.conversationID,
unreadCount: 0
});
}) })
.catch((error) => { .catch((error) => {
console.error("✗ 标记已读失败:", error); console.error("✗ 标记已读失败:", error);
@ -461,9 +511,6 @@ const initTIMCallbacks = async () => {
isCompleted.value = data.isCompleted || false; isCompleted.value = data.isCompleted || false;
isLoadingMore.value = false; isLoadingMore.value = false;
//
checkConsultPendingStatus();
nextTick(() => { nextTick(() => {
if (data.isRefresh) { if (data.isRefresh) {
console.log("后台刷新完成,保持当前滚动位置"); console.log("后台刷新完成,保持当前滚动位置");
@ -558,11 +605,6 @@ const loadMessageList = async () => {
}) })
.then(() => { .then(() => {
console.log("✓ 会话已标记为已读:", chatInfo.value.conversationID); console.log("✓ 会话已标记为已读:", chatInfo.value.conversationID);
//
timChatManager.triggerCallback('onConversationListUpdated', {
conversationID: chatInfo.value.conversationID,
unreadCount: 0
});
}) })
.catch((error) => { .catch((error) => {
console.error("✗ 标记会话已读失败:", error); console.error("✗ 标记会话已读失败:", error);
@ -719,13 +761,32 @@ onShow(() => {
// //
onHide(() => { onHide(() => {
stopIMMonitoring(); stopIMMonitoring();
// ID
timChatManager.currentConversationID = null;
console.log("✓ 页面隐藏已清空当前会话ID");
}); });
const sendCommonPhrase = (content) => { const sendCommonPhrase = (content) => {
if (chatInputRef.value) { if (chatInputRef.value) {
chatInputRef.value.sendTextMessageFromPhrase(content); //
chatInputRef.value.setInputText(content);
} }
}; };
//
const handleStreamText = (char) => {
if (chatInputRef.value) {
chatInputRef.value.appendStreamText(char);
}
};
//
const handleClearInput = () => {
if (chatInputRef.value) {
chatInputRef.value.clearInputText();
}
};
// //
defineExpose({ defineExpose({
sendCommonPhrase, sendCommonPhrase,
@ -824,6 +885,21 @@ const handleRejectReasonConfirm = async (reason) => {
const handleRejectReasonCancel = () => { const handleRejectReasonCancel = () => {
showRejectReasonModal.value = false; showRejectReasonModal.value = false;
}; };
//
const handleViewPatientDetail = () => {
if (!patientId.value) {
uni.showToast({
title: "患者信息不完整",
icon: "none",
});
return;
}
uni.navigateTo({
url: `/pages/case/archive-detail?id=${patientId.value}`,
});
};
// //
const handleEndConsult = async () => { const handleEndConsult = async () => {
try { try {
@ -966,4 +1042,4 @@ uni.$on("sendSurvey", async (data) => {
<style scoped lang="scss"> <style scoped lang="scss">
@import "./chat.scss"; @import "./chat.scss";
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,211 @@
<template>
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="bg-white rounded overflow-hidden" style="width: 690rpx;">
<view class="px-15 py-12 text-center text-lg font-semibold text-dark border-b">
全部筛选
</view>
<view class="py-15">
<scroll-view scroll-y="true" style="max-height: 60vh;">
<view class="px-15">
<view class="text-base font-semibold text-dark">任务状态</view>
<view class="flex flex-wrap">
<view v-for="(i, idx) in tabs" :key="i.value"
class="mt-10 w-72 py-5 text-sm text-center leading-normal text-dark rounded-sm"
:class="[form.eventStatus === i.value ? 'bg-primary border-primary text-white' : 'border', idx % 4 === 0 ? '' : 'ml-5']"
@click="form.eventStatus = i.value">
{{ i.label }}
</view>
</view>
<view class="pt-10 text-base font-semibold text-dark">任务类型</view>
<view class="flex flex-wrap">
<view v-for="(i, idx) in eventTypeList" :key="i.value"
class="mt-10 w-72 py-5 text-sm text-center leading-normal text-dark truncate rounded-sm"
:class="[selectedType[i.value] ? 'bg-primary border-primary text-white' : 'border', idx % 4 === 0 ? '' : 'ml-5']"
@click="changeEvent(i.value)">
{{ i.label }}
</view>
</view>
<view class="py-10 text-base font-semibold text-dark">所属团队</view>
<view class="flex items-center justify-between px-10 py-5 border rounded-sm" @click="selectTeam()">
<view class="mr-10 w-0 flex-grow text-base" :class="teamName ? 'text-dark' : 'text-gray'">
{{ teamName || '全部' }}
</view>
<view class="flex-shrink-0" @click="clearTeam()">
<uni-icons v-if="teamName" type="closeempty" size="16" color="#999"></uni-icons>
<uni-icons v-else type="down" size="16" color="#999"></uni-icons>
</view>
</view>
<view class="py-10 text-base font-semibold text-dark">计划日期</view>
<view class="flex items-center justify-between px-10 py-5 border rounded-sm">
<view class="mr-10 w-0 flex-grow text-base truncate">
<uni-datetime-picker v-model="form.dates" type="daterange">
<view class="w-full truncate">
<text v-if="form.dates.length" class="text-base text-dark">
{{ form.dates[0] }} - {{ form.dates[1] }}
</text>
<text v-else class="text-base text-gray">请选择计划日期</text>
</view>
</uni-datetime-picker>
</view>
<view class="flex-shrink-0" @click="clearDates()">
<uni-icons v-if="form.dates.length" type="closeempty" size="16" color="#999"></uni-icons>
<uni-icons v-else type="down" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
</scroll-view>
</view>
<view class="flex justify-center items-center px-15 py-12 text-center">
<view class="mr-10 py-5 flex-grow border text-base text-dark rounded-sm" @click="close()">取消</view>
<view class="mr-10 py-5 flex-grow border-auto text-base text-primary rounded-sm" @click="reset()">重置</view>
<view class="py-5 flex-grow bg-primary text-base text-white rounded-sm" @click="confirm()">确定</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import { storeToRefs } from "pinia";
import dayjs from 'dayjs';
import { ToDoEventType } from '@/baseData';
import useTeamStore from "@/store/team.js";
const emits = defineEmits(['close', 'confirm'])
const props = defineProps({
data: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
})
const tabs = [
{ label: "全部", value: "all" },
{ label: "待处理", value: "processing" },
{ label: "未开始", value: "notStart" },
{ label: "已完成", value: "treated" },
{ label: "已取消", value: "cancelled" },
{ label: "已过期", value: "expired" },
];
const eventTypeList = [{ label: '全部', value: 'all' }, ...Object.keys(ToDoEventType).map(key => ({ label: ToDoEventType[key], value: key }))];
const popup = ref();
const { teams } = storeToRefs(useTeamStore());
const { getTeams } = useTeamStore();
const form = ref({ eventStatus: 'processing', dates: [], eventType: ['all'], teamId: '' });
const selectedType = computed(() => form.value.eventType.reduce((m, item) => {
m[item] = true;
return m
}, {}))
const teamName = computed(() => {
const t = teams.value.find(i => i.teamId === form.value.teamId);
return t ? t.name : ''
})
function changeEvent(val) {
if (val === 'all') {
form.value.eventType = ['all'];
return
}
if (selectedType.value[val]) {
let newList = form.value.eventType.filter(i => i !== val);
newList = newList.length ? newList : ['all'];
form.value.eventType = newList;
} else {
let newList = [...form.value.eventType].filter(i => i !== 'all');
newList.push(val);
form.value.eventType = newList;
}
}
function clearDates() {
if (form.value.dates.length) {
form.value.dates = [];
}
}
function clearTeam() {
form.value.teamId = '';
}
function close() {
emits('close')
}
function confirm() {
const data = {eventStatus: form.value.eventStatus};
if (form.value.eventType.length && form.value.eventType[0] !== 'all') {
data.eventType = form.value.eventType;
}
if (form.value.teamId) {
data.executeTeamId = form.value.teamId;
}
if (form.value.dates.length) {
data.startDate = form.value.dates[0];
data.endDate = form.value.dates[1];
}
emits('confirm', data)
close()
}
function init() {
const data = props.data || {};
const tab = tabs.find(i => i.value === data.eventStatus);
form.value.eventStatus = tab ? tab.value : 'all';
const eventType = Array.isArray(data.eventType) ? data.eventType : ['all'];
form.value.eventType = eventType;
form.value.teamId = data.executeTeamId || '';
const startDate = data.startDate && dayjs(data.startDate).isValid() ? dayjs(data.startDate).format('YYYY-MM-DD') : '';
const endDate = data.endDate && dayjs(data.endDate).isValid() ? dayjs(data.endDate).format('YYYY-MM-DD') : '';
if (startDate && endDate) {
form.value.dates = [startDate, endDate];
} else {
form.value.dates = [];
}
}
function reset() {
form.value = { eventStatus: 'processing', dates: [], eventType: ['all'], teamId: '' };
}
function selectTeam() {
const list = teams.value.map(i => i.name);
uni.showActionSheet({
itemList: ['全部', ...list],
success: res => {
const index = res.tapIndex - 1;
form.value.teamId = index === -1 ? '' : teams.value[index].teamId;
}
})
}
watch(() => props.visible, async n => {
if (n) {
if (teams.value.length === 0) {
getTeams();
}
init()
popup.value && popup.value.open()
} else {
popup.value && popup.value.close()
}
})
</script>
<style lang="scss" scoped>
.ml-5 {
margin-left: 10rpx;
}
.w-72 {
width: 144rpx;
}
</style>

View File

@ -2,23 +2,59 @@
<full-page> <full-page>
<view class="p-15"> <view class="p-15">
<view class="bg-white px-10 mb-10 rounded"> <view class="bg-white px-10 mb-10 rounded">
<form-input :form="formData" :required="rule.anotherName.required" wordLimit="10" title="anotherName" <form-input
:name="rule.anotherName.name" @change="onChange($event)" /> :form="formData"
:required="rule.anotherName.required"
wordLimit="10"
title="anotherName"
:name="rule.anotherName.name"
@change="onChange($event)"
/>
<common-cell title="avatar" name="头像"> <common-cell title="avatar" name="头像">
<view class="flex-grow flex items-center justify-end" @click="chooseAvatar()"> <view
<image v-if="formData.avatar" class="avatar mr-5 rounded-full" :src="formData.avatar" /> class="flex-grow flex items-center justify-end"
<image v-else class="avatar mr-5 rounded-full" src="/static/default-avatar.png" /> @click="chooseAvatar()"
>
<image
v-if="formData.avatar"
class="avatar mr-5 rounded-full"
:src="formData.avatar"
/>
<image
v-else
class="avatar mr-5 rounded-full"
src="/static/home/avatar.svg"
/>
<uni-icons color="#999" type="right" size="16" /> <uni-icons color="#999" type="right" size="16" />
</view> </view>
</common-cell> </common-cell>
<form-select :form="formData" name="性别" title="gender" :range="genderOptions" @change="onChange($event)" /> <form-select
<form-input :form="formData" disableChange wordLimit="11" title="mobile" name="手机号 (不可修改)" /> :form="formData"
name="性别"
title="gender"
:range="genderOptions"
@change="onChange($event)"
/>
<form-input
:form="formData"
disableChange
wordLimit="11"
title="mobile"
name="手机号 (不可修改)"
/>
</view> </view>
<view class="bg-white px-10 mb-10 rounded"> <view class="bg-white px-10 mb-10 rounded">
<!-- 填写认证资料的时候岗位必填 --> <!-- 填写认证资料的时候岗位必填 -->
<common-cell :required="type === 'cert'" title="job" :name="rule.job.name"> <common-cell
<view class="flex-grow flex items-center justify-end" @click="selectJob()"> :required="type === 'cert'"
title="job"
:name="rule.job.name"
>
<view
class="flex-grow flex items-center justify-end"
@click="selectJob()"
>
<view v-if="jobStr" class="text-base text-base">{{ jobStr }}</view> <view v-if="jobStr" class="text-base text-base">{{ jobStr }}</view>
<!-- <view class="mr-5 rounded-full" style="width: 64rpx;height: 64rpx;background: red;"></view> --> <!-- <view class="mr-5 rounded-full" style="width: 64rpx;height: 64rpx;background: red;"></view> -->
<uni-icons color="#999" type="right" size="16" /> <uni-icons color="#999" type="right" size="16" />
@ -39,12 +75,24 @@
</view> </view>
<view class="bg-white rounded"> <view class="bg-white rounded">
<form-textarea autoHeight :border="false" :form="formData" title="intro" name="个人介绍" :wordLimit="300" <form-textarea
@change="onChange($event)" /> autoHeight
:border="false"
:form="formData"
title="intro"
name="个人介绍"
:wordLimit="300"
@change="onChange($event)"
/>
</view> </view>
</view> </view>
<template #footer> <template #footer>
<button-footer :cancelText="cancelText" :confirmText="confirmText" @confirm="save()" @cancel="back()" /> <button-footer
:cancelText="cancelText"
:confirmText="confirmText"
@confirm="save()"
@cancel="back()"
/>
</template> </template>
</full-page> </full-page>
</template> </template>
@ -58,46 +106,56 @@ import api from "@/utils/api.js";
import { upload } from "@/utils/http.js"; import { upload } from "@/utils/http.js";
import { toast } from "@/utils/widget"; import { toast } from "@/utils/widget";
import buttonFooter from '@/components/button-footer.vue'; import buttonFooter from "@/components/button-footer.vue";
import commonCell from "@/components/form-template/common-cell.vue"; import commonCell from "@/components/form-template/common-cell.vue";
import FormInput from "@/components/form-template/form-cell/form-input.vue"; import FormInput from "@/components/form-template/form-cell/form-input.vue";
import FormSelect from "@/components/form-template/form-cell/form-select.vue"; import FormSelect from "@/components/form-template/form-cell/form-select.vue";
import FormTextarea from "@/components/form-template/form-cell/form-textarea.vue"; import FormTextarea from "@/components/form-template/form-cell/form-textarea.vue";
import fullPage from '@/components/full-page.vue'; import fullPage from "@/components/full-page.vue";
const { account, doctorInfo } = storeToRefs(useAccountStore()); const { account, doctorInfo } = storeToRefs(useAccountStore());
const { useLoad, useShow } = useGuard(); const { useLoad, useShow } = useGuard();
const { getDoctorInfo } = useAccountStore(); const { getDoctorInfo } = useAccountStore();
const job = { assistant: '医生助理', doctor: '医生' }; const job = { assistant: "医生助理", doctor: "医生" };
const form = ref({}); const form = ref({});
const inviteTeamId = ref('') const inviteTeamId = ref("");
const type = ref(''); const type = ref("");
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value, mobile: account.value?.mobile })); const formData = computed(() => ({
const cancelText = computed(() => doctorInfo.value ? '取消' : '暂不填写'); ...(doctorInfo.value || {}),
const confirmText = computed(() => type.value === 'cert' ? '下一步' : '保存'); ...form.value,
mobile: account.value?.mobile,
}));
const cancelText = computed(() => (doctorInfo.value ? "取消" : "暂不填写"));
const confirmText = computed(() => (type.value === "cert" ? "下一步" : "保存"));
const jobStr = computed(() => { const jobStr = computed(() => {
const jobs = formData.value && Array.isArray(formData.value.job) ? formData.value.job.filter(i => i === 'assistant' || i === 'doctor') : []; const jobs =
return jobs[0] && job[jobs[0]] ? job[jobs[0]] : ''; formData.value && Array.isArray(formData.value.job)
}) ? formData.value.job.filter((i) => i === "assistant" || i === "doctor")
: [];
return jobs[0] && job[jobs[0]] ? job[jobs[0]] : "";
});
const rule = computed(() => { const rule = computed(() => {
if (doctorInfo.value && ['verified', 'verifying'].includes(doctorInfo.value.verifyStatus)) { if (
doctorInfo.value &&
["verified", "verifying"].includes(doctorInfo.value.verifyStatus)
) {
return { return {
anotherName: { name: '姓名 (不可修改)', required: false, disabled: true }, anotherName: { name: "姓名 (不可修改)", required: false, disabled: true },
job: { name: '岗位 (不可修改)', disabled: true }, job: { name: "岗位 (不可修改)", disabled: true },
title: { name: '职称 (不可修改)', disabled: true }, title: { name: "职称 (不可修改)", disabled: true },
dept: { name: '科室 (不可修改)', disabled: true }, dept: { name: "科室 (不可修改)", disabled: true },
} };
} }
return { return {
anotherName: { name: '姓名', required: true, disabled: false }, anotherName: { name: "姓名", required: true, disabled: false },
job: { name: '岗位', disabled: false }, job: { name: "岗位", disabled: false },
title: { name: '职称', disabled: false }, title: { name: "职称", disabled: false },
dept: { name: '科室', disabled: false }, dept: { name: "科室", disabled: false },
} };
}) });
// //
const genderOptions = [ const genderOptions = [
@ -139,55 +197,60 @@ function chooseAvatar() {
if (url) { if (url) {
form.value.avatar = url; form.value.avatar = url;
} else { } else {
toast('上传失败') toast("上传失败");
} }
} },
}) });
} }
function onChange({ title, value }) { function onChange({ title, value }) {
form.value[title] = value form.value[title] = value;
} }
function selectJob() { function selectJob() {
if (rule.value.job.disabled) return; if (rule.value.job.disabled) return;
uni.showActionSheet({ uni.showActionSheet({
itemList: ['医生', '医生助理', '无'], itemList: ["医生", "医生助理", "无"],
success: ({ tapIndex }) => { success: ({ tapIndex }) => {
const job = ['doctor', 'assistant',][tapIndex]; const job = ["doctor", "assistant"][tapIndex];
form.value.job = job ? [job] : []; form.value.job = job ? [job] : [];
} },
}) });
} }
function toCert() { function toCert() {
if (jobStr.value === '医生') { if (jobStr.value === "医生") {
uni.navigateTo({ uni.navigateTo({
url: '/pages/work/verify/doctor' url: "/pages/work/verify/doctor",
}) });
} else if (jobStr.value === '医生助理') { } else if (jobStr.value === "医生助理") {
uni.navigateTo({ uni.navigateTo({
url: '/pages/work/verify/assistant' url: "/pages/work/verify/assistant",
}) });
} else { } else {
toast('请选择岗位信息') toast("请选择岗位信息");
} }
} }
async function save() { async function save() {
if (typeof formData.value.anotherName !== 'string' || !formData.value.anotherName.trim()) { if (
return toast('请输入姓名') typeof formData.value.anotherName !== "string" ||
!formData.value.anotherName.trim()
) {
return toast("请输入姓名");
} }
if (type.value === 'cert' && !jobStr.value) { if (type.value === "cert" && !jobStr.value) {
return toast('请选择岗位信息') return toast("请选择岗位信息");
} }
const apiName = doctorInfo.value ? 'updateCorpMemberFromWxapp' : 'addCorpMemberFromWxapp'; const apiName = doctorInfo.value
? "updateCorpMemberFromWxapp"
: "addCorpMemberFromWxapp";
const data = { const data = {
...form.value, ...form.value,
weChatOpenId: account.value.openid, weChatOpenId: account.value.openid,
mobile: account.value.mobile, mobile: account.value.mobile,
corpId: account.value.corpId, corpId: account.value.corpId,
} };
if (doctorInfo.value) { if (doctorInfo.value) {
data.id = doctorInfo.value._id; data.id = doctorInfo.value._id;
} }
@ -196,30 +259,29 @@ async function save() {
} }
const res = await api(apiName, data); const res = await api(apiName, data);
if (res && res.success) { if (res && res.success) {
await getDoctorInfo() await getDoctorInfo();
form.value = {}; form.value = {};
if (type.value === 'cert') { if (type.value === "cert") {
toCert() toCert();
} else { } else {
await toast('保存成功'); await toast("保存成功");
back() back();
} }
} else { } else {
await toast(res?.message || '保存失败'); await toast(res?.message || "保存失败");
} }
} }
useLoad(opts => { useLoad((opts) => {
type.value = opts?.type; type.value = opts?.type;
if (type.value === 'joinTeam' && opts.teamId) { if (type.value === "joinTeam" && opts.teamId) {
inviteTeamId.value = opts.teamId inviteTeamId.value = opts.teamId;
} }
})
useShow(() => {
getDoctorInfo()
}); });
useShow(() => {
getDoctorInfo();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -26,7 +26,7 @@
</view> </view>
</view> </view>
<view v-if="i.qrcode" class="flex justify-center overflow-hidden"> <view v-if="i.qrcode" class="flex justify-center overflow-hidden">
<uqrcode ref="qrcodes" :canvas-id="`qrcode-${idx}`" :value="i.qrcode" :options="options"> <uqrcode ref="qrcodes" :canvasId="`qrcode-${idx}`" :value="i.qrcode" :options="options">
</uqrcode> </uqrcode>
</view> </view>
</swiper-item> </swiper-item>
@ -42,8 +42,8 @@
</view> </view>
<view class="px-15 text-base text-gray leading-normal text-center">进入团队首页即可发起线上咨询建档授权等服务</view> <view class="px-15 text-base text-gray leading-normal text-center">进入团队首页即可发起线上咨询建档授权等服务</view>
<view class="mt-10 flex px-15"> <view class="mt-10 flex px-15">
<view class="mr-10 border-auto rounded py-10 text-base text-primary text-center flex-grow">保存图片</view> <view class="mr-10 border-auto rounded py-10 text-base text-primary text-center flex-grow" @click="saveImage">保存图片</view>
<view class="bg-primary rounded py-10 text-base text-white text-center flex-grow">分享微信</view> <button class=" bg-primary rounded py-10 text-base text-white text-center flex-grow" open-type="share">分享微信</button>
</view> </view>
</view> </view>
</view> </view>
@ -56,7 +56,7 @@ import { onLoad } from "@dcloudio/uni-app";
import useAccountStore from "@/store/account.js"; import useAccountStore from "@/store/account.js";
import useGuard from '@/hooks/useGuard'; import useGuard from '@/hooks/useGuard';
import api from "@/utils/api.js"; import api from "@/utils/api.js";
import { toast } from "@/utils/widget"; import { toast, saveImageToPhotosAlbum, shareToWeChat } from "@/utils/widget";
import emptyData from "@/components/empty-data.vue"; import emptyData from "@/components/empty-data.vue";
import renamePopup from "./rename-popup.vue"; import renamePopup from "./rename-popup.vue";
@ -69,6 +69,7 @@ const current = ref(0);
const list = ref([]); const list = ref([]);
const visible = ref(false); const visible = ref(false);
const teamId = ref('') const teamId = ref('')
const qrcodes = ref(null);
const indicator = computed(() => ({ const indicator = computed(() => ({
prev: current.value > 0, prev: current.value > 0,
next: current.value < list.value.length - 1 next: current.value < list.value.length - 1
@ -113,6 +114,65 @@ async function change(name) {
} }
} }
//
async function saveImage() {
if (!team.value || !team.value.qrcode) {
toast('暂无二维码');
return;
}
try {
const qrcodeComponent = qrcodes.value[current.value];
if (!qrcodeComponent) {
toast('二维码未加载完成');
return;
}
//
const tempFilePath = qrcodeComponent.toTempFilePath();
if (tempFilePath) {
await saveImageToPhotosAlbum(tempFilePath);
} else {
toast('获取二维码失败');
}
} catch (err) {
console.error('保存图片失败:', err);
toast('保存失败');
}
}
//
function onShareAppMessage() {
if (!team.value) {
return shareToWeChat({
title: '邀请患者加入团队',
path: '/pages/work/team/invite/invite-patient'
});
}
return shareToWeChat({
title: `邀请您加入${team.value.name}`,
path: `/pages/work/team/invite/invite-patient?teamId=${team.value.teamId}`,
imageUrl: team.value.qrcode || ''
});
}
//
function onShareTimeline() {
if (!team.value) {
return {
title: '邀请患者加入团队',
path: '/pages/work/team/invite/invite-patient'
};
}
return {
title: `邀请您加入${team.value.name}`,
query: `teamId=${team.value.teamId}`,
imageUrl: team.value.qrcode || ''
};
}
onLoad(opts => { onLoad(opts => {
teamId.value = opts.teamId || ''; teamId.value = opts.teamId || '';
}) })
@ -121,6 +181,12 @@ useShow(() => {
getTeams() getTeams()
}) })
// 使
defineExpose({
onShareAppMessage,
onShareTimeline
})
</script> </script>
<style> <style>
.w-100 { .w-100 {
@ -148,4 +214,15 @@ useShow(() => {
.h-30 { .h-30 {
height: 60rpx; height: 60rpx;
} }
.share-btn {
border: none;
padding: 0;
line-height: normal;
background: transparent;
}
.share-btn::after {
border: none;
}
</style> </style>

View File

@ -7,7 +7,7 @@
<view class="relative user-avatar mr-10" @click="editProfile()"> <view class="relative user-avatar mr-10" @click="editProfile()">
<image v-if="doctorInfo && doctorInfo.avatar" class="avatar-img rounded-full overflow-hidden" <image v-if="doctorInfo && doctorInfo.avatar" class="avatar-img rounded-full overflow-hidden"
:src="doctorInfo.avatar" mode="aspectFill" /> :src="doctorInfo.avatar" mode="aspectFill" />
<image v-else class="avatar-img rounded-full overflow-hidden" src="/static/default-avatar.png" <image v-else class="avatar-img rounded-full overflow-hidden" src="/static/home/avatar.svg"
mode="aspectFill" /> mode="aspectFill" />
<view v-if="doctorInfo" class="edit-sub flex items-center justify-center rounded-full bg-primary"> <view v-if="doctorInfo" class="edit-sub flex items-center justify-center rounded-full bg-primary">
<image class="edit-icon" src="/static/work/pen.svg" mode="aspectFill" /> <image class="edit-icon" src="/static/work/pen.svg" mode="aspectFill" />
@ -46,23 +46,29 @@
<view class="mt-15 px-15 py-12 flex items-center justify-between bg-white"> <view class="mt-15 px-15 py-12 flex items-center justify-between bg-white">
<view class="text-dark text-lg font-semibold">待办列表</view> <view class="text-dark text-lg font-semibold">待办列表</view>
<view class="flex text-base rounded-full bg-gray"> <view class="flex text-base rounded-full bg-gray">
<view class="py-5 px-15 rounded-full bg-primary text-white">个人</view> <view class="py-5 px-15 rounded-full" :class="followUpType === 'person' ? 'bg-primary text-white' : ''"
<view class="py-5 px-15">团队</view> @click="changeFollowType('person')">
个人
</view>
<view class="py-5 px-15 rounded-full" :class="followUpType === 'team' ? 'bg-primary text-white' : ''"
@click="changeFollowType('team')">
团队
</view>
</view> </view>
</view> </view>
<view class="py-10 px-15 flex items-center"> <view class="py-10 px-15 flex items-center">
<view class="flex-shrink-0 text-sm mr-10"> <view class="flex-shrink-0 text-sm mr-10">
<text class="text-dark"></text> <text class="text-dark"></text>
<text class="text-danger">23</text> <text class="text-danger">{{ total }}</text>
<text class="text-dark"></text> <text class="text-dark"></text>
</view> </view>
<view class="flex"> <view class="flex">
<view v-for="i in statusList" :key="i.value" class="mr-5 py-5 px-10 bg-white text-sm rounded-sm" <view v-for="i in statusList" :key="i.value" class="mr-5 py-5 px-10 bg-white text-sm rounded-sm"
:class="current == i.value ? 'text-primary' : 'text-dark'"> :class="filterData.eventStatus == i.value ? 'text-primary' : 'text-dark'" @click="changeStatus(i.value)">
{{ i.label }} {{ i.label }}
</view> </view>
</view> </view>
<view class="flex-shrink-0 flex-grow flex justify-end" @click="filtered = !filtered"> <view class="flex-shrink-0 flex-grow flex justify-end" @click="filterVisible = !filterVisible">
<image class="icon-filter" :src="`/static/work/icon-filter${filtered ? 'ed' : ''}.svg`" /> <image class="icon-filter" :src="`/static/work/icon-filter${filtered ? 'ed' : ''}.svg`" />
</view> </view>
@ -70,28 +76,35 @@
</template> </template>
<scroll-view v-if="list.length" scroll-y="true" class="h-full"> <scroll-view v-if="list.length" scroll-y="true" class="h-full">
<view v-for="i in 10" :key="i" class="mb-10 shadow-lg bg-white"> <view v-for="i in list" :key="i._id" class="mb-10 shadow-lg bg-white">
<view class="flex items-center justify-between px-15 py-10 border-b"> <view class="flex items-center justify-between px-15 py-10 border-b">
<view class="text-base text-dark">计划执行: 2025-10-22</view> <view class="text-base text-dark">计划执行: {{ i.planDate }}</view>
<view class="flex items-center"> <view class="flex items-center">
<view class="text-base text-dark">患者: 李珊珊</view> <view class="text-base text-dark">患者: {{ i.customerName }}</view>
</view> </view>
</view> </view>
<view class="py-10 px-15 flex items-center"> <view class="py-10 px-15 flex items-center">
<view class="mr-5 text-lg font-semibold">患者满意度调查</view> <view class="mr-5 text-lg font-semibold">{{ i.eventTypeLabel }}</view>
<view class="bg-opacity px-10 py-3 leading-normal text-base text-success rounded overflow-hidden"> <view class="bg-opacity px-10 py-3 leading-normal text-base rounded overflow-hidden"
待处理 :class="statusClassNames[i.eventStatus] || 'text-gray'">
{{ i.eventStatusLabel }}
</view> </view>
</view> </view>
<view class="px-15 text-base leading-normal text-gray">对于门诊就诊患者的满意度做统计以便优化</view> <view v-if="i.sendContent" class="px-15 text-base leading-normal text-gray">{{ i.sendContent }}</view>
<view class="mt-10 px-15 flex items-center"> <view v-if="i.enableSend && i.fileList.length" class="mt-10 px-15 flex items-center">
<view class="mr-5 w-0 flex-grow truncate text-base leading-normal text-dark"> <view class="mr-5 w-0 flex-grow">
发送内容XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX <view v-for="(file, idx) in i.fileList" :key="idx" class="truncate text-base leading-normal text-dark">
发送内容{{ file.file.name }}
</view>
</view> </view>
<view class="bg-primary px-10 py-3 text-base text-white rounded-sm">发送</view> <view class="bg-primary px-10 py-3 text-base text-white rounded-sm">发送</view>
</view> </view>
<view class="mt-10 px-15 text-base leading-normal text-gray">张敏西张敏希服务团队</view> <view class="mt-10 px-15 text-base leading-normal text-gray truncate">
<view class="px-15 pb-10 text-base leading-normal text-gray">创建2026-01-08 张敏西</view> {{ i.executorUserName }}{{ i.executeTeamName }}
</view>
<view class="px-15 pb-10 text-base leading-normal text-gray truncate">
创建{{ i.createTime }} {{ i.creatorUserName }}
</view>
</view> </view>
</scroll-view> </scroll-view>
@ -103,19 +116,28 @@
</template> </template>
</full-page> </full-page>
<cert-popup :visible="visible" @close="visible = false" /> <cert-popup :visible="visible" @close="visible = false" />
<filter-popup :data="filterData" :visible="filterVisible" @close="filterVisible = false"
@confirm="changeFilterData($event)" />
</template> </template>
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { statusNames, ToDoEventType, statusClassNames } from '@/baseData';
import useGuard from "@/hooks/useGuard.js"; import useGuard from "@/hooks/useGuard.js";
import useInfoCheck from '@/hooks/useInfoCheck'; import useInfoCheck from '@/hooks/useInfoCheck';
import usePageList from '@/hooks/usePageList';
import useAccountStore from "@/store/account.js"; import useAccountStore from "@/store/account.js";
import useTeamStore from "@/store/team.js";
import api from '@/utils/api';
import { toast } from '@/utils/widget';
import certPopup from "./components/cert-popup.vue"; import certPopup from "./components/cert-popup.vue";
import filterPopup from './components/filter-popup.vue';
import EmptyData from "@/components/empty-data.vue"; import EmptyData from "@/components/empty-data.vue";
import fullPage from '@/components/full-page.vue'; import fullPage from '@/components/full-page.vue';
import { toast } from '@/utils/widget'; import dayjs from 'dayjs';
const certConfig = { const certConfig = {
failed: { text: '认证失败', classnames: 'bg-danger text-white' }, failed: { text: '认证失败', classnames: 'bg-danger text-white' },
@ -123,27 +145,24 @@ const certConfig = {
verifying: { text: '认证中', classnames: 'bg-warning text-white' }, verifying: { text: '认证中', classnames: 'bg-warning text-white' },
unverified: { text: '未认证', classnames: 'bg-gray text-dark' }, unverified: { text: '未认证', classnames: 'bg-gray text-dark' },
}; };
const statusList = [{ label: '全部', value: 'all' }, { label: '待处理', value: 'pending' }, { label: '已处理', value: 'processed' }]
const statusList = [{ label: '全部', value: 'all' }, { label: '待处理', value: 'processing' }, { label: '未开始', value: 'notStart' }]
const { useLoad, useShow } = useGuard(); const { useLoad, useShow } = useGuard();
const { getDoctorInfo } = useAccountStore(); const { getDoctorInfo } = useAccountStore();
const { doctorInfo } = storeToRefs(useAccountStore()); const { account, doctorInfo } = storeToRefs(useAccountStore());
const { chargeTeams } = storeToRefs(useTeamStore());
const { getTeams } = useTeamStore();
const { withInfo } = useInfoCheck(); const { withInfo } = useInfoCheck();
const list = ref([1]);
const visible = ref(false); const visible = ref(false);
const current = ref('all'); const filtered = ref(false);
const filtered = ref(false) const filterVisible = ref(false);
const filterData = ref({ eventStatus: 'processing' });
const followUpType = ref('person') // person team
const { total, list, page, pages, pageSize, changePage } = usePageList(getList)
const certStatus = computed(() => doctorInfo.value?.verifyStatus ? certConfig[doctorInfo.value.verifyStatus] : null) const certStatus = computed(() => doctorInfo.value?.verifyStatus ? certConfig[doctorInfo.value.verifyStatus] : null)
//
const handleVerify = () => {
uni.showToast({
title: "跳转到认证页面",
icon: "none",
});
};
// //
const invitePatient = withInfo(() => uni.navigateTo({ url: '/pages/work/team/invite/invite-patient' })); const invitePatient = withInfo(() => uni.navigateTo({ url: '/pages/work/team/invite/invite-patient' }));
@ -157,6 +176,26 @@ const handleMore = withInfo(() => {
}); });
}) })
function changeFilterData(data) {
filterData.value = data;
const case1 = Object.keys(data).filter(i => i != 'eventStatus').length > 0;
const case2 = statusList.some(i => i.value === data.eventStatus);
filtered.value = case1 || !case2;
changePage(1);
}
function changeFollowType(type) {
if (followUpType.value === type) return;
followUpType.value = type;
changePage(1);
}
function changeStatus(val) {
if (filterData.value.eventStatus === val) return;
filterData.value.eventStatus = val;
changePage(1);
}
function editProfile() { function editProfile() {
uni.navigateTo({ uni.navigateTo({
url: "/pages/work/profile", url: "/pages/work/profile",
@ -171,12 +210,54 @@ function handleCert() {
} }
} }
async function getList() {
if (!doctorInfo.value || !doctorInfo.value.userid) {
return
}
const data = {
corpId: account.value.corpId,
startDate: filterData.value.startDate,
endDate: filterData.value.endDate,
page: page.value,
pageSize: pageSize.value
}
if (followUpType.value === 'person') {
data.executorUserId = doctorInfo.value.userid;
} else {
data.teamIds = chargeTeams.value.map(i => i.teamId);
}
if (filterData.value.eventStatus !== 'all') {
data.statusList = [filterData.value.eventStatus]
}
if (filterData.value.eventType) {
data.eventType = filterData.value.eventType;
}
const res = await api('getTeamTodos', data);
const arr = res && Array.isArray(res.data) ? res.data.map(i => ({
...i,
eventTypeLabel: ToDoEventType[i.eventType],
planDate: i.plannedExecutionTime && dayjs(i.plannedExecutionTime).isValid() ? dayjs(i.plannedExecutionTime).format("YYYY-MM-DD") : "",
endTime: i.endTime && dayjs(i.endTime).isValid() ? dayjs(i.endTime).format("YYYY-MM-DD HH:mm") : "",
createTime: i.createTime && dayjs(i.createTime).isValid() ? dayjs(i.createTime).format("YYYY-MM-DD HH:mm") : "",
eventStatusLabel: statusNames[i.eventStatus],
fileList: Array.isArray(i.fileList) ? i.fileList.filter(i => i && i.file && i.file.name) : []
})) : [];
list.value = page.value === 1 ? arr : [...list.value, ...arr];
total.value = res && res.total > 0 ? res.total : 0;
pages.value = res && res.pages > 0 ? res.pages : 0;
if (!res && !res.success) {
toast(res?.message || '查询待办失败')
}
}
useLoad(() => { useLoad(() => {
console.log("工作台页面加载"); console.log("工作台页面加载");
}); });
useShow(() => { useShow(async () => {
getDoctorInfo(); console.log("工作台页面加!!!@@@载");
await getDoctorInfo();
changePage(1)
}) })
</script> </script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
static/icon/buchong.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/icon/kaiqiAI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
static/icon/zhuiwen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1008 B

1
static/zhuanhua.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="174.60px" viewBox="0 0 1173 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M1166.247916 358.382794a40.871232 40.871232 0 0 1-50.713886 13.785174H40.952129a40.9258 40.9258 0 0 1 0-81.8516h958.345809l-302.775887-215.747174a40.9258 40.9258 0 1 1 46.614486-67.282014l399.026546 284.366098A40.843948 40.843948 0 0 1 1173.232586 331.242168v0.361511a40.612035 40.612035 0 0 1-6.98467 26.779115zM40.952129 645.006632h1091.354658a40.9258 40.9258 0 0 1 0 81.851599H168.306396l312.332061 222.568141a40.9258 40.9258 0 0 1-46.614485 67.282015L21.246356 722.561023a39.561606 39.561606 0 0 1-3.124003-2.666998A40.9258 40.9258 0 0 1 40.952129 645.006632z" /></svg>

After

Width:  |  Height:  |  Size: 840 B

View File

@ -19,7 +19,7 @@ export default defineStore("accountStore", () => {
// 从缓存中恢复数据 // 从缓存中恢复数据
const account = ref(cache.get(CACHE_KEYS.ACCOUNT, null)); const account = ref(cache.get(CACHE_KEYS.ACCOUNT, null));
const loading = ref(false);
const loginPromise = ref(null); const loginPromise = ref(null);
// IM 相关 // IM 相关
const openid = ref(cache.get(CACHE_KEYS.OPENID, "")); const openid = ref(cache.get(CACHE_KEYS.OPENID, ""));
@ -48,6 +48,7 @@ export default defineStore("accountStore", () => {
}); });
if (code) { if (code) {
const res = await api('wxAppLogin', { const res = await api('wxAppLogin', {
appId: appid,
phoneCode, phoneCode,
code, code,
corpId, corpId,

View File

@ -1,4 +1,4 @@
import { ref } from "vue"; import { computed, ref } from "vue";
import { defineStore, storeToRefs } from "pinia"; import { defineStore, storeToRefs } from "pinia";
import api from '@/utils/api'; import api from '@/utils/api';
import { toast } from '@/utils/widget'; import { toast } from '@/utils/widget';
@ -9,6 +9,13 @@ export default defineStore("teamStore", () => {
const { account, doctorInfo } = storeToRefs(useAccountStore()); const { account, doctorInfo } = storeToRefs(useAccountStore());
const teams = ref([]); const teams = ref([]);
const chargeTeams = computed(() => {
const userid = doctorInfo.value?.userid;
return teams.value.filter(team => {
const memberLeaderList = Array.isArray(team.memberLeaderList) ? team.memberLeaderList : [];
return memberLeaderList.includes(userid);
});
})
async function getTeam(teamId) { async function getTeam(teamId) {
if (!teamId || !account.value?.corpId) return; if (!teamId || !account.value?.corpId) return;
const res = await api('getTeamData', { teamId, corpId: account.value.corpId }); const res = await api('getTeamData', { teamId, corpId: account.value.corpId });
@ -21,11 +28,24 @@ export default defineStore("teamStore", () => {
async function getTeams() { async function getTeams() {
const corpId = account.value?.corpId; const corpId = account.value?.corpId;
const mateId = doctorInfo.value?.corpId; const mateId = doctorInfo.value?.userid;
if (!corpId || !mateId) return; if (!corpId || !mateId) return;
const res = await api('getJoinedTeams', { corpId, mateId }); const res = await api('getJoinedTeams', { corpId, mateId });
teams.value = res && Array.isArray(res.data) ? res.data : []; teams.value = res && Array.isArray(res.data) ? res.data : [];
} }
return { teams, getTeam, getTeams } // 获取团队成员头像映射
async function getTeamMemberAvatars(teamId) {
if (!teamId || !account.value?.corpId) return {};
const res = await api('getTeamMemberAvatars', {
teamId,
corpId: account.value.corpId
});
if (res && res.success && res.data) {
return res.data; // 返回 { userId: avatar } 的映射对象
}
return {};
}
return { teams, chargeTeams, getTeam, getTeams, getTeamMemberAvatars }
}) })

View File

@ -25,7 +25,8 @@ const urlsConfig = {
createOwnTeam: 'createOwnTeam', createOwnTeam: 'createOwnTeam',
removeTeammate: "removeTeammate", removeTeammate: "removeTeammate",
toggleTeamLeaderRole: "toggleTeamLeaderRole", toggleTeamLeaderRole: "toggleTeamLeaderRole",
joinTheInvitedTeam: 'joinTheInvitedTeam' joinTheInvitedTeam: 'joinTheInvitedTeam',
getTeamMemberAvatars: 'getTeamMemberAvatars'
}, },
knowledgeBase: { knowledgeBase: {
@ -97,7 +98,9 @@ const urlsConfig = {
acceptConsultation: "acceptConsultation", acceptConsultation: "acceptConsultation",
sendArticleMessage: "sendArticleMessage", sendArticleMessage: "sendArticleMessage",
getChatRecordsByGroupId: "getChatRecordsByGroupId", getChatRecordsByGroupId: "getChatRecordsByGroupId",
getGroupList: "getGroupList" getGroupList: "getGroupList",
followUpInquiry: "followUpInquiry",
supplementMedicalCase: "supplementMedicalCase"
}, },
todo: { todo: {
getCustomerTodos: 'getCustomerTodos', getCustomerTodos: 'getCustomerTodos',
@ -119,6 +122,7 @@ const urlsConfig = {
// 客户流转记录 // 客户流转记录
customerTransferRecord: 'customerTransferRecord', customerTransferRecord: 'customerTransferRecord',
// sendConsultRejectedMessage: "sendConsultRejectedMessage" // sendConsultRejectedMessage: "sendConsultRejectedMessage"
getTeamTodos: 'getTeamTodos'
} }
} }
@ -137,7 +141,7 @@ const urls = Object.keys(urlsConfig).reduce((acc, path) => {
}, {}) }, {})
console.log('urls: ', urls) console.log('urls: ', urls)
export default async function api(urlId, data) { export default async function api(urlId, data, loading) {
const config = urls[urlId]; const config = urls[urlId];
if (!config) { if (!config) {
throw new Error(`Unknown URL ID: ${urlId}`); throw new Error(`Unknown URL ID: ${urlId}`);
@ -148,7 +152,7 @@ export default async function api(urlId, data) {
data: { data: {
...data, ...data,
type, type,
} },
}) }, loading)
} }

View File

@ -42,41 +42,37 @@ export async function mergeConversationWithGroupDetails(conversationList, option
groupIds, groupIds,
...options // 支持传入额外的查询参数corpId, teamId, keyword等 ...options // 支持传入额外的查询参数corpId, teamId, keyword等
} }
const response = await api('getGroupList', requestData) const response = await api('getGroupList', requestData, false)
// 4. 检查响应 // 4. 检查响应
if (!response || !response.success) { if (!response || !response.success) {
console.error('获取群组详细信息失败:', response?.message || '未知错误') console.error('获取群组详细信息失败:', response?.message || '未知错误')
return [] return []
} }
const groupDetailsMap = createGroupDetailsMap(response.data?.list || []) const groupDetailsMap = createGroupDetailsMap(response.data?.list || [])
console.log('获取到的群组详细信息数量:', Object.keys(groupDetailsMap).size) console.log('获取到的群组详细信息数量:', Object.keys(groupDetailsMap).size)
// 5. 合并数据并过滤 // 5. 合并数据并过滤
const mergedList = conversationList const mergedList = conversationList
.map(conversation => mergeConversationData(conversation, groupDetailsMap)) .map(conversation => mergeConversationData(conversation, groupDetailsMap))
.filter(item => item !== null) // 过滤掉后端不存在的会话 .filter(item => item !== null);
console.log('合并后的会话列表数量:', mergedList.length) console.log('合并后的会话列表数量:', mergedList.length)
console.log('过滤掉的会话数量:', conversationList.length - mergedList.length) console.log('过滤掉的会话数量:', conversationList.length - mergedList.length)
// 6. 格式化并排序会话列表 // 6. 格式化并排序会话列表
const formattedList = mergedList const formattedList = mergedList
.map((group) => ({ .map((group) => ({
conversationID: group.conversationID || `GROUP${group.groupID}`, conversationID: group.conversationID || `GROUP${group.groupID}`,
groupID: group.groupID, avatar: group.avatar || "/static/default-patient-avatar.png",
name: group.patientName
? `${group.patientName}的问诊`
: group.name || "问诊群聊",
avatar: group.avatar || "/static/default-avatar.png",
lastMessage: group.lastMessage || "暂无消息", lastMessage: group.lastMessage || "暂无消息",
lastMessageTime: group.lastMessageTime || Date.now(), lastMessageTime: group.lastMessageTime || Date.now(),
groupID: group.groupID,
unreadCount: group.unreadCount || 0, unreadCount: group.unreadCount || 0,
doctorId: group.doctorId, doctorId: group.doctorId,
patientName: group.patientName, patientName: group.patientName,
patientSex: group.patientSex, patientSex: group.patientSex,
patientAge: group.patientAge, patientAge: group.patientAge,
orderStatus: group.orderStatus, orderStatus: group.orderStatus,
teamId: group.teamId,
teamName: group.teamName,
teamMemberList: group.teamMemberList,
})) }))
.sort((a, b) => b.lastMessageTime - a.lastMessageTime) .sort((a, b) => b.lastMessageTime - a.lastMessageTime)
@ -127,9 +123,6 @@ function mergeConversationData(conversation, groupDetailsMap) {
return { return {
// 保留原有的会话信息 // 保留原有的会话信息
...conversation, ...conversation,
// 合并后端的群组信息
_id: groupDetail._id,
corpId: groupDetail.corpId, corpId: groupDetail.corpId,
teamId: groupDetail.teamId, teamId: groupDetail.teamId,
customerId: groupDetail.customerId, customerId: groupDetail.customerId,
@ -148,7 +141,7 @@ function mergeConversationData(conversation, groupDetailsMap) {
teamName: groupDetail.team?.name, teamName: groupDetail.team?.name,
teamMemberList: groupDetail.team?.memberList, teamMemberList: groupDetail.team?.memberList,
teamDescription: groupDetail.team?.description, teamDescription: groupDetail.team?.description,
teamId: groupDetail.teamId,
// 时间信息 // 时间信息
createdAt: groupDetail.createdAt, createdAt: groupDetail.createdAt,
updatedAt: groupDetail.updatedAt, updatedAt: groupDetail.updatedAt,
@ -156,8 +149,8 @@ function mergeConversationData(conversation, groupDetailsMap) {
// 更新显示名称(使用后端的患者信息) // 更新显示名称(使用后端的患者信息)
name: formatConversationName(groupDetail), name: formatConversationName(groupDetail),
// 更新头像 // 更新头像(优先使用已有头像,避免闪动)
avatar: groupDetail.patient?.avatar || conversation.avatar || '/static/default-avatar.png' avatar: conversation.avatar || groupDetail.patient?.avatar
} }
} }

View File

@ -0,0 +1,234 @@
# 微信小程序分享功能使用指南
## 功能说明
提供了完整的微信小程序分享功能,包括:
- 分享给好友
- 分享到朋友圈
- 保存图片到相册
## 使用方法
### 1. 基础分享(在页面中)
```vue
<template>
<view>
<button open-type="share">分享给好友</button>
</view>
</template>
<script setup>
import { createShareMessage, createShareTimeline } from '@/utils/share'
// 分享给好友
function onShareAppMessage() {
return createShareMessage({
title: '分享标题',
path: '/pages/index/index?id=123',
imageUrl: 'https://example.com/share.jpg'
})
}
// 分享到朋友圈(需要在 app.json 中配置)
function onShareTimeline() {
return createShareTimeline({
title: '朋友圈标题',
query: 'id=123',
imageUrl: 'https://example.com/share.jpg'
})
}
// 导出分享方法
defineExpose({
onShareAppMessage,
onShareTimeline
})
</script>
```
### 2. 使用分享组件
```vue
<template>
<view>
<share-actions
@save="handleSave"
:show-save="true"
:show-share="true"
save-text="保存图片"
share-text="分享微信"
/>
</view>
</template>
<script setup>
import { saveImageToAlbum, createShareMessage } from '@/utils/share'
import shareActions from '@/components/share-actions.vue'
// 保存图片
async function handleSave() {
const imagePath = 'https://example.com/image.jpg'
await saveImageToAlbum(imagePath)
}
// 分享配置
function onShareAppMessage() {
return createShareMessage({
title: '分享标题',
path: '/pages/index/index'
})
}
defineExpose({
onShareAppMessage
})
</script>
```
### 3. 保存二维码图片
```vue
<template>
<view>
<uqrcode ref="qrcode" canvasId="qrcode" :value="qrcodeUrl" />
<button @click="saveQrcode">保存二维码</button>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { saveImageToAlbum } from '@/utils/share'
import { toast } from '@/utils/widget'
const qrcode = ref(null)
const qrcodeUrl = ref('https://example.com')
async function saveQrcode() {
try {
if (!qrcode.value) {
toast('二维码未加载完成')
return
}
// 获取二维码临时文件路径
const tempFilePath = qrcode.value.toTempFilePath()
if (tempFilePath) {
await saveImageToAlbum(tempFilePath)
} else {
toast('获取二维码失败')
}
} catch (err) {
console.error('保存失败:', err)
toast('保存失败')
}
}
</script>
```
### 4. 动态分享内容
```vue
<script setup>
import { ref, computed } from 'vue'
import { createShareMessage } from '@/utils/share'
const currentItem = ref({
id: '123',
title: '商品标题',
image: 'https://example.com/product.jpg'
})
// 动态生成分享配置
function onShareAppMessage() {
return createShareMessage({
title: currentItem.value.title,
path: `/pages/detail/detail?id=${currentItem.value.id}`,
imageUrl: currentItem.value.image
})
}
defineExpose({
onShareAppMessage
})
</script>
```
## 配置说明
### 1. 启用分享到朋友圈
`pages.json` 中配置页面:
```json
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enableShareTimeline": true
}
}
```
### 2. 全局分享配置
`App.vue` 中配置全局分享:
```vue
<script>
export default {
onShareAppMessage() {
return {
title: '默认分享标题',
path: '/pages/index/index'
}
},
onShareTimeline() {
return {
title: '默认朋友圈标题'
}
}
}
</script>
```
## API 说明
### createShareMessage(options)
创建分享给好友的配置
**参数:**
- `title` (string): 分享标题
- `path` (string): 分享路径
- `imageUrl` (string): 分享图片URL
**返回:** 分享配置对象
### createShareTimeline(options)
创建分享到朋友圈的配置
**参数:**
- `title` (string): 分享标题
- `query` (string): 分享路径参数
- `imageUrl` (string): 分享图片URL
**返回:** 分享配置对象
### saveImageToAlbum(filePath)
保存图片到相册
**参数:**
- `filePath` (string): 图片路径(本地临时路径或网络路径)
**返回:** Promise<boolean>
## 注意事项
1. 分享图片建议尺寸5:4推荐 500x400px
2. 分享路径必须是已注册的页面路径
3. 保存图片需要用户授权相册权限
4. 分享到朋友圈需要在页面配置中启用
5. 网络图片会自动下载后保存到相册

169
utils/share.js Normal file
View File

@ -0,0 +1,169 @@
/**
* 微信小程序分享工具
*/
import { toast } from './widget'
/**
* 创建分享到好友的配置
* @param {Object} options 分享配置
* @param {string} options.title 分享标题
* @param {string} options.path 分享路径
* @param {string} options.imageUrl 分享图片URL
* @returns {Object} 分享配置对象
*/
export function createShareMessage(options = {}) {
const { title = '', path = '', imageUrl = '' } = options
return {
title,
path,
imageUrl,
success: () => {
toast('分享成功')
},
fail: (err) => {
console.error('分享失败:', err)
toast('分享失败')
}
}
}
/**
* 创建分享到朋友圈的配置
* @param {Object} options 分享配置
* @param {string} options.title 分享标题
* @param {string} options.query 分享路径参数
* @param {string} options.imageUrl 分享图片URL
* @returns {Object} 分享配置对象
*/
export function createShareTimeline(options = {}) {
const { title = '', query = '', imageUrl = '' } = options
return {
title,
query,
imageUrl
}
}
/**
* 在页面中启用分享功能
* 使用方法在页面的 setup 中调用
*
* @example
* import { enableShare } from '@/utils/share'
*
* // 在 setup 中
* enableShare({
* message: {
* title: '分享标题',
* path: '/pages/index/index',
* imageUrl: 'https://example.com/image.jpg'
* },
* timeline: {
* title: '朋友圈标题',
* query: 'id=123',
* imageUrl: 'https://example.com/image.jpg'
* }
* })
*/
export function enableShare(config = {}) {
const { message, timeline } = config
// 分享给好友
if (message) {
uni.$on('onShareAppMessage', () => {
return createShareMessage(message)
})
}
// 分享到朋友圈
if (timeline) {
uni.$on('onShareTimeline', () => {
return createShareTimeline(timeline)
})
}
}
/**
* 保存图片到相册
* @param {string} filePath 图片路径本地临时路径或网络路径
*/
export async function saveImageToAlbum(filePath) {
try {
// 如果是网络图片,先下载
let localPath = filePath
if (filePath.startsWith('http')) {
const res = await uni.downloadFile({ url: filePath })
if (res[0]) {
throw new Error('下载图片失败')
}
localPath = res[1].tempFilePath
}
// 检查授权
const authRes = await uni.getSetting()
if (!authRes[1].authSetting['scope.writePhotosAlbum']) {
// 请求授权
try {
await uni.authorize({ scope: 'scope.writePhotosAlbum' })
} catch (err) {
// 用户拒绝授权,引导去设置
const [modalErr, modalRes] = await uni.showModal({
title: '提示',
content: '需要您授权保存相册',
confirmText: '去设置',
cancelText: '取消'
})
if (modalRes && modalRes.confirm) {
await uni.openSetting()
}
return false
}
}
// 保存图片
const [saveErr] = await uni.saveImageToPhotosAlbum({ filePath: localPath })
if (saveErr) {
throw saveErr
}
await toast('保存成功')
return true
} catch (err) {
console.error('保存图片失败:', err)
await toast('保存失败')
return false
}
}
/**
* 生成带参数的小程序码
* 需要后端接口支持
* @param {Object} options
* @param {string} options.scene 场景值
* @param {string} options.page 页面路径
* @returns {Promise<string>} 返回小程序码图片URL
*/
export async function generateMiniCode(options = {}) {
// 这里需要调用后端接口生成小程序码
// 示例代码,需要根据实际后端接口调整
try {
const res = await uni.request({
url: '/api/wechat/generateMiniCode',
method: 'POST',
data: options
})
if (res[0] || !res[1].data.success) {
throw new Error('生成小程序码失败')
}
return res[1].data.data.url
} catch (err) {
console.error('生成小程序码失败:', err)
throw err
}
}

View File

@ -1086,7 +1086,7 @@ class TimChatManager {
const groupInfo = { const groupInfo = {
groupID: groupID, groupID: groupID,
name: group?.name || '问诊群聊', name: group?.name || '问诊群聊',
avatar: '/static/home/avatar.svg', // avatar: '/static/home/avatar.svg',
memberCount: group?.memberCount || 0 memberCount: group?.memberCount || 0
} }
@ -1172,7 +1172,7 @@ class TimChatManager {
name: conversation.groupProfile?.name || '问诊群聊', name: conversation.groupProfile?.name || '问诊群聊',
doctorId: '', doctorId: '',
patientName: '', patientName: '',
avatar: '/static/home/avatar.svg', // avatar: '/static/home/avatar.svg',
lastMessage: '获取失败', lastMessage: '获取失败',
lastMessageTime: Date.now(), lastMessageTime: Date.now(),
unreadCount: conversation.unreadCount || 0, unreadCount: conversation.unreadCount || 0,
@ -1573,7 +1573,6 @@ class TimChatManager {
} else if (dbMsg.createdAt) { } else if (dbMsg.createdAt) {
lastTime = new Date(dbMsg.createdAt).getTime() lastTime = new Date(dbMsg.createdAt).getTime()
} }
// 构建基础消息对象 // 构建基础消息对象
const message = { const message = {
ID: dbMsg.MsgSeq || dbMsg._id || `db_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, ID: dbMsg.MsgSeq || dbMsg._id || `db_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
@ -2520,8 +2519,8 @@ class TimChatManager {
return '[自定义消息]' return '[自定义消息]'
} }
const customData = typeof payload.data === 'string' const customData = typeof payload.data === 'string'
? JSON.parse(payload.data) ? JSON.parse(payload.data)
: payload.data : payload.data
const messageType = customData.messageType || customData.type const messageType = customData.messageType || customData.type
@ -2569,7 +2568,6 @@ class TimChatManager {
conversationID, conversationID,
groupID, groupID,
name: patientName ? `${patientName}的问诊` : groupName || '问诊群聊', name: patientName ? `${patientName}的问诊` : groupName || '问诊群聊',
avatar: '/static/default-avatar.png',
lastMessage, lastMessage,
lastMessageTime, lastMessageTime,
unreadCount: conversation.unreadCount || 0, unreadCount: conversation.unreadCount || 0,
@ -2582,7 +2580,6 @@ class TimChatManager {
conversationID: conversation.conversationID, conversationID: conversation.conversationID,
groupID: conversation.conversationID?.replace('GROUP', '') || '', groupID: conversation.conversationID?.replace('GROUP', '') || '',
name: '问诊群聊', name: '问诊群聊',
avatar: '/static/default-avatar.png',
lastMessage: '暂无消息', lastMessage: '暂无消息',
lastMessageTime: Date.now(), lastMessageTime: Date.now(),
unreadCount: 0, unreadCount: 0,

View File

@ -50,4 +50,47 @@ export async function confirm(content, opt = {}) {
} }
}) })
}) })
}
// 保存图片到相册
export async function saveImageToPhotosAlbum(filePath) {
try {
// 检查授权
const authRes = await uni.getSetting()
if (!authRes[1].authSetting['scope.writePhotosAlbum']) {
// 请求授权
try {
await uni.authorize({ scope: 'scope.writePhotosAlbum' })
} catch (err) {
await confirm('需要您授权保存相册', { title: '提示', showCancel: false })
await uni.openSetting()
return
}
}
// 保存图片
await uni.saveImageToPhotosAlbum({ filePath })
await toast('保存成功')
} catch (err) {
console.error('保存图片失败:', err)
await toast('保存失败')
}
}
// 分享到微信
export function shareToWeChat(options = {}) {
const { title = '', path = '', imageUrl = '' } = options
return {
title,
path,
imageUrl,
success: () => {
toast('分享成功')
},
fail: (err) => {
console.error('分享失败:', err)
toast('分享失败')
}
}
} }