feat: add business line permissions and Wayne namespace mapping
This commit is contained in:
@@ -12,6 +12,7 @@ export interface UserInfo {
|
||||
display_name: string
|
||||
email: string
|
||||
business_line: string
|
||||
is_admin: boolean
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
@@ -113,6 +114,7 @@ export const authApi = {
|
||||
display_name: data.display_name || data.username,
|
||||
email: data.email || '',
|
||||
business_line: data.business_line || '',
|
||||
is_admin: data.is_admin === true,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
|
||||
export interface BusinessLine {
|
||||
id: number
|
||||
name: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
permission?: 0 | 1
|
||||
}
|
||||
|
||||
export interface WayneNamespace {
|
||||
id: number
|
||||
name: string
|
||||
kubeNamespace: string
|
||||
}
|
||||
|
||||
export const businessLineApi = {
|
||||
async listMine(): Promise<BusinessLine[]> {
|
||||
const token = getToken()
|
||||
const response = await fetch('/auth/api/v1/business-lines', {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
})
|
||||
const data = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`)
|
||||
}
|
||||
return Array.isArray(data.items) ? data.items : []
|
||||
},
|
||||
|
||||
async listAll(): Promise<BusinessLine[]> {
|
||||
const data = await request('/auth/api/v1/business-lines/all')
|
||||
return Array.isArray(data.items) ? data.items : []
|
||||
},
|
||||
|
||||
async create(name: string): Promise<BusinessLine> {
|
||||
return request('/auth/api/v1/business-lines', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
},
|
||||
|
||||
async update(id: number, name: string): Promise<BusinessLine> {
|
||||
return request(`/auth/api/v1/business-lines/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name }),
|
||||
})
|
||||
},
|
||||
|
||||
async remove(id: number): Promise<void> {
|
||||
await request(`/auth/api/v1/business-lines/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
},
|
||||
|
||||
async grant(payload: {
|
||||
business_line_id: number
|
||||
target_user_id: number
|
||||
target_business_line_id: number
|
||||
permission: 0 | 1
|
||||
}): Promise<void> {
|
||||
await request('/auth/api/v1/business-lines/authorizations', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
|
||||
async listWayneNamespaces(): Promise<WayneNamespace[]> {
|
||||
const data = await request('/auth/api/v1/wayne/namespaces')
|
||||
const items = Array.isArray(data.data) ? data.data : Array.isArray(data.items) ? data.items : []
|
||||
return items.map((item: any) => ({
|
||||
id: Number(item.id),
|
||||
name: item.name || '',
|
||||
kubeNamespace: item.kubeNamespace || item.kube_namespace || '',
|
||||
}))
|
||||
},
|
||||
|
||||
async listMappedWayneNamespaces(businessLineId: number): Promise<WayneNamespace[]> {
|
||||
const data = await request(`/auth/api/v1/business-lines/${businessLineId}/wayne-namespaces`)
|
||||
return Array.isArray(data.items) ? data.items : []
|
||||
},
|
||||
|
||||
async replaceMappedWayneNamespaces(businessLineId: number, namespaces: WayneNamespace[]): Promise<void> {
|
||||
await request(`/auth/api/v1/business-lines/${businessLineId}/wayne-namespaces`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ namespaces }),
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
async function request(path: string, init: RequestInit = {}) {
|
||||
const token = getToken()
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...init.headers,
|
||||
},
|
||||
})
|
||||
const data = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ApiResponse } from '@/types/api'
|
||||
import { getToken, removeToken } from '@/utils/auth'
|
||||
import { redirectToSSO } from '@/utils/sso'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
|
||||
export interface Subsystem {
|
||||
id: number
|
||||
@@ -126,7 +127,7 @@ export const subsystemApi = {
|
||||
|
||||
const token = getToken()
|
||||
const openApp = subsystem.name === 'CloudDM' ? 'clouddm' : 'wayne'
|
||||
const path = subsystem.name === 'CloudDM' ? '/auth/api/v1/clouddm/login' : '/auth/api/v1/wayen/login'
|
||||
const path = subsystem.name === 'CloudDM' ? '/auth/api/v1/clouddm/login' : wayneLoginPath()
|
||||
const response = await fetch(path, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@@ -164,3 +165,11 @@ export const subsystemApi = {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
function wayneLoginPath(): string {
|
||||
const businessLineID = useBusinessLineStore().current?.id
|
||||
if (!businessLineID) {
|
||||
return '/auth/api/v1/wayen/login'
|
||||
}
|
||||
return `/auth/api/v1/wayen/login?business_line_id=${encodeURIComponent(String(businessLineID))}`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getToken } from '@/utils/auth'
|
||||
|
||||
export interface UserOption {
|
||||
uid: number
|
||||
username: string
|
||||
}
|
||||
|
||||
export const userApi = {
|
||||
async list(): Promise<UserOption[]> {
|
||||
const token = getToken()
|
||||
const response = await fetch('/auth/api/v1/users', {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
})
|
||||
const data = await response.json().catch(() => ({}))
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`)
|
||||
}
|
||||
return Array.isArray(data.items) ? data.items : []
|
||||
},
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
class="bl-ic"
|
||||
:style="{ background: currentBL?.iconBg, color: currentBL?.iconColor }"
|
||||
>
|
||||
{{ currentBL?.iconText }}
|
||||
{{ currentBL?.iconText || 'BL' }}
|
||||
</div>
|
||||
<span class="bl-name-trigger">{{ currentBL?.name || '未选择' }}</span>
|
||||
<span class="bl-role" v-if="currentBL">{{ currentBL.role }} · 授权 {{ authCount }} 子系统</span>
|
||||
@@ -32,7 +32,7 @@
|
||||
<div class="bl-sub">{{ bl.ou }} · {{ bl.role }}</div>
|
||||
</div>
|
||||
<span v-if="bl.authorized" class="bl-check">✓</span>
|
||||
<span v-else class="bl-lock">🔒 无权限</span>
|
||||
<span v-else class="bl-lock">无权限</span>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -72,6 +72,9 @@ function onClickOutside(e: MouseEvent) {
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onClickOutside)
|
||||
blStore.loadMine().catch(() => {
|
||||
// 页面其他接口会统一展示登录状态,这里只保持切换器为空态。
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -63,6 +63,12 @@
|
||||
<router-link to="/subsystem/authz" class="nav-item" active-class="active">
|
||||
<span class="ic">▦</span>子系统赋权
|
||||
</router-link>
|
||||
<router-link v-if="isPlatformAdmin" to="/business-line/manage" class="nav-item" active-class="active">
|
||||
<span class="ic">▤</span>业务线管理
|
||||
</router-link>
|
||||
<router-link v-if="isBusinessLineAdmin" to="/business-line/assignment" class="nav-item" active-class="active">
|
||||
<span class="ic">▥</span>业务线分配
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-label">审计</div>
|
||||
@@ -81,9 +87,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
const businessLineStore = useBusinessLineStore()
|
||||
const portalExpanded = ref(true)
|
||||
const isPlatformAdmin = computed(() => authStore.user?.is_admin === true)
|
||||
const isBusinessLineAdmin = computed(() => businessLineStore.isCurrentAdmin)
|
||||
|
||||
const subsystems = [
|
||||
{ name: 'Wayne', label: '多集群发布平台', icon: 'W' },
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
import { authApi } from '@/api/auth'
|
||||
import { redirectToSSO } from '@/utils/sso'
|
||||
|
||||
export function useAuth() {
|
||||
const authStore = useAuthStore()
|
||||
const businessLineStore = useBusinessLineStore()
|
||||
|
||||
const login = async (username: string, password: string) => {
|
||||
const response = await authApi.login({ username, password })
|
||||
const { token, user } = response.data
|
||||
authStore.setAuth(token, user)
|
||||
await businessLineStore.loadMine()
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -19,6 +22,7 @@ export function useAuth() {
|
||||
logoutUrl = response.data.logout_url || ''
|
||||
} finally {
|
||||
authStore.clearAuth()
|
||||
businessLineStore.clear()
|
||||
window.location.assign(logoutUrl || '/login?logged_out=1')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { consumeSSOToken, redirectToSSO } from '@/utils/sso'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
import { authApi } from '@/api/auth'
|
||||
|
||||
const router = createRouter({
|
||||
@@ -46,6 +47,22 @@ const router = createRouter({
|
||||
component: () => import('@/views/subsystem/Authorization.vue'),
|
||||
meta: { title: '子系统赋权' },
|
||||
},
|
||||
{
|
||||
path: 'business-line/permissions',
|
||||
redirect: '/business-line/manage',
|
||||
},
|
||||
{
|
||||
path: 'business-line/manage',
|
||||
name: 'BusinessLineManage',
|
||||
component: () => import('@/views/businessLine/Manage.vue'),
|
||||
meta: { title: '业务线管理' },
|
||||
},
|
||||
{
|
||||
path: 'business-line/assignment',
|
||||
name: 'BusinessLineAssignment',
|
||||
component: () => import('@/views/businessLine/Assignment.vue'),
|
||||
meta: { title: '业务线分配' },
|
||||
},
|
||||
{
|
||||
path: 'audit/login',
|
||||
name: 'LoginAudit',
|
||||
@@ -142,6 +159,7 @@ router.beforeEach(async (to) => {
|
||||
if (token && (!authStore.user || ssoToken)) {
|
||||
try {
|
||||
await authStore.refreshUser()
|
||||
await useBusinessLineStore().loadMine()
|
||||
} catch {
|
||||
authStore.clearAuth()
|
||||
const { data } = await authApi.getConfig()
|
||||
@@ -151,6 +169,8 @@ router.beforeEach(async (to) => {
|
||||
redirectToSSO()
|
||||
return false
|
||||
}
|
||||
} else if (token && !useBusinessLineStore().businessLines.length) {
|
||||
await useBusinessLineStore().loadMine().catch(() => {})
|
||||
}
|
||||
if (to.path === '/login' && token) {
|
||||
return '/'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { businessLineApi, type BusinessLine as ApiBusinessLine } from '@/api/businessLine'
|
||||
|
||||
export interface BusinessLine {
|
||||
id: string
|
||||
id: number
|
||||
name: string
|
||||
ou: string
|
||||
role: string
|
||||
@@ -10,63 +11,19 @@ export interface BusinessLine {
|
||||
iconBg: string
|
||||
iconColor: string
|
||||
authorized: boolean
|
||||
permission?: 0 | 1
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'xinfra-current-bl'
|
||||
|
||||
// TODO: 替换为真实 API 调用
|
||||
const MOCK_BUSINESS_LINES: BusinessLine[] = [
|
||||
{
|
||||
id: 'las',
|
||||
name: 'LAS 业务线',
|
||||
ou: 'ou=las',
|
||||
role: 'SRE',
|
||||
iconText: 'LA',
|
||||
iconBg: '#1A1430',
|
||||
iconColor: '#C9A6FF',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'kodo',
|
||||
name: 'Kodo 业务线',
|
||||
ou: 'ou=kodo',
|
||||
role: '研发',
|
||||
iconText: 'KO',
|
||||
iconBg: '#0E1C2C',
|
||||
iconColor: '#8EC8FF',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'lingxi',
|
||||
name: '灵矽 业务线',
|
||||
ou: 'ou=lingxi',
|
||||
role: '未授权',
|
||||
iconText: 'LX',
|
||||
iconBg: '#241B0A',
|
||||
iconColor: '#FFC97A',
|
||||
authorized: false,
|
||||
},
|
||||
{
|
||||
id: 'ltoken',
|
||||
name: 'LTOKEN 业务线',
|
||||
ou: 'ou=ltoken',
|
||||
role: '研发',
|
||||
iconText: 'LT',
|
||||
iconBg: '#1A2A1A',
|
||||
iconColor: '#A6FFB0',
|
||||
authorized: true,
|
||||
},
|
||||
{
|
||||
id: 'maas',
|
||||
name: 'MAAS 业务线',
|
||||
ou: 'ou=maas',
|
||||
role: '未授权',
|
||||
iconText: 'MA',
|
||||
iconBg: '#2A1A2A',
|
||||
iconColor: '#FFB0E0',
|
||||
authorized: false,
|
||||
},
|
||||
]
|
||||
const COLORS = [
|
||||
['#1A1430', '#C9A6FF'],
|
||||
['#0E1C2C', '#8EC8FF'],
|
||||
['#241B0A', '#FFC97A'],
|
||||
['#1A2A1A', '#A6FFB0'],
|
||||
['#2A1A2A', '#FFB0E0'],
|
||||
] as const
|
||||
|
||||
function loadSavedBL(): BusinessLine | null {
|
||||
try {
|
||||
@@ -78,30 +35,88 @@ function loadSavedBL(): BusinessLine | null {
|
||||
return null
|
||||
}
|
||||
|
||||
export const useBusinessLineStore = defineStore('businessLine', () => {
|
||||
const businessLines = ref<BusinessLine[]>(MOCK_BUSINESS_LINES)
|
||||
const currentBL = ref<BusinessLine | null>(loadSavedBL())
|
||||
|
||||
// 默认选中第一个已授权的业务线
|
||||
if (!currentBL.value) {
|
||||
currentBL.value = businessLines.value.find((bl) => bl.authorized) || null
|
||||
function saveBL(bl: BusinessLine | null) {
|
||||
if (!bl) {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(bl))
|
||||
}
|
||||
|
||||
function initials(name: string) {
|
||||
const compact = name.replace(/\s+/g, '')
|
||||
return compact.slice(0, 2).toUpperCase() || 'BL'
|
||||
}
|
||||
|
||||
function mapBusinessLine(item: ApiBusinessLine, index: number): BusinessLine {
|
||||
const [iconBg, iconColor] = COLORS[index % COLORS.length]
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
ou: `ou=${item.name}`,
|
||||
role: item.permission === 0 ? '管理员' : '普通用户',
|
||||
iconText: initials(item.name),
|
||||
iconBg,
|
||||
iconColor,
|
||||
authorized: true,
|
||||
permission: item.permission,
|
||||
created_at: item.created_at,
|
||||
updated_at: item.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
export const useBusinessLineStore = defineStore('businessLine', () => {
|
||||
const businessLines = ref<BusinessLine[]>([])
|
||||
const currentBL = ref<BusinessLine | null>(loadSavedBL())
|
||||
const loading = ref(false)
|
||||
|
||||
const items = computed(() => businessLines.value)
|
||||
const current = computed(() => currentBL.value)
|
||||
const isCurrentAdmin = computed(() => currentBL.value?.permission === 0)
|
||||
|
||||
const authorizedBLs = computed(() =>
|
||||
businessLines.value.filter((bl) => bl.authorized),
|
||||
)
|
||||
|
||||
function switchBL(id: string) {
|
||||
async function loadMine() {
|
||||
loading.value = true
|
||||
try {
|
||||
const rows = await businessLineApi.listMine()
|
||||
const mapped = rows.map(mapBusinessLine)
|
||||
businessLines.value = mapped
|
||||
|
||||
const savedID = currentBL.value?.id
|
||||
const next = mapped.find((bl) => bl.id === savedID) || mapped[0] || null
|
||||
currentBL.value = next
|
||||
saveBL(next)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function switchBL(id: number) {
|
||||
const bl = businessLines.value.find((b) => b.id === id)
|
||||
if (!bl || !bl.authorized) return
|
||||
currentBL.value = bl
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(bl))
|
||||
saveBL(bl)
|
||||
}
|
||||
|
||||
function clear() {
|
||||
businessLines.value = []
|
||||
currentBL.value = null
|
||||
saveBL(null)
|
||||
}
|
||||
|
||||
return {
|
||||
businessLines,
|
||||
items,
|
||||
currentBL,
|
||||
current,
|
||||
loading,
|
||||
isCurrentAdmin,
|
||||
authorizedBLs,
|
||||
loadMine,
|
||||
switchBL,
|
||||
clear,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>业务线分配</h2>
|
||||
<p>{{ currentName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isCurrentBusinessLineAdmin" class="assignment-panel">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="用户">
|
||||
<el-select v-model="grantForm.target_user_id" filterable placeholder="选择用户">
|
||||
<el-option v-for="user in users" :key="user.uid" :label="user.username" :value="user.uid" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="业务线">
|
||||
<el-select v-model="grantForm.target_business_line_id" filterable placeholder="选择业务线">
|
||||
<el-option v-for="item in businessLineStore.items" :key="item.id" :label="item.name" :value="item.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="权限">
|
||||
<el-segmented v-model="grantForm.permission" :options="permissionOptions" />
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="granting" @click="grantPermission">保存分配</el-button>
|
||||
</el-form>
|
||||
|
||||
<div class="section-divider"></div>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="Wayne namespace">
|
||||
<el-select
|
||||
v-model="selectedWayneNamespaceIds"
|
||||
multiple
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
:loading="loadingWayneNamespaces"
|
||||
placeholder="选择 Wayne namespace"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in wayneNamespaces"
|
||||
:key="item.id"
|
||||
:label="`${item.name} / ${item.kubeNamespace}`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-button type="primary" :loading="savingWayneNamespaces" @click="saveWayneNamespaceMapping">保存 Wayne namespace 映射</el-button>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-else class="empty-state">需要当前业务线管理员权限</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { businessLineApi, type WayneNamespace } from '@/api/businessLine'
|
||||
import { userApi, type UserOption } from '@/api/user'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
|
||||
const businessLineStore = useBusinessLineStore()
|
||||
const isCurrentBusinessLineAdmin = computed(() => businessLineStore.isCurrentAdmin)
|
||||
const currentName = computed(() => businessLineStore.current?.name || '未选择业务线')
|
||||
const users = ref<UserOption[]>([])
|
||||
const wayneNamespaces = ref<WayneNamespace[]>([])
|
||||
const selectedWayneNamespaceIds = ref<number[]>([])
|
||||
const granting = ref(false)
|
||||
const loadingWayneNamespaces = ref(false)
|
||||
const savingWayneNamespaces = ref(false)
|
||||
const grantForm = reactive<{
|
||||
target_user_id: number | null
|
||||
target_business_line_id: number | null
|
||||
permission: 0 | 1
|
||||
}>({
|
||||
target_user_id: null,
|
||||
target_business_line_id: null,
|
||||
permission: 1,
|
||||
})
|
||||
|
||||
const permissionOptions = [
|
||||
{ label: '管理员', value: 0 },
|
||||
{ label: '普通用户', value: 1 },
|
||||
]
|
||||
|
||||
watch(
|
||||
() => businessLineStore.current?.id,
|
||||
(id) => {
|
||||
if (id && !grantForm.target_business_line_id) {
|
||||
grantForm.target_business_line_id = id
|
||||
}
|
||||
if (id && isCurrentBusinessLineAdmin.value) {
|
||||
loadWayneNamespaceMapping(id)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
isCurrentBusinessLineAdmin,
|
||||
async (admin) => {
|
||||
if (admin && !users.value.length) {
|
||||
users.value = await userApi.list()
|
||||
}
|
||||
if (admin) {
|
||||
await loadWayneNamespaces()
|
||||
const businessLineID = businessLineStore.current?.id
|
||||
if (businessLineID) {
|
||||
await loadWayneNamespaceMapping(businessLineID)
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
if (isCurrentBusinessLineAdmin.value && !users.value.length) {
|
||||
users.value = await userApi.list()
|
||||
}
|
||||
if (isCurrentBusinessLineAdmin.value) {
|
||||
await loadWayneNamespaces()
|
||||
const businessLineID = businessLineStore.current?.id
|
||||
if (businessLineID) {
|
||||
await loadWayneNamespaceMapping(businessLineID)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
async function grantPermission() {
|
||||
const businessLineID = businessLineStore.current?.id
|
||||
if (!businessLineID || !grantForm.target_user_id || !grantForm.target_business_line_id) {
|
||||
ElMessage.warning('请选择用户和业务线')
|
||||
return
|
||||
}
|
||||
granting.value = true
|
||||
try {
|
||||
await businessLineApi.grant({
|
||||
business_line_id: businessLineID,
|
||||
target_user_id: grantForm.target_user_id,
|
||||
target_business_line_id: grantForm.target_business_line_id,
|
||||
permission: grantForm.permission,
|
||||
})
|
||||
ElMessage.success('已保存业务线分配')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存业务线分配失败')
|
||||
} finally {
|
||||
granting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWayneNamespaces() {
|
||||
if (wayneNamespaces.value.length) {
|
||||
return
|
||||
}
|
||||
loadingWayneNamespaces.value = true
|
||||
try {
|
||||
wayneNamespaces.value = await businessLineApi.listWayneNamespaces()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '查询 Wayne namespace 失败')
|
||||
} finally {
|
||||
loadingWayneNamespaces.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWayneNamespaceMapping(businessLineID: number) {
|
||||
loadingWayneNamespaces.value = true
|
||||
try {
|
||||
const mapped = await businessLineApi.listMappedWayneNamespaces(businessLineID)
|
||||
selectedWayneNamespaceIds.value = mapped.map((item) => item.id)
|
||||
} catch (error) {
|
||||
selectedWayneNamespaceIds.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '查询 Wayne namespace 映射失败')
|
||||
} finally {
|
||||
loadingWayneNamespaces.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveWayneNamespaceMapping() {
|
||||
const businessLineID = businessLineStore.current?.id
|
||||
if (!businessLineID) {
|
||||
ElMessage.warning('请选择当前业务线')
|
||||
return
|
||||
}
|
||||
const selected = wayneNamespaces.value.filter((item) => selectedWayneNamespaceIds.value.includes(item.id))
|
||||
savingWayneNamespaces.value = true
|
||||
try {
|
||||
await businessLineApi.replaceMappedWayneNamespaces(businessLineID, selected)
|
||||
ElMessage.success('已保存 Wayne namespace 映射')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存 Wayne namespace 映射失败')
|
||||
} finally {
|
||||
savingWayneNamespaces.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.assignment-panel {
|
||||
max-width: 520px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
border-top: 1px solid var(--line);
|
||||
margin: 22px 0 18px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 28px 0;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h2>业务线管理</h2>
|
||||
<p>平台管理员维护全局业务线</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="isPlatformAdmin">
|
||||
<div class="toolbar">
|
||||
<el-input v-model="newName" class="name-input" placeholder="业务线名称" @keyup.enter="createBusinessLine" />
|
||||
<el-button type="primary" :icon="Plus" :loading="savingLine" @click="createBusinessLine">新增</el-button>
|
||||
<el-button :loading="loadingLines" @click="loadAllBusinessLines">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="allBusinessLines" v-loading="loadingLines" class="data-table" empty-text="没有业务线数据">
|
||||
<el-table-column prop="id" label="ID" width="90" />
|
||||
<el-table-column label="名称" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-input v-if="editingId === row.id" v-model="editingName" size="small" @keyup.enter="saveBusinessLine(row.id)" />
|
||||
<span v-else>{{ row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" min-width="210" />
|
||||
<el-table-column label="操作" width="190" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="editingId === row.id">
|
||||
<el-button size="small" type="primary" @click="saveBusinessLine(row.id)">保存</el-button>
|
||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button size="small" :icon="Edit" @click="startEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" :icon="Delete" @click="deleteBusinessLine(row)">删除</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
<div v-else class="empty-state">需要平台管理员权限</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Delete, Edit, Plus } from '@element-plus/icons-vue'
|
||||
import { businessLineApi, type BusinessLine } from '@/api/businessLine'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useBusinessLineStore } from '@/stores/businessLine'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const businessLineStore = useBusinessLineStore()
|
||||
const isPlatformAdmin = computed(() => authStore.user?.is_admin === true)
|
||||
|
||||
const allBusinessLines = ref<BusinessLine[]>([])
|
||||
const loadingLines = ref(false)
|
||||
const savingLine = ref(false)
|
||||
const newName = ref('')
|
||||
const editingId = ref<number | null>(null)
|
||||
const editingName = ref('')
|
||||
|
||||
watch(
|
||||
isPlatformAdmin,
|
||||
async (admin) => {
|
||||
if (admin) {
|
||||
await loadAllBusinessLines()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await authStore.refreshUser()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (isPlatformAdmin.value) {
|
||||
await loadAllBusinessLines()
|
||||
}
|
||||
})
|
||||
|
||||
async function loadAllBusinessLines() {
|
||||
loadingLines.value = true
|
||||
try {
|
||||
allBusinessLines.value = await businessLineApi.listAll()
|
||||
} catch (error) {
|
||||
allBusinessLines.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '查询业务线失败')
|
||||
} finally {
|
||||
loadingLines.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createBusinessLine() {
|
||||
const name = newName.value.trim()
|
||||
if (!name) {
|
||||
ElMessage.warning('请输入业务线名称')
|
||||
return
|
||||
}
|
||||
savingLine.value = true
|
||||
try {
|
||||
await businessLineApi.create(name)
|
||||
newName.value = ''
|
||||
await loadAllBusinessLines()
|
||||
await businessLineStore.loadMine()
|
||||
ElMessage.success('已新增业务线')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '新增业务线失败')
|
||||
} finally {
|
||||
savingLine.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startEdit(row: BusinessLine) {
|
||||
editingId.value = row.id
|
||||
editingName.value = row.name
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editingId.value = null
|
||||
editingName.value = ''
|
||||
}
|
||||
|
||||
async function saveBusinessLine(id: number) {
|
||||
const name = editingName.value.trim()
|
||||
if (!name) {
|
||||
ElMessage.warning('请输入业务线名称')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await businessLineApi.update(id, name)
|
||||
cancelEdit()
|
||||
await loadAllBusinessLines()
|
||||
await businessLineStore.loadMine()
|
||||
ElMessage.success('已更新业务线')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '更新业务线失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteBusinessLine(row: BusinessLine) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认删除业务线 ${row.name}?`, '删除业务线', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
})
|
||||
await businessLineApi.remove(row.id)
|
||||
await loadAllBusinessLines()
|
||||
await businessLineStore.loadMine()
|
||||
ElMessage.success('已删除业务线')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel' && error !== 'close') {
|
||||
ElMessage.error(error instanceof Error ? error.message : '删除业务线失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin: 18px 0 14px;
|
||||
}
|
||||
|
||||
.name-input {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 28px 0;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--text-dim);
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -69,8 +69,6 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { subsystemApi, type Subsystem } from '@/api/subsystem'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import { redirectToSSO } from '@/utils/sso'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user