Week 1-8: Spring Boot 学习计划完整项目

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 23:45:17 +08:00
commit f95aa18724
201 changed files with 18595 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
/* ========== 基础重置 ========== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; padding: 20px; color: #333;
}
.container {
max-width: 1100px; margin: 0 auto;
}
/* ========== 头部 ========== */
header {
text-align: center; padding: 30px 20px 20px;
}
header h1 { font-size: 2em; color: #fff; margin-bottom: 6px; }
.subtitle { color: rgba(255,255,255,0.75); font-size: 0.95em; }
.main-header {
display: flex; justify-content: space-between; align-items: center;
padding: 20px; background: #fff; border-radius: 12px 12px 0 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.main-header h1 { font-size: 1.4em; color: #333; margin-bottom: 0; }
.main-header .subtitle { color: #999; margin-top: 2px; }
/* ========== 登录/注册卡片 ========== */
.login-card {
background: #fff; border-radius: 12px; padding: 36px 30px;
max-width: 420px; margin: 0 auto;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
}
.login-card h2 { font-size: 1.3em; margin-bottom: 18px; color: #333; }
.hint { color: #999; font-size: 0.85em; margin-bottom: 14px; }
.hint.small { font-size: 0.8em; text-align: center; }
.hint a { color: #667eea; text-decoration: none; }
/* ========== 表单 ========== */
.form-group { margin-bottom: 14px; }
.form-group label { display: block; font-size: 0.85em; color: #666; margin-bottom: 4px; font-weight: 500; }
.form-group input, .form-group select {
width: 100%; padding: 10px 12px; border: 1.5px solid #e0e0e0;
border-radius: 8px; font-size: 0.95em; transition: border-color 0.2s;
}
.form-group input:focus {
outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102,126,234,0.15);
}
.form-row { display: flex; gap: 12px; }
.form-row .form-group { flex: 1; }
/* ========== 按钮 ========== */
.btn {
display: inline-flex; align-items: center; justify-content: center;
padding: 10px 20px; border: none; border-radius: 8px; font-size: 0.93em;
cursor: pointer; transition: all 0.2s; text-decoration: none;
}
.btn-primary { background: #667eea; color: #fff; }
.btn-primary:hover { background: #5a6fd6; }
.btn-outline { background: #fff; color: #667eea; border: 1.5px solid #667eea; }
.btn-outline:hover { background: #f0f2ff; }
.btn-danger { background: #e74c3c; color: #fff; padding: 5px 12px; font-size: 0.82em; }
.btn-danger:hover { background: #c0392b; }
.btn-edit { background: #2ecc71; color: #fff; padding: 5px 12px; font-size: 0.82em; margin-right: 6px; }
.btn-edit:hover { background: #27ae60; }
.btn-sm { padding: 6px 14px; font-size: 0.82em; }
.btn-block { display: block; width: 100%; }
/* ========== 工具栏 ========== */
.toolbar {
background: #fff; padding: 14px 20px; display: flex; gap: 10px;
border-top: 1px solid #f0f0f0; align-items: center;
}
.toolbar input {
flex: 1; padding: 9px 12px; border: 1.5px solid #e0e0e0; border-radius: 8px;
font-size: 0.93em;
}
.toolbar input:focus { outline: none; border-color: #667eea; }
/* ========== 表格 ========== */
.table-wrap {
background: #fff; padding: 0 20px 20px; overflow-x: auto;
}
table { width: 100%; border-collapse: collapse; font-size: 0.93em; }
thead th {
text-align: left; padding: 12px 10px; border-bottom: 2px solid #667eea;
color: #667eea; font-weight: 600; white-space: nowrap;
}
tbody td {
padding: 12px 10px; border-bottom: 1px solid #f0f0f0;
}
tbody tr:hover { background: #f8f9ff; }
.loading, .empty { text-align: center; color: #999; padding: 40px 0 !important; }
/* ========== 成绩徽章 ========== */
.badge {
display: inline-block; padding: 3px 10px; border-radius: 12px;
font-size: 0.82em; font-weight: 600; color: #fff;
}
.badge-excellent { background: #2ecc71; }
.badge-good { background: #3498db; }
.badge-normal { background: #f39c12; }
.badge-poor { background: #e74c3c; }
/* ========== 统计卡片 ========== */
.stats {
background: #fff; padding: 16px 20px; border-radius: 0 0 12px 12px;
display: flex; gap: 14px; border-top: 1px solid #f0f0f0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.stat-card {
flex: 1; text-align: center; padding: 14px 10px; border-radius: 8px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.stat-value { display: block; font-size: 1.6em; font-weight: 700; color: #fff; }
.stat-label { display: block; font-size: 0.78em; color: rgba(255,255,255,0.8); margin-top: 4px; }
/* ========== 错误消息 ========== */
.error-msg { color: #e74c3c; font-size: 0.85em; margin-bottom: 10px; min-height: 1.2em; }
.error-bar {
background: #fff3cd; color: #856404; padding: 10px 20px; border-left: 4px solid #ffc107;
font-size: 0.88em;
}
/* ========== 弹窗 ========== */
.modal-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.45); display: flex; justify-content: center;
align-items: center; z-index: 1000; opacity: 0; pointer-events: none;
transition: opacity 0.25s;
}
.modal-overlay.show { opacity: 1; pointer-events: auto; }
.modal {
background: #fff; border-radius: 12px; padding: 28px 30px; width: 90%; max-width: 480px;
box-shadow: 0 16px 48px rgba(0,0,0,0.2);
}
.modal h2 { margin-bottom: 18px; font-size: 1.2em; }
.modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px; }
/* ========== 响应式 ========== */
@media (max-width: 600px) {
body { padding: 10px; }
.login-card { padding: 24px 18px; }
.form-row { flex-direction: column; gap: 0; }
.main-header { flex-direction: column; gap: 10px; }
.toolbar { flex-wrap: wrap; }
.stats { flex-direction: column; }
}

View File

@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>学生管理系统 v2</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<!-- 登录面板 -->
<div class="container" id="login-panel">
<header>
<h1>📚 学生管理系统 v2</h1>
<p class="subtitle">Spring Security + JWT + Redis</p>
</header>
<div class="login-card">
<h2>登录</h2>
<p class="hint">默认账号admin/123456 (管理员) 或 user/123456 (普通用户)</p>
<div class="form-group">
<input type="text" id="login-username" placeholder="用户名" autofocus>
</div>
<div class="form-group">
<input type="password" id="login-password" placeholder="密码">
</div>
<div class="error-msg" id="login-error"></div>
<button class="btn btn-primary btn-block" onclick="doLogin()">登录</button>
<p class="hint small" style="margin-top:12px">
没有账号?<a href="#" onclick="showRegister()">注册</a>
</p>
</div>
</div>
<!-- 注册面板 -->
<div class="container" id="register-panel" style="display:none">
<header><h1>📚 注册</h1></header>
<div class="login-card">
<h2>注册新账号</h2>
<div class="form-group"><input type="text" id="reg-username" placeholder="用户名"></div>
<div class="form-group"><input type="password" id="reg-password" placeholder="密码"></div>
<div class="error-msg" id="reg-error"></div>
<button class="btn btn-primary btn-block" onclick="doRegister()">注册</button>
<p class="hint small" style="margin-top:12px"><a href="#" onclick="showLogin()">返回登录</a></p>
</div>
</div>
<!-- 主面板 -->
<div class="container" id="main-panel" style="display:none">
<header class="main-header">
<div>
<h1>📚 学生管理系统 v2</h1>
<span id="user-info"></span>
</div>
<button class="btn btn-outline btn-sm" onclick="logout()">退出</button>
</header>
<div class="toolbar">
<input type="text" id="keyword" placeholder="搜索..." onkeyup="if(event.key==='Enter')search()">
<button class="btn btn-primary" id="btn-add" onclick="showForm(null)">+ 新增</button>
<button class="btn btn-outline" onclick="loadStudents()">刷新</button>
</div>
<div class="error-bar" id="error-bar" style="display:none"></div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>ID</th><th>姓名</th><th>年龄</th><th>邮箱</th><th>成绩</th><th>操作</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</div>
<div class="stats" id="stats-bar">
<div class="stat-card"><span class="stat-value" id="stat-total">-</span><span class="stat-label">总数</span></div>
<div class="stat-card"><span class="stat-value" id="stat-excellent">-</span><span class="stat-label">优秀(≥85)</span></div>
<div class="stat-card"><span class="stat-value" id="stat-orm">-</span><span class="stat-label">ORM 引擎</span></div>
</div>
</div>
<!-- 弹窗 -->
<div class="modal-overlay" id="modal-overlay">
<div class="modal">
<h2 id="modal-title">新增学生</h2>
<form id="student-form" onsubmit="submitForm(event)">
<input type="hidden" id="form-id">
<div class="form-group"><label>姓名 *</label><input type="text" id="form-name" required maxlength="20"></div>
<div class="form-row">
<div class="form-group"><label>年龄 *</label><input type="number" id="form-age" required min="1" max="150"></div>
<div class="form-group"><label>成绩 *</label><input type="number" id="form-score" required min="0" max="100"></div>
</div>
<div class="form-group"><label>邮箱 *</label><input type="email" id="form-email" required></div>
<div class="modal-actions">
<button type="button" class="btn btn-outline" onclick="closeModal()">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
</div>
<script src="js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,210 @@
const BASE = '/api';
let token = localStorage.getItem('wk5_token') || '';
let role = localStorage.getItem('wk5_role') || '';
document.addEventListener('DOMContentLoaded', () => {
if (token) { showMain(); loadStudents(); checkRoleUI(); }
});
// ==================== 认证 ====================
async function doLogin() {
const username = document.getElementById('login-username').value.trim();
const password = document.getElementById('login-password').value.trim();
if (!username || !password) { showLoginError('请输入用户名和密码'); return; }
try {
const resp = await fetch(BASE + '/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const result = await resp.json();
if (resp.ok && result.code === 200) {
token = result.data.token;
role = result.data.role;
localStorage.setItem('wk5_token', token);
localStorage.setItem('wk5_role', role);
showMain(); loadStudents(); checkRoleUI();
} else {
showLoginError(result.message || '登录失败');
}
} catch (e) { showLoginError('连接失败: ' + e.message); }
}
async function doRegister() {
const username = document.getElementById('reg-username').value.trim();
const password = document.getElementById('reg-password').value.trim();
if (!username || !password) { document.getElementById('reg-error').textContent = '请输入用户名和密码'; return; }
try {
const resp = await fetch(BASE + '/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const result = await resp.json();
if (resp.ok && result.code === 200) {
token = result.data.token;
role = result.data.role;
localStorage.setItem('wk5_token', token);
localStorage.setItem('wk5_role', role);
showMain(); loadStudents(); checkRoleUI();
} else {
document.getElementById('reg-error').textContent = result.message || '注册失败';
}
} catch (e) { document.getElementById('reg-error').textContent = '连接失败'; }
}
function showLoginError(msg) { document.getElementById('login-error').textContent = msg; }
function showLogin() {
document.getElementById('login-panel').style.display = 'block';
document.getElementById('register-panel').style.display = 'none';
document.getElementById('main-panel').style.display = 'none';
}
function showRegister() {
document.getElementById('login-panel').style.display = 'none';
document.getElementById('register-panel').style.display = 'block';
}
function showMain() {
document.getElementById('login-panel').style.display = 'none';
document.getElementById('register-panel').style.display = 'none';
document.getElementById('main-panel').style.display = 'block';
}
function logout() {
localStorage.removeItem('wk5_token'); localStorage.removeItem('wk5_role');
token = ''; role = ''; showLogin();
}
/** 根据角色显示/隐藏操作按钮 */
function checkRoleUI() {
document.getElementById('user-info').textContent = '当前: ' + (role || '-') + ' | ';
document.getElementById('btn-add').style.display = (role === 'ADMIN') ? '' : 'none';
}
// ==================== 请求封装 ====================
async function api(url, options) {
options = options || {};
options.headers = options.headers || {};
options.headers['Authorization'] = 'Bearer ' + token;
const resp = await fetch(url, options);
const result = await resp.json();
if (resp.status === 403) {
showError('权限不足:需要 ADMIN 角色'); return null;
}
if ((resp.status === 401 || resp.status === 403) && result.code !== 200) {
logout(); showError('认证失败,请重新登录'); return null;
}
if (result.extra && result.extra.orm) {
document.getElementById('stat-orm').textContent = result.extra.orm;
}
return result;
}
function showError(msg) {
const bar = document.getElementById('error-bar');
bar.textContent = '⚠ ' + msg; bar.style.display = 'block';
setTimeout(() => bar.style.display = 'none', 4000);
}
// ==================== 数据 ====================
async function loadStudents(keyword) {
const tbody = document.getElementById('table-body');
tbody.innerHTML = '<tr><td colspan="6" class="loading">加载中...</td></tr>';
let url = BASE + '/students';
if (keyword) url += '?keyword=' + encodeURIComponent(keyword);
const result = await api(url);
if (!result) { tbody.innerHTML = '<tr><td colspan="6" class="empty">加载失败</td></tr>'; return; }
const students = result.data;
if (!students || students.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty">暂无数据</td></tr>';
} else {
tbody.innerHTML = students.map(s => {
let cls = 'badge-poor';
if (s.score >= 90) cls = 'badge-excellent';
else if (s.score >= 80) cls = 'badge-good';
else if (s.score >= 60) cls = 'badge-normal';
const actions = role === 'ADMIN'
? `<button class="btn-edit" onclick="showForm(${s.id})">编辑</button>
<button class="btn-danger" onclick="deleteStudent(${s.id})">删除</button>`
: '<span style="color:#ccc">--</span>';
return `<tr>
<td>${s.id}</td><td><strong>${escapeHtml(s.name)}</strong></td>
<td>${s.age}</td><td>${escapeHtml(s.email)}</td>
<td><span class="badge ${cls}">${s.score}</span></td>
<td>${actions}</td></tr>`;
}).join('');
}
document.getElementById('stat-total').textContent = result.extra?.total ?? students.length;
const statResp = await api(BASE + '/students/stats/excellent?min=85');
if (statResp) document.getElementById('stat-excellent').textContent = statResp.data;
}
function search() { loadStudents(document.getElementById('keyword').value.trim() || undefined); }
// ==================== 表单 ====================
async function showForm(id) {
document.getElementById('form-id').value = ''; document.getElementById('student-form').reset();
if (id) {
document.getElementById('modal-title').textContent = '编辑学生';
const result = await api(BASE + '/students/' + id);
if (!result) return;
const s = result.data;
document.getElementById('form-id').value = s.id;
document.getElementById('form-name').value = s.name;
document.getElementById('form-age').value = s.age;
document.getElementById('form-email').value = s.email;
document.getElementById('form-score').value = s.score;
} else {
document.getElementById('modal-title').textContent = '新增学生';
}
document.getElementById('modal-overlay').classList.add('show');
}
async function submitForm(e) {
e.preventDefault();
const id = document.getElementById('form-id').value;
const body = {
name: document.getElementById('form-name').value.trim(),
age: parseInt(document.getElementById('form-age').value),
email: document.getElementById('form-email').value.trim(),
score: parseInt(document.getElementById('form-score').value)
};
let result;
if (id) {
result = await api(BASE + '/students/' + id, {
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
});
} else {
result = await api(BASE + '/students', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body)
});
}
if (result) { closeModal(); loadStudents(); }
}
async function deleteStudent(id) {
if (!confirm('确认删除MP 模式下为逻辑删除)')) return;
const result = await api(BASE + '/students/' + id, { method: 'DELETE' });
if (result) loadStudents();
}
function closeModal() { document.getElementById('modal-overlay').classList.remove('show'); }
document.addEventListener('click', e => { if (e.target.id === 'modal-overlay') closeModal(); });
document.addEventListener('keydown', e => {
if (e.key === 'Enter' && document.getElementById('login-panel').style.display !== 'none') doLogin();
});
function escapeHtml(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; }