From ccd86dbb80a1dd5581a18a1f7fb1ec55f8293447 Mon Sep 17 00:00:00 2001 From: matevip Date: Fri, 15 May 2026 10:18:45 +0800 Subject: [PATCH] feat(ui): land viewers on /chat and show real role in switcher --- .../workspace/WorkspaceSwitcher.vue | 32 +++++++++++++++++++ mateclaw-ui/src/views/Login.vue | 12 ++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue b/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue index e0bc3c01..abd99581 100644 --- a/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue +++ b/mateclaw-ui/src/components/workspace/WorkspaceSwitcher.vue @@ -14,6 +14,7 @@ @@ -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)); diff --git a/mateclaw-ui/src/views/Login.vue b/mateclaw-ui/src/views/Login.vue index bb147402..9b5e4add 100644 --- a/mateclaw-ui/src/views/Login.vue +++ b/mateclaw-ui/src/views/Login.vue @@ -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 {