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>
<template v-if="!collapsed">
<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>
</template>
</button>
@ -103,6 +104,25 @@ const workspaces = computed(() => store.workspaces)
const currentWorkspaceId = computed(() => store.currentWorkspaceId)
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(() => {
store.fetchWorkspaces()
})
@ -178,6 +198,18 @@ function onManage() {
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 {
flex-shrink: 0;
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 { useI18n } from 'vue-i18n'
import { authApi } from '@/api/index'
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
const router = useRouter()
const { t } = useI18n()
const workspaceStore = useWorkspaceStore()
const loading = ref(false)
const showPassword = ref(false)
const errorMsg = ref('')
@ -80,7 +82,15 @@ async function handleLogin() {
localStorage.setItem('userId', String(data.id || '1'))
localStorage.setItem('username', data.username || form.username)
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) {
errorMsg.value = typeof e === 'string' ? e : t('login.failed')
} finally {