完善统计功能并优化前端界面

后端:
- 扩展 StatsController,新增趋势分析(/trends)和今日待办(/today-todos)接口
- 更新 application-dev.yml 数据库配置(端口3306,允许公钥检索)
- 完善 pom.xml Maven 编译器插件和 Lombok 版本配置
- 添加 build-with-idea.sh 构建脚本

前端:
- 新增 Register.vue 注册页面
- 优化 Dashboard 仪表盘布局和数据统计展示
- 改进 MainLayout 侧边栏样式和品牌展示
- 更新 Login 登录页面样式
- 新增 theme.css 主题样式文件
- 扩展 API 接口(statsTrends、todayTodos)
- 更新路由和全局样式

文档:
- 添加功能检查报告和功能列表文档
This commit is contained in:
wangziqi
2026-02-11 16:11:31 +08:00
parent f9bfb8556b
commit 77eb648b38
16 changed files with 4487 additions and 176 deletions

View File

@@ -3,9 +3,13 @@ package com.gpf.pethospital.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gpf.pethospital.common.ApiResponse;
import com.gpf.pethospital.entity.Appointment;
import com.gpf.pethospital.entity.Order;
import com.gpf.pethospital.entity.Pet;
import com.gpf.pethospital.entity.User;
import com.gpf.pethospital.entity.Visit;
import com.gpf.pethospital.service.AppointmentService;
import com.gpf.pethospital.service.DrugService;
import com.gpf.pethospital.service.OrderService;
import com.gpf.pethospital.service.PetService;
import com.gpf.pethospital.service.UserService;
@@ -13,12 +17,19 @@ import com.gpf.pethospital.service.VisitService;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/admin/stats")
@@ -28,40 +39,208 @@ public class StatsController {
private final VisitService visitService;
private final PetService petService;
private final UserService userService;
private final DrugService drugService;
public StatsController(OrderService orderService,
AppointmentService appointmentService,
VisitService visitService,
PetService petService,
UserService userService) {
UserService userService,
DrugService drugService) {
this.orderService = orderService;
this.appointmentService = appointmentService;
this.visitService = visitService;
this.petService = petService;
this.userService = userService;
this.drugService = drugService;
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping
public ApiResponse<?> summary() {
Map<String, Object> data = new HashMap<>();
data.put("orders", orderService.count());
data.put("appointments", appointmentService.count());
data.put("visits", visitService.count());
data.put("pets", petService.count());
data.put("customers", userService.count(new LambdaQueryWrapper<User>().eq(User::getRole, "CUSTOMER")));
QueryWrapper<Order> wrapper = new QueryWrapper<>();
wrapper.select("SUM(amount) AS total");
List<Map<String, Object>> result = orderService.listMaps(wrapper);
BigDecimal total = BigDecimal.ZERO;
// 今日预约数
LocalDate today = LocalDate.now();
long todayAppointments = appointmentService.count(
new LambdaQueryWrapper<Appointment>()
.eq(Appointment::getAppointmentDate, today)
.ne(Appointment::getStatus, "CANCELLED")
);
data.put("appointments", todayAppointments);
// 今日待就诊数(预约状态为 CONFIRMED 的今日预约)
long pendingVisits = appointmentService.count(
new LambdaQueryWrapper<Appointment>()
.eq(Appointment::getAppointmentDate, today)
.eq(Appointment::getStatus, "CONFIRMED")
);
data.put("visits", pendingVisits);
// 药品库存总数
QueryWrapper<com.gpf.pethospital.entity.Drug> drugWrapper = new QueryWrapper<>();
drugWrapper.select("SUM(stock) AS totalStock");
List<Map<String, Object>> drugResult = drugService.listMaps(drugWrapper);
Long drugStock = 0L;
if (!drugResult.isEmpty() && drugResult.get(0) != null && drugResult.get(0).get("totalStock") != null) {
drugStock = Long.valueOf(drugResult.get(0).get("totalStock").toString());
}
data.put("drugs", drugStock);
// 今日收入
LocalDateTime todayStart = today.atStartOfDay();
LocalDateTime todayEnd = today.plusDays(1).atStartOfDay();
QueryWrapper<Order> orderWrapper = new QueryWrapper<>();
orderWrapper.select("SUM(amount) AS total");
orderWrapper.ge("create_time", todayStart);
orderWrapper.lt("create_time", todayEnd);
List<Map<String, Object>> result = orderService.listMaps(orderWrapper);
BigDecimal todayRevenue = BigDecimal.ZERO;
if (!result.isEmpty()) {
Map<String, Object> row = result.get(0);
if (row != null && row.get("total") != null) {
todayRevenue = new BigDecimal(row.get("total").toString());
}
}
data.put("revenue", todayRevenue);
// 保留原有统计数据
data.put("orders", orderService.count());
data.put("pets", petService.count());
data.put("customers", userService.count(new LambdaQueryWrapper<User>().eq(User::getRole, "CUSTOMER")));
QueryWrapper<Order> totalWrapper = new QueryWrapper<>();
totalWrapper.select("SUM(amount) AS total");
List<Map<String, Object>> totalResult = orderService.listMaps(totalWrapper);
BigDecimal total = BigDecimal.ZERO;
if (!totalResult.isEmpty()) {
Map<String, Object> row = totalResult.get(0);
if (row != null && row.get("total") != null) {
total = new BigDecimal(row.get("total").toString());
}
}
data.put("orderAmountTotal", total);
return ApiResponse.success(data);
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/trends")
public ApiResponse<?> trends(@RequestParam(defaultValue = "week") String period) {
Map<String, Object> data = new HashMap<>();
LocalDate now = LocalDate.now();
LocalDate startDate;
DateTimeFormatter formatter;
int days;
switch (period) {
case "month":
startDate = now.minusDays(30);
formatter = DateTimeFormatter.ofPattern("MM-dd");
days = 30;
break;
case "year":
startDate = now.minusMonths(12);
formatter = DateTimeFormatter.ofPattern("yyyy-MM");
days = 12;
break;
default: // week
startDate = now.minusDays(6);
formatter = DateTimeFormatter.ofPattern("MM-dd");
days = 7;
break;
}
List<String> labels = new ArrayList<>();
List<Integer> values = new ArrayList<>();
if ("year".equals(period)) {
// 按月统计
for (int i = 0; i < 12; i++) {
LocalDate monthStart = startDate.plusMonths(i);
LocalDateTime monthStartTime = monthStart.atStartOfDay();
LocalDateTime monthEndTime = monthStart.plusMonths(1).atStartOfDay();
long count = visitService.count(
new LambdaQueryWrapper<Visit>()
.ge(Visit::getCreateTime, monthStartTime)
.lt(Visit::getCreateTime, monthEndTime)
);
labels.add(monthStart.format(formatter));
values.add((int) count);
}
} else {
// 按天统计
for (int i = 0; i < days; i++) {
LocalDate date = startDate.plusDays(i);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = date.plusDays(1).atStartOfDay();
long count = visitService.count(
new LambdaQueryWrapper<Visit>()
.ge(Visit::getCreateTime, dayStart)
.lt(Visit::getCreateTime, dayEnd)
);
labels.add(date.format(formatter));
values.add((int) count);
}
}
data.put("labels", labels);
data.put("values", values);
data.put("total", values.stream().mapToInt(Integer::intValue).sum());
// 计算环比
if (values.size() >= 2) {
int current = values.get(values.size() - 1);
int previous = values.get(values.size() - 2);
double growthRate = previous > 0 ? ((double) (current - previous) / previous * 100) : 0;
data.put("growthRate", Math.round(growthRate * 10) / 10.0);
} else {
data.put("growthRate", 0);
}
// 平均日接诊
double avg = values.isEmpty() ? 0 : values.stream().mapToInt(Integer::intValue).average().orElse(0);
data.put("average", Math.round(avg * 10) / 10.0);
return ApiResponse.success(data);
}
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/today-todos")
public ApiResponse<?> todayTodos() {
LocalDate today = LocalDate.now();
// 查询今日待就诊的预约
List<Appointment> appointments = appointmentService.list(
new LambdaQueryWrapper<Appointment>()
.eq(Appointment::getAppointmentDate, today)
.eq(Appointment::getStatus, "CONFIRMED")
.orderByAsc(Appointment::getTimeSlot)
);
List<Map<String, Object>> todoList = appointments.stream().map(appointment -> {
Map<String, Object> todo = new HashMap<>();
// 获取客户信息
User customer = userService.getById(appointment.getCustomerId());
// 获取宠物信息
Pet pet = petService.getById(appointment.getPetId());
todo.put("id", appointment.getId());
todo.put("time", appointment.getTimeSlot());
todo.put("customer", customer != null ? customer.getUsername() : "未知客户");
todo.put("pet", pet != null ? pet.getName() + "(" + pet.getBreed() + ")" : "未知宠物");
todo.put("service", appointment.getDepartment() != null ? appointment.getDepartment() : "常规就诊");
todo.put("status", "待就诊");
todo.put("action", "接诊");
return todo;
}).collect(Collectors.toList());
return ApiResponse.success(todoList);
}
}

View File

@@ -1,5 +1,6 @@
server:
port: 8081
address: 0.0.0.0
servlet:
context-path: /api
@@ -9,7 +10,7 @@ spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3307/pet_hospital_dev?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8
url: jdbc:mysql://localhost:3306/pet_hospital_dev?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=GMT%2B8
username: root
password: qq5211314
hikari: