feat: 知识库 + 经营分析 + 处方点评统计页面增强

- 临床知识库页面增强(统计卡片/新增/详情/搜索筛选)
- 经营分析页面增强(统计卡片/筛选/利润/床位率/住院日)
- 处方点评统计页面增强(医生排名/合理率/科室覆盖)
This commit is contained in:
2026-06-07 20:42:35 +08:00
parent aec389998d
commit 20354e8e19
3 changed files with 378 additions and 39 deletions

View File

@@ -1,29 +1,128 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">经营分析</span></div>
<el-card shadow="never" style="margin-bottom:16px">
<div style="display:flex;gap:30px;text-align:center;flex-wrap:wrap">
<div><div style="font-size:20px;font-weight:bold;color:#409eff">{{ summary.totalRevenue||0 }}</div><div>总收入</div></div>
<div><div style="font-size:20px;font-weight:bold;color:#f56c6c">{{ summary.totalCost||0 }}</div><div>总成本</div></div>
<div><div style="font-size:20px;font-weight:bold;color:#67c23a">{{ summary.totalProfit||0 }}</div><div>总利润</div></div>
<div><div style="font-size:20px;font-weight:bold;color:#e6a23c">{{ summary.totalPatients||0 }}</div><div>总患者</div></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="warning" @click="exportReport">导出报告</el-button>
</div>
</el-card>
<el-table :data="analyticsData" border stripe>
</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.departmentName" placeholder="科室" clearable style="width:140px">
<el-option v-for="dept in departments" :key="dept" :label="dept" :value="dept"/>
</el-select>
<el-date-picker v-model="q.dateRange" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px"/>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="analyticsData" border stripe v-loading="loading">
<el-table-column prop="statDate" label="日期" width="120"/>
<el-table-column prop="departmentName" label="科室" width="120"/>
<el-table-column prop="revenue" label="收入" width="100" align="right"/>
<el-table-column prop="cost" label="成本" width="100" align="right"/>
<el-table-column prop="profit" label="利润" width="100" align="right"/>
<el-table-column prop="revenue" label="收入(万元)" width="110" align="right">
<template #default="{row}">
<span style="color:#67C23A;font-weight:bold">{{ formatMoney(row.revenue) }}</span>
</template>
</el-table-column>
<el-table-column prop="cost" label="成本(万元)" width="110" align="right">
<template #default="{row}">
<span style="color:#F56C6C">{{ formatMoney(row.cost) }}</span>
</template>
</el-table-column>
<el-table-column prop="profit" label="利润(万元)" width="110" align="right">
<template #default="{row}">
<span :style="{color: row.profit >= 0 ? '#67C23A' : '#F56C6C', fontWeight:'bold'}">
{{ formatMoney(row.profit) }}
</span>
</template>
</el-table-column>
<el-table-column prop="patientCount" label="患者数" width="80" align="center"/>
<el-table-column prop="bedOccupancyRate" label="床位率%" width="90" align="center"/>
<el-table-column prop="bedOccupancyRate" label="床位率" width="90" align="center">
<template #default="{row}">
<span :style="{color: row.bedOccupancyRate > 90 ? '#67C23A' : row.bedOccupancyRate > 70 ? '#E6A23C' : '#F56C6C', fontWeight:'bold'}">
{{ row.bedOccupancyRate }}%
</span>
</template>
</el-table-column>
<el-table-column prop="avgStayDays" label="平均住院日" width="100" align="center"/>
<el-table-column prop="bedTurnover" label="床位周转" width="80" align="center"/>
<el-table-column prop="profitRate" label="利润率" width="90" align="center">
<template #default="{row}">
<span :style="{color: row.profitRate > 20 ? '#67C23A' : '#E6A23C', fontWeight:'bold'}">
{{ row.profitRate || 0 }}%
</span>
</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"/>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getAnalyticsPage,getAnalyticsSummary} from './api'
const analyticsData=ref([]),summary=ref({})
const loadData=async()=>{const [a,s]=await Promise.all([getAnalyticsPage({pageNo:1,pageSize:50}),getAnalyticsSummary()]);analyticsData.value=a.data?.records||[];summary.value=s.data||{}}
onMounted(()=>loadData())
import {ref, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getAnalyticsPage, getAnalyticsSummary} from './api'
const loading = ref(false)
const analyticsData = ref([])
const total = ref(0)
const summary = ref({})
const departments = ref(['内科','外科','妇产科','儿科','ICU','急诊科','骨科','神经内科','心内科','呼吸科'])
const statCards = ref([
{label:'总收入(万)', value:0, color:'#409eff'},
{label:'总成本(万)', value:0, color:'#f56c6c'},
{label:'总利润(万)', value:0, color:'#67c23a'},
{label:'总患者', value:0, color:'#e6a23c'},
{label:'平均床位率', value:'0%', color:'#909399'},
{label:'平均住院日', value:0, color:'#409eff'}
])
const q = ref({pageNo:1, pageSize:20, departmentName:'', dateRange:null})
function formatMoney(val) {
if (!val) return '0.00'
return (val / 10000).toFixed(2)
}
async function loadData() {
loading.value = true
try {
const [a, s] = await Promise.all([getAnalyticsPage(q.value), getAnalyticsSummary()])
analyticsData.value = a.data?.records || []
total.value = a.data?.total || 0
summary.value = s.data || {}
statCards.value[0].value = formatMoney(summary.value.totalRevenue)
statCards.value[1].value = formatMoney(summary.value.totalCost)
statCards.value[2].value = formatMoney(summary.value.totalProfit)
statCards.value[3].value = summary.value.totalPatients || 0
statCards.value[4].value = (summary.value.avgBedOccupancy || 0) + '%'
statCards.value[5].value = (summary.value.avgStayDays || 0)
} finally { loading.value = false }
}
function resetQuery() {
q.value = {pageNo:1, pageSize:20, departmentName:'', dateRange:null}
loadData()
}
function exportReport() { ElMessage.info('导出功能开发中') }
onMounted(() => loadData())
</script>

