拆分模块

This commit is contained in:
2025-09-03 20:56:44 +08:00
parent 08cc2c29a5
commit a2f5e4864b
939 changed files with 14227 additions and 9607 deletions

View File

@@ -0,0 +1,128 @@
package com.ecep.contract.util;
import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;
import com.ecep.contract.Desktop;
import com.ecep.contract.SpringApp;
import com.ecep.contract.ds.other.service.SysConfService;
import com.ecep.contract.model.SysConf;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Control;
import lombok.Getter;
import lombok.Setter;
public abstract class AbstractConfigBounder<T> implements ConfigBounder {
@Setter
private SysConfService confService;
String key;
SysConf conf;
@Getter
@Setter
Control control;
@Getter
@Setter
javafx.util.StringConverter<T> converter;
@Setter
Property<T> property;
ObjectProperty<LocalDateTime> modified = new SimpleObjectProperty<>(this, "modified");
ObjectProperty<LocalDateTime> created = new SimpleObjectProperty<>(this, "created");
public AbstractConfigBounder(String key) {
this.key = key;
}
@Override
public CompletableFuture<Void> runAsync(Runnable runnable) {
return CompletableFuture.runAsync(runnable, Desktop.instance.getExecutorService()).exceptionally(ex -> {
ex.printStackTrace();
return null;
});
}
@Override
public SysConfService getConfService() {
if (confService == null) {
confService = SpringApp.getBean(SysConfService.class);
}
return confService;
}
@Override
public void initialize() {
getControl().setDisable(true);
runAsync(this::asyncHandle);
}
protected void asyncHandle() {
conf = getConfService().findById(key);
if (conf == null) {
conf = new SysConf();
conf.setId(key);
conf.setCreated(LocalDateTime.now());
conf.setModified(LocalDateTime.now());
conf = getConfService().save(conf);
}
Property<T> property = getProperty();
property.addListener(this::propertyChanged);
bindBidirectional(property);
modified.set(conf.getModified());
created.set(conf.getCreated());
getControl().setDisable(false);
}
public Property<T> getProperty() {
if (property == null) {
T value = getConverter().fromString(conf.getValue());
property = createProperty(value);
setProperty(property);
}
return property;
}
public ObjectProperty<LocalDateTime> getModifiedProperty() {
return modified;
}
public ObjectProperty<LocalDateTime> getCreatedProperty() {
return created;
}
@SuppressWarnings("unchecked")
protected Property<T> createProperty(T value) {
if (value instanceof Boolean b) {
return (Property<T>) new SimpleBooleanProperty(b);
} else if (value instanceof Integer i) {
return (Property<T>) new SimpleIntegerProperty(i);
} else if (value instanceof Double d) {
return (Property<T>) new SimpleDoubleProperty(d);
} else if (value instanceof String s) {
return (Property<T>) new SimpleStringProperty(s);
}
return new SimpleObjectProperty<>(value);
}
abstract void bindBidirectional(Property<T> property);
void propertyChanged(ObservableValue<? extends T> observable, T oldValue,
T newValue) {
conf.setValue(getConverter().toString(newValue));
conf.setModified(LocalDateTime.now());
save();
}
public void save() {
SysConf saved = getConfService().save(conf);
modified.set(saved.getModified());
created.set(saved.getCreated());
}
}

View File

