feat(menu): 优化菜单路径唯一性校验并更新前端界面

- 在SysLoginController中添加optionMap数据返回
- 添加JSQLParser依赖支持MyBatis Plus功能
- 实现selectMenuByPathExcludeId方法用于排除当前菜单的路径唯一性校验
- 在SysMenuServiceImpl中添加日志记录并优化路径唯一性判断逻辑
- 在SysMenuMapper.xml中添加LIMIT 1限制并实现排除ID查询
- 在前端路由中注释患者管理相关路由配置
- 在用户store中添加optionMap配置项并优先从optionMap获取医院名称
- 重构检查项目设置页面的操作按钮样式为统一的圆形按钮设计
- 更新检查项目设置页面的导航栏样式和交互体验
- 优化门诊记录页面的搜索条件和表格展示功能
- 添加性别和状态筛选条件并改进数据加载逻辑
This commit is contained in:
2026-01-03 23:47:09 +08:00
parent 61f4020487
commit 0c35044231
54 changed files with 5871 additions and 510 deletions

View File

@@ -8,11 +8,9 @@ export function listOutpatienRecords(query) {
})
}
export function listDoctorNames() {
export function listDoctorNames() {
return request({
url: '/patient-manage/records/init',
url: '/patient-manage/records/doctor-names',
method: 'get',
})
}
}

View File

