From fe3db58bfe07eefde72ae40015ffce2f332d5bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Fri, 31 Jul 2026 19:15:34 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=BB=9F=E4=B8=80=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E4=B8=AD=E5=BF=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/notification/FxNotificationCenter.java | 106 ++++++++++++++++++ .../ui/notification/NotificationCenter.java | 24 ++++ .../ui/notification/NotificationLevel.java | 7 ++ .../ui/notification/NotificationMessage.java | 21 ++++ .../FxNotificationCenterTest.java | 20 ++++ .../notification/NotificationMessageTest.java | 51 +++++++++ 6 files changed, 229 insertions(+) create mode 100644 src/main/java/com/example/workutils/system/ui/notification/FxNotificationCenter.java create mode 100644 src/main/java/com/example/workutils/system/ui/notification/NotificationCenter.java create mode 100644 src/main/java/com/example/workutils/system/ui/notification/NotificationLevel.java create mode 100644 src/main/java/com/example/workutils/system/ui/notification/NotificationMessage.java create mode 100644 src/test/java/com/example/workutils/system/ui/notification/FxNotificationCenterTest.java create mode 100644 src/test/java/com/example/workutils/system/ui/notification/NotificationMessageTest.java diff --git a/src/main/java/com/example/workutils/system/ui/notification/FxNotificationCenter.java b/src/main/java/com/example/workutils/system/ui/notification/FxNotificationCenter.java new file mode 100644 index 0000000..5b03720 --- /dev/null +++ b/src/main/java/com/example/workutils/system/ui/notification/FxNotificationCenter.java @@ -0,0 +1,106 @@ +package com.example.workutils.system.ui.notification; + +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import javafx.scene.control.TextArea; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.stage.Window; + +import java.util.Objects; + +public final class FxNotificationCenter implements NotificationCenter { + + private static final int LONG_MESSAGE_LENGTH = 240; + + private final Window owner; + + public FxNotificationCenter(Window owner) { + this.owner = owner; + } + + @Override + public void show(NotificationMessage message) { + Objects.requireNonNull(message, "message"); + if (Platform.isFxApplicationThread()) { + showNow(message); + } else { + Platform.runLater(() -> showNow(message)); + } + } + + @Override + public boolean confirm(String title, String header, String message) { + if (!Platform.isFxApplicationThread()) { + throw new IllegalStateException( + "confirm must be called on the JavaFX Application Thread"); + } + String normalizedMessage = requireMessage(message); + Alert alert = createAlert(Alert.AlertType.CONFIRMATION, title); + alert.setHeaderText(normalizeOptional(header)); + setMessage(alert, normalizedMessage); + return alert.showAndWait().filter(ButtonType.OK::equals).isPresent(); + } + + private void showNow(NotificationMessage message) { + Alert alert = createAlert(alertType(message.level()), message.title()); + alert.setHeaderText(null); + setMessage(alert, message.message()); + alert.showAndWait(); + } + + private Alert createAlert(Alert.AlertType type, String title) { + Alert alert = new Alert(type); + alert.setTitle(normalizeTitle(title)); + if (owner != null) { + alert.initOwner(owner); + } + return alert; + } + + private void setMessage(Alert alert, String message) { + if (!message.contains("\n") && message.length() <= LONG_MESSAGE_LENGTH) { + alert.setContentText(message); + return; + } + TextArea details = new TextArea(message); + details.setEditable(false); + details.setWrapText(true); + details.setMaxWidth(Double.MAX_VALUE); + details.setMaxHeight(Double.MAX_VALUE); + GridPane.setVgrow(details, Priority.ALWAYS); + GridPane.setHgrow(details, Priority.ALWAYS); + alert.getDialogPane().setExpandableContent(details); + alert.getDialogPane().setExpanded(true); + } + + private Alert.AlertType alertType(NotificationLevel level) { + return switch (level) { + case INFO -> Alert.AlertType.INFORMATION; + case WARNING -> Alert.AlertType.WARNING; + case ERROR -> Alert.AlertType.ERROR; + }; + } + + private String normalizeTitle(String title) { + String normalized = title == null ? "" : title.trim(); + return normalized.isEmpty() ? "提示" : normalized; + } + + private String normalizeOptional(String value) { + if (value == null) { + return null; + } + String normalized = value.trim(); + return normalized.isEmpty() ? null : normalized; + } + + private String requireMessage(String message) { + String normalized = Objects.requireNonNull(message, "message").trim(); + if (normalized.isEmpty()) { + throw new IllegalArgumentException("message must not be blank"); + } + return normalized; + } +} diff --git a/src/main/java/com/example/workutils/system/ui/notification/NotificationCenter.java b/src/main/java/com/example/workutils/system/ui/notification/NotificationCenter.java new file mode 100644 index 0000000..4a75cae --- /dev/null +++ b/src/main/java/com/example/workutils/system/ui/notification/NotificationCenter.java @@ -0,0 +1,24 @@ +package com.example.workutils.system.ui.notification; + +import static com.example.workutils.system.ui.notification.NotificationLevel.ERROR; +import static com.example.workutils.system.ui.notification.NotificationLevel.INFO; +import static com.example.workutils.system.ui.notification.NotificationLevel.WARNING; + +public interface NotificationCenter { + + void show(NotificationMessage message); + + boolean confirm(String title, String header, String message); + + default void info(String title, String message) { + show(new NotificationMessage(INFO, title, message)); + } + + default void warning(String title, String message) { + show(new NotificationMessage(WARNING, title, message)); + } + + default void error(String title, String message) { + show(new NotificationMessage(ERROR, title, message)); + } +} diff --git a/src/main/java/com/example/workutils/system/ui/notification/NotificationLevel.java b/src/main/java/com/example/workutils/system/ui/notification/NotificationLevel.java new file mode 100644 index 0000000..ce47506 --- /dev/null +++ b/src/main/java/com/example/workutils/system/ui/notification/NotificationLevel.java @@ -0,0 +1,7 @@ +package com.example.workutils.system.ui.notification; + +public enum NotificationLevel { + INFO, + WARNING, + ERROR +} diff --git a/src/main/java/com/example/workutils/system/ui/notification/NotificationMessage.java b/src/main/java/com/example/workutils/system/ui/notification/NotificationMessage.java new file mode 100644 index 0000000..4fb14ed --- /dev/null +++ b/src/main/java/com/example/workutils/system/ui/notification/NotificationMessage.java @@ -0,0 +1,21 @@ +package com.example.workutils.system.ui.notification; + +import java.util.Objects; + +public record NotificationMessage( + NotificationLevel level, + String title, + String message) { + + public NotificationMessage { + level = Objects.requireNonNull(level, "level"); + title = Objects.requireNonNull(title, "title").trim(); + message = Objects.requireNonNull(message, "message").trim(); + if (title.isEmpty()) { + title = "提示"; + } + if (message.isEmpty()) { + throw new IllegalArgumentException("message must not be blank"); + } + } +} diff --git a/src/test/java/com/example/workutils/system/ui/notification/FxNotificationCenterTest.java b/src/test/java/com/example/workutils/system/ui/notification/FxNotificationCenterTest.java new file mode 100644 index 0000000..64915cd --- /dev/null +++ b/src/test/java/com/example/workutils/system/ui/notification/FxNotificationCenterTest.java @@ -0,0 +1,20 @@ +package com.example.workutils.system.ui.notification; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FxNotificationCenterTest { + + @Test + void rejectsSynchronousConfirmationOutsideFxThread() { + FxNotificationCenter center = new FxNotificationCenter(null); + + IllegalStateException error = assertThrows(IllegalStateException.class, + () -> center.confirm("确认", "继续?", "操作不可撤销")); + + assertEquals("confirm must be called on the JavaFX Application Thread", + error.getMessage()); + } +} diff --git a/src/test/java/com/example/workutils/system/ui/notification/NotificationMessageTest.java b/src/test/java/com/example/workutils/system/ui/notification/NotificationMessageTest.java new file mode 100644 index 0000000..b16e531 --- /dev/null +++ b/src/test/java/com/example/workutils/system/ui/notification/NotificationMessageTest.java @@ -0,0 +1,51 @@ +package com.example.workutils.system.ui.notification; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static com.example.workutils.system.ui.notification.NotificationLevel.ERROR; +import static com.example.workutils.system.ui.notification.NotificationLevel.INFO; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class NotificationMessageTest { + + @Test + void normalizesTitleAndMessageForDisplay() { + NotificationMessage message = new NotificationMessage(INFO, " ", " 完成\n详情 "); + + assertEquals("提示", message.title()); + assertEquals("完成\n详情", message.message()); + } + + @Test + void rejectsBlankMessage() { + assertThrows(IllegalArgumentException.class, + () -> new NotificationMessage(ERROR, "失败", " ")); + } + + @Test + void convenienceMethodsCreateTypedMessages() { + List shown = new ArrayList<>(); + NotificationCenter center = new NotificationCenter() { + @Override + public void show(NotificationMessage message) { + shown.add(message); + } + + @Override + public boolean confirm(String title, String header, String message) { + return false; + } + }; + + center.info("完成", "导出成功"); + center.error("失败", "无法打开文件"); + + assertEquals(List.of( + new NotificationMessage(INFO, "完成", "导出成功"), + new NotificationMessage(ERROR, "失败", "无法打开文件")), shown); + } +}