فهرست منبع

fix:修复交易管理列表中没有买家和卖家名称的问题

yangyi 1 ماه پیش
والد
کامیت
5d5bb0d1eb

+ 267 - 131
client/src/main/java/space/anyi/client/controller/MainController.java

@@ -10,6 +10,7 @@ import javafx.scene.control.*;
 import javafx.scene.control.cell.PropertyValueFactory;
 import javafx.scene.layout.*;
 import javafx.geometry.Pos;
+import javafx.stage.Modality;
 import java.time.format.DateTimeFormatter;
 import space.anyi.client.App;
 import space.anyi.client.service.AiService;
@@ -20,6 +21,7 @@ import space.anyi.client.service.UserService;
 import space.anyi.client.util.LoginUserHolder;
 import space.anyi.common.dto.PageResult;
 import space.anyi.common.dto.R;
+import space.anyi.common.dto.BookDetailDTO;
 import space.anyi.common.entity.Book;
 import space.anyi.common.entity.TransactionRecord;
 import space.anyi.common.entity.User;
@@ -30,6 +32,7 @@ import space.anyi.common.dto.UserTransactionDTO;
 import space.anyi.common.dto.DeptDiscountDTO;
 import space.anyi.common.dto.SalesRecordDTO;
 import space.anyi.common.dto.StatisticsSummary;
+import space.anyi.common.dto.TransactionDetailDTO;
 import java.io.IOException;
 import java.math.BigDecimal;
 import java.util.List;
