Compare commits
24 Commits
33a32e1134
...
4160f737e7
| Author | SHA1 | Date | |
|---|---|---|---|
| 4160f737e7 | |||
| 65c9a3efe5 | |||
| 3ccdc954c2 | |||
| a7d3eeae3a | |||
| dcc84cf449 | |||
|
|
b08830c7ad | ||
|
|
3328435be1 | ||
| 68e4f01bd2 | |||
| b48a00b58a | |||
| 18675de48b | |||
| e26785bf4b | |||
| a83f92023e | |||
| 01575ef51a | |||
|
|
38553df861 | ||
|
|
ceccd59b65 | ||
| 7b222b6143 | |||
| d6851d72f2 | |||
| 3316e1bfa0 | |||
| 3ad6140829 | |||
|
|
a66bfce901 | ||
|
|
ca5ad25854 | ||
| 7f737dfd8a | |||
| 963b4cc780 | |||
| 4fc7a8f6f2 |
@ -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
|
||||||
|
|||||||
@ -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
39
baseData/index.js
Normal 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",
|
||||||
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
89
components/share-actions.vue
Normal file
89
components/share-actions.vue
Normal 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
33
hooks/usePageList.js
Normal 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 }
|
||||||
|
|
||||||
|
}
|
||||||
@ -50,7 +50,7 @@
|
|||||||
"quickapp" : {},
|
"quickapp" : {},
|
||||||
/* 小程序特有相关 */
|
/* 小程序特有相关 */
|
||||||
"mp-weixin" : {
|
"mp-weixin" : {
|
||||||
"appid" : "wx93af55767423938e",
|
"appid" : "wx1d8337a40c11d66c",
|
||||||
"setting" : {
|
"setting" : {
|
||||||
"urlCheck" : false
|
"urlCheck" : false
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
473
pages/case/medical-case-form.vue
Normal file
473
pages/case/medical-case-form.vue
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 时间分割线 */
|
/* 时间分割线 */
|
||||||
|
|||||||
427
pages/message/components/ai-assistant-buttons.vue
Normal file
427
pages/message/components/ai-assistant-buttons.vue
Normal 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
|
||||||
|
); // 第二个参数false表示不显示loading
|
||||||
|
|
||||||
|
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>
|
||||||
@ -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();
|
||||||
|
|||||||
378
pages/message/components/medical-case-progress.vue
Normal file
378
pages/message/components/medical-case-progress.vue
Normal 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>
|
||||||
134
pages/message/components/medical-case-type-selector.vue
Normal file
134
pages/message/components/medical-case-type-selector.vue
Normal 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>
|
||||||
317
pages/message/components/message-header.vue
Normal file
317
pages/message/components/message-header.vue
Normal 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>
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
211
pages/work/components/filter-popup.vue
Normal file
211
pages/work/components/filter-popup.vue
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
BIN
static/default-patient-avatar.png
Normal file
BIN
static/default-patient-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
BIN
static/icon/buchong.png
Normal file
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
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
BIN
static/icon/zhuiwen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1008 B |
1
static/zhuanhua.svg
Normal file
1
static/zhuanhua.svg
Normal 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 |
@ -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,
|
||||||
|
|||||||
@ -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 }
|
||||||
})
|
})
|
||||||
14
utils/api.js
14
utils/api.js
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
234
utils/share-usage-example.md
Normal file
234
utils/share-usage-example.md
Normal 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
169
utils/share.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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('分享失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user