【Spring Boot 快速入门】七、阿里云 OSS 文件上传

发布于:2025-08-06 ⋅ 阅读:(24) ⋅ 点赞:(0)

准备阿里云 OSS

  1. 注册登录阿里云,然后点击控制台,在左上角菜单栏搜索对象存储 OSS,点击并开通
  2. 点击 Bucket 列表并新建一个 Bucket,填写 Bucket 名称和地域在这里插入图片描述
  3. 点击头像下拉框,点击 AccessKey在这里插入图片描述
  4. 创建 AccessKey,获取并保存 AccessKey ID 和 AccessKey Secret
  5. 记下 Bucket 名称、Endpoint、AccessKey ID 和 AccessKey Secret,后续配置要使用

参照官方 SDK 编写入门程序

SDK 文档地址:对象存储 SDK

在代码中引入依赖:

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>

如果使用的是Java 9及以上的版本,则需要添加以下JAXB相关依赖:

<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.3.1</version>
</dependency>
<dependency>
    <groupId>javax.activation</groupId>
    <artifactId>activation</artifactId>
    <version>1.1.1</version>
</dependency>
<!-- no more than 2.3.3-->
<dependency>
    <groupId>org.glassfish.jaxb</groupId>
    <artifactId>jaxb-runtime</artifactId>
    <version>2.3.3</version>
</dependency>

使用文档中的示例代码:

import com.aliyun.oss.*;
import com.aliyun.oss.common.auth.*;
import com.aliyun.oss.common.comm.SignVersion;
import com.aliyun.oss.model.PutObjectRequest;
import com.aliyun.oss.model.PutObjectResult;
import java.io.FileInputStream;
import java.io.InputStream;

public class Demo {

    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        // 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
        EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "examplebucket";
        // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "exampledir/exampleobject.txt";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
        String filePath= "D:\\localpath\\examplefile.txt";
        // 填写Bucket所在地域。以华东1(杭州)为例,Region填写为cn-hangzhou。
        String region = "cn-hangzhou";
        
        // 创建OSSClient实例。
        // 当OSSClient实例不再使用时,调用shutdown方法以释放资源。
        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);        
        OSS ossClient = OSSClientBuilder.create()
        .endpoint(endpoint)
        .credentialsProvider(credentialsProvider)
        .clientConfiguration(clientBuilderConfiguration)
        .region(region)               
        .build();

        try {
            InputStream inputStream = new FileInputStream(filePath);
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, inputStream);
            // 创建PutObject请求。
            PutObjectResult result = ossClient.putObject(putObjectRequest);
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }
} 

案例数据准备

数据库表结构:

CREATE TABLE book_category (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
    name VARCHAR(100) NOT NULL COMMENT '分类名称',
    description VARCHAR(255) COMMENT '分类描述'
) COMMENT='图书分类表';

CREATE TABLE book_info (
    id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '图书ID',
    book_name VARCHAR(200) NOT NULL COMMENT '书名',
    author VARCHAR(100) COMMENT '作者',
    isbn VARCHAR(20) UNIQUE COMMENT 'ISBN编号',
    publisher VARCHAR(100) COMMENT '出版社',
    publish_date DATE COMMENT '出版日期',
    category_id BIGINT NOT NULL COMMENT '分类ID',
    image VARCHAR(255) COMMENT '图书封面',
    description TEXT COMMENT '简介',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '添加时间',
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',

    CONSTRAINT fk_info_category FOREIGN KEY (category_id) REFERENCES book_category(id)
) COMMENT='图书信息表';

图书分类的相关代码如下:

  1. 实体类 BookCategory:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BookCategory {
    private Long id;
    private String name;
    private String description;
}
  1. controller 类 BookCategoryController:
import com.Scarletkite.pojo.BookCategory;
import com.Scarletkite.response.Result;
import com.Scarletkite.service.BookCategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class BookCategoryController {

    @Autowired
    private BookCategoryService bookCategoryService;

    // 查询所有图书分类
    @GetMapping("/getAllBookCategory")
    public Result getAllBookCategory() {
        List<BookCategory> bookCategoryList = bookCategoryService.getAllBookCategory();
        return Result.success(bookCategoryList);
    }

    // 删除图书分类
    @DeleteMapping("/deleteBookCategory/{id}")
    public Result deleteBookCategory(@PathVariable Long id) {
        bookCategoryService.deleteBookCategory(id);
        return Result.success();
    }

    // 新增图书分类
    @PostMapping("/addBookCategory")
    public Result addBookCategory(@RequestBody BookCategory bookCategory) {
        bookCategoryService.addBookCategory(bookCategory);
        return Result.success();
    }

    // 修改图书分类
    @PutMapping("/updateBookCategory{id}")
    public Result updateBookCategory(@PathVariable Long id) {
        bookCategoryService.updateBookCategory(id);
        return Result.success();
    }
}
  1. service 接口 BookCategoryService:
import com.Scarletkite.pojo.BookCategory;

import java.util.List;

public interface BookCategoryService {

    // 查询所有图书分类
    List<BookCategory> getAllBookCategory();

    // 添加图书类别
    void addBookCategory(BookCategory bookCategory);

    // 删除图书分类
    void deleteBookCategory(Long id);

    // 修改图书分类
    void updateBookCategory(Long id);
}
  1. service 接口实现类 BookCategoryServiceImp:
import com.Scarletkite.mapper.BookCategoryMapper;
import com.Scarletkite.pojo.BookCategory;
import com.Scarletkite.service.BookCategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class BookCategoryServiceImp implements BookCategoryService {

    @Autowired
    private BookCategoryMapper bookCategoryMapper;

    // 查询所有图书分类
    @Override
    public List<BookCategory> getAllBookCategory() {
        return bookCategoryMapper.getAllBookCategory();
    }

    // 新增图书分类
    @Override
    public void addBookCategory(BookCategory bookCategory)
    {
        bookCategoryMapper.addBookCategory(bookCategory);
    }

    // 删除图书分类
    @Override
    public void deleteBookCategory(Long id) {
        bookCategoryMapper.deleteBookCategory(id);
    }

    // 修改图书分类
    @Override
    public void updateBookCategory(Long id) {
        bookCategoryMapper.updateBookCategory(id);
    }
}
  1. mapper 接口 BookCategoryMapper:
