diff --git a/README.md b/README.md
index 49fcb7a..7b1ec50 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,10 @@ When the backend is running, open `/index.html` to use the lightweight dashboard
- request a login token
- load project statistics
- query recent projects with keyword, type, level, and status filters
+- query the user directory with keyword, role, and status filters
+- load the active teacher roster
+- view and update the current user's profile details
+- change the current user's password
- visualize status and level breakdowns
- view leader and advisor names directly in project rows
diff --git a/backend/src/main/resources/static/index.html b/backend/src/main/resources/static/index.html
index 9a7ff4f..0e07d11 100644
--- a/backend/src/main/resources/static/index.html
+++ b/backend/src/main/resources/static/index.html
@@ -198,6 +198,7 @@
Use satoken header
Supports quick login
Reads stats and project pages
+ Includes user management
Seed accounts share the bundled demo password:
@@ -290,6 +291,74 @@
Reset filters
+
+
+ People
+ User management
+
+
+ Keyword
+
+
+
+ Role
+
+ All
+ Student
+ Teacher
+ Administrator
+
+
+
+ Status
+
+ All
+ Active
+ Disabled
+
+
+
+
+ Load users
+ Load teachers
+
+
+
+
+ Profile
+ Current account tools
+
+
+ Load profile
+ Save profile
+ Change password
+
+
@@ -343,6 +412,38 @@
+
+
+ Users
+ User directory
+
+
+
+
+ User
+ Role
+ Status
+ Contact
+ Updated
+
+
+
+ No user data loaded yet.
+
+
+
+
+
+
+ Teachers
+ Advisor roster
+
+
+
+ Sign in to load user directory, teacher roster, and editable profile details.
+
+
+
@@ -358,10 +459,22 @@
projectType: document.getElementById('projectType'),
projectLevel: document.getElementById('projectLevel'),
status: document.getElementById('status'),
+ userKeyword: document.getElementById('userKeyword'),
+ userRole: document.getElementById('userRole'),
+ userStatus: document.getElementById('userStatus'),
+ profileRealName: document.getElementById('profileRealName'),
+ profilePhone: document.getElementById('profilePhone'),
+ profileEmail: document.getElementById('profileEmail'),
+ profileAvatar: document.getElementById('profileAvatar'),
+ oldPassword: document.getElementById('oldPassword'),
+ newPassword: document.getElementById('newPassword'),
statusBox: document.getElementById('statusBox'),
projectTable: document.getElementById('projectTable'),
+ userTable: document.getElementById('userTable'),
statusList: document.getElementById('statusList'),
- levelList: document.getElementById('levelList')
+ levelList: document.getElementById('levelList'),
+ teacherList: document.getElementById('teacherList'),
+ sessionSummary: document.getElementById('sessionSummary')
};
const statRefs = {
@@ -382,12 +495,24 @@
student001: { username: 'student001', password: 'admin123' }
};
+ const sessionState = {
+ userId: null,
+ username: '',
+ roleType: null,
+ realName: ''
+ };
+
refs.baseUrl.value = window.location.origin || 'http://localhost:8080';
applyPresetAccount();
document.getElementById('loginBtn').addEventListener('click', login);
document.getElementById('statsBtn').addEventListener('click', loadStats);
document.getElementById('projectsBtn').addEventListener('click', loadProjects);
+ document.getElementById('usersBtn').addEventListener('click', loadUsers);
+ document.getElementById('teachersBtn').addEventListener('click', loadTeachers);
+ document.getElementById('profileBtn').addEventListener('click', loadProfile);
+ document.getElementById('saveProfileBtn').addEventListener('click', saveProfile);
+ document.getElementById('passwordBtn').addEventListener('click', changePassword);
refs.accountPreset.addEventListener('change', applyPresetAccount);
document.getElementById('resetBtn').addEventListener('click', () => {
refs.keyword.value = '';
@@ -412,10 +537,20 @@
if (payload.code !== 200) {
throw new Error(payload.message || 'Login failed.');
}
+ sessionState.userId = payload.data.userId || null;
+ sessionState.username = payload.data.username || '';
+ sessionState.roleType = payload.data.roleType ?? null;
+ sessionState.realName = payload.data.realName || '';
refs.token.value = payload.data.token || '';
+ updateSessionSummary();
setStatus('Login succeeded. Token stored in the satoken field.', 'success');
- await loadStats();
- await loadProjects();
+ await Promise.allSettled([
+ loadStats(),
+ loadProjects(),
+ loadUsers(),
+ loadTeachers(),
+ loadProfile()
+ ]);
} catch (error) {
setStatus(error.message, 'error');
}
@@ -484,13 +619,143 @@
}
}
- async function apiFetch(path) {
- const headers = {};
+ async function loadUsers() {
+ setStatus('Loading user directory...');
+ const params = new URLSearchParams();
+ if (refs.userKeyword.value.trim()) params.set('keyword', refs.userKeyword.value.trim());
+ if (refs.userRole.value) params.set('roleType', refs.userRole.value);
+ if (refs.userStatus.value) params.set('status', refs.userStatus.value);
+ params.set('size', '8');
+
+ try {
+ const payload = await apiFetch('/api/users?' + params.toString());
+ const page = payload.data || {};
+ const records = page.records || [];
+ if (!records.length) {
+ refs.userTable.innerHTML = 'No matching users found. ';
+ } else {
+ refs.userTable.innerHTML = records.map((item) => `
+
+
+ ${escapeHtml(item.realName || item.username || 'Unnamed user')}
+ ${escapeHtml(item.username || '-')}
+
+ ${escapeHtml(item.roleTypeLabel || '-')}
+ ${escapeHtml(item.statusLabel || '-')}
+
+ ${escapeHtml(item.phone || '-')}
+ ${escapeHtml(item.email || '-')}
+
+ ${formatDateTime(item.updateTime || item.createTime)}
+
+ `).join('');
+ }
+ setStatus(`Loaded ${records.length} user record(s).`, 'success');
+ } catch (error) {
+ setStatus(error.message, 'error');
+ }
+ }
+
+ async function loadTeachers() {
+ setStatus('Loading teacher roster...');
+ try {
+ const payload = await apiFetch('/api/users/teachers');
+ const records = payload.data || [];
+ renderTeacherList(records);
+ setStatus(`Loaded ${records.length} teacher record(s).`, 'success');
+ } catch (error) {
+ setStatus(error.message, 'error');
+ }
+ }
+
+ async function loadProfile() {
+ if (!sessionState.userId) {
+ setStatus('Login first so the dashboard knows which profile to load.', 'error');
+ return;
+ }
+
+ setStatus('Loading current profile...');
+ try {
+ const payload = await apiFetch('/api/users/' + sessionState.userId);
+ const profile = payload.data || {};
+ refs.profileRealName.value = profile.realName || '';
+ refs.profilePhone.value = profile.phone || '';
+ refs.profileEmail.value = profile.email || '';
+ refs.profileAvatar.value = profile.avatar || '';
+ sessionState.realName = profile.realName || sessionState.realName;
+ sessionState.roleType = profile.roleType ?? sessionState.roleType;
+ updateSessionSummary();
+ setStatus('Profile loaded successfully.', 'success');
+ } catch (error) {
+ setStatus(error.message, 'error');
+ }
+ }
+
+ async function saveProfile() {
+ if (!sessionState.userId) {
+ setStatus('Login first so the dashboard can update your profile.', 'error');
+ return;
+ }
+
+ setStatus('Saving current profile...');
+ try {
+ const payload = await apiFetch('/api/users/profile', {
+ method: 'PUT',
+ body: JSON.stringify({
+ realName: refs.profileRealName.value.trim(),
+ phone: refs.profilePhone.value.trim(),
+ email: refs.profileEmail.value.trim(),
+ avatar: refs.profileAvatar.value.trim()
+ })
+ });
+ const profile = payload.data || {};
+ sessionState.realName = profile.realName || sessionState.realName;
+ updateSessionSummary();
+ setStatus('Profile saved successfully.', 'success');
+ await loadUsers();
+ await loadTeachers();
+ } catch (error) {
+ setStatus(error.message, 'error');
+ }
+ }
+
+ async function changePassword() {
+ if (!sessionState.userId) {
+ setStatus('Login first so the dashboard can change your password.', 'error');
+ return;
+ }
+ if (!refs.oldPassword.value || !refs.newPassword.value) {
+ setStatus('Enter both the current password and the new password.', 'error');
+ return;
+ }
+
+ setStatus('Changing password...');
+ try {
+ await apiFetch('/api/users/change-password', {
+ method: 'POST',
+ body: JSON.stringify({
+ oldPassword: refs.oldPassword.value,
+ newPassword: refs.newPassword.value
+ })
+ });
+ refs.oldPassword.value = '';
+ refs.newPassword.value = '';
+ setStatus('Password changed successfully.', 'success');
+ } catch (error) {
+ setStatus(error.message, 'error');
+ }
+ }
+
+ async function apiFetch(path, options = {}) {
+ const headers = { ...(options.headers || {}) };
const token = refs.token.value.trim();
if (token) {
headers.satoken = token;
}
- const response = await fetch(buildUrl(path), { headers });
+ if (options.body && !headers['Content-Type']) {
+ headers['Content-Type'] = 'application/json';
+ }
+ const response = await fetch(buildUrl(path), { ...options, headers });
const payload = await response.json();
if (payload.code !== 200) {
throw new Error(payload.message || 'Request failed.');
@@ -511,6 +776,33 @@
`).join('');
}
+ function renderTeacherList(items) {
+ if (!items.length) {
+ refs.teacherList.innerHTML = 'No teachers loaded -
';
+ return;
+ }
+ refs.teacherList.innerHTML = items.map((item) => `
+
+
+ ${escapeHtml(item.realName || item.username || 'Teacher')}
+ ${escapeHtml(item.email || item.phone || '-')}
+
+ ${escapeHtml(item.roleTypeLabel || 'Teacher')}
+
+ `).join('');
+ }
+
+ function updateSessionSummary() {
+ const name = sessionState.realName || sessionState.username || 'Unknown user';
+ const role = roleLabel(sessionState.roleType);
+ refs.sessionSummary.innerHTML = `
+ Current session:
+ ${escapeHtml(name)}
+ ${escapeHtml(sessionState.username || '-')}
+ Role: ${escapeHtml(role)}
+ `;
+ }
+
function buildUrl(path) {
const base = refs.baseUrl.value.trim().replace(/\/$/, '');
return (base || 'http://localhost:8080') + path;
@@ -538,6 +830,23 @@
return parts.length ? parts.join(' ') : '-';
}
+ function formatDateTime(value) {
+ if (!value) {
+ return '-';
+ }
+ const normalized = String(value).replace('T', ' ');
+ return escapeHtml(normalized);
+ }
+
+ function roleLabel(value) {
+ switch (Number(value)) {
+ case 1: return 'Student';
+ case 2: return 'Teacher';
+ case 3: return 'Administrator';
+ default: return 'Unknown';
+ }
+ }
+
function applyPresetAccount() {
const preset = accountPresets[refs.accountPreset.value] || accountPresets.admin;
refs.username.value = preset.username;