@@ -52,7 +55,15 @@ public class MainController {
     @FXML private VBox profilePane;
     @FXML private VBox sellerPane;
     @FXML private ScrollPane statsPane;
-    @FXML private ScrollPane adminPane;
+    @FXML private SplitPane adminPane;
+    @FXML private StackPane adminContentArea;
+
+    // Admin sub-panes
+    @FXML private VBox adminUserPane;
+    @FXML private VBox adminBookPane;
+    @FXML private VBox adminPendingPane;
+    @FXML private VBox adminTransPane;
+    @FXML private VBox adminAnalysisPane;
 
     // Browse tab
     @FXML private TextField searchField;
@@ -129,16 +140,6 @@ public class MainController {
 
     // Admin tab
     @FXML private TableView<User> userTable;
-    @FXML private TitledPane userFormPane;
-    @FXML private Button saveUserBtn;
-    @FXML private TextField adminUsername;
-    @FXML private PasswordField adminPassword;
-    @FXML private TextField adminNickname;
-    @FXML private TextField adminEmail;
-    @FXML private TextField adminPhone;
-    @FXML private TextField adminDept;
-    @FXML private ComboBox<String> adminRole;
-    @FXML private CheckBox adminSeller;
     @FXML private Button adminUserPrevBtn;
     @FXML private Button adminUserNextBtn;
     @FXML private Label adminUserPageLabel;
@@ -152,27 +153,29 @@ public class MainController {
     @FXML private TableColumn<User, Integer> userCreditCol;
     @FXML private TableColumn<User, Integer> userRoleCol;
     @FXML private TableColumn<User, Integer> userSellerCol;
-    @FXML private TableView<Book> adminBookTable;
-    @FXML private TableColumn<Book, Integer> adminBookIdCol;
-    @FXML private TableColumn<Book, String> adminBookTitleCol;
-    @FXML private TableColumn<Book, String> adminBookAuthorCol;
-    @FXML private TableColumn<Book, String> adminBookCategoryCol;
-    @FXML private TableColumn<Book, java.math.BigDecimal> adminBookPriceCol;
-    @FXML private TableColumn<Book, Integer> adminBookStockCol;
-    @FXML private TableColumn<Book, Integer> adminBookSellerCol;
-    @FXML private TableView<Book> pendingBookTable;
-    @FXML private TableColumn<Book, Integer> pendingBookIdCol;
-    @FXML private TableColumn<Book, String> pendingBookTitleCol;
-    @FXML private TableColumn<Book, String> pendingBookAuthorCol;
-    @FXML private TableColumn<Book, Integer> pendingBookSellerCol;
-    @FXML private TableColumn<Book, java.math.BigDecimal> pendingBookPriceCol;
-    @FXML private TableView<TransactionRecord> allTransTable;
-    @FXML private TableColumn<TransactionRecord, Integer> allTransIdCol;
-    @FXML private TableColumn<TransactionRecord, Integer> allTransBookIdCol;
-    @FXML private TableColumn<TransactionRecord, Integer> allTransBuyerIdCol;
-    @FXML private TableColumn<TransactionRecord, Integer> allTransNumCol;
-    @FXML private TableColumn<TransactionRecord, java.math.BigDecimal> allTransPriceCol;
-    @FXML private TableColumn<TransactionRecord, String> allTransTimeCol;
+    @FXML private TableColumn<User, Void> userActionCol;
+    @FXML private TableView<BookDetailDTO> adminBookTable;
+    @FXML private TableColumn<BookDetailDTO, Integer> adminBookIdCol;
+    @FXML private TableColumn<BookDetailDTO, String> adminBookTitleCol;
+    @FXML private TableColumn<BookDetailDTO, String> adminBookAuthorCol;
+    @FXML private TableColumn<BookDetailDTO, String> adminBookCategoryCol;
+    @FXML private TableColumn<BookDetailDTO, java.math.BigDecimal> adminBookPriceCol;
+    @FXML private TableColumn<BookDetailDTO, Integer> adminBookStockCol;
+    @FXML private TableColumn<BookDetailDTO, String> adminBookSellerCol;
+    @FXML private TableView<BookDetailDTO> pendingBookTable;
+    @FXML private TableColumn<BookDetailDTO, Integer> pendingBookIdCol;
+    @FXML private TableColumn<BookDetailDTO, String> pendingBookTitleCol;
+    @FXML private TableColumn<BookDetailDTO, String> pendingBookAuthorCol;
+    @FXML private TableColumn<BookDetailDTO, String> pendingBookSellerCol;
+    @FXML private TableColumn<BookDetailDTO, java.math.BigDecimal> pendingBookPriceCol;
+    @FXML private TableView<TransactionDetailDTO> allTransTable;
+    @FXML private TableColumn<TransactionDetailDTO, Integer> allTransIdCol;
+    @FXML private TableColumn<TransactionDetailDTO, String> allTransBookTitleCol;
+    @FXML private TableColumn<TransactionDetailDTO, String> allTransBuyerNameCol;
+    @FXML private TableColumn<TransactionDetailDTO, String> allTransSellerNameCol;
+    @FXML private TableColumn<TransactionDetailDTO, Integer> allTransNumCol;
+    @FXML private TableColumn<TransactionDetailDTO, java.math.BigDecimal> allTransPriceCol;
+    @FXML private TableColumn<TransactionDetailDTO, java.time.LocalDateTime> allTransTimeCol;
     @FXML private Button generateBtn;
     @FXML private TextArea analysisResult;
 
@@ -184,7 +187,6 @@ public class MainController {
 
     private Book selectedBook;
     private Book editingBook;
-    private User editingUser;
 
     @FXML private Button prevPageBtn;
     @FXML private Button nextPageBtn;
@@ -278,12 +280,27 @@ public class MainController {
         }
     }
 
+    private void switchAdminPane(VBox pane) {
+        for (javafx.scene.Node child : adminContentArea.getChildren()) {
+            child.setVisible(false);
+            child.setManaged(false);
+        }
+        pane.setVisible(true);
+        pane.setManaged(true);
+    }
+
     @FXML private void handleNavBrowse() { switchPane(browsePane); }
     @FXML private void handleNavTrans() { switchPane(transPane); handleLoadTransactions(); }
     @FXML private void handleNavProfile() { switchPane(profilePane); }
     @FXML private void handleNavSeller() { switchPane(sellerPane); handleLoadMyBooks(); }
     @FXML private void handleNavStats() { switchPane(statsPane); handleLoadStatistics(); }
-    @FXML private void handleNavAdmin() { switchPane(adminPane); handleLoadAllUsers(); handleLoadAllBooksAdmin(); handleLoadPendingBooks(); handleLoadAllTransactions(); }
+    @FXML private void handleNavAdmin() { switchPane(adminPane); switchAdminPane(adminUserPane); handleLoadAllUsers(); }
+
+    @FXML private void handleAdminNavUsers() { switchAdminPane(adminUserPane); handleLoadAllUsers(); }
+    @FXML private void handleAdminNavBooks() { switchAdminPane(adminBookPane); handleLoadAllBooksAdmin(); }
+    @FXML private void handleAdminNavPending() { switchAdminPane(adminPendingPane); handleLoadPendingBooks(); }
+    @FXML private void handleAdminNavTransactions() { switchAdminPane(adminTransPane); handleLoadAllTransactions(); }
+    @FXML private void handleAdminNavAnalysis() { switchAdminPane(adminAnalysisPane); }
 
     private void loadSubPane(String path) throws IOException {
         FXMLLoader loader = new FXMLLoader(getClass().getResource(path));
@@ -897,6 +914,60 @@ public class MainController {
         userCreditCol.setCellValueFactory(new PropertyValueFactory<>("credit"));
         userRoleCol.setCellValueFactory(new PropertyValueFactory<>("role"));
         userSellerCol.setCellValueFactory(new PropertyValueFactory<>("isSeller"));
+        userRoleCol.setCellFactory(col -> new TableCell<User, Integer>() {
+            @Override
+            protected void updateItem(Integer item, boolean empty) {
+                super.updateItem(item, empty);
+                if (empty || item == null) {
+                    setText(null);
+                } else {
+                    setText(item == 1 ? "管理员" : "学生");
+                }
+            }
+        });
+        userSellerCol.setCellFactory(col -> new TableCell<User, Integer>() {
+            @Override
+            protected void updateItem(Integer item, boolean empty) {
+                super.updateItem(item, empty);
+                if (empty || item == null) {
+                    setText(null);
+                } else {
+                    setText(item == 1 ? "是" : "否");
+                }
+            }
+        });
+
+        userActionCol.setCellFactory(col -> new TableCell<User, Void>() {
+            private final Button editBtn = new Button("编辑");
+            private final Button delBtn = new Button("删除");
+            private final HBox box = new HBox(6, editBtn, delBtn);
+            {
+                editBtn.getStyleClass().addAll("btn", "btn-warning");
+                editBtn.setStyle("-fx-padding: 4px 12px; -fx-font-size: 12px;");
+                delBtn.getStyleClass().addAll("btn", "btn-danger");
+                delBtn.setStyle("-fx-padding: 4px 12px; -fx-font-size: 12px;");
+                editBtn.setOnAction(e -> {
+                    User u = getTableView().getItems().get(getIndex());
+                    showUserDialog(u);
+                });
+                delBtn.setOnAction(e -> {
+                    User u = getTableView().getItems().get(getIndex());
+                    if (!confirm("确认删除用户「" + u.getUsername() + "」?此操作不可恢复。")) return;
+                    R<Void> r = userService.delete(u.getUserid());
+                    if (r.getCode() == 200) {
+                        showInfo("用户已删除");
+                        handleLoadAllUsers();
+                    } else {
+                        showAlert(r.getMessage());
+                    }
+                });
+            }
+            @Override
+            protected void updateItem(Void item, boolean empty) {
+                super.updateItem(item, empty);
+                setGraphic(empty ? null : box);
+            }
+        });
 
         adminBookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
         adminBookTitleCol.setCellValueFactory(new PropertyValueFactory<>("title"));
@@ -904,22 +975,38 @@ public class MainController {
         adminBookCategoryCol.setCellValueFactory(new PropertyValueFactory<>("category"));
         adminBookPriceCol.setCellValueFactory(new PropertyValueFactory<>("sellingPrice"));
         adminBookStockCol.setCellValueFactory(new PropertyValueFactory<>("stock"));
-        adminBookSellerCol.setCellValueFactory(new PropertyValueFactory<>("sellerId"));
+        adminBookSellerCol.setCellValueFactory(new PropertyValueFactory<>("sellerName"));
 
         pendingBookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
         pendingBookTitleCol.setCellValueFactory(new PropertyValueFactory<>("title"));
         pendingBookAuthorCol.setCellValueFactory(new PropertyValueFactory<>("author"));
-        pendingBookSellerCol.setCellValueFactory(new PropertyValueFactory<>("sellerId"));
+        pendingBookSellerCol.setCellValueFactory(new PropertyValueFactory<>("sellerName"));
         pendingBookPriceCol.setCellValueFactory(new PropertyValueFactory<>("sellingPrice"));
 
         allTransIdCol.setCellValueFactory(new PropertyValueFactory<>("transId"));
-        allTransBookIdCol.setCellValueFactory(new PropertyValueFactory<>("bookId"));
-        allTransBuyerIdCol.setCellValueFactory(new PropertyValueFactory<>("buyerId"));
+        allTransBookTitleCol.setCellValueFactory(new PropertyValueFactory<>("bookTitle"));
+        allTransBuyerNameCol.setCellValueFactory(new PropertyValueFactory<>("buyerName"));
+        allTransSellerNameCol.setCellValueFactory(new PropertyValueFactory<>("sellerName"));
         allTransNumCol.setCellValueFactory(new PropertyValueFactory<>("transactionNum"));
         allTransPriceCol.setCellValueFactory(new PropertyValueFactory<>("transactionPrice"));
         allTransTimeCol.setCellValueFactory(new PropertyValueFactory<>("transactionTime"));
+        java.time.format.DateTimeFormatter dtf = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+        allTransTimeCol.setCellFactory(col -> new TableCell<TransactionDetailDTO, java.time.LocalDateTime>() {
+            @Override
+            protected void updateItem(java.time.LocalDateTime item, boolean empty) {
+                super.updateItem(item, empty);
+                if (empty || item == null) {
+                    setText(null);
+                } else {
+                    setText(item.format(dtf));
+                }
+            }
+        });
 
-        adminRole.getItems().addAll("学生", "管理员");
+        userTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+        adminBookTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+        pendingBookTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+        allTransTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
     }
 
     @FXML
@@ -952,18 +1039,18 @@ public class MainController {
             showAlert("请先选择要编辑的用户");
             return;
         }
-        editingUser = sel;
-        adminUsername.setText(sel.getUsername());
-        adminPassword.setText(sel.getPassword());
-        adminNickname.setText(sel.getNickname());
-        adminEmail.setText(sel.getEmail());
-        adminPhone.setText(sel.getPhone());
-        adminDept.setText(sel.getDept());
-        adminRole.getSelectionModel().select(sel.getRole() != null ? sel.getRole() : 0);
-        adminSeller.setSelected(sel.getIsSeller() != null && sel.getIsSeller() == 1);
-        userFormPane.setText("编辑用户");
-        saveUserBtn.setText("保存修改");
-        userFormPane.setExpanded(true);
+        showUserDialog(sel);
+    }
+
+    private boolean confirm(String message) {
+        Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
+        alert.setTitle("确认操作");
+        alert.setHeaderText(null);
+        Label label = new Label(message);
+        label.setWrapText(true);
+        alert.getDialogPane().setContent(label);
+        alert.getButtonTypes().setAll(ButtonType.CANCEL, ButtonType.OK);
+        return alert.showAndWait().map(b -> b == ButtonType.OK).orElse(false);
     }
 
     @FXML
@@ -973,6 +1060,7 @@ public class MainController {
             showAlert("请先选择要重置密码的用户");
             return;
         }
+        if (!confirm("确认将用户「" + sel.getUsername() + "」的密码重置为 123456?")) return;
         sel.setPassword("123456");
         R<User> result = userService.update(sel.getUserid(), sel);
         if (result.getCode() == 200) {
@@ -1011,66 +1099,8 @@ public class MainController {
     }
 
     @FXML
-    private void handleToggleUserForm() {
-        userFormPane.setExpanded(!userFormPane.isExpanded());
-        if (userFormPane.isExpanded()) {
-            clearUserForm();
-        }
-    }
-
-    @FXML
-    private void handleCancelUserForm() {
-        clearUserForm();
-    }
-
-    @FXML
-    private void handleSaveUser() {
-        try {
-            String username = adminUsername.getText().trim();
-            String password = adminPassword.getText().trim();
-            if (username.isEmpty() || password.isEmpty()) {
-                showAlert("用户名和密码为必填项");
-                return;
-            }
-            if (editingUser != null) {
-                editingUser.setUsername(username);
-                editingUser.setPassword(password);
-                editingUser.setNickname(adminNickname.getText().trim());
-                editingUser.setEmail(adminEmail.getText().trim());
-                editingUser.setPhone(adminPhone.getText().trim());
-                editingUser.setDept(adminDept.getText().trim());
-                editingUser.setRole(adminRole.getSelectionModel().getSelectedIndex());
-                editingUser.setIsSeller(adminSeller.isSelected() ? 1 : 0);
-                R<User> result = userService.update(editingUser.getUserid(), editingUser);
-                if (result.getCode() == 200) {
-                    showInfo("用户信息已更新");
-                    clearUserForm();
-                    handleLoadAllUsers();
-                } else {
-                    showAlert(result.getMessage());
-                }
-            } else {
-                RegisterRequest req = new RegisterRequest();
-                req.setUsername(username);
-                req.setPassword(password);
-                req.setNickname(adminNickname.getText().trim());
-                req.setEmail(adminEmail.getText().trim());
-                req.setPhone(adminPhone.getText().trim());
-                req.setDept(adminDept.getText().trim());
-                req.setRole(adminRole.getSelectionModel().getSelectedIndex());
-                req.setIsSeller(adminSeller.isSelected() ? 1 : 0);
-                R<User> result = userService.register(req);
-                if (result.getCode() == 200) {
-                    showInfo("用户已添加");
-                    clearUserForm();
-                    handleLoadAllUsers();
-                } else {
-                    showAlert(result.getMessage());
-                }
-            }
-        } catch (Exception e) {
-            showAlert("输入无效:" + e.getMessage());
-        }
+    private void handleAddUser() {
+        showUserDialog(null);
     }
 
     @FXML
@@ -1080,6 +1110,7 @@ public class MainController {
             showAlert("请选择要删除的用户");
             return;
         }
+        if (!confirm("确认删除用户「" + sel.getUsername() + "」?此操作不可恢复。")) return;
         R<Void> result = userService.delete(sel.getUserid());
         if (result.getCode() == 200) {
             showInfo("用户已删除");
@@ -1091,7 +1122,7 @@ public class MainController {
 
     @FXML
     private void handleLoadAllBooksAdmin() {
-        R<List<Book>> result = bookService.listAll();
+        R<List<BookDetailDTO>> result = bookService.listAll();
         if (result.getCode() == 200 && result.getData() != null) {
             adminBookTable.setItems(FXCollections.observableArrayList(result.getData()));
         }
@@ -1099,11 +1130,12 @@ public class MainController {
 
     @FXML
     private void handleDeleteBookAdmin() {
-        Book sel = adminBookTable.getSelectionModel().getSelectedItem();
+        BookDetailDTO sel = adminBookTable.getSelectionModel().getSelectedItem();
         if (sel == null) {
             showAlert("请选择要删除的书籍");
             return;
         }
+        if (!confirm("确认删除书籍《" + sel.getTitle() + "》?此操作不可恢复。")) return;
         R<Void> result = bookService.delete(sel.getBookId());
         if (result.getCode() == 200) {
             showInfo("书籍已删除");
@@ -1115,7 +1147,7 @@ public class MainController {
 
     @FXML
     private void handleLoadPendingBooks() {
-        R<List<Book>> result = bookService.getPendingBooks();
+        R<List<BookDetailDTO>> result = bookService.getPendingBooks();
         if (result.getCode() == 200 && result.getData() != null) {
             pendingBookTable.setItems(FXCollections.observableArrayList(result.getData()));
         }
@@ -1123,7 +1155,7 @@ public class MainController {
 
     @FXML
     private void handleApproveBook() {
-        Book sel = pendingBookTable.getSelectionModel().getSelectedItem();
+        BookDetailDTO sel = pendingBookTable.getSelectionModel().getSelectedItem();
         if (sel == null) {
             showAlert("请先选择待审核的书籍");
             return;
@@ -1139,7 +1171,7 @@ public class MainController {
 
     @FXML
     private void handleRejectBook() {
-        Book sel = pendingBookTable.getSelectionModel().getSelectedItem();
+        BookDetailDTO sel = pendingBookTable.getSelectionModel().getSelectedItem();
         if (sel == null) {
             showAlert("请先选择待审核的书籍");
             return;
@@ -1155,7 +1187,7 @@ public class MainController {
 
     @FXML
     private void handleLoadAllTransactions() {
-        R<PageResult<TransactionRecord>> result = transactionService.listAll(adminTransPage, ADMIN_pageSize);
+        R<PageResult<TransactionDetailDTO>> result = transactionService.listAll(adminTransPage, ADMIN_pageSize);
         if (result.getCode() == 200 && result.getData() != null && result.getData().getRecords() != null) {
             allTransTable.setItems(FXCollections.observableArrayList(result.getData().getRecords()));
             long total = result.getData().getTotal();
@@ -1178,11 +1210,12 @@ public class MainController {
 
     @FXML
     private void handleDeleteTransaction() {
-        TransactionRecord sel = allTransTable.getSelectionModel().getSelectedItem();
+        TransactionDetailDTO sel = allTransTable.getSelectionModel().getSelectedItem();
         if (sel == null) {
             showAlert("请选择要删除的交易");
             return;
         }
+        if (!confirm("确认删除交易 ID=" + sel.getTransId() + "?此操作不可恢复。")) return;
         R<Void> result = transactionService.delete(sel.getTransId());
         if (result.getCode() == 200) {
             showInfo("交易已删除");
@@ -1248,19 +1281,122 @@ public class MainController {
         bookFormPane.setExpanded(false);
     }
 
-    private void clearUserForm() {
-        editingUser = null;
-        adminUsername.clear();
-        adminPassword.clear();
-        adminNickname.clear();
-        adminEmail.clear();
-        adminPhone.clear();
-        adminDept.clear();
-        adminRole.getSelectionModel().select(0);
-        adminSeller.setSelected(false);
-        userFormPane.setText("添加用户");
-        saveUserBtn.setText("保存");
-        userFormPane.setExpanded(false);
+    private void showUserDialog(User user) {
+        boolean isNew = (user == null);
+        Dialog<User> dialog = new Dialog<>();
+        dialog.setTitle(isNew ? "添加用户" : "编辑用户");
+        dialog.initModality(Modality.NONE);
+        dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
+        dialog.getDialogPane().setPrefWidth(560);
+
+        GridPane grid = new GridPane();
+        grid.setHgap(12);
+        grid.setVgap(10);
+        grid.setPadding(new javafx.geometry.Insets(20));
+        ColumnConstraints col1 = new ColumnConstraints(60);
+        ColumnConstraints col2 = new ColumnConstraints(190, 190, Double.MAX_VALUE);
+        ColumnConstraints col3 = new ColumnConstraints(60);
+        ColumnConstraints col4 = new ColumnConstraints(190, 190, Double.MAX_VALUE);
+        grid.getColumnConstraints().addAll(col1, col2, col3, col4);
+
+        TextField usernameField = new TextField();
+        usernameField.setPromptText("必填");
+        PasswordField passwordField = new PasswordField();
+        passwordField.setPromptText("必填");
+        TextField nicknameField = new TextField();
+        TextField emailField = new TextField();
+        TextField phoneField = new TextField();
+        TextField deptField = new TextField();
+        ComboBox<String> roleCombo = new ComboBox<>();
+        roleCombo.getItems().addAll("学生", "管理员");
+        ComboBox<String> sellerCombo = new ComboBox<>();
+        sellerCombo.getItems().addAll("否", "是");
+
+        if (!isNew) {
+            usernameField.setText(user.getUsername());
+            passwordField.setText(user.getPassword());
+            nicknameField.setText(user.getNickname());
+            emailField.setText(user.getEmail());
+            phoneField.setText(user.getPhone());
+            deptField.setText(user.getDept());
+            roleCombo.getSelectionModel().select(user.getRole() != null ? user.getRole() : 0);
+            sellerCombo.getSelectionModel().select(user.getIsSeller() != null && user.getIsSeller() == 1 ? 1 : 0);
+        } else {
+            sellerCombo.getSelectionModel().select(0);
+        }
+
+        grid.add(new Label("用户名:"), 0, 0);
+        grid.add(usernameField, 1, 0);
+        grid.add(new Label("邮箱:"), 2, 0);
+        grid.add(emailField, 3, 0);
+        grid.add(new Label("密码:"), 0, 1);
+        grid.add(passwordField, 1, 1);
+        grid.add(new Label("电话:"), 2, 1);
+        grid.add(phoneField, 3, 1);
+        grid.add(new Label("昵称:"), 0, 2);
+        grid.add(nicknameField, 1, 2);
+        grid.add(new Label("院系:"), 2, 2);
+        grid.add(deptField, 3, 2);
+        grid.add(new Label("角色:"), 0, 3);
+        grid.add(roleCombo, 1, 3);
+        grid.add(new Label("卖家:"), 2, 3);
+        grid.add(sellerCombo, 3, 3);
+
+        dialog.getDialogPane().setContent(grid);
+
+        Node okBtn = dialog.getDialogPane().lookupButton(ButtonType.OK);
+        if (isNew) {
+            okBtn.setDisable(true);
+            usernameField.textProperty().addListener((obs, old, val) ->
+                okBtn.setDisable(val.trim().isEmpty() || passwordField.getText().trim().isEmpty()));
+            passwordField.textProperty().addListener((obs, old, val) ->
+                okBtn.setDisable(val.trim().isEmpty() || usernameField.getText().trim().isEmpty()));
+        }
+
+        dialog.setResultConverter(btn -> {
+            if (btn == ButtonType.OK) {
+                User u = isNew ? new User() : user;
+                u.setUsername(usernameField.getText().trim());
+                u.setPassword(passwordField.getText().trim());
+                u.setNickname(nicknameField.getText().trim());
+                u.setEmail(emailField.getText().trim());
+                u.setPhone(phoneField.getText().trim());
+                u.setDept(deptField.getText().trim());
+                u.setRole(roleCombo.getSelectionModel().getSelectedIndex());
+                u.setIsSeller(sellerCombo.getSelectionModel().getSelectedIndex());
+                return u;
+            }
+            return null;
+        });
+
+        dialog.showAndWait().ifPresent(u -> {
+            if (isNew) {
+                RegisterRequest req = new RegisterRequest();
+                req.setUsername(u.getUsername());
+                req.setPassword(u.getPassword());
+                req.setNickname(u.getNickname());
+                req.setEmail(u.getEmail());
+                req.setPhone(u.getPhone());
+                req.setDept(u.getDept());
+                req.setRole(u.getRole());
+                req.setIsSeller(u.getIsSeller());
+                R<User> r = userService.register(req);
+                if (r.getCode() == 200) {
+                    showInfo("用户已添加");
+                    handleLoadAllUsers();
+                } else {
+                    showAlert(r.getMessage());
+                }
+            } else {
+                R<User> r = userService.update(u.getUserid(), u);
+                if (r.getCode() == 200) {
+                    showInfo("用户信息已更新");
+                    handleLoadAllUsers();
+                } else {
+                    showAlert(r.getMessage());
+                }
+            }
+        });
     }
 
     private void showAlert(String message) {

+ 5 - 4
client/src/main/java/space/anyi/client/service/BookService.java

@@ -1,6 +1,7 @@
 package space.anyi.client.service;
 
 import com.fasterxml.jackson.core.type.TypeReference;
+import space.anyi.common.dto.BookDetailDTO;
 import space.anyi.common.dto.PageResult;
 import space.anyi.common.dto.R;
 import space.anyi.common.dto.SalesRecordDTO;
@@ -31,8 +32,8 @@ public class BookService {
         return HttpClientUtil.get("/api/book/my?sellerId=" + sellerId, new TypeReference<R<List<Book>>>() {});
     }
 
-    public R<List<Book>> listAll() {
-        return HttpClientUtil.get("/api/book/all", new TypeReference<R<List<Book>>>() {});
+    public R<List<BookDetailDTO>> listAll() {
+        return HttpClientUtil.get("/api/book/all", new TypeReference<R<List<BookDetailDTO>>>() {});
     }
 
     public R<Book> create(Book book) {
@@ -55,8 +56,8 @@ public class BookService {
         return HttpClientUtil.get("/api/book/sales-summary?sellerId=" + sellerId, new TypeReference<R<Integer>>() {});
     }
 
-    public R<List<Book>> getPendingBooks() {
-        return HttpClientUtil.get("/api/book/pending", new TypeReference<R<List<Book>>>() {});
+    public R<List<BookDetailDTO>> getPendingBooks() {
+        return HttpClientUtil.get("/api/book/pending", new TypeReference<R<List<BookDetailDTO>>>() {});
     }
 
     public R<Book> audit(Integer bookId, Integer status) {

+ 3 - 2
client/src/main/java/space/anyi/client/service/TransactionService.java

@@ -3,6 +3,7 @@ package space.anyi.client.service;
 import com.fasterxml.jackson.core.type.TypeReference;
 import space.anyi.common.dto.PageResult;
 import space.anyi.common.dto.R;
+import space.anyi.common.dto.TransactionDetailDTO;
 import space.anyi.common.dto.UserTransactionDTO;
 import space.anyi.common.entity.TransactionRecord;
 import space.anyi.client.util.HttpClientUtil;
@@ -32,9 +33,9 @@ public class TransactionService {
         return HttpClientUtil.get("/api/transaction/user/" + userId + "/detail", new TypeReference<R<List<UserTransactionDTO>>>() {});
     }
 
-    public R<PageResult<TransactionRecord>> listAll(int page, int size) {
+    public R<PageResult<TransactionDetailDTO>> listAll(int page, int size) {
         return HttpClientUtil.get("/api/transaction?page=" + page + "&size=" + size,
-                new TypeReference<R<PageResult<TransactionRecord>>>() {});
+                new TypeReference<R<PageResult<TransactionDetailDTO>>>() {});
     }
 
     public R<TransactionRecord> update(Integer id, TransactionRecord record) {

+ 40 - 0
client/src/main/resources/css/style.css

@@ -394,3 +394,43 @@
     -fx-font-size: 13px;
     -fx-text-fill: -fx-text-secondary;
 }
+
+/* ── 管理 SplitPane 菜单 ── */
+.admin-menu {
+    -fx-background-color: -fx-bg-card;
+    -fx-border-color: -color-border;
+    -fx-border-width: 0 1px 0 0;
+}
+.admin-menu .button {
+    -fx-background-radius: 6px;
+    -fx-padding: 10px 16px;
+    -fx-font-size: 14px;
+    -fx-alignment: center-left;
+    -fx-background-color: transparent;
+    -fx-text-fill: -fx-text;
+    -fx-cursor: hand;
+}
+.admin-menu .button:hover {
+    -fx-background-color: -fx-primary-light;
+    -fx-text-fill: -fx-primary;
+}
+.admin-menu .button:focused,
+.admin-menu .button:pressed {
+    -fx-background-color: -fx-primary;
+    -fx-text-fill: white;
+    -fx-font-weight: bold;
+}
+.admin-scroll {
+    -fx-background-color: transparent;
+    -fx-border-width: 0;
+}
+.split-pane {
+    -fx-background-color: transparent;
+    -fx-box-border: transparent;
+    -fx-padding: 0;
+}
+.split-pane > .split-pane-divider {
+    -fx-background-color: -color-border;
+    -fx-padding: 0 1px;
+    -fx-border-width: 0;
+}

+ 126 - 135
client/src/main/resources/fxml/admin.fxml

@@ -4,143 +4,134 @@
 <?import javafx.scene.control.*?>
 <?import javafx.scene.layout.*?>
 
-<ScrollPane fx:id="adminPane" xmlns="http://javafx.com/javafx/23" xmlns:fx="http://javafx.com/fxml/1" visible="false" managed="false" fitToWidth="true">
-    <VBox spacing="16">
-        <padding><Insets top="16" right="16" bottom="16" left="16"/></padding>
-
-        <VBox styleClass="well" spacing="12">
-            <HBox spacing="10" alignment="CENTER_LEFT">
-                <Label text="👥 用户管理" style="-fx-font-size: 16px; -fx-font-weight: bold;" HBox.hgrow="ALWAYS"/>
-                <Button text="刷新" onAction="#handleLoadAllUsers" styleClass="btn,btn-secondary"/>
-                <Button text="+ 添加" onAction="#handleToggleUserForm" styleClass="btn,btn-success"/>
-            </HBox>
-            <TableView fx:id="userTable" prefHeight="160">
-                <columns>
-                    <TableColumn fx:id="userIdCol" text="ID" prefWidth="40"/>
-                    <TableColumn fx:id="userNameCol" text="用户名" prefWidth="100"/>
-                    <TableColumn fx:id="userNicknameCol" text="昵称" prefWidth="100"/>
-                    <TableColumn fx:id="userDeptCol" text="院系" prefWidth="120"/>
-                    <TableColumn fx:id="userCreditCol" text="信用" prefWidth="60"/>
-                    <TableColumn fx:id="userRoleCol" text="角色" prefWidth="60"/>
-                    <TableColumn fx:id="userSellerCol" text="卖家" prefWidth="60"/>
-                    <TableColumn text="操作" prefWidth="220"/>
-                </columns>
-            </TableView>
-            <HBox spacing="8">
-                <Button text="编辑选中" onAction="#handleEditUser" styleClass="btn,btn-warning"/>
-                <Button text="重置密码" onAction="#handleResetPassword" styleClass="btn,btn-secondary"/>
-                <Button text="修改信用分" onAction="#handleEditCredit" styleClass="btn,btn-info"/>
-                <Button text="删除选中" onAction="#handleDeleteUser" styleClass="btn,btn-danger"/>
-            </HBox>
-            <HBox spacing="8" alignment="CENTER">
-                <Button fx:id="adminUserPrevBtn" text="← 上一页" onAction="#handleAdminUserPrev" styleClass="btn,btn-secondary"/>
-                <Label fx:id="adminUserPageLabel" text="第 1 页"/>
-                <Button fx:id="adminUserNextBtn" text="下一页 →" onAction="#handleAdminUserNext" styleClass="btn,btn-secondary"/>
-            </HBox>
-            <TitledPane fx:id="userFormPane" text="添加用户" expanded="false" animated="true">
-                <GridPane hgap="16" vgap="10">
-                    <padding><Insets top="12" right="16" bottom="12" left="16"/></padding>
-                    <columnConstraints>
-                        <ColumnConstraints prefWidth="70"/>
-                        <ColumnConstraints hgrow="ALWAYS" prefWidth="200"/>
-                        <ColumnConstraints prefWidth="30"/>
-                        <ColumnConstraints prefWidth="70"/>
-                        <ColumnConstraints hgrow="ALWAYS" prefWidth="200"/>
-                    </columnConstraints>
-                    <Label text="用户名" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
-                    <TextField fx:id="adminUsername" styleClass="form-control" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
-                    <Label text="密码" GridPane.rowIndex="0" GridPane.columnIndex="3"/>
-                    <PasswordField fx:id="adminPassword" styleClass="form-control" GridPane.rowIndex="0" GridPane.columnIndex="4"/>
-                    <Label text="昵称" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
-                    <TextField fx:id="adminNickname" styleClass="form-control" GridPane.rowIndex="1" GridPane.columnIndex="1"/>
-                    <Label text="邮箱" GridPane.rowIndex="1" GridPane.columnIndex="3"/>
-                    <TextField fx:id="adminEmail" styleClass="form-control" GridPane.rowIndex="1" GridPane.columnIndex="4"/>
-                    <Label text="电话" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
-                    <TextField fx:id="adminPhone" styleClass="form-control" GridPane.rowIndex="2" GridPane.columnIndex="1"/>
-                    <Label text="院系" GridPane.rowIndex="2" GridPane.columnIndex="3"/>
-                    <TextField fx:id="adminDept" styleClass="form-control" GridPane.rowIndex="2" GridPane.columnIndex="4"/>
-                    <Label text="角色" GridPane.rowIndex="3" GridPane.columnIndex="0"/>
-                    <ComboBox fx:id="adminRole" styleClass="form-control" GridPane.rowIndex="3" GridPane.columnIndex="1"/>
-                    <Label text="卖家" GridPane.rowIndex="3" GridPane.columnIndex="3"/>
-                    <CheckBox fx:id="adminSeller" GridPane.rowIndex="3" GridPane.columnIndex="4"/>
-                    <HBox spacing="10" GridPane.rowIndex="4" GridPane.columnIndex="4" alignment="CENTER_RIGHT">
-                        <Button text="取消" onAction="#handleCancelUserForm" styleClass="btn,btn-secondary"/>
-                        <Button fx:id="saveUserBtn" text="保存" onAction="#handleSaveUser" styleClass="btn,btn-success"/>
-                    </HBox>
-                </GridPane>
-            </TitledPane>
-        </VBox>
-
-        <VBox styleClass="well" spacing="12">
-            <HBox spacing="10" alignment="CENTER_LEFT">
-                <Label text="📚 书籍管理" style="-fx-font-size: 16px; -fx-font-weight: bold;" HBox.hgrow="ALWAYS"/>
-                <Button text="刷新" onAction="#handleLoadAllBooksAdmin" styleClass="btn,btn-secondary"/>
-            </HBox>
-            <TableView fx:id="adminBookTable" prefHeight="160">
-                <columns>
-                    <TableColumn fx:id="adminBookIdCol" text="ID" prefWidth="40"/>
-                    <TableColumn fx:id="adminBookTitleCol" text="书名" prefWidth="160"/>
-                    <TableColumn fx:id="adminBookAuthorCol" text="作者" prefWidth="100"/>
-                    <TableColumn fx:id="adminBookCategoryCol" text="分类" prefWidth="70"/>
-                    <TableColumn fx:id="adminBookPriceCol" text="售价" prefWidth="80"/>
-                    <TableColumn fx:id="adminBookStockCol" text="库存" prefWidth="60"/>
-                    <TableColumn fx:id="adminBookSellerCol" text="卖家" prefWidth="50"/>
-                </columns>
-            </TableView>
-            <Button text="删除选中书籍" onAction="#handleDeleteBookAdmin" styleClass="btn,btn-danger"/>
+<SplitPane fx:id="adminPane" xmlns="http://javafx.com/javafx/23" xmlns:fx="http://javafx.com/fxml/1"
+            dividerPositions="0.22" visible="false" managed="false">
+    <items>
+        <VBox styleClass="admin-menu" spacing="2" minWidth="160" maxWidth="200">
+            <padding><Insets top="12" right="8" bottom="12" left="8"/></padding>
+            <Label text="管理" style="-fx-font-weight: bold; -fx-font-size: 13px; -fx-text-fill: -fx-text-muted; -fx-padding: 4 12 8 12;"/>
+            <Button text="👥 用户管理" onAction="#handleAdminNavUsers" maxWidth="Infinity"/>
+            <Button text="📚 书籍管理" onAction="#handleAdminNavBooks" maxWidth="Infinity"/>
+            <Button text="⏳ 待审核书籍" onAction="#handleAdminNavPending" maxWidth="Infinity"/>
+            <Button text="💳 交易管理" onAction="#handleAdminNavTransactions" maxWidth="Infinity"/>
+            <Button text="🤖 AI 分析" onAction="#handleAdminNavAnalysis" maxWidth="Infinity"/>
         </VBox>
+        <StackPane fx:id="adminContentArea">
+            <!-- ── 用户管理 ── -->
+            <VBox fx:id="adminUserPane" spacing="16" visible="true" managed="true" maxHeight="Infinity">
+                <padding><Insets top="16" right="24" bottom="16" left="24"/></padding>
+                <VBox VBox.vgrow="ALWAYS" styleClass="well" spacing="12">
+                    <HBox spacing="10" alignment="CENTER_LEFT">
+                        <Label text="👥 用户管理" style="-fx-font-size: 16px; -fx-font-weight: bold;" HBox.hgrow="ALWAYS"/>
+                        <Button text="刷新" onAction="#handleLoadAllUsers" styleClass="btn,btn-secondary"/>
+                        <Button text="重置密码" onAction="#handleResetPassword" styleClass="btn,btn-secondary"/>
+                        <Button text="修改信用分" onAction="#handleEditCredit" styleClass="btn,btn-info"/>
+                        <Button text="+ 添加" onAction="#handleAddUser" styleClass="btn,btn-success"/>
+                    </HBox>
+                    <TableView fx:id="userTable" VBox.vgrow="ALWAYS">
+                        <columns>
+                            <TableColumn fx:id="userIdCol" text="ID" prefWidth="40"/>
+                            <TableColumn fx:id="userNameCol" text="用户名" prefWidth="100"/>
+                            <TableColumn fx:id="userNicknameCol" text="昵称" prefWidth="100"/>
+                            <TableColumn fx:id="userDeptCol" text="院系" prefWidth="120"/>
+                            <TableColumn fx:id="userCreditCol" text="信用" prefWidth="60"/>
+                            <TableColumn fx:id="userRoleCol" text="角色" prefWidth="60"/>
+                            <TableColumn fx:id="userSellerCol" text="卖家" prefWidth="60"/>
+                            <TableColumn fx:id="userActionCol" text="操作" prefWidth="160"/>
+                        </columns>
+                    </TableView>
+                    <HBox spacing="8" alignment="CENTER">
+                        <Button fx:id="adminUserPrevBtn" text="← 上一页" onAction="#handleAdminUserPrev" styleClass="btn,btn-secondary"/>
+                        <Label fx:id="adminUserPageLabel" text="第 1 页"/>
+                        <Button fx:id="adminUserNextBtn" text="下一页 →" onAction="#handleAdminUserNext" styleClass="btn,btn-secondary"/>
+                    </HBox>
+                </VBox>
+            </VBox>
 
-        <VBox styleClass="well" spacing="12">
-            <HBox spacing="10" alignment="CENTER_LEFT">
-                <Label text="⏳ 待审核书籍" style="-fx-font-size: 16px; -fx-font-weight: bold;" HBox.hgrow="ALWAYS"/>
-                <Button text="刷新" onAction="#handleLoadPendingBooks" styleClass="btn,btn-secondary"/>
-            </HBox>
-            <TableView fx:id="pendingBookTable" prefHeight="150">
-                <columns>
-                    <TableColumn fx:id="pendingBookIdCol" text="ID" prefWidth="40"/>
-                    <TableColumn fx:id="pendingBookTitleCol" text="书名" prefWidth="180"/>
-                    <TableColumn fx:id="pendingBookAuthorCol" text="作者" prefWidth="120"/>
-                    <TableColumn fx:id="pendingBookSellerCol" text="卖家" prefWidth="60"/>
-                    <TableColumn fx:id="pendingBookPriceCol" text="售价" prefWidth="80"/>
-                </columns>
-            </TableView>
-            <HBox spacing="8">
-                <Button text="通过" onAction="#handleApproveBook" prefWidth="120" styleClass="btn,btn-success"/>
-                <Button text="拒绝" onAction="#handleRejectBook" prefWidth="120" styleClass="btn,btn-danger"/>
-            </HBox>
-        </VBox>
+            <!-- ── 书籍管理 ── -->
+            <VBox fx:id="adminBookPane" spacing="16" visible="false" managed="false" maxHeight="Infinity">
+                <padding><Insets top="16" right="24" bottom="16" left="24"/></padding>
+                <VBox VBox.vgrow="ALWAYS" styleClass="well" spacing="12">
+                    <HBox spacing="10" alignment="CENTER_LEFT">
+                        <Label text="📚 书籍管理" style="-fx-font-size: 16px; -fx-font-weight: bold;" HBox.hgrow="ALWAYS"/>
+                        <Button text="刷新" onAction="#handleLoadAllBooksAdmin" styleClass="btn,btn-secondary"/>
+                        <Button text="删除选中书籍" onAction="#handleDeleteBookAdmin" styleClass="btn,btn-danger"/>
+                    </HBox>
+                    <TableView fx:id="adminBookTable" VBox.vgrow="ALWAYS">
+                        <columns>
+                            <TableColumn fx:id="adminBookIdCol" text="ID" prefWidth="40"/>
+                            <TableColumn fx:id="adminBookTitleCol" text="书名" prefWidth="160"/>
+                            <TableColumn fx:id="adminBookAuthorCol" text="作者" prefWidth="100"/>
+                            <TableColumn fx:id="adminBookCategoryCol" text="分类" prefWidth="70"/>
+                            <TableColumn fx:id="adminBookPriceCol" text="售价" prefWidth="80"/>
+                            <TableColumn fx:id="adminBookStockCol" text="库存" prefWidth="60"/>
+                            <TableColumn fx:id="adminBookSellerCol" text="卖家" prefWidth="50"/>
+                        </columns>
+                    </TableView>
+                </VBox>
+            </VBox>
 
-        <VBox styleClass="well" spacing="12">
-            <HBox spacing="10" alignment="CENTER_LEFT">
-                <Label text="💳 交易管理" style="-fx-font-size: 16px; -fx-font-weight: bold;" HBox.hgrow="ALWAYS"/>
-                <Button text="刷新" onAction="#handleLoadAllTransactions" styleClass="btn,btn-secondary"/>
-            </HBox>
-            <TableView fx:id="allTransTable" prefHeight="160">
-                <columns>
-                    <TableColumn fx:id="allTransIdCol" text="ID" prefWidth="50"/>
-                    <TableColumn fx:id="allTransBookIdCol" text="书籍" prefWidth="60"/>
-                    <TableColumn fx:id="allTransBuyerIdCol" text="买家" prefWidth="60"/>
-                    <TableColumn fx:id="allTransNumCol" text="数量" prefWidth="50"/>
-                    <TableColumn fx:id="allTransPriceCol" text="总价" prefWidth="100"/>
-                    <TableColumn fx:id="allTransTimeCol" text="时间" prefWidth="180"/>
-                </columns>
-            </TableView>
-            <HBox spacing="8" alignment="CENTER">
-                <Button fx:id="adminTransPrevBtn" text="← 上一页" onAction="#handleAdminTransPrev" styleClass="btn,btn-secondary"/>
-                <Label fx:id="adminTransPageLabel" text="第 1 页"/>
-                <Button fx:id="adminTransNextBtn" text="下一页 →" onAction="#handleAdminTransNext" styleClass="btn,btn-secondary"/>
-            </HBox>
-            <Button text="删除选中交易" onAction="#handleDeleteTransaction" styleClass="btn,btn-danger"/>
-        </VBox>
+            <!-- ── 待审核书籍 ── -->
+            <VBox fx:id="adminPendingPane" spacing="16" visible="false" managed="false" maxHeight="Infinity">
+                <padding><Insets top="16" right="24" bottom="16" left="24"/></padding>
+                <VBox VBox.vgrow="ALWAYS" styleClass="well" spacing="12">
+                    <HBox spacing="10" alignment="CENTER_LEFT">
+                        <Label text="⏳ 待审核书籍" style="-fx-font-size: 16px; -fx-font-weight: bold;" HBox.hgrow="ALWAYS"/>
+                        <Button text="刷新" onAction="#handleLoadPendingBooks" styleClass="btn,btn-secondary"/>
+                        <Button text="通过" onAction="#handleApproveBook" prefWidth="120" styleClass="btn,btn-success"/>
+                        <Button text="拒绝" onAction="#handleRejectBook" prefWidth="120" styleClass="btn,btn-danger"/>
+                    </HBox>
+                    <TableView fx:id="pendingBookTable" VBox.vgrow="ALWAYS">
+                        <columns>
+                            <TableColumn fx:id="pendingBookIdCol" text="ID" prefWidth="40"/>
+                            <TableColumn fx:id="pendingBookTitleCol" text="书名" prefWidth="180"/>
+                            <TableColumn fx:id="pendingBookAuthorCol" text="作者" prefWidth="120"/>
+                            <TableColumn fx:id="pendingBookSellerCol" text="卖家" prefWidth="60"/>
+                            <TableColumn fx:id="pendingBookPriceCol" text="售价" prefWidth="80"/>
+                        </columns>
+                    </TableView>
+                </VBox>
+            </VBox>
 
-        <VBox styleClass="well" spacing="12">
-            <HBox spacing="10" alignment="CENTER_LEFT">
-                <Label text="🤖 AI 分析" style="-fx-font-size: 16px; -fx-font-weight: bold;" HBox.hgrow="ALWAYS"/>
-                <Button fx:id="generateBtn" text="生成分析报告" onAction="#handleGenerateAnalysis" styleClass="btn,btn-primary"/>
-            </HBox>
-            <TextArea fx:id="analysisResult" editable="false" wrapText="true" prefHeight="200" styleClass="form-control"/>
-        </VBox>
+            <!-- ── 交易管理 ── -->
+            <VBox fx:id="adminTransPane" spacing="16" visible="false" managed="false" maxHeight="Infinity">
+                <padding><Insets top="16" right="24" bottom="16" left="24"/></padding>
+                <VBox VBox.vgrow="ALWAYS" styleClass="well" spacing="12">
+                    <HBox spacing="10" alignment="CENTER_LEFT">
+                        <Label text="💳 交易管理" style="-fx-font-size: 16px; -fx-font-weight: bold;" HBox.hgrow="ALWAYS"/>
+                        <Button text="刷新" onAction="#handleLoadAllTransactions" styleClass="btn,btn-secondary"/>
+                        <Button text="删除选中交易" onAction="#handleDeleteTransaction" styleClass="btn,btn-danger"/>
+                    </HBox>
+                    <TableView fx:id="allTransTable" VBox.vgrow="ALWAYS">
+                        <columns>
+                            <TableColumn fx:id="allTransIdCol" text="ID" prefWidth="50"/>
+                            <TableColumn fx:id="allTransBookTitleCol" text="书名" prefWidth="160"/>
+                            <TableColumn fx:id="allTransBuyerNameCol" text="买家" prefWidth="80"/>
+                            <TableColumn fx:id="allTransSellerNameCol" text="卖家" prefWidth="80"/>
+                            <TableColumn fx:id="allTransNumCol" text="数量" prefWidth="50"/>
+                            <TableColumn fx:id="allTransPriceCol" text="总价" prefWidth="100"/>
+                            <TableColumn fx:id="allTransTimeCol" text="时间" prefWidth="180"/>
+                        </columns>
+                    </TableView>
+                    <HBox spacing="8" alignment="CENTER">
+                        <Button fx:id="adminTransPrevBtn" text="← 上一页" onAction="#handleAdminTransPrev" styleClass="btn,btn-secondary"/>
+                        <Label fx:id="adminTransPageLabel" text="第 1 页"/>
+                        <Button fx:id="adminTransNextBtn" text="下一页 →" onAction="#handleAdminTransNext" styleClass="btn,btn-secondary"/>
+                    </HBox>
+                </VBox>
+            </VBox>
 
-        <Label text=" " prefHeight="20"/>
-    </VBox>
-</ScrollPane>
+            <!-- ── AI 分析 ── -->
+            <VBox fx:id="adminAnalysisPane" spacing="16" visible="false" managed="false" maxHeight="Infinity">
+                <padding><Insets top="16" right="24" bottom="16" left="24"/></padding>
+                <VBox VBox.vgrow="ALWAYS" styleClass="well" spacing="12">
+                    <HBox spacing="10" alignment="CENTER_LEFT">
+                        <Label text="🤖 AI 分析" style="-fx-font-size: 16px; -fx-font-weight: bold;" HBox.hgrow="ALWAYS"/>
+                        <Button fx:id="generateBtn" text="生成分析报告" onAction="#handleGenerateAnalysis" styleClass="btn,btn-primary"/>
+                    </HBox>
+                    <TextArea fx:id="analysisResult" editable="false" wrapText="true" VBox.vgrow="ALWAYS" styleClass="form-control"/>
+                </VBox>
+            </VBox>
+        </StackPane>
+    </items>
+</SplitPane>

+ 17 - 0
common/src/main/java/space/anyi/common/dto/BookDetailDTO.java

@@ -0,0 +1,17 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+
+@Data
+public class BookDetailDTO {
+    private Integer bookId;
+    private String sellerName;
+    private String title;
+    private String author;
+    private String category;
+    private BigDecimal originalPrice;
+    private BigDecimal sellingPrice;
+    private Integer stock;
+    private Integer status;
+}

+ 16 - 0
common/src/main/java/space/anyi/common/dto/TransactionDetailDTO.java

@@ -0,0 +1,16 @@
+package space.anyi.common.dto;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+public class TransactionDetailDTO {
+    private Integer transId;
+    private String bookTitle;
+    private String buyerName;
+    private String sellerName;
+    private Integer transactionNum;
+    private BigDecimal transactionPrice;
+    private LocalDateTime transactionTime;
+}

+ 5 - 4
server/src/main/java/space/anyi/server/controller/BookController.java

@@ -1,6 +1,7 @@
 package space.anyi.server.controller;
 
 import org.springframework.web.bind.annotation.*;
+import space.anyi.common.dto.BookDetailDTO;
 import space.anyi.common.dto.PageResult;
 import space.anyi.common.dto.R;
 import space.anyi.common.dto.SalesRecordDTO;
@@ -35,8 +36,8 @@ public class BookController {
     }
 
     @GetMapping("/all")
-    public R<List<Book>> listAll() {
-        return R.ok(bookService.listAll());
+    public R<List<BookDetailDTO>> listAll() {
+        return R.ok(bookService.listAllWithSellerName());
     }
 
     @GetMapping("/{id}")
@@ -64,8 +65,8 @@ public class BookController {
     }
 
     @GetMapping("/pending")
-    public R<List<Book>> getPendingBooks() {
-        return R.ok(bookService.getPendingBooks());
+    public R<List<BookDetailDTO>> getPendingBooks() {
+        return R.ok(bookService.getPendingWithSellerName());
     }
 
     @PutMapping("/{id}/audit")

+ 4 - 3
server/src/main/java/space/anyi/server/controller/TransactionController.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.springframework.web.bind.annotation.*;
 import space.anyi.common.dto.R;
+import space.anyi.common.dto.TransactionDetailDTO;
 import space.anyi.common.dto.UserTransactionDTO;
 import space.anyi.common.entity.TransactionRecord;
 import space.anyi.server.service.TransactionService;
@@ -34,9 +35,9 @@ public class TransactionController {
     }
 
     @GetMapping
-    public R<IPage<TransactionRecord>> listAll(@RequestParam(defaultValue = "1") int page,
-                                                @RequestParam(defaultValue = "20") int size) {
-        return R.ok(transactionService.page(new Page<>(page, size)));
+    public R<IPage<TransactionDetailDTO>> listAll(@RequestParam(defaultValue = "1") int page,
+                                                   @RequestParam(defaultValue = "20") int size) {
+        return R.ok(transactionService.pageWithDetail(new Page<>(page, size)));
     }
 
     @GetMapping("/{id}")

+ 13 - 0
server/src/main/java/space/anyi/server/mapper/BookMapper.java

@@ -3,6 +3,7 @@ package space.anyi.server.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
+import space.anyi.common.dto.BookDetailDTO;
 import space.anyi.common.dto.SalesRecordDTO;
 import space.anyi.common.entity.Book;
 import java.util.List;
@@ -18,4 +19,16 @@ public interface BookMapper extends BaseMapper<Book> {
 
     @Select("SELECT COALESCE(SUM(t.transaction_num), 0) FROM transaction_record t JOIN book b ON t.book_id = b.book_id WHERE b.seller_id = #{sellerId}")
     Integer selectTotalSoldBySellerId(@Param("sellerId") Integer sellerId);
+
+    @Select("SELECT b.book_id, u.nickname AS seller_name, b.title, b.author, b.category, " +
+            "b.original_price, b.selling_price, b.stock, b.status " +
+            "FROM book b LEFT JOIN user u ON b.seller_id = u.userid " +
+            "ORDER BY b.created_at DESC")
+    List<BookDetailDTO> selectAllWithSellerName();
+
+    @Select("SELECT b.book_id, u.nickname AS seller_name, b.title, b.author, b.category, " +
+            "b.original_price, b.selling_price, b.stock, b.status " +
+            "FROM book b LEFT JOIN user u ON b.seller_id = u.userid " +
+            "WHERE b.status = 0 ORDER BY b.created_at DESC")
+    List<BookDetailDTO> selectPendingWithSellerName();
 }

+ 12 - 0
server/src/main/java/space/anyi/server/mapper/TransactionRecordMapper.java

@@ -1,7 +1,10 @@
 package space.anyi.server.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.apache.ibatis.annotations.Select;
+import space.anyi.common.dto.TransactionDetailDTO;
 import space.anyi.common.dto.UserTransactionDTO;
 import space.anyi.common.entity.TransactionRecord;
 import java.util.List;
@@ -11,4 +14,13 @@ public interface TransactionRecordMapper extends BaseMapper<TransactionRecord> {
             "FROM transaction_record t LEFT JOIN book b ON t.book_id = b.book_id " +
             "WHERE t.buyer_id = #{userId} ORDER BY t.transaction_time DESC")
     List<UserTransactionDTO> selectByUserIdWithTitle(Integer userId);
+
+    @Select("SELECT t.trans_id, b.title AS book_title, u1.nickname AS buyer_name, u2.nickname AS seller_name, " +
+            "t.transaction_num, t.transaction_price, t.transaction_time " +
+            "FROM transaction_record t " +
+            "LEFT JOIN book b ON t.book_id = b.book_id " +
+            "LEFT JOIN user u1 ON t.buyer_id = u1.userid " +
+            "LEFT JOIN user u2 ON b.seller_id = u2.userid " +
+            "ORDER BY t.transaction_time DESC")
+    IPage<TransactionDetailDTO> selectPageWithDetail(Page<?> page);
 }

+ 3 - 2
server/src/main/java/space/anyi/server/service/BookService.java

@@ -2,6 +2,7 @@ package space.anyi.server.service;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.common.dto.BookDetailDTO;
 import space.anyi.common.dto.SalesRecordDTO;
 import space.anyi.common.entity.Book;
 import java.util.List;
@@ -9,8 +10,8 @@ import java.util.List;
 public interface BookService extends IService<Book> {
     IPage<Book> searchBooks(String keyword, String category, int page, int size);
     List<Book> getBySellerId(Integer sellerId);
-    List<Book> listAll();
+    List<BookDetailDTO> listAllWithSellerName();
     List<SalesRecordDTO> getSalesByBookId(Integer bookId);
-    List<Book> getPendingBooks();
+    List<BookDetailDTO> getPendingWithSellerName();
     Integer getTotalSoldBySellerId(Integer sellerId);
 }

+ 4 - 0
server/src/main/java/space/anyi/server/service/TransactionService.java

@@ -1,6 +1,9 @@
 package space.anyi.server.service;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
+import space.anyi.common.dto.TransactionDetailDTO;
 import space.anyi.common.dto.UserTransactionDTO;
 import space.anyi.common.entity.TransactionRecord;
 import java.util.List;
@@ -9,4 +12,5 @@ public interface TransactionService extends IService<TransactionRecord> {
     TransactionRecord purchase(Integer bookId, Integer buyerId, Integer quantity);
     List<TransactionRecord> getByUserId(Integer userId);
     List<UserTransactionDTO> getByUserIdWithTitle(Integer userId);
+    IPage<TransactionDetailDTO> pageWithDetail(Page<TransactionRecord> page);
 }

+ 5 - 7
server/src/main/java/space/anyi/server/service/impl/BookServiceImpl.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import org.springframework.stereotype.Service;
+import space.anyi.common.dto.BookDetailDTO;
 import space.anyi.common.dto.SalesRecordDTO;
 import space.anyi.common.entity.Book;
 import space.anyi.server.mapper.BookMapper;
@@ -44,8 +45,8 @@ public class BookServiceImpl extends ServiceImpl<BookMapper, Book> implements Bo
     }
 
     @Override
-    public List<Book> listAll() {
-        return lambdaQuery().orderByDesc(Book::getCreatedAt).list();
+    public List<BookDetailDTO> listAllWithSellerName() {
+        return bookMapper.selectAllWithSellerName();
     }
 
     @Override
@@ -54,11 +55,8 @@ public class BookServiceImpl extends ServiceImpl<BookMapper, Book> implements Bo
     }
 
     @Override
-    public List<Book> getPendingBooks() {
-        LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(Book::getStatus, 0)
-               .orderByDesc(Book::getCreatedAt);
-        return list(wrapper);
+    public List<BookDetailDTO> getPendingWithSellerName() {
+        return bookMapper.selectPendingWithSellerName();
     }
 
     @Override

+ 8 - 0
server/src/main/java/space/anyi/server/service/impl/TransactionServiceImpl.java

@@ -1,9 +1,12 @@
 package space.anyi.server.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import space.anyi.common.dto.TransactionDetailDTO;
 import space.anyi.common.dto.UserTransactionDTO;
 import space.anyi.common.entity.Book;
 import space.anyi.common.entity.TransactionRecord;
@@ -61,4 +64,9 @@ public class TransactionServiceImpl extends ServiceImpl<TransactionRecordMapper,
     public List<UserTransactionDTO> getByUserIdWithTitle(Integer userId) {
         return transactionRecordMapper.selectByUserIdWithTitle(userId);
     }
+
+    @Override
+    public IPage<TransactionDetailDTO> pageWithDetail(Page<TransactionRecord> page) {
+        return transactionRecordMapper.selectPageWithDetail(page);
+    }
 }