import com.Scarletkite.pojo.BookCategory;
import org.apache.ibatis.annotations.*;

import java.util.List;

@Mapper
public interface BookCategoryMapper {

    // 查询所有图书分类
    @Select("select * from book_category")
    List<BookCategory> getAllBookCategory();

    // 新增图书分类
    @Insert("insert into book_category (name, description) values (#{name}, #{description})")
    void addBookCategory(BookCategory bookCategory);

    // 删除图书分类
    @Delete("delete from book_category where id = #{id}")
    void deleteBookCategory(Long id);

    // 修改图书分类
    @Update("update book_category set name = #{name}, description = #{description} where id = #{id}")
    void updateBookCategory(Long id);
}

图书信息相关代码如下:

  1. 实体类 BookInfo:
import java.time.LocalDateTime;
import java.util.Date;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BookInfo {
    private Long id;
    private String bookName;
    private String author;
    private String isbn;
    private String publisher;
    private Date publishDate;
    private Long categoryId;
    private String image;
    private String description;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
  1. controller 类:
import com.Scarletkite.pojo.BookInfo;
import com.Scarletkite.pojo.PageBean;
import com.Scarletkite.response.Result;
import com.Scarletkite.service.BookInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

@RestController
public class BookInfoController {

    @Autowired
    private BookInfoService bookInfoService;

    // 新增图书信息
    @PostMapping("/addBookInfo")
    public Result addBookInfo(@RequestBody BookInfo bookInfo) {
        bookInfoService.addBookInfo(bookInfo);
        return Result.success();
    }

    // 查询所有图书信息
    @GetMapping("/getAllBookInfo")
    public Result getAllBookInfo(@RequestParam(defaultValue = "1") Integer page,
                                 @RequestParam(defaultValue = "10") Integer pageSize,
                                 @RequestParam(required = false) String bookName,
                                 @RequestParam(required = false) String author,
                                 @RequestParam(required = false) String isbn,
                                 @RequestParam(required = false) String publisher,
                                 @RequestParam(required = false) Date publishDate,
                                 @RequestParam(required = false) Long categoryId) {
        PageBean pageBean = bookInfoService.getAllBookInfo(page, pageSize, bookName, author, isbn, publisher, publishDate, categoryId);
        return Result.success(pageBean);
    }

    // 根据id回显图书信息
    @GetMapping("/getBookInfoById/{id}")
    public Result getBookInfoById(@PathVariable Long id) {
        BookInfo bookInfo = bookInfoService.getBookInfoById(id);
        return Result.success(bookInfo);
    }

    // 更新图书信息
    @PutMapping("/updateBookInfo")
    public Result updateBookInfo(@RequestBody BookInfo bookInfo) {
        //bookInfo.setId(id);
        bookInfoService.updateBookInfo(bookInfo);
        return Result.success();
    }

    // 删除图书信息
    @DeleteMapping("/deleteBookInfo/{id}")
    public Result deleteBookInfo(@PathVariable Long id) {
        bookInfoService.deleteBookInfo(id);
        return Result.success();
    }
}
  1. service 接口 BookInfoService:
import com.Scarletkite.pojo.BookInfo;
import com.Scarletkite.pojo.PageBean;
import java.util.Date;

public interface BookInfoService {

    // 新增图书信息
    void addBookInfo(BookInfo bookInfo);

    // 查询所有图书信息
    PageBean getAllBookInfo(Integer page, Integer pageSize, String bookName, String author,
                            String isbn, String publisher, Date publishDate, Long categoryId);

    // 根据id查询图书信息
    BookInfo getBookInfoById(Long id);

    // 更新图书信息
    void updateBookInfo(BookInfo bookInfo);

    // 删除图书信息
    void deleteBookInfo(Long id);
}
  1. service 接口实现类 BookInfoServiceImp:
import com.Scarletkite.mapper.BookInfoMapper;
import com.Scarletkite.pojo.BookInfo;
import com.Scarletkite.pojo.PageBean;
import com.Scarletkite.service.BookInfoService;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;

@Service
public class BookInfoServiceImp implements BookInfoService {

    @Autowired
    private BookInfoMapper bookInfoMapper;

	// 新增图书信息
    @Override
    public void addBookInfo(BookInfo bookInfo) {
        bookInfo.setCreateTime(LocalDateTime.now());
        bookInfo.setUpdateTime(LocalDateTime.now());
        bookInfoMapper.addBookInfo(bookInfo);
    }

    // 查询所有图书信息
    @Override
    public PageBean getAllBookInfo(Integer page, Integer pageSize, String bookName, String author,
                                   String isbn, String publisher, Date publishDate, Long categoryId) {
        // 1. 设置分页参数
        PageHelper.startPage(page, pageSize);

        // 2. 执行查询
        List<BookInfo> bookInfoList = bookInfoMapper.getAllBookInfo(bookName, author, isbn, publisher, publishDate, categoryId);
        Page<BookInfo> p = (Page<BookInfo>) bookInfoList;

        // 3. 封装PageBean
        PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
        return pageBean;
    }

    // 根据id查询图书信息
    @Override
    public BookInfo getBookInfoById(Long id) {
        return bookInfoMapper.getBookInfoById(id);
    }

    // 更新图书信息
    @Override
    public void updateBookInfo(BookInfo bookInfo) {
        bookInfo.setUpdateTime(LocalDateTime.now());
        bookInfoMapper.updateBookInfo(bookInfo);
    }

    // 删除图书信息
    @Override
    public void deleteBookInfo(Long id) {
        bookInfoMapper.deleteBookInfo(id);
    }
}
  1. mapper 接口:
import com.Scarletkite.pojo.BookInfo;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.Date;
import java.util.List;

@Mapper
public interface BookInfoMapper {

    // 新增图书信息
    @Insert("insert into book_info (book_name, author, isbn, publisher, " +
            "publish_date, category_id, image, description, create_time, update_time) " +
            "values (#{bookName}, #{author}, #{isbn}, #{publisher}, #{publishDate}, " +
            "#{categoryId}, #{image}, #{description}, #{createTime}, #{updateTime})")
    void addBookInfo(BookInfo bookInfo);

    // 查询所有图书信息
    //@Select("select * from book_info")
    List<BookInfo> getAllBookInfo(String bookName, String author, String isbn,
                                  String publisher, Date publishDate, Long categoryId);

    // 根据id查询图书信息
    @Select("select * from book_info where id = #{id}")
    BookInfo getBookInfoById(Long id);

    // 更新图书信息
    void updateBookInfo(BookInfo bookInfo);

    // 删除图书信息
    @Delete("delete from book_info where id = #{id}")
    void deleteBookInfo(Long id);
}
  1. XML 映射文件 BookInfoMapper:
<update id="updateBookInfo">
    update book_info
    <set>
        <if test="bookName != null">book_name = #{bookName},</if>
        <if test="author != null">author = #{author},</if>
        <if test="publisher != null">publisher = #{publisher},</if>
        <if test="publishDate != null">publish_date = #{publishDate},</if>
        <if test="categoryId != null">category_id = #{categoryId},</if>
        <if test="image != null">image = #{image},</if>
        <if test="description != null">description = #{description},</if>
        update_time = #{updateTime}
    </set>
    where id = #{id}
</update>

<select id="getAllBookInfo" resultType="com.Scarletkite.pojo.BookInfo">
    select * from book_info
    <where>
        <if test="bookName != null and bookName != ''">
            book_name like concat('%', #{bookName}, '%')
        </if>
        <if test="author != null and author != ''">
            and author like concat('%', #{author}, '%')
        </if>
        <if test="isbn != null and isbn != ''">
            and isbn like concat('%', #{isbn}, '%')
        </if>
        <if test="publisher != null and publisher != ''">
            and publisher like concat('%', #{publisher}, '%')
        </if>
        <if test="publishDate != null">
            and publish_date = #{publishDate}
        </if>
        <if test="categoryId != null">
            and category_id = #{categoryId}
        </if>
    </where>
    order by id desc
</select>

案例集成阿里云 OSS

以下是上传功能的逻辑图:
在这里插入图片描述

  1. 首先上传图片,然后通过 UploadController 上传到阿里云 OSS 中,并返回访问图片的 URL
  2. 点击添加,通过 BookInfoController 来进行新增操作

将文档中的示例代码改为一个工具类 AliOSSUtils:

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.util.UUID;

@Component
public class AliOSSUtils {
    private String endpoint = "";
    private String accessKeyId = "";
    private String accessKeySecret = "";
    private String bucketName = "";

    public  String upload(MultipartFile file) throws Exception {
        // 获取上传文件的输入流
        InputStream inputStream = file.getInputStream();

        // 避免文件覆盖
        String originalFilename = file.getOriginalFilename();
        String fileName = UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf("."));

        // 上传文件到OSS
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        ossClient.putObject(bucketName, fileName, inputStream);

        //文件访问路径
        String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;

        // 关闭ossClient
        ossClient.shutdown();

        return url;// 把上传到oss的路径返回
    }
}

新增一个 controller 类 UploadController:

import com.Scarletkite.response.Result;
import com.Scarletkite.utils.AliOSSUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
public class UploadController {

    @Autowired
    private AliOSSUtils aliOSSUtils;

    @PostMapping("/upload")
    public Result upload(MultipartFile image) throws Exception {

        // 调用阿里云OSS工具类来进行上传
        String url = aliOSSUtils.upload(image);
        return Result.success(url);
    }
}

这样就实现了文件的上传功能

前端测试代码

以下是用来对后端进行测试用的前端代码

app.js

// 全局变量
let currentPage = 1;
let pageSize = 10;
let categories = [];

// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
    loadCategories();
    loadBooks();
});

// 显示不同的功能区域
function showSection(section) {
    // 隐藏所有区域
    document.getElementById('books-section').style.display = 'none';
    document.getElementById('categories-section').style.display = 'none';
    
    // 显示选中的区域
    document.getElementById(section + '-section').style.display = 'block';
    
    // 更新导航状态
    document.querySelectorAll('.nav-link').forEach(link => {
        link.classList.remove('active');
    });
    event.target.classList.add('active');
    
    // 根据选中的区域加载数据
    if (section === 'books') {
        loadBooks();
    } else if (section === 'categories') {
        loadCategories();
    }
}

// 加载图书分类
async function loadCategories() {
    try {
        console.log('请求分类数据...');
        const response = await fetch('/getAllBookCategory');
        console.log('分类响应状态:', response.status);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const result = await response.json();
        console.log('分类响应数据:', result);
        
        if (result.code === 200) {
            categories = result.data || [];
            updateCategorySelects();
            updateCategoriesTable();
        } else {
            showAlert('加载分类失败: ' + result.message, 'danger');
        }
    } catch (error) {
        console.error('加载分类失败:', error);
        showAlert('加载分类失败: ' + error.message, 'danger');
    }
}

// 更新分类下拉选择框
function updateCategorySelects() {
    const selects = [
        document.getElementById('searchCategory'),
        document.querySelector('select[name="categoryId"]'),
        document.getElementById('editCategoryId')
    ];
    
    selects.forEach(select => {
        if (select) {
            // 保留第一个选项
            const firstOption = select.firstElementChild;
            select.innerHTML = '';
            select.appendChild(firstOption);
            
            // 添加分类选项
            categories.forEach(category => {
                const option = document.createElement('option');
                option.value = category.id;
                option.textContent = category.name;
                select.appendChild(option);
            });
        }
    });
}

// 更新分类表格
function updateCategoriesTable() {
    const tbody = document.getElementById('categoriesTableBody');
    tbody.innerHTML = '';
    
    categories.forEach(category => {
        const row = document.createElement('tr');
        row.innerHTML = `
            <td>${category.id}</td>
            <td>${category.name}</td>
            <td>${category.description || '-'}</td>
            <td>
                <button class="btn btn-sm btn-outline-danger" onclick="deleteCategory(${category.id})">
                    <i class="bi bi-trash"></i> 删除
                </button>
            </td>
        `;
        tbody.appendChild(row);
    });
}

// 加载图书列表
async function loadBooks(page = 1) {
    try {
        currentPage = page;
        const bookName = document.getElementById('searchBookName')?.value || '';
        const author = document.getElementById('searchAuthor')?.value || '';
        const categoryId = document.getElementById('searchCategory')?.value || '';
        
        const params = new URLSearchParams({
            page: currentPage,
            pageSize: pageSize
        });
        
        // 只添加非空参数
        if (bookName.trim()) {
            params.append('bookName', bookName.trim());
        }
        if (author.trim()) {
            params.append('author', author.trim());
        }
        if (categoryId) {
            params.append('categoryId', categoryId);
        }
        
        console.log('请求URL:', `/getAllBookInfo?${params}`);
        const response = await fetch(`/getAllBookInfo?${params}`);
        console.log('响应状态:', response.status);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const result = await response.json();
        console.log('响应数据:', result);
        
        if (result.code === 200) {
            updateBooksTable(result.data.rows || []);
            updatePagination(result.data.total, currentPage, pageSize);
        } else {
            showAlert('加载图书失败: ' + result.message, 'danger');
        }
    } catch (error) {
        console.error('加载图书失败:', error);
        showAlert('加载图书失败: ' + error.message, 'danger');
    }
}

// 更新图书表格
// 处理图片URL的辅助函数
function processImageUrl(imageUrl) {
    // 添加调试信息
    console.log('原始图片URL:', imageUrl);
    
    if (!imageUrl) {
        console.log('图片URL为空,使用占位符');
        return 'https://via.placeholder.com/60x80?text=No+Image';
    }
    
    // 如果是完整的HTTP/HTTPS URL(OSS),直接使用
    if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
        console.log('检测到完整URL(OSS),直接使用:', imageUrl);
        return imageUrl;
    }
    
    // 如果是相对路径(本地存储),确保以/开头
    if (!imageUrl.startsWith('/')) {
        imageUrl = '/' + imageUrl;
    }
    
    console.log('处理后的本地路径URL:', imageUrl);
    return imageUrl;
}

function updateBooksTable(books) {
    const tbody = document.getElementById('booksTableBody');
    tbody.innerHTML = '';
    
    books.forEach(book => {
        const category = categories.find(c => c.id === book.categoryId);
        const categoryName = category ? category.name : '-';
        const publishDate = book.publishDate ? new Date(book.publishDate).toLocaleDateString() : '-';
        const imageUrl = processImageUrl(book.image);
        
        const row = document.createElement('tr');
        row.innerHTML = `
            <td><img src="${imageUrl}" alt="封面" class="book-image" onerror="this.src='https://via.placeholder.com/60x80?text=No+Image'"></td>
            <td>${book.bookName}</td>
            <td>${book.author}</td>
            <td>${book.isbn}</td>
            <td>${book.publisher}</td>
            <td>${publishDate}</td>
            <td>${categoryName}</td>
            <td>
                <button class="btn btn-sm btn-outline-primary me-1" onclick="editBook(${book.id})">
                    <i class="bi bi-pencil"></i>
                </button>
                <button class="btn btn-sm btn-outline-danger" onclick="deleteBook(${book.id})">
                    <i class="bi bi-trash"></i>
                </button>
            </td>
        `;
        tbody.appendChild(row);
    });
}

// 更新分页
function updatePagination(total, pageNum, pageSize) {
    const pagination = document.getElementById('booksPagination');
    pagination.innerHTML = '';
    
    const totalPages = Math.ceil(total / pageSize);
    
    // 上一页
    const prevLi = document.createElement('li');
    prevLi.className = `page-item ${pageNum <= 1 ? 'disabled' : ''}`;
    prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadBooks(${pageNum - 1})">上一页</a>`;
    pagination.appendChild(prevLi);
    
    // 页码
    for (let i = Math.max(1, pageNum - 2); i <= Math.min(totalPages, pageNum + 2); i++) {
        const li = document.createElement('li');
        li.className = `page-item ${i === pageNum ? 'active' : ''}`;
        li.innerHTML = `<a class="page-link" href="#" onclick="loadBooks(${i})">${i}</a>`;
        pagination.appendChild(li);
    }
    
    // 下一页
    const nextLi = document.createElement('li');
    nextLi.className = `page-item ${pageNum >= totalPages ? 'disabled' : ''}`;
    nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadBooks(${pageNum + 1})">下一页</a>`;
    pagination.appendChild(nextLi);
}

// 搜索图书
function searchBooks() {
    loadBooks(1);
}

// 添加图书
async function addBook() {
    const form = document.getElementById('addBookForm');
    
    // 验证必填字段
    if (!form.bookName.value.trim()) {
        alert('请输入图书名称!');
        return;
    }
    if (!form.author.value.trim()) {
        alert('请输入作者!');
        return;
    }
    if (!form.isbn.value.trim()) {
        alert('请输入ISBN!');
        return;
    }
    if (!form.publisher.value.trim()) {
        alert('请输入出版社!');
        return;
    }
    if (!form.categoryId.value) {
        alert('请选择图书分类!');
        return;
    }
    
    try {
        let imageUrl = null;
        
        // 如果有图片文件,先上传图片
        if (form.image.files[0]) {
            const imageFormData = new FormData();
            imageFormData.append('image', form.image.files[0]);
            
            const uploadResponse = await fetch('/upload', {
                method: 'POST',
                body: imageFormData
            });
            
            if (!uploadResponse.ok) {
                throw new Error('图片上传失败');
            }
            
            const uploadResult = await uploadResponse.json();
            if (uploadResult.code === 200) {
                imageUrl = uploadResult.data;
            } else {
                throw new Error('图片上传失败: ' + uploadResult.message);
            }
        }
        
        // 构建图书信息JSON对象
        const bookData = {
            bookName: form.bookName.value.trim(),
            author: form.author.value.trim(),
            isbn: form.isbn.value.trim(),
            publisher: form.publisher.value.trim(),
            publishDate: form.publishDate.value || null,
            categoryId: parseInt(form.categoryId.value),
            description: form.description.value || '',
            image: imageUrl
        };
        
        console.log('发送添加图书请求...', bookData);
        
        const response = await fetch('/addBookInfo', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(bookData)
        });
        
        console.log('响应状态:', response.status);
        
        if (!response.ok) {
            const errorText = await response.text();
            console.log('错误响应内容:', errorText);
            throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
        }
        
        const result = await response.json();
        console.log('响应数据:', result);
        
        if (result.code === 200) {
            showAlert('添加图书成功!', 'success');
            bootstrap.Modal.getInstance(document.getElementById('addBookModal')).hide();
            form.reset();
            loadBooks();
        } else {
            showAlert('添加图书失败: ' + result.message, 'danger');
        }
    } catch (error) {
        console.error('添加图书失败:', error);
        showAlert('添加图书失败: ' + error.message, 'danger');
    }
}

