這里寫自定義目錄標題
- 準備阿里云 OSS
- 參照官方 SDK 編寫入門程序
- 案例數據準備
- 案例集成阿里云 OSS
- 前端測試代碼
- app.js
- style.css
- index.html
- 效果圖
準備阿里云 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 {@Autowiredprivate 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 {@Autowiredprivate BookCategoryMapper bookCategoryMapper;// 查詢所有圖書分類@Overridepublic List<BookCategory> getAllBookCategory() {return bookCategoryMapper.getAllBookCategory();}// 新增圖書分類@Overridepublic void addBookCategory(BookCategory bookCategory){bookCategoryMapper.addBookCategory(bookCategory);}// 刪除圖書分類@Overridepublic void deleteBookCategory(Long id) {bookCategoryMapper.deleteBookCategory(id);}// 修改圖書分類@Overridepublic 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 {@Autowiredprivate 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 {@Autowiredprivate BookInfoMapper bookInfoMapper;// 新增圖書信息@Overridepublic void addBookInfo(BookInfo bookInfo) {bookInfo.setCreateTime(LocalDateTime.now());bookInfo.setUpdateTime(LocalDateTime.now());bookInfoMapper.addBookInfo(bookInfo);}// 查詢所有圖書信息@Overridepublic 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. 封裝PageBeanPageBean pageBean = new PageBean(p.getTotal(), p.getResult());return pageBean;}// 根據id查詢圖書信息@Overridepublic BookInfo getBookInfoById(Long id) {return bookInfoMapper.getBookInfoById(id);}// 更新圖書信息@Overridepublic void updateBookInfo(BookInfo bookInfo) {bookInfo.setUpdateTime(LocalDateTime.now());bookInfoMapper.updateBookInfo(bookInfo);}// 刪除圖書信息@Overridepublic 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("."));// 上傳文件到OSSOSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);ossClient.putObject(bucketName, fileName, inputStream);//文件訪問路徑String url = endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + fileName;// 關閉ossClientossClient.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 {@Autowiredprivate 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};// 如果上傳了新圖片,則更新圖片URLif (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>
效果圖