This commit is contained in:
2025-08-11 17:32:15 +08:00
parent 449bd0c09d
commit cd6dd3d202
8 changed files with 594 additions and 300 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ export const PAGE_SIZE_OPTIONS = ['5', '10', '15', '20', '30', '40', '50', '75',
//登录页面名字
export const PAGE_PATH_LOGIN = '/login';
//首页页面名字
export const HOME_PAGE_PATH = '/home';
export const HOME_PAGE_PATH = '/case-clinical-article/list';
//404页面名字
export const PAGE_PATH_404 = '/404';
+2 -2
View File
@@ -9,7 +9,7 @@
*/
//首页页面名字
export const HOME_PAGE_NAME = 'Home';
export const HOME_PAGE_NAME = 'CaseClinicalArticleList';
//首页页面路径
export const HOME_PAGE_PATH = '/home';
export const HOME_PAGE_PATH = '/case-clinical-article/list';
+3 -1
View File
@@ -8,6 +8,7 @@
* @Copyright 1024创新实验室 https://1024lab.net ),Since 2012
*/
import { homeRouters } from './system/home';
import { caseClinicalArticleRouters } from './system/case-clinical-article';
import { loginRouters } from './system/login';
import { helpDocRouters } from './support/help-doc';
import NotFound from '/@/views/system/40X/404.vue';
@@ -15,7 +16,8 @@ import NoPrivilege from '/@/views/system/40X/403.vue';
export const routerArray = [
...loginRouters,
...homeRouters,
...homeRouters,
...caseClinicalArticleRouters,
...helpDocRouters,
{ path: '/:pathMatch(.*)*', name: '404', component: NotFound },
{ path: '/403', name: '403', component: NoPrivilege }
@@ -0,0 +1,47 @@
/*
* 文章病例库管理路由
*
* @Author: xing
* @Date: 2025-01-27
* @Copyright gdxz
*/
import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
import SmartLayout from '/@/layout/index.vue';
export const caseClinicalArticleRouters = [
{
path: '/case-clinical-article',
name: 'CaseClinicalArticle',
component: SmartLayout,
meta: {
title: '文章病例库管理',
menuType: MENU_TYPE_ENUM.CATALOG.value,
icon: 'FileTextOutlined',
},
children: [
{
path: '/case-clinical-article/list',
name: 'CaseClinicalArticleList',
meta: {
title: '文章病例库列表',
menuType: MENU_TYPE_ENUM.MENU.value,
icon: 'FileTextOutlined',
parentMenuList: [{ name: 'CaseClinicalArticle', title: '文章病例库管理' }],
},
component: () => import('/@/views/business/case-clinical-article/case-clinical-article-list.vue'),
},
{
path: '/case-clinical-article/form',
name: 'CaseClinicalArticleForm',
meta: {
title: '文章病例库表单',
menuType: MENU_TYPE_ENUM.MENU.value,
icon: 'FileTextOutlined',
hideInMenu: true,
parentMenuList: [{ name: 'CaseClinicalArticle', title: '文章病例库管理' }],
},
component: () => import('/@/views/business/case-clinical-article/case-clinical-article-form.vue'),
},
],
},
];
+7 -7
View File
@@ -18,21 +18,21 @@ export const homeRouters = [
redirect: { name: HOME_PAGE_NAME },
component: SmartLayout,
meta: {
title: '首页',
title: '文章病例库管理',
menuType: MENU_TYPE_ENUM.CATALOG.value,
icon: 'HomeOutlined',
icon: 'FileTextOutlined',
},
children: [
{
path: '/home',
path: '/case-clinical-article/list',
name: HOME_PAGE_NAME,
meta: {
title: '首页',
title: '文章病例库管理',
menuType: MENU_TYPE_ENUM.MENU.value,
icon: 'HomeOutlined',
parentMenuList: [{ name: '_home', title: '首页' }],
icon: 'FileTextOutlined',
parentMenuList: [{ name: '_home', title: '文章病例库管理' }],
},
component: () => import('/@/views/system/home/index.vue'),
component: () => import('/@/views/business/case-clinical-article/case-clinical-article-list.vue'),
},
{
path: '/account',
@@ -7,84 +7,116 @@
-->
<template>
<a-modal
:title="form.doctorId ? '编辑' : '添加'"
:width="1000"
:open="visibleFlag"
@cancel="onClose"
:maskClosable="false"
:destroyOnClose="true"
:title="form.doctorId ? '编辑医生' : '添加医生'"
:width="800"
:open="visibleFlag"
@cancel="onClose"
:maskClosable="false"
:destroyOnClose="true"
>
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 5 }" >
<a-form-item label="用户名称" name="doctorName">
<a-input-group compact>
<a-input
style="calc(100% - 80px)"
v-model:value="form.doctorName"
placeholder="请输入医生姓名"
:disabled="!!form.doctorId"
@pressEnter="searchAppDoctor"
/>
<a-button
v-if="!form.doctorId"
type="primary"
style="width: 80px"
@click="searchAppDoctor"
:loading="searchLoading"
>
搜索
</a-button>
</a-input-group>
<!-- 搜索到的APP医生信息提示 -->
<div v-if="appDoctorInfo && !form.doctorId" style="margin-top: 8px;">
<a-alert
:message="`找到APP医生:${appDoctorInfo.doctorName}`"
:description="`手机号:${appDoctorInfo.phone || '未知'} | 省份:${appDoctorInfo.province || '未知'}`"
type="info"
show-icon
closable
@close="clearAppDoctorInfo"
>
<template #action>
<a-button size="small" type="primary" @click="useAppDoctorInfo">
使用此信息
</a-button>
</template>
</a-alert>
</div>
</a-form-item>
<a-form-item label="所属医院" name="hospitalId">
<a-select
v-model:value="form.hospitalId"
placeholder="请选择医院"
style="width: 100%"
:options="hospitalOptions"
:field-names="{ label: 'hospitalName', value: 'hospitalId' }"
:loading="hospitalLoading"
<a-form ref="formRef" :model="form" :rules="rules" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
<!-- 医生姓名 -->
<a-form-item label="医生姓名" name="doctorName">
<a-input-group compact>
<a-input
v-model:value="form.doctorName"
placeholder="请输入医生姓名"
:disabled="!!form.doctorId"
@focus="onHospitalFocus"
@search="onHospitalSearch"
show-search
allowClear
style="width: calc(100% - 80px)"
@pressEnter="searchDoctor"
/>
</a-form-item>
<!-- 编辑时显示状态 -->
<a-form-item v-if="form.doctorId" label="状态" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="1">正常</a-radio>
<a-radio :value="2">禁用</a-radio>
</a-radio-group>
</a-form-item>
<!-- 编辑时显示头像 -->
<a-form-item v-if="form.doctorId" label="头像" name="avatar">
<div v-if="form.avatar && typeof form.avatar === 'string' && form.avatar.trim()" class="avatar-display">
<img :src="form.avatar" alt="头像" style="width: 80px; height: 80px; border-radius: 50%; object-fit: cover;" />
<a-button
v-if="!form.doctorId"
type="primary"
style="width: 80px"
@click="searchDoctor"
:loading="searchLoading"
>
搜索
</a-button>
</a-input-group>
</a-form-item>
<!-- 医生搜索结果 -->
<a-form-item v-if="doctorList.length > 0 && !form.doctorId" label="搜索结果">
<div class="doctor-list">
<a-list
:data-source="doctorList"
item-layout="horizontal"
size="small"
>
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #title>
<div class="doctor-info">
<span class="doctor-name">{{ item.doctorName }}</span>
<span class="doctor-phone" v-if="item.phone">({{ item.phone }})</span>
</div>
</template>
<template #description>
<div class="doctor-details">
<span v-if="item.province">省份{{ item.province }}</span>
<span v-if="item.hospitalName" style="margin-left: 16px;">医院{{ item.hospitalName }}</span>
</div>
</template>
</a-list-item-meta>
<template #actions>
<a-button type="primary" size="small" @click="selectDoctor(item)">
选择此医生
</a-button>
</template>
</a-list-item>
</template>
</a-list>
</div>
</a-form-item>
<!-- 无搜索结果提示 -->
<a-form-item v-if="showNoResult && !form.doctorId" label="搜索结果">
<a-alert
message="未找到医生信息"
description="请手动填写医生信息"
type="info"
show-icon
/>
</a-form-item>
<!-- 所属医院 -->
<a-form-item label="所属医院" name="hospitalId">
<a-select
v-model:value="form.hospitalId"
placeholder="请选择医院"
style="width: 100%"
:options="hospitalOptions"
:loading="hospitalLoading"
@search="onHospitalSearch"
@change="onHospitalChange"
show-search
allowClear
:filter-option="false"
/>
</a-form-item>
<!-- 编辑时显示状态 -->
<a-form-item v-if="form.doctorId" label="状态" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="1">正常</a-radio>
<a-radio :value="2">禁用</a-radio>
</a-radio-group>
</a-form-item>
<!-- 编辑时显示头像 -->
<a-form-item v-if="form.doctorId" label="头像" name="avatar">
<div v-if="form.avatar && typeof form.avatar === 'string' && form.avatar.trim()" class="avatar-display">
<img :src="form.avatar" alt="头像" style="width: 80px; height: 80px; border-radius: 50%; object-fit: cover;" />
</div>
<div v-else class="avatar-placeholder">
<div style="width: 80px; height: 80px; border-radius: 50%; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999; font-size: 12px; border: 1px dashed #d9d9d9;">
默认头像
</div>
<div v-else class="avatar-placeholder">
<div style="width: 80px; height: 80px; border-radius: 50%; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #999; font-size: 12px; border: 1px dashed #d9d9d9;">
默认头像
</div>
</div>
</a-form-item>
</div>
</a-form-item>
</a-form>
<template #footer>
@@ -95,238 +127,332 @@
</template>
</a-modal>
</template>
<script setup>
import { reactive, ref, nextTick } from 'vue';
import _ from 'lodash';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { caseClinicalDoctorApi } from '/@/api/business/case-clinical-doctor/case-clinical-doctor-api';
import { basicHospitalApi } from '/@/api/business/basic-hospital/basic-hospital-api';
import { smartSentry } from '/@/lib/smart-sentry';
import { debounce } from 'lodash';
import { reactive, ref, nextTick } from 'vue';
import { message } from 'ant-design-vue';
import { SmartLoading } from '/@/components/framework/smart-loading';
import { caseClinicalDoctorApi } from '/@/api/business/case-clinical-doctor/case-clinical-doctor-api';
import { basicHospitalApi } from '/@/api/business/basic-hospital/basic-hospital-api';
import { smartSentry } from '/@/lib/smart-sentry';
// ------------------------ 事件 ------------------------
// 事件
const emits = defineEmits(['reloadList']);
const emits = defineEmits(['reloadList']);
// 组件引用
const formRef = ref();
// ------------------------ 显示与隐藏 ------------------------
// 是否显示
const visibleFlag = ref(false);
// 显示状态
const visibleFlag = ref(false);
async function show(rowData) {
Object.assign(form, formDefault);
hospitalOptions.value = [];
appDoctorInfo.value = null;
if (rowData && !_.isEmpty(rowData)) {
if (rowData.doctorId) {
// 编辑模式:获取完整的医生详情信息
try {
SmartLoading.show();
const result = await caseClinicalDoctorApi.getDetail(rowData.doctorId);
Object.assign(form, result.data);
// 如果有医院信息,将当前医院添加到选项中以便正确显示
if (result.data.hospitalId && result.data.hospitalName) {
hospitalOptions.value = [{
hospitalId: result.data.hospitalId,
hospitalName: result.data.hospitalName
}];
}
} catch (e) {
smartSentry.captureError(e);
// 如果获取失败,使用传入的数据
Object.assign(form, rowData);
} finally {
SmartLoading.hide();
}
} else {
// 新增模式:使用传入的数据
Object.assign(form, rowData);
}
}
visibleFlag.value = true;
nextTick(() => {
formRef.value.clearValidate();
});
}
// 搜索状态
const searchLoading = ref(false);
const showNoResult = ref(false);
function onClose() {
Object.assign(form, formDefault);
hospitalOptions.value = [];
appDoctorInfo.value = null;
searchLoading.value = false;
visibleFlag.value = false;
}
// 是否从APP列表选择了医生,用于控制自动保存
const selectedFromApp = ref(false);
// 自动保存中标记,避免重复提交
const autoSaving = ref(false);
// ------------------------ 医生名称搜索 ------------------------
// 搜索APP医生
async function searchAppDoctor() {
// 编辑模式下不搜索
if (form.doctorId || !form.doctorName || form.doctorName.trim() === '') {
message.warning('请输入医生姓名');
return;
}
searchLoading.value = true;
// 医生列表
const doctorList = ref([]);
// 医院相关
const hospitalOptions = ref([]);
const hospitalLoading = ref(false);
const hospitalSearchKeyword = ref('');
// 表单默认值
const formDefault = {
doctorId: undefined,
doctorName: undefined,
doctorIden: undefined,
hospitalId: undefined,
hospitalUuid: undefined,
hospitalName: undefined,
status: 1,
avatar: undefined,
};
// 表单数据
let form = reactive({ ...formDefault });
// 表单验证规则
const rules = {
doctorName: [{ required: true, message: '医生姓名必填' }],
hospitalId: [{ required: true, message: '所属医院必填' }],
};
// 显示表单
async function show(rowData) {
// 重置表单
Object.assign(form, formDefault);
doctorList.value = [];
hospitalOptions.value = [];
showNoResult.value = false;
searchLoading.value = false;
hospitalLoading.value = false;
hospitalSearchKeyword.value = '';
selectedFromApp.value = false;
autoSaving.value = false;
// 如果是编辑模式,加载医生详情
if (rowData && rowData.doctorId) {
try {
const result = await caseClinicalDoctorApi.getAppDoctor(form.doctorName);
if (result.data) {
// 保存APP医生信息,等待用户确认
appDoctorInfo.value = result.data;
message.info('找到APP医生信息,请确认是否使用');
}
} catch (e) {
// 没找到医生信息,清空之前的搜索结果
appDoctorInfo.value = null;
message.info('未找到APP医生信息,请手动选择医院');
SmartLoading.show();
const result = await caseClinicalDoctorApi.getDetail(rowData.doctorId);
Object.assign(form, result.data);
// 自动加载医院列表供用户选择
if (hospitalOptions.value.length === 0) {
loadHospitalOptions();
}
} finally {
searchLoading.value = false;
}
}
// 使用APP医生信息
function useAppDoctorInfo() {
if (appDoctorInfo.value) {
form.doctorName = appDoctorInfo.value.doctorName;
form.doctorIden = appDoctorInfo.value.uuid; // 设置app唯一标识
form.hospitalUuid = appDoctorInfo.value.hospitalUuid; // 设置医院UUID
// 如果APP医生有医院信息,直接使用
if (appDoctorInfo.value.hospitalId && appDoctorInfo.value.hospitalName) {
form.hospitalId = appDoctorInfo.value.hospitalId;
form.hospitalName = appDoctorInfo.value.hospitalName;
// 添加到选项中
// 如果有医院信息,添加到选项中
if (result.data.hospitalId && result.data.hospitalName) {
hospitalOptions.value = [{
hospitalId: appDoctorInfo.value.hospitalId,
hospitalName: appDoctorInfo.value.hospitalName
label: result.data.hospitalName,
value: result.data.hospitalId,
hospitalId: result.data.hospitalId,
hospitalName: result.data.hospitalName,
hospitalUuid: result.data.hospitalUuid,
}];
} else {
// 没有医院信息,需要用户手动选择
form.hospitalId = undefined;
form.hospitalName = undefined;
// 自动加载医院列表供用户选择
if (hospitalOptions.value.length === 0) {
loadHospitalOptions();
}
}
// 清空APP医生信息
appDoctorInfo.value = null;
message.success('已使用APP医生信息');
}
}
// 清空APP医生信息
function clearAppDoctorInfo() {
appDoctorInfo.value = null;
}
// ------------------------ 医院搜索 ------------------------
// 加载医院选项
async function loadHospitalOptions() {
hospitalLoading.value = true;
try {
const result = await basicHospitalApi.queryList({
keywords: hospitalSearchKeyword.value
});
hospitalOptions.value = result.data || [];
} catch (e) {
smartSentry.captureError(e);
hospitalOptions.value = [];
} finally {
hospitalLoading.value = false;
}
}
// 医院搜索
const debouncedSearch = debounce(() => {
loadHospitalOptions();
}, 300);
function onHospitalSearch(value) {
hospitalSearchKeyword.value = value;
debouncedSearch();
}
function onHospitalFocus() {
if (hospitalOptions.value.length === 0) {
loadHospitalOptions();
}
}
// ------------------------ 表单 ------------------------
// 组件ref
const formRef = ref();
// 医院选项
const hospitalOptions = ref([]);
const hospitalLoading = ref(false);
const hospitalSearchKeyword = ref('');
// APP医生信息
const appDoctorInfo = ref(null);
const searchLoading = ref(false);
const formDefault = {
doctorId: undefined, //医生ID
doctorName: undefined, //用户名称
doctorIden: undefined, //app唯一标识
hospitalId: undefined, //所属医院
hospitalUuid: undefined, //医院UUID
hospitalName: undefined, //医院名称
status: 1, //状态(1:正常 2:禁用)- 默认为正常
avatar: undefined, //头像
};
let form = reactive({ ...formDefault });
const rules = {
doctorName: [{ required: true, message: '用户名称 必填' }],
hospitalId: [{ required: true, message: '所属医院 必填' }],
};
// 点击确定,验证表单
async function onSubmit() {
try {
await formRef.value.validateFields();
save();
} catch (err) {
message.error('参数验证错误,请仔细填写表单数据!');
}
}
// 新建、编辑API
async function save() {
SmartLoading.show();
try {
if (form.doctorId) {
await caseClinicalDoctorApi.update(form);
} else {
await caseClinicalDoctorApi.add(form);
}
message.success('操作成功');
emits('reloadList');
onClose();
} catch (err) {
smartSentry.captureError(err);
Object.assign(form, rowData);
} finally {
SmartLoading.hide();
}
}
defineExpose({
show,
visibleFlag.value = true;
nextTick(() => {
formRef.value?.clearValidate();
});
}
// 关闭表单
function onClose() {
visibleFlag.value = false;
selectedFromApp.value = false;
autoSaving.value = false;
}
// 搜索医生
async function searchDoctor() {
if (!form.doctorName || form.doctorName.trim() === '') {
message.warning('请输入医生姓名');
return;
}
searchLoading.value = true;
showNoResult.value = false;
doctorList.value = [];
try {
const result = await caseClinicalDoctorApi.getAppDoctor(form.doctorName.trim());
if (result.data && Array.isArray(result.data) && result.data.length > 0) {
if (result.data.length === 1) {
// 只有一个结果,直接选择
selectDoctor(result.data[0]);
} else {
// 多个结果,显示列表
doctorList.value = result.data;
message.info(`找到${result.data.length}个医生,请选择其中一个`);
}
} else {
// 没有结果
showNoResult.value = true;
message.info('未找到医生信息,请手动填写');
}
} catch (e) {
smartSentry.captureError(e);
showNoResult.value = true;
message.info('搜索失败,请手动填写');
} finally {
searchLoading.value = false;
}
}
// 选择医生
function selectDoctor(doctor) {
selectedFromApp.value = true;
form.doctorName = doctor.doctorName;
form.doctorIden = doctor.uuid;
form.hospitalUuid = doctor.hospitalUuid;
// 如果医生有医院信息,自动设置
if (doctor.hospitalName) {
form.hospitalName = doctor.hospitalName;
form.hospitalId = doctor.hospitalId;
// 添加到医院选项中
if (doctor.hospitalId) {
hospitalOptions.value = [{
label: doctor.hospitalName,
value: doctor.hospitalId,
hospitalId: doctor.hospitalId,
hospitalName: doctor.hospitalName,
hospitalUuid: doctor.hospitalUuid,
}];
}
message.success(`已选择医生:${doctor.doctorName},医院:${doctor.hospitalName}`);
} else {
form.hospitalName = undefined;
form.hospitalId = undefined;
message.success(`已选择医生:${doctor.doctorName},请选择医院`);
}
// 清空搜索结果
doctorList.value = [];
showNoResult.value = false;
}
// 加载医院选项
async function loadHospitalOptions() {
hospitalLoading.value = true;
try {
const result = await basicHospitalApi.queryList({
keywords: hospitalSearchKeyword.value || ''
});
if (result.data && Array.isArray(result.data)) {
// 标准化为 {label, value}
const formattedData = result.data.map(item => {
const hospitalId = item.hospitalId || item.id;
const hospitalName = item.hospitalName || item.name || item.hospital_name;
const hospitalUuid = item.hospitalUuid || item.uuid;
return {
label: hospitalName,
value: hospitalId,
hospitalId,
hospitalName,
hospitalUuid,
};
});
hospitalOptions.value = formattedData;
} else {
hospitalOptions.value = [];
}
} catch (e) {
smartSentry.captureError(e);
hospitalOptions.value = [];
} finally {
hospitalLoading.value = false;
}
}
// 医院搜索
function onHospitalSearch(value) {
hospitalSearchKeyword.value = value;
if (value && value.trim()) {
loadHospitalOptions();
} else {
hospitalOptions.value = [];
}
}
// 选择医院变更
async function onHospitalChange(value, option) {
if (!value || !option) {
form.hospitalId = undefined;
form.hospitalName = undefined;
form.hospitalUuid = undefined;
return;
}
form.hospitalId = option.hospitalId ?? value;
form.hospitalName = option.hospitalName ?? option.label;
form.hospitalUuid = option.hospitalUuid ?? option.uuid;
// 如果是从APP结果选择了医生,则用户选择医院后自动保存为新增
if (selectedFromApp.value && !form.doctorId) {
if (autoSaving.value) return;
autoSaving.value = true;
try {
await formRef.value.validateFields();
await save();
} catch (err) {
// 校验失败时不提交
} finally {
autoSaving.value = false;
}
}
}
// 提交表单
async function onSubmit() {
try {
await formRef.value.validateFields();
await save();
} catch (err) {
message.error('请检查表单填写是否正确');
}
}
// 保存数据
async function save() {
SmartLoading.show();
try {
if (form.doctorId) {
await caseClinicalDoctorApi.update(form);
message.success('更新成功');
} else {
await caseClinicalDoctorApi.add(form);
message.success('添加成功');
}
emits('reloadList');
onClose();
} catch (err) {
smartSentry.captureError(err);
message.error('操作失败');
} finally {
SmartLoading.hide();
}
}
// 暴露方法
defineExpose({
show,
});
</script>
<style scoped>
.doctor-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #d9d9d9;
border-radius: 6px;
padding: 8px;
}
.doctor-info {
display: flex;
align-items: center;
gap: 8px;
}
.doctor-name {
font-weight: 500;
color: #262626;
}
.doctor-phone {
color: #8c8c8c;
font-size: 12px;
}
.doctor-details {
color: #595959;
font-size: 12px;
}
.avatar-display img {
border: 2px solid #f0f0f0;
}
.avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
}
</style>
+2 -1
View File
@@ -96,6 +96,7 @@
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
import { useDictStore } from '/@/store/modules/system/dict.js';
import { dictApi } from '/@/api/support/dict-api.js';
import { HOME_PAGE_PATH } from '/@/constants/common-const';
//--------------------- 登录表单 ---------------------------------
@@ -149,7 +150,7 @@
useDictStore().initData(dictRes.data);
//构建系统的路由
buildRoutes();
router.push('/home');
router.replace(HOME_PAGE_PATH);
} catch (e) {
if (e.data && e.data.code !== 0) {
loginForm.captchaCode = '';