372 lines
12 KiB
Vue
372 lines
12 KiB
Vue
<template>
|
||
<div>
|
||
<h2 class="text-xl font-bold text-gray-800 mb-6">群組與權限管理</h2>
|
||
|
||
<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="220" />
|
||
<el-table-column prop="status" label="狀態" width="120" />
|
||
<el-table-column label="操作" width="300">
|
||
<template #default="{ row }">
|
||
<el-button size="small" @click="openEditGroup(row)">編輯</el-button>
|
||
<el-button size="small" @click="openBindingDialog(row)">設定關聯</el-button>
|
||
<el-button size="small" type="danger" @click="handleDeleteGroup(row)">刪除</el-button>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
</el-card>
|
||
|
||
<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="系統(多選)">
|
||
<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="bindingForm.module_keys" multiple filterable clearable style="width: 100%" placeholder="選擇模組(可空,空值代表系統層)">
|
||
<el-option
|
||
v-for="m in filteredModuleOptions"
|
||
: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="bindingForm.member_subs" multiple filterable clearable style="width: 100%" placeholder="選擇會員">
|
||
<el-option
|
||
v-for="m in members"
|
||
:key="m.user_sub"
|
||
:label="`${m.display_name || m.email || '(no-name)'} (${m.user_sub})`"
|
||
:value="m.user_sub"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<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
|
||
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>
|
||
|
||
<el-dialog v-model="showCreateGroup" title="新增群組" @close="resetCreateForm">
|
||
<el-form :model="createForm" label-width="120px">
|
||
<el-form-item label="群組名稱">
|
||
<el-input v-model="createForm.name" placeholder="群組名稱" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="showCreateGroup = false">取消</el-button>
|
||
<el-button type="primary" :loading="creatingGroup" @click="handleCreateGroup">確認</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<el-dialog v-model="showEditGroup" title="編輯群組" @close="resetEditGroupForm">
|
||
<el-form :model="editGroupForm" label-width="120px">
|
||
<el-form-item label="Group Key">
|
||
<el-input :model-value="editGroupForm.group_key" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="群組名稱">
|
||
<el-input v-model="editGroupForm.name" />
|
||
</el-form-item>
|
||
<el-form-item label="狀態">
|
||
<el-select v-model="editGroupForm.status" style="width: 100%">
|
||
<el-option label="active" value="active" />
|
||
<el-option label="inactive" value="inactive" />
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="showEditGroup = false">取消</el-button>
|
||
<el-button type="primary" :loading="savingGroup" @click="handleEditGroup">儲存</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, onMounted, computed } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { Plus } from '@element-plus/icons-vue'
|
||
import {
|
||
getPermissionGroups,
|
||
createPermissionGroup,
|
||
updatePermissionGroup,
|
||
deletePermissionGroup,
|
||
getPermissionGroupPermissions,
|
||
getPermissionGroupBindings,
|
||
updatePermissionGroupBindings
|
||
} from '@/api/permission-groups'
|
||
import { getSystems } from '@/api/systems'
|
||
import { getModules } from '@/api/modules'
|
||
import { getSites } from '@/api/sites'
|
||
import { getCompanies } from '@/api/companies'
|
||
import { getMembers } from '@/api/members'
|
||
|
||
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({ 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.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
|
||
try {
|
||
const res = await getPermissionGroups()
|
||
groups.value = res.data?.items || []
|
||
} catch (err) {
|
||
ElMessage.error('載入群組失敗')
|
||
} finally {
|
||
loadingGroups.value = false
|
||
}
|
||
}
|
||
|
||
async function loadCatalogs() {
|
||
const [systemsRes, modulesRes, sitesRes, companiesRes, membersRes] = await Promise.all([
|
||
getSystems(),
|
||
getModules(),
|
||
getSites(),
|
||
getCompanies(),
|
||
getMembers()
|
||
])
|
||
systems.value = systemsRes.data?.items || []
|
||
modules.value = modulesRes.data?.items || []
|
||
sites.value = sitesRes.data?.items || []
|
||
companies.value = companiesRes.data?.items || []
|
||
members.value = membersRes.data?.items || []
|
||
}
|
||
|
||
async function handleCreateGroup() {
|
||
if (!createForm.name) {
|
||
ElMessage.warning('請填寫完整資訊')
|
||
return
|
||
}
|
||
creatingGroup.value = true
|
||
try {
|
||
const res = await createPermissionGroup(createForm)
|
||
ElMessage.success(`新增成功:${res.data?.group_key || ''}`)
|
||
showCreateGroup.value = false
|
||
resetCreateForm()
|
||
await loadGroups()
|
||
} catch (err) {
|
||
ElMessage.error('新增失敗')
|
||
} finally {
|
||
creatingGroup.value = false
|
||
}
|
||
}
|
||
|
||
function openEditGroup(row) {
|
||
editGroupForm.group_key = row.group_key
|
||
editGroupForm.name = row.name
|
||
editGroupForm.status = row.status || 'active'
|
||
showEditGroup.value = true
|
||
}
|
||
|
||
async function handleEditGroup() {
|
||
savingGroup.value = true
|
||
try {
|
||
await updatePermissionGroup(editGroupForm.group_key, {
|
||
name: editGroupForm.name,
|
||
status: editGroupForm.status
|
||
})
|
||
ElMessage.success('群組更新成功')
|
||
showEditGroup.value = false
|
||
await loadGroups()
|
||
} catch (err) {
|
||
ElMessage.error('群組更新失敗')
|
||
} finally {
|
||
savingGroup.value = false
|
||
}
|
||
}
|
||
|
||
async function handleDeleteGroup(row) {
|
||
try {
|
||
await ElMessageBox.confirm(`確認刪除群組 ${row.name}(${row.group_key})?`, '刪除確認', { type: 'warning' })
|
||
await deletePermissionGroup(row.group_key)
|
||
ElMessage.success('刪除成功')
|
||
await loadGroups()
|
||
} catch (err) {
|
||
if (err === 'cancel') return
|
||
ElMessage.error(err.response?.data?.detail || '刪除失敗')
|
||
}
|
||
}
|
||
|
||
async function openBindingDialog(row) {
|
||
bindingGroupKey.value = row.group_key
|
||
showBindingDialog.value = true
|
||
bindingLoading.value = true
|
||
resetBindingForm()
|
||
try {
|
||
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('載入群組關聯失敗')
|
||
} finally {
|
||
bindingLoading.value = false
|
||
}
|
||
}
|
||
|
||
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 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 {
|
||
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()])
|
||
})
|
||
</script>
|