feat: multi-tenant workspace isolation, RBAC, sidebar nav, billing, and Android enhancements
- Backend: workspace_id isolation for 14 model tables + safe migration/backfill - Backend: RBAC system with 4 roles and 23 permissions, seeded on startup - Backend: workspace admin endpoints (list/manage all workspaces) - Backend: admin user management API (CRUD, reset password) - Backend: billing API with subscription plans, usage tracking, rate limiting - Backend: fix system_logs.py UNION query and wrong column references - Backend: WebSocket JWT auth and workspace enforcement - Frontend: sidebar navigation replacing top dropdown menu - Frontend: user management page (Users.vue) for admins - Frontend: enhanced Workspaces.vue with admin table view - Frontend: workspace RBAC computed properties in user store - Android: agent marketplace, billing/subscription UI, onboarding wizard - Android: phone login, analytics tracker, crash handler, network diagnostics - Android: splash screen, encrypted token storage, app update enhancements - Docs: multi-tenant RBAC guide with 8 sections and role-permission matrix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,96 +1,125 @@
|
||||
<template>
|
||||
<div class="main-layout">
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="header-content">
|
||||
<div class="header-left">
|
||||
<h1>天工智能体平台</h1>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<NotificationCenter ref="notifyCenterRef" />
|
||||
<el-button
|
||||
text
|
||||
class="theme-toggle-btn"
|
||||
:aria-label="themeStore.isDark ? '切换到亮色模式' : '切换到暗色模式'"
|
||||
@click="themeStore.toggle()"
|
||||
>
|
||||
<el-icon :size="20">
|
||||
<Sunny v-if="themeStore.isDark" />
|
||||
<Moon v-else />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-select
|
||||
v-if="userStore.workspaces.length > 1"
|
||||
:model-value="userStore.currentWorkspaceId"
|
||||
class="workspace-selector"
|
||||
placeholder="选择工作区"
|
||||
@change="handleSwitchWorkspace"
|
||||
>
|
||||
<el-option
|
||||
v-for="ws in userStore.workspaces"
|
||||
:key="ws.id"
|
||||
:label="ws.name"
|
||||
:value="ws.id"
|
||||
>
|
||||
<span style="float: left">{{ ws.name }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px; margin-left: 10px">{{ ws.role === 'admin' ? '管理员' : '成员' }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<span v-if="userStore.user">欢迎,{{ userStore.user.username }}</span>
|
||||
<el-button @click="handleLogout">退出</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<!-- 分组下拉导航菜单 -->
|
||||
<nav class="menu-bar">
|
||||
<el-dropdown
|
||||
v-for="group in menuGroups"
|
||||
:key="group.name"
|
||||
trigger="hover"
|
||||
class="menu-dropdown"
|
||||
popper-class="menu-dropdown-popper"
|
||||
<!-- 顶栏 -->
|
||||
<el-header height="52px">
|
||||
<div class="header-left">
|
||||
<el-button
|
||||
class="collapse-btn"
|
||||
text
|
||||
@click="sidebarCollapsed = !sidebarCollapsed"
|
||||
>
|
||||
<el-button type="default" text size="large" class="menu-group-btn">
|
||||
<el-icon><component :is="group.icon" /></el-icon>
|
||||
<span>{{ group.label }}</span>
|
||||
<el-icon :size="20">
|
||||
<Fold v-if="!sidebarCollapsed" />
|
||||
<Expand v-else />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<span class="brand-name">天工智能体平台</span>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<NotificationCenter ref="notifyCenterRef" />
|
||||
<el-button
|
||||
text
|
||||
class="theme-toggle-btn"
|
||||
:aria-label="themeStore.isDark ? '切换到亮色模式' : '切换到暗色模式'"
|
||||
@click="themeStore.toggle()"
|
||||
>
|
||||
<el-icon :size="18">
|
||||
<Sunny v-if="themeStore.isDark" />
|
||||
<Moon v-else />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-select
|
||||
v-if="userStore.workspaces.length > 1"
|
||||
:model-value="userStore.currentWorkspaceId"
|
||||
class="workspace-selector"
|
||||
placeholder="选择工作区"
|
||||
size="small"
|
||||
@change="handleSwitchWorkspace"
|
||||
>
|
||||
<el-option
|
||||
v-for="ws in userStore.workspaces"
|
||||
:key="ws.id"
|
||||
:label="ws.name"
|
||||
:value="ws.id"
|
||||
>
|
||||
<span style="float: left">{{ ws.name }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 12px; margin-left: 8px">
|
||||
{{ ws.role === 'admin' ? '管理员' : '成员' }}
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-dropdown trigger="click">
|
||||
<span class="user-info">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<span class="username">{{ userStore.user?.username }}</span>
|
||||
<el-icon class="arrow-icon"><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item in group.children"
|
||||
:key="item.path"
|
||||
:class="{ 'is-active': isActiveMenu(item) }"
|
||||
@click="navigateTo(item)"
|
||||
>
|
||||
<div class="dropdown-item-content">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<div class="item-text">
|
||||
<span class="item-label">{{ item.label }}</span>
|
||||
<span class="item-desc">{{ item.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown-item @click="router.push('/profile')">
|
||||
<el-icon><User /></el-icon>个人设置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="handleLogout">
|
||||
<el-icon><SwitchButton /></el-icon>退出登录
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</nav>
|
||||
|
||||
<!-- 面包屑 -->
|
||||
<BreadcrumbNav />
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<div class="page-content">
|
||||
<slot />
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 反馈浮动按钮 -->
|
||||
<FeedbackWidget />
|
||||
</el-main>
|
||||
<el-container class="body-container">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside :width="sidebarCollapsed ? '64px' : '220px'" class="sidebar-aside">
|
||||
<el-menu
|
||||
:collapse="sidebarCollapsed"
|
||||
:default-active="activeMenu"
|
||||
:default-openeds="defaultOpeneds"
|
||||
router
|
||||
class="sidebar-menu"
|
||||
:collapse-transition="false"
|
||||
>
|
||||
<!-- 主控台(独立入口) -->
|
||||
<el-menu-item index="/console">
|
||||
<el-icon><Grid /></el-icon>
|
||||
<span>主控台</span>
|
||||
</el-menu-item>
|
||||
|
||||
<!-- 分组菜单 -->
|
||||
<el-sub-menu
|
||||
v-for="group in menuGroups"
|
||||
:key="group.name"
|
||||
:index="group.name"
|
||||
>
|
||||
<template #title>
|
||||
<el-icon><component :is="group.icon" /></el-icon>
|
||||
<span>{{ group.label }}</span>
|
||||
</template>
|
||||
<el-menu-item
|
||||
v-for="item in group.children"
|
||||
:key="item.index"
|
||||
:index="item.path"
|
||||
>
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
<span>{{ item.label }}</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<el-main>
|
||||
<BreadcrumbNav />
|
||||
<div class="page-content">
|
||||
<slot />
|
||||
</div>
|
||||
<FeedbackWidget />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
@@ -104,7 +133,7 @@ import {
|
||||
Grid, Document, User, ChatLineSquare, Share, UserFilled,
|
||||
Clock, Monitor, List, Connection, Setting, Star, Shop,
|
||||
Tools, Bell, Tickets, OfficeBuilding, Lock, ArrowDown, DataAnalysis,
|
||||
Sunny, Moon, Iphone
|
||||
Sunny, Moon, Iphone, Fold, Expand, SwitchButton
|
||||
} from '@element-plus/icons-vue'
|
||||
import GlobalSearch from '@/components/GlobalSearch.vue'
|
||||
import BreadcrumbNav from '@/components/BreadcrumbNav.vue'
|
||||
@@ -117,7 +146,9 @@ const userStore = useUserStore()
|
||||
const themeStore = useThemeStore()
|
||||
const notifyCenterRef = ref<InstanceType<typeof NotificationCenter> | null>(null)
|
||||
|
||||
// ──────────── 菜单分组定义 ────────────
|
||||
const sidebarCollapsed = ref(false)
|
||||
|
||||
// ──────────── 菜单定义 ────────────
|
||||
interface MenuItem {
|
||||
index: string
|
||||
label: string
|
||||
@@ -142,7 +173,6 @@ const menuGroups = computed<MenuGroup[]>(() => {
|
||||
label: '核心功能',
|
||||
icon: Grid,
|
||||
children: [
|
||||
{ index: 'console', label: '主控台', description: '平台概览仪表盘', path: '/console', icon: Grid },
|
||||
{ index: 'workflows', label: '工作流管理', description: '创建和管理工作流', path: '/', pathPattern: '/workflow', icon: Document },
|
||||
{ index: 'agents', label: 'Agent管理', description: '管理所有智能体', path: '/agents', pathPattern: '/agents', icon: User },
|
||||
],
|
||||
@@ -186,37 +216,62 @@ const menuGroups = computed<MenuGroup[]>(() => {
|
||||
icon: Setting,
|
||||
children: [
|
||||
{ index: 'data-sources', label: '数据源管理', description: '配置数据源', path: '/data-sources', icon: Connection },
|
||||
{ index: 'model-configs', label: '模型配置管理', description: '管理LLM模型', path: '/model-configs', icon: Setting },
|
||||
{ index: 'model-configs', label: '模型配置', description: '管理LLM模型', path: '/model-configs', icon: Setting },
|
||||
{ index: 'workspaces', label: '工作区', description: '管理多工作区', path: '/workspaces', pathPattern: '/workspace', icon: OfficeBuilding },
|
||||
{ index: 'permissions', label: '权限管理', description: '用户权限管理', path: '/permissions', icon: Lock, adminOnly: true },
|
||||
{ index: 'app-versions', label: 'App版本管理', description: '发布App新版本', path: '/app-versions', icon: Iphone, adminOnly: true },
|
||||
{ index: 'users', label: '用户管理', description: '用户列表与权限管理', path: '/users', icon: User, adminOnly: true },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// Filter admin-only items
|
||||
// 过滤管理员专属项
|
||||
return groups.map(g => ({
|
||||
...g,
|
||||
children: g.children.filter(item => !item.adminOnly || userStore.user?.role === 'admin'),
|
||||
})).filter(g => g.children.length > 0)
|
||||
})
|
||||
|
||||
// ──────────── 菜单激活状态 ────────────
|
||||
function isActiveMenu(item: MenuItem): boolean {
|
||||
if (item.pathPattern) {
|
||||
return route.path.startsWith(item.pathPattern)
|
||||
}
|
||||
if (route.path === item.path) return true
|
||||
// 根路径特殊处理
|
||||
if (item.index === 'workflows' && (route.path === '/' || route.path.startsWith('/workflow/'))) return true
|
||||
return false
|
||||
}
|
||||
// ──────────── 活动菜单计算 ────────────
|
||||
const activeMenu = computed(() => {
|
||||
const path = route.path
|
||||
|
||||
// ──────────── 路由导航 ────────────
|
||||
function navigateTo(item: MenuItem) {
|
||||
if (item.path === route.path) return // already there
|
||||
router.push(item.path)
|
||||
}
|
||||
// 遍历所有菜单项找匹配
|
||||
for (const group of menuGroups.value) {
|
||||
for (const item of group.children) {
|
||||
if (item.pathPattern) {
|
||||
if (path.startsWith(item.pathPattern)) return item.path
|
||||
}
|
||||
if (path === item.path) return item.path
|
||||
// 根路径匹配工作流
|
||||
if (item.index === 'workflows' && (path === '/' || path.startsWith('/workflow/'))) {
|
||||
return item.path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主控台
|
||||
if (path === '/console') return '/console'
|
||||
|
||||
// 默认返回当前路径
|
||||
return path
|
||||
})
|
||||
|
||||
// ──────────── 默认展开的菜单组 ────────────
|
||||
const defaultOpeneds = computed(() => {
|
||||
// 找到当前活动菜单所属的 group
|
||||
for (const group of menuGroups.value) {
|
||||
for (const item of group.children) {
|
||||
if (activeMenu.value === item.path) return [group.name]
|
||||
// 路径匹配
|
||||
if (item.pathPattern && route.path.startsWith(item.pathPattern)) return [group.name]
|
||||
if (item.index === 'workflows' && (route.path === '/' || route.path.startsWith('/workflow/'))) {
|
||||
return [group.name]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ['core']
|
||||
})
|
||||
|
||||
// ──────────── 工作区切换 ────────────
|
||||
const handleSwitchWorkspace = async (workspaceId: string) => {
|
||||
@@ -241,21 +296,45 @@ const handleLogout = () => {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
/* ══════════ Header ══════════ */
|
||||
:deep(.el-header) {
|
||||
--el-header-padding: 0;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 0 20px;
|
||||
background: #409eff;
|
||||
color: white;
|
||||
gap: 24px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
|
||||
z-index: 100;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-left h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
html.dark :deep(.el-header) {
|
||||
background: #1d1e1f;
|
||||
border-color: var(--el-border-color-darker);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-left: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
color: var(--el-text-color-regular);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -263,122 +342,103 @@ const handleLogout = () => {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 480px;
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
white-space: nowrap;
|
||||
gap: 10px;
|
||||
padding-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.workspace-selector {
|
||||
width: 180px;
|
||||
width: 170px;
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
color: white;
|
||||
color: var(--el-text-color-regular);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.theme-toggle-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: var(--el-fill-color-light);
|
||||
}
|
||||
|
||||
/* ───── 菜单栏 ───── */
|
||||
.menu-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 4px 0 2px;
|
||||
margin-bottom: 2px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
overflow-x: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-dropdown {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-group-btn {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-regular);
|
||||
padding: 8px 14px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.menu-group-btn:hover {
|
||||
.user-info:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.username {
|
||||
max-width: 80px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
/* ───── 下拉项 ───── */
|
||||
:deep(.menu-dropdown-popper) {
|
||||
min-width: 240px;
|
||||
/* ══════════ Body ══════════ */
|
||||
.body-container {
|
||||
height: calc(100vh - 52px);
|
||||
}
|
||||
|
||||
:deep(.menu-dropdown-popper .el-dropdown-menu__item) {
|
||||
padding: 8px 12px;
|
||||
/* ══════════ Sidebar ══════════ */
|
||||
.sidebar-aside {
|
||||
transition: width 0.2s ease;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.dropdown-item-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-item-content > .el-icon {
|
||||
margin-top: 2px;
|
||||
font-size: 18px;
|
||||
color: var(--el-color-primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item.is-active) {
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item.is-active .item-label) {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* ───── 适配 ───── */
|
||||
:deep(.el-main) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 20px 20px;
|
||||
.sidebar-menu {
|
||||
border-right: none;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar-menu:not(.el-menu--collapse) {
|
||||
width: 219px;
|
||||
}
|
||||
|
||||
/* 子菜单缩进微调 */
|
||||
.sidebar-menu :deep(.el-sub-menu .el-menu-item) {
|
||||
padding-left: 56px !important;
|
||||
}
|
||||
|
||||
.sidebar-menu.el-menu--collapse :deep(.el-sub-menu .el-menu-item) {
|
||||
padding-left: 24px !important;
|
||||
}
|
||||
|
||||
/* 子菜单标题图标间距 */
|
||||
.sidebar-menu :deep(.el-sub-menu__title .el-icon) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* ══════════ Main Content ══════════ */
|
||||
:deep(.el-main) {
|
||||
--el-main-padding: 12px 20px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
background: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.page-content {
|
||||
@@ -388,18 +448,30 @@ const handleLogout = () => {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 搜索框暗色适配 */
|
||||
/* ══════════ Dark mode adjustments ══════════ */
|
||||
html.dark .sidebar-aside {
|
||||
background: #1d1e1f;
|
||||
border-color: var(--el-border-color-darker);
|
||||
}
|
||||
|
||||
html.dark :deep(.el-main) {
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
/* ══════════ Header search adapt ══════════ */
|
||||
.header-center :deep(.search-trigger) {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
background: var(--el-fill-color-light);
|
||||
border-color: var(--el-border-color-light);
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.header-center :deep(.search-trigger):hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
background: var(--el-fill-color);
|
||||
border-color: var(--el-border-color);
|
||||
}
|
||||
|
||||
.header-center :deep(.search-trigger kbd) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
background: var(--el-fill-color);
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -213,6 +213,12 @@ const router = createRouter({
|
||||
name: 'app-versions',
|
||||
component: () => import('@/views/AppVersionManage.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'users',
|
||||
component: () => import('@/views/Users.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -18,6 +18,19 @@ export const useUserStore = defineStore('user', () => {
|
||||
return workspaces.value.find(w => w.id === currentWorkspaceId.value) || null
|
||||
})
|
||||
|
||||
// 当前工作区角色
|
||||
const currentWorkspaceRole = computed(() => {
|
||||
if (!currentWorkspaceId.value) return null
|
||||
const ws = workspaces.value.find(w => w.id === currentWorkspaceId.value)
|
||||
return ws?.role || null
|
||||
})
|
||||
|
||||
// 是否为工作区管理员(平台管理员或工作区admin)
|
||||
const isWorkspaceAdmin = computed(() => {
|
||||
if (user.value?.role === 'admin') return true
|
||||
return currentWorkspaceRole.value === 'admin'
|
||||
})
|
||||
|
||||
// 设置token
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken
|
||||
@@ -136,6 +149,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
workspaces,
|
||||
currentWorkspaceId,
|
||||
currentWorkspace,
|
||||
currentWorkspaceRole,
|
||||
isWorkspaceAdmin,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
|
||||
408
frontend/src/views/Users.vue
Normal file
408
frontend/src/views/Users.vue
Normal file
@@ -0,0 +1,408 @@
|
||||
<template>
|
||||
<MainLayout>
|
||||
<div class="users-page">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<h2>用户管理</h2>
|
||||
<el-button type="primary" @click="openCreateDialog">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建用户
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索和过滤 -->
|
||||
<div class="toolbar">
|
||||
<el-input
|
||||
v-model="search"
|
||||
placeholder="搜索用户名或邮箱"
|
||||
clearable
|
||||
style="width: 260px"
|
||||
@clear="fetchUsers"
|
||||
@keyup.enter="fetchUsers"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select v-model="statusFilter" placeholder="状态" clearable style="width: 140px" @change="fetchUsers">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="正常" value="active" />
|
||||
<el-option label="已禁用" value="disabled" />
|
||||
<el-option label="已删除" value="deleted" />
|
||||
</el-select>
|
||||
<el-select v-model="roleFilter" placeholder="角色" clearable style="width: 140px" @change="fetchUsers">
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="管理员" value="admin" />
|
||||
<el-option label="普通用户" value="user" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="fetchUsers">查询</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户表格 -->
|
||||
<el-table :data="users" stripe v-loading="loading" style="margin-top: 16px">
|
||||
<el-table-column prop="username" label="用户名" width="120" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="180" />
|
||||
<el-table-column prop="role" label="角色" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.role === 'admin' ? 'danger' : 'primary'" size="small">
|
||||
{{ row.role === 'admin' ? '管理员' : '用户' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="phone" label="手机号" width="130">
|
||||
<template #default="{ row }">
|
||||
{{ row.phone || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="statusTagType(row.status)"
|
||||
size="small"
|
||||
>
|
||||
{{ statusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="subscription_tier" label="订阅" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="subTagType(row.subscription_tier)" size="small">
|
||||
{{ subLabel(row.subscription_tier) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="workspace_count" label="工作区" width="80" align="center" />
|
||||
<el-table-column prop="daily_usage_count" label="今日用量" width="90" align="center" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button size="small" type="warning" plain @click="openResetPwdDialog(row)">重置密码</el-button>
|
||||
<el-button
|
||||
v-if="row.status !== 'deleted'"
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<el-pagination
|
||||
v-model:current-page="page"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
style="margin-top: 16px; justify-content: flex-end"
|
||||
@current-change="fetchUsers"
|
||||
@size-change="fetchUsers"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 创建/编辑用户对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editingUser ? '编辑用户' : '创建用户'"
|
||||
width="520px"
|
||||
@closed="resetForm"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="form.username" :disabled="!!editingUser" placeholder="2-50个字符" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="user@example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!editingUser" label="密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" show-password placeholder="至少6位" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-select v-model="form.role" style="width: 100%">
|
||||
<el-option label="普通用户" value="user" />
|
||||
<el-option label="管理员" value="admin" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="form.phone" placeholder="选填" />
|
||||
</el-form-item>
|
||||
<el-form-item label="订阅等级">
|
||||
<el-select v-model="form.subscription_tier" style="width: 100%">
|
||||
<el-option label="免费版" value="free" />
|
||||
<el-option label="专业版" value="pro" />
|
||||
<el-option label="企业版" value="enterprise" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="editingUser" label="状态">
|
||||
<el-select v-model="form.status" style="width: 100%">
|
||||
<el-option label="正常" value="active" />
|
||||
<el-option label="已禁用" value="disabled" />
|
||||
<el-option label="已删除" value="deleted" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
{{ editingUser ? '保存' : '创建' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 重置密码对话框 -->
|
||||
<el-dialog
|
||||
v-model="pwdDialogVisible"
|
||||
title="重置密码"
|
||||
width="400px"
|
||||
@closed="pwdForm.new_password = ''"
|
||||
>
|
||||
<p style="margin-bottom: 12px; color: #666">
|
||||
正在重置用户 <strong>{{ pwdTarget?.username }}</strong> 的密码
|
||||
</p>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="新密码">
|
||||
<el-input v-model="pwdForm.new_password" type="password" show-password placeholder="至少6位" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="pwdDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleResetPwd" :loading="pwdSubmitting">
|
||||
确认重置
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</MainLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Search } from '@element-plus/icons-vue'
|
||||
import MainLayout from '@/components/MainLayout.vue'
|
||||
import api from '@/api'
|
||||
|
||||
const loading = ref(false)
|
||||
const users = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const search = ref('')
|
||||
const statusFilter = ref('')
|
||||
const roleFilter = ref('')
|
||||
|
||||
// ── Dialog state ──
|
||||
const dialogVisible = ref(false)
|
||||
const editingUser = ref<any>(null)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref()
|
||||
const form = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user',
|
||||
phone: '',
|
||||
status: 'active',
|
||||
subscription_tier: 'free',
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '用户名长度 2-50', trigger: 'blur' },
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码至少6位', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// ── Reset password dialog ──
|
||||
const pwdDialogVisible = ref(false)
|
||||
const pwdTarget = ref<any>(null)
|
||||
const pwdSubmitting = ref(false)
|
||||
const pwdForm = reactive({ new_password: '' })
|
||||
|
||||
// ── API calls ──
|
||||
async function fetchUsers() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params: any = { page: page.value, page_size: pageSize.value }
|
||||
if (search.value) params.search = search.value
|
||||
if (statusFilter.value) params.status = statusFilter.value
|
||||
if (roleFilter.value) params.role = roleFilter.value
|
||||
|
||||
const res = await api.get('/api/v1/admin/users', { params })
|
||||
users.value = res.data.items
|
||||
total.value = res.data.total
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '获取用户列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
editingUser.value = null
|
||||
resetForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEditDialog(user: any) {
|
||||
editingUser.value = user
|
||||
form.username = user.username
|
||||
form.email = user.email
|
||||
form.role = user.role
|
||||
form.phone = user.phone || ''
|
||||
form.status = user.status
|
||||
form.subscription_tier = user.subscription_tier
|
||||
form.password = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.username = ''
|
||||
form.email = ''
|
||||
form.password = ''
|
||||
form.role = 'user'
|
||||
form.phone = ''
|
||||
form.status = 'active'
|
||||
form.subscription_tier = 'free'
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
if (editingUser.value) {
|
||||
await api.put(`/api/v1/admin/users/${editingUser.value.id}`, {
|
||||
username: form.username,
|
||||
email: form.email,
|
||||
role: form.role,
|
||||
phone: form.phone || null,
|
||||
status: form.status,
|
||||
subscription_tier: form.subscription_tier,
|
||||
})
|
||||
ElMessage.success('用户信息已更新')
|
||||
} else {
|
||||
await api.post('/api/v1/admin/users', {
|
||||
username: form.username,
|
||||
email: form.email,
|
||||
password: form.password,
|
||||
role: form.role,
|
||||
phone: form.phone || null,
|
||||
status: form.status,
|
||||
})
|
||||
ElMessage.success('用户已创建')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchUsers()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(user: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除用户 "${user.username}" 吗?此操作将软删除用户(标记为 deleted)。`,
|
||||
'确认删除',
|
||||
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' },
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await api.delete(`/api/v1/admin/users/${user.id}`)
|
||||
ElMessage.success('用户已删除')
|
||||
fetchUsers()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
function openResetPwdDialog(user: any) {
|
||||
pwdTarget.value = user
|
||||
pwdForm.new_password = ''
|
||||
pwdDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleResetPwd() {
|
||||
if (!pwdForm.new_password || pwdForm.new_password.length < 6) {
|
||||
ElMessage.warning('密码至少6位')
|
||||
return
|
||||
}
|
||||
pwdSubmitting.value = true
|
||||
try {
|
||||
await api.post(`/api/v1/admin/users/${pwdTarget.value.id}/reset-password`, {
|
||||
new_password: pwdForm.new_password,
|
||||
})
|
||||
ElMessage.success('密码已重置')
|
||||
pwdDialogVisible.value = false
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.detail || '重置失败')
|
||||
} finally {
|
||||
pwdSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
function statusTagType(s: string) {
|
||||
return s === 'active' ? 'success' : s === 'disabled' ? 'warning' : 'info'
|
||||
}
|
||||
function statusLabel(s: string) {
|
||||
return s === 'active' ? '正常' : s === 'disabled' ? '已禁用' : '已删除'
|
||||
}
|
||||
function subTagType(t: string) {
|
||||
return t === 'enterprise' ? 'danger' : t === 'pro' ? 'warning' : 'info'
|
||||
}
|
||||
function subLabel(t: string) {
|
||||
return t === 'enterprise' ? '企业版' : t === 'pro' ? '专业版' : '免费版'
|
||||
}
|
||||
function formatDate(d: string | null) {
|
||||
if (!d) return '-'
|
||||
return new Date(d).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
onMounted(fetchUsers)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.users-page {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.card-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -8,7 +8,60 @@
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 工作区列表 -->
|
||||
<!-- 平台管理员:所有工作区表格 -->
|
||||
<div v-if="isPlatformAdmin" class="admin-section">
|
||||
<h3>所有工作区(平台管理员视图)</h3>
|
||||
<div class="admin-toolbar">
|
||||
<el-input v-model="adminSearch" placeholder="搜索工作区名称" clearable style="width: 240px" @change="loadAdminWorkspaces" />
|
||||
<el-select v-model="adminStatusFilter" placeholder="状态筛选" clearable style="width: 140px; margin-left: 12px" @change="loadAdminWorkspaces">
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="停用" value="disabled" />
|
||||
<el-option label="已删除" value="deleted" />
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table :data="adminWorkspaces" border stripe v-loading="adminLoading" style="margin-top: 12px">
|
||||
<el-table-column prop="name" label="名称" min-width="140" />
|
||||
<el-table-column prop="owner_name" label="所有者" width="120" />
|
||||
<el-table-column prop="member_count" label="成员数" width="80" />
|
||||
<el-table-column prop="max_members" label="上限" width="70" />
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'active' ? 'success' : row.status === 'deleted' ? 'danger' : 'warning'" size="small">
|
||||
{{ row.status === 'active' ? '启用' : row.status === 'deleted' ? '已删除' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_default" label="默认" width="70">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.is_default" type="success" size="small">是</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.created_at ? new Date(row.created_at).toLocaleDateString() : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="goWorkspace(row.id)">进入</el-button>
|
||||
<el-button size="small" type="primary" @click="openAdminManage(row)">管理</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="admin-pagination" style="margin-top: 16px; text-align: right">
|
||||
<el-pagination
|
||||
v-model:current-page="adminPage"
|
||||
:page-size="adminPageSize"
|
||||
:total="adminTotal"
|
||||
layout="total, prev, pager, next"
|
||||
@current-change="loadAdminWorkspaces"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 我的工作区 -->
|
||||
<h3 v-if="isPlatformAdmin" style="margin-top: 32px">我的工作区</h3>
|
||||
<el-row :gutter="20">
|
||||
<el-col v-for="ws in workspaces" :key="ws.id" :span="8">
|
||||
<el-card class="workspace-card" shadow="hover">
|
||||
@@ -55,21 +108,45 @@
|
||||
<el-button type="primary" :loading="creating" @click="handleCreate">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 管理员编辑对话框 -->
|
||||
<el-dialog v-model="showAdminEditDialog" title="管理工作区" width="500px">
|
||||
<el-form v-if="adminEditWs" label-width="100px">
|
||||
<el-form-item label="名称">
|
||||
<el-input v-model="adminEditWs.name" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所有者">{{ adminEditWs.owner_name }}</el-form-item>
|
||||
<el-form-item label="成员数">{{ adminEditWs.member_count }} / {{ adminEditWs.max_members }}</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="adminEditWs.status">
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="停用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAdminEditDialog = false">取消</el-button>
|
||||
<el-button type="danger" @click="handleAdminDelete">删除工作区</el-button>
|
||||
<el-button type="primary" @click="handleAdminSave">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import api from '@/api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { Workspace } from '@/types'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const isPlatformAdmin = computed(() => userStore.user?.role === 'admin')
|
||||
|
||||
const workspaces = ref<Workspace[]>([])
|
||||
const showCreateDialog = ref(false)
|
||||
const creating = ref(false)
|
||||
@@ -79,6 +156,19 @@ const createForm = ref({
|
||||
max_members: 50,
|
||||
})
|
||||
|
||||
// 管理员视图
|
||||
const adminWorkspaces = ref<any[]>([])
|
||||
const adminLoading = ref(false)
|
||||
const adminSearch = ref('')
|
||||
const adminStatusFilter = ref('')
|
||||
const adminPage = ref(1)
|
||||
const adminPageSize = ref(20)
|
||||
const adminTotal = ref(0)
|
||||
|
||||
// 管理员编辑
|
||||
const showAdminEditDialog = ref(false)
|
||||
const adminEditWs = ref<any>(null)
|
||||
|
||||
const loadWorkspaces = async () => {
|
||||
try {
|
||||
const res = await api.get('/api/v1/workspaces')
|
||||
@@ -88,6 +178,22 @@ const loadWorkspaces = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const loadAdminWorkspaces = async () => {
|
||||
adminLoading.value = true
|
||||
try {
|
||||
const params: any = { page: adminPage.value, page_size: adminPageSize.value }
|
||||
if (adminSearch.value) params.search = adminSearch.value
|
||||
if (adminStatusFilter.value) params.status = adminStatusFilter.value
|
||||
const res = await api.get('/api/v1/admin/workspaces', { params })
|
||||
adminWorkspaces.value = res.data.items
|
||||
adminTotal.value = res.data.total
|
||||
} catch (e: any) {
|
||||
ElMessage.error('加载所有工作区失败')
|
||||
} finally {
|
||||
adminLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!createForm.value.name.trim()) {
|
||||
ElMessage.warning('请输入名称')
|
||||
@@ -103,8 +209,9 @@ const handleCreate = async () => {
|
||||
ElMessage.success('工作区已创建')
|
||||
showCreateDialog.value = false
|
||||
createForm.value = { name: '', description: '', max_members: 50 }
|
||||
await userStore.fetchUser() // 刷新用户信息以更新工作区列表
|
||||
await userStore.fetchUser()
|
||||
await loadWorkspaces()
|
||||
if (isPlatformAdmin.value) await loadAdminWorkspaces()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '创建失败')
|
||||
} finally {
|
||||
@@ -125,8 +232,51 @@ const openManage = (id: string) => {
|
||||
router.push(`/workspace?id=${id}`)
|
||||
}
|
||||
|
||||
const openAdminManage = async (ws: any) => {
|
||||
try {
|
||||
const res = await api.get(`/api/v1/admin/workspaces/${ws.id}`)
|
||||
adminEditWs.value = res.data
|
||||
showAdminEditDialog.value = true
|
||||
} catch (e: any) {
|
||||
ElMessage.error('加载工作区详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdminSave = async () => {
|
||||
if (!adminEditWs.value) return
|
||||
try {
|
||||
await api.put(`/api/v1/admin/workspaces/${adminEditWs.value.id}`, {
|
||||
name: adminEditWs.value.name,
|
||||
status: adminEditWs.value.status,
|
||||
})
|
||||
ElMessage.success('已保存')
|
||||
showAdminEditDialog.value = false
|
||||
await loadAdminWorkspaces()
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdminDelete = async () => {
|
||||
if (!adminEditWs.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除工作区 "${adminEditWs.value.name}"?`, '危险操作', { type: 'error', confirmButtonText: '确认删除' })
|
||||
await api.delete(`/api/v1/admin/workspaces/${adminEditWs.value.id}`)
|
||||
ElMessage.success('工作区已删除')
|
||||
showAdminEditDialog.value = false
|
||||
await loadAdminWorkspaces()
|
||||
} catch (e: any) {
|
||||
if (e !== 'cancel') {
|
||||
ElMessage.error(e.response?.data?.detail || '删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadWorkspaces()
|
||||
if (isPlatformAdmin.value) {
|
||||
loadAdminWorkspaces()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -146,6 +296,19 @@ onMounted(() => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.admin-section {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.admin-section h3 {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.admin-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.workspace-card {
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
Reference in New Issue
Block a user