diff --git a/gradle.properties b/gradle.properties index 2eb2e5d..82c2d2c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ project_group =top.r3944realms.docchecktoolrefacored project_name = doc-check-tool -project_version = 1.0.0.3 \ No newline at end of file +project_version = 1.0.0.5 \ No newline at end of file diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java b/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java index d0782cf..7b5b404 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/JavaFxApplication.java @@ -12,7 +12,7 @@ public class JavaFxApplication extends Application { @Override public void init() throws Exception { super.init(); - System.setVersion("1.0.0.4"); + System.setVersion("1.0.0.5"); } @Override diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/System.java b/src/main/java/top/r3944realms/docchecktoolrefactored/System.java index 0d19c30..91ed30a 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/System.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/System.java @@ -3,8 +3,10 @@ package top.r3944realms.docchecktoolrefactored; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import top.r3944realms.docchecktoolrefactored.core.Setting; +import top.r3944realms.docchecktoolrefactored.ui.module.ProjectInfoPaneController; import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; import java.io.*; @@ -29,6 +31,8 @@ public enum System { private static final long DEFAULT_SINGLE_TIMEOUT = 30; private static final long DEFAULT_TOTAL_TIMEOUT = 12600; private static final boolean DEFAULT_ENABLE_STEP = false; + @Getter + private static final ProjectInfoPaneController.ProjectInfo projectInfo = new ProjectInfoPaneController.ProjectInfo(); public static void init() { loadSettings(); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java index 4480534..2a8c893 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/MainStageController.java @@ -3,19 +3,29 @@ package top.r3944realms.docchecktoolrefactored.ui; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.*; +import javafx.scene.control.Button; +import javafx.scene.control.Menu; +import javafx.scene.control.MenuItem; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import top.r3944realms.docchecktoolrefactored.System; import top.r3944realms.docchecktoolrefactored.core.Setting; import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; import top.r3944realms.docchecktoolrefactored.util.LoggerMarker; - +import java.awt.*; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; @@ -161,14 +171,89 @@ public class MainStageController { @FXML void onOpenHelpDoc(ActionEvent actionEvent) { - DialogUtil.showDetailedInformationDialog("帮助文档", "操作手册","见.exe程序同文件夹下的“操作手册。docx”文件。"); + try { + // 获取帮助文档文件路径(如果不存在则从资源中释放) + File helpFile = getOrExtractHelpDocument(); + + if (helpFile.exists()) { + // 打开文档 + openDocument(helpFile); + } else { + DialogUtil.showErrorDialog("打开失败", "帮助文档不存在", + "无法找到帮助文档,请联系技术支持。"); + } + } catch (Exception e) { + DialogUtil.showDetailedErrorDialog("打开失败", "无法打开帮助文档", + "错误信息:" + e.getMessage()); + } + } + + /** + * 获取或从资源中提取帮助文档 + */ + private File getOrExtractHelpDocument() throws IOException { + // 获取程序运行目录 + String appDir = java.lang.System.getProperty("user.dir"); + File helpFile = new File(appDir, HELP_DOC_FILE_NAME); + + // 如果文件不存在,则从资源中复制出来 + if (!helpFile.exists()) { + try (InputStream resourceStream = getClass().getResourceAsStream(HELP_DOC_RESOURCE_PATH)) { + if (resourceStream == null) { + throw new FileNotFoundException("资源中未找到帮助文档: " + HELP_DOC_RESOURCE_PATH); + } + + // 复制资源到程序目录 + Files.copy(resourceStream, helpFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + } + + return helpFile; + } + + // 帮助文档在资源中的路径 + private static final String HELP_DOC_RESOURCE_PATH = "/docs/UserHelpDocument.docx"; + // 释放到外部的文件名 + private static final String HELP_DOC_FILE_NAME = "操作手册.docx"; + + /** + * 使用系统默认程序打开文档 + */ + private void openDocument(File file) throws IOException { + if (Desktop.isDesktopSupported()) { + Desktop desktop = Desktop.getDesktop(); + if (desktop.isSupported(Desktop.Action.OPEN)) { + desktop.open(file); + } else { + throw new UnsupportedOperationException("系统不支持打开文件操作"); + } + } else { + // 对于不支持Desktop的环境(如某些Linux系统) + String os = java.lang.System.getProperty("os.name").toLowerCase(); + ProcessBuilder pb = getProcessBuilder(file, os); + + pb.start(); + } + } + + private static @NotNull ProcessBuilder getProcessBuilder(File file, String os) { + ProcessBuilder pb; + + if (os.contains("win")) { + pb = new ProcessBuilder("cmd.exe", "/c", file.getAbsolutePath()); + } else if (os.contains("mac")) { + pb = new ProcessBuilder("open", file.getAbsolutePath()); + } else if (os.contains("nix") || os.contains("nux")) { + pb = new ProcessBuilder("xdg-open", file.getAbsolutePath()); + } else { + throw new UnsupportedOperationException("不支持的操作系统"); + } + return pb; } - - @FXML void onAbout(ActionEvent actionEvent) { - DialogUtil.showDetailedInformationDialog("版本", "版本信息","1.0.0-beta"); + DialogUtil.showDetailedInformationDialog("关于软件", "数字化验收工具","软件版本:" + System.version() + "\n"); } public void updateStepButtonsVisibility() { Setting setting = System.getSetting(); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java index 6b132ac..50d1167 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/PathCheckPaneController.java @@ -135,7 +135,7 @@ public class PathCheckPaneController implements Initializable { FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择保存逻辑地址文件的位置"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv")); - fileChooser.setInitialFileName(selectedMode.toString() + "逻辑地址文件.csv"); + fileChooser.setInitialFileName(System.getProjectInfo().makeProjectInfoIntoFileNamePrefix() + "-" + selectedMode.toString() + "-逻辑地址文件 " + ".csv"); File outputFile = fileChooser.showSaveDialog(generateLogicalAddress2B.getScene().getWindow()); if (outputFile == null) { @@ -251,7 +251,7 @@ public class PathCheckPaneController implements Initializable { FileChooser fileChooser = System.getFileChooser(); fileChooser.setTitle("选择保存物理地址文件的位置"); fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv")); - fileChooser.setInitialFileName(selectedMode.toString() + "物理地址文件.csv"); + fileChooser.setInitialFileName(System.getProjectInfo().makeProjectInfoIntoFileNamePrefix() + "-" + selectedMode.toString() + "-物理地址文件.csv"); // 使用当前窗口作为父窗口显示文件选择对话框 File outputFile = fileChooser.showSaveDialog(selectJPGFolder2B.getScene().getWindow()); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java index 5f78036..175cb55 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/ProjectInfoPaneController.java @@ -2,13 +2,21 @@ package top.r3944realms.docchecktoolrefactored.ui.module; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.fxml.Initializable; import javafx.scene.control.TextField; +import lombok.Getter; +import lombok.Setter; +import top.r3944realms.docchecktoolrefactored.System; import top.r3944realms.docchecktoolrefactored.ui.utils.DialogUtil; +import top.r3944realms.docchecktoolrefactored.util.FileUtil; + +import java.net.URL; +import java.util.ResourceBundle; /** * The type Project info pane controller. */ -public class ProjectInfoPaneController { +public class ProjectInfoPaneController implements Initializable { @FXML private TextField projectNameTF; @FXML private TextField AcceptanceTimeTF; @FXML private TextField totalCatalogNumberTF; @@ -23,6 +31,46 @@ public class ProjectInfoPaneController { totalCatalogNumberTF.clear(); AcceptanceTimeTF.clear(); } + } + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + addAutoSaveListener(projectNameTF, "projectName"); + addAutoSaveListener(AcceptanceTimeTF, "acceptanceTime"); + addAutoSaveListener(totalCatalogNumberTF, "totalCatalogNumber"); + addAutoSaveListener(fileCategoriesTF, "fileCategory"); + addAutoSaveListener(fileYearTF, "fileYear"); + } + private void addAutoSaveListener(TextField field, String fieldName) { + field.textProperty().addListener((observable, oldValue, newValue) -> { + switch (fieldName) { + case "projectName" -> System.getProjectInfo().setProjectName(newValue); + case "acceptanceTime" -> System.getProjectInfo().setAcceptanceTime(newValue); + case "totalCatalogNumber" -> System.getProjectInfo().setTotalCatalogNumber(newValue); + case "fileCategory" -> System.getProjectInfo().setFileCategory(newValue); + case "fileYear" -> System.getProjectInfo().setFileYear(newValue); + } + }); + } + @Setter @Getter + public static class ProjectInfo { + private String projectName; + private String acceptanceTime; + private String totalCatalogNumber; + private String fileCategory; + private String fileYear; + @Override + public String toString() { + return "ProjectInfo{" + + "projectName='" + projectName + '\'' + + ", acceptanceTime='" + acceptanceTime + '\'' + + ", totalCatalogNumber='" + totalCatalogNumber + '\'' + + ", fileCategory='" + fileCategory + '\'' + + ", fileYear='" + fileYear + '\'' + + '}'; + } + public String makeProjectInfoIntoFileNamePrefix() { + return FileUtil.ensureValidFileName(projectName); + } } } diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java index d173f98..72cd8aa 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/module/StorageCarrierPaneController.java @@ -207,7 +207,7 @@ public class StorageCarrierPaneController { fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV Files", "*.csv")); // 设置默认文件名 - fileChooser.setInitialFileName("哈希值列表文件.csv"); + fileChooser.setInitialFileName(System.getProjectInfo().makeProjectInfoIntoFileNamePrefix() + "-哈希值列表文件.csv"); // 使用当前窗口作为父窗口显示文件选择对话框 File outputFile = fileChooser.showSaveDialog(selectLoadDigitalOutcomes7B.getScene().getWindow()); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java index 2e10dbf..b1e9917 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/task/DuplicateDocumentDetectionTask.java @@ -68,7 +68,10 @@ public class DuplicateDocumentDetectionTask extends Task{ if (total > 0) { // 控制更新频率 String msg = switch (phase) { - case GROUP_BY_SIZE -> String.format("正在按文件大小分组: %d/%d", current, total); + case GROUP_BY_SIZE -> { + totalFiles.getAndIncrement(); + yield String.format("正在按文件大小分组: %d/%d", current, total); + } case CALCULATE_HASH -> String.format("正在计算哈希值: %d/%d", current, total); }; updateMessage(msg); diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java index d68747b..4f9bc0d 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/ui/utils/DialogUtil.java @@ -7,6 +7,7 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.Priority; import javafx.stage.Stage; import javafx.stage.Window; +import org.jetbrains.annotations.NotNull; import top.r3944realms.docchecktoolrefactored.Main; import java.util.Optional; @@ -23,7 +24,7 @@ public class DialogUtil { ).toExternalForm(); private static final Image DIALOG_ICON = new Image( - Objects.requireNonNull(Main.class.getResourceAsStream("/img/logo256x.ico")) + Objects.requireNonNull(Main.class.getResourceAsStream("/img/icon.jpg")) ); /** * Show exit confirmation boolean. @@ -321,10 +322,12 @@ public class DialogUtil { dialogPane.getStyleClass().add("dialog-pane"); } // 封装设置Alert图标方法 - private static void setAlertIcon(Alert alert) { - // 间接获取Alert对应的Stage - Stage alertStage = (Stage) alert.getDialogPane().getScene().getWindow(); - // 添加图标(可添加多个尺寸,系统自动适配) - alertStage.getIcons().add(DIALOG_ICON); + private static void setAlertIcon(@NotNull Alert alert) { + Platform.runLater(() -> { + Stage alertStage = (Stage) alert.getDialogPane().getScene().getWindow(); + if (alertStage != null) { + alertStage.getIcons().add(DIALOG_ICON); + } + }); } } \ No newline at end of file diff --git a/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java b/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java index c311110..8ebdfc5 100644 --- a/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java +++ b/src/main/java/top/r3944realms/docchecktoolrefactored/util/FileUtil.java @@ -95,4 +95,84 @@ public class FileUtil { return file; } + /** + * 检查文件名是否合法(跨平台) + * @param fileName 文件名(不含路径) + * @return true 合法;false 非法 + */ + public static boolean isValidFileName(String fileName) { + if (fileName == null || fileName.isBlank()) return false; + + // Windows 非法字符 + String invalidChars = "<>:\"/\\\\|?*"; + for (char c : invalidChars.toCharArray()) { + if (fileName.indexOf(c) >= 0) return false; + } + + // Windows 保留名称 + List reservedNames = Arrays.asList( + "CON", "PRN", "AUX", "NUL", + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" + ); + String upperName = fileName.toUpperCase(Locale.ROOT); + String nameWithoutExt = upperName.contains(".") ? upperName.substring(0, upperName.indexOf('.')) : upperName; + if (reservedNames.contains(nameWithoutExt)) return false; + + // 不能以空格或句点结尾 + if (fileName.endsWith(" ") || fileName.endsWith(".")) return false; + + return true; + } + + /** + * 自动修复非法文件名: + * - 替换非法字符为下划线 + * - 去除末尾空格和句点 + * - 避免使用系统保留名 + * @param fileName 原始文件名 + * @return 修复后的安全文件名 + */ + public static String sanitizeFileName(String fileName) { + if (fileName == null || fileName.isBlank()) { + return "untitled"; + } + + // 替换非法字符为下划线 + String sanitized = fileName.replaceAll("[<>:\"/\\\\|?*]", "_"); + + // 去掉结尾空格和句点 + sanitized = sanitized.replaceAll("[ \\.]+$", ""); + + // Windows 保留名修复 + List reservedNames = Arrays.asList( + "CON", "PRN", "AUX", "NUL", + "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" + ); + String upperName = sanitized.toUpperCase(Locale.ROOT); + String nameWithoutExt = upperName.contains(".") ? upperName.substring(0, upperName.indexOf('.')) : upperName; + if (reservedNames.contains(nameWithoutExt)) { + sanitized = "_" + sanitized; // 添加前缀防止冲突 + } + + // 如果修复后为空,则命名为 untitled + if (sanitized.isBlank()) sanitized = "untitled"; + + return sanitized; + } + + /** + * 校验或修复文件名: + * 若合法则直接返回; + * 若非法则自动修复并打印日志 + */ + public static String ensureValidFileName(String fileName) { + if (isValidFileName(fileName)) { + return fileName; + } + String sanitized = sanitizeFileName(fileName); + log.warn(LoggerMarker.TRACE_MARKER, "非法文件名已修复: {} → {}", fileName, sanitized); + return sanitized; + } } diff --git a/src/main/resources/docs/UserHelpDocument.docx b/src/main/resources/docs/UserHelpDocument.docx new file mode 100644 index 0000000..48b11c9 Binary files /dev/null and b/src/main/resources/docs/UserHelpDocument.docx differ