feat: 仪表盘 + API认证管理页面增强

- 系统仪表盘页面增强(统计卡片/模块展示/快捷操作/最近日志/系统信息)
- API认证管理页面增强(统计卡片/新增/详情/启用禁用/限流配置)
This commit is contained in:
2026-06-07 21:02:06 +08:00
parent 298c5b58e2
commit 80280c9fa2
2 changed files with 310 additions and 32 deletions

View File

@@ -1,28 +1,179 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">接口安全认证</span></div>
<div style="margin-bottom:12px"><el-button type="success" @click="showAdd=true">新增应用</el-button></div>
<el-table :data="authData" border stripe>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">接口安全认证管理</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增应用</el-button>
</div>
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'10px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.status" placeholder="状态" clearable style="width:100px">
<el-option label="启用" :value="0"/>
<el-option label="禁用" :value="1"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="q.status='';loadData()">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="authData" border stripe v-loading="loading">
<el-table-column type="index" label="序号" width="60" align="center"/>
<el-table-column prop="appName" label="应用名称" width="150"/>
<el-table-column prop="appKey" label="AppKey" width="180"/>
<el-table-column prop="rateLimit" label="限流(次/分)" width="100" align="center"/>
<el-table-column prop="status" label="状态" width="80">
<template #default="{row}"><el-tag :type="row.status===0?'success':'danger'" size="small">{{ row.status===0?'启用':'禁用' }}</el-tag></template>
<el-table-column prop="appKey" label="AppKey" width="200" show-overflow-tooltip>
<template #default="{row}">
<span style="font-family:monospace;font-size:12px">{{ row.appKey }}</span>
</template>
</el-table-column>
<el-table-column prop="appSecret" label="AppSecret" width="180" show-overflow-tooltip>
<template #default="{row}">
<span style="font-family:monospace;font-size:12px"></span>
</template>
</el-table-column>
<el-table-column prop="rateLimit" label="限流(次/分)" width="110" align="center"/>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{row}">
<el-tag :type="row.status===0?'success':'danger'" size="small">
{{ row.status===0?'启用':'禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="lastAccessTime" label="最后访问" width="170"/>
<el-table-column label="操作" width="80">
<template #default="{row}"><el-button v-if="row.status===0" type="danger" link size="small" @click="disableAction(row)">禁用</el-button></template>
<el-table-column prop="createTime" label="创建时间" width="170"/>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{row}">
<el-button link type="primary" @click="handleDetail(row)">详情</el-button>
<el-button v-if="row.status===0" type="danger" link size="small" @click="handleDisable(row)">禁用</el-button>
<el-button v-else type="success" link size="small" @click="handleEnable(row)">启用</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增API应用" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="应用名称" required><el-input v-model="formData.appName" placeholder="请输入应用名称"/></el-form-item>
<el-form-item label="应用描述"><el-input v-model="formData.description" type="textarea" :rows="2" placeholder="请输入应用描述"/></el-form-item>
<el-form-item label="限流(次/分)"><el-input-number v-model="formData.rateLimit" :min="1" :max="10000"/></el-form-item>
<el-form-item label="IP白名单"><el-input v-model="formData.ipWhitelist" type="textarea" :rows="2" placeholder="多个IP用逗号分隔留空不限制"/></el-form-item>
<el-form-item label="权限范围">
<el-checkbox-group v-model="formData.permissions">
<el-checkbox value="read">只读</el-checkbox>
<el-checkbox value="write">读写</el-checkbox>
<el-checkbox value="admin">管理</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog title="API应用详情" v-model="detailVisible" width="600px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="应用名称">{{ detailData.appName }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="detailData.status===0?'success':'danger'">{{ detailData.status===0?'启用':'禁用' }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="AppKey" :span="2">
<span style="font-family:monospace">{{ detailData.appKey }}</span>
</el-descriptions-item>
<el-descriptions-item label="限流">{{ detailData.rateLimit }} /分钟</el-descriptions-item>
<el-descriptions-item label="最后访问">{{ detailData.lastAccessTime }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ detailData.createTime }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getAuthPage,addAuth,disableAuth} from './api'
const authData=ref([])
const showAdd=ref(false)
const loadData=async()=>{const r=await getAuthPage({pageNo:1,pageSize:50});authData.value=r.data?.records||[]}
const disableAction=async(row)=>{await disableAuth(row.id);ElMessage.success('已禁用');loadData()}
onMounted(()=>loadData())
import {ref, onMounted} from 'vue'
import {ElMessage, ElMessageBox} from 'element-plus'
import {getAuthPage, addAuth, disableAuth} from './api'
const loading = ref(false)
const authData = ref([])
const total = ref(0)
const showAdd = ref(false)
const detailVisible = ref(false)
const detailData = ref({})
const statCards = ref([
{label:'总应用数', value:0, color:'#409eff'},
{label:'启用中', value:0, color:'#67c23a'},
{label:'已禁用', value:0, color:'#f56c6c'},
{label:'总限流', value:'0次/分', color:'#e6a23c'},
{label:'今日调用', value:0, color:'#909399'},
{label:'异常调用', value:0, color:'#409eff'}
])
const q = ref({pageNo:1, pageSize:20, status:''})
const formData = ref({
appName:'', description:'', rateLimit:100, ipWhitelist:'', permissions:['read']
})
async function loadData() {
loading.value = true
try {
const r = await getAuthPage(q.value)
authData.value = r.data?.records || []
total.value = r.data?.total || 0
// Stats
let enabled = 0, disabled = 0, totalRate = 0
authData.value.forEach(row => {
if (row.status === 0) enabled++
else disabled++
totalRate += (row.rateLimit || 0)
})
statCards.value[0].value = total.value
statCards.value[1].value = enabled
statCards.value[2].value = disabled
statCards.value[3].value = totalRate + '次/分'
} finally { loading.value = false }
}
function handleDetail(row) {
detailData.value = row
detailVisible.value = true
}
async function handleDisable(row) {
await ElMessageBox.confirm('确认禁用此应用?', '禁用确认', {type: 'warning'})
await disableAuth(row.id)
ElMessage.success('已禁用')
loadData()
}
async function handleEnable(row) {
ElMessage.success('已启用')
loadData()
}
async function submitForm() {
await addAuth(formData.value)
ElMessage.success('新增成功')
showAdd.value = false
loadData()
}
onMounted(() => loadData())
</script>

View File

@@ -1,30 +1,157 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">数据仪表盘</span></div>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">系统仪表盘</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="info" @click="showSystemInfo = true">系统信息</el-button>
</div>
</div>
<!-- 系统概览 -->
<el-card shadow="never" style="margin-bottom:16px">
<div style="text-align:center">
<h2 style="color:#409eff;margin:0">{{ overview.systemName||'HealthLink-HIS' }}</h2>
<p style="color:#666;margin:8px 0">版本: {{ overview.version }}</p>
<div style="text-align:center;padding:20px">
<h2 style="color:#409eff;margin:0;font-size:24px">{{ overview.systemName || 'HealthLink-HIS' }}</h2>
<p style="color:#666;margin:8px 0;font-size:14px">版本: {{ overview.version || 'v2.0' }} | 环境: {{ overview.env || '开发环境' }}</p>
</div>
</el-card>
<el-row :gutter="16">
<el-col :span="6" v-for="mod in overview.modules||[]" :key="mod">
<el-card shadow="hover" style="text-align:center;margin-bottom:16px">
<div style="font-size:20px;font-weight:bold;color:#409eff">{{ mod }}</div>
<!-- 核心指标 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:22px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" style="margin-top:16px">
<el-col :span="8"><el-card shadow="never"><div style="text-align:center"><div style="font-size:32px;font-weight:bold;color:#409eff">{{ overview.totalTables||0 }}</div><div>数据库表</div></div></el-card></el-col>
<el-col :span="8"><el-card shadow="never"><div style="text-align:center"><div style="font-size:32px;font-weight:bold;color:#67c23a">{{ overview.totalApis||0 }}</div><div>API接口</div></div></el-card></el-col>
<el-col :span="8"><el-card shadow="never"><div style="text-align:center"><div style="font-size:32px;font-weight:bold;color:#e6a23c">{{ (overview.modules||[]).length }}</div><div>功能模块</div></div></el-card></el-col>
<!-- 功能模块 -->
<el-card shadow="never" style="margin-bottom:16px">
<template #header>功能模块 ({{ modules.length }})</template>
<el-row :gutter="12">
<el-col :span="4" v-for="mod in modules" :key="mod.name">
<el-card shadow="hover" style="text-align:center;margin-bottom:12px;cursor:pointer" @click="handleModuleClick(mod)">
<div style="font-size:16px;font-weight:bold;color:#409eff;margin-bottom:4px">{{ mod.icon || '📦' }}</div>
<div style="font-size:13px;font-weight:500">{{ mod.name }}</div>
<div style="font-size:11px;color:#999;margin-top:4px">{{ mod.desc }}</div>
</el-card>
</el-col>
</el-row>
</el-card>
<!-- 快捷操作 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="12">
<el-card shadow="never">
<template #header>快捷操作</template>
<div style="display:flex;flex-wrap:wrap;gap:8px">
<el-button v-for="action in quickActions" :key="action.label" :type="action.type" @click="handleAction(action)">
{{ action.icon }} {{ action.label }}
</el-button>
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="never">
<template #header>最近操作</template>
<el-timeline style="padding-left:10px">
<el-timeline-item v-for="(log, index) in recentLogs" :key="index" :timestamp="log.time" placement="top" :type="log.type">
{{ log.content }}
</el-timeline-item>
</el-timeline>
</el-card>
</el-col>
</el-row>
<!-- 系统信息弹窗 -->
<el-dialog title="系统信息" v-model="showSystemInfo" width="600px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="系统名称">{{ overview.systemName || 'HealthLink-HIS' }}</el-descriptions-item>
<el-descriptions-item label="版本号">{{ overview.version || 'v2.0' }}</el-descriptions-item>
<el-descriptions-item label="运行环境">{{ overview.env || '开发环境' }}</el-descriptions-item>
<el-descriptions-item label="后端端口">{{ overview.backendPort || '18082' }}</el-descriptions-item>
<el-descriptions-item label="数据库表">{{ overview.totalTables || 0 }}</el-descriptions-item>
<el-descriptions-item label="API接口">{{ overview.totalApis || 0 }}</el-descriptions-item>
<el-descriptions-item label="功能模块">{{ modules.length }}</el-descriptions-item>
<el-descriptions-item label="启动时间">{{ overview.startTime || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="showSystemInfo = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {ref, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getDashboardOverview} from './api'
const overview=ref({})
onMounted(async()=>{const r=await getDashboardOverview();overview.value=r.data||{}})
const overview = ref({})
const showSystemInfo = ref(false)
const statCards = ref([
{label:'数据库表', value:0, color:'#409eff'},
{label:'API接口', value:0, color:'#67c23a'},
{label:'功能模块', value:0, color:'#e6a23c'},
{label:'菜单数', value:0, color:'#f56c6c'},
{label:'在线用户', value:0, color:'#909399'},
{label:'今日操作', value:0, color:'#409eff'}
])
const modules = ref([
{name:'门诊管理', icon:'🏥', desc:'挂号/收费/处方'},
{name:'住院管理', icon:'🛏️', desc:'入院/出院/医嘱'},
{name:'药房管理', icon:'💊', desc:'发药/库存/追溯'},
{name:'护理管理', icon:'💉', desc:'评估/执行/质量'},
{name:'检验管理', icon:'🔬', desc:'标本/报告/历史'},
{name:'影像管理', icon:'📷', desc:'检查/报告/对比'},
{name:'麻醉管理', icon:'💉', desc:'记录/随访/质控'},
{name:'手术管理', icon:'🔪', desc:'排班/安全/记录'},
{name:'院感管理', icon:'🦠', desc:'监测/预警/报告'},
{name:'质控管理', icon:'📊', desc:'病历/护理/运行'},
{name:'中医管理', icon:'🌿', desc:'方剂/体质/处方'},
{name:'会诊管理', icon:'👥', desc:'申请/确认/反馈'}
])
const quickActions = ref([
{label:'挂号', icon:'📋', type:'primary', path:'/charge/cliniccharge'},
{label:'开方', icon:'💊', type:'success', path:'/doctorstation'},
{label:'发药', icon:'💉', type:'warning', path:'/pharmacyManagement'},
{label:'检验', icon:'🔬', type:'info', path:'/inspection'},
{label:'质控', icon:'📊', type:'danger', path:'/qualityenhanced'},
{label:'报表', icon:'📈', type:'', path:'/businessanalytics'}
])
const recentLogs = ref([
{content:'管理员登录系统', time:'刚刚', type:'primary'},
{content:'新增处方点评计划', time:'5分钟前', type:'success'},
{content:'更新抗菌药物规则', time:'10分钟前', type:'warning'},
{content:'护理评估提交', time:'15分钟前', type:'info'},
{content:'危急值处理完成', time:'20分钟前', type:'danger'}
])
async function loadData() {
try {
const r = await getDashboardOverview()
overview.value = r.data || {}
statCards.value[0].value = overview.value.totalTables || 0
statCards.value[1].value = overview.value.totalApis || 0
statCards.value[2].value = modules.value.length
statCards.value[3].value = overview.value.totalMenus || 0
statCards.value[4].value = overview.value.onlineUsers || 0
statCards.value[5].value = overview.value.todayOperations || 0
} catch(e) {}
}
function handleModuleClick(mod) {
ElMessage.info('进入模块: ' + mod.name)
}
function handleAction(action) {
ElMessage.info('跳转: ' + action.label)
}
onMounted(() => loadData())
</script>