@@ -0,0 +1,122 @@
package com.ecep.contract.util;
import java.util.concurrent.*;
import java.util.function.Supplier;
import com.ecep.contract.Desktop;
/**
* 异步任务工具类 - 提供超时处理等增强功能
*/
public class AsyncUtils {
/**
* 执行带超时的CompletableFuture任务
*/
public static <T> CompletableFuture<T> supplyAsyncWithTimeout(Supplier<T> supplier, long timeout, TimeUnit unit) {
CompletableFuture<T> future = CompletableFuture.supplyAsync(supplier);
return withTimeout(future, timeout, unit);
}
/**
* 执行带超时的CompletableFuture任务并指定线程池
*/
public static <T> CompletableFuture<T> supplyAsyncWithTimeout(Supplier<T> supplier, Executor executor, long timeout,
TimeUnit unit) {
CompletableFuture<T> future = CompletableFuture.supplyAsync(supplier, executor);
return withTimeout(future, timeout, unit);
}
/**
* 为已有的CompletableFuture添加超时处理
*/
public static <T> CompletableFuture<T> withTimeout(CompletableFuture<T> future, long timeout, TimeUnit unit) {
CompletableFuture<T> timeoutFuture = new CompletableFuture<>();
ScheduledExecutorService executor = Desktop.instance.getExecutorService();
ScheduledFuture<?> schedule = executor.schedule(() -> {
TimeoutException timeoutException = new TimeoutException(
"Task timed out after " + timeout + " " + unit.name());
timeoutFuture.completeExceptionally(timeoutException);
}, timeout, unit);
;
return future.applyToEither(timeoutFuture, t -> t)
.whenComplete((t, ex) -> schedule.cancel(true));
}
/**
* 带重试机制的CompletableFuture任务
*/
public static <T> CompletableFuture<T> supplyAsyncWithRetry(Supplier<T> supplier, int maxRetries,
long delayBetweenRetries, TimeUnit unit) {
return supplyAsyncWithRetry(supplier, maxRetries, delayBetweenRetries, unit, null);
}
/**
* 带重试机制的CompletableFuture任务并指定哪些异常需要重试
*/
public static <T> CompletableFuture<T> supplyAsyncWithRetry(
Supplier<T> supplier,
int maxRetries,
long delayBetweenRetries,
TimeUnit unit,
Class<? extends Exception>[] retryableExceptions) {
CompletableFuture<T> resultFuture = new CompletableFuture<>();
// 执行任务的方法
executeWithRetry(supplier, resultFuture, 0, maxRetries, delayBetweenRetries, unit, retryableExceptions);
return resultFuture;
}
private static <T> void executeWithRetry(
Supplier<T> supplier,
CompletableFuture<T> resultFuture,
int currentAttempt,
int maxRetries,
long delayBetweenRetries,
TimeUnit unit,
Class<? extends Exception>[] retryableExceptions) {
CompletableFuture.supplyAsync(supplier)
.thenAccept(resultFuture::complete)
.exceptionally(ex -> {
if (currentAttempt < maxRetries && shouldRetry(ex, retryableExceptions)) {
// 延迟后重试
try {
Thread.sleep(unit.toMillis(delayBetweenRetries));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
resultFuture.completeExceptionally(ie);
return null;
}
// 递归调用,增加尝试次数
executeWithRetry(supplier, resultFuture, currentAttempt + 1,
maxRetries, delayBetweenRetries, unit, retryableExceptions);
} else {
// 达到最大重试次数或异常不可重试完成Future
resultFuture.completeExceptionally(ex);
}
return null;
});
}
private static boolean shouldRetry(Throwable ex, Class<? extends Exception>[] retryableExceptions) {
// 如果未指定重试异常类型,则所有异常都重试
if (retryableExceptions == null || retryableExceptions.length == 0) {
return true;
}
// 检查异常是否属于可重试类型
for (Class<? extends Exception> exceptionClass : retryableExceptions) {
if (exceptionClass.isInstance(ex.getCause())) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,40 @@
package com.ecep.contract.util;
import org.controlsfx.control.ToggleSwitch;
import javafx.beans.property.Property;
import javafx.util.StringConverter;
import javafx.util.converter.BooleanStringConverter;
public class BooleanConfig extends AbstractConfigBounder<Boolean> {
public BooleanConfig(String key) {
super(key);
}
@Override
public ToggleSwitch getControl() {
return (ToggleSwitch) super.getControl();
}
@Override
public StringConverter<Boolean> getConverter() {
StringConverter<Boolean> converter = super.getConverter();
if (converter == null) {
converter = new BooleanStringConverter();
setConverter(converter);
}
return converter;
}
@Override
protected Property<Boolean> createProperty(Boolean value) {
// fixbug when value is null
value = value == null ? false : value;
return super.createProperty(value);
}
@Override
void bindBidirectional(Property<Boolean> property) {
getControl().selectedProperty().bindBidirectional(property);
}
}

View File

@@ -0,0 +1,29 @@
package com.ecep.contract.util;
import com.ecep.contract.service.SysConfService;
import javafx.beans.property.SimpleBooleanProperty;
public class BooleanConfigProperty extends SimpleBooleanProperty {
private final String key;
private final SysConfService service;
public BooleanConfigProperty(String key, SysConfService service) {
this(key, service, service.getBoolean(key));
}
public BooleanConfigProperty(String key, SysConfService service, boolean initialValue) {
super(initialValue);
System.out.println("key = " + key + ", initialValue = " + initialValue);
this.key = key;
this.service = service;
}
@Override
protected void invalidated() {
super.invalidated();
System.out.println("key = " + key + ", set = " + get());
service.set(key, Boolean.toString(get()));
}
}

View File

@@ -0,0 +1,17 @@
package com.ecep.contract.util;
import java.util.concurrent.CompletableFuture;
import com.ecep.contract.service.SysConfService;
public interface ConfigBounder {
void setConfService(SysConfService confService);
void initialize();
CompletableFuture<Void> runAsync(Runnable runnable);
SysConfService getConfService();
}

View File

@@ -0,0 +1,89 @@
package com.ecep.contract.util;
import javafx.beans.value.ObservableValue;
import lombok.Setter;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* 延时执行器
*/
public class DelayOnceExecutor {
private final Runnable runnable;
private final Executor executor;
private CompletableFuture<Void> future;
/**
* 定义一个Consumer类型的异常处理器
*/
@Setter
private Consumer<Exception> exceptionHandler;
/**
* 构造函数
*/
public DelayOnceExecutor(Runnable runnable, long delay, TimeUnit unit) {
this.runnable = runnable;
executor = CompletableFuture.delayedExecutor(delay, unit);
}
/**
* 执行 runnable如果future不为空则取消之前的任务
*/
public void tick() {
if (future != null) {
future.cancel(true);
}
future = CompletableFuture.runAsync(this::run, executor);
}
public void tickNow() {
if (future != null) {
future.cancel(true);
}
future = CompletableFuture.runAsync(this::run);
}
void run() {
try {
runnable.run();
} catch (Exception e) {
if (exceptionHandler != null) {
exceptionHandler.accept(e);
}
}
}
/**
* 取消任务
*/
public void cancel() {
if (future != null) {
future.cancel(true);
}
}
/**
* 监听属性变化,当属性变化时,执行 runnable
*
* @param properties 要监听的属性
*/
public DelayOnceExecutor listen(ObservableValue<?>... properties) {
for (ObservableValue<?> property : properties) {
property.addListener((observable, oldValue, newValue) -> this.tick());
}
return this;
}
/**
* Consumer<Exception> exceptionHandler;
*
* @param exceptionHandler 异常处理
*/
public DelayOnceExecutor exception(Consumer<Exception> exceptionHandler) {
this.exceptionHandler = exceptionHandler;
return this;
}
}

View File

@@ -0,0 +1,172 @@
package com.ecep.contract.util;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellAddress;
import org.apache.poi.ss.util.CellRangeAddress;
import java.time.LocalDate;
/**
*
*/
public class ExcelUtils {
public static Row getRow(Sheet sheet, int rowIndex, boolean autoCreate) {
Row row = sheet.getRow(rowIndex);
if (row == null) {
if (autoCreate) {
return sheet.createRow(rowIndex);
}
return null;
}
return row;
}
public static Cell getCell(Sheet sheet, String cellAddress) {
return getCell(sheet, cellAddress, false);
}
public static Cell getCell(Sheet sheet, String cellAddress, boolean autoCreate) {
CellAddress address = new CellAddress(cellAddress);
Row row = getRow(sheet, address.getRow(), autoCreate);
if (row == null) {
return null;
}
return getCell(row, address.getColumn(), autoCreate);
}
public static Cell getCell(Sheet sheet, int rowIndex, int columnIndex, boolean autoCreate) {
Row row = getRow(sheet, rowIndex, autoCreate);
if (row == null) {
return null;
}
return getCell(row, columnIndex, autoCreate);
}
public static Cell getCell(Row row, int columnIndex, boolean autoCreate) {
Cell cell = row.getCell(columnIndex);
if (cell == null) {
if (autoCreate) {
return row.createCell(columnIndex);
}
return null;
}
return cell;
}
public static String getCellValue(Sheet sheet, String cellAddress) {
Cell cell = getCell(sheet, cellAddress);
if (cell == null) {
return null;
}
return cell.getStringCellValue();
}
public static Cell setCellValue(Sheet sheet, String cellAddress, String text) {
CellAddress address = new CellAddress(cellAddress);
return setCellValue(sheet, address.getRow(), address.getColumn(), text);
}
public static Cell setCellValue(Sheet sheet, String cellAddress, double value) {
CellAddress address = new CellAddress(cellAddress);
return setCellValue(sheet, address.getRow(), address.getColumn(), value);
}
public static Cell setCellValue(Sheet sheet, String cellAddress, LocalDate date) {
CellAddress address = new CellAddress(cellAddress);
return setCellValue(sheet, address.getRow(), address.getColumn(), date);
}
public static Cell setCellValue(Sheet sheet, int rowIndex, int columnIndex, String text) {
Row row = getRow(sheet, rowIndex, true);
return setCellValue(row, columnIndex, text);
}
public static Cell setCellValue(Row row, int columnIndex, String text) {
assert row != null;
Cell cell = getCell(row, columnIndex, true);
assert cell != null;
cell.setCellValue(text);
return cell;
}
public static Cell setCellValue(Sheet sheet, int rowIndex, int columnIndex, double value) {
Row row = getRow(sheet, rowIndex, true);
return setCellValue(row, columnIndex, value);
}
public static Cell setCellValue(Row row, int columnIndex, double value) {
assert row != null;
Cell cell = getCell(row, columnIndex, true);
assert cell != null;
cell.setCellValue(value);
return cell;
}
public static Cell setCellValue(Sheet sheet, int rowIndex, int columnIndex, LocalDate date) {
Row row = getRow(sheet, rowIndex, true);
assert row != null;
return setCellValue(row, columnIndex, date);
}
public static Cell setCellValue(Row row, int columnIndex, LocalDate date) {
assert row != null;
Cell cell = getCell(row, columnIndex, true);
assert cell != null;
cell.setCellValue(date);
return cell;
}
public static Cell setCellValue(Sheet sheet, int rowIndex, int columnIndex, Double value) {
Row row = getRow(sheet, rowIndex, true);
assert row != null;
Cell cell = getCell(row, columnIndex, true);
assert cell != null;
if (value == null) {
cell.setBlank();
} else {
cell.setCellValue(value);
}
return cell;
}
public static void setBorder(CellStyle cellStyle, BorderStyle borderStyle) {
cellStyle.setBorderTop(borderStyle);
cellStyle.setBorderBottom(borderStyle);
cellStyle.setBorderLeft(borderStyle);
cellStyle.setBorderRight(borderStyle);
}
public static void mergedCells(Sheet sheet, String address) {
CellRangeAddress cellAddresses = CellRangeAddress.valueOf(address);
sheet.addMergedRegion(cellAddresses);
}
public static void mergedCells(Cell cell, int rows, int cols) {
Sheet sheet = cell.getSheet();
CellRangeAddress region = new CellRangeAddress(cell.getRowIndex(), cell.getRowIndex() + rows, cell.getColumnIndex(), cell.getColumnIndex() + cols);
sheet.addMergedRegion(region);
}
public static void copyStyle(Sheet source, String sourceCellAddress, Sheet target, String targetCellAddress) {
Cell sourceCell = getCell(source, sourceCellAddress);
Cell targetCell = getCell(target, targetCellAddress);
copyStyle(sourceCell, targetCell);
}
public static void copyStyle(Sheet source, String sourceCellAddress, Cell targetCell) {
Cell sourceCell = getCell(source, sourceCellAddress);
copyStyle(sourceCell, targetCell);
}
public static void copyStyle(Cell source, Cell target) {
if (source == null) {
return;
}
if (target == null) {
return;
}
target.setCellStyle(source.getCellStyle());
}
}

View File

@@ -0,0 +1,10 @@
package com.ecep.contract.util;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FxmlPath {
String value() default "";
}

View File

@@ -0,0 +1,88 @@
package com.ecep.contract.util;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
import com.ecep.contract.AppV2;
import com.ecep.contract.Desktop;
import com.ecep.contract.SpringApp;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
public class FxmlUtils {
public static FXMLLoader newLoader(String path) {
FXMLLoader loader = new FXMLLoader();
URL location = AppV2.class.getResource(path);
if (location == null) {
throw new RuntimeException("无法找到窗口资源文件 " + path);
}
loader.setLocation(location);
loader.setControllerFactory(SpringApp::getBean);
return loader;
}
public static CompletableFuture<FXMLLoader> newLoaderAsync(String path,
java.util.function.Consumer<FXMLLoader> consumer) {
return CompletableFuture.supplyAsync(() -> {
FXMLLoader loader = newLoader(path);
try {
loader.load();
} catch (IOException e) {
throw new RuntimeException("Unable open " + path, e);
}
consumer.accept(loader);
return loader;
}).whenComplete((v, ex) -> {
if (ex != null) {
UITools.showExceptionAndWait("无法打开 " + path + " 界面", ex);
}
});
}
/**
* 异步载入显示界面
*
* @param path fxml文件路径类地址 / 开头 根路径
* @param initializeLoader fxml 文件加载完毕后,回调函数
* @param runLater 在JavaFx线程中执行的回调函数
* @return CompletableFuture
*/
public static CompletableFuture<Void> newLoaderAsyncWithRunLater(
String path,
java.util.function.Consumer<FXMLLoader> initializeLoader,
java.util.function.Consumer<FXMLLoader> runLater) {
CompletableFuture<Void> future = new CompletableFuture<>();
CompletableFuture.runAsync(() -> {
try {
FXMLLoader loader = newLoader(path);
if (initializeLoader != null) {
initializeLoader.accept(loader);
}
loader.load();
Platform.runLater(() -> {
try {
runLater.accept(loader);
future.complete(null);
} catch (Exception e) {
future.completeExceptionally(e);
}
});
} catch (IOException e) {
future.completeExceptionally(new RuntimeException("Unable open " + path, e));
}
}, Desktop.instance.getExecutorService()).whenComplete((v, ex) -> {
if (ex != null) {
future.completeExceptionally(ex);
}
});
future.whenComplete((v, ex) -> {
if (ex != null) {
UITools.showExceptionAndWait("无法打开 " + path + " 界面", ex);
}
});
return future;
}
}

View File

@@ -0,0 +1,25 @@
package com.ecep.contract.util;
import com.ecep.contract.service.SysConfService;
import javafx.beans.property.SimpleIntegerProperty;
public class IntegerConfigProperty extends SimpleIntegerProperty
// implements Property<Integer>
{
private final String key;
private final SysConfService service;
public IntegerConfigProperty(String key, SysConfService service) {
super(service.getInt(key));
this.key = key;
this.service = service;
}
@Override
protected void invalidated() {
super.invalidated();
service.set(key, Integer.toString(get()));
}
}

View File

@@ -0,0 +1,37 @@
package com.ecep.contract.util;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import javafx.beans.property.Property;
import javafx.scene.control.DatePicker;
import javafx.util.StringConverter;
import javafx.util.converter.LocalDateStringConverter;
public class LocalDateConfig extends AbstractConfigBounder<LocalDate> {
public LocalDateConfig(String string) {
super(string);
}
@Override
public DatePicker getControl() {
return (DatePicker) super.getControl();
}
@Override
public StringConverter<LocalDate> getConverter() {
StringConverter<LocalDate> converter = super.getConverter();
if (converter == null) {
converter = new LocalDateStringConverter(DateTimeFormatter.ISO_LOCAL_DATE, null);
setConverter(converter);
}
return converter;
}
@Override
void bindBidirectional(Property<LocalDate> property) {
getControl().valueProperty().bindBidirectional(property);
}
}

View File

@@ -0,0 +1,45 @@
package com.ecep.contract.util;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import javafx.beans.property.Property;
import javafx.scene.control.TextField;
import javafx.util.StringConverter;
import javafx.util.converter.LocalDateTimeStringConverter;
import lombok.Getter;
import lombok.Setter;
public class LocalDateTimeConfig extends AbstractConfigBounder<LocalDateTime> {
@Getter
@Setter
private javafx.util.StringConverter<LocalDateTime> controlConverter;
public LocalDateTimeConfig(String string) {
super(string);
}
@Override
public TextField getControl() {
return (TextField) super.getControl();
}
@Override
public javafx.util.StringConverter<LocalDateTime> getConverter() {
javafx.util.StringConverter<LocalDateTime> converter = super.getConverter();
if (converter == null) {
converter = new LocalDateTimeStringConverter(DateTimeFormatter.ISO_LOCAL_DATE_TIME, null);
setConverter(converter);
}
return converter;
}
@Override
void bindBidirectional(Property<LocalDateTime> property) {
StringConverter<LocalDateTime> converter = getControlConverter();
if (converter == null) {
converter = getConverter();
}
getControl().textProperty().bindBidirectional(property, converter);
}
}

View File

@@ -0,0 +1,23 @@
package com.ecep.contract.util;
import javafx.beans.property.SimpleObjectProperty;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
public class MyDateTimePropertyUtils {
/**
* 本地日期时间转换为时间戳
*
* @param property
* @return
*/
public static Instant localDateTimeToInstant(SimpleObjectProperty<LocalDateTime> property) {
LocalDateTime dateTime = property.get();
if (dateTime != null) {
return dateTime.toInstant(ZoneOffset.ofHours(8));
}
return null;
}
}

View File

@@ -0,0 +1,52 @@
package com.ecep.contract.util;
import javafx.beans.property.Property;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.util.StringConverter;
public class StringConfig extends AbstractConfigBounder<String> {
public StringConfig(String string) {
super(string);
}
// @Override
// public TextField getControl() {
// return (TextField) super.getControl();
// }
@Override
public StringConverter<String> getConverter() {
StringConverter<String> converter = super.getConverter();
if (converter == null) {
converter = new StringConverter<String>() {
@Override
public String toString(String object) {
return object;
}
@Override
public String fromString(String string) {
return string;
}
};
setConverter(converter);
}
return converter;
}
@Override
void bindBidirectional(Property<String> property) {
Control ctrl = getControl();
if (ctrl == null) {
return;
}
if (ctrl instanceof TextField textField) {
textField.textProperty().bindBidirectional(property);
} else if (ctrl instanceof Label label) {
label.textProperty().bindBidirectional(property);
}
}
}

View File

@@ -0,0 +1,98 @@
package com.ecep.contract.util;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import org.springframework.data.domain.Sort;
import javafx.scene.Node;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.input.PickResult;
public class TableViewUtils {
/**
* 获取 Table 的排序
*/
public static <T> List<Sort.Order> getOrders(TableView<T> table) {
return table.getSortOrder().stream().filter(c -> {
if (!c.isSortable()) {
return false;
}
String id = c.getId();
return id.endsWith("Column");
}).map(c -> {
String id = c.getId();
id = id.replace(table.getId() + "_", "");
id = id.replace("Column", "");
Sort.Direction dir = c.getSortType() == TableColumn.SortType.ASCENDING ? Sort.Direction.ASC : Sort.Direction.DESC;
return new Sort.Order(dir, id);
}).toList();
}
public static <T> void bindDoubleClicked(TableView<T> table, Consumer<T> consumer) {
table.setOnMouseClicked(event -> {
if (event.getClickCount() == 2 && event.getButton() == MouseButton.PRIMARY) {
PickResult pickResult = event.getPickResult();
Node intersectedNode = pickResult.getIntersectedNode();
if (intersectedNode instanceof TableRow) {
// 鼠标点击的地方没有定义列,只返回了行,跳过不做处理
TableRow<T> row = (TableRow<T>) intersectedNode;
row.getItem();
return;
}
T selectedItem = table.getSelectionModel().getSelectedItem();
if (selectedItem != null) {
try {
consumer.accept(selectedItem);
} catch (UnsupportedOperationException e) {
UITools.showExceptionAndWait("当前操作不支持", e);
} catch (Exception e) {
UITools.showExceptionAndWait(e.getMessage(), e);
}
}
}
});
}
public static <T> void bindEnterPressed(TableView<T> table, Consumer<T> consumer) {
table.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) {
T selectedItem = table.getSelectionModel().getSelectedItem();
if (selectedItem != null) {
consumer.accept(selectedItem);
}
}
});
}
/**
* 计算表格的可视行数
*
* @param table 表格视图
* @param <T> T
* @return 可视行数
*/
public static <T> int getTableViewVisibleRows(TableView<T> table) {
int rows = 0;
Set<Node> tableRow = table.lookupAll("TableRow");
for (Node node : tableRow) {
if (node instanceof TableRow) {
if (!node.isVisible()) {
continue;
}
rows++;
}
}
return rows;
}
}

