Compare commits
No commits in common. "7f737dfd8ab10428fe79fb4d67bee9d52361f55d" and "e639ddc3a2a2750d386abfd71ee1b24d940e0ea5" have entirely different histories.
7f737dfd8a
...
e639ddc3a2
@ -115,12 +115,6 @@
|
|||||||
"navigationBarTitleText": "病历详情"
|
"navigationBarTitleText": "病历详情"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "pages/case/medical-case-form",
|
|
||||||
"style": {
|
|
||||||
"navigationBarTitleText": "添加病历"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "pages/case/service-record-detail",
|
"path": "pages/case/service-record-detail",
|
||||||
"style": {
|
"style": {
|
||||||
|
|||||||
@ -1,486 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="medical-case-form">
|
|
||||||
<view class="form-container">
|
|
||||||
<!-- 门诊病历 -->
|
|
||||||
<template v-if="caseType === 'outpatient'">
|
|
||||||
<view class="form-item required">
|
|
||||||
<view class="item-label">就诊机构</view>
|
|
||||||
<input
|
|
||||||
class="item-input"
|
|
||||||
v-model="formData.hospital"
|
|
||||||
placeholder="暂无"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item required">
|
|
||||||
<view class="item-label">就诊日期</view>
|
|
||||||
<picker
|
|
||||||
mode="date"
|
|
||||||
:value="formData.visitTime"
|
|
||||||
@change="onDateChange('visitTime', $event)"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
>
|
|
||||||
<view class="picker-value">
|
|
||||||
{{ formData.visitTime || '暂无' }}
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item required">
|
|
||||||
<view class="item-label">门诊诊断</view>
|
|
||||||
<textarea
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData.diagnosisName"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item">
|
|
||||||
<view class="item-label">治疗方案</view>
|
|
||||||
<textarea
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData.treatmentPlan"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 住院病历 -->
|
|
||||||
<template v-if="caseType === 'inpatient'">
|
|
||||||
<view class="form-item required">
|
|
||||||
<view class="item-label">就诊机构</view>
|
|
||||||
<input
|
|
||||||
class="item-input"
|
|
||||||
v-model="formData.hospital"
|
|
||||||
placeholder="暂无"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item required">
|
|
||||||
<view class="item-label">入院日期</view>
|
|
||||||
<picker
|
|
||||||
mode="date"
|
|
||||||
:value="formData.inhosDate"
|
|
||||||
@change="onDateChange('inhosDate', $event)"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
>
|
|
||||||
<view class="picker-value">
|
|
||||||
{{ formData.inhosDate || '暂无' }}
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item required">
|
|
||||||
<view class="item-label">住院主诊断</view>
|
|
||||||
<textarea
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData.diagnosisName"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item">
|
|
||||||
<view class="item-label">手术名称</view>
|
|
||||||
<textarea
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData.operation"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item">
|
|
||||||
<view class="item-label">手术日期</view>
|
|
||||||
<picker
|
|
||||||
mode="date"
|
|
||||||
:value="formData.operationDate"
|
|
||||||
@change="onDateChange('operationDate', $event)"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
>
|
|
||||||
<view class="picker-value">
|
|
||||||
{{ formData.operationDate || '暂无' }}
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item">
|
|
||||||
<view class="item-label">治疗方案</view>
|
|
||||||
<textarea
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData.treatmentPlan"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 体检记录 -->
|
|
||||||
<template v-if="caseType === 'physicalExam'">
|
|
||||||
<view class="form-item required">
|
|
||||||
<view class="item-label">就诊机构</view>
|
|
||||||
<input
|
|
||||||
class="item-input"
|
|
||||||
v-model="formData.hospital"
|
|
||||||
placeholder="暂无"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item required">
|
|
||||||
<view class="item-label">体检日期</view>
|
|
||||||
<picker
|
|
||||||
mode="date"
|
|
||||||
:value="formData.inspectTime"
|
|
||||||
@change="onDateChange('inspectTime', $event)"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
>
|
|
||||||
<view class="picker-value">
|
|
||||||
{{ formData.inspectTime || '暂无' }}
|
|
||||||
</view>
|
|
||||||
</picker>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item">
|
|
||||||
<view class="item-label">体检小结</view>
|
|
||||||
<textarea
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData.inspectSummary"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item">
|
|
||||||
<view class="item-label">阳性发现及处理意见</view>
|
|
||||||
<textarea
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData.positiveFind"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 预问诊记录 -->
|
|
||||||
<template v-if="caseType === 'preConsultation'">
|
|
||||||
<view class="form-item">
|
|
||||||
<view class="item-label">主诉</view>
|
|
||||||
<textarea
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData.chiefComplaint"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item">
|
|
||||||
<view class="item-label">现病史</view>
|
|
||||||
<textarea
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData.presentIllnessHistory"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-item">
|
|
||||||
<view class="item-label">既往史</view>
|
|
||||||
<textarea
|
|
||||||
class="item-textarea"
|
|
||||||
v-model="formData.pastMedicalHistory"
|
|
||||||
placeholder="请输入"
|
|
||||||
:disabled="!isEditing"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<view class="tips-box">
|
|
||||||
<text class="tips-text">
|
|
||||||
1、门诊、住院病历记录生成,生成后支持医生在线编辑,并保存至档案,或者重新生成;
|
|
||||||
</text>
|
|
||||||
<text class="tips-text">
|
|
||||||
2、若未来集到有效信息则以模板字段中默认项写无内容生成,医生可以直接在存字段上进行编辑。
|
|
||||||
</text>
|
|
||||||
</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, onMounted } from 'vue';
|
|
||||||
|
|
||||||
const caseType = ref('');
|
|
||||||
const formData = ref({});
|
|
||||||
const isEditing = ref(true);
|
|
||||||
const customerId = ref('');
|
|
||||||
const groupId = ref('');
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const pages = getCurrentPages();
|
|
||||||
const currentPage = pages[pages.length - 1];
|
|
||||||
const options = currentPage.options;
|
|
||||||
|
|
||||||
caseType.value = options.caseType || '';
|
|
||||||
customerId.value = options.customerId || '';
|
|
||||||
groupId.value = options.groupId || '';
|
|
||||||
|
|
||||||
// 从 options 中解析表单数据
|
|
||||||
if (options.formData) {
|
|
||||||
try {
|
|
||||||
formData.value = JSON.parse(decodeURIComponent(options.formData));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('解析表单数据失败:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置页面标题
|
|
||||||
const titles = {
|
|
||||||
outpatient: '添加门诊病历',
|
|
||||||
inpatient: '添加住院病历',
|
|
||||||
physicalExam: '添加体检记录',
|
|
||||||
preConsultation: '添加预问诊记录'
|
|
||||||
};
|
|
||||||
uni.setNavigationBarTitle({
|
|
||||||
title: titles[caseType.value] || '添加病历'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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 api = (await import('@/utils/api.js')).default;
|
|
||||||
const result = await api('addMedicalRecord', {
|
|
||||||
customerId: customerId.value,
|
|
||||||
caseType: caseType.value,
|
|
||||||
...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 = () => {
|
|
||||||
const fieldsMap = {
|
|
||||||
outpatient: [
|
|
||||||
{ key: 'hospital', label: '就诊机构' },
|
|
||||||
{ key: 'visitTime', label: '就诊日期' },
|
|
||||||
{ key: 'diagnosisName', label: '门诊诊断' }
|
|
||||||
],
|
|
||||||
inpatient: [
|
|
||||||
{ key: 'hospital', label: '就诊机构' },
|
|
||||||
{ key: 'inhosDate', label: '入院日期' },
|
|
||||||
{ key: 'diagnosisName', label: '住院主诊断' }
|
|
||||||
],
|
|
||||||
physicalExam: [
|
|
||||||
{ key: 'hospital', label: '就诊机构' },
|
|
||||||
{ key: 'inspectTime', label: '体检日期' }
|
|
||||||
],
|
|
||||||
preConsultation: []
|
|
||||||
};
|
|
||||||
|
|
||||||
return fieldsMap[caseType.value] || [];
|
|
||||||
};
|
|
||||||
</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: 160rpx;
|
|
||||||
padding: 20rpx 24rpx;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #333333;
|
|
||||||
|
|
||||||
&[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;
|
|
||||||
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>
|
|
||||||
@ -348,30 +348,23 @@ $primary-color: #0877F1;
|
|||||||
.text-input,
|
.text-input,
|
||||||
.voice-input-btn {
|
.voice-input-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 16rpx 46rpx;
|
padding: 0 46rpx;
|
||||||
background-color: #f3f5fa;
|
background-color: #f3f5fa;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
margin: 0 16rpx;
|
margin: 0 16rpx;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
min-height: 80rpx;
|
height: 80rpx;
|
||||||
max-height: 200rpx;
|
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
line-height: 1.5;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 96rpx;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.voice-input-btn {
|
|
||||||
height: 80rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 46rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voice-input-btn {
|
.voice-input-btn {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 80rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.more-panel {
|
.more-panel {
|
||||||
|
|||||||
@ -1,411 +0,0 @@
|
|||||||
<template>
|
|
||||||
<view class="ai-assistant-buttons">
|
|
||||||
<view
|
|
||||||
v-for="button in buttons"
|
|
||||||
:key="button.id"
|
|
||||||
class="ai-button"
|
|
||||||
:class="{ loading: button.loading }"
|
|
||||||
@click="handleButtonClick(button)"
|
|
||||||
>
|
|
||||||
<image class="button-icon" :src="button.icon" mode="aspectFit" />
|
|
||||||
<text class="button-text">{{
|
|
||||||
button.loading && button.loadingText ? button.loadingText : button.text
|
|
||||||
}}</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 病历类型选择弹窗 -->
|
|
||||||
<medical-case-type-selector
|
|
||||||
ref="typeSelectorRef"
|
|
||||||
@select="handleCaseTypeSelect"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 进度显示弹窗 -->
|
|
||||||
<medical-case-progress
|
|
||||||
ref="progressRef"
|
|
||||||
@regenerate="handleRegenerateFromProgress"
|
|
||||||
@next="handleNextFromProgress"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
|
||||||
import request from "@/utils/http.js";
|
|
||||||
import api from "@/utils/api.js";
|
|
||||||
import MedicalCaseTypeSelector from "./medical-case-type-selector.vue";
|
|
||||||
import MedicalCaseProgress from "./medical-case-progress.vue";
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
groupId: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
patientAccountId: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
corpId: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
customerId: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const emit = defineEmits(["streamText", "clearInput"]);
|
|
||||||
|
|
||||||
const typeSelectorRef = ref(null);
|
|
||||||
const progressRef = ref(null);
|
|
||||||
const buttons = ref([
|
|
||||||
{
|
|
||||||
id: "followUp",
|
|
||||||
text: "追问病情",
|
|
||||||
loadingText: "AI分析中,正在为您生成追问建议…",
|
|
||||||
icon: "/static/icon/zhuiwen.png",
|
|
||||||
loading: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "aiAssistant",
|
|
||||||
text: "开启AI助手",
|
|
||||||
icon: "/static/icon/kaiqiAI.png",
|
|
||||||
loading: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "supplementRecord",
|
|
||||||
text: "补充病历",
|
|
||||||
icon: "/static/icon/buchong.png",
|
|
||||||
loading: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 处理按钮点击
|
|
||||||
const handleButtonClick = async (button) => {
|
|
||||||
if (button.loading) return;
|
|
||||||
|
|
||||||
switch (button.id) {
|
|
||||||
case "followUp":
|
|
||||||
await handleFollowUpInquiry(button);
|
|
||||||
break;
|
|
||||||
case "aiAssistant":
|
|
||||||
handleAIAssistant(button);
|
|
||||||
break;
|
|
||||||
case "supplementRecord":
|
|
||||||
handleSupplementRecord(button);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理追问病情
|
|
||||||
const handleFollowUpInquiry = async (button) => {
|
|
||||||
try {
|
|
||||||
button.loading = true;
|
|
||||||
|
|
||||||
// 如果没有提供患者账号ID,先获取群组信息
|
|
||||||
let finalPatientAccountId = props.patientAccountId;
|
|
||||||
|
|
||||||
if (!finalPatientAccountId) {
|
|
||||||
// 从聊天记录中获取患者账号ID(非当前用户的发送者)
|
|
||||||
const chatRecordsResult = await api("getChatRecordsByGroupId", {
|
|
||||||
groupID: props.groupId,
|
|
||||||
count: 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
chatRecordsResult.success &&
|
|
||||||
chatRecordsResult.data &&
|
|
||||||
chatRecordsResult.data.records
|
|
||||||
) {
|
|
||||||
const records = chatRecordsResult.data.records;
|
|
||||||
// 找到第一个非当前用户的发送者作为患者账号
|
|
||||||
const currentUserId = uni.getStorageSync("openid") || "";
|
|
||||||
const patientMessage = records.find(
|
|
||||||
(record) =>
|
|
||||||
record.From_Account && record.From_Account !== currentUserId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (patientMessage) {
|
|
||||||
finalPatientAccountId = patientMessage.From_Account;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!finalPatientAccountId) {
|
|
||||||
uni.showToast({
|
|
||||||
title: "无法获取患者信息",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
button.loading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用追问病情接口(不显示全局loading)
|
|
||||||
const result = await request(
|
|
||||||
{
|
|
||||||
url: "/getYoucanData/im",
|
|
||||||
data: {
|
|
||||||
type: "followUpInquiry",
|
|
||||||
groupId: props.groupId,
|
|
||||||
patientAccountId: finalPatientAccountId,
|
|
||||||
corpId: props.corpId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
false
|
|
||||||
); // 第二个参数false表示不显示loading
|
|
||||||
|
|
||||||
if (result.success && result.data && result.data.suggestion) {
|
|
||||||
// 流式输出文本到输入框
|
|
||||||
streamTextToInput(result.data.suggestion);
|
|
||||||
} else {
|
|
||||||
uni.showToast({
|
|
||||||
title: result.message || "获取追问建议失败",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("追问病情失败:", error);
|
|
||||||
uni.showToast({
|
|
||||||
title: "操作失败,请重试",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
button.loading = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 流式输出文本到输入框
|
|
||||||
const streamTextToInput = (text) => {
|
|
||||||
if (!text) return;
|
|
||||||
|
|
||||||
// 先清空输入框
|
|
||||||
emit("clearInput");
|
|
||||||
|
|
||||||
let currentIndex = 0;
|
|
||||||
const speed = 50; // 每个字符的延迟时间(毫秒)
|
|
||||||
|
|
||||||
// 延迟一小段时间后开始流式输出,确保清空操作完成
|
|
||||||
setTimeout(() => {
|
|
||||||
const streamInterval = setInterval(() => {
|
|
||||||
if (currentIndex < text.length) {
|
|
||||||
const char = text[currentIndex];
|
|
||||||
emit("streamText", char);
|
|
||||||
currentIndex++;
|
|
||||||
} else {
|
|
||||||
clearInterval(streamInterval);
|
|
||||||
}
|
|
||||||
}, speed);
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理AI助手
|
|
||||||
const handleAIAssistant = (button) => {
|
|
||||||
uni.showToast({
|
|
||||||
title: "AI助手功能开发中",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理补充病历
|
|
||||||
const handleSupplementRecord = (button) => {
|
|
||||||
typeSelectorRef.value?.open();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理病历类型选择
|
|
||||||
const handleCaseTypeSelect = async (type) => {
|
|
||||||
try {
|
|
||||||
// 打开进度弹窗
|
|
||||||
progressRef.value?.open(type.id);
|
|
||||||
progressRef.value?.updateProgress(10);
|
|
||||||
|
|
||||||
// 调用补充病历接口(流式处理)
|
|
||||||
await requestWithStream({
|
|
||||||
url: "/getYoucanData/im",
|
|
||||||
data: {
|
|
||||||
type: "supplementMedicalCase",
|
|
||||||
groupId: props.groupId,
|
|
||||||
patientAccountId: props.patientAccountId || props.customerId,
|
|
||||||
corpId: props.corpId,
|
|
||||||
caseType: type.id,
|
|
||||||
},
|
|
||||||
onProgress: (data) => {
|
|
||||||
// 处理流式数据
|
|
||||||
handleStreamData(data, type.id);
|
|
||||||
},
|
|
||||||
onComplete: (finalData) => {
|
|
||||||
// 完成后跳转
|
|
||||||
handleComplete(finalData, type.id);
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
progressRef.value?.close();
|
|
||||||
uni.showToast({
|
|
||||||
title: error.message || "生成病历失败",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("补充病历失败:", error);
|
|
||||||
progressRef.value?.close();
|
|
||||||
uni.showToast({
|
|
||||||
title: "操作失败,请重试",
|
|
||||||
icon: "none",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 流式请求处理
|
|
||||||
const requestWithStream = async ({ url, data, onProgress, onComplete, onError }) => {
|
|
||||||
try {
|
|
||||||
// 调用接口时不显示全局 loading(第二个参数为 false)
|
|
||||||
const result = await request({
|
|
||||||
url,
|
|
||||||
data,
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
if (result.success && result.data) {
|
|
||||||
// 模拟流式处理(如果后端返回的是完整数据)
|
|
||||||
const extractedData = result.data.extractedData || {};
|
|
||||||
|
|
||||||
// 逐个字段动态显示(包括空值字段)
|
|
||||||
let progressValue = 20;
|
|
||||||
const fields = Object.entries(extractedData);
|
|
||||||
const delay = 300; // 每个字段显示间隔
|
|
||||||
|
|
||||||
for (let i = 0; i < fields.length; i++) {
|
|
||||||
const [key, value] = fields[i];
|
|
||||||
|
|
||||||
// 显示所有字段,包括空值(会在组件中显示为"暂无")
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
|
||||||
onProgress({ key, value });
|
|
||||||
progressValue += Math.floor(60 / fields.length);
|
|
||||||
progressRef.value?.updateProgress(Math.min(progressValue, 80));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 完成
|
|
||||||
onComplete(result.data);
|
|
||||||
} else {
|
|
||||||
onError(new Error(result.message || "请求失败"));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
onError(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理流式数据
|
|
||||||
const handleStreamData = (data, caseType) => {
|
|
||||||
const { key, value } = data;
|
|
||||||
|
|
||||||
// 添加检测到的信息
|
|
||||||
progressRef.value?.addDetectedInfo(key, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理完成
|
|
||||||
const handleComplete = (finalData, caseType) => {
|
|
||||||
progressRef.value?.updateProgress(90);
|
|
||||||
progressRef.value?.setGenerating(true);
|
|
||||||
|
|
||||||
// 延迟后完成
|
|
||||||
setTimeout(() => {
|
|
||||||
progressRef.value?.updateProgress(100);
|
|
||||||
progressRef.value?.setGenerating(false);
|
|
||||||
|
|
||||||
// 延迟后显示操作按钮(不自动跳转)
|
|
||||||
setTimeout(() => {
|
|
||||||
progressRef.value?.setCompleted(true, finalData);
|
|
||||||
}, 500);
|
|
||||||
}, 800);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理从进度弹窗点击重新生成
|
|
||||||
const handleRegenerateFromProgress = (data) => {
|
|
||||||
const type = { id: data.caseType };
|
|
||||||
handleCaseTypeSelect(type);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理从进度弹窗点击下一步
|
|
||||||
const handleNextFromProgress = (data) => {
|
|
||||||
// 跳转到病历填写页面
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/case/medical-case-form?caseType=${data.caseType}&customerId=${
|
|
||||||
props.customerId || props.patientAccountId
|
|
||||||
}&groupId=${props.groupId}&formData=${encodeURIComponent(
|
|
||||||
JSON.stringify(data.data?.extractedData || {})
|
|
||||||
)}`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听重新生成事件
|
|
||||||
onMounted(() => {
|
|
||||||
uni.$on("regenerateMedicalCase", handleRegenerateMedicalCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
uni.$off("regenerateMedicalCase", handleRegenerateMedicalCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleRegenerateMedicalCase = (data) => {
|
|
||||||
const type = { id: data.caseType };
|
|
||||||
handleCaseTypeSelect(type);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.ai-assistant-buttons {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16rpx;
|
|
||||||
padding: 16rpx 24rpx;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-bottom: 1rpx solid #e5e5e5;
|
|
||||||
|
|
||||||
.ai-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8rpx;
|
|
||||||
padding: 12rpx 20rpx;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: 1rpx solid #e0e0e0;
|
|
||||||
border-radius: 40rpx;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&.loading {
|
|
||||||
opacity: 0.8;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
.loading-icon {
|
|
||||||
width: 32rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
border: 3rpx solid #e0e0e0;
|
|
||||||
border-top-color: #1890ff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-icon {
|
|
||||||
width: 32rpx;
|
|
||||||
height: 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #333333;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -6,9 +6,8 @@
|
|||||||
<uni-icons v-else type="mic" size="28" color="#666" />
|
<uni-icons v-else type="mic" size="28" color="#666" />
|
||||||
</view>
|
</view>
|
||||||
<view class="input-area">
|
<view class="input-area">
|
||||||
<textarea v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..."
|
<input v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..."
|
||||||
@confirm="sendTextMessage" @focus="handleInputFocus" @input="handleInput"
|
@confirm="sendTextMessage" @focus="handleInputFocus" />
|
||||||
:auto-height="true" :show-confirm-bar="false" :adjust-position="true" />
|
|
||||||
<input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord"
|
<input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord"
|
||||||
@touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled>
|
@touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled>
|
||||||
</input>
|
</input>
|
||||||
@ -26,6 +25,7 @@
|
|||||||
<text>{{ btn.text }}</text>
|
<text>{{ btn.text }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 录音遮罩层 -->
|
<!-- 录音遮罩层 -->
|
||||||
<view v-if="isRecording" class="recording-overlay">
|
<view v-if="isRecording" class="recording-overlay">
|
||||||
<view class="recording-modal" :class="{ 'cancel-mode': isCancelMode }">
|
<view class="recording-modal" :class="{ 'cancel-mode': isCancelMode }">
|
||||||
@ -95,11 +95,6 @@ const cloudCustomData = computed(() => {
|
|||||||
return arr.filter(Boolean).join("|");
|
return arr.filter(Boolean).join("|");
|
||||||
});
|
});
|
||||||
|
|
||||||
// 流式输入文本
|
|
||||||
const appendStreamText = (char) => {
|
|
||||||
inputText.value += char;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 录音相关扩展状态(特效 + 取消逻辑)
|
// 录音相关扩展状态(特效 + 取消逻辑)
|
||||||
const recordingDuration = ref(0);
|
const recordingDuration = ref(0);
|
||||||
let recordingTimer = null;
|
let recordingTimer = null;
|
||||||
@ -170,22 +165,9 @@ const sendTextMessageFromPhrase = async (content) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 设置输入框文本(覆盖原内容)
|
|
||||||
const setInputText = (text) => {
|
|
||||||
inputText.value = text;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 清空输入框
|
|
||||||
const clearInputText = () => {
|
|
||||||
inputText.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 暴露方法给父组件调用
|
// 暴露方法给父组件调用
|
||||||
defineExpose({
|
defineExpose({
|
||||||
sendTextMessageFromPhrase,
|
sendTextMessageFromPhrase
|
||||||
appendStreamText,
|
|
||||||
setInputText,
|
|
||||||
clearInputText
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 发送图片消息
|
// 发送图片消息
|
||||||
@ -450,13 +432,6 @@ function handleInputFocus() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInput(e) {
|
|
||||||
// textarea 输入时触发,可以在这里处理额外逻辑
|
|
||||||
nextTick().then(() => {
|
|
||||||
emit("scrollToBottom");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化录音管理器
|
// 初始化录音管理器
|
||||||
initRecorderManager();
|
initRecorderManager();
|
||||||
|
|||||||
@ -1,374 +0,0 @@
|
|||||||
<template>
|
|
||||||
<uni-popup ref="popup" type="center" :mask-click="false">
|
|
||||||
<view class="progress-modal">
|
|
||||||
<view class="close-btn" @click="close">
|
|
||||||
<text class="close-icon">✕</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="progress-content">
|
|
||||||
<view class="progress-title">{{ progressTitle }}</view>
|
|
||||||
|
|
||||||
<view class="progress-bar-wrapper">
|
|
||||||
<view class="progress-bar">
|
|
||||||
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
|
|
||||||
</view>
|
|
||||||
<text class="progress-text">{{ progress }}%</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="detected-info">
|
|
||||||
<text class="detected-title">检测到以下{{ caseTypeName }}信息:</text>
|
|
||||||
<view class="info-list">
|
|
||||||
<view
|
|
||||||
v-for="(item, index) in detectedInfo"
|
|
||||||
:key="index"
|
|
||||||
class="info-item"
|
|
||||||
:class="{ 'fade-in': item.animated }"
|
|
||||||
>
|
|
||||||
<text class="check-icon">✓</text>
|
|
||||||
<text
|
|
||||||
class="info-text"
|
|
||||||
:class="{ 'empty-value': item.value === '暂无' }"
|
|
||||||
>
|
|
||||||
{{ item.label }}:{{ item.value }}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view v-if="isGenerating" class="generating-text">
|
|
||||||
<text class="dot-animation">正在生成结构化{{ caseTypeName }}</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 完成后的操作按钮 -->
|
|
||||||
<view v-if="isCompleted" class="action-buttons">
|
|
||||||
<view class="action-button secondary" @click="handleRegenerate">
|
|
||||||
<text class="button-text">重新生成</text>
|
|
||||||
</view>
|
|
||||||
<view class="action-button primary" @click="handleNext">
|
|
||||||
<text class="button-text">下一步</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</uni-popup>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, computed } from 'vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['regenerate', 'next']);
|
|
||||||
|
|
||||||
const popup = ref(null);
|
|
||||||
const progress = ref(0);
|
|
||||||
const detectedInfo = ref([]);
|
|
||||||
const isGenerating = ref(false);
|
|
||||||
const isCompleted = ref(false);
|
|
||||||
const caseType = ref('');
|
|
||||||
const finalData = ref(null);
|
|
||||||
|
|
||||||
const CASE_TYPE_NAMES = {
|
|
||||||
outpatient: '门诊病历',
|
|
||||||
inpatient: '住院病历',
|
|
||||||
physicalExam: '体检记录',
|
|
||||||
preConsultation: '预问诊记录'
|
|
||||||
};
|
|
||||||
|
|
||||||
const FIELD_LABELS = {
|
|
||||||
// 门诊病历
|
|
||||||
visitTime: '就诊日期',
|
|
||||||
chiefComplaint: '主诉',
|
|
||||||
medicalHistorySummary: '病史概要',
|
|
||||||
examination: '检查',
|
|
||||||
diagnosisName: '门诊诊断',
|
|
||||||
// 住院病历
|
|
||||||
inhosDate: '入院日期',
|
|
||||||
operation: '手术记录',
|
|
||||||
operationDate: '手术日期',
|
|
||||||
treatmentPlan: '术后病程',
|
|
||||||
outhosAdvice: '出院医嘱',
|
|
||||||
// 体检记录
|
|
||||||
inspectTime: '体检日期',
|
|
||||||
inspectSummary: '体检小结',
|
|
||||||
positiveFind: '阳性发现及处理意见',
|
|
||||||
// 预问诊记录
|
|
||||||
presentIllnessHistory: '现病史',
|
|
||||||
pastMedicalHistory: '既往史'
|
|
||||||
};
|
|
||||||
|
|
||||||
const caseTypeName = computed(() => CASE_TYPE_NAMES[caseType.value] || '病历');
|
|
||||||
|
|
||||||
const progressTitle = computed(() => {
|
|
||||||
if (progress.value < 100) {
|
|
||||||
return `正在智能整理${caseTypeName.value}...`;
|
|
||||||
}
|
|
||||||
return `${caseTypeName.value}生成完成`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const open = (type) => {
|
|
||||||
caseType.value = type;
|
|
||||||
progress.value = 0;
|
|
||||||
detectedInfo.value = [];
|
|
||||||
isGenerating.value = false;
|
|
||||||
isCompleted.value = false;
|
|
||||||
finalData.value = null;
|
|
||||||
popup.value?.open();
|
|
||||||
};
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
popup.value?.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateProgress = (value) => {
|
|
||||||
progress.value = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const addDetectedInfo = (fieldKey, fieldValue) => {
|
|
||||||
const label = FIELD_LABELS[fieldKey] || fieldKey;
|
|
||||||
// 如果字段值为空,显示"暂无"
|
|
||||||
const displayValue = (fieldValue && fieldValue.trim()) ? fieldValue : '暂无';
|
|
||||||
detectedInfo.value.push({
|
|
||||||
label,
|
|
||||||
value: displayValue,
|
|
||||||
animated: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const setGenerating = (value) => {
|
|
||||||
isGenerating.value = value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setCompleted = (value, data = null) => {
|
|
||||||
isCompleted.value = value;
|
|
||||||
finalData.value = data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
progress.value = 0;
|
|
||||||
detectedInfo.value = [];
|
|
||||||
isGenerating.value = false;
|
|
||||||
isCompleted.value = false;
|
|
||||||
finalData.value = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRegenerate = () => {
|
|
||||||
emit('regenerate', { caseType: caseType.value });
|
|
||||||
close();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNext = () => {
|
|
||||||
emit('next', {
|
|
||||||
caseType: caseType.value,
|
|
||||||
data: finalData.value
|
|
||||||
});
|
|
||||||
close();
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open,
|
|
||||||
close,
|
|
||||||
updateProgress,
|
|
||||||
addDetectedInfo,
|
|
||||||
setGenerating,
|
|
||||||
setCompleted,
|
|
||||||
reset
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.progress-modal {
|
|
||||||
width: 600rpx;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
padding: 48rpx 40rpx;
|
|
||||||
position: relative;
|
|
||||||
max-height: 80vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
position: absolute;
|
|
||||||
top: 20rpx;
|
|
||||||
right: 20rpx;
|
|
||||||
width: 48rpx;
|
|
||||||
height: 48rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 10;
|
|
||||||
|
|
||||||
.close-icon {
|
|
||||||
font-size: 40rpx;
|
|
||||||
color: #999999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-content {
|
|
||||||
.progress-title {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333333;
|
|
||||||
margin-bottom: 32rpx;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16rpx;
|
|
||||||
margin-bottom: 32rpx;
|
|
||||||
|
|
||||||
.progress-bar {
|
|
||||||
flex: 1;
|
|
||||||
height: 16rpx;
|
|
||||||
background-color: #e5e5e5;
|
|
||||||
border-radius: 8rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.progress-fill {
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, #1890ff 0%, #40a9ff 100%);
|
|
||||||
transition: width 0.5s ease;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #1890ff;
|
|
||||||
font-weight: 600;
|
|
||||||
min-width: 80rpx;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.detected-info {
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
|
|
||||||
.detected-title {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #666666;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-list {
|
|
||||||
max-height: 400rpx;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 12rpx;
|
|
||||||
margin-bottom: 12rpx;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-20rpx);
|
|
||||||
|
|
||||||
&.fade-in {
|
|
||||||
animation: fadeInSlide 0.4s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-icon {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #52c41a;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-top: 2rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-text {
|
|
||||||
flex: 1;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #333333;
|
|
||||||
line-height: 1.6;
|
|
||||||
word-break: break-all;
|
|
||||||
|
|
||||||
// 暂无数据的样式
|
|
||||||
&.empty-value {
|
|
||||||
color: #999999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.generating-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #1890ff;
|
|
||||||
text-align: center;
|
|
||||||
padding: 16rpx 0;
|
|
||||||
|
|
||||||
.dot-animation::after {
|
|
||||||
content: '...';
|
|
||||||
animation: dots 1.5s steps(4, end) infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 16rpx;
|
|
||||||
margin-top: 32rpx;
|
|
||||||
|
|
||||||
.action-button {
|
|
||||||
flex: 1;
|
|
||||||
height: 80rpx;
|
|
||||||
border-radius: 40rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
background: linear-gradient(90deg, #1890ff 0%, #40a9ff 100%);
|
|
||||||
|
|
||||||
.button-text {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 0.8;
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.secondary {
|
|
||||||
background-color: #ffffff;
|
|
||||||
border: 2rpx solid #d9d9d9;
|
|
||||||
|
|
||||||
.button-text {
|
|
||||||
color: #666666;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-text {
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeInSlide {
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes dots {
|
|
||||||
0%, 20% {
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
content: '.';
|
|
||||||
}
|
|
||||||
60% {
|
|
||||||
content: '..';
|
|
||||||
}
|
|
||||||
80%, 100% {
|
|
||||||
content: '...';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,134 +0,0 @@
|
|||||||
<template>
|
|
||||||
<uni-popup ref="popup" type="center" :mask-click="false">
|
|
||||||
<view class="medical-case-selector">
|
|
||||||
<view class="selector-header">
|
|
||||||
<text class="header-title">快速生成:</text>
|
|
||||||
<view class="close-btn" @click="close">
|
|
||||||
<text class="close-icon">✕</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="case-type-grid">
|
|
||||||
<view
|
|
||||||
v-for="type in caseTypes"
|
|
||||||
:key="type.id"
|
|
||||||
class="case-type-item"
|
|
||||||
@click="selectType(type)"
|
|
||||||
>
|
|
||||||
<text class="type-name">{{ type.name }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</uni-popup>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
const emit = defineEmits(['select']);
|
|
||||||
|
|
||||||
const popup = ref(null);
|
|
||||||
|
|
||||||
const caseTypes = [
|
|
||||||
{
|
|
||||||
id: 'outpatient',
|
|
||||||
name: '门诊病历'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'inpatient',
|
|
||||||
name: '住院病历'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'physicalExam',
|
|
||||||
name: '体检记录'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'preConsultation',
|
|
||||||
name: '预问诊记录'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const open = () => {
|
|
||||||
popup.value?.open();
|
|
||||||
};
|
|
||||||
|
|
||||||
const close = () => {
|
|
||||||
popup.value?.close();
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectType = (type) => {
|
|
||||||
emit('select', type);
|
|
||||||
close();
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
open,
|
|
||||||
close
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
.medical-case-selector {
|
|
||||||
width: 600rpx;
|
|
||||||
background-color: #ffffff;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
padding: 40rpx;
|
|
||||||
|
|
||||||
.selector-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 40rpx;
|
|
||||||
|
|
||||||
.header-title {
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-btn {
|
|
||||||
width: 48rpx;
|
|
||||||
height: 48rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
.close-icon {
|
|
||||||
font-size: 40rpx;
|
|
||||||
color: #999999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-type-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
gap: 24rpx;
|
|
||||||
|
|
||||||
.case-type-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 40rpx 20rpx;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border: 2rpx solid #e5e5e5;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: #e8f4ff;
|
|
||||||
border-color: #1890ff;
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-name {
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: #333333;
|
|
||||||
text-align: center;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -125,16 +125,6 @@
|
|||||||
@cancel="handleRejectReasonCancel"
|
@cancel="handleRejectReasonCancel"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- AI助手按钮组 -->
|
|
||||||
<AIAssistantButtons
|
|
||||||
v-if="!isEvaluationPopupOpen && !showConsultAccept && orderStatus === 'processing'"
|
|
||||||
:groupId="groupId"
|
|
||||||
:patientAccountId="chatInfo.userID || ''"
|
|
||||||
:corpId="corpId"
|
|
||||||
@streamText="handleStreamText"
|
|
||||||
@clearInput="handleClearInput"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 聊天输入组件 -->
|
<!-- 聊天输入组件 -->
|
||||||
<ChatInput
|
<ChatInput
|
||||||
v-if="!isEvaluationPopupOpen && !showConsultAccept"
|
v-if="!isEvaluationPopupOpen && !showConsultAccept"
|
||||||
@ -185,7 +175,6 @@ import ChatInput from "./components/chat-input.vue";
|
|||||||
import SystemMessage from "./components/system-message.vue";
|
import SystemMessage from "./components/system-message.vue";
|
||||||
import ConsultAccept from "./components/consult-accept.vue";
|
import ConsultAccept from "./components/consult-accept.vue";
|
||||||
import RejectReasonModal from "./components/reject-reason-modal.vue";
|
import RejectReasonModal from "./components/reject-reason-modal.vue";
|
||||||
import AIAssistantButtons from "./components/ai-assistant-buttons.vue";
|
|
||||||
|
|
||||||
const timChatManager = globalTimChatManager;
|
const timChatManager = globalTimChatManager;
|
||||||
|
|
||||||
@ -734,25 +723,9 @@ onHide(() => {
|
|||||||
|
|
||||||
const sendCommonPhrase = (content) => {
|
const sendCommonPhrase = (content) => {
|
||||||
if (chatInputRef.value) {
|
if (chatInputRef.value) {
|
||||||
// 覆盖输入框内容,而不是直接发送
|
chatInputRef.value.sendTextMessageFromPhrase(content);
|
||||||
chatInputRef.value.setInputText(content);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理流式文本输入
|
|
||||||
const handleStreamText = (char) => {
|
|
||||||
if (chatInputRef.value) {
|
|
||||||
chatInputRef.value.appendStreamText(char);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理清空输入框
|
|
||||||
const handleClearInput = () => {
|
|
||||||
if (chatInputRef.value) {
|
|
||||||
chatInputRef.value.clearInputText();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 暴露方法给常用语页面调用
|
// 暴露方法给常用语页面调用
|
||||||
defineExpose({
|
defineExpose({
|
||||||
sendCommonPhrase,
|
sendCommonPhrase,
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1008 B |
@ -91,9 +91,7 @@ const urlsConfig = {
|
|||||||
acceptConsultation: "acceptConsultation",
|
acceptConsultation: "acceptConsultation",
|
||||||
sendArticleMessage: "sendArticleMessage",
|
sendArticleMessage: "sendArticleMessage",
|
||||||
getChatRecordsByGroupId: "getChatRecordsByGroupId",
|
getChatRecordsByGroupId: "getChatRecordsByGroupId",
|
||||||
getGroupList: "getGroupList",
|
getGroupList: "getGroupList"
|
||||||
followUpInquiry: "followUpInquiry",
|
|
||||||
supplementMedicalCase: "supplementMedicalCase"
|
|
||||||
},
|
},
|
||||||
todo: {
|
todo: {
|
||||||
getCustomerTodos: 'getCustomerTodos',
|
getCustomerTodos: 'getCustomerTodos',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user