View File

@@ -1,27 +1,173 @@
<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="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>
<!-- 搜索 -->
<el-card shadow="never" style="margin-bottom:16px">
<el-form inline>
<el-form-item label="搜索"><el-input v-model="keyword" placeholder="指南/药物/诊断" clearable style="width:200px"/></el-form-item>
<el-form-item label="分类"><el-select v-model="category" clearable style="width:120px"><el-option v-for="c in [{l:'指南',v:'guideline'},{l:'药物',v:'drug'},{l:'诊断',v:'diagnosis'},{l:'操作',v:'procedure'}]" :key="c.v" :label="c.l" :value="c.v"/></el-select></el-form-item>
<el-form-item><el-button type="primary" @click="loadData">搜索</el-button></el-form-item>
<el-form-item label="关键词"><el-input v-model="keyword" placeholder="指南/药物/诊断/操作" clearable style="width:200px" @keyup.enter="loadData"/></el-form-item>
<el-form-item label="分类">
<el-select v-model="category" clearable style="width:140px">
<el-option label="临床指南" value="guideline"/>
<el-option label="药物信息" value="drug"/>
<el-option label="诊断标准" value="diagnosis"/>
<el-option label="操作规范" value="procedure"/>
<el-option label="护理规范" value="nursing"/>
<el-option label="感染控制" value="infection"/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData">搜索</el-button>
<el-button @click="keyword='';category='';loadData()">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-table :data="kbData" border stripe>
<el-table-column prop="category" label="分类" width="80"/>
<el-table-column prop="title" label="标题" min-width="200"/>
<!-- 数据表格 -->
<el-table :data="kbData" border stripe v-loading="loading">
<el-table-column prop="category" label="分类" width="100" align="center">
<template #default="{row}">
<el-tag size="small">{{ categoryText(row.category) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip/>
<el-table-column prop="keywords" label="关键词" width="150" show-overflow-tooltip/>
<el-table-column prop="source" label="来源" width="120"/>
<el-table-column prop="version" label="版本" width="80"/>
<el-table-column prop="version" label="版本" width="80" align="center"/>
<el-table-column prop="status" label="状态" width="80" align="center">
<template #default="{row}">
<el-tag :type="row.status==='ACTIVE'?'success':'info'" size="small">
{{ row.status==='ACTIVE'?'启用':'停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="updateTime" label="更新时间" width="160"/>
<el-table-column label="操作" width="100" fixed="right">
<template #default="{row}">
<el-button link type="primary" @click="handleDetail(row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="pageNo" v-model:page-size="pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增知识条目" v-model="showAdd" width="650px" append-to-body>
<el-form :model="formData" label-width="100px">
<el-form-item label="标题" required><el-input v-model="formData.title" placeholder="请输入标题"/></el-form-item>
<el-form-item label="分类" required>
<el-select v-model="formData.category" placeholder="请选择">
<el-option label="临床指南" value="guideline"/>
<el-option label="药物信息" value="drug"/>
<el-option label="诊断标准" value="diagnosis"/>
<el-option label="操作规范" value="procedure"/>
<el-option label="护理规范" value="nursing"/>
<el-option label="感染控制" value="infection"/>
</el-select>
</el-form-item>
<el-form-item label="关键词"><el-input v-model="formData.keywords" placeholder="多个关键词用逗号分隔"/></el-form-item>
<el-form-item label="来源"><el-input v-model="formData.source" placeholder="如: 中华医学会"/></el-form-item>
<el-form-item label="内容"><el-input v-model="formData.content" type="textarea" :rows="6" placeholder="请输入知识内容"/></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="detailData.title" v-model="detailVisible" width="700px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="分类">{{ categoryText(detailData.category) }}</el-descriptions-item>
<el-descriptions-item label="来源">{{ detailData.source }}</el-descriptions-item>
<el-descriptions-item label="关键词" :span="2">{{ detailData.keywords }}</el-descriptions-item>
<el-descriptions-item label="版本">{{ detailData.version }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ detailData.status==='ACTIVE'?'启用':'停用' }}</el-descriptions-item>
</el-descriptions>
<div style="margin-top:16px;padding:12px;background:#f5f7fa;border-radius:4px;min-height:100px;white-space:pre-wrap;">{{ detailData.content || '暂无内容' }}</div>
<template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getKBPage} from './api'
const keyword=ref(''),category=ref('')
const kbData=ref([])
const loadData=async()=>{const r=await getKBPage({keyword:keyword.value,category:category.value,pageNo:1,pageSize:50});kbData.value=r.data?.records||[]}
onMounted(()=>loadData())
import {ref, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getKBPage, addKB} from './api'
const loading = ref(false)
const kbData = ref([])
const total = ref(0)
const keyword = ref('')
const category = ref('')
const pageNo = ref(1)
const pageSize = ref(20)
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:'#e6a23c'},
{label:'诊断', value:0, color:'#f56c6c'},
{label:'操作规范', value:0, color:'#909399'},
{label:'其他', value:0, color:'#409eff'}
])
const formData = ref({title:'', category:'guideline', keywords:'', source:'', content:''})
function categoryText(c) {
return {guideline:'临床指南',drug:'药物信息',diagnosis:'诊断标准',procedure:'操作规范',nursing:'护理规范',infection:'感染控制'}[c] || c
}
async function loadData() {
loading.value = true
try {
const r = await getKBPage({keyword:keyword.value, category:category.value, pageNo:pageNo.value, pageSize:pageSize.value})
kbData.value = r.data?.records || []
total.value = r.data?.total || 0
// Stats
let counts = {guideline:0, drug:0, diagnosis:0, procedure:0, nursing:0, infection:0}
kbData.value.forEach(row => { if (counts[row.category] !== undefined) counts[row.category]++ })
statCards.value[0].value = total.value
statCards.value[1].value = counts.guideline
statCards.value[2].value = counts.drug
statCards.value[3].value = counts.diagnosis
statCards.value[4].value = counts.procedure
statCards.value[5].value = counts.nursing + counts.infection
} finally { loading.value = false }
}
function handleDetail(row) {
detailData.value = row
detailVisible.value = true
}
async function submitForm() {
await addKB(formData.value)
ElMessage.success('新增成功')
showAdd.value = false
loadData()
}
onMounted(() => loadData())
</script>

