chore: initial project setup

This commit is contained in:
liu
2026-02-03 23:24:32 +08:00
commit 28b517da40
32 changed files with 3776 additions and 0 deletions

46
.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# IDE
.idea/
*.iml
*.iws
*.ipr
.vscode/
.settings/
.classpath
.project
.factorypath
# OS
.DS_Store
Thumbs.db
*.swp
*.swo
*~
# Logs
*.log
logs/
# Java
*.class
*.jar
*.war
*.ear
*.nar
hs_err_pid*
# Temporary files
*.tmp
*.bak
*.cache

129
01-技术选型.md Normal file
View File

@@ -0,0 +1,129 @@
# 01-技术选型
## 推荐技术栈
### 1. UI框架
**选择JavaFX**
**理由:**
- 现代化UI组件支持CSS样式定制
- 响应式布局适合构建复杂的SVN管理界面
- 官方Scene Builder可视化设计工具开发效率高
- 跨平台性好
**对比Swing**
- Swing虽然成熟但UI组件较为陈旧
- JavaFX提供更丰富的动画和视觉效果
- JavaFX更适合现代桌面应用开发
### 2. 构建工具
**选择Maven**
**理由:**
- 生态成熟,依赖管理简单
- 项目结构标准化
- 插件丰富
- 社区支持好
### 3. 进程执行
**选择ProcessBuilder**
**理由:**
- Java原生无需额外依赖
- 足够应对SVN命令调用需求
- 支持环境变量配置和工作目录设置
**可选增强:**
- Apache Commons Exec提供更高级的进程管理功能
### 4. Java版本
**选择Java 11+**
**理由:**
- LTS长期支持版本稳定可靠
- JavaFX支持良好
- 性能优化完善
### 5. 辅助库(可选)
- **Jackson/Gson**: JSON解析解析SVN的XML/JSON输出格式
- **Logback/SLF4J**: 日志管理
- **JUnit 5**: 单元测试
## 架构设计
### MVC模式
```
├── Controller/ # 处理UI交互
│ ├── MainController.java
│ ├── CheckoutController.java
│ ├── UpdateController.java
│ └── CommitController.java
├── Service/ # 封装SVN命令
│ ├── SvnService.java
│ ├── CheckoutService.java
│ ├── UpdateService.java
│ ├── CommitService.java
│ └── StatusService.java
├── Model/ # SVN输出解析器
│ ├── SvnStatus.java
│ ├── SvnLog.java
│ └── SvnInfo.java
└── Utils/ # 工具类
├── ProcessUtil.java
├── LogUtil.java
└── ConfigUtil.java
```
## 项目结构
```
svn-manager/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/svnmanager/
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ ├── model/
│ │ │ ├── util/
│ │ │ └── MainApp.java
│ │ └── resources/
│ │ ├── fxml/
│ │ ├── css/
│ │ └── application.properties
│ └── test/
│ └── java/
├── pom.xml
└── docs/
└── 01-技术选型.md
```
## 核心功能模块
### 1. 仓库管理
- Checkout检出仓库
- Update更新仓库
- Commit提交修改
### 2. 文件操作
- Add添加文件
- Delete删除文件
- Revert回退文件
### 3. 版本查看
- Status查看状态
- Log查看日志
- Diff查看差异
- Info查看信息
### 4. 分支管理
- Create Branch创建分支
- Switch切换分支
- Merge合并分支
## 技术优势总结
1. **轻量级**: 基于原生SVN命令无需复杂的SVN客户端库
2. **跨平台**: JavaFX + Java 11实现一次编写多处运行
3. **易维护**: 标准的Maven项目结构清晰的代码组织
4. **可扩展**: MVC架构便于功能扩展和维护
5. **用户友好**: JavaFX提供现代化的用户体验

94
README.md Normal file
View File

@@ -0,0 +1,94 @@
# SVN Manager
SVN管理工具 - 多项目管理界面
## 项目简介
基于 JavaFX 开发的 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 合并分支
## 项目结构
```
svn-manager/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/svnmanager/
│ │ │ ├── controller/ # UI控制器
│ │ │ ├── service/ # SVN服务封装
│ │ │ ├── model/ # 数据模型
│ │ │ ├── util/ # 工具类
│ │ │ └── MainApp.java
│ │ └── resources/
│ │ ├── fxml/ # FXML界面文件
│ │ ├── css/ # 样式文件
│ │ └── application.properties
│ └── test/
├── pom.xml
└── README.md
```
## 构建与运行
### 前置要求
- JDK 11 或更高版本
- Maven 3.6+
- SVN 客户端已安装并配置在系统 PATH 中
### 编译项目
```bash
mvn clean compile
```
### 运行项目
```bash
mvn javafx:run
```
### 打包项目
```bash
mvn clean package
```
## 开发说明
项目采用 MVC 架构模式:
- **Controller**: 处理 UI 交互逻辑
- **Service**: 封装 SVN 命令调用
- **Model**: 解析 SVN 输出数据
- **Util**: 提供通用工具方法
## 许可证
MIT License

29
init-git.bat Normal file
View File

@@ -0,0 +1,29 @@
@echo off
chcp 65001 >nul
echo 开始初始化 Git 仓库...
if exist .git (
echo Git 仓库已存在,跳过初始化
) else (
git init
echo Git 仓库初始化完成
)
echo 添加文件到暂存区...
git add .
echo 提交更改...
git commit -m "chore: initial project setup"
echo 添加远程仓库...
git remote remove origin 2>nul
git remote add origin git@gitee.com:liujingaiyuanjiao/svn-manager.git
echo 推送到 Gitee...
git branch -M master 2>nul
git push -u origin master
echo.
echo 完成!
echo 仓库地址: https://gitee.com/liujingaiyuanjiao/svn-manager
pause

52
init-git.ps1 Normal file
View File

@@ -0,0 +1,52 @@
# Git 初始化并推送到 Gitee 脚本
# 使用方法:在 PowerShell 中执行 .\init-git.ps1
Write-Host "开始初始化 Git 仓库..." -ForegroundColor Green
# 检查是否已初始化
if (Test-Path .git) {
Write-Host "Git 仓库已存在,跳过初始化" -ForegroundColor Yellow
} else {
git init
Write-Host "Git 仓库初始化完成" -ForegroundColor Green
}
# 添加所有文件
Write-Host "添加文件到暂存区..." -ForegroundColor Green
git add .
# 检查是否有未提交的更改
$status = git status --porcelain
if ($status) {
# 提交更改
Write-Host "提交更改..." -ForegroundColor Green
git commit -m "chore: initial project setup"
Write-Host "提交完成" -ForegroundColor Green
} else {
Write-Host "没有需要提交的更改" -ForegroundColor Yellow
}
# 检查远程仓库是否已配置
$remote = git remote get-url origin 2>$null
if ($LASTEXITCODE -ne 0) {
# 添加远程仓库
Write-Host "添加远程仓库..." -ForegroundColor Green
git remote add origin git@gitee.com:liujingaiyuanjiao/svn-manager.git
Write-Host "远程仓库已添加" -ForegroundColor Green
} else {
Write-Host "远程仓库已配置: $remote" -ForegroundColor Yellow
}
# 获取当前分支名
$branch = git branch --show-current
if (-not $branch) {
$branch = "master"
git branch -M master
}
Write-Host "推送到 Gitee..." -ForegroundColor Green
Write-Host "分支: $branch" -ForegroundColor Cyan
git push -u origin $branch
Write-Host "完成!" -ForegroundColor Green
Write-Host "仓库地址: https://gitee.com/liujingaiyuanjiao/svn-manager" -ForegroundColor Cyan

