Files
his/openhis-ui-vue3/src/views/system/menu/index.vue
chenqi 9ed43c9413 feat(home): 添加医生专属患者统计和菜单跳转功能
- 在HomeStatisticsDto中新增我的患者数量和待写病历数量字段
- 实现医生患者查询功能,支持按租户隔离数据
- 更新首页统计服务,为医生用户提供专属患者统计数据
- 添加菜单名称点击跳转功能,支持路由导航和外部链接打开
- 修复首页统计数据显示,确保医生看到正确的患者数量
- 添加医保日结结算相关实体、服务和前端页面
- 配置前端路由控制器,支持Vue Router History模式
2026-02-02 16:28:31 +08:00

584 lines
23 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" class="query-form">
<el-form-item label="菜单名称" prop="menuName">
<el-input
v-model="queryParams.menuName"
placeholder="请输入菜单名称"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable style="width: 200px">
<el-option
v-for="dict in sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="显示状态" prop="visible">
<el-select v-model="queryParams.visible" placeholder="显示状态" clearable style="width: 200px">
<el-option
v-for="dict in sys_show_hide"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item class="search-buttons">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="button-group">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="Plus"
@click="handleAdd"
v-hasPermi="['system:menu:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="info"
plain
icon="Sort"
@click="toggleExpandAll"
>展开/折叠</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-table
v-if="refreshTable"
v-loading="loading"
:data="menuList"
row-key="menuId"
:default-expand-all="isExpandAll"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
>
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160">
<template #default="scope">
<span
v-if="scope.row.menuType === 'C'"
class="menu-name-link"
@click="handleMenuClick(scope.row)"
:title="`点击跳转到${scope.row.menuName}模块`"
style="cursor: pointer; color: #409EFF;">
{{ scope.row.menuName }}
</span>
<span v-else>{{ scope.row.menuName }}</span>
</template>
</el-table-column>
<el-table-column prop="icon" label="图标" align="center" width="100">
<template #default="scope">
<svg-icon :icon-class="scope.row.icon" />
</template>
</el-table-column>
<el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="path" label="路由地址" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="fullPath" label="完整路径" :show-overflow-tooltip="true">
<template #default="scope">
<span v-if="scope.row.fullPath">{{ scope.row.fullPath }}</span>
<span v-else-if="scope.row.path">{{ scope.row.path }}</span>
</template>
</el-table-column>
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
<dict-tag :options="sys_normal_disable" :value="scope.row.status" class="dict-tag" />
</template>
</el-table-column>
<el-table-column prop="visible" label="显示状态" width="100">
<template #default="scope">
<dict-tag :options="sys_show_hide" :value="scope.row.visible" class="dict-tag" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" width="160" prop="createTime">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="210" class-name="small-padding fixed-width">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:menu:edit']" class="action-button">修改</el-button>
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:menu:add']" class="action-button">新增</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:menu:remove']" class="action-button">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加或修改菜单对话框 -->
<el-dialog :title="title" v-model="open" width="680px" append-to-body>
<el-form ref="menuRef" :model="form" :rules="rules" label-width="100px">
<el-row>
<el-col :span="24">
<el-form-item label="上级菜单">
<el-tree-select
v-model="form.parentId"
:data="menuOptions"
:props="{ value: 'menuId', label: 'menuName', children: 'children' }"
value-key="menuId"
placeholder="选择上级菜单"
check-strictly
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="菜单类型" prop="menuType">
<el-radio-group v-model="form.menuType">
<el-radio value="M">目录</el-radio>
<el-radio value="C">菜单</el-radio>
<el-radio value="F">按钮</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="24" v-if="form.menuType != 'F'">
<el-form-item label="菜单图标" prop="icon">
<el-popover
placement="bottom-start"
:width="540"
trigger="click"
>
<template #reference>
<el-input v-model="form.icon" placeholder="点击选择图标" @blur="showSelectIcon" readonly>
<template #prefix>
<svg-icon
v-if="form.icon"
:icon-class="form.icon"
class="el-input__icon"
style="height: 32px;width: 16px;"
/>
<el-icon v-else style="height: 32px;width: 16px;"><search /></el-icon>
</template>
</el-input>
</template>
<icon-select ref="iconSelectRef" @selected="selected" :active-icon="form.icon" />
</el-popover>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="菜单名称" prop="menuName">
<el-input v-model="form.menuName" placeholder="请输入菜单名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="显示排序" prop="orderNum">
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>是否外链
</span>
</template>
<el-radio-group v-model="form.isFrame">
<el-radio value="0"></el-radio>
<el-radio value="1"></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item prop="path">
<template #label>
<span>
<el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
路由地址
</span>
</template>
<el-input v-model="form.path" placeholder="请输入路由地址" />
</el-form-item>
</el-col>
<el-col :span="24" v-if="form.menuType != 'F' && form.fullPath">
<el-form-item label="完整路径">
<el-input v-model="form.fullPath" readonly placeholder="完整路径" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item prop="component">
<template #label>
<span>
<el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
组件路径
</span>
</template>
<el-input v-model="form.component" placeholder="请输入组件路径" />
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'M'">
<el-form-item>
<el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
<template #label>
<span>
<el-tooltip content="控制器中定义的权限字符,如:@PreAuthorize(`@ss.hasPermi('system:user:list')`)" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
权限字符
</span>
</template>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item>
<el-input v-model="form.query" placeholder="请输入路由参数" maxlength="255" />
<template #label>
<span>
<el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
路由参数
</span>
</template>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType == 'C'">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
是否缓存
</span>
</template>
<el-radio-group v-model="form.isCache">
<el-radio value="0">缓存</el-radio>
<el-radio value="1">不缓存</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.menuType != 'F'">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
显示状态
</span>
</template>
<el-radio-group v-model="form.visible">
<el-radio
v-for="dict in sys_show_hide"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item>
<template #label>
<span>
<el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
<el-icon><question-filled /></el-icon>
</el-tooltip>
菜单状态
</span>
</template>
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in sys_normal_disable"
:key="dict.value"
:value="dict.value"
>{{ dict.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.menu-name-link:hover {
text-decoration: underline;
}
</style>
<script setup name="Menu">
import {addMenu, delMenu, getMenu, listMenu, updateMenu, treeselect} from "@/api/system/menu";
import SvgIcon from "@/components/SvgIcon";
import IconSelect from "@/components/IconSelect";
import {getNormalPath} from "@/utils/openhis";
const { proxy } = getCurrentInstance();
const { sys_show_hide, sys_normal_disable } = proxy.useDict("sys_show_hide", "sys_normal_disable");
const menuList = ref([]);
const open = ref(false);
const loading = ref(true);
const showSearch = ref(true);
const title = ref("");
const menuOptions = ref([]);
const isExpandAll = ref(false);
const refreshTable = ref(true);
const iconSelectRef = ref(null);
const data = reactive({
form: {},
queryParams: {
menuName: undefined,
visible: undefined
},
rules: {
menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
},
});
const { queryParams, form, rules } = toRefs(data);
/** 查询菜单列表 */
async function getList() {
loading.value = true;
try {
const response = await listMenu(queryParams.value);
// 后端已经返回了带完整路径的菜单树,直接使用即可
menuList.value = response.data;
} catch (error) {
console.error('获取菜单列表失败:', error);
} finally {
loading.value = false;
}
}
/** 查询菜单下拉树结构 */
function getTreeselect() {
menuOptions.value = [];
// 使用专门的treeselect API它返回预构建的树形结构
treeselect().then(response => {
// TreeSelect对象使用id、label、children字段但el-tree-select组件期望menuId、menuName、children字段
// 需要将TreeSelect对象转换为el-tree-select组件期望的格式
const convertTreeSelectToMenuFormat = (treeSelectList) => {
return treeSelectList.map(item => ({
menuId: item.id,
menuName: item.label,
value: item.id,
label: item.label,
children: item.children ? convertTreeSelectToMenuFormat(item.children) : []
}));
};
const rootNode = {
menuId: 0,
menuName: "主类目",
value: 0,
label: "主类目",
children: convertTreeSelectToMenuFormat(response.data)
};
menuOptions.value.push(rootNode);
});
}
/** 取消按钮 */
function cancel() {
open.value = false;
reset();
}
/** 表单重置 */
function reset() {
form.value = {
menuId: undefined,
parentId: 0,
menuName: undefined,
icon: undefined,
menuType: "M",
orderNum: undefined,
isFrame: "1",
isCache: "0",
visible: "0",
status: "0"
};
proxy.resetForm("menuRef");
}
/** 展示下拉图标 */
function showSelectIcon() {
iconSelectRef.value.reset();
}
/** 选择图标 */
function selected(name) {
form.value.icon = name;
}
/** 搜索按钮操作 */
function handleQuery() {
getList();
}
/** 重置按钮操作 */
function resetQuery() {
proxy.resetForm("queryRef");
handleQuery();
}
/** 新增按钮操作 */
async function handleAdd(row) {
reset();
await getTreeselect();
if (row != null && row.menuId) {
form.value.parentId = row.menuId;
// 使用后端返回的完整路径
form.value.parentFullPath = row.fullPath || row.path;
} else {
form.value.parentId = 0;
form.value.parentFullPath = '';
}
open.value = true;
title.value = "添加菜单";
}
/** 展开/折叠操作 */
function toggleExpandAll() {
refreshTable.value = false;
isExpandAll.value = !isExpandAll.value;
nextTick(() => {
refreshTable.value = true;
});
}
/** 修改按钮操作 */
async function handleUpdate(row) {
reset();
await getTreeselect();
try {
const response = await getMenu(row.menuId);
form.value = response.data;
// 使用后端返回的完整路径
form.value.fullPath = response.data.fullPath || response.data.path;
open.value = true;
title.value = "修改菜单";
} catch (error) {
console.error('获取菜单信息失败:', error);
}
}
/** 提交按钮 */
function submitForm() {
proxy.$refs["menuRef"].validate(valid => {
if (valid) {
if (form.value.menuId != undefined) {
updateMenu(form.value).then(data => {
if (data === -1) {
proxy.$modal.msgError("路由地址已存在");
} else {
proxy.$modal.msgSuccess("修改成功");
open.value = false;
getList();
}
}).catch(() => {
// 可以在这里添加自定义的错误处理,或者使用默认的错误提示
proxy.$modal.msgError("路由地址已存在");
});
} else {
addMenu(form.value).then(data => {
if (data === -1) {
proxy.$modal.msgError("路由地址已存在");
} else {
proxy.$modal.msgSuccess("新增成功");
open.value = false;
getList();
}
}).catch(() => {
// 可以在这里添加自定义的错误处理,或者使用默认的错误提示
proxy.$modal.msgError("路由地址已存在");
});
}
}
});
}
/** 删除按钮操作 */
function handleDelete(row) {
proxy.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?').then(function() {
return delMenu(row.menuId);
}).then(() => {
getList();
proxy.$modal.msgSuccess("删除成功");
}).catch(() => {});
}
/** 处理菜单点击事件,跳转到对应功能模块 */
function handleMenuClick(row) {
// 只有菜单类型(C)才会进入此函数,因为模板中已限制
// 检查菜单是否有对应的路由路径
if (!row.path) {
proxy.$modal.msgWarning(`${row.menuName} 暂无对应的功能模块`);
return;
}
// 如果是外部链接,新开窗口打开
if (row.isFrame === '0' && (row.path.startsWith('http://') || row.path.startsWith('https://'))) {
window.open(row.path, '_blank');
return;
}
// 使用完整路径作为主要路径,但如果它包含 /system 前缀而原始路径不包含,
// 则使用原始路径,以避免路由系统添加额外的 /system 前缀
let routePath = row.fullPath || row.path;
// 特殊处理:如果完整路径以 /system/ 开头,但菜单本身路径不包含 /system/
// 则使用菜单路径,避免重复添加 /system 前缀
if (row.fullPath && row.path &&
row.fullPath.startsWith('/system/') &&
!row.path.startsWith('/system/')) {
routePath = row.path;
}
// 确保路径以 / 开头
if (!routePath.startsWith('/')) {
routePath = '/' + routePath;
}
// 规范化路径,处理可能的路径问题
const normalizedPath = getNormalPath(routePath);
// 尝试导航到对应路由
try {
// 使用 router.push 导航到目标路由
proxy.$router.push({
path: normalizedPath
}).catch(err => {
// 如果路由导航失败,尝试另一种方式
console.error(`路由导航失败,尝试备用方案: ${normalizedPath}`, err);
// 尝试使用 name 进行路由跳转(如果菜单有路由名称)
if (row.routeName) {
try {
proxy.$router.push({ name: row.routeName }).catch(nameErr => {
console.error(`使用路由名称跳转也失败: ${row.routeName}`, nameErr);
proxy.$modal.msgError(`${row.menuName} 模块暂无法访问,请检查权限或联系管理员`);
});
} catch (nameErr) {
console.error(`使用路由名称跳转异常: ${row.routeName}`, nameErr);
proxy.$modal.msgError(`${row.menuName} 模块跳转失败`);
}
} else {
proxy.$modal.msgError(`${row.menuName} 模块暂无法访问,请检查权限或联系管理员`);
}
});
} catch (error) {
console.error(`跳转到 ${row.menuName} 模块失败:`, error);
proxy.$modal.msgError(`${row.menuName} 模块跳转失败`);
}
}
getList();
</script>