forked from admin/innovation-platform
feat: expand dashboard user workflows
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
<span class="pill">Use satoken header</span>
|
||||
<span class="pill">Supports quick login</span>
|
||||
<span class="pill">Reads stats and project pages</span>
|
||||
<span class="pill">Includes user management</span>
|
||||
</div>
|
||||
<div class="notice">
|
||||
Seed accounts share the bundled demo password:
|
||||
@@ -290,6 +291,74 @@
|
||||
<button class="btn-soft" type="button" id="resetBtn">Reset filters</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">People</div>
|
||||
<h2>User management</h2>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label for="userKeyword">Keyword</label>
|
||||
<input id="userKeyword" placeholder="Username, phone, email..." />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="userRole">Role</label>
|
||||
<select id="userRole">
|
||||
<option value="">All</option>
|
||||
<option value="1">Student</option>
|
||||
<option value="2">Teacher</option>
|
||||
<option value="3">Administrator</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="userStatus">Status</label>
|
||||
<select id="userStatus">
|
||||
<option value="">All</option>
|
||||
<option value="1">Active</option>
|
||||
<option value="0">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-actions" style="margin-top: 14px;">
|
||||
<button class="btn" type="button" id="usersBtn">Load users</button>
|
||||
<button class="btn-soft" type="button" id="teachersBtn">Load teachers</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Profile</div>
|
||||
<h2>Current account tools</h2>
|
||||
<div class="field-grid">
|
||||
<div class="field">
|
||||
<label for="profileRealName">Real name</label>
|
||||
<input id="profileRealName" placeholder="Display name" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="profilePhone">Phone</label>
|
||||
<input id="profilePhone" placeholder="13900000000" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="profileEmail">Email</label>
|
||||
<input id="profileEmail" placeholder="demo@example.com" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="profileAvatar">Avatar URL</label>
|
||||
<input id="profileAvatar" placeholder="https://..." />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="oldPassword">Current password</label>
|
||||
<input id="oldPassword" type="password" placeholder="Current password" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="newPassword">New password</label>
|
||||
<input id="newPassword" type="password" placeholder="New password" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-actions" style="margin-top: 14px;">
|
||||
<button class="btn" type="button" id="profileBtn">Load profile</button>
|
||||
<button class="btn-soft" type="button" id="saveProfileBtn">Save profile</button>
|
||||
<button class="btn-soft" type="button" id="passwordBtn">Change password</button>
|
||||
</div>
|
||||
</section>
|
||||
</aside>
|
||||
|
||||
<main class="content">
|
||||
@@ -343,6 +412,38 @@
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Users</div>
|
||||
<h2>User directory</h2>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Contact</th>
|
||||
<th>Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTable">
|
||||
<tr><td colspan="5">No user data loaded yet.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="eyebrow">Teachers</div>
|
||||
<h2>Advisor roster</h2>
|
||||
<div class="sub-grid">
|
||||
<div class="list" id="teacherList"></div>
|
||||
<div class="notice" id="sessionSummary">
|
||||
Sign in to load user directory, teacher roster, and editable profile details.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 = '<tr><td colspan="5">No matching users found.</td></tr>';
|
||||
} else {
|
||||
refs.userTable.innerHTML = records.map((item) => `
|
||||
<tr>
|
||||
<td>
|
||||
<strong>${escapeHtml(item.realName || item.username || 'Unnamed user')}</strong><br />
|
||||
<span>${escapeHtml(item.username || '-')}</span>
|
||||
</td>
|
||||
<td>${escapeHtml(item.roleTypeLabel || '-')}</td>
|
||||
<td>${escapeHtml(item.statusLabel || '-')}</td>
|
||||
<td>
|
||||
<span>${escapeHtml(item.phone || '-')}</span><br />
|
||||
<span>${escapeHtml(item.email || '-')}</span>
|
||||
</td>
|
||||
<td>${formatDateTime(item.updateTime || item.createTime)}</td>
|
||||
</tr>
|
||||
`).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 = '<div class="list-item"><span>No teachers loaded</span><strong>-</strong></div>';
|
||||
return;
|
||||
}
|
||||
refs.teacherList.innerHTML = items.map((item) => `
|
||||
<div class="list-item">
|
||||
<span>
|
||||
<strong>${escapeHtml(item.realName || item.username || 'Teacher')}</strong><br />
|
||||
${escapeHtml(item.email || item.phone || '-')}
|
||||
</span>
|
||||
<strong>${escapeHtml(item.roleTypeLabel || 'Teacher')}</strong>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function updateSessionSummary() {
|
||||
const name = sessionState.realName || sessionState.username || 'Unknown user';
|
||||
const role = roleLabel(sessionState.roleType);
|
||||
refs.sessionSummary.innerHTML = `
|
||||
Current session:
|
||||
<br />${escapeHtml(name)}
|
||||
<br />${escapeHtml(sessionState.username || '-')}
|
||||
<br />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('<br />') : '-';
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user