feat(admin): implement group-centric relations and system/module/company linkage views
This commit is contained in:
@@ -56,7 +56,6 @@ const userTabs = [
|
||||
]
|
||||
|
||||
const adminTabs = [
|
||||
{ to: '/admin/permissions', label: '權限管理' },
|
||||
{ to: '/admin/systems', label: '系統' },
|
||||
{ to: '/admin/modules', label: '模組' },
|
||||
{ to: '/admin/companies', label: '公司' },
|
||||
|
||||
@@ -3,3 +3,4 @@ import { adminHttp } from './http'
|
||||
export const getCompanies = () => adminHttp.get('/admin/companies')
|
||||
export const createCompany = (data) => adminHttp.post('/admin/companies', data)
|
||||
export const updateCompany = (companyKey, data) => adminHttp.patch(`/admin/companies/${companyKey}`, data)
|
||||
export const getCompanySites = (companyKey) => adminHttp.get(`/admin/companies/${companyKey}/sites`)
|
||||
|
||||
@@ -3,3 +3,5 @@ import { adminHttp } from './http'
|
||||
export const getModules = () => adminHttp.get('/admin/modules')
|
||||
export const createModule = (data) => adminHttp.post('/admin/modules', data)
|
||||
export const updateModule = (moduleKey, data) => adminHttp.patch(`/admin/modules/${moduleKey}`, data)
|
||||
export const getModuleGroups = (moduleKey) => adminHttp.get(`/admin/modules/${moduleKey}/groups`)
|
||||
export const getModuleMembers = (moduleKey) => adminHttp.get(`/admin/modules/${moduleKey}/members`)
|
||||
|
||||
@@ -4,6 +4,9 @@ export const getPermissionGroups = () => adminHttp.get('/admin/permission-groups
|
||||
export const createPermissionGroup = (data) => adminHttp.post('/admin/permission-groups', data)
|
||||
export const updatePermissionGroup = (groupKey, data) => adminHttp.patch(`/admin/permission-groups/${groupKey}`, data)
|
||||
export const getPermissionGroupPermissions = (groupKey) => adminHttp.get(`/admin/permission-groups/${groupKey}/permissions`)
|
||||
export const getPermissionGroupBindings = (groupKey) => adminHttp.get(`/admin/permission-groups/${groupKey}/bindings`)
|
||||
export const updatePermissionGroupBindings = (groupKey, data) =>
|
||||
adminHttp.put(`/admin/permission-groups/${groupKey}/bindings`, data)
|
||||
|
||||
export const addMemberToGroup = (groupKey, authentikSub) =>
|
||||
adminHttp.post(`/admin/permission-groups/${groupKey}/members/${authentikSub}`)
|
||||
|
||||
@@ -3,3 +3,5 @@ import { adminHttp } from './http'
|
||||
export const getSystems = () => adminHttp.get('/admin/systems')
|
||||
export const createSystem = (data) => adminHttp.post('/admin/systems', data)
|
||||
export const updateSystem = (systemKey, data) => adminHttp.patch(`/admin/systems/${systemKey}`, data)
|
||||
export const getSystemGroups = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/groups`)
|
||||
export const getSystemMembers = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/members`)
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
<el-table-column prop="company_key" label="Company Key" width="220" />
|
||||
<el-table-column prop="name" label="名稱" min-width="200" />
|
||||
<el-table-column prop="status" label="狀態" width="120" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<el-table-column label="操作" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
||||
<el-button size="small" @click="openSites(row)">站台</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -47,6 +48,18 @@
|
||||
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showSitesDialog" :title="`公司站台:${selectedCompanyKey}`" width="900px">
|
||||
<el-table :data="companySites" border stripe v-loading="sitesLoading">
|
||||
<template #empty><el-empty description="此公司目前沒有站台" /></template>
|
||||
<el-table-column prop="site_key" label="Site Key" width="220" />
|
||||
<el-table-column prop="name" label="名稱" min-width="220" />
|
||||
<el-table-column prop="status" label="狀態" width="120" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="showSitesDialog = false">關閉</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -54,7 +67,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { getCompanies, createCompany, updateCompany } from '@/api/companies'
|
||||
import { getCompanies, createCompany, updateCompany, getCompanySites } from '@/api/companies'
|
||||
|
||||
const companies = ref([])
|
||||
const loading = ref(false)
|
||||
@@ -73,6 +86,11 @@ const rules = {
|
||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const showSitesDialog = ref(false)
|
||||
const sitesLoading = ref(false)
|
||||
const selectedCompanyKey = ref('')
|
||||
const companySites = ref([])
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
@@ -133,5 +151,19 @@ async function handleEdit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openSites(row) {
|
||||
selectedCompanyKey.value = row.company_key
|
||||
showSitesDialog.value = true
|
||||
sitesLoading.value = true
|
||||
try {
|
||||
const res = await getCompanySites(row.company_key)
|
||||
companySites.value = res.data?.items || []
|
||||
} catch (err) {
|
||||
ElMessage.error('載入公司站台失敗')
|
||||
} finally {
|
||||
sitesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -19,12 +19,14 @@
|
||||
<el-table v-else :data="modules" stripe border class="w-full shadow-sm">
|
||||
<template #empty><el-empty description="目前無模組" /></template>
|
||||
<el-table-column prop="system_key" label="System" width="140" />
|
||||
<el-table-column prop="module_key" label="Module Key" width="180" />
|
||||
<el-table-column prop="module_key" label="Module Key" width="220" />
|
||||
<el-table-column prop="name" label="名稱" min-width="180" />
|
||||
<el-table-column prop="status" label="狀態" width="120" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<el-table-column label="操作" width="260">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
||||
<el-button size="small" @click="openRelations(row, 'groups')">群組</el-button>
|
||||
<el-button size="small" @click="openRelations(row, 'members')">會員</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -69,6 +71,33 @@
|
||||
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showRelationDialog" :title="`模組關聯:${relationModuleKey}`" width="900px">
|
||||
<el-tabs v-model="relationTab">
|
||||
<el-tab-pane label="所屬群組" name="groups">
|
||||
<el-table :data="relationGroups" border stripe v-loading="relationLoading">
|
||||
<template #empty><el-empty description="尚無關聯群組" /></template>
|
||||
<el-table-column prop="group_key" label="Group Key" width="220" />
|
||||
<el-table-column prop="group_name" label="名稱" min-width="220" />
|
||||
<el-table-column prop="status" label="狀態" width="120" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="涉及會員" name="members">
|
||||
<el-table :data="relationMembers" border stripe v-loading="relationLoading">
|
||||
<template #empty><el-empty description="尚無關聯會員" /></template>
|
||||
<el-table-column prop="authentik_sub" label="Authentik Sub" min-width="260" />
|
||||
<el-table-column prop="email" label="Email" min-width="220" />
|
||||
<el-table-column prop="display_name" label="顯示名稱" min-width="160" />
|
||||
<el-table-column label="啟用" width="80">
|
||||
<template #default="{ row }">{{ row.is_active ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #footer>
|
||||
<el-button @click="showRelationDialog = false">關閉</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -76,7 +105,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { getModules, createModule, updateModule } from '@/api/modules'
|
||||
import { getModules, createModule, updateModule, getModuleGroups, getModuleMembers } from '@/api/modules'
|
||||
import { getSystems } from '@/api/systems'
|
||||
|
||||
const modules = ref([])
|
||||
@@ -98,6 +127,13 @@ const rules = {
|
||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const showRelationDialog = ref(false)
|
||||
const relationLoading = ref(false)
|
||||
const relationModuleKey = ref('')
|
||||
const relationTab = ref('groups')
|
||||
const relationGroups = ref([])
|
||||
const relationMembers = ref([])
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
@@ -166,5 +202,24 @@ async function handleEdit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openRelations(row, tab) {
|
||||
relationModuleKey.value = row.module_key
|
||||
relationTab.value = tab
|
||||
showRelationDialog.value = true
|
||||
relationLoading.value = true
|
||||
try {
|
||||
const [groupsRes, membersRes] = await Promise.all([
|
||||
getModuleGroups(row.module_key),
|
||||
getModuleMembers(row.module_key)
|
||||
])
|
||||
relationGroups.value = groupsRes.data?.items || []
|
||||
relationMembers.value = membersRes.data?.items || []
|
||||
} catch (err) {
|
||||
ElMessage.error('載入模組關聯資料失敗')
|
||||
} finally {
|
||||
relationLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -1,105 +1,96 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-6">權限群組管理</h2>
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-6">群組與權限管理</h2>
|
||||
|
||||
<el-tabs v-model="activeTab" type="border-card" class="shadow-sm">
|
||||
<!-- Groups Tab -->
|
||||
<el-tab-pane label="群組管理" name="groups">
|
||||
<div class="mt-4">
|
||||
<el-button type="primary" @click="showCreateGroup = true" :icon="Plus" class="mb-4">
|
||||
新增群組
|
||||
</el-button>
|
||||
<el-card class="mb-6 shadow-sm">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium">群組列表</span>
|
||||
<el-button type="primary" @click="showCreateGroup = true" :icon="Plus">新增群組</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-skeleton v-if="loadingGroups" :rows="4" animated />
|
||||
|
||||
<el-table v-else :data="groups" stripe border class="w-full">
|
||||
<template #empty><el-empty description="目前無群組" /></template>
|
||||
<el-table-column prop="group_key" label="Group Key" width="180" />
|
||||
<el-table-column prop="name" label="群組名稱" min-width="200" />
|
||||
<el-table-column prop="name" label="群組名稱" min-width="220" />
|
||||
<el-table-column prop="status" label="狀態" width="120" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<el-table-column label="操作" width="220">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEditGroup(row)">編輯</el-button>
|
||||
<el-button size="small" class="ml-2" @click="openPermissionsDialog(row)">權限</el-button>
|
||||
<el-button size="small" @click="openBindingDialog(row)">設定關聯</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-card>
|
||||
|
||||
<!-- Permissions Tab -->
|
||||
<el-tab-pane label="群組授權" name="permissions">
|
||||
<div class="mt-4">
|
||||
<el-form :model="groupPermForm" label-width="120px" class="max-w-xl mb-4">
|
||||
<el-form-item label="Group Key">
|
||||
<el-select v-model="groupPermForm.groupKey" placeholder="選擇群組">
|
||||
<el-option v-for="g in groups" :key="g.group_key" :label="`${g.name} (${g.group_key})`" :value="g.group_key" />
|
||||
<el-dialog v-model="showBindingDialog" :title="`群組關聯設定:${bindingGroupKey}`" width="980px">
|
||||
<el-form :model="bindingForm" label-width="130px">
|
||||
<el-form-item label="站台(公司/站台)">
|
||||
<el-select v-model="bindingForm.site_keys" multiple filterable clearable style="width: 100%" placeholder="選擇站台">
|
||||
<el-option v-for="s in siteOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Scope Type">
|
||||
<el-select v-model="groupPermForm.scope_type" placeholder="company or site">
|
||||
<el-option label="Company" value="company" />
|
||||
<el-option label="Site" value="site" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Scope ID">
|
||||
<el-select v-model="groupPermForm.scope_id" placeholder="選擇 Scope ID" filterable style="width: 100%">
|
||||
<el-option
|
||||
v-for="s in scopeOptions"
|
||||
:key="s.value"
|
||||
:label="s.label"
|
||||
:value="s.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="系統">
|
||||
<el-select v-model="groupPermForm.system" placeholder="選擇系統" filterable style="width: 100%">
|
||||
|
||||
<el-form-item label="系統(多選)">
|
||||
<el-select v-model="bindingForm.system_keys" multiple filterable clearable style="width: 100%" placeholder="選擇系統">
|
||||
<el-option v-for="s in systems" :key="s.system_key" :label="`${s.name} (${s.system_key})`" :value="s.system_key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模組(選填)">
|
||||
<el-select v-model="groupPermForm.module" placeholder="系統層(留空) 或選模組" clearable filterable style="width: 100%">
|
||||
|
||||
<el-form-item label="模組(多選)">
|
||||
<el-select v-model="bindingForm.module_keys" multiple filterable clearable style="width: 100%" placeholder="選擇模組(可空,空值代表系統層)">
|
||||
<el-option
|
||||
v-for="m in filteredModuleOptions"
|
||||
:key="m.value"
|
||||
:label="m.label"
|
||||
:value="m.value"
|
||||
:key="m.module_key"
|
||||
:label="`${m.name} (${m.module_key})`"
|
||||
:value="m.module_key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作">
|
||||
<el-select v-model="groupPermForm.action" filterable allow-create default-first-option style="width: 100%">
|
||||
<el-option v-for="a in actionOptions" :key="a" :label="a" :value="a" />
|
||||
|
||||
<el-form-item label="會員(多選)">
|
||||
<el-select v-model="bindingForm.member_subs" multiple filterable clearable style="width: 100%" placeholder="選擇會員">
|
||||
<el-option
|
||||
v-for="m in members"
|
||||
:key="m.authentik_sub"
|
||||
:label="`${m.display_name || m.email || '(no-name)'} (${m.authentik_sub})`"
|
||||
:value="m.authentik_sub"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="grantingGroupPerm"
|
||||
@click="handleGroupGrant"
|
||||
:disabled="!groupPermForm.groupKey || !groupPermForm.scope_type || !groupPermForm.scope_id || !groupPermForm.system || !groupPermForm.action"
|
||||
>
|
||||
Grant 授權
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
class="ml-2"
|
||||
:loading="revokingGroupPerm"
|
||||
@click="handleGroupRevoke"
|
||||
:disabled="!groupPermForm.groupKey || !groupPermForm.scope_type || !groupPermForm.scope_id || !groupPermForm.system || !groupPermForm.action"
|
||||
>
|
||||
Revoke 撤銷
|
||||
</el-button>
|
||||
|
||||
<el-form-item label="操作(多選)">
|
||||
<el-select v-model="bindingForm.actions" multiple style="width: 100%" placeholder="選擇操作">
|
||||
<el-option label="view" value="view" />
|
||||
<el-option label="edit" value="edit" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-alert v-if="groupPermError" :title="groupPermError" type="error" show-icon :closable="false" class="mt-3" />
|
||||
<el-alert v-if="groupPermSuccess" :title="groupPermSuccess" type="success" show-icon :closable="false" class="mt-3" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-alert
|
||||
title="規則:scope 固定為 site;action 只允許 view/edit(可同時選)"
|
||||
type="info"
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<el-table :data="bindingPreview" border stripe v-loading="bindingLoading" max-height="260">
|
||||
<template #empty><el-empty description="目前沒有授權規則" /></template>
|
||||
<el-table-column prop="scope_display" label="公司/站台" min-width="220" />
|
||||
<el-table-column prop="system" label="系統" width="120" />
|
||||
<el-table-column prop="module" label="模組" min-width="180" />
|
||||
<el-table-column prop="action" label="操作" width="100" />
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="showBindingDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="savingBinding" @click="saveBindings">儲存關聯</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- Create Group Dialog -->
|
||||
<el-dialog v-model="showCreateGroup" title="新增群組" @close="resetCreateForm">
|
||||
<el-form :model="createForm" label-width="120px">
|
||||
<el-form-item label="Group Key">
|
||||
@@ -135,28 +126,11 @@
|
||||
<el-button type="primary" :loading="savingGroup" @click="handleEditGroup">儲存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showPermissionsDialog" title="群組權限列表" width="900px">
|
||||
<div class="mb-3 text-sm text-gray-600">
|
||||
Group: <span class="font-medium">{{ selectedGroupKey }}</span>
|
||||
</div>
|
||||
<el-table :data="selectedGroupPermissions" border stripe v-loading="loadingGroupPermissions">
|
||||
<template #empty><el-empty description="此群組目前沒有權限" /></template>
|
||||
<el-table-column prop="scope_type" label="Scope" width="100" />
|
||||
<el-table-column prop="scope_id" label="Scope ID" min-width="140" />
|
||||
<el-table-column prop="system" label="系統" width="120" />
|
||||
<el-table-column prop="module" label="模組" width="180" />
|
||||
<el-table-column prop="action" label="操作" width="120" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="showPermissionsDialog = false">關閉</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import {
|
||||
@@ -164,46 +138,84 @@ import {
|
||||
createPermissionGroup,
|
||||
updatePermissionGroup,
|
||||
getPermissionGroupPermissions,
|
||||
groupGrant,
|
||||
groupRevoke
|
||||
getPermissionGroupBindings,
|
||||
updatePermissionGroupBindings
|
||||
} from '@/api/permission-groups'
|
||||
import { getSystems } from '@/api/systems'
|
||||
import { getModules } from '@/api/modules'
|
||||
import { getCompanies } from '@/api/companies'
|
||||
import { getSites } from '@/api/sites'
|
||||
import { getCompanies } from '@/api/companies'
|
||||
import { getMembers } from '@/api/members'
|
||||
|
||||
const activeTab = ref('groups')
|
||||
const systems = ref([])
|
||||
const modules = ref([])
|
||||
const companies = ref([])
|
||||
const sites = ref([])
|
||||
const actionOptions = ['view', 'edit', 'manage', 'admin']
|
||||
|
||||
const filteredModuleOptions = computed(() => {
|
||||
if (!groupPermForm.system) return []
|
||||
return modules.value
|
||||
.filter(m => m.system_key === groupPermForm.system && !m.module_key.endsWith('.__system__'))
|
||||
.map(m => ({
|
||||
value: m.module_key.startsWith(`${groupPermForm.system}.`)
|
||||
? m.module_key.slice(groupPermForm.system.length + 1)
|
||||
: m.module_key,
|
||||
label: `${m.name} (${m.module_key})`
|
||||
}))
|
||||
})
|
||||
|
||||
const scopeOptions = computed(() => {
|
||||
if (groupPermForm.scope_type === 'company') {
|
||||
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
|
||||
}
|
||||
if (groupPermForm.scope_type === 'site') {
|
||||
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
// Groups
|
||||
const groups = ref([])
|
||||
const loadingGroups = ref(false)
|
||||
const systems = ref([])
|
||||
const modules = ref([])
|
||||
const sites = ref([])
|
||||
const companies = ref([])
|
||||
const members = ref([])
|
||||
|
||||
const showCreateGroup = ref(false)
|
||||
const creatingGroup = ref(false)
|
||||
const createForm = reactive({ group_key: '', name: '' })
|
||||
|
||||
const showEditGroup = ref(false)
|
||||
const savingGroup = ref(false)
|
||||
const editGroupForm = reactive({ group_key: '', name: '', status: 'active' })
|
||||
|
||||
const showBindingDialog = ref(false)
|
||||
const bindingGroupKey = ref('')
|
||||
const bindingLoading = ref(false)
|
||||
const savingBinding = ref(false)
|
||||
const bindingPreview = ref([])
|
||||
const bindingForm = reactive({
|
||||
site_keys: [],
|
||||
system_keys: [],
|
||||
module_keys: [],
|
||||
member_subs: [],
|
||||
actions: ['view']
|
||||
})
|
||||
|
||||
const companyLookup = computed(() => {
|
||||
const map = {}
|
||||
for (const c of companies.value) map[c.company_key] = c.name
|
||||
return map
|
||||
})
|
||||
|
||||
const siteOptions = computed(() => {
|
||||
return sites.value.map(s => {
|
||||
const companyName = companyLookup.value[s.company_key] || s.company_key
|
||||
return {
|
||||
value: s.site_key,
|
||||
label: `${companyName}/${s.name} (${s.site_key})`
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const filteredModuleOptions = computed(() => {
|
||||
if (bindingForm.system_keys.length === 0) return modules.value
|
||||
return modules.value.filter(m => bindingForm.system_keys.includes(m.system_key) && !m.module_key.endsWith('.__system__'))
|
||||
})
|
||||
|
||||
function resetCreateForm() {
|
||||
createForm.group_key = ''
|
||||
createForm.name = ''
|
||||
}
|
||||
|
||||
function resetEditGroupForm() {
|
||||
editGroupForm.group_key = ''
|
||||
editGroupForm.name = ''
|
||||
editGroupForm.status = 'active'
|
||||
}
|
||||
|
||||
function resetBindingForm() {
|
||||
bindingForm.site_keys = []
|
||||
bindingForm.system_keys = []
|
||||
bindingForm.module_keys = []
|
||||
bindingForm.member_subs = []
|
||||
bindingForm.actions = ['view']
|
||||
bindingPreview.value = []
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
loadingGroups.value = true
|
||||
@@ -211,10 +223,6 @@ async function loadGroups() {
|
||||
const res = await getPermissionGroups()
|
||||
groups.value = res.data?.items || []
|
||||
} catch (err) {
|
||||
if (err.response?.status === 422) {
|
||||
ElMessage.error('缺少管理員 API 認證,請檢查前端 .env.development')
|
||||
return
|
||||
}
|
||||
ElMessage.error('載入群組失敗')
|
||||
} finally {
|
||||
loadingGroups.value = false
|
||||
@@ -222,29 +230,18 @@ async function loadGroups() {
|
||||
}
|
||||
|
||||
async function loadCatalogs() {
|
||||
const [systemsRes, modulesRes, companiesRes, sitesRes] = await Promise.all([
|
||||
const [systemsRes, modulesRes, sitesRes, companiesRes, membersRes] = await Promise.all([
|
||||
getSystems(),
|
||||
getModules(),
|
||||
getSites(),
|
||||
getCompanies(),
|
||||
getSites()
|
||||
getMembers()
|
||||
])
|
||||
systems.value = systemsRes.data?.items || []
|
||||
modules.value = modulesRes.data?.items || []
|
||||
companies.value = companiesRes.data?.items || []
|
||||
sites.value = sitesRes.data?.items || []
|
||||
}
|
||||
|
||||
// Create Group
|
||||
const showCreateGroup = ref(false)
|
||||
const creatingGroup = ref(false)
|
||||
const createForm = reactive({ group_key: '', name: '' })
|
||||
const showEditGroup = ref(false)
|
||||
const savingGroup = ref(false)
|
||||
const editGroupForm = reactive({ group_key: '', name: '', status: 'active' })
|
||||
|
||||
function resetCreateForm() {
|
||||
createForm.group_key = ''
|
||||
createForm.name = ''
|
||||
companies.value = companiesRes.data?.items || []
|
||||
members.value = membersRes.data?.items || []
|
||||
}
|
||||
|
||||
async function handleCreateGroup() {
|
||||
@@ -273,12 +270,6 @@ function openEditGroup(row) {
|
||||
showEditGroup.value = true
|
||||
}
|
||||
|
||||
function resetEditGroupForm() {
|
||||
editGroupForm.group_key = ''
|
||||
editGroupForm.name = ''
|
||||
editGroupForm.status = 'active'
|
||||
}
|
||||
|
||||
async function handleEditGroup() {
|
||||
savingGroup.value = true
|
||||
try {
|
||||
@@ -296,76 +287,73 @@ async function handleEditGroup() {
|
||||
}
|
||||
}
|
||||
|
||||
const showPermissionsDialog = ref(false)
|
||||
const loadingGroupPermissions = ref(false)
|
||||
const selectedGroupPermissions = ref([])
|
||||
const selectedGroupKey = ref('')
|
||||
|
||||
async function openPermissionsDialog(row) {
|
||||
selectedGroupKey.value = row.group_key
|
||||
showPermissionsDialog.value = true
|
||||
loadingGroupPermissions.value = true
|
||||
async function openBindingDialog(row) {
|
||||
bindingGroupKey.value = row.group_key
|
||||
showBindingDialog.value = true
|
||||
bindingLoading.value = true
|
||||
resetBindingForm()
|
||||
try {
|
||||
const res = await getPermissionGroupPermissions(row.group_key)
|
||||
selectedGroupPermissions.value = res.data?.items || []
|
||||
const [bindingsRes, previewRes] = await Promise.all([
|
||||
getPermissionGroupBindings(row.group_key),
|
||||
getPermissionGroupPermissions(row.group_key)
|
||||
])
|
||||
const data = bindingsRes.data || {}
|
||||
bindingForm.site_keys = data.site_keys || []
|
||||
bindingForm.system_keys = data.system_keys || []
|
||||
bindingForm.module_keys = data.module_keys || []
|
||||
bindingForm.member_subs = data.member_subs || []
|
||||
bindingForm.actions = (data.actions && data.actions.length > 0) ? data.actions : ['view']
|
||||
bindingPreview.value = toPreview(previewRes.data?.items || [])
|
||||
} catch (err) {
|
||||
ElMessage.error('載入群組權限失敗')
|
||||
ElMessage.error('載入群組關聯失敗')
|
||||
} finally {
|
||||
loadingGroupPermissions.value = false
|
||||
bindingLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Group Grant/Revoke
|
||||
const groupPermForm = reactive({
|
||||
groupKey: '',
|
||||
scope_type: '',
|
||||
scope_id: '',
|
||||
system: '',
|
||||
module: '',
|
||||
action: ''
|
||||
})
|
||||
const grantingGroupPerm = ref(false)
|
||||
const revokingGroupPerm = ref(false)
|
||||
const groupPermError = ref('')
|
||||
const groupPermSuccess = ref('')
|
||||
function toPreview(items) {
|
||||
const siteLabelMap = {}
|
||||
for (const s of siteOptions.value) siteLabelMap[s.value] = s.label
|
||||
return items.map(i => ({
|
||||
...i,
|
||||
module: i.module || '(系統層)',
|
||||
scope_display: siteLabelMap[i.scope_id] || i.scope_id
|
||||
}))
|
||||
}
|
||||
|
||||
async function handleGroupGrant() {
|
||||
groupPermError.value = ''
|
||||
groupPermSuccess.value = ''
|
||||
grantingGroupPerm.value = true
|
||||
async function saveBindings() {
|
||||
if (!bindingGroupKey.value) return
|
||||
if (bindingForm.site_keys.length === 0) {
|
||||
ElMessage.warning('至少需要選擇 1 個站台')
|
||||
return
|
||||
}
|
||||
if (bindingForm.system_keys.length === 0 && bindingForm.module_keys.length === 0) {
|
||||
ElMessage.warning('至少需要選擇 1 個系統或模組')
|
||||
return
|
||||
}
|
||||
if (bindingForm.actions.length === 0) {
|
||||
ElMessage.warning('至少需要選擇 1 個操作')
|
||||
return
|
||||
}
|
||||
|
||||
savingBinding.value = true
|
||||
try {
|
||||
const { groupKey, ...permData } = groupPermForm
|
||||
await groupGrant(groupKey, permData)
|
||||
groupPermSuccess.value = 'Grant 成功'
|
||||
} catch (err) {
|
||||
groupPermError.value = 'Grant 失敗'
|
||||
} finally {
|
||||
grantingGroupPerm.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleGroupRevoke() {
|
||||
groupPermError.value = ''
|
||||
groupPermSuccess.value = ''
|
||||
revokingGroupPerm.value = true
|
||||
try {
|
||||
const { groupKey, ...permData } = groupPermForm
|
||||
await groupRevoke(groupKey, permData)
|
||||
groupPermSuccess.value = 'Revoke 成功'
|
||||
} catch (err) {
|
||||
groupPermError.value = 'Revoke 失敗'
|
||||
} finally {
|
||||
revokingGroupPerm.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => groupPermForm.scope_type, () => {
|
||||
groupPermForm.scope_id = ''
|
||||
})
|
||||
|
||||
watch(() => groupPermForm.system, () => {
|
||||
groupPermForm.module = ''
|
||||
await updatePermissionGroupBindings(bindingGroupKey.value, {
|
||||
site_keys: bindingForm.site_keys,
|
||||
system_keys: bindingForm.system_keys,
|
||||
module_keys: bindingForm.module_keys,
|
||||
member_subs: bindingForm.member_subs,
|
||||
actions: bindingForm.actions
|
||||
})
|
||||
const previewRes = await getPermissionGroupPermissions(bindingGroupKey.value)
|
||||
bindingPreview.value = toPreview(previewRes.data?.items || [])
|
||||
ElMessage.success('群組關聯已更新')
|
||||
} catch (err) {
|
||||
ElMessage.error(err.response?.data?.detail || '更新失敗')
|
||||
} finally {
|
||||
savingBinding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadGroups(), loadCatalogs()])
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
<el-table-column prop="system_key" label="System Key" width="200" />
|
||||
<el-table-column prop="name" label="名稱" min-width="180" />
|
||||
<el-table-column prop="status" label="狀態" width="120" />
|
||||
<el-table-column label="操作" width="120">
|
||||
<el-table-column label="操作" width="260">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
||||
<el-button size="small" @click="openRelations(row, 'groups')">群組</el-button>
|
||||
<el-button size="small" @click="openRelations(row, 'members')">會員</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -63,6 +65,33 @@
|
||||
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showRelationDialog" :title="`系統關聯:${relationSystemKey}`" width="900px">
|
||||
<el-tabs v-model="relationTab">
|
||||
<el-tab-pane label="所屬群組" name="groups">
|
||||
<el-table :data="relationGroups" border stripe v-loading="relationLoading">
|
||||
<template #empty><el-empty description="尚無關聯群組" /></template>
|
||||
<el-table-column prop="group_key" label="Group Key" width="220" />
|
||||
<el-table-column prop="group_name" label="名稱" min-width="220" />
|
||||
<el-table-column prop="status" label="狀態" width="120" />
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="涉及會員" name="members">
|
||||
<el-table :data="relationMembers" border stripe v-loading="relationLoading">
|
||||
<template #empty><el-empty description="尚無關聯會員" /></template>
|
||||
<el-table-column prop="authentik_sub" label="Authentik Sub" min-width="260" />
|
||||
<el-table-column prop="email" label="Email" min-width="220" />
|
||||
<el-table-column prop="display_name" label="顯示名稱" min-width="160" />
|
||||
<el-table-column label="啟用" width="80">
|
||||
<template #default="{ row }">{{ row.is_active ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<template #footer>
|
||||
<el-button @click="showRelationDialog = false">關閉</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -70,7 +99,7 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { getSystems, createSystem, updateSystem } from '@/api/systems'
|
||||
import { getSystems, createSystem, updateSystem, getSystemGroups, getSystemMembers } from '@/api/systems'
|
||||
|
||||
const systems = ref([])
|
||||
const loading = ref(false)
|
||||
@@ -89,6 +118,13 @@ const rules = {
|
||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const showRelationDialog = ref(false)
|
||||
const relationLoading = ref(false)
|
||||
const relationSystemKey = ref('')
|
||||
const relationTab = ref('groups')
|
||||
const relationGroups = ref([])
|
||||
const relationMembers = ref([])
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
@@ -156,5 +192,24 @@ async function handleEdit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function openRelations(row, tab) {
|
||||
relationSystemKey.value = row.system_key
|
||||
relationTab.value = tab
|
||||
showRelationDialog.value = true
|
||||
relationLoading.value = true
|
||||
try {
|
||||
const [groupsRes, membersRes] = await Promise.all([
|
||||
getSystemGroups(row.system_key),
|
||||
getSystemMembers(row.system_key)
|
||||
])
|
||||
relationGroups.value = groupsRes.data?.items || []
|
||||
relationMembers.value = membersRes.data?.items || []
|
||||
} catch (err) {
|
||||
ElMessage.error('載入系統關聯資料失敗')
|
||||
} finally {
|
||||
relationLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="Scope 類型" prop="scope_type">
|
||||
<el-select v-model="grantForm.scope_type" placeholder="選擇 Scope 類型">
|
||||
<el-option label="Company" value="company" />
|
||||
<el-option label="Site" value="site" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -99,7 +98,6 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="Scope 類型" prop="scope_type">
|
||||
<el-select v-model="revokeForm.scope_type" placeholder="選擇 Scope 類型">
|
||||
<el-option label="Company" value="company" />
|
||||
<el-option label="Site" value="site" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -162,7 +160,6 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<el-input v-model="listFilters.keyword" placeholder="搜尋 email/sub/module/action" clearable style="width: 280px" @keyup.enter="loadDirectPermissionList" />
|
||||
<el-select v-model="listFilters.scope_type" clearable placeholder="Scope" style="width: 140px">
|
||||
<el-option label="Company" value="company" />
|
||||
<el-option label="Site" value="site" />
|
||||
</el-select>
|
||||
<el-button :loading="listLoading" @click="loadDirectPermissionList">查詢</el-button>
|
||||
@@ -210,7 +207,7 @@ const modules = ref([])
|
||||
const companies = ref([])
|
||||
const sites = ref([])
|
||||
const members = ref([])
|
||||
const actionOptions = ['view', 'edit', 'manage', 'admin']
|
||||
const actionOptions = ['view', 'edit']
|
||||
const listFilters = reactive({ keyword: '', scope_type: '' })
|
||||
const listLoading = ref(false)
|
||||
const directPermissions = ref([])
|
||||
@@ -246,9 +243,6 @@ const grantModuleOptions = computed(() => {
|
||||
})
|
||||
|
||||
const grantScopeOptions = computed(() => {
|
||||
if (grantForm.scope_type === 'company') {
|
||||
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
|
||||
}
|
||||
if (grantForm.scope_type === 'site') {
|
||||
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
||||
}
|
||||
@@ -318,9 +312,6 @@ const revokeModuleOptions = computed(() => {
|
||||
})
|
||||
|
||||
const revokeScopeOptions = computed(() => {
|
||||
if (revokeForm.scope_type === 'company') {
|
||||
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
|
||||
}
|
||||
if (revokeForm.scope_type === 'site') {
|
||||
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user