116
pom.xml Normal file
View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.svnmanager</groupId>
<artifactId>svn-manager</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>SVN Manager</name>
<description>SVN管理工具 - 多项目管理界面</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<javafx.version>17.0.2</javafx.version>
<jackson.version>2.15.2</jackson.version>
<logback.version>1.4.8</logback.version>
<slf4j.version>2.0.7</slf4j.version>
<junit.version>5.10.0</junit.version>
</properties>
<dependencies>
<!-- JavaFX -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- Jackson for JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- JavaFX Maven Plugin -->
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>com.svnmanager.MainApp</mainClass>
</configuration>
</plugin>
<!-- Maven Surefire Plugin for tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,48 @@
package com.svnmanager;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
* SVN管理器主应用
*/
public class MainApp extends Application {
private static final Logger logger = LoggerFactory.getLogger(MainApp.class);
@Override
public void start(Stage primaryStage) {
try {
logger.info("启动SVN管理器应用");
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"));
Scene scene = new Scene(loader.load(), 1200, 800);
primaryStage.setTitle("SVN管理器 - 多项目管理");
primaryStage.setScene(scene);
primaryStage.setMinWidth(1000);
primaryStage.setMinHeight(600);
// 设置窗口关闭事件
primaryStage.setOnCloseRequest(e -> {
logger.info("应用关闭");
System.exit(0);
});
primaryStage.show();
logger.info("应用启动成功");
} catch (IOException e) {
logger.error("启动应用失败", e);
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}

View File

@@ -0,0 +1,559 @@
package com.svnmanager.controller;
import com.svnmanager.model.Project;
import com.svnmanager.model.SvnFileStatus;
import com.svnmanager.model.SvnStatus;
import com.svnmanager.service.*;
import com.svnmanager.util.ConfigUtil;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* 主窗口控制器
*/
public class MainController {
private static final Logger logger = LoggerFactory.getLogger(MainController.class);
@FXML private VBox sidebar;
@FXML private VBox projectList;
@FXML private Label projectCountLabel;
@FXML private Button addProjectButton;
@FXML private Label projectTitleLabel;
@FXML private Label projectPathLabel;
@FXML private Button refreshButton;
@FXML private Button executeButton;
@FXML private Button checkoutButton;
@FXML private Button updateButton;
@FXML private Button commitButton;
@FXML private Button statusButton;
@FXML private Button logButton;
@FXML private Button diffButton;
@FXML private Button infoButton;
@FXML private Label currentVersionLabel;
@FXML private Label workingCopyLabel;
@FXML private Label modifiedFilesLabel;
@FXML private TableView<SvnFileStatus> fileStatusTable;
@FXML private TableColumn<SvnFileStatus, String> statusColumn;
@FXML private TableColumn<SvnFileStatus, String> pathColumn;
@FXML private TableColumn<SvnFileStatus, String> actionColumn;
private Project currentProject;
private ObservableList<SvnFileStatus> fileStatusList;
private ObservableList<Project> projects;
// 服务实例
private CheckoutService checkoutService;
private UpdateService updateService;
private CommitService commitService;
private StatusService statusService;
private LogService logService;
private DiffService diffService;
private InfoService infoService;
@FXML
public void initialize() {
logger.info("初始化主控制器");
// 初始化服务
checkoutService = new CheckoutService();
updateService = new UpdateService();
commitService = new CommitService();
statusService = new StatusService();
logService = new LogService();
diffService = new DiffService();
infoService = new InfoService();
// 初始化文件状态列表
fileStatusList = FXCollections.observableArrayList();
fileStatusTable.setItems(fileStatusList);
// 配置表格列
statusColumn.setCellValueFactory(data -> {
SvnFileStatus.FileStatus status = data.getValue().getStatus();
return new javafx.beans.property.SimpleStringProperty(status.getDisplayName());
});
pathColumn.setCellValueFactory(data ->
new javafx.beans.property.SimpleStringProperty(data.getValue().getPath()));
actionColumn.setCellFactory(param -> new TableCell<SvnFileStatus, String>() {
private final Button viewButton = new Button("查看");
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
viewButton.setOnAction(e -> handleViewFile(getTableView().getItems().get(getIndex())));
setGraphic(viewButton);
}
}
});
// 加载项目列表
loadProjects();
}
/**
* 加载项目列表
*/
private void loadProjects() {
projects = FXCollections.observableArrayList(ConfigUtil.loadProjects());
projectList.getChildren().clear();
for (Project project : projects) {
VBox projectCard = createProjectCard(project);
projectList.getChildren().add(projectCard);
}
updateProjectCount();
}
/**
* 创建项目卡片
*/
private VBox createProjectCard(Project project) {
VBox card = new VBox(8);
card.getStyleClass().add("project-card");
card.setPrefWidth(280);
card.setPadding(new javafx.geometry.Insets(16));
HBox content = new HBox(12);
// 项目图标
Circle icon = new Circle(24);
icon.setFill(Color.web("#6366f1"));
VBox info = new VBox(4);
Label nameLabel = new Label(project.getName());
nameLabel.getStyleClass().add("project-name");
Label pathLabel = new Label(project.getPath());
pathLabel.getStyleClass().add("project-path");
pathLabel.setWrapText(true);
// 状态徽章
HBox statusBox = new HBox(4);
Circle statusDot = new Circle(4);
statusDot.getStyleClass().add("status-dot");
Label statusLabel = new Label(project.getStatus().getDisplayName());
statusBox.getChildren().addAll(statusDot, statusLabel);
statusBox.getStyleClass().add("status-badge");
info.getChildren().addAll(nameLabel, pathLabel, statusBox);
content.getChildren().addAll(icon, info);
card.getChildren().add(content);
// 点击事件
card.setOnMouseClicked(e -> selectProject(project));
return card;
}
/**
* 选择项目
*/
private void selectProject(Project project) {
currentProject = project;
projectTitleLabel.setText(project.getName());
projectPathLabel.setText(project.getPath());
// 更新项目卡片样式
for (int i = 0; i < projectList.getChildren().size(); i++) {
VBox card = (VBox) projectList.getChildren().get(i);
if (i == projects.indexOf(project)) {
card.getStyleClass().add("active");
} else {
card.getStyleClass().remove("active");
}
}
// 刷新项目状态
refreshProjectStatus();
}
/**
* 刷新项目状态
*/
private void refreshProjectStatus() {
if (currentProject == null) {
return;
}
Task<Void> task = new Task<Void>() {
@Override
protected Void call() throws Exception {
try {
// 获取SVN信息
com.svnmanager.model.SvnInfo info = infoService.getInfo(currentProject.getPath());
if (info != null && info.getRevision() != null) {
Platform.runLater(() -> {
currentVersionLabel.setText("r" + info.getRevision());
workingCopyLabel.setText("r" + info.getRevision());
});
}
// 获取状态
SvnStatus status = statusService.getStatus(currentProject.getPath());
Platform.runLater(() -> {
fileStatusList.clear();
fileStatusList.addAll(status.getFiles());
modifiedFilesLabel.setText(String.valueOf(status.getTotalChangedFiles()));
});
} catch (IllegalArgumentException e) {
// 无效的工作副本
logger.warn("无效的工作副本: {}", e.getMessage());
Platform.runLater(() -> {
currentVersionLabel.setText("r0");
workingCopyLabel.setText("r0");
modifiedFilesLabel.setText("0");
fileStatusList.clear();
showWarning("警告", "无效的SVN工作副本: " + e.getMessage());
});
} catch (Exception e) {
logger.error("刷新项目状态失败", e);
Platform.runLater(() -> {
showError("刷新失败", e.getMessage());
});
}
return null;
}
};
new Thread(task).start();
}
/**
* 更新项目计数
*/
private void updateProjectCount() {
projectCountLabel.setText("" + projects.size() + " 个项目");
}
@FXML
private void handleAddProject() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/project-dialog.fxml"));
DialogPane dialogPane = loader.load();
ProjectDialogController controller = loader.getController();
Dialog<ButtonType> dialog = new Dialog<>();
dialog.setDialogPane(dialogPane);
dialog.setTitle("添加项目");
dialog.showAndWait().ifPresent(result -> {
if (result == ButtonType.OK) {
Project project = controller.getProject();
if (project != null) {
ConfigUtil.addProject(project);
loadProjects();
}
}
});
} catch (IOException e) {
logger.error("打开添加项目对话框失败", e);
showError("错误", "无法打开添加项目对话框");
}
}
@FXML
private void handleRefresh() {
refreshProjectStatus();
}
@FXML
private void handleExecute() {
// 执行操作按钮的功能可以根据需要实现
showInfo("提示", "请选择具体的操作");
}
@FXML
private void handleCheckout() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
// 实现Checkout功能
showInfo("提示", "Checkout功能待实现");
}
@FXML
private void handleUpdate() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
Task<UpdateService.UpdateResult> task = new Task<UpdateService.UpdateResult>() {
@Override
protected UpdateService.UpdateResult call() throws Exception {
return updateService.update(currentProject.getPath());
}
};
task.setOnSucceeded(e -> {
UpdateService.UpdateResult result = task.getValue();
if (result.isSuccess()) {
showInfo("成功", "更新成功,版本: " + result.getRevision());
refreshProjectStatus();
} else {
showError("失败", result.getError());
}
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
}
@FXML
private void handleCommit() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
TextInputDialog dialog = new TextInputDialog();
dialog.setTitle("提交修改");
dialog.setHeaderText("请输入提交消息");
dialog.setContentText("消息:");
dialog.showAndWait().ifPresent(message -> {
if (message.trim().isEmpty()) {
showWarning("警告", "提交消息不能为空");
return;
}
Task<CommitService.CommitResult> task = new Task<CommitService.CommitResult>() {
@Override
protected CommitService.CommitResult call() throws Exception {
return commitService.commit(currentProject.getPath(), message);
}
};
task.setOnSucceeded(e -> {
CommitService.CommitResult result = task.getValue();
if (result.isSuccess()) {
showInfo("成功", "提交成功,版本: " + result.getRevision());
refreshProjectStatus();
} else {
showError("失败", result.getError());
}
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
});
}
@FXML
private void handleStatus() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
Task<SvnStatus> task = new Task<SvnStatus>() {
@Override
protected SvnStatus call() throws Exception {
return statusService.getStatus(currentProject.getPath());
}
};
task.setOnSucceeded(e -> {
SvnStatus status = task.getValue();
fileStatusList.clear();
fileStatusList.addAll(status.getFiles());
modifiedFilesLabel.setText(String.valueOf(status.getTotalChangedFiles()));
showInfo("状态", "" + status.getTotalChangedFiles() + " 个文件有变更");
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
}
@FXML
private void handleLog() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
Task<List<com.svnmanager.model.SvnLog>> task = new Task<List<com.svnmanager.model.SvnLog>>() {
@Override
protected List<com.svnmanager.model.SvnLog> call() throws Exception {
return logService.getLog(currentProject.getPath(), 20);
}
};
task.setOnSucceeded(e -> {
List<com.svnmanager.model.SvnLog> logs = task.getValue();
showLogDialog(logs);
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
}
@FXML
private void handleDiff() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
Task<String> task = new Task<String>() {
@Override
protected String call() throws Exception {
return diffService.getDiff(currentProject.getPath());
}
};
task.setOnSucceeded(e -> {
String diff = task.getValue();
showDiffDialog(diff);
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
}
@FXML
private void handleInfo() {
if (currentProject == null) {
showWarning("警告", "请先选择项目");
return;
}
Task<com.svnmanager.model.SvnInfo> task = new Task<com.svnmanager.model.SvnInfo>() {
@Override
protected com.svnmanager.model.SvnInfo call() throws Exception {
return infoService.getInfo(currentProject.getPath());
}
};
task.setOnSucceeded(e -> {
com.svnmanager.model.SvnInfo info = task.getValue();
showInfoDialog(info);
});
task.setOnFailed(e -> {
showError("错误", task.getException().getMessage());
});
new Thread(task).start();
}
private void handleViewFile(SvnFileStatus fileStatus) {
// 实现查看文件功能
showInfo("文件", fileStatus.getPath());
}
private void showLogDialog(List<com.svnmanager.model.SvnLog> logs) {
Dialog<Void> dialog = new Dialog<>();
dialog.setTitle("SVN日志");
TextArea textArea = new TextArea();
StringBuilder sb = new StringBuilder();
for (com.svnmanager.model.SvnLog log : logs) {
sb.append("r").append(log.getRevision()).append(" | ")
.append(log.getAuthor()).append(" | ")
.append(log.getDate()).append("\n")
.append(log.getMessage()).append("\n\n");
}
textArea.setText(sb.toString());
textArea.setEditable(false);
dialog.getDialogPane().setContent(textArea);
dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
dialog.showAndWait();
}
private void showDiffDialog(String diff) {
Dialog<Void> dialog = new Dialog<>();
dialog.setTitle("差异对比");
TextArea textArea = new TextArea(diff);
textArea.setEditable(false);
textArea.setPrefSize(800, 600);
dialog.getDialogPane().setContent(textArea);
dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
dialog.showAndWait();
}
private void showInfoDialog(com.svnmanager.model.SvnInfo info) {
Dialog<Void> dialog = new Dialog<>();
dialog.setTitle("SVN信息");
TextArea textArea = new TextArea();
StringBuilder sb = new StringBuilder();
sb.append("路径: ").append(info.getPath()).append("\n");
sb.append("URL: ").append(info.getUrl()).append("\n");
sb.append("版本: ").append(info.getRevision()).append("\n");
sb.append("仓库根: ").append(info.getRepositoryRoot()).append("\n");
sb.append("最后修改作者: ").append(info.getLastChangedAuthor()).append("\n");
sb.append("最后修改版本: ").append(info.getLastChangedRev()).append("\n");
sb.append("最后修改日期: ").append(info.getLastChangedDate()).append("\n");
textArea.setText(sb.toString());
textArea.setEditable(false);
dialog.getDialogPane().setContent(textArea);
dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
dialog.showAndWait();
}
private void showError(String title, String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setContentText(message);
alert.showAndWait();
}
private void showWarning(String title, String message) {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle(title);
alert.setContentText(message);
alert.showAndWait();
}
private void showInfo(String title, String message) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setContentText(message);
alert.showAndWait();
}
}

View File

@@ -0,0 +1,79 @@
package com.svnmanager.controller;
import com.svnmanager.model.Project;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import java.io.File;
/**
* 项目对话框控制器
*/
public class ProjectDialogController {
@FXML private TextField nameField;
@FXML private TextField pathField;
@FXML private TextField svnUrlField;
private Project project;
@FXML
public void initialize() {
// 初始化
}
/**
* 浏览路径
*/
@FXML
private void handleBrowsePath() {
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle("选择项目目录");
File selectedDirectory = directoryChooser.showDialog(pathField.getScene().getWindow());
if (selectedDirectory != null) {
pathField.setText(selectedDirectory.getAbsolutePath());
}
}
/**
* 获取项目
*
* @return 项目对象
*/
public Project getProject() {
String name = nameField.getText().trim();
String path = pathField.getText().trim();
String svnUrl = svnUrlField.getText().trim();
if (name.isEmpty() || path.isEmpty()) {
return null;
}
if (project == null) {
project = new Project();
}
project.setName(name);
project.setPath(path);
project.setSvnUrl(svnUrl);
return project;
}
/**
* 设置项目(用于编辑)
*
* @param project 项目对象
*/
public void setProject(Project project) {
this.project = project;
if (project != null) {
nameField.setText(project.getName());
pathField.setText(project.getPath());
svnUrlField.setText(project.getSvnUrl());
}
}
}

View File

@@ -0,0 +1,117 @@
package com.svnmanager.model;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* 项目数据模型
*/
public class Project {
@JsonProperty("id")
private String id;
@JsonProperty("name")
private String name;
@JsonProperty("path")
private String path;
@JsonProperty("svnUrl")
private String svnUrl;
@JsonProperty("currentVersion")
private String currentVersion;
@JsonProperty("workingCopyVersion")
private String workingCopyVersion;
@JsonProperty("status")
private ProjectStatus status;
public Project() {
this.status = ProjectStatus.UNKNOWN;
}
public Project(String id, String name, String path, String svnUrl) {
this.id = id;
this.name = name;
this.path = path;
this.svnUrl = svnUrl;
this.status = ProjectStatus.UNKNOWN;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
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 String getSvnUrl() {
return svnUrl;
}
public void setSvnUrl(String svnUrl) {
this.svnUrl = svnUrl;
}
public String getCurrentVersion() {
return currentVersion;
}
public void setCurrentVersion(String currentVersion) {
this.currentVersion = currentVersion;
}
public String getWorkingCopyVersion() {
return workingCopyVersion;
}
public void setWorkingCopyVersion(String workingCopyVersion) {
this.workingCopyVersion = workingCopyVersion;
}
public ProjectStatus getStatus() {
return status;
}
public void setStatus(ProjectStatus status) {
this.status = status;
}
/**
* 项目状态枚举
*/
public enum ProjectStatus {
SYNCED("已同步"),
UPDATES_AVAILABLE("有更新"),
DISCONNECTED("未连接"),
UNKNOWN("未知");
private final String displayName;
ProjectStatus(String displayName) {
this.displayName = displayName;
}
public String getDisplayName() {
return displayName;
}
}
}

View File

@@ -0,0 +1,91 @@
package com.svnmanager.model;
/**
* SVN文件状态模型
*/
public class SvnFileStatus {
private String path;
private FileStatus status;
private String workingCopyStatus;
private String repositoryStatus;
public SvnFileStatus() {
}
public SvnFileStatus(String path, FileStatus status) {
this.path = path;
this.status = status;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public FileStatus getStatus() {
return status;
}
public void setStatus(FileStatus status) {
this.status = status;
}
public String getWorkingCopyStatus() {
return workingCopyStatus;
}
public void setWorkingCopyStatus(String workingCopyStatus) {
this.workingCopyStatus = workingCopyStatus;
}
public String getRepositoryStatus() {
return repositoryStatus;
}
public void setRepositoryStatus(String repositoryStatus) {
this.repositoryStatus = repositoryStatus;
}
/**
* 文件状态枚举
*/
public enum FileStatus {
MODIFIED('M', "已修改"),
ADDED('A', "已添加"),
DELETED('D', "已删除"),
CONFLICTED('C', "冲突"),
UNVERSIONED('?', "未版本控制"),
MISSING('!', "缺失"),
EXTERNAL('X', "外部"),
IGNORED('I', "已忽略"),
NORMAL(' ', "正常");
private final char code;
private final String displayName;
FileStatus(char code, String displayName) {
this.code = code;
this.displayName = displayName;
}
public char getCode() {
return code;
}
public String getDisplayName() {
return displayName;
}
public static FileStatus fromCode(char code) {
for (FileStatus status : values()) {
if (status.code == code) {
return status;
}
}
return NORMAL;
}
}
}

View File

@@ -0,0 +1,100 @@
package com.svnmanager.model;
/**
* SVN信息模型
*/
public class SvnInfo {
private String path;
private String url;
private String repositoryRoot;
private String repositoryUuid;
private String revision;
private String nodeKind;
private String schedule;
private String lastChangedAuthor;
private String lastChangedRev;
private String lastChangedDate;
public SvnInfo() {
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getRepositoryRoot() {
return repositoryRoot;
}
public void setRepositoryRoot(String repositoryRoot) {
this.repositoryRoot = repositoryRoot;
}
public String getRepositoryUuid() {
return repositoryUuid;
}
public void setRepositoryUuid(String repositoryUuid) {
this.repositoryUuid = repositoryUuid;
}
public String getRevision() {
return revision;
}
public void setRevision(String revision) {
this.revision = revision;
}
public String getNodeKind() {
return nodeKind;
}
public void setNodeKind(String nodeKind) {
this.nodeKind = nodeKind;
}
public String getSchedule() {
return schedule;
}
public void setSchedule(String schedule) {
this.schedule = schedule;
}
public String getLastChangedAuthor() {
return lastChangedAuthor;
}
public void setLastChangedAuthor(String lastChangedAuthor) {
this.lastChangedAuthor = lastChangedAuthor;
}
public String getLastChangedRev() {
return lastChangedRev;
}
public void setLastChangedRev(String lastChangedRev) {
this.lastChangedRev = lastChangedRev;
}
public String getLastChangedDate() {
return lastChangedDate;
}
public void setLastChangedDate(String lastChangedDate) {
this.lastChangedDate = lastChangedDate;
}
}

View File

@@ -0,0 +1,72 @@
package com.svnmanager.model;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* SVN日志模型
*/
public class SvnLog {
private String revision;
private String author;
private LocalDateTime date;
private String message;
private List<String> changedPaths;
public SvnLog() {
this.changedPaths = new ArrayList<>();
}
public SvnLog(String revision, String author, LocalDateTime date, String message) {
this.revision = revision;
this.author = author;
this.date = date;
this.message = message;
this.changedPaths = new ArrayList<>();
}
public String getRevision() {
return revision;
}
public void setRevision(String revision) {
this.revision = revision;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public LocalDateTime getDate() {
return date;
}
public void setDate(LocalDateTime date) {
this.date = date;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public List<String> getChangedPaths() {
return changedPaths;
}
public void setChangedPaths(List<String> changedPaths) {
this.changedPaths = changedPaths;
}
public void addChangedPath(String path) {
this.changedPaths.add(path);
}
}

View File

@@ -0,0 +1,95 @@
package com.svnmanager.model;
import java.util.ArrayList;
import java.util.List;
/**
* SVN状态模型
*/
public class SvnStatus {
private String workingCopyPath;
private String revision;
private List<SvnFileStatus> files;
private int modifiedCount;
private int addedCount;
private int deletedCount;
private int conflictedCount;
public SvnStatus() {
this.files = new ArrayList<>();
}
public String getWorkingCopyPath() {
return workingCopyPath;
}
public void setWorkingCopyPath(String workingCopyPath) {
this.workingCopyPath = workingCopyPath;
}
public String getRevision() {
return revision;
}
public void setRevision(String revision) {
this.revision = revision;
}
public List<SvnFileStatus> getFiles() {
return files;
}
public void setFiles(List<SvnFileStatus> files) {
this.files = files;
updateCounts();
}
public void addFile(SvnFileStatus file) {
this.files.add(file);
updateCounts();
}
public int getModifiedCount() {
return modifiedCount;
}
public int getAddedCount() {
return addedCount;
}
public int getDeletedCount() {
return deletedCount;
}
public int getConflictedCount() {
return conflictedCount;
}
public int getTotalChangedFiles() {
return modifiedCount + addedCount + deletedCount + conflictedCount;
}
private void updateCounts() {
modifiedCount = 0;
addedCount = 0;
deletedCount = 0;
conflictedCount = 0;
for (SvnFileStatus file : files) {
switch (file.getStatus()) {
case MODIFIED:
modifiedCount++;
break;
case ADDED:
addedCount++;
break;
case DELETED:
deletedCount++;
break;
case CONFLICTED:
conflictedCount++;
break;
}
}
}
}

View File

@@ -0,0 +1,67 @@
package com.svnmanager.service;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Checkout服务
*/
public class CheckoutService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(CheckoutService.class);
/**
* 检出SVN仓库
*
* @param svnUrl SVN仓库URL
* @param targetPath 目标路径
* @param revision 版本号可选null表示最新版本
* @return 是否成功
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public boolean checkout(String svnUrl, String targetPath, String revision)
throws IOException, InterruptedException, TimeoutException {
logger.info("检出仓库: {} 到 {}", svnUrl, targetPath);
List<String> args = new ArrayList<>();
args.add(svnUrl);
args.add(targetPath);
if (revision != null && !revision.isEmpty()) {
args.add("-r");
args.add(revision);
}
ProcessUtil.ProcessResult result = executeSvnCommand("checkout", args, null);
if (result.isSuccess()) {
logger.info("检出成功");
return true;
} else {
logger.error("检出失败: {}", result.getErrorAsString());
return false;
}
}
/**
* 检出SVN仓库最新版本
*
* @param svnUrl SVN仓库URL
* @param targetPath 目标路径
* @return 是否成功
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public boolean checkout(String svnUrl, String targetPath)
throws IOException, InterruptedException, TimeoutException {
return checkout(svnUrl, targetPath, null);
}
}

View File

@@ -0,0 +1,134 @@
package com.svnmanager.service;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Commit服务
*/
public class CommitService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(CommitService.class);
/**
* 提交修改
*
* @param workingDirectory 工作目录
* @param message 提交消息
* @param files 要提交的文件列表null表示提交所有修改
* @return 提交结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public CommitResult commit(String workingDirectory, String message, List<String> files)
throws IOException, InterruptedException, TimeoutException {
logger.info("提交修改: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
if (message == null || message.trim().isEmpty()) {
throw new IllegalArgumentException("提交消息不能为空");
}
List<String> args = new ArrayList<>();
args.add("-m");
args.add(message);
if (files != null && !files.isEmpty()) {
args.addAll(files);
}
ProcessUtil.ProcessResult result = executeSvnCommand("commit", args, workingDirectory);
CommitResult commitResult = new CommitResult();
commitResult.setSuccess(result.isSuccess());
commitResult.setOutput(result.getOutputAsString());
commitResult.setError(result.getErrorAsString());
if (result.isSuccess()) {
// 解析提交后的版本号
String output = result.getOutputAsString();
String revisionLine = output.lines()
.filter(line -> line.contains("Committed revision"))
.findFirst()
.orElse("");
if (!revisionLine.isEmpty()) {
String[] parts = revisionLine.split(" ");
if (parts.length > 0) {
String rev = parts[parts.length - 1].replace(".", "");
commitResult.setRevision(rev);
}
}
logger.info("提交成功,版本: {}", commitResult.getRevision());
} else {
logger.error("提交失败: {}", result.getErrorAsString());
}
return commitResult;
}
/**
* 提交所有修改
*
* @param workingDirectory 工作目录
* @param message 提交消息
* @return 提交结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public CommitResult commit(String workingDirectory, String message)
throws IOException, InterruptedException, TimeoutException {
return commit(workingDirectory, message, 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;
}
}
}

View File

@@ -0,0 +1,64 @@
package com.svnmanager.service;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Diff服务
*/
public class DiffService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(DiffService.class);
/**
* 获取差异
*
* @param workingDirectory 工作目录
* @param filePath 文件路径null表示所有文件
* @return 差异内容
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public String getDiff(String workingDirectory, String filePath)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取差异: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
List<String> args = new ArrayList<>();
if (filePath != null && !filePath.isEmpty()) {
args.add(filePath);
}
ProcessUtil.ProcessResult result = executeSvnCommand("diff", args, workingDirectory);
if (result.isSuccess()) {
return result.getOutputAsString();
} else {
logger.warn("获取差异失败: {}", result.getErrorAsString());
return result.getErrorAsString();
}
}
/**
* 获取所有文件的差异
*
* @param workingDirectory 工作目录
* @return 差异内容
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public String getDiff(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return getDiff(workingDirectory, null);
}
}

View File

@@ -0,0 +1,84 @@
package com.svnmanager.service;
import com.svnmanager.model.SvnInfo;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* Info服务
*/
public class InfoService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(InfoService.class);
/**
* 获取SVN信息
*
* @param workingDirectory 工作目录
* @return SVN信息
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public SvnInfo getInfo(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取信息: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
ProcessUtil.ProcessResult result = executeSvnCommand("info", workingDirectory);
SvnInfo info = new SvnInfo();
if (result.isSuccess()) {
// 将输出行合并为一个字符串,便于解析
String output = String.join("\n", result.getOutput());
parseInfoOutput(output, info);
} else {
logger.warn("获取信息失败: {}", result.getErrorAsString());
}
return info;
}
/**
* 解析svn info输出
*
* @param output 命令输出
* @param info 信息对象
*/
private void parseInfoOutput(String output, SvnInfo info) {
for (String line : output.split("\n")) {
line = line.trim();
if (line.isEmpty()) {
continue;
}
if (line.startsWith("Path: ")) {
info.setPath(line.substring(6).trim());
} else if (line.startsWith("URL: ")) {
info.setUrl(line.substring(5).trim());
} else if (line.startsWith("Repository Root: ")) {
info.setRepositoryRoot(line.substring(17).trim());
} else if (line.startsWith("Repository UUID: ")) {
info.setRepositoryUuid(line.substring(18).trim());
} else if (line.startsWith("Revision: ")) {
info.setRevision(line.substring(11).trim());
} else if (line.startsWith("Node Kind: ")) {
info.setNodeKind(line.substring(11).trim());
} else if (line.startsWith("Schedule: ")) {
info.setSchedule(line.substring(11).trim());
} else if (line.startsWith("Last Changed Author: ")) {
info.setLastChangedAuthor(line.substring(22).trim());
} else if (line.startsWith("Last Changed Rev: ")) {
info.setLastChangedRev(line.substring(19).trim());
} else if (line.startsWith("Last Changed Date: ")) {
info.setLastChangedDate(line.substring(20).trim());
}
}
}
}

View File

@@ -0,0 +1,139 @@
package com.svnmanager.service;
import com.svnmanager.model.SvnLog;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Log服务
*/
public class LogService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(LogService.class);
private static final DateTimeFormatter SVN_DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/**
* 获取SVN日志
*
* @param workingDirectory 工作目录
* @param limit 限制条数null表示不限制
* @return 日志列表
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public List<SvnLog> getLog(String workingDirectory, Integer limit)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取日志: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
List<String> args = new ArrayList<>();
args.add("-v"); // 详细输出
if (limit != null && limit > 0) {
args.add("-l");
args.add(String.valueOf(limit));
}
ProcessUtil.ProcessResult result = executeSvnCommand("log", args, workingDirectory);
List<SvnLog> logs = new ArrayList<>();
if (result.isSuccess()) {
// 将输出行合并为一个字符串,便于解析
String output = String.join("\n", result.getOutput());
parseLogOutput(output, logs);
} else {
logger.warn("获取日志失败: {}", result.getErrorAsString());
}
return logs;
}
/**
* 获取SVN日志默认限制50条
*
* @param workingDirectory 工作目录
* @return 日志列表
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public List<SvnLog> getLog(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return getLog(workingDirectory, 50);
}
/**
* 解析svn log输出
*
* @param output 命令输出
* @param logs 日志列表
*/
private void parseLogOutput(String output, List<SvnLog> logs) {
String[] lines = output.split("\n");
SvnLog currentLog = null;
for (String line : lines) {
line = line.trim();
if (line.startsWith("------------------------------------------------------------------------")) {
if (currentLog != null) {
logs.add(currentLog);
}
currentLog = new SvnLog();
continue;
}
if (currentLog == null) {
continue;
}
if (line.startsWith("r")) {
// 解析版本号、作者、日期
// 格式: "r1234 | author | 2024-01-01 12:00:00 +0800 (Mon, 01 Jan 2024) | 1 line"
String[] parts = line.split("\\|");
if (parts.length >= 3) {
currentLog.setRevision(parts[0].trim());
currentLog.setAuthor(parts[1].trim());
// 解析日期
String dateStr = parts[2].trim();
String datePart = dateStr.split("\\(")[0].trim();
try {
LocalDateTime date = LocalDateTime.parse(datePart, SVN_DATE_FORMATTER);
currentLog.setDate(date);
} catch (Exception e) {
logger.debug("解析日期失败: {}", datePart);
}
}
} else if (line.startsWith("Changed paths:")) {
// 跳过Changed paths标题
continue;
} else if (line.startsWith(" ")) {
// 变更路径
String path = line.trim();
currentLog.addChangedPath(path);
} else if (!line.isEmpty() && currentLog.getMessage() == null) {
// 提交消息
currentLog.setMessage(line);
} else if (!line.isEmpty() && currentLog.getMessage() != null) {
// 追加提交消息(多行)
currentLog.setMessage(currentLog.getMessage() + "\n" + line);
}
}
if (currentLog != null) {
logs.add(currentLog);
}
}
}

View File

@@ -0,0 +1,102 @@
package com.svnmanager.service;
import com.svnmanager.model.SvnFileStatus;
import com.svnmanager.model.SvnStatus;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Status服务
*/
public class StatusService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(StatusService.class);
/**
* 获取工作副本状态
*
* @param workingDirectory 工作目录
* @return SVN状态
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public SvnStatus getStatus(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
logger.debug("获取状态: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
List<String> args = new ArrayList<>();
args.add("-v"); // 详细输出
ProcessUtil.ProcessResult result = executeSvnCommand("status", args, workingDirectory);
SvnStatus status = new SvnStatus();
status.setWorkingCopyPath(workingDirectory);
if (result.isSuccess()) {
// 将输出行合并为一个字符串,便于解析
String output = String.join("\n", result.getOutput());
parseStatusOutput(output, status);
} else {
logger.warn("获取状态失败: {}", result.getErrorAsString());
}
return status;
}
/**
* 解析svn status输出
*
* @param output 命令输出
* @param status 状态对象
*/
private void parseStatusOutput(String output, SvnStatus status) {
List<SvnFileStatus> files = new ArrayList<>();
for (String line : output.split("\n")) {
line = line.trim();
if (line.isEmpty()) {
continue;
}
// SVN status格式: "状态码 工作副本状态 版本号 文件路径"
// 例如: "M 1234 src/Main.java"
if (line.length() < 8) {
continue;
}
char workingCopyStatus = line.charAt(0);
char repositoryStatus = line.length() > 1 ? line.charAt(1) : ' ';
// 跳过标题行
if (workingCopyStatus == 'S' && line.contains("Status")) {
continue;
}
SvnFileStatus.FileStatus fileStatus = SvnFileStatus.FileStatus.fromCode(workingCopyStatus);
// 提取文件路径(跳过状态码和版本号)
String filePath = line.substring(8).trim();
if (filePath.isEmpty()) {
continue;
}
SvnFileStatus fileStatusObj = new SvnFileStatus(filePath, fileStatus);
fileStatusObj.setWorkingCopyStatus(String.valueOf(workingCopyStatus));
fileStatusObj.setRepositoryStatus(String.valueOf(repositoryStatus));
files.add(fileStatusObj);
}
status.setFiles(files);
}
}

View File

@@ -0,0 +1,64 @@
package com.svnmanager.service;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* SVN服务基类
*/
public abstract class SvnService {
protected static final Logger logger = LoggerFactory.getLogger(SvnService.class);
/**
* 执行SVN命令
*
* @param svnCommand SVN子命令
* @param args 命令参数
* @param workingDirectory 工作目录
* @return 命令执行结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
protected ProcessUtil.ProcessResult executeSvnCommand(String svnCommand, List<String> args, String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return ProcessUtil.executeSvnCommand(svnCommand, args, workingDirectory);
}
/**
* 执行SVN命令无参数
*
* @param svnCommand SVN子命令
* @param workingDirectory 工作目录
* @return 命令执行结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
protected ProcessUtil.ProcessResult executeSvnCommand(String svnCommand, String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return executeSvnCommand(svnCommand, new ArrayList<>(), workingDirectory);
}
/**
* 验证工作目录是否为有效的SVN工作副本
*
* @param workingDirectory 工作目录
* @return 是否为有效的SVN工作副本
*/
protected boolean isValidWorkingCopy(String workingDirectory) {
try {
ProcessUtil.ProcessResult result = executeSvnCommand("info", workingDirectory);
return result.isSuccess();
} catch (Exception e) {
logger.debug("验证工作副本失败: {}", e.getMessage());
return false;
}
}
}

View File

@@ -0,0 +1,126 @@
package com.svnmanager.service;
import com.svnmanager.util.ProcessUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
/**
* Update服务
*/
public class UpdateService extends SvnService {
private static final Logger logger = LoggerFactory.getLogger(UpdateService.class);
/**
* 更新工作副本
*
* @param workingDirectory 工作目录
* @param revision 版本号可选null表示最新版本
* @return 更新结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public UpdateResult update(String workingDirectory, String revision)
throws IOException, InterruptedException, TimeoutException {
logger.info("更新工作副本: {}", workingDirectory);
if (!isValidWorkingCopy(workingDirectory)) {
throw new IllegalArgumentException("无效的SVN工作副本: " + workingDirectory);
}
List<String> args = new ArrayList<>();
if (revision != null && !revision.isEmpty()) {
args.add("-r");
args.add(revision);
}
ProcessUtil.ProcessResult result = executeSvnCommand("update", args, workingDirectory);
UpdateResult updateResult = new UpdateResult();
updateResult.setSuccess(result.isSuccess());
updateResult.setOutput(result.getOutputAsString());
updateResult.setError(result.getErrorAsString());
if (result.isSuccess()) {
// 解析更新后的版本号
String output = result.getOutputAsString();
String revisionLine = output.lines()
.filter(line -> line.contains("Updated to revision"))
.findFirst()
.orElse("");
if (!revisionLine.isEmpty()) {
String[] parts = revisionLine.split(" ");
if (parts.length > 0) {
String rev = parts[parts.length - 1].replace(".", "");
updateResult.setRevision(rev);
}
}
logger.info("更新成功,版本: {}", updateResult.getRevision());
} else {
logger.error("更新失败: {}", result.getErrorAsString());
}
return updateResult;
}
/**
* 更新工作副本到最新版本
*
* @param workingDirectory 工作目录
* @return 更新结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public UpdateResult update(String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return update(workingDirectory, 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;
}
}
}

View File

@@ -0,0 +1,157 @@
package com.svnmanager.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.svnmanager.model.Project;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 配置管理工具类
*/
public class ConfigUtil {
private static final Logger logger = LoggerFactory.getLogger(ConfigUtil.class);
private static final String CONFIG_DIR = System.getProperty("user.home") + File.separator + ".svn-manager";
private static final String PROJECTS_FILE = CONFIG_DIR + File.separator + "projects.json";
private static final ObjectMapper objectMapper = new ObjectMapper();
static {
// 确保配置目录存在
try {
Path configPath = Paths.get(CONFIG_DIR);
if (!Files.exists(configPath)) {
Files.createDirectories(configPath);
}
} catch (IOException e) {
logger.error("创建配置目录失败", e);
}
}
/**
* 加载项目列表
*
* @return 项目列表
*/
public static List<Project> loadProjects() {
File file = new File(PROJECTS_FILE);
if (!file.exists()) {
logger.info("项目配置文件不存在,返回空列表");
return new ArrayList<>();
}
try {
CollectionType listType = objectMapper.getTypeFactory()
.constructCollectionType(List.class, Project.class);
List<Project> projects = objectMapper.readValue(file, listType);
logger.info("成功加载 {} 个项目", projects.size());
return projects;
} catch (IOException e) {
logger.error("加载项目配置失败", e);
return new ArrayList<>();
}
}
/**
* 保存项目列表
*
* @param projects 项目列表
* @return 是否保存成功
*/
public static boolean saveProjects(List<Project> projects) {
File file = new File(PROJECTS_FILE);
try {
objectMapper.writerWithDefaultPrettyPrinter().writeValue(file, projects);
logger.info("成功保存 {} 个项目", projects.size());
return true;
} catch (IOException e) {
logger.error("保存项目配置失败", e);
return false;
}
}
/**
* 添加项目
*
* @param project 项目
* @return 是否添加成功
*/
public static boolean addProject(Project project) {
if (project.getId() == null || project.getId().isEmpty()) {
project.setId(UUID.randomUUID().toString());
}
List<Project> projects = loadProjects();
projects.add(project);
return saveProjects(projects);
}
/**
* 更新项目
*
* @param project 项目
* @return 是否更新成功
*/
public static boolean updateProject(Project project) {
List<Project> projects = loadProjects();
for (int i = 0; i < projects.size(); i++) {
if (projects.get(i).getId().equals(project.getId())) {
projects.set(i, project);
return saveProjects(projects);
}
}
return false;
}
/**
* 删除项目
*
* @param projectId 项目ID
* @return 是否删除成功
*/
public static boolean deleteProject(String projectId) {
List<Project> projects = loadProjects();
projects.removeIf(p -> p.getId().equals(projectId));
return saveProjects(projects);
}
/**
* 根据ID获取项目
*
* @param projectId 项目ID
* @return 项目如果不存在返回null
*/
public static Project getProjectById(String projectId) {
List<Project> projects = loadProjects();
return projects.stream()
.filter(p -> p.getId().equals(projectId))
.findFirst()
.orElse(null);
}
/**
* 获取配置目录路径
*
* @return 配置目录路径
*/
public static String getConfigDir() {
return CONFIG_DIR;
}
/**
* 获取项目配置文件路径
*
* @return 项目配置文件路径
*/
public static String getProjectsFilePath() {
return PROJECTS_FILE;
}
}

View File

@@ -0,0 +1,51 @@
package com.svnmanager.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 日志工具类
*/
public class LogUtil {
/**
* 获取Logger实例
*
* @param clazz 类
* @return Logger实例
*/
public static Logger getLogger(Class<?> clazz) {
return LoggerFactory.getLogger(clazz);
}
/**
* 记录错误日志
*
* @param logger Logger实例
* @param message 消息
* @param throwable 异常
*/
public static void logError(Logger logger, String message, Throwable throwable) {
logger.error(message, throwable);
}
/**
* 记录信息日志
*
* @param logger Logger实例
* @param message 消息
*/
public static void logInfo(Logger logger, String message) {
logger.info(message);
}
/**
* 记录调试日志
*
* @param logger Logger实例
* @param message 消息
*/
public static void logDebug(Logger logger, String message) {
logger.debug(message);
}
}

View File

@@ -0,0 +1,159 @@
package com.svnmanager.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* 进程执行工具类
*/
public class ProcessUtil {
private static final Logger logger = LoggerFactory.getLogger(ProcessUtil.class);
private static final int DEFAULT_TIMEOUT_SECONDS = 300;
/**
* 执行命令并返回输出
*
* @param command 命令数组
* @param workingDirectory 工作目录
* @return 命令输出行列表
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static ProcessResult executeCommand(String[] command, String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
return executeCommand(command, workingDirectory, DEFAULT_TIMEOUT_SECONDS);
}
/**
* 执行命令并返回输出
*
* @param command 命令数组
* @param workingDirectory 工作目录
* @param timeoutSeconds 超时时间(秒)
* @return 命令输出结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static ProcessResult executeCommand(String[] command, String workingDirectory, int timeoutSeconds)
throws IOException, InterruptedException, TimeoutException {
logger.debug("执行命令: {}", String.join(" ", command));
logger.debug("工作目录: {}", workingDirectory);
ProcessBuilder processBuilder = new ProcessBuilder(command);
if (workingDirectory != null && !workingDirectory.isEmpty()) {
processBuilder.directory(new java.io.File(workingDirectory));
}
processBuilder.redirectErrorStream(true);
Process process = processBuilder.start();
List<String> outputLines = new ArrayList<>();
List<String> errorLines = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream(), Charset.defaultCharset()));
BufferedReader errorReader = new BufferedReader(
new InputStreamReader(process.getErrorStream(), Charset.defaultCharset()))) {
// 读取标准输出
String line;
while ((line = reader.readLine()) != null) {
outputLines.add(line);
}
// 读取错误输出
while ((line = errorReader.readLine()) != null) {
errorLines.add(line);
}
}
boolean finished = process.waitFor(timeoutSeconds, TimeUnit.SECONDS);
if (!finished) {
process.destroyForcibly();
throw new TimeoutException("命令执行超时: " + String.join(" ", command));
}
int exitCode = process.exitValue();
ProcessResult result = new ProcessResult(exitCode, outputLines, errorLines);
logger.debug("命令执行完成,退出码: {}", exitCode);
if (exitCode != 0) {
logger.warn("命令执行失败: {}", String.join("\n", errorLines));
}
return result;
}
/**
* 执行SVN命令
*
* @param svnCommand SVN子命令如 "status", "update"
* @param args 命令参数
* @param workingDirectory 工作目录
* @return 命令输出结果
* @throws IOException IO异常
* @throws InterruptedException 中断异常
* @throws TimeoutException 超时异常
*/
public static ProcessResult executeSvnCommand(String svnCommand, List<String> args, String workingDirectory)
throws IOException, InterruptedException, TimeoutException {
List<String> command = new ArrayList<>();
command.add("svn");
command.add(svnCommand);
if (args != null) {
command.addAll(args);
}
return executeCommand(command.toArray(new String[0]), workingDirectory);
}
/**
* 进程执行结果
*/
public static class ProcessResult {
private final int exitCode;
private final List<String> output;
private final List<String> error;
public ProcessResult(int exitCode, List<String> output, List<String> error) {
this.exitCode = exitCode;
this.output = output;
this.error = error;
}
public int getExitCode() {
return exitCode;
}
public List<String> getOutput() {
return output;
}
public List<String> getError() {
return error;
}
public String getOutputAsString() {
return String.join("\n", output);
}
public String getErrorAsString() {
return String.join("\n", error);
}
public boolean isSuccess() {
return exitCode == 0;
}
}
}

View File

@@ -0,0 +1,11 @@
# SVN Manager Application Configuration
app.name=SVN管理器
app.version=1.0.0
# SVN Command Configuration
svn.command=svn
svn.timeout=300
# UI Configuration
ui.theme=default
ui.language=zh_CN

View File

@@ -0,0 +1,377 @@
/* 全局样式 */
.root {
-fx-font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
-fx-font-size: 14px;
}
/* 左侧边栏 */
.sidebar {
-fx-background-color: white;
-fx-border-color: #e2e8f0;
-fx-border-width: 0 1 0 0;
}
.sidebar-header {
-fx-background-color: white;
-fx-border-color: #e2e8f0;
-fx-border-width: 0 0 1 0;
}
.logo-icon {
-fx-background-color: linear-gradient(to bottom right, #6366f1, #8b5cf6);
-fx-background-radius: 8;
}
.app-title {
-fx-font-size: 20px;
-fx-font-weight: bold;
-fx-text-fill: #111827;
}
.app-subtitle {
-fx-font-size: 12px;
-fx-text-fill: #6b7280;
}
.add-project-button {
-fx-background-color: #4f46e5;
-fx-text-fill: white;
-fx-font-weight: 500;
-fx-background-radius: 8;
-fx-padding: 10 16;
-fx-cursor: hand;
}
.add-project-button:hover {
-fx-background-color: #4338ca;
}
.project-list {
-fx-background-color: white;
}
.project-card {
-fx-background-color: #f9fafb;
-fx-background-radius: 12;
-fx-padding: 16;
-fx-cursor: hand;
}
.project-card:hover {
-fx-background-color: #f3f4f6;
-fx-translate-x: 4;
}
.project-card.active {
-fx-background-color: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.project-card.active .project-name {
-fx-text-fill: white;
}
.project-card.active .project-path {
-fx-text-fill: rgba(255, 255, 255, 0.8);
}
.project-icon {
-fx-background-color: #e0e7ff;
-fx-background-radius: 8;
-fx-pref-width: 48;
-fx-pref-height: 48;
}
.project-card.active .project-icon {
-fx-background-color: rgba(255, 255, 255, 0.2);
}
.project-name {
-fx-font-size: 14px;
-fx-font-weight: 600;
-fx-text-fill: #111827;
}
.project-path {
-fx-font-size: 12px;
-fx-text-fill: #6b7280;
}
.status-badge {
-fx-background-color: #d1fae5;
-fx-background-radius: 12;
-fx-padding: 4 8;
}
.status-badge.synced {
-fx-background-color: #d1fae5;
}
.status-badge.updates {
-fx-background-color: #fef3c7;
}
.status-badge.disconnected {
-fx-background-color: #f3f4f6;
}
.status-dot {
-fx-background-color: #10b981;
-fx-background-radius: 4;
-fx-pref-width: 8;
-fx-pref-height: 8;
}
.status-dot.updates {
-fx-background-color: #f59e0b;
}
.status-dot.disconnected {
-fx-background-color: #9ca3af;
}
.sidebar-footer {
-fx-background-color: white;
-fx-border-color: #e2e8f0;
-fx-border-width: 1 0 0 0;
}
.project-count {
-fx-font-size: 12px;
-fx-text-fill: #6b7280;
-fx-alignment: center;
}
/* 工具栏 */
.toolbar {
-fx-background-color: white;
-fx-border-color: #e2e8f0;
-fx-border-width: 0 0 1 0;
}
.project-title-label {
-fx-font-size: 24px;
-fx-font-weight: bold;
-fx-text-fill: #111827;
}
.project-path-label {
-fx-font-size: 14px;
-fx-text-fill: #6b7280;
}
.refresh-button {
-fx-background-color: #f3f4f6;
-fx-text-fill: #374151;
-fx-font-weight: 500;
-fx-background-radius: 8;
-fx-padding: 8 16;
-fx-cursor: hand;
}
.refresh-button:hover {
-fx-background-color: #e5e7eb;
}
.execute-button {
-fx-background-color: #4f46e5;
-fx-text-fill: white;
-fx-font-weight: 500;
-fx-background-radius: 8;
-fx-padding: 8 16;
-fx-cursor: hand;
}
.execute-button:hover {
-fx-background-color: #4338ca;
}
/* 操作按钮组 */
.action-buttons {
-fx-background-color: white;
-fx-border-color: #e2e8f0;
-fx-border-width: 0 0 1 0;
}
.action-btn {
-fx-font-weight: 500;
-fx-background-radius: 8;
-fx-padding: 8 16;
-fx-cursor: hand;
-fx-border-width: 1;
-fx-border-radius: 8;
}
.checkout-btn {
-fx-background-color: #dbeafe;
-fx-text-fill: #1e40af;
-fx-border-color: #bfdbfe;
}
.checkout-btn:hover {
-fx-background-color: #bfdbfe;
-fx-translate-y: -2;
}
.update-btn {
-fx-background-color: #d1fae5;
-fx-text-fill: #065f46;
-fx-border-color: #a7f3d0;
}
.update-btn:hover {
-fx-background-color: #a7f3d0;
-fx-translate-y: -2;
}
.commit-btn {
-fx-background-color: #f3e8ff;
-fx-text-fill: #6b21a8;
-fx-border-color: #e9d5ff;
}
.commit-btn:hover {
-fx-background-color: #e9d5ff;
-fx-translate-y: -2;
}
.status-btn {
-fx-background-color: #fef3c7;
-fx-text-fill: #92400e;
-fx-border-color: #fde68a;
}
.status-btn:hover {
-fx-background-color: #fde68a;
-fx-translate-y: -2;
}
.log-btn {
-fx-background-color: #e0e7ff;
-fx-text-fill: #3730a3;
-fx-border-color: #c7d2fe;
}
.log-btn:hover {
-fx-background-color: #c7d2fe;
-fx-translate-y: -2;
}
.diff-btn {
-fx-background-color: #f3f4f6;
-fx-text-fill: #374151;
-fx-border-color: #e5e7eb;
}
.diff-btn:hover {
-fx-background-color: #e5e7eb;
-fx-translate-y: -2;
}
.info-btn {
-fx-background-color: #f3f4f6;
-fx-text-fill: #374151;
-fx-border-color: #e5e7eb;
}
.info-btn:hover {
-fx-background-color: #e5e7eb;
-fx-translate-y: -2;
}
/* 统计卡片 */
.stat-card {
-fx-background-color: white;
-fx-background-radius: 12;
-fx-border-color: #e5e7eb;
-fx-border-radius: 12;
-fx-padding: 20;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.05), 4, 0, 0, 2);
}
.stat-label {
-fx-font-size: 14px;
-fx-text-fill: #6b7280;
}
.stat-value {
-fx-font-size: 24px;
-fx-font-weight: bold;
-fx-text-fill: #111827;
}
.stat-description {
-fx-font-size: 12px;
-fx-text-fill: #6b7280;
}
/* 文件状态卡片 */
.file-status-card {
-fx-background-color: white;
-fx-background-radius: 12;
-fx-border-color: #e5e7eb;
-fx-border-radius: 12;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.05), 4, 0, 0, 2);
}
.file-status-header {
-fx-background-color: #f8fafc;
-fx-border-color: #e5e7eb;
-fx-border-width: 0 0 1 0;
}
.file-status-title {
-fx-font-size: 16px;
-fx-font-weight: 600;
-fx-text-fill: #111827;
}
.file-status-table {
-fx-background-color: white;
}
.file-status-table .table-row-cell {
-fx-background-color: white;
}
.file-status-table .table-row-cell:hover {
-fx-background-color: #f8fafc;
}
.file-status-table .table-row-cell:selected {
-fx-background-color: #e0e7ff;
}
/* 对话框样式 */
.dialog-title {
-fx-font-size: 18px;
-fx-font-weight: bold;
-fx-text-fill: #111827;
}
/* 文件状态徽章 */
.file-status-badge {
-fx-background-radius: 4;
-fx-padding: 2 8;
-fx-font-size: 12px;
-fx-font-weight: 500;
}
.file-status-badge.modified {
-fx-background-color: #d1fae5;
-fx-text-fill: #065f46;
}
.file-status-badge.added {
-fx-background-color: #dbeafe;
-fx-text-fill: #1e40af;
}
.file-status-badge.deleted {
-fx-background-color: #fee2e2;
-fx-text-fill: #991b1b;
}
.file-status-badge.conflicted {
-fx-background-color: #fef3c7;
-fx-text-fill: #92400e;
}

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<?import java.net.URL?>
<BorderPane xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.svnmanager.controller.MainController">
<stylesheets>
<URL value="@../css/styles.css" />
</stylesheets>
<!-- 左侧边栏 -->
<left>
<VBox fx:id="sidebar" styleClass="sidebar" prefWidth="320" spacing="0">
<!-- 头部 -->
<VBox styleClass="sidebar-header" spacing="12" style="-fx-padding: 24;">
<HBox spacing="12" alignment="CENTER_LEFT">
<Region prefWidth="40" prefHeight="40" styleClass="logo-icon"/>
<VBox spacing="2">
<Label text="SVN管理器" styleClass="app-title"/>
<Label text="多项目管理" styleClass="app-subtitle"/>
</VBox>
</HBox>
<Button fx:id="addProjectButton" text="添加新项目" styleClass="add-project-button" onAction="#handleAddProject" maxWidth="Infinity"/>
</VBox>
<!-- 项目列表 -->
<ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS">
<VBox fx:id="projectList" styleClass="project-list" spacing="8" style="-fx-padding: 16;">
<!-- 项目卡片将通过代码动态添加 -->
</VBox>
</ScrollPane>
<!-- 底部信息 -->
<VBox styleClass="sidebar-footer" style="-fx-padding: 16; -fx-border-color: #e2e8f0; -fx-border-width: 1 0 0 0;">
<Label fx:id="projectCountLabel" text="共 0 个项目" styleClass="project-count"/>
</VBox>
</VBox>
</left>
<!-- 右侧主内容区 -->
<center>
<VBox spacing="0" VBox.vgrow="ALWAYS">
<!-- 顶部工具栏 -->
<HBox styleClass="toolbar" alignment="CENTER_LEFT" spacing="16" style="-fx-padding: 16 24;">
<VBox spacing="4" HBox.hgrow="ALWAYS">
<Label fx:id="projectTitleLabel" text="请选择项目" styleClass="project-title-label"/>
<Label fx:id="projectPathLabel" text="" styleClass="project-path-label"/>
</VBox>
<HBox spacing="12">
<Button fx:id="refreshButton" text="刷新状态" styleClass="refresh-button" onAction="#handleRefresh"/>
<Button fx:id="executeButton" text="执行操作" styleClass="execute-button" onAction="#handleExecute"/>
</HBox>
</HBox>
<!-- 操作按钮组 -->
<HBox styleClass="action-buttons" spacing="12" style="-fx-padding: 16 24; -fx-background-color: white; -fx-border-color: #e2e8f0; -fx-border-width: 0 0 1 0;">
<Button fx:id="checkoutButton" text="Checkout" styleClass="action-btn checkout-btn" onAction="#handleCheckout"/>
<Button fx:id="updateButton" text="Update" styleClass="action-btn update-btn" onAction="#handleUpdate"/>
<Button fx:id="commitButton" text="Commit" styleClass="action-btn commit-btn" onAction="#handleCommit"/>
<Button fx:id="statusButton" text="Status" styleClass="action-btn status-btn" onAction="#handleStatus"/>
<Button fx:id="logButton" text="Log" styleClass="action-btn log-btn" onAction="#handleLog"/>
<Button fx:id="diffButton" text="Diff" styleClass="action-btn diff-btn" onAction="#handleDiff"/>
<Button fx:id="infoButton" text="Info" styleClass="action-btn info-btn" onAction="#handleInfo"/>
</HBox>
<!-- 内容区域 -->
<ScrollPane fitToWidth="true" VBox.vgrow="ALWAYS" style="-fx-background-color: #f8fafc;">
<VBox spacing="24" style="-fx-padding: 24;">
<!-- 统计信息卡片 -->
<HBox spacing="16" HBox.hgrow="ALWAYS">
<!-- 当前版本卡片 -->
<VBox styleClass="stat-card" HBox.hgrow="ALWAYS">
<HBox alignment="CENTER_LEFT" spacing="8">
<Label text="当前版本" styleClass="stat-label"/>
<Region HBox.hgrow="ALWAYS"/>
</HBox>
<Label fx:id="currentVersionLabel" text="r0" styleClass="stat-value"/>
<Label text="最新版本" styleClass="stat-description"/>
</VBox>
<!-- 工作副本卡片 -->
<VBox styleClass="stat-card" HBox.hgrow="ALWAYS">
<HBox alignment="CENTER_LEFT" spacing="8">
<Label text="工作副本" styleClass="stat-label"/>
<Region HBox.hgrow="ALWAYS"/>
</HBox>
<Label fx:id="workingCopyLabel" text="r0" styleClass="stat-value"/>
<Label text="需要更新" styleClass="stat-description"/>
</VBox>
<!-- 修改文件卡片 -->
<VBox styleClass="stat-card" HBox.hgrow="ALWAYS">
<HBox alignment="CENTER_LEFT" spacing="8">
<Label text="修改文件" styleClass="stat-label"/>
<Region HBox.hgrow="ALWAYS"/>
</HBox>
<Label fx:id="modifiedFilesLabel" text="0" styleClass="stat-value"/>
<Label text="待提交" styleClass="stat-description"/>
</VBox>
</HBox>
<!-- 文件状态列表 -->
<VBox styleClass="file-status-card" spacing="0">
<HBox styleClass="file-status-header" alignment="CENTER_LEFT" style="-fx-padding: 16 24; -fx-background-color: #f8fafc;">
<Label text="文件状态" styleClass="file-status-title"/>
</HBox>
<TableView fx:id="fileStatusTable" styleClass="file-status-table">
<columns>
<TableColumn fx:id="statusColumn" text="状态" prefWidth="60"/>
<TableColumn fx:id="pathColumn" text="文件路径" prefWidth="400"/>
<TableColumn fx:id="actionColumn" text="操作" prefWidth="80"/>
</columns>
</TableView>
</VBox>
</VBox>
</ScrollPane>
</VBox>
</center>
</BorderPane>

View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<DialogPane xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.svnmanager.controller.ProjectDialogController">
<stylesheets>
<URL value="@../css/styles.css" />
</stylesheets>
<headerText>
<Label text="添加/编辑项目" styleClass="dialog-title"/>
</headerText>
<content>
<VBox spacing="16" style="-fx-padding: 20;">
<GridPane hgap="12" vgap="12">
<columnConstraints>
<ColumnConstraints minWidth="100" prefWidth="100"/>
<ColumnConstraints hgrow="ALWAYS" minWidth="300"/>
</columnConstraints>
<!-- 项目名称 -->
<Label text="项目名称:" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
<TextField fx:id="nameField" promptText="请输入项目名称" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
<!-- 项目路径 -->
<Label text="项目路径:" GridPane.columnIndex="0" GridPane.rowIndex="1"/>
<HBox spacing="8" GridPane.columnIndex="1" GridPane.rowIndex="1">
<TextField fx:id="pathField" promptText="请输入项目路径" HBox.hgrow="ALWAYS"/>
<Button text="浏览..." onAction="#handleBrowsePath"/>
</HBox>
<!-- SVN URL -->
<Label text="SVN URL:" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
<TextField fx:id="svnUrlField" promptText="请输入SVN仓库URL" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
</GridPane>
</VBox>
</content>
<buttonTypes>
<ButtonType text="确定" buttonData="OK_DONE"/>
<ButtonType text="取消" buttonData="CANCEL_CLOSE"/>
</buttonTypes>
</DialogPane>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${user.home}/.svn-manager/svn-manager.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${user.home}/.svn-manager/svn-manager.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<logger name="com.svnmanager" level="DEBUG" />
</configuration>

View File

@@ -0,0 +1 @@
[]

418
ui-preview.html Normal file
View File

@@ -0,0 +1,418 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SVN管理器 - 多项目管理界面</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
* {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.project-card {
transition: all 0.2s ease;
}
.project-card:hover {
transform: translateX(4px);
}
.project-card.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.project-card.active .project-icon {
background: rgba(255, 255, 255, 0.2);
}
.action-btn {
transition: all 0.2s ease;
}
.action-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 4px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.file-item {
transition: background-color 0.15s ease;
}
.file-item:hover {
background-color: #f8fafc;
}
.sidebar {
border-right: 1px solid #e2e8f0;
}
.main-content {
background: #f8fafc;
}
</style>
</head>
<body class="bg-gray-50">
<div class="flex h-screen overflow-hidden">
<!-- 左侧边栏 - 项目列表 -->
<div class="sidebar w-80 bg-white flex flex-col">
<!-- 头部 -->
<div class="p-6 border-b border-gray-200">
<div class="flex items-center gap-3 mb-4">
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-lg flex items-center justify-center">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
</svg>
</div>
<div>
<h1 class="text-xl font-bold text-gray-900">SVN管理器</h1>
<p class="text-sm text-gray-500">多项目管理</p>
</div>
</div>
<button class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
添加新项目
</button>
</div>
<!-- 项目列表 -->
<div class="flex-1 overflow-y-auto p-4 space-y-2">
<!-- 项目A -->
<div class="project-card active p-4 rounded-xl cursor-pointer" onclick="selectProject('project-a')">
<div class="flex items-start gap-3">
<div class="project-icon w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-sm mb-1 truncate">项目A - 前端开发</h3>
<p class="text-xs opacity-80 mb-2 truncate">/Users/workspace/project-a</p>
<div class="flex items-center gap-2">
<span class="status-badge text-xs px-2 py-0.5 rounded-full bg-green-100 text-green-700">
<span class="status-dot bg-green-500"></span>
已同步
</span>
</div>
</div>
</div>
</div>
<!-- 项目B -->
<div class="project-card p-4 rounded-xl cursor-pointer bg-gray-50 hover:bg-gray-100" onclick="selectProject('project-b')">
<div class="flex items-start gap-3">
<div class="project-icon w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-sm mb-1 truncate text-gray-900">项目B - 后端服务</h3>
<p class="text-xs text-gray-500 mb-2 truncate">/Users/workspace/project-b</p>
<div class="flex items-center gap-2">
<span class="status-badge text-xs px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-700">
<span class="status-dot bg-yellow-500"></span>
有更新
</span>
</div>
</div>
</div>
</div>
<!-- 项目C -->
<div class="project-card p-4 rounded-xl cursor-pointer bg-gray-50 hover:bg-gray-100" onclick="selectProject('project-c')">
<div class="flex items-start gap-3">
<div class="project-icon w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-sm mb-1 truncate text-gray-900">项目C - 移动端</h3>
<p class="text-xs text-gray-500 mb-2 truncate">/Users/workspace/project-c</p>
<div class="flex items-center gap-2">
<span class="status-badge text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-700">
<span class="status-dot bg-gray-400"></span>
未连接
</span>
</div>
</div>
</div>
</div>
</div>
<!-- 底部信息 -->
<div class="p-4 border-t border-gray-200">
<div class="text-xs text-gray-500 text-center">
共 3 个项目
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content flex-1 flex flex-col overflow-hidden">
<!-- 顶部工具栏 -->
<div class="bg-white border-b border-gray-200 px-6 py-4">
<div class="flex items-center justify-between">
<div>
<h2 class="text-2xl font-bold text-gray-900" id="project-title">项目A - 前端开发</h2>
<p class="text-sm text-gray-500 mt-1" id="project-path">/Users/workspace/project-a</p>
</div>
<div class="flex items-center gap-3">
<button class="action-btn px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 font-medium rounded-lg text-sm">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
刷新状态
</button>
<button class="action-btn px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white font-medium rounded-lg text-sm">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg>
执行操作
</button>
</div>
</div>
</div>
<!-- 操作按钮组 -->
<div class="bg-white px-6 py-4 border-b border-gray-200">
<div class="flex items-center gap-3 flex-wrap">
<button class="action-btn px-4 py-2 bg-blue-50 hover:bg-blue-100 text-blue-700 font-medium rounded-lg text-sm border border-blue-200">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"></path>
</svg>
Checkout
</button>
<button class="action-btn px-4 py-2 bg-green-50 hover:bg-green-100 text-green-700 font-medium rounded-lg text-sm border border-green-200">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
Update
</button>
<button class="action-btn px-4 py-2 bg-purple-50 hover:bg-purple-100 text-purple-700 font-medium rounded-lg text-sm border border-purple-200">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path>
</svg>
Commit
</button>
<button class="action-btn px-4 py-2 bg-yellow-50 hover:bg-yellow-100 text-yellow-700 font-medium rounded-lg text-sm border border-yellow-200">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
Status
</button>
<button class="action-btn px-4 py-2 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 font-medium rounded-lg text-sm border border-indigo-200">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Log
</button>
<button class="action-btn px-4 py-2 bg-gray-50 hover:bg-gray-100 text-gray-700 font-medium rounded-lg text-sm border border-gray-200">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
Diff
</button>
<button class="action-btn px-4 py-2 bg-gray-50 hover:bg-gray-100 text-gray-700 font-medium rounded-lg text-sm border border-gray-200">
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Info
</button>
</div>
</div>
<!-- 内容区域 -->
<div class="flex-1 overflow-y-auto p-6">
<!-- 项目信息卡片 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div class="bg-white rounded-xl p-5 border border-gray-200 shadow-sm">
<div class="flex items-center justify-between mb-3">
<span class="text-sm text-gray-500">当前版本</span>
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
</div>
<div class="text-2xl font-bold text-gray-900">r1234</div>
<div class="text-xs text-gray-500 mt-1">最新版本</div>
</div>
<div class="bg-white rounded-xl p-5 border border-gray-200 shadow-sm">
<div class="flex items-center justify-between mb-3">
<span class="text-sm text-gray-500">工作副本</span>
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path>
</svg>
</div>
<div class="text-2xl font-bold text-gray-900">r1230</div>
<div class="text-xs text-gray-500 mt-1">需要更新</div>
</div>
<div class="bg-white rounded-xl p-5 border border-gray-200 shadow-sm">
<div class="flex items-center justify-between mb-3">
<span class="text-sm text-gray-500">修改文件</span>
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<div class="text-2xl font-bold text-gray-900">5</div>
<div class="text-xs text-gray-500 mt-1">待提交</div>
</div>
</div>
<!-- 文件状态列表 -->
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h3 class="font-semibold text-gray-900">文件状态</h3>
</div>
<div class="divide-y divide-gray-100">
<div class="file-item px-6 py-4 flex items-center justify-between hover:bg-gray-50">
<div class="flex items-center gap-3 flex-1">
<div class="w-2 h-2 rounded-full bg-green-500"></div>
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-900">src/components/Header.vue</div>
<div class="text-xs text-gray-500">已修改</div>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-xs px-2 py-1 bg-green-100 text-green-700 rounded">M</span>
<button class="text-gray-400 hover:text-gray-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</button>
</div>
</div>
<div class="file-item px-6 py-4 flex items-center justify-between hover:bg-gray-50">
<div class="flex items-center gap-3 flex-1">
<div class="w-2 h-2 rounded-full bg-yellow-500"></div>
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-900">src/utils/api.js</div>
<div class="text-xs text-gray-500">冲突</div>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-xs px-2 py-1 bg-yellow-100 text-yellow-700 rounded">C</span>
<button class="text-gray-400 hover:text-gray-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</button>
</div>
</div>
<div class="file-item px-6 py-4 flex items-center justify-between hover:bg-gray-50">
<div class="flex items-center gap-3 flex-1">
<div class="w-2 h-2 rounded-full bg-blue-500"></div>
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-gray-900">src/styles/main.css</div>
<div class="text-xs text-gray-500">已添加</div>
</div>
</div>
<div class="flex items-center gap-2">
<span class="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded">A</span>
<button class="text-gray-400 hover:text-gray-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 项目数据
const projects = {
'project-a': {
title: '项目A - 前端开发',
path: '/Users/workspace/project-a',
version: 'r1234',
workingCopy: 'r1230',
modifiedFiles: 5
},
'project-b': {
title: '项目B - 后端服务',
path: '/Users/workspace/project-b',
version: 'r2456',
workingCopy: 'r2456',
modifiedFiles: 2
},
'project-c': {
title: '项目C - 移动端',
path: '/Users/workspace/project-c',
version: 'r1890',
workingCopy: 'r1885',
modifiedFiles: 0
}
};
function selectProject(projectId) {
// 更新项目卡片状态
document.querySelectorAll('.project-card').forEach(card => {
card.classList.remove('active');
card.classList.add('bg-gray-50', 'hover:bg-gray-100');
});
const clickedCard = event.currentTarget;
clickedCard.classList.add('active');
clickedCard.classList.remove('bg-gray-50', 'hover:bg-gray-100');
// 更新主内容区
const project = projects[projectId];
if (project) {
document.getElementById('project-title').textContent = project.title;
document.getElementById('project-path').textContent = project.path;
// 更新统计信息
const stats = document.querySelectorAll('.bg-white.rounded-xl.p-5');
if (stats.length >= 3) {
stats[0].querySelector('.text-2xl').textContent = project.version;
stats[1].querySelector('.text-2xl').textContent = project.workingCopy;
stats[2].querySelector('.text-2xl').textContent = project.modifiedFiles;
}
}
}
</script>
</body>
</html>