From 7531b6c466b409331d985ce200702e7425028301 Mon Sep 17 00:00:00 2001 From: liu <362165265@qq.com> Date: Thu, 29 Jan 2026 18:26:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 93 +- backend/pom.xml | 89 + .../java/com/music/MangToolApplication.java | 15 + .../main/java/com/music/common/Result.java | 39 + .../java/com/music/config/CorsConfig.java | 25 + .../com/music/config/WebSocketConfig.java | 26 + .../music/controller/AggregateController.java | 67 + .../music/controller/ConvertController.java | 59 + .../com/music/controller/DedupController.java | 72 + .../music/controller/HealthController.java | 15 + .../music/controller/ProgressController.java | 23 + .../music/controller/ZhConvertController.java | 72 + .../java/com/music/dto/AggregateRequest.java | 21 + .../java/com/music/dto/ConvertRequest.java | 21 + .../main/java/com/music/dto/DedupRequest.java | 41 + .../java/com/music/dto/ProgressMessage.java | 18 + .../java/com/music/dto/ZhConvertRequest.java | 44 + .../music/exception/BusinessException.java | 16 + .../exception/GlobalExceptionHandler.java | 32 + .../com/music/service/AggregatorService.java | 202 ++ .../com/music/service/ConvertService.java | 219 ++ .../java/com/music/service/DedupService.java | 454 ++++ .../java/com/music/service/ProgressStore.java | 30 + .../service/TraditionalFilterService.java | 147 ++ .../com/music/service/ZhConvertService.java | 286 +++ backend/src/main/resources/application.yml | 16 + frontend/index.html | 19 + frontend/package-lock.json | 2085 +++++++++++++++++ frontend/package.json | 24 + frontend/src/App.vue | 183 ++ frontend/src/api/aggregate.ts | 18 + frontend/src/api/convert.ts | 18 + frontend/src/api/dedup.ts | 21 + frontend/src/api/progress.ts | 18 + frontend/src/api/request.ts | 36 + frontend/src/api/zhconvert.ts | 20 + frontend/src/components/AggregateTab.vue | 526 +++++ frontend/src/components/ConvertTab.vue | 638 +++++ frontend/src/components/DedupTab.vue | 516 ++++ frontend/src/components/MergeTab.vue | 96 + frontend/src/components/RenameTab.vue | 105 + frontend/src/components/SettingsTab.vue | 90 + .../src/components/TraditionalFilterTab.vue | 554 +++++ frontend/src/composables/useWebSocket.ts | 100 + frontend/src/main.ts | 14 + frontend/src/styles/reset.css | 26 + frontend/vite.config.mts | 24 + 47 files changed, 7257 insertions(+), 16 deletions(-) create mode 100644 backend/pom.xml create mode 100644 backend/src/main/java/com/music/MangToolApplication.java create mode 100644 backend/src/main/java/com/music/common/Result.java create mode 100644 backend/src/main/java/com/music/config/CorsConfig.java create mode 100644 backend/src/main/java/com/music/config/WebSocketConfig.java create mode 100644 backend/src/main/java/com/music/controller/AggregateController.java create mode 100644 backend/src/main/java/com/music/controller/ConvertController.java create mode 100644 backend/src/main/java/com/music/controller/DedupController.java create mode 100644 backend/src/main/java/com/music/controller/HealthController.java create mode 100644 backend/src/main/java/com/music/controller/ProgressController.java create mode 100644 backend/src/main/java/com/music/controller/ZhConvertController.java create mode 100644 backend/src/main/java/com/music/dto/AggregateRequest.java create mode 100644 backend/src/main/java/com/music/dto/ConvertRequest.java create mode 100644 backend/src/main/java/com/music/dto/DedupRequest.java create mode 100644 backend/src/main/java/com/music/dto/ProgressMessage.java create mode 100644 backend/src/main/java/com/music/dto/ZhConvertRequest.java create mode 100644 backend/src/main/java/com/music/exception/BusinessException.java create mode 100644 backend/src/main/java/com/music/exception/GlobalExceptionHandler.java create mode 100644 backend/src/main/java/com/music/service/AggregatorService.java create mode 100644 backend/src/main/java/com/music/service/ConvertService.java create mode 100644 backend/src/main/java/com/music/service/DedupService.java create mode 100644 backend/src/main/java/com/music/service/ProgressStore.java create mode 100644 backend/src/main/java/com/music/service/TraditionalFilterService.java create mode 100644 backend/src/main/java/com/music/service/ZhConvertService.java create mode 100644 backend/src/main/resources/application.yml create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/aggregate.ts create mode 100644 frontend/src/api/convert.ts create mode 100644 frontend/src/api/dedup.ts create mode 100644 frontend/src/api/progress.ts create mode 100644 frontend/src/api/request.ts create mode 100644 frontend/src/api/zhconvert.ts create mode 100644 frontend/src/components/AggregateTab.vue create mode 100644 frontend/src/components/ConvertTab.vue create mode 100644 frontend/src/components/DedupTab.vue create mode 100644 frontend/src/components/MergeTab.vue create mode 100644 frontend/src/components/RenameTab.vue create mode 100644 frontend/src/components/SettingsTab.vue create mode 100644 frontend/src/components/TraditionalFilterTab.vue create mode 100644 frontend/src/composables/useWebSocket.ts create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/styles/reset.css create mode 100644 frontend/vite.config.mts diff --git a/.gitignore b/.gitignore index ff82742..b83f7bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,45 @@ -# ---> Java -# Compiled class file +# +# 通用 +# +.DS_Store +Thumbs.db + +# 依赖缓存归项目外管理 +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +*.tgz + +# +# IDE / 编辑器 +# +# VS Code +.vscode/ + +# JetBrains / IDEA +.idea/ +*.iml + +# 其他编辑器 +*.swp +*.swo +*.tmp + +# +# Java / Maven / Gradle +# +# 编译输出 *.class -# Log file +# Java 日志 *.log -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) +# J2ME 临时目录 .mtj.tmp/ -# Package Files # +# 构建产物(不入库) *.jar *.war *.nar @@ -20,18 +48,51 @@ *.tar.gz *.rar -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +# JVM 崩溃日志 hs_err_pid* replay_pid* -# ---> Vue -# gitignore template for Vue.js projects +# Maven 目标目录 +target/ +backend/target/ + +# Gradle 目录(如果用得到) +.gradle/ +build/ + # -# Recommended template: Node.gitignore +# Node / Vue / Vite +# +# 通用前端依赖与构建输出 +frontend/node_modules/ +frontend/dist/ -# TODO: where does this rule come from? -docs/_book +# Vite / Rollup 缓存 +frontend/.vite/ +.vite/ -# TODO: where does this rule come from? -test/ +# Vue / 前端其它构建目录(按需) +.cache/ +coverage/ +*.local +# +# 操作系统杂项 +# +# macOS +*.DS_Store + +# Windows +Thumbs.db +Desktop.ini + +# +# 其他可选忽略(按需开启) +# +# 环境变量 +#.env +#.env.*.local + +# 本地工具配置 +#.idea/ +#.vscode/ \ No newline at end of file diff --git a/backend/pom.xml b/backend/pom.xml new file mode 100644 index 0000000..1afb35e --- /dev/null +++ b/backend/pom.xml @@ -0,0 +1,89 @@ + + 4.0.0 + + com.music + mangtool-backend + 0.0.1-SNAPSHOT + MangTool Backend + Music toolbox backend (Spring Boot) + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + + 1.8 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-websocket + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-actuator + + + + com.fasterxml.jackson.core + jackson-databind + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + net.jthink + jaudiotagger + 2.2.5 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 1.8 + 1.8 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/backend/src/main/java/com/music/MangToolApplication.java b/backend/src/main/java/com/music/MangToolApplication.java new file mode 100644 index 0000000..70dbc42 --- /dev/null +++ b/backend/src/main/java/com/music/MangToolApplication.java @@ -0,0 +1,15 @@ +package com.music; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; + +@SpringBootApplication +@EnableAsync +public class MangToolApplication { + + public static void main(String[] args) { + SpringApplication.run(MangToolApplication.class, args); + } +} + diff --git a/backend/src/main/java/com/music/common/Result.java b/backend/src/main/java/com/music/common/Result.java new file mode 100644 index 0000000..849bfde --- /dev/null +++ b/backend/src/main/java/com/music/common/Result.java @@ -0,0 +1,39 @@ +package com.music.common; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Result { + + /** + * 0 = 成功,非 0 = 业务错误代码 + */ + private int code; + + /** + * 提示信息 + */ + private String message; + + /** + * 业务数据 + */ + private T data; + + public static Result success(T data) { + return new Result<>(0, "success", data); + } + + public static Result success(String message, T data) { + return new Result<>(0, message, data); + } + + public static Result failure(int code, String message) { + return new Result<>(code, message, null); + } +} + diff --git a/backend/src/main/java/com/music/config/CorsConfig.java b/backend/src/main/java/com/music/config/CorsConfig.java new file mode 100644 index 0000000..4ca1c82 --- /dev/null +++ b/backend/src/main/java/com/music/config/CorsConfig.java @@ -0,0 +1,25 @@ +package com.music.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOriginPattern("*"); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return new CorsFilter(source); + } +} + diff --git a/backend/src/main/java/com/music/config/WebSocketConfig.java b/backend/src/main/java/com/music/config/WebSocketConfig.java new file mode 100644 index 0000000..0cc5c7f --- /dev/null +++ b/backend/src/main/java/com/music/config/WebSocketConfig.java @@ -0,0 +1,26 @@ +package com.music.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/ws") + .setAllowedOriginPatterns("*") + .withSockJS(); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/topic"); + registry.setApplicationDestinationPrefixes("/app"); + } +} + diff --git a/backend/src/main/java/com/music/controller/AggregateController.java b/backend/src/main/java/com/music/controller/AggregateController.java new file mode 100644 index 0000000..21f13a3 --- /dev/null +++ b/backend/src/main/java/com/music/controller/AggregateController.java @@ -0,0 +1,67 @@ +package com.music.controller; + +import com.music.common.Result; +import com.music.dto.AggregateRequest; +import com.music.exception.BusinessException; +import com.music.service.AggregatorService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.UUID; + +@RestController +@RequestMapping("/api/aggregate") +@Validated +public class AggregateController { + + private final AggregatorService aggregatorService; + + public AggregateController(AggregatorService aggregatorService) { + this.aggregatorService = aggregatorService; + } + + /** + * 启动音频文件汇聚任务 + */ + @PostMapping("/start") + public Result start(@Valid @RequestBody AggregateRequest request) { + // 验证模式参数 + if (!"copy".equalsIgnoreCase(request.getMode()) && + !"move".equalsIgnoreCase(request.getMode())) { + throw new BusinessException(400, "模式参数错误,必须是 copy 或 move"); + } + + // 生成任务ID + String taskId = UUID.randomUUID().toString(); + + // 异步执行任务 + aggregatorService.aggregate( + taskId, + request.getSrcDir(), + request.getDstDir(), + request.getMode() + ); + + return Result.success(new StartResponse(taskId)); + } + + /** + * 启动响应 + */ + public static class StartResponse { + private String taskId; + + public StartResponse(String taskId) { + this.taskId = taskId; + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + } +} diff --git a/backend/src/main/java/com/music/controller/ConvertController.java b/backend/src/main/java/com/music/controller/ConvertController.java new file mode 100644 index 0000000..c8070c2 --- /dev/null +++ b/backend/src/main/java/com/music/controller/ConvertController.java @@ -0,0 +1,59 @@ +package com.music.controller; + +import com.music.common.Result; +import com.music.dto.ConvertRequest; +import com.music.exception.BusinessException; +import com.music.service.ConvertService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.UUID; + +@RestController +@RequestMapping("/api/convert") +@Validated +public class ConvertController { + + private final ConvertService convertService; + + public ConvertController(ConvertService convertService) { + this.convertService = convertService; + } + + /** + * 启动音频格式智能处理(转码)任务 + */ + @PostMapping("/start") + public Result start(@Valid @RequestBody ConvertRequest request) { + if (!"copy".equalsIgnoreCase(request.getMode()) && !"move".equalsIgnoreCase(request.getMode())) { + throw new BusinessException(400, "模式参数错误,必须是 copy 或 move"); + } + + String taskId = UUID.randomUUID().toString(); + convertService.convert( + taskId, + request.getSrcDir(), + request.getDstDir(), + request.getMode() + ); + + return Result.success(new StartResponse(taskId)); + } + + public static class StartResponse { + private String taskId; + + public StartResponse(String taskId) { + this.taskId = taskId; + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + } +} diff --git a/backend/src/main/java/com/music/controller/DedupController.java b/backend/src/main/java/com/music/controller/DedupController.java new file mode 100644 index 0000000..b7a2a7e --- /dev/null +++ b/backend/src/main/java/com/music/controller/DedupController.java @@ -0,0 +1,72 @@ +package com.music.controller; + +import com.music.common.Result; +import com.music.dto.DedupRequest; +import com.music.exception.BusinessException; +import com.music.service.DedupService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.UUID; + +/** + * 音乐去重任务控制器 + */ +@RestController +@RequestMapping("/api/dedup") +@Validated +public class DedupController { + + private final DedupService dedupService; + + public DedupController(DedupService dedupService) { + this.dedupService = dedupService; + } + + /** + * 启动音乐去重任务 + */ + @PostMapping("/start") + public Result start(@Valid @RequestBody DedupRequest request) { + // 模式校验 + if (!"copy".equalsIgnoreCase(request.getMode()) && + !"move".equalsIgnoreCase(request.getMode())) { + throw new BusinessException(400, "模式参数错误,必须是 copy 或 move"); + } + + // 至少启用一种策略 + if (!request.isUseMd5() && !request.isUseMetadata()) { + throw new BusinessException(400, "至少需要启用一种去重策略(MD5 或元数据匹配)"); + } + + String taskId = UUID.randomUUID().toString(); + dedupService.dedup( + taskId, + request.getLibraryDir(), + request.getTrashDir(), + request.isUseMd5(), + request.isUseMetadata(), + request.getMode() + ); + + return Result.success(new StartResponse(taskId)); + } + + public static class StartResponse { + private String taskId; + + public StartResponse(String taskId) { + this.taskId = taskId; + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + } +} + diff --git a/backend/src/main/java/com/music/controller/HealthController.java b/backend/src/main/java/com/music/controller/HealthController.java new file mode 100644 index 0000000..608e1bc --- /dev/null +++ b/backend/src/main/java/com/music/controller/HealthController.java @@ -0,0 +1,15 @@ +package com.music.controller; + +import com.music.common.Result; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthController { + + @GetMapping("/api/health") + public Result health() { + return Result.success("OK"); + } +} + diff --git a/backend/src/main/java/com/music/controller/ProgressController.java b/backend/src/main/java/com/music/controller/ProgressController.java new file mode 100644 index 0000000..61dbe48 --- /dev/null +++ b/backend/src/main/java/com/music/controller/ProgressController.java @@ -0,0 +1,23 @@ +package com.music.controller; + +import com.music.common.Result; +import com.music.dto.ProgressMessage; +import com.music.service.ProgressStore; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/progress") +public class ProgressController { + + private final ProgressStore progressStore; + + public ProgressController(ProgressStore progressStore) { + this.progressStore = progressStore; + } + + @GetMapping("/{taskId}") + public Result get(@PathVariable("taskId") String taskId) { + return Result.success(progressStore.get(taskId)); + } +} + diff --git a/backend/src/main/java/com/music/controller/ZhConvertController.java b/backend/src/main/java/com/music/controller/ZhConvertController.java new file mode 100644 index 0000000..860f873 --- /dev/null +++ b/backend/src/main/java/com/music/controller/ZhConvertController.java @@ -0,0 +1,72 @@ +package com.music.controller; + +import com.music.common.Result; +import com.music.dto.ZhConvertRequest; +import com.music.exception.BusinessException; +import com.music.service.ZhConvertService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.UUID; + +/** + * 音乐元数据繁体转简体任务控制器 + */ +@RestController +@RequestMapping("/api/zhconvert") +@Validated +public class ZhConvertController { + + private final ZhConvertService zhConvertService; + + public ZhConvertController(ZhConvertService zhConvertService) { + this.zhConvertService = zhConvertService; + } + + /** + * 启动繁体检测 / 转换任务 + */ + @PostMapping("/start") + public Result start(@Valid @RequestBody ZhConvertRequest request) { + String mode = request.getMode(); + if (!"preview".equalsIgnoreCase(mode) && !"execute".equalsIgnoreCase(mode)) { + throw new BusinessException(400, "处理模式错误,必须是 preview 或 execute"); + } + + int threshold = request.getThreshold(); + if (threshold < 1 || threshold > 100) { + throw new BusinessException(400, "繁体占比阈值必须在 1-100 之间"); + } + + String taskId = UUID.randomUUID().toString(); + double thresholdRatio = threshold / 100.0; + + zhConvertService.process( + taskId, + request.getScanDir(), + request.getOutputDir(), + mode, + thresholdRatio + ); + + return Result.success(new StartResponse(taskId)); + } + + public static class StartResponse { + private String taskId; + + public StartResponse(String taskId) { + this.taskId = taskId; + } + + public String getTaskId() { + return taskId; + } + + public void setTaskId(String taskId) { + this.taskId = taskId; + } + } +} + diff --git a/backend/src/main/java/com/music/dto/AggregateRequest.java b/backend/src/main/java/com/music/dto/AggregateRequest.java new file mode 100644 index 0000000..0a954a3 --- /dev/null +++ b/backend/src/main/java/com/music/dto/AggregateRequest.java @@ -0,0 +1,21 @@ +package com.music.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class AggregateRequest { + + @NotBlank(message = "源目录不能为空") + private String srcDir; + + @NotBlank(message = "目标目录不能为空") + private String dstDir; + + /** + * 模式:copy(复制模式)或 move(移动模式) + */ + @NotBlank(message = "模式不能为空") + private String mode; +} diff --git a/backend/src/main/java/com/music/dto/ConvertRequest.java b/backend/src/main/java/com/music/dto/ConvertRequest.java new file mode 100644 index 0000000..e0e0980 --- /dev/null +++ b/backend/src/main/java/com/music/dto/ConvertRequest.java @@ -0,0 +1,21 @@ +package com.music.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +public class ConvertRequest { + + @NotBlank(message = "输入目录不能为空") + private String srcDir; + + @NotBlank(message = "输出目录不能为空") + private String dstDir; + + /** + * 模式:copy(复制模式)或 move(移动模式) + */ + @NotBlank(message = "模式不能为空") + private String mode; +} diff --git a/backend/src/main/java/com/music/dto/DedupRequest.java b/backend/src/main/java/com/music/dto/DedupRequest.java new file mode 100644 index 0000000..5df8e44 --- /dev/null +++ b/backend/src/main/java/com/music/dto/DedupRequest.java @@ -0,0 +1,41 @@ +package com.music.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 音乐去重任务请求 + */ +@Data +public class DedupRequest { + + /** + * 音乐库根目录 + */ + @NotBlank(message = "音乐库目录不能为空") + private String libraryDir; + + /** + * 回收站目录(重复文件移动/复制到此目录) + */ + @NotBlank(message = "回收站目录不能为空") + private String trashDir; + + /** + * 是否启用 MD5 哈希去重 + */ + private boolean useMd5 = true; + + /** + * 是否启用元数据匹配去重 + */ + private boolean useMetadata = true; + + /** + * 执行模式:copy(复制)或 move(移动) + */ + @NotBlank(message = "执行模式不能为空") + private String mode; +} + diff --git a/backend/src/main/java/com/music/dto/ProgressMessage.java b/backend/src/main/java/com/music/dto/ProgressMessage.java new file mode 100644 index 0000000..efd6e49 --- /dev/null +++ b/backend/src/main/java/com/music/dto/ProgressMessage.java @@ -0,0 +1,18 @@ +package com.music.dto; + +import lombok.Data; + +@Data +public class ProgressMessage { + + private String taskId; // 任务 ID + private String type; // 任务类型,如 aggregate/convert/dedup 等 + private int total; // 总数 + private int processed; // 已处理 + private int success; // 成功数 + private int failed; // 失败数 + private String currentFile; // 当前处理文件 + private String message; // 进度消息 + private boolean completed; // 是否完成 +} + diff --git a/backend/src/main/java/com/music/dto/ZhConvertRequest.java b/backend/src/main/java/com/music/dto/ZhConvertRequest.java new file mode 100644 index 0000000..422766f --- /dev/null +++ b/backend/src/main/java/com/music/dto/ZhConvertRequest.java @@ -0,0 +1,44 @@ +package com.music.dto; + +import lombok.Data; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; + +/** + * 音乐元数据繁体转简体任务请求 + */ +@Data +public class ZhConvertRequest { + + /** + * 待扫描标签的根目录 + */ + @NotBlank(message = "扫描目录不能为空") + private String scanDir; + + /** + * 输出目录(可选) + * + * - 预览模式:忽略该字段 + * - 执行模式: + * - 为空:在原文件上就地修改标签 + * - 非空:在该目录下生成一份带简体标签的副本 + */ + private String outputDir; + + /** + * 繁体占比阈值(百分比 1-100) + */ + @Min(value = 1, message = "繁体占比阈值不能小于 1%") + @Max(value = 100, message = "繁体占比阈值不能大于 100%") + private int threshold; + + /** + * 处理模式:preview(仅预览) 或 execute(执行转换) + */ + @NotBlank(message = "处理模式不能为空") + private String mode; +} + diff --git a/backend/src/main/java/com/music/exception/BusinessException.java b/backend/src/main/java/com/music/exception/BusinessException.java new file mode 100644 index 0000000..a848de2 --- /dev/null +++ b/backend/src/main/java/com/music/exception/BusinessException.java @@ -0,0 +1,16 @@ +package com.music.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/backend/src/main/java/com/music/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/music/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..8e1532d --- /dev/null +++ b/backend/src/main/java/com/music/exception/GlobalExceptionHandler.java @@ -0,0 +1,32 @@ +package com.music.exception; + +import com.music.common.Result; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + public Result handleBusinessException(BusinessException ex) { + return Result.failure(ex.getCode(), ex.getMessage()); + } + + @ExceptionHandler({ + MethodArgumentNotValidException.class, + BindException.class, + HttpMessageNotReadableException.class + }) + public Result handleValidationException(Exception ex) { + return Result.failure(400, "请求参数错误:" + ex.getMessage()); + } + + @ExceptionHandler(Exception.class) + public Result handleOther(Exception ex) { + return Result.failure(500, "服务器内部错误:" + ex.getMessage()); + } +} + diff --git a/backend/src/main/java/com/music/service/AggregatorService.java b/backend/src/main/java/com/music/service/AggregatorService.java new file mode 100644 index 0000000..fa13b2a --- /dev/null +++ b/backend/src/main/java/com/music/service/AggregatorService.java @@ -0,0 +1,202 @@ +package com.music.service; + +import com.music.dto.ProgressMessage; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +@Service +public class AggregatorService { + + private static final Set AUDIO_EXTENSIONS = new HashSet<>(Arrays.asList( + "mp3", "flac", "wav", "m4a", "aac", "ogg", "wma", "ape" + )); + + private final SimpMessagingTemplate messagingTemplate; + private final ProgressStore progressStore; + + public AggregatorService(SimpMessagingTemplate messagingTemplate, ProgressStore progressStore) { + this.messagingTemplate = messagingTemplate; + this.progressStore = progressStore; + } + + /** + * 异步执行音频文件汇聚任务 + */ + @Async + public void aggregate(String taskId, String srcDir, String dstDir, String mode) { + Path sourcePath = Paths.get(srcDir); + Path targetPath = Paths.get(dstDir); + + try { + // 验证源目录 + if (!Files.exists(sourcePath) || !Files.isDirectory(sourcePath)) { + sendProgress(taskId, 0, 0, 0, 0, null, "源目录不存在或不是目录", true); + return; + } + + // 创建目标目录 + if (!Files.exists(targetPath)) { + Files.createDirectories(targetPath); + } + + // 收集所有音频文件 + List audioFiles = new ArrayList<>(); + Files.walkFileTree(sourcePath, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (isAudioFile(file)) { + audioFiles.add(file); + } + return FileVisitResult.CONTINUE; + } + }); + + int total = audioFiles.size(); + AtomicInteger processed = new AtomicInteger(0); + AtomicInteger success = new AtomicInteger(0); + AtomicInteger failed = new AtomicInteger(0); + + // 发送初始进度 + sendProgress(taskId, total, 0, 0, 0, null, "开始汇聚任务...", false); + + // 处理每个文件 + for (Path sourceFile : audioFiles) { + String fileName = sourceFile.getFileName().toString(); + try { + Path targetFile = resolveTargetFile(targetPath, fileName); + + if ("move".equalsIgnoreCase(mode)) { + // 移动模式 + Files.move(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); + } else { + // 复制模式 + Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); + } + + success.incrementAndGet(); + sendProgress(taskId, total, processed.incrementAndGet(), + success.get(), failed.get(), fileName, + String.format("已处理: %s", fileName), false); + } catch (Exception e) { + failed.incrementAndGet(); + sendProgress(taskId, total, processed.incrementAndGet(), + success.get(), failed.get(), fileName, + String.format("处理失败: %s - %s", fileName, e.getMessage()), false); + } + } + + // 移动模式下清理空目录 + if ("move".equalsIgnoreCase(mode)) { + cleanupEmptyDirectories(sourcePath); + } + + // 发送完成消息 + sendProgress(taskId, total, processed.get(), success.get(), failed.get(), + null, String.format("任务完成!成功: %d, 失败: %d", success.get(), failed.get()), true); + + } catch (Exception e) { + sendProgress(taskId, 0, 0, 0, 0, null, + "任务执行失败: " + e.getMessage(), true); + } + } + + /** + * 判断是否为音频文件 + */ + private boolean isAudioFile(Path file) { + String fileName = file.getFileName().toString().toLowerCase(); + int lastDot = fileName.lastIndexOf('.'); + if (lastDot == -1) { + return false; + } + String extension = fileName.substring(lastDot + 1); + return AUDIO_EXTENSIONS.contains(extension); + } + + /** + * 解析目标文件路径,处理文件名冲突 + */ + private Path resolveTargetFile(Path targetDir, String fileName) { + Path targetFile = targetDir.resolve(fileName); + + // 如果文件不存在,直接返回 + if (!Files.exists(targetFile)) { + return targetFile; + } + + // 处理文件名冲突:文件名 (1).ext, 文件名 (2).ext ... + int lastDot = fileName.lastIndexOf('.'); + String baseName; + String extension; + + if (lastDot == -1) { + baseName = fileName; + extension = ""; + } else { + baseName = fileName.substring(0, lastDot); + extension = fileName.substring(lastDot); + } + + int counter = 1; + while (Files.exists(targetFile)) { + String newFileName = String.format("%s (%d)%s", baseName, counter, extension); + targetFile = targetDir.resolve(newFileName); + counter++; + } + + return targetFile; + } + + /** + * 清理空目录(移动模式下使用) + */ + private void cleanupEmptyDirectories(Path rootPath) { + try { + Files.walkFileTree(rootPath, new SimpleFileVisitor() { + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + // 不删除根目录本身 + if (dir.equals(rootPath)) { + return FileVisitResult.CONTINUE; + } + + try { + Files.deleteIfExists(dir); + } catch (DirectoryNotEmptyException e) { + // 目录不为空,忽略 + } + return FileVisitResult.CONTINUE; + } + }); + } catch (IOException e) { + // 清理失败不影响主流程 + } + } + + /** + * 发送进度消息 + */ + private void sendProgress(String taskId, int total, int processed, int success, + int failed, String currentFile, String message, boolean completed) { + ProgressMessage progress = new ProgressMessage(); + progress.setTaskId(taskId); + progress.setType("aggregate"); + progress.setTotal(total); + progress.setProcessed(processed); + progress.setSuccess(success); + progress.setFailed(failed); + progress.setCurrentFile(currentFile); + progress.setMessage(message); + progress.setCompleted(completed); + + progressStore.put(progress); + messagingTemplate.convertAndSend("/topic/progress/" + taskId, progress); + } +} diff --git a/backend/src/main/java/com/music/service/ConvertService.java b/backend/src/main/java/com/music/service/ConvertService.java new file mode 100644 index 0000000..a7ff88b --- /dev/null +++ b/backend/src/main/java/com/music/service/ConvertService.java @@ -0,0 +1,219 @@ +package com.music.service; + +import com.music.dto.ProgressMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +@Service +public class ConvertService { + + private static final Logger log = LoggerFactory.getLogger(ConvertService.class); + + /** 无损格式,需转换为 FLAC */ + private static final Set LOSSLESS_EXTENSIONS = new HashSet<>(Arrays.asList( + "wav", "ape", "aiff", "aif", "wv", "tta" + )); + + /** 有损格式及 FLAC:跳过,不处理 */ + private static final Set SKIP_EXTENSIONS = new HashSet<>(Arrays.asList( + "flac", "mp3", "m4a", "aac", "ogg", "opus", "wma" + )); + + private static final int FFMPEG_COMPRESSION_LEVEL = 5; + + private final SimpMessagingTemplate messagingTemplate; + private final ProgressStore progressStore; + + public ConvertService(SimpMessagingTemplate messagingTemplate, ProgressStore progressStore) { + this.messagingTemplate = messagingTemplate; + this.progressStore = progressStore; + } + + @Async + public void convert(String taskId, String srcDir, String dstDir, String mode) { + Path srcPath = Paths.get(srcDir); + Path dstPath = Paths.get(dstDir); + + try { + // 验证输入目录路径是否填写 + if (srcDir == null || srcDir.trim().isEmpty()) { + sendProgress(taskId, 0, 0, 0, 0, null, "输入目录路径不能为空", true); + return; + } + + // 验证输入目录是否存在 + if (!Files.exists(srcPath) || !Files.isDirectory(srcPath)) { + sendProgress(taskId, 0, 0, 0, 0, null, "输入目录不存在或不是目录", true); + return; + } + + // 验证输出目录路径是否填写 + if (dstDir == null || dstDir.trim().isEmpty()) { + sendProgress(taskId, 0, 0, 0, 0, null, "输出目录路径不能为空", true); + return; + } + + // 移动模式下,输入和输出目录不能相同 + if ("move".equalsIgnoreCase(mode) && srcPath.equals(dstPath)) { + sendProgress(taskId, 0, 0, 0, 0, null, "移动模式下,输入目录和输出目录不能相同", true); + return; + } + + // 创建输出目录(如果不存在) + if (!Files.exists(dstPath)) { + Files.createDirectories(dstPath); + } + + // 扫描输入目录,查找需要转换的文件 + List toConvert = new ArrayList<>(); + Files.walkFileTree(srcPath, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (shouldConvert(file)) { + toConvert.add(file); + } + return FileVisitResult.CONTINUE; + } + }); + + int total = toConvert.size(); + AtomicInteger processed = new AtomicInteger(0); + AtomicInteger success = new AtomicInteger(0); + AtomicInteger failed = new AtomicInteger(0); + + log.info("转码任务开始,扫描目录: {}, 待转码文件数: {}", srcPath, total); + sendProgress(taskId, total, 0, 0, 0, null, "扫描完成,开始转码任务...", false); + + // 如果目录存在但没有需要转换的文件,正常完成任务 + if (total == 0) { + log.info("目录 {} 中未找到需要转码的无损音频文件", srcPath); + sendProgress(taskId, 0, 0, 0, 0, null, + "目录扫描完成,未找到需要转码的无损音频文件(WAV/APE/AIFF/WV/TTA)。目录中可能只有 FLAC 或有损格式文件,已自动跳过。", true); + return; + } + + for (Path srcFile : toConvert) { + String fileName = srcFile.getFileName().toString(); + String baseName = getBaseName(fileName); + String outFileName = baseName + ".flac"; + Path outFile = resolveTargetFile(dstPath, outFileName); + + try { + runFfmpeg(srcFile, outFile); + success.incrementAndGet(); + + if ("move".equalsIgnoreCase(mode)) { + try { + Files.deleteIfExists(srcFile); + } catch (IOException e) { + log.warn("移动模式删除源文件失败: {}", srcFile, e); + } + } + + sendProgress(taskId, total, processed.incrementAndGet(), + success.get(), failed.get(), fileName, + "已处理: " + fileName, false); + } catch (Exception e) { + failed.incrementAndGet(); + log.warn("转码失败: {} - {}", fileName, e.getMessage()); + sendProgress(taskId, total, processed.incrementAndGet(), + success.get(), failed.get(), fileName, + "转码失败: " + fileName + " - " + e.getMessage(), false); + } + } + + sendProgress(taskId, total, processed.get(), success.get(), failed.get(), + null, String.format("任务完成!成功: %d, 失败: %d", success.get(), failed.get()), true); + + } catch (Exception e) { + log.error("转码任务执行失败", e); + sendProgress(taskId, 0, 0, 0, 0, null, + "任务执行失败: " + e.getMessage(), true); + } + } + + private boolean shouldConvert(Path file) { + String ext = getExtension(file.getFileName().toString()); + if (ext == null) return false; + return LOSSLESS_EXTENSIONS.contains(ext); + } + + private String getExtension(String fileName) { + int i = fileName.lastIndexOf('.'); + if (i == -1 || i == fileName.length() - 1) return null; + return fileName.substring(i + 1).toLowerCase(); + } + + private String getBaseName(String fileName) { + int i = fileName.lastIndexOf('.'); + if (i <= 0) return fileName; + return fileName.substring(0, i); + } + + private void runFfmpeg(Path input, Path output) throws IOException, InterruptedException { + ProcessBuilder pb = new ProcessBuilder( + "ffmpeg", + "-y", + "-i", input.toAbsolutePath().toString(), + "-compression_level", String.valueOf(FFMPEG_COMPRESSION_LEVEL), + output.toAbsolutePath().toString() + ); + pb.redirectErrorStream(true); + Process p = pb.start(); + int exit = p.waitFor(); + if (exit != 0) { + throw new RuntimeException("ffmpeg 退出码: " + exit); + } + } + + private Path resolveTargetFile(Path targetDir, String fileName) throws IOException { + Path target = targetDir.resolve(fileName); + if (!Files.exists(target)) return target; + + int lastDot = fileName.lastIndexOf('.'); + String base = lastDot > 0 ? fileName.substring(0, lastDot) : fileName; + String ext = lastDot > 0 ? fileName.substring(lastDot) : ""; + int n = 1; + while (Files.exists(target)) { + String next = base + " (" + n + ")" + ext; + target = targetDir.resolve(next); + n++; + } + return target; + } + + private void sendProgress(String taskId, int total, int processed, int success, + int failed, String currentFile, String message, boolean completed) { + try { + ProgressMessage pm = new ProgressMessage(); + pm.setTaskId(taskId); + pm.setType("convert"); + pm.setTotal(total); + pm.setProcessed(processed); + pm.setSuccess(success); + pm.setFailed(failed); + pm.setCurrentFile(currentFile); + pm.setMessage(message); + pm.setCompleted(completed); + progressStore.put(pm); + messagingTemplate.convertAndSend("/topic/progress/" + taskId, pm); + log.debug("发送进度消息: taskId={}, total={}, processed={}, success={}, failed={}, completed={}", + taskId, total, processed, success, failed, completed); + } catch (Exception e) { + log.error("发送进度消息失败", e); + } + } +} diff --git a/backend/src/main/java/com/music/service/DedupService.java b/backend/src/main/java/com/music/service/DedupService.java new file mode 100644 index 0000000..3d9d4c8 --- /dev/null +++ b/backend/src/main/java/com/music/service/DedupService.java @@ -0,0 +1,454 @@ +package com.music.service; + +import com.music.dto.ProgressMessage; +import org.jaudiotagger.audio.AudioFile; +import org.jaudiotagger.audio.AudioFileIO; +import org.jaudiotagger.tag.FieldKey; +import org.jaudiotagger.tag.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 音乐去重服务(首版:仅实现 MD5 去重) + * + * 说明: + * - 目前实现的是基于 MD5 的二进制级别去重,用于识别完全相同的文件拷贝。 + * - 元数据匹配与智能评分策略后续迭代中补充。 + */ +@Service +public class DedupService { + + private static final Logger log = LoggerFactory.getLogger(DedupService.class); + + private static final Set AUDIO_EXTENSIONS = new HashSet<>(Arrays.asList( + "mp3", "flac", "wav", "m4a", "aac", "ogg", "wma", "ape", "aiff", "aif", "wv", "tta", "opus" + )); + + /** 元数据匹配允许的时长误差(秒) */ + private static final int DURATION_TOLERANCE_SECONDS = 5; + + private final SimpMessagingTemplate messagingTemplate; + private final ProgressStore progressStore; + + public DedupService(SimpMessagingTemplate messagingTemplate, ProgressStore progressStore) { + this.messagingTemplate = messagingTemplate; + this.progressStore = progressStore; + } + + /** + * 异步执行去重任务 + */ + @Async + public void dedup(String taskId, + String libraryDir, + String trashDir, + boolean useMd5, + boolean useMetadata, + String mode) { + + Path libraryPath = Paths.get(libraryDir); + Path trashPath = Paths.get(trashDir); + + try { + // 基本校验 + if (!Files.exists(libraryPath) || !Files.isDirectory(libraryPath)) { + sendProgress(taskId, 0, 0, 0, 0, + "音乐库目录不存在或不是目录", true); + return; + } + + if (!Files.exists(trashPath)) { + Files.createDirectories(trashPath); + } + + if (!"copy".equalsIgnoreCase(mode) && !"move".equalsIgnoreCase(mode)) { + sendProgress(taskId, 0, 0, 0, 0, + "执行模式错误,必须是 copy 或 move", true); + return; + } + + if (!useMd5 && !useMetadata) { + sendProgress(taskId, 0, 0, 0, 0, + "至少需要启用一种去重策略(MD5 或元数据匹配)", true); + return; + } + + // 收集所有音频文件 + List audioFiles = new ArrayList<>(); + Files.walkFileTree(libraryPath, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (isAudioFile(file)) { + audioFiles.add(file); + } + return FileVisitResult.CONTINUE; + } + }); + + int total = audioFiles.size(); + if (total == 0) { + sendProgress(taskId, 0, 0, 0, 0, + "未在音乐库中找到音频文件", true); + return; + } + + AtomicInteger scanned = new AtomicInteger(0); + AtomicInteger duplicateGroups = new AtomicInteger(0); + AtomicInteger moved = new AtomicInteger(0); + AtomicInteger failed = new AtomicInteger(0); + + sendProgress(taskId, total, 0, 0, 0, + "开始扫描音乐库...", false); + + Map> md5Groups = new HashMap<>(); + Map> metadataGroups = new HashMap<>(); + + // 第一阶段:扫描并根据配置构建分组 + for (Path file : audioFiles) { + try { + if (useMd5) { + String md5 = calculateMd5(file); + md5Groups.computeIfAbsent(md5, k -> new ArrayList<>()).add(file); + } + + if (useMetadata) { + Optional keyOpt = readMetadataKey(file); + keyOpt.ifPresent(key -> metadataGroups + .computeIfAbsent(key, k -> new ArrayList<>()) + .add(file)); + } + + int currentScanned = scanned.incrementAndGet(); + if (currentScanned % 50 == 0) { + sendProgress(taskId, total, currentScanned, + duplicateGroups.get(), moved.get(), + String.format("扫描中(%d/%d)", currentScanned, total), + false); + } + } catch (Exception e) { + failed.incrementAndGet(); + log.warn("扫描文件失败: {}", file, e); + } + } + + // 第二阶段:处理 MD5 去重结果(完全二进制重复) + if (useMd5) { + for (Map.Entry> entry : md5Groups.entrySet()) { + List group = entry.getValue(); + if (group.size() <= 1) { + continue; + } + + duplicateGroups.incrementAndGet(); + + Path keep = chooseBestFileByScore(group); + List duplicates = new ArrayList<>(group); + duplicates.remove(keep); + + moved.addAndGet(handleDuplicates(duplicates, keep, trashPath, mode, taskId, total, + scanned, duplicateGroups, failed)); + } + } + + if (useMetadata) { + // 第三阶段:处理元数据匹配去重结果 + for (Map.Entry> entry : metadataGroups.entrySet()) { + List group = entry.getValue(); + if (group.size() <= 1) { + continue; + } + + duplicateGroups.incrementAndGet(); + + Path keep = chooseBestFileByScore(group); + List duplicates = new ArrayList<>(group); + duplicates.remove(keep); + + moved.addAndGet(handleDuplicates(duplicates, keep, trashPath, mode, taskId, total, + scanned, duplicateGroups, failed)); + } + } + + sendProgress(taskId, total, scanned.get(), + duplicateGroups.get(), moved.get(), + String.format("任务完成!扫描文件: %d, 重复组: %d, 移动/复制文件: %d", + scanned.get(), duplicateGroups.get(), moved.get()), + true); + + } catch (Exception e) { + log.error("去重任务执行失败", e); + sendProgress(taskId, 0, 0, 0, 0, + "任务执行失败: " + e.getMessage(), true); + } + } + + private boolean isAudioFile(Path file) { + String name = file.getFileName().toString().toLowerCase(); + int idx = name.lastIndexOf('.'); + if (idx <= 0 || idx == name.length() - 1) { + return false; + } + String ext = name.substring(idx + 1); + return AUDIO_EXTENSIONS.contains(ext); + } + + private String calculateMd5(Path file) throws IOException, NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("MD5"); + try (InputStream is = Files.newInputStream(file); + DigestInputStream dis = new DigestInputStream(is, md)) { + byte[] buffer = new byte[8192]; + // 读取整个文件,结果自动更新到 md 中 + while (dis.read(buffer) != -1) { + // no-op + } + } + byte[] digest = md.digest(); + StringBuilder sb = new StringBuilder(digest.length * 2); + for (byte b : digest) { + sb.append(String.format("%02x", b)); + } + return sb.toString(); + } + + /** + * 元数据分组键:艺术家 + 标题 + 专辑 + 时长(按 5 秒误差归一) + */ + private static class MetadataKey { + private final String artist; + private final String title; + private final String album; + private final int normalizedDuration; + + private MetadataKey(String artist, String title, String album, int normalizedDuration) { + this.artist = artist; + this.title = title; + this.album = album; + this.normalizedDuration = normalizedDuration; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MetadataKey)) return false; + MetadataKey that = (MetadataKey) o; + return normalizedDuration == that.normalizedDuration && + Objects.equals(artist, that.artist) && + Objects.equals(title, that.title) && + Objects.equals(album, that.album); + } + + @Override + public int hashCode() { + return Objects.hash(artist, title, album, normalizedDuration); + } + } + + /** + * 从音频文件读取用于匹配的元数据键 + */ + private Optional readMetadataKey(Path file) { + try { + AudioFile audioFile = AudioFileIO.read(file.toFile()); + Tag tag = audioFile.getTag(); + if (tag == null) { + return Optional.empty(); + } + + String artist = normalize(tag.getFirst(FieldKey.ARTIST)); + String title = normalize(tag.getFirst(FieldKey.TITLE)); + String album = normalize(tag.getFirst(FieldKey.ALBUM)); + int lengthSec = audioFile.getAudioHeader().getTrackLength(); + + if (artist.isEmpty() || title.isEmpty()) { + // 核心标签缺失则跳过元数据分组 + return Optional.empty(); + } + + // 将时长按 5 秒误差容忍度归一化 + int normalizedDuration = lengthSec / DURATION_TOLERANCE_SECONDS; + + return Optional.of(new MetadataKey(artist, title, album, normalizedDuration)); + } catch (Exception e) { + // 标签损坏或不支持的格式时,忽略元数据去重 + log.debug("读取元数据失败: {}", file, e); + return Optional.empty(); + } + } + + private String normalize(String s) { + if (s == null) { + return ""; + } + return s.trim().toLowerCase(); + } + + /** + * 对一组候选文件进行综合评分,选择最佳保留文件 + * + * 评分策略: + * - 格式优先:FLAC > 其他无损 > 有损 + * - 码率优先:高码率得分更高(如果可获取) + * - 文件大小:极小文件减分 + * - 文件名噪声惩罚:含样本/preview 等噪声词减分 + */ + private Path chooseBestFileByScore(List candidates) { + if (candidates.size() == 1) { + return candidates.get(0); + } + + return candidates.stream() + .max(Comparator.comparingDouble(this::scoreFile)) + .orElse(candidates.get(0)); + } + + private double scoreFile(Path file) { + double score = 0.0; + + String name = file.getFileName().toString().toLowerCase(); + String ext = ""; + int idx = name.lastIndexOf('.'); + if (idx > 0 && idx < name.length() - 1) { + ext = name.substring(idx + 1); + } + + // 格式权重 + if ("flac".equals(ext)) { + score += 100; + } else if (Arrays.asList("wav", "ape", "aiff", "aif", "wv", "tta").contains(ext)) { + score += 80; + } else { + score += 50; // 有损格式 + } + + // 文件大小(KB)加权:更大的通常音质更好,但极大文件不再线性加分 + try { + long size = Files.size(file); + double sizeKB = size / 1024.0; + if (sizeKB < 128) { + score -= 30; // 极小文件,疑似样本/损坏 + } else { + score += Math.min(sizeKB / 100.0, 40.0); + } + } catch (IOException e) { + // 忽略大小获取失败 + } + + // 文件名噪声惩罚 + if (name.contains("sample") || name.contains("preview") || name.contains("demo")) { + score -= 20; + } + if (name.matches(".*\\b(live|remix|karaoke)\\b.*")) { + // 某些版本可能不是首选,略微扣分(具体偏好可根据需要调整) + score -= 5; + } + + // TODO:如有需要,可从音频头中读取比特率,进一步加权 + + return score; + } + + /** + * 将重复文件移动/复制到回收站,并更新统计与进度 + * + * @return 实际成功移动/复制的文件数量 + */ + private int handleDuplicates(List duplicates, + Path keep, + Path trashPath, + String mode, + String taskId, + int total, + AtomicInteger scanned, + AtomicInteger duplicateGroups, + AtomicInteger failed) { + int movedCount = 0; + for (Path dup : duplicates) { + try { + Path target = resolveTargetFile(trashPath, dup.getFileName().toString()); + if ("move".equalsIgnoreCase(mode)) { + Files.move(dup, target, StandardCopyOption.REPLACE_EXISTING); + } else { + Files.copy(dup, target, StandardCopyOption.REPLACE_EXISTING); + } + movedCount++; + sendProgress(taskId, total, scanned.get(), + duplicateGroups.get(), movedCount, + String.format("重复文件: %s (保留: %s)", + dup.getFileName(), keep.getFileName()), + false); + } catch (Exception e) { + failed.incrementAndGet(); + log.warn("处理重复文件失败: {}", dup, e); + } + } + return movedCount; + } + + /** + * 解析回收站中的目标文件名,处理重名冲突 + */ + private Path resolveTargetFile(Path targetDir, String fileName) throws IOException { + Path target = targetDir.resolve(fileName); + if (!Files.exists(target)) { + return target; + } + + int lastDot = fileName.lastIndexOf('.'); + String base = lastDot > 0 ? fileName.substring(0, lastDot) : fileName; + String ext = lastDot > 0 ? fileName.substring(lastDot) : ""; + int n = 1; + while (Files.exists(target)) { + String next = base + " (" + n + ")" + ext; + target = targetDir.resolve(next); + n++; + } + return target; + } + + /** + * 发送进度消息 + * + * 字段语义(供前端展示用): + * - total:扫描到的音频文件总数 + * - processed:已扫描文件数 + * - success:重复组数量 + * - failed:移动/复制的重复文件数量 + * + * 由于进度字段在不同任务中的含义略有差异,前端可根据 type === "dedup" 做专门映射。 + */ + private void sendProgress(String taskId, + int total, + int processed, + int success, + int failed, + String message, + boolean completed) { + ProgressMessage pm = new ProgressMessage(); + pm.setTaskId(taskId); + pm.setType("dedup"); + pm.setTotal(total); + pm.setProcessed(processed); + pm.setSuccess(success); + pm.setFailed(failed); + pm.setCurrentFile(null); + pm.setMessage(message); + pm.setCompleted(completed); + + progressStore.put(pm); + messagingTemplate.convertAndSend("/topic/progress/" + taskId, pm); + } +} + diff --git a/backend/src/main/java/com/music/service/ProgressStore.java b/backend/src/main/java/com/music/service/ProgressStore.java new file mode 100644 index 0000000..77fbd1b --- /dev/null +++ b/backend/src/main/java/com/music/service/ProgressStore.java @@ -0,0 +1,30 @@ +package com.music.service; + +import com.music.dto.ProgressMessage; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ConcurrentHashMap; + +/** + * 进度缓存:用于 WebSocket 消息丢失时的兜底查询 + */ +@Service +public class ProgressStore { + + private final ConcurrentHashMap latest = new ConcurrentHashMap<>(); + + public void put(ProgressMessage message) { + if (message == null || message.getTaskId() == null) { + return; + } + latest.put(message.getTaskId(), message); + } + + public ProgressMessage get(String taskId) { + if (taskId == null) { + return null; + } + return latest.get(taskId); + } +} + diff --git a/backend/src/main/java/com/music/service/TraditionalFilterService.java b/backend/src/main/java/com/music/service/TraditionalFilterService.java new file mode 100644 index 0000000..4567ecd --- /dev/null +++ b/backend/src/main/java/com/music/service/TraditionalFilterService.java @@ -0,0 +1,147 @@ +package com.music.service; + +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 繁体检测与转换工具服务 + * + *

