feat(ui): land viewers on /chat and show real role in switcher

This commit is contained in:
matevip 2026-05-15 10:18:45 +08:00
parent 777e2ecee2
commit ccd86dbb80
2 changed files with 43 additions and 1 deletions

View File

@ -14,6 +14,7 @@
</span> </span>
<template v-if="!collapsed"> <template v-if="!collapsed">
<span class="ws-trigger__name">{{ currentLabel }}</span> <span class="ws-trigger__name">{{ currentLabel }}</span>
<span v-if="roleBadge" class="ws-trigger__role" :title="roleBadge.tooltip">{{ roleBadge.label }}</span>
<svg class="ws-trigger__arrow" :class="{ open }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg> <svg class="ws-trigger__arrow" :class="{ open }" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
</template> </template>
</button> </button>
@ -103,6 +104,25 @@ const workspaces = computed(() => store.workspaces)
const currentWorkspaceId = computed(() => store.currentWorkspaceId) const currentWorkspaceId = computed(() => store.currentWorkspaceId)
const currentLabel = computed(() => store.currentWorkspace?.name || 'Workspace') const currentLabel = computed(() => store.currentWorkspace?.name || 'Workspace')
// Show the REAL memberRole never effective so a global admin viewing a
// workspace they have not joined sees "Global admin (non-member)" instead of
// being silently labelled as owner. See WorkspaceWithRoleVO docs.
const roleBadge = computed(() => {
const ws = store.currentWorkspace
if (!ws) return null
const real = ws.memberRole as string | null | undefined
if (ws.isGlobalAdmin) {
return {
label: t('nav.roleAdmin'),
tooltip: real
? `${t('nav.roleAdmin')} (${real})`
: `${t('nav.roleAdmin')} (non-member)`,
}
}
if (!real) return null
return { label: real, tooltip: real }
})
onMounted(() => { onMounted(() => {
store.fetchWorkspaces() store.fetchWorkspaces()
}) })
@ -178,6 +198,18 @@ function onManage() {
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }
.ws-trigger__role {
flex-shrink: 0;
padding: 1px 6px;
border-radius: 8px;
background: var(--mc-primary-bg, rgba(217, 109, 70, 0.12));
color: var(--mc-primary, #d96d46);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.ws-trigger__arrow { .ws-trigger__arrow {
flex-shrink: 0; flex-shrink: 0;
color: var(--mc-sidebar-text, var(--mc-text-tertiary)); color: var(--mc-sidebar-text, var(--mc-text-tertiary));

View File

@ -61,9 +61,11 @@ import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { authApi } from '@/api/index' import { authApi } from '@/api/index'
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
const router = useRouter() const router = useRouter()
const { t } = useI18n() const { t } = useI18n()
const workspaceStore = useWorkspaceStore()
const loading = ref(false) const loading = ref(false)
const showPassword = ref(false) const showPassword = ref(false)
const errorMsg = ref('') const errorMsg = ref('')
@ -80,7 +82,15 @@ async function handleLogin() {
localStorage.setItem('userId', String(data.id || '1')) localStorage.setItem('userId', String(data.id || '1'))
localStorage.setItem('username', data.username || form.username) localStorage.setItem('username', data.username || form.username)
localStorage.setItem('role', data.role || 'user') localStorage.setItem('role', data.role || 'user')
router.push('/') // Resolve capabilities before deciding the landing route so a viewer
// lands on /chat (their only capability) and member+ on /dashboard.
try {
await workspaceStore.fetchWorkspaces()
} catch {
/* default-deny is fine; router guard will still steer */
}
const target = workspaceStore.can('view:dashboard') ? '/dashboard' : '/chat'
router.push(target)
} catch (e: any) { } catch (e: any) {
errorMsg.value = typeof e === 'string' ? e : t('login.failed') errorMsg.value = typeof e === 'string' ? e : t('login.failed')
} finally { } finally {