feat(perm): hide UI a member lacks capability for

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.8 committed 2026-05-30 13:55:31 +08:00
1 parent cf76e0f69a
commit 221f7c8dcf
3 files changed
+36 -10

No files matched your search

+8 -3
View File
@@ -10,8 +10,8 @@
</div> </div>
<div class="nav-right"> <div class="nav-right">
<!-- Bot selector (always shown when at least one bot exists) --> <!-- Bot selector (always shown when at least one controllable bot exists) -->
<div v-if="store.bots.length > 0" class="bot-selector" ref="selectorRef"> <div v-if="controllableBots.length > 0" class="bot-selector" ref="selectorRef">
<button class="bot-selector-btn" @click="dropdownOpen = !dropdownOpen"> <button class="bot-selector-btn" @click="dropdownOpen = !dropdownOpen">
<span class="bot-dot" :class="{ online: activeBot?.connected }" /> <span class="bot-dot" :class="{ online: activeBot?.connected }" />
<span class="bot-selector-name">{{ activeBot?.name ?? '选择机器人' }}</span> <span class="bot-selector-name">{{ activeBot?.name ?? '选择机器人' }}</span>
@@ -22,7 +22,7 @@
<div v-if="dropdownOpen" class="bot-dropdown"> <div v-if="dropdownOpen" class="bot-dropdown">
<div class="bot-dropdown-header">机器人</div> <div class="bot-dropdown-header">机器人</div>
<div <div
v-for="bot in store.bots" v-for="bot in controllableBots"
:key="bot.id" :key="bot.id"
class="bot-card" class="bot-card"
:class="{ active: bot.id === store.activeBotId }" :class="{ active: bot.id === store.activeBotId }"
@@ -131,12 +131,17 @@ import { useSession } from '../composables/useSession.js';
const store = usePlayerStore(); const store = usePlayerStore();
const session = useSession(); const session = useSession();
const { canControlBot } = session;
const navRouter = useRouter(); const navRouter = useRouter();
async function onLogout() { async function onLogout() {
await session.logout(); await session.logout();
navRouter.replace({ name: 'login' }); navRouter.replace({ name: 'login' });
} }
// Belt-and-suspenders: the backend already scopes store.bots to the allowed
// set for members, but filtering here keeps the UI correct if an admin (who
// sees all bots) is constrained, or if the list ever isn't pre-filtered.
const controllableBots = computed(() => store.bots.filter((b) => canControlBot(b.id)));
const activeBot = computed(() => store.activeBot); const activeBot = computed(() => store.activeBot);
const dropdownOpen = ref(false); const dropdownOpen = ref(false);
const selectorRef = ref<HTMLElement | null>(null); const selectorRef = ref<HTMLElement | null>(null);
+21 -1
View File
@@ -3,9 +3,10 @@
<Queue :open="showQueue" @close="showQueue = false" /> <Queue :open="showQueue" @close="showQueue = false" />
<div class="player-bar frosted-glass"> <div class="player-bar frosted-glass">
<!-- Progress bar --> <!-- Progress bar (read-only display; seek interaction gated on player.control) -->
<div <div
class="progress-bar-container" class="progress-bar-container"
:class="{ 'no-seek': !canControl }"
ref="progressBarRef" ref="progressBarRef"
@click="onProgressClick" @click="onProgressClick"
@mousemove="onProgressHover" @mousemove="onProgressHover"
@@ -37,6 +38,8 @@
<div class="player-center"> <div class="player-center">
<span class="time-display time-current">{{ formatTime(currentElapsed) }}</span> <span class="time-display time-current">{{ formatTime(currentElapsed) }}</span>
<!-- Transport controls require player.control -->
<template v-if="canControl">
<button class="control-btn" @click="store.prev()"> <button class="control-btn" @click="store.prev()">
<Icon icon="mdi:skip-previous" /> <Icon icon="mdi:skip-previous" />
</button> </button>
@@ -50,10 +53,13 @@
<Icon :icon="modeIcon" /> <Icon :icon="modeIcon" />
<span class="mode-label">{{ modeLabel }}</span> <span class="mode-label">{{ modeLabel }}</span>
</button> </button>
</template>
<span class="time-display time-total">{{ formatTime(currentSong?.duration ?? 0) }}</span> <span class="time-display time-total">{{ formatTime(currentSong?.duration ?? 0) }}</span>
</div> </div>
<div class="player-right"> <div class="player-right">
<!-- Volume requires player.control -->
<template v-if="canControl">
<Icon icon="mdi:volume-high" class="volume-icon" /> <Icon icon="mdi:volume-high" class="volume-icon" />
<input <input
type="range" type="range"
@@ -63,6 +69,7 @@
@change="onVolumeChange" @change="onVolumeChange"
class="volume-slider" class="volume-slider"
/> />
</template>
<button class="control-btn" :class="{ active: showQueue }" @click="showQueue = !showQueue"> <button class="control-btn" :class="{ active: showQueue }" @click="showQueue = !showQueue">
<Icon icon="mdi:playlist-music" /> <Icon icon="mdi:playlist-music" />
</button> </button>
@@ -79,6 +86,7 @@ import { computed, ref, onMounted, onUnmounted } from 'vue';
import { Icon } from '@iconify/vue'; import { Icon } from '@iconify/vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { usePlayerStore } from '../stores/player.js'; import { usePlayerStore } from '../stores/player.js';
import { useSession } from '../composables/useSession.js';
import CoverArt from './CoverArt.vue'; import CoverArt from './CoverArt.vue';
import Queue from './Queue.vue'; import Queue from './Queue.vue';
@@ -86,6 +94,9 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const showQueue = ref(false); const showQueue = ref(false);
const { can } = useSession();
const canControl = computed(() => can('player.control'));
const store = usePlayerStore(); const store = usePlayerStore();
const activeBot = computed(() => store.activeBot); const activeBot = computed(() => store.activeBot);
const currentSong = computed(() => store.currentSong); const currentSong = computed(() => store.currentSong);
@@ -133,6 +144,7 @@ function updateProgress() {
} }
async function onProgressClick(e: MouseEvent) { async function onProgressClick(e: MouseEvent) {
if (!canControl.value) return; // seek requires player.control
const bar = progressBarRef.value; const bar = progressBarRef.value;
if (!bar) return; if (!bar) return;
const rect = bar.getBoundingClientRect(); const rect = bar.getBoundingClientRect();
@@ -237,6 +249,14 @@ function cycleMode() {
.progress-bar-bg { height: 4px; } .progress-bar-bg { height: 4px; }
.progress-bar-thumb { opacity: 1; transform: scale(1); } .progress-bar-thumb { opacity: 1; transform: scale(1); }
} }
&.no-seek {
cursor: default;
&:hover {
.progress-bar-bg { height: 2px; }
.progress-bar-thumb { opacity: 0; transform: scale(0); }
}
}
} }
.progress-bar-bg { .progress-bar-bg {
+7 -6
View File
@@ -50,8 +50,8 @@
<p v-if="ownPwSuccess" class="user-success">{{ ownPwSuccess }}</p> <p v-if="ownPwSuccess" class="user-success">{{ ownPwSuccess }}</p>
</section> </section>
<!-- Bot Management --> <!-- Bot Management (create/edit/delete/start-stop) requires bot.manage -->
<section class="settings-section"> <section v-if="can('bot.manage')" class="settings-section">
<h2 class="section-title">机器人管理</h2> <h2 class="section-title">机器人管理</h2>
<div class="bot-list"> <div class="bot-list">
<div v-for="bot in store.bots" :key="bot.id" class="bot-item"> <div v-for="bot in store.bots" :key="bot.id" class="bot-item">
@@ -157,8 +157,8 @@
</div> </div>
</section> </section>
<!-- Music Account - QR Code Login --> <!-- Music Account - QR Code Login (platform auth) requires platform.auth -->
<section class="settings-section"> <section v-if="can('platform.auth')" class="settings-section">
<h2 class="section-title">音乐账号</h2> <h2 class="section-title">音乐账号</h2>
<!-- NetEase --> <!-- NetEase -->
@@ -371,8 +371,8 @@
</div> </div>
</section> </section>
<!-- Audio Quality --> <!-- Audio Quality requires quality -->
<section class="settings-section"> <section v-if="can('quality')" class="settings-section">
<h2 class="section-title">音质设置</h2> <h2 class="section-title">音质设置</h2>
<div class="setting-row"> <div class="setting-row">
<div class="setting-label"> <div class="setting-label">
@@ -967,6 +967,7 @@ async function updateProfile(botId: string, key: keyof ProfileConfig, value: boo
// --- User Management --- // --- User Management ---
const session = useSession(); const session = useSession();
const { can } = session;
// --- Own password change (available to all authenticated users) --- // --- Own password change (available to all authenticated users) ---
const ownPw = reactive({ old: '', new: '', confirm: '' }); const ownPw = reactive({ old: '', new: '', confirm: '' });