mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
feat(perm): admin permission editor in user management
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
5177b41951
commit
d70664067c
1 file changed
+166
-1
+166
-1
@@ -492,7 +492,8 @@
|
|||||||
<section v-if="session.isAdmin.value" class="settings-section">
|
<section v-if="session.isAdmin.value" class="settings-section">
|
||||||
<h2 class="section-title">用户管理</h2>
|
<h2 class="section-title">用户管理</h2>
|
||||||
<div class="user-list">
|
<div class="user-list">
|
||||||
<div v-for="u in userList" :key="u.id" class="user-item">
|
<div v-for="u in userList" :key="u.id" class="user-row-wrap">
|
||||||
|
<div class="user-item">
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<div class="user-name">
|
<div class="user-name">
|
||||||
{{ u.username }}
|
{{ u.username }}
|
||||||
@@ -504,6 +505,15 @@
|
|||||||
<div class="user-created">创建于 {{ formatDate(u.createdAt) }}</div>
|
<div class="user-created">创建于 {{ formatDate(u.createdAt) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user-actions">
|
<div class="user-actions">
|
||||||
|
<span v-if="u.role === 'admin'" class="perm-admin-label">全部权限(管理员)</span>
|
||||||
|
<button
|
||||||
|
v-else
|
||||||
|
class="btn-sm"
|
||||||
|
:class="{ 'btn-primary': permEditingId === u.id }"
|
||||||
|
@click="onTogglePermEditor(u)"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:shield-key" /> 权限
|
||||||
|
</button>
|
||||||
<button class="btn-sm" @click="openResetPassword(u)">
|
<button class="btn-sm" @click="openResetPassword(u)">
|
||||||
<Icon icon="mdi:lock-reset" /> 重置密码
|
<Icon icon="mdi:lock-reset" /> 重置密码
|
||||||
</button>
|
</button>
|
||||||
@@ -526,6 +536,54 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Inline permission editor (members only) -->
|
||||||
|
<div v-if="permEditingId === u.id" class="perm-editor">
|
||||||
|
<div v-if="permLoading" class="user-empty">加载权限中…</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="perm-group">
|
||||||
|
<div class="perm-group-title">能力</div>
|
||||||
|
<div class="perm-checks">
|
||||||
|
<label v-for="cap in CAPABILITIES" :key="cap.token" class="perm-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="permDraft.capabilities.includes(cap.token)"
|
||||||
|
@change="toggleCapability(cap.token, ($event.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
{{ cap.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="perm-group">
|
||||||
|
<div class="perm-group-title">机器人</div>
|
||||||
|
<label class="perm-check">
|
||||||
|
<input type="checkbox" v-model="permDraft.botsAll" />
|
||||||
|
全部机器人
|
||||||
|
</label>
|
||||||
|
<div v-if="!permDraft.botsAll" class="perm-checks perm-bots">
|
||||||
|
<label v-for="bot in store.bots" :key="bot.id" class="perm-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="permDraft.selectedBotIds.includes(bot.id)"
|
||||||
|
@change="toggleBotSelection(bot.id, ($event.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
{{ bot.name }}
|
||||||
|
</label>
|
||||||
|
<span v-if="store.bots.length === 0" class="user-empty">还没有机器人。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="permError" class="user-error">{{ permError }}</p>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn-sm" @click="permEditingId = null">取消</button>
|
||||||
|
<button class="btn-sm btn-primary" :disabled="permSaving" @click="onSavePermissions(u)">
|
||||||
|
{{ permSaving ? '保存中…' : '保存' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="userList.length === 0 && !userLoadError" class="user-empty">加载中…</div>
|
<div v-if="userList.length === 0 && !userLoadError" class="user-empty">加载中…</div>
|
||||||
<div v-if="userLoadError" class="user-error">{{ userLoadError }}</div>
|
<div v-if="userLoadError" class="user-error">{{ userLoadError }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1133,6 +1191,91 @@ async function onConfirmReset() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Per-user permission editor (members only) ---
|
||||||
|
const CAPABILITIES: { token: string; label: string }[] = [
|
||||||
|
{ token: 'player.control', label: '播放控制' },
|
||||||
|
{ token: 'player.queue', label: '队列管理' },
|
||||||
|
{ token: 'bot.manage', label: '机器人管理' },
|
||||||
|
{ token: 'platform.auth', label: '平台登录凭据' },
|
||||||
|
{ token: 'quality', label: '音质设置' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const permEditingId = ref<string | null>(null);
|
||||||
|
const permLoading = ref(false);
|
||||||
|
const permSaving = ref(false);
|
||||||
|
const permError = ref('');
|
||||||
|
const permDraft = reactive<{ capabilities: string[]; botsAll: boolean; selectedBotIds: string[] }>({
|
||||||
|
capabilities: [],
|
||||||
|
botsAll: true,
|
||||||
|
selectedBotIds: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onTogglePermEditor(u: UserListEntry) {
|
||||||
|
if (permEditingId.value === u.id) {
|
||||||
|
permEditingId.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
permEditingId.value = u.id;
|
||||||
|
permError.value = '';
|
||||||
|
permLoading.value = true;
|
||||||
|
permDraft.capabilities = [];
|
||||||
|
permDraft.botsAll = true;
|
||||||
|
permDraft.selectedBotIds = [];
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/users/${u.id}/permissions`);
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const body = await res.json();
|
||||||
|
permDraft.capabilities = Array.isArray(body.capabilities) ? [...body.capabilities] : [];
|
||||||
|
if (body.bots === 'all') {
|
||||||
|
permDraft.botsAll = true;
|
||||||
|
permDraft.selectedBotIds = [];
|
||||||
|
} else {
|
||||||
|
permDraft.botsAll = false;
|
||||||
|
permDraft.selectedBotIds = Array.isArray(body.bots) ? [...body.bots] : [];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
permError.value = (e as Error).message;
|
||||||
|
} finally {
|
||||||
|
permLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCapability(token: string, checked: boolean) {
|
||||||
|
const has = permDraft.capabilities.includes(token);
|
||||||
|
if (checked && !has) permDraft.capabilities.push(token);
|
||||||
|
else if (!checked && has) permDraft.capabilities = permDraft.capabilities.filter((t) => t !== token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBotSelection(id: string, checked: boolean) {
|
||||||
|
const has = permDraft.selectedBotIds.includes(id);
|
||||||
|
if (checked && !has) permDraft.selectedBotIds.push(id);
|
||||||
|
else if (!checked && has) permDraft.selectedBotIds = permDraft.selectedBotIds.filter((b) => b !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSavePermissions(u: UserListEntry) {
|
||||||
|
permSaving.value = true;
|
||||||
|
permError.value = '';
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/users/${u.id}/permissions`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
capabilities: [...permDraft.capabilities],
|
||||||
|
bots: permDraft.botsAll ? 'all' : [...permDraft.selectedBotIds],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok && res.status !== 204) {
|
||||||
|
const b = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(b.error ?? `HTTP ${res.status}`);
|
||||||
|
}
|
||||||
|
permEditingId.value = null;
|
||||||
|
} catch (e) {
|
||||||
|
permError.value = (e as Error).message;
|
||||||
|
} finally {
|
||||||
|
permSaving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatDate(ms: number): string {
|
function formatDate(ms: number): string {
|
||||||
const d = new Date(ms);
|
const d = new Date(ms);
|
||||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||||
@@ -1905,6 +2048,28 @@ onUnmounted(() => {
|
|||||||
.role-member { background: rgba(150, 150, 150, 0.18); color: var(--text-secondary); }
|
.role-member { background: rgba(150, 150, 150, 0.18); color: var(--text-secondary); }
|
||||||
.user-role-select { flex: 0 0 110px; }
|
.user-role-select { flex: 0 0 110px; }
|
||||||
|
|
||||||
|
.user-row-wrap { display: flex; flex-direction: column; gap: 0; }
|
||||||
|
.perm-admin-label { font-size: 12px; color: var(--text-secondary); align-self: center; }
|
||||||
|
.perm-editor {
|
||||||
|
margin-top: -2px;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.perm-group { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.perm-group-title { font-size: 13px; font-weight: 500; color: var(--text-primary); }
|
||||||
|
.perm-checks { display: flex; flex-wrap: wrap; gap: 8px 16px; }
|
||||||
|
.perm-bots { padding-left: 16px; }
|
||||||
|
.perm-check {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
font-size: 13px; color: var(--text-secondary); cursor: pointer;
|
||||||
|
}
|
||||||
|
.perm-check input { cursor: pointer; }
|
||||||
|
|
||||||
// --- Account section (own password change) ---
|
// --- Account section (own password change) ---
|
||||||
.account-info-card {
|
.account-info-card {
|
||||||
display: flex; flex-direction: column; gap: 8px;
|
display: flex; flex-direction: column; gap: 8px;
|
||||||
|
|||||||
Reference in new issue
Block a user