Week 1-8: Spring Boot 学习计划完整项目
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
210
week7/backend/src/main/resources/static/js/app.js
Normal file
210
week7/backend/src/main/resources/static/js/app.js
Normal 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; }
|
||||
Reference in New Issue
Block a user