添加图表数据展示功能
- 后端新增/chart-data接口用于获取图表所需数据 - 实现近6个月出入库金额统计和药品分类占比功能 - 前端集成echarts图表组件展示数据可视化 - 添加月度出入库柱状图和药品分类饼图 - 优化仪表盘界面布局和样式
This commit is contained in:
@@ -28,4 +28,9 @@ public class DashboardController {
|
|||||||
@RequestParam(defaultValue = "10") Integer size) {
|
@RequestParam(defaultValue = "10") Integer size) {
|
||||||
return Result.success(dashboardService.getExpiryWarning(new Page<>(page, size)));
|
return Result.success(dashboardService.getExpiryWarning(new Page<>(page, size)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/chart-data")
|
||||||
|
public Result<Map<String, Object>> chartData() {
|
||||||
|
return Result.success(dashboardService.getChartData());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import com.kronecker.backend.entity.Stock;
|
|||||||
import com.kronecker.backend.entity.StockInRecord;
|
import com.kronecker.backend.entity.StockInRecord;
|
||||||
import com.kronecker.backend.entity.StockOutRecord;
|
import com.kronecker.backend.entity.StockOutRecord;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public interface DashboardService {
|
public interface DashboardService {
|
||||||
java.util.Map<String, Object> getStatistics();
|
Map<String, Object> getStatistics();
|
||||||
Page<Stock> getExpiryWarning(Page<Stock> page);
|
Page<Stock> getExpiryWarning(Page<Stock> page);
|
||||||
Page<StockInRecord> getRecentStockIn(Page<StockInRecord> page);
|
Page<StockInRecord> getRecentStockIn(Page<StockInRecord> page);
|
||||||
Page<StockOutRecord> getRecentStockOut(Page<StockOutRecord> page);
|
Page<StockOutRecord> getRecentStockOut(Page<StockOutRecord> page);
|
||||||
|
Map<String, Object> getChartData();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.kronecker.backend.entity.*;
|
import com.kronecker.backend.entity.*;
|
||||||
import com.kronecker.backend.mapper.*;
|
import com.kronecker.backend.mapper.*;
|
||||||
|
import com.kronecker.backend.entity.*;
|
||||||
|
import com.kronecker.backend.mapper.*;
|
||||||
import com.kronecker.backend.service.DashboardService;
|
import com.kronecker.backend.service.DashboardService;
|
||||||
import com.kronecker.backend.utils.RedisCacheService;
|
import com.kronecker.backend.utils.RedisCacheService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -17,6 +19,7 @@ import java.util.*;
|
|||||||
public class DashboardServiceImpl implements DashboardService {
|
public class DashboardServiceImpl implements DashboardService {
|
||||||
|
|
||||||
private final MedicineMapper medicineMapper;
|
private final MedicineMapper medicineMapper;
|
||||||
|
private final MedicineCategoryMapper medicineCategoryMapper;
|
||||||
private final StockMapper stockMapper;
|
private final StockMapper stockMapper;
|
||||||
private final StockInRecordMapper stockInRecordMapper;
|
private final StockInRecordMapper stockInRecordMapper;
|
||||||
private final StockOutRecordMapper stockOutRecordMapper;
|
private final StockOutRecordMapper stockOutRecordMapper;
|
||||||
@@ -97,4 +100,50 @@ public class DashboardServiceImpl implements DashboardService {
|
|||||||
records.forEach(r -> { if (r.getCustomerId() != null) r.setCustomerName(custMap.getOrDefault(r.getCustomerId(), "")); });
|
records.forEach(r -> { if (r.getCustomerId() != null) r.setCustomerName(custMap.getOrDefault(r.getCustomerId(), "")); });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getChartData() {
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
|
||||||
|
// 近6个月出入库金额统计
|
||||||
|
List<Map<String, Object>> monthlyStats = new ArrayList<>();
|
||||||
|
for (int i = 5; i >= 0; i--) {
|
||||||
|
String month = LocalDate.now().minusMonths(i).format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"));
|
||||||
|
Double inAmount = stockInRecordMapper.selectList(new LambdaQueryWrapper<StockInRecord>()
|
||||||
|
.eq(StockInRecord::getStatus, 1)
|
||||||
|
.likeRight(StockInRecord::getInDate, month))
|
||||||
|
.stream().mapToDouble(r -> r.getTotalAmount().doubleValue()).sum();
|
||||||
|
Double outAmount = stockOutRecordMapper.selectList(new LambdaQueryWrapper<StockOutRecord>()
|
||||||
|
.eq(StockOutRecord::getStatus, 1)
|
||||||
|
.likeRight(StockOutRecord::getOutDate, month))
|
||||||
|
.stream().mapToDouble(r -> r.getTotalAmount().doubleValue()).sum();
|
||||||
|
Map<String, Object> m = new HashMap<>();
|
||||||
|
m.put("month", month);
|
||||||
|
m.put("inAmount", Math.round(inAmount * 100.0) / 100.0);
|
||||||
|
m.put("outAmount", Math.round(outAmount * 100.0) / 100.0);
|
||||||
|
monthlyStats.add(m);
|
||||||
|
}
|
||||||
|
data.put("monthlyStats", monthlyStats);
|
||||||
|
|
||||||
|
// 药品分类库存占比
|
||||||
|
List<Map<String, Object>> categoryStats = new ArrayList<>();
|
||||||
|
List<MedicineCategory> categories = medicineCategoryMapper.selectList(null);
|
||||||
|
for (MedicineCategory cat : categories) {
|
||||||
|
int count = medicineMapper.selectCount(new LambdaQueryWrapper<Medicine>().eq(Medicine::getCategoryId, cat.getId())).intValue();
|
||||||
|
if (count > 0) {
|
||||||
|
Map<String, Object> cs = new HashMap<>();
|
||||||
|
cs.put("name", cat.getName());
|
||||||
|
cs.put("value", count);
|
||||||
|
categoryStats.add(cs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.put("categoryStats", categoryStats);
|
||||||
|
|
||||||
|
// 库存总价值
|
||||||
|
double totalStockValue = stockMapper.selectList(new LambdaQueryWrapper<Stock>().gt(Stock::getQuantity, 0))
|
||||||
|
.stream().mapToDouble(s -> s.getCostPrice().doubleValue() * s.getQuantity()).sum();
|
||||||
|
data.put("totalStockValue", Math.round(totalStockValue * 100.0) / 100.0);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
frontend/package-lock.json
generated
43
frontend/package-lock.json
generated
@@ -10,9 +10,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.18.1",
|
"axios": "^1.18.1",
|
||||||
|
"echarts": "^6.1.0",
|
||||||
"element-plus": "^2.14.2",
|
"element-plus": "^2.14.2",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
"vue": "^3.5.39",
|
"vue": "^3.5.39",
|
||||||
|
"vue-echarts": "^8.0.1",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1097,6 +1099,22 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "6.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/echarts/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/element-plus": {
|
"node_modules/element-plus": {
|
||||||
"version": "2.14.2",
|
"version": "2.14.2",
|
||||||
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.2.tgz",
|
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.14.2.tgz",
|
||||||
@@ -2081,6 +2099,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-echarts": {
|
||||||
|
"version": "8.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vue-echarts/-/vue-echarts-8.0.1.tgz",
|
||||||
|
"integrity": "sha512-23rJTFLu1OUEGRWjJGmdGt8fP+8+ja1gVgzMYPIPaHWpXegcO1viIAaeu2H4QHESlVeHzUAHIxKXGrwjsyXAaA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"echarts": "^6.0.0",
|
||||||
|
"vue": "^3.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "4.6.4",
|
"version": "4.6.4",
|
||||||
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
|
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz",
|
||||||
@@ -2095,6 +2123,21 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "^3.5.0"
|
"vue": "^3.5.0"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"license": "0BSD"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,11 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.18.1",
|
"axios": "^1.18.1",
|
||||||
|
"echarts": "^6.1.0",
|
||||||
"element-plus": "^2.14.2",
|
"element-plus": "^2.14.2",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
"vue": "^3.5.39",
|
"vue": "^3.5.39",
|
||||||
|
"vue-echarts": "^8.0.1",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -7,3 +7,7 @@ export function getStatistics() {
|
|||||||
export function getExpiryWarning() {
|
export function getExpiryWarning() {
|
||||||
return request.get('/dashboard/expiry-warning')
|
return request.get('/dashboard/expiry-warning')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getChartData() {
|
||||||
|
return request.get('/dashboard/chart-data')
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { createPinia } from 'pinia'
|
|||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import ECharts from 'vue-echarts'
|
||||||
|
import 'echarts'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import permission from './directives/permission'
|
import permission from './directives/permission'
|
||||||
@@ -15,6 +17,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|||||||
app.component(key, component)
|
app.component(key, component)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
app.component('v-chart', ECharts)
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(ElementPlus, { size: 'default' })
|
app.use(ElementPlus, { size: 'default' })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { getStatistics, getExpiryWarning } from '@/api/dashboard'
|
import { getStatistics, getExpiryWarning, getChartData } from '@/api/dashboard'
|
||||||
import { getStockInList } from '@/api/stockIn'
|
import { getStockInList } from '@/api/stockIn'
|
||||||
import { getStockOutList } from '@/api/stockOut'
|
import { getStockOutList } from '@/api/stockOut'
|
||||||
import { Goods, Box, DocumentChecked, Warning, Clock } from '@element-plus/icons-vue'
|
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 expiryList = ref([])
|
||||||
const recentInList = ref([])
|
const recentInList = ref([])
|
||||||
const recentOutList = ref([])
|
const recentOutList = ref([])
|
||||||
|
const chartData = ref({ monthlyStats: [], categoryStats: [], totalStockValue: 0 })
|
||||||
|
|
||||||
const statCards = [
|
const statCards = [
|
||||||
{ key: 'medicineCount', label: '药品总数', color: '#409eff', bg: 'linear-gradient(135deg, #409eff, #66b1ff)', icon: Goods },
|
{ key: 'medicineCount', label: '药品总数', icon: Goods, bg: 'linear-gradient(135deg,#409eff,#66b1ff)' },
|
||||||
{ key: 'stockBatchCount', label: '库存批次', color: '#67c23a', bg: 'linear-gradient(135deg, #67c23a, #95d475)', icon: Box },
|
{ key: 'stockBatchCount', label: '库存批次', icon: Box, bg: 'linear-gradient(135deg,#67c23a,#95d475)' },
|
||||||
{ key: 'pendingStockIn', label: '待审入库', color: '#e6a23c', bg: 'linear-gradient(135deg, #e6a23c, #f3d19e)', icon: DocumentChecked },
|
{ key: 'pendingStockIn', label: '待审入库', icon: DocumentChecked, bg: 'linear-gradient(135deg,#e6a23c,#f3d19e)' },
|
||||||
{ key: 'expiryWarningCount', label: '效期预警', color: '#f56c6c', bg: 'linear-gradient(135deg, #f56c6c, #fab6b6)', icon: Warning },
|
{ key: 'expiryWarningCount', label: '效期预警', icon: Warning, bg: 'linear-gradient(135deg,#f56c6c,#fab6b6)' },
|
||||||
]
|
]
|
||||||
|
|
||||||
function expiryColor(date) {
|
function expiryColor(date) {
|
||||||
if (!date) return ''
|
if (!date) return ''
|
||||||
const days = Math.ceil((new Date(date) - new Date()) / (1000 * 60 * 60 * 24))
|
const days = Math.ceil((new Date(date) - new Date()) / (1000 * 60 * 60 * 24))
|
||||||
if (days < 30) return 'danger'
|
return days < 30 ? 'danger' : days < 90 ? 'warning' : 'success'
|
||||||
if (days < 90) return 'warning'
|
|
||||||
return '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 () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const [statRes, warnRes, inRes, outRes] = await Promise.allSettled([
|
const [statRes, warnRes, inRes, outRes, chartRes] = await Promise.allSettled([
|
||||||
getStatistics(), getExpiryWarning(),
|
getStatistics(), getExpiryWarning(),
|
||||||
getStockInList({ page: 1, size: 4 }),
|
getStockInList({ page: 1, size: 4 }),
|
||||||
getStockOutList({ page: 1, size: 4 }),
|
getStockOutList({ page: 1, size: 4 }),
|
||||||
|
getChartData(),
|
||||||
])
|
])
|
||||||
if (statRes.status === 'fulfilled') statistics.value = statRes.value.data || {}
|
if (statRes.status === 'fulfilled') statistics.value = statRes.value.data || {}
|
||||||
if (warnRes.status === 'fulfilled') expiryList.value = warnRes.value.data?.records || warnRes.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 (inRes.status === 'fulfilled') recentInList.value = inRes.value.data?.records || []
|
||||||
if (outRes.status === 'fulfilled') recentOutList.value = outRes.value.data?.records || []
|
if (outRes.status === 'fulfilled') recentOutList.value = outRes.value.data?.records || []
|
||||||
|
if (chartRes.status === 'fulfilled') chartData.value = chartRes.value.data || {}
|
||||||
} catch { /* */ }
|
} catch { /* */ }
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -45,10 +71,12 @@ onMounted(async () => {
|
|||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<span class="page-title">仪表盘</span>
|
<span class="page-title">仪表盘</span>
|
||||||
<span style="color:#909399;font-size:13px">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 统计卡片 -->
|
||||||
<div class="stat-cards">
|
<div class="stat-cards">
|
||||||
<div v-for="card in statCards" :key="card.key" class="stat-card">
|
<div v-for="card in statCards" :key="card.key" class="stat-card">
|
||||||
<div class="stat-icon" :style="{ background: card.bg }">
|
<div class="stat-icon" :style="{ background: card.bg }">
|
||||||
@@ -61,12 +89,28 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</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 style="display:grid;grid-template-columns:1fr 1fr;gap:20px">
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<h4 style="margin-bottom:12px;color:#e6a23c"><el-icon><Warning /></el-icon> 效期预警(90天内到期)</h4>
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
||||||
<el-table :data="expiryList" size="small" empty-text="暂无预警 ✨">
|
<h4 style="color:#e6a23c"><el-icon><Warning /></el-icon> 效期预警</h4>
|
||||||
<el-table-column prop="medicineName" label="药品" min-width="140" />
|
<span style="font-size:12px;color:#909399">90天内到期 · 共 {{ expiryList.length }} 条</span>
|
||||||
<el-table-column prop="batchNo" label="批号" width="110" />
|
</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="quantity" label="库存" width="70" align="center" />
|
||||||
<el-table-column prop="expiryDate" label="有效期至" width="120" align="center">
|
<el-table-column prop="expiryDate" label="有效期至" width="120" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
@@ -76,31 +120,36 @@ onMounted(async () => {
|
|||||||
</el-table>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
<div class="section-card">
|
<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 style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||||||
<div>
|
<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-if="recentInList.length">
|
||||||
<div v-for="item in recentInList" :key="item.id" class="record-item">
|
<div v-for="item in recentInList" :key="item.id" class="record-item">
|
||||||
<span style="font-size:13px;font-weight:500">{{ item.recordNo }}</span>
|
<span style="font-size:12px;font-weight:500">{{ item.recordNo }}</span>
|
||||||
<el-tag :type="item.status===1?'success':item.status===0?'warning':'info'" size="small" effect="plain">
|
<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>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-else description="暂无" :image-size="40" />
|
<el-empty v-else description="暂无" :image-size="36" />
|
||||||
</div>
|
</div>
|
||||||
<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-if="recentOutList.length">
|
||||||
<div v-for="item in recentOutList" :key="item.id" class="record-item">
|
<div v-for="item in recentOutList" :key="item.id" class="record-item">
|
||||||
<span style="font-size:13px;font-weight:500">{{ item.recordNo }}</span>
|
<span style="font-size:12px;font-weight:500">{{ item.recordNo }}</span>
|
||||||
<el-tag :type="item.status===1?'success':item.status===0?'warning':'info'" size="small" effect="plain">
|
<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>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-empty v-else description="暂无" :image-size="40" />
|
<el-empty v-else description="暂无" :image-size="36" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,7 +160,7 @@ onMounted(async () => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.record-item {
|
.record-item {
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
padding: 8px 0; border-bottom: 1px dashed #ebeef5;
|
padding: 6px 0; border-bottom: 1px dashed #ebeef5;
|
||||||
&:last-child { border-bottom: none; }
|
&:last-child { border-bottom: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user