// 编辑图书
async function editBook(id) {
    try {
        // 使用后端接口获取图书详情
        const response = await fetch(`/getBookInfoById/${id}`);
        const result = await response.json();
        
        if (result.code === 200) {
            const bookInfo = result.data;
            console.log('获取到的图书信息:', bookInfo);
            
            // 填充编辑表单
            document.getElementById('editBookId').value = id;
            document.getElementById('editBookName').value = bookInfo.bookName || '';
            document.getElementById('editAuthor').value = bookInfo.author || '';
            document.getElementById('editIsbn').value = bookInfo.isbn || '';
            document.getElementById('editPublisher').value = bookInfo.publisher || '';
            
            // 处理日期格式
            if (bookInfo.publishDate) {
                const date = new Date(bookInfo.publishDate);
                const formattedDate = date.toISOString().split('T')[0];
                document.getElementById('editPublishDate').value = formattedDate;
            } else {
                document.getElementById('editPublishDate').value = '';
            }
            
            // 先更新分类下拉框选项
            updateCategorySelects();
            
            // 然后设置分类值和描述
            document.getElementById('editCategoryId').value = bookInfo.categoryId || '';
            document.getElementById('editDescription').value = bookInfo.description || '';
            
            // 清空文件输入框
            document.getElementById('editImage').value = '';
            
            // 处理图片显示
            const imagePreview = document.getElementById('editImagePreview');
            const noImageText = document.getElementById('editNoImage');
            
            if (bookInfo.image) {
                // 使用统一的图片URL处理函数
                imagePreview.src = processImageUrl(bookInfo.image);
                imagePreview.style.display = 'block';
                noImageText.style.display = 'none';
            } else {
                imagePreview.style.display = 'none';
                noImageText.style.display = 'block';
            }
            
            // 显示编辑模态框
            new bootstrap.Modal(document.getElementById('editBookModal')).show();
        } else {
            showAlert('获取图书信息失败: ' + result.message, 'danger');
        }
    } catch (error) {
        console.error('获取图书信息失败:', error);
        showAlert('获取图书信息失败: ' + error.message, 'danger');
    }
}