说明: + * - 这里实现的是一个轻量级的基于字典的繁简转换和检测 + * - 仅覆盖常见的繁体字,后续可接入更完整的第三方词库或服务 + */ +@Service +public class TraditionalFilterService { + + /** + * 常见繁体字符映射表(可按需扩展) + */ + private static final Map TRAD_TO_SIMP_MAP = new HashMap<>(); + + /** + * 常见繁体字符集合,加速检测 + */ + private static final Set TRADITIONAL_SET = new HashSet<>(); + + static { + // 人名 / 常用字 + addMapping('倫', '伦'); + addMapping('愛', '爱'); + addMapping('聲', '声'); + addMapping('轉', '转'); + addMapping('幹', '干'); + addMapping('後', '后'); + addMapping('來', '来'); + addMapping('體', '体'); + addMapping('風', '风'); + addMapping('陽', '阳'); + addMapping('廣', '广'); + addMapping('門', '门'); + addMapping('馬', '马'); + addMapping('國', '国'); + addMapping('書', '书'); + addMapping('樂', '乐'); + addMapping('現', '现'); + addMapping('時', '时'); + addMapping('總', '总'); + addMapping('開', '开'); + addMapping('關', '关'); + addMapping('電', '电'); + addMapping('錄', '录'); + addMapping('戲', '戏'); + addMapping('畫', '画'); + addMapping('聲', '声'); + + // 可以根据需要逐步补充更多常见映射 + } + + private static void addMapping(char trad, char simp) { + TRAD_TO_SIMP_MAP.put(trad, simp); + TRADITIONAL_SET.add(trad); + } + + /** + * 检测字符串中的中文/繁体字数量 + */ + public ZhStats analyze(String text) { + if (text == null || text.isEmpty()) { + return new ZhStats(0, 0); + } + + int chineseCount = 0; + int traditionalCount = 0; + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (isCjk(c)) { + chineseCount++; + if (TRADITIONAL_SET.contains(c)) { + traditionalCount++; + } + } + } + + return new ZhStats(chineseCount, traditionalCount); + } + + /** + * 将字符串中的常见繁体字转换为简体 + */ + public String toSimplified(String text) { + if (text == null || text.isEmpty()) { + return text; + } + StringBuilder sb = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + Character mapped = TRAD_TO_SIMP_MAP.get(c); + sb.append(mapped != null ? mapped : c); + } + return sb.toString(); + } + + /** + * 判断是否为 CJK 统一表意文字(大致范围) + */ + private boolean isCjk(char c) { + // 基本汉字 + 扩展区的一部分 + return (c >= 0x4E00 && c <= 0x9FFF) // CJK Unified Ideographs + || (c >= 0x3400 && c <= 0x4DBF); // CJK Unified Ideographs Extension A + } + + /** + * 繁体检测统计结果 + */ + public static class ZhStats { + private final int chineseCount; + private final int traditionalCount; + + public ZhStats(int chineseCount, int traditionalCount) { + this.chineseCount = chineseCount; + this.traditionalCount = traditionalCount; + } + + public int getChineseCount() { + return chineseCount; + } + + public int getTraditionalCount() { + return traditionalCount; + } + + /** + * 繁体占比(0.0 - 1.0) + */ + public double getRatio() { + if (chineseCount <= 0) { + return 0.0; + } + return (double) traditionalCount / (double) chineseCount; + } + } +} + diff --git a/backend/src/main/java/com/music/service/ZhConvertService.java b/backend/src/main/java/com/music/service/ZhConvertService.java new file mode 100644 index 0000000..4ed4330 --- /dev/null +++ b/backend/src/main/java/com/music/service/ZhConvertService.java @@ -0,0 +1,286 @@ +package com.music.service; + +import com.music.dto.ProgressMessage; +import com.music.service.TraditionalFilterService.ZhStats; +import org.jaudiotagger.audio.AudioFile; +import org.jaudiotagger.audio.AudioFileIO; +import org.jaudiotagger.tag.FieldKey; +import org.jaudiotagger.tag.Tag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 音乐元数据繁体转简体服务 + * + *

进度字段语义(type = "zhconvert"): + * - total:扫描到的音频文件总数 + * - processed:已扫描文件数 + * - success:检测到的繁体标签条目数量(字段级:Title/Artist/Album/AlbumArtist) + * - failed:处理失败的文件数量 + * - currentFile:当前正在处理的文件名 + */ +@Service +public class ZhConvertService { + + private static final Logger log = LoggerFactory.getLogger(ZhConvertService.class); + + private static final Set AUDIO_EXTENSIONS = ConcurrentHashMap.newKeySet(); + + static { + AUDIO_EXTENSIONS.addAll(Arrays.asList( + "mp3", "flac", "wav", "m4a", "aac", "ogg", "wma", "ape", "aiff", "aif", "wv", "tta", "opus" + )); + } + + private final SimpMessagingTemplate messagingTemplate; + private final ProgressStore progressStore; + private final TraditionalFilterService traditionalFilterService; + + public ZhConvertService(SimpMessagingTemplate messagingTemplate, + ProgressStore progressStore, + TraditionalFilterService traditionalFilterService) { + this.messagingTemplate = messagingTemplate; + this.progressStore = progressStore; + this.traditionalFilterService = traditionalFilterService; + } + + /** + * 异步执行繁体检测与转换任务 + * + * @param mode preview / execute + * @param thresholdRatio 触发阈值(0.0-1.0) + */ + @Async + public void process(String taskId, + String scanDir, + String outputDir, + String mode, + double thresholdRatio) { + Path scanPath = Paths.get(scanDir); + + try { + if (!Files.exists(scanPath) || !Files.isDirectory(scanPath)) { + sendProgress(taskId, 0, 0, 0, 0, null, + "扫描目录不存在或不是目录", true); + return; + } + + Path outputPath = null; + boolean execute = "execute".equalsIgnoreCase(mode); + if (execute && outputDir != null && !outputDir.trim().isEmpty()) { + outputPath = Paths.get(outputDir.trim()); + if (!Files.exists(outputPath)) { + Files.createDirectories(outputPath); + } + } + + // 收集音频文件 + List audioFiles = new ArrayList<>(); + Files.walkFileTree(scanPath, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (isAudioFile(file)) { + audioFiles.add(file); + } + return FileVisitResult.CONTINUE; + } + }); + + int total = audioFiles.size(); + if (total == 0) { + sendProgress(taskId, 0, 0, 0, 0, null, + "目录中未找到音频文件", true); + return; + } + + AtomicInteger processed = new AtomicInteger(0); + AtomicInteger traditionalEntries = new AtomicInteger(0); + AtomicInteger failed = new AtomicInteger(0); + + sendProgress(taskId, total, 0, 0, 0, null, + execute ? "开始执行繁体→简体转换任务..." : "开始繁体检测预览任务...", false); + + for (Path srcFile : audioFiles) { + String fileName = srcFile.getFileName().toString(); + try { + Path targetFile = resolveTargetFileForExecute(srcFile, outputPath, execute); + + int addedEntries = handleSingleFile(targetFile, thresholdRatio, execute); + if (addedEntries > 0) { + traditionalEntries.addAndGet(addedEntries); + } + + int currentProcessed = processed.incrementAndGet(); + sendProgress(taskId, + total, + currentProcessed, + traditionalEntries.get(), + failed.get(), + fileName, + String.format("已扫描 (%d/%d):%s", currentProcessed, total, fileName), + false); + } catch (Exception e) { + failed.incrementAndGet(); + int currentProcessed = processed.incrementAndGet(); + log.warn("处理文件失败: {}", srcFile, e); + sendProgress(taskId, + total, + currentProcessed, + traditionalEntries.get(), + failed.get(), + fileName, + String.format("处理失败: %s - %s", fileName, e.getMessage()), + false); + } + } + + sendProgress(taskId, + total, + processed.get(), + traditionalEntries.get(), + failed.get(), + null, + String.format("任务完成!检测到繁体标签条目: %d,失败文件: %d", + traditionalEntries.get(), failed.get()), + true); + } catch (Exception e) { + log.error("繁体转换任务执行失败", e); + sendProgress(taskId, 0, 0, 0, 0, null, + "任务执行失败: " + e.getMessage(), true); + } + } + + /** + * 处理单个音频文件 + * + * @return 新增的“繁体标签条目”数量(字段级) + */ + private int handleSingleFile(Path file, double thresholdRatio, boolean execute) throws Exception { + AudioFile audioFile = AudioFileIO.read(file.toFile()); + Tag tag = audioFile.getTag(); + if (tag == null) { + return 0; + } + + int entries = 0; + + entries += handleSingleField(tag, FieldKey.TITLE, thresholdRatio, execute); + entries += handleSingleField(tag, FieldKey.ARTIST, thresholdRatio, execute); + entries += handleSingleField(tag, FieldKey.ALBUM, thresholdRatio, execute); + entries += handleSingleField(tag, FieldKey.ALBUM_ARTIST, thresholdRatio, execute); + + // 如果是执行模式且有字段被修改,则写回文件 + if (execute && entries > 0) { + audioFile.commit(); + } + + return entries; + } + + /** + * 处理单个标签字段:检测 + 可选转换 + * + * @return 如果该字段触发阈值,则返回 1,否则返回 0 + */ + private int handleSingleField(Tag tag, + FieldKey fieldKey, + double thresholdRatio, + boolean execute) { + String original = tag.getFirst(fieldKey); + if (original == null || original.trim().isEmpty()) { + return 0; + } + + ZhStats stats = traditionalFilterService.analyze(original); + if (stats.getTraditionalCount() <= 0) { + return 0; + } + + if (stats.getRatio() < thresholdRatio) { + return 0; + } + + if (execute) { + String converted = traditionalFilterService.toSimplified(original); + try { + tag.setField(fieldKey, converted); + } catch (Exception e) { + // 单个字段失败不影响整体流程 + log.debug("更新标签字段失败: {} - {}", fieldKey.name(), e.getMessage()); + } + } + + // 该字段计为 1 个“繁体标签条目” + return 1; + } + + private boolean isAudioFile(Path file) { + String name = file.getFileName().toString().toLowerCase(); + int idx = name.lastIndexOf('.'); + if (idx <= 0 || idx == name.length() - 1) { + return false; + } + String ext = name.substring(idx + 1); + return AUDIO_EXTENSIONS.contains(ext); + } + + /** + * 决定实际处理的目标文件: + * - 预览模式:始终返回原文件路径 + * - 执行模式: + * - 未配置输出目录:在原文件上就地修改 + * - 配置了输出目录:先复制到输出目录,再在副本上修改 + */ + private Path resolveTargetFileForExecute(Path srcFile, + Path outputPath, + boolean execute) throws Exception { + if (!execute || outputPath == null) { + return srcFile; + } + + Path targetFile = outputPath.resolve(srcFile.getFileName()); + if (!Files.exists(targetFile)) { + Files.copy(srcFile, targetFile, StandardCopyOption.COPY_ATTRIBUTES); + } + return targetFile; + } + + private void sendProgress(String taskId, + int total, + int processed, + int entries, + int failed, + String currentFile, + String message, + boolean completed) { + try { + ProgressMessage pm = new ProgressMessage(); + pm.setTaskId(taskId); + pm.setType("zhconvert"); + pm.setTotal(total); + pm.setProcessed(processed); + pm.setSuccess(entries); + pm.setFailed(failed); + pm.setCurrentFile(currentFile); + pm.setMessage(message); + pm.setCompleted(completed); + progressStore.put(pm); + messagingTemplate.convertAndSend("/topic/progress/" + taskId, pm); + } catch (Exception e) { + log.error("发送繁体转换进度消息失败", e); + } + } +} + diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..67107c1 --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,16 @@ +server: + port: 8080 + +spring: + application: + name: mangtool-backend + + jackson: + serialization: + write-dates-as-timestamps: false + +logging: + level: + root: INFO + com.music: DEBUG + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..b438654 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,19 @@ + + + + + MangTool 音乐工具箱 + + + + +

+ + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..7c69130 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2085 @@ +{ + "name": "mangtool-frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mangtool-frontend", + "version": "0.0.1", + "dependencies": { + "axios": "^1.7.7", + "element-plus": "^2.8.8", + "pinia": "^2.2.6", + "sockjs-client": "^1.6.1", + "stompjs": "^2.3.3", + "vue": "^3.5.12" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.4", + "vite": "^5.4.8" + } + }, + "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.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@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/@rollup/rollup-android-arm-eabi": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.0.tgz", + "integrity": "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.0.tgz", + "integrity": "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.0.tgz", + "integrity": "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.0.tgz", + "integrity": "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.0.tgz", + "integrity": "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.0.tgz", + "integrity": "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.0.tgz", + "integrity": "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.0.tgz", + "integrity": "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.0.tgz", + "integrity": "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.0.tgz", + "integrity": "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.0.tgz", + "integrity": "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.0.tgz", + "integrity": "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.0.tgz", + "integrity": "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.0.tgz", + "integrity": "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.0.tgz", + "integrity": "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.0.tgz", + "integrity": "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.0.tgz", + "integrity": "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.0.tgz", + "integrity": "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.0.tgz", + "integrity": "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.0.tgz", + "integrity": "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.0.tgz", + "integrity": "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.0.tgz", + "integrity": "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.0.tgz", + "integrity": "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.0.tgz", + "integrity": "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.0.tgz", + "integrity": "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ==", + "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", + "peer": true, + "dependencies": { + "@types/lodash": "*" + } + }, + "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": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz", + "integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/shared": "3.5.27", + "entities": "^7.0.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz", + "integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz", + "integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@vue/compiler-core": "3.5.27", + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27", + "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.27", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz", + "integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "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" + }, + "node_modules/@vue/reactivity": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.27.tgz", + "integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.27.tgz", + "integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/shared": "3.5.27" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz", + "integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.27", + "@vue/runtime-core": "3.5.27", + "@vue/shared": "3.5.27", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.27.tgz", + "integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "vue": "3.5.27" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.27.tgz", + "integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==", + "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/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/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.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", + "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bufferutil": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz", + "integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "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/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/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "optional": true, + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "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/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "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/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "optional": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "optional": true, + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "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/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "optional": true, + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "optional": true, + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "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/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT", + "optional": true + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT", + "peer": true + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT", + "peer": true + }, + "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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC", + "optional": true + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "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/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/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^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/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.57.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.0.tgz", + "integrity": "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA==", + "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.57.0", + "@rollup/rollup-android-arm64": "4.57.0", + "@rollup/rollup-darwin-arm64": "4.57.0", + "@rollup/rollup-darwin-x64": "4.57.0", + "@rollup/rollup-freebsd-arm64": "4.57.0", + "@rollup/rollup-freebsd-x64": "4.57.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", + "@rollup/rollup-linux-arm-musleabihf": "4.57.0", + "@rollup/rollup-linux-arm64-gnu": "4.57.0", + "@rollup/rollup-linux-arm64-musl": "4.57.0", + "@rollup/rollup-linux-loong64-gnu": "4.57.0", + "@rollup/rollup-linux-loong64-musl": "4.57.0", + "@rollup/rollup-linux-ppc64-gnu": "4.57.0", + "@rollup/rollup-linux-ppc64-musl": "4.57.0", + "@rollup/rollup-linux-riscv64-gnu": "4.57.0", + "@rollup/rollup-linux-riscv64-musl": "4.57.0", + "@rollup/rollup-linux-s390x-gnu": "4.57.0", + "@rollup/rollup-linux-x64-gnu": "4.57.0", + "@rollup/rollup-linux-x64-musl": "4.57.0", + "@rollup/rollup-openbsd-x64": "4.57.0", + "@rollup/rollup-openharmony-arm64": "4.57.0", + "@rollup/rollup-win32-arm64-msvc": "4.57.0", + "@rollup/rollup-win32-ia32-msvc": "4.57.0", + "@rollup/rollup-win32-x64-gnu": "4.57.0", + "@rollup/rollup-win32-x64-msvc": "4.57.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sockjs-client": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", + "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "eventsource": "^2.0.2", + "faye-websocket": "^0.11.4", + "inherits": "^2.0.4", + "url-parse": "^1.5.10" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://tidelift.com/funding/github/npm/sockjs-client" + } + }, + "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/stompjs": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/stompjs/-/stompjs-2.3.3.tgz", + "integrity": "sha512-5l/Ogz0DTFW7TrpHF0LAETGqM/so8UxNJvYZjJKqcX31EVprSQgnGkO80tZctPC/lFBDUrSFiTG3xd0R27XAIA==", + "license": "Apache-2.0", + "optionalDependencies": { + "websocket": "latest" + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC", + "optional": true + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz", + "integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/compiler-dom": "3.5.27", + "@vue/compiler-sfc": "3.5.27", + "@vue/runtime-dom": "3.5.27", + "@vue/server-renderer": "3.5.27", + "@vue/shared": "3.5.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/websocket": { + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz", + "integrity": "sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bufferutil": "^4.0.1", + "debug": "^2.2.0", + "es5-ext": "^0.10.63", + "typedarray-to-buffer": "^3.1.5", + "utf-8-validate": "^5.0.2", + "yaeti": "^0.0.6" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/websocket/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT", + "optional": true + }, + "node_modules/yaeti": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", + "integrity": "sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.32" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..1b734d9 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,24 @@ +{ + "name": "mangtool-frontend", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "echo \"no linter configured yet\"" + }, + "dependencies": { + "axios": "^1.7.7", + "element-plus": "^2.8.8", + "pinia": "^2.2.6", + "sockjs-client": "^1.6.1", + "stompjs": "^2.3.3", + "vue": "^3.5.12" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.4", + "vite": "^5.4.8" + } +} + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..d17781a --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,183 @@ + + + + + + diff --git a/frontend/src/api/aggregate.ts b/frontend/src/api/aggregate.ts new file mode 100644 index 0000000..21b025b --- /dev/null +++ b/frontend/src/api/aggregate.ts @@ -0,0 +1,18 @@ +import request from './request'; + +export interface AggregateRequest { + srcDir: string; + dstDir: string; + mode: 'copy' | 'move'; +} + +export interface AggregateResponse { + taskId: string; +} + +/** + * 启动音频文件汇聚任务 + */ +export function startAggregate(params: AggregateRequest): Promise { + return request.post('/api/aggregate/start', params); +} diff --git a/frontend/src/api/convert.ts b/frontend/src/api/convert.ts new file mode 100644 index 0000000..c63d0fe --- /dev/null +++ b/frontend/src/api/convert.ts @@ -0,0 +1,18 @@ +import request from './request'; + +export interface ConvertRequest { + srcDir: string; + dstDir: string; + mode: 'copy' | 'move'; +} + +export interface ConvertResponse { + taskId: string; +} + +/** + * 启动音频格式智能处理(转码)任务 + */ +export function startConvert(params: ConvertRequest): Promise { + return request.post('/api/convert/start', params); +} diff --git a/frontend/src/api/dedup.ts b/frontend/src/api/dedup.ts new file mode 100644 index 0000000..aebc102 --- /dev/null +++ b/frontend/src/api/dedup.ts @@ -0,0 +1,21 @@ +import request from './request'; + +export interface DedupRequest { + libraryDir: string; + trashDir: string; + useMd5: boolean; + useMetadata: boolean; + mode: 'copy' | 'move'; +} + +export interface DedupResponse { + taskId: string; +} + +/** + * 启动音乐去重任务 + */ +export function startDedup(params: DedupRequest): Promise { + return request.post('/api/dedup/start', params); +} + diff --git a/frontend/src/api/progress.ts b/frontend/src/api/progress.ts new file mode 100644 index 0000000..99b86fd --- /dev/null +++ b/frontend/src/api/progress.ts @@ -0,0 +1,18 @@ +import request from './request'; + +export interface ProgressMessage { + taskId: string; + type: string; + total: number; + processed: number; + success: number; + failed: number; + currentFile: string; + message: string; + completed: boolean; +} + +export function getProgress(taskId: string): Promise { + return request.get(`/api/progress/${taskId}`); +} + diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts new file mode 100644 index 0000000..3de1a3d --- /dev/null +++ b/frontend/src/api/request.ts @@ -0,0 +1,36 @@ +import axios from 'axios'; +import { ElMessage } from 'element-plus'; + +const request = axios.create({ + baseURL: 'http://localhost:8080', + timeout: 30000, + headers: { + 'Content-Type': 'application/json' + } +}); + +// 响应拦截器:统一处理 Result 格式 +request.interceptors.response.use( + (response) => { + const result = response.data; + // 如果后端返回的是 Result 格式 + if (result && typeof result === 'object' && 'code' in result) { + if (result.code === 0) { + // 成功,直接返回 data + return result.data; + } else { + // 失败,显示错误信息并 reject + ElMessage.error(result.message || '请求失败'); + return Promise.reject(new Error(result.message || '请求失败')); + } + } + // 如果不是 Result 格式,直接返回 + return result; + }, + (error) => { + ElMessage.error(error.message || '网络错误'); + return Promise.reject(error); + } +); + +export default request; diff --git a/frontend/src/api/zhconvert.ts b/frontend/src/api/zhconvert.ts new file mode 100644 index 0000000..4766e5b --- /dev/null +++ b/frontend/src/api/zhconvert.ts @@ -0,0 +1,20 @@ +import request from './request'; + +export interface ZhConvertRequest { + scanDir: string; + outputDir?: string; + threshold: number; + mode: 'preview' | 'execute'; +} + +export interface ZhConvertResponse { + taskId: string; +} + +/** + * 启动音乐元数据繁体转简体任务 + */ +export function startZhConvert(params: ZhConvertRequest): Promise { + return request.post('/api/zhconvert/start', params); +} + diff --git a/frontend/src/components/AggregateTab.vue b/frontend/src/components/AggregateTab.vue new file mode 100644 index 0000000..d0e018a --- /dev/null +++ b/frontend/src/components/AggregateTab.vue @@ -0,0 +1,526 @@ + + + + + + diff --git a/frontend/src/components/ConvertTab.vue b/frontend/src/components/ConvertTab.vue new file mode 100644 index 0000000..7efaf67 --- /dev/null +++ b/frontend/src/components/ConvertTab.vue @@ -0,0 +1,638 @@ + + + + + diff --git a/frontend/src/components/DedupTab.vue b/frontend/src/components/DedupTab.vue new file mode 100644 index 0000000..452adc4 --- /dev/null +++ b/frontend/src/components/DedupTab.vue @@ -0,0 +1,516 @@ + + + + + + diff --git a/frontend/src/components/MergeTab.vue b/frontend/src/components/MergeTab.vue new file mode 100644 index 0000000..f212bf3 --- /dev/null +++ b/frontend/src/components/MergeTab.vue @@ -0,0 +1,96 @@ + + + + + + diff --git a/frontend/src/components/RenameTab.vue b/frontend/src/components/RenameTab.vue new file mode 100644 index 0000000..d1b6aa1 --- /dev/null +++ b/frontend/src/components/RenameTab.vue @@ -0,0 +1,105 @@ + + + + + + diff --git a/frontend/src/components/SettingsTab.vue b/frontend/src/components/SettingsTab.vue new file mode 100644 index 0000000..cdc524a --- /dev/null +++ b/frontend/src/components/SettingsTab.vue @@ -0,0 +1,90 @@ + + + + + + diff --git a/frontend/src/components/TraditionalFilterTab.vue b/frontend/src/components/TraditionalFilterTab.vue new file mode 100644 index 0000000..9e82ff8 --- /dev/null +++ b/frontend/src/components/TraditionalFilterTab.vue @@ -0,0 +1,554 @@ + + + + + diff --git a/frontend/src/composables/useWebSocket.ts b/frontend/src/composables/useWebSocket.ts new file mode 100644 index 0000000..3ebdcfd --- /dev/null +++ b/frontend/src/composables/useWebSocket.ts @@ -0,0 +1,100 @@ +import { ref } from 'vue'; +import SockJS from 'sockjs-client'; +import Stomp from 'stompjs'; + +export interface ProgressMessage { + taskId: string; + type: string; + total: number; + processed: number; + success: number; + failed: number; + currentFile: string; + message: string; + completed: boolean; +} + +export function useWebSocket(taskId: string | null, onMessage: (msg: ProgressMessage) => void) { + const connected = ref(false); + const error = ref(null); + let stompClient: Stomp.Client | null = null; + + const connect = () => { + if (!taskId) { + return; + } + + // 如果已有连接,先断开 + if (stompClient) { + disconnect(); + } + + try { + const socket = new SockJS('http://localhost:8080/ws'); + stompClient = Stomp.over(socket); + + // 禁用调试日志 + stompClient.debug = () => {}; + + stompClient.connect( + {}, + () => { + // 连接成功 + connected.value = true; + error.value = null; + + // 订阅任务进度 + if (stompClient) { + stompClient.subscribe(`/topic/progress/${taskId}`, (message) => { + try { + const progress: ProgressMessage = JSON.parse(message.body); + onMessage(progress); + } catch (e) { + console.error('Failed to parse progress message:', e); + } + }); + } + }, + (errorFrame) => { + // 连接失败 + const errorMsg = errorFrame.headers?.['message'] || errorFrame.toString() || 'WebSocket 连接错误'; + error.value = errorMsg; + connected.value = false; + console.error('WebSocket 连接失败:', errorMsg, errorFrame); + } + ); + } catch (e) { + const errorMsg = e instanceof Error ? e.message : 'WebSocket 初始化失败'; + error.value = errorMsg; + connected.value = false; + console.error('WebSocket 初始化失败:', e); + } + }; + + const disconnect = () => { + if (stompClient) { + try { + if (connected.value) { + stompClient.disconnect(() => { + connected.value = false; + }); + } else { + // 如果连接未成功,直接清理 + connected.value = false; + } + } catch (e) { + console.error('断开 WebSocket 连接时出错:', e); + connected.value = false; + } + stompClient = null; + } + error.value = null; + }; + + return { + connected, + error, + connect, + disconnect + }; +} diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..b6c65d1 --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,14 @@ +import { createApp } from 'vue'; +import { createPinia } from 'pinia'; +import ElementPlus from 'element-plus'; +import 'element-plus/dist/index.css'; +import './styles/reset.css'; +import App from './App.vue'; + +const app = createApp(App); + +app.use(createPinia()); +app.use(ElementPlus); + +app.mount('#app'); + diff --git a/frontend/src/styles/reset.css b/frontend/src/styles/reset.css new file mode 100644 index 0000000..f6e1623 --- /dev/null +++ b/frontend/src/styles/reset.css @@ -0,0 +1,26 @@ +/* 重置 body 默认样式 */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif, 'Noto Sans SC', 'HarmonyOS Sans SC'; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +#app { + width: 100%; + height: 100%; +} diff --git a/frontend/vite.config.mts b/frontend/vite.config.mts new file mode 100644 index 0000000..ec645ea --- /dev/null +++ b/frontend/vite.config.mts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +export default defineConfig({ + plugins: [vue()], + define: { + global: 'globalThis', + }, + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true + }, + '/ws': { + target: 'http://localhost:8080', + changeOrigin: true, + ws: true + } + } + } +}); +