@@ -1,6 +1,6 @@
< script setup >
import { ref , onMounted } from 'vue'
import { getStatistics , getExpiryWarning } from '@/api/dashboard'
import { ref , computed , onMounted } from 'vue'
import { getStatistics , getExpiryWarning , getChartData } from '@/api/dashboard'
import { getStockInList } from '@/api/stockIn'
import { getStockOutList } from '@/api/stockOut'
import { Goods , Box , DocumentChecked , Warning , Clock } from '@element-plus/icons-vue'
@@ -9,33 +9,59 @@ const statistics = ref({ medicineCount: 0, stockBatchCount: 0, pendingStockIn: 0
const expiryList = ref ( [ ] )
const recentInList = ref ( [ ] )
const recentOutList = ref ( [ ] )
const chartData = ref ( { monthlyStats : [ ] , categoryStats : [ ] , totalStockValue : 0 } )
const statCards = [
{ key : 'medicineCount' , label : '药品总数' , color : '#409eff' , bg : 'linear-gradient(135deg, #409eff, #66b1ff)' , icon : Goods } ,
{ key : 'stockBatchCount' , label : '库存批次' , color : '#67c23a' , bg : 'linear-gradient(135deg, #67c23a, #95d475)' , icon : Box } ,
{ key : 'pendingStockIn' , label : '待审入库' , color : '#e6a23c' , bg : 'linear-gradient(135deg, #e6a23c, #f3d19e)' , icon : DocumentChecked } ,
{ key : 'expiryWarningCount' , label : '效期预警' , color : '#f56c6c' , bg : 'linear-gradient(135deg, #f56c6c, #fab6b6)' , icon : Warning } ,
{ key : 'medicineCount' , label : '药品总数' , i con : Goods , bg : 'linear-gradient(135deg,#409eff,#66b1ff)' } ,
{ key : 'stockBatchCount' , label : '库存批次' , i con : Box , bg : 'linear-gradient(135deg,#67c23a,#95d475)' } ,
{ key : 'pendingStockIn' , label : '待审入库' , i con : DocumentChecked , bg : 'linear-gradient(135deg,#e6a23c,#f3d19e)' } ,
{ key : 'expiryWarningCount' , label : '效期预警' , i con : Warning , bg : 'linear-gradient(135deg,#f56c6c,#fab6b6)' } ,
]
function expiryColor ( date ) {
if ( ! date ) return ''
const days = Math . ceil ( ( new Date ( date ) - new Date ( ) ) / ( 1000 * 60 * 60 * 24 ) )
if ( days < 30 ) return 'danger '
if ( days < 90 ) return 'warning'
return 'success'
return days < 30 ? 'danger' : days < 90 ? 'warning' : 'success '
}
// 柱状图:月度出入库
const barOption = computed ( ( ) => ( {
tooltip : { trigger : 'axis' } ,
legend : { data : [ '入库金额' , '出库金额' ] , bottom : 0 } ,
grid : { left : 60 , right : 20 , top : 20 , bottom : 30 } ,
xAxis : { type : 'category' , data : chartData . value . monthlyStats ? . map ( m => m . month . substring ( 5 ) ) || [ ] } ,
yAxis : { type : 'value' , axisLabel : { formatter : v => v >= 10000 ? ( v / 10000 ) . toFixed ( 1 ) + '万' : v } } ,
series : [
{ name : '入库金额' , type : 'bar' , data : chartData . value . monthlyStats ? . map ( m => m . inAmount ) || [ ] , itemStyle : { color : '#67c23a' , borderRadius : [ 4 , 4 , 0 , 0 ] } , barMaxWidth : 30 } ,
{ name : '出库金额' , type : 'bar' , data : chartData . value . monthlyStats ? . map ( m => m . outAmount ) || [ ] , itemStyle : { color : '#f56c6c' , borderRadius : [ 4 , 4 , 0 , 0 ] } , barMaxWidth : 30 } ,
] ,
} ) )
// 饼图:药品分类占比
const pieOption = computed ( ( ) => ( {
tooltip : { trigger : 'item' , formatter : '{b}: {c} ({d}%)' } ,
legend : { type : 'scroll' , orient : 'vertical' , right : 10 , top : 20 , bottom : 20 } ,
series : [ {
type : 'pie' , radius : [ '45%' , '75%' ] , center : [ '35%' , '55%' ] ,
itemStyle : { borderRadius : 6 , borderColor : '#fff' , borderWidth : 3 } ,
label : { show : false } ,
data : chartData . value . categoryStats || [ ] ,
} ] ,
} ) )
onMounted ( async ( ) => {
try {
const [ statRes , warnRes , inRes , outRes ] = await Promise . allSettled ( [
const [ statRes , warnRes , inRes , outRes , chartRes ] = await Promise . allSettled ( [
getStatistics ( ) , getExpiryWarning ( ) ,
getStockInList ( { page : 1 , size : 4 } ) ,
getStockOutList ( { page : 1 , size : 4 } ) ,
getChartData ( ) ,
] )
if ( statRes . status === 'fulfilled' ) statistics . value = statRes . value . data || { }
if ( warnRes . status === 'fulfilled' ) expiryList . value = warnRes . value . data ? . records || warnRes . value . data || [ ]
if ( inRes . status === 'fulfilled' ) recentInList . value = inRes . value . data ? . records || [ ]
if ( outRes . status === 'fulfilled' ) recentOutList . value = outRes . value . data ? . records || [ ]
if ( chartRes . status === 'fulfilled' ) chartData . value = chartRes . value . data || { }
} catch { /* */ }
} )
< / script >
@@ -45,10 +71,12 @@ onMounted(async () => {
< div class = "page-header" >
< span class = "page-title" > 仪表盘 < / span >
< span style = "color:#909399;font-size:13px" >
< el-icon > < Clock / > < / el-icon > { { new Date ( ) . toLocaleDateString ( 'zh-CN' , { year : 'numeric' , month : 'long' , day : 'numeric' , weekday : 'long' } ) } }
< el-icon > < Clock / > < / el-icon >
{ { new Date ( ) . toLocaleDateString ( 'zh-CN' , { year : 'numeric' , month : 'long' , day : 'numeric' , weekday : 'long' } ) } }
< / span >
< / div >
<!-- 统计卡片 -- >
< div class = "stat-cards" >
< div v-for = "card in statCards" :key="card.key" class="stat-card" >
< div class = "stat-icon" : style = "{ background: card.bg }" >
@@ -61,12 +89,28 @@ onMounted(async () => {
< / div >
< / div >
<!-- 图表区 -- >
< div style = "display:grid;grid-template-columns:3fr 2fr;gap:20px;margin-bottom:20px" >
< div class = "section-card" >
< h4 style = "margin-bottom:8px" > 近6个月出入库金额 < / h4 >
< v-chart :option = "barOption" style = "height:280px" autoresize / >
< / div >
< div class = "section-card" >
< h4 style = "margin-bottom:8px" > 药品分类占比 < / h4 >
< v-chart :option = "pieOption" style = "height:280px" autoresize / >
< / div >
< / div >
<!-- 底部双栏 -- >
< div style = "display:grid;grid-template-columns:1fr 1fr;gap:20px" >
< div class = "section-card" >
< h4 style = "margin-bottom:12px;color:#e6a23c" > < el-icon > < W arn ing / > < / el-icon > 效期预警 ( 90 天内到期 ) < / h4 >
< el-table :data = "expiryList" size = "small" empty -text = " 暂无预警 ✨ " >
< el-table-column prop = "medicineName" label = "药品" min -width = " 140 " / >
< el-table-column prop = "batchNo" label = "批号" width = "110" / >
< div style = "display:flex;justify-content:space-between;align-items:center;m arg in-bottom:12px" >
< h4 style = "color:#e6a23c" > < el-icon > < Warning / > < / el-icon > 效期预警 < / h4 >
< span style = "font-size:12px;color:#909399" > 90 天内到期 · 共 { { expiryList . length } } 条 < / span >
< /div >
< el-table :data = "expiryList" size = "small" empty -text = " 暂无预警 ✨ " max -height = " 220 " >
< el-table-column prop = "medicineName" label = "药品" min -width = " 120 " / >
< el-table-column prop = "batchNo" label = "批号" width = "100" / >
< el-table-column prop = "quantity" label = "库存" width = "70" align = "center" / >
< el-table-column prop = "expiryDate" label = "有效期至" width = "120" align = "center" >
< template # default = "{ row }" >
@@ -76,31 +120,36 @@ onMounted(async () => {
< / el-table >
< / div >
< div class = "section-card" >
< h4 style = "margin-bottom:12px;color:#409eff" > < el-icon > < DocumentChecked / > < / el-icon > 最近业务 < / h4 >
< div style = "display:flex;justify-content:space-between;align-items:center;margin-bottom:12px" >
< h4 style = "color:#409eff" > < el-icon > < DocumentChecked / > < / el-icon > 最近业务 < / h4 >
< span style = "font-size:13px;color:#409eff;font-weight:600" >
库存总值 ¥ { { ( chartData . totalStockValue || 0 ) . toLocaleString ( ) } }
< / span >
< / div >
< div style = "display:grid;grid-template-columns:1fr 1fr;gap:16px" >
< div >
< h5 style = "margin-bottom:8px;font-size:13px;color:#909399 " > 最近入库 < / h5 >
< h5 style = "margin-bottom:8px;font-size:13px;color:#67c23a " > 最近入库 < / h5 >
< div v-if = "recentInList.length" >
< div v-for = "item in recentInList" :key="item.id" class="record-item" >
< span style = "font-size:13 px;font-weight:500" > { { item . recordNo } } < / span >
< span style = "font-size:12 px;font-weight:500" > { { item . recordNo } } < / span >
< el-tag :type = "item.status===1?'success':item.status===0?'warning':'info'" size = "small" effect = "plain" >
{ { item . status === 1 ? '已入库' : item . status === 0 ? '待审核' : '已取消 ' } }
{ { item . status === 1 ? '已入库' : '待审核 ' } }
< / el-tag >
< / div >
< / div >
< el-empty v-else description = "暂无" :image-size = "40 " / >
< el-empty v-else description = "暂无" :image-size = "36 " / >
< / div >
< div >
< h5 style = "margin-bottom:8px;font-size:13px;color:#909399 " > 最近出库 < / h5 >
< h5 style = "margin-bottom:8px;font-size:13px;color:#f56c6c " > 最近出库 < / h5 >
< div v-if = "recentOutList.length" >
< div v-for = "item in recentOutList" :key="item.id" class="record-item" >
< span style = "font-size:13 px;font-weight:500" > { { item . recordNo } } < / span >
< span style = "font-size:12 px;font-weight:500" > { { item . recordNo } } < / span >
< el-tag :type = "item.status===1?'success':item.status===0?'warning':'info'" size = "small" effect = "plain" >
{ { item . status === 1 ? '已出库' : item . status === 0 ? '待审核' : '已取消 ' } }
{ { item . status === 1 ? '已出库' : '待审核 ' } }
< / el-tag >
< / div >
< / div >
< el-empty v-else description = "暂无" :image-size = "40 " / >
< el-empty v-else description = "暂无" :image-size = "36 " / >
< / div >
< / div >
< / div >
@@ -111,7 +160,7 @@ onMounted(async () => {
< style lang = "scss" scoped >
. record - item {
display : flex ; justify - content : space - between ; align - items : center ;
padding : 8 px 0 ; border - bottom : 1 px dashed # ebeef5 ;
padding : 6 px 0 ; border - bottom : 1 px dashed # ebeef5 ;
& : last - child { border - bottom : none ; }
}
< / style >