// 更新图书
async function updateBook() {
    const id = document.getElementById('editBookId').value;
    const imageInput = document.getElementById('editImage');
    
    try {
        let imageUrl = null;
        
        // 如果用户选择了新的图片文件,先上传图片
        if (imageInput.files[0]) {
            const imageFormData = new FormData();
            imageFormData.append('image', imageInput.files[0]);
            
            const uploadResponse = await fetch('/upload', {
                method: 'POST',
                body: imageFormData
            });
            
            if (!uploadResponse.ok) {
                throw new Error('图片上传失败');
            }
            
            const uploadResult = await uploadResponse.json();
            if (uploadResult.code === 200) {
                imageUrl = uploadResult.data;
            } else {
                throw new Error('图片上传失败: ' + uploadResult.message);
            }
        }
        
        // 构建更新数据
        const bookData = {
            id: parseInt(id),
            bookName: document.getElementById('editBookName').value,
            author: document.getElementById('editAuthor').value,
            isbn: document.getElementById('editIsbn').value,
            publisher: document.getElementById('editPublisher').value,
            publishDate: document.getElementById('editPublishDate').value || null,
            categoryId: parseInt(document.getElementById('editCategoryId').value),
            description: document.getElementById('editDescription').value
        };
        
        // 如果上传了新图片,则更新图片URL
        if (imageUrl) {
            bookData.image = imageUrl;
        }
        
        console.log('发送更新图书请求...', bookData);
        
        const response = await fetch('/updateBookInfo', {
            method: 'PUT',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(bookData)
        });
        
        const result = await response.json();
        
        if (result.code === 200) {
            showAlert('更新图书成功!', 'success');
            bootstrap.Modal.getInstance(document.getElementById('editBookModal')).hide();
            loadBooks();
        } else {
            showAlert('更新图书失败: ' + result.message, 'danger');
        }
    } catch (error) {
        console.error('更新图书失败:', error);
        showAlert('更新图书失败: ' + error.message, 'danger');
    }
}