View File

@@ -1,15 +1,109 @@
<template>
<div class="app-container">
<el-row :gutter="20" class="mb8">
<el-col :span="6"><el-card shadow="hover"><el-statistic title="点评计划" :value="stats.totalPlans || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="点评处方" :value="stats.totalRecords || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="不合理处方" :value="stats.unreasonableCount || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="合理率" :value="stats.reasonableRate || 100" suffix="%" /></el-card></el-col>
<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="warning" @click="exportReport">导出报告</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:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;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:16px;display:flex;gap:8px;flex-wrap:wrap">
<el-date-picker v-model="q.dateRange" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px"/>
<el-select v-model="q.departmentName" placeholder="科室" clearable style="width:140px">
<el-option v-for="dept in departments" :key="dept" :label="dept" :value="dept"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="rankingData" border stripe v-loading="loading">
<el-table-column type="index" label="排名" width="60" align="center"/>
<el-table-column prop="doctorName" label="医生" width="120"/>
<el-table-column prop="departmentName" label="科室" width="120"/>
<el-table-column prop="totalPrescriptions" label="处方总数" width="100" align="center"/>
<el-table-column prop="unreasonableCount" label="不合理数" width="100" align="center">
<template #default="{row}">
<span :style="{color: row.unreasonableCount > 0 ? '#F56C6C' : '#67C23A', fontWeight:'bold'}">
{{ row.unreasonableCount }}
</span>
</template>
</el-table-column>
<el-table-column prop="reasonableRate" label="合理率" width="100" align="center">
<template #default="{row}">
<el-progress :percentage="row.reasonableRate || 0" :color="row.reasonableRate >= 90 ? '#67C23A' : '#F56C6C'" :stroke-width="12"/>
</template>
</el-table-column>
<el-table-column prop="unreasonableType" label="主要问题" min-width="150" show-overflow-tooltip/>
</el-table>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'; import { getStatistics } from '@/api/review'
import {ref, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getStatistics, getDoctorRanking} from '@/api/review'
const loading = ref(false)
const stats = ref({})
onMounted(async () => { const r = await getStatistics(); stats.value = r.data || {} })
const rankingData = ref([])
const departments = ref(['内科','外科','妇产科','儿科','ICU','急诊科','骨科','神经内科'])
const q = ref({dateRange:null, departmentName:''})
const statCards = ref([
{label:'点评计划', value:0, color:'#409eff'},
{label:'点评处方', value:0, color:'#67c23a'},
{label:'不合理处方', value:0, color:'#f56c6c'},
{label:'合理率', value:'100%', color:'#e6a23c'},
{label:'点评医生数', value:0, color:'#909399'},
{label:'科室覆盖', value:0, color:'#409eff'}
])
async function loadData() {
loading.value = true
try {
const [s, r] = await Promise.all([
getStatistics(),
getDoctorRanking({startDate: q.value.dateRange?.[0], endDate: q.value.dateRange?.[1]})
])
stats.value = s.data || {}
rankingData.value = r.data || []
// Update stats
statCards.value[0].value = stats.value.totalPlans || 0
statCards.value[1].value = stats.value.totalRecords || 0
statCards.value[2].value = stats.value.unreasonableCount || 0
statCards.value[3].value = (stats.value.reasonableRate || 100) + '%'
// Count unique doctors and departments
let doctorSet = new Set(), deptSet = new Set()
rankingData.value.forEach(row => {
doctorSet.add(row.doctorName)
deptSet.add(row.departmentName)
})
statCards.value[4].value = doctorSet.size
statCards.value[5].value = deptSet.size
} finally { loading.value = false }
}
function resetQuery() {
q.value = {dateRange:null, departmentName:''}
loadData()
}
function exportReport() { ElMessage.info('导出功能开发中') }
onMounted(() => loadData())
</script>