feat:界面变动,逻辑优化
This commit is contained in:
parent
dcfc19aafa
commit
1bf1579e43
15
build.gradle
15
build.gradle
|
|
@ -64,7 +64,6 @@ dependencies {
|
|||
|
||||
// 你项目的其他依赖(老库走classpath)
|
||||
implementation 'ch.qos.logback:logback-classic:1.5.6'
|
||||
implementation 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0'
|
||||
implementation 'commons-cli:commons-cli:1.9.0'
|
||||
implementation 'com.alibaba:easyexcel:4.0.3'
|
||||
implementation 'org.apache.pdfbox:pdfbox:3.0.5'
|
||||
|
|
@ -131,18 +130,22 @@ tasks.register('runCli', JavaExec) {
|
|||
}
|
||||
|
||||
// 可选:创建生成可执行JAR的任务
|
||||
tasks.register('buildCliJar', Jar) {
|
||||
tasks.register('buildFatJar', Jar) {
|
||||
group = 'build'
|
||||
description = 'Builds a standalone JAR for CLI mode'
|
||||
description = 'Builds a standalone JAR with all dependencies'
|
||||
|
||||
manifest {
|
||||
attributes 'Main-Class': 'top.r3944realms.docchecktoolrefactored.Main'
|
||||
attributes(
|
||||
'Main-Class': 'top.r3944realms.docchecktoolrefactored.Main'
|
||||
)
|
||||
}
|
||||
|
||||
from {
|
||||
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
|
||||
} with jar
|
||||
}
|
||||
with jar // 继承主 jar 的内容
|
||||
|
||||
archiveBaseName = 'doc-check-tool-cli'
|
||||
archiveBaseName.set('doc-check-tool-cli')
|
||||
archiveVersion.set('1.0')
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
# 改动
|
||||
1. 改名
|
||||
2. 界面颜色
|
||||
3.
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
package top.r3944realms.docchecktoolrefactored;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.Stage;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.SceneManager;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class JavaFxApplication extends Application {
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
|
|
@ -13,6 +16,7 @@ public class JavaFxApplication extends Application {
|
|||
@Override
|
||||
public void start(Stage primaryStage) throws Exception {
|
||||
SceneManager.init(primaryStage);
|
||||
primaryStage.getIcons().add(new Image(Objects.requireNonNull(getClass().getResourceAsStream("/img/icon.jpg"))));
|
||||
SceneManager.switchLoginView();
|
||||
primaryStage.show();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored;
|
|||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.cil.CliProcessor;
|
||||
import top.r3944realms.docchecktoolrefactored.core.Setting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
|
@ -12,14 +13,13 @@ import java.util.List;
|
|||
*/
|
||||
@Slf4j
|
||||
public class Main {
|
||||
|
||||
/**
|
||||
* The entry point of application.
|
||||
*
|
||||
* @param args the input arguments
|
||||
*/
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
public static void main(String[] args) {
|
||||
System.init();
|
||||
// log.info(StringUtil.NO_BUG);
|
||||
// 检查是否有 --cli 参数
|
||||
List<String> list = Arrays.asList(args);
|
||||
|
|
|
|||
188
src/main/java/top/r3944realms/docchecktoolrefactored/System.java
Normal file
188
src/main/java/top/r3944realms/docchecktoolrefactored/System.java
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
package top.r3944realms.docchecktoolrefactored;
|
||||
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.core.Setting;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.Properties;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
|
||||
@Slf4j
|
||||
public enum System {
|
||||
INSTANCE;
|
||||
private volatile Setting setting;
|
||||
private volatile File lastModifiedFile;
|
||||
private static final String CONFIG_FILE_NAME = "config.ini";
|
||||
private static final Properties properties = new Properties();
|
||||
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
// 默认值
|
||||
private static final long DEFAULT_SINGLE_TIMEOUT = 30;
|
||||
private static final long DEFAULT_TOTAL_TIMEOUT = 300;
|
||||
private static final boolean DEFAULT_ENABLE_STEP = false;
|
||||
|
||||
public static void init() {
|
||||
loadSettings();
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(System::saveSettings));
|
||||
}
|
||||
|
||||
/** 加载配置文件 */
|
||||
private static void loadSettings() {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
Path configPath = getConfigPath();
|
||||
if (Files.exists(configPath)) {
|
||||
try (InputStream input = new FileInputStream(configPath.toFile())) {
|
||||
properties.load(input);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "配置文件加载成功: {}", configPath);
|
||||
INSTANCE.setting = propertiesToSetting(properties);
|
||||
} catch (IOException e) {
|
||||
log.error(LoggerHelper.DEBUG_MARKER, "读取配置文件失败: {}, 使用默认配置", e.getMessage());
|
||||
INSTANCE.setting = defaultSetting();
|
||||
settingToProperties(INSTANCE.setting, properties);
|
||||
}
|
||||
} else {
|
||||
INSTANCE.setting = defaultSetting();
|
||||
settingToProperties(INSTANCE.setting, properties);
|
||||
saveSettings(); // 首次启动保存默认配置
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "配置文件不存在,已创建默认配置: {}", configPath);
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 保存配置文件 */
|
||||
private static void saveSettings() {
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
if (INSTANCE.setting != null) {
|
||||
Path configPath = getConfigPath();
|
||||
Path configDir = configPath.getParent();
|
||||
if (!Files.exists(configDir)) {
|
||||
Files.createDirectories(configDir);
|
||||
}
|
||||
|
||||
settingToProperties(INSTANCE.setting, properties);
|
||||
|
||||
try (OutputStream output = new FileOutputStream(configPath.toFile())) {
|
||||
properties.store(new OutputStreamWriter(output, StandardCharsets.UTF_8),
|
||||
"DocCheckTool Configuration");
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "配置文件保存成功: {}", configPath);
|
||||
} catch (IOException e) {
|
||||
log.error(LoggerHelper.DEBUG_MARKER, "保存配置文件失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error(LoggerHelper.DEBUG_MARKER, "创建配置目录失败: {}", e.getMessage());
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取配置文件路径 */
|
||||
private static Path getConfigPath() {
|
||||
String userHome = java.lang.System.getProperty("user.home");
|
||||
return Paths.get(userHome, ".docchecktool", CONFIG_FILE_NAME);
|
||||
}
|
||||
|
||||
/** 将Setting对象转换为Properties */
|
||||
private static void settingToProperties(Setting setting, Properties props) {
|
||||
props.setProperty("singleTimeout", String.valueOf(setting.getSingleTimeout()));
|
||||
props.setProperty("totalTimeout", String.valueOf(setting.getTotalTimeout()));
|
||||
props.setProperty("enableStep", String.valueOf(setting.isEnableStep()));
|
||||
}
|
||||
|
||||
/** 将Properties转换为Setting对象 */
|
||||
private static Setting propertiesToSetting(Properties props) {
|
||||
Setting s = new Setting();
|
||||
|
||||
try {
|
||||
s.setSingleTimeout(Long.parseLong(props.getProperty("singleTimeout", String.valueOf(DEFAULT_SINGLE_TIMEOUT))));
|
||||
} catch (NumberFormatException e) {
|
||||
s.setSingleTimeout(DEFAULT_SINGLE_TIMEOUT);
|
||||
log.error(LoggerHelper.DEBUG_MARKER, "singleTimeout格式错误,使用默认值{}", DEFAULT_SINGLE_TIMEOUT);
|
||||
}
|
||||
|
||||
try {
|
||||
s.setTotalTimeout(Long.parseLong(props.getProperty("totalTimeout", String.valueOf(DEFAULT_TOTAL_TIMEOUT))));
|
||||
} catch (NumberFormatException e) {
|
||||
s.setTotalTimeout(DEFAULT_TOTAL_TIMEOUT);
|
||||
log.error(LoggerHelper.DEBUG_MARKER, "totalTimeout格式错误,使用默认值{}", DEFAULT_TOTAL_TIMEOUT);
|
||||
}
|
||||
try {
|
||||
s.setEnableStep(Boolean.parseBoolean(props.getProperty("enableStep", String.valueOf(Boolean.FALSE))));
|
||||
} catch (Exception e) {
|
||||
s.setEnableStep(DEFAULT_ENABLE_STEP);
|
||||
log.error(LoggerHelper.DEBUG_MARKER, "enableStep格式错误,使用默认值{}", DEFAULT_TOTAL_TIMEOUT);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 获取默认Setting */
|
||||
private static Setting defaultSetting() {
|
||||
Setting s = new Setting();
|
||||
s.setSingleTimeout(DEFAULT_SINGLE_TIMEOUT);
|
||||
s.setTotalTimeout(DEFAULT_TOTAL_TIMEOUT);
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 手动保存 */
|
||||
public static void saveSettingsNow() {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
/** 重新加载 */
|
||||
public static void reloadSettings() {
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
/** 获取Setting对象 */
|
||||
public static Setting getSetting() {
|
||||
return INSTANCE.setting;
|
||||
}
|
||||
/** 获取File 对象 */
|
||||
public static File getlastModifiedFile() {
|
||||
return INSTANCE.lastModifiedFile;
|
||||
}
|
||||
|
||||
/** 获取配置目录路径 */
|
||||
public static String getConfigDirectory() {
|
||||
return getConfigPath().getParent().toString();
|
||||
}
|
||||
|
||||
/** 获取配置文件路径 */
|
||||
public static String getConfigFilePath() {
|
||||
return getConfigPath().toString();
|
||||
}
|
||||
public static void setLastModifiedFile(File lastModifiedFile) {
|
||||
INSTANCE.lastModifiedFile = lastModifiedFile;
|
||||
}
|
||||
public static FileChooser getFileChooser() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
File lastFile = getlastModifiedFile();
|
||||
if (lastFile != null) {
|
||||
File parentDir = lastFile.getParentFile();
|
||||
if (parentDir != null && parentDir.exists() && parentDir.isDirectory()) {
|
||||
fileChooser.setInitialDirectory(parentDir);
|
||||
}
|
||||
}
|
||||
return fileChooser;
|
||||
}
|
||||
public static DirectoryChooser getDirectoryChooser() {
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
File lastFile = getlastModifiedFile();
|
||||
if (lastFile != null) {
|
||||
File parentDir = lastFile.getParentFile();
|
||||
if (parentDir != null && parentDir.exists() && parentDir.isDirectory()) {
|
||||
directoryChooser.setInitialDirectory(parentDir);
|
||||
}
|
||||
}
|
||||
return directoryChooser;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
|
|||
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup;
|
||||
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
|
|
@ -102,7 +103,7 @@ public class CliProcessor {
|
|||
printHelp(options);
|
||||
System.exit(1);
|
||||
} catch (Exception e) {
|
||||
log.error("Error processing CLI command", e);
|
||||
log.error(LoggerHelper.DEBUG_MARKER, "Error processing CLI command", e);
|
||||
System.err.println("Error: " + e.getMessage());
|
||||
System.exit(1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored.core;
|
|||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
|
|
@ -64,8 +65,7 @@ public class AddressFileComparator {
|
|||
int physicalCount = physicalRecords.size();
|
||||
int logicalCount = logicalRecords.size();
|
||||
|
||||
log.info("读取物理地址文件记录数: {}", physicalCount);
|
||||
log.info("读取逻辑地址文件记录数: {}", logicalCount);
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "读取物理地址文件记录数: {}, 读取逻辑地址文件记录数: {}", physicalCount, logicalCount);
|
||||
|
||||
List<String> forwardComparisonResults = new ArrayList<>(); // 物理文件在逻辑文件中未找到
|
||||
List<String> backwardComparisonResults = new ArrayList<>(); // 逻辑文件在物理文件中未找到
|
||||
|
|
@ -178,7 +178,7 @@ public class AddressFileComparator {
|
|||
try {
|
||||
File file = new File(filePath);
|
||||
if (!file.exists()) {
|
||||
log.error("CSV文件不存在: {}", filePath);
|
||||
log.error(LoggerHelper.RELEASE_MARKER, "CSV文件不存在: {}", filePath);
|
||||
return records;
|
||||
}
|
||||
|
||||
|
|
@ -198,10 +198,10 @@ public class AddressFileComparator {
|
|||
}
|
||||
|
||||
reader.close();
|
||||
log.info("成功读取CSV文件,共 {} 行记录", records.size());
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "成功读取CSV文件,共 {} 行记录", records.size());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("读取CSV文件时出错: {}", e.getMessage(), e);
|
||||
log.error(LoggerHelper.RELEASE_MARKER, "读取CSV文件时出错: {}", e.getMessage(), e);
|
||||
}
|
||||
|
||||
return records;
|
||||
|
|
@ -211,49 +211,46 @@ public class AddressFileComparator {
|
|||
List<String> forwardResults, List<String> backwardResults,
|
||||
List<String> pathMismatchResults, List<String> pageCountMismatchResults,
|
||||
CompareMode compareMode) {
|
||||
log.info("=== 文件比较结果 ===");
|
||||
log.info("物理地址文件记录数: {}", physicalCount);
|
||||
log.info("逻辑地址文件记录数: {}", logicalCount);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("=== 文件比较结果 ===\n");
|
||||
sb.append("物理地址文件记录数: ").append(physicalCount).append("\n");
|
||||
sb.append("逻辑地址文件记录数: ").append(logicalCount).append("\n");
|
||||
|
||||
if (pathMismatchResults.isEmpty()) {
|
||||
log.info("没有路径错误");
|
||||
sb.append("没有路径错误\n");
|
||||
} else {
|
||||
log.info("文件名相同但路径不一致的记录数量: {}", pathMismatchResults.size());
|
||||
for (String result : pathMismatchResults) {
|
||||
log.info("\t{}", result);
|
||||
}
|
||||
sb.append("文件名相同但路径不一致的记录数量: ").append(pathMismatchResults.size()).append("\n");
|
||||
pathMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n"));
|
||||
}
|
||||
|
||||
if (compareMode == CompareMode.FILE_LEVEL) {
|
||||
if (pageCountMismatchResults.isEmpty()) {
|
||||
log.info("没有页数错误");
|
||||
sb.append("没有页数错误\n");
|
||||
} else {
|
||||
log.info("文件名和路径相同但页数不一致的记录数量: {}", pageCountMismatchResults.size());
|
||||
for (String result : pageCountMismatchResults) {
|
||||
log.info("\t{}", result);
|
||||
}
|
||||
sb.append("文件名和路径相同但页数不一致的记录数量: ")
|
||||
.append(pageCountMismatchResults.size()).append("\n");
|
||||
pageCountMismatchResults.forEach(result -> sb.append("\t").append(result).append("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
if (forwardResults.isEmpty()) {
|
||||
log.info("没有物理存在而逻辑不存在的文件");
|
||||
sb.append("没有物理存在而逻辑不存在的文件\n");
|
||||
} else {
|
||||
log.info("物理文件在逻辑文件中未找到的记录数量: {}", forwardResults.size());
|
||||
for (String result : forwardResults) {
|
||||
log.info("\t{}", result);
|
||||
}
|
||||
sb.append("物理文件在逻辑文件中未找到的记录数量: ").append(forwardResults.size()).append("\n");
|
||||
forwardResults.forEach(result -> sb.append("\t").append(result).append("\n"));
|
||||
}
|
||||
|
||||
if (backwardResults.isEmpty()) {
|
||||
log.info("没有逻辑存在而物理不存在的文件");
|
||||
sb.append("没有逻辑存在而物理不存在的文件\n");
|
||||
} else {
|
||||
log.info("逻辑文件在物理文件中未找到的记录数量: {}", backwardResults.size());
|
||||
for (String result : backwardResults) {
|
||||
log.info("\t{}", result);
|
||||
}
|
||||
sb.append("逻辑文件在物理文件中未找到的记录数量: ").append(backwardResults.size()).append("\n");
|
||||
backwardResults.forEach(result -> sb.append("\t").append(result).append("\n"));
|
||||
}
|
||||
|
||||
log.info("=== 比较完成 ===");
|
||||
sb.append("=== 比较完成 ===");
|
||||
|
||||
log.info(LoggerHelper.RELEASE_MARKER, sb.toString()); // 一次性输出, 减少 I/O
|
||||
}
|
||||
|
||||
// 为向后兼容保留原来的日志方法
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import java.io.File;
|
|||
|
||||
public interface AddressFileGenerator {
|
||||
/**
|
||||
* 页面级
|
||||
* 页面级 JPG那种
|
||||
*/
|
||||
int PAGE_TYPE = 1;
|
||||
/**
|
||||
* 文件级
|
||||
* 文件级 PDF那种
|
||||
*/
|
||||
int FILE_TYPE = 2;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
import com.sun.scenario.Settings;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup;
|
||||
import top.r3944realms.docchecktoolrefactored.model.FileMetadata;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -17,64 +21,103 @@ import java.util.stream.Collectors;
|
|||
/**
|
||||
* 重复文件查找核心类
|
||||
*/
|
||||
//TODO;代替DuplicateDocumentDetectionTask
|
||||
@Slf4j
|
||||
public class DuplicateFinder {
|
||||
private final FileScanner fileScanner;
|
||||
private final FileHashCalculator hashCalculator;
|
||||
private final boolean enableProgress;
|
||||
private final ExecutorService executorService;
|
||||
// 进度回调接口
|
||||
public interface ProgressCallback {
|
||||
default void onPhaseStarted(Phase phase) {}
|
||||
default void onPhaseProgress(Phase phase, int current, int total) {}
|
||||
default void onPhaseCompleted(Phase phase) {}
|
||||
}
|
||||
public enum Phase {
|
||||
GROUP_BY_SIZE, // 按大小分组阶段
|
||||
CALCULATE_HASH // 计算哈希阶段
|
||||
}
|
||||
@Setter
|
||||
private ProgressCallback progressCallback;
|
||||
private static final int PROGRESS_REPORT_INTERVAL = 100;
|
||||
private static final int BATCH_SIZE = 100;
|
||||
private final List<Exception> errors = new CopyOnWriteArrayList<>();
|
||||
@Getter
|
||||
private long timeout = -1;
|
||||
public DuplicateFinder(FileScanner fileScanner, FileHashCalculator hashCalculator, boolean enableProgress) {
|
||||
this.fileScanner = Objects.requireNonNull(fileScanner);
|
||||
this.hashCalculator = Objects.requireNonNull(hashCalculator);
|
||||
this.enableProgress = enableProgress;
|
||||
// 根据CPU核心数设置线程池大小
|
||||
int poolSize = Runtime.getRuntime().availableProcessors();
|
||||
this.executorService = Executors.newFixedThreadPool(poolSize);
|
||||
}
|
||||
public DuplicateFinder(FileScanner fileScanner, FileHashCalculator hashCalculator) {
|
||||
this(fileScanner, hashCalculator, false);
|
||||
}
|
||||
|
||||
public DuplicateFinder applySetting(Setting setting) {
|
||||
this.timeout = setting.getSingleTimeout();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找重复文件
|
||||
* @param rootDir 要扫描的根目录
|
||||
* @return 按哈希值分组的重复文件列表
|
||||
*/
|
||||
public List<DuplicateGroup> findDuplicates(Path rootDir) throws IOException {
|
||||
// 清理错误列表
|
||||
errors.clear();
|
||||
// -----------------------------
|
||||
// 第一阶段:按文件大小分组
|
||||
// -----------------------------
|
||||
if (progressCallback != null) {
|
||||
progressCallback.onPhaseStarted(Phase.GROUP_BY_SIZE);
|
||||
}
|
||||
Map<Long, List<FileMetadata>> sizeGroups = groupFilesBySize(rootDir);
|
||||
|
||||
// 计算需要处理的总文件数(大小分组中可能有重复的文件)
|
||||
int totalFilesToProcess = sizeGroups.values().stream()
|
||||
.filter(group -> group.size() > 1)
|
||||
.mapToInt(List::size)
|
||||
.sum();
|
||||
|
||||
if (totalFilesToProcess == 0) {
|
||||
return Collections.emptyList();
|
||||
if (progressCallback != null) {
|
||||
progressCallback.onPhaseCompleted(Phase.GROUP_BY_SIZE);
|
||||
}
|
||||
|
||||
// 第二阶段:对可能重复的文件计算哈希
|
||||
// -----------------------------
|
||||
// 第二阶段:按文件组计算哈希
|
||||
// -----------------------------
|
||||
if (progressCallback != null) progressCallback.onPhaseStarted(Phase.CALCULATE_HASH);
|
||||
|
||||
Map<String, List<FileMetadata>> hashGroups = new ConcurrentHashMap<>();
|
||||
AtomicInteger processedFiles = new AtomicInteger(0);
|
||||
|
||||
sizeGroups.values().parallelStream()
|
||||
.filter(group -> group.size() > 1) // 只处理可能重复的文件
|
||||
.forEach(group -> group.parallelStream().forEach(file -> {
|
||||
try {
|
||||
String hash = hashCalculator.calculateHash(file.getPath());
|
||||
file.setHash(hash);
|
||||
hashGroups.computeIfAbsent(hash, k -> new ArrayList<>()).add(file);
|
||||
// 更新进度
|
||||
int current = processedFiles.incrementAndGet();
|
||||
if (enableProgress) {
|
||||
printProgress("Calculating hashes", current, totalFilesToProcess);
|
||||
// 获取候选文件组(每组内至少2个文件)
|
||||
List<List<FileMetadata>> candidateGroups = sizeGroups.values().stream()
|
||||
.filter(group -> group.size() > 1)
|
||||
.toList();
|
||||
|
||||
int totalFilesToProcess = candidateGroups.stream().mapToInt(List::size).sum();
|
||||
if (totalFilesToProcess == 0) return Collections.emptyList();
|
||||
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
|
||||
// 分批提交线程池,每组作为一个批次
|
||||
for (List<FileMetadata> group : candidateGroups) {
|
||||
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
|
||||
for (FileMetadata file : group) {
|
||||
processFile(file, hashGroups, processedFiles, totalFilesToProcess);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// 记录错误但继续处理其他文件
|
||||
log.error("Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage());
|
||||
}
|
||||
}));
|
||||
if (enableProgress) {
|
||||
System.out.println(); // 完成进度条后换行
|
||||
}, executorService);
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
|
||||
if (progressCallback != null) progressCallback.onPhaseCompleted(Phase.CALCULATE_HASH);
|
||||
if (enableProgress) System.out.println();
|
||||
|
||||
// -----------------------------
|
||||
// 第三阶段:构建结果
|
||||
// -----------------------------
|
||||
return hashGroups.values().stream()
|
||||
.filter(group -> group.size() > 1)
|
||||
.map(group -> new DuplicateGroup(
|
||||
|
|
@ -91,13 +134,15 @@ public class DuplicateFinder {
|
|||
*/
|
||||
private Map<Long, List<FileMetadata>> groupFilesBySize(Path rootDir) throws IOException {
|
||||
Map<Long, List<FileMetadata>> sizeGroups = new ConcurrentHashMap<>();
|
||||
boolean flag = timeout != -1 && timeout > 0;
|
||||
FileScanner.ProgressAwareListener listener = new FileScanner.ProgressAwareListener() {
|
||||
@Override
|
||||
public void onProgressUpdate(int current, int total) {
|
||||
if (enableProgress) {
|
||||
printProgress("Scanning files", current, total);
|
||||
} else {
|
||||
log.info("Scanning progress: {} / {} ", current, total);
|
||||
}
|
||||
if (progressCallback != null) {
|
||||
progressCallback.onPhaseProgress(Phase.GROUP_BY_SIZE, current, total);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -109,19 +154,20 @@ public class DuplicateFinder {
|
|||
meta.setSize(Files.size(file));
|
||||
sizeGroups.computeIfAbsent(meta.getSize(), k -> new ArrayList<>()).add(meta);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to get file's size: {}", file);
|
||||
log.error(LoggerHelper.TRACE_MARKER, "Failed to get file's size: {}", file);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void onScanComplete() {}
|
||||
@Override public void onError(Path file, Exception e) {
|
||||
log.error("Error on scanning file: {}, {}", file, e.getMessage());
|
||||
log.error(LoggerHelper.TRACE_MARKER, "Error on scanning file: {}, {}", file, e.getMessage());
|
||||
errors.add(e);
|
||||
}
|
||||
};
|
||||
if(enableProgress)
|
||||
fileScanner.scanWithProgress(rootDir, listener);
|
||||
if (flag) fileScanner.scanWithProgress(rootDir, listener, timeout); else fileScanner.scanWithProgress(rootDir, listener);
|
||||
else
|
||||
fileScanner.scan(rootDir, listener);
|
||||
if (flag) fileScanner.scan(rootDir, listener, timeout); else fileScanner.scan(rootDir, listener);
|
||||
return sizeGroups;
|
||||
}
|
||||
/**
|
||||
|
|
@ -144,4 +190,35 @@ public class DuplicateFinder {
|
|||
|
||||
System.out.print(progressBar);
|
||||
}
|
||||
private void processFile(FileMetadata file,
|
||||
Map<String, List<FileMetadata>> hashGroups,
|
||||
AtomicInteger processedFiles,
|
||||
int totalFilesToProcess) {
|
||||
try {
|
||||
String hash = file.getSize() > 10_000_000 ?
|
||||
hashCalculator.calculateHashStreaming(file.getPath()) :
|
||||
hashCalculator.calculateHash(file.getPath());
|
||||
|
||||
file.setHash(hash);
|
||||
synchronized (hashGroups) {
|
||||
hashGroups.computeIfAbsent(hash, k -> new ArrayList<>()).add(file);
|
||||
}
|
||||
|
||||
int current = processedFiles.incrementAndGet();
|
||||
if (enableProgress && (current % PROGRESS_REPORT_INTERVAL == 0 || current == totalFilesToProcess)) {
|
||||
printProgress("Calculating hashes", current, totalFilesToProcess);
|
||||
}
|
||||
|
||||
if (progressCallback != null) {
|
||||
progressCallback.onPhaseProgress(Phase.CALCULATE_HASH, current, totalFilesToProcess);
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error(LoggerHelper.TRACE_MARKER, "Failed to calculate file's hash: {}, {}", file.getPath(), e.getMessage());
|
||||
errors.add(e);
|
||||
}
|
||||
}
|
||||
public void shutdown() {
|
||||
executorService.shutdown();
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,13 @@ public interface FileHashCalculator {
|
|||
*/
|
||||
String calculateHash(Path file) throws IOException;
|
||||
|
||||
/**
|
||||
* 流式计算文件哈希值(适用于大文件)
|
||||
* @param file 要计算的文件路径
|
||||
* @return 文件的哈希值字符串
|
||||
*/
|
||||
String calculateHashStreaming(Path file) throws IOException;
|
||||
|
||||
/**
|
||||
* 计算文件部分hash值方法
|
||||
* @param file 要计算的文件路径
|
||||
|
|
@ -36,7 +43,15 @@ public interface FileHashCalculator {
|
|||
* @return 块大小
|
||||
*/
|
||||
default int getPartialSize() {
|
||||
return 4096;// 4 * 1024
|
||||
return 4096;// 4KB
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流式处理的缓冲区大小
|
||||
* @return 缓冲区大小
|
||||
*/
|
||||
default int getStreamingBufferSize() {
|
||||
return 8192; // 8KB
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileWriter;
|
||||
|
|
@ -13,7 +15,7 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@Slf4j
|
||||
public class HashFileGenerator {
|
||||
|
||||
public interface ProgressListener {
|
||||
|
|
@ -47,7 +49,7 @@ public class HashFileGenerator {
|
|||
|
||||
@Override
|
||||
public void onError(Path path, Exception e) {
|
||||
System.err.println("Error scanning path: " + path + " - " + e.getMessage());
|
||||
log.error(LoggerHelper.TRACE_MARKER, "Error scanning path: {} - {}", path, e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -74,7 +76,7 @@ public class HashFileGenerator {
|
|||
listener.onProgressUpdate(processed, totalFiles);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
System.err.println("无法计算该文件哈希值: " + file + " - " + e.getMessage());
|
||||
log.error(LoggerHelper.DEBUG_MARKER, "无法计算该文件哈希值: {} - {}", file, e.getMessage());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,23 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.HexFormat;
|
||||
|
||||
import static org.apache.commons.codec.digest.MessageDigestAlgorithms.MD5;
|
||||
|
||||
/**
|
||||
* MD5哈希计算实现
|
||||
*/
|
||||
public class MD5HashCalculator implements FileHashCalculator {
|
||||
private static final int BUFFER_SIZE = 8192;
|
||||
private static final HexFormat HEX_FORMAT = HexFormat.of();
|
||||
|
||||
@Override
|
||||
public String calculateHash(Path file) throws IOException {
|
||||
|
|
@ -29,6 +35,25 @@ public class MD5HashCalculator implements FileHashCalculator {
|
|||
throw new RuntimeException("MD5算法不可用", e);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public String calculateHashStreaming(Path file) throws IOException {
|
||||
try (InputStream is = Files.newInputStream(file);
|
||||
DigestInputStream dis = new DigestInputStream(is, MessageDigest.getInstance(MD5))) {
|
||||
|
||||
byte[] buffer = new byte[getStreamingBufferSize()];
|
||||
// 读取整个文件以更新摘要
|
||||
while (dis.read(buffer) != -1) {
|
||||
// 只需读取即可,DigestInputStream会自动更新摘要
|
||||
//noinspection UnnecessaryContinue
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] digest = dis.getMessageDigest().digest();
|
||||
return HEX_FORMAT.formatHex(digest);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("MD5 algorithm not available", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <h1>读取文件头部:</h1>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package top.r3944realms.docchecktoolrefactored.core;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
|
|
@ -143,7 +144,7 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator {
|
|||
document.close();
|
||||
return pageCount;
|
||||
} catch (Exception e) {
|
||||
log.warn("无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e);
|
||||
log.warn(LoggerHelper.RELEASE_MARKER, "无法获取PDF文件页数: {}", pdfFile.getAbsolutePath(), e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -215,7 +216,7 @@ public class PhysicalAddressFileGenerator implements AddressFileGenerator {
|
|||
for (int i = 0; i < pathParts.length; i++) {
|
||||
if (!foundPrefix) {
|
||||
// 检查当前部分是否包含prefix
|
||||
if (pathParts[i].contains(prefix)) {
|
||||
if (pathParts[i].equals(prefix)) {
|
||||
foundPrefix = true;
|
||||
resultPath.append(pathParts[i]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ScanningException extends RuntimeException {
|
||||
public final List<Exception> exceptions;
|
||||
public ScanningException(List<Exception> exceptions) {
|
||||
this.exceptions = exceptions;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package top.r3944realms.docchecktoolrefactored.core;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@Slf4j
|
||||
public class Setting {
|
||||
private long singleTimeout = 30;
|
||||
private long totalTimeout = 60 * 5;
|
||||
private boolean enableStep = false;
|
||||
}
|
||||
|
|
@ -5,12 +5,14 @@ import com.linuxense.javadbf.DBFRow;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
|
|
@ -20,8 +22,11 @@ import java.util.Optional;
|
|||
@Slf4j
|
||||
public class DbfFileReader implements CatalogFileReader {
|
||||
// 常量定义(避免硬编码)
|
||||
private static final String FIELD_ARCHIVE_CODE = "档号";
|
||||
private static final String FIELD_PAGE = "页数";
|
||||
private static final List<String> ARCHIVE_CODE_TAG_CANDIDATES =
|
||||
Arrays.asList("档号", "dangan", "fileNo", "DH");
|
||||
|
||||
private static final List<String> PAGE_COUNT_TAG_CANDIDATES =
|
||||
Arrays.asList("页数", "pages", "pageCount", "YS");
|
||||
|
||||
@Override
|
||||
public List<LogicalAddressFileGenerator.Record> readCatalogFile(String filePath) throws Exception {
|
||||
|
|
@ -33,36 +38,35 @@ public class DbfFileReader implements CatalogFileReader {
|
|||
DBFReader reader = new DBFReader(fis)
|
||||
) {
|
||||
int fieldCount = reader.getFieldCount();
|
||||
log.debug("开始读取DBF文件: {}, DBF文件字段数: {}",filePath, fieldCount);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "开始读取DBF文件: {}, DBF文件字段数: {}",filePath, fieldCount);
|
||||
|
||||
// 查找"档号"和"页数"字段的索引
|
||||
int archiveCodeIndex = -1;
|
||||
int pageIndex = -1;
|
||||
for (int i = 0; i < fieldCount; i++) {
|
||||
if (archiveCodeIndex != -1 && pageIndex != -1) {
|
||||
log.debug("已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "已找到所需字段,跳出循环,档号: {}, 页数: {}", archiveCodeIndex, pageIndex);
|
||||
break;
|
||||
}
|
||||
String fieldName = reader.getField(i).getName();
|
||||
log.debug("发现字段: {}", fieldName);
|
||||
if (FIELD_ARCHIVE_CODE.equals(fieldName)) {
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "发现字段: {}", fieldName);
|
||||
if (ARCHIVE_CODE_TAG_CANDIDATES.contains(fieldName)) {
|
||||
archiveCodeIndex = i;
|
||||
log.debug("找到‘档号’字段,索引: {}", archiveCodeIndex);
|
||||
} else if (FIELD_PAGE.equals(fieldName)) {
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "匹配到档号字段: {}, 索引: {}", fieldName, archiveCodeIndex);
|
||||
} else if (PAGE_COUNT_TAG_CANDIDATES.contains(fieldName)) {
|
||||
pageIndex = i;
|
||||
log.debug("找到‘页数’字段,索引: {}", pageIndex);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "匹配到页数字段: {}, 索引: {}", fieldName, pageIndex);
|
||||
}
|
||||
|
||||
}
|
||||
if (archiveCodeIndex == -1 || pageIndex == -1) {
|
||||
log.error("未找到必要字段,档号: {}, 页数: {}",
|
||||
log.error(LoggerHelper.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}",
|
||||
archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex,
|
||||
pageIndex == -1 ? "未找到" : pageIndex
|
||||
);
|
||||
throw new IllegalArgumentException(
|
||||
String.format("DBF文件缺少必要字段: %s=%s, %s=%s",
|
||||
FIELD_ARCHIVE_CODE, archiveCodeIndex == -1,
|
||||
FIELD_PAGE, pageIndex == -1
|
||||
String.format("DBF文件缺少必要字段: 档号=%s, 页数=%s",
|
||||
archiveCodeIndex == -1, pageIndex == -1
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -85,7 +89,7 @@ public class DbfFileReader implements CatalogFileReader {
|
|||
return Integer.parseInt(i.toString().trim());
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("无法将页数值转换为整数: {}", i);
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "无法将页数值转换为整数: {}", i);
|
||||
return 0;
|
||||
}
|
||||
}).orElse(0);
|
||||
|
|
@ -94,17 +98,17 @@ public class DbfFileReader implements CatalogFileReader {
|
|||
if (!archiveCode.isEmpty() && page > 0) {
|
||||
records.add(new LogicalAddressFileGenerator.Record(archiveCode, page));
|
||||
validRecords++;
|
||||
log.debug("读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
|
||||
} else {
|
||||
skippedRecords++;
|
||||
if (!archiveCode.isEmpty() || page > 0) {
|
||||
log.debug("跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "跳过无效记录 - 档号: {}, 页数: {}", archiveCode, page);
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("DBF文件读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
|
||||
log.info(LoggerHelper.RELEASE_MARKER, "DBF文件读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
|
||||
} catch (IOException e) {
|
||||
log.error("读取DBF文件失败: {}", filePath, e);
|
||||
log.error(LoggerHelper.RELEASE_MARKER, "读取DBF文件失败: {}", filePath, e);
|
||||
throw new UncheckedIOException("DBF文件读取异常", e);
|
||||
}
|
||||
return records;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import org.apache.poi.ss.usermodel.*;
|
|||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
|
|
@ -26,33 +27,33 @@ public class ExcelFileReader implements CatalogFileReader {
|
|||
}
|
||||
private List<LogicalAddressFileGenerator.Record> readExcelFile(File file, boolean isXlsx) throws Exception {
|
||||
List<LogicalAddressFileGenerator.Record> records = new ArrayList<>();
|
||||
log.debug("开始解析Excel文件,格式: {}", isXlsx ? "xlsx" : "xls");
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "开始解析Excel文件,格式: {}", isXlsx ? "xlsx" : "xls");
|
||||
try (FileInputStream fis = new FileInputStream(file);
|
||||
Workbook workbook = isXlsx ? new XSSFWorkbook(fis) : new HSSFWorkbook(fis)) {
|
||||
// 获取第一个工作表
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
log.debug("读取工作表: {}", sheet.getSheetName());
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "读取工作表: {}", sheet.getSheetName());
|
||||
|
||||
// 获取标题行
|
||||
Row headerRow = sheet.getRow(0);
|
||||
if (headerRow == null) {
|
||||
log.error("Excel文件缺少标题行");
|
||||
log.error(LoggerHelper.RELEASE_MARKER, "Excel文件缺少标题行");
|
||||
throw new IllegalArgumentException("Excel文件缺少标题行");
|
||||
}
|
||||
// 查找"档号"和"页数"列的索引
|
||||
int archiveCodeIndex = -1;
|
||||
int pageIndex = -1;
|
||||
log.debug("开始查找'档号'和'页数'列的索引");
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "开始查找'档号'和'页数'列的索引");
|
||||
boolean foundExactMatch = false;
|
||||
for (Cell cell : headerRow) {
|
||||
String cellValue = getCellValueAsString(cell).trim();
|
||||
if (FIELD_ARCHIVE_CODE.equals(cellValue)) {
|
||||
archiveCodeIndex = cell.getColumnIndex();
|
||||
log.debug("找到'档号'列,索引: {}", archiveCodeIndex);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "找到'档号'列,索引: {}", archiveCodeIndex);
|
||||
} else if (FIELD_PAGE.equals(cellValue)) {
|
||||
pageIndex = cell.getColumnIndex();
|
||||
foundExactMatch = true;
|
||||
log.debug("找到精确匹配'页数'列,索引: {}", pageIndex);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "找到精确匹配'页数'列,索引: {}", pageIndex);
|
||||
}
|
||||
}
|
||||
// 如果没有精确匹配,进行模糊查找
|
||||
|
|
@ -61,13 +62,13 @@ public class ExcelFileReader implements CatalogFileReader {
|
|||
String cellValue = getCellValueAsString(cell).trim();
|
||||
if (cellValue.contains(FIELD_PAGE)) {
|
||||
pageIndex = cell.getColumnIndex();
|
||||
log.debug("找到模糊匹配'页数'列,索引: {}", pageIndex);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "找到模糊匹配'页数'列,索引: {}", pageIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 检查是否找到必需的列
|
||||
if (archiveCodeIndex == -1 || pageIndex == -1) {
|
||||
log.error("未找到必要字段,档号: {}, 页数: {}",
|
||||
log.error(LoggerHelper.RELEASE_MARKER, "未找到必要字段,档号: {}, 页数: {}",
|
||||
archiveCodeIndex == -1 ? "未找到" : archiveCodeIndex,
|
||||
pageIndex == -1 ? "未找到" : pageIndex
|
||||
);
|
||||
|
|
@ -83,7 +84,7 @@ public class ExcelFileReader implements CatalogFileReader {
|
|||
int validRecords = 0;
|
||||
int skippedRecords = 0;
|
||||
|
||||
for (int i = 1; i <= totalRows; i++) {
|
||||
for (int i = 1; i < totalRows; i++) {
|
||||
Row row = sheet.getRow(i);
|
||||
if (row == null) {
|
||||
skippedRecords++;
|
||||
|
|
@ -115,12 +116,12 @@ public class ExcelFileReader implements CatalogFileReader {
|
|||
// 只有数据有效时才添加记录
|
||||
records.add(new LogicalAddressFileGenerator.Record(archiveCode, page));
|
||||
validRecords++;
|
||||
log.debug("读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "读取有效记录 - 档号: {}, 页数: {}", archiveCode, page);
|
||||
}
|
||||
|
||||
log.info("数据读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
|
||||
log.info(LoggerHelper.RELEASE_MARKER, "数据读取完成,有效记录: {} 条,跳过记录: {} 条", validRecords, skippedRecords);
|
||||
} catch (Exception e) {
|
||||
log.error("读取Excel文件时发生错误: {}", e.getMessage(), e);
|
||||
log.error(LoggerHelper.RELEASE_MARKER, "读取Excel文件时发生错误: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
return records;
|
||||
|
|
@ -188,7 +189,7 @@ public class ExcelFileReader implements CatalogFileReader {
|
|||
return 0;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("无法将单元格值转换为整数: {}", cell);
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "无法将单元格值转换为整数: {}", cell);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import org.w3c.dom.Node;
|
|||
import org.w3c.dom.NodeList;
|
||||
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.util.FileUtil;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
|
|
@ -23,10 +24,10 @@ public class XmlFileReader implements CatalogFileReader {
|
|||
Arrays.asList("row", "record", "data", "item", "档案");
|
||||
|
||||
private static final List<String> ARCHIVE_CODE_TAG_CANDIDATES =
|
||||
Arrays.asList("档号", "dangan", "fileNo");
|
||||
Arrays.asList("档号", "dangan", "fileNo", "DH");
|
||||
|
||||
private static final List<String> PAGE_COUNT_TAG_CANDIDATES =
|
||||
Arrays.asList("页数", "pages", "pageCount");
|
||||
Arrays.asList("页数", "pages", "pageCount", "YS");
|
||||
|
||||
|
||||
@Override
|
||||
|
|
@ -42,11 +43,11 @@ public class XmlFileReader implements CatalogFileReader {
|
|||
Document doc = builder.parse(file);
|
||||
doc.getDocumentElement().normalize();
|
||||
|
||||
log.debug("开始解析XML文件: {}, 根元素: {}",
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "开始解析XML文件: {}, 根元素: {}",
|
||||
file.getName(), doc.getDocumentElement().getNodeName());
|
||||
// 查找记录元素
|
||||
NodeList recordNodes = findAllRecordNodes(doc);
|
||||
log.debug("找到 {} 个潜在记录节点", recordNodes.getLength());
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "找到 {} 个潜在记录节点", recordNodes.getLength());
|
||||
|
||||
// 解析每个记录元素
|
||||
int validCount = 0;
|
||||
|
|
@ -61,16 +62,16 @@ public class XmlFileReader implements CatalogFileReader {
|
|||
if (record != null) {
|
||||
records.add(record);
|
||||
validCount++;
|
||||
log.debug("解析到有效记录: {}", record);
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "解析到有效记录: {}", record);
|
||||
} else {
|
||||
invalidCount++;
|
||||
log.debug("跳过无效记录节点");
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "跳过无效记录节点");
|
||||
}
|
||||
}
|
||||
}
|
||||
log.info("XML解析完成 - 有效记录: {}, 无效记录: {}", validCount, invalidCount);
|
||||
log.info(LoggerHelper.RELEASE_MARKER, "XML解析完成 - 有效记录: {}, 无效记录: {}", validCount, invalidCount);
|
||||
} catch (Exception e) {
|
||||
log.error("解析XML文件失败: {}", filePath, e);
|
||||
log.error(LoggerHelper.RELEASE_MARKER, "解析XML文件失败: {}", filePath, e);
|
||||
throw new Exception("解析XML文件失败: " + filePath, e);
|
||||
}
|
||||
return records;
|
||||
|
|
@ -92,11 +93,11 @@ public class XmlFileReader implements CatalogFileReader {
|
|||
for (String tagName : RECORD_TAG_CANDIDATES) {
|
||||
NodeList nodes = doc.getElementsByTagName(tagName);
|
||||
if (nodes.getLength() > 0) {
|
||||
log.debug("使用标签名 '{}' 找到 {} 个记录节点", tagName, nodes.getLength());
|
||||
log.debug(LoggerHelper.DEBUG_MARKER, "使用标签名 '{}' 找到 {} 个记录节点", tagName, nodes.getLength());
|
||||
return nodes;
|
||||
}
|
||||
}
|
||||
log.warn("未找到任何记录节点,尝试的标签名: {}", RECORD_TAG_CANDIDATES);
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "未找到任何记录节点,尝试的标签名: {}", RECORD_TAG_CANDIDATES);
|
||||
return new EmptyNodeList(); // 返回空节点列表而不是null
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +106,7 @@ public class XmlFileReader implements CatalogFileReader {
|
|||
String pageStr = findFirstNonEmptyTextContent(recordElement, PAGE_COUNT_TAG_CANDIDATES);
|
||||
|
||||
if (archiveCode == null || archiveCode.isEmpty()) {
|
||||
log.debug("记录缺少档号字段");
|
||||
log.debug(LoggerHelper.RELEASE_MARKER, "记录缺少档号字段");
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -114,11 +115,11 @@ public class XmlFileReader implements CatalogFileReader {
|
|||
if (page > 0) {
|
||||
return new LogicalAddressFileGenerator.Record(archiveCode, page);
|
||||
} else {
|
||||
log.debug("无效的页数值: {}", pageStr);
|
||||
log.debug(LoggerHelper.RELEASE_MARKER, "无效的页数值: {}", pageStr);
|
||||
return null;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("页数字段格式错误: {}", pageStr);
|
||||
log.warn(LoggerHelper.RELEASE_MARKER, "页数字段格式错误: {}", pageStr);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,23 @@ public interface FileScanner {
|
|||
default void scanWithProgress(Path rootPath, ProgressAwareListener listener) {
|
||||
throw new UnsupportedOperationException("Please implement FileScanner, ProgressAwareListener.");
|
||||
}
|
||||
/**
|
||||
* 扫描指定路径下的文件 (带超时)
|
||||
*
|
||||
* @param rootPath 根路径
|
||||
* @param listener 文件发现监听器
|
||||
* @param timeout 超时(s)
|
||||
*/
|
||||
default void scan(Path rootPath, FileScanListener listener, long timeout) {
|
||||
throw new UnsupportedOperationException("Please implement FileScanner, FileScannerListener.");
|
||||
}
|
||||
/**
|
||||
* 扫描指定路径下的文件(带进度反馈)(带超时)
|
||||
* @param timeout 超时(s)
|
||||
*/
|
||||
default void scanWithProgress(Path rootPath, ProgressAwareListener listener, long timeout) {
|
||||
throw new UnsupportedOperationException("Please implement FileScanner, ProgressAwareListener.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件扫描监听器
|
||||
|
|
|
|||
|
|
@ -1,135 +0,0 @@
|
|||
package top.r3944realms.docchecktoolrefactored.io.scanner;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* The type Parallel file scanner.
|
||||
* <p>
|
||||
* 这个没法正常使用,目前遇到的问题
|
||||
* <p>
|
||||
* * 目录遍历时遇到权限问题(静默失败)
|
||||
* <p>
|
||||
* * 存在符号链接循环
|
||||
* <p>
|
||||
* * 文件系统驱动程序卡死
|
||||
* <p>
|
||||
* * JVM与NTFS文件系统兼容性问题
|
||||
*/
|
||||
@Slf4j
|
||||
@Deprecated
|
||||
public class ParallelFileScanner implements FileScanner ,AutoCloseable {
|
||||
private final ForkJoinPool forkJoinPool;
|
||||
private volatile boolean cancelled = false;
|
||||
/**
|
||||
* 使用默认并行度(CPU核心数)
|
||||
*/
|
||||
public ParallelFileScanner() {
|
||||
this(Runtime.getRuntime().availableProcessors());
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiates a new Parallel file scanner.
|
||||
*
|
||||
* @param parallelism 并行度(线程数)
|
||||
*/
|
||||
public ParallelFileScanner(int parallelism) {
|
||||
this.forkJoinPool = new ForkJoinPool(parallelism);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scan(Path rootPath, FileScanListener listener) {
|
||||
scanInternal(rootPath, listener, null);
|
||||
}
|
||||
@Override
|
||||
public void scanWithProgress(Path rootPath, ProgressAwareListener listener) {
|
||||
// 先快速统计总文件数
|
||||
long totalFiles = countFiles(rootPath);
|
||||
scanInternal(rootPath, listener, totalFiles);
|
||||
}
|
||||
|
||||
private long countFiles(Path rootPath) {
|
||||
try(Stream<Path> pathStream = Files.walk(rootPath)
|
||||
.parallel()
|
||||
.filter(Files::isRegularFile)) {
|
||||
return pathStream.count();
|
||||
} catch (IOException e) {
|
||||
return -1; // 表示无法确定总数
|
||||
}
|
||||
}
|
||||
private void scanInternal(Path rootPath, FileScanListener listener, Long totalFiles) {
|
||||
log.debug("ThreadPool Status: {}", forkJoinPool.isShutdown() ? "Closed" : "Running");
|
||||
forkJoinPool.submit(() -> { // 方法没问题,可能就是在线程这里被卡死了
|
||||
try {
|
||||
AtomicInteger processed = new AtomicInteger(0);
|
||||
log.debug("Scanning files in {}", rootPath);
|
||||
// 收集所有文件到List(避免Stream被重复使用)
|
||||
@SuppressWarnings("resource") List<Path> files = Files.walk(rootPath)
|
||||
.peek(p -> log.trace("visiting: {}", p))
|
||||
.parallel()
|
||||
.filter(p -> {
|
||||
boolean isRegular = Files.isRegularFile(p);
|
||||
if (!isRegular) {
|
||||
log.debug("Skip non-regular : {} ", p);
|
||||
}
|
||||
return isRegular;
|
||||
})
|
||||
.peek(p -> log.trace("Found file: {}", p))
|
||||
.toList(); // 立即消费Stream
|
||||
if (files.isEmpty()) {
|
||||
log.warn("No files found in directory: {}", rootPath);
|
||||
} else log.debug("Found {} files in {}", files.size(), rootPath);
|
||||
files.forEach(file -> {
|
||||
if (cancelled) {
|
||||
log.debug("Cancelled scanning file {}", file);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log.debug("Handle file {}", file);
|
||||
listener.onFileFound(file);
|
||||
|
||||
// 进度更新
|
||||
if (listener instanceof ProgressAwareListener progressListener) {
|
||||
int current = processed.incrementAndGet();
|
||||
progressListener.onProgressUpdate(
|
||||
current,
|
||||
totalFiles != null ? totalFiles.intValue() : -1
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Error Handle file {}", file, e);
|
||||
listener.onError(file, e);
|
||||
}
|
||||
});
|
||||
if (!cancelled) {
|
||||
log.debug("Finished scanning files in {}", rootPath);
|
||||
listener.onScanComplete();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
listener.onError(rootPath, e);
|
||||
} catch (Exception e) {
|
||||
log.error("Unexpected error in scan thread", e);
|
||||
listener.onError(rootPath, e);
|
||||
}
|
||||
});
|
||||
log.debug("Task submitted to thread pool");
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
cancelled = true;
|
||||
forkJoinPool.shutdownNow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package top.r3944realms.docchecktoolrefactored.io.scanner;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
|
@ -26,17 +27,24 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable {
|
|||
this.forkJoinPool = new ForkJoinPool(parallelism);
|
||||
this.maxDepth = maxDepth; // 防止无限递归
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scan(Path rootPath, FileScanListener listener) {
|
||||
scanInternal(rootPath, listener, null);
|
||||
public void scan(Path rootPath, FileScanListener listener, long timeout) {
|
||||
scanInternal(rootPath, listener, null, timeout);
|
||||
}
|
||||
@Override
|
||||
public void scanWithProgress(Path rootPath, ProgressAwareListener listener) {
|
||||
public void scan(Path rootPath, FileScanListener listener) {
|
||||
scan(rootPath, listener, 30);
|
||||
}
|
||||
@Override
|
||||
public void scanWithProgress(Path rootPath, ProgressAwareListener listener, long timeout) {
|
||||
// 预扫描阶段:计算总文件数
|
||||
AtomicLong totalFiles = new AtomicLong(0);
|
||||
countFiles(rootPath, totalFiles);
|
||||
scanInternal(rootPath, listener, totalFiles);
|
||||
scanInternal(rootPath, listener, totalFiles, timeout);
|
||||
}
|
||||
@Override
|
||||
public void scanWithProgress(Path rootPath, ProgressAwareListener listener) {
|
||||
scanWithProgress(rootPath, listener, 30);
|
||||
}
|
||||
private void countFiles(Path dir, AtomicLong counter) {
|
||||
if (cancelled) return;
|
||||
|
|
@ -52,10 +60,13 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable {
|
|||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("Failed to pre-scan: {}", dir, e);
|
||||
log.warn(LoggerHelper.TRACE_MARKER, "Failed to pre-scan: {}", dir, e);
|
||||
}
|
||||
}
|
||||
private void scanInternal(Path rootPath, FileScanListener listener, AtomicLong totalFiles) {
|
||||
scanInternal(rootPath, listener, totalFiles, 30);
|
||||
}
|
||||
private void scanInternal(Path rootPath, FileScanListener listener, AtomicLong totalFiles, long timeout) {
|
||||
try {
|
||||
validateDirectory(rootPath);
|
||||
|
||||
|
|
@ -70,9 +81,9 @@ public class RobustParallelScanner implements FileScanner, AutoCloseable {
|
|||
} catch (Exception e) {
|
||||
listener.onError(rootPath, e);
|
||||
}
|
||||
}).get(30, TimeUnit.SECONDS);
|
||||
}).get(timeout, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
log.error("Scan timeout: {}", rootPath, e);
|
||||
log.error(LoggerHelper.TRACE_MARKER, "Scan timeout: {}", rootPath, e);
|
||||
forkJoinPool.shutdownNow();
|
||||
listener.onError(rootPath, new TimeoutException("扫描超时30秒"));
|
||||
} catch (Exception e) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import javafx.scene.input.KeyEvent;
|
|||
import javafx.scene.layout.BorderPane;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
|
@ -38,10 +39,10 @@ public class LoginStageController implements Initializable {
|
|||
String password = passwordField.getText();
|
||||
|
||||
if ("admin".equals(username) && "admin".equals(password)) {
|
||||
log.info("{} Login successful", username);
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "{} Login successful", username);
|
||||
SceneManager.switchMainView();
|
||||
} else {
|
||||
log.info("Invalid username or password");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "Invalid username or password");
|
||||
DialogUtil.showErrorDialog("错误", null, "用户名或密码错误!");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,25 @@
|
|||
package top.r3944realms.docchecktoolrefactored.ui;
|
||||
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import top.r3944realms.docchecktoolrefactored.System;
|
||||
import top.r3944realms.docchecktoolrefactored.core.Setting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The type Main stage controller.
|
||||
*/
|
||||
public class MainStageController {
|
||||
|
||||
@FXML public Button nextB;
|
||||
@FXML public Button prevB;
|
||||
@FXML private Tab step1T;
|
||||
@FXML private Tab step2T;
|
||||
@FXML private Tab step3T;
|
||||
|
|
@ -16,11 +27,110 @@ public class MainStageController {
|
|||
@FXML private Tab step5T;
|
||||
@FXML private Tab step6T;
|
||||
@FXML private Tab step7T;
|
||||
|
||||
@FXML private TabPane tabPane;
|
||||
@FXML private Menu helpM;
|
||||
@FXML private MenuItem aboutSoftwareMI;
|
||||
@FXML private MenuItem helpDocMI;
|
||||
@FXML private MenuItem exitMI;
|
||||
@FXML private MenuItem logoutMI;
|
||||
@FXML private MenuItem settingMI;
|
||||
|
||||
private List<Tab> tabs;
|
||||
private int currentIndex = 0;
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
// 初始化所有Tab的集合
|
||||
tabs = new ArrayList<>();
|
||||
tabs.add(step1T);
|
||||
tabs.add(step2T);
|
||||
tabs.add(step3T);
|
||||
tabs.add(step4T);
|
||||
tabs.add(step5T);
|
||||
tabs.add(step6T);
|
||||
tabs.add(step7T);
|
||||
|
||||
// 初始状态设置
|
||||
updateButtonStates();
|
||||
updateStepButtonsVisibility(); // 根据配置初始可见性
|
||||
// 监听Tab切换事件
|
||||
tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> {
|
||||
currentIndex = tabs.indexOf(newTab);
|
||||
updateButtonStates();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新按钮状态
|
||||
*/
|
||||
private void updateButtonStates() {
|
||||
prevB.setDisable(currentIndex == 0);
|
||||
nextB.setDisable(currentIndex == tabs.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到下一个Tab
|
||||
* @param actionEvent 事件
|
||||
*/
|
||||
@FXML
|
||||
void onNext(ActionEvent actionEvent) {
|
||||
if (currentIndex < tabs.size() - 1) {
|
||||
tabPane.getSelectionModel().select(tabs.get(currentIndex + 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到上一个Tab
|
||||
* @param actionEvent 事件
|
||||
*/
|
||||
@FXML
|
||||
void onPrev(ActionEvent actionEvent) {
|
||||
if (currentIndex > 0) {
|
||||
tabPane.getSelectionModel().select(tabs.get(currentIndex - 1));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理键盘快捷键
|
||||
* @param keyEvent 键盘事件
|
||||
*/
|
||||
@FXML
|
||||
void handleKeyPressed(KeyEvent keyEvent) {
|
||||
// Ctrl+> 或 Ctrl+. 切换到下一个Tab
|
||||
if (new KeyCodeCombination(KeyCode.PERIOD, KeyCombination.CONTROL_DOWN).match(keyEvent) ||
|
||||
new KeyCodeCombination(KeyCode.GREATER, KeyCombination.CONTROL_DOWN).match(keyEvent)) {
|
||||
onNext(null);
|
||||
keyEvent.consume();
|
||||
}
|
||||
// Ctrl+< 或 Ctrl+, 切换到上一个Tab
|
||||
else if (new KeyCodeCombination(KeyCode.COMMA, KeyCombination.CONTROL_DOWN).match(keyEvent) ||
|
||||
new KeyCodeCombination(KeyCode.LESS, KeyCombination.CONTROL_DOWN).match(keyEvent)) {
|
||||
onPrev(null);
|
||||
keyEvent.consume();
|
||||
}
|
||||
}
|
||||
|
||||
@FXML void onLogout(ActionEvent actionEvent) {
|
||||
SceneManager.switchLoginView();
|
||||
}
|
||||
|
||||
@FXML void onExit(ActionEvent actionEvent) {
|
||||
|
||||
}
|
||||
|
||||
@FXML void onOpenSetting(ActionEvent actionEvent) {
|
||||
SceneManager.openSettingView();
|
||||
}
|
||||
|
||||
@FXML void onOpenHelpDoc(ActionEvent actionEvent) {
|
||||
}
|
||||
|
||||
@FXML void onAbout(ActionEvent actionEvent) {
|
||||
}
|
||||
public void updateStepButtonsVisibility() {
|
||||
Setting setting = System.getSetting();
|
||||
boolean visible = setting.isEnableStep(); // 由enableStep控制
|
||||
nextB.setVisible(visible);
|
||||
prevB.setVisible(visible);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import javafx.fxml.FXMLLoader;
|
|||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import lombok.Getter;
|
||||
|
|
@ -13,6 +14,7 @@ import lombok.Setter;
|
|||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.Main;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -29,7 +31,8 @@ public class SceneManager {
|
|||
@Getter
|
||||
@Setter
|
||||
private static Stage primaryStage;
|
||||
|
||||
@Getter
|
||||
private static MainStageController mainController;
|
||||
@Getter
|
||||
private static final List<Stage> openStages = new ArrayList<>();
|
||||
|
||||
|
|
@ -46,14 +49,59 @@ public class SceneManager {
|
|||
* Switch login view.
|
||||
*/
|
||||
public static void switchLoginView() {
|
||||
SceneManager.loadView("/fxml/login-view.fxml","淮阴区数字化档案检查验收系统 - 登录", 400, 300);
|
||||
SceneManager.loadView("/fxml/login-view.fxml","数字化验收工具 - 登录", 400, 300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch main view.
|
||||
*/
|
||||
public static void switchMainView() {
|
||||
SceneManager.loadView("/fxml/main-view.fxml", "淮阴区数字化档案检查验收系统 - 主界面", 1200, 900);
|
||||
try {
|
||||
FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(Main.class.getResource("/fxml/main-view.fxml")));
|
||||
Parent root = loader.load();
|
||||
mainController = loader.getController(); // 保存控制器引用
|
||||
Scene newScene = new Scene(root, 1200, 900);
|
||||
|
||||
applyFadeTransition(root);
|
||||
|
||||
primaryStage.setScene(newScene);
|
||||
primaryStage.centerOnScreen();
|
||||
primaryStage.setTitle("数字化验收工具 - 主界面");
|
||||
|
||||
// 设置窗口关闭确认
|
||||
primaryStage.setOnCloseRequest(event -> {
|
||||
if (!DialogUtil.showExitConfirmation(primaryStage)) {
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error(LoggerHelper.TRACE_MARKER, "Failed to load main view", e);
|
||||
DialogUtil.showErrorDialog("错误", "加载主界面失败", e.getMessage());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 打开设置窗口
|
||||
*/
|
||||
public static void openSettingView() {
|
||||
try {
|
||||
Parent root = FXMLLoader.load(Objects.requireNonNull(Main.class.getResource("/fxml/setting-view.fxml")));
|
||||
Stage settingStage = new Stage();
|
||||
settingStage.setTitle("数字化验收工具 - 设置");
|
||||
Scene scene = new Scene(root, 300, 206);
|
||||
settingStage.setScene(scene); // 默认大小可调
|
||||
settingStage.initOwner(primaryStage); // 设置父窗口
|
||||
settingStage.initModality(Modality.WINDOW_MODAL);
|
||||
settingStage.setResizable(false);
|
||||
settingStage.centerOnScreen();
|
||||
|
||||
settingStage.show();
|
||||
openStages.add(settingStage);
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error(LoggerHelper.TRACE_MARKER, "Failed to open setting view: {}", e.getMessage(), e);
|
||||
DialogUtil.showErrorDialog("错误", "加载设置窗口失败", "无法加载设置窗口: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -100,9 +148,8 @@ public class SceneManager {
|
|||
event.consume();
|
||||
}
|
||||
});
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to load view: {}", fxmlPath, e);
|
||||
log.error(LoggerHelper.TRACE_MARKER, "Failed to load view: {}", fxmlPath, e);
|
||||
DialogUtil.showErrorDialog("错误", "加载视图失败", "无法加载视图: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,126 @@
|
|||
package top.r3944realms.docchecktoolrefactored.ui;
|
||||
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.input.MouseDragEvent;
|
||||
import javafx.stage.Stage;
|
||||
import top.r3944realms.docchecktoolrefactored.System;
|
||||
import top.r3944realms.docchecktoolrefactored.core.Setting;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.extend.LongSpinnerValueFactory;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class SettingDialogController implements Initializable {
|
||||
|
||||
@FXML private CheckBox enableStepCB;
|
||||
@FXML private Button resetB, saveB, cancelB;
|
||||
@FXML private Spinner<Long> scanTotalTimeOutS, scanSingleTimeOutS;
|
||||
|
||||
private Setting setting;
|
||||
|
||||
// Spinner 范围常量
|
||||
private static final int SINGLE_MIN = 1;
|
||||
private static final int SINGLE_MAX = 3600;
|
||||
private static final int TOTAL_MIN = 1;
|
||||
private static final int TOTAL_MAX = 86400;
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle resourceBundle) {
|
||||
// 获取当前配置
|
||||
setting = System.getSetting();
|
||||
|
||||
// 初始化 Spinner
|
||||
scanSingleTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600, setting.getSingleTimeout()));
|
||||
scanSingleTimeOutS.setEditable(true);
|
||||
scanTotalTimeOutS.setValueFactory(new LongSpinnerValueFactory(1, 3600 * 24, setting.getTotalTimeout()));
|
||||
scanTotalTimeOutS.setEditable(true);
|
||||
|
||||
// 添加焦点离开时校验
|
||||
addSpinnerValidation(scanSingleTimeOutS, SINGLE_MIN, SINGLE_MAX);
|
||||
addSpinnerValidation(scanTotalTimeOutS, TOTAL_MIN, TOTAL_MAX);
|
||||
enableStepCB.setSelected(setting.isEnableStep());
|
||||
}
|
||||
|
||||
/** 保存修改 */
|
||||
@FXML
|
||||
void onSave(ActionEvent actionEvent) {
|
||||
// 更新配置对象
|
||||
setting.setSingleTimeout(scanSingleTimeOutS.getValue());
|
||||
setting.setTotalTimeout(scanTotalTimeOutS.getValue());
|
||||
setting.setEnableStep(enableStepCB.isSelected());
|
||||
// 保存到配置文件
|
||||
System.saveSettingsNow();
|
||||
// 通知主界面刷新按钮状态
|
||||
MainStageController mainController = SceneManager.getMainController();
|
||||
if(mainController != null){
|
||||
mainController.updateStepButtonsVisibility();
|
||||
}
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
/** 重置为默认值 */
|
||||
@FXML
|
||||
void onReset(ActionEvent actionEvent) {
|
||||
scanSingleTimeOutS.getValueFactory().setValue(30L); // 默认单次超时
|
||||
scanTotalTimeOutS.getValueFactory().setValue(300L); // 默认总超时
|
||||
enableStepCB.setSelected(false);
|
||||
}
|
||||
|
||||
/** 取消修改 */
|
||||
@FXML
|
||||
void onCancel(ActionEvent actionEvent) {
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
@FXML
|
||||
void onCheckOne(MouseDragEvent mouseDragEvent) {
|
||||
validateSpinnerValue(scanSingleTimeOutS, 30, 3600);
|
||||
}
|
||||
|
||||
@FXML
|
||||
void onCheckTwo(MouseDragEvent mouseDragEvent) {
|
||||
validateSpinnerValue(scanTotalTimeOutS, 60, 3600 * 24);
|
||||
}
|
||||
@FXML
|
||||
void onSettingThree(ActionEvent actionEvent) {
|
||||
|
||||
}
|
||||
|
||||
/** 给 Spinner 添加离开焦点校验 */
|
||||
private void addSpinnerValidation(Spinner<Long> spinner, long min, long max) {
|
||||
spinner.getEditor().focusedProperty().addListener((obs, oldVal, newVal) -> {
|
||||
if (!newVal) { // 焦点离开时触发
|
||||
validateSpinnerValue(spinner, min, max);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 校验 Spinner 的值是否在范围内,如果超出则纠正并提示 */
|
||||
private void validateSpinnerValue(Spinner<Long> spinner, long min, long max) {
|
||||
try {
|
||||
long value = Integer.parseInt(spinner.getEditor().getText());
|
||||
if (value < min || value > max) {
|
||||
// 超出范围则纠正
|
||||
long corrected = Math.max(min, Math.min(value, max));
|
||||
spinner.getValueFactory().setValue(corrected);
|
||||
|
||||
// 弹出提示
|
||||
DialogUtil.showWarningDialog("输入警告", "输入的值超出范围,已自动调整为 " + corrected);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// 非数字输入恢复原来的值
|
||||
spinner.getEditor().setText(spinner.getValue().toString());
|
||||
DialogUtil.showWarningDialog("输入警告","输入非法,已恢复为原值 " + spinner.getValue());
|
||||
}
|
||||
}
|
||||
/** 关闭对话框 */
|
||||
private void closeDialog() {
|
||||
Stage stage = (Stage) cancelB.getScene().getWindow();
|
||||
stage.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package top.r3944realms.docchecktoolrefactored.ui.extend;
|
||||
|
||||
import javafx.scene.control.SpinnerValueFactory;
|
||||
import lombok.Setter;
|
||||
|
||||
@Setter
|
||||
public class LongSpinnerValueFactory extends SpinnerValueFactory<Long> {
|
||||
|
||||
public LongSpinnerValueFactory(long min, long max, long initialValue) {
|
||||
setMin(min);
|
||||
setMax(max);
|
||||
setValue(initialValue);
|
||||
|
||||
setConverter(new javafx.util.StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Long object) {
|
||||
return object.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long fromString(String string) {
|
||||
try {
|
||||
return Long.parseLong(string);
|
||||
} catch (NumberFormatException e) {
|
||||
return getValue();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private long min;
|
||||
private long max;
|
||||
|
||||
@Override
|
||||
public void decrement(int steps) {
|
||||
long newValue = getValue() - steps;
|
||||
if (newValue < min) newValue = min;
|
||||
setValue(newValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void increment(int steps) {
|
||||
long newValue = getValue() + steps;
|
||||
if (newValue > max) newValue = max;
|
||||
setValue(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package top.r3944realms.docchecktoolrefactored.ui.module;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
|
|
@ -8,11 +9,11 @@ import javafx.scene.control.TextField;
|
|||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.core.DuplicateFinder;
|
||||
import top.r3944realms.docchecktoolrefactored.core.FileHashCalculator;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.System;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.SceneManager;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.task.DuplicateDocumentDetectionTask;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.utils.ProgressBarUtil;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
|
|
@ -21,22 +22,26 @@ import java.io.File;
|
|||
*/
|
||||
@Slf4j
|
||||
public class DuplicateDocumentPaneController {
|
||||
|
||||
@FXML private TextArea result1B;
|
||||
@FXML private TextField loadFolder1TF;
|
||||
@FXML private Button selectLoadFolder1B;
|
||||
@FXML private Button start1B;
|
||||
|
||||
@FXML private Button cancel1B;
|
||||
private final ProgressBarUtil progressBarUtil = new ProgressBarUtil();
|
||||
private DuplicateDocumentDetectionTask currentTask; // 保存任务引用
|
||||
/**
|
||||
* On select folder.
|
||||
*
|
||||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onSelectFolder(ActionEvent actionEvent) {
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
DirectoryChooser directoryChooser = System.getDirectoryChooser();
|
||||
directoryChooser.setTitle("选择要检查的文件夹");
|
||||
File selectedFolder = directoryChooser.showDialog(new Stage());
|
||||
if (selectedFolder != null) {
|
||||
loadFolder1TF.setText(selectedFolder.getAbsolutePath());
|
||||
System.setLastModifiedFile(selectedFolder);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,39 +51,84 @@ public class DuplicateDocumentPaneController {
|
|||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onStart(ActionEvent actionEvent) {
|
||||
log.info("用户点击了开始查重按钮");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了开始查重按钮");
|
||||
String folderPath = loadFolder1TF.getText();
|
||||
if (folderPath == null || folderPath.trim().isEmpty()) {
|
||||
log.warn("未选择文件夹,无法进行查重");
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "未选择文件夹,无法进行查重");
|
||||
result1B.setText("请选择要检查的文件夹。");
|
||||
return;
|
||||
}
|
||||
// 禁用开始按钮避免重复点击
|
||||
start1B.setDisable(true);
|
||||
cancel1B.setDisable(false);
|
||||
// 显示进度条窗口
|
||||
progressBarUtil.showProgress(SceneManager.getPrimaryStage(), "重复文件检测", "正在初始化扫描...");
|
||||
|
||||
// 创建并启动后台任务
|
||||
DuplicateDocumentDetectionTask task = new DuplicateDocumentDetectionTask(folderPath);
|
||||
// 保存到字段
|
||||
currentTask = task;
|
||||
// 绑定任务属性到UI
|
||||
ChangeListener<Number> progressChangeListener = (obs, oldVal, newVal) -> {
|
||||
if (newVal != null) {
|
||||
progressBarUtil.updateProgress(newVal.doubleValue(),
|
||||
task.getMessage() != null ? task.getMessage() : "处理中...");
|
||||
}
|
||||
};
|
||||
task.progressProperty().addListener(progressChangeListener);
|
||||
|
||||
// 绑定任务的消息到结果文本区域
|
||||
task.messageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
ChangeListener<String> messageChangeListener = (observable, oldValue, newValue) -> {
|
||||
result1B.setText(newValue);
|
||||
});
|
||||
};
|
||||
task.messageProperty().addListener(messageChangeListener);
|
||||
|
||||
// 当任务完成时显示完整结果
|
||||
task.setOnSucceeded(e -> {
|
||||
progressBarUtil.closeProgress();
|
||||
result1B.setText(task.getValue());
|
||||
log.info("查重任务完成,结果如下:{}", task.getValue());
|
||||
start1B.setDisable(false);
|
||||
cancel1B.setDisable(true);
|
||||
log.info(LoggerHelper.RELEASE_MARKER, "查重任务完成,结果如下:{}", task.getValue());
|
||||
});
|
||||
|
||||
// 处理任务失败情况
|
||||
task.setOnFailed(e -> {
|
||||
progressBarUtil.closeProgress();
|
||||
Throwable exception = task.getException();
|
||||
result1B.setText("检测过程中发生错误: " + exception.getMessage());
|
||||
log.error("error", exception);
|
||||
log.info("查重任务失败,错误信息: {}", exception.getMessage());
|
||||
start1B.setDisable(false);
|
||||
cancel1B.setDisable(true);
|
||||
log.error(LoggerHelper.RELEASE_MARKER, "查重任务失败", exception);
|
||||
});
|
||||
|
||||
// 处理任务取消情况
|
||||
task.setOnCancelled(e -> {
|
||||
progressBarUtil.closeProgress();
|
||||
result1B.appendText("\n检测已取消");
|
||||
start1B.setDisable(false);
|
||||
cancel1B.setDisable(true);
|
||||
currentTask.progressProperty().removeListener(progressChangeListener);
|
||||
currentTask.messageProperty().removeListener(messageChangeListener);
|
||||
log.info(LoggerHelper.RELEASE_MARKER, "查重任务已被取消");
|
||||
});
|
||||
// 绑定取消按钮 -> task.cancel()
|
||||
progressBarUtil.setOnCancel(() -> {
|
||||
if (currentTask != null && currentTask.isRunning()) {
|
||||
currentTask.cancel();
|
||||
}
|
||||
});
|
||||
// 在新线程中执行任务
|
||||
Thread thread = new Thread(task);
|
||||
thread.setDaemon(true);
|
||||
thread.start();
|
||||
}
|
||||
|
||||
public void onCancel(ActionEvent actionEvent) {
|
||||
if (currentTask != null && currentTask.isRunning()) {
|
||||
currentTask.cancel(); // 触发 setOnCancelled
|
||||
} else {
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "没有正在运行的任务可取消");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,12 @@ import javafx.scene.control.TextArea;
|
|||
import javafx.scene.control.TextField;
|
||||
import javafx.stage.FileChooser;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.System;
|
||||
import top.r3944realms.docchecktoolrefactored.core.AddressFileComparator;
|
||||
import top.r3944realms.docchecktoolrefactored.core.AddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.core.LogicalAddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.core.PhysicalAddressFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
|
|
@ -49,8 +51,8 @@ public class PathCheckPaneController implements Initializable {
|
|||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onSelectLC(ActionEvent actionEvent) {
|
||||
log.info("用户点击了选择目录文件按钮");
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了选择目录文件按钮");
|
||||
FileChooser fileChooser = System.getFileChooser();
|
||||
fileChooser.setTitle("选择目录文件");
|
||||
|
||||
// 设置文件过滤器,只允许DBF、XML、xlsx、xls格式
|
||||
|
|
@ -60,7 +62,7 @@ public class PathCheckPaneController implements Initializable {
|
|||
FileChooser.ExtensionFilter dbfFilter = new FileChooser.ExtensionFilter("DBF Files (*.dbf)", "*.dbf");
|
||||
FileChooser.ExtensionFilter xmlFilter = new FileChooser.ExtensionFilter("XML Files (*.xml)", "*.xml");
|
||||
|
||||
fileChooser.getExtensionFilters().addAll( xlsxFilter, xlsFilter,dbfFilter, xmlFilter);
|
||||
fileChooser.getExtensionFilters().addAll(xlsxFilter, xlsFilter,dbfFilter, xmlFilter);
|
||||
|
||||
// 显示文件选择对话框
|
||||
File selectedFile = fileChooser.showOpenDialog(selectLoadCatalog2B.getScene().getWindow());
|
||||
|
|
@ -68,9 +70,10 @@ public class PathCheckPaneController implements Initializable {
|
|||
// 如果选择了文件,则将文件路径显示在loadCatalog2TF上
|
||||
if (selectedFile != null) {
|
||||
loadCatalog2TF.setText(selectedFile.getAbsolutePath());
|
||||
log.info("选择的目录文件路径为:{}", selectedFile.getAbsolutePath());
|
||||
System.setLastModifiedFile(selectedFile);
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "选择的目录文件路径为:{}", selectedFile.getAbsolutePath());
|
||||
}else{
|
||||
log.warn("用户未选择任何文件夹");
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "用户未选择任何文件夹");
|
||||
result2TA.setText("未选择任何文件夹,请重新选择。");
|
||||
}
|
||||
}
|
||||
|
|
@ -81,7 +84,7 @@ public class PathCheckPaneController implements Initializable {
|
|||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onSelectJPGF(ActionEvent actionEvent) {
|
||||
javafx.stage.DirectoryChooser directoryChooser = new javafx.stage.DirectoryChooser();
|
||||
javafx.stage.DirectoryChooser directoryChooser = System.getDirectoryChooser();
|
||||
// 正确获取当前选中的值
|
||||
Mode selectedMode = loadFolderType2CB.getValue();
|
||||
if (selectedMode == Mode.PAGE_TYPE) {
|
||||
|
|
@ -89,14 +92,15 @@ public class PathCheckPaneController implements Initializable {
|
|||
} else if (selectedMode == Mode.FILE_TYPE) {
|
||||
directoryChooser.setTitle("选择文件级文件夹");
|
||||
}
|
||||
log.info("用户选择的模式为:{}", selectedMode);
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户选择的模式为:{}", selectedMode);
|
||||
|
||||
File selectedDirectory = directoryChooser.showDialog(selectJPGFolder2B.getScene().getWindow());
|
||||
|
||||
if (selectedDirectory != null) {
|
||||
loadJPGFolder2TF.setText(selectedDirectory.getAbsolutePath());
|
||||
log.info("选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath());
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "选择的{}文件夹路径为:{}", selectedMode,selectedDirectory.getAbsolutePath());
|
||||
}
|
||||
System.setLastModifiedFile(selectedDirectory);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -105,14 +109,14 @@ public class PathCheckPaneController implements Initializable {
|
|||
* @param actionEvent the action event
|
||||
*/
|
||||
@FXML void onGenerateLA(ActionEvent actionEvent) {
|
||||
log.info("用户点击了生成逻辑地址文件按钮");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了生成逻辑地址文件按钮");
|
||||
String filePath = loadCatalog2TF.getText();
|
||||
if (filePath.isEmpty()) {
|
||||
result2TA.setText("请先选择目录文件。");
|
||||
return;
|
||||
}
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
FileChooser fileChooser = System.getFileChooser();
|
||||
fileChooser.setTitle("选择保存逻辑地址文件的位置");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
|
||||
fileChooser.setInitialFileName("逻辑地址文件.csv");
|
||||
|
|
@ -120,10 +124,10 @@ public class PathCheckPaneController implements Initializable {
|
|||
File outputFile = fileChooser.showSaveDialog(generateLogicalAddress2B.getScene().getWindow());
|
||||
if (outputFile == null) {
|
||||
result2TA.setText("未选择保存位置");
|
||||
log.warn("用户未选择任何文件");
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "用户未选择任何文件");
|
||||
return;
|
||||
}
|
||||
|
||||
System.setLastModifiedFile(outputFile);
|
||||
// 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
|
||||
final File finalOutputFile;
|
||||
if (!outputFile.getName().toLowerCase().endsWith(".csv")) {
|
||||
|
|
@ -134,7 +138,7 @@ public class PathCheckPaneController implements Initializable {
|
|||
|
||||
// 保存生成的文件路径
|
||||
logicalAddressFilePath = finalOutputFile.getAbsolutePath();
|
||||
log.info("选择的输出文件路径: {}", logicalAddressFilePath);
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "选择的输出文件路径: {}", logicalAddressFilePath);
|
||||
// 创建后台任务来处理文件生成
|
||||
Thread backgroundThread = new Thread(() -> {
|
||||
// 获取当前选择的文件夹类型
|
||||
|
|
@ -180,7 +184,7 @@ public class PathCheckPaneController implements Initializable {
|
|||
return;
|
||||
}
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
FileChooser fileChooser = System.getFileChooser();
|
||||
fileChooser.setTitle("选择保存物理地址文件的位置");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
|
||||
fileChooser.setInitialFileName("物理地址文件.csv");
|
||||
|
|
@ -191,6 +195,7 @@ public class PathCheckPaneController implements Initializable {
|
|||
result2TA.setText("未选择保存位置");
|
||||
return;
|
||||
}
|
||||
System.setLastModifiedFile(outputFile);
|
||||
|
||||
// 正确处理文件扩展名 - 只有在没有.csv扩展名时才添加
|
||||
final File finalOutputFile;
|
||||
|
|
@ -240,7 +245,7 @@ public class PathCheckPaneController implements Initializable {
|
|||
*/
|
||||
@FXML
|
||||
void onStart(ActionEvent actionEvent) {
|
||||
log.info("用户点击了开始对比按钮");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户点击了开始对比按钮");
|
||||
|
||||
// 检查是否已生成两个文件
|
||||
if (logicalAddressFilePath == null || physicalAddressFilePath == null) {
|
||||
|
|
@ -248,8 +253,8 @@ public class PathCheckPaneController implements Initializable {
|
|||
return;
|
||||
}
|
||||
|
||||
log.info("逻辑地址文件路径为:{}", logicalAddressFilePath);
|
||||
log.info("物理地址文件路径为:{}", physicalAddressFilePath);
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "逻辑地址文件路径为:{}", logicalAddressFilePath);
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "物理地址文件路径为:{}", physicalAddressFilePath);
|
||||
|
||||
// 使用新创建的核心类进行文件比较
|
||||
AddressFileComparator comparator = new AddressFileComparator();
|
||||
|
|
@ -323,13 +328,23 @@ public class PathCheckPaneController implements Initializable {
|
|||
*/
|
||||
enum Mode {
|
||||
/**
|
||||
* Jpg mode.
|
||||
* Jpg mode. 文件以JPG
|
||||
*/
|
||||
PAGE_TYPE("jpg"),
|
||||
PAGE_TYPE("jpg") {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "页面级";
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Pdf mode.
|
||||
* Pdf mode. 文件以PDF
|
||||
*/
|
||||
FILE_TYPE("pdf");
|
||||
FILE_TYPE("pdf") {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "文件级";
|
||||
}
|
||||
};
|
||||
/**
|
||||
* The Id.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@ package top.r3944realms.docchecktoolrefactored.ui.module;
|
|||
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.TextField;
|
||||
import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil;
|
||||
|
||||
/**
|
||||
* The type Project info pane controller.
|
||||
|
|
@ -15,12 +17,13 @@ public class ProjectInfoPaneController {
|
|||
@FXML private TextField fileYearTF;
|
||||
@FXML
|
||||
void onReset(ActionEvent event) {
|
||||
// 清空所有文本字段
|
||||
if (DialogUtil.showConfirmationDialog("确认","是否清除(该操作不可逆)", "")){// 清空所有文本字段
|
||||
projectNameTF.clear();
|
||||
fileYearTF.clear();
|
||||
fileCategoriesTF.clear();
|
||||
totalCatalogNumberTF.clear();
|
||||
AcceptanceTimeTF.clear();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// StorageCarrierPaneController.java
|
||||
package top.r3944realms.docchecktoolrefactored.ui.module;
|
||||
|
||||
import javafx.concurrent.Task;
|
||||
|
|
@ -11,8 +10,10 @@ import javafx.stage.DirectoryChooser;
|
|||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.System;
|
||||
import top.r3944realms.docchecktoolrefactored.core.HashFileGenerator;
|
||||
import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator;
|
||||
import top.r3944realms.docchecktoolrefactored.util.LoggerHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
|
@ -52,12 +53,13 @@ public class StorageCarrierPaneController {
|
|||
|
||||
@FXML
|
||||
void onSelectLD(ActionEvent event) {
|
||||
log.info("用户点击选择文件夹按钮");
|
||||
DirectoryChooser directoryChooser = new DirectoryChooser();
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户点击选择文件夹按钮");
|
||||
DirectoryChooser directoryChooser = System.getDirectoryChooser();
|
||||
directoryChooser.setTitle("选择要检查的文件夹(页面级文件夹和文件级文件夹等不包括目录文件夹)");
|
||||
|
||||
File selectedFolder = directoryChooser.showDialog(new Stage());
|
||||
if (selectedFolder != null) {
|
||||
System.setLastModifiedFile(selectedFolder);
|
||||
String currentText = loadDigitalOutcomes.getText();
|
||||
String folderPath = selectedFolder.getAbsolutePath();
|
||||
|
||||
|
|
@ -79,18 +81,18 @@ public class StorageCarrierPaneController {
|
|||
loadDigitalOutcomes.setText(currentText + File.pathSeparator + folderPath);
|
||||
}
|
||||
}
|
||||
log.info("用户选择了文件夹: {}", selectedFolder.getAbsolutePath());
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户选择了文件夹: {}", selectedFolder.getAbsolutePath());
|
||||
} else {
|
||||
log.info("用户取消了文件夹选择");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户取消了文件夹选择");
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
void onClearSelectedFolders(ActionEvent event) {
|
||||
log.info("用户点击清除已选择文件夹按钮");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户点击清除已选择文件夹按钮");
|
||||
loadDigitalOutcomes.setText("");
|
||||
result7TA.setText("已清除所有已选择的文件夹");
|
||||
log.info("已清除所有已选择的文件夹");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "已清除所有已选择的文件夹");
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -98,54 +100,56 @@ public class StorageCarrierPaneController {
|
|||
|
||||
@FXML
|
||||
void onSelectLC(ActionEvent event) {
|
||||
log.info("用户点击选择RAR文件按钮");
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户点击选择RAR文件按钮");
|
||||
FileChooser fileChooser = System.getFileChooser();
|
||||
fileChooser.setTitle("选择一个 .rar 文件");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("RAR Files", "*.rar"));
|
||||
File selectedFile = fileChooser.showOpenDialog(new Stage());
|
||||
if (selectedFile != null) {
|
||||
System.setLastModifiedFile(selectedFile);
|
||||
loadCompressedFile.setText(selectedFile.getAbsolutePath());
|
||||
log.info("用户选择了RAR文件: {}", selectedFile.getAbsolutePath());
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户选择了RAR文件: {}", selectedFile.getAbsolutePath());
|
||||
} else {
|
||||
log.info("用户取消了RAR文件选择");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户取消了RAR文件选择");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@FXML
|
||||
void onCaculateHash(ActionEvent event) {
|
||||
log.info("开始计算RAR文件的MD5哈希值");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "开始计算RAR文件的MD5哈希值");
|
||||
String filePath = loadCompressedFile.getText();
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
log.warn("未选择RAR文件,无法计算哈希值");
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "未选择RAR文件,无法计算哈希值");
|
||||
result7TA.setText("请先选择一个 .rar 文件");
|
||||
return;
|
||||
}
|
||||
|
||||
File file = new File(filePath);
|
||||
if (!file.exists() || !file.isFile() || !filePath.endsWith(".rar")) {
|
||||
log.warn("选择的文件无效或不是RAR文件: {}", filePath);
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "选择的文件无效或不是RAR文件: {}", filePath);
|
||||
result7TA.setText("所选文件不存在或不是一个有效的 .rar 文件");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("开始计算文件哈希值: {}", filePath);
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "开始计算文件哈希值: {}", filePath);
|
||||
MD5HashCalculator hashCalculator = new MD5HashCalculator();
|
||||
String hashResult = hashCalculator.calculateHash(file.toPath());
|
||||
result7TA.setText("计算结果:\n" + hashResult);
|
||||
log.info("文件哈希值计算完成: {}", hashResult);
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "文件哈希值计算完成: {}", hashResult);
|
||||
} catch (IOException e) {
|
||||
log.error("计算文件哈希值时出错: {}", filePath, e);
|
||||
log.error(LoggerHelper.DEBUG_MARKER, "计算文件哈希值时出错: {}", filePath, e);
|
||||
result7TA.setText("计算哈希值时出错: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
void onGenerateHF(ActionEvent event) {
|
||||
log.info("开始生成哈希列表文件");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "开始生成哈希列表文件");
|
||||
String folderPathsText = loadDigitalOutcomes.getText();
|
||||
if (folderPathsText == null || folderPathsText.isEmpty()) {
|
||||
log.warn("未选择文件夹,无法生成哈希列表文件");
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "未选择文件夹,无法生成哈希列表文件");
|
||||
result7TA.setText("请先选择一个文件夹");
|
||||
return;
|
||||
}
|
||||
|
|
@ -159,13 +163,13 @@ public class StorageCarrierPaneController {
|
|||
if (folder.exists() && folder.isDirectory()) {
|
||||
folders.add(folder);
|
||||
} else {
|
||||
log.warn("选择的路径无效或不是文件夹: {}", path);
|
||||
log.warn(LoggerHelper.DEBUG_MARKER, "选择的路径无效或不是文件夹: {}", path);
|
||||
result7TA.setText("所选路径不存在或不是一个有效的文件夹: " + path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
FileChooser fileChooser = System.getFileChooser();
|
||||
fileChooser.setTitle("选择保存哈希列表文件的位置");
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv"));
|
||||
|
||||
|
|
@ -176,7 +180,7 @@ public class StorageCarrierPaneController {
|
|||
File outputFile = fileChooser.showSaveDialog(selectLoadDigitalOutcomes7B.getScene().getWindow());
|
||||
|
||||
if (outputFile == null) {
|
||||
log.info("用户取消了文件保存操作");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "用户取消了文件保存操作");
|
||||
result7TA.setText("未选择保存位置");
|
||||
return;
|
||||
}
|
||||
|
|
@ -189,13 +193,13 @@ public class StorageCarrierPaneController {
|
|||
finalOutputFile = outputFile;
|
||||
}
|
||||
|
||||
log.info("选择的输出文件路径: {}", finalOutputFile.getAbsolutePath());
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "选择的输出文件路径: {}", finalOutputFile.getAbsolutePath());
|
||||
|
||||
// 创建后台任务
|
||||
Task<String> task = new Task<String>() {
|
||||
Task<String> task = new Task<>() {
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
log.info("开始执行哈希文件生成任务");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "开始执行哈希文件生成任务");
|
||||
updateMessage("开始生成哈希文件...");
|
||||
|
||||
HashFileGenerator generator = new HashFileGenerator();
|
||||
|
|
@ -209,11 +213,11 @@ public class StorageCarrierPaneController {
|
|||
updateProgress(current, total);
|
||||
updateMessage("处理文件: " + current + "/" + total);
|
||||
if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志
|
||||
log.info("处理进度: {}/{}", current, total);
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "处理进度: {}/{}", current, total);
|
||||
}
|
||||
});
|
||||
|
||||
log.info("哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath());
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务完成,输出文件: {}", finalOutputFile.getAbsolutePath());
|
||||
return "哈希列表文件已生成: " + finalOutputFile.getAbsolutePath();
|
||||
}
|
||||
};
|
||||
|
|
@ -225,7 +229,7 @@ public class StorageCarrierPaneController {
|
|||
|
||||
// 任务成功完成
|
||||
task.setOnSucceeded(e -> {
|
||||
log.info("哈希文件生成任务成功完成");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务成功完成");
|
||||
result7TA.setText(task.getValue());
|
||||
});
|
||||
|
||||
|
|
@ -233,13 +237,13 @@ public class StorageCarrierPaneController {
|
|||
task.setOnFailed(e -> {
|
||||
Throwable exception = task.getException();
|
||||
String errorMsg = "生成哈希文件时出错: " + (exception != null ? exception.getMessage() : "未知错误");
|
||||
log.error("哈希文件生成任务失败", exception);
|
||||
log.error(LoggerHelper.RELEASE_MARKER, "哈希文件生成任务失败", exception);
|
||||
result7TA.setText(errorMsg);
|
||||
});
|
||||
|
||||
// 任务取消处理
|
||||
task.setOnCancelled(e -> {
|
||||
log.info("哈希文件生成任务被用户取消");
|
||||
log.info(LoggerHelper.DEBUG_MARKER, "哈希文件生成任务被用户取消");
|
||||
result7TA.setText("哈希文件生成操作已取消");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -2,28 +2,23 @@ package top.r3944realms.docchecktoolrefactored.ui.task;
|
|||
|
||||
import javafx.concurrent.Task;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import top.r3944realms.docchecktoolrefactored.System;
|
||||
import top.r3944realms.docchecktoolrefactored.core.DuplicateFinder;
|
||||
import top.r3944realms.docchecktoolrefactored.core.MD5HashCalculator;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.FileScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.core.ScanningException;
|
||||
import top.r3944realms.docchecktoolrefactored.io.scanner.RobustParallelScanner;
|
||||
import top.r3944realms.docchecktoolrefactored.model.DuplicateGroup;
|
||||
|
||||
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.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
public class DuplicateDocumentDetectionTask extends Task<String> {
|
||||
public class DuplicateDocumentDetectionTask extends Task<String>{
|
||||
private final String folderPath;
|
||||
private final MD5HashCalculator hashCalculator;
|
||||
private volatile RobustParallelScanner scanner;
|
||||
|
|
@ -33,137 +28,166 @@ public class DuplicateDocumentDetectionTask extends Task<String> {
|
|||
this.hashCalculator = new MD5HashCalculator();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
updateMessage("正在初始化扫描...");
|
||||
|
||||
|
||||
Path rootPath = Paths.get(folderPath);
|
||||
if (!Files.exists(rootPath) || !Files.isDirectory(rootPath)) {
|
||||
throw new IllegalArgumentException("指定路径不是有效目录: " + folderPath);
|
||||
}
|
||||
|
||||
// 使用 RobustParallelScanner 和 MD5HashCalculator 进行并行扫描和哈希计算
|
||||
Map<String, List<Path>> hashToFileMap = new ConcurrentHashMap<>();
|
||||
AtomicInteger processed = new AtomicInteger(0);
|
||||
AtomicReference<Exception> errorRef = new AtomicReference<>(null);
|
||||
AtomicBoolean scanCompleted = new AtomicBoolean(false);
|
||||
|
||||
// 使用 CountDownLatch 等待扫描完成
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 创建扫描器
|
||||
// 创建带进度更新的扫描器
|
||||
scanner = new RobustParallelScanner(10);
|
||||
|
||||
// 异步启动扫描任务
|
||||
Thread scanThread = new Thread(() -> {
|
||||
try {
|
||||
scanner.scanWithProgress(rootPath, new FileScanner.ProgressAwareListener() {
|
||||
// 创建带有进度监听的 DuplicateFinder
|
||||
DuplicateFinder duplicateFinder = new DuplicateFinder(scanner, hashCalculator, true)
|
||||
.applySetting(System.getSetting());
|
||||
|
||||
// 用于统计文件总数
|
||||
AtomicInteger totalFiles = new AtomicInteger(0);
|
||||
// 使用 RobustParallelScanner 和 MD5HashCalculator 进行并行扫描和哈希计算
|
||||
// 设置进度回调
|
||||
duplicateFinder.setProgressCallback(new DuplicateFinder.ProgressCallback() {
|
||||
@Override
|
||||
public void onFileFound(Path file) {
|
||||
if (isCancelled()) {
|
||||
scanner.cancel();
|
||||
return;
|
||||
public void onPhaseStarted(DuplicateFinder.Phase phase) {
|
||||
switch (phase) {
|
||||
case GROUP_BY_SIZE:
|
||||
updateMessage("正在按文件大小分组...");
|
||||
break;
|
||||
case CALCULATE_HASH:
|
||||
updateMessage("正在计算可能重复文件的哈希值...");
|
||||
break;
|
||||
}
|
||||
try {
|
||||
String hash = hashCalculator.calculatePartialHash(file);
|
||||
hashToFileMap.computeIfAbsent(hash, k -> new ArrayList<>()).add(file);
|
||||
} catch (IOException e) {
|
||||
// 记录无法计算哈希的文件,但不中断整个过程
|
||||
updateMessage("警告: 无法处理文件 " + file.toString() + " - " + e.getMessage());
|
||||
}
|
||||
processed.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Path path, Exception e) {
|
||||
// 记录错误但不中断扫描过程
|
||||
errorRef.set(e);
|
||||
updateMessage("扫描错误: " + path.toString() + " - " + e.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScanComplete() {
|
||||
// 扫描完成
|
||||
scanCompleted.set(true);
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressUpdate(int current, int total) {
|
||||
if (isCancelled()) {
|
||||
scanner.cancel();
|
||||
return;
|
||||
}
|
||||
public void onPhaseProgress(DuplicateFinder.Phase phase, int current, int total) {
|
||||
if (total > 0) {
|
||||
updateProgress(current, total);
|
||||
updateMessage("正在处理文件: " + current + "/" + total);
|
||||
if (current % 500 == 0 || current == total) { // 每500个文件或完成时记录一次日志
|
||||
log.info("处理进度: {}/{}", current, total);
|
||||
switch (phase) {
|
||||
case GROUP_BY_SIZE:
|
||||
totalFiles.set(total);
|
||||
updateMessage(String.format("正在按文件大小分组: %d/%d", current, total));
|
||||
break;
|
||||
case CALCULATE_HASH:
|
||||
updateMessage(String.format("正在计算哈希值: %d/%d", current, total));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPhaseCompleted(DuplicateFinder.Phase phase) {
|
||||
switch (phase) {
|
||||
case GROUP_BY_SIZE:
|
||||
updateMessage("文件大小分组完成");
|
||||
break;
|
||||
case CALCULATE_HASH:
|
||||
updateMessage("哈希值计算完成");
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AtomicReference<List<DuplicateGroup>> resultRef = new AtomicReference<>();
|
||||
List<Exception> errors = new CopyOnWriteArrayList<>();
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
// 在单独线程中执行查找
|
||||
Thread findThread = new Thread(() -> {
|
||||
try {
|
||||
List<DuplicateGroup> duplicates = duplicateFinder.findDuplicates(rootPath);
|
||||
resultRef.set(duplicates);
|
||||
} catch (Exception e) {
|
||||
errorRef.set(e);
|
||||
errors.add(e);
|
||||
} finally {
|
||||
latch.countDown();
|
||||
}
|
||||
});
|
||||
|
||||
scanThread.setDaemon(true);
|
||||
scanThread.start();
|
||||
findThread.setDaemon(true);
|
||||
findThread.start();
|
||||
|
||||
// 等待扫描完成,设置更长的超时时间(例如5分钟)
|
||||
if (!latch.await(5*60, TimeUnit.MINUTES)) {
|
||||
// 等待扫描完成,设置超时时间(例如5分钟)
|
||||
long totalTimeout = System.getSetting().getTotalTimeout();
|
||||
if (!latch.await(totalTimeout, TimeUnit.SECONDS)) {
|
||||
scanner.cancel();
|
||||
throw new TimeoutException("扫描超时(5分钟)");
|
||||
throw new TimeoutException(String.format("扫描超时(%d秒)", totalTimeout));
|
||||
}
|
||||
|
||||
// 检查是否被取消
|
||||
long start = java.lang.System.currentTimeMillis();
|
||||
try {
|
||||
boolean finished = false;
|
||||
while (!finished) {
|
||||
// 每 200ms 等待一次 latch,避免忙等待
|
||||
finished = latch.await(200, TimeUnit.MILLISECONDS);
|
||||
|
||||
// 检查是否被取消
|
||||
if (isCancelled()) {
|
||||
scanner.cancel();
|
||||
return "操作已被取消";
|
||||
}
|
||||
|
||||
// 如果有错误且扫描未完成,抛出异常
|
||||
if (errorRef.get() != null && !scanCompleted.get()) {
|
||||
throw errorRef.get();
|
||||
// 检查是否超时
|
||||
if (java.lang.System.currentTimeMillis() - start > totalTimeout * 1000L) {
|
||||
scanner.cancel();
|
||||
throw new TimeoutException(String.format("扫描超时(%d秒)", totalTimeout));
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
scanner.cancel();
|
||||
Thread.currentThread().interrupt();
|
||||
return "操作被中断";
|
||||
}
|
||||
|
||||
// 分析重复文件并构建结果
|
||||
updateMessage("正在分析重复文件...");
|
||||
// 检查是否有错误
|
||||
if (!errors.isEmpty()) {
|
||||
throw new ScanningException(errors);
|
||||
}
|
||||
|
||||
List<DuplicateGroup> duplicateGroups = resultRef.get();
|
||||
|
||||
// 构建最终结果
|
||||
StringBuilder result = new StringBuilder();
|
||||
result.append("重复文件检测结果:\n");
|
||||
|
||||
if (errorRef.get() != null) {
|
||||
result.append("警告: 扫描过程中发生错误 - ").append(errorRef.get().getMessage()).append("\n\n");
|
||||
}
|
||||
|
||||
result.append("总共处理 ").append(processed.get()).append(" 个文件\n");
|
||||
// 计算总文件数(所有组中的文件数)
|
||||
int totalDuplicateFiles = duplicateGroups.stream()
|
||||
.mapToInt(group -> group.fileMetas().size())
|
||||
.sum();
|
||||
|
||||
List<Map.Entry<String, List<Path>>> duplicateGroups = hashToFileMap.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().size() > 1)
|
||||
.collect(Collectors.toList());
|
||||
int totalGroups = duplicateGroups.size();
|
||||
|
||||
result.append(String.format("总共扫描文件数: %d\n", totalFiles.get()));
|
||||
result.append(String.format("发现重复文件组数: %d\n", totalGroups));
|
||||
result.append(String.format("重复文件总数: %d\n", totalDuplicateFiles));
|
||||
|
||||
if (!duplicateGroups.isEmpty()) {
|
||||
result.append("有 ").append(duplicateGroups.size()).append(" 组重复文件\n\n");
|
||||
result.append("\n详细重复文件信息:\n");
|
||||
result.append("----------------------------------------\n");
|
||||
|
||||
int groupIndex = 1;
|
||||
for (Map.Entry<String, List<Path>> entry : duplicateGroups) {
|
||||
result.append("第 ").append(groupIndex).append(" 组\t");
|
||||
result.append("哈希值: ").append(entry.getKey()).append("\n");
|
||||
for (DuplicateGroup group : duplicateGroups) {
|
||||
result.append(String.format("第 %d 组 (哈希值: %s, 大小: %d 字节)\n",
|
||||
groupIndex, group.hash(), group.size()));
|
||||
|
||||
int fileIndex = 1;
|
||||
for (Path file : entry.getValue()) {
|
||||
result.append("文件名").append(fileIndex).append(": ").append(file.getFileName()).append("\t\t");
|
||||
result.append("文件路径").append(fileIndex).append(": ").append(file.toAbsolutePath()).append("\n");
|
||||
for (var file : group.fileMetas()) {
|
||||
Path filePath = file.getPath();
|
||||
result.append(String.format(" 文件%d: %s\n", fileIndex, filePath.toAbsolutePath()));
|
||||
fileIndex++;
|
||||
}
|
||||
result.append("\n");
|
||||
groupIndex++;
|
||||
}
|
||||
} else {
|
||||
result.append("没有重复文件\n");
|
||||
result.append("\n没有发现重复文件\n");
|
||||
}
|
||||
|
||||
updateMessage("检测完成!");
|
||||
result.append("检测完成!\n");
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package top.r3944realms.docchecktoolrefactored.ui.utils;
|
|||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.stage.Window;
|
||||
|
||||
|
|
@ -18,14 +19,7 @@ public class DialogUtil {
|
|||
* @return the boolean
|
||||
*/
|
||||
public static boolean showExitConfirmation(Window owner) {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
|
||||
alert.initOwner(owner);
|
||||
alert.setTitle("确认退出");
|
||||
alert.setHeaderText("您确定要退出程序吗?");
|
||||
alert.setContentText("请确认您的操作");
|
||||
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
return result.isPresent() && result.get() == ButtonType.OK;
|
||||
return showConfirmationDialog("确认退出", "您确定要退出程序吗?", "请确认您的操作");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -41,8 +35,11 @@ public class DialogUtil {
|
|||
alert.setTitle(title);
|
||||
alert.setHeaderText(header);
|
||||
alert.setContentText(content);
|
||||
ButtonType yesButton = new ButtonType("Yes", ButtonBar.ButtonData.YES);
|
||||
ButtonType noButton = new ButtonType("No", ButtonBar.ButtonData.NO);
|
||||
alert.getButtonTypes().setAll(yesButton, noButton);
|
||||
Optional<ButtonType> result = alert.showAndWait();
|
||||
return result.isPresent() && result.get() == ButtonType.OK;
|
||||
return result.isPresent() && result.get() == yesButton;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,150 @@
|
|||
package top.r3944realms.docchecktoolrefactored.ui.utils;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* 进度条窗口工具类,支持取消按钮
|
||||
*/
|
||||
public class ProgressBarUtil {
|
||||
|
||||
private Stage progressStage;
|
||||
private ProgressBar progressBar;
|
||||
private Label messageLabel;
|
||||
private Button cancelButton;
|
||||
private final AtomicBoolean cancelled = new AtomicBoolean(false);
|
||||
private Runnable onCancelCallback;
|
||||
/**
|
||||
* 显示进度条窗口
|
||||
* @param ownerStage 父窗口
|
||||
* @param title 窗口标题
|
||||
* @param initialMessage 初始消息
|
||||
*/
|
||||
public void showProgress(Stage ownerStage, String title, String initialMessage) {
|
||||
Platform.runLater(() -> {
|
||||
progressStage = new Stage();
|
||||
progressStage.initOwner(ownerStage);
|
||||
progressStage.initStyle(StageStyle.UTILITY);
|
||||
progressStage.initModality(Modality.APPLICATION_MODAL);
|
||||
progressStage.setTitle(title);
|
||||
progressStage.setResizable(false);
|
||||
|
||||
// 创建进度条
|
||||
progressBar = new ProgressBar();
|
||||
progressBar.setPrefWidth(300);
|
||||
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
|
||||
|
||||
// 创建消息标签
|
||||
messageLabel = new Label(initialMessage);
|
||||
|
||||
// 创建取消按钮
|
||||
cancelButton = new Button("取消");
|
||||
cancelButton.setOnAction(e -> {
|
||||
cancelled.set(true);
|
||||
if (onCancelCallback != null) {
|
||||
onCancelCallback.run();
|
||||
}
|
||||
closeProgress();
|
||||
});
|
||||
|
||||
// 如果 setOnCancel 先调用过,绑定回调
|
||||
if (onCancelCallback != null) {
|
||||
cancelButton.setOnAction(e -> {
|
||||
cancelled.set(true);
|
||||
onCancelCallback.run();
|
||||
closeProgress();
|
||||
});
|
||||
}
|
||||
|
||||
// 布局
|
||||
VBox root = new VBox(10, messageLabel, progressBar, cancelButton);
|
||||
root.setStyle("-fx-padding: 20; -fx-alignment: center;");
|
||||
|
||||
Scene scene = new Scene(root);
|
||||
progressStage.setScene(scene);
|
||||
progressStage.sizeToScene();
|
||||
progressStage.show();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新进度和消息
|
||||
* @param progress 进度值 (0.0 - 1.0)
|
||||
* @param message 要显示的消息
|
||||
*/
|
||||
public void updateProgress(double progress, String message) {
|
||||
Platform.runLater(() -> {
|
||||
if (progressBar != null) {
|
||||
progressBar.setProgress(progress);
|
||||
}
|
||||
if (messageLabel != null) {
|
||||
messageLabel.setText(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭进度条窗口
|
||||
*/
|
||||
public void closeProgress() {
|
||||
Platform.runLater(() -> {
|
||||
if (progressStage != null) {
|
||||
progressStage.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已取消
|
||||
*/
|
||||
public boolean isCancelled() {
|
||||
return cancelled.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速显示一个进度条窗口并执行任务
|
||||
* @param ownerStage 父窗口
|
||||
* @param title 窗口标题
|
||||
* @param initialMessage 初始消息
|
||||
* @param task 要执行的任务(可检查 isCancelled() 中途退出)
|
||||
*/
|
||||
public static void showAndExecute(Stage ownerStage, String title, String initialMessage, CancellableTask task) {
|
||||
ProgressBarUtil progressBarUtil = new ProgressBarUtil();
|
||||
progressBarUtil.showProgress(ownerStage, title, initialMessage);
|
||||
|
||||
new Thread(() -> {
|
||||
try {
|
||||
task.run(progressBarUtil);
|
||||
} finally {
|
||||
progressBarUtil.closeProgress();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
@FunctionalInterface
|
||||
public interface CancellableTask {
|
||||
void run(ProgressBarUtil util);
|
||||
}
|
||||
public void setOnCancel(Runnable onCancel) {
|
||||
if (cancelButton != null) {
|
||||
cancelButton.setOnAction(e -> {
|
||||
cancelled.set(true);
|
||||
if (onCancel != null) {
|
||||
onCancel.run();
|
||||
}
|
||||
closeProgress();
|
||||
});
|
||||
} else {
|
||||
// 保存回调,稍后在按钮创建后绑定
|
||||
this.onCancelCallback = onCancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ public class FileUtil {
|
|||
Path path = Paths.get(filePath).normalize();
|
||||
File file = path.toFile();
|
||||
if (!file.exists()) {
|
||||
log.error("文件不存在: {}", filePath);
|
||||
log.error(LoggerHelper.TRACE_MARKER, "文件不存在: {}", filePath);
|
||||
throw new NoSuchFileException("文件不存在: " + filePath);
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ public class FileUtil {
|
|||
File file = path.toFile();
|
||||
|
||||
if (!file.exists()) {
|
||||
log.error("文件不存在: {}", filePath);
|
||||
log.error(LoggerHelper.TRACE_MARKER, "文件不存在: {}", filePath);
|
||||
throw new NoSuchFileException("文件不存在: " + filePath);
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ public class FileUtil {
|
|||
.toList();
|
||||
|
||||
if (!supportedExtensions.contains(fileExtension)) {
|
||||
log.error("不支持的文件格式: {}", fileExtension);
|
||||
log.error(LoggerHelper.TRACE_MARKER, "不支持的文件格式: {}", fileExtension);
|
||||
throw new IllegalArgumentException("不支持的文件格式,预期: "
|
||||
+ supportedExtensions + ",实际: " + fileExtension);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package top.r3944realms.docchecktoolrefactored.util;
|
||||
|
||||
import org.slf4j.Marker;
|
||||
import org.slf4j.MarkerFactory;
|
||||
|
||||
public class LoggerHelper {
|
||||
public static final Marker TRACE_MARKER = MarkerFactory.getMarker("TRACE");
|
||||
public static final Marker DEBUG_MARKER = MarkerFactory.getMarker("DEBUG");
|
||||
public static final Marker RELEASE_MARKER = MarkerFactory.getMarker("RELEASE");
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
/* 默认Tab形状容器 */
|
||||
.tab .tab-container {
|
||||
-fx-background-color: #13b72b;
|
||||
-fx-background-color: #7BB0D9;
|
||||
-fx-shape: "M0,5 L65,5 L75,15 L65,25 L0,25 L10,15 Z";
|
||||
-fx-background-insets: 0;
|
||||
-fx-border-width: 0; /* 确保无边框 */
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
}
|
||||
|
||||
.tab:selected .tab-container {
|
||||
-fx-background-color: #9b0e0e; /* 只改变背景色 */
|
||||
-fx-background-color: #0063b0; /* 只改变背景色 */
|
||||
-fx-effect: null; /* 移除所有效果 */
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
</padding>
|
||||
|
||||
<top>
|
||||
<Label alignment="CENTER" text="淮阴区数字化档案检查验收系统" textAlignment="CENTER" textFill="rgb(66,133,244)">
|
||||
<Label alignment="CENTER" text="数字化验收工具" textAlignment="CENTER" textFill="rgb(66,133,244)" BorderPane.alignment="TOP_CENTER">
|
||||
<font>
|
||||
<Font name="Microsoft YaHei" size="24.0" />
|
||||
</font>
|
||||
|
|
|
|||
|
|
@ -1,33 +1,43 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Menu?>
|
||||
<?import javafx.scene.control.MenuBar?>
|
||||
<?import javafx.scene.control.MenuItem?>
|
||||
<?import javafx.scene.control.SeparatorMenuItem?>
|
||||
<?import javafx.scene.control.Tab?>
|
||||
<?import javafx.scene.control.TabPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/17.0.2-ea" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.MainStageController">
|
||||
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="1000.0" prefWidth="1200.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.MainStageController">
|
||||
<children>
|
||||
<MenuBar prefWidth="2558.0" VBox.vgrow="ALWAYS">
|
||||
<menus>
|
||||
<Menu mnemonicParsing="false" text="文件">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="关闭" />
|
||||
</items>
|
||||
</Menu>
|
||||
<Menu mnemonicParsing="false" text="编辑">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="未完成" />
|
||||
<MenuItem fx:id="settingMI" mnemonicParsing="false" onAction="#onOpenSetting" text="设置" />
|
||||
<SeparatorMenuItem mnemonicParsing="false" />
|
||||
<MenuItem fx:id="logoutMI" mnemonicParsing="false" onAction="#onLogout" text="登出" />
|
||||
<SeparatorMenuItem mnemonicParsing="false" />
|
||||
<MenuItem fx:id="exitMI" mnemonicParsing="false" onAction="#onExit" text="退出" />
|
||||
</items>
|
||||
</Menu>
|
||||
<Menu fx:id="helpM" mnemonicParsing="false" text="帮助">
|
||||
<items>
|
||||
<MenuItem mnemonicParsing="false" text="关于" />
|
||||
<MenuItem fx:id="helpDocMI" mnemonicParsing="false" onAction="#onOpenHelpDoc" text="帮助文档" />
|
||||
<SeparatorMenuItem mnemonicParsing="false" />
|
||||
<MenuItem fx:id="aboutSoftwareMI" mnemonicParsing="false" onAction="#onAbout" text="关于软件" />
|
||||
</items>
|
||||
</Menu>
|
||||
</menus>
|
||||
</MenuBar>
|
||||
<!-- 导入项目信息面板 -->
|
||||
<fx:include source="module/project-info-pane.fxml" VBox.vgrow="ALWAYS"/>
|
||||
<fx:include source="module/project-info-pane.fxml" VBox.vgrow="ALWAYS" />
|
||||
<!-- 导入项目内容面板 -->
|
||||
<TabPane stylesheets="@../css/custom-tab.css" tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
|
||||
<TabPane fx:id="tabPane" onKeyPressed="#handleKeyPressed" stylesheets="@../css/custom-tab.css" tabClosingPolicy="UNAVAILABLE" VBox.vgrow="ALWAYS">
|
||||
<tabs>
|
||||
<Tab id="startTab" fx:id="step1T" text="1. 查重复文件">
|
||||
<content>
|
||||
|
|
@ -66,5 +76,34 @@
|
|||
</Tab>
|
||||
</tabs>
|
||||
</TabPane>
|
||||
<HBox nodeOrientation="RIGHT_TO_LEFT" prefHeight="100.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
|
||||
<children>
|
||||
<Button fx:id="nextB" mnemonicParsing="false" onAction="#onNext" prefHeight="100.0" prefWidth="500.0" text="下一步">
|
||||
<HBox.margin>
|
||||
<Insets bottom="20.0" left="40.0" right="40.0" top="20.0" />
|
||||
</HBox.margin>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font name="System Bold" size="19.0" />
|
||||
</font>
|
||||
</Button>
|
||||
<Button fx:id="prevB" mnemonicParsing="false" onAction="#onPrev" prefHeight="100.0" prefWidth="500.0" text="上一步">
|
||||
<HBox.margin>
|
||||
<Insets bottom="20.0" left="40.0" right="40.0" top="20.0" />
|
||||
</HBox.margin>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
<font>
|
||||
<Font name="System Bold" size="19.0" />
|
||||
</font>
|
||||
</Button>
|
||||
</children>
|
||||
<VBox.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</VBox.margin>
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.DuplicateDocumentPaneController">
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.DuplicateDocumentPaneController">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="288.0" minWidth="0.0" percentWidth="0.0" prefWidth="82.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="1263.9999633789064" minWidth="0.0" prefWidth="745.0" />
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
<RowConstraints maxHeight="592.6666666666666" prefHeight="581.3333536783855" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<TextArea fx:id="result1B" editable="false" GridPane.columnSpan="3" GridPane.rowIndex="3">
|
||||
<TextArea fx:id="result1B" editable="false" prefHeight="414.0" prefWidth="683.0" GridPane.columnSpan="3" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
<Insets bottom="2.0" left="10.0" right="2.0" top="2.0" />
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
<Label text="反馈结果:" GridPane.rowIndex="2">
|
||||
<Label text="结果反馈:" GridPane.rowIndex="2">
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin></Label>
|
||||
|
|
@ -71,6 +71,11 @@
|
|||
<Insets left="10.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<Button fx:id="cancel1B" alignment="CENTER" mnemonicParsing="false" onAction="#onCancel" prefHeight="52.0" prefWidth="117.0" text="取消检查" GridPane.columnIndex="3" GridPane.rowIndex="1">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</GridPane.margin>
|
||||
</Button>
|
||||
</children>
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.PathCheckPaneController">
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.PathCheckPaneController">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="940.0" minWidth="0.0" percentWidth="0.0" prefWidth="104.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="940.0" minWidth="10.0" percentWidth="0.0" prefWidth="104.0" />
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.TextArea?>
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane prefHeight="8000.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17">
|
||||
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<TextArea editable="false" text="工作内容: 对照《元数据检查登记表》(附件4)检查并登记数字化项目信息、技术环境及技术参数的完整性等情况。" wrapText="true" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0">
|
||||
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" scrollLeft="1.0" text="工作内容: 对照《元数据检查登记表》(附件4)检查并登记数字化项目信息、技术环境及技术参数的完整性等情况。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||
<font>
|
||||
<Font size="18.0" />
|
||||
</font></TextArea>
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@
|
|||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17">
|
||||
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<padding>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</padding>
|
||||
<children>
|
||||
<TextArea editable="false" prefHeight="200.0" prefWidth="200.0" text="工作内容: ①检查档案管理系统或电子目录的挂接准确率(要求100%) ②逐件验证数字化成果与目录的关联性 ③结果填入《挂接检查登记表》(附件5)。" wrapText="true" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0">
|
||||
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" text="工作内容: ①检查档案管理系统或电子目录的挂接准确率(要求100%) ②逐件验证数字化成果与目录的关联性 ③结果填入《挂接检查登记表》(附件5)。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||
<font>
|
||||
<Font size="18.0" />
|
||||
</font></TextArea>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<AnchorPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
|
||||
<children>
|
||||
<TextArea editable="false" prefHeight="200.0" prefWidth="200.0" text="工作内容: 对照《工作记录检查登记表》(附件6)检查数字化工作台帐的规范性及与成果的一致性,并在表格中登记检查情况。" wrapText="true" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0">
|
||||
<TextArea editable="false" prefHeight="800.0" prefWidth="1000.0" text="工作内容: 对照《工作记录检查登记表》(附件6)检查数字化工作台帐的规范性及与成果的一致性,并在表格中登记检查情况。" wrapText="true" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
|
||||
<font>
|
||||
<Font size="18.0" />
|
||||
</font></TextArea>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.StorageCarrierPaneController">
|
||||
<GridPane prefHeight="800.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.module.StorageCarrierPaneController">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="226.33331298828125" minWidth="10.0" percentWidth="10.0" prefWidth="108.33333333333334" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="559.3333511352539" minWidth="10.0" percentWidth="40.0" prefWidth="373.00002034505206" />
|
||||
|
|
@ -18,11 +18,10 @@
|
|||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="500.0" minWidth="10.0" percentWidth="25.0" prefWidth="400.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints maxHeight="151.33334350585938" percentHeight="7.0" prefHeight="55.00001017252603" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="407.0" percentHeight="7.0" prefHeight="73.33333841959634" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="508.33333333333326" percentHeight="7.0" prefHeight="73.99999491373697" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="591.6666666666667" percentHeight="7.0" prefHeight="43.33332316080731" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="602.0000152587891" minHeight="10.0" prefHeight="534.6666615804037" vgrow="SOMETIMES" />
|
||||
<RowConstraints maxHeight="151.33334350585938" percentHeight="7.0" prefHeight="55.00001017252603" vgrow="NEVER" />
|
||||
<RowConstraints maxHeight="508.33333333333326" percentHeight="7.0" prefHeight="73.99999491373697" vgrow="NEVER" />
|
||||
<RowConstraints maxHeight="592.6666666666666" percentHeight="7.0" prefHeight="581.3333536783855" vgrow="NEVER" />
|
||||
<RowConstraints maxHeight="1.7976931348623157E308" prefHeight="581.3333536783855" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label text="载入数字化成果:">
|
||||
|
|
@ -33,7 +32,7 @@
|
|||
<Insets left="5.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<Label text="将档案目录、哈希值列表文件和检测过程文件打包制成打包制成“数字化验收检测包.rar”压缩包" GridPane.columnSpan="4" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
|
||||
<Label text="将档案目录、哈希值列表文件和检测过程文件打包制成打包制成“数字化验收检测包.rar”压缩包" GridPane.columnSpan="5" GridPane.halignment="CENTER" GridPane.rowSpan="2" GridPane.valignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
|
|
@ -41,7 +40,7 @@
|
|||
<Font name="System Bold" size="14.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<Label text="反馈结果:" GridPane.rowIndex="3">
|
||||
<Label text="结果反馈:" GridPane.rowIndex="2">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
|
|
@ -49,7 +48,7 @@
|
|||
<Insets left="5.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<Label text="载入压缩包:" GridPane.rowIndex="2">
|
||||
<Label text="载入压缩包:" GridPane.rowIndex="1">
|
||||
<padding>
|
||||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
|
|
@ -65,7 +64,7 @@
|
|||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
</TextField>
|
||||
<TextField fx:id="loadCompressedFile" GridPane.columnIndex="1" GridPane.rowIndex="2">
|
||||
<TextField fx:id="loadCompressedFile" GridPane.columnIndex="1" GridPane.rowIndex="1">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
|
|
@ -73,7 +72,7 @@
|
|||
<Insets bottom="2.0" left="2.0" right="2.0" top="2.0" />
|
||||
</padding>
|
||||
</TextField>
|
||||
<TextArea fx:id="result7TA" editable="false" GridPane.columnSpan="3" GridPane.hgrow="ALWAYS" GridPane.rowIndex="4">
|
||||
<TextArea fx:id="result7TA" editable="false" prefWidth="400.0" GridPane.columnSpan="3" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
|
|
@ -103,7 +102,7 @@
|
|||
<Font size="14.0" />
|
||||
</font>
|
||||
</Button>
|
||||
<Button fx:id="selectLoadCompressedFile7B" mnemonicParsing="false" onAction="#onSelectLC" text="选择文件" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER">
|
||||
<Button fx:id="selectLoadCompressedFile7B" mnemonicParsing="false" onAction="#onSelectLC" text="选择文件" GridPane.columnIndex="2" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
|
|
@ -114,7 +113,7 @@
|
|||
<Font size="14.0" />
|
||||
</font>
|
||||
</Button>
|
||||
<Button fx:id="caculateHash7B" mnemonicParsing="false" onAction="#onCaculateHash" text="计算哈希值" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.rowIndex="2" GridPane.valignment="CENTER">
|
||||
<Button fx:id="caculateHash7B" mnemonicParsing="false" onAction="#onCaculateHash" text="计算哈希值" GridPane.columnIndex="3" GridPane.halignment="CENTER" GridPane.rowIndex="1" GridPane.valignment="CENTER">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
|
|
@ -125,7 +124,7 @@
|
|||
<Font size="14.0" />
|
||||
</font>
|
||||
</Button>
|
||||
<TextArea editable="false" prefWidth="400.0" text="①对照《存储载体检查登记表》(附件7)检查并记录存储载体的类型/数量/内容/可读性情况。 ②将档案目录、哈希值列表文件和检测过程文件打包制成打包制成“数字化验收检测包.rar”压缩包 ③计算并验证压缩包的MD5或哈希值" wrapText="true" GridPane.columnIndex="3" GridPane.rowIndex="4">
|
||||
<TextArea editable="false" maxWidth="1.7976931348623157E308" prefWidth="400.0" text="①对照《存储载体检查登记表》(附件7)检查并记录存储载体的类型/数量/内容/可读性情况。 ②将数字化成果(包括单页、多页文件及目录)打包生成"数字化验收检测包.rar"(含目录、哈希值列表、检测文件) ③计算并验证压缩包的MD5或哈希值" wrapText="true" GridPane.columnIndex="3" GridPane.rowIndex="3">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
|
|
@ -136,7 +135,7 @@
|
|||
<Font size="14.0" />
|
||||
</font>
|
||||
</TextArea>
|
||||
<Label text="工作内容:" GridPane.columnIndex="3" GridPane.rowIndex="3">
|
||||
<Label text="工作内容:" GridPane.columnIndex="3" GridPane.rowIndex="2">
|
||||
<GridPane.margin>
|
||||
<Insets left="10.0" />
|
||||
</GridPane.margin>
|
||||
|
|
|
|||
83
src/main/resources/fxml/setting-view.fxml
Normal file
83
src/main/resources/fxml/setting-view.fxml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.Spinner?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Font?>
|
||||
|
||||
<VBox xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="top.r3944realms.docchecktoolrefactored.ui.SettingDialogController">
|
||||
<children>
|
||||
<GridPane>
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="138.0" minWidth="10.0" prefWidth="133.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="109.0" minWidth="10.0" prefWidth="94.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="95.0" minWidth="10.0" prefWidth="73.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label text="单个扫描超时时间:" GridPane.rowIndex="1">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<Spinner fx:id="scanSingleTimeOutS" onMouseDragExited="#onCheckOne" GridPane.columnIndex="1" GridPane.rowIndex="1">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</Spinner>
|
||||
<Label prefWidth="114.0" text="总扫描超时时间:" GridPane.rowIndex="2">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</Label>
|
||||
<Spinner fx:id="scanTotalTimeOutS" onMouseDragExited="#onCheckTwo" GridPane.columnIndex="1" GridPane.rowIndex="2">
|
||||
<GridPane.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</GridPane.margin>
|
||||
</Spinner>
|
||||
<Label alignment="CENTER" contentDisplay="CENTER" prefHeight="28.0" prefWidth="332.0" text="设置" GridPane.columnSpan="3">
|
||||
<font>
|
||||
<Font size="21.0" />
|
||||
</font>
|
||||
</Label>
|
||||
<Label text="秒" GridPane.columnIndex="2" GridPane.rowIndex="1" />
|
||||
<Label text="秒" GridPane.columnIndex="2" GridPane.rowIndex="2" />
|
||||
<CheckBox fx:id="enableStepCB" mnemonicParsing="false" onAction="#onSettingThree" text="启用步骤辅助" GridPane.columnSpan="3" GridPane.halignment="CENTER" GridPane.rowIndex="3" GridPane.valignment="CENTER" />
|
||||
</children>
|
||||
</GridPane>
|
||||
<HBox alignment="CENTER">
|
||||
<children>
|
||||
<Button fx:id="resetB" mnemonicParsing="false" onAction="#onReset" text="恢复默认值" />
|
||||
</children>
|
||||
<VBox.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</VBox.margin>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER" nodeOrientation="RIGHT_TO_LEFT">
|
||||
<children>
|
||||
<Button fx:id="cancelB" mnemonicParsing="false" onAction="#onCancel" prefWidth="100.0" text="取消">
|
||||
<HBox.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</HBox.margin>
|
||||
</Button>
|
||||
<Button fx:id="saveB" mnemonicParsing="false" onAction="#onSave" prefWidth="100.0" text="保存">
|
||||
<HBox.margin>
|
||||
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
|
||||
</HBox.margin>
|
||||
</Button>
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
</VBox>
|
||||
BIN
src/main/resources/img/icon.jpg
Normal file
BIN
src/main/resources/img/icon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Loading…
Reference in New Issue
Block a user