# 客户端 Controller 类规则

## 1. 目录结构

客户端控制器位于`client/src/main/java/com/ecep/contract/controller/`目录下，按业务模块组织：

- **根目录**：包含基础控制器、抽象控制器和主窗口控制器
- **业务子包**：按业务领域组织，如`company/`、`project/`、`contract/`、`vendor/`等
- **tab/子包**：包含所有Tab皮肤控制器和相关接口、抽象类
- **table/子包**：包含表格相关控制器和单元格实现

## 2. 命名规范

- **基础控制器**：`BaseController.java`
- **抽象控制器**：以`Abst`开头，如`AbstEntityController.java`、`AbstManagerWindowController.java`
- **窗口控制器**：以`WindowController`结尾，如`HomeWindowController.java`
- **管理窗口控制器**：以`ManagerWindowController`结尾，如`CompanyManagerWindowController.java`
- **Tab皮肤控制器**：以`TabSkin`结尾，如`CompanyTabSkinBase.java`
- **管理器皮肤**：以`ManagerSkin`结尾，如`CompanyManagerSkin.java`
- **组件控制器**：如`OkHttpLoginController.java`

## 3. 继承关系

控制器类遵循以下继承层次结构：

- `BaseController`：所有控制器的基础类，提供窗口显示、异常处理等通用功能
  - `AbstEntityBasedController`：基于实体的控制器抽象类
    - `AbstManagerWindowController`：管理窗口控制器的抽象类，包含搜索、分页等功能
  - `AbstEntityController<T extends IdentityEntity, TV extends IdentityViewModel<T>>`：实体详情窗口控制器的抽象类
    - 具体业务窗口控制器，如`CompanyWindowController`、`ProjectWindowController`等

Tab相关控制器继承关系：

- `Skin`：皮肤接口基础
  - `TabSkin`：Tab皮肤接口
    - `AbstGenericTabSkin<C extends BaseController>`：通用Tab皮肤抽象类
      - `AbstEntityBasedTabSkin<C extends AbstEntityController<T, V>, T extends IdentityEntity, V extends IdentityViewModel<T>>`：基于实体的Tab皮肤抽象类
        - 具体业务Tab皮肤控制器，如`CompanyTabSkinBase.java`、`ContractTabSkinFiles.java`等

## 4. 注解使用

客户端控制器类应使用以下注解：

- `@Component`：声明为Spring组件，使其可被Spring容器管理
- `@Scope("prototype")`：设置为原型作用域，确保每次请求创建新实例
- `@Lazy`：延迟加载，提高应用启动性能
- `@FxmlPath("/ui/[业务模块]/[文件名].fxml")`：指定对应的FXML文件路径
- `@Autowired`：自动注入依赖的服务层组件

## 5. FXML文件规范

- **文件位置**：FXML文件通常位于`/client/src/main/resources/ui/`目录下，按业务模块组织子目录
- **命名规范**：使用小写字母和下划线，如`home.fxml`、`company/company.fxml`
- **关联方式**：通过`@FxmlPath`注解指定控制器对应的FXML文件路径
- **组件ID**：FXML文件中组件的id应与控制器中对应的字段名保持一致（驼峰命名法）

## 6. 控制器方法规范

### 6.1 初始化方法

- `initialize()`：控制器初始化方法，用于设置UI组件初始状态、绑定事件监听器等
  - 此方法会在FXML加载完成后自动调用

### 6.2 窗口生命周期方法

- `onShown(WindowEvent windowEvent)`：窗口显示时触发的方法
  - 用于执行窗口显示后的初始化操作，如数据加载、组件状态更新等
  - 通常需要调用`super.onShown(windowEvent)`确保父类逻辑执行

### 6.3 窗口显示方法

- `public static void show()`：静态方法，用于便捷地显示控制器对应的窗口
  - 通常有多个重载版本，支持不同参数（如viewModel、owner窗口等）
  - 内部调用`BaseController.show()`方法实现窗口显示逻辑

### 6.4 Tab相关方法

Tab皮肤控制器包含以下核心方法：

- `install()`：安装Tab皮肤，加载FXML并初始化UI组件
- `initializeUIComponents()`：初始化UI组件，设置数据绑定和事件监听
- `getTab()`：获取对应的Tab对象
- `save()`：保存Tab中的数据变更
- `dispose()`：释放资源，清理监听器等

## 7. 字段规范

- **UI组件字段**：与FXML文件中的组件id对应，使用`public`访问修饰符
- **服务依赖字段**：使用`@Autowired`注解注入，通常使用`private`访问修饰符
- **ViewModel字段**：通常作为控制器的核心数据模型，提供`getter/setter`方法
- **命名规范**：使用驼峰命名法，如`nameField`、`tabPane`、`saveBtn`等

## 8. 数据绑定与更新

- **ViewModel绑定**：控制器通常通过ViewModel与实体数据进行绑定
- **异步数据加载**：使用`CompletableFuture`进行异步数据加载，避免阻塞UI线程
- **UI更新**：在JavaFX应用线程中更新UI组件，可使用`Platform.runLater()`确保线程安全

## 9. 事件处理

- **按钮点击事件**：通过`setOnAction()`方法绑定事件处理器
- **表单字段变更**：可通过JavaFX的属性绑定机制监听字段变更
- **Tab切换事件**：实现`onTabShown()`方法处理Tab显示事件

## 10. 异常处理

- 使用`handleException()`方法统一处理异常
- 异常信息通常会显示在状态栏并记录日志
- 异步操作中的异常应通过`exceptionally()`或`handle()`方法捕获处理

## 11. 最佳实践

- **职责分离**：控制器应专注于UI交互和数据展示，业务逻辑应委托给服务层
- **避免重复代码**：利用抽象类和接口提取通用逻辑
- **资源管理**：及时释放资源，避免内存泄漏
- **线程安全**：确保在JavaFX应用线程中更新UI
- **代码可读性**：添加适当的注释，遵循命名规范

## 12. 示例代码结构

```java
@Lazy
@Scope("prototype")
@Component
@FxmlPath("/ui/[模块名]/[文件名].fxml")
public class [业务]WindowController extends AbstEntityController<[Vo类型], [ViewModel类型]> {
    private static final Logger logger = LoggerFactory.getLogger([业务]WindowController.class);
    
    @Autowired
    private [业务]Service [业务]Service;
    
    // UI组件字段
    public BorderPane root;
    public TabPane tabPane;
    public TextField nameField;
    // ...其他UI组件
    
    public static void show([Vo类型] entity, Window window) {
        show([ViewModel类型].from(entity), window);
    }
    
    public static void show([ViewModel类型] viewModel, Window window) {
        BaseController.show([业务]WindowController.class, viewModel, window);
    }
    
    @Override
    public void initialize() {
        // 初始化UI组件和事件监听
    }
    
    @Override
    public void onShown(WindowEvent windowEvent) {
        super.onShown(windowEvent);
        // 窗口显示后的逻辑
    }
}
```