这里写自定义目录标题
准备阿里云 OSS
- 注册登录阿里云,然后点击控制台,在左上角菜单栏搜索对象存储 OSS,点击并开通
- 点击 Bucket 列表并新建一个 Bucket,填写 Bucket 名称和地域
- 点击头像下拉框,点击 AccessKey
- 创建 AccessKey,获取并保存 AccessKey ID 和 AccessKey Secret
- 记下 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='图书信息表';
图书分类的相关代码如下:
- 实体类 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;
}
- 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();
}
}
- 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);
}
- 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);
}
}
- 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);
}
图书信息相关代码如下:
- 实体类 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;
}
- 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();
}
}
- 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);
}
- 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);
}
}
- 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);
}
- 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
以下是上传功能的逻辑图:
- 首先上传图片,然后通过 UploadController 上传到阿里云 OSS 中,并返回访问图片的 URL
- 点击添加,通过 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>
效果图