Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb
This commit is contained in:
commit
25a230ece7
10
.env.production
Normal file
10
.env.production
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
MP_API_BASE_URL=https://ytk.youcan365.com
|
||||||
|
MP_IMAGE_URL=https://ytk.youcan365.com
|
||||||
|
MP_CACHE_PREFIX=production
|
||||||
|
MP_WX_APP_ID=wx1d8337a40c11d66c
|
||||||
|
MP_CORP_ID=wpLgjyawAA8N0gWmXgyJq8wpjGcOT7fg
|
||||||
|
MP_TIM_SDK_APP_ID=1600123876
|
||||||
|
MP_INVITE_TEAMMATE_QRCODE=https://ykt.youcan365.com/invite-teammate
|
||||||
|
MP_INVITE_PATIENT_QRCODE=https://ykt.youcan365.com/invite-patient
|
||||||
|
MP_PATIENT_PAGE_BASE_URL= 'https://www.youcan365.com/h5/#/'
|
||||||
|
MP_SURVEY_URL= 'https://www.youcan365.com/surveyDev/#/pages/survey/survey'
|
||||||
@ -11,6 +11,11 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-cell-required {
|
||||||
|
margin-left: 4rpx;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
.form-content__wrapper {
|
.form-content__wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -58,4 +63,4 @@
|
|||||||
.form-row__content {
|
.form-row__content {
|
||||||
width: 0;
|
width: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="form-row" @click="handleClick">
|
<view class="form-row" @click="handleClick">
|
||||||
<view class="form-row__label">
|
<view class="form-row__label">
|
||||||
{{ name }}<text v-if="required" class="form-cell--required"></text>
|
{{ name }}<text v-if="required" class="form-cell-required">*</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="form-row__content">
|
<view class="form-row__content">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="files-wrap">
|
<view class="files-wrap">
|
||||||
<view class="files-label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
|
<view class="files-label">
|
||||||
|
{{ name }}<text v-if="required" class="form-cell-required">*</text>
|
||||||
|
</view>
|
||||||
<view class="grid">
|
<view class="grid">
|
||||||
<view v-for="(f, idx) in files" :key="idx" class="item" @click="preview(idx)">
|
<view v-for="(f, idx) in files" :key="idx" class="item" @click="preview(idx)">
|
||||||
<image class="thumb" :src="f.url" mode="aspectFill" />
|
<image class="thumb" :src="f.url" mode="aspectFill" />
|
||||||
@ -15,7 +17,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { chooseAndUploadImage } from '@/utils/file';
|
import { chooseAndUploadImage, normalizeFileUrl } from '@/utils/file';
|
||||||
import { toast } from '@/utils/widget';
|
import { toast } from '@/utils/widget';
|
||||||
|
|
||||||
const emits = defineEmits(['change']);
|
const emits = defineEmits(['change']);
|
||||||
@ -38,13 +40,13 @@ const files = computed(() => {
|
|||||||
if (Array.isArray(v)) {
|
if (Array.isArray(v)) {
|
||||||
return v
|
return v
|
||||||
.map((i) => {
|
.map((i) => {
|
||||||
if (typeof i === 'string') return { url: i };
|
if (typeof i === 'string') return { url: normalizeFileUrl(i) };
|
||||||
if (i && typeof i === 'object' && i.url) return { url: String(i.url) };
|
if (i && typeof i === 'object' && i.url) return { url: normalizeFileUrl(String(i.url)) };
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
if (typeof v === 'string' && v) return [{ url: v }];
|
if (typeof v === 'string' && v) return [{ url: normalizeFileUrl(v) }];
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -141,4 +143,3 @@ async function add() {
|
|||||||
line-height: 56rpx;
|
line-height: 56rpx;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="multi-wrap">
|
<view class="multi-wrap">
|
||||||
<view class="label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
|
<view class="label">
|
||||||
|
{{ name }}<text v-if="required" class="form-cell-required">*</text>
|
||||||
|
</view>
|
||||||
<view class="options" :class="hasOtherSelected ? 'with-other' : ''">
|
<view class="options" :class="hasOtherSelected ? 'with-other' : ''">
|
||||||
<view
|
<view
|
||||||
v-for="opt in displayOptions"
|
v-for="opt in displayOptions"
|
||||||
@ -165,4 +167,3 @@ function onOtherInput(e) {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="form-row">
|
<view class="form-row">
|
||||||
<view class="form-row__label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
|
<view class="form-row__label">
|
||||||
|
{{ name }}<text v-if="required" class="form-cell-required">*</text>
|
||||||
|
</view>
|
||||||
<view class="form-row__content runtime">
|
<view class="form-row__content runtime">
|
||||||
<input :disabled="disableChange" class="num" type="number" :value="year" @input="onInput($event, 'year')" />
|
<input :disabled="disableChange" class="num" type="number" :value="year" @input="onInput($event, 'year')" />
|
||||||
<text class="unit">年</text>
|
<text class="unit">年</text>
|
||||||
@ -86,4 +88,3 @@ function onInput(e, key) {
|
|||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="form-row">
|
<view class="form-row">
|
||||||
<view class="form-row__label" :class="required ? 'form-cell--required' : ''">{{ name }}</view>
|
<view class="form-row__label">
|
||||||
|
{{ name }}<text v-if="required" class="form-cell-required">*</text>
|
||||||
|
</view>
|
||||||
<view class="form-row__content content">
|
<view class="form-row__content content">
|
||||||
<input
|
<input
|
||||||
:disabled="disableChange"
|
:disabled="disableChange"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="textarea-row">
|
<view class="textarea-row">
|
||||||
<view class="form-row__label">
|
<view class="form-row__label">
|
||||||
{{ name }}<text v-if="required" class="form-cell--required"></text>
|
{{ name }}<text v-if="required" class="form-cell-required">*</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="mt-10">
|
<view class="mt-10">
|
||||||
<textarea
|
<textarea
|
||||||
@ -94,6 +94,8 @@ function change(e) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@import '../cell-style.css';
|
||||||
|
|
||||||
.textarea-row {
|
.textarea-row {
|
||||||
padding: 24rpx 30rpx;
|
padding: 24rpx 30rpx;
|
||||||
border-bottom: 1px solid #eee;
|
border-bottom: 1px solid #eee;
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
@change="change"
|
@change="change"
|
||||||
/>
|
/>
|
||||||
<form-diagnosis-picker
|
<form-diagnosis-picker
|
||||||
v-else-if="attrs.title === 'diagnosis' || attrs.title === 'diagnosisName'"
|
v-else-if="attrs.type === 'diagnosis' || attrs.__originType === 'diagnosis' || attrs.title === 'diagnosis' || attrs.title === 'diagnosisName'"
|
||||||
v-bind="attrs"
|
v-bind="attrs"
|
||||||
:form="form"
|
:form="form"
|
||||||
:disableChange="disableChange"
|
:disableChange="disableChange"
|
||||||
|
|||||||
@ -238,6 +238,12 @@
|
|||||||
"navigationBarTitleText": "添加病历"
|
"navigationBarTitleText": "添加病历"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "ai-medical-case-form",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "添加病历"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "service-record-detail",
|
"path": "service-record-detail",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
480
pages/case/ai-medical-case-form.vue
Normal file
480
pages/case/ai-medical-case-form.vue
Normal file
@ -0,0 +1,480 @@
|
|||||||
|
<template>
|
||||||
|
<view class="medical-case-form">
|
||||||
|
<view class="form-container">
|
||||||
|
<!-- 动态渲染表单字段 -->
|
||||||
|
<view
|
||||||
|
v-for="field in currentFields"
|
||||||
|
:key="field.key"
|
||||||
|
class="form-item"
|
||||||
|
:class="{ required: field.required }"
|
||||||
|
>
|
||||||
|
<view class="item-label">{{ field.label }}</view>
|
||||||
|
|
||||||
|
<!-- 日期选择器 -->
|
||||||
|
<picker
|
||||||
|
v-if="field.type === 'date'"
|
||||||
|
mode="date"
|
||||||
|
:value="formData[field.key]"
|
||||||
|
@change="onDateChange(field.key, $event)"
|
||||||
|
:disabled="!isEditing"
|
||||||
|
>
|
||||||
|
<view class="picker-value">
|
||||||
|
{{ formData[field.key] || "暂无" }}
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
|
|
||||||
|
<!-- 多行文本 -->
|
||||||
|
<textarea
|
||||||
|
v-else-if="field.type === 'textarea'"
|
||||||
|
class="item-textarea"
|
||||||
|
v-model="formData[field.key]"
|
||||||
|
placeholder="请输入"
|
||||||
|
:disabled="!isEditing"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 单行文本 -->
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
class="item-input"
|
||||||
|
v-model="formData[field.key]"
|
||||||
|
placeholder="暂无"
|
||||||
|
:disabled="!isEditing"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="footer-buttons">
|
||||||
|
<view class="btn-regenerate" @click="handleRegenerate">
|
||||||
|
<text class="btn-text">重新生成</text>
|
||||||
|
</view>
|
||||||
|
<view class="btn-save" @click="handleSave">
|
||||||
|
<text class="btn-text">保存至档案</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from "vue";
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import useAccountStore from "@/store/account";
|
||||||
|
import api from "@/utils/api.js";
|
||||||
|
const caseType = ref("");
|
||||||
|
const formData = ref({});
|
||||||
|
const isEditing = ref(true);
|
||||||
|
const customerId = ref("");
|
||||||
|
const groupId = ref("");
|
||||||
|
const accountStore = useAccountStore();
|
||||||
|
const { doctorInfo } = storeToRefs(accountStore);
|
||||||
|
// 病历类型名称
|
||||||
|
const CASE_TYPE_NAMES = {
|
||||||
|
outpatient: "门诊病历",
|
||||||
|
inhospital: "住院病历",
|
||||||
|
physicalExaminationTemplate: "体检记录",
|
||||||
|
preConsultation: "预问诊记录",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 字段标签
|
||||||
|
const FIELD_LABELS = {
|
||||||
|
// 门诊病历
|
||||||
|
visitTime: "就诊日期",
|
||||||
|
chiefComplaint: "主诉",
|
||||||
|
medicalHistorySummary: "病史概要",
|
||||||
|
examination: "检查",
|
||||||
|
diagnosisName: "门诊诊断",
|
||||||
|
// 住院病历
|
||||||
|
inhosDate: "入院日期",
|
||||||
|
operation: "手术记录",
|
||||||
|
operationDate: "手术日期",
|
||||||
|
treatmentPlan: "治疗方案",
|
||||||
|
// 体检记录
|
||||||
|
inspectTime: "体检日期",
|
||||||
|
inspectSummary: "体检小结",
|
||||||
|
positiveFind: "阳性发现及处理意见",
|
||||||
|
// 预问诊记录
|
||||||
|
consultationDate: "问诊日期",
|
||||||
|
presentIllnessHistory: "现病史",
|
||||||
|
pastMedicalHistory: "既往史",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 字段配置:根据病历类型定义字段
|
||||||
|
const FIELD_CONFIG = {
|
||||||
|
outpatient: [
|
||||||
|
{
|
||||||
|
key: "visitTime",
|
||||||
|
label: FIELD_LABELS.visitTime,
|
||||||
|
type: "date",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "diagnosisName",
|
||||||
|
label: FIELD_LABELS.diagnosisName,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "chiefComplaint",
|
||||||
|
label: FIELD_LABELS.chiefComplaint,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "medicalHistorySummary",
|
||||||
|
label: FIELD_LABELS.medicalHistorySummary,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "examination",
|
||||||
|
label: FIELD_LABELS.examination,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "treatmentPlan",
|
||||||
|
label: "治疗方案",
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inhospital: [
|
||||||
|
{
|
||||||
|
key: "inhosDate",
|
||||||
|
label: FIELD_LABELS.inhosDate,
|
||||||
|
type: "date",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "diagnosisName",
|
||||||
|
label: "住院主诊断",
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "operation",
|
||||||
|
label: FIELD_LABELS.operation,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "operationDate",
|
||||||
|
label: FIELD_LABELS.operationDate,
|
||||||
|
type: "date",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "treatmentPlan",
|
||||||
|
label: FIELD_LABELS.treatmentPlan,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "chiefComplaint",
|
||||||
|
label: FIELD_LABELS.chiefComplaint,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "medicalHistorySummary",
|
||||||
|
label: FIELD_LABELS.medicalHistorySummary,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "examination",
|
||||||
|
label: FIELD_LABELS.examination,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "treatmentPlan",
|
||||||
|
label: "治疗方案",
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
physicalExaminationTemplate: [
|
||||||
|
{
|
||||||
|
key: "inspectTime",
|
||||||
|
label: FIELD_LABELS.inspectTime,
|
||||||
|
type: "date",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "inspectSummary",
|
||||||
|
label: FIELD_LABELS.inspectSummary,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "positiveFind",
|
||||||
|
label: FIELD_LABELS.positiveFind,
|
||||||
|
type: "textarea",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
preConsultation: [
|
||||||
|
{
|
||||||
|
key: "consultationDate",
|
||||||
|
label: FIELD_LABELS.consultationDate,
|
||||||
|
type: "date",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "chiefComplaint",
|
||||||
|
label: FIELD_LABELS.chiefComplaint,
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "presentIllnessHistory",
|
||||||
|
label: FIELD_LABELS.presentIllnessHistory,
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "pastMedicalHistory",
|
||||||
|
label: FIELD_LABELS.pastMedicalHistory,
|
||||||
|
type: "textarea",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 当前病历类型的字段配置
|
||||||
|
const currentFields = computed(() => {
|
||||||
|
return FIELD_CONFIG[caseType.value] || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
const currentPage = pages[pages.length - 1];
|
||||||
|
const options = currentPage.options;
|
||||||
|
|
||||||
|
caseType.value = options.caseType || "";
|
||||||
|
customerId.value = options.patientId || "";
|
||||||
|
groupId.value = options.groupId || "";
|
||||||
|
// 从 options 中解析表单数据
|
||||||
|
if (options.formData) {
|
||||||
|
try {
|
||||||
|
formData.value = JSON.parse(decodeURIComponent(options.formData));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("解析表单数据失败:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置页面标题
|
||||||
|
const title = CASE_TYPE_NAMES[caseType.value]
|
||||||
|
? `添加${CASE_TYPE_NAMES[caseType.value]}`
|
||||||
|
: "添加病历";
|
||||||
|
uni.setNavigationBarTitle({ title });
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDateChange = (field, event) => {
|
||||||
|
formData.value[field] = event.detail.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRegenerate = () => {
|
||||||
|
uni.showModal({
|
||||||
|
title: "提示",
|
||||||
|
content: "确定要重新生成吗?当前编辑的内容将被覆盖",
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
// 返回上一页并触发重新生成
|
||||||
|
uni.navigateBack({
|
||||||
|
success: () => {
|
||||||
|
uni.$emit("regenerateMedicalCase", {
|
||||||
|
caseType: caseType.value,
|
||||||
|
customerId: customerId.value,
|
||||||
|
groupId: groupId.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
// 验证必填项
|
||||||
|
const requiredFields = getRequiredFields();
|
||||||
|
const missingFields = requiredFields.filter(
|
||||||
|
(field) => !formData.value[field.key]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
uni.showToast({
|
||||||
|
title: `请填写${missingFields[0].label}`,
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
uni.showLoading({ title: "保存中..." });
|
||||||
|
const result = await api("addMedicalRecord", {
|
||||||
|
medicalType: caseType.value,
|
||||||
|
memberId: customerId.value,
|
||||||
|
creator: doctorInfo.value.userid,
|
||||||
|
...formData.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
uni.hideLoading();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
uni.showToast({
|
||||||
|
title: "保存成功",
|
||||||
|
icon: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateBack();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: result.message || "保存失败",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
uni.hideLoading();
|
||||||
|
console.error("保存病历失败:", error);
|
||||||
|
uni.showToast({
|
||||||
|
title: "保存失败,请重试",
|
||||||
|
icon: "none",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRequiredFields = () => {
|
||||||
|
return currentFields.value.filter((field) => field.required);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.medical-case-form {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
padding-bottom: 120rpx;
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
background-color: #ffffff;
|
||||||
|
padding: 32rpx;
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 32rpx;
|
||||||
|
|
||||||
|
&.required .item-label::before {
|
||||||
|
content: "*";
|
||||||
|
color: #ff4d4f;
|
||||||
|
margin-right: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-input,
|
||||||
|
.picker-value {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-value {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100rpx;
|
||||||
|
padding: 20rpx 24rpx;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
height: 100px;
|
||||||
|
&[disabled] {
|
||||||
|
color: #999999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips-box {
|
||||||
|
margin-top: 32rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
background-color: #fffbe6;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
|
||||||
|
.tips-text {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666666;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-buttons {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 24rpx 32rpx;
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
.btn-regenerate,
|
||||||
|
.btn-save {
|
||||||
|
flex: 1;
|
||||||
|
height: 88rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 44rpx;
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-regenerate {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 2rpx solid #1890ff;
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
background-color: #1890ff;
|
||||||
|
|
||||||
|
.btn-text {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -2,18 +2,12 @@
|
|||||||
<view class="page">
|
<view class="page">
|
||||||
<view class="card">
|
<view class="card">
|
||||||
<view class="header">
|
<view class="header">
|
||||||
<view class="avatar">
|
|
||||||
<image v-if="archive.avatar" class="avatar-img" :src="archive.avatar" mode="aspectFill" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="header-main">
|
<view class="header-main">
|
||||||
<view class="name-row">
|
<view class="name-row">
|
||||||
<text class="name">{{ archive.name || '-' }}</text>
|
<text class="name">{{ archive.name || '-' }}</text>
|
||||||
<text v-if="sexOrAge" class="meta">{{ sexOrAge }}</text>
|
<text v-if="sexOrAge" class="meta">{{ sexOrAge }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="archive.mobile" class="sub-line">{{ archive.mobile }}</view>
|
|
||||||
|
|
||||||
<view v-if="idRows.length" class="id-rows">
|
<view v-if="idRows.length" class="id-rows">
|
||||||
<view v-for="row in idRows" :key="row.label" class="id-row">
|
<view v-for="row in idRows" :key="row.label" class="id-row">
|
||||||
<text class="id-label">{{ row.label }}:</text>
|
<text class="id-label">{{ row.label }}:</text>
|
||||||
@ -125,10 +119,10 @@
|
|||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="showBindWechat" class="footer">
|
<view v-if="showGoChat" class="footer">
|
||||||
<button class="bind-btn" @click="bindWechat">
|
<button class="bind-btn" @click="goChat">
|
||||||
<uni-icons type="email" size="18" color="#fff" />
|
<uni-icons type="chat-filled" size="18" color="#fff" />
|
||||||
<text class="bind-text">关联患者微信</text>
|
<text class="bind-text">去聊天</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@ -268,6 +262,7 @@ const archive = ref({
|
|||||||
creator: '',
|
creator: '',
|
||||||
createdByDoctor: true,
|
createdByDoctor: true,
|
||||||
hasBindWechat: false,
|
hasBindWechat: false,
|
||||||
|
chatGroupId: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
groupIds: []
|
groupIds: []
|
||||||
});
|
});
|
||||||
@ -289,6 +284,17 @@ function getCorpId() {
|
|||||||
return String(d.corpId || a.corpId || team.corpId || '') || '';
|
return String(d.corpId || a.corpId || team.corpId || '') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentTeamId() {
|
||||||
|
const team = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || {};
|
||||||
|
return String(team?.teamId || team?._id || team?.id || '') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeGroupId(v) {
|
||||||
|
const s = String(v || '').trim();
|
||||||
|
if (!s) return '';
|
||||||
|
return s.startsWith('GROUP') ? s.slice(5) : s;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeArchiveFromApi(raw) {
|
function normalizeArchiveFromApi(raw) {
|
||||||
const r = raw && typeof raw === 'object' ? raw : {};
|
const r = raw && typeof raw === 'object' ? raw : {};
|
||||||
const next = {
|
const next = {
|
||||||
@ -296,7 +302,7 @@ function normalizeArchiveFromApi(raw) {
|
|||||||
name: r.name || '',
|
name: r.name || '',
|
||||||
sex: r.sex || r.gender || '',
|
sex: r.sex || r.gender || '',
|
||||||
age: r.age ?? '',
|
age: r.age ?? '',
|
||||||
avatar: r.avatar || '',
|
avatar: r.avatar || r.avatarUrl || r.headImgUrl || r.headimgurl || r.headImageUrl || r.profilePhoto || r.photoUrl || r.photo || '',
|
||||||
mobile: r.mobile || r.phone1 || r.phone || '',
|
mobile: r.mobile || r.phone1 || r.phone || '',
|
||||||
outpatientNo: r.outpatientNo || '',
|
outpatientNo: r.outpatientNo || '',
|
||||||
inpatientNo: r.inpatientNo || '',
|
inpatientNo: r.inpatientNo || '',
|
||||||
@ -308,6 +314,7 @@ function normalizeArchiveFromApi(raw) {
|
|||||||
creator: r.creator || '',
|
creator: r.creator || '',
|
||||||
notes: r.notes || r.remark || '',
|
notes: r.notes || r.remark || '',
|
||||||
groupIds: Array.isArray(r.groupIds) ? r.groupIds : [],
|
groupIds: Array.isArray(r.groupIds) ? r.groupIds : [],
|
||||||
|
chatGroupId: normalizeGroupId(r.chatGroupId || r.groupId || r.groupID || r.imGroupId || r.imGroupID || r.consultGroupId || ''),
|
||||||
};
|
};
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
@ -336,6 +343,23 @@ async function fetchArchive() {
|
|||||||
saveToStorage();
|
saveToStorage();
|
||||||
loadTeamMembers();
|
loadTeamMembers();
|
||||||
await fetchTeamGroups(true);
|
await fetchTeamGroups(true);
|
||||||
|
chatGroupId.value = normalizeGroupId(archive.value.chatGroupId || '');
|
||||||
|
if (!chatGroupId.value) {
|
||||||
|
refreshChatRoom();
|
||||||
|
} else {
|
||||||
|
const meta = await getChatRoomMeta(chatGroupId.value);
|
||||||
|
const ok = isChatRoomForArchive(meta, archiveId.value);
|
||||||
|
const currentTeamId = getCurrentTeamId();
|
||||||
|
const metaTeamId = String(meta?.teamId || meta?.team?.teamId || meta?.team?._id || '');
|
||||||
|
if (!ok || (currentTeamId && metaTeamId && metaTeamId !== currentTeamId)) {
|
||||||
|
chatGroupId.value = '';
|
||||||
|
archive.value.chatGroupId = '';
|
||||||
|
refreshChatRoom();
|
||||||
|
} else {
|
||||||
|
archive.value.chatGroupId = chatGroupId.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveToStorage();
|
||||||
// 档案信息刷新后,tabs 的位置可能变化,重新测量
|
// 档案信息刷新后,tabs 的位置可能变化,重新测量
|
||||||
nextTick(() => setTimeout(measureTabsTop, 30));
|
nextTick(() => setTimeout(measureTabsTop, 30));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -479,16 +503,20 @@ onLoad((options) => {
|
|||||||
fromChat.value = options?.fromChat === 'true' || options?.fromChat === true;
|
fromChat.value = options?.fromChat === 'true' || options?.fromChat === true;
|
||||||
|
|
||||||
const cached = uni.getStorageSync(STORAGE_KEY);
|
const cached = uni.getStorageSync(STORAGE_KEY);
|
||||||
if (cached && typeof cached === 'object') {
|
const cachedObj = cached && typeof cached === 'object' ? cached : null;
|
||||||
|
const cachedId = cachedObj ? String(cachedObj._id || cachedObj.id || '') : '';
|
||||||
|
const canUseCached = Boolean(cachedObj && (!cachedId || !archiveId.value || cachedId === archiveId.value));
|
||||||
|
if (canUseCached) {
|
||||||
archive.value = {
|
archive.value = {
|
||||||
...archive.value,
|
...archive.value,
|
||||||
...cached,
|
...cached,
|
||||||
groupIds: Array.isArray(cached.groupIds) ? cached.groupIds : archive.value.groupIds
|
groupIds: Array.isArray(cached.groupIds) ? cached.groupIds : archive.value.groupIds
|
||||||
};
|
};
|
||||||
|
chatGroupId.value = normalizeGroupId(archive.value.chatGroupId || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!archive.value.mobile) {
|
if (!archive.value.mobile) {
|
||||||
const mobiles = cached && Array.isArray(cached.mobiles) ? cached.mobiles : [];
|
const mobiles = canUseCached && cachedObj && Array.isArray(cachedObj.mobiles) ? cachedObj.mobiles : [];
|
||||||
if (mobiles.length) archive.value.mobile = String(mobiles[0]);
|
if (mobiles.length) archive.value.mobile = String(mobiles[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -518,12 +546,14 @@ onReady(() => {
|
|||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
const cached = uni.getStorageSync(STORAGE_KEY);
|
const cached = uni.getStorageSync(STORAGE_KEY);
|
||||||
if (cached && typeof cached === 'object') {
|
const cachedId = cached && typeof cached === 'object' ? String(cached._id || cached.id || '') : '';
|
||||||
|
if (cached && typeof cached === 'object' && (!cachedId || !archiveId.value || cachedId === archiveId.value)) {
|
||||||
archive.value = {
|
archive.value = {
|
||||||
...archive.value,
|
...archive.value,
|
||||||
...cached,
|
...cached,
|
||||||
groupIds: Array.isArray(cached.groupIds) ? cached.groupIds : archive.value.groupIds,
|
groupIds: Array.isArray(cached.groupIds) ? cached.groupIds : archive.value.groupIds,
|
||||||
};
|
};
|
||||||
|
chatGroupId.value = normalizeGroupId(archive.value.chatGroupId || '');
|
||||||
}
|
}
|
||||||
setTimeout(measureTabsTop, 30);
|
setTimeout(measureTabsTop, 30);
|
||||||
fetchArchive();
|
fetchArchive();
|
||||||
@ -562,12 +592,16 @@ const createText = computed(() => {
|
|||||||
const creatorId = ['-', '—', '--'].includes(rawCreator.trim()) ? '' : rawCreator.trim();
|
const creatorId = ['-', '—', '--'].includes(rawCreator.trim()) ? '' : rawCreator.trim();
|
||||||
const creatorName = creatorId ? resolveUserName(creatorId) : '';
|
const creatorName = creatorId ? resolveUserName(creatorId) : '';
|
||||||
if (time && creatorName) return `${time} ${creatorName}创建`;
|
if (time && creatorName) return `${time} ${creatorName}创建`;
|
||||||
|
if (time && !rawCreator.trim()) return `${time} 患者创建`;
|
||||||
if (time) return `${time} 创建`;
|
if (time) return `${time} 创建`;
|
||||||
|
if (!rawCreator.trim()) return '患者创建';
|
||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
const showBindWechat = computed(() => Boolean(archive.value.createdByDoctor && !archive.value.hasBindWechat));
|
const chatGroupId = ref('');
|
||||||
const floatingBottom = computed(() => (showBindWechat.value ? 90 : 16));
|
const currentChatGroupId = computed(() => normalizeGroupId(chatGroupId.value || ''));
|
||||||
|
const showGoChat = computed(() => Boolean(currentChatGroupId.value));
|
||||||
|
const floatingBottom = computed(() => (showGoChat.value ? 90 : 16));
|
||||||
|
|
||||||
// const contactTitle = computed(() => (archive.value.mobile ? '联系方式' : '添加联系电话'));
|
// const contactTitle = computed(() => (archive.value.mobile ? '联系方式' : '添加联系电话'));
|
||||||
// const notesTitle = computed(() => (archive.value.notes ? '备注' : '添加备注'));
|
// const notesTitle = computed(() => (archive.value.notes ? '备注' : '添加备注'));
|
||||||
@ -581,10 +615,140 @@ const goEdit = () => {
|
|||||||
uni.navigateTo({ url: `/pages/case/archive-edit?archiveId=${encodeURIComponent(archiveId.value || '')}` });
|
uni.navigateTo({ url: `/pages/case/archive-edit?archiveId=${encodeURIComponent(archiveId.value || '')}` });
|
||||||
};
|
};
|
||||||
|
|
||||||
const bindWechat = () => {
|
const goChat = async () => {
|
||||||
uni.showToast({ title: '关联患者微信功能待接入', icon: 'none' });
|
let gid = normalizeGroupId(currentChatGroupId.value || '');
|
||||||
|
if (!gid) {
|
||||||
|
await refreshChatRoom();
|
||||||
|
gid = normalizeGroupId(currentChatGroupId.value || '');
|
||||||
|
}
|
||||||
|
if (!gid) {
|
||||||
|
toast('暂无可进入的会话');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = await getChatRoomMeta(gid);
|
||||||
|
const ok = isChatRoomForArchive(meta, archiveId.value);
|
||||||
|
const currentTeamId = getCurrentTeamId();
|
||||||
|
const metaTeamId = String(meta?.teamId || meta?.team?.teamId || meta?.team?._id || '');
|
||||||
|
if (!ok || (currentTeamId && metaTeamId && metaTeamId !== currentTeamId)) {
|
||||||
|
chatGroupId.value = '';
|
||||||
|
archive.value.chatGroupId = '';
|
||||||
|
saveToStorage();
|
||||||
|
await refreshChatRoom();
|
||||||
|
gid = normalizeGroupId(currentChatGroupId.value || '');
|
||||||
|
}
|
||||||
|
if (!gid) {
|
||||||
|
toast('暂无可进入的会话');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const conversationID = `GROUP${gid}`;
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/message/index?conversationID=${encodeURIComponent(conversationID)}&groupID=${encodeURIComponent(gid)}&fromCase=true`,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isRefreshingChatRoom = ref(false);
|
||||||
|
let lastRefreshChatRoomAt = 0;
|
||||||
|
|
||||||
|
function isChatRoomForArchive(meta, customerId) {
|
||||||
|
const cid = String(meta?.customerId || '');
|
||||||
|
const pid = String(meta?.patientId || '');
|
||||||
|
const id = String(customerId || '');
|
||||||
|
if (!meta || !id) return false;
|
||||||
|
return (cid && cid === id) || (pid && pid === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getChatRoomMeta(groupId) {
|
||||||
|
const gid = normalizeGroupId(groupId || '');
|
||||||
|
if (!gid) return null;
|
||||||
|
try {
|
||||||
|
const res = await api('getGroupListByGroupId', { groupId: gid }, false);
|
||||||
|
if (!res?.success || !res?.data) return null;
|
||||||
|
return res.data;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAnyTimeMs(v) {
|
||||||
|
if (v === null || v === undefined) return 0;
|
||||||
|
if (typeof v === 'number') return v;
|
||||||
|
const s = String(v).trim();
|
||||||
|
if (!s) return 0;
|
||||||
|
if (/^\d{10,13}$/.test(s)) return Number(s.length === 10 ? `${s}000` : s);
|
||||||
|
const d = dayjs(s);
|
||||||
|
return d.isValid() ? d.valueOf() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshChatRoom() {
|
||||||
|
const customerId = String(archiveId.value || '');
|
||||||
|
if (!customerId) return;
|
||||||
|
if (isRefreshingChatRoom.value) return;
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastRefreshChatRoomAt < 5000) return;
|
||||||
|
lastRefreshChatRoomAt = now;
|
||||||
|
|
||||||
|
isRefreshingChatRoom.value = true;
|
||||||
|
try {
|
||||||
|
const corpId = getCorpId();
|
||||||
|
const teamId = getCurrentTeamId();
|
||||||
|
|
||||||
|
const baseQuery = {
|
||||||
|
corpId,
|
||||||
|
customerId,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryWithTeam = teamId ? { ...baseQuery, teamId } : baseQuery;
|
||||||
|
let detailRes = await api('getGroupList', queryWithTeam, false);
|
||||||
|
let details = Array.isArray(detailRes?.data?.list) ? detailRes.data.list : [];
|
||||||
|
|
||||||
|
if (!details.length && teamId) {
|
||||||
|
detailRes = await api('getGroupList', baseQuery, false);
|
||||||
|
details = Array.isArray(detailRes?.data?.list) ? detailRes.data.list : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detailRes?.success || !details.length) {
|
||||||
|
chatGroupId.value = '';
|
||||||
|
archive.value.chatGroupId = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentTeamId = getCurrentTeamId();
|
||||||
|
const detailsForCurrentTeam = currentTeamId
|
||||||
|
? details.filter((g) => String(g?.teamId || g?.team?._id || g?.team?.teamId || '') === currentTeamId)
|
||||||
|
: [];
|
||||||
|
const candidates = detailsForCurrentTeam.length ? detailsForCurrentTeam : details;
|
||||||
|
|
||||||
|
const statusRank = (s) => (s === 'processing' ? 3 : s === 'pending' ? 2 : 1);
|
||||||
|
candidates.sort((a, b) => {
|
||||||
|
const ra = statusRank(String(a?.orderStatus || ''));
|
||||||
|
const rb = statusRank(String(b?.orderStatus || ''));
|
||||||
|
if (rb !== ra) return rb - ra;
|
||||||
|
const ta = parseAnyTimeMs(a?.updatedAt) || parseAnyTimeMs(a?.createdAt);
|
||||||
|
const tb = parseAnyTimeMs(b?.updatedAt) || parseAnyTimeMs(b?.createdAt);
|
||||||
|
return tb - ta;
|
||||||
|
});
|
||||||
|
|
||||||
|
const best = candidates[0] || {};
|
||||||
|
const gid = normalizeGroupId(best.groupId || best.groupID || best.group_id || '');
|
||||||
|
if (gid) {
|
||||||
|
chatGroupId.value = String(gid);
|
||||||
|
archive.value.chatGroupId = chatGroupId.value;
|
||||||
|
saveToStorage();
|
||||||
|
} else {
|
||||||
|
chatGroupId.value = '';
|
||||||
|
archive.value.chatGroupId = '';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
isRefreshingChatRoom.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const makeCall = () => {
|
const makeCall = () => {
|
||||||
if (archive.value.mobile) {
|
if (archive.value.mobile) {
|
||||||
uni.makePhoneCall({ phoneNumber: archive.value.mobile });
|
uni.makePhoneCall({ phoneNumber: archive.value.mobile });
|
||||||
@ -778,25 +942,10 @@ const saveAddGroup = async () => {
|
|||||||
border-bottom: 2rpx solid #f2f2f2;
|
border-bottom: 2rpx solid #f2f2f2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 112rpx;
|
|
||||||
height: 112rpx;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
border: 2rpx solid #e8e8e8;
|
|
||||||
background: #fafafa;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-img {
|
|
||||||
width: 112rpx;
|
|
||||||
height: 112rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-main {
|
.header-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0 20rpx;
|
padding: 0 20rpx 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.name-row {
|
.name-row {
|
||||||
|
|||||||
@ -1,37 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="transfer-container">
|
<view class="transfer-container">
|
||||||
<view class="content">
|
<view class="content">
|
||||||
<view class="section-title">选择新负责团队</view>
|
<view class="tips">处理中...</view>
|
||||||
<view class="selector-item" @click="selectTeam">
|
|
||||||
<text :class="team ? '' : 'placeholder'">{{ team ? team.name : '请选择团队' }}</text>
|
|
||||||
<uni-icons type="arrowdown" size="16" color="#999" />
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<template v-if="team">
|
|
||||||
<view class="section-title">选择责任人</view>
|
|
||||||
<view class="selector-item" @click="selectUser">
|
|
||||||
<text :class="userId ? '' : 'placeholder'">{{ userLabel || '请选择责任人' }}</text>
|
|
||||||
<uni-icons type="arrowdown" size="16" color="#999" />
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<view class="tips">客户将与本团队解除服务关系,本团队成员将没有权限查询到客户档案。</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="footer">
|
|
||||||
<button class="btn plain" @click="cancel">取消</button>
|
|
||||||
<button class="btn primary" @click="save">保存</button>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import useAccountStore from '@/store/account';
|
import useAccountStore from '@/store/account';
|
||||||
import { hideLoading, loading, toast } from '@/utils/widget';
|
import { confirm as uniConfirm, hideLoading, loading as showLoading, toast } from '@/utils/widget';
|
||||||
|
|
||||||
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
|
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
|
||||||
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
|
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
|
||||||
@ -44,15 +25,6 @@ const { getDoctorInfo } = accountStore;
|
|||||||
const customerIds = ref([]);
|
const customerIds = ref([]);
|
||||||
const currentTeam = ref(null);
|
const currentTeam = ref(null);
|
||||||
const teams = ref([]);
|
const teams = ref([]);
|
||||||
const team = ref(null);
|
|
||||||
const teamMembers = ref([]);
|
|
||||||
const userId = ref('');
|
|
||||||
|
|
||||||
const userLabel = computed(() => {
|
|
||||||
const list = Array.isArray(teamMembers.value) ? teamMembers.value : [];
|
|
||||||
const found = list.find((m) => String(m?.userid || '') === String(userId.value));
|
|
||||||
return found ? String(found.anotherName || found.name || found.userid || '') : '';
|
|
||||||
});
|
|
||||||
|
|
||||||
function getUserId() {
|
function getUserId() {
|
||||||
const d = doctorInfo.value || {};
|
const d = doctorInfo.value || {};
|
||||||
@ -71,6 +43,21 @@ function getCurrentTeamId() {
|
|||||||
return String(currentTeam.value?.teamId || '') || '';
|
return String(currentTeam.value?.teamId || '') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showActionSheet(itemList = [], title = '') {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!Array.isArray(itemList) || itemList.length === 0) {
|
||||||
|
reject(new Error('empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.showActionSheet({
|
||||||
|
title,
|
||||||
|
itemList,
|
||||||
|
success: ({ tapIndex }) => resolve(tapIndex),
|
||||||
|
fail: () => reject(new Error('cancel')),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeTeam(raw) {
|
function normalizeTeam(raw) {
|
||||||
if (!raw || typeof raw !== 'object') return null;
|
if (!raw || typeof raw !== 'object') return null;
|
||||||
const teamId = raw.teamId || raw.id || raw._id || '';
|
const teamId = raw.teamId || raw.id || raw._id || '';
|
||||||
@ -94,114 +81,174 @@ async function loadTeams() {
|
|||||||
await ensureDoctor();
|
await ensureDoctor();
|
||||||
const corpId = getCorpId();
|
const corpId = getCorpId();
|
||||||
const userId = getUserId();
|
const userId = getUserId();
|
||||||
if (!corpId || !userId) return;
|
if (!corpId || !userId) return [];
|
||||||
const res = await api('getTeamBymember', { corpId, corpUserId: userId });
|
const res = await api('getTeamBymember', { corpId, corpUserId: userId });
|
||||||
if (!res?.success) {
|
if (!res?.success) {
|
||||||
toast(res?.message || '获取团队失败');
|
toast(res?.message || '获取团队失败');
|
||||||
return;
|
return [];
|
||||||
}
|
}
|
||||||
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
|
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
|
||||||
teams.value = list.map(normalizeTeam).filter(Boolean);
|
teams.value = list.map(normalizeTeam).filter(Boolean);
|
||||||
|
return teams.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTeamMembers(teamId) {
|
async function fetchTeamMembers(teamId) {
|
||||||
const corpId = getCorpId();
|
const corpId = getCorpId();
|
||||||
if (!teamId) return;
|
if (!teamId) return [];
|
||||||
const res = await api('getTeamData', { corpId, teamId });
|
const res = await api('getTeamData', { corpId, teamId });
|
||||||
if (!res?.success) {
|
if (!res?.success) {
|
||||||
toast(res?.message || '获取团队成员失败');
|
toast(res?.message || '获取团队成员失败');
|
||||||
teamMembers.value = [];
|
return [];
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const t = res?.data && typeof res.data === 'object' ? res.data : {};
|
const t = res?.data && typeof res.data === 'object' ? res.data : {};
|
||||||
teamMembers.value = Array.isArray(t.memberList) ? t.memberList : [];
|
return Array.isArray(t.memberList) ? t.memberList : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectTeam = async () => {
|
async function transferToCustomerPool() {
|
||||||
if (!teams.value.length) await loadTeams();
|
try {
|
||||||
const currentId = getCurrentTeamId();
|
await uniConfirm('客户将与本团队解除服务关系,本团队成员将没有权限查询到客户档案。');
|
||||||
const candidates = teams.value.filter((t) => t.teamId !== currentId);
|
} catch (e) {
|
||||||
if (!candidates.length) {
|
return false;
|
||||||
toast('暂无可选团队');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
uni.showActionSheet({
|
|
||||||
itemList: candidates.map((t) => t.name),
|
|
||||||
success: async (res) => {
|
|
||||||
team.value = candidates[res.tapIndex] || null;
|
|
||||||
userId.value = '';
|
|
||||||
teamMembers.value = [];
|
|
||||||
if (team.value) await loadTeamMembers(team.value.teamId);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectUser = () => {
|
|
||||||
const list = Array.isArray(teamMembers.value) ? teamMembers.value : [];
|
|
||||||
if (!list.length) {
|
|
||||||
toast('当前团队暂无可选成员');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const labels = list.map((m) => String(m?.anotherName || m?.name || m?.userid || ''));
|
|
||||||
uni.showActionSheet({
|
|
||||||
itemList: labels,
|
|
||||||
success: (res) => {
|
|
||||||
const picked = list[res.tapIndex];
|
|
||||||
userId.value = picked?.userid ? String(picked.userid) : '';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const cancel = () => {
|
|
||||||
uni.navigateBack();
|
|
||||||
};
|
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
if (!team.value) return toast('请选择团队');
|
|
||||||
if (!userId.value) return toast('请选择负责人');
|
|
||||||
|
|
||||||
const corpId = getCorpId();
|
const corpId = getCorpId();
|
||||||
const currentTeamId = getCurrentTeamId();
|
const currentTeamId = getCurrentTeamId();
|
||||||
const creatorUserId = getUserId();
|
const creatorUserId = getUserId();
|
||||||
if (!corpId || !currentTeamId || !creatorUserId) return toast('缺少用户/团队信息');
|
if (!corpId || !currentTeamId || !creatorUserId) {
|
||||||
|
toast('缺少用户/团队信息');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
loading('保存中...');
|
showLoading('保存中...');
|
||||||
try {
|
try {
|
||||||
const res = await api('transferCustomers', {
|
const res = await api('transferCustomers', {
|
||||||
corpId,
|
corpId,
|
||||||
customerIds: customerIds.value,
|
customerIds: customerIds.value,
|
||||||
currentTeamId,
|
currentTeamId,
|
||||||
targetTeamId: team.value.teamId,
|
operationType: 'transferToCustomerPool',
|
||||||
targetUserId: userId.value,
|
creatorUserId,
|
||||||
|
});
|
||||||
|
if (!res?.success) {
|
||||||
|
toast(res?.message || '操作失败');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
toast('操作成功');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
toast('操作失败');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transferToOtherTeam() {
|
||||||
|
const corpId = getCorpId();
|
||||||
|
const currentTeamId = getCurrentTeamId();
|
||||||
|
const creatorUserId = getUserId();
|
||||||
|
if (!corpId || !currentTeamId || !creatorUserId) {
|
||||||
|
toast('缺少用户/团队信息');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!teams.value.length) await loadTeams();
|
||||||
|
const candidates = teams.value.filter((t) => String(t?.teamId || '') !== String(currentTeamId));
|
||||||
|
if (!candidates.length) {
|
||||||
|
toast('暂无可选团队');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let teamIndex;
|
||||||
|
try {
|
||||||
|
teamIndex = await showActionSheet(candidates.map((t) => t.name || ''), '选择新负责团队');
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const pickedTeam = candidates[teamIndex];
|
||||||
|
if (!pickedTeam?.teamId) {
|
||||||
|
toast('团队信息异常');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const members = await fetchTeamMembers(pickedTeam.teamId);
|
||||||
|
if (!members.length) {
|
||||||
|
toast('当前团队暂无可选成员');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let userIndex;
|
||||||
|
try {
|
||||||
|
userIndex = await showActionSheet(
|
||||||
|
members.map((m) => String(m?.anotherName || m?.name || m?.userid || '')),
|
||||||
|
'选择责任人'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUserId = String(members[userIndex]?.userid || '') || '';
|
||||||
|
if (!targetUserId) {
|
||||||
|
toast('责任人信息异常');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading('保存中...');
|
||||||
|
try {
|
||||||
|
const res = await api('transferCustomers', {
|
||||||
|
corpId,
|
||||||
|
customerIds: customerIds.value,
|
||||||
|
currentTeamId,
|
||||||
|
targetTeamId: String(pickedTeam.teamId),
|
||||||
|
targetUserId,
|
||||||
operationType: 'transferToOtherTeam',
|
operationType: 'transferToOtherTeam',
|
||||||
creatorUserId,
|
creatorUserId,
|
||||||
});
|
});
|
||||||
if (!res?.success) {
|
if (!res?.success) {
|
||||||
toast(res?.message || '操作失败');
|
toast(res?.message || '操作失败');
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
toast('操作成功');
|
toast('操作成功');
|
||||||
uni.removeStorageSync(BATCH_CUSTOMER_IDS_KEY);
|
return true;
|
||||||
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
|
|
||||||
uni.navigateBack();
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast('操作失败');
|
toast('操作失败');
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
onLoad(async () => {
|
async function runFlow(options = {}) {
|
||||||
|
let mode = options?.mode === 'pool' ? 'pool' : options?.mode === 'team' ? 'team' : '';
|
||||||
|
if (!mode) {
|
||||||
|
let pick;
|
||||||
|
try {
|
||||||
|
pick = await showActionSheet(['转移给其他团队', '转移至客户公共池']);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mode = pick === 0 ? 'team' : 'pool';
|
||||||
|
}
|
||||||
|
return mode === 'team' ? await transferToOtherTeam() : await transferToCustomerPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(async (options = {}) => {
|
||||||
customerIds.value = Array.isArray(uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY))
|
customerIds.value = Array.isArray(uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY))
|
||||||
? uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY).map(String).filter(Boolean)
|
? uni.getStorageSync(BATCH_CUSTOMER_IDS_KEY).map(String).filter(Boolean)
|
||||||
: [];
|
: [];
|
||||||
currentTeam.value = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || null;
|
currentTeam.value = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY) || null;
|
||||||
|
|
||||||
if (!customerIds.value.length) {
|
if (!customerIds.value.length) {
|
||||||
toast('未选择客户');
|
toast('未选择客户');
|
||||||
setTimeout(() => uni.navigateBack(), 200);
|
setTimeout(() => uni.navigateBack(), 200);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await loadTeams();
|
|
||||||
|
const ok = await runFlow(options);
|
||||||
|
if (ok) {
|
||||||
|
uni.removeStorageSync(BATCH_CUSTOMER_IDS_KEY);
|
||||||
|
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
|
||||||
|
}
|
||||||
|
setTimeout(() => uni.navigateBack(), 50);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -40,13 +40,6 @@
|
|||||||
<image class="pen" src="/static/icons/icon-pen.svg" />
|
<image class="pen" src="/static/icons/icon-pen.svg" />
|
||||||
</view>
|
</view>
|
||||||
<view class="rows">
|
<view class="rows">
|
||||||
<view class="row" @click="openTransferRecord">
|
|
||||||
<view class="label">院内来源</view>
|
|
||||||
<view class="val link">
|
|
||||||
{{ latestTransferRecord?.executeTeamName || '点击查看' }}
|
|
||||||
<uni-icons type="arrowright" size="14" color="#0877F1" />
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<form-template
|
<form-template
|
||||||
v-if="editing && effectiveInternalItems.length"
|
v-if="editing && effectiveInternalItems.length"
|
||||||
ref="internalFormRef"
|
ref="internalFormRef"
|
||||||
@ -148,7 +141,15 @@ const fallbackInternalItems = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const effectiveBaseItems = computed(() => (Array.isArray(props.baseItems) && props.baseItems.length ? props.baseItems : fallbackBaseItems));
|
const effectiveBaseItems = computed(() => (Array.isArray(props.baseItems) && props.baseItems.length ? props.baseItems : fallbackBaseItems));
|
||||||
const effectiveInternalItems = computed(() => (Array.isArray(props.internalItems) && props.internalItems.length ? props.internalItems : fallbackInternalItems));
|
function isInHospitalSourceItem(item) {
|
||||||
|
const title = String(item?.title || '');
|
||||||
|
const name = String(item?.name || '');
|
||||||
|
return title === 'inHospitalSource' || title === 'hospitalSource' || name.includes('院内来源');
|
||||||
|
}
|
||||||
|
const effectiveInternalItems = computed(() => {
|
||||||
|
const raw = Array.isArray(props.internalItems) && props.internalItems.length ? props.internalItems : fallbackInternalItems;
|
||||||
|
return raw.filter((i) => i && !isInHospitalSourceItem(i));
|
||||||
|
});
|
||||||
|
|
||||||
const filterRule = {
|
const filterRule = {
|
||||||
reference(formModel) {
|
reference(formModel) {
|
||||||
|
|||||||
@ -90,11 +90,13 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<button
|
<button
|
||||||
v-if="fromChat"
|
v-if="fromChat && isExecutor(i)"
|
||||||
class="action-btn send-btn"
|
class="action-btn send-btn"
|
||||||
|
:class="{ loading: sendingFollowUp }"
|
||||||
|
:disabled="sendingFollowUp"
|
||||||
@click.stop="sendFollowUp(i)"
|
@click.stop="sendFollowUp(i)"
|
||||||
>
|
>
|
||||||
发送
|
{{ sendingFollowUp ? "发送中..." : "发送" }}
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="i.status === 'treated'" class="result"
|
<view v-if="i.status === 'treated'" class="result"
|
||||||
@ -238,6 +240,12 @@ function getUserId() {
|
|||||||
return doctorInfo.value?.userid || "";
|
return doctorInfo.value?.userid || "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isExecutor(todo) {
|
||||||
|
const currentUserId = getUserId();
|
||||||
|
const executorUserId = String(todo?.executorUserId || "");
|
||||||
|
return currentUserId && executorUserId && currentUserId === executorUserId;
|
||||||
|
}
|
||||||
|
|
||||||
function getCorpId() {
|
function getCorpId() {
|
||||||
const team = uni.getStorageSync("ykt_case_current_team") || {};
|
const team = uni.getStorageSync("ykt_case_current_team") || {};
|
||||||
return team.corpId || doctorInfo.value?.corpId || "";
|
return team.corpId || doctorInfo.value?.corpId || "";
|
||||||
@ -276,6 +284,7 @@ const pages = ref(1);
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const userNameMap = ref({});
|
const userNameMap = ref({});
|
||||||
|
const sendingFollowUp = ref(false);
|
||||||
|
|
||||||
const moreStatus = computed(() => {
|
const moreStatus = computed(() => {
|
||||||
if (loading.value) return "loading";
|
if (loading.value) return "loading";
|
||||||
@ -347,7 +356,6 @@ function eventTypeLabel(eventType) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveUserName(userId) {
|
function resolveUserName(userId) {
|
||||||
const id = String(userId || "");
|
|
||||||
if (!id) return "";
|
if (!id) return "";
|
||||||
const map = userNameMap.value || {};
|
const map = userNameMap.value || {};
|
||||||
return String(map[id] || "") || id;
|
return String(map[id] || "") || id;
|
||||||
@ -512,73 +520,88 @@ function toDetail(todo) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function sendFollowUp(todo) {
|
async function sendFollowUp(todo) {
|
||||||
|
if (sendingFollowUp.value) {
|
||||||
|
toast("正在发送中,请稍候...");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!todo.sendContent && (!todo.fileList || todo.fileList.length === 0)) {
|
if (!todo.sendContent && (!todo.fileList || todo.fileList.length === 0)) {
|
||||||
toast("没有发送内容");
|
toast("没有发送内容");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = [];
|
sendingFollowUp.value = true;
|
||||||
|
try {
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
// 1. 发送文字内容
|
// 1. 发送文字内容
|
||||||
if (todo.sendContent) {
|
if (todo.sendContent) {
|
||||||
messages.push({
|
messages.push({
|
||||||
type: "text",
|
type: "text",
|
||||||
content: todo.sendContent,
|
content: todo.sendContent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log("==============>fileList", todo.fileList);
|
console.log("==============>fileList", todo.fileList);
|
||||||
|
|
||||||
// 2. 处理文件列表(图片、宣教文章、问卷)
|
// 2. 处理文件列表(图片、宣教文章、问卷)
|
||||||
if (Array.isArray(todo.fileList)) {
|
if (Array.isArray(todo.fileList)) {
|
||||||
for (const file of todo.fileList) {
|
for (const file of todo.fileList) {
|
||||||
if (file.type === "image" && file.URL) {
|
if (file.type === "image" && file.URL) {
|
||||||
// 发送图片
|
// 发送图片
|
||||||
messages.push({
|
messages.push({
|
||||||
type: "image",
|
type: "image",
|
||||||
content: file.URL,
|
content: file.URL,
|
||||||
name: file.file?.name || file.name || "图片",
|
name: file.file?.name || file.name || "图片",
|
||||||
});
|
});
|
||||||
} else if (file.file.type === "article" && file.file?.url) {
|
} else if (file.file.type === "article" && file.file?.url) {
|
||||||
// 发送宣教文章 - 从 URL 中解析 id
|
// 发送宣教文章 - 从 URL 中解析 id
|
||||||
const articleId = extractIdFromUrl(file.file.url);
|
const articleId = extractIdFromUrl(file.file.url);
|
||||||
messages.push({
|
messages.push({
|
||||||
type: "article",
|
type: "article",
|
||||||
content: {
|
content: {
|
||||||
_id: articleId,
|
_id: articleId,
|
||||||
title: file.file?.name || "宣教文章",
|
title: file.file?.name || "宣教文章",
|
||||||
url: file.file?.url || file.URL,
|
url: file.file?.url || file.URL,
|
||||||
subtitle: file.file?.subtitle || "",
|
subtitle: file.file?.subtitle || "",
|
||||||
cover: file.file?.cover || "",
|
cover: file.file?.cover || "",
|
||||||
articleId: articleId,
|
articleId: articleId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (file.file.type === "questionnaire" && file.file?.surveryId) {
|
} else if (
|
||||||
// 发送问卷
|
file.file.type === "questionnaire" &&
|
||||||
messages.push({
|
(file.file?.url || file.URL)
|
||||||
type: "questionnaire",
|
) {
|
||||||
content: {
|
// 发送问卷 - 从 URL 中解析 surveryId
|
||||||
_id: file.file?._id || file._id,
|
const surveryUrl = file.file?.url || file.URL;
|
||||||
name: file.file?.name || file.name || "问卷",
|
const surveryId = extractSurveryIdFromUrl(surveryUrl);
|
||||||
surveryId: file.file?.surveryId || file.surveryId,
|
messages.push({
|
||||||
url: file.file?.url || file.URL,
|
type: "questionnaire",
|
||||||
},
|
content: {
|
||||||
});
|
_id: surveryId,
|
||||||
|
name: file.file?.name || file.name || "问卷",
|
||||||
|
surveryId: surveryId,
|
||||||
|
url: surveryUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 调用统一的消息发送处理函数
|
// 调用统一的消息发送处理函数
|
||||||
const success = await handleFollowUpMessages(messages, {
|
const success = await handleFollowUpMessages(messages, {
|
||||||
userId: getUserId(),
|
userId: getUserId(),
|
||||||
customerId: props.archiveId,
|
customerId: props.archiveId,
|
||||||
customerName: props.data?.name || "",
|
customerName: props.data?.name || "",
|
||||||
corpId: getCorpId(),
|
corpId: getCorpId(),
|
||||||
env: __VITE_ENV__,
|
env: __VITE_ENV__,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
toast("消息已发送");
|
toast("消息已发送");
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
sendingFollowUp.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -606,6 +629,24 @@ function extractIdFromUrl(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从问卷 URL 中提取 surveryId 参数
|
||||||
|
* @param {string} url - 完整的 URL,格式如: https://www.youcan365.com/patientDeploy/#/pages/survery/fill?corpId=wwe3fb2faa52cf9dfb&surveryId=9ji5kg2oa9x52oyg9w4rj5k81769510562099
|
||||||
|
* @returns {string} 提取出的 surveryId 值
|
||||||
|
*/
|
||||||
|
function extractSurveryIdFromUrl(url) {
|
||||||
|
if (!url) return "";
|
||||||
|
try {
|
||||||
|
// 使用正则表达式提取 surveryId 参数
|
||||||
|
// 处理格式: ?surveryId=xxx 或 &surveryId=xxx
|
||||||
|
const match = url.match(/[?&]surveryId=([^&]+)/);
|
||||||
|
return match ? decodeURIComponent(match[1]) : "";
|
||||||
|
} catch (error) {
|
||||||
|
console.error("解析问卷 URL 失败:", error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- filter popup ----
|
// ---- filter popup ----
|
||||||
const filterPopupRef = ref(null);
|
const filterPopupRef = ref(null);
|
||||||
const state = ref(null);
|
const state = ref(null);
|
||||||
@ -976,6 +1017,14 @@ watch(
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.send-btn.loading {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
padding: 120px 0;
|
padding: 120px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@ -49,18 +49,30 @@
|
|||||||
<view v-if="records.length === 0" class="empty">暂无数据</view>
|
<view v-if="records.length === 0" class="empty">暂无数据</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="fab" :style="{ bottom: `${floatingBottom}px` }" @click="add">
|
<picker
|
||||||
<uni-icons type="plusempty" size="24" color="#fff" />
|
class="fab-picker"
|
||||||
</view>
|
mode="selector"
|
||||||
|
:range="selectableTemplates"
|
||||||
|
range-key="name"
|
||||||
|
:disabled="fabPickerDisabled"
|
||||||
|
:style="{ bottom: `${floatingBottom}px` }"
|
||||||
|
@change="pickAddType"
|
||||||
|
>
|
||||||
|
<view class="fab" :class="{ 'fab--disabled': selectableTemplates.length === 0 }" @tap="onFabTap">
|
||||||
|
<uni-icons type="plusempty" size="24" color="#fff" />
|
||||||
|
</view>
|
||||||
|
</picker>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { VISIT_RECORD_TEMPLATES } from './templates';
|
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import { loading, hideLoading } from '@/utils/widget';
|
import { loading, hideLoading, toast } from '@/utils/widget';
|
||||||
|
import { normalizeTemplate } from '../../utils/template';
|
||||||
|
import { normalizeVisitRecordFormData } from '../../utils/visit-record';
|
||||||
|
import { normalizeFileUrl } from '@/utils/file';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: { type: Object, default: () => ({}) },
|
data: { type: Object, default: () => ({}) },
|
||||||
@ -68,7 +80,16 @@ const props = defineProps({
|
|||||||
floatingBottom: { type: Number, default: 16 },
|
floatingBottom: { type: Number, default: 16 },
|
||||||
});
|
});
|
||||||
|
|
||||||
const templates = ref(VISIT_RECORD_TEMPLATES.map(t => ({ name: t.templateName, templateType: t.templateType })));
|
const FALLBACK_TEMPLATE_TYPES = ['outpatient', 'inhospital', 'preConsultation', 'physicalExaminationTemplate'];
|
||||||
|
const templates = ref([]);
|
||||||
|
const selectableTemplates = computed(() => templates.value.filter((i) => i && i.templateType && typeof i.name === 'string' && i.name.trim()));
|
||||||
|
const useActionSheet = computed(() => selectableTemplates.value.length > 0 && selectableTemplates.value.length <= 6);
|
||||||
|
const fabPickerDisabled = computed(() => selectableTemplates.value.length === 0 || useActionSheet.value);
|
||||||
|
const templateMap = computed(() => templates.value.reduce((m, t) => {
|
||||||
|
if (t?.templateType) m[String(t.templateType)] = t;
|
||||||
|
return m;
|
||||||
|
}, {}));
|
||||||
|
const availableTypes = computed(() => (templates.value.length ? templates.value.map((i) => i.templateType) : FALLBACK_TEMPLATE_TYPES));
|
||||||
|
|
||||||
const typeRange = computed(() => [{ name: '全部', value: 'ALL' }, ...templates.value.map((t) => ({ name: t.name, value: t.templateType }))]);
|
const typeRange = computed(() => [{ name: '全部', value: 'ALL' }, ...templates.value.map((t) => ({ name: t.name, value: t.templateType }))]);
|
||||||
const currentType = ref({ name: '全部', value: 'ALL' });
|
const currentType = ref({ name: '全部', value: 'ALL' });
|
||||||
@ -108,6 +129,72 @@ function getCorpId() {
|
|||||||
return team?.corpId ? String(team.corpId) : '';
|
return team?.corpId ? String(team.corpId) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadedCorpId = ref('');
|
||||||
|
let loadVisitTemplatesPromise = null;
|
||||||
|
let loadVisitTemplatesCorpId = '';
|
||||||
|
async function loadVisitTemplates() {
|
||||||
|
const corpId = getCorpId();
|
||||||
|
if (!corpId) return;
|
||||||
|
if (loadedCorpId.value === corpId && templates.value.length) return;
|
||||||
|
if (loadVisitTemplatesPromise && loadVisitTemplatesCorpId === corpId) return loadVisitTemplatesPromise;
|
||||||
|
|
||||||
|
loadVisitTemplatesCorpId = corpId;
|
||||||
|
loadVisitTemplatesPromise = (async () => {
|
||||||
|
const groupRes = await api('getTemplateGroup', { corpId, parentType: 'medicalRecord' });
|
||||||
|
const group = groupRes?.data && Array.isArray(groupRes.data?.data) ? groupRes.data.data : Array.isArray(groupRes?.data) ? groupRes.data : [];
|
||||||
|
const list = Array.isArray(group) ? group : [];
|
||||||
|
const groupNameMap = list.reduce((m, i) => {
|
||||||
|
const t = i?.templateType ? String(i.templateType) : '';
|
||||||
|
const name = i?.name ? String(i.name) : '';
|
||||||
|
if (t && name) m[t] = name;
|
||||||
|
return m;
|
||||||
|
}, {});
|
||||||
|
const enabled = list.filter((i) => i && i.templateType !== 'healthTemplate' && i.templateStatus !== 'disable');
|
||||||
|
const typeList = enabled.map((i) => String(i.templateType || '')).filter(Boolean);
|
||||||
|
if (!typeList.length) return;
|
||||||
|
|
||||||
|
const detailRes = await api('getTemplateListByTemptype', { corpId, templateTypeList: typeList });
|
||||||
|
const detail = detailRes?.data && Array.isArray(detailRes.data?.data) ? detailRes.data.data : Array.isArray(detailRes?.data) ? detailRes.data : [];
|
||||||
|
const temps = Array.isArray(detail) ? detail : [];
|
||||||
|
const byType = temps.reduce((m, t) => {
|
||||||
|
const k = t?.templateType ? String(t.templateType) : '';
|
||||||
|
if (k) m[k] = t;
|
||||||
|
return m;
|
||||||
|
}, {});
|
||||||
|
const ordered = typeList.map((t) => byType[String(t)]).filter(Boolean);
|
||||||
|
|
||||||
|
const next = ordered
|
||||||
|
.map((t) => {
|
||||||
|
const temp = normalizeTemplate(t);
|
||||||
|
const rawType = String(temp?.templateType || '');
|
||||||
|
const name = String(groupNameMap[rawType] || temp?.name || temp?.templateName || temp?.templateTypeName || '') || rawType;
|
||||||
|
return {
|
||||||
|
templateType: rawType,
|
||||||
|
name,
|
||||||
|
service: temp?.service || {},
|
||||||
|
templateList: Array.isArray(temp?.templateList) ? temp.templateList : [],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((i) => i && i.templateType);
|
||||||
|
|
||||||
|
if (next.length) {
|
||||||
|
templates.value = next;
|
||||||
|
loadedCorpId.value = corpId;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error('loadVisitTemplates error:', e);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (loadVisitTemplatesCorpId === corpId) {
|
||||||
|
loadVisitTemplatesPromise = null;
|
||||||
|
loadVisitTemplatesCorpId = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return loadVisitTemplatesPromise;
|
||||||
|
}
|
||||||
|
|
||||||
const userNameMap = ref({});
|
const userNameMap = ref({});
|
||||||
const loadedTeamId = ref('');
|
const loadedTeamId = ref('');
|
||||||
function resolveUserName(userId) {
|
function resolveUserName(userId) {
|
||||||
@ -138,10 +225,14 @@ async function loadTeamMembers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSortTimeTitle(templateType) {
|
function getSortTimeTitle(templateType) {
|
||||||
if (templateType === 'outpatient') return 'visitTime';
|
const rawType = String(templateType || '');
|
||||||
if (templateType === 'inhospital') return 'inhosDate';
|
const t = templateMap.value[rawType] || {};
|
||||||
if (templateType === 'preConsultation') return 'consultDate';
|
if (t?.service?.timeTitle) return String(t.service.timeTitle);
|
||||||
if (templateType === 'physicalExaminationTemplate') return 'inspectDate';
|
const ui = normalizeMedicalType(rawType);
|
||||||
|
if (ui === 'outpatient') return 'visitTime';
|
||||||
|
if (ui === 'inhospital') return 'inhosDate';
|
||||||
|
if (ui === 'preConsultation') return 'consultDate';
|
||||||
|
if (ui === 'physicalExaminationTemplate') return 'inspectDate';
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,20 +263,53 @@ function formatPositiveFind(v, { withOpinion = false } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getTemplateName(type) {
|
function getTemplateName(type) {
|
||||||
const t = VISIT_RECORD_TEMPLATES.find((i) => i && i.templateType === type);
|
const t = templateMap.value[String(type || '')];
|
||||||
return t?.templateName ? String(t.templateName) : '';
|
return t?.name ? String(t.name) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDateStr(sortTime) {
|
function toDateStr(sortTime) {
|
||||||
if (!sortTime) return '';
|
return formatAnyDate(sortTime, 'YYYY-MM-DD');
|
||||||
const d = dayjs(sortTime);
|
}
|
||||||
return d.isValid() ? d.format('YYYY-MM-DD') : '';
|
|
||||||
|
function normalizeMedicalType(raw) {
|
||||||
|
const s = String(raw || '').trim();
|
||||||
|
if (!s) return '';
|
||||||
|
const lower = s.toLowerCase();
|
||||||
|
if (lower.includes('preconsult') || (lower.includes('pre') && lower.includes('consult'))) return 'preConsultation';
|
||||||
|
if (lower === 'outpatient' || lower === 'out_patient' || lower === 'out-patient') return 'outpatient';
|
||||||
|
if (lower === 'inhospital' || lower === 'in_hospital' || lower === 'in-hospital' || lower === 'inpatient') return 'inhospital';
|
||||||
|
if (lower === 'preconsultation' || lower === 'pre_consultation' || lower === 'pre-consultation') return 'preConsultation';
|
||||||
|
if (lower === 'physicalexaminationtemplate' || lower === 'physicalexamination' || lower === 'physical_examination') return 'physicalExaminationTemplate';
|
||||||
|
if (s === 'outPatient') return 'outpatient';
|
||||||
|
if (s === 'inHospital') return 'inhospital';
|
||||||
|
if (s === 'preConsultation') return 'preConsultation';
|
||||||
|
if (s === 'physicalExaminationTemplate') return 'physicalExaminationTemplate';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAnyTimeMs(v) {
|
||||||
|
if (v === null || v === undefined) return 0;
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
// 10位秒级时间戳
|
||||||
|
if (v > 1e9 && v < 1e12) return v * 1000;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
const s = String(v).trim();
|
||||||
|
if (!s) return 0;
|
||||||
|
if (/^\d{10,13}$/.test(s)) return Number(s.length === 10 ? `${s}000` : s);
|
||||||
|
const d = dayjs(s);
|
||||||
|
return d.isValid() ? d.valueOf() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAnyDate(v, fmt = 'YYYY-MM-DD') {
|
||||||
|
const ms = parseAnyTimeMs(v);
|
||||||
|
if (!ms) return '';
|
||||||
|
const d = dayjs(ms);
|
||||||
|
return d.isValid() ? d.format(fmt) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDateTimeStr(ts) {
|
function toDateTimeStr(ts) {
|
||||||
if (!ts) return '';
|
return formatAnyDate(ts, 'YYYY-MM-DD HH:mm');
|
||||||
const d = dayjs(ts);
|
|
||||||
return d.isValid() ? d.format('YYYY-MM-DD HH:mm') : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshList() {
|
async function refreshList() {
|
||||||
@ -193,6 +317,7 @@ async function refreshList() {
|
|||||||
|
|
||||||
const corpId = getCorpId();
|
const corpId = getCorpId();
|
||||||
if (!corpId) return;
|
if (!corpId) return;
|
||||||
|
await loadVisitTemplates();
|
||||||
loadTeamMembers();
|
loadTeamMembers();
|
||||||
|
|
||||||
loading('加载中...');
|
loading('加载中...');
|
||||||
@ -201,7 +326,7 @@ async function refreshList() {
|
|||||||
|
|
||||||
// 添加类型筛选
|
// 添加类型筛选
|
||||||
params.medicalType =
|
params.medicalType =
|
||||||
currentType.value.value === 'ALL' ? templates.value.map((i) => i.templateType) : currentType.value.value;
|
currentType.value.value === 'ALL' ? availableTypes.value : currentType.value.value;
|
||||||
|
|
||||||
// 添加时间筛选
|
// 添加时间筛选
|
||||||
if (Array.isArray(dateRange.value) && dateRange.value.length === 2 && dateRange.value[0] && dateRange.value[1]) {
|
if (Array.isArray(dateRange.value) && dateRange.value.length === 2 && dateRange.value[0] && dateRange.value[1]) {
|
||||||
@ -219,21 +344,31 @@ async function refreshList() {
|
|||||||
? res.list
|
? res.list
|
||||||
: Array.isArray(res?.data?.list)
|
: Array.isArray(res?.data?.list)
|
||||||
? res.data.list
|
? res.data.list
|
||||||
|
: Array.isArray(res?.data?.data)
|
||||||
|
? res.data.data
|
||||||
|
: Array.isArray(res?.data?.data?.list)
|
||||||
|
? res.data.data.list
|
||||||
: Array.isArray(res?.data)
|
: Array.isArray(res?.data)
|
||||||
? res.data
|
? res.data
|
||||||
: [];
|
: [];
|
||||||
if (list.length) {
|
if (list.length) {
|
||||||
const mapped = list.map((r) => {
|
const mapped = list.map((r) => {
|
||||||
const t = String(r?.medicalType || r?.templateType || '') || '';
|
const rawType = String(r?.medicalType || r?.templateType || '') || '';
|
||||||
const timeTitle = getSortTimeTitle(t);
|
const uiType = normalizeMedicalType(rawType);
|
||||||
const dateStr = timeTitle ? normalizeText(r?.[timeTitle]) : '';
|
const normalized = normalizeVisitRecordFormData(uiType, r);
|
||||||
|
const timeTitle = getSortTimeTitle(rawType);
|
||||||
|
const rawTime = timeTitle ? (normalized?.[timeTitle] ?? r?.[timeTitle]) : '';
|
||||||
|
const dateStr = rawTime ? formatAnyDate(rawTime, 'YYYY-MM-DD') : '';
|
||||||
return {
|
return {
|
||||||
...r,
|
...r,
|
||||||
templateType: t,
|
...normalized,
|
||||||
|
medicalType: rawType,
|
||||||
|
templateType: uiType,
|
||||||
|
rawTemplateType: rawType,
|
||||||
dateStr: dateStr || toDateStr(r?.sortTime),
|
dateStr: dateStr || toDateStr(r?.sortTime),
|
||||||
createDateStr: r?.createTime ? dayjs(r.createTime).format('YYYY-MM-DD') : '',
|
createDateStr: r?.createTime ? formatAnyDate(r.createTime, 'YYYY-MM-DD') : '',
|
||||||
createTimeStr: toDateTimeStr(r?.createTime),
|
createTimeStr: toDateTimeStr(r?.createTime),
|
||||||
tempName: r?.tempName || getTemplateName(t) || '病历',
|
tempName: r?.tempName || getTemplateName(rawType) || '病历',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -258,9 +393,21 @@ const tagClass = {
|
|||||||
physicalExaminationTemplate: 'bg-green',
|
physicalExaminationTemplate: 'bg-green',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function resolveRecordType(r) {
|
||||||
|
if (!r) return '';
|
||||||
|
const direct = normalizeMedicalType(r.uiType || r.templateType || r.rawTemplateType || r.medicalType || '');
|
||||||
|
if (direct) return direct;
|
||||||
|
// fallback by known fields
|
||||||
|
if (r.inspectDate || r.positiveFind || r.inspectSummary) return 'physicalExaminationTemplate';
|
||||||
|
if (r.inhosDate || r.surgeryName || r.surgeryDate || r.operationDate) return 'inhospital';
|
||||||
|
if (r.visitTime || r.disposePlan || r.treatmentPlan) return 'outpatient';
|
||||||
|
if (r.consultDate || r.presentIllness || r.presentIllnessHistory || r.pastHistory) return 'preConsultation';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function getDiagnosis(r) {
|
function getDiagnosis(r) {
|
||||||
if (!r) return '--';
|
if (!r) return '--';
|
||||||
const t = r.templateType || r.medicalType;
|
const t = resolveRecordType(r);
|
||||||
if (t === 'preConsultation') return normalizeText(r.chiefComplaint) || normalizeText(r.summary) || '--';
|
if (t === 'preConsultation') return normalizeText(r.chiefComplaint) || normalizeText(r.summary) || '--';
|
||||||
if (t === 'physicalExaminationTemplate') return formatPositiveFind(r.positiveFind) || normalizeText(r.summary) || '--';
|
if (t === 'physicalExaminationTemplate') return formatPositiveFind(r.positiveFind) || normalizeText(r.summary) || '--';
|
||||||
if (t === 'outpatient' || t === 'inhospital') return normalizeText(r.diagnosisName || r.diagnosis) || normalizeText(r.summary) || '--';
|
if (t === 'outpatient' || t === 'inhospital') return normalizeText(r.diagnosisName || r.diagnosis) || normalizeText(r.summary) || '--';
|
||||||
@ -273,7 +420,7 @@ function firstLine(v) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayLines(r) {
|
function getDisplayLines(r) {
|
||||||
const t = r?.templateType || r?.medicalType;
|
const t = resolveRecordType(r);
|
||||||
if (t === 'outpatient') {
|
if (t === 'outpatient') {
|
||||||
return [{ label: '门诊诊断:', value: firstLine(r.diagnosisName || r.diagnosis) }];
|
return [{ label: '门诊诊断:', value: firstLine(r.diagnosisName || r.diagnosis) }];
|
||||||
}
|
}
|
||||||
@ -284,12 +431,12 @@ function getDisplayLines(r) {
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
if (t === 'physicalExaminationTemplate') {
|
if (t === 'physicalExaminationTemplate') {
|
||||||
return [{ label: '体检小结:', value: firstLine(r.summary) }];
|
return [{ label: '体检小结:', value: firstLine(r.summary || r.inspectSummary) }];
|
||||||
}
|
}
|
||||||
if (t === 'preConsultation') {
|
if (t === 'preConsultation') {
|
||||||
const lines = [
|
const lines = [
|
||||||
{ label: '主诉:', value: firstLine(r.chiefComplaint) },
|
{ label: '主诉:', value: firstLine(r.chiefComplaint) },
|
||||||
{ label: '现病史:', value: firstLine(r.presentIllness) },
|
{ label: '现病史:', value: firstLine(r.presentIllness || r.presentIllnessHistory) },
|
||||||
];
|
];
|
||||||
const past = normalizeText(r.pastHistory);
|
const past = normalizeText(r.pastHistory);
|
||||||
if (past) lines.push({ label: '既往史:', value: past });
|
if (past) lines.push({ label: '既往史:', value: past });
|
||||||
@ -320,7 +467,11 @@ function pickTimeRange(val) {
|
|||||||
|
|
||||||
function getFiles(r) {
|
function getFiles(r) {
|
||||||
const arr = r?.files;
|
const arr = r?.files;
|
||||||
return Array.isArray(arr) ? arr.filter((i) => i && i.url) : [];
|
return Array.isArray(arr)
|
||||||
|
? arr
|
||||||
|
.filter((i) => i && i.url)
|
||||||
|
.map((i) => ({ ...i, url: normalizeFileUrl(i.url) }))
|
||||||
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function previewFiles(r, idx) {
|
function previewFiles(r, idx) {
|
||||||
@ -329,18 +480,58 @@ function previewFiles(r, idx) {
|
|||||||
uni.previewImage({ urls, current: urls[idx] });
|
uni.previewImage({ urls, current: urls[idx] });
|
||||||
}
|
}
|
||||||
|
|
||||||
function add() {
|
function goAdd(t) {
|
||||||
|
if (!t?.templateType) return;
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/case/visit-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&type=${encodeURIComponent(t.templateType)}&name=${encodeURIComponent(props.data?.name || '')}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickAddType(e) {
|
||||||
|
if (!props.archiveId) return toast('缺少档案信息');
|
||||||
|
const idx = Number(e?.detail?.value ?? -1);
|
||||||
|
const t = selectableTemplates.value[idx];
|
||||||
|
if (!t) return;
|
||||||
|
goAdd(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAddActionSheet() {
|
||||||
|
if (!props.archiveId) return toast('缺少档案信息');
|
||||||
|
const list = selectableTemplates.value;
|
||||||
|
if (!list.length) return toast('暂无可用病历模板');
|
||||||
uni.showActionSheet({
|
uni.showActionSheet({
|
||||||
itemList: templates.value.map((i) => i.name),
|
itemList: list.map((i) => i.name),
|
||||||
success: ({ tapIndex }) => {
|
success: ({ tapIndex }) => {
|
||||||
const t = templates.value[tapIndex];
|
const t = list[tapIndex];
|
||||||
uni.navigateTo({
|
if (!t) return;
|
||||||
url: `/pages/case/visit-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&type=${encodeURIComponent(t.templateType)}&name=${encodeURIComponent(props.data?.name || '')}`,
|
goAdd(t);
|
||||||
});
|
},
|
||||||
|
fail: (e) => {
|
||||||
|
// 用户取消无需提示;其他错误忽略
|
||||||
|
const errMsg = String(e?.errMsg || '');
|
||||||
|
if (errMsg && errMsg.includes('cancel')) return;
|
||||||
|
if (errMsg) console.warn('[health-profile-tab] showActionSheet fail:', errMsg);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onFabTap() {
|
||||||
|
if (!props.archiveId) return toast('缺少档案信息');
|
||||||
|
|
||||||
|
// 模板可能尚未加载:先拉一次
|
||||||
|
if (!templates.value.length) {
|
||||||
|
loading('加载模板...');
|
||||||
|
try {
|
||||||
|
await loadVisitTemplates();
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// <=6:用 actionSheet;>6:由 picker 自己弹(这里不做额外处理)
|
||||||
|
if (useActionSheet.value) showAddActionSheet();
|
||||||
|
}
|
||||||
|
|
||||||
function edit(record) {
|
function edit(record) {
|
||||||
const type = String(record?.medicalType || record?.templateType || '') || '';
|
const type = String(record?.medicalType || record?.templateType || '') || '';
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
@ -349,8 +540,6 @@ function edit(record) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// archiveId 可能后置赋值:这里保留一次兜底刷新,主逻辑交给 watch
|
|
||||||
refreshList();
|
|
||||||
uni.$on('archive-detail:visit-record-changed', refreshList);
|
uni.$on('archive-detail:visit-record-changed', refreshList);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -533,9 +722,14 @@ watch(
|
|||||||
font-size: 26rpx;
|
font-size: 26rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fab {
|
.fab-picker {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
right: 32rpx;
|
right: 32rpx;
|
||||||
|
width: 104rpx;
|
||||||
|
height: 104rpx;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
.fab {
|
||||||
width: 104rpx;
|
width: 104rpx;
|
||||||
height: 104rpx;
|
height: 104rpx;
|
||||||
border-radius: 52rpx;
|
border-radius: 52rpx;
|
||||||
@ -544,6 +738,8 @@ watch(
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 20rpx 36rpx rgba(79, 110, 247, 0.35);
|
box-shadow: 0 20rpx 36rpx rgba(79, 110, 247, 0.35);
|
||||||
z-index: 20;
|
}
|
||||||
|
.fab--disabled {
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -40,8 +40,8 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="meta">
|
<view class="meta">
|
||||||
<view class="tag">{{ i.typeStr }}</view>
|
<view class="tag">{{ i.typeStr }}</view>
|
||||||
<view class="meta-text">{{ i.executorName }}</view>
|
<view class="meta-text">{{ executorText(i) }}</view>
|
||||||
<view class="meta-text truncate">{{ i.executeTeamName }}</view>
|
<view class="meta-text truncate">{{ executeTeamText(i) }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="body">
|
<view class="body">
|
||||||
<view class="content" :class="{ clamp: !expandMap[i._id] }">
|
<view class="content" :class="{ clamp: !expandMap[i._id] }">
|
||||||
@ -79,6 +79,31 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</uni-popup>
|
</uni-popup>
|
||||||
|
|
||||||
|
<!-- 编辑服务内容 -->
|
||||||
|
<uni-popup ref="editPopupRef" type="bottom" :mask-click="true" @maskClick="closeEditPopup">
|
||||||
|
<view class="edit-sheet">
|
||||||
|
<view class="edit-header">
|
||||||
|
<view class="edit-header-left" />
|
||||||
|
<view class="edit-title">修改服务内容</view>
|
||||||
|
<view class="edit-close" @click="closeEditPopup">
|
||||||
|
<uni-icons type="closeempty" size="18" color="#333" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="edit-body">
|
||||||
|
<textarea
|
||||||
|
v-model="editContent"
|
||||||
|
class="edit-textarea"
|
||||||
|
placeholder="请输入服务内容"
|
||||||
|
:maxlength="1000"
|
||||||
|
/>
|
||||||
|
<view class="counter">{{ (editContent || '').length }}/1000</view>
|
||||||
|
</view>
|
||||||
|
<view class="edit-footer">
|
||||||
|
<button class="btn primary" @click="saveEdit">保存</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -130,7 +155,8 @@ async function ensureDoctor() {
|
|||||||
function getUserId() {
|
function getUserId() {
|
||||||
const d = doctorInfo.value || {};
|
const d = doctorInfo.value || {};
|
||||||
const a = account.value || {};
|
const a = account.value || {};
|
||||||
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
|
const t = uni.getStorageSync('ykt_case_current_team') || {};
|
||||||
|
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || t.userId || t.userid || t.corpUserId || '') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCorpId() {
|
function getCorpId() {
|
||||||
@ -145,6 +171,58 @@ function getCurrentTeamId() {
|
|||||||
return String(t.teamId || '') || '';
|
return String(t.teamId || '') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeName(v) {
|
||||||
|
const s = v === 0 ? '0' : v ? String(v) : '';
|
||||||
|
const trimmed = s.trim();
|
||||||
|
if (!trimmed) return '';
|
||||||
|
if (['-', '—', '--'].includes(trimmed)) return '';
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExecutorId(r) {
|
||||||
|
const row = r && typeof r === 'object' ? r : {};
|
||||||
|
return String(
|
||||||
|
row.executorUserId ||
|
||||||
|
row.executorId ||
|
||||||
|
row.executor ||
|
||||||
|
row.creatorUserId ||
|
||||||
|
row.creator ||
|
||||||
|
row.updateUserId ||
|
||||||
|
''
|
||||||
|
) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function executorText(r) {
|
||||||
|
const row = r && typeof r === 'object' ? r : {};
|
||||||
|
const fromRow = normalizeName(row.executorName || row.executorUserName || row.creatorName || row.updateUserName || '');
|
||||||
|
if (fromRow) return fromRow;
|
||||||
|
const uid = getExecutorId(row);
|
||||||
|
const mapped = normalizeName(resolveUserName(uid));
|
||||||
|
return mapped || (uid ? uid : '--');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExecuteTeamId(r) {
|
||||||
|
const row = r && typeof r === 'object' ? r : {};
|
||||||
|
return String(row.executeTeamId || row.teamId || row.teamID || '') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveTeamName(teamId) {
|
||||||
|
const tid = String(teamId || '') || '';
|
||||||
|
if (!tid) return '';
|
||||||
|
const list = teamList.value || [];
|
||||||
|
const hit = list.find((i) => i && i.value === tid);
|
||||||
|
return hit?.label ? String(hit.label) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeTeamText(r) {
|
||||||
|
const row = r && typeof r === 'object' ? r : {};
|
||||||
|
const fromRow = normalizeName(row.executeTeamName || row.teamName || '');
|
||||||
|
if (fromRow) return fromRow;
|
||||||
|
const tid = getExecuteTeamId(row) || getCurrentTeamId();
|
||||||
|
const mapped = normalizeName(resolveTeamName(tid));
|
||||||
|
return mapped || (tid ? tid : '--');
|
||||||
|
}
|
||||||
|
|
||||||
function resolveUserName(userId) {
|
function resolveUserName(userId) {
|
||||||
const id = String(userId || '');
|
const id = String(userId || '');
|
||||||
if (!id) return '';
|
if (!id) return '';
|
||||||
@ -181,9 +259,12 @@ function displayTaskContent(r) {
|
|||||||
return formatTaskContent(String(r?.taskContent || ''));
|
return formatTaskContent(String(r?.taskContent || ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadedTeamMemberIds = new Set();
|
||||||
async function loadTeamMembers(teamId) {
|
async function loadTeamMembers(teamId) {
|
||||||
const tid = String(teamId || '') || '';
|
const tid = String(teamId || '') || '';
|
||||||
if (!tid) return;
|
if (!tid) return;
|
||||||
|
if (loadedTeamMemberIds.has(tid)) return;
|
||||||
|
loadedTeamMemberIds.add(tid);
|
||||||
await ensureDoctor();
|
await ensureDoctor();
|
||||||
const corpId = getCorpId();
|
const corpId = getCorpId();
|
||||||
if (!corpId) return;
|
if (!corpId) return;
|
||||||
@ -216,6 +297,8 @@ function mapRow(i) {
|
|||||||
return {
|
return {
|
||||||
...i,
|
...i,
|
||||||
_id: String(i?._id || i?.id || ''),
|
_id: String(i?._id || i?.id || ''),
|
||||||
|
executorUserId: getExecutorId(i),
|
||||||
|
executeTeamId: getExecuteTeamId(i),
|
||||||
hasFile,
|
hasFile,
|
||||||
fileType,
|
fileType,
|
||||||
timeStr: i.executionTime ? dayjs(i.executionTime).format('YYYY-MM-DD HH:mm') : '--',
|
timeStr: i.executionTime ? dayjs(i.executionTime).format('YYYY-MM-DD HH:mm') : '--',
|
||||||
@ -288,6 +371,10 @@ async function getMore() {
|
|||||||
const mapped = arr.map(mapRow).filter((i) => i && i._id);
|
const mapped = arr.map(mapRow).filter((i) => i && i._id);
|
||||||
list.value = page.value === 1 ? mapped : [...list.value, ...mapped];
|
list.value = page.value === 1 ? mapped : [...list.value, ...mapped];
|
||||||
page.value += 1;
|
page.value += 1;
|
||||||
|
|
||||||
|
// 尽量加载记录所属团队成员,用于执行人展示
|
||||||
|
const teamIds = mapped.map((i) => i.executeTeamId).filter(Boolean);
|
||||||
|
Array.from(new Set(teamIds)).forEach((tid) => loadTeamMembers(tid));
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@ -330,21 +417,10 @@ function add() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function edit(record) {
|
function edit(record) {
|
||||||
const archive = props.data || {};
|
if (!record?._id) return;
|
||||||
const customerUserId = String(archive.externalUserId || archive.customerUserId || '') || '';
|
editingRecord.value = record;
|
||||||
uni.setStorageSync('service-record-detail', {
|
editContent.value = String(record?.taskContent || '') || '';
|
||||||
customerId: String(props.archiveId),
|
editPopupRef.value?.open?.();
|
||||||
customerName: String(archive.name || ''),
|
|
||||||
customerUserId,
|
|
||||||
id: String(record?._id || ''),
|
|
||||||
executionTime: record?.executionTime || 0,
|
|
||||||
executeTeamId: String(record?.executeTeamId || ''),
|
|
||||||
executeTeamName: String(record?.executeTeamName || ''),
|
|
||||||
eventType: String(record?.eventType || ''),
|
|
||||||
taskContent: String(record?.taskContent || ''),
|
|
||||||
pannedEventSendFile: record?.pannedEventSendFile || null,
|
|
||||||
});
|
|
||||||
uni.navigateTo({ url: `/pages/case/service-record-detail?archiveId=${encodeURIComponent(props.archiveId)}&mode=edit&id=${encodeURIComponent(record._id)}` });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePopupRef = ref(null);
|
const filePopupRef = ref(null);
|
||||||
@ -380,6 +456,45 @@ function copyFile() {
|
|||||||
closeFilePopup();
|
closeFilePopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editPopupRef = ref(null);
|
||||||
|
const editingRecord = ref(null);
|
||||||
|
const editContent = ref('');
|
||||||
|
|
||||||
|
function closeEditPopup() {
|
||||||
|
editPopupRef.value?.close?.();
|
||||||
|
editingRecord.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveEdit() {
|
||||||
|
const r = editingRecord.value;
|
||||||
|
if (!r?._id) return;
|
||||||
|
if (!String(editContent.value || '').trim()) return toast('请输入服务内容');
|
||||||
|
|
||||||
|
await ensureDoctor();
|
||||||
|
const corpId = getCorpId();
|
||||||
|
const userId = getUserId();
|
||||||
|
if (!corpId || !userId) return toast('缺少用户信息');
|
||||||
|
|
||||||
|
const res = await api('updateServiceRecord', {
|
||||||
|
corpId,
|
||||||
|
id: String(r._id),
|
||||||
|
params: {
|
||||||
|
taskContent: String(editContent.value || ''),
|
||||||
|
updateUserId: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res?.success) return toast(res?.message || '修改失败');
|
||||||
|
|
||||||
|
// 本地同步,避免闪烁
|
||||||
|
const idx = list.value.findIndex((i) => i && i._id === String(r._id));
|
||||||
|
if (idx > -1) list.value[idx] = { ...list.value[idx], taskContent: String(editContent.value || '') };
|
||||||
|
|
||||||
|
uni.$emit('archive-detail:service-record-changed');
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||||
|
closeEditPopup();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTeams() {
|
async function loadTeams() {
|
||||||
await ensureDoctor();
|
await ensureDoctor();
|
||||||
const corpId = getCorpId();
|
const corpId = getCorpId();
|
||||||
@ -640,4 +755,56 @@ watch(
|
|||||||
background: #0877F1;
|
background: #0877F1;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-sheet {
|
||||||
|
background: #fff;
|
||||||
|
border-top-left-radius: 20rpx;
|
||||||
|
border-top-right-radius: 20rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.edit-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 28rpx;
|
||||||
|
border-bottom: 2rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.edit-header-left {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
}
|
||||||
|
.edit-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.edit-close {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.edit-body {
|
||||||
|
padding: 28rpx 28rpx 0;
|
||||||
|
}
|
||||||
|
.edit-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 260rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
border: 2rpx solid #e5e7eb;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.counter {
|
||||||
|
margin-top: 12rpx;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.edit-footer {
|
||||||
|
padding: 24rpx 28rpx 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -8,9 +8,9 @@ export const VISIT_RECORD_TEMPLATES = [
|
|||||||
{ title: 'chiefComplaint', name: '主诉', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请输入或说话录音转录问题', rows: 1, autoHeight: true },
|
{ title: 'chiefComplaint', name: '主诉', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请输入或说话录音转录问题', rows: 1, autoHeight: true },
|
||||||
{ title: 'medicalHistory', name: '病史概要', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请简述患者情况,量键既往病史、用药史等', rows: 3, autoHeight: true },
|
{ title: 'medicalHistory', name: '病史概要', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请简述患者情况,量键既往病史、用药史等', rows: 3, autoHeight: true },
|
||||||
{ title: 'examination', name: '检查', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请填写关键项目或异常结果描述', rows: 1, autoHeight: true },
|
{ title: 'examination', name: '检查', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请填写关键项目或异常结果描述', rows: 1, autoHeight: true },
|
||||||
{ title: 'diagnosis', name: '诊断', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请填写诊断名称', supportVoice: true, rows: 1, autoHeight: true },
|
{ title: 'diagnosis', name: '诊断', type: 'diagnosis', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请选择诊断', supportVoice: true, rows: 1, autoHeight: true },
|
||||||
{ title: 'treatmentPlan', name: '治疗方案', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 1000, placeholder: '请简述治疗方案及效果', rows: 3, autoHeight: true },
|
{ title: 'treatmentPlan', name: '治疗方案', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 1000, placeholder: '请简述治疗方案及效果', rows: 3, autoHeight: true },
|
||||||
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持≤5M文件,pdf文件格式)', maxSize: 5, accept: 'pdf' },
|
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(仅支持拍摄/相册选择图片,≤5M)', maxSize: 5, accept: 'image' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -22,11 +22,11 @@ export const VISIT_RECORD_TEMPLATES = [
|
|||||||
{ title: 'chiefComplaint', name: '主诉', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请输入病症与转诊问题', rows: 1, autoHeight: true },
|
{ title: 'chiefComplaint', name: '主诉', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请输入病症与转诊问题', rows: 1, autoHeight: true },
|
||||||
{ title: 'medicalHistory', name: '病史概要', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请简述患者情况,量键既往病史、用药史等症状', rows: 3, autoHeight: true },
|
{ title: 'medicalHistory', name: '病史概要', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请简述患者情况,量键既往病史、用药史等症状', rows: 3, autoHeight: true },
|
||||||
{ title: 'examination', name: '检查', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请填写关键项目或异常结果描述', rows: 1, autoHeight: true },
|
{ title: 'examination', name: '检查', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '请填写关键项目或异常结果描述', rows: 1, autoHeight: true },
|
||||||
{ title: 'diagnosis', name: '住院主诊断', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请填写诊断名称', rows: 1, autoHeight: true },
|
{ title: 'diagnosis', name: '住院主诊断', type: 'diagnosis', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请选择诊断', rows: 1, autoHeight: true },
|
||||||
{ title: 'surgeryDate', name: '手术日期', type: 'date', operateType: 'formCell', required: false, format: 'YYYY-MM-DD', placeholder: '请选择手术日期' },
|
{ title: 'surgeryDate', name: '手术日期', type: 'date', operateType: 'formCell', required: false, format: 'YYYY-MM-DD', placeholder: '请选择手术日期' },
|
||||||
{ title: 'surgeryName', name: '手术名称', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 100, placeholder: '请填写手术名称', rows: 1, autoHeight: true },
|
{ title: 'surgeryName', name: '手术名称', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 100, placeholder: '请填写手术名称', rows: 1, autoHeight: true },
|
||||||
{ title: 'treatmentPlan', name: '治疗方案', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 1000, placeholder: '请简述治疗方案及效果', rows: 3, autoHeight: true },
|
{ title: 'treatmentPlan', name: '治疗方案', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 1000, placeholder: '请简述治疗方案及效果', rows: 3, autoHeight: true },
|
||||||
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持≤5M文件,pdf文件格式)', maxSize: 5, accept: 'pdf' },
|
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(仅支持拍摄/相册选择图片,≤5M)', maxSize: 5, accept: 'image' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -37,7 +37,7 @@ export const VISIT_RECORD_TEMPLATES = [
|
|||||||
{ title: 'chiefComplaint', name: '主诉', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请输入症状及转诊问题', rows: 1, autoHeight: true },
|
{ title: 'chiefComplaint', name: '主诉', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请输入症状及转诊问题', rows: 1, autoHeight: true },
|
||||||
{ title: 'presentIllness', name: '现病史', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 800, placeholder: '请述述发病的过程、发展、诊疗经过及当前病情', rows: 3, autoHeight: true },
|
{ title: 'presentIllness', name: '现病史', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 800, placeholder: '请述述发病的过程、发展、诊疗经过及当前病情', rows: 3, autoHeight: true },
|
||||||
{ title: 'pastHistory', name: '既往史', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 800, placeholder: '请填写既往疾病、手术/外伤史、药物/食物过敏史', rows: 3, autoHeight: true },
|
{ title: 'pastHistory', name: '既往史', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 800, placeholder: '请填写既往疾病、手术/外伤史、药物/食物过敏史', rows: 3, autoHeight: true },
|
||||||
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持≤5M文件,pdf文件格式)', maxSize: 5, accept: 'pdf' },
|
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(仅支持拍摄/相册选择图片,≤5M)', maxSize: 5, accept: 'image' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -48,7 +48,7 @@ export const VISIT_RECORD_TEMPLATES = [
|
|||||||
{ title: 'inspectDate', name: '体检日期', type: 'date', operateType: 'formCell', required: false, format: 'YYYY-MM-DD', placeholder: '请选择体检日期' },
|
{ title: 'inspectDate', name: '体检日期', type: 'date', operateType: 'formCell', required: false, format: 'YYYY-MM-DD', placeholder: '请选择体检日期' },
|
||||||
{ title: 'summary', name: '体检小结', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请填写本次体检的小结', rows: 3, autoHeight: true },
|
{ title: 'summary', name: '体检小结', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 200, placeholder: '请填写本次体检的小结', rows: 3, autoHeight: true },
|
||||||
{ title: 'positiveFind', name: '阳性发现及处理意见', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '可参照医客通现有模式', rows: 3, refNote: '可参照医客通现有模式', autoHeight: true },
|
{ title: 'positiveFind', name: '阳性发现及处理意见', type: 'textarea', operateType: 'formCell', required: false, wordLimit: 500, placeholder: '可参照医客通现有模式', rows: 3, refNote: '可参照医客通现有模式', autoHeight: true },
|
||||||
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(支持≤5M文件,pdf文件格式)', maxSize: 5, accept: 'pdf' },
|
{ title: 'files', name: '文件上传', type: 'files', required: false, desc: '(仅支持拍摄/相册选择图片,≤5M)', maxSize: 5, accept: 'image' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<view class="manage-container">
|
<view class="manage-container">
|
||||||
<view class="group-list">
|
<view class="group-list">
|
||||||
<scroll-view scroll-y class="sort-scroll">
|
<scroll-view scroll-y class="sort-scroll">
|
||||||
<movable-area class="drag-area" :style="{ height: dragAreaHeight + 'px' }">
|
<movable-area :key="areaKey" class="drag-area" :style="{ height: dragAreaHeight + 'px' }">
|
||||||
<movable-view
|
<movable-view
|
||||||
v-for="(item, index) in groups"
|
v-for="(item, index) in groups"
|
||||||
:key="item._id"
|
:key="item._id"
|
||||||
@ -81,7 +81,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed, nextTick } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
@ -91,6 +91,8 @@ import { hideLoading, loading, toast } from '@/utils/widget';
|
|||||||
// State
|
// State
|
||||||
const groups = ref([]);
|
const groups = ref([]);
|
||||||
const originalGroups = ref([]);
|
const originalGroups = ref([]);
|
||||||
|
const areaKey = ref(0);
|
||||||
|
const loadSeq = ref(0);
|
||||||
|
|
||||||
const ITEM_HEIGHT = 74; // px,需与样式保持一致
|
const ITEM_HEIGHT = 74; // px,需与样式保持一致
|
||||||
const draggingId = ref('');
|
const draggingId = ref('');
|
||||||
@ -151,6 +153,7 @@ async function ensureDoctor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadGroups() {
|
async function loadGroups() {
|
||||||
|
const seq = (loadSeq.value += 1);
|
||||||
await ensureDoctor();
|
await ensureDoctor();
|
||||||
const corpId = getCorpId();
|
const corpId = getCorpId();
|
||||||
const teamId = getTeamId();
|
const teamId = getTeamId();
|
||||||
@ -166,9 +169,17 @@ async function loadGroups() {
|
|||||||
}
|
}
|
||||||
const list = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.data) ? res.data.data : [];
|
const list = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.data) ? res.data.data : [];
|
||||||
const sorted = sortGroupList(list);
|
const sorted = sortGroupList(list);
|
||||||
groups.value = sorted.map((i, idx) => ({ ...i, _y: idx * ITEM_HEIGHT }));
|
if (seq !== loadSeq.value) return;
|
||||||
originalGroups.value = sorted.map((i, idx) => ({ ...i, _y: idx * ITEM_HEIGHT }));
|
const next = sorted.map((i, idx) => ({ ...i, _y: idx * ITEM_HEIGHT }));
|
||||||
|
|
||||||
|
// movable-view 在频繁新增后偶发错位:清空再赋值,并通过 key 强制重建 movable-area
|
||||||
|
groups.value = [];
|
||||||
|
await nextTick();
|
||||||
|
if (seq !== loadSeq.value) return;
|
||||||
|
groups.value = next;
|
||||||
|
originalGroups.value = next.map((i) => ({ ...i }));
|
||||||
lastSavedOrderKey.value = getOrderKey(groups.value);
|
lastSavedOrderKey.value = getOrderKey(groups.value);
|
||||||
|
areaKey.value += 1;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast('获取分组失败');
|
toast('获取分组失败');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -1,473 +1,277 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="medical-case-form">
|
<view class="page">
|
||||||
<view class="form-container">
|
<view class="body">
|
||||||
<!-- 动态渲染表单字段 -->
|
<scroll-view scroll-y class="scroll">
|
||||||
<view
|
<view class="header">
|
||||||
v-for="field in currentFields"
|
<view class="header-title">{{ pageTitle }}</view>
|
||||||
:key="field.key"
|
</view>
|
||||||
class="form-item"
|
|
||||||
:class="{ required: field.required }"
|
|
||||||
>
|
|
||||||
<view class="item-label">{{ field.label }}</view>
|
|
||||||
|
|
||||||
<!-- 日期选择器 -->
|
<view class="form-wrap">
|
||||||
<picker
|
<FormTemplate v-if="temp" ref="formRef" :items="showItems" :form="form" @change="onChange" />
|
||||||
v-if="field.type === 'date'"
|
</view>
|
||||||
mode="date"
|
|
||||||
:value="formData[field.key]"
|
|
||||||
@change="onDateChange(field.key, $event)"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
>
|
|
||||||
<view class="picker-value">
|
|
||||||
{{ formData[field.key] || "暂无" }}
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
|
|
||||||
<!-- 多行文本 -->
|
<view style="height: 240rpx;"></view>
|
||||||
<textarea
|
</scroll-view>
|
||||||
v-else-if="field.type === 'textarea'"
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData[field.key]"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 单行文本 -->
|
<view class="footer">
|
||||||
<input
|
<button class="btn plain" @click="handleRegenerate">重新生成</button>
|
||||||
v-else
|
<button class="btn primary" @click="handleSave">保存至档案</button>
|
||||||
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>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { computed, reactive, ref } from 'vue';
|
||||||
import { storeToRefs } from "pinia";
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import useAccountStore from "@/store/account";
|
import dayjs from 'dayjs';
|
||||||
import api from "@/utils/api.js";
|
import { storeToRefs } from 'pinia';
|
||||||
const caseType = ref("");
|
import FormTemplate from '@/components/form-template/index.vue';
|
||||||
const formData = ref({});
|
import api from '@/utils/api.js';
|
||||||
const isEditing = ref(true);
|
import useAccountStore from '@/store/account';
|
||||||
const customerId = ref("");
|
import { toast, confirm, loading as uniLoading, hideLoading } from '@/utils/widget';
|
||||||
const groupId = ref("");
|
import { normalizeVisitRecordFormData } from './utils/visit-record';
|
||||||
|
import { normalizeTemplate, unwrapTemplateResponse } from './utils/template';
|
||||||
|
|
||||||
|
const caseType = ref('');
|
||||||
|
const customerId = ref('');
|
||||||
|
const groupId = ref('');
|
||||||
|
|
||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const { doctorInfo } = storeToRefs(accountStore);
|
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||||
// 病历类型名称
|
const { getDoctorInfo } = 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
async function ensureDoctor() {
|
||||||
|
if (doctorInfo.value) return;
|
||||||
|
if (!account.value?.openid) return;
|
||||||
try {
|
try {
|
||||||
uni.showLoading({ title: "保存中..." });
|
await getDoctorInfo();
|
||||||
const result = await api("addMedicalRecord", {
|
} catch {
|
||||||
medicalType: caseType.value,
|
// ignore
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getUserId() {
|
||||||
|
const d = doctorInfo.value || {};
|
||||||
|
const a = account.value || {};
|
||||||
|
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCorpId() {
|
||||||
|
const d = doctorInfo.value || {};
|
||||||
|
const a = account.value || {};
|
||||||
|
const t = uni.getStorageSync('ykt_case_current_team') || {};
|
||||||
|
return String(d.corpId || a.corpId || t.corpId || '') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const temp = ref(null);
|
||||||
|
const titleText = computed(() => {
|
||||||
|
const t = temp.value || {};
|
||||||
|
return String(t.name || t.templateName || t.templateTypeName || '').trim();
|
||||||
|
});
|
||||||
|
const pageTitle = computed(() => {
|
||||||
|
const name = titleText.value;
|
||||||
|
return name ? `添加${name}` : '添加病历';
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = reactive({});
|
||||||
|
const forms = computed(() => form);
|
||||||
|
const showItems = computed(() => {
|
||||||
|
const list = temp.value?.templateList || [];
|
||||||
|
return list.filter((i) => {
|
||||||
|
if (i?.type === 'files') return false;
|
||||||
|
if (i && typeof i.referenceField === 'string') {
|
||||||
|
return forms.value[i.referenceField] === i.referenceValue;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
|
||||||
|
function onChange({ title, value }) {
|
||||||
|
form[title] = value;
|
||||||
|
const item = showItems.value.find((i) => i.title === title);
|
||||||
|
if (!item) return;
|
||||||
|
const relat = (temp.value?.templateList || []).filter((i) => i.referenceField === title);
|
||||||
|
relat.forEach((i) => (form[i.title] = ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setDefaultDates() {
|
||||||
|
const timeKey = temp.value?.service?.timeTitle || '';
|
||||||
|
if (timeKey && !form[timeKey]) form[timeKey] = dayjs().format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyInitialFormData(raw) {
|
||||||
|
const normalized = normalizeVisitRecordFormData(caseType.value, raw);
|
||||||
|
Object.assign(form, normalized);
|
||||||
|
setDefaultDates();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTemplate(t) {
|
||||||
|
const corpId = getCorpId();
|
||||||
|
if (!corpId) return null;
|
||||||
|
try {
|
||||||
|
const res = await api('getCurrentTemplate', { corpId, templateType: t });
|
||||||
|
if (!res?.success) {
|
||||||
|
toast(res?.message || '获取模板失败');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const raw = unwrapTemplateResponse(res);
|
||||||
|
return normalizeTemplate(raw);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('loadTemplate error:', e);
|
||||||
|
toast('获取模板失败');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad(async (options) => {
|
||||||
|
caseType.value = options?.caseType || options?.type || '';
|
||||||
|
customerId.value = options?.patientId || options?.memberId || '';
|
||||||
|
groupId.value = options?.groupId || '';
|
||||||
|
|
||||||
|
if (!caseType.value) caseType.value = 'outpatient';
|
||||||
|
|
||||||
|
temp.value = await loadTemplate(caseType.value);
|
||||||
|
if (temp.value?.templateType) caseType.value = String(temp.value.templateType);
|
||||||
|
|
||||||
|
if (options?.formData) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(decodeURIComponent(options.formData));
|
||||||
|
applyInitialFormData(parsed);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析表单数据失败:', e);
|
||||||
|
applyInitialFormData({});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
applyInitialFormData({});
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.setNavigationBarTitle({ title: pageTitle.value });
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleRegenerate() {
|
||||||
|
try {
|
||||||
|
await confirm('确定要重新生成吗?当前编辑的内容将被覆盖');
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.navigateBack({
|
||||||
|
success: () => {
|
||||||
|
uni.$emit('regenerateMedicalCase', {
|
||||||
|
caseType: caseType.value,
|
||||||
|
customerId: customerId.value,
|
||||||
|
groupId: groupId.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
if (!customerId.value) return toast('缺少患者信息');
|
||||||
|
await ensureDoctor();
|
||||||
|
const corpId = getCorpId();
|
||||||
|
const userId = getUserId();
|
||||||
|
if (!corpId || !userId) return toast('缺少用户信息');
|
||||||
|
if (formRef.value?.verify && !formRef.value.verify()) return;
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
...form,
|
||||||
|
corpId,
|
||||||
|
memberId: customerId.value,
|
||||||
|
medicalType: caseType.value,
|
||||||
|
creator: userId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 门诊/住院:与模板字段对齐(diagnosisName)
|
||||||
|
if ((caseType.value === 'outpatient' || caseType.value === 'inhospital') && form.diagnosis && !form.diagnosisName) {
|
||||||
|
params.diagnosisName = form.diagnosis;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortTimeKey = temp.value?.service?.timeTitle || '';
|
||||||
|
if (sortTimeKey && form[sortTimeKey] && dayjs(form[sortTimeKey]).isValid()) {
|
||||||
|
params.sortTime = dayjs(form[sortTimeKey]).valueOf();
|
||||||
|
} else {
|
||||||
|
params.sortTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
uniLoading('保存中...');
|
||||||
|
try {
|
||||||
|
const res = await api('addMedicalRecord', params);
|
||||||
|
hideLoading();
|
||||||
|
if (res.success) {
|
||||||
|
uni.$emit('archive-detail:visit-record-changed');
|
||||||
|
toast(res.message || '保存成功');
|
||||||
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
|
} else {
|
||||||
|
toast(res.message || '保存失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('保存病历失败:', error);
|
||||||
|
toast('保存失败,请重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: #f5f6f8;
|
||||||
|
padding-bottom: calc(152rpx + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.body {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.scroll {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.header-title {
|
||||||
|
padding: 28rpx 28rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.form-wrap {
|
||||||
|
background: #fff;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 8rpx 0;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: #fff;
|
||||||
|
padding: 24rpx 28rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 88rpx;
|
||||||
|
line-height: 88rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
}
|
||||||
|
.btn::after {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.btn.plain {
|
||||||
|
background: #fff;
|
||||||
|
color: #0877F1;
|
||||||
|
border: 2rpx solid #0877F1;
|
||||||
|
}
|
||||||
|
.btn.primary {
|
||||||
|
background: #0877F1;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -267,6 +267,12 @@ function isTagItem(i) {
|
|||||||
return title === 'tagIds' || title === 'tag' || type === 'tag' || name.includes('标签');
|
return title === 'tagIds' || title === 'tag' || type === 'tag' || name.includes('标签');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInHospitalSourceItem(i) {
|
||||||
|
const title = String(i?.title || '');
|
||||||
|
const name = String(i?.name || '');
|
||||||
|
return title === 'inHospitalSource' || title === 'hospitalSource' || name.includes('院内来源');
|
||||||
|
}
|
||||||
|
|
||||||
async function loadInternalTemplate() {
|
async function loadInternalTemplate() {
|
||||||
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
|
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
|
||||||
if (!corpId) return;
|
if (!corpId) return;
|
||||||
@ -304,6 +310,7 @@ async function loadInternalTemplate() {
|
|||||||
items.value = list
|
items.value = list
|
||||||
.filter((i) => i && i.fieldStatus !== 'disable')
|
.filter((i) => i && i.fieldStatus !== 'disable')
|
||||||
.filter((i) => i.operateType !== 'onlyRead')
|
.filter((i) => i.operateType !== 'onlyRead')
|
||||||
|
.filter((i) => !isInHospitalSourceItem(i))
|
||||||
.map(normalizeTemplateItem);
|
.map(normalizeTemplateItem);
|
||||||
|
|
||||||
const debugStage = items.value.find(isStageItem);
|
const debugStage = items.value.find(isStageItem);
|
||||||
@ -392,6 +399,20 @@ function buildPayload(base, inner) {
|
|||||||
return { payload, team };
|
return { payload, team };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCreatedCustomerId(res) {
|
||||||
|
const root = res && typeof res === 'object' ? res : {};
|
||||||
|
const d = root.data && typeof root.data === 'object' ? root.data : {};
|
||||||
|
return String(
|
||||||
|
d._id ||
|
||||||
|
d.id ||
|
||||||
|
d.customerId ||
|
||||||
|
d.memberId ||
|
||||||
|
root.customerId ||
|
||||||
|
root.memberId ||
|
||||||
|
''
|
||||||
|
) || '';
|
||||||
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (formRef.value?.verify && !formRef.value.verify()) return;
|
if (formRef.value?.verify && !formRef.value.verify()) return;
|
||||||
|
|
||||||
@ -420,10 +441,23 @@ async function save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast('新增成功');
|
toast('新增成功');
|
||||||
|
const createdId = getCreatedCustomerId(res);
|
||||||
uni.removeStorageSync(BASE_KEY);
|
uni.removeStorageSync(BASE_KEY);
|
||||||
uni.removeStorageSync(INNER_KEY);
|
uni.removeStorageSync(INNER_KEY);
|
||||||
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
|
uni.setStorageSync(NEED_RELOAD_STORAGE_KEY, 1);
|
||||||
uni.navigateBack({ delta: 2 });
|
uni.navigateBack({
|
||||||
|
delta: 2,
|
||||||
|
success: () => {
|
||||||
|
if (!createdId) return;
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({ url: `/pages/case/archive-detail?id=${encodeURIComponent(createdId)}` });
|
||||||
|
}, 30);
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
if (!createdId) return;
|
||||||
|
uni.redirectTo({ url: `/pages/case/archive-detail?id=${encodeURIComponent(createdId)}` });
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
toast('新增失败');
|
toast('新增失败');
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/service-record-detail/service-record-detail.vue -->
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/service-record-detail/service-record-detail.vue -->
|
||||||
<view class="body">
|
<view v-if="!recordId" class="body">
|
||||||
<scroll-view scroll-y class="scroll">
|
<scroll-view scroll-y class="scroll">
|
||||||
<view class="card">
|
<view class="card">
|
||||||
<view class="section-title">执行日期</view>
|
<view class="section-title">执行日期</view>
|
||||||
@ -31,12 +31,11 @@
|
|||||||
</picker>
|
</picker>
|
||||||
|
|
||||||
<view class="section-title">所属团队</view>
|
<view class="section-title">所属团队</view>
|
||||||
<picker mode="selector" :range="teamOptions" range-key="label" @change="pickTeam" :disabled="Boolean(recordId)">
|
<picker mode="selector" :range="teamOptions" range-key="label" :disabled="true">
|
||||||
<view class="picker-box between">
|
<view class="picker-box">
|
||||||
<view class="picker-text" :class="{ muted: !currentTeam?.value }">
|
<view class="picker-text" :class="{ muted: !currentTeam?.value }">
|
||||||
{{ currentTeam?.value ? currentTeam.label : '请选择所属团队' }}
|
{{ currentTeam?.value ? currentTeam.label : '请选择所属团队' }}
|
||||||
</view>
|
</view>
|
||||||
<uni-icons type="arrowright" size="16" color="#999" />
|
|
||||||
</view>
|
</view>
|
||||||
</picker>
|
</picker>
|
||||||
|
|
||||||
@ -55,14 +54,37 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="recordId" class="delete-fab" @click="remove">
|
<!-- 编辑:仅支持修改服务内容(底部弹层) -->
|
||||||
<uni-icons type="trash" size="22" color="#ff4d4f" />
|
<uni-popup v-else ref="editPopupRef" type="bottom" :mask-click="true" @maskClick="closeEdit">
|
||||||
</view>
|
<view class="sheet">
|
||||||
|
<view class="sheet-header">
|
||||||
|
<view class="sheet-header-left" />
|
||||||
|
<view class="sheet-title">修改服务内容</view>
|
||||||
|
<view class="sheet-close" @click="closeEdit">
|
||||||
|
<uni-icons type="closeempty" size="18" color="#333" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="sheet-body">
|
||||||
|
<textarea
|
||||||
|
v-model="form.taskContent"
|
||||||
|
class="sheet-textarea"
|
||||||
|
placeholder="请输入服务内容"
|
||||||
|
:maxlength="1000"
|
||||||
|
/>
|
||||||
|
<view class="counter">{{ (form.taskContent || '').length }}/1000</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="sheet-footer">
|
||||||
|
<button class="primary-btn" @click="saveEdit">保存</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</uni-popup>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref } from 'vue';
|
import { nextTick, reactive, ref } from 'vue';
|
||||||
import { onLoad } from '@dcloudio/uni-app';
|
import { onLoad } from '@dcloudio/uni-app';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@ -107,7 +129,8 @@ async function ensureDoctor() {
|
|||||||
function getUserId() {
|
function getUserId() {
|
||||||
const d = doctorInfo.value || {};
|
const d = doctorInfo.value || {};
|
||||||
const a = account.value || {};
|
const a = account.value || {};
|
||||||
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
|
const t = uni.getStorageSync('ykt_case_current_team') || {};
|
||||||
|
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || t.userId || t.userid || t.corpUserId || '') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCorpId() {
|
function getCorpId() {
|
||||||
@ -124,25 +147,6 @@ function getCurrentTeam() {
|
|||||||
return teamId ? { value: teamId, label: name || teamId } : null;
|
return teamId ? { value: teamId, label: name || teamId } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadTeams() {
|
|
||||||
await ensureDoctor();
|
|
||||||
const corpId = getCorpId();
|
|
||||||
const userId = getUserId();
|
|
||||||
if (!corpId || !userId) return;
|
|
||||||
const res = await api('getTeamBymember', { corpId, corpUserId: userId });
|
|
||||||
if (!res?.success) return;
|
|
||||||
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
|
|
||||||
teamOptions.value = list
|
|
||||||
.map((raw) => {
|
|
||||||
if (!raw || typeof raw !== 'object') return null;
|
|
||||||
const teamId = raw.teamId || raw.id || raw._id || '';
|
|
||||||
const name = raw.name || raw.teamName || raw.team || '';
|
|
||||||
if (!teamId) return null;
|
|
||||||
return { label: String(name || teamId), value: String(teamId) };
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
archiveId.value = options?.archiveId ? String(options.archiveId) : '';
|
||||||
recordId.value = options?.id ? String(options.id) : '';
|
recordId.value = options?.id ? String(options.id) : '';
|
||||||
@ -162,26 +166,22 @@ onLoad((options) => {
|
|||||||
const seedType = String(seed?.eventType || '') || '';
|
const seedType = String(seed?.eventType || '') || '';
|
||||||
currentType.value = typeOptions.find((i) => i.value === seedType) || null;
|
currentType.value = typeOptions.find((i) => i.value === seedType) || null;
|
||||||
|
|
||||||
const seedTeamId = String(seed?.executeTeamId || '') || '';
|
// 新建:团队固定为当前团队,不支持切换
|
||||||
const seedTeamName = String(seed?.executeTeamName || '') || '';
|
const cur = getCurrentTeam();
|
||||||
if (seedTeamId) currentTeam.value = { value: seedTeamId, label: seedTeamName || seedTeamId };
|
currentTeam.value = cur;
|
||||||
else currentTeam.value = getCurrentTeam();
|
teamOptions.value = cur ? [cur] : [];
|
||||||
|
|
||||||
loadTeams().then(() => {
|
if (recordId.value) {
|
||||||
if (seedTeamId) currentTeam.value = teamOptions.value.find((t) => t.value === seedTeamId) || currentTeam.value;
|
uni.setNavigationBarTitle({ title: '修改服务内容' });
|
||||||
else {
|
nextTick(() => editPopupRef.value?.open?.());
|
||||||
const cur = getCurrentTeam();
|
} else {
|
||||||
if (cur) currentTeam.value = teamOptions.value.find((t) => t.value === cur.value) || cur;
|
uni.setNavigationBarTitle({ title: '新建服务记录' });
|
||||||
}
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function pickType(e) {
|
function pickType(e) {
|
||||||
currentType.value = typeOptions[e.detail.value] || null;
|
currentType.value = typeOptions[e.detail.value] || null;
|
||||||
}
|
}
|
||||||
function pickTeam(e) {
|
|
||||||
currentTeam.value = teamOptions.value[e.detail.value] || null;
|
|
||||||
}
|
|
||||||
function pickDate(e) {
|
function pickDate(e) {
|
||||||
date.value = e.detail.value || '';
|
date.value = e.detail.value || '';
|
||||||
}
|
}
|
||||||
@ -205,6 +205,45 @@ function save() {
|
|||||||
submit(executionTime);
|
submit(executionTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const editPopupRef = ref(null);
|
||||||
|
function closeEdit() {
|
||||||
|
editPopupRef.value?.close?.();
|
||||||
|
setTimeout(() => uni.navigateBack(), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveEdit() {
|
||||||
|
if (!String(form.taskContent || '').trim()) return uni.showToast({ title: '请输入服务内容', icon: 'none' });
|
||||||
|
submitEdit();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitEdit() {
|
||||||
|
await ensureDoctor();
|
||||||
|
const corpId = getCorpId();
|
||||||
|
const userId = getUserId();
|
||||||
|
if (!corpId || !userId) {
|
||||||
|
toast('缺少用户/团队信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!recordId.value) return;
|
||||||
|
|
||||||
|
const res = await api('updateServiceRecord', {
|
||||||
|
corpId,
|
||||||
|
id: recordId.value,
|
||||||
|
params: {
|
||||||
|
taskContent: form.taskContent,
|
||||||
|
updateUserId: userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res?.success) {
|
||||||
|
toast(res?.message || '修改失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.$emit('archive-detail:service-record-changed');
|
||||||
|
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||||
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
|
}
|
||||||
|
|
||||||
async function submit(executionTime) {
|
async function submit(executionTime) {
|
||||||
await ensureDoctor();
|
await ensureDoctor();
|
||||||
const corpId = getCorpId();
|
const corpId = getCorpId();
|
||||||
@ -252,27 +291,6 @@ async function submit(executionTime) {
|
|||||||
uni.showToast({ title: '保存成功', icon: 'success' });
|
uni.showToast({ title: '保存成功', icon: 'success' });
|
||||||
setTimeout(() => uni.navigateBack(), 300);
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove() {
|
|
||||||
uni.showModal({
|
|
||||||
title: '提示',
|
|
||||||
content: '确定删除当前记录?',
|
|
||||||
success: async (res) => {
|
|
||||||
if (!res.confirm) return;
|
|
||||||
await ensureDoctor();
|
|
||||||
const corpId = getCorpId();
|
|
||||||
if (!corpId || !recordId.value) return;
|
|
||||||
const resp = await api('removeServiceRecord', { corpId, id: recordId.value });
|
|
||||||
if (!resp?.success) {
|
|
||||||
toast(resp?.message || '删除失败');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uni.$emit('archive-detail:service-record-changed');
|
|
||||||
uni.showToast({ title: '已删除', icon: 'success' });
|
|
||||||
setTimeout(() => uni.navigateBack(), 300);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -377,18 +395,60 @@ function remove() {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-fab {
|
.sheet {
|
||||||
position: fixed;
|
|
||||||
right: 16px;
|
|
||||||
bottom: calc(96px + env(safe-area-inset-bottom));
|
|
||||||
width: 52px;
|
|
||||||
height: 52px;
|
|
||||||
border-radius: 26px;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
border-top-left-radius: 14px;
|
||||||
|
border-top-right-radius: 14px;
|
||||||
|
padding-bottom: calc(14px + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
.sheet-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.sheet-header-left {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
.sheet-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.sheet-close {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 10px 18px rgba(0, 0, 0, 0.12);
|
}
|
||||||
z-index: 30;
|
.sheet-body {
|
||||||
|
padding: 14px 16px 0;
|
||||||
|
}
|
||||||
|
.sheet-textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #e6e6e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.sheet-footer {
|
||||||
|
padding: 12px 16px 0;
|
||||||
|
}
|
||||||
|
.primary-btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #0877F1;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.primary-btn::after {
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
94
pages/case/utils/template.js
Normal file
94
pages/case/utils/template.js
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
function normalizeOptions(options) {
|
||||||
|
if (!Array.isArray(options)) return [];
|
||||||
|
if (!options.length) return [];
|
||||||
|
if (typeof options[0] === 'string') return options.filter((i) => typeof i === 'string');
|
||||||
|
if (typeof options[0] === 'object') {
|
||||||
|
return options
|
||||||
|
.map((i) => {
|
||||||
|
const label = i?.label ?? i?.name ?? i?.text ?? i?.title ?? '';
|
||||||
|
const value = i?.value ?? i?.id ?? i?.key ?? label;
|
||||||
|
if (!label && (value === undefined || value === null || value === '')) return null;
|
||||||
|
return { label: String(label || value), value: String(value) };
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTemplateItem(item) {
|
||||||
|
const next = { ...(item || {}) };
|
||||||
|
|
||||||
|
if (next.operateType === 'custom') next.operateType = 'formCell';
|
||||||
|
|
||||||
|
const originalType = next.type;
|
||||||
|
const customTypeMap = {
|
||||||
|
customerSource: 'select',
|
||||||
|
customerStage: 'select',
|
||||||
|
tag: 'multiSelectAndOther',
|
||||||
|
reference: 'input',
|
||||||
|
selectWwuser: 'select',
|
||||||
|
files: 'files',
|
||||||
|
corpProject: 'select',
|
||||||
|
diagnosis: 'diagnosis',
|
||||||
|
BMI: 'input',
|
||||||
|
bloodPressure: 'textarea',
|
||||||
|
selfMultipleDiseases: 'textarea',
|
||||||
|
};
|
||||||
|
if (originalType && customTypeMap[originalType]) {
|
||||||
|
next.__originType = originalType;
|
||||||
|
next.type = customTypeMap[originalType];
|
||||||
|
}
|
||||||
|
|
||||||
|
const aliasTypeMap = {
|
||||||
|
text: 'input',
|
||||||
|
string: 'input',
|
||||||
|
number: 'input',
|
||||||
|
integer: 'input',
|
||||||
|
int: 'input',
|
||||||
|
};
|
||||||
|
if (next.type && aliasTypeMap[next.type]) {
|
||||||
|
next.type = aliasTypeMap[next.type];
|
||||||
|
if (!next.inputType && (originalType === 'number' || originalType === 'integer' || originalType === 'int')) next.inputType = 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawRange = next.range || next.options || next.optionList || next.values || [];
|
||||||
|
const range = normalizeOptions(rawRange);
|
||||||
|
|
||||||
|
if (next.type === 'select' || next.type === 'selectAndOther' || next.type === 'selectAndImage') {
|
||||||
|
next.range = range;
|
||||||
|
} else if (next.type === 'radio') {
|
||||||
|
// wxapp 目前 radio 组件只支持字符串列表;模板如为对象选项则降级为 select
|
||||||
|
if (range.length && typeof range[0] === 'object') {
|
||||||
|
next.type = 'select';
|
||||||
|
next.range = range;
|
||||||
|
} else {
|
||||||
|
next.range = Array.isArray(rawRange) ? rawRange : [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!next.operateType) next.operateType = 'formCell';
|
||||||
|
next.required = Boolean(next.required);
|
||||||
|
|
||||||
|
if (next.type === 'input' && (next.wordLimit === undefined || next.wordLimit === null || next.wordLimit === '')) next.wordLimit = 20;
|
||||||
|
if (next.type === 'textarea' && (next.wordLimit === undefined || next.wordLimit === null || next.wordLimit === '')) next.wordLimit = 200;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unwrapTemplateResponse(res) {
|
||||||
|
const d = res?.data;
|
||||||
|
if (d && typeof d === 'object') {
|
||||||
|
if (d.data && typeof d.data === 'object') return d.data;
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
return res && typeof res === 'object' ? res : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTemplate(temp) {
|
||||||
|
const t = temp && typeof temp === 'object' ? { ...temp } : {};
|
||||||
|
const list = Array.isArray(t.templateList) ? t.templateList : [];
|
||||||
|
t.templateList = list
|
||||||
|
.filter((i) => i && i.fieldStatus !== 'disable')
|
||||||
|
.filter((i) => i.operateType !== 'onlyRead')
|
||||||
|
.map(normalizeTemplateItem);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
40
pages/case/utils/visit-record.js
Normal file
40
pages/case/utils/visit-record.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
function isEmpty(v) {
|
||||||
|
if (v === null || v === undefined) return true;
|
||||||
|
if (Array.isArray(v)) return v.length === 0;
|
||||||
|
if (typeof v === 'string') return v.trim() === '';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ALIAS_MAP = {
|
||||||
|
outpatient: {
|
||||||
|
diagnosisName: 'diagnosis',
|
||||||
|
medicalHistorySummary: 'medicalHistory',
|
||||||
|
},
|
||||||
|
inhospital: {
|
||||||
|
diagnosisName: 'diagnosis',
|
||||||
|
medicalHistorySummary: 'medicalHistory',
|
||||||
|
operationDate: 'surgeryDate',
|
||||||
|
operation: 'surgeryName',
|
||||||
|
},
|
||||||
|
physicalExaminationTemplate: {
|
||||||
|
inspectTime: 'inspectDate',
|
||||||
|
inspectSummary: 'summary',
|
||||||
|
},
|
||||||
|
preConsultation: {
|
||||||
|
presentIllnessHistory: 'presentIllness',
|
||||||
|
pastMedicalHistory: 'pastHistory',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeVisitRecordFormData(templateType, raw) {
|
||||||
|
const input = raw && typeof raw === 'object' ? raw : {};
|
||||||
|
const out = { ...input };
|
||||||
|
const map = ALIAS_MAP[String(templateType || '')] || {};
|
||||||
|
|
||||||
|
Object.entries(map).forEach(([from, to]) => {
|
||||||
|
if (isEmpty(out[to]) && !isEmpty(out[from])) out[to] = out[from];
|
||||||
|
});
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
@ -3,29 +3,8 @@
|
|||||||
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/visit-record-detail/visit-record-detail.vue -->
|
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/visit-record-detail/visit-record-detail.vue -->
|
||||||
<view class="body">
|
<view class="body">
|
||||||
<scroll-view scroll-y class="scroll">
|
<scroll-view scroll-y class="scroll">
|
||||||
<view class="header">
|
|
||||||
<view class="header-title">{{ template?.templateName || '健康档案' }}</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-wrap">
|
<view class="form-wrap">
|
||||||
<FormTemplate v-if="template" ref="formRef" :items="showItems" :form="forms" @change="onChange" />
|
<FormTemplate v-if="temp" ref="formRef" :items="showItems" :form="forms" @change="onChange" />
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 附件上传(FormTemplate 不支持 files,单独实现) -->
|
|
||||||
<view v-if="hasFilesField" class="upload-wrap">
|
|
||||||
<view class="upload-row">
|
|
||||||
<view class="upload-label">文件上传</view>
|
|
||||||
<view class="upload-desc">(支持≤5M文件,pdf文件格式)</view>
|
|
||||||
</view>
|
|
||||||
<view class="upload-grid">
|
|
||||||
<view v-for="(f, idx) in fileList" :key="idx" class="upload-item" @click="previewFile(idx)">
|
|
||||||
<image class="upload-thumb" :src="f.url" mode="aspectFill" />
|
|
||||||
<view class="upload-remove" @click.stop="removeFile(idx)">×</view>
|
|
||||||
</view>
|
|
||||||
<view class="upload-add" @click="addFiles">
|
|
||||||
<view class="plus">+</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view style="height: 240rpx;"></view>
|
<view style="height: 240rpx;"></view>
|
||||||
@ -49,10 +28,11 @@ import { onLoad } from '@dcloudio/uni-app';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import FormTemplate from '@/components/form-template/index.vue';
|
import FormTemplate from '@/components/form-template/index.vue';
|
||||||
import { getVisitRecordTemplate } from './components/archive-detail/templates';
|
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import useAccountStore from '@/store/account';
|
import useAccountStore from '@/store/account';
|
||||||
import { toast, confirm, loading as uniLoading, hideLoading } from '@/utils/widget';
|
import { toast, confirm, loading as uniLoading, hideLoading } from '@/utils/widget';
|
||||||
|
import { normalizeVisitRecordFormData } from './utils/visit-record';
|
||||||
|
import { normalizeTemplate, unwrapTemplateResponse } from './utils/template';
|
||||||
|
|
||||||
const accountStore = useAccountStore();
|
const accountStore = useAccountStore();
|
||||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||||
@ -84,17 +64,43 @@ function getCorpId() {
|
|||||||
const memberId = ref('');
|
const memberId = ref('');
|
||||||
const recordId = ref('');
|
const recordId = ref('');
|
||||||
const templateType = ref('');
|
const templateType = ref('');
|
||||||
const customerName = ref('');
|
|
||||||
|
|
||||||
const template = computed(() => getVisitRecordTemplate(templateType.value));
|
const temp = ref(null);
|
||||||
|
const titleText = computed(() => {
|
||||||
|
const t = temp.value || {};
|
||||||
|
return String(t.name || t.templateName || t.templateTypeName || '').trim();
|
||||||
|
});
|
||||||
const detail = ref({});
|
const detail = ref({});
|
||||||
const form = reactive({});
|
const form = reactive({});
|
||||||
const forms = computed(() => ({ ...detail.value, ...form }));
|
const forms = computed(() => ({ ...detail.value, ...form }));
|
||||||
|
const HIDDEN_FIELD_NAMES = {
|
||||||
|
outpatient: ['就诊机构', '就诊科室', '机构名称', '责任医生'],
|
||||||
|
inhospital: ['就诊机构', '机构名称'],
|
||||||
|
};
|
||||||
|
const HIDDEN_FIELD_TITLES = {
|
||||||
|
// 对应 systemFieldName/title(来自模板):
|
||||||
|
// - 就诊机构: corp
|
||||||
|
// - 就诊科室: deptName
|
||||||
|
// - 机构名称: corpName
|
||||||
|
// - 责任医生: doctor
|
||||||
|
outpatient: ['corp', 'deptName', 'corpName', 'doctor'],
|
||||||
|
// - 就诊机构: corp
|
||||||
|
// - 机构名称: corpName
|
||||||
|
inhospital: ['corp', 'corpName'],
|
||||||
|
};
|
||||||
|
function shouldHideField(item) {
|
||||||
|
const t = String(templateType.value || '');
|
||||||
|
const name = item?.name ? String(item.name).trim() : '';
|
||||||
|
const title = item?.title ? String(item.title).trim() : '';
|
||||||
|
const hiddenNames = HIDDEN_FIELD_NAMES[t] || [];
|
||||||
|
const hiddenTitles = HIDDEN_FIELD_TITLES[t] || [];
|
||||||
|
return (name && hiddenNames.includes(name)) || (title && hiddenTitles.includes(title));
|
||||||
|
}
|
||||||
const showItems = computed(() => {
|
const showItems = computed(() => {
|
||||||
const list = template.value?.templateList || [];
|
const list = temp.value?.templateList || [];
|
||||||
// referenceField 兼容(与 mobile 一致)
|
// referenceField 兼容(与 mobile 一致)
|
||||||
return list.filter((i) => {
|
return list.filter((i) => {
|
||||||
if (i?.type === 'files') return false;
|
if (shouldHideField(i)) return false;
|
||||||
if (i && typeof i.referenceField === 'string') {
|
if (i && typeof i.referenceField === 'string') {
|
||||||
return forms.value[i.referenceField] === i.referenceValue;
|
return forms.value[i.referenceField] === i.referenceValue;
|
||||||
}
|
}
|
||||||
@ -102,16 +108,7 @@ const showItems = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const hasFilesField = computed(() => {
|
|
||||||
const list = template.value?.templateList || [];
|
|
||||||
return list.some((i) => i && (i.type === 'files' || i.title === 'files'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const formRef = ref(null);
|
const formRef = ref(null);
|
||||||
const fileList = computed(() => {
|
|
||||||
const arr = forms.value?.files;
|
|
||||||
return Array.isArray(arr) ? arr.filter((i) => i && i.url) : [];
|
|
||||||
});
|
|
||||||
|
|
||||||
function ensureFilesField() {
|
function ensureFilesField() {
|
||||||
if (form.files !== undefined) return;
|
if (form.files !== undefined) return;
|
||||||
@ -119,30 +116,43 @@ function ensureFilesField() {
|
|||||||
form.files = [];
|
form.files = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadTemplate(t) {
|
||||||
|
const corpId = getCorpId();
|
||||||
|
if (!corpId) return null;
|
||||||
|
try {
|
||||||
|
const res = await api('getCurrentTemplate', { corpId, templateType: t });
|
||||||
|
if (!res?.success) {
|
||||||
|
toast(res?.message || '获取模板失败');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const raw = unwrapTemplateResponse(res);
|
||||||
|
return normalizeTemplate(raw);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('loadTemplate error:', e);
|
||||||
|
toast('获取模板失败');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onLoad(async (options) => {
|
onLoad(async (options) => {
|
||||||
memberId.value = options?.memberId || options?.archiveId || '';
|
memberId.value = options?.memberId || options?.archiveId || '';
|
||||||
recordId.value = options?.id || '';
|
recordId.value = options?.id || '';
|
||||||
templateType.value = options?.type || '';
|
templateType.value = options?.type || '';
|
||||||
customerName.value = decodeURIComponent(options?.customerName || '');
|
|
||||||
|
|
||||||
if (recordId.value) {
|
if (recordId.value) {
|
||||||
await getDetail();
|
await getDetail();
|
||||||
} else {
|
} else {
|
||||||
if (!templateType.value) templateType.value = 'outpatient';
|
if (!templateType.value) templateType.value = 'outpatient';
|
||||||
|
temp.value = await loadTemplate(templateType.value);
|
||||||
|
if (temp.value?.templateType) templateType.value = String(temp.value.templateType);
|
||||||
ensureFilesField();
|
ensureFilesField();
|
||||||
// 门诊记录默认今日日期
|
|
||||||
if (templateType.value === 'outpatient') {
|
// 默认填充模板时间字段
|
||||||
form.visitTime = dayjs().format('YYYY-MM-DD');
|
const timeKey = temp.value?.service?.timeTitle || '';
|
||||||
}
|
if (timeKey && !form[timeKey]) form[timeKey] = dayjs().format('YYYY-MM-DD');
|
||||||
// 住院记录默认今日日期
|
|
||||||
if (templateType.value === 'inhospital') {
|
|
||||||
form.inhosDate = dayjs().format('YYYY-MM-DD');
|
|
||||||
}
|
|
||||||
// 体检记录默认今日日期
|
|
||||||
if (templateType.value === 'physicalExaminationTemplate') {
|
|
||||||
form.inspectDate = dayjs().format('YYYY-MM-DD');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (titleText.value) uni.setNavigationBarTitle({ title: titleText.value });
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getDetail() {
|
async function getDetail() {
|
||||||
@ -162,14 +172,14 @@ async function getDetail() {
|
|||||||
const record = res?.record || res?.data?.record || null;
|
const record = res?.record || res?.data?.record || null;
|
||||||
if (res?.success && record) {
|
if (res?.success && record) {
|
||||||
templateType.value = record.templateType || record.medicalType || templateType.value;
|
templateType.value = record.templateType || record.medicalType || templateType.value;
|
||||||
|
detail.value = normalizeVisitRecordFormData(templateType.value, record);
|
||||||
// 兼容模板字段:wxapp 使用 diagnosis,但接口通常返回 diagnosisName
|
|
||||||
if ((record.medicalType === 'outpatient' || record.medicalType === 'inhospital') && !record.diagnosis && record.diagnosisName) {
|
|
||||||
record.diagnosis = record.diagnosisName;
|
|
||||||
}
|
|
||||||
|
|
||||||
detail.value = record;
|
|
||||||
ensureFilesField();
|
ensureFilesField();
|
||||||
|
// 详情可能返回真实 templateType:与模板保持一致
|
||||||
|
if (!temp.value || temp.value?.templateType !== templateType.value) {
|
||||||
|
temp.value = await loadTemplate(templateType.value);
|
||||||
|
if (temp.value?.templateType) templateType.value = String(temp.value.templateType);
|
||||||
|
if (titleText.value) uni.setNavigationBarTitle({ title: titleText.value });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast(res.message || '加载失败');
|
toast(res.message || '加载失败');
|
||||||
}
|
}
|
||||||
@ -185,7 +195,7 @@ function onChange({ title, value }) {
|
|||||||
const item = showItems.value.find((i) => i.title === title);
|
const item = showItems.value.find((i) => i.title === title);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
// 关联字段变化时清理被关联字段(与 mobile 行为一致)
|
// 关联字段变化时清理被关联字段(与 mobile 行为一致)
|
||||||
const relat = (template.value?.templateList || []).filter((i) => i.referenceField === title);
|
const relat = (temp.value?.templateList || []).filter((i) => i.referenceField === title);
|
||||||
relat.forEach((i) => (form[i.title] = ''));
|
relat.forEach((i) => (form[i.title] = ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +234,7 @@ async function save() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sortTime:使用模板中的时间字段
|
// sortTime:使用模板中的时间字段
|
||||||
const sortTimeKey = template.value?.service?.timeTitle || '';
|
const sortTimeKey = temp.value?.service?.timeTitle || '';
|
||||||
if (sortTimeKey && form[sortTimeKey] && dayjs(form[sortTimeKey]).isValid()) {
|
if (sortTimeKey && form[sortTimeKey] && dayjs(form[sortTimeKey]).isValid()) {
|
||||||
params.sortTime = dayjs(form[sortTimeKey]).valueOf();
|
params.sortTime = dayjs(form[sortTimeKey]).valueOf();
|
||||||
} else {
|
} else {
|
||||||
@ -252,76 +262,39 @@ async function save() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function remove() {
|
async function remove() {
|
||||||
confirm('确定删除当前记录?', async () => {
|
try {
|
||||||
if (!memberId.value || !recordId.value) return toast('缺少必要信息');
|
await confirm('确定删除当前记录?');
|
||||||
await ensureDoctor();
|
} catch {
|
||||||
const corpId = getCorpId();
|
return;
|
||||||
if (!corpId) return toast('缺少必要信息');
|
}
|
||||||
uniLoading('删除中...');
|
if (!memberId.value || !recordId.value) return toast('缺少必要信息');
|
||||||
try {
|
await ensureDoctor();
|
||||||
const res = await api('removeMedicalRecord', {
|
const corpId = getCorpId();
|
||||||
corpId,
|
if (!corpId) return toast('缺少必要信息');
|
||||||
memberId: memberId.value,
|
uniLoading('删除中...');
|
||||||
medicalType: templateType.value,
|
try {
|
||||||
_id: recordId.value,
|
const res = await api('removeMedicalRecord', {
|
||||||
});
|
corpId,
|
||||||
hideLoading();
|
memberId: memberId.value,
|
||||||
if (res.success) {
|
medicalType: templateType.value,
|
||||||
uni.$emit('archive-detail:visit-record-changed');
|
_id: recordId.value,
|
||||||
toast(res.message || '已删除');
|
});
|
||||||
setTimeout(() => uni.navigateBack(), 300);
|
hideLoading();
|
||||||
} else {
|
if (res.success) {
|
||||||
toast(res.message || '删除失败');
|
uni.$emit('archive-detail:visit-record-changed');
|
||||||
}
|
toast(res.message || '已删除');
|
||||||
} catch (error) {
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
hideLoading();
|
} else {
|
||||||
console.error('remove error:', error);
|
toast(res.message || '删除失败');
|
||||||
toast('删除失败');
|
|
||||||
}
|
}
|
||||||
});
|
} catch (error) {
|
||||||
|
hideLoading();
|
||||||
|
console.error('remove error:', error);
|
||||||
|
toast('删除失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function addFiles() {
|
|
||||||
const fileConfig = template.value?.templateList?.find(i => i.type === 'files');
|
|
||||||
const maxSize = fileConfig?.maxSize || 5; // MB
|
|
||||||
const accept = fileConfig?.accept || 'pdf';
|
|
||||||
|
|
||||||
uni.chooseMessageFile({
|
|
||||||
count: 9,
|
|
||||||
type: 'file',
|
|
||||||
extension: [accept],
|
|
||||||
success: (res) => {
|
|
||||||
const files = Array.isArray(res.tempFiles) ? res.tempFiles : [];
|
|
||||||
const maxBytes = maxSize * 1024 * 1024;
|
|
||||||
|
|
||||||
// 验证文件大小
|
|
||||||
const invalidFiles = files.filter(f => f.size > maxBytes);
|
|
||||||
if (invalidFiles.length > 0) {
|
|
||||||
toast(`文件大小不能超过${maxSize}M`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const next = files.map((f) => ({
|
|
||||||
url: f.path,
|
|
||||||
name: f.name || '',
|
|
||||||
size: f.size
|
|
||||||
}));
|
|
||||||
const cur = Array.isArray(forms.value.files) ? forms.value.files : [];
|
|
||||||
form.files = [...cur, ...next];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeFile(idx) {
|
|
||||||
const cur = Array.isArray(forms.value.files) ? forms.value.files : [];
|
|
||||||
form.files = cur.filter((_, i) => i !== idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
function previewFile(idx) {
|
|
||||||
const urls = fileList.value.map((i) => i.url);
|
|
||||||
uni.previewImage({ urls, current: urls[idx] });
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -354,71 +327,6 @@ function previewFile(idx) {
|
|||||||
padding: 8rpx 0;
|
padding: 8rpx 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-wrap {
|
|
||||||
background: #fff;
|
|
||||||
padding: 24rpx 30rpx;
|
|
||||||
border-bottom: 2rpx solid #eee;
|
|
||||||
}
|
|
||||||
.upload-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
margin-bottom: 18rpx;
|
|
||||||
}
|
|
||||||
.upload-label {
|
|
||||||
font-size: 28rpx;
|
|
||||||
line-height: 42rpx;
|
|
||||||
color: #111827;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.upload-desc {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #9ca3af;
|
|
||||||
margin-left: 8rpx;
|
|
||||||
}
|
|
||||||
.upload-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 18rpx;
|
|
||||||
}
|
|
||||||
.upload-item {
|
|
||||||
width: 180rpx;
|
|
||||||
height: 140rpx;
|
|
||||||
position: relative;
|
|
||||||
border: 2rpx solid #e5e7eb;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #f9fafb;
|
|
||||||
}
|
|
||||||
.upload-thumb {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.upload-remove {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 36rpx;
|
|
||||||
height: 36rpx;
|
|
||||||
line-height: 36rpx;
|
|
||||||
text-align: center;
|
|
||||||
background: rgba(0, 0, 0, 0.55);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
.upload-add {
|
|
||||||
width: 180rpx;
|
|
||||||
height: 140rpx;
|
|
||||||
border: 2rpx dashed #d1d5db;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
.plus {
|
|
||||||
font-size: 52rpx;
|
|
||||||
line-height: 52rpx;
|
|
||||||
}
|
|
||||||
.footer {
|
.footer {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|||||||
@ -12,10 +12,10 @@
|
|||||||
<view class="value">{{ typeLabel }}</view>
|
<view class="value">{{ typeLabel }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="row">
|
<view class="row">
|
||||||
<view class="label">就诊日期</view>
|
<view class="label">{{ visitDateLabel }}</view>
|
||||||
<view class="value">{{ visitDate || '--' }}</view>
|
<view class="value">{{ visitDate || '--' }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="row">
|
<view v-if="showDiagnosisRow" class="row">
|
||||||
<view class="label">诊断</view>
|
<view class="label">诊断</view>
|
||||||
<view class="value">{{ diagnosisText }}</view>
|
<view class="value">{{ diagnosisText }}</view>
|
||||||
</view>
|
</view>
|
||||||
@ -30,7 +30,7 @@
|
|||||||
<view class="p">{{ s.value }}</view>
|
<view class="p">{{ s.value }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="section">
|
<view v-if="showFilesSection" class="section">
|
||||||
<view class="h2">文件上传</view>
|
<view class="h2">文件上传</view>
|
||||||
<view v-if="files.length" class="files">
|
<view v-if="files.length" class="files">
|
||||||
<view v-for="(f, idx) in files" :key="idx" class="file" @click="preview(idx)">
|
<view v-for="(f, idx) in files" :key="idx" class="file" @click="preview(idx)">
|
||||||
@ -55,24 +55,70 @@ import dayjs from 'dayjs';
|
|||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import { loading, hideLoading, toast } from '@/utils/widget';
|
import { loading, hideLoading, toast } from '@/utils/widget';
|
||||||
import { getVisitRecordTemplate } from './components/archive-detail/templates';
|
import { getVisitRecordTemplate } from './components/archive-detail/templates';
|
||||||
|
import { normalizeVisitRecordFormData } from './utils/visit-record';
|
||||||
|
import { normalizeTemplate, unwrapTemplateResponse } from './utils/template';
|
||||||
|
import { normalizeFileUrl } from '@/utils/file';
|
||||||
|
|
||||||
const archiveId = ref('');
|
const archiveId = ref('');
|
||||||
const id = ref('');
|
const id = ref('');
|
||||||
const medicalType = ref('');
|
const medicalType = ref('');
|
||||||
|
const rawType = ref('');
|
||||||
const record = ref({});
|
const record = ref({});
|
||||||
|
const temp = ref(null);
|
||||||
|
|
||||||
const files = computed(() => {
|
const files = computed(() => {
|
||||||
const arr = record.value?.files;
|
const arr = record.value?.files;
|
||||||
return Array.isArray(arr) ? arr.filter((i) => i && i.url) : [];
|
return Array.isArray(arr)
|
||||||
|
? arr
|
||||||
|
.filter((i) => i && i.url)
|
||||||
|
.map((i) => ({ ...i, url: normalizeFileUrl(i.url) }))
|
||||||
|
: [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const templateType = computed(() => record.value?.templateType || record.value?.medicalType || '');
|
function normalizeMedicalType(raw) {
|
||||||
|
const s = String(raw || '').trim();
|
||||||
|
if (!s) return '';
|
||||||
|
const lower = s.toLowerCase();
|
||||||
|
if (lower.includes('preconsult') || (lower.includes('pre') && lower.includes('consult'))) return 'preConsultation';
|
||||||
|
if (lower === 'outpatient' || lower === 'out_patient' || lower === 'out-patient') return 'outpatient';
|
||||||
|
if (lower === 'inhospital' || lower === 'in_hospital' || lower === 'in-hospital' || lower === 'inpatient') return 'inhospital';
|
||||||
|
if (lower === 'preconsultation' || lower === 'pre_consultation' || lower === 'pre-consultation') return 'preConsultation';
|
||||||
|
if (lower === 'physicalexaminationtemplate' || lower === 'physicalexamination' || lower === 'physical_examination') return 'physicalExaminationTemplate';
|
||||||
|
if (s === 'outPatient') return 'outpatient';
|
||||||
|
if (s === 'inHospital') return 'inhospital';
|
||||||
|
if (s === 'preConsultation') return 'preConsultation';
|
||||||
|
if (s === 'physicalExaminationTemplate') return 'physicalExaminationTemplate';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
const typeLabel = computed(() => record.value?.tempName || getVisitRecordTemplate(templateType.value || medicalType.value)?.templateName || '病历');
|
const templateType = computed(() => normalizeMedicalType(rawType.value || medicalType.value || record.value?.templateType || record.value?.medicalType || ''));
|
||||||
|
|
||||||
|
const typeLabel = computed(() => record.value?.tempName || temp.value?.name || getVisitRecordTemplate(templateType.value || medicalType.value)?.templateName || '病历');
|
||||||
|
|
||||||
|
function getDefaultTimeTitle(t) {
|
||||||
|
if (t === 'outpatient') return 'visitTime';
|
||||||
|
if (t === 'inhospital') return 'inhosDate';
|
||||||
|
if (t === 'preConsultation') return 'consultDate';
|
||||||
|
if (t === 'physicalExaminationTemplate') return 'inspectDate';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultTimeName(t) {
|
||||||
|
if (t === 'outpatient') return '就诊日期';
|
||||||
|
if (t === 'inhospital') return '入院日期';
|
||||||
|
if (t === 'preConsultation') return '问诊日期';
|
||||||
|
if (t === 'physicalExaminationTemplate') return '体检日期';
|
||||||
|
return '日期';
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeText(v) {
|
function normalizeText(v) {
|
||||||
if (Array.isArray(v)) return v.filter((i) => i !== null && i !== undefined && String(i).trim()).join(',');
|
if (Array.isArray(v)) return v.filter((i) => i !== null && i !== undefined && String(i).trim()).join(',');
|
||||||
if (v === 0) return '0';
|
if (v === 0) return '0';
|
||||||
|
if (v && typeof v === 'object') {
|
||||||
|
const o = v;
|
||||||
|
const candidate = o.label ?? o.name ?? o.text ?? o.title ?? o.value ?? o.code ?? '';
|
||||||
|
return candidate ? String(candidate) : '';
|
||||||
|
}
|
||||||
return v ? String(v) : '';
|
return v ? String(v) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,68 +147,139 @@ function getCorpId() {
|
|||||||
return team?.corpId ? String(team.corpId) : '';
|
return team?.corpId ? String(team.corpId) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseAnyTimeMs(v) {
|
||||||
|
if (v === null || v === undefined) return 0;
|
||||||
|
if (typeof v === 'number') {
|
||||||
|
// 10位秒级时间戳
|
||||||
|
if (v > 1e9 && v < 1e12) return v * 1000;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
const s = String(v).trim();
|
||||||
|
if (!s) return 0;
|
||||||
|
if (/^\d{10,13}$/.test(s)) return Number(s.length === 10 ? `${s}000` : s);
|
||||||
|
const d = dayjs(s);
|
||||||
|
return d.isValid() ? d.valueOf() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAnyDate(v, fmt = 'YYYY-MM-DD') {
|
||||||
|
const ms = parseAnyTimeMs(v);
|
||||||
|
if (!ms) return '';
|
||||||
|
const d = dayjs(ms);
|
||||||
|
return d.isValid() ? d.format(fmt) : '';
|
||||||
|
}
|
||||||
|
|
||||||
const visitDate = computed(() => {
|
const visitDate = computed(() => {
|
||||||
const t = templateType.value;
|
const t = templateType.value;
|
||||||
if (t === 'outpatient') return record.value?.visitTime || '';
|
const timeTitle = temp.value?.service?.timeTitle || getDefaultTimeTitle(t);
|
||||||
if (t === 'inhospital') return record.value?.inhosDate || '';
|
const raw = timeTitle ? record.value?.[timeTitle] : (record.value?.dateStr ?? record.value?.sortTime ?? '');
|
||||||
if (t === 'preConsultation') return record.value?.consultDate || '';
|
return formatAnyDate(raw) || normalizeText(raw) || '';
|
||||||
if (t === 'physicalExaminationTemplate') return record.value?.inspectDate || '';
|
});
|
||||||
return record.value?.dateStr || '';
|
|
||||||
|
const visitDateLabel = computed(() => {
|
||||||
|
const t = templateType.value;
|
||||||
|
return String(temp.value?.service?.timeName || getDefaultTimeName(t) || '日期');
|
||||||
|
});
|
||||||
|
|
||||||
|
const showDiagnosisRow = computed(() => {
|
||||||
|
const list = Array.isArray(temp.value?.templateList)
|
||||||
|
? temp.value.templateList
|
||||||
|
: Array.isArray(getVisitRecordTemplate(templateType.value)?.templateList)
|
||||||
|
? getVisitRecordTemplate(templateType.value).templateList
|
||||||
|
: [];
|
||||||
|
return list.some((i) => i && (i.title === 'diagnosis' || i.title === 'diagnosisName'));
|
||||||
});
|
});
|
||||||
|
|
||||||
const diagnosisText = computed(() => {
|
const diagnosisText = computed(() => {
|
||||||
const t = templateType.value;
|
const t = templateType.value;
|
||||||
if (t === 'preConsultation') return normalizeText(record.value?.chiefComplaint) || normalizeText(record.value?.summary) || '--';
|
if (!showDiagnosisRow.value) return '--';
|
||||||
if (t === 'physicalExaminationTemplate') return formatPositiveFind(record.value?.positiveFind) || normalizeText(record.value?.summary) || '--';
|
|
||||||
if (t === 'outpatient' || t === 'inhospital') return normalizeText(record.value?.diagnosisName || record.value?.diagnosis) || normalizeText(record.value?.summary) || '--';
|
if (t === 'outpatient' || t === 'inhospital') return normalizeText(record.value?.diagnosisName || record.value?.diagnosis) || normalizeText(record.value?.summary) || '--';
|
||||||
return normalizeText(record.value?.diagnosisName || record.value?.diagnosis || record.value?.summary) || '--';
|
return normalizeText(record.value?.diagnosisName || record.value?.diagnosis || record.value?.summary) || '--';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const showFilesSection = computed(() => {
|
||||||
|
if (files.value.length) return true;
|
||||||
|
const list = Array.isArray(temp.value?.templateList)
|
||||||
|
? temp.value.templateList
|
||||||
|
: Array.isArray(getVisitRecordTemplate(templateType.value)?.templateList)
|
||||||
|
? getVisitRecordTemplate(templateType.value).templateList
|
||||||
|
: [];
|
||||||
|
return list.some((i) => i && (i.type === 'files' || i.title === 'files'));
|
||||||
|
});
|
||||||
|
|
||||||
const sections = computed(() => {
|
const sections = computed(() => {
|
||||||
const t = templateType.value;
|
const t = templateType.value;
|
||||||
const push = (title, value) => {
|
const hiddenKeys = new Set(t === 'outpatient'
|
||||||
const v = value === 0 ? '0' : value ? String(value) : '';
|
? ['corp', 'deptName', 'corpName', 'doctor']
|
||||||
if (!v.trim()) return;
|
: t === 'inhospital'
|
||||||
return { title, value: v };
|
? ['corp', 'corpName']
|
||||||
};
|
: []);
|
||||||
|
|
||||||
const list = [];
|
const list = [];
|
||||||
if (t === 'outpatient') {
|
const pushedNames = new Set();
|
||||||
const corp = push('就诊机构', record.value?.corpName);
|
|
||||||
const dept = push('科室', record.value?.deptName);
|
const pushRow = (name, value) => {
|
||||||
const doctor = push('医生', record.value?.doctor);
|
const v = normalizeText(value);
|
||||||
const treatment = push('治疗方案', record.value?.treatmentPlan);
|
if (!v.trim()) return;
|
||||||
const dispose = push('处置计划', record.value?.disposePlan);
|
if (pushedNames.has(name)) return;
|
||||||
const summary = push('备注/摘要', record.value?.summary);
|
pushedNames.add(name);
|
||||||
[corp, dept, doctor, treatment, dispose, summary].forEach((i) => i && list.push(i));
|
list.push({ title: name, value: v });
|
||||||
return list;
|
};
|
||||||
}
|
|
||||||
if (t === 'inhospital') {
|
const resolveOptionLabel = (item, candidate) => {
|
||||||
const corp = push('住院机构', record.value?.corpName);
|
const range = Array.isArray(item?.range) ? item.range : [];
|
||||||
const summary = push('摘要', record.value?.summary);
|
if (!range.length) return normalizeText(candidate);
|
||||||
[corp, summary].forEach((i) => i && list.push(i));
|
const isObjectRange = range[0] && typeof range[0] === 'object';
|
||||||
return list;
|
const toLabel = (v) => {
|
||||||
}
|
const s = normalizeText(v);
|
||||||
if (t === 'preConsultation') {
|
if (!s) return '';
|
||||||
const illness = push('现病史', record.value?.presentIllness);
|
if (!isObjectRange) return s;
|
||||||
const past = push('既往史', record.value?.pastHistory);
|
const found = range.find((opt) => opt && typeof opt === 'object' && String(opt.value) === String(s));
|
||||||
const allergy = push('过敏史', record.value?.allergyHistory);
|
return found ? String(found.label ?? found.value ?? s) : s;
|
||||||
const summary = push('摘要', record.value?.summary);
|
};
|
||||||
[illness, past, allergy, summary].forEach((i) => i && list.push(i));
|
if (Array.isArray(candidate)) return candidate.map(toLabel).filter((i) => String(i).trim()).join(',');
|
||||||
return list;
|
return toLabel(candidate);
|
||||||
}
|
};
|
||||||
if (t === 'physicalExaminationTemplate') {
|
|
||||||
const corp = push('体检机构', record.value?.corpName);
|
const templateList = Array.isArray(temp.value?.templateList)
|
||||||
const pkg = push('体检套餐', record.value?.inspectPakageName);
|
? temp.value.templateList
|
||||||
const positive = push('阳性发现及处理意见', formatPositiveFind(record.value?.positiveFind, { withOpinion: true }));
|
: Array.isArray(getVisitRecordTemplate(t)?.templateList)
|
||||||
const summary = push('摘要', record.value?.summary);
|
? getVisitRecordTemplate(t).templateList
|
||||||
[corp, pkg, positive, summary].forEach((i) => i && list.push(i));
|
: [];
|
||||||
return list;
|
const timeTitle = temp.value?.service?.timeTitle || getDefaultTimeTitle(t);
|
||||||
}
|
|
||||||
const summary = push('摘要', record.value?.summary);
|
templateList.forEach((item) => {
|
||||||
if (summary) list.push(summary);
|
const key = item?.title ? String(item.title) : '';
|
||||||
|
if (!key) return;
|
||||||
|
if (key === 'files') return;
|
||||||
|
if (key === 'diagnosis' || key === 'diagnosisName') return;
|
||||||
|
if (timeTitle && key === timeTitle) return;
|
||||||
|
if (hiddenKeys.has(key)) return;
|
||||||
|
|
||||||
|
const raw = record.value?.[key];
|
||||||
|
const display = key === 'positiveFind'
|
||||||
|
? formatPositiveFind(raw, { withOpinion: true })
|
||||||
|
: item?.type === 'date'
|
||||||
|
? (formatAnyDate(raw) || normalizeText(raw))
|
||||||
|
: resolveOptionLabel(item, raw);
|
||||||
|
pushRow(String(item?.name || key), display);
|
||||||
|
});
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadTemplate(t) {
|
||||||
|
const corpId = getCorpId();
|
||||||
|
if (!corpId || !t) return null;
|
||||||
|
try {
|
||||||
|
const res = await api('getCurrentTemplate', { corpId, templateType: t });
|
||||||
|
if (!res?.success) return null;
|
||||||
|
const raw = unwrapTemplateResponse(res);
|
||||||
|
return normalizeTemplate(raw);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const topText = computed(() => {
|
const topText = computed(() => {
|
||||||
const time = record.value?.createTime ? dayjs(record.value.createTime).format('YYYY-MM-DD HH:mm') : '';
|
const time = record.value?.createTime ? dayjs(record.value.createTime).format('YYYY-MM-DD HH:mm') : '';
|
||||||
const rawName = record.value?.creatorName ? String(record.value.creatorName) : '';
|
const rawName = record.value?.creatorName ? String(record.value.creatorName) : '';
|
||||||
@ -206,13 +323,11 @@ onLoad(async (opt) => {
|
|||||||
setTimeout(() => uni.navigateBack(), 300);
|
setTimeout(() => uni.navigateBack(), 300);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const raw = String(r?.templateType || r?.medicalType || medicalType.value || '');
|
||||||
// 兼容模板字段:wxapp 使用 diagnosis,但接口通常返回 diagnosisName
|
rawType.value = raw;
|
||||||
if ((r.medicalType === 'outpatient' || r.medicalType === 'inhospital') && !r.diagnosis && r.diagnosisName) {
|
const ui = normalizeMedicalType(raw);
|
||||||
r.diagnosis = r.diagnosisName;
|
record.value = normalizeVisitRecordFormData(ui, r);
|
||||||
}
|
temp.value = await loadTemplate(raw);
|
||||||
|
|
||||||
record.value = r;
|
|
||||||
uni.setNavigationBarTitle({ title: String(typeLabel.value || '病历详情') });
|
uni.setNavigationBarTitle({ title: String(typeLabel.value || '病历详情') });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
|
|||||||
@ -30,13 +30,8 @@
|
|||||||
<view class="tabs-area">
|
<view class="tabs-area">
|
||||||
<scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
|
<scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
|
||||||
<view class="tabs-container">
|
<view class="tabs-container">
|
||||||
<view
|
<view v-for="tab in tabs" :key="tab.key" class="tab-item" :class="{ active: currentTabKey === tab.key }"
|
||||||
v-for="tab in tabs"
|
@click="onTabClick(tab)">
|
||||||
:key="tab.key"
|
|
||||||
class="tab-item"
|
|
||||||
:class="{ active: currentTabKey === tab.key }"
|
|
||||||
@click="onTabClick(tab)"
|
|
||||||
>
|
|
||||||
{{ tab.label }}
|
{{ tab.label }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -47,32 +42,24 @@
|
|||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<view class="content-body">
|
<view class="content-body">
|
||||||
<!-- Patient List -->
|
<!-- Patient List -->
|
||||||
<scroll-view
|
<scroll-view scroll-y class="patient-list" :scroll-into-view="scrollIntoId" :scroll-with-animation="true"
|
||||||
scroll-y
|
lower-threshold="80" @scrolltolower="loadMore">
|
||||||
class="patient-list"
|
|
||||||
:scroll-into-view="scrollIntoId"
|
|
||||||
:scroll-with-animation="true"
|
|
||||||
lower-threshold="80"
|
|
||||||
@scrolltolower="loadMore"
|
|
||||||
>
|
|
||||||
<view v-for="group in patientList" :key="group.letter" :id="letterToDomId(group.letter)">
|
<view v-for="group in patientList" :key="group.letter" :id="letterToDomId(group.letter)">
|
||||||
<view class="group-title">{{ group.letter }}</view>
|
<view class="group-title">{{ group.letter }}</view>
|
||||||
|
|
||||||
<view v-for="(patient, pIndex) in group.data" :key="pIndex" class="patient-card" @click="handlePatientClick(patient)">
|
<view v-for="(patient, pIndex) in group.data" :key="pIndex" class="patient-card"
|
||||||
|
@click="handlePatientClick(patient)">
|
||||||
<!-- Checkbox for Batch Mode -->
|
<!-- Checkbox for Batch Mode -->
|
||||||
<view v-if="isBatchMode" class="checkbox-area">
|
<view v-if="isBatchMode" class="checkbox-area">
|
||||||
<uni-icons
|
<uni-icons :type="selectedItems.includes(getSelectId(patient)) ? 'checkbox-filled' : 'checkbox'" size="24"
|
||||||
:type="selectedItems.includes(getSelectId(patient)) ? 'checkbox-filled' : 'checkbox'"
|
:color="selectedItems.includes(getSelectId(patient)) ? '#007aff' : '#ccc'"></uni-icons>
|
||||||
size="24"
|
|
||||||
:color="selectedItems.includes(getSelectId(patient)) ? '#007aff' : '#ccc'"
|
|
||||||
></uni-icons>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="card-content">
|
<view class="card-content">
|
||||||
<!-- Row 1 -->
|
<!-- Row 1 -->
|
||||||
<view class="card-row-top">
|
<view class="card-row-top">
|
||||||
<view class="patient-info">
|
<view class="patient-info">
|
||||||
<text class="patient-name">{{ patient.name }}</text>
|
<text class="patient-name">{{ patient.name }}</text>
|
||||||
<text class="patient-meta">{{ patient.gender }}/{{ patient.age }}岁</text>
|
<text class="patient-meta">{{ patient.gender }}{{ patient.age ? '/' + patient.age + '岁' : '' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="patient-tags">
|
<view class="patient-tags">
|
||||||
<view v-for="(tag, tIndex) in resolveGroupTags(patient)" :key="tIndex" class="tag">
|
<view v-for="(tag, tIndex) in resolveGroupTags(patient)" :key="tIndex" class="tag">
|
||||||
@ -80,12 +67,20 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Row 2 / 3 -->
|
<!-- Row 2 -->
|
||||||
|
<view v-if="currentTabKey === 'new'" class="card-row-middle">
|
||||||
|
<text v-if="patient.record" class="record-text record-ellipsis">
|
||||||
|
{{ patient.record.type }} / {{ patient.record.date }} / {{ patient.record.diagnosis }}
|
||||||
|
</text>
|
||||||
|
<text v-else class="no-record">暂无病历记录</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- Row 3 -->
|
||||||
<view class="card-row-bottom">
|
<view class="card-row-bottom">
|
||||||
<template v-if="currentTabKey === 'new'"> <!-- New Patient Tab -->
|
<template v-if="currentTabKey === 'new'"> <!-- New Patient Tab -->
|
||||||
<text class="record-text">
|
<text class="record-text">
|
||||||
{{ patient.createTime || '-' }} / {{ resolveCreatorName(patient) || '-' }}
|
{{ resolveRecentAddTime(patient) }} {{ resolveRecentAddMeta(patient) }}
|
||||||
</text>
|
</text>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@ -104,12 +99,7 @@
|
|||||||
|
|
||||||
<!-- Sidebar Index -->
|
<!-- Sidebar Index -->
|
||||||
<view v-if="!isBatchMode" class="sidebar-index">
|
<view v-if="!isBatchMode" class="sidebar-index">
|
||||||
<view
|
<view v-for="letter in indexList" :key="letter" class="index-item" @click="scrollToLetter(letter)">
|
||||||
v-for="letter in indexList"
|
|
||||||
:key="letter"
|
|
||||||
class="index-item"
|
|
||||||
@click="scrollToLetter(letter)"
|
|
||||||
>
|
|
||||||
{{ letter }}
|
{{ letter }}
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -118,11 +108,9 @@
|
|||||||
<!-- Batch Actions Footer -->
|
<!-- Batch Actions Footer -->
|
||||||
<view v-if="isBatchMode" class="batch-footer">
|
<view v-if="isBatchMode" class="batch-footer">
|
||||||
<view class="left-action" @click="handleSelectAll">
|
<view class="left-action" @click="handleSelectAll">
|
||||||
<uni-icons
|
<uni-icons
|
||||||
:type="selectedItems.length > 0 && selectedItems.length === patientList.flatMap(g => g.data).length ? 'checkbox-filled' : 'checkbox'"
|
:type="selectedItems.length > 0 && selectedItems.length === patientList.flatMap(g => g.data).length ? 'checkbox-filled' : 'checkbox'"
|
||||||
size="24"
|
size="24" color="#666"></uni-icons>
|
||||||
color="#666"
|
|
||||||
></uni-icons>
|
|
||||||
<text class="footer-text">全选 ({{ selectedItems.length }})</text>
|
<text class="footer-text">全选 ({{ selectedItems.length }})</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="right-actions">
|
<view class="right-actions">
|
||||||
@ -142,7 +130,7 @@ import dayjs from 'dayjs';
|
|||||||
|
|
||||||
import api from '@/utils/api';
|
import api from '@/utils/api';
|
||||||
import useAccountStore from '@/store/account';
|
import useAccountStore from '@/store/account';
|
||||||
import { toast } from '@/utils/widget';
|
import { confirm as uniConfirm, hideLoading, loading as showLoading, toast } from '@/utils/widget';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const teams = ref([]);
|
const teams = ref([]);
|
||||||
@ -202,7 +190,7 @@ const accountStore = useAccountStore();
|
|||||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||||
const { getDoctorInfo } = accountStore;
|
const { getDoctorInfo } = accountStore;
|
||||||
|
|
||||||
const teamDisplay = computed(() => `${currentTeam.value?.name || ''}(${managedArchiveCountAllTeams.value})`);
|
const teamDisplay = computed(() => `${currentTeam.value?.name || ''}`);
|
||||||
|
|
||||||
function asArray(value) {
|
function asArray(value) {
|
||||||
return Array.isArray(value) ? value : [];
|
return Array.isArray(value) ? value : [];
|
||||||
@ -234,6 +222,19 @@ function getTeamId() {
|
|||||||
return String(currentTeam.value?.teamId || '') || '';
|
return String(currentTeam.value?.teamId || '') || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasCompleteUserInfo() {
|
||||||
|
const userId = getUserId();
|
||||||
|
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
|
||||||
|
const anotherName = String(doctorInfo.value?.anotherName || '').trim();
|
||||||
|
return Boolean(corpId && userId && anotherName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureUserInfoForFeature() {
|
||||||
|
if (hasCompleteUserInfo()) return true;
|
||||||
|
toast('请先完善个人信息再使用该功能');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTeamMembers() {
|
async function loadTeamMembers() {
|
||||||
const corpId = getCorpId();
|
const corpId = getCorpId();
|
||||||
const teamId = getTeamId();
|
const teamId = getTeamId();
|
||||||
@ -246,10 +247,10 @@ async function loadTeamMembers() {
|
|||||||
const members = Array.isArray(t.memberList) ? t.memberList : [];
|
const members = Array.isArray(t.memberList) ? t.memberList : [];
|
||||||
// Update map
|
// Update map
|
||||||
members.forEach(m => {
|
members.forEach(m => {
|
||||||
const uid = String(m?.userid || '');
|
const uid = String(m?.userid || '');
|
||||||
if (uid) {
|
if (uid) {
|
||||||
userNameMap.value[uid] = String(m?.anotherName || m?.name || m?.userid || '') || uid;
|
userNameMap.value[uid] = String(m?.anotherName || m?.name || m?.userid || '') || uid;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('获取团队成员失败', e);
|
console.error('获取团队成员失败', e);
|
||||||
@ -262,6 +263,32 @@ function resolveCreatorName(patient) {
|
|||||||
return userNameMap.value[val] || val;
|
return userNameMap.value[val] || val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveRecentAddTime(patient) {
|
||||||
|
return patient?.recentAddTime || patient?.createTime || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRecentAddOperatorName(patient) {
|
||||||
|
const uid = patient?.recentAddOperatorUserId || patient?.creator || '';
|
||||||
|
if (!uid) return '';
|
||||||
|
return userNameMap.value[uid] || uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRecentAddAction(patient) {
|
||||||
|
const t = String(patient?.recentAddType || '').trim();
|
||||||
|
if (!t || t === 'create') return '创建';
|
||||||
|
if (t === 'share') return '共享';
|
||||||
|
if (t.startsWith('transfer')) return '转移';
|
||||||
|
return '创建';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRecentAddMeta(patient) {
|
||||||
|
const name = resolveRecentAddOperatorName(patient);
|
||||||
|
const action = resolveRecentAddAction(patient);
|
||||||
|
if (name) return `${name}${action}`;
|
||||||
|
if (action === '创建') return '患者创建';
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function applyVerifyStatus(status, reason) {
|
function applyVerifyStatus(status, reason) {
|
||||||
verifyStatus.value = status || '';
|
verifyStatus.value = status || '';
|
||||||
@ -380,13 +407,21 @@ function formatPatient(raw) {
|
|||||||
const createTimeStr = createTime ? createTime.format('YYYY-MM-DD HH:mm') : '';
|
const createTimeStr = createTime ? createTime.format('YYYY-MM-DD HH:mm') : '';
|
||||||
const createTimeTs = createTime ? createTime.valueOf() : 0;
|
const createTimeTs = createTime ? createTime.valueOf() : 0;
|
||||||
|
|
||||||
|
// 最近一次“新增到当前团队”的时间(后端计算:创建/转移/共享),没有则退化为 createTime
|
||||||
|
const recentAddTimeRaw = raw?.recentAddTime ?? raw?.recentAddAt ?? raw?.recentTime;
|
||||||
|
const recentAddTime = parseCreateTime(recentAddTimeRaw) || createTime;
|
||||||
|
const recentAddTimeStr = recentAddTime ? recentAddTime.format('YYYY-MM-DD HH:mm') : '';
|
||||||
|
const recentAddTimeTs = recentAddTime ? recentAddTime.valueOf() : 0;
|
||||||
|
const recentAddType = String(raw?.recentAddType || (recentAddTimeRaw ? '' : 'create') || '');
|
||||||
|
const recentAddOperatorUserId = String(raw?.recentAddOperatorUserId || raw?.recentAddOperator || raw?.creator || '');
|
||||||
|
|
||||||
// 优先使用后端返回的 tagNames(标签名称数组)
|
// 优先使用后端返回的 tagNames(标签名称数组)
|
||||||
const rawTagNames = asArray(raw?.tagNames).filter((i) => typeof i === 'string' && i.trim());
|
const rawTagNames = asArray(raw?.tagNames).filter((i) => typeof i === 'string' && i.trim());
|
||||||
// 其次使用 tags(如果是字符串数组)
|
// 其次使用 tags(如果是字符串数组)
|
||||||
const rawTags = asArray(raw?.tags).filter((i) => typeof i === 'string' && i.trim());
|
const rawTags = asArray(raw?.tags).filter((i) => typeof i === 'string' && i.trim());
|
||||||
// 最后才使用 tagIds(仅作为兜底,不推荐显示)
|
// 最后才使用 tagIds(仅作为兜底,不推荐显示)
|
||||||
const tagIds = asArray(raw?.tagIds).map(String).filter(Boolean);
|
const tagIds = asArray(raw?.tagIds).map(String).filter(Boolean);
|
||||||
|
|
||||||
// 解析标签:优先 tagNames > tags(字符串) > tagIds
|
// 解析标签:优先 tagNames > tags(字符串) > tagIds
|
||||||
const displayTags = rawTagNames.length ? rawTagNames : (rawTags.length ? rawTags : []);
|
const displayTags = rawTagNames.length ? rawTagNames : (rawTags.length ? rawTags : []);
|
||||||
|
|
||||||
@ -418,6 +453,10 @@ function formatPatient(raw) {
|
|||||||
mobile,
|
mobile,
|
||||||
createTime: createTimeStr,
|
createTime: createTimeStr,
|
||||||
createTimeTs,
|
createTimeTs,
|
||||||
|
recentAddTime: recentAddTimeStr,
|
||||||
|
recentAddTimeTs,
|
||||||
|
recentAddType,
|
||||||
|
recentAddOperatorUserId,
|
||||||
creator: raw?.creatorName || raw?.creator || '',
|
creator: raw?.creatorName || raw?.creator || '',
|
||||||
hospitalId: raw?.customerNumber || raw?.hospitalId || '',
|
hospitalId: raw?.customerNumber || raw?.hospitalId || '',
|
||||||
record,
|
record,
|
||||||
@ -508,8 +547,10 @@ async function reload(reset = true) {
|
|||||||
} else if (currentTab.value.kind === 'new') {
|
} else if (currentTab.value.kind === 'new') {
|
||||||
const start = dayjs().subtract(7, 'day').startOf('day').valueOf();
|
const start = dayjs().subtract(7, 'day').startOf('day').valueOf();
|
||||||
const end = dayjs().endOf('day').valueOf();
|
const end = dayjs().endOf('day').valueOf();
|
||||||
query.startCreateTime = start;
|
// “新患者”= 最近7天新增到当前团队:创建 + 转移/共享(时间来自服务记录)
|
||||||
query.endCreateTime = end;
|
query.startRecentTime = start;
|
||||||
|
query.endRecentTime = end;
|
||||||
|
query.includeRecentAddTime = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@ -535,11 +576,11 @@ async function reload(reset = true) {
|
|||||||
managedArchiveCountAllTeams.value =
|
managedArchiveCountAllTeams.value =
|
||||||
Number(
|
Number(
|
||||||
payload.totalAllTeams ||
|
payload.totalAllTeams ||
|
||||||
payload.totalAllTeam ||
|
payload.totalAllTeam ||
|
||||||
payload.totalAllTeamsCount ||
|
payload.totalAllTeamsCount ||
|
||||||
managedArchiveCountAllTeams.value ||
|
managedArchiveCountAllTeams.value ||
|
||||||
totalFromApi.value ||
|
totalFromApi.value ||
|
||||||
0
|
0
|
||||||
) || (totalFromApi.value || 0);
|
) || (totalFromApi.value || 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -576,10 +617,14 @@ const patientList = computed(() => {
|
|||||||
if (currentTab.value.kind === 'new') {
|
if (currentTab.value.kind === 'new') {
|
||||||
const sevenDaysAgo = dayjs().subtract(7, 'day').startOf('day').valueOf();
|
const sevenDaysAgo = dayjs().subtract(7, 'day').startOf('day').valueOf();
|
||||||
const flatList = all
|
const flatList = all
|
||||||
.filter((p) => Number(p?.createTimeTs || 0) >= sevenDaysAgo)
|
.filter((p) => Number(p?.recentAddTimeTs || p?.createTimeTs || 0) >= sevenDaysAgo)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => Number(b?.createTimeTs || 0) - Number(a?.createTimeTs || 0));
|
.sort(
|
||||||
return [{ letter: '最近新增', data: flatList }];
|
(a, b) =>
|
||||||
|
Number(b?.recentAddTimeTs || b?.createTimeTs || 0) -
|
||||||
|
Number(a?.recentAddTimeTs || a?.createTimeTs || 0)
|
||||||
|
);
|
||||||
|
return [{ letter: '最近7天新增', data: flatList }];
|
||||||
}
|
}
|
||||||
|
|
||||||
return groupByLetter(all);
|
return groupByLetter(all);
|
||||||
@ -587,12 +632,14 @@ const patientList = computed(() => {
|
|||||||
|
|
||||||
const indexList = computed(() => {
|
const indexList = computed(() => {
|
||||||
if (currentTab.value.kind === 'new') return []; // No index bar for new patient
|
if (currentTab.value.kind === 'new') return []; // No index bar for new patient
|
||||||
return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').filter(l => patientList.value.some(g => g.letter === l));
|
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('').filter(l => patientList.value.some(g => g.letter === l));
|
||||||
|
return letters;
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalPatients = computed(() => {
|
const totalPatients = computed(() => {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
patientList.value.forEach(g => count += g.data.length);
|
patientList.value.forEach(g => count += g.data.length);
|
||||||
|
if (currentTab.value.kind === 'new') return count;
|
||||||
return totalFromApi.value || count;
|
return totalFromApi.value || count;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -638,6 +685,7 @@ const goToSearch = () => {
|
|||||||
|
|
||||||
const goToGroupManage = () => {
|
const goToGroupManage = () => {
|
||||||
if (checkBatchMode()) return;
|
if (checkBatchMode()) return;
|
||||||
|
if (!ensureUserInfoForFeature()) return;
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/case/group-manage'
|
url: '/pages/case/group-manage'
|
||||||
});
|
});
|
||||||
@ -657,40 +705,63 @@ const toggleBatchMode = () => {
|
|||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
if (checkBatchMode()) return;
|
if (checkBatchMode()) return;
|
||||||
// 100上限:无法继续新增 -> 引导联系客服(预留入口)
|
const rawMax = doctorInfo.value?.maxCustomerArchive;
|
||||||
if (managedArchiveCountAllTeams.value >= 100) {
|
const hasMaxField = rawMax !== undefined && rawMax !== null && String(rawMax).trim() !== '';
|
||||||
uni.showModal({
|
const maxCustomerArchive = hasMaxField ? Number(rawMax) : NaN;
|
||||||
title: '提示',
|
|
||||||
content: '当前管理档案数已达 100 个,无法继续新增。如需提升档案管理数,请联系客服处理。',
|
|
||||||
cancelText: '知道了',
|
|
||||||
confirmText: '添加客服',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
openAddCustomerServiceEntry();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未认证 + 达到10上限:提示去认证
|
// maxCustomerArchive:
|
||||||
if (!isVerified.value && managedArchiveCountAllTeams.value >= 10) {
|
// -1 = 无限;存在该字段则优先按该字段限制;不存在则沿用原有规则(未认证10/已认证100)
|
||||||
if (verifyStatus.value === 'verifying') {
|
if (hasMaxField && Number.isFinite(maxCustomerArchive)) {
|
||||||
toast('信息认证中,请耐心等待!');
|
if (maxCustomerArchive !== -1 && managedArchiveCountAllTeams.value >= maxCustomerArchive) {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: `当前管理档案数已达上限 ${maxCustomerArchive} 个,无法继续新增。如需提升档案管理数,请联系客服处理。`,
|
||||||
|
cancelText: '知道了',
|
||||||
|
confirmText: '添加客服',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
openAddCustomerServiceEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
uni.showModal({
|
} else {
|
||||||
title: '提示',
|
// 100上限:无法继续新增 -> 引导联系客服(预留入口)
|
||||||
content: '当前管理档案数已达上限 10 个,完成认证即可升级至 100 个。',
|
if (managedArchiveCountAllTeams.value >= 100) {
|
||||||
cancelText: '暂不认证',
|
uni.showModal({
|
||||||
confirmText: '去认证',
|
title: '提示',
|
||||||
success: (res) => {
|
content: '当前管理档案数已达 100 个,无法继续新增。如需提升档案管理数,请联系客服处理。',
|
||||||
if (res.confirm) {
|
cancelText: '知道了',
|
||||||
startVerifyFlow();
|
confirmText: '添加客服',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
openAddCustomerServiceEntry();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 未认证 + 达到10上限:提示去认证
|
||||||
|
if (!isVerified.value && managedArchiveCountAllTeams.value >= 10) {
|
||||||
|
if (verifyStatus.value === 'verifying') {
|
||||||
|
toast('信息认证中,请耐心等待!');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
uni.showModal({
|
||||||
return;
|
title: '提示',
|
||||||
|
content: '当前管理档案数已达上限 10 个,完成认证即可升级至 100 个。',
|
||||||
|
cancelText: '暂不认证',
|
||||||
|
confirmText: '去认证',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
startVerifyFlow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 未达上限:显示新增入口
|
// 未达上限:显示新增入口
|
||||||
@ -752,7 +823,7 @@ const openCreatePatientEntry = () => {
|
|||||||
// Batch Operations
|
// Batch Operations
|
||||||
const toggleSelect = (patient) => {
|
const toggleSelect = (patient) => {
|
||||||
if (!isBatchMode.value) return; // Should not happen if click handler is correct
|
if (!isBatchMode.value) return; // Should not happen if click handler is correct
|
||||||
|
|
||||||
const id = patient._id || patient.id || patient.phone || patient.mobile; // Prefer server id
|
const id = patient._id || patient.id || patient.phone || patient.mobile; // Prefer server id
|
||||||
const index = selectedItems.value.indexOf(id);
|
const index = selectedItems.value.indexOf(id);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
@ -777,14 +848,149 @@ const cancelBatch = () => {
|
|||||||
selectedItems.value = [];
|
selectedItems.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTransfer = () => {
|
function showActionSheet(itemList = [], title = '') {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (!Array.isArray(itemList) || itemList.length === 0) {
|
||||||
|
reject(new Error('empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uni.showActionSheet({
|
||||||
|
title,
|
||||||
|
itemList,
|
||||||
|
success: ({ tapIndex }) => resolve(tapIndex),
|
||||||
|
fail: () => reject(new Error('cancel')),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchTeamMembersByTeamId(teamId) {
|
||||||
|
const corpId = getCorpId();
|
||||||
|
if (!corpId || !teamId) return [];
|
||||||
|
const res = await api('getTeamData', { corpId, teamId });
|
||||||
|
if (!res?.success) return [];
|
||||||
|
const t = res?.data && typeof res.data === 'object' ? res.data : {};
|
||||||
|
return Array.isArray(t.memberList) ? t.memberList : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transferToCustomerPool(customerIds) {
|
||||||
|
try {
|
||||||
|
await uniConfirm('客户将与本团队解除服务关系,本团队成员将没有权限查询到客户档案。');
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const corpId = getCorpId();
|
||||||
|
const currentTeamId = getTeamId();
|
||||||
|
const creatorUserId = getUserId();
|
||||||
|
if (!corpId || !currentTeamId || !creatorUserId) return toast('缺少用户/团队信息');
|
||||||
|
|
||||||
|
showLoading('保存中...');
|
||||||
|
try {
|
||||||
|
const res = await api('transferCustomers', {
|
||||||
|
corpId,
|
||||||
|
customerIds,
|
||||||
|
currentTeamId,
|
||||||
|
operationType: 'transferToCustomerPool',
|
||||||
|
creatorUserId,
|
||||||
|
});
|
||||||
|
if (!res?.success) {
|
||||||
|
toast(res?.message || '操作失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast('操作成功');
|
||||||
|
cancelBatch();
|
||||||
|
await reload(true);
|
||||||
|
} catch (e) {
|
||||||
|
toast('操作失败');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transferToOtherTeam(customerIds) {
|
||||||
|
const corpId = getCorpId();
|
||||||
|
const currentTeamId = getTeamId();
|
||||||
|
const creatorUserId = getUserId();
|
||||||
|
if (!corpId || !currentTeamId || !creatorUserId) return toast('缺少用户/团队信息');
|
||||||
|
|
||||||
|
if (!teams.value.length) await loadTeams();
|
||||||
|
const candidates = teams.value.filter((t) => String(t?.teamId || '') !== String(currentTeamId));
|
||||||
|
if (!candidates.length) return toast('暂无可选团队');
|
||||||
|
|
||||||
|
let teamIndex;
|
||||||
|
try {
|
||||||
|
teamIndex = await showActionSheet(candidates.map((t) => t.name || ''), '选择新负责团队');
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pickedTeam = candidates[teamIndex];
|
||||||
|
if (!pickedTeam?.teamId) return toast('团队信息异常');
|
||||||
|
|
||||||
|
let members = [];
|
||||||
|
try {
|
||||||
|
members = await fetchTeamMembersByTeamId(pickedTeam.teamId);
|
||||||
|
} catch (e) {
|
||||||
|
members = [];
|
||||||
|
}
|
||||||
|
if (!members.length) return toast('当前团队暂无可选成员');
|
||||||
|
|
||||||
|
const labels = members.map((m) => String(m?.anotherName || m?.name || m?.userid || ''));
|
||||||
|
let userIndex;
|
||||||
|
try {
|
||||||
|
userIndex = await showActionSheet(labels, '选择责任人');
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pickedUser = members[userIndex] || null;
|
||||||
|
const targetUserId = String(pickedUser?.userid || '') || '';
|
||||||
|
if (!targetUserId) return toast('责任人信息异常');
|
||||||
|
|
||||||
|
showLoading('保存中...');
|
||||||
|
try {
|
||||||
|
const res = await api('transferCustomers', {
|
||||||
|
corpId,
|
||||||
|
customerIds,
|
||||||
|
currentTeamId,
|
||||||
|
targetTeamId: String(pickedTeam.teamId),
|
||||||
|
targetUserId,
|
||||||
|
operationType: 'transferToOtherTeam',
|
||||||
|
creatorUserId,
|
||||||
|
});
|
||||||
|
if (!res?.success) {
|
||||||
|
toast(res?.message || '操作失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast('操作成功');
|
||||||
|
cancelBatch();
|
||||||
|
await reload(true);
|
||||||
|
} catch (e) {
|
||||||
|
toast('操作失败');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTransfer = async () => {
|
||||||
if (selectedItems.value.length === 0) {
|
if (selectedItems.value.length === 0) {
|
||||||
uni.showToast({ title: '请选择患者', icon: 'none' });
|
uni.showToast({ title: '请选择患者', icon: 'none' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
uni.setStorageSync(BATCH_CUSTOMER_IDS_KEY, selectedItems.value.slice());
|
|
||||||
// Navigate to Transfer Page
|
let tapIndex;
|
||||||
uni.navigateTo({ url: '/pages/case/batch-transfer' });
|
try {
|
||||||
|
tapIndex = await showActionSheet(['转移给其他团队', '转移至客户公共池']);
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customerIds = selectedItems.value.slice().map(String).filter(Boolean);
|
||||||
|
if (!customerIds.length) return toast('请选择患者');
|
||||||
|
|
||||||
|
if (tapIndex === 0) {
|
||||||
|
await transferToOtherTeam(customerIds);
|
||||||
|
} else if (tapIndex === 1) {
|
||||||
|
await transferToCustomerPool(customerIds);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
@ -817,6 +1023,8 @@ function onTabClick(tab) {
|
|||||||
if (checkBatchMode()) return;
|
if (checkBatchMode()) return;
|
||||||
if (!tab || !tab.key) return;
|
if (!tab || !tab.key) return;
|
||||||
if (currentTabKey.value === tab.key) return;
|
if (currentTabKey.value === tab.key) return;
|
||||||
|
|
||||||
|
if (tab.kind === 'group' && !ensureUserInfoForFeature()) return;
|
||||||
currentTabKey.value = tab.key;
|
currentTabKey.value = tab.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -861,11 +1069,11 @@ onShow(async () => {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding-bottom: 0; // Default
|
padding-bottom: 0; // Default
|
||||||
|
|
||||||
// Padding for batch footer
|
// Padding for batch footer
|
||||||
/* &.is-batch {
|
/* &.is-batch {
|
||||||
padding-bottom: 100rpx;
|
padding-bottom: 100rpx;
|
||||||
} */
|
} */
|
||||||
// We can't use &.is-batch because scoped style and root element is tricky depending on uni-app version/style
|
// We can't use &.is-batch because scoped style and root element is tricky depending on uni-app version/style
|
||||||
// Instead we handle it in content-body or separate view
|
// Instead we handle it in content-body or separate view
|
||||||
}
|
}
|
||||||
@ -884,11 +1092,11 @@ onShow(async () => {
|
|||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
|
||||||
.team-name {
|
.team-name {
|
||||||
margin-right: 10rpx;
|
margin-right: 10rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-icon {
|
.team-icon {
|
||||||
font-size: 32rpx;
|
font-size: 32rpx;
|
||||||
color: #333;
|
color: #333;
|
||||||
@ -898,13 +1106,13 @@ onShow(async () => {
|
|||||||
.header-actions {
|
.header-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 30rpx;
|
gap: 30rpx;
|
||||||
|
|
||||||
.action-item {
|
.action-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
.action-text {
|
.action-text {
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
color: #333;
|
color: #333;
|
||||||
@ -925,11 +1133,11 @@ onShow(async () => {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.tabs-container {
|
.tabs-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 20rpx 30rpx;
|
padding: 20rpx 30rpx;
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
padding: 10rpx 30rpx;
|
padding: 10rpx 30rpx;
|
||||||
margin-right: 20rpx;
|
margin-right: 20rpx;
|
||||||
@ -938,7 +1146,7 @@ onShow(async () => {
|
|||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
color: #5d8aff;
|
color: #5d8aff;
|
||||||
background-color: #e6f0ff;
|
background-color: #e6f0ff;
|
||||||
@ -953,7 +1161,7 @@ onShow(async () => {
|
|||||||
color: #666;
|
color: #666;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
min-width: 100rpx;
|
min-width: 70rpx;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1022,7 +1230,7 @@ onShow(async () => {
|
|||||||
.patient-tags {
|
.patient-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10rpx;
|
gap: 10rpx;
|
||||||
|
|
||||||
.tag {
|
.tag {
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
color: #5d8aff;
|
color: #5d8aff;
|
||||||
@ -1035,13 +1243,14 @@ onShow(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-row-bottom {
|
.card-row-middle {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
|
||||||
.record-text {
|
.record-text {
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-ellipsis {
|
.record-ellipsis {
|
||||||
display: block;
|
display: block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -1049,7 +1258,27 @@ onShow(async () => {
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-record {
|
||||||
|
color: #bdc3c7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-row-bottom {
|
||||||
|
font-size: 28rpx;
|
||||||
|
|
||||||
|
.record-text {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-ellipsis {
|
||||||
|
display: block;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.no-record {
|
.no-record {
|
||||||
color: #bdc3c7;
|
color: #bdc3c7;
|
||||||
}
|
}
|
||||||
@ -1067,12 +1296,13 @@ onShow(async () => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 30rpx;
|
padding: 0 30rpx;
|
||||||
box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.05);
|
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
|
|
||||||
.left-action {
|
.left-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.footer-text {
|
.footer-text {
|
||||||
margin-left: 10rpx;
|
margin-left: 10rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
@ -1083,7 +1313,7 @@ onShow(async () => {
|
|||||||
.right-actions {
|
.right-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 20rpx;
|
gap: 20rpx;
|
||||||
|
|
||||||
.footer-btn {
|
.footer-btn {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
padding: 0 30rpx;
|
padding: 0 30rpx;
|
||||||
@ -1091,13 +1321,13 @@ onShow(async () => {
|
|||||||
line-height: 64rpx;
|
line-height: 64rpx;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 8rpx;
|
border-radius: 8rpx;
|
||||||
|
|
||||||
&.plain {
|
&.plain {
|
||||||
border: 2rpx solid #ddd;
|
border: 2rpx solid #ddd;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background-color: #5d8aff;
|
background-color: #5d8aff;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -1123,7 +1353,7 @@ onShow(async () => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
.index-item {
|
.index-item {
|
||||||
font-size: 20rpx;
|
font-size: 20rpx;
|
||||||
color: #555;
|
color: #555;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
|||||||
<view class="label">{{ item.label }}</view>
|
<view class="label">{{ item.label }}</view>
|
||||||
<uni-icons :type="selectedMap[item.label] ? 'checkmarkempty' : ''" size="22" color="#007aff" />
|
<uni-icons :type="selectedMap[item.label] ? 'checkmarkempty' : ''" size="22" color="#007aff" />
|
||||||
</view>
|
</view>
|
||||||
<view v-if="showList.length === 0" class="empty">暂无诊断数据</view>
|
<view v-if="showList.length === 0" class="empty">{{ keyword.trim() ? '暂无诊断数据' : '请输入关键词搜索' }}</view>
|
||||||
<view style="height: 240rpx;"></view>
|
<view style="height: 240rpx;"></view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
@ -31,6 +31,7 @@ const list = ref([]);
|
|||||||
const selections = ref([]);
|
const selections = ref([]);
|
||||||
const mult = ref(false);
|
const mult = ref(false);
|
||||||
const eventName = ref('change-diagnosis');
|
const eventName = ref('change-diagnosis');
|
||||||
|
let lastQueryId = 0;
|
||||||
|
|
||||||
const selectedMap = computed(() => selections.value.reduce((m, i) => ((m[i] = true), m), {}));
|
const selectedMap = computed(() => selections.value.reduce((m, i) => ((m[i] = true), m), {}));
|
||||||
|
|
||||||
@ -38,22 +39,8 @@ function normalizeText(v) {
|
|||||||
return v ? String(v) : '';
|
return v ? String(v) : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullMatched = computed(() => {
|
|
||||||
const value = String(keyword.value || '').trim();
|
|
||||||
if (!value) return null;
|
|
||||||
return { label: value, value, key: `full_${value}` };
|
|
||||||
});
|
|
||||||
|
|
||||||
const showList = computed(() => {
|
const showList = computed(() => {
|
||||||
const base = Array.isArray(list.value) ? list.value : [];
|
return Array.isArray(list.value) ? list.value : [];
|
||||||
const arr = [];
|
|
||||||
if (fullMatched.value) arr.push(fullMatched.value);
|
|
||||||
base.forEach((i) => {
|
|
||||||
if (!i || !i.label) return;
|
|
||||||
if (fullMatched.value && i.label === fullMatched.value.label) return;
|
|
||||||
arr.push(i);
|
|
||||||
});
|
|
||||||
return arr;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let timer = null;
|
let timer = null;
|
||||||
@ -89,11 +76,25 @@ onLoad((opt) => {
|
|||||||
async function query() {
|
async function query() {
|
||||||
if (!ready.value) return;
|
if (!ready.value) return;
|
||||||
const value = String(keyword.value || '').trim();
|
const value = String(keyword.value || '').trim();
|
||||||
|
if (!value) {
|
||||||
|
list.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryId = ++lastQueryId;
|
||||||
|
|
||||||
uni.showLoading({ title: '加载中...' });
|
|
||||||
try {
|
try {
|
||||||
|
uni.showLoading({ title: '加载中...' });
|
||||||
const res = await api('getDisease', { diseaseName: value });
|
const res = await api('getDisease', { diseaseName: value });
|
||||||
const arr = Array.isArray(res?.data?.data) ? res.data.data : [];
|
if (queryId !== lastQueryId) return;
|
||||||
|
const arr =
|
||||||
|
Array.isArray(res?.data?.data) ? res.data.data
|
||||||
|
: Array.isArray(res?.data?.list) ? res.data.list
|
||||||
|
: Array.isArray(res?.data?.data?.data) ? res.data.data.data
|
||||||
|
: Array.isArray(res?.data) ? res.data
|
||||||
|
: Array.isArray(res?.list) ? res.list
|
||||||
|
: Array.isArray(res?.data?.data?.list) ? res.data.data.list
|
||||||
|
: [];
|
||||||
list.value = arr
|
list.value = arr
|
||||||
.map((i) => {
|
.map((i) => {
|
||||||
const label = normalizeText(i?.diseaseName);
|
const label = normalizeText(i?.diseaseName);
|
||||||
@ -103,9 +104,9 @@ async function query() {
|
|||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
list.value = [];
|
if (queryId === lastQueryId) list.value = [];
|
||||||
} finally {
|
} finally {
|
||||||
uni.hideLoading();
|
if (queryId === lastQueryId) uni.hideLoading();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,13 @@
|
|||||||
<view class="mt-12 text-base text-dark">全周期健康管理伙伴</view>
|
<view class="mt-12 text-base text-dark">全周期健康管理伙伴</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="login-btn-wrap">
|
<view class="login-btn-wrap">
|
||||||
<button v-if="checked" class="login-btn" type="primary" open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">
|
<button
|
||||||
|
v-if="checked"
|
||||||
|
class="login-btn"
|
||||||
|
type="primary"
|
||||||
|
open-type="getPhoneNumber"
|
||||||
|
@getphonenumber="getPhoneNumber"
|
||||||
|
>
|
||||||
手机号快捷登录
|
手机号快捷登录
|
||||||
</button>
|
</button>
|
||||||
<!-- <button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()">
|
<!-- <button v-if="checked" class="login-btn" type="primary" @click="getPhoneNumber()">
|
||||||
@ -15,7 +21,10 @@
|
|||||||
手机号快捷登录
|
手机号快捷登录
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="flex items-center justify-center mt-12 px-15" @click="checked = !checked">
|
<view
|
||||||
|
class="flex items-center justify-center mt-12 px-15"
|
||||||
|
@click="checked = !checked"
|
||||||
|
>
|
||||||
<checkbox :checked="checked" style="transform: scale(0.7)" />
|
<checkbox :checked="checked" style="transform: scale(0.7)" />
|
||||||
<view class="text-sm text-gray">我已阅读并同意</view>
|
<view class="text-sm text-gray">我已阅读并同意</view>
|
||||||
<view class="text-sm text-primary">《用户协议》、</view>
|
<view class="text-sm text-primary">《用户协议》、</view>
|
||||||
@ -76,8 +85,8 @@ async function getPhoneNumber(e) {
|
|||||||
await attempToPage(redirectUrl.value);
|
await attempToPage(redirectUrl.value);
|
||||||
} else if (res && !(doctorInfo.value && doctorInfo.value.anotherName)) {
|
} else if (res && !(doctorInfo.value && doctorInfo.value.anotherName)) {
|
||||||
uni.redirectTo({
|
uni.redirectTo({
|
||||||
url: '/pages/work/profile'
|
url: "/pages/work/profile",
|
||||||
})
|
});
|
||||||
} else if (res) {
|
} else if (res) {
|
||||||
toHome();
|
toHome();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
// SCSS 变量定义
|
// SCSS 变量定义
|
||||||
$font-size-text: 28rpx;
|
$font-size-text: 30rpx;
|
||||||
$font-size-tip: 24rpx;
|
$font-size-tip: 28rpx;
|
||||||
$font-size-title: 32rpx;
|
$font-size-title: 32rpx;
|
||||||
$text-color-sub: #999;
|
$text-color-sub: #999;
|
||||||
$primary-color: #0877F1;
|
$primary-color: #0877F1;
|
||||||
@ -11,16 +11,23 @@ $primary-color: #0877F1;
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 患者信息栏样式 */
|
/* 患者信息栏样式 - 固定在顶部 */
|
||||||
.patient-info-bar {
|
.patient-info-bar {
|
||||||
position: relative;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-bottom: 1rpx solid #f0f0f0;
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
padding: 20rpx 32rpx;
|
padding: 20rpx 32rpx;
|
||||||
z-index: 10;
|
z-index: 100;
|
||||||
flex-shrink: 0; /* 防止被压缩 */
|
flex-shrink: 0; /* 防止被压缩 */
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.patient-info-content {
|
.patient-info-content {
|
||||||
@ -87,7 +94,12 @@ $primary-color: #0877F1;
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
margin-top: 120rpx;
|
||||||
|
margin-bottom: 0;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-content-compressed {
|
.chat-content-compressed {
|
||||||
@ -331,7 +343,7 @@ $primary-color: #0877F1;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-text {
|
.message-text {
|
||||||
font-size: $font-size-text;
|
font-size: 30rpx;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
|||||||
@ -348,7 +348,7 @@ const handleNextFromProgress = (data) => {
|
|||||||
|
|
||||||
// 跳转到病历填写页面
|
// 跳转到病历填写页面
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/case/medical-case-form?caseType=${data.caseType}&patientId=${
|
url: `/pages/case/ai-medical-case-form?caseType=${data.caseType}&patientId=${
|
||||||
props.patientId
|
props.patientId
|
||||||
}&groupId=${props.groupId}&formData=${encodeURIComponent(
|
}&groupId=${props.groupId}&formData=${encodeURIComponent(
|
||||||
JSON.stringify(extractedData)
|
JSON.stringify(extractedData)
|
||||||
|
|||||||
@ -93,6 +93,7 @@ const FIELD_LABELS = {
|
|||||||
inspectSummary: "体检小结",
|
inspectSummary: "体检小结",
|
||||||
positiveFind: "阳性发现及处理意见",
|
positiveFind: "阳性发现及处理意见",
|
||||||
// 预问诊记录
|
// 预问诊记录
|
||||||
|
consultationDate: "问诊日期",
|
||||||
presentIllnessHistory: "现病史",
|
presentIllnessHistory: "现病史",
|
||||||
pastMedicalHistory: "既往史",
|
pastMedicalHistory: "既往史",
|
||||||
};
|
};
|
||||||
|
|||||||
@ -367,23 +367,35 @@ function getBubbleClass(message) {
|
|||||||
return message.flow === "out" ? "user-bubble" : "doctor-bubble";
|
return message.flow === "out" ? "user-bubble" : "doctor-bubble";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面加载
|
// 页面加载
|
||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
groupId.value = options.groupID || "";
|
const decodeQueryValue = (v) => {
|
||||||
messageList.value = [];
|
const s = typeof v === "string" ? v : String(v || "");
|
||||||
isLoading.value = false;
|
if (!s) return "";
|
||||||
if (options.conversationID) {
|
try {
|
||||||
chatInfo.value.conversationID = options.conversationID;
|
return decodeURIComponent(s);
|
||||||
timChatManager.setConversationID(options.conversationID);
|
} catch (e) {
|
||||||
console.log("设置当前会话ID:", options.conversationID);
|
return s;
|
||||||
}
|
}
|
||||||
if (options.userID) {
|
};
|
||||||
chatInfo.value.userID = options.userID;
|
|
||||||
}
|
const rawGroupId = decodeQueryValue(options.groupID || "");
|
||||||
|
groupId.value = rawGroupId.startsWith("GROUP") ? rawGroupId.replace(/^GROUP/, "") : rawGroupId;
|
||||||
checkLoginAndInitTIM();
|
messageList.value = [];
|
||||||
updateNavigationTitle();
|
isLoading.value = false;
|
||||||
});
|
if (options.conversationID) {
|
||||||
|
const cid = decodeQueryValue(options.conversationID);
|
||||||
|
chatInfo.value.conversationID = cid;
|
||||||
|
timChatManager.setConversationID(cid);
|
||||||
|
console.log("设置当前会话ID:", cid);
|
||||||
|
}
|
||||||
|
if (options.userID) {
|
||||||
|
chatInfo.value.userID = decodeQueryValue(options.userID);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkLoginAndInitTIM();
|
||||||
|
updateNavigationTitle();
|
||||||
|
});
|
||||||
|
|
||||||
// 检查登录状态并初始化IM
|
// 检查登录状态并初始化IM
|
||||||
const checkLoginAndInitTIM = async () => {
|
const checkLoginAndInitTIM = async () => {
|
||||||
@ -393,13 +405,13 @@ const checkLoginAndInitTIM = async () => {
|
|||||||
});
|
});
|
||||||
const success = await initIMAfterLogin();
|
const success = await initIMAfterLogin();
|
||||||
uni.hideLoading();
|
uni.hideLoading();
|
||||||
if (!success) {
|
// if (!success) {
|
||||||
uni.showToast({
|
// uni.showToast({
|
||||||
title: "IM连接失败,请重试",
|
// title: "IM连接失败,请重试",
|
||||||
icon: "none",
|
// icon: "none",
|
||||||
});
|
// });
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
} else if (!timChatManager.isLoggedIn) {
|
} else if (!timChatManager.isLoggedIn) {
|
||||||
uni.showLoading({
|
uni.showLoading({
|
||||||
title: "重连中...",
|
title: "重连中...",
|
||||||
@ -769,12 +781,29 @@ onShow(() => {
|
|||||||
checkLoginAndInitTIM();
|
checkLoginAndInitTIM();
|
||||||
} else if (timChatManager.tim && !timChatManager.isLoggedIn) {
|
} else if (timChatManager.tim && !timChatManager.isLoggedIn) {
|
||||||
timChatManager.ensureIMConnection();
|
timChatManager.ensureIMConnection();
|
||||||
|
} else if (timChatManager.tim && timChatManager.isLoggedIn && chatInfo.value.conversationID) {
|
||||||
|
// 页面从后台返回时,重新加载消息列表
|
||||||
|
console.log("页面从后台返回,重新加载消息列表");
|
||||||
|
messageList.value = [];
|
||||||
|
isCompleted.value = false;
|
||||||
|
lastFirstMessageId.value = "";
|
||||||
|
loadMessageList();
|
||||||
}
|
}
|
||||||
|
|
||||||
startIMMonitoring(30000);
|
startIMMonitoring(30000);
|
||||||
|
|
||||||
// 监听回访任务发送事件
|
// 监听回访任务发送事件
|
||||||
uni.$on("send-followup-message", handleSendFollowUpMessage);
|
uni.$on("send-followup-message", handleSendFollowUpMessage);
|
||||||
|
|
||||||
|
// 监听键盘高度变化,自动滚动到底部
|
||||||
|
uni.onKeyboardHeightChange((res) => {
|
||||||
|
if (res.height > 0) {
|
||||||
|
// 键盘弹出,延迟滚动到底部
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollToBottom(true);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理发送回访任务消息
|
// 处理发送回访任务消息
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,118 +1,141 @@
|
|||||||
<template>
|
<template>
|
||||||
<view v-if="team" class="flex flex-col justify-center h-full bg-white">
|
<view v-if="team" class="flex flex-col justify-center h-full bg-white">
|
||||||
<view>
|
<view>
|
||||||
<view class="text-dark text-lg font-semibold text-center mb-10">
|
<view class="text-dark text-lg font-semibold text-center mb-10">
|
||||||
{{ team.name }}
|
{{ team.name }}
|
||||||
</view>
|
</view>
|
||||||
<view class="mb-10 text-dark text-lg font-semibold text-center mb-10">
|
<view class="mb-10 text-dark text-lg font-semibold text-center mb-10">
|
||||||
成员邀请码
|
成员邀请码
|
||||||
</view>
|
</view>
|
||||||
<view class="flex justify-center overflow-hidden">
|
<view class="flex justify-center overflow-hidden">
|
||||||
<uqrcode canvas-id="qrcode" :value="qrcode" :options="options"></uqrcode>
|
<uqrcode canvas-id="qrcode" :value="qrcode" :options="options"></uqrcode>
|
||||||
</view>
|
</view>
|
||||||
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
|
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
|
||||||
微信扫一扫上面的二维码
|
微信扫一扫上面的二维码
|
||||||
</view>
|
</view>
|
||||||
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
|
<view class="mt-10 px-15 text-base text-dark leading-normal text-center">
|
||||||
加入我的团队,协同开展患者管理服务
|
加入我的团队,协同开展患者管理服务
|
||||||
</view>
|
</view>
|
||||||
<view class="mt-10 flex px-15 leading-normal text-center">
|
<view class="mt-10 flex px-15 leading-normal text-center">
|
||||||
<button class="mr-10 border-auto rounded py-5 text-base text-primary flex-grow" @click="saveImage('save')">
|
<button class="mr-10 border-auto rounded py-5 text-base text-primary flex-grow"
|
||||||
保存图片
|
@click="saveImage('save')">
|
||||||
</button>
|
保存图片
|
||||||
<button class="bg-primary rounded py-5 text-base text-white flex-grow" open-type="share">分享微信</button>
|
</button>
|
||||||
</view>
|
<button class="bg-primary rounded py-5 text-base text-white flex-grow" open-type="share">分享微信</button>
|
||||||
<view class="canvas-box">
|
</view>
|
||||||
<l-painter ref="painterRef" :board="poster" />
|
<view class="canvas-box">
|
||||||
</view>
|
<l-painter ref="painterRef" :board="poster" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</view>
|
||||||
<script setup>
|
</template>
|
||||||
import { computed, ref } from "vue";
|
<script setup>
|
||||||
import { storeToRefs } from "pinia";
|
import {
|
||||||
import { onLoad, onShareAppMessage } from "@dcloudio/uni-app";
|
computed,
|
||||||
import useGuard from "@/hooks/useGuard.js";
|
ref
|
||||||
import useAccountStore from "@/store/account.js";
|
} from "vue";
|
||||||
import api from '@/utils/api';
|
import {
|
||||||
import { toast } from "@/utils/widget";
|
storeToRefs
|
||||||
import { getInviteMatePoster } from './base-poster-data';
|
} from "pinia";
|
||||||
|
import {
|
||||||
const env = __VITE_ENV__;
|
onLoad,
|
||||||
const inviteQrcode = env.MP_INVITE_TEAMMATE_QRCODE;
|
onShareAppMessage
|
||||||
|
} from "@dcloudio/uni-app";
|
||||||
const options = { margin: 10 };
|
import useGuard from "@/hooks/useGuard.js";
|
||||||
const team = ref(null);
|
import useAccountStore from "@/store/account.js";
|
||||||
const teamId = ref('');
|
import api from '@/utils/api';
|
||||||
const painterRef = ref()
|
import {
|
||||||
const poster = ref({})
|
toast
|
||||||
|
} from "@/utils/widget";
|
||||||
const { useLoad, useShow } = useGuard();
|
import {
|
||||||
const { account } = storeToRefs(useAccountStore());
|
getInviteMatePoster
|
||||||
|
} from './base-poster-data';
|
||||||
const qrcode = computed(() => `${inviteQrcode}?type=inviteTeam&teamId=${teamId.value}`)
|
|
||||||
|
const env = __VITE_ENV__;
|
||||||
async function getTeam() {
|
const inviteQrcode = env.MP_INVITE_TEAMMATE_QRCODE;
|
||||||
const res = await api('getTeamData', { teamId: teamId.value, corpId: account.value.corpId });
|
|
||||||
if (res && res.data) {
|
const options = {
|
||||||
team.value = res.data;
|
margin: 10
|
||||||
} else {
|
};
|
||||||
toast(res?.message || '获取团队信息失败')
|
const team = ref(null);
|
||||||
}
|
const teamId = ref('');
|
||||||
}
|
const painterRef = ref()
|
||||||
|
const poster = ref({})
|
||||||
async function saveImage(action = 'save') {
|
|
||||||
const data = getInviteMatePoster(team.value.name, qrcode.value)
|
const {
|
||||||
try {
|
useLoad,
|
||||||
await painterRef.value.render(data);
|
useShow
|
||||||
painterRef.value.canvasToTempFilePathSync({
|
} = useGuard();
|
||||||
fileType: "jpg",
|
const {
|
||||||
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum,需要设置 pathType为url
|
account
|
||||||
pathType: 'url',
|
} = storeToRefs(useAccountStore());
|
||||||
quality: 1,
|
|
||||||
success: (res) => {
|
const qrcode = computed(() => `${inviteQrcode}?type=inviteTeam&teamId=${teamId.value}`)
|
||||||
console.log(res.tempFilePath);
|
|
||||||
if (action === 'save') {
|
async function getTeam() {
|
||||||
uni.saveImageToPhotosAlbum({
|
const res = await api('getTeamData', {
|
||||||
filePath: res.tempFilePath,
|
teamId: teamId.value,
|
||||||
success: function () {
|
corpId: account.value.corpId
|
||||||
console.log('save success');
|
});
|
||||||
}
|
if (res && res.data) {
|
||||||
});
|
team.value = res.data;
|
||||||
} else if (action === 'share') {
|
} else {
|
||||||
wx.showShareImageMenu({
|
toast(res?.message || '获取团队信息失败')
|
||||||
path: res.tempFilePath,
|
}
|
||||||
needShowEntrance: false
|
}
|
||||||
})
|
|
||||||
}
|
async function saveImage(action = 'save') {
|
||||||
},
|
const data = getInviteMatePoster(team.value.name, qrcode.value)
|
||||||
});
|
try {
|
||||||
} catch (e) {
|
await painterRef.value.render(data);
|
||||||
toast(e?.message)
|
painterRef.value.canvasToTempFilePathSync({
|
||||||
}
|
fileType: "jpg",
|
||||||
}
|
// 如果返回的是base64是无法使用 saveImageToPhotosAlbum,需要设置 pathType为url
|
||||||
useLoad(options => {
|
pathType: 'url',
|
||||||
teamId.value = options.teamId;
|
quality: 1,
|
||||||
})
|
success: (res) => {
|
||||||
|
console.log(res.tempFilePath);
|
||||||
useShow(() => {
|
if (action === 'save') {
|
||||||
getTeam()
|
uni.saveImageToPhotosAlbum({
|
||||||
});
|
filePath: res.tempFilePath,
|
||||||
|
success: function() {
|
||||||
onShareAppMessage(() => {
|
console.log('save success');
|
||||||
return {
|
}
|
||||||
title: '邀请团队成员',
|
});
|
||||||
path: `pages/login/redirect-page?type=inviteTeam&teamId=${teamId.value}`
|
} else if (action === 'share') {
|
||||||
}
|
wx.showShareImageMenu({
|
||||||
})
|
path: res.tempFilePath,
|
||||||
</script>
|
needShowEntrance: false
|
||||||
<style>
|
})
|
||||||
.canvas-box {
|
}
|
||||||
top: 10000rpx;
|
},
|
||||||
position: absolute;
|
});
|
||||||
z-index: -1;
|
} catch (e) {
|
||||||
width: 0;
|
toast(e?.message)
|
||||||
height: 0;
|
}
|
||||||
overflow: hidden;
|
}
|
||||||
}
|
useLoad(options => {
|
||||||
|
teamId.value = options.teamId;
|
||||||
|
})
|
||||||
|
|
||||||
|
useShow(() => {
|
||||||
|
getTeam()
|
||||||
|
});
|
||||||
|
|
||||||
|
onShareAppMessage(() => {
|
||||||
|
return {
|
||||||
|
title: '邀请团队成员',
|
||||||
|
path: `pages/login/redirect-page?type=inviteTeam&teamId=${teamId.value}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.canvas-box {
|
||||||
|
top: 10000rpx;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -41,6 +41,7 @@ export default defineStore("accountStore", () => {
|
|||||||
|
|
||||||
async function loginByCode(phoneCode = '') {
|
async function loginByCode(phoneCode = '') {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const { code } = await uni.login({
|
const { code } = await uni.login({
|
||||||
appid,
|
appid,
|
||||||
provider: "weixin",
|
provider: "weixin",
|
||||||
@ -61,13 +62,12 @@ export default defineStore("accountStore", () => {
|
|||||||
}
|
}
|
||||||
account.value = res.data;
|
account.value = res.data;
|
||||||
openid.value = res.data.openid;
|
openid.value = res.data.openid;
|
||||||
|
|
||||||
// 持久化账户信息
|
// 持久化账户信息
|
||||||
cache.set(CACHE_KEYS.ACCOUNT, res.data);
|
cache.set(CACHE_KEYS.ACCOUNT, res.data);
|
||||||
cache.set(CACHE_KEYS.OPENID, res.data.openid);
|
cache.set(CACHE_KEYS.OPENID, res.data.openid);
|
||||||
|
|
||||||
// 登录成功后初始化腾讯IM
|
// 登录成功后初始化腾讯IM
|
||||||
await getDoctorInfo(openid.value);
|
await getDoctorInfo({ openid: openid.value });
|
||||||
await initIMAfterLogin();
|
await initIMAfterLogin();
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
@ -109,8 +109,7 @@ export default defineStore("accountStore", () => {
|
|||||||
async function initIMAfterLogin() {
|
async function initIMAfterLogin() {
|
||||||
if (isIMInitialized.value) return true;
|
if (isIMInitialized.value) return true;
|
||||||
if (!doctorInfo.value) {
|
if (!doctorInfo.value) {
|
||||||
console.error('医生信息未获取,无法初始化IM');
|
await getDoctorInfo();
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const userID = doctorInfo.value.userid;
|
const userID = doctorInfo.value.userid;
|
||||||
@ -120,13 +119,13 @@ export default defineStore("accountStore", () => {
|
|||||||
throw new Error('无法获取用户ID');
|
throw new Error('无法获取用户ID');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await initGlobalTIM(userID);
|
const success = await initGlobalTIM(userID);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
console.error('initGlobalTIM 返回失败');
|
console.error('initGlobalTIM 返回失败');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isIMInitialized.value = true;
|
isIMInitialized.value = true;
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -10,6 +10,8 @@ const urlsConfig = {
|
|||||||
getTeamData: 'getTeamData',
|
getTeamData: 'getTeamData',
|
||||||
getTeamBymember: 'getTeamBymember',
|
getTeamBymember: 'getTeamBymember',
|
||||||
getCurrentTemplate: 'getCurrentTemplate',
|
getCurrentTemplate: 'getCurrentTemplate',
|
||||||
|
getTemplateGroup: 'getTemplateGroup',
|
||||||
|
getTemplateListByTemptype: 'getTemplateListByTemptype',
|
||||||
wxAppLogin: 'wxAppLogin',
|
wxAppLogin: 'wxAppLogin',
|
||||||
getDeptList: 'getRealDeptList',
|
getDeptList: 'getRealDeptList',
|
||||||
getHospitalList: 'getRealHospital',
|
getHospitalList: 'getRealHospital',
|
||||||
|
|||||||
@ -1,10 +1,39 @@
|
|||||||
const env = __VITE_ENV__;
|
const env = __VITE_ENV__;
|
||||||
|
|
||||||
|
function joinBaseAndPath(base, path) {
|
||||||
|
const b = String(base || '').replace(/\/+$/, '');
|
||||||
|
const p = String(path || '');
|
||||||
|
if (!p) return b;
|
||||||
|
if (/^https?:\/\//i.test(p)) return p;
|
||||||
|
return `${b}/${p.replace(/^\/+/, '')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容历史数据:某些链接缺少域名后的 /
|
||||||
|
export function normalizeFileUrl(url) {
|
||||||
|
const u = String(url || '').trim();
|
||||||
|
if (!u) return '';
|
||||||
|
if (/^https?:\/\//i.test(u)) {
|
||||||
|
const afterProtocolIndex = u.indexOf('://') + 3;
|
||||||
|
const firstSlashIndex = u.indexOf('/', afterProtocolIndex);
|
||||||
|
if (firstSlashIndex > 0) {
|
||||||
|
const prefix = u.slice(0, afterProtocolIndex);
|
||||||
|
const host = u.slice(afterProtocolIndex, firstSlashIndex);
|
||||||
|
const path = u.slice(firstSlashIndex);
|
||||||
|
if (host.toLowerCase().endsWith('uploads') && !path.toLowerCase().startsWith('/uploads/')) {
|
||||||
|
const fixedHost = host.slice(0, -'uploads'.length);
|
||||||
|
const normalizedPath = `/uploads${path.startsWith('/') ? '' : '/'}${path.replace(/^\/+/, '')}`;
|
||||||
|
return `${prefix}${fixedHost}${normalizedPath}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return u.replace(/^(https?:\/\/[^/]+)(uploads\/)/i, '$1/$2');
|
||||||
|
}
|
||||||
|
|
||||||
export async function uploadFile(tempFilePath) {
|
export async function uploadFile(tempFilePath) {
|
||||||
try {
|
try {
|
||||||
const res = await new Promise((resolve, reject) => {
|
const res = await new Promise((resolve, reject) => {
|
||||||
uni.uploadFile({
|
uni.uploadFile({
|
||||||
url: `${env.MP_API_BASE_URL}/upload`,
|
url: joinBaseAndPath(env.MP_API_BASE_URL, 'upload'),
|
||||||
filePath: tempFilePath,
|
filePath: tempFilePath,
|
||||||
name: 'file',
|
name: 'file',
|
||||||
success: (resp) => resolve(resp),
|
success: (resp) => resolve(resp),
|
||||||
@ -14,7 +43,7 @@ export async function uploadFile(tempFilePath) {
|
|||||||
|
|
||||||
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
const data = typeof res.data === 'string' ? JSON.parse(res.data) : res.data;
|
||||||
if (data && data.success && data.filePath) {
|
if (data && data.success && data.filePath) {
|
||||||
return `${env.MP_API_BASE_URL}${data.filePath}`;
|
return normalizeFileUrl(joinBaseAndPath(env.MP_API_BASE_URL, data.filePath));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('upload file error:', e);
|
console.log('upload file error:', e);
|
||||||
@ -58,5 +87,3 @@ export async function chooseAndUploadImage(options = {}) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -283,7 +283,6 @@ function generateSendLink(survey, answerId, customerId, customerName, sendSurvey
|
|||||||
const { corpId, userId } = context;
|
const { corpId, userId } = context;
|
||||||
const isSystem = survey.createBy === 'system';
|
const isSystem = survey.createBy === 'system';
|
||||||
let url = '';
|
let url = '';
|
||||||
debugger
|
|
||||||
if (isSystem) {
|
if (isSystem) {
|
||||||
// 系统问卷:使用 VITE_SURVEY_URL
|
// 系统问卷:使用 VITE_SURVEY_URL
|
||||||
url = `${env?.MP_SURVEY_URL}?corpId=${corpId}&surveryId=${survey.surveryId}&memberId=${customerId}&sendSurveyId=${sendSurveyId}&userId=${userId}`;
|
url = `${env?.MP_SURVEY_URL}?corpId=${corpId}&surveryId=${survey.surveryId}&memberId=${customerId}&sendSurveyId=${sendSurveyId}&userId=${userId}`;
|
||||||
@ -359,6 +358,7 @@ export async function handleFollowUpMessages(messages, context = {}) {
|
|||||||
corpId: context.corpId,
|
corpId: context.corpId,
|
||||||
});
|
});
|
||||||
} else if (msg.type === 'questionnaire') {
|
} else if (msg.type === 'questionnaire') {
|
||||||
|
|
||||||
success = await sendSurveyMessage(msg.content, {
|
success = await sendSurveyMessage(msg.content, {
|
||||||
userId: context.userId,
|
userId: context.userId,
|
||||||
customerId: context.customerId,
|
customerId: context.customerId,
|
||||||
|
|||||||
@ -1030,6 +1030,13 @@ class TimChatManager {
|
|||||||
// 获取群聊列表
|
// 获取群聊列表
|
||||||
getGroupList() {
|
getGroupList() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// 检查userId是否存在,不存在则不需要初始化
|
||||||
|
if (!this.currentUserID) {
|
||||||
|
console.error('currentUserID不存在,无法获取群聊列表')
|
||||||
|
reject(new Error('用户ID不存在'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 如果 TIM 实例不存在,等待初始化
|
// 如果 TIM 实例不存在,等待初始化
|
||||||
if (!this.tim) {
|
if (!this.tim) {
|
||||||
console.log('TIM实例不存在,等待初始化...')
|
console.log('TIM实例不存在,等待初始化...')
|
||||||
@ -1071,12 +1078,13 @@ class TimChatManager {
|
|||||||
if (timeoutHandle) clearTimeout(timeoutHandle)
|
if (timeoutHandle) clearTimeout(timeoutHandle)
|
||||||
this.getGroupListInternal().then(resolve).catch(reject)
|
this.getGroupListInternal().then(resolve).catch(reject)
|
||||||
} else if (waitTime >= maxWaitTime) {
|
} else if (waitTime >= maxWaitTime) {
|
||||||
console.error('等待SDK就绪超时')
|
console.error('等待SDK就绪超时,当前isLoggedIn:', this.isLoggedIn)
|
||||||
if (timeoutHandle) clearTimeout(timeoutHandle)
|
if (timeoutHandle) clearTimeout(timeoutHandle)
|
||||||
|
// 超时时返回错误而不是继续等待
|
||||||
reject(new Error('SDK初始化超时,请检查网络连接'))
|
reject(new Error('SDK初始化超时,请检查网络连接'))
|
||||||
} else {
|
} else {
|
||||||
waitTime += checkInterval
|
waitTime += checkInterval
|
||||||
console.log(`等待SDK就绪... (${Math.floor(waitTime / 1000)}/${Math.floor(maxWaitTime / 1000)}秒)`)
|
console.log(`等待SDK就绪... (${Math.floor(waitTime / 1000)}/${Math.floor(maxWaitTime / 1000)}秒, isLoggedIn: ${this.isLoggedIn})`)
|
||||||
timeoutHandle = setTimeout(checkSDKReady, checkInterval)
|
timeoutHandle = setTimeout(checkSDKReady, checkInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2753,6 +2761,7 @@ class TimChatManager {
|
|||||||
|
|
||||||
// 标记会话为已读
|
// 标记会话为已读
|
||||||
markConversationAsRead(conversationID) {
|
markConversationAsRead(conversationID) {
|
||||||
|
|
||||||
if (!this.tim || !this.isLoggedIn) {
|
if (!this.tim || !this.isLoggedIn) {
|
||||||
console.log('⚠️ TIM未初始化或未登录,无法标记会话已读');
|
console.log('⚠️ TIM未初始化或未登录,无法标记会话已读');
|
||||||
return;
|
return;
|
||||||
@ -2772,6 +2781,7 @@ class TimChatManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 更新会话列表
|
// 更新会话列表
|
||||||
updateConversationListOnNewMessage(message) {
|
updateConversationListOnNewMessage(message) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user