// 删除图书
async function deleteBook(id) {
    if (!confirm('确定要删除这本图书吗?')) {
        return;
    }
    
    try {
        const response = await fetch(`/deleteBookInfo/${id}`, {
            method: 'DELETE'
        });
        
        const result = await response.json();
        
        if (result.code === 200) {
            showAlert('删除图书成功!', 'success');
            loadBooks();
        } else {
            showAlert('删除图书失败: ' + result.message, 'danger');
        }
    } catch (error) {
        console.error('删除图书失败:', error);
        showAlert('删除图书失败: ' + error.message, 'danger');
    }
}

// 添加分类
async function addCategory() {
    const form = document.getElementById('addCategoryForm');
    const formData = new FormData(form);
    
    const categoryData = {
        name: formData.get('name'),
        description: formData.get('description')
    };
    
    try {
        const response = await fetch('/addBookCategory', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(categoryData)
        });
        
        const result = await response.json();
        
        if (result.code === 200) {
            showAlert('添加分类成功!', 'success');
            bootstrap.Modal.getInstance(document.getElementById('addCategoryModal')).hide();
            form.reset();
            loadCategories();
        } else {
            showAlert('添加分类失败: ' + result.message, 'danger');
        }
    } catch (error) {
        console.error('添加分类失败:', error);
        showAlert('添加分类失败: ' + error.message, 'danger');
    }
}

