Files
LM/classes/artifacts/LM_Web_exploded/view/Reader.jsp
LAPTOP-I47JE7I7\Lenovo e45549ec5a 重新提交
2026-04-08 10:56:37 +08:00

688 lines
24 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.chinasofti.model.Reader" %>
<%@ page import="com.chinasofti.enums.ReaderType" %>
<%
Reader currentReader = (Reader) session.getAttribute("currentReader");
// Login.jsp 在 /view/Login.jsp需要返回上级目录
if (currentReader == null) {
response.sendRedirect(request.getContextPath() + "/../Login.jsp");
return;
}
// 准备传递给前端的数据
String readerName = currentReader.getName();
String readerTypeDesc = "";
int typeCode = currentReader.getReaderType();
if (typeCode == ReaderType.SENIOR.getCode()) readerTypeDesc = "老年";
else if (typeCode == ReaderType.ADULT.getCode()) readerTypeDesc = "成人";
else if (typeCode == ReaderType.CHILD.getCode()) readerTypeDesc = "儿童";
String readerAccount = currentReader.getCardNumber();
String readerJson = String.format(
"{\"name\":\"%s\",\"type\":\"%s\",\"account\":\"%s\"}",
readerName, readerTypeDesc, readerAccount
);
%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>读者主页 · 借阅与活动</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
/* 样式与原来相同,此处省略(请保留原样式) */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #eef2f6;
font-family: 'Inter', sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
}
#app {
width: 100%;
max-width: 1400px;
}
.app-container {
background: white;
border-radius: 32px;
box-shadow: 0 30px 50px -20px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.2rem 2rem;
background: #fff;
border-bottom: 1px solid #eaf0f6;
}
.user-info {
display: flex;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.user-greeting {
font-weight: 600;
font-size: 1.2rem;
color: #1a2b3c;
background: #f0f6fe;
padding: 0.4rem 1.2rem;
border-radius: 40px;
}
.logout-btn {
background: white;
border: 1.5px solid #dae2ed;
color: #3b4e62;
padding: 0.4rem 1.4rem;
border-radius: 40px;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.15s;
}
.logout-btn:hover {
background: #f1f7fd;
border-color: #9bb1c9;
}
.child-note {
background: #fdf6e6;
color: #926d39;
border-radius: 40px;
padding: 0.3rem 1.2rem;
font-size: 0.8rem;
font-weight: 500;
display: inline-block;
}
.nav-tabs {
display: flex;
gap: 0.8rem;
}
.nav-tab {
background: transparent;
border: none;
padding: 0.6rem 1.8rem;
font-size: 1rem;
font-weight: 600;
color: #5d7184;
border-radius: 40px;
cursor: pointer;
transition: 0.15s;
border: 1.5px solid transparent;
}
.nav-tab.active {
background: #2c3e50;
color: white;
}
.nav-tab:not(.active):hover {
background: #eef3f9;
color: #2c3e50;
}
.main-content {
padding: 2rem;
}
.borrow-layout {
display: flex;
flex-direction: column;
gap: 2.5rem;
}
.section-card {
background: #f9fcff;
border-radius: 24px;
padding: 1.6rem 1.8rem;
border: 1px solid #e6edf5;
}
.section-title {
font-size: 1.2rem;
font-weight: 600;
color: #1e3b5c;
margin-bottom: 1.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.search-row {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: flex-end;
margin-bottom: 2rem;
}
.search-field {
flex: 1 1 160px;
}
.search-field label {
display: block;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
color: #5f748b;
margin-bottom: 0.3rem;
}
.search-field input {
width: 100%;
padding: 0.6rem 1rem;
border: 1.5px solid #d8e2ee;
border-radius: 30px;
font-size: 0.95rem;
background: white;
}
.search-field input:focus {
outline: none;
border-color: #7e99b3;
}
.search-btn {
background: #2c3e50;
color: white;
border: none;
padding: 0.6rem 2rem;
border-radius: 30px;
font-weight: 500;
cursor: pointer;
font-size: 0.95rem;
transition: background 0.15s;
height: fit-content;
align-self: center;
}
.search-btn:hover {
background: #1a2b3c;
}
.table-wrapper {
overflow-x: auto;
border-radius: 18px;
background: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
margin-top: 1rem;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.9rem;
min-width: 1000px;
}
th {
background: #eef4fa;
color: #1d3b5c;
font-weight: 600;
padding: 1rem 0.8rem;
text-align: left;
white-space: nowrap;
}
td {
padding: 1rem 0.8rem;
border-bottom: 1px solid #eef3f8;
color: #1f2f40;
}
.action-btn {
background: #2c3e50;
color: white;
border: none;
padding: 0.4rem 1.2rem;
border-radius: 30px;
font-size: 0.8rem;
font-weight: 500;
cursor: pointer;
transition: background 0.15s;
}
.action-btn.return {
background: #9b6b43;
}
.action-btn.return:hover {
background: #7e4f2e;
}
.action-btn:hover {
background: #1a2b3c;
}
.child-badge {
background: #c9dff3;
color: #144a70;
padding: 0.2rem 0.8rem;
border-radius: 30px;
font-size: 0.75rem;
font-weight: 600;
display: inline-block;
}
.empty-row td {
padding: 2rem;
text-align: center;
color: #8c9db2;
font-style: italic;
}
.activity-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 1.8rem;
margin-top: 1.5rem;
}
.activity-card {
background: white;
border-radius: 24px;
padding: 1.5rem;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.03);
border: 1px solid #e6edf5;
transition: transform 0.1s;
display: flex;
flex-direction: column;
}
.activity-card:hover {
transform: translateY(-2px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.05);
}
.activity-name {
font-size: 1.2rem;
font-weight: 600;
color: #1e3b5c;
margin-bottom: 0.5rem;
}
.activity-time {
color: #6e8aa8;
font-size: 0.9rem;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.3rem;
}
.activity-desc {
color: #3a4e66;
font-size: 0.95rem;
line-height: 1.4;
margin: 0.8rem 0 1.2rem;
flex: 1;
}
.signup-btn {
background: #2c3e50;
color: white;
border: none;
padding: 0.6rem 1rem;
border-radius: 40px;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
transition: background 0.15s;
align-self: flex-start;
width: 100%;
}
.signup-btn:hover {
background: #1a2b3c;
}
.info-note {
background: #eaf2fb;
padding: 0.5rem 1.2rem;
border-radius: 40px;
color: #2c577c;
font-size: 0.85rem;
display: inline-block;
}
.empty-placeholder {
text-align: center;
padding: 2rem;
background: white;
border-radius: 18px;
color: #8f9fb1;
}
</style>
</head>
<body>
<div id="app">
<div class="app-container">
<div class="header">
<div class="user-info">
<span class="user-greeting">👋 {{ currentUser.name }}</span>
<button class="logout-btn" @click="logout">退出登录</button>
<span v-if="currentUser.type === '儿童'" class="child-note">🧸 儿童读者 · 仅显示儿童书目</span>
</div>
<div class="nav-tabs">
<button class="nav-tab" :class="{ active: activeTab === 'borrow' }" @click="activeTab = 'borrow'">📚 借阅</button>
<button class="nav-tab" :class="{ active: activeTab === 'activity' }" @click="activeTab = 'activity'">🎉 活动</button>
</div>
</div>
<div class="main-content">
<div v-if="activeTab === 'borrow'" class="borrow-layout">
<!-- 书目查询 -->
<div class="section-card">
<div class="section-title">
<span>🔍 书目查询 · 借阅</span>
<span class="info-note">支持 ISBN / 书名 / 作者</span>
</div>
<div class="search-row">
<div class="search-field">
<label>ISBN</label>
<input v-model="searchIsbn" type="text" placeholder="如 978-7-01" />
</div>
<div class="search-field">
<label>书名</label>
<input v-model="searchTitle" type="text" placeholder="关键词" />
</div>
<div class="search-field">
<label>作者</label>
<input v-model="searchAuthor" type="text" placeholder="关键词" />
</div>
<button class="search-btn" @click="searchBooks">查询书目</button>
</div>
<div v-if="bookTableVisible" class="table-wrapper">
<table>
<thead>
<tr>
<th>book_id</th><th>ISBN</th><th>书名</th><th>作者</th><th>出版社</th><th>出版年</th><th>可借</th><th>儿童书</th><th>简介</th><th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="filteredBooks.length === 0">
<td colspan="10" class="empty-row">没有符合条件的书籍</td>
</tr>
<tr v-for="book in filteredBooks" :key="book.bookId">
<td>{{ book.bookId }}</td>
<td>{{ book.isbn }}</td>
<td>{{ book.title }}</td>
<td>{{ book.author }}</td>
<td>{{ book.publisher }}</td>
<td>{{ book.publishYear }}</td>
<td>{{ book.availableCopies }}</td>
<td>
<span v-if="book.targetAudience === '儿童'" class="child-badge">儿童书</span>
<span v-else style="color:#8a9bb0;">通用</span>
</td>
<td>{{ book.description || '—' }}</td>
<td><button class="action-btn" @click="borrowBook(book)">借阅</button></td>
</tr>
</tbody>
</table>
</div>
<div v-else class="empty-placeholder">
⚡ 请输入条件并点击“查询书目”查看结果
</div>
</div>
<!-- 我的借阅 -->
<div class="section-card">
<div class="section-title">
<span>📋 我的借阅 · 归还</span>
<span class="info-note">输入账号号码查询</span>
</div>
<div class="search-row" style="margin-bottom:1.5rem;">
<div class="search-field" style="flex:2;">
<label>读者证号</label>
<input v-model="borrowerAccount" type="text" placeholder="例如 R2024001" readonly/>
</div>
<button class="search-btn" @click="searchBorrowed">查询借阅</button>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>借阅ID</th><th>ISBN</th><th>书名</th><th>作者</th><th>借阅日期</th><th>应还日期</th><th>状态</th><th>操作</th>
</tr>
</thead>
<tbody>
<tr v-if="borrowedList.length === 0">
<td colspan="8" class="empty-row">暂无借阅记录</td>
</tr>
<tr v-for="item in borrowedList" :key="item.borrowId">
<td>{{ item.borrowId }}</td>
<td>{{ item.isbn }}</td>
<td>{{ item.bookTitle }}</td>
<td>{{ item.author }}</td>
<td>{{ item.borrowDate }}</td>
<td>{{ item.dueDate }}</td>
<td><span style="color:#2e7d5e;">{{ item.status }}</span></td>
<td><button class="action-btn return" @click="returnBook(item)">归还</button></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 活动页面 -->
<div v-else class="activity-page">
<h3 style="font-size:1.4rem; color:#1e3b5c; margin-bottom:1.2rem;">🎈 近期活动</h3>
<div class="activity-grid">
<div v-for="act in activities" :key="act.id" class="activity-card">
<div class="activity-name">{{ act.name }}</div>
<div class="activity-time">📅 {{ act.startTime }} ~ {{ act.endTime }}</div>
<div class="activity-desc">{{ act.description }}</div>
<button class="signup-btn" @click="signupActivity(act)">报名参加</button>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
(function() {
const { createApp, ref, computed, onMounted } = Vue;
// 从服务器端传递的初始数据
const initialUser = <%= readerJson %>;
const app = createApp({
setup() {
const currentUser = ref(initialUser);
const activeTab = ref('borrow');
// 书目查询
const searchIsbn = ref('');
const searchTitle = ref('');
const searchAuthor = ref('');
const bookTableVisible = ref(false);
const books = ref([]); // 存储查询结果
const filteredBooks = computed(() => books.value);
const searchBooks = async () => {
const params = new URLSearchParams({
isbn: searchIsbn.value,
title: searchTitle.value,
author: searchAuthor.value
});
try {
const response = await fetch('${pageContext.request.contextPath}/api/books/search?' + params, {
method: 'GET',
credentials: 'include',
});
if (!response.ok) throw new Error('查询失败');
books.value = await response.json();
bookTableVisible.value = true;
} catch (e) {
alert('查询失败:' + e.message);
}
};
const borrowBook = async (book) => {
const params = new URLSearchParams({
readerCard: currentUser.value.account,
bookId: book.bookId
});
try {
const response = await fetch('${pageContext.request.contextPath}/api/borrow', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
credentials: 'include' // 添加此行
});
const result = await response.json();
if (result.success) {
alert('借阅成功');
await searchBorrowed(); // 刷新借阅列表
} else {
alert(result.message || '借阅失败');
}
} catch (e) {
alert('网络错误');
}
};
// 借阅记录
const borrowerAccount = ref(currentUser.value.account);
const borrowedList = ref([]);
const searchBorrowed = async () => {
if (!borrowerAccount.value) {
alert('请输入读者证号');
return;
}
try {
const response = await fetch('${pageContext.request.contextPath}/api/borrows?readerCard=' + encodeURIComponent(borrowerAccount.value), {
method: 'GET',
credentials: 'include',
});
if (!response.ok) throw new Error('查询失败');
borrowedList.value = await response.json();
} catch (e) {
alert('查询借阅记录失败');
}
};
const returnBook = async (item) => {
console.log('>>> [归还] 点击归还按钮borrowId=' + item.borrowId);
const params = new URLSearchParams({
borrowId: item.borrowId
});
try {
const response = await fetch('${pageContext.request.contextPath}/api/borrow', {
method: 'PUT',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
credentials: 'include',
});
console.log('>>> [归还] HTTP状态码=' + response.status);
const result = await response.json();
console.log('>>> [归还] 后端返回=' + JSON.stringify(result));
if (result.success) {
alert('归还成功');
await searchBorrowed();
} else {
alert('归还失败:' + (result.message || '未知原因,请查看控制台日志'));
}
} catch (e) {
console.error('>>> [归还] 请求异常:' + e);
alert('网络错误:' + e.message);
}
};
// 活动
const activities = ref([]);
const loadActivities = async () => {
try {
const response = await fetch('${pageContext.request.contextPath}/api/activities');
if (!response.ok) throw new Error('加载活动失败');
activities.value = await response.json();
} catch (e) {
console.error('加载活动失败', e);
}
};
const signupActivity = async (act) => {
const params = new URLSearchParams({
activityId: act.id
});
try {
const response = await fetch('${pageContext.request.contextPath}/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params,
credentials: 'include' // 确保携带 session cookie
});
const result = await response.json();
if (result.success) {
alert('报名成功');
} else {
alert(result.message || '报名失败');
}
} catch (e) {
alert('网络错误');
}
};
// 退出登录
const logout = async () => {
// 可调用登出 API如清除 session然后跳转
window.location.href = '${pageContext.request.contextPath}/view/Login.jsp';
};
onMounted(() => {
loadActivities();
searchBorrowed(); // 加载当前读者的借阅记录
});
return {
currentUser,
activeTab,
searchIsbn,
searchTitle,
searchAuthor,
bookTableVisible,
filteredBooks,
searchBooks,
borrowBook,
borrowerAccount,
borrowedList,
searchBorrowed,
returnBook,
activities,
signupActivity,
logout
};
}
});
app.mount('#app');
})();
</script>
</body>
</html>