重构所有VO对象,统一字段命名规范,移除冗余字段,优化接口实现 新增Voable接口用于VO对象转换 调整BaseViewModel和ProjectBasedViewModel接口定义 更新相关服务和控制器以适应VO对象变更
626 lines
20 KiB
Java
626 lines
20 KiB
Java
package com.ecep.contract.controller;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.List;
|
||
import java.util.Locale;
|
||
import java.util.Map;
|
||
import java.util.concurrent.CompletableFuture;
|
||
import java.util.concurrent.ScheduledFuture;
|
||
import java.util.concurrent.TimeUnit;
|
||
import java.util.function.Function;
|
||
|
||
import com.ecep.contract.util.ParamUtils;
|
||
import org.slf4j.Logger;
|
||
import org.slf4j.LoggerFactory;
|
||
import org.springframework.beans.BeansException;
|
||
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 com.ecep.contract.controller.table.EditableEntityTableTabSkin;
|
||
import com.ecep.contract.controller.table.TableTabSkin;
|
||
import com.ecep.contract.model.IdentityEntity;
|
||
import com.ecep.contract.service.QueryService;
|
||
import com.ecep.contract.service.ViewModelService;
|
||
import com.ecep.contract.util.TableViewUtils;
|
||
import com.ecep.contract.util.UITools;
|
||
import com.ecep.contract.vm.IdentityViewModel;
|
||
|
||
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();
|
||
// 是否允许调整表格高度
|
||
private boolean allowResize = true;
|
||
// 记录延时任务信息
|
||
private ScheduledFuture<?> loadTableDataSetFuture;
|
||
|
||
public AbstEntityManagerSkin(C controller) {
|
||
this.controller = controller;
|
||
}
|
||
|
||
@Override
|
||
public <TT> TT getBean(Class<TT> requiredType) throws BeansException {
|
||
return controller.getCachedBean(requiredType);
|
||
}
|
||
|
||
@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 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理单元格编辑事件
|
||
*
|
||
* @param <K>
|
||
* @param event
|
||
* @param propGetter
|
||
*/
|
||
protected <K> void acceptCellEditEvent(TableColumn.CellEditEvent<TV, K> event,
|
||
Function<TV, Property<K>> propGetter) {
|
||
TV row = event.getRowValue();
|
||
Property<K> property = propGetter.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);
|
||
}
|
||
|
||
/**
|
||
* 加载行数据
|
||
*
|
||
* @param row
|
||
* @return
|
||
*/
|
||
public T loadRowData(TV row) {
|
||
if (row.getId() == null) {
|
||
return null;
|
||
}
|
||
T entity = getViewModelService().findById(row.getId().get());
|
||
return entity;
|
||
}
|
||
|
||
/**
|
||
* 删除行数据
|
||
*
|
||
* @param entity
|
||
*/
|
||
public void deleteRowData(T entity) {
|
||
if (entity == null) {
|
||
return;
|
||
}
|
||
ViewModelService<T, TV> service = getViewModelService();
|
||
getViewModelService().delete(entity);
|
||
}
|
||
|
||
/**
|
||
* 保存行数据
|
||
*
|
||
* @param entity
|
||
* @return
|
||
*/
|
||
public T saveRowData(T entity) {
|
||
if (entity == null) {
|
||
return null;
|
||
}
|
||
ViewModelService<T, TV> service = getViewModelService();
|
||
return getViewModelService().save(entity);
|
||
}
|
||
|
||
@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("载入中...");
|
||
// 异步加载数据
|
||
if (getViewModelService() instanceof QueryService<T, TV> queryService) {
|
||
asyncLoadTableData(queryService, future);
|
||
return;
|
||
}
|
||
// 同步加载方法
|
||
List<TV> models = loadTableData();
|
||
_updateModels(models, future);
|
||
}).exceptionally(ex -> {
|
||
future.completeExceptionally(ex);
|
||
return null;
|
||
});
|
||
});
|
||
return future;
|
||
}
|
||
|
||
|
||
private void _updateModels(List<TV> models, CompletableFuture<Void> future) {
|
||
Platform.runLater(() -> {
|
||
try {
|
||
updateTableDataSet(models);
|
||
allowResize = true; // 恢复调整
|
||
future.complete(null);
|
||
} catch (Exception e) {
|
||
allowResize = true; // 恢复调整
|
||
future.completeExceptionally(e);
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 更新表格数据
|
||
*
|
||
* @param models
|
||
*/
|
||
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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 加载表格数据
|
||
*
|
||
* @return
|
||
*/
|
||
protected List<TV> loadTableData() {
|
||
ParamUtils.Builder params = getSpecification();
|
||
ViewModelService<T, TV> service = getViewModelService();
|
||
long timeMillis = System.currentTimeMillis();
|
||
Page<T> page = service.findAll(params == null ? null : params.build(), 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();
|
||
}
|
||
|
||
|
||
/**
|
||
* 异步加载表格数据
|
||
*
|
||
* @param queryService
|
||
* @param future
|
||
*/
|
||
private void asyncLoadTableData(QueryService<T, TV> queryService, CompletableFuture<Void> future) {
|
||
ParamUtils.Builder params = getSpecification();
|
||
queryService.asyncFindAll(params == null ? null : params.build(), getPageable()).whenComplete((result, ex) -> {
|
||
if (ex != null) {
|
||
future.completeExceptionally(ex);
|
||
return;
|
||
}
|
||
updateFooter(result);
|
||
List<TV> models = result.map(getViewModelService()::from).toList();
|
||
_updateModels(models, future);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取ViewModelService
|
||
*
|
||
* @return
|
||
*/
|
||
protected ViewModelService<T, TV> getViewModelService() {
|
||
ViewModelService<T, TV> service = controller.getViewModelService();
|
||
if (service == null) {
|
||
throw new IllegalArgumentException("ViewModelService is null");
|
||
}
|
||
return service;
|
||
}
|
||
|
||
/**
|
||
* 获取查询条件
|
||
*
|
||
* @return
|
||
*/
|
||
public ParamUtils.Builder getSpecification() {
|
||
TextField field = controller.searchKeyField;
|
||
if (field != null) {
|
||
return getViewModelService().getSpecification(field.getText());
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取查询条件
|
||
*
|
||
* @param searchText
|
||
* @return
|
||
*/
|
||
protected ParamUtils.Builder getSpecification(String searchText) {
|
||
return getViewModelService().getSpecification(searchText);
|
||
}
|
||
|
||
/**
|
||
* 当表格行被双击时触发
|
||
*
|
||
* @param item 被双击的行数据
|
||
*/
|
||
protected void onTableRowDoubleClickedAction(TV item) {
|
||
}
|
||
|
||
/**
|
||
* 更新页脚
|
||
*
|
||
* @param page
|
||
*/
|
||
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() + " 条");
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取表格排序
|
||
*
|
||
* @return
|
||
*/
|
||
public List<Sort.Order> getTableOrders() {
|
||
return TableViewUtils.getOrders(getTableView());
|
||
}
|
||
|
||
/**
|
||
* 获取表格排序
|
||
*
|
||
* @return
|
||
*/
|
||
public Sort getSortByTable() {
|
||
if (getTableView() == null) {
|
||
return Sort.unsorted();
|
||
}
|
||
return Sort.by(getTableOrders());
|
||
}
|
||
|
||
/**
|
||
* 获取分页参数
|
||
*
|
||
* @return
|
||
*/
|
||
public Pageable getPageable() {
|
||
Sort sort = getSortByTable();
|
||
return currentPageable.withSort(sort);
|
||
}
|
||
|
||
/**
|
||
* 显示在当前窗口为父窗口的新窗口
|
||
*
|
||
* @param <Controller> 控制器类型
|
||
* @param clz 控制器类
|
||
* @param model 数据
|
||
*/
|
||
protected <Controller extends AbstEntityController<T, TV>> void showInOwner(Class<Controller> clz, TV model) {
|
||
BaseController.show(clz, model, getTableView().getScene().getWindow());
|
||
}
|
||
}
|