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_IMAGE_URL=https://patient.youcan365.com
|
||||
MP_CACHE_PREFIX=development
|
||||
MP_WX_APP_ID=wx93af55767423938e
|
||||
MP_WX_APP_ID=wx1d8337a40c11d66c
|
||||
MP_CORP_ID=wwe3fb2faa52cf9dfb
|
||||
MP_TIM_SDK_APP_ID=1600123876
|
||||
MP_INVITE_TEAMMATE_QRCODE=https://patient.youcan365.com/invite-teammate
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
MP_API_BASE_URL=http://localhost:8080
|
||||
MP_IMAGE_URL=https://patient.youcan365.com
|
||||
MP_CACHE_PREFIX=development
|
||||
MP_WX_APP_ID=wx93af55767423938e
|
||||
MP_WX_APP_ID=wx1d8337a40c11d66c
|
||||
MP_CORP_ID=wwe3fb2faa52cf9dfb
|
||||
MP_TIM_SDK_APP_ID=1600123876
|
||||
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">
|
||||
<slot></slot>
|
||||
</view>
|
||||
<scroll-view v-else scroll-y="true" :scroll-top="scrollTop" class="page-scroll" @scrolltolower="scrolltolower"
|
||||
@scroll="onScroll">
|
||||
<scroll-view
|
||||
v-else
|
||||
scroll-y="true"
|
||||
:scroll-top="scrollTop"
|
||||
class="page-scroll"
|
||||
@scrolltolower="scrolltolower"
|
||||
@scroll="onScroll"
|
||||
>
|
||||
<slot></slot>
|
||||
</scroll-view>
|
||||
</view>
|
||||
@ -16,22 +22,22 @@
|
||||
<slot name="footer"></slot>
|
||||
</view>
|
||||
<!-- #ifdef MP-->
|
||||
<view v-if="showSafeArea" class="safeareaBottom"></view>
|
||||
<!-- <view v-if="showSafeArea" class="safeareaBottom"></view> -->
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, useSlots, ref } from 'vue';
|
||||
import useDebounce from '@/utils/useDebounce';
|
||||
import { computed, useSlots, ref } from "vue";
|
||||
import useDebounce from "@/utils/useDebounce";
|
||||
|
||||
const emits = defineEmits(['reachBottom']);
|
||||
const emits = defineEmits(["reachBottom"]);
|
||||
const props = defineProps({
|
||||
customScroll: { type: Boolean, default: false },
|
||||
mainClass: { type: String, default: '' },
|
||||
mainStyle: { default: '' },
|
||||
pageClass: { type: String, default: '' },
|
||||
pageStyle: { default: '' },
|
||||
showSafeArea: { type: Boolean, default: true }
|
||||
mainClass: { type: String, default: "" },
|
||||
mainStyle: { default: "" },
|
||||
pageClass: { type: String, default: "" },
|
||||
pageStyle: { default: "" },
|
||||
showSafeArea: { type: Boolean, default: true },
|
||||
});
|
||||
const slots = useSlots();
|
||||
const hasHeader = computed(() => !!slots.header);
|
||||
@ -40,7 +46,7 @@ const hasFooter = computed(() => !!slots.footer);
|
||||
const scrollTop = ref(0);
|
||||
|
||||
const scrolltolower = useDebounce(() => {
|
||||
emits('reachBottom');
|
||||
emits("reachBottom");
|
||||
});
|
||||
|
||||
const onScroll = useDebounce((e) => {
|
||||
@ -52,9 +58,8 @@ function scrollToBottom() {
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
scrollToBottom
|
||||
})
|
||||
|
||||
scrollToBottom,
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.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" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "wx93af55767423938e",
|
||||
"appid" : "wx1d8337a40c11d66c",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
|
||||
@ -115,6 +115,12 @@
|
||||
"navigationBarTitleText": "病历详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/case/medical-case-form",
|
||||
"style": {
|
||||
"navigationBarTitleText": "添加病历"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/case/service-record-detail",
|
||||
"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>
|
||||
<view class="login-btn-wrap">
|
||||
<!-- <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 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 v-else class="login-btn" type="primary" @click="remind()">
|
||||
手机号快捷登录
|
||||
</button>
|
||||
|
||||
@ -120,7 +120,7 @@ const corpId = env.MP_CORP_ID;
|
||||
// 从页面参数获取群组信息
|
||||
const pageParams = ref({
|
||||
groupId: "",
|
||||
userId: "",
|
||||
patientId: "",
|
||||
corpId: "",
|
||||
});
|
||||
|
||||
@ -323,8 +323,19 @@ const sendArticle = async (article) => {
|
||||
imgUrl: article.cover || "",
|
||||
desc: "点击查看详情",
|
||||
});
|
||||
|
||||
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();
|
||||
} else {
|
||||
throw new Error(result.message || "发送失败");
|
||||
@ -350,8 +361,8 @@ onLoad((options) => {
|
||||
if (options.groupId) {
|
||||
pageParams.value.groupId = options.groupId;
|
||||
}
|
||||
if (options.userId) {
|
||||
pageParams.value.userId = options.userId;
|
||||
if (options.patientId) {
|
||||
pageParams.value.patientId = options.patientId;
|
||||
}
|
||||
if (options.corpId) {
|
||||
pageParams.value.corpId = options.corpId;
|
||||
|
||||
@ -16,6 +16,76 @@ $primary-color: #0877F1;
|
||||
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 {
|
||||
flex: 1;
|
||||
box-sizing: border-box;
|
||||
@ -348,23 +418,28 @@ $primary-color: #0877F1;
|
||||
.text-input,
|
||||
.voice-input-btn {
|
||||
flex: 1;
|
||||
padding: 0 46rpx;
|
||||
padding: 16rpx 46rpx;
|
||||
background-color: #f3f5fa;
|
||||
border-radius: 20rpx;
|
||||
margin: 0 16rpx;
|
||||
font-size: 28rpx;
|
||||
height: 80rpx;
|
||||
min-height: 80rpx;
|
||||
max-height: 200rpx;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 96rpx;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.voice-input-btn {
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 46rpx;
|
||||
text-align: center;
|
||||
line-height: 80rpx;
|
||||
}
|
||||
|
||||
.more-panel {
|
||||
@ -926,7 +1001,15 @@ $primary-color: #0877F1;
|
||||
.text-input::-moz-placeholder,
|
||||
.text-input:-ms-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" />
|
||||
</view>
|
||||
<view class="input-area">
|
||||
<input v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..."
|
||||
@confirm="sendTextMessage" @focus="handleInputFocus" />
|
||||
<textarea v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..."
|
||||
@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"
|
||||
@touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled>
|
||||
</input>
|
||||
@ -25,7 +26,6 @@
|
||||
<text>{{ btn.text }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 录音遮罩层 -->
|
||||
<view v-if="isRecording" class="recording-overlay">
|
||||
<view class="recording-modal" :class="{ 'cancel-mode': isCancelMode }">
|
||||
@ -75,6 +75,7 @@ const props = defineProps({
|
||||
formatTime: { type: Function, required: true },
|
||||
groupId: { type: String, default: '' },
|
||||
userId: { type: String, default: '' },
|
||||
patientId: { type: String, default: '' },
|
||||
corpId: { type: String, default: '' },
|
||||
});
|
||||
|
||||
@ -95,6 +96,11 @@ const cloudCustomData = computed(() => {
|
||||
return arr.filter(Boolean).join("|");
|
||||
});
|
||||
|
||||
// 流式输入文本
|
||||
const appendStreamText = (char) => {
|
||||
inputText.value += char;
|
||||
};
|
||||
|
||||
// 录音相关扩展状态(特效 + 取消逻辑)
|
||||
const recordingDuration = ref(0);
|
||||
let recordingTimer = null;
|
||||
@ -165,9 +171,22 @@ const sendTextMessageFromPhrase = async (content) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 设置输入框文本(覆盖原内容)
|
||||
const setInputText = (text) => {
|
||||
inputText.value = text;
|
||||
};
|
||||
|
||||
// 清空输入框
|
||||
const clearInputText = () => {
|
||||
inputText.value = '';
|
||||
};
|
||||
|
||||
// 暴露方法给父组件调用
|
||||
defineExpose({
|
||||
sendTextMessageFromPhrase
|
||||
sendTextMessageFromPhrase,
|
||||
appendStreamText,
|
||||
setInputText,
|
||||
clearInputText
|
||||
});
|
||||
|
||||
// 发送图片消息
|
||||
@ -367,7 +386,7 @@ const goToCommonPhrases = () => {
|
||||
// 跳转到宣教文章页面
|
||||
const goToArticleList = () => {
|
||||
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(() => {
|
||||
// 初始化录音管理器
|
||||
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 { onShow, onUnload } from '@dcloudio/uni-app'
|
||||
import api from '@/utils/api.js'
|
||||
import useTeamStore from '@/store/team.js'
|
||||
|
||||
/**
|
||||
* 简单的群聊hook
|
||||
@ -8,6 +10,9 @@ import { onShow, onUnload } from '@dcloudio/uni-app'
|
||||
export default function useGroupChat(groupID) {
|
||||
const groupInfo = ref({})
|
||||
const members = ref([])
|
||||
const teamMemberIds = ref([]) // 存储团队成员的userId列表
|
||||
const patientId = ref('') // 存储患者ID
|
||||
const teamStore = useTeamStore()
|
||||
|
||||
// 群聊成员映射
|
||||
const chatMember = computed(() => {
|
||||
@ -15,30 +20,79 @@ export default function useGroupChat(groupID) {
|
||||
members.value.forEach(member => {
|
||||
res[member.id] = {
|
||||
name: member.name,
|
||||
avatar: member.avatar || '/static/default-avatar.png'
|
||||
avatar: member.avatar,
|
||||
isTeamMember: member.isTeamMember // 标记是否为团队成员
|
||||
}
|
||||
})
|
||||
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() {
|
||||
const gid = typeof groupID === 'string' ? groupID : groupID.value
|
||||
if (!gid) return
|
||||
|
||||
try {
|
||||
// 这里可以调用API获取群聊信息
|
||||
// const res = await getGroupDetail(gid)
|
||||
// if (res && res.success) {
|
||||
// groupInfo.value = res.data
|
||||
// members.value = res.data.members || []
|
||||
// }
|
||||
// 1. 获取群聊基本信息
|
||||
const groupResult = await api('getGroupListByGroupId', { groupId: gid })
|
||||
|
||||
// 暂时使用本地数据
|
||||
groupInfo.value = {
|
||||
groupID: gid,
|
||||
name: '群聊',
|
||||
status: 'active'
|
||||
if (groupResult && groupResult.success && groupResult.data) {
|
||||
groupInfo.value = {
|
||||
groupID: gid,
|
||||
name: groupResult.data.team?.name || '群聊',
|
||||
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) {
|
||||
console.error('获取群聊信息失败:', error)
|
||||
@ -57,6 +111,8 @@ export default function useGroupChat(groupID) {
|
||||
groupInfo,
|
||||
members,
|
||||
chatMember,
|
||||
getGroupInfo
|
||||
getGroupInfo,
|
||||
isTeamMember,
|
||||
getUserAvatar
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,20 @@
|
||||
<template>
|
||||
<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
|
||||
class="chat-content"
|
||||
@ -50,23 +65,16 @@
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<view v-else class="message-content">
|
||||
<!-- 医生头像(左侧) -->
|
||||
<image
|
||||
v-if="message.flow === 'in'"
|
||||
class="doctor-msg-avatar"
|
||||
:src="
|
||||
chatMember[message.from]?.avatar || '/static/default-avatar.png'
|
||||
"
|
||||
:src="getUserAvatar(message.from)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
|
||||
<!-- 患者头像(右侧) -->
|
||||
<image
|
||||
v-if="message.flow === 'out'"
|
||||
class="user-msg-avatar"
|
||||
:src="
|
||||
chatMember[message.from]?.avatar || '/static/home/avatar.svg'
|
||||
"
|
||||
:src="getUserAvatar(message.from)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
|
||||
@ -125,6 +133,21 @@
|
||||
@cancel="handleRejectReasonCancel"
|
||||
/>
|
||||
|
||||
<!-- AI助手按钮组 -->
|
||||
<AIAssistantButtons
|
||||
v-if="
|
||||
!isEvaluationPopupOpen &&
|
||||
!showConsultAccept &&
|
||||
orderStatus === 'processing'
|
||||
"
|
||||
:groupId="groupId"
|
||||
:patientAccountId="chatInfo.userID || ''"
|
||||
:patientId="patientId"
|
||||
:corpId="corpId"
|
||||
@streamText="handleStreamText"
|
||||
@clearInput="handleClearInput"
|
||||
/>
|
||||
|
||||
<!-- 聊天输入组件 -->
|
||||
<ChatInput
|
||||
v-if="!isEvaluationPopupOpen && !showConsultAccept"
|
||||
@ -137,6 +160,7 @@
|
||||
: ''
|
||||
"
|
||||
:userId="openid"
|
||||
:patientId="patientId"
|
||||
:corpId="corpId"
|
||||
@scrollToBottom="() => scrollToBottom(true)"
|
||||
@messageSent="() => scrollToBottom(true)"
|
||||
@ -175,6 +199,7 @@ import ChatInput from "./components/chat-input.vue";
|
||||
import SystemMessage from "./components/system-message.vue";
|
||||
import ConsultAccept from "./components/consult-accept.vue";
|
||||
import RejectReasonModal from "./components/reject-reason-modal.vue";
|
||||
import AIAssistantButtons from "./components/ai-assistant-buttons.vue";
|
||||
|
||||
const timChatManager = globalTimChatManager;
|
||||
|
||||
@ -190,12 +215,12 @@ const { initIMAfterLogin } = useAccountStore();
|
||||
const chatInputRef = ref(null);
|
||||
|
||||
const groupId = ref("");
|
||||
const { chatMember, getGroupInfo } = useGroupChat(groupId);
|
||||
const { chatMember, getGroupInfo, getUserAvatar } = useGroupChat(groupId);
|
||||
|
||||
// 动态设置导航栏标题
|
||||
const updateNavigationTitle = () => {
|
||||
const updateNavigationTitle = (title = "群聊") => {
|
||||
uni.setNavigationBarTitle({
|
||||
title: "群聊",
|
||||
title: title,
|
||||
});
|
||||
};
|
||||
|
||||
@ -211,6 +236,17 @@ const isEvaluationPopupOpen = ref(false);
|
||||
// 订单状态
|
||||
const orderStatus = ref("");
|
||||
|
||||
// 患者信息
|
||||
const patientInfo = ref({
|
||||
name: "",
|
||||
sex: "",
|
||||
age: "",
|
||||
mobile: "",
|
||||
});
|
||||
|
||||
// 患者ID
|
||||
const patientId = ref("");
|
||||
|
||||
// 计算弹框显示状态 - 只有 pending 状态才显示接受问诊组件
|
||||
const showConsultAccept = computed(() => orderStatus.value === "pending");
|
||||
|
||||
@ -270,8 +306,29 @@ const fetchGroupOrderStatus = async () => {
|
||||
|
||||
if (result.success && result.data) {
|
||||
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("获取群组订单状态:", {
|
||||
orderStatus: orderStatus.value,
|
||||
teamName: teamName,
|
||||
patientInfo: patientInfo.value,
|
||||
groupId: groupId.value,
|
||||
});
|
||||
} else {
|
||||
@ -282,12 +339,6 @@ const fetchGroupOrderStatus = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 检查是否有待接诊的系统消息
|
||||
function checkConsultPendingStatus() {
|
||||
// 直接获取最新的订单状态
|
||||
fetchGroupOrderStatus();
|
||||
}
|
||||
|
||||
// 获取消息气泡样式类
|
||||
function getBubbleClass(message) {
|
||||
// 图片消息不需要气泡背景
|
||||
@ -390,18 +441,17 @@ const initTIMCallbacks = async () => {
|
||||
});
|
||||
|
||||
// 立即标记会话为已读,确保未读数为0
|
||||
if (timChatManager.tim && timChatManager.isLoggedIn && chatInfo.value.conversationID) {
|
||||
if (
|
||||
timChatManager.tim &&
|
||||
timChatManager.isLoggedIn &&
|
||||
chatInfo.value.conversationID
|
||||
) {
|
||||
timChatManager.tim
|
||||
.setMessageRead({
|
||||
conversationID: chatInfo.value.conversationID,
|
||||
})
|
||||
.then(() => {
|
||||
console.log("✓ 收到新消息后已标记为已读");
|
||||
// 触发会话列表更新,确保未读数为0
|
||||
timChatManager.triggerCallback('onConversationListUpdated', {
|
||||
conversationID: chatInfo.value.conversationID,
|
||||
unreadCount: 0
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("✗ 标记已读失败:", error);
|
||||
@ -461,9 +511,6 @@ const initTIMCallbacks = async () => {
|
||||
isCompleted.value = data.isCompleted || false;
|
||||
isLoadingMore.value = false;
|
||||
|
||||
// 检查是否有待接诊的系统消息
|
||||
checkConsultPendingStatus();
|
||||
|
||||
nextTick(() => {
|
||||
if (data.isRefresh) {
|
||||
console.log("后台刷新完成,保持当前滚动位置");
|
||||
@ -558,11 +605,6 @@ const loadMessageList = async () => {
|
||||
})
|
||||
.then(() => {
|
||||
console.log("✓ 会话已标记为已读:", chatInfo.value.conversationID);
|
||||
// 触发会话列表更新回调,通知消息列表页面清空未读数
|
||||
timChatManager.triggerCallback('onConversationListUpdated', {
|
||||
conversationID: chatInfo.value.conversationID,
|
||||
unreadCount: 0
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("✗ 标记会话已读失败:", error);
|
||||
@ -719,13 +761,32 @@ onShow(() => {
|
||||
// 页面隐藏
|
||||
onHide(() => {
|
||||
stopIMMonitoring();
|
||||
// 清空当前会话ID,避免离开页面后收到的消息被错误标记为已读
|
||||
timChatManager.currentConversationID = null;
|
||||
console.log("✓ 页面隐藏,已清空当前会话ID");
|
||||
});
|
||||
|
||||
const sendCommonPhrase = (content) => {
|
||||
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({
|
||||
sendCommonPhrase,
|
||||
@ -824,6 +885,21 @@ const handleRejectReasonConfirm = async (reason) => {
|
||||
const handleRejectReasonCancel = () => {
|
||||
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 () => {
|
||||
try {
|
||||
|
||||
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>
|
||||
<view class="p-15">
|
||||
<view class="bg-white px-10 mb-10 rounded">
|
||||
<form-input :form="formData" :required="rule.anotherName.required" wordLimit="10" title="anotherName"
|
||||
:name="rule.anotherName.name" @change="onChange($event)" />
|
||||
<form-input
|
||||
:form="formData"
|
||||
:required="rule.anotherName.required"
|
||||
wordLimit="10"
|
||||
title="anotherName"
|
||||
:name="rule.anotherName.name"
|
||||
@change="onChange($event)"
|
||||
/>
|
||||
<common-cell title="avatar" name="头像">
|
||||
<view class="flex-grow flex items-center justify-end" @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/default-avatar.png" />
|
||||
<view
|
||||
class="flex-grow flex items-center justify-end"
|
||||
@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" />
|
||||
</view>
|
||||
</common-cell>
|
||||
<form-select :form="formData" name="性别" title="gender" :range="genderOptions" @change="onChange($event)" />
|
||||
<form-input :form="formData" disableChange wordLimit="11" title="mobile" name="手机号 (不可修改)" />
|
||||
<form-select
|
||||
:form="formData"
|
||||
name="性别"
|
||||
title="gender"
|
||||
:range="genderOptions"
|
||||
@change="onChange($event)"
|
||||
/>
|
||||
<form-input
|
||||
:form="formData"
|
||||
disableChange
|
||||
wordLimit="11"
|
||||
title="mobile"
|
||||
name="手机号 (不可修改)"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="bg-white px-10 mb-10 rounded">
|
||||
<!-- 填写认证资料的时候岗位必填 -->
|
||||
<common-cell :required="type === 'cert'" title="job" :name="rule.job.name">
|
||||
<view class="flex-grow flex items-center justify-end" @click="selectJob()">
|
||||
<common-cell
|
||||
: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 class="mr-5 rounded-full" style="width: 64rpx;height: 64rpx;background: red;"></view> -->
|
||||
<uni-icons color="#999" type="right" size="16" />
|
||||
@ -39,12 +75,24 @@
|
||||
</view>
|
||||
|
||||
<view class="bg-white rounded">
|
||||
<form-textarea autoHeight :border="false" :form="formData" title="intro" name="个人介绍" :wordLimit="300"
|
||||
@change="onChange($event)" />
|
||||
<form-textarea
|
||||
autoHeight
|
||||
:border="false"
|
||||
:form="formData"
|
||||
title="intro"
|
||||
name="个人介绍"
|
||||
:wordLimit="300"
|
||||
@change="onChange($event)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<template #footer>
|
||||
<button-footer :cancelText="cancelText" :confirmText="confirmText" @confirm="save()" @cancel="back()" />
|
||||
<button-footer
|
||||
:cancelText="cancelText"
|
||||
:confirmText="confirmText"
|
||||
@confirm="save()"
|
||||
@cancel="back()"
|
||||
/>
|
||||
</template>
|
||||
</full-page>
|
||||
</template>
|
||||
@ -58,46 +106,56 @@ import api from "@/utils/api.js";
|
||||
import { upload } from "@/utils/http.js";
|
||||
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 FormInput from "@/components/form-template/form-cell/form-input.vue";
|
||||
import FormSelect from "@/components/form-template/form-cell/form-select.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 { useLoad, useShow } = useGuard();
|
||||
const { getDoctorInfo } = useAccountStore();
|
||||
|
||||
const job = { assistant: '医生助理', doctor: '医生' };
|
||||
const job = { assistant: "医生助理", doctor: "医生" };
|
||||
|
||||
const form = ref({});
|
||||
const inviteTeamId = ref('')
|
||||
const type = ref('');
|
||||
const inviteTeamId = ref("");
|
||||
const type = ref("");
|
||||
|
||||
const formData = computed(() => ({ ...(doctorInfo.value || {}), ...form.value, mobile: account.value?.mobile }));
|
||||
const cancelText = computed(() => doctorInfo.value ? '取消' : '暂不填写');
|
||||
const confirmText = computed(() => type.value === 'cert' ? '下一步' : '保存');
|
||||
const formData = computed(() => ({
|
||||
...(doctorInfo.value || {}),
|
||||
...form.value,
|
||||
mobile: account.value?.mobile,
|
||||
}));
|
||||
const cancelText = computed(() => (doctorInfo.value ? "取消" : "暂不填写"));
|
||||
const confirmText = computed(() => (type.value === "cert" ? "下一步" : "保存"));
|
||||
const jobStr = computed(() => {
|
||||
const jobs = 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 jobs =
|
||||
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(() => {
|
||||
if (doctorInfo.value && ['verified', 'verifying'].includes(doctorInfo.value.verifyStatus)) {
|
||||
if (
|
||||
doctorInfo.value &&
|
||||
["verified", "verifying"].includes(doctorInfo.value.verifyStatus)
|
||||
) {
|
||||
return {
|
||||
anotherName: { name: '姓名 (不可修改)', required: false, disabled: true },
|
||||
job: { name: '岗位 (不可修改)', disabled: true },
|
||||
title: { name: '职称 (不可修改)', disabled: true },
|
||||
dept: { name: '科室 (不可修改)', disabled: true },
|
||||
}
|
||||
anotherName: { name: "姓名 (不可修改)", required: false, disabled: true },
|
||||
job: { name: "岗位 (不可修改)", disabled: true },
|
||||
title: { name: "职称 (不可修改)", disabled: true },
|
||||
dept: { name: "科室 (不可修改)", disabled: true },
|
||||
};
|
||||
}
|
||||
return {
|
||||
anotherName: { name: '姓名', required: true, disabled: false },
|
||||
job: { name: '岗位', disabled: false },
|
||||
title: { name: '职称', disabled: false },
|
||||
dept: { name: '科室', disabled: false },
|
||||
}
|
||||
})
|
||||
anotherName: { name: "姓名", required: true, disabled: false },
|
||||
job: { name: "岗位", disabled: false },
|
||||
title: { name: "职称", disabled: false },
|
||||
dept: { name: "科室", disabled: false },
|
||||
};
|
||||
});
|
||||
|
||||
// 选项数据
|
||||
const genderOptions = [
|
||||
@ -139,55 +197,60 @@ function chooseAvatar() {
|
||||
if (url) {
|
||||
form.value.avatar = url;
|
||||
} else {
|
||||
toast('上传失败')
|
||||
toast("上传失败");
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function onChange({ title, value }) {
|
||||
form.value[title] = value
|
||||
form.value[title] = value;
|
||||
}
|
||||
|
||||
function selectJob() {
|
||||
if (rule.value.job.disabled) return;
|
||||
uni.showActionSheet({
|
||||
itemList: ['医生', '医生助理', '无'],
|
||||
itemList: ["医生", "医生助理", "无"],
|
||||
success: ({ tapIndex }) => {
|
||||
const job = ['doctor', 'assistant',][tapIndex];
|
||||
const job = ["doctor", "assistant"][tapIndex];
|
||||
form.value.job = job ? [job] : [];
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function toCert() {
|
||||
if (jobStr.value === '医生') {
|
||||
if (jobStr.value === "医生") {
|
||||
uni.navigateTo({
|
||||
url: '/pages/work/verify/doctor'
|
||||
})
|
||||
} else if (jobStr.value === '医生助理') {
|
||||
url: "/pages/work/verify/doctor",
|
||||
});
|
||||
} else if (jobStr.value === "医生助理") {
|
||||
uni.navigateTo({
|
||||
url: '/pages/work/verify/assistant'
|
||||
})
|
||||
url: "/pages/work/verify/assistant",
|
||||
});
|
||||
} else {
|
||||
toast('请选择岗位信息')
|
||||
toast("请选择岗位信息");
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (typeof formData.value.anotherName !== 'string' || !formData.value.anotherName.trim()) {
|
||||
return toast('请输入姓名')
|
||||
if (
|
||||
typeof formData.value.anotherName !== "string" ||
|
||||
!formData.value.anotherName.trim()
|
||||
) {
|
||||
return toast("请输入姓名");
|
||||
}
|
||||
if (type.value === 'cert' && !jobStr.value) {
|
||||
return toast('请选择岗位信息')
|
||||
if (type.value === "cert" && !jobStr.value) {
|
||||
return toast("请选择岗位信息");
|
||||
}
|
||||
const apiName = doctorInfo.value ? 'updateCorpMemberFromWxapp' : 'addCorpMemberFromWxapp';
|
||||
const apiName = doctorInfo.value
|
||||
? "updateCorpMemberFromWxapp"
|
||||
: "addCorpMemberFromWxapp";
|
||||
const data = {
|
||||
...form.value,
|
||||
weChatOpenId: account.value.openid,
|
||||
mobile: account.value.mobile,
|
||||
corpId: account.value.corpId,
|
||||
}
|
||||
};
|
||||
if (doctorInfo.value) {
|
||||
data.id = doctorInfo.value._id;
|
||||
}
|
||||
@ -196,30 +259,29 @@ async function save() {
|
||||
}
|
||||
const res = await api(apiName, data);
|
||||
if (res && res.success) {
|
||||
await getDoctorInfo()
|
||||
await getDoctorInfo();
|
||||
form.value = {};
|
||||
if (type.value === 'cert') {
|
||||
toCert()
|
||||
if (type.value === "cert") {
|
||||
toCert();
|
||||
} else {
|
||||
await toast('保存成功');
|
||||
back()
|
||||
await toast("保存成功");
|
||||
back();
|
||||
}
|
||||
} else {
|
||||
await toast(res?.message || '保存失败');
|
||||
await toast(res?.message || "保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
useLoad(opts => {
|
||||
useLoad((opts) => {
|
||||
type.value = opts?.type;
|
||||
if (type.value === 'joinTeam' && opts.teamId) {
|
||||
inviteTeamId.value = opts.teamId
|
||||
if (type.value === "joinTeam" && opts.teamId) {
|
||||
inviteTeamId.value = opts.teamId;
|
||||
}
|
||||
})
|
||||
|
||||
useShow(() => {
|
||||
getDoctorInfo()
|
||||
});
|
||||
|
||||
useShow(() => {
|
||||
getDoctorInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<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>
|
||||
</view>
|
||||
</swiper-item>
|
||||
@ -42,8 +42,8 @@
|
||||
</view>
|
||||
<view class="px-15 text-base text-gray leading-normal text-center">进入团队首页,即可发起线上咨询、建档授权等服务</view>
|
||||
<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="bg-primary rounded py-10 text-base text-white 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>
|
||||
<button class=" bg-primary rounded py-10 text-base text-white text-center flex-grow" open-type="share">分享微信</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -56,7 +56,7 @@ import { onLoad } from "@dcloudio/uni-app";
|
||||
import useAccountStore from "@/store/account.js";
|
||||
import useGuard from '@/hooks/useGuard';
|
||||
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 renamePopup from "./rename-popup.vue";
|
||||
@ -69,6 +69,7 @@ const current = ref(0);
|
||||
const list = ref([]);
|
||||
const visible = ref(false);
|
||||
const teamId = ref('')
|
||||
const qrcodes = ref(null);
|
||||
const indicator = computed(() => ({
|
||||
prev: current.value > 0,
|
||||
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 => {
|
||||
teamId.value = opts.teamId || '';
|
||||
})
|
||||
@ -121,6 +181,12 @@ useShow(() => {
|
||||
getTeams()
|
||||
})
|
||||
|
||||
// 导出分享方法供页面使用
|
||||
defineExpose({
|
||||
onShareAppMessage,
|
||||
onShareTimeline
|
||||
})
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.w-100 {
|
||||
@ -148,4 +214,15 @@ useShow(() => {
|
||||
.h-30 {
|
||||
height: 60rpx;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
border: none;
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.share-btn::after {
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
@ -7,7 +7,7 @@
|
||||
<view class="relative user-avatar mr-10" @click="editProfile()">
|
||||
<image v-if="doctorInfo && doctorInfo.avatar" class="avatar-img rounded-full overflow-hidden"
|
||||
: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" />
|
||||
<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" />
|
||||
@ -46,23 +46,29 @@
|
||||
<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="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">团队</view>
|
||||
<view class="py-5 px-15 rounded-full" :class="followUpType === 'person' ? 'bg-primary text-white' : ''"
|
||||
@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 class="py-10 px-15 flex items-center">
|
||||
<view class="flex-shrink-0 text-sm mr-10">
|
||||
<text class="text-dark">共</text>
|
||||
<text class="text-danger">23</text>
|
||||
<text class="text-danger">{{ total }}</text>
|
||||
<text class="text-dark">条</text>
|
||||
</view>
|
||||
<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"
|
||||
:class="current == i.value ? 'text-primary' : 'text-dark'">
|
||||
:class="filterData.eventStatus == i.value ? 'text-primary' : 'text-dark'" @click="changeStatus(i.value)">
|
||||
{{ i.label }}
|
||||
</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`" />
|
||||
</view>
|
||||
|
||||
@ -70,28 +76,35 @@
|
||||
</template>
|
||||
|
||||
<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="text-base text-dark">计划执行: 2025-10-22</view>
|
||||
<view class="text-base text-dark">计划执行: {{ i.planDate }}</view>
|
||||
<view class="flex items-center">
|
||||
<view class="text-base text-dark">患者: 李珊珊</view>
|
||||
<view class="text-base text-dark">患者: {{ i.customerName }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="py-10 px-15 flex items-center">
|
||||
<view class="mr-5 text-lg font-semibold">患者满意度调查</view>
|
||||
<view class="bg-opacity px-10 py-3 leading-normal text-base text-success rounded overflow-hidden">
|
||||
待处理
|
||||
<view class="mr-5 text-lg font-semibold">{{ i.eventTypeLabel }}</view>
|
||||
<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 class="px-15 text-base leading-normal text-gray">对于门诊就诊患者的满意度做统计,以便优化…</view>
|
||||
<view class="mt-10 px-15 flex items-center">
|
||||
<view class="mr-5 w-0 flex-grow truncate text-base leading-normal text-dark">
|
||||
发送内容:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX…
|
||||
<view v-if="i.sendContent" class="px-15 text-base leading-normal text-gray">{{ i.sendContent }}</view>
|
||||
<view v-if="i.enableSend && i.fileList.length" class="mt-10 px-15 flex items-center">
|
||||
<view class="mr-5 w-0 flex-grow">
|
||||
<view v-for="(file, idx) in i.fileList" :key="idx" class="truncate text-base leading-normal text-dark">
|
||||
发送内容:{{ file.file.name }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="bg-primary px-10 py-3 text-base text-white rounded-sm">发送</view>
|
||||
</view>
|
||||
<view class="mt-10 px-15 text-base leading-normal text-gray">张敏西(张敏希服务团队)</view>
|
||||
<view class="px-15 pb-10 text-base leading-normal text-gray">创建:2026-01-08 张敏西</view>
|
||||
<view class="mt-10 px-15 text-base leading-normal text-gray truncate">
|
||||
{{ i.executorUserName }}({{ i.executeTeamName }})
|
||||
</view>
|
||||
<view class="px-15 pb-10 text-base leading-normal text-gray truncate">
|
||||
创建:{{ i.createTime }} {{ i.creatorUserName }}
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
@ -103,19 +116,28 @@
|
||||
</template>
|
||||
</full-page>
|
||||
<cert-popup :visible="visible" @close="visible = false" />
|
||||
<filter-popup :data="filterData" :visible="filterVisible" @close="filterVisible = false"
|
||||
@confirm="changeFilterData($event)" />
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { storeToRefs } from "pinia";
|
||||
import { statusNames, ToDoEventType, statusClassNames } from '@/baseData';
|
||||
import useGuard from "@/hooks/useGuard.js";
|
||||
import useInfoCheck from '@/hooks/useInfoCheck';
|
||||
import usePageList from '@/hooks/usePageList';
|
||||
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 filterPopup from './components/filter-popup.vue';
|
||||
import EmptyData from "@/components/empty-data.vue";
|
||||
import fullPage from '@/components/full-page.vue';
|
||||
import { toast } from '@/utils/widget';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const certConfig = {
|
||||
failed: { text: '认证失败', classnames: 'bg-danger text-white' },
|
||||
@ -123,27 +145,24 @@ const certConfig = {
|
||||
verifying: { text: '认证中', classnames: 'bg-warning text-white' },
|
||||
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 { getDoctorInfo } = useAccountStore();
|
||||
const { doctorInfo } = storeToRefs(useAccountStore());
|
||||
const { account, doctorInfo } = storeToRefs(useAccountStore());
|
||||
const { chargeTeams } = storeToRefs(useTeamStore());
|
||||
const { getTeams } = useTeamStore();
|
||||
|
||||
const { withInfo } = useInfoCheck();
|
||||
const list = ref([1]);
|
||||
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 handleVerify = () => {
|
||||
uni.showToast({
|
||||
title: "跳转到认证页面",
|
||||
icon: "none",
|
||||
});
|
||||
};
|
||||
|
||||
// 邀请患者
|
||||
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() {
|
||||
uni.navigateTo({
|
||||
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(() => {
|
||||
console.log("工作台页面加载");
|
||||
});
|
||||
|
||||
useShow(() => {
|
||||
getDoctorInfo();
|
||||
useShow(async () => {
|
||||
console.log("工作台页面加!!!@@@载");
|
||||
await getDoctorInfo();
|
||||
changePage(1)
|
||||
})
|
||||
</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 loading = ref(false);
|
||||
|
||||
const loginPromise = ref(null);
|
||||
// IM 相关
|
||||
const openid = ref(cache.get(CACHE_KEYS.OPENID, ""));
|
||||
@ -48,6 +48,7 @@ export default defineStore("accountStore", () => {
|
||||
});
|
||||
if (code) {
|
||||
const res = await api('wxAppLogin', {
|
||||
appId: appid,
|
||||
phoneCode,
|
||||
code,
|
||||
corpId,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { defineStore, storeToRefs } from "pinia";
|
||||
import api from '@/utils/api';
|
||||
import { toast } from '@/utils/widget';
|
||||
@ -9,6 +9,13 @@ export default defineStore("teamStore", () => {
|
||||
const { account, doctorInfo } = storeToRefs(useAccountStore());
|
||||
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) {
|
||||
if (!teamId || !account.value?.corpId) return;
|
||||
const res = await api('getTeamData', { teamId, corpId: account.value.corpId });
|
||||
@ -21,11 +28,24 @@ export default defineStore("teamStore", () => {
|
||||
|
||||
async function getTeams() {
|
||||
const corpId = account.value?.corpId;
|
||||
const mateId = doctorInfo.value?.corpId;
|
||||
const mateId = doctorInfo.value?.userid;
|
||||
if (!corpId || !mateId) return;
|
||||
const res = await api('getJoinedTeams', { corpId, mateId });
|
||||
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',
|
||||
removeTeammate: "removeTeammate",
|
||||
toggleTeamLeaderRole: "toggleTeamLeaderRole",
|
||||
joinTheInvitedTeam: 'joinTheInvitedTeam'
|
||||
joinTheInvitedTeam: 'joinTheInvitedTeam',
|
||||
getTeamMemberAvatars: 'getTeamMemberAvatars'
|
||||
},
|
||||
|
||||
knowledgeBase: {
|
||||
@ -97,7 +98,9 @@ const urlsConfig = {
|
||||
acceptConsultation: "acceptConsultation",
|
||||
sendArticleMessage: "sendArticleMessage",
|
||||
getChatRecordsByGroupId: "getChatRecordsByGroupId",
|
||||
getGroupList: "getGroupList"
|
||||
getGroupList: "getGroupList",
|
||||
followUpInquiry: "followUpInquiry",
|
||||
supplementMedicalCase: "supplementMedicalCase"
|
||||
},
|
||||
todo: {
|
||||
getCustomerTodos: 'getCustomerTodos',
|
||||
@ -119,6 +122,7 @@ const urlsConfig = {
|
||||
// 客户流转记录
|
||||
customerTransferRecord: 'customerTransferRecord',
|
||||
// sendConsultRejectedMessage: "sendConsultRejectedMessage"
|
||||
getTeamTodos: 'getTeamTodos'
|
||||
}
|
||||
|
||||
}
|
||||
@ -137,7 +141,7 @@ const urls = Object.keys(urlsConfig).reduce((acc, path) => {
|
||||
}, {})
|
||||
console.log('urls: ', urls)
|
||||
|
||||
export default async function api(urlId, data) {
|
||||
export default async function api(urlId, data, loading) {
|
||||
const config = urls[urlId];
|
||||
if (!config) {
|
||||
throw new Error(`Unknown URL ID: ${urlId}`);
|
||||
@ -148,7 +152,7 @@ export default async function api(urlId, data) {
|
||||
data: {
|
||||
...data,
|
||||
type,
|
||||
}
|
||||
})
|
||||
},
|
||||
}, loading)
|
||||
}
|
||||
|
||||
|
||||
@ -42,41 +42,37 @@ export async function mergeConversationWithGroupDetails(conversationList, option
|
||||
groupIds,
|
||||
...options // 支持传入额外的查询参数(corpId, teamId, keyword等)
|
||||
}
|
||||
const response = await api('getGroupList', requestData)
|
||||
const response = await api('getGroupList', requestData, false)
|
||||
// 4. 检查响应
|
||||
if (!response || !response.success) {
|
||||
console.error('获取群组详细信息失败:', response?.message || '未知错误')
|
||||
return []
|
||||
}
|
||||
|
||||
const groupDetailsMap = createGroupDetailsMap(response.data?.list || [])
|
||||
console.log('获取到的群组详细信息数量:', Object.keys(groupDetailsMap).size)
|
||||
|
||||
// 5. 合并数据并过滤
|
||||
const mergedList = conversationList
|
||||
.map(conversation => mergeConversationData(conversation, groupDetailsMap))
|
||||
.filter(item => item !== null) // 过滤掉后端不存在的会话
|
||||
|
||||
.filter(item => item !== null);
|
||||
console.log('合并后的会话列表数量:', mergedList.length)
|
||||
console.log('过滤掉的会话数量:', conversationList.length - mergedList.length)
|
||||
|
||||
// 6. 格式化并排序会话列表
|
||||
const formattedList = mergedList
|
||||
.map((group) => ({
|
||||
conversationID: group.conversationID || `GROUP${group.groupID}`,
|
||||
groupID: group.groupID,
|
||||
name: group.patientName
|
||||
? `${group.patientName}的问诊`
|
||||
: group.name || "问诊群聊",
|
||||
avatar: group.avatar || "/static/default-avatar.png",
|
||||
avatar: group.avatar || "/static/default-patient-avatar.png",
|
||||
lastMessage: group.lastMessage || "暂无消息",
|
||||
lastMessageTime: group.lastMessageTime || Date.now(),
|
||||
groupID: group.groupID,
|
||||
unreadCount: group.unreadCount || 0,
|
||||
doctorId: group.doctorId,
|
||||
patientName: group.patientName,
|
||||
patientSex: group.patientSex,
|
||||
patientAge: group.patientAge,
|
||||
orderStatus: group.orderStatus,
|
||||
teamId: group.teamId,
|
||||
teamName: group.teamName,
|
||||
teamMemberList: group.teamMemberList,
|
||||
}))
|
||||
.sort((a, b) => b.lastMessageTime - a.lastMessageTime)
|
||||
|
||||
@ -127,9 +123,6 @@ function mergeConversationData(conversation, groupDetailsMap) {
|
||||
return {
|
||||
// 保留原有的会话信息
|
||||
...conversation,
|
||||
|
||||
// 合并后端的群组信息
|
||||
_id: groupDetail._id,
|
||||
corpId: groupDetail.corpId,
|
||||
teamId: groupDetail.teamId,
|
||||
customerId: groupDetail.customerId,
|
||||
@ -148,7 +141,7 @@ function mergeConversationData(conversation, groupDetailsMap) {
|
||||
teamName: groupDetail.team?.name,
|
||||
teamMemberList: groupDetail.team?.memberList,
|
||||
teamDescription: groupDetail.team?.description,
|
||||
|
||||
teamId: groupDetail.teamId,
|
||||
// 时间信息
|
||||
createdAt: groupDetail.createdAt,
|
||||
updatedAt: groupDetail.updatedAt,
|
||||
@ -156,8 +149,8 @@ function mergeConversationData(conversation, groupDetailsMap) {
|
||||
// 更新显示名称(使用后端的患者信息)
|
||||
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 = {
|
||||
groupID: groupID,
|
||||
name: group?.name || '问诊群聊',
|
||||
avatar: '/static/home/avatar.svg',
|
||||
// avatar: '/static/home/avatar.svg',
|
||||
memberCount: group?.memberCount || 0
|
||||
}
|
||||
|
||||
@ -1172,7 +1172,7 @@ class TimChatManager {
|
||||
name: conversation.groupProfile?.name || '问诊群聊',
|
||||
doctorId: '',
|
||||
patientName: '',
|
||||
avatar: '/static/home/avatar.svg',
|
||||
// avatar: '/static/home/avatar.svg',
|
||||
lastMessage: '获取失败',
|
||||
lastMessageTime: Date.now(),
|
||||
unreadCount: conversation.unreadCount || 0,
|
||||
@ -1573,7 +1573,6 @@ class TimChatManager {
|
||||
} else if (dbMsg.createdAt) {
|
||||
lastTime = new Date(dbMsg.createdAt).getTime()
|
||||
}
|
||||
|
||||
// 构建基础消息对象
|
||||
const message = {
|
||||
ID: dbMsg.MsgSeq || dbMsg._id || `db_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
@ -2569,7 +2568,6 @@ class TimChatManager {
|
||||
conversationID,
|
||||
groupID,
|
||||
name: patientName ? `${patientName}的问诊` : groupName || '问诊群聊',
|
||||
avatar: '/static/default-avatar.png',
|
||||
lastMessage,
|
||||
lastMessageTime,
|
||||
unreadCount: conversation.unreadCount || 0,
|
||||
@ -2582,7 +2580,6 @@ class TimChatManager {
|
||||
conversationID: conversation.conversationID,
|
||||
groupID: conversation.conversationID?.replace('GROUP', '') || '',
|
||||
name: '问诊群聊',
|
||||
avatar: '/static/default-avatar.png',
|
||||
lastMessage: '暂无消息',
|
||||
lastMessageTime: Date.now(),
|
||||
unreadCount: 0,
|
||||
|
||||
@ -51,3 +51,46 @@ 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