Files
contract-manager/src/main/java/com/ecep/contract/manager/ui/AbstEntityManagerSkin.java
songqq fb28bac53a refactor(ui): 优化表格数据加载和异步单元格更新逻辑
修复表格高度调整时频繁触发数据加载的问题,增加调整阈值判断
改进异步单元格更新逻辑,增加取消机制和状态检查
重构常量命名,使用更清晰的KEY_SYNC_BY前缀
添加日志记录和注释说明
2025-08-27 14:43:55 +08:00

493 lines
17 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.ecep.contract.manager.ui;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import com.ecep.contract.manager.ui.table.EditableEntityTableTabSkin;
import com.ecep.contract.manager.ui.table.TableTabSkin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import com.ecep.contract.manager.ds.other.model.IdentityEntity;
import com.ecep.contract.manager.ds.other.vo.IdentityViewModel;
import com.ecep.contract.manager.util.TableViewUtils;
import com.ecep.contract.manager.util.UITools;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent;
import javafx.util.converter.NumberStringConverter;
/**
* @param <T> Entity 的类型
* @param <TV> Entity 对应的ViewModel
* @param <Skin> Skin 的类型
* @param <C>
*/
public abstract class AbstEntityManagerSkin<T extends IdentityEntity, TV extends IdentityViewModel<T>, Skin extends ManagerSkin, C extends AbstManagerWindowController<T, TV, Skin>>
implements ManagerSkin, TableTabSkin<T, TV>, EditableEntityTableTabSkin<T, TV> {
private static final Logger logger = LoggerFactory.getLogger(AbstEntityManagerSkin.class);
/**
*
*/
protected C controller;
protected CompletableFuture<Void> loadedFuture;
protected ObservableList<TV> dataSet = FXCollections.observableArrayList();
protected PageRequest currentPageable = PageRequest.ofSize(25);
protected final SimpleIntegerProperty currentPageNumber = new SimpleIntegerProperty();
public AbstEntityManagerSkin(C controller) {
this.controller = controller;
}
@Override
public TableView<TV> getTableView() {
return controller.table;
}
@Override
public void handleException(String message, Throwable ex) {
if (controller != null) {
controller.handleException(message, ex);
return;
}
if (logger.isErrorEnabled()) {
logger.error(message, ex);
}
UITools.showExceptionAndWait(message, ex);
}
public Locale getLocale() {
return controller.getLocale();
}
public void install() {
onShown();
// 注册 F5 和 Ctrl+R 刷新快捷键
KeyCodeCombination ctrlRCombination = new KeyCodeCombination(KeyCode.R, KeyCombination.SHORTCUT_DOWN);
KeyCodeCombination f5Combination = new KeyCodeCombination(KeyCode.F5);
getTableView().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (ctrlRCombination.match(event) || f5Combination.match(event)) {
System.out.println("loadTableDataSetFuture = " + loadTableDataSetFuture);
if (loadTableDataSetFuture == null) {
loadTableDataSet(false);
}
event.consume();
}
});
}
private void onShown() {
if (loadedFuture == null) {
loadedFuture = runAsync(() -> {
Platform.runLater(() -> {
getTableView().setItems(dataSet);
});
initializeSearchBar();
initializeTable();
initializeFooter();
TableView<TV> table = getTableView();
// 视图更新时
table.layoutBoundsProperty().addListener(this::resizeTable);
// 启用行编辑功能
if (!table.isEditable()) {
TableViewUtils.bindDoubleClicked(table, this::onTableRowDoubleClickedAction);
}
if (table.contextMenuProperty().get() == null) {
ContextMenu contextMenu = new ContextMenu();
createContextMenu(contextMenu);
table.setContextMenu(contextMenu);
}
table.setSortPolicy(v -> {
if (loadTableDataSetFuture == null) {
loadTableDataSet(false);
}
return true;
});
loadTableDataSet(true);
});
}
}
private void initializeSearchBar() {
if (controller.searchKeyField != null) {
controller.searchKeyField.setOnKeyReleased(event -> {
if (event.getCode() == KeyCode.ENTER) {
controller.searchBtn.fire();
}
});
}
if (controller.searchBtn != null) {
controller.searchBtn.setOnAction(this::onSearchAction);
}
}
private void initializeFooter() {
currentPageNumber.addListener(this::currentPageNumberListener);
//
controller.currentPageNumberField.textProperty().bindBidirectional(currentPageNumber,
new NumberStringConverter());
controller.previousPageBtn.setOnAction(event -> {
try {
currentPageable = currentPageable.previous();
loadTableDataSet(true);
} catch (Exception e) {
logger.warn("previous page error", e);
}
});
controller.nextPageBtn.setOnAction(event -> {
try {
currentPageable = currentPageable.next();
loadTableDataSet(true);
} catch (Exception e) {
logger.warn("next page error", e);
}
});
}
private void currentPageNumberListener(Object obj, Number old, Number newValue) {
int page = newValue.intValue();
if (page < 0) {
page = 0;
}
if (currentPageable.getPageNumber() == page) {
return;
}
currentPageable = currentPageable.withPage(page);
loadTableDataSet(false);
}
public void onSearchAction(ActionEvent event) {
currentPageable = currentPageable.withPage(0);
loadTableDataSet(true);
}
private boolean allowResize = true;
/**
* 根据表格高度重新计算分页的页大小
*/
private void resizeTable(Object observable, Bounds old, Bounds newBounds) {
if (!allowResize) {
return;
}
double tableHeight = newBounds.getHeight();
if (tableHeight <= 0) {
return;
}
TableView<TV> table = getTableView();
Node lookup = table.lookup("TableRow");
if (lookup != null) {
double rowHeight = lookup.prefHeight(-1);
int rows = (int) Math.round(table.getHeight() / rowHeight) - 1;
// 只有当行数变化超过一定阈值时才重新加载数据
int currentRows = currentPageable.getPageSize();
if (Math.abs(rows - currentRows) <= 2) {
return; // 避免微小变化导致频繁刷新
}
int pageNumber = (int) Math.abs(currentPageable.getOffset() / rows);
if (currentPageable.getPageNumber() == pageNumber && currentPageable.getPageSize() == rows) {
return;
}
currentPageable = PageRequest.of(pageNumber, rows);
loadTableDataSet(false);
}
}
protected void createContextMenu(ContextMenu contextMenu) {
MenuItem item2 = new MenuItem("刷新");
item2.setOnAction(this::onTableRefreshAction);
contextMenu.getItems().add(item2);
if (this instanceof EditableEntityTableTabSkin) {
MenuItem item1 = new MenuItem("新建");
item1.setOnAction(this::onTableCreateNewAction);
MenuItem item3 = new MenuItem("删除");
item3.setOnAction(this::onTableDeleteAction);
contextMenu.getItems().addAll(item1, item3);
}
}
protected void onTableCreateNewAction(ActionEvent event) {
TV viewModel = createNewViewModel();
dataSet.add(viewModel);
}
protected void onTableRefreshAction(ActionEvent event) {
loadTableDataSet(false);
}
protected void onTableDeleteAction(ActionEvent event) {
ObservableList<TV> selectedItems = getTableView().getSelectionModel().getSelectedItems();
if (selectedItems.isEmpty()) {
return;
}
if (!UITools.showConfirmDialog("删除行", "确认删除选中的" + selectedItems.size() + "条数据?")) {
return;
}
for (TV selectedItem : new ArrayList<>(selectedItems)) {
if (deleteRow(selectedItem)) {
dataSet.remove(selectedItem);
}
}
}
protected <K> void acceptCellEditEvent(TableColumn.CellEditEvent<TV, K> event, Function<TV, Property<K>> function) {
TV row = event.getRowValue();
Property<K> property = function.apply(row);
property.setValue(event.getNewValue());
try {
saveRowData(row);
} catch (Exception e) {
handleException("保存出错", e);
}
}
protected boolean deleteRow(TV row) {
ViewModelService<T, TV> service = getViewModelService();
T entity = service.findById(row.getId().get());
if (entity != null) {
try {
service.delete(entity);
return true;
} catch (UnsupportedOperationException e) {
handleException("删除出错,此操作不支持", e);
} catch (Exception e) {
handleException("删除出错", e);
}
}
return false;
}
protected T createNewEntity(TV row) {
ViewModelService<T, TV> service = getViewModelService();
if (service != null) {
T entity = service.createNewEntity();
if (entity != null) {
return entity;
}
}
return null;
}
protected TV createNewViewModel() {
ViewModelService<T, TV> service = getViewModelService();
if (service != null) {
TV model = service.createNewViewModel();
if (model != null) {
return model;
}
}
return null;
}
protected void saveRowData(TV row) {
ViewModelService<T, TV> service = getViewModelService();
if (service == null) {
handleException("ViewModelService is null", new RuntimeException());
return;
}
row.saveInFxApplicationThread(service);
}
public T loadRowData(TV row) {
return getViewModelService().findById(row.getId().get());
}
public void deleteRowData(T entity) {
getViewModelService().delete(entity);
}
public T saveRowData(T entity) {
return getViewModelService().save(entity);
}
// 记录延时任务信息
private ScheduledFuture<?> loadTableDataSetFuture;
@Override
public void loadTableDataSet() {
loadTableDataSet(false);
}
/**
* 加载表格数据
* 延时任务未执行前,再次调用此函数时,重新延时
*
* @param reloadNow 是否立即刷新立即刷新将直接submit一个任务到 Executor否则 schedule 一个618毫秒的延时任务
*/
public void loadTableDataSet(boolean reloadNow) {
if (loadTableDataSetFuture != null) {
loadTableDataSetFuture.cancel(true);
}
loadTableDataSetFuture = getExecutorService().schedule(() -> {
try {
_reloadTableData().thenRun(() -> loadTableDataSetFuture = null).exceptionally(this::handleException);
} catch (Exception ex) {
handleException("加载表格数据出错", ex);
}
}, reloadNow ? 0 : 618, TimeUnit.MILLISECONDS);
}
private CompletableFuture<Void> _reloadTableData() {
CompletableFuture<Void> future = new CompletableFuture<>();
Platform.runLater(() -> {
allowResize = false; // 禁用调整
dataSet.clear();
runAsync(() -> {
controller.setStatus("载入中...");
List<TV> models = loadTableData();
Platform.runLater(() -> {
try {
updateTableDataSet(models);
allowResize = true; // 恢复调整
future.complete(null);
} catch (Exception e) {
allowResize = true; // 恢复调整
future.completeExceptionally(e);
}
});
}).exceptionally(ex -> {
future.completeExceptionally(ex);
return null;
});
});
return future;
}
protected void updateTableDataSet(List<TV> models) {
long timeMillis = System.currentTimeMillis();
// 清除所有选择状态,避免选择状态混乱
if (getTableView() != null && getTableView().getSelectionModel() != null) {
getTableView().getSelectionModel().clearSelection();
}
// 先清空再设置新数据,避免数据叠加
dataSet.clear();
if (models != null) {
dataSet.addAll(models);
}
// 强制刷新表格布局
if (getTableView() != null) {
getTableView().requestLayout();
getTableView().refresh();
}
long timeCost = System.currentTimeMillis() - timeMillis;
if (logger.isDebugEnabled()) {
logger.debug("update table dataSet cost: {} ms", timeCost);
}
}
protected List<TV> loadTableData() {
Specification<T> spec = getSpecification();
ViewModelService<T, TV> service = getViewModelService();
long timeMillis = System.currentTimeMillis();
Page<T> page = service.findAll(spec, getPageable());
long timeCost = System.currentTimeMillis() - timeMillis;
if (logger.isDebugEnabled()) {
logger.debug("load table data cost: {} ms", timeCost);
}
if (timeCost > 1000) {
controller.setStatus("used " + timeCost + " ms");
}
updateFooter(page);
return page.map(service::from).toList();
}
protected ViewModelService<T, TV> getViewModelService() {
ViewModelService<T, TV> service = controller.getViewModelService();
if (service == null) {
if (logger.isWarnEnabled()) {
logger.warn("ViewModelService is null");
}
}
return service;
}
public Specification<T> getSpecification() {
TextField field = controller.searchKeyField;
if (field != null) {
return getViewModelService().getSpecification(field.getText());
}
return null;
}
protected Specification<T> getSpecification(String searchText) {
return controller.getViewModelService().getSpecification(searchText);
}
/**
* 当表格行被双击时触发
*
* @param item 被双击的行数据
*/
protected void onTableRowDoubleClickedAction(TV item) {
}
protected void updateFooter(Page<T> page) {
Platform.runLater(() -> {
controller.previousPageBtn.setDisable(!page.hasPrevious());
controller.nextPageBtn.setDisable(!page.hasNext());
currentPageNumber.set(page.getNumber());
controller.setStatus(
(page.getNumber() + 1) + "/" + page.getTotalPages() + " 页, 总 " + page.getTotalElements() + "");
});
}
public List<Sort.Order> getTableOrders() {
return TableViewUtils.getOrders(getTableView());
}
public Sort getSortByTable() {
return Sort.by(getTableOrders());
}
public Pageable getPageable() {
Sort sort = getSortByTable();
return currentPageable.withSort(sort);
}
protected <Controller extends AbstEntityController<T, TV>> void showInOwner(Class<Controller> clz, TV model) {
BaseController.show(clz, model, getTableView().getScene().getWindow());
}
}