commit 6df6ab856b3bb94313084a6aece5a16c7a1913e1 Author: pony <1356137040@qq.com> Date: Tue Jan 20 15:28:01 2026 +0800 v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8d850d --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# --- Common --- +.DS_Store +Thumbs.db +Desktop.ini +*.log +*.tmp +*.bak +*.swp + +# --- IDEs & Editors --- +.idea/ +.vscode/ +*.iml +*.ipr +*.iws +.classpath +.project +.settings/ +.factorypath + +# --- Java / Maven (Backend) --- +target/ +*.class +*.jar +*.war +*.ear +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# --- Node / Vue (Frontend) --- +node_modules/ +dist/ +dist-ssr/ +coverage/ +*.local +.npm/ +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# --- Application Specific --- +# Uploaded files +uploads/ +# Ignore local environment override files if any +.env.local +.env.development.local +.env.test.local +.env.production.local diff --git a/hertz_springboot/pom.xml b/hertz_springboot/pom.xml new file mode 100644 index 0000000..6c61e00 --- /dev/null +++ b/hertz_springboot/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.1 + + + + com.hertz + hertz-springboot-backend + 0.0.1-SNAPSHOT + hertz-springboot-backend + Hertz 权限管理系统后端 + + + 21 + 21 + 3.5.8 + 0.12.6 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-security + + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + + com.mysql + mysql-connector-j + runtime + + + + org.projectlombok + lombok + true + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + + + enforce-java + + enforce + + + + + [21,) + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/hertz_springboot/src/main/java/com/hertz/HertzApplication.java b/hertz_springboot/src/main/java/com/hertz/HertzApplication.java new file mode 100644 index 0000000..e1c2645 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/HertzApplication.java @@ -0,0 +1,14 @@ +package com.hertz; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan("com.hertz.**.mapper") +public class HertzApplication { + + public static void main(String[] args) { + SpringApplication.run(HertzApplication.class, args); + } +} diff --git a/hertz_springboot/src/main/java/com/hertz/common/api/ApiResponse.java b/hertz_springboot/src/main/java/com/hertz/common/api/ApiResponse.java new file mode 100644 index 0000000..279b6ed --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/common/api/ApiResponse.java @@ -0,0 +1,19 @@ +package com.hertz.common.api; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ApiResponse(int code, String message, T data) { + public static ApiResponse ok(T data) { + return new ApiResponse<>(0, "ok", data); + } + + public static ApiResponse ok() { + return new ApiResponse<>(0, "ok", null); + } + + public static ApiResponse fail(int code, String message) { + return new ApiResponse<>(code, message, null); + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/common/exception/BusinessException.java b/hertz_springboot/src/main/java/com/hertz/common/exception/BusinessException.java new file mode 100644 index 0000000..e6a1500 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/common/exception/BusinessException.java @@ -0,0 +1,15 @@ +package com.hertz.common.exception; + +public class BusinessException extends RuntimeException { + private final int code; + + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + public int getCode() { + return code; + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java b/hertz_springboot/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..f34d956 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,59 @@ +package com.hertz.common.exception; + +import com.hertz.common.api.ApiResponse; +import jakarta.validation.ConstraintViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public ResponseEntity> handleBusinessException(BusinessException e) { + var status = HttpStatus.BAD_REQUEST; + if (e.getCode() == 40100) { + status = HttpStatus.UNAUTHORIZED; + } else if (e.getCode() == 40300) { + status = HttpStatus.FORBIDDEN; + } + return ResponseEntity.status(status).body(ApiResponse.fail(e.getCode(), e.getMessage())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleMethodArgumentNotValid(MethodArgumentNotValidException e) { + var first = e.getBindingResult().getFieldErrors().stream().findFirst().orElse(null); + var message = first == null ? "参数错误" : first.getField() + " " + first.getDefaultMessage(); + return ApiResponse.fail(40001, message); + } + + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiResponse handleConstraintViolation(ConstraintViolationException e) { + return ApiResponse.fail(40001, e.getMessage()); + } + + @ExceptionHandler(AuthenticationException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public ApiResponse handleAuthentication(AuthenticationException e) { + return ApiResponse.fail(40100, "未登录或登录已过期"); + } + + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ApiResponse handleAccessDenied(AccessDeniedException e) { + return ApiResponse.fail(40300, "无权限"); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ApiResponse handleException(Exception e) { + return ApiResponse.fail(50000, "系统异常"); + } +} diff --git a/hertz_springboot/src/main/java/com/hertz/security/JwtAuthFilter.java b/hertz_springboot/src/main/java/com/hertz/security/JwtAuthFilter.java new file mode 100644 index 0000000..f7bf564 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/security/JwtAuthFilter.java @@ -0,0 +1,57 @@ +package com.hertz.security; + +import com.hertz.system.service.AuthzService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.stream.Collectors; +import org.springframework.http.HttpHeaders; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +@Component +public class JwtAuthFilter extends OncePerRequestFilter { + private final JwtService jwtService; + private final AuthzService authzService; + + public JwtAuthFilter(JwtService jwtService, AuthzService authzService) { + this.jwtService = jwtService; + this.authzService = authzService; + } + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + var header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header == null || !header.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + var token = header.substring("Bearer ".length()).trim(); + try { + var claims = jwtService.parse(token); + var userId = claims.get("uid", Number.class).longValue(); + var username = claims.getSubject(); + var authorities = authzService.loadAuthorities(userId).stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); + var auth = new UsernamePasswordAuthenticationToken(username, null, authorities); + auth.setDetails(userId); + SecurityContextHolder.getContext().setAuthentication(auth); + } catch (Exception ignored) { + SecurityContextHolder.clearContext(); + } + + filterChain.doFilter(request, response); + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/security/JwtService.java b/hertz_springboot/src/main/java/com/hertz/security/JwtService.java new file mode 100644 index 0000000..87ee606 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/security/JwtService.java @@ -0,0 +1,48 @@ +package com.hertz.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Date; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class JwtService { + private final SecretKey key; + private final long expireSeconds; + + public JwtService( + @Value("${app.jwt.secret}") String secret, + @Value("${app.jwt.expire-seconds}") long expireSeconds + ) { + if (secret == null || secret.getBytes(StandardCharsets.UTF_8).length < 32) { + throw new IllegalArgumentException("app.jwt.secret 至少 32 字节"); + } + this.key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.expireSeconds = expireSeconds; + } + + public String createToken(long userId, String username) { + var now = Instant.now(); + return Jwts.builder() + .subject(username) + .claim("uid", userId) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plusSeconds(expireSeconds))) + .signWith(key) + .compact(); + } + + public Claims parse(String token) { + return Jwts.parser() + .verifyWith(key) + .build() + .parseSignedClaims(token) + .getPayload(); + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/security/SecurityConfig.java b/hertz_springboot/src/main/java/com/hertz/security/SecurityConfig.java new file mode 100644 index 0000000..fa24fd0 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/security/SecurityConfig.java @@ -0,0 +1,58 @@ +package com.hertz.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableMethodSecurity +public class SecurityConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + var config = new CorsConfiguration(); + config.addAllowedOriginPattern("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + config.setAllowCredentials(true); + + var source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + + @Bean + public SecurityFilterChain filterChain( + HttpSecurity http, + JwtAuthFilter jwtAuthFilter + ) throws Exception { + return http + .csrf(csrf -> csrf.disable()) + .cors(Customizer.withDefaults()) + .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/auth/**", "/error", "/uploads/**").permitAll() + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) + .build(); + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/security/SecurityUtils.java b/hertz_springboot/src/main/java/com/hertz/security/SecurityUtils.java new file mode 100644 index 0000000..bff1232 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/security/SecurityUtils.java @@ -0,0 +1,25 @@ +package com.hertz.security; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +public final class SecurityUtils { + private SecurityUtils() { + } + + public static Long getCurrentUserId() { + return getCurrentUserId(SecurityContextHolder.getContext().getAuthentication()); + } + + public static Long getCurrentUserId(Authentication authentication) { + if (authentication == null) { + return null; + } + var details = authentication.getDetails(); + if (details instanceof Number n) { + return n.longValue(); + } + return null; + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/config/MybatisPlusConfig.java b/hertz_springboot/src/main/java/com/hertz/system/config/MybatisPlusConfig.java new file mode 100644 index 0000000..e8aa64a --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/config/MybatisPlusConfig.java @@ -0,0 +1,17 @@ +package com.hertz.system.config; + +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MybatisPlusConfig { + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + var interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + return interceptor; + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/config/WebMvcConfig.java b/hertz_springboot/src/main/java/com/hertz/system/config/WebMvcConfig.java new file mode 100644 index 0000000..c93dc66 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/config/WebMvcConfig.java @@ -0,0 +1,19 @@ +package com.hertz.system.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Value("${app.upload.root-path}") + private String uploadRootPath; + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/uploads/**") + .addResourceLocations("file:" + uploadRootPath + "/"); + } +} diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/AuthController.java b/hertz_springboot/src/main/java/com/hertz/system/controller/AuthController.java new file mode 100644 index 0000000..8e40ee7 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/controller/AuthController.java @@ -0,0 +1,109 @@ +package com.hertz.system.controller; + +import com.hertz.common.api.ApiResponse; +import com.hertz.common.exception.BusinessException; +import com.hertz.security.JwtService; +import com.hertz.security.SecurityUtils; +import com.hertz.system.dto.AuthDtos; +import com.hertz.system.mapper.SysRoleMapper; +import com.hertz.system.service.UserService; +import jakarta.validation.Valid; +import java.util.List; +import org.springframework.security.core.Authentication; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + private final UserService userService; + private final PasswordEncoder passwordEncoder; + private final JwtService jwtService; + private final SysRoleMapper roleMapper; + + public AuthController( + UserService userService, + PasswordEncoder passwordEncoder, + JwtService jwtService, + SysRoleMapper roleMapper + ) { + this.userService = userService; + this.passwordEncoder = passwordEncoder; + this.jwtService = jwtService; + this.roleMapper = roleMapper; + } + + @PostMapping("/register") + public ApiResponse register(@Valid @RequestBody AuthDtos.RegisterRequest req) { + var user = userService.register(req.username(), req.password(), req.nickname()); + return ApiResponse.ok(new AuthDtos.MeResponse( + user.getId(), + user.getUsername(), + user.getNickname(), + user.getAvatarPath(), + user.getPhone(), + user.getEmail(), + user.getGender(), + List.of() + )); + } + + @PostMapping("/login") + public ApiResponse login(@Valid @RequestBody AuthDtos.LoginRequest req) { + var user = userService.findByUsername(req.username()); + if (user == null || user.getStatus() == null || user.getStatus() != 1) { + throw new BusinessException(40003, "用户名或密码错误"); + } + if (!passwordEncoder.matches(req.password(), user.getPassword())) { + throw new BusinessException(40003, "用户名或密码错误"); + } + var token = jwtService.createToken(user.getId(), user.getUsername()); + var roles = roleMapper.selectRoleKeysByUserId(user.getId()); + return ApiResponse.ok(new AuthDtos.LoginResponse(token, user.getId(), user.getUsername(), roles)); + } + + @GetMapping("/me") + public ApiResponse me(Authentication authentication) { + var userId = SecurityUtils.getCurrentUserId(authentication); + if (userId == null) { + throw new BusinessException(40100, "未登录或登录已过期"); + } + var user = userService.findByUsername(authentication.getName()); + var roles = roleMapper.selectRoleKeysByUserId(userId); + return ApiResponse.ok(new AuthDtos.MeResponse( + userId, + authentication.getName(), + user == null ? null : user.getNickname(), + user == null ? null : user.getAvatarPath(), + user == null ? null : user.getPhone(), + user == null ? null : user.getEmail(), + user == null ? null : user.getGender(), + roles + )); + } + + @PostMapping("/profile") + public ApiResponse updateProfile(@RequestBody @Valid AuthDtos.UpdateProfileRequest req) { + var userId = SecurityUtils.getCurrentUserId(); + if (userId == null) { + throw new BusinessException(40100, "未登录或登录已过期"); + } + userService.updateProfile(userId, req); + return ApiResponse.ok(); + } + + @PostMapping("/password") + public ApiResponse updatePassword(@RequestBody @Valid AuthDtos.UpdatePasswordRequest req) { + var userId = SecurityUtils.getCurrentUserId(); + if (userId == null) { + throw new BusinessException(40100, "未登录或登录已过期"); + } + userService.updatePassword(userId, req); + return ApiResponse.ok(); + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/FileController.java b/hertz_springboot/src/main/java/com/hertz/system/controller/FileController.java new file mode 100644 index 0000000..ebe3f47 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/controller/FileController.java @@ -0,0 +1,68 @@ +package com.hertz.system.controller; + +import com.hertz.common.api.ApiResponse; +import com.hertz.common.exception.BusinessException; +import java.io.File; +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.UUID; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/api/common/file") +public class FileController { + + @Value("${app.upload.root-path}") + private String uploadRootPath; + + @Value("${app.upload.avatar-path}") + private String avatarPath; + + @PostMapping("/upload") + public ApiResponse upload(@RequestParam("file") MultipartFile file) { + if (file.isEmpty()) { + throw new BusinessException(400, "File cannot be empty"); + } + + String originalFilename = file.getOriginalFilename(); + String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); + if (!suffix.equalsIgnoreCase(".jpg") && !suffix.equalsIgnoreCase(".png") && !suffix.equalsIgnoreCase(".jpeg")) { + throw new BusinessException(400, "Only JPG/PNG formats are supported"); + } + + if (file.getSize() > 2 * 1024 * 1024) { + throw new BusinessException(400, "File size exceeds 2MB"); + } + + String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + String fileName = UUID.randomUUID().toString().replace("-", "") + suffix; + // Construct the relative path (e.g., avatar/2023/10/27/uuid.jpg) + // Note: avatarPath should end with / + String relativePath = avatarPath + datePath + "/" + fileName; + // Construct full path + String fullPath = uploadRootPath + File.separator + relativePath; + + File dest = new File(fullPath); + if (!dest.getParentFile().exists()) { + dest.getParentFile().mkdirs(); + } + + try { + file.transferTo(dest); + + // Return relative path. Frontend should prepend the base URL. + return ApiResponse.ok(new UploadResult("/uploads/" + relativePath, null)); + } catch (IOException e) { + throw new BusinessException(500, "File upload failed: " + e.getMessage()); + } + } + + public record UploadResult(String url, String thumbUrl) { + } +} diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/MenuController.java b/hertz_springboot/src/main/java/com/hertz/system/controller/MenuController.java new file mode 100644 index 0000000..df9187f --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/controller/MenuController.java @@ -0,0 +1,72 @@ +package com.hertz.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.hertz.common.api.ApiResponse; +import com.hertz.common.exception.BusinessException; +import com.hertz.security.SecurityUtils; +import com.hertz.system.dto.MenuDto; +import com.hertz.system.entity.SysMenu; +import com.hertz.system.service.MenuService; +import java.util.List; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/system/menus") +public class MenuController { + private final MenuService menuService; + + public MenuController(MenuService menuService) { + this.menuService = menuService; + } + + @GetMapping("/tree") + public ApiResponse> tree(Authentication authentication) { + var userId = SecurityUtils.getCurrentUserId(authentication); + if (userId == null) { + throw new BusinessException(40100, "未登录或登录已过期"); + } + return ApiResponse.ok(menuService.getMenuTreeByUserId(userId)); + } + + @GetMapping("/page") + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:menu:view')") + public ApiResponse> page( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String keyword + ) { + return ApiResponse.ok(menuService.pageMenus(page, size, keyword)); + } + + @PostMapping + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:menu:add')") + public ApiResponse create(@RequestBody SysMenu menu) { + menuService.saveMenu(menu); + return ApiResponse.ok(); + } + + @PutMapping + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:menu:edit')") + public ApiResponse update(@RequestBody SysMenu menu) { + menuService.updateMenu(menu); + return ApiResponse.ok(); + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:menu:remove')") + public ApiResponse delete(@PathVariable("id") Long id) { + menuService.deleteMenu(id); + return ApiResponse.ok(); + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/RoleController.java b/hertz_springboot/src/main/java/com/hertz/system/controller/RoleController.java new file mode 100644 index 0000000..082f900 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/controller/RoleController.java @@ -0,0 +1,81 @@ +package com.hertz.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.hertz.common.api.ApiResponse; +import com.hertz.system.entity.SysRole; +import com.hertz.system.service.RoleService; +import java.util.List; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/system/roles") +public class RoleController { + private final RoleService roleService; + + public RoleController(RoleService roleService) { + this.roleService = roleService; + } + + @GetMapping + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:view')") + public ApiResponse> list() { + return ApiResponse.ok(roleService.listEnabledRoles()); + } + + @GetMapping("/page") + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:view')") + public ApiResponse> page( + @RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String keyword + ) { + return ApiResponse.ok(roleService.pageRoles(page, size, keyword)); + } + + @PostMapping + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:add')") + public ApiResponse create(@RequestBody SysRole role) { + roleService.saveRole(role); + return ApiResponse.ok(); + } + + @PutMapping + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:edit')") + public ApiResponse update(@RequestBody SysRole role) { + roleService.updateRole(role); + return ApiResponse.ok(); + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:remove')") + public ApiResponse delete(@PathVariable("id") Long id) { + roleService.deleteRole(id); + return ApiResponse.ok(); + } + + @GetMapping("/{id}/menus") + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:view')") + public ApiResponse> getRoleMenus(@PathVariable("id") Long id) { + return ApiResponse.ok(roleService.getRoleMenuIds(id)); + } + + public record UpdateRoleMenusRequest(List menuIds) { + } + + @PutMapping("/{id}/menus") + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:role:assign')") + public ApiResponse updateRoleMenus(@PathVariable("id") Long id, @RequestBody UpdateRoleMenusRequest req) { + roleService.updateRolePermissions(id, req.menuIds()); + return ApiResponse.ok(); + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/UserController.java b/hertz_springboot/src/main/java/com/hertz/system/controller/UserController.java new file mode 100644 index 0000000..4b23be6 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/controller/UserController.java @@ -0,0 +1,153 @@ +package com.hertz.system.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.hertz.common.api.ApiResponse; +import com.hertz.system.entity.SysUser; +import com.hertz.system.service.UserService; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import java.time.LocalDateTime; +import java.util.List; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/system/users") +public class UserController { + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @GetMapping + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:view')") + public ApiResponse> page( + @RequestParam(defaultValue = "1") @Min(1) int page, + @RequestParam(defaultValue = "10") @Min(1) @Max(200) int size, + @RequestParam(required = false) String keyword + ) { + var p = userService.pageUsers(page, size, keyword); + var dtoPage = Page.of(p.getCurrent(), p.getSize(), p.getTotal()); + dtoPage.setRecords(p.getRecords().stream().map(u -> { + var roles = userService.getUserRoles(u.getId()).stream() + .map(com.hertz.system.entity.SysRole::getRoleName) + .toList(); + return new UserListItem( + u.getId(), + u.getUsername(), + u.getNickname(), + u.getAvatarPath(), + u.getPhone(), + u.getEmail(), + u.getGender(), + u.getStatus(), + u.getCreatedAt(), + roles + ); + }).toList()); + return ApiResponse.ok(dtoPage); + } + + public record UserListItem( + Long id, + String username, + String nickname, + String avatarPath, + String phone, + String email, + Integer gender, + Integer status, + LocalDateTime createdAt, + List roles + ) { + } + + public record CreateUserRequest( + @NotBlank(message = "用户名不能为空") String username, + @NotBlank(message = "密码不能为空") String password, + @NotBlank(message = "昵称不能为空") String nickname, + String avatarPath, + String phone, + String email, + Integer gender, + Integer status + ) {} + + @PostMapping + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:add')") + public ApiResponse create(@RequestBody @Valid CreateUserRequest req) { + var u = new SysUser(); + u.setUsername(req.username()); + u.setPassword(req.password()); + u.setNickname(req.nickname()); + u.setAvatarPath(req.avatarPath()); + u.setPhone(req.phone()); + u.setEmail(req.email()); + u.setGender(req.gender()); + u.setStatus(req.status()); + userService.createUser(u); + return ApiResponse.ok(); + } + + public record UpdateUserRequest( + Long id, + String password, + @NotBlank(message = "昵称不能为空") String nickname, + String avatarPath, + String phone, + String email, + Integer gender, + Integer status + ) {} + + @PutMapping + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:edit')") + public ApiResponse update(@RequestBody @Valid UpdateUserRequest req) { + var u = new SysUser(); + u.setId(req.id()); + u.setPassword(req.password()); + u.setNickname(req.nickname()); + u.setAvatarPath(req.avatarPath()); + u.setPhone(req.phone()); + u.setEmail(req.email()); + u.setGender(req.gender()); + u.setStatus(req.status()); + userService.updateUser(u); + return ApiResponse.ok(); + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:remove')") + public ApiResponse delete(@PathVariable Long id) { + userService.deleteUser(id); + return ApiResponse.ok(); + } + + @GetMapping("/{id}/roles") + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:view')") + public ApiResponse> roleIds(@PathVariable("id") long userId) { + return ApiResponse.ok(userService.getUserRoleIds(userId)); + } + + public record UpdateRolesRequest(List roleIds) { + } + + @PutMapping("/{id}/roles") + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:assign')") + public ApiResponse updateRoles(@PathVariable("id") long userId, @RequestBody UpdateRolesRequest req) { + userService.updateUserRoles(userId, req == null ? List.of() : req.roleIds()); + return ApiResponse.ok(); + } +} diff --git a/hertz_springboot/src/main/java/com/hertz/system/dto/AuthDtos.java b/hertz_springboot/src/main/java/com/hertz/system/dto/AuthDtos.java new file mode 100644 index 0000000..93ece57 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/dto/AuthDtos.java @@ -0,0 +1,55 @@ +package com.hertz.system.dto; + +import jakarta.validation.constraints.NotBlank; +import java.util.List; + +public class AuthDtos { + public record LoginRequest( + @NotBlank String username, + @NotBlank String password + ) { + } + + public record RegisterRequest( + @NotBlank String username, + @NotBlank String password, + String nickname + ) { + } + + public record LoginResponse( + String token, + long userId, + String username, + List roles + ) { + } + + public record MeResponse( + long userId, + String username, + String nickname, + String avatarPath, + String phone, + String email, + Integer gender, + List roles + ) { + } + + public record UpdateProfileRequest( + @NotBlank(message = "昵称不能为空") String nickname, + String avatarPath, + String phone, + String email, + Integer gender + ) { + } + + public record UpdatePasswordRequest( + @NotBlank(message = "旧密码不能为空") String oldPassword, + @NotBlank(message = "新密码不能为空") String newPassword + ) { + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/dto/MenuDto.java b/hertz_springboot/src/main/java/com/hertz/system/dto/MenuDto.java new file mode 100644 index 0000000..af84093 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/dto/MenuDto.java @@ -0,0 +1,20 @@ +package com.hertz.system.dto; + +import java.util.ArrayList; +import java.util.List; +import lombok.Data; + +@Data +public class MenuDto { + private Long id; + private Long parentId; + private String type; + private String name; + private String path; + private String component; + private String perms; + private String icon; + private Integer sort; + private List children = new ArrayList<>(); +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysMenu.java b/hertz_springboot/src/main/java/com/hertz/system/entity/SysMenu.java new file mode 100644 index 0000000..34d34b5 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/entity/SysMenu.java @@ -0,0 +1,27 @@ +package com.hertz.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@TableName("sys_menu") +public class SysMenu { + @TableId(type = IdType.AUTO) + private Long id; + private Long parentId; + private String type; + private String name; + private String path; + private String component; + private String perms; + private String icon; + private Integer sort; + private Integer visible; + private Integer status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysRole.java b/hertz_springboot/src/main/java/com/hertz/system/entity/SysRole.java new file mode 100644 index 0000000..d3610ce --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/entity/SysRole.java @@ -0,0 +1,20 @@ +package com.hertz.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@TableName("sys_role") +public class SysRole { + @TableId(type = IdType.AUTO) + private Long id; + private String roleKey; + private String roleName; + private Integer status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysRoleMenu.java b/hertz_springboot/src/main/java/com/hertz/system/entity/SysRoleMenu.java new file mode 100644 index 0000000..ebe428f --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/entity/SysRoleMenu.java @@ -0,0 +1,16 @@ +package com.hertz.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("sys_role_menu") +public class SysRoleMenu { + @TableId(type = IdType.AUTO) + private Long id; + private Long roleId; + private Long menuId; +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysUser.java b/hertz_springboot/src/main/java/com/hertz/system/entity/SysUser.java new file mode 100644 index 0000000..1ba0a19 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/entity/SysUser.java @@ -0,0 +1,28 @@ +package com.hertz.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@TableName("sys_user") +public class SysUser { + @TableId(type = IdType.AUTO) + private Long id; + private String username; + private String password; + private String nickname; + private String avatarPath; + private String phone; + private String email; + /** + * 0-未知 1-男 2-女 + */ + private Integer gender; + private Integer status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysUserRole.java b/hertz_springboot/src/main/java/com/hertz/system/entity/SysUserRole.java new file mode 100644 index 0000000..33c8039 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/entity/SysUserRole.java @@ -0,0 +1,16 @@ +package com.hertz.system.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +@Data +@TableName("sys_user_role") +public class SysUserRole { + @TableId(type = IdType.AUTO) + private Long id; + private Long userId; + private Long roleId; +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysMenuMapper.java b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysMenuMapper.java new file mode 100644 index 0000000..8618c31 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysMenuMapper.java @@ -0,0 +1,56 @@ +package com.hertz.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.hertz.system.entity.SysMenu; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface SysMenuMapper extends BaseMapper { + @Select(""" + SELECT DISTINCT m.* + FROM sys_menu m + INNER JOIN sys_role_menu rm ON rm.menu_id = m.id + INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id + WHERE ur.user_id = #{userId} + AND m.status = 1 + AND m.visible = 1 + AND m.type IN ('D','M') + ORDER BY m.sort ASC, m.id ASC + """) + List selectMenusByUserId(@Param("userId") long userId); + + @Select(""" + SELECT m.* + FROM sys_menu m + WHERE m.status = 1 + AND m.visible = 1 + AND m.type IN ('D','M') + ORDER BY m.sort ASC, m.id ASC + """) + List selectAllVisibleMenus(); + + @Select(""" + SELECT DISTINCT m.perms + FROM sys_menu m + INNER JOIN sys_role_menu rm ON rm.menu_id = m.id + INNER JOIN sys_user_role ur ON ur.role_id = rm.role_id + WHERE ur.user_id = #{userId} + AND m.status = 1 + AND m.perms IS NOT NULL + AND m.perms <> '' + """) + List selectPermsByUserId(@Param("userId") long userId); + + @Select(""" + SELECT DISTINCT m.perms + FROM sys_menu m + WHERE m.status = 1 + AND m.perms IS NOT NULL + AND m.perms <> '' + """) + List selectAllPerms(); +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMapper.java b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMapper.java new file mode 100644 index 0000000..13c30f5 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMapper.java @@ -0,0 +1,29 @@ +package com.hertz.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.hertz.system.entity.SysRole; +import java.util.List; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface SysRoleMapper extends BaseMapper { + @Select(""" + SELECT r.role_key + FROM sys_role r + INNER JOIN sys_user_role ur ON ur.role_id = r.id + WHERE ur.user_id = #{userId} AND r.status = 1 + """) + List selectRoleKeysByUserId(@Param("userId") long userId); + + @Select(""" + SELECT r.* + FROM sys_role r + INNER JOIN sys_user_role ur ON ur.role_id = r.id + WHERE ur.user_id = #{userId} AND r.status = 1 + ORDER BY r.id ASC + """) + List selectRolesByUserId(@Param("userId") long userId); +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMenuMapper.java b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMenuMapper.java new file mode 100644 index 0000000..b2e084c --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMenuMapper.java @@ -0,0 +1,9 @@ +package com.hertz.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.hertz.system.entity.SysRoleMenu; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SysRoleMenuMapper extends BaseMapper { +} diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserMapper.java b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserMapper.java new file mode 100644 index 0000000..50de1f7 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserMapper.java @@ -0,0 +1,10 @@ +package com.hertz.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.hertz.system.entity.SysUser; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SysUserMapper extends BaseMapper { +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserRoleMapper.java b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserRoleMapper.java new file mode 100644 index 0000000..1380349 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserRoleMapper.java @@ -0,0 +1,10 @@ +package com.hertz.system.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.hertz.system.entity.SysUserRole; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SysUserRoleMapper extends BaseMapper { +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/AuthzService.java b/hertz_springboot/src/main/java/com/hertz/system/service/AuthzService.java new file mode 100644 index 0000000..495e5cc --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/service/AuthzService.java @@ -0,0 +1,8 @@ +package com.hertz.system.service; + +import java.util.Set; + +public interface AuthzService { + Set loadAuthorities(long userId); +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/MenuService.java b/hertz_springboot/src/main/java/com/hertz/system/service/MenuService.java new file mode 100644 index 0000000..95b1977 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/service/MenuService.java @@ -0,0 +1,19 @@ +package com.hertz.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.hertz.system.dto.MenuDto; +import com.hertz.system.entity.SysMenu; +import java.util.List; + +public interface MenuService { + List getMenuTreeByUserId(long userId); + + IPage pageMenus(int page, int size, String keyword); + + void saveMenu(SysMenu menu); + + void updateMenu(SysMenu menu); + + void deleteMenu(Long id); +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/RoleService.java b/hertz_springboot/src/main/java/com/hertz/system/service/RoleService.java new file mode 100644 index 0000000..fd208ea --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/service/RoleService.java @@ -0,0 +1,22 @@ +package com.hertz.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.hertz.system.entity.SysRole; +import java.util.List; + +public interface RoleService { + List listEnabledRoles(); + + IPage pageRoles(int page, int size, String keyword); + + void saveRole(SysRole role); + + void updateRole(SysRole role); + + void deleteRole(Long id); + + void updateRolePermissions(Long roleId, List menuIds); + + List getRoleMenuIds(Long roleId); +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/UserService.java b/hertz_springboot/src/main/java/com/hertz/system/service/UserService.java new file mode 100644 index 0000000..0cc4e6f --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/service/UserService.java @@ -0,0 +1,31 @@ +package com.hertz.system.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.hertz.system.dto.AuthDtos; +import com.hertz.system.entity.SysUser; +import java.util.List; + +public interface UserService { + SysUser register(String username, String rawPassword, String nickname); + + SysUser findByUsername(String username); + + IPage pageUsers(int page, int size, String keyword); + + void createUser(SysUser user); + + void updateUser(SysUser user); + + void updateProfile(Long userId, AuthDtos.UpdateProfileRequest req); + + void updatePassword(Long userId, AuthDtos.UpdatePasswordRequest req); + + void deleteUser(Long id); + + void updateUserRoles(long userId, List roleIds); + + List getUserRoleIds(long userId); + + List getUserRoles(long userId); +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/AuthzServiceImpl.java b/hertz_springboot/src/main/java/com/hertz/system/service/impl/AuthzServiceImpl.java new file mode 100644 index 0000000..5f98db9 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/service/impl/AuthzServiceImpl.java @@ -0,0 +1,36 @@ +package com.hertz.system.service.impl; + +import com.hertz.system.mapper.SysMenuMapper; +import com.hertz.system.mapper.SysRoleMapper; +import com.hertz.system.service.AuthzService; +import java.util.HashSet; +import org.springframework.stereotype.Service; + +@Service +public class AuthzServiceImpl implements AuthzService { + private final SysRoleMapper roleMapper; + private final SysMenuMapper menuMapper; + + public AuthzServiceImpl(SysRoleMapper roleMapper, SysMenuMapper menuMapper) { + this.roleMapper = roleMapper; + this.menuMapper = menuMapper; + } + + @Override + public HashSet loadAuthorities(long userId) { + var roleKeys = roleMapper.selectRoleKeysByUserId(userId); + var authorities = new HashSet(); + for (var roleKey : roleKeys) { + if (roleKey != null && !roleKey.isBlank()) { + authorities.add("ROLE_" + roleKey); + } + } + if (roleKeys.stream().anyMatch(r -> "ADMIN".equalsIgnoreCase(r))) { + authorities.addAll(menuMapper.selectAllPerms()); + } else { + authorities.addAll(menuMapper.selectPermsByUserId(userId)); + } + return authorities; + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/MenuServiceImpl.java b/hertz_springboot/src/main/java/com/hertz/system/service/impl/MenuServiceImpl.java new file mode 100644 index 0000000..69aabea --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/service/impl/MenuServiceImpl.java @@ -0,0 +1,129 @@ +package com.hertz.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.hertz.common.exception.BusinessException; +import com.hertz.system.dto.MenuDto; +import com.hertz.system.entity.SysMenu; +import com.hertz.system.entity.SysRoleMenu; +import com.hertz.system.mapper.SysMenuMapper; +import com.hertz.system.mapper.SysRoleMapper; +import com.hertz.system.mapper.SysRoleMenuMapper; +import com.hertz.system.service.MenuService; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class MenuServiceImpl implements MenuService { + private final SysMenuMapper menuMapper; + private final SysRoleMapper roleMapper; + private final SysRoleMenuMapper roleMenuMapper; + + public MenuServiceImpl(SysMenuMapper menuMapper, SysRoleMapper roleMapper, SysRoleMenuMapper roleMenuMapper) { + this.menuMapper = menuMapper; + this.roleMapper = roleMapper; + this.roleMenuMapper = roleMenuMapper; + } + + @Override + public List getMenuTreeByUserId(long userId) { + var roleKeys = roleMapper.selectRoleKeysByUserId(userId); + var menus = roleKeys.stream().anyMatch(r -> "ADMIN".equalsIgnoreCase(r)) + ? menuMapper.selectAllVisibleMenus() + : menuMapper.selectMenusByUserId(userId); + + var map = new HashMap(); + for (var m : menus) { + var dto = new MenuDto(); + dto.setId(m.getId()); + dto.setParentId(m.getParentId()); + dto.setType(m.getType()); + dto.setName(m.getName()); + dto.setPath(m.getPath()); + dto.setComponent(m.getComponent()); + dto.setPerms(m.getPerms()); + dto.setIcon(m.getIcon()); + dto.setSort(m.getSort()); + map.put(dto.getId(), dto); + } + + var roots = new ArrayList(); + for (var dto : map.values()) { + var parentId = dto.getParentId() == null ? 0L : dto.getParentId(); + if (parentId == 0L || !map.containsKey(parentId)) { + roots.add(dto); + } else { + map.get(parentId).getChildren().add(dto); + } + } + + Comparator comparator = Comparator + .comparing((MenuDto d) -> d.getSort() == null ? 0 : d.getSort()) + .thenComparing(d -> d.getId() == null ? 0 : d.getId()); + sortRecursively(roots, comparator); + return roots; + } + + @Override + public IPage pageMenus(int page, int size, String keyword) { + var wrapper = new LambdaQueryWrapper().orderByAsc(SysMenu::getSort); + if (keyword != null && !keyword.isBlank()) { + wrapper.like(SysMenu::getName, keyword); + } + return menuMapper.selectPage(Page.of(page, size), wrapper); + } + + @Override + @Transactional + public void saveMenu(SysMenu menu) { + menu.setCreatedAt(java.time.LocalDateTime.now()); + menu.setUpdatedAt(java.time.LocalDateTime.now()); + menuMapper.insert(menu); + } + + @Override + @Transactional + public void updateMenu(SysMenu menu) { + var existing = menuMapper.selectById(menu.getId()); + if (existing == null) { + throw new BusinessException(404, "Menu not found"); + } + existing.setParentId(menu.getParentId()); + existing.setType(menu.getType()); + existing.setName(menu.getName()); + existing.setPath(menu.getPath()); + existing.setComponent(menu.getComponent()); + existing.setPerms(menu.getPerms()); + existing.setIcon(menu.getIcon()); + existing.setSort(menu.getSort()); + existing.setVisible(menu.getVisible()); + existing.setStatus(menu.getStatus()); + existing.setUpdatedAt(java.time.LocalDateTime.now()); + menuMapper.updateById(existing); + } + + @Override + @Transactional + public void deleteMenu(Long id) { + if (menuMapper.selectCount(new LambdaQueryWrapper().eq(SysMenu::getParentId, id)) > 0) { + throw new BusinessException(400, "Has sub-menus, cannot delete"); + } + menuMapper.deleteById(id); + roleMenuMapper.delete(new LambdaQueryWrapper().eq(SysRoleMenu::getMenuId, id)); + } + + private void sortRecursively(List nodes, Comparator comparator) { + nodes.sort(comparator); + for (var n : nodes) { + if (n.getChildren() != null && !n.getChildren().isEmpty()) { + sortRecursively(n.getChildren(), comparator); + } + } + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/RoleServiceImpl.java b/hertz_springboot/src/main/java/com/hertz/system/service/impl/RoleServiceImpl.java new file mode 100644 index 0000000..1a67d6a --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/service/impl/RoleServiceImpl.java @@ -0,0 +1,94 @@ +package com.hertz.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.hertz.common.exception.BusinessException; +import com.hertz.system.entity.SysRole; +import com.hertz.system.entity.SysRoleMenu; +import com.hertz.system.mapper.SysRoleMapper; +import com.hertz.system.mapper.SysRoleMenuMapper; +import com.hertz.system.service.RoleService; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class RoleServiceImpl implements RoleService { + private final SysRoleMapper roleMapper; + private final SysRoleMenuMapper roleMenuMapper; + + public RoleServiceImpl(SysRoleMapper roleMapper, SysRoleMenuMapper roleMenuMapper) { + this.roleMapper = roleMapper; + this.roleMenuMapper = roleMenuMapper; + } + + @Override + public List listEnabledRoles() { + return roleMapper.selectList(new LambdaQueryWrapper() + .eq(SysRole::getStatus, 1) + .orderByAsc(SysRole::getId)); + } + + @Override + public IPage pageRoles(int page, int size, String keyword) { + var wrapper = new LambdaQueryWrapper().orderByDesc(SysRole::getId); + if (keyword != null && !keyword.isBlank()) { + wrapper.and(w -> w.like(SysRole::getRoleName, keyword).or().like(SysRole::getRoleKey, keyword)); + } + return roleMapper.selectPage(Page.of(page, size), wrapper); + } + + @Override + @Transactional + public void saveRole(SysRole role) { + if (roleMapper.selectCount(new LambdaQueryWrapper().eq(SysRole::getRoleKey, role.getRoleKey())) > 0) { + throw new BusinessException(400, "Role key already exists"); + } + role.setCreatedAt(java.time.LocalDateTime.now()); + role.setUpdatedAt(java.time.LocalDateTime.now()); + roleMapper.insert(role); + } + + @Override + @Transactional + public void updateRole(SysRole role) { + var existing = roleMapper.selectById(role.getId()); + if (existing == null) { + throw new BusinessException(404, "Role not found"); + } + existing.setRoleName(role.getRoleName()); + existing.setRoleKey(role.getRoleKey()); + existing.setStatus(role.getStatus()); + existing.setUpdatedAt(java.time.LocalDateTime.now()); + roleMapper.updateById(existing); + } + + @Override + @Transactional + public void deleteRole(Long id) { + roleMapper.deleteById(id); + roleMenuMapper.delete(new LambdaQueryWrapper().eq(SysRoleMenu::getRoleId, id)); + } + + @Override + @Transactional + public void updateRolePermissions(Long roleId, List menuIds) { + roleMenuMapper.delete(new LambdaQueryWrapper().eq(SysRoleMenu::getRoleId, roleId)); + if (menuIds != null) { + menuIds.stream().distinct().forEach(menuId -> { + SysRoleMenu rm = new SysRoleMenu(); + rm.setRoleId(roleId); + rm.setMenuId(menuId); + roleMenuMapper.insert(rm); + }); + } + } + + @Override + public List getRoleMenuIds(Long roleId) { + return roleMenuMapper.selectList(new LambdaQueryWrapper().eq(SysRoleMenu::getRoleId, roleId)) + .stream().map(SysRoleMenu::getMenuId).toList(); + } +} + diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java b/hertz_springboot/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..73dd436 --- /dev/null +++ b/hertz_springboot/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java @@ -0,0 +1,174 @@ +package com.hertz.system.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.hertz.common.exception.BusinessException; +import com.hertz.system.entity.SysUser; +import com.hertz.system.entity.SysUserRole; +import com.hertz.system.mapper.SysRoleMapper; +import com.hertz.system.mapper.SysUserMapper; +import com.hertz.system.mapper.SysUserRoleMapper; +import com.hertz.system.service.UserService; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UserServiceImpl implements UserService { + private final SysUserMapper userMapper; + private final SysUserRoleMapper userRoleMapper; + private final SysRoleMapper roleMapper; + private final PasswordEncoder passwordEncoder; + + public UserServiceImpl(SysUserMapper userMapper, SysUserRoleMapper userRoleMapper, SysRoleMapper roleMapper, PasswordEncoder passwordEncoder) { + this.userMapper = userMapper; + this.userRoleMapper = userRoleMapper; + this.roleMapper = roleMapper; + this.passwordEncoder = passwordEncoder; + } + + @Override + public SysUser register(String username, String rawPassword, String nickname) { + var exists = userMapper.selectCount(new LambdaQueryWrapper() + .eq(SysUser::getUsername, username)) > 0; + if (exists) { + throw new BusinessException(40002, "用户名已存在"); + } + var user = new SysUser(); + user.setUsername(username); + user.setPassword(passwordEncoder.encode(rawPassword)); + user.setNickname(nickname); + user.setStatus(1); + userMapper.insert(user); + return user; + } + + @Override + public SysUser findByUsername(String username) { + return userMapper.selectOne(new LambdaQueryWrapper() + .eq(SysUser::getUsername, username) + .last("LIMIT 1")); + } + + @Override + public IPage pageUsers(int page, int size, String keyword) { + var wrapper = new LambdaQueryWrapper() + .orderByDesc(SysUser::getId); + if (keyword != null && !keyword.isBlank()) { + wrapper.and(w -> w.like(SysUser::getUsername, keyword) + .or().like(SysUser::getNickname, keyword) + .or().like(SysUser::getPhone, keyword) + .or().like(SysUser::getEmail, keyword)); + } + return userMapper.selectPage(Page.of(page, size), wrapper); + } + + @Override + @Transactional + public void createUser(SysUser user) { + if (userMapper.selectCount(new LambdaQueryWrapper().eq(SysUser::getUsername, user.getUsername())) > 0) { + throw new BusinessException(400, "用户名已存在"); + } + user.setPassword(passwordEncoder.encode(user.getPassword())); + user.setCreatedAt(java.time.LocalDateTime.now()); + user.setUpdatedAt(java.time.LocalDateTime.now()); + userMapper.insert(user); + } + + @Override + @Transactional + public void updateUser(SysUser user) { + var existing = userMapper.selectById(user.getId()); + if (existing == null) { + throw new BusinessException(404, "用户不存在"); + } + existing.setNickname(user.getNickname()); + existing.setPhone(user.getPhone()); + existing.setEmail(user.getEmail()); + existing.setGender(user.getGender()); + existing.setStatus(user.getStatus()); + existing.setAvatarPath(user.getAvatarPath()); + existing.setUpdatedAt(java.time.LocalDateTime.now()); + + if (user.getPassword() != null && !user.getPassword().isBlank()) { + existing.setPassword(passwordEncoder.encode(user.getPassword())); + } + + userMapper.updateById(existing); + } + + @Override + @Transactional + public void updateProfile(Long userId, com.hertz.system.dto.AuthDtos.UpdateProfileRequest req) { + var existing = userMapper.selectById(userId); + if (existing == null) { + throw new BusinessException(404, "用户不存在"); + } + existing.setNickname(req.nickname()); + // Only update avatarPath if it's provided (not null and not empty) + // Actually, req.avatarPath() might be empty string if cleared, but here we assume if it is provided we update it. + // If the frontend sends null, we might want to skip update or clear it? + // Let's assume frontend sends the new path if updated. + if (req.avatarPath() != null) { + existing.setAvatarPath(req.avatarPath()); + } + existing.setPhone(req.phone()); + existing.setEmail(req.email()); + existing.setGender(req.gender()); + existing.setUpdatedAt(java.time.LocalDateTime.now()); + userMapper.updateById(existing); + } + + @Override + @Transactional + public void updatePassword(Long userId, com.hertz.system.dto.AuthDtos.UpdatePasswordRequest req) { + var existing = userMapper.selectById(userId); + if (existing == null) { + throw new BusinessException(404, "用户不存在"); + } + if (!passwordEncoder.matches(req.oldPassword(), existing.getPassword())) { + throw new BusinessException(400, "旧密码错误"); + } + existing.setPassword(passwordEncoder.encode(req.newPassword())); + existing.setUpdatedAt(java.time.LocalDateTime.now()); + userMapper.updateById(existing); + } + + @Override + @Transactional + public void deleteUser(Long id) { + userMapper.deleteById(id); + userRoleMapper.delete(new LambdaQueryWrapper().eq(SysUserRole::getUserId, id)); + } + + @Override + @Transactional + public void updateUserRoles(long userId, List roleIds) { + userRoleMapper.delete(new LambdaQueryWrapper().eq(SysUserRole::getUserId, userId)); + if (roleIds == null || roleIds.isEmpty()) { + return; + } + for (var roleId : roleIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList())) { + var ur = new SysUserRole(); + ur.setUserId(userId); + ur.setRoleId(roleId); + userRoleMapper.insert(ur); + } + } + + @Override + public List getUserRoleIds(long userId) { + var list = userRoleMapper.selectList(new LambdaQueryWrapper().eq(SysUserRole::getUserId, userId)); + return list.stream().map(SysUserRole::getRoleId).filter(Objects::nonNull).distinct().toList(); + } + + @Override + public List getUserRoles(long userId) { + return roleMapper.selectRolesByUserId(userId); + } +} + diff --git a/hertz_springboot/src/main/resources/application.yml b/hertz_springboot/src/main/resources/application.yml new file mode 100644 index 0000000..c4009ba --- /dev/null +++ b/hertz_springboot/src/main/resources/application.yml @@ -0,0 +1,30 @@ +server: + port: 8080 + +spring: + application: + name: Hertz-Springboot + sql: + init: + mode: never # 禁用 SQL 初始化(不自动执行 schema.sql / data.sql)可选:always(默认)、never + servlet: + multipart: + max-file-size: 2MB + max-request-size: 10MB + datasource: + url: jdbc:mysql://localhost:3306/hertz_springboot?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai + username: root + password: 123456 + driver-class-name: com.mysql.cj.jdbc.Driver + +mybatis-plus: + configuration: + map-underscore-to-camel-case: true + +app: + jwt: + secret: change-me-to-a-long-random-string-change-me-to-a-long-random-string + expire-seconds: 86400 + upload: + root-path: d:\LocalFile\hertz_springboot\uploads + avatar-path: avatar/ diff --git a/hertz_springboot/src/main/resources/schema.sql b/hertz_springboot/src/main/resources/schema.sql new file mode 100644 index 0000000..ef388e8 --- /dev/null +++ b/hertz_springboot/src/main/resources/schema.sql @@ -0,0 +1,107 @@ +-- ---------------------------- +-- Table structure for sys_user +-- ---------------------------- +DROP TABLE IF EXISTS `sys_user`; +CREATE TABLE `sys_user` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `username` varchar(50) NOT NULL COMMENT '用户名', + `password` varchar(100) NOT NULL COMMENT '加密密码', + `nickname` varchar(50) NOT NULL COMMENT '用户昵称', + `avatar_path` varchar(255) DEFAULT NULL COMMENT '头像路径', + `phone` varchar(20) DEFAULT NULL COMMENT '手机号', + `email` varchar(100) DEFAULT NULL COMMENT '邮箱', + `gender` tinyint(1) DEFAULT '0' COMMENT '0-未知 1-男 2-女', + `status` tinyint DEFAULT '1' COMMENT '0-禁用 1-启用', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`), + KEY `idx_status` (`status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; + +-- ---------------------------- +-- Table structure for sys_role +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role`; +CREATE TABLE `sys_role` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID', + `role_key` varchar(50) NOT NULL COMMENT '角色标识', + `role_name` varchar(50) NOT NULL COMMENT '角色名称', + `status` tinyint DEFAULT '1' COMMENT '0-禁用 1-启用', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_role_key` (`role_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统角色表'; + +-- ---------------------------- +-- Table structure for sys_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sys_menu`; +CREATE TABLE `sys_menu` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `parent_id` bigint DEFAULT '0' COMMENT '父菜单ID', + `type` varchar(10) NOT NULL COMMENT 'D-目录 M-菜单 B-按钮', + `name` varchar(50) NOT NULL COMMENT '菜单名称', + `path` varchar(200) DEFAULT NULL COMMENT '路由路径', + `component` varchar(200) DEFAULT NULL COMMENT '组件路径', + `perms` varchar(100) DEFAULT NULL COMMENT '权限标识', + `icon` varchar(100) DEFAULT NULL COMMENT '菜单图标', + `sort` int DEFAULT '0' COMMENT '排序', + `visible` tinyint DEFAULT '1' COMMENT '0-隐藏 1-显示', + `status` tinyint DEFAULT '1' COMMENT '0-禁用 1-启用', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单表'; + +-- ---------------------------- +-- Table structure for sys_user_role +-- ---------------------------- +DROP TABLE IF EXISTS `sys_user_role`; +CREATE TABLE `sys_user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `role_id` bigint NOT NULL COMMENT '角色ID', + PRIMARY KEY (`id`), + KEY `idx_user_role` (`user_id`,`role_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表'; + +-- ---------------------------- +-- Table structure for sys_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role_menu`; +CREATE TABLE `sys_role_menu` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `role_id` bigint NOT NULL COMMENT '角色ID', + `menu_id` bigint NOT NULL COMMENT '菜单ID', + PRIMARY KEY (`id`), + KEY `idx_role_menu` (`role_id`,`menu_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表'; + +-- ---------------------------- +-- Init Data +-- ---------------------------- +INSERT INTO `sys_role` (`id`, `role_key`, `role_name`) VALUES + (1, 'ADMIN', '管理员'); + +INSERT INTO `sys_menu` (`id`, `parent_id`, `type`, `name`, `path`, `component`, `perms`, `icon`, `sort`) VALUES + (1, 0, 'M', '仪表盘', '/admin/dashboard', 'admin/Dashboard', NULL, 'DataLine', 0), + (2, 0, 'D', '系统管理', '/admin/system', NULL, NULL, 'Setting', 10), + (3, 2, 'M', '用户管理', '/admin/system/user', 'admin/system/User', 'system:user:view', 'User', 0), + (4, 2, 'M', '角色管理', '/admin/system/role', 'admin/system/Role', 'system:role:view', 'Tickets', 1), + (5, 2, 'M', '菜单管理', '/admin/system/menu', 'admin/system/Menu', 'system:menu:view', 'Menu', 2); + +INSERT INTO `sys_user` (`id`, `username`, `password`, `nickname`, `phone`, `email`, `gender`, `status`) VALUES + (1, 'hertz', '$2a$10$Gker6.ggCxG3wfZ13rE/Eu7aDnB.DX2JmP6h6vct30RTtBr9.q5Pq', '管理员', '18888888888', 'hertz@hertz.com', 1, 1), + (2, 'demo', '$2a$10$PSIz9pWXAwXfB32HWSxTjeGhVi0bixsSKxzeX8YAdKnRRXPxJC3Xe', '普通用户', '13888888888', 'demo@hertz.com', 1, 1); + +INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES + (1, 1); + +INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES + (1, 1), + (1, 2), + (1, 3), + (1, 4), + (1, 5); diff --git a/hertz_springboot/src/test/java/com/hertz/HertzApplicationTests.java b/hertz_springboot/src/test/java/com/hertz/HertzApplicationTests.java new file mode 100644 index 0000000..6274ae5 --- /dev/null +++ b/hertz_springboot/src/test/java/com/hertz/HertzApplicationTests.java @@ -0,0 +1,13 @@ +package com.hertz; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class HertzApplicationTests { + + @Test + void contextLoads() { + } +} + diff --git a/hertz_springboot_ui/.env.development b/hertz_springboot_ui/.env.development new file mode 100644 index 0000000..e2da3fa --- /dev/null +++ b/hertz_springboot_ui/.env.development @@ -0,0 +1,2 @@ +VITE_API_BASE=http://localhost:8080 + diff --git a/hertz_springboot_ui/README.md b/hertz_springboot_ui/README.md new file mode 100644 index 0000000..33895ab --- /dev/null +++ b/hertz_springboot_ui/README.md @@ -0,0 +1,5 @@ +# Vue 3 + TypeScript + Vite + +This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` + + diff --git a/hertz_springboot_ui/jsconfig.json b/hertz_springboot_ui/jsconfig.json new file mode 100644 index 0000000..b8aada2 --- /dev/null +++ b/hertz_springboot_ui/jsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "moduleResolution": "bundler", + "strict": false, + "jsx": "preserve", + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "lib": ["esnext", "dom"], + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*.js", "src/**/*.vue", "vite.config.js"], + "exclude": ["node_modules"] +} diff --git a/hertz_springboot_ui/package-lock.json b/hertz_springboot_ui/package-lock.json new file mode 100644 index 0000000..66c359f --- /dev/null +++ b/hertz_springboot_ui/package-lock.json @@ -0,0 +1,2045 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.13.2", + "element-plus": "^2.13.1", + "pinia": "^3.0.4", + "vue": "^3.5.24", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "vite": "^7.2.4" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.3.tgz", + "integrity": "sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "1.0.0-beta.53" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.26.tgz", + "integrity": "sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.26", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.26.tgz", + "integrity": "sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.26.tgz", + "integrity": "sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.26", + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz", + "integrity": "sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz", + "integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.9" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", + "integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.9", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.9", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", + "integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.26.tgz", + "integrity": "sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.26.tgz", + "integrity": "sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/shared": "3.5.26" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.26.tgz", + "integrity": "sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.26", + "@vue/runtime-core": "3.5.26", + "@vue/shared": "3.5.26", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.26.tgz", + "integrity": "sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "vue": "3.5.26" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.26.tgz", + "integrity": "sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==", + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.1.tgz", + "integrity": "sha512-eG4BDBGdAsUGN6URH1PixzZb0ngdapLivIk1meghS1uEueLvQ3aljSKrCt5x6sYb6mUk8eGtzTQFgsPmLavQcA==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "^10.11.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/entities": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.22", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", + "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.26", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz", + "integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.26", + "@vue/compiler-sfc": "3.5.26", + "@vue/runtime-dom": "3.5.26", + "@vue/server-renderer": "3.5.26", + "@vue/shared": "3.5.26" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + } + } +} diff --git a/hertz_springboot_ui/package.json b/hertz_springboot_ui/package.json new file mode 100644 index 0000000..65f9064 --- /dev/null +++ b/hertz_springboot_ui/package.json @@ -0,0 +1,23 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.13.2", + "element-plus": "^2.13.1", + "pinia": "^3.0.4", + "vue": "^3.5.24", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^6.0.1", + "vite": "^7.2.4" + } +} diff --git a/hertz_springboot_ui/public/index.png b/hertz_springboot_ui/public/index.png new file mode 100644 index 0000000..2b2fed8 Binary files /dev/null and b/hertz_springboot_ui/public/index.png differ diff --git a/hertz_springboot_ui/public/index1.png b/hertz_springboot_ui/public/index1.png new file mode 100644 index 0000000..e34de08 Binary files /dev/null and b/hertz_springboot_ui/public/index1.png differ diff --git a/hertz_springboot_ui/public/logo.png b/hertz_springboot_ui/public/logo.png new file mode 100644 index 0000000..2040f6c Binary files /dev/null and b/hertz_springboot_ui/public/logo.png differ diff --git a/hertz_springboot_ui/src/App.vue b/hertz_springboot_ui/src/App.vue new file mode 100644 index 0000000..98240ae --- /dev/null +++ b/hertz_springboot_ui/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/hertz_springboot_ui/src/api/auth.js b/hertz_springboot_ui/src/api/auth.js new file mode 100644 index 0000000..1f5583f --- /dev/null +++ b/hertz_springboot_ui/src/api/auth.js @@ -0,0 +1,24 @@ +import { http } from './http' + +export async function login(req) { + const { data } = await http.post('/api/auth/login', req) + return data.data +} + +export async function register(req) { + const { data } = await http.post('/api/auth/register', req) + return data.data +} + +export async function me() { + const { data } = await http.get('/api/auth/me') + return data.data +} + +export async function updateProfile(data) { + await http.post('/api/auth/profile', data) +} + +export async function updatePassword(data) { + await http.post('/api/auth/password', data) +} diff --git a/hertz_springboot_ui/src/api/common.js b/hertz_springboot_ui/src/api/common.js new file mode 100644 index 0000000..5ea8fd5 --- /dev/null +++ b/hertz_springboot_ui/src/api/common.js @@ -0,0 +1,12 @@ +import { http } from './http' + +export async function uploadFile(file) { + const formData = new FormData() + formData.append('file', file) + const { data } = await http.post('/api/common/file/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }) + return data.data +} diff --git a/hertz_springboot_ui/src/api/http.js b/hertz_springboot_ui/src/api/http.js new file mode 100644 index 0000000..57b7e44 --- /dev/null +++ b/hertz_springboot_ui/src/api/http.js @@ -0,0 +1,39 @@ +import axios from 'axios' +import { useAuthStore } from '../stores/auth' + +export const http = axios.create({ + baseURL: import.meta.env.VITE_API_BASE || 'http://localhost:8080', + timeout: 15000, +}) + +http.interceptors.request.use((config) => { + const auth = useAuthStore() + if (auth.token) { + config.headers = config.headers ?? {} + config.headers.Authorization = `Bearer ${auth.token}` + } + return config +}) + +http.interceptors.response.use( + (resp) => resp, + (err) => { + if (err?.response?.status === 401) { + const auth = useAuthStore() + auth.logout() + } + if (err?.response?.status === 403) { + import('../router') + .then(({ default: router }) => { + const current = router.currentRoute.value + if (current.path !== '/403') { + router.replace({ path: '/403', query: { from: current.fullPath } }) + } + }) + .catch(() => { + if (window.location.pathname !== '/403') window.location.replace('/403') + }) + } + return Promise.reject(err) + }, +) diff --git a/hertz_springboot_ui/src/api/system.js b/hertz_springboot_ui/src/api/system.js new file mode 100644 index 0000000..b0c6b62 --- /dev/null +++ b/hertz_springboot_ui/src/api/system.js @@ -0,0 +1,81 @@ +import { http } from './http' + +export async function fetchMenuTree() { + const { data } = await http.get('/api/system/menus/tree') + return data.data +} + +export async function pageMenus(params) { + const { data } = await http.get('/api/system/menus/page', { params }) + return data.data +} + +export async function createMenu(data) { + await http.post('/api/system/menus', data) +} + +export async function updateMenu(data) { + await http.put('/api/system/menus', data) +} + +export async function deleteMenu(id) { + await http.delete(`/api/system/menus/${id}`) +} + +export async function fetchUsers(params) { + const { data } = await http.get('/api/system/users', { params }) + return data.data +} + +export async function createUser(data) { + await http.post('/api/system/users', data) +} + +export async function updateUser(data) { + await http.put('/api/system/users', data) +} + +export async function deleteUser(id) { + await http.delete(`/api/system/users/${id}`) +} + +export async function fetchRoles() { + const { data } = await http.get('/api/system/roles') + return data.data +} + +export async function pageRoles(params) { + const { data } = await http.get('/api/system/roles/page', { params }) + return data.data +} + +export async function createRole(data) { + await http.post('/api/system/roles', data) +} + +export async function updateRole(data) { + await http.put('/api/system/roles', data) +} + +export async function deleteRole(id) { + await http.delete(`/api/system/roles/${id}`) +} + +export async function fetchRoleMenuIds(roleId) { + const { data } = await http.get(`/api/system/roles/${roleId}/menus`) + return data.data +} + +export async function updateRoleMenus(roleId, menuIds) { + await http.put(`/api/system/roles/${roleId}/menus`, { menuIds }) +} + +export async function fetchUserRoleIds(userId) { + const { data } = await http.get(`/api/system/users/${userId}/roles`) + return data.data +} + +export async function updateUserRoles(userId, roleIds) { + const { data } = await http.put(`/api/system/users/${userId}/roles`, { roleIds }) + return data.data +} diff --git a/hertz_springboot_ui/src/assets/img/profile_bg.jpg b/hertz_springboot_ui/src/assets/img/profile_bg.jpg new file mode 100644 index 0000000..2af007b Binary files /dev/null and b/hertz_springboot_ui/src/assets/img/profile_bg.jpg differ diff --git a/hertz_springboot_ui/src/assets/style.css b/hertz_springboot_ui/src/assets/style.css new file mode 100644 index 0000000..0ff6052 --- /dev/null +++ b/hertz_springboot_ui/src/assets/style.css @@ -0,0 +1,242 @@ +:root { + /* iOS Color Palette */ + --ios-primary: #007AFF; + --ios-primary-light: #47a0ff; + --ios-success: #34C759; + --ios-warning: #FF9500; + --ios-danger: #FF3B30; + --ios-gray: #8E8E93; + --ios-bg: #F2F4F8; + --ios-surface: rgba(255, 255, 255, 0.7); + --ios-border: rgba(0, 0, 0, 0.05); + + /* Shapes & Shadows */ + --ios-radius: 16px; + --ios-shadow: 0 4px 24px rgba(0, 0, 0, 0.04); + --ios-shadow-hover: 0 8px 32px rgba(0, 0, 0, 0.08); + + /* Fonts */ + font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Element Plus Overrides Variables */ + --el-color-primary: var(--ios-primary); + --el-border-radius-base: 8px; + --el-border-radius-small: 6px; + --el-border-radius-round: 20px; + --el-bg-color-page: var(--ios-bg); +} + +body { + margin: 0; + padding: 0; + background-color: var(--ios-bg); + color: #1d1d1f; + font-size: 14px; + line-height: 1.5; +} + +/* Glassmorphism Utilities */ +.glass { + background: var(--ios-surface); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.3); +} + +/* Card Styling */ +.el-card, .ios-card { + border: none !important; + border-radius: var(--ios-radius) !important; + background: white; + transition: all 0.3s ease; +} + + +.el-card__header { + border-bottom: 1px solid var(--ios-border) !important; + padding: 16px 24px !important; + font-weight: 600; + font-size: 16px; +} + +/* Button Styling */ +.el-button { + border-radius: 10px !important; /* Pill shape */ + font-weight: 500 !important; + transition: all 0.2s ease !important; +} + +.el-button:not(.is-circle):not(.is-text) { + height: 36px !important; + padding: 0 20px !important; +} + + +/* Input Styling */ +.el-input__wrapper { + height: 36px !important; + border-radius: 10px !important; + padding: 0 12px !important; + background-color: rgba(255,255,255,0.8) !important; + transition: all 0.2s ease; +} + +.el-input__wrapper.is-focus { + background-color: white !important; +} + +/* Table Styling */ +.el-table { + --el-table-border-color: var(--ios-border); + --el-table-header-bg-color: transparent; + background-color: transparent !important; + border-radius: var(--ios-radius); + overflow: hidden; +} + +.el-table th.el-table__cell { + background-color: #fafafa !important; + font-weight: 600; + color: #86868b; + border-bottom: 1px solid var(--ios-border) !important; +} + +.el-table td.el-table__cell { + border-bottom: 1px solid var(--ios-border) !important; +} + +.el-table--enable-row-hover .el-table__body tr:hover > td.el-table__cell { + background-color: rgba(0, 122, 255, 0.03) !important; +} + +/* Dialog Styling */ +.el-dialog { + border-radius: 20px !important; + overflow: hidden; +} + +.el-dialog__header { + margin-right: 0 !important; + padding: 20px 24px !important; +} + +/* Dropdown (User menu) */ +.ios-dropdown { + min-width: 220px; + padding: 10px !important; +} + +.ios-dropdown-popper { + margin-top: 8px !important; +} + +.ios-dropdown-popper.el-popper { + border-radius: 18px !important; + border: 1px solid var(--ios-border) !important; + box-shadow: var(--ios-shadow) !important; + overflow: hidden !important; + padding: 0 !important; + background: transparent !important; +} + +.ios-dropdown-popper .el-popper__content { + border-radius: 18px !important; + overflow: hidden !important; + padding: 0 !important; + background: rgba(255, 255, 255, 0.98) !important; +} + +.ios-dropdown-popper .el-popper__arrow { + display: none !important; +} + +.ios-dropdown .el-dropdown-menu__item { + height: 44px; + line-height: 44px; + border-radius: 14px; + margin: 2px 0; + padding: 0 12px; + display: flex; + align-items: center; + gap: 10px; + color: #1d1d1f; +} + +.ios-dropdown .el-dropdown-menu__item:not(.is-disabled):hover { + background-color: rgba(0, 0, 0, 0.04) !important; +} + +.ios-dropdown .el-dropdown-menu__item.is-disabled { + cursor: default; + color: inherit; + opacity: 1; +} + +.ios-dropdown .el-dropdown-menu__item.is-divided { + margin-top: 10px; +} + +.ios-dropdown .el-dropdown-menu__item.is-divided::before { + height: 1px; + background-color: var(--ios-border); + left: 10px; + right: 10px; + top: -6px; +} + +.ios-dropdown .ios-user-card { + display: flex; + align-items: center; + gap: 12px; + width: 100%; + padding: 4px 2px; +} + +.ios-dropdown .ios-user-meta { + display: flex; + flex-direction: column; + min-width: 0; +} + +.ios-dropdown .ios-user-name { + font-weight: 700; + font-size: 15px; + line-height: 20px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ios-dropdown .ios-user-email { + font-size: 12px; + color: #8E8E93; + line-height: 16px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ios-dropdown .ios-logout-item { + justify-content: center; + border: 1px solid rgba(0, 0, 0, 0.08); + background: white; +} + +.ios-dropdown .ios-logout-item:hover { + background-color: rgba(0, 0, 0, 0.02) !important; +} + +.ios-dropdown .el-icon { + font-size: 16px; +} + +/* Scrollbar Hiding (Global) */ +::-webkit-scrollbar { + display: none; +} +html, body { + scrollbar-width: none; + -ms-overflow-style: none; +} diff --git a/hertz_springboot_ui/src/components/Error.vue b/hertz_springboot_ui/src/components/Error.vue new file mode 100644 index 0000000..bd393bb --- /dev/null +++ b/hertz_springboot_ui/src/components/Error.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/hertz_springboot_ui/src/layouts/AdminLayout.vue b/hertz_springboot_ui/src/layouts/AdminLayout.vue new file mode 100644 index 0000000..5d51c43 --- /dev/null +++ b/hertz_springboot_ui/src/layouts/AdminLayout.vue @@ -0,0 +1,236 @@ + + + + + diff --git a/hertz_springboot_ui/src/layouts/PortalLayout.vue b/hertz_springboot_ui/src/layouts/PortalLayout.vue new file mode 100644 index 0000000..ba34b93 --- /dev/null +++ b/hertz_springboot_ui/src/layouts/PortalLayout.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/hertz_springboot_ui/src/main.js b/hertz_springboot_ui/src/main.js new file mode 100644 index 0000000..bdf847e --- /dev/null +++ b/hertz_springboot_ui/src/main.js @@ -0,0 +1,22 @@ +import { createApp } from 'vue' +import App from './App.vue' +import { createPinia } from 'pinia' +import router from './router' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import zhCn from 'element-plus/es/locale/lang/zh-cn' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import './assets/style.css' + +const app = createApp(App) +app.use(createPinia()) +app.use(router) +app.use(ElementPlus, { + locale: zhCn, +}) + +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.mount('#app') diff --git a/hertz_springboot_ui/src/router/index.js b/hertz_springboot_ui/src/router/index.js new file mode 100644 index 0000000..0d16a15 --- /dev/null +++ b/hertz_springboot_ui/src/router/index.js @@ -0,0 +1,59 @@ +import { createRouter, createWebHistory } from 'vue-router' +import PortalLayout from '../layouts/PortalLayout.vue' +import AdminLayout from '../layouts/AdminLayout.vue' +import Login from '../views/Login.vue' +import Register from '../views/Register.vue' +import { setupGuards } from './setupGuards' + +export const ROUTE_NAMES = { + AdminRoot: 'AdminRoot', + AdminLayout: 'AdminLayout', +} + +const routes = [ + { path: '/', redirect: '/portal/home' }, + { path: '/login', component: Login }, + { path: '/register', component: Register }, + { + path: '/403', + component: () => import('../components/Error.vue'), + props: { + code: 403, + title: '抱歉,您没有权限访问该页面', + subTitle: '请联系管理员开通权限,或切换账号后重试。', + }, + }, + { + path: '/portal', + component: PortalLayout, + children: [ + { path: '', redirect: '/portal/home' }, + { path: 'home', component: () => import('../views/portal/Home.vue') }, + { path: 'about', component: () => import('../views/portal/About.vue') }, + { path: 'profile', component: () => import('../views/Profile.vue') }, + ], + }, + { + path: '/admin', + name: ROUTE_NAMES.AdminLayout, + component: AdminLayout, + children: [ + { path: '', name: ROUTE_NAMES.AdminRoot, redirect: '/admin/dashboard' }, + { path: 'profile', component: () => import('../views/Profile.vue') }, + ], + }, + { + path: '/:pathMatch(.*)*', + component: () => import('../components/Error.vue'), + props: { code: 404 }, + }, +] + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +setupGuards(router) + +export default router diff --git a/hertz_springboot_ui/src/router/setupGuards.js b/hertz_springboot_ui/src/router/setupGuards.js new file mode 100644 index 0000000..32af692 --- /dev/null +++ b/hertz_springboot_ui/src/router/setupGuards.js @@ -0,0 +1,40 @@ +import { useAuthStore } from '../stores/auth' +import { useMenuStore } from '../stores/menu' +import { buildAdminRoutesFromMenus } from './utils' +import { ROUTE_NAMES } from './index' + +export function setupGuards(router) { + router.beforeEach(async (to) => { + const auth = useAuthStore() + const menu = useMenuStore() + + const isAdminPath = to.path.startsWith('/admin') + + if (!auth.token) { + if (isAdminPath) return { path: '/login', query: { redirect: to.fullPath } } + return true + } + + if (!auth.meLoaded) { + await auth.fetchMe() + } + + if (auth.roles.length === 0) { + if (isAdminPath) return '/portal/home' + return true + } + + if (to.path === '/login' || to.path === '/register') return '/admin' + + if (isAdminPath && !menu.routesLoaded) { + await menu.fetchMenus() + const adminRoutes = buildAdminRoutesFromMenus(menu.menuTree) + for (const r of adminRoutes) router.addRoute(ROUTE_NAMES.AdminLayout, r) + menu.routesLoaded = true + return { ...to, replace: true } + } + + return true + }) +} + diff --git a/hertz_springboot_ui/src/router/utils.js b/hertz_springboot_ui/src/router/utils.js new file mode 100644 index 0000000..b1f0c3e --- /dev/null +++ b/hertz_springboot_ui/src/router/utils.js @@ -0,0 +1,41 @@ + +const viewModules = import.meta.glob('../views/**/*.vue') + +function resolveView(component) { + if (!component) return undefined + const key = `../views/${component}.vue` + const loader = viewModules[key] + if (!loader) return undefined + return loader +} + +export function buildAdminRoutesFromMenus(menus) { + const routes = [] + + const normalizeAdminChildPath = (rawPath) => { + if (!rawPath) return '' + if (rawPath.startsWith('/admin/')) return rawPath.slice('/admin/'.length) + if (rawPath === '/admin') return '' + return rawPath.startsWith('/') ? rawPath.slice(1) : rawPath + } + + const walk = (nodes) => { + for (const n of nodes) { + if (n.type === 'M') { + const component = resolveView(n.component) + if (!component) continue + const childPath = normalizeAdminChildPath(n.path || '') + if (!childPath) continue + routes.push({ + path: childPath, + component, + meta: { title: n.name, icon: n.icon }, + }) + } + if (n.children?.length) walk(n.children) + } + } + + walk(menus) + return routes +} diff --git a/hertz_springboot_ui/src/stores/auth.js b/hertz_springboot_ui/src/stores/auth.js new file mode 100644 index 0000000..c48237f --- /dev/null +++ b/hertz_springboot_ui/src/stores/auth.js @@ -0,0 +1,57 @@ +import { defineStore } from 'pinia' +import { login, me, register } from '../api/auth' + +export const useAuthStore = defineStore('auth', { + state: () => ({ + token: localStorage.getItem('token') || '', + meLoaded: false, + userId: 0, + username: '', + nickname: '', + avatarPath: '', + phone: '', + email: '', + gender: 0, + roles: [], + }), + actions: { + async login(req) { + const res = await login(req) + this.token = res.token + localStorage.setItem('token', this.token) + // Login response only has limited info, fetch full profile + await this.fetchMe() + return res + }, + async register(req) { + return register(req) + }, + async fetchMe() { + const res = await me() + this.userId = res.userId + this.username = res.username + this.nickname = res.nickname || '' + this.avatarPath = res.avatarPath || '' + this.phone = res.phone || '' + this.email = res.email || '' + this.gender = res.gender || 0 + this.roles = res.roles || [] + this.meLoaded = true + return res + }, + logout() { + this.token = '' + localStorage.clear() + this.meLoaded = false + this.userId = 0 + this.username = '' + this.nickname = '' + this.avatarPath = '' + this.phone = '' + this.email = '' + this.gender = 0 + this.roles = [] + }, + }, +}) + diff --git a/hertz_springboot_ui/src/stores/menu.js b/hertz_springboot_ui/src/stores/menu.js new file mode 100644 index 0000000..82c9bb5 --- /dev/null +++ b/hertz_springboot_ui/src/stores/menu.js @@ -0,0 +1,20 @@ +import { defineStore } from 'pinia' +import { fetchMenuTree } from '../api/system' + +export const useMenuStore = defineStore('menu', { + state: () => ({ + menuTree: [], + routesLoaded: false, + }), + actions: { + async fetchMenus() { + this.menuTree = await fetchMenuTree() + return this.menuTree + }, + reset() { + this.menuTree = [] + this.routesLoaded = false + }, + }, +}) + diff --git a/hertz_springboot_ui/src/views/Login.vue b/hertz_springboot_ui/src/views/Login.vue new file mode 100644 index 0000000..47f082d --- /dev/null +++ b/hertz_springboot_ui/src/views/Login.vue @@ -0,0 +1,122 @@ + + + + + + + + diff --git a/hertz_springboot_ui/src/views/Profile.vue b/hertz_springboot_ui/src/views/Profile.vue new file mode 100644 index 0000000..58dbf49 --- /dev/null +++ b/hertz_springboot_ui/src/views/Profile.vue @@ -0,0 +1,376 @@ + + + + + + + diff --git a/hertz_springboot_ui/src/views/Register.vue b/hertz_springboot_ui/src/views/Register.vue new file mode 100644 index 0000000..3d7b87a --- /dev/null +++ b/hertz_springboot_ui/src/views/Register.vue @@ -0,0 +1,121 @@ + + + + + + + + diff --git a/hertz_springboot_ui/src/views/admin/Dashboard.vue b/hertz_springboot_ui/src/views/admin/Dashboard.vue new file mode 100644 index 0000000..29629ea --- /dev/null +++ b/hertz_springboot_ui/src/views/admin/Dashboard.vue @@ -0,0 +1,389 @@ + + + + + diff --git a/hertz_springboot_ui/src/views/admin/system/Menu.vue b/hertz_springboot_ui/src/views/admin/system/Menu.vue new file mode 100644 index 0000000..7d8fdf9 --- /dev/null +++ b/hertz_springboot_ui/src/views/admin/system/Menu.vue @@ -0,0 +1,269 @@ + + + + diff --git a/hertz_springboot_ui/src/views/admin/system/Role.vue b/hertz_springboot_ui/src/views/admin/system/Role.vue new file mode 100644 index 0000000..7e759f1 --- /dev/null +++ b/hertz_springboot_ui/src/views/admin/system/Role.vue @@ -0,0 +1,246 @@ + + + + diff --git a/hertz_springboot_ui/src/views/admin/system/User.vue b/hertz_springboot_ui/src/views/admin/system/User.vue new file mode 100644 index 0000000..5b618a8 --- /dev/null +++ b/hertz_springboot_ui/src/views/admin/system/User.vue @@ -0,0 +1,347 @@ + + + + + diff --git a/hertz_springboot_ui/src/views/portal/About.vue b/hertz_springboot_ui/src/views/portal/About.vue new file mode 100644 index 0000000..901b1b5 --- /dev/null +++ b/hertz_springboot_ui/src/views/portal/About.vue @@ -0,0 +1,8 @@ + + + \ No newline at end of file diff --git a/hertz_springboot_ui/src/views/portal/Home.vue b/hertz_springboot_ui/src/views/portal/Home.vue new file mode 100644 index 0000000..4fc6c60 --- /dev/null +++ b/hertz_springboot_ui/src/views/portal/Home.vue @@ -0,0 +1,88 @@ + + + + + + diff --git a/hertz_springboot_ui/vite.config.js b/hertz_springboot_ui/vite.config.js new file mode 100644 index 0000000..59e16fd --- /dev/null +++ b/hertz_springboot_ui/vite.config.js @@ -0,0 +1,13 @@ +import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [vue()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)) + } + } +}) diff --git a/数据库说明文档.md b/数据库说明文档.md new file mode 100644 index 0000000..9dd9c50 --- /dev/null +++ b/数据库说明文档.md @@ -0,0 +1,125 @@ +# Hertz 权限管理系统数据库说明文档 + +## 1. 数据库概述 + +- **数据库名称**: `hertz_springboot` +- **数据库类型**: MySQL +- **字符集**: utf8mb4 +- **排序规则**: utf8mb4_general_ci (推荐) + +## 2. ER 图设计概要 + +系统主要包含 5 张表,采用标准的 RBAC(用户-角色-权限)设计模型: + +- `sys_user`: 用户表 +- `sys_role`: 角色表 +- `sys_menu`: 菜单/权限表 +- `sys_user_role`: 用户-角色关联表 +- `sys_role_menu`: 角色-菜单关联表 + +## 3. 表结构详解 + +### 3.1 系统用户表 (sys_user) + +存储系统的登录用户信息。 + +| 字段名 | 类型 | 长度 | 允许空 | 默认值 | 说明 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **id** | bigint | 20 | NO | AUTO_INCREMENT | 主键 ID | +| username | varchar | 50 | NO | | 用户名 (唯一) | +| password | varchar | 100 | NO | | 加密密码 (BCrypt) | +| nickname | varchar | 50 | NO | | 用户昵称 | +| avatar_path | varchar | 255 | YES | NULL | 头像路径 | +| phone | varchar | 20 | YES | NULL | 手机号 | +| email | varchar | 100 | YES | NULL | 邮箱 | +| gender | tinyint | 1 | YES | 0 | 0-未知 1-男 2-女 | +| status | tinyint | 4 | YES | 1 | 0-禁用 1-启用 | +| created_at | datetime | | YES | CURRENT_TIMESTAMP | 创建时间 | +| updated_at | datetime | | YES | CURRENT_TIMESTAMP | 更新时间 | + +### 3.2 系统角色表 (sys_role) + +存储系统的角色信息。 + +| 字段名 | 类型 | 长度 | 允许空 | 默认值 | 说明 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **id** | bigint | 20 | NO | AUTO_INCREMENT | 主键 ID | +| role_key | varchar | 50 | NO | | 角色标识 (唯一,如 ADMIN) | +| role_name | varchar | 50 | NO | | 角色名称 (如 管理员) | +| status | tinyint | 4 | YES | 1 | 0-禁用 1-启用 | +| created_at | datetime | | YES | CURRENT_TIMESTAMP | 创建时间 | +| updated_at | datetime | | YES | CURRENT_TIMESTAMP | 更新时间 | + +### 3.3 系统菜单表 (sys_menu) + +存储菜单和按钮权限信息,支持树形结构。 + +| 字段名 | 类型 | 长度 | 允许空 | 默认值 | 说明 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **id** | bigint | 20 | NO | AUTO_INCREMENT | 主键 ID | +| parent_id | bigint | 20 | YES | 0 | 父菜单 ID (0为顶级) | +| type | varchar | 10 | NO | | 菜单类型: D-目录 M-菜单 B-按钮 | +| name | varchar | 50 | NO | | 菜单名称/按钮名称 | +| path | varchar | 200 | YES | NULL | 路由路径 (前端路由) | +| component | varchar | 200 | YES | NULL | 组件路径 (Vue组件) | +| perms | varchar | 100 | YES | NULL | 权限标识 (如 system:user:view) | +| icon | varchar | 100 | YES | NULL | 菜单图标 | +| sort | int | 11 | YES | 0 | 排序 (数值越小越靠前) | +| visible | tinyint | 4 | YES | 1 | 0-隐藏 1-显示 | +| status | tinyint | 4 | YES | 1 | 0-禁用 1-启用 | +| created_at | datetime | | YES | CURRENT_TIMESTAMP | 创建时间 | +| updated_at | datetime | | YES | CURRENT_TIMESTAMP | 更新时间 | + +### 3.4 用户角色关联表 (sys_user_role) + +用户与角色的多对多关系表。 + +| 字段名 | 类型 | 长度 | 允许空 | 默认值 | 说明 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **id** | bigint | 20 | NO | AUTO_INCREMENT | 主键 ID | +| user_id | bigint | 20 | NO | | 用户 ID | +| role_id | bigint | 20 | NO | | 角色 ID | + +### 3.5 角色菜单关联表 (sys_role_menu) + +角色与菜单(权限)的多对多关系表。 + +| 字段名 | 类型 | 长度 | 允许空 | 默认值 | 说明 | +| :--- | :--- | :--- | :--- | :--- | :--- | +| **id** | bigint | 20 | NO | AUTO_INCREMENT | 主键 ID | +| role_id | bigint | 20 | NO | | 角色 ID | +| menu_id | bigint | 20 | NO | | 菜单 ID | + +## 4. 初始数据说明 + +系统初始化脚本 (`schema.sql`) 会预置以下数据: + +1. **初始角色**: + - `ADMIN`: 超级管理员,拥有所有权限。 + +2. **初始菜单结构**: + - 仪表盘 + - 系统管理 + - 用户管理 + - 角色管理 + - 菜单管理 + +3. **初始用户**: + - `hertz`: 管理员账号,已绑定 ADMIN 角色。 + - `demo`: 演示账号。 + +## 5. 数据字典与枚举 + +- **用户/角色/菜单状态 (status)**: + - `1`: 启用 (Normal) + - `0`: 禁用 (Disabled) + +- **菜单类型 (type)**: + - `D`: 目录 (Directory) - 不对应具体页面,仅用于分组 + - `M`: 菜单 (Menu) - 对应具体的前端页面 + - `B`: 按钮 (Button) - 页面内的功能按钮,用于权限控制 + +- **性别 (gender)**: + - `0`: 未知 + - `1`: 男 + - `2`: 女 diff --git a/项目说明文档.md b/项目说明文档.md new file mode 100644 index 0000000..bc36ae6 --- /dev/null +++ b/项目说明文档.md @@ -0,0 +1,108 @@ +# Hertz 权限管理系统项目说明文档 + +## 1. 项目简介 + +Hertz 权限管理系统是一个基于前后端分离架构的轻量级权限管理平台。系统集成了用户管理、角色管理、菜单管理等核心功能,采用 RBAC(Role-Based Access Control)模型实现细粒度的权限控制。 + +### 1.1 项目结构 + +项目采用典型的多模块(或目录分离)结构: + +- **hertz_springboot**: 后端工程,基于 Spring Boot 3 + MyBatis-Plus。 +- **hertz_springboot_ui**: 前端工程,基于 Vue 3 + Vite + Element Plus。 + +## 2. 技术栈 + +### 2.1 后端技术栈 (hertz_springboot) + +- **核心框架**: Spring Boot 3.4.1 +- **持久层框架**: MyBatis-Plus 3.5.8 +- **安全框架**: Spring Security + JJWT 0.12.6 (实现无状态 JWT 认证) +- **数据库连接池**: HikariCP (Spring Boot 默认) +- **数据库驱动**: MySQL Connector/J +- **工具库**: Lombok +- **运行环境**: Java 21 + +### 2.2 前端技术栈 (hertz_springboot_ui) + +- **核心框架**: Vue 3.5.24 +- **构建工具**: Vite 7.2.4 +- **UI 组件库**: Element Plus 2.13.1 +- **状态管理**: Pinia 3.0.4 +- **路由管理**: Vue Router 4.6.4 +- **HTTP 客户端**: Axios 1.13.2 + +## 3. 功能模块 + +1. **认证模块**: 支持用户登录、注册(可选)、JWT Token 颁发与验证。 +2. **系统管理**: + - **用户管理**: 用户的增删改查、分配角色、状态控制。 + - **角色管理**: 角色的增删改查、分配菜单权限。 + - **菜单管理**: 动态菜单配置,支持目录、菜单、按钮三种类型,支持权限标识配置。 +3. **个人中心**: 用户资料修改、密码修改、头像上传。 + +## 4. 快速开始 + +### 4.1 环境准备 + +- JDK 21+ +- Node.js 18+ +- MySQL 8.0+ + +### 4.2 后端启动 + +1. 进入后端目录: + ```bash + cd hertz_springboot + ``` +2. 配置数据库: + - 创建数据库 `hertz_springboot`。 + - 导入 `src/main/resources/schema.sql` 初始化表结构和数据。 + - 修改 `src/main/resources/application.yml` 中的数据库连接信息(url, username, password)。 +3. 运行项目: + ```bash + mvn spring-boot:run + ``` + 或者在 IDE 中运行 `HertzApplication.java`。 + 后端服务将启动在 `http://localhost:8080`。 + +### 4.3 前端启动 + +1. 进入前端目录: + ```bash + cd hertz_springboot_ui + ``` +2. 安装依赖: + ```bash + npm install + ``` +3. 启动开发服务器: + ```bash + npm run dev + ``` + 前端服务通常启动在 `http://localhost:5173`。 + +## 5. 默认账号 + +初始化 SQL 脚本中包含以下默认账号(密码均为 `123456`): + +- **管理员**: `hertz` +- **普通用户**: `demo` + +## 6. 配置说明 + +### 6.1 后端配置 (application.yml) + +- **Server Port**: 8080 +- **File Upload**: + - 最大文件大小: 2MB + - 最大请求大小: 10MB + - 上传根路径: `d:\LocalFile\hertz_springboot\uploads` (请根据实际环境修改) +- **JWT**: + - 密钥: `app.jwt.secret` (建议在生产环境中修改为强随机字符串) + - 过期时间: 86400秒 (24小时) + +## 7. 注意事项 + +- **文件上传**: 默认配置了本地文件存储路径,请确保该路径存在或有写入权限,或者在 `application.yml` 中修改为合适的路径。 +- **跨域**: 前端开发环境通常通过 Vite 代理解决跨域问题,生产环境需配置 Nginx 或后端 CORS。