diff --git a/backend/src/main/java/com/kronecker/backend/controller/DashboardController.java b/backend/src/main/java/com/kronecker/backend/controller/DashboardController.java index ef38971..f079701 100644 --- a/backend/src/main/java/com/kronecker/backend/controller/DashboardController.java +++ b/backend/src/main/java/com/kronecker/backend/controller/DashboardController.java @@ -28,4 +28,9 @@ public class DashboardController { @RequestParam(defaultValue = "10") Integer size) { return Result.success(dashboardService.getExpiryWarning(new Page<>(page, size))); } + + @GetMapping("/chart-data") + public Result> chartData() { + return Result.success(dashboardService.getChartData()); + } } diff --git a/backend/src/main/java/com/kronecker/backend/service/DashboardService.java b/backend/src/main/java/com/kronecker/backend/service/DashboardService.java index 877a172..9d31a60 100644 --- a/backend/src/main/java/com/kronecker/backend/service/DashboardService.java +++ b/backend/src/main/java/com/kronecker/backend/service/DashboardService.java @@ -5,10 +5,13 @@ import com.kronecker.backend.entity.Stock; import com.kronecker.backend.entity.StockInRecord; import com.kronecker.backend.entity.StockOutRecord; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import java.util.List; +import java.util.Map; public interface DashboardService { - java.util.Map getStatistics(); + Map getStatistics(); Page getExpiryWarning(Page page); Page getRecentStockIn(Page page); Page getRecentStockOut(Page page); + Map getChartData(); } diff --git a/backend/src/main/java/com/kronecker/backend/service/impl/DashboardServiceImpl.java b/backend/src/main/java/com/kronecker/backend/service/impl/DashboardServiceImpl.java index dfa24c9..8ab8b9d 100644 --- a/backend/src/main/java/com/kronecker/backend/service/impl/DashboardServiceImpl.java +++ b/backend/src/main/java/com/kronecker/backend/service/impl/DashboardServiceImpl.java @@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.kronecker.backend.entity.*; 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.utils.RedisCacheService; import lombok.RequiredArgsConstructor; @@ -17,6 +19,7 @@ import java.util.*; public class DashboardServiceImpl implements DashboardService { private final MedicineMapper medicineMapper; + private final MedicineCategoryMapper medicineCategoryMapper; private final StockMapper stockMapper; private final StockInRecordMapper stockInRecordMapper; 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(), "")); }); } } + + @Override + public Map getChartData() { + Map data = new HashMap<>(); + + // 近6个月出入库金额统计 + List> 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() + .eq(StockInRecord::getStatus, 1) + .likeRight(StockInRecord::getInDate, month)) + .stream().mapToDouble(r -> r.getTotalAmount().doubleValue()).sum(); + Double outAmount = stockOutRecordMapper.selectList(new LambdaQueryWrapper() + .eq(StockOutRecord::getStatus, 1) + .likeRight(StockOutRecord::getOutDate, month)) + .stream().mapToDouble(r -> r.getTotalAmount().doubleValue()).sum(); + Map 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> categoryStats = new ArrayList<>(); + List categories = medicineCategoryMapper.selectList(null); + for (MedicineCategory cat : categories) { + int count = medicineMapper.selectCount(new LambdaQueryWrapper().eq(Medicine::getCategoryId, cat.getId())).intValue(); + if (count > 0) { + Map 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().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; + } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0f79bb9..7698c50 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,9 +10,11 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.2", "axios": "^1.18.1", + "echarts": "^6.1.0", "element-plus": "^2.14.2", "pinia": "^2.3.1", "vue": "^3.5.39", + "vue-echarts": "^8.0.1", "vue-router": "^4.6.4" }, "devDependencies": { @@ -1097,6 +1099,22 @@ "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": { "version": "2.14.2", "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": { "version": "4.6.4", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", @@ -2095,6 +2123,21 @@ "peerDependencies": { "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" } } } diff --git a/frontend/package.json b/frontend/package.json index 8356e35..a052f3f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,9 +11,11 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.2", "axios": "^1.18.1", + "echarts": "^6.1.0", "element-plus": "^2.14.2", "pinia": "^2.3.1", "vue": "^3.5.39", + "vue-echarts": "^8.0.1", "vue-router": "^4.6.4" }, "devDependencies": { diff --git a/frontend/src/api/dashboard.js b/frontend/src/api/dashboard.js index 5974456..004fbac 100644 --- a/frontend/src/api/dashboard.js +++ b/frontend/src/api/dashboard.js @@ -7,3 +7,7 @@ export function getStatistics() { export function getExpiryWarning() { return request.get('/dashboard/expiry-warning') } + +export function getChartData() { + return request.get('/dashboard/chart-data') +} diff --git a/frontend/src/main.js b/frontend/src/main.js index e4eba25..b9182b7 100644 --- a/frontend/src/main.js +++ b/frontend/src/main.js @@ -3,6 +3,8 @@ import { createPinia } from 'pinia' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import ECharts from 'vue-echarts' +import 'echarts' import App from './App.vue' import router from './router' import permission from './directives/permission' @@ -15,6 +17,7 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } +app.component('v-chart', ECharts) app.use(createPinia()) app.use(router) app.use(ElementPlus, { size: 'default' }) diff --git a/frontend/src/views/dashboard/DashboardView.vue b/frontend/src/views/dashboard/DashboardView.vue index 98551da..3d5d927 100644 --- a/frontend/src/views/dashboard/DashboardView.vue +++ b/frontend/src/views/dashboard/DashboardView.vue @@ -1,6 +1,6 @@ @@ -45,10 +71,12 @@ onMounted(async () => { +
@@ -61,12 +89,28 @@ onMounted(async () => {
+ +
+
+

近6个月出入库金额

+ +
+
+

药品分类占比

+ +
+
+ +
-

效期预警(90天内到期)

- - - +
+

效期预警

+ 90天内到期 · 共 {{ expiryList.length }} 条 +
+ + +