mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat(web): role-aware UI (badge, selector, toggle button, hide admin-only sections for members)
This commit is contained in:
1 parent
a73f797bcb
commit
780726a4e3
3 files changed
+78
-9
No files matched your search
@@ -91,6 +91,9 @@
|
||||
|
||||
<div v-if="session.currentUser.value" class="nav-user">
|
||||
<span class="nav-user-name">{{ session.currentUser.value.username }}</span>
|
||||
<span class="nav-user-role" :class="`role-${session.currentUser.value.role}`">
|
||||
{{ session.currentUser.value.role === 'admin' ? '管理员' : '成员' }}
|
||||
</span>
|
||||
<button class="nav-user-logout" @click="onLogout" title="退出">
|
||||
<Icon icon="mdi:logout" />
|
||||
</button>
|
||||
@@ -670,4 +673,11 @@ onUnmounted(() => {
|
||||
border-radius: var(--radius-sm);
|
||||
&:hover { background: var(--bg-secondary); color: var(--text-primary); }
|
||||
}
|
||||
|
||||
.nav-user-role {
|
||||
font-size: 11px; padding: 2px 6px; border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.role-admin { background: rgba(99, 145, 226, 0.18); color: #6391e2; }
|
||||
.role-member { background: rgba(150, 150, 150, 0.18); color: var(--text-secondary); }
|
||||
</style>
|
||||
@@ -3,6 +3,7 @@ import { ref, computed, readonly } from "vue";
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
role: 'admin' | 'member';
|
||||
}
|
||||
|
||||
const currentUser = ref<User | null>(null);
|
||||
@@ -75,6 +76,7 @@ export function useSession() {
|
||||
currentUser: readonly(currentUser),
|
||||
needsSetup: readonly(needsSetup),
|
||||
isAuthenticated: computed(() => currentUser.value !== null),
|
||||
isAdmin: computed(() => currentUser.value?.role === 'admin'),
|
||||
ready: readonly(ready),
|
||||
refresh,
|
||||
login,
|
||||
|
||||
@@ -460,13 +460,16 @@
|
||||
</section>
|
||||
|
||||
<!-- User Management -->
|
||||
<section class="settings-section">
|
||||
<section v-if="session.isAdmin.value" class="settings-section">
|
||||
<h2 class="section-title">用户管理</h2>
|
||||
<div class="user-list">
|
||||
<div v-for="u in userList" :key="u.id" class="user-item">
|
||||
<div class="user-info">
|
||||
<div class="user-name">
|
||||
{{ u.username }}
|
||||
<span class="user-role-badge" :class="`role-${u.role}`">
|
||||
{{ u.role === 'admin' ? '管理员' : '成员' }}
|
||||
</span>
|
||||
<span v-if="session.currentUser.value && u.id === session.currentUser.value.id" class="user-self-badge">本人</span>
|
||||
</div>
|
||||
<div class="user-created">创建于 {{ formatDate(u.createdAt) }}</div>
|
||||
@@ -475,10 +478,19 @@
|
||||
<button class="btn-sm" @click="openResetPassword(u)">
|
||||
<Icon icon="mdi:lock-reset" /> 重置密码
|
||||
</button>
|
||||
<button
|
||||
class="btn-sm"
|
||||
:disabled="changingRoleId === u.id || isLastAdmin(u)"
|
||||
:title="isLastAdmin(u) ? '不能降级唯一的管理员' : (u.role === 'admin' ? '降级为成员' : '提升为管理员')"
|
||||
@click="onToggleRole(u)"
|
||||
>
|
||||
<Icon icon="mdi:account-cog" />
|
||||
{{ u.role === 'admin' ? '降为成员' : '提升管理员' }}
|
||||
</button>
|
||||
<button
|
||||
class="btn-sm btn-delete"
|
||||
:disabled="!!(session.currentUser.value && u.id === session.currentUser.value.id)"
|
||||
:title="session.currentUser.value && u.id === session.currentUser.value.id ? '不能删除自己' : ''"
|
||||
:disabled="!!(session.currentUser.value && u.id === session.currentUser.value.id) || isLastAdmin(u)"
|
||||
:title="session.currentUser.value && u.id === session.currentUser.value.id ? '不能删除自己' : (isLastAdmin(u) ? '不能删除唯一的管理员' : '')"
|
||||
@click="onDeleteUser(u)"
|
||||
>
|
||||
<Icon icon="mdi:delete" />
|
||||
@@ -492,6 +504,10 @@
|
||||
<form class="user-add-form" @submit.prevent="onCreateUser">
|
||||
<input v-model="newUser.username" class="input" placeholder="新用户名 (3-32 字符)" required />
|
||||
<input v-model="newUser.password" type="password" class="input" placeholder="密码 (≥8 位)" minlength="8" required />
|
||||
<select v-model="newUser.role" class="input user-role-select">
|
||||
<option value="member">成员</option>
|
||||
<option value="admin">管理员</option>
|
||||
</select>
|
||||
<button class="btn-sm btn-primary" type="submit" :disabled="creatingUser">
|
||||
{{ creatingUser ? '创建中…' : '添加用户' }}
|
||||
</button>
|
||||
@@ -519,7 +535,7 @@
|
||||
</section>
|
||||
|
||||
<!-- Audit Log -->
|
||||
<section class="settings-section">
|
||||
<section v-if="session.isAdmin.value" class="settings-section">
|
||||
<h2 class="section-title">
|
||||
操作审计
|
||||
<button class="audit-refresh-btn" @click="loadAudit" :disabled="auditLoading" title="刷新">
|
||||
@@ -923,16 +939,46 @@ async function updateProfile(botId: string, key: keyof ProfileConfig, value: boo
|
||||
// --- User Management ---
|
||||
const session = useSession();
|
||||
|
||||
interface UserListEntry { id: string; username: string; createdAt: number }
|
||||
interface UserListEntry { id: string; username: string; createdAt: number; role: 'admin' | 'member' }
|
||||
const userList = ref<UserListEntry[]>([]);
|
||||
const userLoadError = ref('');
|
||||
const userMutationError = ref('');
|
||||
const newUser = reactive({ username: '', password: '' });
|
||||
const newUser = reactive({ username: '', password: '', role: 'member' as 'admin' | 'member' });
|
||||
const creatingUser = ref(false);
|
||||
const resetTarget = ref<UserListEntry | null>(null);
|
||||
const resetPassword = ref('');
|
||||
const resetError = ref('');
|
||||
const resettingPw = ref(false);
|
||||
const changingRoleId = ref<string | null>(null);
|
||||
|
||||
function isLastAdmin(u: UserListEntry): boolean {
|
||||
if (u.role !== 'admin') return false;
|
||||
const adminCount = userList.value.filter((x) => x.role === 'admin').length;
|
||||
return adminCount <= 1;
|
||||
}
|
||||
|
||||
async function onToggleRole(u: UserListEntry) {
|
||||
const newRole = u.role === 'admin' ? 'member' : 'admin';
|
||||
if (!confirm(`确认将 ${u.username} 切换为${newRole === 'admin' ? '管理员' : '成员'}?`)) return;
|
||||
userMutationError.value = '';
|
||||
changingRoleId.value = u.id;
|
||||
try {
|
||||
const res = await fetch(`/api/users/${u.id}/role`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ role: newRole }),
|
||||
});
|
||||
if (!res.ok && res.status !== 204) {
|
||||
const b = await res.json().catch(() => ({}));
|
||||
throw new Error(b.error ?? `HTTP ${res.status}`);
|
||||
}
|
||||
await loadUsers();
|
||||
} catch (e) {
|
||||
userMutationError.value = (e as Error).message;
|
||||
} finally {
|
||||
changingRoleId.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
userLoadError.value = '';
|
||||
@@ -953,7 +999,7 @@ async function onCreateUser() {
|
||||
const res = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: newUser.username, password: newUser.password }),
|
||||
body: JSON.stringify({ username: newUser.username, password: newUser.password, role: newUser.role }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const b = await res.json().catch(() => ({}));
|
||||
@@ -961,6 +1007,7 @@ async function onCreateUser() {
|
||||
}
|
||||
newUser.username = '';
|
||||
newUser.password = '';
|
||||
newUser.role = 'member';
|
||||
await loadUsers();
|
||||
} catch (e) {
|
||||
userMutationError.value = (e as Error).message;
|
||||
@@ -1080,8 +1127,10 @@ onMounted(() => {
|
||||
checkAuthStatus();
|
||||
loadQuality();
|
||||
loadIdleTimeout();
|
||||
loadUsers();
|
||||
loadAudit();
|
||||
if (session.isAdmin.value) {
|
||||
loadUsers();
|
||||
loadAudit();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -1776,4 +1825,12 @@ onUnmounted(() => {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-role-badge {
|
||||
font-size: 11px; padding: 2px 6px; border-radius: 4px; margin-left: 6px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.role-admin { background: rgba(99, 145, 226, 0.18); color: #6391e2; }
|
||||
.role-member { background: rgba(150, 150, 150, 0.18); color: var(--text-secondary); }
|
||||
.user-role-select { flex: 0 0 110px; }
|
||||
</style>
|
||||
Reference in new issue
Block a user