// 删除分类
async function deleteCategory(id) {
    if (!confirm('确定要删除这个分类吗?')) {
        return;
    }
    
    try {
        const response = await fetch(`/deleteBookCategory/${id}`, {
            method: 'DELETE'
        });
        
        const result = await response.json();
        
        if (result.code === 200) {
            showAlert('删除分类成功!', 'success');
            loadCategories();
        } else {
            showAlert('删除分类失败: ' + result.message, 'danger');
        }
    } catch (error) {
        console.error('删除分类失败:', error);
        showAlert('删除分类失败: ' + error.message, 'danger');
    }
}

// 图片预览功能
document.addEventListener('DOMContentLoaded', function() {
    // 添加图书时的图片预览功能
    const imageInput = document.getElementById('image');
    if (imageInput) {
        imageInput.addEventListener('change', function(e) {
            const file = e.target.files[0];
            const preview = document.getElementById('imagePreview');
            
            if (file) {
                const reader = new FileReader();
                reader.onload = function(e) {
                    preview.src = e.target.result;
                    preview.style.display = 'block';
                };
                reader.readAsDataURL(file);
            } else {
                preview.style.display = 'none';
            }
        });
    }

    // 编辑图书时的图片预览功能
    const editImageInput = document.getElementById('editImage');
    if (editImageInput) {
        editImageInput.addEventListener('change', function(e) {
            const file = e.target.files[0];
            const preview = document.getElementById('editImagePreview');
            const noImageText = document.getElementById('editNoImage');
            
            if (file) {
                const reader = new FileReader();
                reader.onload = function(e) {
                    preview.src = e.target.result;
                    preview.style.display = 'block';
                    if (noImageText) {
                        noImageText.style.display = 'none';
                    }
                };
                reader.readAsDataURL(file);
            }
        });
    }
});

// 显示提示消息
function showAlert(message, type = 'info') {
    // 创建提示框
    const alertDiv = document.createElement('div');
    alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
    alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
    alertDiv.innerHTML = `
        ${message}
        <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
    `;
    
    document.body.appendChild(alertDiv);
    
    // 3秒后自动消失
    setTimeout(() => {
        if (alertDiv.parentNode) {
            alertDiv.remove();
        }
    }, 3000);
}

style.css

/* 自定义样式 */
:root {
    --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    --primary-color: #667eea;
    --secondary-color: #764ba2;
    --success-color: #28a745;
    --danger-color: #dc3545;
    --warning-color: #ffc107;
    --info-color: #17a2b8;
    --light-color: #f8f9fa;
    --dark-color: #343a40;
}

/* 全局样式 */
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background-color: #f5f6fa;
}

/* 侧边栏样式 */
.sidebar {
    min-height: 100vh;
    background: var(--primary-gradient);
    box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}

.sidebar h4 {
    font-weight: 600;
    text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}

.nav-link {
    color: rgba(255,255,255,0.8) !important;
    transition: all 0.3s ease;
    border-radius: 8px;
    margin-bottom: 5px;
    font-weight: 500;
}

.nav-link:hover, .nav-link.active {
    color: white !important;
    background-color: rgba(255,255,255,0.15);
    transform: translateX(5px);
}

.nav-link i {
    width: 20px;
    text-align: center;
}

/* 卡片样式 */
.card {
    border: none;
    box-shadow: 0 4px 15px rgba(0,0,0,0.08);
    transition: all 0.3s ease;
    border-radius: 12px;
}

.card:hover {
    transform: translateY(-3px);
    box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}

.card-header {
    background: var(--primary-gradient);
    color: white;
    border-radius: 12px 12px 0 0 !important;
    font-weight: 600;
}

/* 按钮样式 */
.btn-primary {
    background: var(--primary-gradient);
    border: none;
    border-radius: 8px;
    font-weight: 500;
    transition: all 0.3s ease;
}

.btn-primary:hover {
    background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
    transform: translateY(-2px);
    box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}

.btn-outline-primary {
    border-color: var(--primary-color);
    color: var(--primary-color);
    border-radius: 6px;
    transition: all 0.3s ease;
}

.btn-outline-primary:hover {
    background: var(--primary-gradient);
    border-color: var(--primary-color);
    transform: translateY(-1px);
}

.btn-outline-danger {
    border-radius: 6px;
    transition: all 0.3s ease;
}

.btn-outline-danger:hover {
    transform: translateY(-1px);
}

/* 表格样式 */
.table {
    border-radius: 8px;
    overflow: hidden;
}

.table thead th {
    background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
    border: none;
    font-weight: 600;
    color: var(--dark-color);
    padding: 15px;
}

.table tbody tr {
    transition: all 0.2s ease;
}

.table tbody tr:hover {
    background-color: rgba(102, 126, 234, 0.05);
    transform: scale(1.01);
}

.table tbody td {
    padding: 12px 15px;
    vertical-align: middle;
    border-color: #f1f3f4;
}

/* 图书封面样式 */
.book-image {
    width: 60px;
    height: 80px;
    object-fit: cover;
    border-radius: 6px;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    transition: transform 0.2s ease;
}

.book-image:hover {
    transform: scale(1.1);
}

/* 表格容器 */
.table-container {
    max-height: 600px;
    overflow-y: auto;
    border-radius: 8px;
}

.table-container::-webkit-scrollbar {
    width: 6px;
}

.table-container::-webkit-scrollbar-track {
    background: #f1f1f1;
    border-radius: 3px;
}