View File

@@ -0,0 +1,322 @@
package com.ecep.contract.util;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
import org.controlsfx.control.textfield.AutoCompletionBinding;
import org.controlsfx.control.textfield.TextFields;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import com.ecep.contract.Desktop;
import com.ecep.contract.Message;
import com.ecep.contract.converter.EntityStringConverter;
import com.ecep.contract.task.Tasker;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Screen;
import javafx.util.StringConverter;
public class UITools {
private static final Logger logger = LoggerFactory.getLogger(UITools.class);
public static void showExceptionAndWait(String title, Throwable e) {
String stackTrace = printStackTrace(e);
Label label = new Label("异常堆栈信息如下:");
TextArea textArea = new TextArea(stackTrace);
textArea.setEditable(false);
textArea.setWrapText(true);
GridPane.setVgrow(textArea, Priority.ALWAYS);
GridPane.setHgrow(textArea, Priority.ALWAYS);
GridPane expContent = new GridPane();
expContent.setMaxWidth(Double.MAX_VALUE);
expContent.add(label, 0, 0);
expContent.add(textArea, 0, 1);
String message = null;
if (e instanceof org.springframework.orm.ObjectOptimisticLockingFailureException) {
message = "锁冲突, 数据可能已经被修改, 请重新加载, 再次重试.";
} else {
message = e.getMessage();
}
showAndWait(Alert.AlertType.ERROR, "程序异常", title, message, expContent);
}
/**
* 显示 Alert 窗口
*
* @param alertType alert 类型
* @param title 标题
* @param header 头
* @param message 消息内容
* @param content 扩张节点
*/
public static CompletableFuture<ButtonType> showAndWait(Alert.AlertType alertType, String title, String header,
String message, Node content) {
CompletableFuture<ButtonType> future = new CompletableFuture<>();
Platform.runLater(() -> {
Alert alert = new Alert(alertType);
if (content != null) {
alert.getDialogPane().setExpandableContent(content);
alert.getDialogPane().setExpanded(true);
}
alert.setTitle(title);
if (header != null)
alert.setHeaderText(header);
if (message != null)
alert.setContentText(message);
Rectangle2D visualBounds = Screen.getPrimary().getVisualBounds();
alert.getDialogPane().setMaxWidth(visualBounds.getWidth() * 0.9);
alert.showAndWait();
future.complete(alert.getResult());
});
return future;
}
public static CompletableFuture<ButtonType> showConfirmation(String title, String message) {
return showAndWait(Alert.AlertType.CONFIRMATION, "请选择", title, message, null);
}
public static <T> Dialog<T> createDialog() {
Dialog<T> dialog = new Dialog<>();
VBox box = new VBox();
Label headerLabel = new Label();
headerLabel.setId("header");
VBox.setMargin(headerLabel, new Insets(5, 0, 8, 0));
ListView<T> listView = new ListView<>();
listView.setMinHeight(50);
listView.setId("list-view");
ObservableList<T> listViewDataSet = FXCollections.observableArrayList();
listView.setItems(listViewDataSet);
VBox.setVgrow(listView, Priority.ALWAYS);
box.getChildren().setAll(headerLabel, listView);
dialog.getDialogPane().setContent(box);
dialog.getDialogPane().getButtonTypes().add(ButtonType.OK);
dialog.setResizable(true);
dialog.setWidth(600);
dialog.setHeight(500);
return dialog;
}
/**
* 显示一个对话框, 并等待用户关闭对话框
* <p>
* 对话框内嵌一个 ListView可在 consumer 中向 ListView 添加内容
*
* @param title 对话框标题
* @param header 对话框头
* @param consumer 对话框内容生成器
*/
public static void showDialogAndWait(String title, String header, Consumer<ObservableList<String>> consumer) {
Dialog<String> dialog = createDialog();
dialog.setTitle(title);
DialogPane dialogPane = dialog.getDialogPane();
Node content = dialogPane.getContent();
Label headerLabel = (Label) content.lookup("#header");
ListView<String> listView = (ListView<String>) content.lookup("#list-view");
headerLabel.setText(header);
CompletableFuture.runAsync(() -> {
consumer.accept(listView.getItems());
}).exceptionally(e -> {
Platform.runLater(() -> {
listView.getItems().add(e.getMessage());
});
if (logger.isErrorEnabled()) {
logger.error(e.getMessage(), e);
}
return null;
});
dialog.showAndWait();
}
/**
* 显示一个对话框, 并等待用户关闭对话框
*
* @param title 对话框标题
* @param task 任务
* @param init 初始化
*/
public static void showTaskDialogAndWait(String title, javafx.concurrent.Task<?> task,
Consumer<Consumer<Message>> init) {
Dialog<Message> dialog = createDialog();
dialog.getDialogPane().getScene().getStylesheets().add("/ui/dialog.css");
dialog.setTitle(title);
DialogPane dialogPane = dialog.getDialogPane();
VBox box = (VBox) dialogPane.getContent();
Label headerLabel = (Label) box.lookup("#header");
ListView<Message> listView = (ListView<Message>) box.lookup("#list-view");
listView.setCellFactory(param -> new Tasker.MessageListCell());
headerLabel.textProperty().bind(task.titleProperty());
java.util.function.Predicate<Message> consumer = message -> {
if (Platform.isFxApplicationThread()) {
listView.getItems().add(message);
} else {
Platform.runLater(() -> listView.getItems().add(message));
}
return false;
};
if (task instanceof Tasker<?> tasker) {
tasker.setMessageHandler(consumer);
} else {
task.messageProperty().addListener((observable, oldValue, newValue) -> {
consumer.test(Message.info(newValue));
});
}
// 加一个进度条
ProgressBar progressBar = new ProgressBar(0);
progressBar.setPrefHeight(12);
progressBar.setPrefWidth(200);
progressBar.prefWidthProperty().bind(box.widthProperty().subtract(10));
progressBar.setMinHeight(12);
progressBar.setVisible(true);
VBox.setMargin(progressBar, new Insets(6, 0, 6, 0));
Platform.runLater(() -> {
box.getChildren().add(progressBar);
});
// progressBar.disabledProperty().bind(task.runningProperty());
progressBar.visibleProperty().bind(task.runningProperty());
progressBar.progressProperty().bind(task.progressProperty());
if (task instanceof Tasker<?> tasker) {
// 提交任务
Desktop.instance.getExecutorService().submit(tasker);
}
if (init != null) {
init.accept(msg -> consumer.test(msg));
}
dialog.showAndWait();
if (task.isRunning()) {
task.cancel();
}
}
private static String printStackTrace(Throwable e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
e.printStackTrace(pw);
return sw.toString();
}
public static void showAlertAndWait(String message) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setContentText(message);
alert.showAndWait();
}
public static <T> AutoCompletionBinding<T> autoCompletion(
TextField textField,
SimpleObjectProperty<T> property,
EntityStringConverter<T> converter) {
return autoCompletion(textField, property, converter::suggest, converter);
}
/**
* 给 TextField 增加一个 自动补全功能
*
* @param textField 要补全的文本框
* @param property 绑定的属性
* @param func 获取补全数据的方法
* @param converter 对象转换器
* @param <T> 对象类型
* @return AutoCompletionBinding
*/
public static <T> AutoCompletionBinding<T> autoCompletion(
TextField textField,
SimpleObjectProperty<T> property,
Function<AutoCompletionBinding.ISuggestionRequest, List<T>> func,
StringConverter<T> converter) {
// 先赋值
textField.textProperty().set(converter.toString(property.get()));
// 监听 property 变化
property.addListener((observable, oldValue, newValue) -> {
textField.textProperty().set(converter.toString(newValue));
});
// 关联一个 自动完成
AutoCompletionBinding<T> completionBinding = TextFields.bindAutoCompletion(textField, p -> {
if (p.isCancelled()) {
return null;
}
try {
return func.apply(p);
} catch (Exception e) {
textField.setText(e.getMessage());
throw new RuntimeException(e);
}
}, converter);
// 设置自动补全选中值到 property
completionBinding.setOnAutoCompleted(event -> {
property.set(event.getCompletion());
});
// fixed 当输入框丢失焦点时输入框内容为空时设置property 为null
textField.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
if (!StringUtils.hasText(textField.getText())) {
property.set(null);
}
}
});
return completionBinding;
}
/**
* 确认对话框
*
* @param title
* @param text
* @return
*/
public static boolean showConfirmDialog(String title, String text) {
// TODO
Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
alert.setTitle(title);
alert.setHeaderText(text);
if (alert.showAndWait().get() == ButtonType.OK) {
return true;
}
return false;
}
}