页面样式,svg

This commit is contained in:
2025-11-14 15:29:02 +08:00
parent 46003a646e
commit 6be3cc6abd
27 changed files with 585 additions and 180 deletions

View File

@@ -169,7 +169,7 @@ INSERT INTO `tb_sys_menu` VALUES
('8002', 'menu_admin_crontab_log', '执行日志', 'menu_admin_crontab_manage', '/admin/manage/crontab/log', 'admin/manage/crontab/LogManagementView', NULL, 2, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
('8003', 'menu_admin_news_crawler', '新闻爬虫配置', 'menu_admin_crontab_manage', '/admin/manage/crontab/news-crawler', 'admin/manage/crontab/NewsCrawlerView', NULL, 3, 0, 'SidebarLayout', '1', NULL, '2025-10-27 17:26:06', '2025-10-29 11:48:39', NULL, 0),
-- 消息通知模块菜单 (9000-9999)
('9001', 'menu_admin_message_manage', '消息管理', NULL, '/admin/manage/message', 'admin/manage/message/MessageManageView', 'admin/message.svg', 9, 0, 'SidebarLayout', '1', NULL, '2025-11-13 10:00:00', '2025-11-13 10:00:00', NULL, 0),
('9001', 'menu_admin_message_manage', '消息管理', NULL, '/admin/manage/message', 'admin/manage/message/MessageManageView', 'admin/notice.svg', 9, 0, 'SidebarLayout', '1', NULL, '2025-11-13 10:00:00', '2025-11-13 10:00:00', NULL, 0),
-- 用户端消息中心菜单 (650-699)
('650', 'menu_user_message_center', '消息中心', NULL, '/user/message', 'user/message/MyMessageListView', NULL, 7, 1, 'NavigationLayout', '1', NULL, '2025-11-13 10:00:00', '2025-11-13 10:00:00', NULL, 0),
('651', 'menu_user_message_detail', '消息详情', 'menu_user_message_center', '/user/message/detail/:messageID', 'user/message/MyMessageDetailView', NULL, 1, 3, 'NavigationLayout', '1', NULL, '2025-11-13 10:00:00', '2025-11-13 10:00:00', NULL, 0);

View File

@@ -25,12 +25,12 @@ public interface LoginService {
/**
* @description 退出登录
* @param loginDomain 登录域
* @param request HttpServletRequest
* @return ResultDomain<String> 返回结果
* @author yslg
* @since 2025-09-28
*/
ResultDomain<String> logout(LoginDomain loginDomain);
ResultDomain<String> logout(HttpServletRequest request);
}

View File

@@ -70,8 +70,8 @@ public class AuthController {
* @since 2025-09-28
*/
@PostMapping("/logout")
public ResultDomain<String> logout(@RequestBody LoginDomain loginDomain) {
return loginService.logout(loginDomain);
public ResultDomain<String> logout(HttpServletRequest request) {
return loginService.logout(request);
}
/**

View File

@@ -72,7 +72,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
}
// 【优化】从Redis缓存中获取LoginDomain避免每次都查数据库
String redisKey = REDIS_LOGIN_PREFIX + userId;
// 多设备登录场景下以token为维度存储和获取会话信息
String redisKey = REDIS_LOGIN_PREFIX + token;
LoginDomain loginDomain = (LoginDomain) redisService.get(redisKey);
if (loginDomain != null && loginDomain.getUser() != null) {

View File

@@ -137,10 +137,11 @@ public class LoginServiceImpl implements LoginService {
loginDomain.setToken(jwtTokenUtil.generateToken(loginDomain));
// 将LoginDomain存储到Redis中根据rememberMe设置不同的过期时间
String redisKey = "login:token:" + user.getID();
String token = loginDomain.getToken();
String redisKey = "login:token:" + token;
long expireTime = loginParam.isRememberMe()
? 7 * 24 * 60 * 60 // rememberMe: 7天
: 24 * 60 * 60; // 不rememberMe: 1天
? 7 * 24 * 60 * 60
: 24 * 60 * 60;
redisService.set(redisKey, loginDomain, expireTime, TimeUnit.SECONDS);
// 登录成功后清除失败次数并记录成功日志
@@ -160,14 +161,33 @@ public class LoginServiceImpl implements LoginService {
}
@Override
public ResultDomain<String> logout(LoginDomain loginDomain) {
public ResultDomain<String> logout(HttpServletRequest request) {
ResultDomain<String> result = new ResultDomain<>();
try {
// TODO: 将token加入黑名单或从Redis中删除
// 这里可以实现token黑名单机制
// 从请求头中获取 Bearer Token
String bearerToken = request.getHeader("Authorization");
if (!StringUtils.hasText(bearerToken) || !bearerToken.startsWith("Bearer ")) {
result.fail("未提供有效的认证信息");
return result;
}
result.success("退出登录成功", (String)null);
String token = bearerToken.substring(7);
// 解析 token 获取 userId作为基本校验
String userId = jwtTokenUtil.getUserIdFromToken(token);
if (!StringUtils.hasText(userId)) {
result.fail("无效的令牌");
return result;
}
// 删除当前token对应的 Redis 登录信息(多设备登录场景下不影响其他设备)
String redisKey = "login:token:" + token;
redisService.delete(redisKey);
// TODO: 如有需要,可在此处增加 token 黑名单机制
result.success("退出登录成功", (String) null);
} catch (Exception e) {
result.fail("退出登录失败: " + e.getMessage());
}

View File

@@ -68,16 +68,16 @@ public class HttpLoginArgumentResolver implements HandlerMethodArgumentResolver
try {
// 验证token格式和有效性
if (!tokenParser.validateToken(token, tokenParser.getUserIdFromToken(token))) {
String userId = tokenParser.getUserIdFromToken(token);
if (!tokenParser.validateToken(token, userId)) {
if (httpLogin.required()) {
throw new IllegalArgumentException(httpLogin.message());
}
return null;
}
// 从Redis中获取LoginDomain
String userId = tokenParser.getUserIdFromToken(token);
String redisKey = REDIS_LOGIN_PREFIX + userId;
// 从Redis中获取LoginDomain按token维度存储和获取会话信息
String redisKey = REDIS_LOGIN_PREFIX + token;
LoginDomain loginDomain = (LoginDomain) redisService.get(redisKey);
if (loginDomain == null) {

View File

@@ -57,7 +57,7 @@
<spring-data-redis.version>3.5.4</spring-data-redis.version>
<!-- excel -->
<poi.version>5.2.3</poi.version>
<poi.version>5.4.1</poi.version>
<lombok.version>1.18.40</lombok.version>
</properties>

View File

@@ -2,9 +2,15 @@ package org.xyzh.system.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
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 org.xyzh.common.core.domain.ResultDomain;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@@ -24,9 +30,25 @@ public class SystemOverviewController {
*/
@GetMapping("/statistics")
public ResultDomain<Map<String, Object>> getSystemStatistics() {
// TODO: 实现获取系统总览数据统计
// 统计系统总用户数、资源总数、今日访问量
return null;
// TODO: 后续接入真实统计数据(用户表、资源表、访问统计表等)
ResultDomain<Map<String, Object>> result = new ResultDomain<>();
Map<String, Object> data = new HashMap<>();
// 顶部统计卡片:总用户数、总资源数、今日访问量、活跃用户
data.put("totalUsers", 1234);
data.put("totalUsersChange", "+12%");
data.put("totalResources", 5678);
data.put("totalResourcesChange", "+8%");
data.put("todayVisits", 892);
data.put("todayVisitsChange", "+15%");
data.put("activeUsers", 456);
data.put("activeUsersChange", "+5%");
result.success("获取系统总览统计成功", data);
return result;
}
/**
@@ -34,9 +56,23 @@ public class SystemOverviewController {
*/
@GetMapping("/active-users")
public ResultDomain<Map<String, Object>> getActiveUsersChart(
@RequestParam(required = false) Integer days) {
// TODO: 实现获取活跃用户图表数据折线图展示7/30天活跃用户数)
return null;
@RequestParam(required = true, name = "start") String start, @RequestParam(required = true, name = "end") String end) {
// TODO: 后续根据days参数7/30天查询真实活跃用户统计
ResultDomain<Map<String, Object>> result = new ResultDomain<>();
Map<String, Object> data = new HashMap<>();
// 默认展示7天
LocalDate startDate = LocalDate.parse(start);
LocalDate endDate = LocalDate.parse(end);
long days = startDate.until(endDate).getDays();
// X轴周一 ~ 周日(示例)
data.put("labels", List.of("周一", "周二", "周三", "周四", "周五", "周六", "周日"));
// Y轴数据活跃用户数量示例数据
data.put("values", List.of(120, 200, 150, 80, 70, 110, 130));
result.success("获取活跃用户图表数据成功", data);
return result;
}
/**
@@ -44,17 +80,22 @@ public class SystemOverviewController {
*/
@GetMapping("/resource-category-stats")
public ResultDomain<Map<String, Object>> getResourceCategoryStats() {
// TODO: 实现获取资源分类统计(饼图,展示各类型资源占比)
return null;
}
// TODO: 后续从资源表统计各类型资源数量
ResultDomain<Map<String, Object>> result = new ResultDomain<>();
Map<String, Object> data = new HashMap<>();
/**
* 获取用户活跃度数据
*/
@GetMapping("/user-activity")
public ResultDomain<Map<String, Object>> getUserActivityData() {
// TODO: 实现获取用户活跃度数据
return null;
// 饼图数据:名称 + 数量
List<Map<String, Object>> categories = List.of(
createCategory("文章", 1048),
createCategory("视频", 735),
createCategory("音频", 580),
createCategory("课程", 484),
createCategory("其他", 300)
);
data.put("items", categories);
result.success("获取资源分类统计成功", data);
return result;
}
/**
@@ -62,8 +103,17 @@ public class SystemOverviewController {
*/
@GetMapping("/today-visits")
public ResultDomain<Map<String, Object>> getTodayVisits() {
// TODO: 实现获取今日访问量统计
return null;
// TODO: 后续接入访问日志/统计表,计算今日指标
ResultDomain<Map<String, Object>> result = new ResultDomain<>();
Map<String, Object> data = new HashMap<>();
data.put("uv", 892);
data.put("pv", 3456);
data.put("avgVisitDuration", "5分32秒");
data.put("bounceRate", "35.6%");
result.success("获取今日访问量统计成功", data);
return result;
}
/**
@@ -71,7 +121,21 @@ public class SystemOverviewController {
*/
@GetMapping("/system-status")
public ResultDomain<Map<String, Object>> getSystemStatus() {
// TODO: 实现获取系统运行状态
return null;
// TODO: 后续接入真实系统运行状态CPU、内存、服务可用性等
ResultDomain<Map<String, Object>> result = new ResultDomain<>();
Map<String, Object> data = new HashMap<>();
data.put("status", "UP");
data.put("message", "系统运行正常");
result.success("获取系统运行状态成功", data);
return result;
}
private Map<String, Object> createCategory(String name, int value) {
Map<String, Object> item = new HashMap<>();
item.put("name", name);
item.put("value", value);
return item;
}
}

View File

@@ -59,8 +59,8 @@ public class LoginUtil {
return null;
}
// 从Redis获取LoginDomain
String redisKey = REDIS_LOGIN_PREFIX + userId;
// 从Redis获取LoginDomain按token维度存储和获取会话信息
String redisKey = REDIS_LOGIN_PREFIX + token;
LoginDomain loginDomain = (LoginDomain) redisService.get(redisKey);
if (loginDomain != null) {

View File

@@ -0,0 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6667 14.1665H18.3334V15.8332H1.66669V14.1665H3.33335V8.33317C3.33335 4.65127 6.31812 1.6665 10 1.6665C13.6819 1.6665 16.6667 4.65127 16.6667 8.33317V14.1665ZM15 14.1665V8.33317C15 5.57175 12.7614 3.33317 10 3.33317C7.2386 3.33317 5.00002 5.57175 5.00002 8.33317V14.1665H15ZM7.50002 17.4998H12.5V19.1665H7.50002V17.4998Z" fill="#141F38"/>
</svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@@ -0,0 +1,60 @@
/**
* @description 系统总览相关API
*/
import { api } from '@/apis/index';
import type { ResultDomain } from '@/types';
export interface SystemStatisticsDTO {
totalUsers: number;
totalUsersChange: string;
totalResources: number;
totalResourcesChange: string;
todayVisits: number;
todayVisitsChange: string;
activeUsers: number;
activeUsersChange: string;
}
export interface ActiveUsersChartDTO {
labels: string[];
values: number[];
}
export interface ResourceCategoryItemDTO {
name: string;
value: number;
}
export interface ResourceCategoryStatsDTO {
items: ResourceCategoryItemDTO[];
}
export interface TodayVisitsDTO {
uv: number;
pv: number;
avgVisitDuration: string;
bounceRate: string;
}
export const systemOverviewApi = {
async getStatistics(): Promise<ResultDomain<SystemStatisticsDTO>> {
const response = await api.get<SystemStatisticsDTO>('/system/overview/statistics');
return response.data;
},
async getActiveUsersChart(start: string, end: string): Promise<ResultDomain<ActiveUsersChartDTO>> {
const response = await api.get<ActiveUsersChartDTO>('/system/overview/active-users', { start, end });
return response.data;
},
async getResourceCategoryStats(): Promise<ResultDomain<ResourceCategoryStatsDTO>> {
const response = await api.get<ResourceCategoryStatsDTO>('/system/overview/resource-category-stats');
return response.data;
},
async getTodayVisits(): Promise<ResultDomain<TodayVisitsDTO>> {
const response = await api.get<TodayVisitsDTO>('/system/overview/today-visits');
return response.data;
}
};

View File

@@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.69434 14.7235C10.274 14.0085 14 11.8 14 6.87379V3.89768C14 3.05925 14 2.6394 13.8367 2.31885C13.6929 2.0366 13.4628 1.8073 13.1805 1.66349C12.8597 1.5 12.4402 1.5 11.6001 1.5H4.40015C3.56007 1.5 3.13972 1.5 2.81885 1.66349C2.5366 1.8073 2.3073 2.0366 2.16349 2.31885C2 2.63972 2 3.06007 2 3.90015V6.87378C2 11.8 5.72585 14.0086 7.30546 14.7235C7.47292 14.7993 7.55707 14.8372 7.7466 14.8697C7.86616 14.8902 8.1346 14.8902 8.25415 14.8697C8.44294 14.8373 8.52621 14.7996 8.69234 14.7244L8.69434 14.7235Z" stroke="#0A0A0A" stroke-width="1.6875" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="8" cy="7" r="1.25" stroke="#0A0A0A" stroke-width="1.5"/>
<path d="M8 8.5V10" stroke="#0A0A0A" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 867 B

View File

@@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.00004 11.6668C6.67921 11.6668 6.40465 11.5527 6.17638 11.3244C5.9481 11.0961 5.83376 10.8214 5.83338 10.5002C5.83299 10.1789 5.94732 9.90439 6.17638 9.6765C6.40543 9.44861 6.67999 9.33428 7.00004 9.3335C7.3201 9.33272 7.59485 9.44706 7.82429 9.6765C8.05374 9.90594 8.16788 10.1805 8.16671 10.5002C8.16554 10.8198 8.0514 11.0946 7.82429 11.3244C7.59718 11.5543 7.32243 11.6684 7.00004 11.6668ZM7.00004 8.16683C6.67921 8.16683 6.40465 8.05269 6.17638 7.82442C5.9481 7.59614 5.83376 7.32139 5.83338 7.00017C5.83299 6.67894 5.94732 6.40439 6.17638 6.1765C6.40543 5.94861 6.67999 5.83428 7.00004 5.8335C7.3201 5.83272 7.59485 5.94706 7.82429 6.1765C8.05374 6.40594 8.16788 6.6805 8.16671 7.00017C8.16554 7.31983 8.0514 7.59458 7.82429 7.82442C7.59718 8.05425 7.32243 8.16839 7.00004 8.16683ZM7.00004 4.66683C6.67921 4.66683 6.40465 4.55269 6.17638 4.32442C5.9481 4.09614 5.83376 3.82139 5.83338 3.50017C5.83299 3.17894 5.94732 2.90439 6.17638 2.6765C6.40543 2.44861 6.67999 2.33428 7.00004 2.3335C7.3201 2.33272 7.59485 2.44706 7.82429 2.6765C8.05374 2.90594 8.16788 3.1805 8.16671 3.50017C8.16554 3.81983 8.0514 4.09458 7.82429 4.32442C7.59718 4.55425 7.32243 4.66839 7.00004 4.66683Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,17 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1357_6679)">
<mask id="mask0_1357_6679" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="16" height="16">
<path d="M16 0H0V16H16V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_1357_6679)">
<path d="M8 12V9.5" stroke="#0A0A0A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.7132 1.87974L2.09318 5.57974C1.57318 5.99307 1.23985 6.86637 1.35318 7.5197L2.23985 12.8264C2.39985 13.773 3.30652 14.5397 4.26652 14.5397H11.7332C12.6865 14.5397 13.5999 13.7664 13.7599 12.8264L14.6465 7.5197C14.7532 6.86637 14.4199 5.99307 13.9065 5.57974L9.28653 1.8864C8.5732 1.31307 7.41987 1.31307 6.7132 1.87974Z" stroke="#0A0A0A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
<defs>
<clipPath id="clip0_1357_6679">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1010 B

View File

@@ -0,0 +1,4 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 20C4.47715 20 0 15.5228 0 10C0 4.47715 4.47715 0 10 0C13.2713 0 16.1757 1.57078 18.0002 3.99923L15.2909 3.99931C13.8807 2.75499 12.0285 2 10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C12.029 18 13.8816 17.2446 15.2919 15.9998H18.0009C16.1765 18.4288 13.2717 20 10 20ZM17 14V11H9V9H17V6L22 10L17 14Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

View File

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M20 17H22V19H2V17H4V10C4 5.58172 7.58172 2 12 2C16.4183 2 20 5.58172 20 10V17ZM9 21H15V23H9V21Z" fill="black"/>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.6667 14.1665H18.3334V15.8332H1.66669V14.1665H3.33335V8.33317C3.33335 4.65127 6.31812 1.6665 10 1.6665C13.6819 1.6665 16.6667 4.65127 16.6667 8.33317V14.1665ZM15 14.1665V8.33317C15 5.57175 12.7614 3.33317 10 3.33317C7.2386 3.33317 5.00002 5.57175 5.00002 8.33317V14.1665H15ZM7.50002 17.4998H12.5V19.1665H7.50002V17.4998Z" fill="#141F38"/>
</svg>

Before

Width:  |  Height:  |  Size: 225 B

After

Width:  |  Height:  |  Size: 455 B

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.44734 2.75711C6.48407 2.37067 6.66356 2.01181 6.95074 1.75063C7.23792 1.48945 7.61216 1.34473 8.00034 1.34473C8.38852 1.34473 8.76276 1.48945 9.04994 1.75063C9.33712 2.01181 9.51661 2.37067 9.55334 2.75711C9.57542 3.00675 9.65731 3.24739 9.79209 3.45867C9.92688 3.66995 10.1106 3.84565 10.3276 3.97089C10.5447 4.09614 10.7888 4.16724 11.0391 4.17819C11.2895 4.18913 11.5388 4.1396 11.766 4.03378C12.1187 3.87363 12.5185 3.85045 12.8873 3.96877C13.2562 4.08708 13.5679 4.33841 13.7617 4.67385C13.9555 5.00928 14.0175 5.40482 13.9358 5.78349C13.854 6.16215 13.6343 6.49685 13.3193 6.72244C13.1143 6.86634 12.9469 7.05751 12.8313 7.27979C12.7157 7.50207 12.6554 7.74891 12.6554 7.99944C12.6554 8.24997 12.7157 8.49681 12.8313 8.71909C12.9469 8.94137 13.1143 9.13254 13.3193 9.27644C13.6343 9.50203 13.854 9.83673 13.9358 10.2154C14.0175 10.5941 13.9555 10.9896 13.7617 11.325C13.5679 11.6605 13.2562 11.9118 12.8873 12.0301C12.5185 12.1484 12.1187 12.1253 11.766 11.9651C11.5388 11.8593 11.2895 11.8097 11.0391 11.8207C10.7888 11.8316 10.5447 11.9027 10.3276 12.028C10.1106 12.1532 9.92688 12.3289 9.79209 12.5402C9.65731 12.7515 9.57542 12.9921 9.55334 13.2418C9.51661 13.6282 9.33712 13.9871 9.04994 14.2483C8.76276 14.5094 8.38852 14.6542 8.00034 14.6542C7.61216 14.6542 7.23792 14.5094 6.95074 14.2483C6.66356 13.9871 6.48407 13.6282 6.44734 13.2418C6.4253 12.992 6.3434 12.7513 6.20858 12.54C6.07376 12.3286 5.88999 12.1528 5.67283 12.0276C5.45567 11.9023 5.21152 11.8313 4.96106 11.8204C4.7106 11.8095 4.46121 11.8591 4.234 11.9651C3.88126 12.1253 3.48156 12.1484 3.11267 12.0301C2.74379 11.9118 2.43212 11.6605 2.23833 11.325C2.04453 10.9896 1.98248 10.5941 2.06424 10.2154C2.14601 9.83673 2.36574 9.50203 2.68067 9.27644C2.88575 9.13254 3.05316 8.94137 3.16873 8.71909C3.2843 8.49681 3.34464 8.24997 3.34464 7.99944C3.34464 7.74891 3.2843 7.50207 3.16873 7.27979C3.05316 7.05751 2.88575 6.86634 2.68067 6.72244C2.36618 6.49674 2.14684 6.16217 2.06527 5.78376C1.98371 5.40535 2.04574 5.01014 2.23933 4.67492C2.43291 4.3397 2.74421 4.08843 3.11273 3.96994C3.48125 3.85144 3.88066 3.8742 4.23334 4.03378C4.46051 4.1396 4.70983 4.18913 4.96021 4.17819C5.21058 4.16724 5.45463 4.09614 5.6717 3.97089C5.88877 3.84565 6.07247 3.66995 6.20725 3.45867C6.34203 3.24739 6.42393 3.00675 6.446 2.75711" stroke="#0A0A0A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z" stroke="#0A0A0A" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.3333 14.6665H12V13.3332C12 12.2286 11.1046 11.3332 9.99996 11.3332H5.99996C4.89539 11.3332 3.99996 12.2286 3.99996 13.3332V14.6665H2.66663V13.3332C2.66663 11.4922 4.15901 9.99984 5.99996 9.99984H9.99996C11.8409 9.99984 13.3333 11.4922 13.3333 13.3332V14.6665ZM7.99996 8.6665C5.79082 8.6665 3.99996 6.87564 3.99996 4.6665C3.99996 2.45736 5.79082 0.666504 7.99996 0.666504C10.2091 0.666504 12 2.45736 12 4.6665C12 6.87564 10.2091 8.6665 7.99996 8.6665ZM7.99996 7.33317C9.47269 7.33317 10.6666 6.13926 10.6666 4.6665C10.6666 3.19374 9.47269 1.99984 7.99996 1.99984C6.5272 1.99984 5.33329 3.19374 5.33329 4.6665C5.33329 6.13926 6.5272 7.33317 7.99996 7.33317Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 794 B

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,9 +1,9 @@
<template>
<div class="change-home" v-if="isAdmin">
<el-button type="primary" @click="changeHome">
<div class="change-home-item" @click="changeHome">
<span v-if="home">前往用户页</span>
<span v-else>前往管理页</span>
</el-button>
</div>
</div>
</template>
<script setup lang="ts">
@@ -32,4 +32,5 @@ onMounted(() => {
home.value = router.currentRoute.value.path.startsWith('/admin');
});
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
</style>

View File

@@ -48,7 +48,6 @@
<div class="nav-right">
<!-- 搜索框 -->
<Search @search="handleSearch" />
<ChangeHome />
<Notice />
<UserDropdown :user="userInfo" @logout="handleLogout" />
</div>
@@ -63,7 +62,7 @@ import { useStore } from 'vuex';
import type { SysMenu } from '@/types';
import { MenuType } from '@/types/enums';
// @ts-ignore - Vue 3.5 组件导入兼容性
import {UserDropdown, Search, Notice, ChangeHome} from '@/components/base';
import {UserDropdown, Search, Notice} from '@/components/base';
const router = useRouter();
const route = useRoute();
const store = useStore();

View File

@@ -60,13 +60,20 @@
class="dropdown-item"
@click="goToMenu(menu)"
>
<i class="item-icon">{{ getMenuIcon(menu) }}</i>
<img class="item-icon" :src="getMenuIcon(menu)" alt="菜单图标">
<span>{{ menu.name }}</span>
</div>
<div class="dropdown-divider" v-if="userMenus.length > 0"></div>
<div class="dropdown-item danger" @click="handleLogout">
<i class="item-icon icon-logout"></i>
<span>退出登录</span>
<div class="dropdown-footer">
<div class="dropdown-item info">
<img class="item-icon icon-logout" src="@/assets/imgs/admin-home.svg" alt="切换页面">
<ChangeHome />
</div>
<div class="dropdown-item danger" @click="handleLogout">
<img class="item-icon icon-logout" src="@/assets/imgs/logout.svg" alt="退出登录">
<span>退出登录</span>
</div>
</div>
</template>
</div>
@@ -79,7 +86,9 @@ import { ref, computed } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';
import type { UserVO, SysMenu } from '@/types';
import { ChangeHome } from '@/components/base';
import userIcon from '@/assets/imgs/user.svg';
import settingsIcon from '@/assets/imgs/settings.svg';
// Props
interface Props {
user?: UserVO | null;
@@ -213,11 +222,10 @@ function goToMenu(menu: SysMenu) {
function getMenuIcon(menu: SysMenu) {
// 根据菜单ID返回对应图标
if (menu.menuID === 'menu_user_center') {
return '👤';
return userIcon;
} else if (menu.menuID === 'menu_profile') {
return '⚙️';
return settingsIcon;
}
return '📋';
}
function handleLogout() {
@@ -368,6 +376,14 @@ const vClickOutside = {
background-color: #f5f5f5;
}
&.info {
color: #1890ff;
&:hover {
background-color: #e6f7ff;
}
}
&.danger {
color: #ff4d4f;
@@ -379,9 +395,12 @@ const vClickOutside = {
.item-icon {
width: 16px;
height: 16px;
margin-right: 12px;
text-align: center;
font-size: 14px;
flex-shrink: 0;
object-fit: contain;
}
.dropdown-divider {
@@ -390,6 +409,13 @@ const vClickOutside = {
margin: 4px 0;
}
.dropdown-footer {
display: flex;
flex-direction: column;
// padding: 12px 16px;
text-align: center;
}
/* 图标字体类 */
.icon-login::before { content: "🔑"; }
.icon-register::before { content: "📝"; }

View File

@@ -4,29 +4,50 @@
<div class="layout-content">
<!-- 侧边栏和内容 -->
<div class="content-wrapper" v-if="hasSidebarMenus">
<!-- 侧边栏 -->
<aside class="sidebar">
<!-- Logo区域 -->
<div class="sidebar-header">
<div class="logo-container">
<img src="@/assets/imgs/logo-icon.svg" alt="Logo" class="logo-icon" />
<div class="logo-text">
<div class="logo-title">管理后台</div>
<div class="logo-subtitle">红色思政平台</div>
<!-- 侧边栏 -->
<aside class="sidebar">
<!-- Logo区域 -->
<div class="sidebar-header">
<div class="logo-container">
<img src="@/assets/imgs/logo-icon.svg" alt="Logo" class="logo-icon" />
<div class="logo-text">
<div class="logo-title">管理后台</div>
<div class="logo-subtitle">红色思政平台</div>
</div>
</div>
</div>
</div>
<!-- 导航菜单 -->
<nav class="sidebar-nav">
<ChangeHome />
<MenuSidebar
:menus="sidebarMenus"
:collapsed="false"
@menu-click="handleMenuClick"
/>
</nav>
</aside>
<!-- 导航菜单 -->
<nav class="sidebar-nav">
<MenuSidebar :menus="sidebarMenus" :collapsed="false" @menu-click="handleMenuClick" />
</nav>
<div class="sidebar-footer">
<div class="sidebar-footer-contain">
<!-- 用户头像+名称 -->
<div class="user-info">
<img :src="getUserAvatar()" alt="用户头像" class="user-avatar" />
<span>{{ user?.username }}</span>
</div>
<div class="dropdown-item" @click="toggleDropdown">
<img src="@/assets/imgs/else.svg" alt="其他" class="dropdown-icon" />
</div>
</div>
<transition name="dropdown-up">
<div v-if="showDropdown" class="dropdown-menu" @mouseleave="showDropdown = false">
<div class="dropdown-item">
<img src="@/assets/imgs/home.svg" alt="用户界面" class="dropdown-icon" />
<ChangeHome />
</div>
<div class="dropdown-item" @click="handleLogout">
<img src="@/assets/imgs/logout.svg" alt="退出登录" class="dropdown-icon" />
<span>退出登录</span>
</div>
</div>
</transition>
</div>
</aside>
<!-- 页面内容 -->
<main class="main-content">
@@ -45,17 +66,23 @@
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useStore } from 'vuex';
import type { SysMenu } from '@/types';
import { MenuType } from '@/types/enums';
import { MenuSidebar, ChangeHome } from '@/components';
import { FILE_DOWNLOAD_URL } from '@/config';
import defaultAvatar from '@/assets/imgs/default-avatar.png';
const route = useRoute();
const router = useRouter();
const store = useStore();
const user = computed(() => store.getters['auth/user']);
const showDropdown = ref(false);
// 获取所有菜单
const allMenus = computed(() => store.getters['auth/menuTree']);
@@ -78,6 +105,18 @@ function handleMenuClick(menu: SysMenu) {
router.push(menu.url);
}
}
function getUserAvatar() {
return user.value?.avatar ? `${FILE_DOWNLOAD_URL}/${user.value?.avatar}` : defaultAvatar;
}
function handleLogout() {
store.dispatch('auth/logout');
}
function toggleDropdown() {
showDropdown.value = !showDropdown.value;
}
</script>
<style lang="scss" scoped>
@@ -116,7 +155,7 @@ function handleMenuClick(menu: SysMenu) {
.sidebar-header {
padding: 24px 24px 1px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
height: 97px;
height: 100px;
}
.logo-container {
@@ -176,6 +215,114 @@ function handleMenuClick(menu: SysMenu) {
}
}
.sidebar-footer {
height: 50px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.sidebar-footer-contain {
display: flex;
align-items: center;
justify-content: space-between;
width: 80%;
height: 80%;
border-radius: 8px;
background-color: #F6F6F6;
.user-info {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #1f2933;
height: 100%;
}
.user-avatar {
/* width: 20px; */
height: 80%;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
.dropdown-item {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
cursor: pointer;
border-radius: 6px;
&:hover {
background-color: #f5f5f5;
}
.dropdown-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
object-fit: contain;
}
}
}
}
.dropdown-menu {
position: absolute;
bottom: 52px;
left: 50%;
transform: translateX(-50%);
background: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
border-radius: 6px;
padding: 8px 0;
min-width: 180px;
z-index: 10;
.dropdown-item {
padding: 8px 16px;
display: flex;
align-items: center;
cursor: pointer;
span,
.change-home-text {
font-size: 14px;
}
.dropdown-icon {
width: 16px;
height: 16px;
margin-right: 8px;
flex-shrink: 0;
object-fit: contain;
}
&:hover {
background-color: #f5f5f5;
}
}
}
.dropdown-up-enter-active,
.dropdown-up-leave-active {
transition: all 0.15s ease-out;
}
.dropdown-up-enter-from,
.dropdown-up-leave-to {
opacity: 0;
transform: translate(-50%, 6px);
}
.dropdown-up-enter-to,
.dropdown-up-leave-from {
opacity: 1;
transform: translate(-50%, 0);
}
.main-content {
flex: 1;
background: #F9FAFB;
@@ -216,6 +363,7 @@ function handleMenuClick(menu: SysMenu) {
padding: 20px;
box-sizing: border-box;
widows: 100vw;
// 美化滚动条
&::-webkit-scrollbar {
width: 8px;
@@ -236,4 +384,3 @@ function handleMenuClick(menu: SysMenu) {
}
}
</style>

View File

@@ -6,7 +6,7 @@ import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
export const routes: Array<RouteRecordRaw> = [
{
path: "/",
redirect: "/home",
redirect: "/login",
},
{
path: "/login",

View File

@@ -15,7 +15,6 @@ const WHITE_LIST = [
'/login',
'/register',
'/forgot-password',
'/home',
'/403',
'/404', // 404页面允许访问但未登录时不会被路由到这里
'/500'

View File

@@ -6,7 +6,7 @@
<h3 class="task-title">{{ task.learningTask.name }}</h3>
<div class="task-meta">
<!-- <div class="meta-item">
<img class="icon" src="@/assets/imgs/usermange.svg" alt="部门" />
<img class="icon" src="@/assets/imgs/usermanage.svg" alt="部门" />
<span class="meta-text">{{ getDeptName }}</span>
</div> -->
<div class="meta-item">

View File

@@ -68,6 +68,7 @@
import { ref, onMounted, onUnmounted } from 'vue';
import { ElRow, ElCol, ElCard, ElDatePicker } from 'element-plus';
import * as echarts from 'echarts';
import { systemOverviewApi } from '@/apis/system/overview';
const dateRange = ref<[Date, Date] | null>(null);
const activityChart = ref<HTMLElement | null>(null);
@@ -75,51 +76,20 @@ const resourcePieChart = ref<HTMLElement | null>(null);
let activityChartInstance: echarts.ECharts | null = null;
let pieChartInstance: echarts.ECharts | null = null;
const statistics = ref([
{
icon: '👥',
label: '总用户数',
value: '1,234',
change: '+12%',
trend: 'up',
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
},
{
icon: '📚',
label: '总资源数',
value: '5,678',
change: '+8%',
trend: 'up',
color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
},
{
icon: '👁',
label: '今日访问量',
value: '892',
change: '+15%',
trend: 'up',
color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
},
{
icon: '✅',
label: '活跃用户',
value: '456',
change: '+5%',
trend: 'up',
color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)'
}
]);
const statistics = ref<{
icon: string;
label: string;
value: string | number;
change: string;
trend: 'up' | 'down';
color: string;
}[]>([]);
const visitStats = ref([
{ label: 'UV(独立访客)', value: '892' },
{ label: 'PV(页面浏览量)', value: '3,456' },
{ label: '平均访问时长', value: '5分32秒' },
{ label: '跳出率', value: '35.6%' }
]);
const visitStats = ref<{ label: string; value: string | number }[]>([]);
onMounted(() => {
onMounted(async () => {
initCharts();
// TODO: 加载实际数据
await loadOverviewData();
});
onUnmounted(() => {
@@ -131,54 +101,129 @@ onUnmounted(() => {
}
});
async function loadOverviewData() {
try {
const [statRes, activeRes, pieRes, todayRes] = await Promise.all([
systemOverviewApi.getStatistics(),
systemOverviewApi.getActiveUsersChart('2025-10-15', '2025-10-21'),
systemOverviewApi.getResourceCategoryStats(),
systemOverviewApi.getTodayVisits()
]);
if (statRes.success && statRes.data) {
const d = statRes.data;
statistics.value = [
{
icon: '👥',
label: '总用户数',
value: d.totalUsers,
change: d.totalUsersChange,
trend: d.totalUsersChange.startsWith('-') ? 'down' : 'up',
color: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
},
{
icon: '📚',
label: '总资源数',
value: d.totalResources,
change: d.totalResourcesChange,
trend: d.totalResourcesChange.startsWith('-') ? 'down' : 'up',
color: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)'
},
{
icon: '👁',
label: '今日访问量',
value: d.todayVisits,
change: d.todayVisitsChange,
trend: d.todayVisitsChange.startsWith('-') ? 'down' : 'up',
color: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)'
},
{
icon: '✅',
label: '活跃用户',
value: d.activeUsers,
change: d.activeUsersChange,
trend: d.activeUsersChange.startsWith('-') ? 'down' : 'up',
color: 'linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)'
}
];
}
if (todayRes.success && todayRes.data) {
const t = todayRes.data;
visitStats.value = [
{ label: 'UV(独立访客)', value: t.uv },
{ label: 'PV(页面浏览量)', value: t.pv },
{ label: '平均访问时长', value: t.avgVisitDuration },
{ label: '跳出率', value: t.bounceRate }
];
}
if (activeRes.success && activeRes.data) {
updateActivityChart(activeRes.data.labels, activeRes.data.values);
}
if (pieRes.success && pieRes.data) {
updatePieChart(pieRes.data.items);
}
} catch (error) {
console.error('加载系统总览数据失败:', error);
}
}
function initCharts() {
if (activityChart.value) {
activityChartInstance = echarts.init(activityChart.value);
const activityOption = {
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
},
yAxis: {
type: 'value'
},
series: [{
data: [120, 200, 150, 80, 70, 110, 130],
type: 'line',
smooth: true,
areaStyle: {}
}]
};
activityChartInstance.setOption(activityOption);
}
if (resourcePieChart.value) {
pieChartInstance = echarts.init(resourcePieChart.value);
const pieOption = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
}
}
function updateActivityChart(labels: string[], values: number[]) {
if (!activityChartInstance) return;
const option = {
tooltip: {
trigger: 'axis'
},
xAxis: {
type: 'category',
data: labels
},
yAxis: {
type: 'value'
},
series: [
{
data: values,
type: 'line',
smooth: true,
areaStyle: {}
}
]
};
activityChartInstance.setOption(option);
}
function updatePieChart(items: { name: string; value: number }[]) {
if (!pieChartInstance) return;
const option = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [
{
type: 'pie',
radius: '50%',
data: [
{ value: 1048, name: '文章' },
{ value: 735, name: '视频' },
{ value: 580, name: '音频' },
{ value: 484, name: '课程' },
{ value: 300, name: '其他' }
]
}]
};
pieChartInstance.setOption(pieOption);
}
data: items
}
]
};
pieChartInstance.setOption(option);
}
</script>