.table-container::-webkit-scrollbar-thumb {
    background: var(--primary-color);
    border-radius: 3px;
}

.table-container::-webkit-scrollbar-thumb:hover {
    background: var(--secondary-color);
}

/* 分页样式 */
.pagination .page-link {
    border: none;
    color: var(--primary-color);
    border-radius: 6px;
    margin: 0 2px;
    transition: all 0.2s ease;
}

.pagination .page-link:hover {
    background: var(--primary-gradient);
    color: white;
    transform: translateY(-1px);
}

.pagination .page-item.active .page-link {
    background: var(--primary-gradient);
    border-color: var(--primary-color);
}

/* 模态框样式 */
.modal-content {
    border: none;
    border-radius: 12px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}

.modal-header {
    background: var(--primary-gradient);
    color: white;
    border-radius: 12px 12px 0 0;
    border-bottom: none;
}

.modal-header .btn-close {
    filter: invert(1);
}

.modal-body {
    padding: 30px;
}

.modal-footer {
    border-top: 1px solid #e9ecef;
    padding: 20px 30px;
}

/* 表单样式 */
.form-control, .form-select {
    border: 2px solid #e9ecef;
    border-radius: 8px;
    padding: 12px 15px;
    transition: all 0.3s ease;
}

.form-control:focus, .form-select:focus {
    border-color: var(--primary-color);
    box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}

.form-label {
    font-weight: 600;
    color: var(--dark-color);
    margin-bottom: 8px;
}

/* 搜索区域样式 */
.search-card {
    background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
    border: 1px solid #e9ecef;
}

/* 提示框样式 */
.alert {
    border: none;
    border-radius: 8px;
    font-weight: 500;
    box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}

.alert-success {
    background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
    color: #155724;
}

.alert-danger {
    background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
    color: #721c24;
}

