diff --git a/README.md b/README.md
index 628792c..4fbdda6 100644
--- a/README.md
+++ b/README.md
@@ -1,93 +1,72 @@
# SVN Manager
-SVN管理工具 - 多项目管理界面
-
-## 项目简介
-
-基于 JavaFX 开发的 SVN 管理工具,提供图形化界面进行 SVN 仓库的日常操作。
+SVN 管理工具 - 多项目管理界面
## 技术栈
-- **Java**: 11+
-- **JavaFX**: 17.0.2
-- **Maven**: 构建工具
-- **Jackson**: JSON 解析
-- **Logback**: 日志管理
-
-## 功能特性
-
-### 1. 仓库管理
-- Checkout 检出仓库
-- Update 更新仓库
-- Commit 提交修改
-
-### 2. 文件操作
-- Add 添加文件
-- Delete 删除文件
-- Revert 回退文件
-
-### 3. 版本查看
-- Status 查看状态
-- Log 查看日志
-- Diff 查看差异
-- Info 查看信息
-
-### 4. 分支管理
-- Create Branch 创建分支
-- Switch 切换分支
-- Merge 合并分支
+- **前端**: Vue 3 + Vite + Pinia + Axios(IDEA 风格深色主题)
+- **后端**: Java 17 + Spring Boot 3,REST API,本地调用 `svn` CLI
+- **配置**: 项目列表存储在 `~/.svn-manager/projects.json`
## 项目结构
```
svn-manager/
-├── src/
-│ ├── main/
-│ │ ├── java/
-│ │ │ └── com/svnmanager/
-│ │ │ ├── controller/ # UI控制器
-│ │ │ ├── service/ # SVN服务封装
-│ │ │ ├── model/ # 数据模型
-│ │ │ ├── util/ # 工具类
-│ │ │ └── MainApp.java
-│ │ └── resources/
-│ │ ├── fxml/ # FXML界面文件
-│ │ ├── css/ # 样式文件
-│ │ └── application.properties
-│ └── test/
-├── pom.xml
+├── backend/ # Java Spring Boot 后端
+│ ├── src/main/java/com/svnmanager/
+│ │ ├── controller/ # REST 控制器
+│ │ ├── service/ # SVN 服务与文件树
+│ │ ├── model/ # 数据模型
+│ │ └── util/ # 配置与进程工具
+│ └── pom.xml
+├── frontend/ # Vue 3 前端
+│ ├── src/
+│ │ ├── api/ # API 客户端
+│ │ ├── components/ # 侧栏、工具栏、文件树、弹窗
+│ │ ├── stores/ # Pinia 状态
+│ │ └── App.vue
+│ └── package.json
└── README.md
```
-## 构建与运行
+## 运行方式
### 前置要求
-- JDK 11 或更高版本
-- Maven 3.6+
-- SVN 客户端已安装并配置在系统 PATH 中
-### 编译项目
+- JDK 17+
+- Node.js 18+
+- 系统已安装 `svn` 并已在 PATH 中
+
+### 1. 启动后端
+
```bash
-mvn clean compile
+cd backend
+mvn spring-boot:run
```
-### 运行项目
+后端默认端口:`8080`,API 前缀:`/api`。
+
+### 2. 启动前端
+
```bash
-mvn javafx:run
+cd frontend
+npm install
+npm run dev
```
-### 打包项目
-```bash
-mvn clean package
-```
+浏览器访问 Vite 提供的地址(如 `http://localhost:5173`)。开发环境下 Vite 会将 `/api` 代理到 `http://localhost:8080`。
-## 开发说明
+### 3. 生产构建
-项目采用 MVC 架构模式:
-- **Controller**: 处理 UI 交互逻辑
-- **Service**: 封装 SVN 命令调用
-- **Model**: 解析 SVN 输出数据
-- **Util**: 提供通用工具方法
+- 后端:`cd backend && mvn package`,运行 `java -jar target/svn-manager-backend-1.0.0.jar`
+- 前端:`cd frontend && npm run build`,将 `dist/` 部署到任意静态服务器,并配置 API 代理或同域部署
+
+## 功能说明
+
+- **项目列表**:左侧栏展示已配置项目,点击切换当前项目
+- **添加项目**:填写本地工作副本路径(及可选 SVN 地址、账号密码)保存到 `~/.svn-manager/projects.json`
+- **文件树**:选中项目后自动拉取 `svn status` 并展示树形结构,支持 SVN 状态徽章(M/A/D/C/?)
+- **工具栏**:刷新状态、更新、提交、日志、差异;提交/日志/差异通过弹窗完成
## 许可证
diff --git a/backend/pom.xml b/backend/pom.xml
new file mode 100644
index 0000000..77f07f7
--- /dev/null
+++ b/backend/pom.xml
@@ -0,0 +1,69 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.0
+
+
+
+ com.svnmanager
+ svn-manager-backend
+ 1.0.0
+ jar
+ SVN Manager Backend
+ SVN 管理工具 - REST API 后端
+
+
+ 17
+ 2.15.2
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+ ch.qos.logback
+ logback-classic
+
+
+
+ com.tmatesoft.svnkit
+ svnkit
+ 1.10.13
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
diff --git a/backend/src/main/java/com/svnmanager/SvnManagerApplication.java b/backend/src/main/java/com/svnmanager/SvnManagerApplication.java
new file mode 100644
index 0000000..6952f6d
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/SvnManagerApplication.java
@@ -0,0 +1,11 @@
+package com.svnmanager;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class SvnManagerApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(SvnManagerApplication.class, args);
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/config/CorsConfig.java b/backend/src/main/java/com/svnmanager/config/CorsConfig.java
new file mode 100644
index 0000000..4830106
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/config/CorsConfig.java
@@ -0,0 +1,23 @@
+package com.svnmanager.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;
+import java.util.List;
+
+@Configuration
+public class CorsConfig {
+ @Bean
+ public CorsFilter corsFilter() {
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowCredentials(true);
+ config.setAllowedOriginPatterns(List.of("*"));
+ config.addAllowedHeader("*");
+ config.addAllowedMethod("*");
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/api/**", config);
+ return new CorsFilter(source);
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/controller/ProjectController.java b/backend/src/main/java/com/svnmanager/controller/ProjectController.java
new file mode 100644
index 0000000..f24ddd4
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/controller/ProjectController.java
@@ -0,0 +1,40 @@
+package com.svnmanager.controller;
+
+import com.svnmanager.model.Project;
+import com.svnmanager.util.ConfigUtil;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/projects")
+public class ProjectController {
+
+ @GetMapping
+ public List list() {
+ return ConfigUtil.loadProjects();
+ }
+
+ @PostMapping
+ public ResponseEntity> add(@RequestBody Project project) {
+ if (project.getName() == null || project.getName().trim().isEmpty()
+ || project.getPath() == null || project.getPath().trim().isEmpty()) {
+ return ResponseEntity.badRequest().body("项目名称和路径不能为空");
+ }
+ boolean saved = ConfigUtil.addProject(project);
+ return saved ? ResponseEntity.ok(project) : ResponseEntity.internalServerError().build();
+ }
+
+ @PutMapping
+ public ResponseEntity> update(@RequestBody Project project) {
+ boolean ok = ConfigUtil.updateProject(project);
+ return ok ? ResponseEntity.ok(project) : ResponseEntity.notFound().build();
+ }
+
+ @DeleteMapping("/{id}")
+ public ResponseEntity> delete(@PathVariable String id) {
+ boolean ok = ConfigUtil.deleteProject(id);
+ return ok ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/controller/SvnController.java b/backend/src/main/java/com/svnmanager/controller/SvnController.java
new file mode 100644
index 0000000..1e6407f
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/controller/SvnController.java
@@ -0,0 +1,156 @@
+package com.svnmanager.controller;
+
+import com.svnmanager.model.FileTreeItem;
+import com.svnmanager.model.Project;
+import com.svnmanager.model.SvnLog;
+import com.svnmanager.model.SvnStatus;
+import com.svnmanager.service.*;
+import com.svnmanager.util.ConfigUtil;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/svn")
+public class SvnController {
+
+ private final StatusService statusService;
+ private final UpdateService updateService;
+ private final CommitService commitService;
+ private final LogService logService;
+ private final DiffService diffService;
+ private final InfoService infoService;
+ private final FileTreeService fileTreeService;
+
+ public SvnController(StatusService statusService, UpdateService updateService,
+ CommitService commitService, LogService logService,
+ DiffService diffService, InfoService infoService,
+ FileTreeService fileTreeService) {
+ this.statusService = statusService;
+ this.updateService = updateService;
+ this.commitService = commitService;
+ this.logService = logService;
+ this.diffService = diffService;
+ this.infoService = infoService;
+ this.fileTreeService = fileTreeService;
+ }
+
+ @GetMapping("/status/{projectId}")
+ public ResponseEntity> status(@PathVariable String projectId) {
+ Project project = ConfigUtil.getProjectById(projectId);
+ if (project == null) return ResponseEntity.notFound().build();
+ try {
+ SvnStatus status = statusService.getStatus(project.getPath(), project.getUsername(), project.getPassword());
+ FileTreeItem fileTree = fileTreeService.buildFileTree(project.getPath(), status);
+ return ResponseEntity.ok(new StatusResponse(status, fileTree));
+ } catch (Exception e) {
+ return ResponseEntity.internalServerError().body(e.getMessage());
+ }
+ }
+
+ @PostMapping("/update/{projectId}")
+ public ResponseEntity> update(@PathVariable String projectId) {
+ Project project = ConfigUtil.getProjectById(projectId);
+ if (project == null) return ResponseEntity.notFound().build();
+ try {
+ UpdateService.UpdateResult r = updateService.update(project.getPath(), project.getUsername(), project.getPassword());
+ return ResponseEntity.ok(r);
+ } catch (Exception e) {
+ return ResponseEntity.internalServerError().body(e.getMessage());
+ }
+ }
+
+ @PostMapping("/commit/{projectId}")
+ public ResponseEntity> commit(@PathVariable String projectId, @RequestBody CommitRequest body) {
+ Project project = ConfigUtil.getProjectById(projectId);
+ if (project == null) return ResponseEntity.notFound().build();
+ if (body == null || body.message == null || body.message.trim().isEmpty()) {
+ return ResponseEntity.badRequest().body("提交消息不能为空");
+ }
+ try {
+ CommitService.CommitResult r = commitService.commit(project.getPath(), body.message,
+ project.getUsername(), project.getPassword());
+ return ResponseEntity.ok(r);
+ } catch (Exception e) {
+ return ResponseEntity.internalServerError().body(e.getMessage());
+ }
+ }
+
+ @GetMapping("/log/{projectId}")
+ public ResponseEntity> log(@PathVariable String projectId,
+ @RequestParam(defaultValue = "20") int limit) {
+ Project project = ConfigUtil.getProjectById(projectId);
+ if (project == null) return ResponseEntity.notFound().build();
+ try {
+ List logs = logService.getLog(project.getPath(), limit, project.getUsername(), project.getPassword());
+ return ResponseEntity.ok(logs);
+ } catch (Exception e) {
+ return ResponseEntity.internalServerError().body(e.getMessage());
+ }
+ }
+
+ @GetMapping("/diff/{projectId}")
+ public ResponseEntity> diff(@PathVariable String projectId,
+ @RequestParam(required = false) String path) {
+ Project project = ConfigUtil.getProjectById(projectId);
+ if (project == null) return ResponseEntity.notFound().build();
+ try {
+ String diff = diffService.getDiff(project.getPath(), path, project.getUsername(), project.getPassword());
+ return ResponseEntity.ok(new DiffResponse(diff));
+ } catch (Exception e) {
+ return ResponseEntity.internalServerError().body(e.getMessage());
+ }
+ }
+
+ @GetMapping(value = "/file/{projectId}", produces = MediaType.TEXT_PLAIN_VALUE)
+ public ResponseEntity> fileContent(@PathVariable String projectId,
+ @RequestParam String path) {
+ Project project = ConfigUtil.getProjectById(projectId);
+ if (project == null) return ResponseEntity.notFound().build();
+ if (path == null || path.trim().isEmpty()) {
+ return ResponseEntity.badRequest().body("path 不能为空");
+ }
+ try {
+ Path projectPath = Paths.get(project.getPath()).toAbsolutePath().normalize();
+ Path filePath = projectPath.resolve(path.replace("\\", "/")).normalize();
+ if (!filePath.startsWith(projectPath)) {
+ return ResponseEntity.badRequest().body("无效路径");
+ }
+ if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
+ return ResponseEntity.notFound().build();
+ }
+ String content = Files.readString(filePath, StandardCharsets.UTF_8);
+ return ResponseEntity.ok()
+ .header(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE + "; charset=UTF-8")
+ .body(content);
+ } catch (IOException e) {
+ return ResponseEntity.internalServerError().body(e.getMessage());
+ }
+ }
+
+ public static class StatusResponse {
+ public SvnStatus status;
+ public FileTreeItem fileTree;
+ public StatusResponse(SvnStatus status, FileTreeItem fileTree) {
+ this.status = status;
+ this.fileTree = fileTree;
+ }
+ }
+
+ public static class CommitRequest {
+ public String message;
+ }
+
+ public static class DiffResponse {
+ public String diff;
+ public DiffResponse(String diff) { this.diff = diff; }
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/model/FileTreeItem.java b/backend/src/main/java/com/svnmanager/model/FileTreeItem.java
new file mode 100644
index 0000000..2694067
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/model/FileTreeItem.java
@@ -0,0 +1,81 @@
+package com.svnmanager.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 文件树节点模型
+ * 用于TreeView展示文件和目录的层级结构
+ */
+public class FileTreeItem {
+ private String name;
+ private String path;
+ private boolean isDirectory;
+ private SvnFileStatus.FileStatus status;
+ private List children;
+
+ public FileTreeItem(String name, String path, boolean isDirectory) {
+ this.name = name;
+ this.path = path;
+ this.isDirectory = isDirectory;
+ this.status = SvnFileStatus.FileStatus.NORMAL;
+ this.children = new ArrayList<>();
+ }
+
+ public FileTreeItem(String name, String path, boolean isDirectory, SvnFileStatus.FileStatus status) {
+ this.name = name;
+ this.path = path;
+ this.isDirectory = isDirectory;
+ this.status = status;
+ this.children = new ArrayList<>();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public void setPath(String path) {
+ this.path = path;
+ }
+
+ public boolean isDirectory() {
+ return isDirectory;
+ }
+
+ public void setDirectory(boolean directory) {
+ isDirectory = directory;
+ }
+
+ public SvnFileStatus.FileStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(SvnFileStatus.FileStatus status) {
+ this.status = status;
+ }
+
+ public List getChildren() {
+ return children;
+ }
+
+ public void setChildren(List children) {
+ this.children = children;
+ }
+
+ public void addChild(FileTreeItem child) {
+ this.children.add(child);
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/src/main/java/com/svnmanager/model/Project.java b/backend/src/main/java/com/svnmanager/model/Project.java
similarity index 100%
rename from src/main/java/com/svnmanager/model/Project.java
rename to backend/src/main/java/com/svnmanager/model/Project.java
diff --git a/src/main/java/com/svnmanager/model/SvnFileStatus.java b/backend/src/main/java/com/svnmanager/model/SvnFileStatus.java
similarity index 100%
rename from src/main/java/com/svnmanager/model/SvnFileStatus.java
rename to backend/src/main/java/com/svnmanager/model/SvnFileStatus.java
diff --git a/src/main/java/com/svnmanager/model/SvnInfo.java b/backend/src/main/java/com/svnmanager/model/SvnInfo.java
similarity index 100%
rename from src/main/java/com/svnmanager/model/SvnInfo.java
rename to backend/src/main/java/com/svnmanager/model/SvnInfo.java
diff --git a/src/main/java/com/svnmanager/model/SvnLog.java b/backend/src/main/java/com/svnmanager/model/SvnLog.java
similarity index 100%
rename from src/main/java/com/svnmanager/model/SvnLog.java
rename to backend/src/main/java/com/svnmanager/model/SvnLog.java
diff --git a/src/main/java/com/svnmanager/model/SvnStatus.java b/backend/src/main/java/com/svnmanager/model/SvnStatus.java
similarity index 100%
rename from src/main/java/com/svnmanager/model/SvnStatus.java
rename to backend/src/main/java/com/svnmanager/model/SvnStatus.java
diff --git a/backend/src/main/java/com/svnmanager/service/CheckoutService.java b/backend/src/main/java/com/svnmanager/service/CheckoutService.java
new file mode 100644
index 0000000..9af3a24
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/service/CheckoutService.java
@@ -0,0 +1,46 @@
+package com.svnmanager.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.SVNURL;
+import org.tmatesoft.svn.core.wc.SVNClientManager;
+import org.tmatesoft.svn.core.SVNDepth;
+import org.tmatesoft.svn.core.wc.SVNRevision;
+
+import java.io.File;
+
+/**
+ * Checkout 服务(基于 SVNKit)
+ */
+@Service
+public class CheckoutService extends SvnService {
+ private static final Logger logger = LoggerFactory.getLogger(CheckoutService.class);
+
+ public boolean checkout(String svnUrl, String targetPath, String revision) {
+ logger.info("检出仓库: {} 到 {}", svnUrl, targetPath);
+
+ SVNRevision rev = (revision != null && !revision.isEmpty())
+ ? SVNRevision.create(Long.parseLong(revision.replaceAll("\\D", "")))
+ : SVNRevision.HEAD;
+
+ SVNClientManager clientManager = createClientManager(null, null);
+ try {
+ SVNURL url = SVNURL.parseURIEncoded(svnUrl);
+ File target = new File(targetPath);
+ clientManager.getUpdateClient().doCheckout(url, target, rev, rev, SVNDepth.INFINITY, true);
+ logger.info("检出成功");
+ return true;
+ } catch (SVNException e) {
+ logger.error("检出失败: {}", e.getMessage());
+ return false;
+ } finally {
+ clientManager.dispose();
+ }
+ }
+
+ public boolean checkout(String svnUrl, String targetPath) {
+ return checkout(svnUrl, targetPath, null);
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/service/CommitService.java b/backend/src/main/java/com/svnmanager/service/CommitService.java
new file mode 100644
index 0000000..fc212ae
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/service/CommitService.java
@@ -0,0 +1,115 @@
+package com.svnmanager.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.tmatesoft.svn.core.SVNCommitInfo;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.wc.SVNClientManager;
+import org.tmatesoft.svn.core.SVNDepth;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Commit 服务(基于 SVNKit)
+ */
+@Service
+public class CommitService extends SvnService {
+ private static final Logger logger = LoggerFactory.getLogger(CommitService.class);
+
+ public CommitResult commit(String workingDirectory, String message, List files,
+ String username, String password) {
+ logger.info("提交修改: {}", workingDirectory);
+
+ if (!isValidWorkingCopy(workingDirectory)) {
+ throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
+ }
+
+ if (message == null || message.trim().isEmpty()) {
+ throw new IllegalArgumentException("提交消息不能为空");
+ }
+
+ File wcRoot = new File(workingDirectory);
+ File[] commitPaths = (files != null && !files.isEmpty())
+ ? files.stream().map(f -> new File(wcRoot, f)).toArray(File[]::new)
+ : new File[]{wcRoot};
+
+ CommitResult commitResult = new CommitResult();
+ SVNClientManager clientManager = createClientManager(username, password);
+ try {
+ SVNCommitInfo info = clientManager.getCommitClient().doCommit(
+ commitPaths,
+ true,
+ message,
+ null,
+ null,
+ false,
+ false,
+ SVNDepth.INFINITY);
+ commitResult.setSuccess(true);
+ if (info.getNewRevision() >= 0) {
+ commitResult.setRevision(String.valueOf(info.getNewRevision()));
+ }
+ logger.info("提交成功,版本: {}", commitResult.getRevision());
+ } catch (SVNException e) {
+ logger.error("提交失败: {}", e.getMessage());
+ commitResult.setSuccess(false);
+ commitResult.setError(e.getMessage());
+ } finally {
+ clientManager.dispose();
+ }
+
+ return commitResult;
+ }
+
+ public CommitResult commit(String workingDirectory, String message, String username, String password) {
+ return commit(workingDirectory, message, null, username, password);
+ }
+
+ public CommitResult commit(String workingDirectory, String message) {
+ return commit(workingDirectory, message, null, null);
+ }
+
+ /**
+ * 提交结果
+ */
+ public static class CommitResult {
+ private boolean success;
+ private String revision;
+ private String output;
+ private String error;
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ public String getRevision() {
+ return revision;
+ }
+
+ public void setRevision(String revision) {
+ this.revision = revision;
+ }
+
+ public String getOutput() {
+ return output;
+ }
+
+ public void setOutput(String output) {
+ this.output = output;
+ }
+
+ public String getError() {
+ return error;
+ }
+
+ public void setError(String error) {
+ this.error = error;
+ }
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/service/DiffService.java b/backend/src/main/java/com/svnmanager/service/DiffService.java
new file mode 100644
index 0000000..cbc0525
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/service/DiffService.java
@@ -0,0 +1,67 @@
+package com.svnmanager.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.wc.SVNClientManager;
+import org.tmatesoft.svn.core.SVNDepth;
+import org.tmatesoft.svn.core.wc.SVNRevision;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Diff 服务(基于 SVNKit)
+ */
+@Service
+public class DiffService extends SvnService {
+ private static final Logger logger = LoggerFactory.getLogger(DiffService.class);
+
+ public String getDiff(String workingDirectory, String filePath, String username, String password) {
+ logger.debug("获取差异: {}", workingDirectory);
+
+ if (!isValidWorkingCopy(workingDirectory)) {
+ throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
+ }
+
+ File wcRoot = new File(workingDirectory);
+ File[] paths = (filePath != null && !filePath.isEmpty())
+ ? new File[]{new File(wcRoot, filePath)}
+ : new File[]{wcRoot};
+
+ // 使用双路径形式 (path@BASE vs path@WORKING),无需访问远程仓库,避免 E195002 pegged diff 错误
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ SVNClientManager clientManager = createClientManager(username, password);
+ try {
+ for (File path : paths) {
+ clientManager.getDiffClient().doDiff(
+ path,
+ SVNRevision.BASE,
+ path,
+ SVNRevision.WORKING,
+ SVNDepth.INFINITY,
+ false,
+ out,
+ null);
+ }
+ return out.toString(StandardCharsets.UTF_8.name());
+ } catch (SVNException e) {
+ logger.warn("获取差异失败: {}", e.getMessage());
+ throw new RuntimeException(e);
+ } catch (java.io.UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ } finally {
+ clientManager.dispose();
+ }
+ }
+
+ public String getDiff(String workingDirectory) {
+ return getDiff(workingDirectory, null, null, null);
+ }
+
+ public String getDiff(String workingDirectory, String filePath) {
+ return getDiff(workingDirectory, filePath, null, null);
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/service/FileTreeService.java b/backend/src/main/java/com/svnmanager/service/FileTreeService.java
new file mode 100644
index 0000000..8413e20
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/service/FileTreeService.java
@@ -0,0 +1,59 @@
+package com.svnmanager.service;
+
+import com.svnmanager.model.FileTreeItem;
+import com.svnmanager.model.SvnFileStatus;
+import com.svnmanager.model.SvnStatus;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Map;
+
+@Service
+public class FileTreeService {
+
+ public FileTreeItem buildFileTree(String projectPath, SvnStatus status) {
+ File projectDir = new File(projectPath);
+ FileTreeItem root = new FileTreeItem(projectDir.getName(), projectPath, true);
+ root.setStatus(SvnFileStatus.FileStatus.NORMAL);
+
+ Map pathMap = new HashMap<>();
+ pathMap.put(projectPath, root);
+
+ for (SvnFileStatus fileStatus : status.getFiles()) {
+ String relativePath = fileStatus.getPath();
+ File file = new File(projectPath, relativePath);
+ String fullPath = file.getAbsolutePath();
+ String name = file.getName();
+
+ FileTreeItem parent = ensureParentExists(file.getParentFile(), pathMap, projectPath);
+ FileTreeItem node = new FileTreeItem(name, fullPath, file.isDirectory(), fileStatus.getStatus());
+ parent.getChildren().add(node);
+ pathMap.put(fullPath, node);
+ }
+
+ sortTree(root);
+ return root;
+ }
+
+ private FileTreeItem ensureParentExists(File parent, Map pathMap, String projectPath) {
+ if (parent == null) return pathMap.get(projectPath);
+ String parentPath = parent.getAbsolutePath();
+ if (parentPath.equals(projectPath)) return pathMap.get(projectPath);
+ if (pathMap.containsKey(parentPath)) return pathMap.get(parentPath);
+ FileTreeItem grandParent = ensureParentExists(parent.getParentFile(), pathMap, projectPath);
+ FileTreeItem dirItem = new FileTreeItem(parent.getName(), parentPath, true);
+ grandParent.getChildren().add(dirItem);
+ pathMap.put(parentPath, dirItem);
+ return dirItem;
+ }
+
+ private void sortTree(FileTreeItem item) {
+ if (item.getChildren().isEmpty()) return;
+ item.getChildren().sort(Comparator
+ .comparing(FileTreeItem::isDirectory, Comparator.reverseOrder())
+ .thenComparing(FileTreeItem::getName, String.CASE_INSENSITIVE_ORDER));
+ item.getChildren().forEach(this::sortTree);
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/service/InfoService.java b/backend/src/main/java/com/svnmanager/service/InfoService.java
new file mode 100644
index 0000000..388c2b5
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/service/InfoService.java
@@ -0,0 +1,81 @@
+package com.svnmanager.service;
+
+import com.svnmanager.model.SvnInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.SVNNodeKind;
+import org.tmatesoft.svn.core.wc.SVNClientManager;
+import org.tmatesoft.svn.core.wc.SVNRevision;
+import org.tmatesoft.svn.core.wc.SVNInfo;
+
+import java.io.File;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Info 服务(基于 SVNKit)
+ */
+@Service
+public class InfoService extends SvnService {
+ private static final Logger logger = LoggerFactory.getLogger(InfoService.class);
+ private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ public SvnInfo getInfo(String workingDirectory, String username, String password) {
+ logger.debug("获取信息: {}", workingDirectory);
+
+ if (!isValidWorkingCopy(workingDirectory)) {
+ throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
+ }
+
+ SVNClientManager clientManager = createClientManager(username, password);
+ try {
+ SVNInfo info = clientManager.getWCClient().doInfo(new File(workingDirectory), SVNRevision.WORKING);
+ return mapToSvnInfo(info, workingDirectory);
+ } catch (SVNException e) {
+ logger.warn("获取信息失败: {}", e.getMessage());
+ throw new RuntimeException(e);
+ } finally {
+ clientManager.dispose();
+ }
+ }
+
+ public SvnInfo getInfo(String workingDirectory) {
+ return getInfo(workingDirectory, null, null);
+ }
+
+ private static SvnInfo mapToSvnInfo(SVNInfo info, String workingDirectory) {
+ SvnInfo out = new SvnInfo();
+ if (info.getFile() != null) {
+ out.setPath(info.getFile().getAbsolutePath());
+ } else {
+ out.setPath(workingDirectory);
+ }
+ if (info.getURL() != null) {
+ out.setUrl(info.getURL().toString());
+ }
+ if (info.getRepositoryRootURL() != null) {
+ out.setRepositoryRoot(info.getRepositoryRootURL().toString());
+ }
+ out.setRepositoryUuid(info.getRepositoryUUID());
+ if (info.getRevision() != null && info.getRevision().isValid()) {
+ out.setRevision(String.valueOf(info.getRevision().getNumber()));
+ }
+ SVNNodeKind kind = info.getKind();
+ if (kind != null) {
+ out.setNodeKind(kind.toString());
+ }
+ String schedule = info.getSchedule();
+ out.setSchedule(schedule != null ? schedule : "normal");
+ out.setLastChangedAuthor(info.getAuthor());
+ if (info.getCommittedRevision() != null && info.getCommittedRevision().isValid()) {
+ out.setLastChangedRev(String.valueOf(info.getCommittedRevision().getNumber()));
+ }
+ if (info.getCommittedDate() != null) {
+ out.setLastChangedDate(LocalDateTime.ofInstant(info.getCommittedDate().toInstant(), ZoneId.systemDefault()).format(DATE_FORMAT));
+ }
+ return out;
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/service/LogService.java b/backend/src/main/java/com/svnmanager/service/LogService.java
new file mode 100644
index 0000000..3cdee00
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/service/LogService.java
@@ -0,0 +1,81 @@
+package com.svnmanager.service;
+
+import com.svnmanager.model.SvnLog;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.SVNLogEntryPath;
+import org.tmatesoft.svn.core.wc.SVNClientManager;
+import org.tmatesoft.svn.core.wc.SVNRevision;
+
+import java.io.File;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Log 服务(基于 SVNKit)
+ */
+@Service
+public class LogService extends SvnService {
+ private static final Logger logger = LoggerFactory.getLogger(LogService.class);
+
+ public List getLog(String workingDirectory, Integer limit, String username, String password) {
+ logger.debug("获取日志: {}", workingDirectory);
+
+ if (!isValidWorkingCopy(workingDirectory)) {
+ throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
+ }
+
+ List logs = new ArrayList<>();
+ File wcRoot = new File(workingDirectory);
+ int limitVal = (limit != null && limit > 0) ? limit : 50;
+
+ SVNClientManager clientManager = createClientManager(username, password);
+ try {
+ clientManager.getLogClient().doLog(
+ new File[]{wcRoot},
+ SVNRevision.HEAD,
+ SVNRevision.create(0),
+ SVNRevision.HEAD,
+ true,
+ true,
+ false,
+ limitVal,
+ null,
+ entry -> {
+ SvnLog log = new SvnLog();
+ log.setRevision(String.valueOf(entry.getRevision()));
+ log.setAuthor(entry.getAuthor());
+ if (entry.getDate() != null) {
+ log.setDate(LocalDateTime.ofInstant(entry.getDate().toInstant(), ZoneId.systemDefault()));
+ }
+ log.setMessage(entry.getMessage() != null ? entry.getMessage() : "");
+ if (entry.getChangedPaths() != null) {
+ for (Map.Entry e : entry.getChangedPaths().entrySet()) {
+ log.addChangedPath(e.getKey());
+ }
+ }
+ logs.add(log);
+ });
+ } catch (SVNException e) {
+ logger.warn("获取日志失败: {}", e.getMessage());
+ throw new RuntimeException(e);
+ } finally {
+ clientManager.dispose();
+ }
+
+ return logs;
+ }
+
+ public List getLog(String workingDirectory, String username, String password) {
+ return getLog(workingDirectory, 50, username, password);
+ }
+
+ public List getLog(String workingDirectory) {
+ return getLog(workingDirectory, 50, null, null);
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/service/StatusService.java b/backend/src/main/java/com/svnmanager/service/StatusService.java
new file mode 100644
index 0000000..63141a6
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/service/StatusService.java
@@ -0,0 +1,83 @@
+package com.svnmanager.service;
+
+import com.svnmanager.model.SvnFileStatus;
+import com.svnmanager.model.SvnStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.wc.SVNClientManager;
+import org.tmatesoft.svn.core.wc.SVNStatus;
+import org.tmatesoft.svn.core.wc.SVNStatusType;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Status 服务(基于 SVNKit)
+ */
+@Service
+public class StatusService extends SvnService {
+ private static final Logger logger = LoggerFactory.getLogger(StatusService.class);
+
+ public SvnStatus getStatus(String workingDirectory, String username, String password) {
+ logger.debug("获取状态: {}", workingDirectory);
+
+ if (!isValidWorkingCopy(workingDirectory)) {
+ throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
+ }
+
+ File wcRoot = new File(workingDirectory);
+ String wcRootPath = wcRoot.getAbsolutePath();
+ List files = new ArrayList<>();
+
+ SVNClientManager clientManager = createClientManager(username, password);
+ try {
+ // reportAll=true: 列出所有版本化/未版本化项,与 svn status -v 一致,供右侧文件树完整展示
+ clientManager.getStatusClient().doStatus(wcRoot, true, false, true, false, new org.tmatesoft.svn.core.wc.ISVNStatusHandler() {
+ @Override
+ public void handleStatus(SVNStatus status) {
+ SvnFileStatus.FileStatus fs = mapStatusType(status.getContentsStatus());
+ File file = status.getFile();
+ if (file == null) return;
+ String absPath = file.getAbsolutePath();
+ String relativePath = absPath.startsWith(wcRootPath)
+ ? absPath.substring(wcRootPath.length()).replace(File.separatorChar, '/').replaceAll("^/", "")
+ : file.getName();
+ if (relativePath.isEmpty()) return;
+ SvnFileStatus f = new SvnFileStatus(relativePath, fs);
+ f.setWorkingCopyStatus(String.valueOf(fs.getCode()));
+ files.add(f);
+ }
+ });
+ } catch (SVNException e) {
+ logger.warn("获取状态失败: {}", e.getMessage());
+ throw new RuntimeException(e);
+ } finally {
+ clientManager.dispose();
+ }
+
+ SvnStatus status = new SvnStatus();
+ status.setWorkingCopyPath(workingDirectory);
+ status.setFiles(files);
+ return status;
+ }
+
+ public SvnStatus getStatus(String workingDirectory) {
+ return getStatus(workingDirectory, null, null);
+ }
+
+ private static SvnFileStatus.FileStatus mapStatusType(SVNStatusType t) {
+ if (t == null) return SvnFileStatus.FileStatus.NORMAL;
+ if (t == SVNStatusType.STATUS_MODIFIED) return SvnFileStatus.FileStatus.MODIFIED;
+ if (t == SVNStatusType.STATUS_ADDED) return SvnFileStatus.FileStatus.ADDED;
+ if (t == SVNStatusType.STATUS_DELETED) return SvnFileStatus.FileStatus.DELETED;
+ if (t == SVNStatusType.STATUS_CONFLICTED) return SvnFileStatus.FileStatus.CONFLICTED;
+ if (t == SVNStatusType.STATUS_UNVERSIONED) return SvnFileStatus.FileStatus.UNVERSIONED;
+ if (t == SVNStatusType.STATUS_MISSING) return SvnFileStatus.FileStatus.MISSING;
+ if (t == SVNStatusType.STATUS_IGNORED) return SvnFileStatus.FileStatus.IGNORED;
+ if (t == SVNStatusType.STATUS_EXTERNAL) return SvnFileStatus.FileStatus.EXTERNAL;
+ return SvnFileStatus.FileStatus.NORMAL;
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/service/SvnService.java b/backend/src/main/java/com/svnmanager/service/SvnService.java
new file mode 100644
index 0000000..1fb7b39
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/service/SvnService.java
@@ -0,0 +1,53 @@
+package com.svnmanager.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.wc.SVNClientManager;
+import org.tmatesoft.svn.core.wc.SVNWCUtil;
+
+import java.io.File;
+
+/**
+ * SVN服务基类(基于 SVNKit)
+ */
+public abstract class SvnService {
+ protected static final Logger logger = LoggerFactory.getLogger(SvnService.class);
+
+ /**
+ * 创建带可选认证的 SVNClientManager。
+ *
+ * @param username 用户名(可为空)
+ * @param password 密码(可为空)
+ * @return SVNClientManager 实例,调用方使用后需 dispose
+ */
+ protected SVNClientManager createClientManager(String username, String password) {
+ if (username != null && !username.isEmpty()) {
+ return SVNClientManager.newInstance(SVNWCUtil.createDefaultOptions(true), username, password != null ? password : "");
+ }
+ return SVNClientManager.newInstance(SVNWCUtil.createDefaultOptions(true));
+ }
+
+ /**
+ * 验证工作目录是否为有效的 SVN 工作副本(基于 SVNKit)。
+ */
+ protected boolean isValidWorkingCopy(String workingDirectory) {
+ if (workingDirectory == null || workingDirectory.isEmpty()) {
+ return false;
+ }
+ File dir = new File(workingDirectory);
+ if (!dir.isDirectory()) {
+ return false;
+ }
+ SVNClientManager clientManager = createClientManager(null, null);
+ try {
+ clientManager.getStatusClient().doStatus(dir, false);
+ return true;
+ } catch (SVNException e) {
+ logger.debug("验证工作副本失败: {}", e.getMessage());
+ return false;
+ } finally {
+ clientManager.dispose();
+ }
+ }
+}
diff --git a/backend/src/main/java/com/svnmanager/service/UpdateService.java b/backend/src/main/java/com/svnmanager/service/UpdateService.java
new file mode 100644
index 0000000..2adba2b
--- /dev/null
+++ b/backend/src/main/java/com/svnmanager/service/UpdateService.java
@@ -0,0 +1,99 @@
+package com.svnmanager.service;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import org.tmatesoft.svn.core.SVNException;
+import org.tmatesoft.svn.core.wc.SVNClientManager;
+import org.tmatesoft.svn.core.SVNDepth;
+import org.tmatesoft.svn.core.wc.SVNRevision;
+
+import java.io.File;
+
+/**
+ * Update 服务(基于 SVNKit)
+ */
+@Service
+public class UpdateService extends SvnService {
+ private static final Logger logger = LoggerFactory.getLogger(UpdateService.class);
+
+ public UpdateResult update(String workingDirectory, String revision, String username, String password) {
+ logger.info("更新工作副本: {}", workingDirectory);
+
+ if (!isValidWorkingCopy(workingDirectory)) {
+ throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
+ }
+
+ File wcRoot = new File(workingDirectory);
+ SVNRevision targetRevision = (revision != null && !revision.isEmpty())
+ ? SVNRevision.create(Long.parseLong(revision.replaceAll("\\D", "")))
+ : SVNRevision.HEAD;
+
+ UpdateResult updateResult = new UpdateResult();
+ SVNClientManager clientManager = createClientManager(username, password);
+ try {
+ long rev = clientManager.getUpdateClient().doUpdate(wcRoot, targetRevision, SVNDepth.INFINITY, true, true);
+ updateResult.setSuccess(true);
+ updateResult.setRevision(String.valueOf(rev));
+ logger.info("更新成功,版本: {}", rev);
+ } catch (SVNException e) {
+ logger.error("更新失败: {}", e.getMessage());
+ updateResult.setSuccess(false);
+ updateResult.setError(e.getMessage());
+ } finally {
+ clientManager.dispose();
+ }
+
+ return updateResult;
+ }
+
+ public UpdateResult update(String workingDirectory, String username, String password) {
+ return update(workingDirectory, null, username, password);
+ }
+
+ public UpdateResult update(String workingDirectory) {
+ return update(workingDirectory, null, null, null);
+ }
+
+ /**
+ * 更新结果
+ */
+ public static class UpdateResult {
+ private boolean success;
+ private String revision;
+ private String output;
+ private String error;
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ public String getRevision() {
+ return revision;
+ }
+
+ public void setRevision(String revision) {
+ this.revision = revision;
+ }
+
+ public String getOutput() {
+ return output;
+ }
+
+ public void setOutput(String output) {
+ this.output = output;
+ }
+
+ public String getError() {
+ return error;
+ }
+
+ public void setError(String error) {
+ this.error = error;
+ }
+ }
+}
diff --git a/src/main/java/com/svnmanager/util/ConfigUtil.java b/backend/src/main/java/com/svnmanager/util/ConfigUtil.java
similarity index 94%
rename from src/main/java/com/svnmanager/util/ConfigUtil.java
rename to backend/src/main/java/com/svnmanager/util/ConfigUtil.java
index 63fe2a5..21bbff0 100644
--- a/src/main/java/com/svnmanager/util/ConfigUtil.java
+++ b/backend/src/main/java/com/svnmanager/util/ConfigUtil.java
@@ -104,6 +104,11 @@ public class ConfigUtil {
List projects = loadProjects();
for (int i = 0; i < projects.size(); i++) {
if (projects.get(i).getId().equals(project.getId())) {
+ Project existing = projects.get(i);
+ // 密码留空时保留原密码
+ if (project.getPassword() == null || project.getPassword().isEmpty()) {
+ project.setPassword(existing.getPassword());
+ }
projects.set(i, project);
return saveProjects(projects);
}
diff --git a/src/main/java/com/svnmanager/util/LogUtil.java b/backend/src/main/java/com/svnmanager/util/LogUtil.java
similarity index 100%
rename from src/main/java/com/svnmanager/util/LogUtil.java
rename to backend/src/main/java/com/svnmanager/util/LogUtil.java
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
new file mode 100644
index 0000000..3fc30a9
--- /dev/null
+++ b/backend/src/main/resources/application.properties
@@ -0,0 +1,2 @@
+server.port=8080
+spring.application.name=svn-manager-backend
diff --git a/src/main/resources/logback.xml b/backend/src/main/resources/logback.xml
similarity index 98%
rename from src/main/resources/logback.xml
rename to backend/src/main/resources/logback.xml
index 5486341..bff127d 100644
--- a/src/main/resources/logback.xml
+++ b/backend/src/main/resources/logback.xml
@@ -5,7 +5,6 @@
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
-
${user.home}/.svn-manager/svn-manager.log
@@ -16,11 +15,9 @@
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
-
-
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..1511959
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,5 @@
+# Vue 3 + Vite
+
+This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `
+