首次创建结构

This commit is contained in:
2026-03-23 08:59:33 +08:00
commit 143f90e2d5
235 changed files with 42768 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
Manifest-Version: 1.0
Class-Path:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
</web-app>

310
WebContent/view/Login.jsp Normal file
View File

@@ -0,0 +1,310 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!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:520px; }
.card { background:white; border-radius:28px; box-shadow:0 25px 45px -12px rgba(0,0,0,0.15); padding:2.2rem 2rem; width:100%; }
h2 { font-size:1.9rem; font-weight:600; color:#1a2b3c; margin-bottom:1.5rem; border-bottom:2px solid #f0f4f9; padding-bottom:1rem; }
h3 { font-size:1.2rem; font-weight:500; color:#2c3e50; margin-bottom:1.2rem; }
.field { margin-bottom:1.3rem; }
label { display:block; font-size:0.8rem; font-weight:600; text-transform:uppercase; color:#4a5e74; margin-bottom:0.3rem; }
input, select { width:100%; padding:0.8rem 1rem; font-size:1rem; border:1.5px solid #e2e9f2; border-radius:16px; background:#fff; transition:border 0.15s; outline:none; }
input:focus, select:focus { border-color:#7c8ea0; background:#fafcff; }
button { background:#2c3e50; color:white; border:none; padding:0.9rem 1.5rem; font-size:1rem; font-weight:500; border-radius:40px; cursor:pointer; transition:background 0.15s; width:100%; margin-top:0.3rem; }
button:hover { background:#1d2c3a; }
.secondary-btn { background:white; color:#2c3e50; border:1.5px solid #d0ddee; margin-top:1rem; }
.secondary-btn:hover { background:#f2f6fc; border-color:#a0b8cf; }
.nav-links { display:flex; justify-content:space-between; margin-top:1.5rem; gap:0.5rem; flex-wrap:wrap; }
.nav-link { background:#f0f5fb; color:#2e405b; padding:0.6rem 1.2rem; border-radius:40px; font-size:0.9rem; font-weight:500; cursor:pointer; border:1px solid transparent; transition:background 0.15s; text-align:center; flex:1 1 auto; }
.nav-link:hover { background:#dae2ed; color:#0e1e2f; }
.active-view { margin-bottom:1rem; }
.note { font-size:0.75rem; color:#8f9fb1; margin-top:0.5rem; text-align:center; }
.compact-form { background:#f9fcff; border-radius:20px; padding:1.5rem 1.2rem; margin:0.5rem 0 1rem; }
.nav-link.active-nav { background:#2c3e50; color:white; border-color:#2c3e50; }
.nav-link.active-nav:hover { background:#1d2c3a; }
</style>
</head>
<body>
<div id="app">
<div class="card">
<h2>📖 读者门户</h2>
<!-- 动态视图:读者登录 / 读者注册 / 员工登录 -->
<div class="active-view">
<!-- 读者登录视图 -->
<div v-if="currentView === 'reader-login'">
<h3>👤 读者登录</h3>
<div class="field">
<label>证号</label>
<input v-model="readerLogin.cardNumber" type="text" placeholder="例如R2024001" />
</div>
<div class="field">
<label>姓名</label>
<input v-model="readerLogin.name" type="text" placeholder="你的名字" />
</div>
<button @click="handleReaderLogin">登录</button>
<div class="note">跳转与验证由后端处理</div>
</div>
<!-- 读者注册视图 -->
<div v-else-if="currentView === 'reader-register'">
<h3>📝 注册新读者</h3>
<div class="compact-form">
<div class="field">
<label>证号</label>
<input v-model="registerForm.cardNumber" type="text" placeholder="唯一证号" />
</div>
<div class="field">
<label>姓名</label>
<input v-model="registerForm.name" type="text" placeholder="真实姓名" />
</div>
<div class="field">
<label>年龄(选填)</label>
<input v-model="registerForm.age" type="number" placeholder="可选" min="0" max="120" />
</div>
<div class="field">
<label>读者类型</label>
<select v-model="registerForm.type">
<option>老年</option>
<option>成人</option>
<option>儿童</option>
</select>
</div>
<div class="field">
<label>联系方式</label>
<input v-model="registerForm.contact" type="text" placeholder="手机 / 邮箱" />
</div>
<div class="field">
<label>注册日期</label>
<input v-model="registerForm.regDate" type="date" />
</div>
</div>
<button @click="handleRegister">提交注册</button>
</div>
<!-- 员工登录视图 -->
<div v-else-if="currentView === 'employee-login'">
<h3>🔐 员工登录</h3>
<div class="compact-form">
<div class="field">
<label>员工工号</label>
<input v-model="employeeLogin.employeeCode" type="text" placeholder="员工工号" />
</div>
<div class="field">
<label>姓名</label>
<input v-model="employeeLogin.name" type="text" placeholder="姓名" />
</div>
</div>
<button @click="handleEmployeeLogin">登录</button>
<div class="note">经理页面另有添加用户功能</div>
</div>
</div>
<!-- 导航链接 -->
<div class="nav-links">
<span class="nav-link" @click="switchView('reader-login')" :class="{ 'active-nav': currentView === 'reader-login' }">🔑 读者登录</span>
<span class="nav-link" @click="switchView('reader-register')" :class="{ 'active-nav': currentView === 'reader-register' }"> 注册</span>
<span class="nav-link" @click="switchView('employee-login')" :class="{ 'active-nav': currentView === 'employee-login' }">👔 员工登录</span>
</div>
<div style="margin-top: 1.2rem; text-align: center; font-size:0.8rem; color:#abb9c9;">
点击上方标签切换功能
</div>
</div>
</div>
<script>
(function() {
const { createApp, ref } = Vue;
// 工具函数:获取今日日期字符串 yyyy-MM-dd
const getTodayString = () => {
const d = new Date();
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// 工具函数:验证日期字符串是否符合 yyyy-MM-dd 格式
const isValidDate = (dateStr) => {
if (!dateStr) return false;
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(dateStr)) return false;
const date = new Date(dateStr);
return !isNaN(date.getTime());
};
const app = createApp({
setup() {
const currentView = ref('reader-login');
// 读者登录数据
const readerLogin = ref({
cardNumber: '',
name: ''
});
// 员工登录数据
const employeeLogin = ref({
employeeCode: '',
name: ''
});
// 注册表单默认值
const getDefaultRegisterForm = () => ({
cardNumber: '',
name: '',
age: '',
type: '成人',
contact: '',
regDate: getTodayString()
});
const registerForm = ref(getDefaultRegisterForm());
// 切换视图时重置数据
const switchView = (view) => {
currentView.value = view;
if (view === 'reader-register') {
registerForm.value = getDefaultRegisterForm();
} else if (view === 'reader-login') {
readerLogin.value = { cardNumber: '', name: '' };
} else if (view === 'employee-login') {
employeeLogin.value = { employeeCode: '', name: '' };
}
};
// 读者登录
const handleReaderLogin = async () => {
if (!readerLogin.value.cardNumber || !readerLogin.value.name) {
alert('请填写证号和姓名');
return;
}
const params = new URLSearchParams({
cardNumber: readerLogin.value.cardNumber,
name: readerLogin.value.name
});
try {
const response = await fetch('${pageContext.request.contextPath}/api/login/reader', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
const result = await response.json();
if (result.success) {
window.location.href = '${pageContext.request.contextPath}/view/Reader.jsp';
} else {
alert(result.message || '登录失败');
}
} catch (e) {
console.error(e);
alert('网络错误,请检查后端服务是否正常');
}
};
// 读者注册
const handleRegister = async () => {
// 前端基础校验
if (!registerForm.value.cardNumber) {
alert('证号不能为空');
return;
}
if (!registerForm.value.name) {
alert('姓名不能为空');
return;
}
// 年龄如果是空字符串或非数字设为空后端会处理为null
let ageVal = registerForm.value.age;
if (ageVal !== '' && isNaN(Number(ageVal))) {
alert('年龄请输入数字');
return;
}
// 日期处理:如果无效或为空,使用今天日期
let regDateVal = registerForm.value.regDate;
if (!isValidDate(regDateVal)) {
regDateVal = getTodayString();
registerForm.value.regDate = regDateVal; // 同步更新显示
}
const params = new URLSearchParams({
cardNumber: registerForm.value.cardNumber,
name: registerForm.value.name,
age: ageVal,
type: registerForm.value.type,
contact: registerForm.value.contact || '',
regDate: regDateVal
});
try {
const response = await fetch('${pageContext.request.contextPath}/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
const result = await response.json();
if (result.success) {
alert('注册成功,请登录');
switchView('reader-login');
} else {
alert(result.message || '注册失败');
}
} catch (e) {
console.error(e);
alert('网络错误,请检查后端服务是否正常');
}
};
// 员工登录
const handleEmployeeLogin = async () => {
if (!employeeLogin.value.employeeCode || !employeeLogin.value.name) {
alert('请填写工号和姓名');
return;
}
const params = new URLSearchParams({
employeeCode: employeeLogin.value.employeeCode,
name: employeeLogin.value.name
});
try {
const response = await fetch('${pageContext.request.contextPath}/api/login/employee', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
const result = await response.json();
if (result.success) {
window.location.href = '${pageContext.request.contextPath}/view/Work.jsp';
} else {
alert(result.message || '登录失败');
}
} catch (e) {
console.error(e);
alert('网络错误,请检查后端服务是否正常');
}
};
return {
currentView,
readerLogin,
employeeLogin,
registerForm,
switchView,
handleReaderLogin,
handleRegister,
handleEmployeeLogin
};
}
});
app.mount('#app');
})();
</script>
</body>
</html>

382
WebContent/view/Reader.jsp Normal file
View File

@@ -0,0 +1,382 @@
<%@ 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");
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" />
</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'
});
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
});
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'
});
if (!response.ok) throw new Error('查询失败');
borrowedList.value = await response.json();
} catch (e) {
alert('查询借阅记录失败');
}
};
const returnBook = async (item) => {
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
});
const result = await response.json();
if (result.success) {
alert('归还成功');
await searchBorrowed();
} else {
alert('归还失败');
}
} catch (e) {
alert('网络错误');
}
};
// 活动
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
});
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>

618
WebContent/view/Work.jsp Normal file
View File

@@ -0,0 +1,618 @@
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
com.chinasofti.model.Employee currentEmployee = (com.chinasofti.model.Employee) session.getAttribute("currentEmployee");
if (currentEmployee == null) {
response.sendRedirect(request.getContextPath() + "/Login.jsp");
return;
}
String positionDesc = "";
switch (currentEmployee.getPosition()) {
case 1: positionDesc = "馆员"; break;
case 2: positionDesc = "工作人员"; break;
case 3: positionDesc = "经理"; break;
}
String empJson = String.format(
"{\"id\":\"%s\",\"name\":\"%s\",\"position\":\"%s\"}",
currentEmployee.getEmployeeCode(),
currentEmployee.getName(),
positionDesc
);
request.setAttribute("empJson", empJson);
%>
<!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; }
.employee-header { display:flex; align-items:center; justify-content:space-between; padding:1.2rem 2rem; background:#fff; border-bottom:1px solid #eaf0f6; flex-wrap:wrap; gap:1rem; }
.employee-info { display:flex; align-items:center; gap:2rem; flex-wrap:wrap; }
.info-item { background:#f0f6fe; padding:0.4rem 1.2rem; border-radius:40px; font-weight:500; color:#1a2b3c; }
.position-selector { display:flex; align-items:center; gap:1rem; }
.position-selector label { font-weight:600; color:#3a4e66; font-size:0.9rem; }
.position-dropdown { padding:0.5rem 2rem 0.5rem 1.2rem; border:1.5px solid #d0ddee; border-radius:40px; font-size:1rem; background:white; cursor:pointer; outline:none; font-weight:500; color:#1a2b3c; }
.position-dropdown:focus { border-color:#7c8ea0; }
.main-content { padding:2rem; }
.section-card { background:#f9fcff; border-radius:24px; padding:1.6rem 1.8rem; border:1px solid #e6edf5; margin-bottom:2rem; }
.section-title { font-size:1.2rem; font-weight:600; color:#1e3b5c; margin-bottom:1.5rem; display:flex; align-items:center; gap:0.5rem; }
.form-row { display:flex; flex-wrap:wrap; gap:1rem; align-items:flex-end; margin-bottom:1.5rem; }
.form-field { flex:1 1 160px; }
.form-field label { display:block; font-size:0.75rem; font-weight:600; text-transform:uppercase; color:#5f748b; margin-bottom:0.3rem; }
.form-field input, .form-field select { width:100%; padding:0.6rem 1rem; border:1.5px solid #d8e2ee; border-radius:30px; font-size:0.95rem; background:white; }
.form-field input:focus, .form-field select:focus { outline:none; border-color:#7e99b3; }
.btn { background:#2c3e50; color:white; border:none; padding:0.6rem 1.8rem; border-radius:30px; font-weight:500; cursor:pointer; font-size:0.95rem; transition:background 0.15s; height:fit-content; border:1px solid transparent; }
.btn:hover { background:#1a2b3c; }
.btn-outline { background:white; color:#2c3e50; border:1.5px solid #d0ddee; }
.btn-outline:hover { background:#f2f6fc; border-color:#a0b8cf; }
.btn-sm { padding:0.3rem 1rem; font-size:0.8rem; }
.table-wrapper { overflow-x:auto; border-radius:18px; background:white; box-shadow:0 4px 12px rgba(0,0,0,0.02); margin-top:1.5rem; }
table { width:100%; border-collapse:collapse; font-size:0.9rem; min-width:800px; }
th { background:#eef4fa; color:#1d3b5c; font-weight:600; padding:1rem 0.8rem; text-align:left; }
td { padding:1rem 0.8rem; border-bottom:1px solid #eef3f8; color:#1f2f40; }
.action-cell { display:flex; gap:0.5rem; flex-wrap:wrap; }
.badge { background:#c9dff3; color:#144a70; padding:0.2rem 0.8rem; border-radius:30px; font-size:0.75rem; font-weight:600; display:inline-block; }
.grid-2col { display:grid; grid-template-columns:1fr 1fr; gap:2rem; }
@media (max-width:800px) { .grid-2col { grid-template-columns:1fr; } }
.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="employee-header">
<div class="employee-info">
<span class="info-item">🆔 工号:{{ employee.id }}</span>
<span class="info-item">👤 姓名:{{ employee.name }}</span>
<span class="info-item">📌 职位:{{ employee.position }}</span>
</div>
<div class="position-selector">
<label for="positionSelect">切换职位(模拟权限):</label>
<select id="positionSelect" v-model="selectedPosition" class="position-dropdown">
<option value="馆员">馆员 (staff)</option>
<option value="工作人员">工作人员 (worker)</option>
<option value="经理">经理 (manager)</option>
</select>
</div>
</div>
<!-- 主区域:根据 selectedPosition 渲染不同功能 -->
<div class="main-content">
<!-- 馆员视图:图书增删改查 -->
<div v-if="selectedPosition === '馆员'">
<div class="section-card">
<div class="section-title">📚 图书管理 · 馆员</div>
<div class="form-row">
<div class="form-field">
<label>ISBN</label>
<input v-model="bookForm.isbn" type="text" placeholder="ISBN" />
</div>
<div class="form-field">
<label>书名</label>
<input v-model="bookForm.title" type="text" placeholder="书名" />
</div>
<div class="form-field">
<label>作者</label>
<input v-model="bookForm.author" type="text" placeholder="作者" />
</div>
<div class="form-field">
<label>出版社</label>
<input v-model="bookForm.publisher" type="text" placeholder="出版社" />
</div>
<div class="form-field">
<label>出版年</label>
<input v-model.number="bookForm.publishYear" type="number" placeholder="2025" />
</div>
<div class="form-field">
<label>总库存</label>
<input v-model.number="bookForm.totalStock" type="number" placeholder="库存" />
</div>
<div class="form-field">
<label>面向群体</label>
<select v-model="bookForm.targetAudience">
<option value="通用">通用</option>
<option value="儿童">儿童</option>
<option value="成人">成人</option>
<option value="老年">老年</option>
</select>
</div>
<div class="form-field">
<label>简介</label>
<input v-model="bookForm.description" type="text" placeholder="简介" />
</div>
<button class="btn" @click="addBook">添加图书</button>
</div>
<div 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><th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="book in books" :key="book.id">
<td>{{ book.id }}</td>
<td>{{ book.isbn }}</td>
<td>{{ book.title }}</td>
<td>{{ book.author }}</td>
<td>{{ book.publisher }}</td>
<td>{{ book.publishYear }}</td>
<td>{{ book.totalStock }}</td>
<td>{{ book.borrowedCount || 0 }}</td>
<td><span v-if="book.targetAudience === '儿童'" class="badge">儿童</span><span v-else>通用</span></td>
<td>{{ book.description || '—' }}</td>
<td class="action-cell">
<button class="btn btn-sm" @click="editBook(book)">编辑</button>
<button class="btn btn-sm btn-outline" @click="deleteBook(book)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 工作人员视图:帮助注册 + 借阅/归还 -->
<div v-else-if="selectedPosition === '工作人员'">
<div class="grid-2col">
<!-- 左侧:特殊群体读者注册 -->
<div class="section-card">
<div class="section-title">👥 帮助注册(老年/特殊群体)</div>
<div class="form-field" style="margin-bottom: 1rem;">
<label>证号</label>
<input v-model="registerForm.cardNumber" placeholder="例如 R2025001" />
</div>
<div class="form-field" style="margin-bottom: 1rem;">
<label>姓名</label>
<input v-model="registerForm.name" placeholder="姓名" />
</div>
<div class="form-field" style="margin-bottom: 1rem;">
<label>年龄(选填)</label>
<input v-model.number="registerForm.age" type="number" placeholder="年龄" />
</div>
<div class="form-field" style="margin-bottom: 1rem;">
<label>读者类型</label>
<select v-model="registerForm.type">
<option>老年</option>
<option>成人</option>
<option>儿童</option>
</select>
</div>
<div class="form-field" style="margin-bottom: 1.5rem;">
<label>联系方式</label>
<input v-model="registerForm.contact" placeholder="手机/邮箱" />
</div>
<button class="btn" @click="staffRegister">注册读者</button>
</div>
<!-- 右侧:借阅/归还(基于读者账号) -->
<div class="section-card">
<div class="section-title">📖 代借阅/归还</div>
<div class="form-row" style="margin-bottom: 1.5rem;">
<div class="form-field" style="flex:2;">
<label>读者证号</label>
<input v-model="borrowerAccount" placeholder="如 R2024001" />
</div>
<button class="btn" @click="searchBorrowedStaff">查询借阅</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></tr></thead>
<tbody>
<tr v-if="staffBorrowedList.length === 0"><td colspan="7" class="empty-placeholder">暂无借阅记录</td></tr>
<tr v-for="item in staffBorrowedList" :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><button class="btn btn-sm" @click="returnBookStaff(item)">归还</button></td>
</tr>
</tbody>
</table>
</div>
<div style="margin-top:1.5rem;">
<div class="form-row">
<div class="form-field">
<label>ISBN/书名</label>
<input v-model="quickBorrowIsbn" placeholder="输入ISBN或书名" />
</div>
<button class="btn" @click="quickBorrow">快速借书</button>
</div>
</div>
</div>
</div>
</div>
<!-- 经理视图:活动增删改查 + 员工注册 -->
<div v-else-if="selectedPosition === '经理'">
<div class="grid-2col">
<!-- 左侧:活动管理 -->
<div class="section-card">
<div class="section-title">🎉 活动管理</div>
<div class="form-row">
<div class="form-field">
<label>活动名称</label>
<input v-model="activityForm.name" />
</div>
<div class="form-field">
<label>开始时间</label>
<input v-model="activityForm.startTime" placeholder="2025-05-10 15:00" />
</div>
<div class="form-field">
<label>结束时间</label>
<input v-model="activityForm.endTime" placeholder="2025-05-10 17:00" />
</div>
<div class="form-field">
<label>地点</label>
<input v-model="activityForm.location" />
</div>
<div class="form-field">
<label>简介</label>
<input v-model="activityForm.description" />
</div>
<button class="btn btn-sm" @click="addActivity">添加</button>
</div>
<div class="table-wrapper">
<table>
<thead><tr><th>名称</th><th>时间</th><th>地点</th><th>简介</th><th>操作</th></tr></thead>
<tbody>
<tr v-for="act in activities" :key="act.id">
<td>{{ act.name }}</td><td>{{ act.startTime }}</td><td>{{ act.location }}</td><td>{{ act.description }}</td>
<td class="action-cell">
<button class="btn btn-sm" @click="editActivity(act)">编辑</button>
<button class="btn btn-sm btn-outline" @click="deleteActivity(act)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 右侧:新员工注册 -->
<div class="section-card">
<div class="section-title">👔 新员工注册</div>
<div class="form-field" style="margin-bottom:1rem;">
<label>工号</label>
<input v-model="newEmployee.employeeCode" placeholder="如 E001" />
</div>
<div class="form-field" style="margin-bottom:1rem;">
<label>姓名</label>
<input v-model="newEmployee.name" placeholder="姓名" />
</div>
<div class="form-field" style="margin-bottom:1.5rem;">
<label>职位</label>
<select v-model="newEmployee.position">
<option>馆员</option>
<option>工作人员</option>
<option>经理</option>
</select>
</div>
<button class="btn" @click="addEmployee">注册新员工</button>
<div style="margin-top:2rem;">
<div class="section-title" style="margin-bottom:0.8rem;">📋 现有员工</div>
<div class="table-wrapper">
<table>
<thead><tr><th>工号</th><th>姓名</th><th>职位</th></tr></thead>
<tbody>
<tr v-for="emp in employees" :key="emp.employeeCode">
<td>{{ emp.employeeCode }}</td><td>{{ emp.name }}</td><td>{{ emp.positionDesc }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
(function() {
const { createApp, ref, onMounted } = Vue;
// 从服务器端传递的初始员工数据
const initialEmployee = ${empJson};
const app = createApp({
setup() {
const employee = ref(initialEmployee);
const selectedPosition = ref(employee.value.position);
// ---------- 图书数据 ----------
const books = ref([]);
const bookForm = ref({
isbn: '',
title: '',
author: '',
publisher: '',
publishYear: null,
totalStock: null,
targetAudience: '通用',
description: ''
});
const loadBooks = async () => {
try {
const response = await fetch('${pageContext.request.contextPath}/api/books');
books.value = await response.json();
} catch (e) {
console.error('加载图书失败', e);
}
};
const addBook = async () => {
const params = new URLSearchParams(bookForm.value);
try {
const response = await fetch('${pageContext.request.contextPath}/api/books', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
const result = await response.json();
if (result.success) {
alert('添加成功');
await loadBooks();
bookForm.value = { isbn: '', title: '', author: '', publisher: '', publishYear: null, totalStock: null, targetAudience: '通用', description: '' };
} else {
alert(result.message || '添加失败');
}
} catch (e) {
alert('网络错误');
}
};
const editBook = (book) => {
alert('编辑功能待实现,请直接使用更新接口');
};
const deleteBook = async (book) => {
if (!confirm('确定删除吗?')) return;
try {
const response = await fetch('${pageContext.request.contextPath}/api/books/' + book.id, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
alert('删除成功');
await loadBooks();
} else {
alert(result.message || '删除失败');
}
} catch (e) {
alert('网络错误');
}
};
// ---------- 工作人员:注册表单 ----------
const registerForm = ref({
cardNumber: '',
name: '',
age: null,
type: '老年',
contact: '',
regDate: new Date().toISOString().slice(0,10)
});
const staffRegister = async () => {
const params = new URLSearchParams(registerForm.value);
try {
const response = await fetch('${pageContext.request.contextPath}/api/staff/register', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
const result = await response.json();
if (result.success) {
alert('注册成功');
registerForm.value = { cardNumber: '', name: '', age: null, type: '老年', contact: '', regDate: new Date().toISOString().slice(0,10) };
} else {
alert(result.message || '注册失败');
}
} catch (e) {
alert('网络错误');
}
};
// 借阅数据
const staffBorrowedList = ref([]);
const borrowerAccount = ref('');
const searchBorrowedStaff = async () => {
if (!borrowerAccount.value) {
alert('请输入读者证号');
return;
}
try {
const response = await fetch('${pageContext.request.contextPath}/api/borrows?readerCard=' + encodeURIComponent(borrowerAccount.value));
staffBorrowedList.value = await response.json();
} catch (e) {
alert('查询失败');
}
};
const returnBookStaff = async (item) => {
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
});
const result = await response.json();
if (result.success) {
alert('归还成功');
await searchBorrowedStaff();
} else {
alert(result.message || '归还失败');
}
} catch (e) {
alert('网络错误');
}
};
const quickBorrowIsbn = ref('');
const quickBorrow = async () => {
if (!borrowerAccount.value) {
alert('请先输入读者证号');
return;
}
const params = new URLSearchParams({
readerCard: borrowerAccount.value,
isbnOrTitle: quickBorrowIsbn.value
});
try {
const response = await fetch('${pageContext.request.contextPath}/api/borrow/quick', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
const result = await response.json();
if (result.success) {
alert('快速借书成功');
await searchBorrowedStaff();
quickBorrowIsbn.value = '';
} else {
alert(result.message || '快速借书失败');
}
} catch (e) {
alert('网络错误');
}
};
// ---------- 经理:活动管理 ----------
const activities = ref([]);
const activityForm = ref({ name: '', startTime: '', endTime: '', location: '', description: '' });
const loadActivities = async () => {
try {
const response = await fetch('${pageContext.request.contextPath}/api/activities');
activities.value = await response.json();
} catch (e) {
console.error('加载活动失败', e);
}
};
const addActivity = async () => {
const params = new URLSearchParams(activityForm.value);
try {
const response = await fetch('${pageContext.request.contextPath}/api/activities', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
const result = await response.json();
if (result.success) {
alert('活动添加成功');
await loadActivities();
activityForm.value = { name: '', startTime: '', endTime: '', location: '', description: '' };
} else {
alert(result.message || '添加失败');
}
} catch (e) {
alert('网络错误');
}
};
const editActivity = (act) => {
alert('编辑功能待实现');
};
const deleteActivity = async (act) => {
if (!confirm('确定删除吗?')) return;
try {
const response = await fetch('${pageContext.request.contextPath}/api/activities/' + act.id, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
alert('删除成功');
await loadActivities();
} else {
alert(result.message || '删除失败');
}
} catch (e) {
alert('网络错误');
}
};
// 员工管理
const employees = ref([]);
const newEmployee = ref({ employeeCode: '', name: '', position: '馆员' });
const loadEmployees = async () => {
try {
const response = await fetch('${pageContext.request.contextPath}/api/employees');
employees.value = await response.json();
} catch (e) {
console.error('加载员工失败', e);
}
};
const addEmployee = async () => {
const params = new URLSearchParams(newEmployee.value);
try {
const response = await fetch('${pageContext.request.contextPath}/api/employees', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params
});
const result = await response.json();
if (result.success) {
alert('员工注册成功');
await loadEmployees();
newEmployee.value = { employeeCode: '', name: '', position: '馆员' };
} else {
alert(result.message || '注册失败');
}
} catch (e) {
alert('网络错误');
}
};
onMounted(() => {
loadBooks();
loadActivities();
loadEmployees();
});
return {
employee,
selectedPosition,
books,
bookForm,
addBook,
editBook,
deleteBook,
registerForm,
staffRegister,
staffBorrowedList,
borrowerAccount,
searchBorrowedStaff,
returnBookStaff,
quickBorrowIsbn,
quickBorrow,
activities,
activityForm,
addActivity,
editActivity,
deleteActivity,
employees,
newEmployee,
addEmployee
};
}
});
app.mount('#app');
})();
</script>
</body>
</html>

18272
WebContent/view/vue3.js Normal file

File diff suppressed because it is too large Load Diff