.alert-info {
    background: linear-gradient(135deg, #d1ecf1 0%, #bee5eb 100%);
    color: #0c5460;
}

/* 响应式设计 */
@media (max-width: 768px) {
    .sidebar {
        min-height: auto;
    }
    
    .table-container {
        max-height: 400px;
    }
    
    .modal-dialog {
        margin: 10px;
    }
    
    .card {
        margin-bottom: 20px;
    }
}

/* 加载动画 */
.loading {
    display: inline-block;
    width: 20px;
    height: 20px;
    border: 3px solid rgba(255,255,255,.3);
    border-radius: 50%;
    border-top-color: #fff;
    animation: spin 1s ease-in-out infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* 空状态样式 */
.empty-state {
    text-align: center;
    padding: 60px 20px;
    color: #6c757d;
}

.empty-state i {
    font-size: 4rem;
    margin-bottom: 20px;
    opacity: 0.5;
}

.empty-state h5 {
    margin-bottom: 10px;
    color: #495057;
}

/* 统计卡片 */
.stats-card {
    background: var(--primary-gradient);
    color: white;
    border-radius: 12px;
    padding: 20px;
    text-align: center;
}

.stats-card h3 {
    font-size: 2.5rem;
    font-weight: 700;
    margin-bottom: 5px;
}

.stats-card p {
    margin: 0;
    opacity: 0.9;
}

index.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图书管理系统测试界面</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css" rel="stylesheet">
    <link href="style.css" rel="stylesheet">
</head>
<body>
    <div class="container-fluid">
        <div class="row">
            <!-- 侧边栏 -->
            <div class="col-md-2 sidebar p-3">
                <h4 class="text-white mb-4">
                    <i class="bi bi-book"></i> 图书管理系统
                </h4>
                <ul class="nav nav-pills flex-column">
                    <li class="nav-item mb-2">
                        <a class="nav-link active" href="#" onclick="showSection('books')">
                            <i class="bi bi-book-fill me-2"></i>图书信息管理
                        </a>
                    </li>
                    <li class="nav-item mb-2">
                        <a class="nav-link" href="#" onclick="showSection('categories')">
                            <i class="bi bi-tags-fill me-2"></i>分类管理
                        </a>
                    </li>
                </ul>
            </div>

            <!-- 主内容区 -->
            <div class="col-md-10 p-4">
                <!-- 图书管理区域 -->
                <div id="books-section">
                    <div class="d-flex justify-content-between align-items-center mb-4">
                        <h2><i class="bi bi-book-fill text-primary me-2"></i>图书管理</h2>
                        <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addBookModal">
                            <i class="bi bi-plus-circle me-1"></i>添加图书
                        </button>
                    </div>

                    <!-- 搜索过滤器 -->
                    <div class="card mb-4">
                        <div class="card-body">
                            <div class="row g-3">
                                <div class="col-md-3">
                                    <input type="text" class="form-control" id="searchBookName" placeholder="图书名称">
                                </div>
                                <div class="col-md-3">
                                    <input type="text" class="form-control" id="searchAuthor" placeholder="作者">
                                </div>
                                <div class="col-md-3">
                                    <select class="form-select" id="searchCategory">
                                        <option value="">选择分类</option>
                                    </select>
                                </div>
                                <div class="col-md-3">
                                    <button class="btn btn-outline-primary w-100" onclick="searchBooks()">
                                        <i class="bi bi-search me-1"></i>搜索
                                    </button>
                                </div>
                            </div>
                        </div>
                    </div>

                    <!-- 图书列表 -->
                    <div class="card">
                        <div class="card-body">
                            <div class="table-container">
                                <table class="table table-hover">
                                    <thead class="table-light">
                                        <tr>
                                            <th>封面</th>
                                            <th>书名</th>
                                            <th>作者</th>
                                            <th>ISBN</th>
                                            <th>出版社</th>
                                            <th>出版日期</th>
                                            <th>分类</th>
                                            <th>操作</th>
                                        </tr>
                                    </thead>
                                    <tbody id="booksTableBody">
                                        <!-- 动态加载图书数据 -->
                                    </tbody>
                                </table>
                            </div>
                            <!-- 分页 -->
                            <nav aria-label="Page navigation" class="mt-3">
                                <ul class="pagination justify-content-center" id="booksPagination">
                                    <!-- 动态生成分页 -->
                                </ul>
                            </nav>
                        </div>
                    </div>
                </div>

                <!-- 分类管理区域 -->
                <div id="categories-section" style="display: none;">
                    <div class="d-flex justify-content-between align-items-center mb-4">
                        <h2><i class="bi bi-tags-fill text-primary me-2"></i>分类管理</h2>
                        <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addCategoryModal">
                            <i class="bi bi-plus-circle me-1"></i>添加分类
                        </button>
                    </div>

                    <!-- 分类列表 -->
                    <div class="card">
                        <div class="card-body">
                            <table class="table table-hover">
                                <thead class="table-light">
                                    <tr>
                                        <th>ID</th>
                                        <th>分类名称</th>
                                        <th>描述</th>
                                        <th>操作</th>
                                    </tr>
                                </thead>
                                <tbody id="categoriesTableBody">
                                    <!-- 动态加载分类数据 -->
                                </tbody>
                            </table>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <!-- 添加图书模态框 -->
    <div class="modal fade" id="addBookModal" tabindex="-1">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">添加图书</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <form id="addBookForm" enctype="multipart/form-data">
                        <div class="row g-3">
                            <div class="col-md-6">
                                <label class="form-label">图书名称 *</label>
                                <input type="text" class="form-control" name="bookName" required>
                            </div>
                            <div class="col-md-6">
                                <label class="form-label">作者 *</label>
                                <input type="text" class="form-control" name="author" required>
                            </div>
                            <div class="col-md-6">
                                <label class="form-label">ISBN *</label>
                                <input type="text" class="form-control" name="isbn" required>
                            </div>
                            <div class="col-md-6">
                                <label class="form-label">出版社 *</label>
                                <input type="text" class="form-control" name="publisher" required>
                            </div>
                            <div class="col-md-6">
                                <label class="form-label">出版日期</label>
                                <input type="date" class="form-control" name="publishDate">
                            </div>
                            <div class="col-md-6">
                                <label class="form-label">分类 *</label>
                                <select class="form-select" name="categoryId" required>
                                    <option value="">选择分类</option>
                                </select>
                            </div>
                            <div class="col-12">
                                <label class="form-label">封面图片</label>
                                <input type="file" class="form-control" name="image" accept="image/*">
                            </div>
                            <div class="col-12">
                                <label class="form-label">描述</label>
                                <textarea class="form-control" name="description" rows="3"></textarea>
                            </div>
                        </div>
                    </form>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                    <button type="button" class="btn btn-primary" onclick="addBook()">添加</button>
                </div>
            </div>
        </div>
    </div>

    <!-- 编辑图书模态框 -->
    <div class="modal fade" id="editBookModal" tabindex="-1">
        <div class="modal-dialog modal-lg">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">编辑图书</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <form id="editBookForm">
                        <input type="hidden" id="editBookId">
                        <div class="row g-4">
                            <!-- 左侧:当前封面 -->
                            <div class="col-md-4">
                                <div class="text-center">
                                    <label class="form-label fw-bold">当前封面</label>
                                    <div id="editCurrentImage" class="border rounded p-3" style="min-height: 300px; display: flex; align-items: center; justify-content: center;">
                                        <img id="editImagePreview" src="" alt="图书封面" class="img-fluid rounded" style="max-width: 100%; max-height: 280px; display: none;">
                                        <p id="editNoImage" class="text-muted mb-0">暂无封面图片</p>
                                    </div>
                                </div>
                            </div>
                            
                            <!-- 右侧:表单字段 -->
                            <div class="col-md-8">
                                <div class="row g-3">
                                    <div class="col-md-6">
                                        <label class="form-label">图书名称 *</label>
                                        <input type="text" class="form-control" id="editBookName" required>
                                    </div>
                                    <div class="col-md-6">
                                        <label class="form-label">作者 *</label>
                                        <input type="text" class="form-control" id="editAuthor" required>
                                    </div>
                                    <div class="col-md-6">
                                        <label class="form-label">ISBN *</label>
                                        <input type="text" class="form-control" id="editIsbn" required>
                                    </div>
                                    <div class="col-md-6">
                                        <label class="form-label">出版社 *</label>
                                        <input type="text" class="form-control" id="editPublisher" required>
                                    </div>
                                    <div class="col-md-6">
                                        <label class="form-label">出版日期</label>
                                        <input type="date" class="form-control" id="editPublishDate">
                                    </div>
                                    <div class="col-md-6">
                                        <label class="form-label">分类 *</label>
                                        <select class="form-select" id="editCategoryId" required>
                                            <option value="">选择分类</option>
                                        </select>
                                    </div>
                                    <div class="col-12">
                                        <label class="form-label">更换封面图片</label>
                                        <input type="file" class="form-control" id="editImage" accept="image/*">
                                        <small class="form-text text-muted">如需更换封面,请选择新的图片文件</small>
                                    </div>
                                    <div class="col-12">
                                        <label class="form-label">描述</label>
                                        <textarea class="form-control" id="editDescription" rows="4"></textarea>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </form>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                    <button type="button" class="btn btn-primary" onclick="updateBook()">保存</button>
                </div>
            </div>
        </div>
    </div>

    <!-- 添加分类模态框 -->
    <div class="modal fade" id="addCategoryModal" tabindex="-1">
        <div class="modal-dialog">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">添加分类</h5>
                    <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
                </div>
                <div class="modal-body">
                    <form id="addCategoryForm">
                        <div class="mb-3">
                            <label class="form-label">分类名称 *</label>
                            <input type="text" class="form-control" name="name" required>
                        </div>
                        <div class="mb-3">
                            <label class="form-label">描述</label>
                            <textarea class="form-control" name="description" rows="3"></textarea>
                        </div>
                    </form>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                    <button type="button" class="btn btn-primary" onclick="addCategory()">添加</button>
                </div>
            </div>
        </div>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
    <script src="app.js"></script>
</body>
</html>

效果图

在这里插入图片描述
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到