@@ -4,9 +4,9 @@
<el-form-item label="查询内容" prop="searchKey">
<el-input
v-model="queryParams.searchKey"
placeholder="身份证号/病人ID/门诊号/姓名"
placeholder="姓名/身份证号/病人ID/门诊号"
clearable
style="width: 210px"
style="width: 240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
@@ -15,7 +15,7 @@
v-model="queryParams.phone"
placeholder="请输入联系方式"
clearable
style="width: 200px"
style="width: 150px"
@keyup.enter="handleQuery"
/>
</el-form-item>
@@ -27,14 +27,39 @@
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
style="width: 240px"
></el-date-picker>
</el-form-item>
<el-form-item label="性别" prop="genderEnum">
<el-select
v-model="queryParams.genderEnum"
placeholder="请选择性别"
clearable
style="width: 100px"
>
<el-option label="男" :value="1" />
<el-option label="女" :value="2" />
<el-option label="未知" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="subjectStatusEnum">
<el-select
v-model="queryParams.subjectStatusEnum"
placeholder="请选择状态"
clearable
style="width: 120px"
>
<el-option label="待就诊" :value="1" />
<el-option label="就诊中" :value="2" />
<el-option label="已完成" :value="3" />
<el-option label="已取消" :value="4" />
</el-select>
</el-form-item>
<el-form-item label="医生" prop="doctorName">
<el-select
v-model="queryParams.doctorName"
placeholder="请选择医生"
clearable
@keyup.enter="handleQuery"
style="width: 160px"
>
<el-option
@@ -51,18 +76,34 @@
</el-form-item>
</el-form>
<el-table :data="outpatienRecordsList" border style="width: 100%">
<el-table-column prop="name" label="患者" width="180" />
<el-table-column prop="idCard" label="身份证" width="180" />
<el-table-column prop="description" label="疾病" width="180" />
<el-table-column prop="patientBusNo" label="病人ID" width="180" />
<el-table-column prop="encounterBusNo" label="门诊号" width="180" />
<el-table-column prop="genderEnum_enumText" label="性别" width="80" />
<el-table
:data="outpatienRecordsList"
border
style="width: 100%"
:default-sort="{ prop: 'encounterTime', order: 'descending' }"
v-loading="loading"
:header-cell-style="{ background: '#f5f7fa', fontWeight: 'bold' }"
>
<el-table-column prop="name" label="患者" min-width="100" />
<el-table-column prop="genderEnum_enumText" label="性别" width="80" align="center" />
<el-table-column prop="idCard" label="身份证" min-width="160" :show-overflow-tooltip="true" />
<el-table-column prop="phone" label="电话" width="120" />
<el-table-column prop="encounterTime" label="就诊时间" width="180" />
<el-table-column prop="subjectStatusEnum_enumText" label="状态" width="120" />
<el-table-column prop="organizationName" label="接诊医院" width="180" />
<el-table-column prop="doctorName" label="接诊医生" width="180" />
<el-table-column prop="patientBusNo" label="病人ID" width="100" align="center" />
<el-table-column prop="encounterBusNo" label="门诊号" width="120" align="center" />
<el-table-column prop="encounterTime" label="就诊时间" width="160" sortable />
<el-table-column prop="doctorName" label="接诊医生" width="120" />
<el-table-column prop="organizationName" label="医疗机构" min-width="120" :show-overflow-tooltip="true" />
<el-table-column prop="subjectStatusEnum_enumText" label="状态" width="100" align="center">
<template #default="scope">
<el-tag
:type="getStatusTagType(scope.row.subjectStatusEnum)"
size="small"
>
{{ scope.row.subjectStatusEnum_enumText }}
</el-tag>
</template>
</el-table-column>
<!-- 移除疾病描述列因为当前数据中没有这个字段 -->
</el-table>
<pagination
v-show="total > 0"
@@ -75,16 +116,19 @@
</template>
<script setup name="outpatienRecords">
import {computed, ref} from 'vue';
import {computed, ref, reactive, toRefs, getCurrentInstance} from 'vue';
import {listDoctorNames, listOutpatienRecords} from './component/api';
import { useRoute } from 'vue-router';
const showSearch = ref(true);
const total = ref(0);
const dateRange = ref([]);
const outpatienRecordsList = ref([]);
const doctorList = ref([]);
const loading = ref(false);
const { proxy } = getCurrentInstance();
const route = useRoute();
const data = reactive({
form: {},
@@ -94,7 +138,8 @@ const data = reactive({
doctorName: undefined,
searchKey: undefined,
phone: undefined,
patientid: undefined,
genderEnum: undefined,
subjectStatusEnum: undefined,
},
});
const { queryParams } = toRefs(data);
@@ -108,23 +153,60 @@ const doctorOptions = computed(() => {
/** 查询门诊记录列表 */
function getList() {
loading.value = true;
// 如果路由中有患者ID参数则自动填充到查询条件中
if (route.query.patientId) {
queryParams.value.searchKey = route.query.patientId;
}
if (route.query.patientName) {
// 可以在页面标题或其他地方显示患者姓名
console.log('当前查看患者:', route.query.patientName);
}
listOutpatienRecords(queryParams.value).then((response) => {
outpatienRecordsList.value = response.data.records;
total.value = response.data.total;
loading.value = false;
}).catch(() => {
loading.value = false;
});
listDoctorNames().then((response) => {
console.log(response);
doctorList.value = response.data;
console.log(doctorList.value, 'doctorList.value');
});
// 只在医生列表为空时加载医生列表
if (doctorList.value.length === 0) {
listDoctorNames().then((response) => {
doctorList.value = response.data;
});
}
}
/** 根据状态获取标签类型 */
function getStatusTagType(status) {
// 假设状态值1-待就诊2-就诊中3-已完成4-已取消
switch (status) {
case 1:
return 'warning'; // 待就诊 - 黄色
case 2:
return 'primary'; // 就诊中 - 蓝色
case 3:
return 'success'; // 已完成 - 绿色
case 4:
return 'info'; // 已取消 - 灰色
default:
return '';
}
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.value.startTimeSTime =
dateRange.value && dateRange.value.length == 2 ? dateRange.value[0] : '';
queryParams.value.startTimeETime =
dateRange.value && dateRange.value.length == 2 ? dateRange.value[1] : '';
// 处理时间范围参数
if (dateRange.value && dateRange.value.length === 2) {
queryParams.value.startTimeSTime = dateRange.value[0];
queryParams.value.startTimeETime = dateRange.value[1];
} else {
queryParams.value.startTimeSTime = '';
queryParams.value.startTimeETime = '';
}
queryParams.value.pageNo = 1;
getList();
}

View File

@@ -35,11 +35,11 @@
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table :data="patientList" border>
<el-table-column prop="idCard" label="身份证号" width="180" />
<el-table-column prop="busNo" label="病人ID" width="180" />
<el-table-column prop="name" label="病人名称" width="180" />
<el-table-column prop="genderEnum_enumText" label="性别" width="180">
<el-table :data="patientList" border size="small" :header-cell-style="{padding: '8px 0'}" :cell-style="{padding: '6px 0'}">
<el-table-column prop="idCard" label="身份证号" min-width="120" show-overflow-tooltip />
<el-table-column prop="busNo" label="病人ID" width="100" show-overflow-tooltip />
<el-table-column prop="name" label="病人名称" width="80" />
<el-table-column prop="genderEnum_enumText" label="性别" width="60" align="center">
<template #default="scope">
<dict-tag :options="patient_gender_enum" :value="scope.row.genderEnum" class="dict-tag" />
</template>
@@ -47,55 +47,57 @@
<el-table-column
prop="maritalStatusEnum_enumText"
label="婚姻状况"
width="180"
width="80"
align="center"
>
<template #default="scope">
<dict-tag :options="marital_status_enum" :value="scope.row.maritalStatusEnum" class="dict-tag" />
</template>
</el-table-column>
<el-table-column prop="nationalityCode" label="民族" width="180">
<el-table-column prop="nationalityCode" label="民族" width="60" align="center">
<template #default="scope">
<dict-tag :options="nationality_code" :value="scope.row.nationalityCode" class="dict-tag" />
</template>
</el-table-column>
<el-table-column prop="birthDate" label="生日" width="160" />
<el-table-column prop="phone" label="电话" width="140" />
<el-table-column prop="bloodAbo_enumText" label="血型ABO" width="140">
<el-table-column prop="birthDate" label="生日" width="100" />
<el-table-column prop="phone" label="电话" width="110" />
<el-table-column prop="bloodAbo_enumText" label="血型ABO" width="70" align="center">
<template #default="scope">
<dict-tag :options="blood_abo" :value="scope.row.bloodAbo" class="dict-tag" />
</template>
</el-table-column>
<el-table-column prop="bloodRh_enumText" label="血型RH" width="140">
<el-table-column prop="bloodRh_enumText" label="血型RH" width="70" align="center">
<template #default="scope">
<dict-tag :options="blood_rh" :value="scope.row.bloodRh" class="dict-tag" />
</template>
</el-table-column>
<el-table-column prop="linkName" label="联系人" width="180" />
<el-table-column prop="linkTelcom" label="联系人电话" width="180" />
<el-table-column prop="linkRelationCode_enumText" label="联系人关系" width="180">
<el-table-column prop="linkName" label="联系人" width="80" />
<el-table-column prop="linkTelcom" label="联系人电话" width="110" />
<el-table-column prop="linkRelationCode_enumText" label="联系人关系" width="80" align="center">
<template #default="scope">
<dict-tag :options="link_relation_code" :value="scope.row.linkRelationCode" class="dict-tag" />
</template>
</el-table-column>
<el-table-column prop="address" label="家庭地址" width="180" />
<el-table-column prop="prfsEnum_enumText" label="职业" width="180">
<el-table-column prop="address" label="家庭地址" min-width="150" show-overflow-tooltip />
<el-table-column prop="prfsEnum_enumText" label="职业" width="80" align="center">
<template #default="scope">
<dict-tag :options="prfs_enum" :value="scope.row.prfsEnum" class="dict-tag" />
</template>
</el-table-column>
<el-table-column prop="workCompany" label="工作单位" width="180" />
<el-table-column prop="organizationName" label="登记医院" width="180" />
<el-table-column prop="deceasedDate" label="死亡时间" width="180" />
<el-table-column prop="createTime" label="登记时间" width="180" />
<el-table-column prop="workCompany" label="工作单位" min-width="120" show-overflow-tooltip />
<el-table-column prop="organizationName" label="登记医院" width="100" show-overflow-tooltip />
<el-table-column prop="deceasedDate" label="死亡时间" width="100" />
<el-table-column prop="createTime" label="登记时间" width="140" />
<el-table-column
label="操作"
align="center"
width="210"
class-name="small-padding fixed-width"
>
width="220"
fixed="right"
>
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" class="action-button">修改</el-button>
<el-button link type="primary" icon="View" @click="handleSee(scope.row)" class="action-button">查看</el-button>
<el-button link type="success" icon="Clock" @click="handleVisitHistory(scope.row)" class="action-button">就诊历史</el-button>
</template>
</el-table-column>
</el-table>
@@ -174,7 +176,15 @@
</el-col>
<el-col :span="8">
<el-form-item label="证件号码" prop="idCard">
<el-input v-model="form.idCard" clearable :disabled="isViewMode" />
<el-input
v-model="form.idCard"
clearable
:disabled="isViewMode"
placeholder="请输入18位身份证号"
maxlength="18"
show-word-limit
@blur="handleIdCardBlur"
/>
</el-form-item>
<el-form-item label="生日" prop="birthDate" v-show="false">
<el-input v-model="form.birthDate" v-show="false" />
@@ -424,6 +434,10 @@ const data = reactive({
name: [{ required: true, message: '姓名不能为空', trigger: 'blur' }],
age: [{ required: true, message: '年龄不能为空', trigger: 'change' }],
phone: [{ required: true, message: '联系方式不能为空', trigger: 'blur' }],
idCard: [
{ required: true, message: '身份证号不能为空', trigger: 'blur' },
{ validator: validateIdCard, trigger: 'blur' }
],
},
});
const { queryParams, form, rules, isViewMode } = toRefs(data);
@@ -572,6 +586,18 @@ function handleSee(row) {
title.value = '查看患者';
});
}
// 查看就诊历史
function handleVisitHistory(row) {
// 跳转到门诊记录页面并传递患者ID参数
proxy.$router.push({
path: '/patient/patienrecords',
query: {
patientId: row.busNo,
patientName: row.name
}
});
}
// 映射
const nationalityDict = (code) => {
const findObj = nationality_code.value.find((item) => item.value === code);
@@ -638,6 +664,199 @@ function getAddress(form) {
return part ? acc + part : acc;
}, '');
}
/** 身份证号校验函数 */
function validateIdCard(rule, value, callback) {
if (!value) {
return callback(new Error('身份证号不能为空'));
}
// 基本格式校验
const idCardReg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
if (!idCardReg.test(value)) {
return callback(new Error('身份证号格式不正确'));
}
// 校验码验证
const isValid = checkIdCardCode(value);
if (!isValid) {
return callback(new Error('身份证号校验码不正确,请检查'));
}
// 日期验证
const isValidDate = checkIdCardDate(value);
if (!isValidDate) {
return callback(new Error('身份证号中的日期不合法'));
}
// 地区码验证(可选)
const isValidArea = checkIdCardArea(value);
if (!isValidArea) {
return callback(new Error('身份证号中的地区码不合法'));
}
callback();
}
/** 身份证号校验码验证 */
function checkIdCardCode(idCard) {
const city = [11, 12, 13, 14, 15, 21, 22, 23, 31, 32, 33, 34, 35, 36, 37, 41, 42, 43, 44, 45, 46, 50, 51, 52, 53, 54, 61, 62, 63, 64, 65, 71, 81, 82, 91];
if (idCard.length === 15) {
// 15位身份证号不校验码
return true;
}
if (idCard.length === 18) {
// 18位身份证号校验码验证
const coefficient = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const remainder = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
let sum = 0;
for (let i = 0; i < 17; i++) {
sum += parseInt(idCard.charAt(i)) * coefficient[i];
}
const code = sum % 11;
const lastChar = idCard.charAt(17).toUpperCase();
return lastChar === remainder[code];
}
return false;
}
/** 身份证号日期验证 */
function checkIdCardDate(idCard) {
let year, month, day;
if (idCard.length === 15) {
// 15位身份证号6位年份
year = '19' + idCard.substring(6, 8);
month = idCard.substring(8, 10);
day = idCard.substring(10, 12);
} else if (idCard.length === 18) {
// 18位身份证号4位年份
year = idCard.substring(6, 10);
month = idCard.substring(10, 12);
day = idCard.substring(12, 14);
} else {
return false;
}
// 检查日期是否合法
const date = new Date(year, parseInt(month) - 1, parseInt(day));
const now = new Date();
// 检查年月日是否有效
if (date.getFullYear() !== parseInt(year) ||
date.getMonth() + 1 !== parseInt(month) ||
date.getDate() !== parseInt(day)) {
return false;
}
// 检查是否是未来日期
if (date > now) {
return false;
}
// 检查是否是合理的年份100岁以上或刚出生
const age = now.getFullYear() - date.getFullYear();
if (age < 0 || age > 150) {
return false;
}
return true;
}
/** 身份证号地区码验证 */
function checkIdCardArea(idCard) {
const areaCode = idCard.substring(0, 6);
const validAreas = [
'110000', '110101', '110102', '110105', '110106', '110107', '110108', '110109', '110111', '110112', '110113', '110114', '110115', '110116', '110117', '110118', '110119', // 北京
'120000', '120101', '120102', '120103', '120104', '120105', '120106', '120110', '120111', '120112', '120113', '120114', '120115', '120116', '120117', '120118', '120119', // 天津
'130000', // 河北
'140000', // 山西
'150000', // 内蒙古
'210000', // 辽宁
'220000', // 吉林
'230000', // 黑龙江
'310000', '310101', '310104', '310105', '310106', '310110', '310112', '310113', '310114', '310115', '310116', '310117', '310118', '310120', '310151', // 上海
'320000', // 江苏
'330000', // 浙江
'340000', // 安徽
'350000', // 福建
'360000', // 江西
'370000', // 山东
'410000', // 河南
'420000', // 湖北
'430000', // 湖南
'440000', // 广东
'450000', // 广西
'460000', // 海南
'500000', '500101', '500102', '500103', '500104', '500105', '500106', '500107', '500108', '500109', '500110', '500111', '500112', '500113', '500114', '500115', '500116', '500117', '500118', '500119', '500120', '500151', // 重庆
'510000', // 四川
'520000', // 贵州
'530000', // 云南
'540000', // 西藏
'610000', // 陕西
'620000', // 甘肃
'630000', // 青海
'640000', // 宁夏
'650000', // 新疆
'710000', // 台湾
'810000', // 香港
'820000' // 澳门
];
// 检查前6位是否在有效的地区码列表中
// 简化验证:只检查省级代码
const provinceCode = areaCode.substring(0, 2);
const validProvinceCodes = ['11', '12', '13', '14', '15', '21', '22', '23', '31', '32', '33', '34', '35', '36', '37', '41', '42', '43', '44', '45', '46', '50', '51', '52', '53', '54', '61', '62', '63', '64', '65', '71', '81', '82'];
return validProvinceCodes.includes(provinceCode);
}
/** 身份证号失焦处理 */
function handleIdCardBlur() {
const idCard = form.value.idCard;
if (!idCard) return;
// 基本格式校验
const idCardReg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
if (!idCardReg.test(idCard)) {
proxy.$message.warning('身份证号格式不正确请输入15位或18位身份证号');
return;
}
// 校验码验证
const isValidCode = checkIdCardCode(idCard);
if (!isValidCode) {
proxy.$message.warning('身份证号校验码不正确,请检查输入是否正确');
return;
}
// 日期验证
const isValidDate = checkIdCardDate(idCard);
if (!isValidDate) {
proxy.$message.warning('身份证号中的日期不合法,请检查');
return;
}
// 地区码验证
const isValidArea = checkIdCardArea(idCard);
if (!isValidArea) {
proxy.$message.warning('身份证号中的地区码不合法,请检查');
return;
}
// 所有验证通过,自动填充性别
if (idCard.length === 18) {
const genderCode = parseInt(idCard.charAt(16));
// 男性:奇数,女性:偶数
if (!form.value.genderEnum && genderCode) {
form.value.genderEnum = genderCode % 2 === 1 ? 1 : 2;
proxy.$message.success('身份证号验证通过,已自动填充性别信息');
}
}
}
/** 提交按钮 */
function submitForm() {
console.log('selectedOptions=====>', JSON.stringify(selectedOptions.value));
@@ -694,21 +913,90 @@ onMounted(() => {
// 优化按钮组间距
.button-group {
margin-bottom: 16px;
margin-bottom: 12px;
}
// 优化表格样式
// 优化表格样式 - 紧凑模式
.el-table {
// 移除固定宽度,让列自适应
// 表头样式
:deep(th) {
background-color: #f8f9fa;
color: #606266;
font-weight: 600;
padding: 12px 0;
padding: 6px 8px !important;
font-size: 13px;
height: 36px !important;
line-height: 36px !important;
}
// 单元格样式
:deep(td) {
padding: 12px 0;
padding: 4px 8px !important;
font-size: 13px;
height: 36px !important;
line-height: 36px !important;
}
// 表格整体字体
:deep(.cell) {
padding: 0 !important;
}
// 字典标签样式优化
:deep(.dict-tag) {
font-size: 12px;
padding: 2px 6px;
line-height: 1.5;
}
// 操作按钮样式优化
:deep(.action-button) {
padding: 2px 4px;
font-size: 12px;
}
// 小屏幕优化
@media (max-height: 800px) {
:deep(th) {
font-size: 12px;
padding: 4px 6px !important;
height: 32px !important;
line-height: 32px !important;
}
:deep(td) {
font-size: 12px;
padding: 3px 6px !important;
height: 32px !important;
line-height: 32px !important;
}
}
}
// 优化查询表单样式
.query-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
:deep(.el-input) {
font-size: 13px;
}
:deep(.el-button) {
padding: 8px 16px;
font-size: 13px;
}
}
// 优化分页样式
.pagination-container {
margin-top: 12px;
padding: 12px 0;
}
// 操作按钮间距
.el-table :deep(.el-button + .el-button) {
margin-left: 4px;
}
</style>