新增统一通知中心

This commit is contained in:
张家训
2026-07-31 19:15:34 +08:00
parent 6fe37bf941
commit fe3db58bfe
6 changed files with 229 additions and 0 deletions
@@ -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;
}
}
@@ -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));
}
}
@@ -0,0 +1,7 @@
package com.example.workutils.system.ui.notification;
public enum NotificationLevel {
INFO,
WARNING,
ERROR
}
@@ -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");
}
}
}
@@ -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());
}
}
@@ -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<NotificationMessage> 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);
}
}