feat(frontend): migrate admin UI to role-site model and clean legacy pages
This commit is contained in:
15
src/App.vue
15
src/App.vue
@@ -2,22 +2,16 @@
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<header v-if="showNav" class="bg-white border-b border-gray-200 sticky top-0 z-50 shadow-sm">
|
||||
<div class="max-w-7xl mx-auto px-6 flex items-center justify-between h-14">
|
||||
|
||||
<!-- Logo 區(之後換圖) -->
|
||||
<div class="flex-shrink-0 w-36">
|
||||
<span class="text-sm font-semibold text-gray-700 tracking-wide">member.ose.tw</span>
|
||||
</div>
|
||||
|
||||
<!-- Tab 導覽 -->
|
||||
<nav class="flex items-stretch h-full overflow-x-auto gap-0 flex-1 min-w-0">
|
||||
<NavTab v-for="tab in userTabs" :key="tab.to" :to="tab.to">{{ tab.label }}</NavTab>
|
||||
|
||||
<span class="self-center mx-3 text-gray-200 select-none text-lg">|</span>
|
||||
|
||||
<NavTab v-for="tab in adminTabs" :key="tab.to" :to="tab.to">{{ tab.label }}</NavTab>
|
||||
</nav>
|
||||
|
||||
<!-- 右側:登出 -->
|
||||
<div class="flex-shrink-0 ml-4">
|
||||
<button
|
||||
@click="logout"
|
||||
@@ -26,7 +20,6 @@
|
||||
登出
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -52,20 +45,18 @@ const showNav = computed(() => {
|
||||
|
||||
const userTabs = [
|
||||
{ to: '/me', label: '我的資料' },
|
||||
{ to: '/me/permissions', label: '我的權限' }
|
||||
{ to: '/me/permissions', label: '我的角色' }
|
||||
]
|
||||
|
||||
const adminTabs = [
|
||||
{ to: '/admin/systems', label: '系統' },
|
||||
{ to: '/admin/modules', label: '模組' },
|
||||
{ to: '/admin/companies', label: '公司' },
|
||||
{ to: '/admin/sites', label: '站台' },
|
||||
{ to: '/admin/systems', label: '系統' },
|
||||
{ to: '/admin/roles', label: '角色' },
|
||||
{ to: '/admin/members', label: '會員' },
|
||||
{ to: '/admin/permission-groups', label: '群組' },
|
||||
{ to: '/admin/api-clients', label: 'API Clients' }
|
||||
]
|
||||
|
||||
// 行內 NavTab 元件:避免另開檔案
|
||||
const NavTab = defineComponent({
|
||||
props: { to: String },
|
||||
setup(props, { slots }) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { adminHttp } from './http'
|
||||
|
||||
export const getCompanies = () => adminHttp.get('/admin/companies')
|
||||
export const getCompanies = (params) => adminHttp.get('/admin/companies', { params })
|
||||
export const createCompany = (data) => adminHttp.post('/admin/companies', data)
|
||||
export const updateCompany = (companyKey, data) => adminHttp.patch(`/admin/companies/${companyKey}`, data)
|
||||
export const deleteCompany = (companyKey) => adminHttp.delete(`/admin/companies/${companyKey}`)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { adminHttp } from './http'
|
||||
|
||||
export const getMembers = () => adminHttp.get('/admin/members')
|
||||
export const upsertMember = (data) => adminHttp.post('/admin/members/upsert', data)
|
||||
export const getMembers = (params) => adminHttp.get('/admin/members', { params })
|
||||
export const createMember = (data) => adminHttp.post('/admin/members', data)
|
||||
export const updateMember = (userSub, data) => adminHttp.patch(`/admin/members/${userSub}`, data)
|
||||
export const deleteMember = (userSub) => adminHttp.delete(`/admin/members/${userSub}`)
|
||||
export const deleteMember = (userSub, syncToIdp = true) => adminHttp.delete(`/admin/members/${userSub}`, { params: { sync_to_idp: syncToIdp } })
|
||||
export const resetMemberPassword = (userSub) => adminHttp.post(`/admin/members/${userSub}/password/reset`)
|
||||
export const getMemberPermissionGroups = (userSub) => adminHttp.get(`/admin/members/${userSub}/permission-groups`)
|
||||
export const setMemberPermissionGroups = (userSub, groupKeys) =>
|
||||
adminHttp.put(`/admin/members/${userSub}/permission-groups`, { group_keys: groupKeys })
|
||||
export const getMemberSites = (userSub) => adminHttp.get(`/admin/members/${userSub}/sites`)
|
||||
export const setMemberSites = (userSub, siteKeys) => adminHttp.put(`/admin/members/${userSub}/sites`, { site_keys: siteKeys })
|
||||
export const getMemberRoles = (userSub) => adminHttp.get(`/admin/members/${userSub}/roles`)
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
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 deleteModule = (moduleKey) => adminHttp.delete(`/admin/modules/${moduleKey}`)
|
||||
export const getModuleGroups = (moduleKey) => adminHttp.get(`/admin/modules/${moduleKey}/groups`)
|
||||
export const getModuleMembers = (moduleKey) => adminHttp.get(`/admin/modules/${moduleKey}/members`)
|
||||
@@ -1,6 +0,0 @@
|
||||
import { adminHttp } from './http'
|
||||
|
||||
export const grantPermission = (data) => adminHttp.post('/admin/permissions/grant', data)
|
||||
export const revokePermission = (data) => adminHttp.post('/admin/permissions/revoke', data)
|
||||
export const listDirectPermissions = (params) => adminHttp.get('/admin/permissions/direct', { params })
|
||||
export const revokeDirectPermissionById = (permissionId) => adminHttp.delete(`/admin/permissions/direct/${permissionId}`)
|
||||
@@ -1,22 +0,0 @@
|
||||
import { adminHttp } from './http'
|
||||
|
||||
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 deletePermissionGroup = (groupKey) => adminHttp.delete(`/admin/permission-groups/${groupKey}`)
|
||||
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, idpSub) =>
|
||||
adminHttp.post(`/admin/permission-groups/${groupKey}/members/${idpSub}`)
|
||||
|
||||
export const removeMemberFromGroup = (groupKey, idpSub) =>
|
||||
adminHttp.delete(`/admin/permission-groups/${groupKey}/members/${idpSub}`)
|
||||
|
||||
export const groupGrant = (groupKey, data) =>
|
||||
adminHttp.post(`/admin/permission-groups/${groupKey}/permissions/grant`, data)
|
||||
|
||||
export const groupRevoke = (groupKey, data) =>
|
||||
adminHttp.post(`/admin/permission-groups/${groupKey}/permissions/revoke`, data)
|
||||
7
src/api/roles.js
Normal file
7
src/api/roles.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { adminHttp } from './http'
|
||||
|
||||
export const getRoles = (params) => adminHttp.get('/admin/roles', { params })
|
||||
export const createRole = (data) => adminHttp.post('/admin/roles', data)
|
||||
export const updateRole = (roleKey, data) => adminHttp.patch(`/admin/roles/${roleKey}`, data)
|
||||
export const deleteRole = (roleKey) => adminHttp.delete(`/admin/roles/${roleKey}`)
|
||||
export const getRoleSites = (roleKey) => adminHttp.get(`/admin/roles/${roleKey}/sites`)
|
||||
@@ -1,6 +1,9 @@
|
||||
import { adminHttp } from './http'
|
||||
|
||||
export const getSites = () => adminHttp.get('/admin/sites')
|
||||
export const getSites = (params) => adminHttp.get('/admin/sites', { params })
|
||||
export const createSite = (data) => adminHttp.post('/admin/sites', data)
|
||||
export const updateSite = (siteKey, data) => adminHttp.patch(`/admin/sites/${siteKey}`, data)
|
||||
export const deleteSite = (siteKey) => adminHttp.delete(`/admin/sites/${siteKey}`)
|
||||
export const getSiteRoles = (siteKey) => adminHttp.get(`/admin/sites/${siteKey}/roles`)
|
||||
export const setSiteRoles = (siteKey, roleKeys) => adminHttp.put(`/admin/sites/${siteKey}/roles`, { role_keys: roleKeys })
|
||||
export const getSiteMembers = (siteKey) => adminHttp.get(`/admin/sites/${siteKey}/members`)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { adminHttp } from './http'
|
||||
|
||||
export const getSystems = () => adminHttp.get('/admin/systems')
|
||||
export const getSystems = (params) => adminHttp.get('/admin/systems', { params })
|
||||
export const createSystem = (data) => adminHttp.post('/admin/systems', data)
|
||||
export const updateSystem = (systemKey, data) => adminHttp.patch(`/admin/systems/${systemKey}`, data)
|
||||
export const deleteSystem = (systemKey) => adminHttp.delete(`/admin/systems/${systemKey}`)
|
||||
export const getSystemGroups = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/groups`)
|
||||
export const getSystemMembers = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/members`)
|
||||
export const getSystemRoles = (systemKey) => adminHttp.get(`/admin/systems/${systemKey}/roles`)
|
||||
|
||||
@@ -78,9 +78,9 @@ onMounted(async () => {
|
||||
// 導向原頁面或預設的 /me
|
||||
sessionStorage.removeItem('oidc_expected_state')
|
||||
sessionStorage.removeItem('oidc_pkce_verifier')
|
||||
const redirect = '/login'
|
||||
const redirect = sessionStorage.getItem('post_login_redirect') || '/me'
|
||||
sessionStorage.removeItem('post_login_redirect')
|
||||
router.push(redirect)
|
||||
router.replace(redirect)
|
||||
} catch (err) {
|
||||
sessionStorage.removeItem('oidc_expected_state')
|
||||
sessionStorage.removeItem('oidc_pkce_verifier')
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800">公司管理</h2>
|
||||
<el-button type="primary" @click="showDialog = true" :icon="Plus">新增公司</el-button>
|
||||
<el-button type="primary" @click="showCreateDialog = true" :icon="Plus">新增公司</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="error" :title="errorMsg" type="error" show-icon :closable="false" class="mb-4" />
|
||||
@@ -11,31 +11,40 @@
|
||||
<el-table v-else :data="companies" stripe border class="w-full shadow-sm">
|
||||
<template #empty><el-empty description="目前無公司" /></template>
|
||||
<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 prop="display_name" label="顯示名稱" min-width="180" />
|
||||
<el-table-column prop="legal_name" label="公司名稱" min-width="220" />
|
||||
<el-table-column prop="status" label="狀態" width="110" />
|
||||
<el-table-column label="操作" width="280">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
||||
<el-button size="small" @click="openSites(row)">站台</el-button>
|
||||
<el-button size="small" @click="openSites(row)">查看站台</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="showDialog" title="新增公司" @close="resetForm">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="名稱" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-dialog v-model="showCreateDialog" title="新增公司" width="560px" @close="resetCreateForm">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="rules" label-width="120px">
|
||||
<el-form-item label="顯示名稱" prop="display_name"><el-input v-model="createForm.display_name" /></el-form-item>
|
||||
<el-form-item label="公司名稱"><el-input v-model="createForm.legal_name" /></el-form-item>
|
||||
<el-form-item label="狀態">
|
||||
<el-select v-model="createForm.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="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
||||
<el-button @click="showCreateDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="handleCreate">建立</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showEditDialog" title="編輯公司" @close="resetEditForm">
|
||||
<el-form :model="editForm" label-width="100px">
|
||||
<el-dialog v-model="showEditDialog" title="編輯公司" width="560px" @close="resetEditForm">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="Company Key"><el-input :model-value="editForm.company_key" disabled /></el-form-item>
|
||||
<el-form-item label="名稱"><el-input v-model="editForm.name" /></el-form-item>
|
||||
<el-form-item label="顯示名稱"><el-input v-model="editForm.display_name" /></el-form-item>
|
||||
<el-form-item label="公司名稱"><el-input v-model="editForm.legal_name" /></el-form-item>
|
||||
<el-form-item label="狀態">
|
||||
<el-select v-model="editForm.status" style="width: 100%">
|
||||
<el-option label="active" value="active" />
|
||||
@@ -45,16 +54,17 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEditDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleEdit">儲存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showSitesDialog" :title="`公司站台:${selectedCompanyKey}`" width="900px">
|
||||
<el-dialog v-model="showSitesDialog" :title="`站台列表:${selectedCompanyDisplayName}`" width="960px">
|
||||
<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-column prop="site_key" label="Site Key" width="210" />
|
||||
<el-table-column prop="display_name" label="站台名稱" min-width="200" />
|
||||
<el-table-column prop="domain" label="Domain" min-width="220" />
|
||||
<el-table-column prop="status" label="狀態" width="110" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="showSitesDialog = false">關閉</el-button>
|
||||
@@ -73,21 +83,22 @@ const companies = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const showDialog = ref(false)
|
||||
const submitting = ref(false)
|
||||
const showEditDialog = ref(false)
|
||||
const savingEdit = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
const form = ref({ name: '' })
|
||||
const editForm = ref({ company_key: '', name: '', status: 'active' })
|
||||
const showCreateDialog = ref(false)
|
||||
const showEditDialog = ref(false)
|
||||
const creating = ref(false)
|
||||
const saving = ref(false)
|
||||
const createFormRef = ref()
|
||||
|
||||
const createForm = ref({ display_name: '', legal_name: '', status: 'active' })
|
||||
const editForm = ref({ company_key: '', display_name: '', legal_name: '', status: 'active' })
|
||||
const rules = {
|
||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||
display_name: [{ required: true, message: '請輸入顯示名稱', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const showSitesDialog = ref(false)
|
||||
const sitesLoading = ref(false)
|
||||
const selectedCompanyKey = ref('')
|
||||
const selectedCompanyDisplayName = ref('')
|
||||
const companySites = ref([])
|
||||
|
||||
async function load() {
|
||||
@@ -98,67 +109,75 @@ async function load() {
|
||||
companies.value = res.data?.items || []
|
||||
} catch (err) {
|
||||
error.value = true
|
||||
errorMsg.value = err.response?.status === 422
|
||||
? '缺少管理員 API 認證,請檢查前端 .env.development'
|
||||
: '載入失敗,請稍後再試'
|
||||
errorMsg.value = err.response?.data?.detail || '載入公司失敗'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = { name: '' }
|
||||
function resetCreateForm() {
|
||||
createForm.value = { display_name: '', legal_name: '', status: 'active' }
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editForm.value = { company_key: row.company_key, name: row.name, status: row.status || 'active' }
|
||||
editForm.value = {
|
||||
company_key: row.company_key,
|
||||
display_name: row.display_name,
|
||||
legal_name: row.legal_name || '',
|
||||
status: row.status || 'active'
|
||||
}
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
function resetEditForm() {
|
||||
editForm.value = { company_key: '', name: '', status: 'active' }
|
||||
editForm.value = { company_key: '', display_name: '', legal_name: '', status: 'active' }
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
const valid = await createFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
submitting.value = true
|
||||
creating.value = true
|
||||
try {
|
||||
const res = await createCompany(form.value)
|
||||
const res = await createCompany(createForm.value)
|
||||
ElMessage.success(`新增成功:${res.data?.company_key || ''}`)
|
||||
showDialog.value = false
|
||||
resetForm()
|
||||
showCreateDialog.value = false
|
||||
resetCreateForm()
|
||||
await load()
|
||||
} catch (err) {
|
||||
ElMessage.error('新增失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '新增公司失敗')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
savingEdit.value = true
|
||||
saving.value = true
|
||||
try {
|
||||
await updateCompany(editForm.value.company_key, { name: editForm.value.name, status: editForm.value.status })
|
||||
await updateCompany(editForm.value.company_key, {
|
||||
display_name: editForm.value.display_name,
|
||||
legal_name: editForm.value.legal_name || null,
|
||||
status: editForm.value.status
|
||||
})
|
||||
ElMessage.success('更新成功')
|
||||
showEditDialog.value = false
|
||||
await load()
|
||||
} catch (err) {
|
||||
ElMessage.error('更新失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '更新公司失敗')
|
||||
} finally {
|
||||
savingEdit.value = false
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openSites(row) {
|
||||
selectedCompanyKey.value = row.company_key
|
||||
showSitesDialog.value = true
|
||||
selectedCompanyDisplayName.value = `${row.display_name} (${row.company_key})`
|
||||
sitesLoading.value = true
|
||||
try {
|
||||
const res = await getCompanySites(row.company_key)
|
||||
companySites.value = res.data?.items || []
|
||||
} catch (err) {
|
||||
ElMessage.error('載入公司站台失敗')
|
||||
companySites.value = res.data?.sites || []
|
||||
} catch (_err) {
|
||||
ElMessage.error('載入站台列表失敗')
|
||||
companySites.value = []
|
||||
} finally {
|
||||
sitesLoading.value = false
|
||||
}
|
||||
@@ -167,7 +186,7 @@ async function openSites(row) {
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`確認刪除公司 ${row.name}(${row.company_key})?此操作會一併刪除旗下站台與相關授權。`,
|
||||
`確認刪除公司 ${row.display_name}(${row.company_key})?`,
|
||||
'刪除確認',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
@@ -176,7 +195,7 @@ async function handleDelete(row) {
|
||||
await load()
|
||||
} catch (err) {
|
||||
if (err === 'cancel') return
|
||||
ElMessage.error(err.response?.data?.detail || '刪除失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '刪除公司失敗')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,29 +14,35 @@
|
||||
<el-table v-else :data="members" stripe border class="w-full shadow-sm">
|
||||
<template #empty><el-empty description="目前無會員" /></template>
|
||||
<el-table-column prop="user_sub" label="User Sub" min-width="260" />
|
||||
<el-table-column prop="username" label="Username" min-width="160" />
|
||||
<el-table-column prop="username" label="Username" min-width="150" />
|
||||
<el-table-column prop="email" label="Email" min-width="220" />
|
||||
<el-table-column prop="display_name" label="顯示名稱" min-width="180" />
|
||||
<el-table-column prop="is_active" label="啟用" width="100">
|
||||
<el-table-column prop="display_name" label="顯示名稱" min-width="170" />
|
||||
<el-table-column label="啟用" width="80">
|
||||
<template #default="{ row }">{{ row.is_active ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220">
|
||||
<el-table-column label="操作" width="360">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
||||
<el-button size="small" @click="openRoles(row)">角色</el-button>
|
||||
<el-button size="small" type="warning" @click="handleResetPassword(row)">重設密碼</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="showCreateDialog" title="新增會員" @close="resetCreateForm">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="120px">
|
||||
<el-dialog v-model="showCreateDialog" title="新增會員" width="760px" @close="resetCreateForm">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="130px">
|
||||
<el-form-item label="Username" prop="username"><el-input v-model="createForm.username" /></el-form-item>
|
||||
<el-form-item label="Email" prop="email"><el-input v-model="createForm.email" /></el-form-item>
|
||||
<el-form-item label="顯示名稱" prop="display_name"><el-input v-model="createForm.display_name" /></el-form-item>
|
||||
<el-form-item label="權限群組">
|
||||
<el-select v-model="createForm.group_keys" multiple filterable clearable style="width: 100%" placeholder="可選多個群組">
|
||||
<el-option v-for="g in groups" :key="g.group_key" :label="`${g.name} (${g.group_key})`" :value="g.group_key" />
|
||||
<el-form-item label="顯示名稱"><el-input v-model="createForm.display_name" /></el-form-item>
|
||||
<el-form-item label="所屬站台">
|
||||
<el-select v-model="createForm.site_keys" multiple filterable clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="site in siteOptions"
|
||||
:key="site.site_key"
|
||||
:label="`${site.company_display_name} / ${site.display_name} (${site.site_key})`"
|
||||
:value="site.site_key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="啟用"><el-switch v-model="createForm.is_active" /></el-form-item>
|
||||
@@ -48,15 +54,20 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showEditDialog" title="編輯會員" @close="resetEditForm">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-dialog v-model="showEditDialog" title="編輯會員" width="760px" @close="resetEditForm">
|
||||
<el-form :model="editForm" label-width="130px">
|
||||
<el-form-item label="User Sub"><el-input :model-value="editForm.user_sub" disabled /></el-form-item>
|
||||
<el-form-item label="Username"><el-input v-model="editForm.username" /></el-form-item>
|
||||
<el-form-item label="Email"><el-input v-model="editForm.email" /></el-form-item>
|
||||
<el-form-item label="顯示名稱"><el-input v-model="editForm.display_name" /></el-form-item>
|
||||
<el-form-item label="權限群組">
|
||||
<el-select v-model="editForm.group_keys" multiple filterable clearable style="width: 100%" placeholder="可選多個群組">
|
||||
<el-option v-for="g in groups" :key="g.group_key" :label="`${g.name} (${g.group_key})`" :value="g.group_key" />
|
||||
<el-form-item label="所屬站台">
|
||||
<el-select v-model="editForm.site_keys" multiple filterable clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="site in siteOptions"
|
||||
:key="site.site_key"
|
||||
:label="`${site.company_display_name} / ${site.display_name} (${site.site_key})`"
|
||||
:value="site.site_key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="啟用"><el-switch v-model="editForm.is_active" /></el-form-item>
|
||||
@@ -67,6 +78,20 @@
|
||||
<el-button type="primary" :loading="saving" @click="handleEdit">儲存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showRolesDialog" :title="`會員角色:${selectedUserLabel}`" width="1080px">
|
||||
<el-table :data="effectiveRoles" border stripe v-loading="rolesLoading">
|
||||
<template #empty><el-empty description="此會員目前沒有角色" /></template>
|
||||
<el-table-column prop="company_display_name" label="公司" min-width="160" />
|
||||
<el-table-column prop="site_display_name" label="站台" min-width="170" />
|
||||
<el-table-column prop="system_name" label="系統" min-width="150" />
|
||||
<el-table-column prop="role_name" label="角色" min-width="160" />
|
||||
<el-table-column prop="idp_role_name" label="Keycloak Role" min-width="190" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="showRolesDialog = false">關閉</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -74,31 +99,32 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { getSites } from '@/api/sites'
|
||||
import {
|
||||
getMembers,
|
||||
upsertMember,
|
||||
createMember,
|
||||
updateMember,
|
||||
deleteMember,
|
||||
resetMemberPassword,
|
||||
getMemberPermissionGroups,
|
||||
setMemberPermissionGroups
|
||||
getMemberSites,
|
||||
setMemberSites,
|
||||
getMemberRoles
|
||||
} from '@/api/members'
|
||||
import { getPermissionGroups } from '@/api/permission-groups'
|
||||
|
||||
const members = ref([])
|
||||
const groups = ref([])
|
||||
const siteOptions = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const errorMsg = ref('')
|
||||
|
||||
const showCreateDialog = ref(false)
|
||||
const createFormRef = ref()
|
||||
const creating = ref(false)
|
||||
const createFormRef = ref()
|
||||
const createForm = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
display_name: '',
|
||||
group_keys: [],
|
||||
site_keys: [],
|
||||
is_active: true,
|
||||
sync_to_idp: true
|
||||
})
|
||||
@@ -114,21 +140,30 @@ const editForm = ref({
|
||||
username: '',
|
||||
email: '',
|
||||
display_name: '',
|
||||
group_keys: [],
|
||||
site_keys: [],
|
||||
is_active: true,
|
||||
sync_to_idp: true
|
||||
})
|
||||
|
||||
const showRolesDialog = ref(false)
|
||||
const selectedUserLabel = ref('')
|
||||
const effectiveRoles = ref([])
|
||||
const rolesLoading = ref(false)
|
||||
|
||||
async function loadCatalogs() {
|
||||
const res = await getSites({ limit: 500, offset: 0 })
|
||||
siteOptions.value = res.data?.items || []
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
try {
|
||||
const [membersRes, groupsRes] = await Promise.all([getMembers(), getPermissionGroups()])
|
||||
const [membersRes] = await Promise.all([getMembers(), loadCatalogs()])
|
||||
members.value = membersRes.data?.items || []
|
||||
groups.value = groupsRes.data?.items || []
|
||||
} catch (err) {
|
||||
error.value = true
|
||||
errorMsg.value = err.response?.data?.detail || '載入失敗,請稍後再試'
|
||||
errorMsg.value = err.response?.data?.detail || '載入會員失敗'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -139,38 +174,7 @@ function resetCreateForm() {
|
||||
username: '',
|
||||
email: '',
|
||||
display_name: '',
|
||||
group_keys: [],
|
||||
is_active: true,
|
||||
sync_to_idp: true
|
||||
}
|
||||
}
|
||||
|
||||
async function openEdit(row) {
|
||||
editForm.value = {
|
||||
user_sub: row.user_sub,
|
||||
username: row.username || '',
|
||||
email: row.email || '',
|
||||
display_name: row.display_name || '',
|
||||
group_keys: [],
|
||||
is_active: !!row.is_active,
|
||||
sync_to_idp: true
|
||||
}
|
||||
try {
|
||||
const res = await getMemberPermissionGroups(row.user_sub)
|
||||
editForm.value.group_keys = res.data?.group_keys || []
|
||||
} catch (err) {
|
||||
ElMessage.warning('載入會員群組失敗,仍可先編輯基本資料')
|
||||
}
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
function resetEditForm() {
|
||||
editForm.value = {
|
||||
user_sub: '',
|
||||
username: '',
|
||||
email: '',
|
||||
display_name: '',
|
||||
group_keys: [],
|
||||
site_keys: [],
|
||||
is_active: true,
|
||||
sync_to_idp: true
|
||||
}
|
||||
@@ -181,23 +185,60 @@ async function handleCreate() {
|
||||
if (!valid) return
|
||||
creating.value = true
|
||||
try {
|
||||
const created = await upsertMember({ ...createForm.value })
|
||||
const createdSub = created.data?.user_sub
|
||||
if (createdSub && createForm.value.group_keys.length > 0) {
|
||||
await setMemberPermissionGroups(createdSub, createForm.value.group_keys)
|
||||
const payload = {
|
||||
username: createForm.value.username || null,
|
||||
email: createForm.value.email || null,
|
||||
display_name: createForm.value.display_name || null,
|
||||
is_active: createForm.value.is_active,
|
||||
sync_to_idp: createForm.value.sync_to_idp
|
||||
}
|
||||
const res = await createMember(payload)
|
||||
const userSub = res.data?.user_sub
|
||||
if (userSub) {
|
||||
await setMemberSites(userSub, createForm.value.site_keys || [])
|
||||
}
|
||||
ElMessage.success('新增會員成功')
|
||||
showCreateDialog.value = false
|
||||
resetCreateForm()
|
||||
await load()
|
||||
} catch (err) {
|
||||
const detail = err.response?.data?.detail
|
||||
ElMessage.error(detail || '新增會員失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '新增會員失敗')
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openEdit(row) {
|
||||
editForm.value = {
|
||||
user_sub: row.user_sub,
|
||||
username: row.username || '',
|
||||
email: row.email || '',
|
||||
display_name: row.display_name || '',
|
||||
site_keys: [],
|
||||
is_active: !!row.is_active,
|
||||
sync_to_idp: true
|
||||
}
|
||||
try {
|
||||
const res = await getMemberSites(row.user_sub)
|
||||
editForm.value.site_keys = (res.data?.sites || []).map((site) => site.site_key)
|
||||
} catch (_err) {
|
||||
ElMessage.warning('讀取會員站台失敗,仍可編輯基本資料')
|
||||
}
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
function resetEditForm() {
|
||||
editForm.value = {
|
||||
user_sub: '',
|
||||
username: '',
|
||||
email: '',
|
||||
display_name: '',
|
||||
site_keys: [],
|
||||
is_active: true,
|
||||
sync_to_idp: true
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
saving.value = true
|
||||
try {
|
||||
@@ -208,13 +249,12 @@ async function handleEdit() {
|
||||
is_active: editForm.value.is_active,
|
||||
sync_to_idp: editForm.value.sync_to_idp
|
||||
})
|
||||
await setMemberPermissionGroups(editForm.value.user_sub, editForm.value.group_keys || [])
|
||||
await setMemberSites(editForm.value.user_sub, editForm.value.site_keys || [])
|
||||
ElMessage.success('更新會員成功')
|
||||
showEditDialog.value = false
|
||||
await load()
|
||||
} catch (err) {
|
||||
const detail = err.response?.data?.detail
|
||||
ElMessage.error(detail || '更新會員失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '更新會員失敗')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
@@ -223,16 +263,15 @@ async function handleEdit() {
|
||||
async function handleResetPassword(row) {
|
||||
try {
|
||||
const res = await resetMemberPassword(row.user_sub)
|
||||
const pwd = res.data?.temporary_password || ''
|
||||
if (!pwd) {
|
||||
ElMessage.success('密碼已重設')
|
||||
const tempPassword = res.data?.temporary_password
|
||||
if (tempPassword) {
|
||||
await navigator.clipboard.writeText(tempPassword)
|
||||
ElMessage.success('已重設密碼,臨時密碼已複製')
|
||||
return
|
||||
}
|
||||
await navigator.clipboard.writeText(pwd)
|
||||
ElMessage.success(`已重設密碼,臨時密碼已複製:${pwd}`)
|
||||
ElMessage.success('已重設密碼')
|
||||
} catch (err) {
|
||||
const detail = err.response?.data?.detail
|
||||
ElMessage.error(detail || '重設密碼失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '重設密碼失敗')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,12 +282,27 @@ async function handleDelete(row) {
|
||||
'刪除確認',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
await deleteMember(row.user_sub)
|
||||
await deleteMember(row.user_sub, true)
|
||||
ElMessage.success('刪除成功')
|
||||
await load()
|
||||
} catch (err) {
|
||||
if (err === 'cancel') return
|
||||
ElMessage.error(err.response?.data?.detail || '刪除失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '刪除會員失敗')
|
||||
}
|
||||
}
|
||||
|
||||
async function openRoles(row) {
|
||||
selectedUserLabel.value = `${row.display_name || row.username || row.user_sub}`
|
||||
showRolesDialog.value = true
|
||||
rolesLoading.value = true
|
||||
try {
|
||||
const res = await getMemberRoles(row.user_sub)
|
||||
effectiveRoles.value = res.data?.roles || []
|
||||
} catch (_err) {
|
||||
ElMessage.error('載入會員角色失敗')
|
||||
effectiveRoles.value = []
|
||||
} finally {
|
||||
rolesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800">模組管理</h2>
|
||||
<el-button type="primary" @click="showDialog = true" :icon="Plus">新增模組</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="error"
|
||||
:title="errorMsg"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<el-skeleton v-if="loading" :rows="4" animated />
|
||||
|
||||
<el-table v-else :data="modules" stripe border class="w-full shadow-sm">
|
||||
<template #empty><el-empty description="目前無模組" /></template>
|
||||
<el-table-column label="System" min-width="200">
|
||||
<template #default="{ row }">{{ getSystemName(row.system_key) }}</template>
|
||||
</el-table-column>
|
||||
<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="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>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="showDialog" title="新增模組" @close="resetForm">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="System Key" prop="system_key">
|
||||
<el-select v-model="form.system_key" placeholder="選擇系統" filterable style="width: 100%">
|
||||
<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="名稱" prop="name">
|
||||
<el-input v-model="form.name" placeholder="行銷活動" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showEditDialog" title="編輯模組" @close="resetEditForm">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="Module Key">
|
||||
<el-input :model-value="editForm.module_key" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="名稱">
|
||||
<el-input v-model="editForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="狀態">
|
||||
<el-select v-model="editForm.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="showEditDialog = false">取消</el-button>
|
||||
<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="user_sub" label="User 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>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { getModules, createModule, updateModule, deleteModule, getModuleGroups, getModuleMembers } from '@/api/modules'
|
||||
import { getSystems } from '@/api/systems'
|
||||
|
||||
const modules = ref([])
|
||||
const systems = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const showDialog = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref()
|
||||
const showEditDialog = ref(false)
|
||||
const savingEdit = ref(false)
|
||||
|
||||
const form = ref({ system_key: '', name: '' })
|
||||
const editForm = ref({ module_key: '', name: '', status: 'active' })
|
||||
const rules = {
|
||||
system_key: [{ required: true, message: '請輸入 System Key', trigger: 'blur' }],
|
||||
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
|
||||
try {
|
||||
const [modulesRes, systemsRes] = await Promise.all([getModules(), getSystems()])
|
||||
modules.value = modulesRes.data?.items || []
|
||||
systems.value = systemsRes.data?.items || []
|
||||
} catch (err) {
|
||||
error.value = true
|
||||
errorMsg.value = err.response?.status === 422
|
||||
? '缺少管理員 API 認證,請檢查前端 .env.development'
|
||||
: '載入失敗,請稍後再試'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = { system_key: '', name: '' }
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editForm.value = {
|
||||
module_key: row.module_key,
|
||||
name: row.name,
|
||||
status: row.status || 'active'
|
||||
}
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
function resetEditForm() {
|
||||
editForm.value = { module_key: '', name: '', status: 'active' }
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await createModule(form.value)
|
||||
ElMessage.success(`新增成功:${res.data?.module_key || ''}`)
|
||||
showDialog.value = false
|
||||
resetForm()
|
||||
await load()
|
||||
} catch (err) {
|
||||
ElMessage.error('新增失敗,請稍後再試')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
savingEdit.value = true
|
||||
try {
|
||||
await updateModule(editForm.value.module_key, {
|
||||
name: editForm.value.name,
|
||||
status: editForm.value.status
|
||||
})
|
||||
ElMessage.success('更新成功')
|
||||
showEditDialog.value = false
|
||||
await load()
|
||||
} catch (err) {
|
||||
ElMessage.error('更新失敗')
|
||||
} finally {
|
||||
savingEdit.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`確認刪除模組 ${row.name}(${row.module_key})?`, '刪除確認', { type: 'warning' })
|
||||
await deleteModule(row.module_key)
|
||||
ElMessage.success('刪除成功')
|
||||
await load()
|
||||
} catch (err) {
|
||||
if (err === 'cancel') return
|
||||
ElMessage.error(err.response?.data?.detail || '刪除失敗')
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
function getSystemName(systemKey) {
|
||||
const system = systems.value.find(s => s.system_key === systemKey)
|
||||
return system ? system.name : systemKey
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
@@ -1,371 +0,0 @@
|
||||
<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>
|
||||
269
src/pages/admin/RolesPage.vue
Normal file
269
src/pages/admin/RolesPage.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800">角色管理</h2>
|
||||
<el-button type="primary" @click="showCreateDialog = true" :icon="Plus">新增角色</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="error" :title="errorMsg" type="error" show-icon :closable="false" class="mb-4" />
|
||||
<el-skeleton v-if="loading" :rows="4" animated />
|
||||
|
||||
<el-table v-else :data="roles" stripe border class="w-full shadow-sm">
|
||||
<template #empty><el-empty description="目前無角色" /></template>
|
||||
<el-table-column prop="role_key" label="Role Key" width="200" />
|
||||
<el-table-column prop="system_name" label="系統" min-width="150" />
|
||||
<el-table-column prop="name" label="角色名稱" min-width="180" />
|
||||
<el-table-column prop="idp_role_name" label="Keycloak Role" min-width="190" />
|
||||
<el-table-column prop="status" label="狀態" width="110" />
|
||||
<el-table-column label="操作" width="280">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
||||
<el-button size="small" @click="openSites(row)">站台</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="showCreateDialog" title="新增角色" width="660px" @close="resetCreateForm">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="rules" label-width="150px">
|
||||
<el-form-item label="系統" prop="system_key">
|
||||
<el-select v-model="createForm.system_key" filterable style="width: 100%">
|
||||
<el-option
|
||||
v-for="system in systems"
|
||||
:key="system.system_key"
|
||||
:label="`${system.name} (${system.system_key})`"
|
||||
:value="system.system_key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色名稱" prop="name"><el-input v-model="createForm.name" /></el-form-item>
|
||||
<el-form-item label="Keycloak Role" prop="idp_role_name"><el-input v-model="createForm.idp_role_name" /></el-form-item>
|
||||
<el-form-item label="描述"><el-input v-model="createForm.description" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="狀態">
|
||||
<el-select v-model="createForm.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="showCreateDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="handleCreate">建立</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showEditDialog" title="編輯角色" width="660px" @close="resetEditForm">
|
||||
<el-form :model="editForm" label-width="150px">
|
||||
<el-form-item label="Role Key"><el-input :model-value="editForm.role_key" disabled /></el-form-item>
|
||||
<el-form-item label="系統">
|
||||
<el-select v-model="editForm.system_key" filterable style="width: 100%">
|
||||
<el-option
|
||||
v-for="system in systems"
|
||||
:key="system.system_key"
|
||||
:label="`${system.name} (${system.system_key})`"
|
||||
:value="system.system_key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色名稱"><el-input v-model="editForm.name" /></el-form-item>
|
||||
<el-form-item label="Keycloak Role"><el-input v-model="editForm.idp_role_name" /></el-form-item>
|
||||
<el-form-item label="描述"><el-input v-model="editForm.description" type="textarea" :rows="2" /></el-form-item>
|
||||
<el-form-item label="狀態">
|
||||
<el-select v-model="editForm.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="showEditDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleEdit">儲存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showSitesDialog" :title="`角色綁定站台:${selectedRoleLabel}`" width="980px">
|
||||
<el-table :data="roleSites" border stripe v-loading="sitesLoading">
|
||||
<template #empty><el-empty description="此角色尚未綁定站台" /></template>
|
||||
<el-table-column prop="company_display_name" label="公司" min-width="160" />
|
||||
<el-table-column prop="site_display_name" label="站台" min-width="170" />
|
||||
<el-table-column prop="site_key" label="Site Key" min-width="180" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="showSitesDialog = false">關閉</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { getRoles, createRole, updateRole, deleteRole, getRoleSites } from '@/api/roles'
|
||||
import { getSystems } from '@/api/systems'
|
||||
|
||||
const roles = ref([])
|
||||
const systems = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const errorMsg = ref('')
|
||||
|
||||
const showCreateDialog = ref(false)
|
||||
const showEditDialog = ref(false)
|
||||
const creating = ref(false)
|
||||
const saving = ref(false)
|
||||
const createFormRef = ref()
|
||||
|
||||
const createForm = ref({
|
||||
system_key: '',
|
||||
name: '',
|
||||
idp_role_name: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
})
|
||||
const editForm = ref({
|
||||
role_key: '',
|
||||
system_key: '',
|
||||
name: '',
|
||||
idp_role_name: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
})
|
||||
const rules = {
|
||||
system_key: [{ required: true, message: '請選擇系統', trigger: 'change' }],
|
||||
name: [{ required: true, message: '請輸入角色名稱', trigger: 'blur' }],
|
||||
idp_role_name: [{ required: true, message: '請輸入 Keycloak Role', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const showSitesDialog = ref(false)
|
||||
const selectedRoleLabel = ref('')
|
||||
const roleSites = ref([])
|
||||
const sitesLoading = ref(false)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
try {
|
||||
const [rolesRes, systemsRes] = await Promise.all([
|
||||
getRoles({ limit: 500, offset: 0 }),
|
||||
getSystems({ limit: 500, offset: 0 })
|
||||
])
|
||||
roles.value = rolesRes.data?.items || []
|
||||
systems.value = systemsRes.data?.items || []
|
||||
} catch (err) {
|
||||
error.value = true
|
||||
errorMsg.value = err.response?.data?.detail || '載入角色失敗'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetCreateForm() {
|
||||
createForm.value = {
|
||||
system_key: '',
|
||||
name: '',
|
||||
idp_role_name: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editForm.value = {
|
||||
role_key: row.role_key,
|
||||
system_key: row.system_key,
|
||||
name: row.name,
|
||||
idp_role_name: row.idp_role_name,
|
||||
description: row.description || '',
|
||||
status: row.status || 'active'
|
||||
}
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
function resetEditForm() {
|
||||
editForm.value = {
|
||||
role_key: '',
|
||||
system_key: '',
|
||||
name: '',
|
||||
idp_role_name: '',
|
||||
description: '',
|
||||
status: 'active'
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
const valid = await createFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
creating.value = true
|
||||
try {
|
||||
await createRole({
|
||||
system_key: createForm.value.system_key,
|
||||
name: createForm.value.name,
|
||||
idp_role_name: createForm.value.idp_role_name,
|
||||
description: createForm.value.description || null,
|
||||
status: createForm.value.status
|
||||
})
|
||||
ElMessage.success('新增角色成功')
|
||||
showCreateDialog.value = false
|
||||
resetCreateForm()
|
||||
await load()
|
||||
} catch (err) {
|
||||
ElMessage.error(err.response?.data?.detail || '新增角色失敗')
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
saving.value = true
|
||||
try {
|
||||
await updateRole(editForm.value.role_key, {
|
||||
system_key: editForm.value.system_key,
|
||||
name: editForm.value.name,
|
||||
idp_role_name: editForm.value.idp_role_name,
|
||||
description: editForm.value.description || null,
|
||||
status: editForm.value.status
|
||||
})
|
||||
ElMessage.success('更新成功')
|
||||
showEditDialog.value = false
|
||||
await load()
|
||||
} catch (err) {
|
||||
ElMessage.error(err.response?.data?.detail || '更新角色失敗')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`確認刪除角色 ${row.name}(${row.role_key})?`,
|
||||
'刪除確認',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
await deleteRole(row.role_key)
|
||||
ElMessage.success('刪除成功')
|
||||
await load()
|
||||
} catch (err) {
|
||||
if (err === 'cancel') return
|
||||
ElMessage.error(err.response?.data?.detail || '刪除角色失敗')
|
||||
}
|
||||
}
|
||||
|
||||
async function openSites(row) {
|
||||
selectedRoleLabel.value = `${row.system_name} / ${row.name}`
|
||||
showSitesDialog.value = true
|
||||
sitesLoading.value = true
|
||||
try {
|
||||
const res = await getRoleSites(row.role_key)
|
||||
roleSites.value = res.data?.sites || []
|
||||
} catch (_err) {
|
||||
ElMessage.error('載入角色站台失敗')
|
||||
roleSites.value = []
|
||||
} finally {
|
||||
sitesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800">站台管理</h2>
|
||||
<el-button type="primary" @click="showDialog = true" :icon="Plus">新增站台</el-button>
|
||||
<el-button type="primary" @click="showCreateDialog = true" :icon="Plus">新增站台</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="error" :title="errorMsg" type="error" show-icon :closable="false" class="mb-4" />
|
||||
@@ -11,43 +11,62 @@
|
||||
<el-table v-else :data="sites" stripe border class="w-full shadow-sm">
|
||||
<template #empty><el-empty description="目前無站台" /></template>
|
||||
<el-table-column prop="site_key" label="Site Key" width="180" />
|
||||
<el-table-column label="Company" min-width="220">
|
||||
<template #default="{ row }">{{ getCompanyName(row.company_key) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" label="名稱" min-width="180" />
|
||||
<el-table-column prop="status" label="狀態" width="120" />
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column prop="company_display_name" label="公司" min-width="180" />
|
||||
<el-table-column prop="display_name" label="站台名稱" min-width="180" />
|
||||
<el-table-column prop="domain" label="Domain" min-width="220" />
|
||||
<el-table-column prop="status" label="狀態" width="110" />
|
||||
<el-table-column label="操作" width="350">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">編輯</el-button>
|
||||
<el-button size="small" @click="openRoles(row)">角色</el-button>
|
||||
<el-button size="small" @click="openMembers(row)">會員</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="showDialog" title="新增站台" @close="resetForm">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="Company" prop="company_key">
|
||||
<el-select v-model="form.company_key" style="width: 100%" filterable>
|
||||
<el-option v-for="c in companies" :key="c.company_key" :label="`${c.name} (${c.company_key})`" :value="c.company_key" />
|
||||
<el-dialog v-model="showCreateDialog" title="新增站台" width="620px" @close="resetCreateForm">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="rules" label-width="120px">
|
||||
<el-form-item label="公司" prop="company_key">
|
||||
<el-select v-model="createForm.company_key" filterable style="width: 100%">
|
||||
<el-option
|
||||
v-for="company in companies"
|
||||
:key="company.company_key"
|
||||
:label="`${company.display_name} (${company.company_key})`"
|
||||
:value="company.company_key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="站台名稱" prop="display_name"><el-input v-model="createForm.display_name" /></el-form-item>
|
||||
<el-form-item label="Domain"><el-input v-model="createForm.domain" /></el-form-item>
|
||||
<el-form-item label="狀態">
|
||||
<el-select v-model="createForm.status" style="width: 100%">
|
||||
<el-option label="active" value="active" />
|
||||
<el-option label="inactive" value="inactive" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="名稱" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
||||
<el-button @click="showCreateDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="handleCreate">建立</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showEditDialog" title="編輯站台" @close="resetEditForm">
|
||||
<el-dialog v-model="showEditDialog" title="編輯站台" width="620px" @close="resetEditForm">
|
||||
<el-form :model="editForm" label-width="120px">
|
||||
<el-form-item label="Site Key"><el-input :model-value="editForm.site_key" disabled /></el-form-item>
|
||||
<el-form-item label="Company">
|
||||
<el-select v-model="editForm.company_key" style="width: 100%" filterable>
|
||||
<el-option v-for="c in companies" :key="c.company_key" :label="`${c.name} (${c.company_key})`" :value="c.company_key" />
|
||||
<el-form-item label="公司">
|
||||
<el-select v-model="editForm.company_key" filterable style="width: 100%">
|
||||
<el-option
|
||||
v-for="company in companies"
|
||||
:key="company.company_key"
|
||||
:label="`${company.display_name} (${company.company_key})`"
|
||||
:value="company.company_key"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="名稱"><el-input v-model="editForm.name" /></el-form-item>
|
||||
<el-form-item label="站台名稱"><el-input v-model="editForm.display_name" /></el-form-item>
|
||||
<el-form-item label="Domain"><el-input v-model="editForm.domain" /></el-form-item>
|
||||
<el-form-item label="狀態">
|
||||
<el-select v-model="editForm.status" style="width: 100%">
|
||||
<el-option label="active" value="active" />
|
||||
@@ -57,7 +76,53 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEditDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="handleEdit">儲存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showRolesDialog" :title="`站台角色:${selectedSiteLabel}`" width="1000px">
|
||||
<div class="mb-4">
|
||||
<el-select
|
||||
v-model="selectedRoleKeys"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
style="width: 100%"
|
||||
placeholder="可多選,儲存後覆蓋站台角色"
|
||||
>
|
||||
<el-option
|
||||
v-for="role in roleOptions"
|
||||
:key="role.role_key"
|
||||
:label="`${role.system_name} / ${role.name} (${role.role_key})`"
|
||||
:value="role.role_key"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
<el-table :data="siteRoles" border stripe v-loading="rolesLoading">
|
||||
<template #empty><el-empty description="目前無站台角色" /></template>
|
||||
<el-table-column prop="role_key" label="Role Key" width="200" />
|
||||
<el-table-column prop="system_name" label="系統" min-width="160" />
|
||||
<el-table-column prop="role_name" label="角色" min-width="180" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="showRolesDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="rolesSaving" @click="handleSaveSiteRoles">儲存角色</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showMembersDialog" :title="`站台會員:${selectedSiteLabel}`" width="920px">
|
||||
<el-table :data="siteMembers" border stripe v-loading="membersLoading">
|
||||
<template #empty><el-empty description="此站台目前沒有會員" /></template>
|
||||
<el-table-column prop="user_sub" label="User Sub" min-width="250" />
|
||||
<el-table-column prop="username" label="Username" min-width="150" />
|
||||
<el-table-column prop="email" label="Email" min-width="220" />
|
||||
<el-table-column prop="display_name" label="顯示名稱" min-width="170" />
|
||||
<el-table-column label="啟用" width="80">
|
||||
<template #default="{ row }">{{ row.is_active ? '是' : '否' }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="showMembersDialog = false">關閉</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -67,110 +132,123 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { getSites, createSite, updateSite, deleteSite } from '@/api/sites'
|
||||
import { getSites, createSite, updateSite, deleteSite, getSiteRoles, setSiteRoles, getSiteMembers } from '@/api/sites'
|
||||
import { getCompanies } from '@/api/companies'
|
||||
import { getRoles } from '@/api/roles'
|
||||
|
||||
const sites = ref([])
|
||||
const companies = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const showDialog = ref(false)
|
||||
const submitting = ref(false)
|
||||
const showEditDialog = ref(false)
|
||||
const savingEdit = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
const form = ref({ company_key: '', name: '' })
|
||||
const editForm = ref({ site_key: '', company_key: '', name: '', status: 'active' })
|
||||
const showCreateDialog = ref(false)
|
||||
const showEditDialog = ref(false)
|
||||
const creating = ref(false)
|
||||
const saving = ref(false)
|
||||
const createFormRef = ref()
|
||||
|
||||
const createForm = ref({ company_key: '', display_name: '', domain: '', status: 'active' })
|
||||
const editForm = ref({ site_key: '', company_key: '', display_name: '', domain: '', status: 'active' })
|
||||
const rules = {
|
||||
company_key: [{ required: true, message: '請選擇公司', trigger: 'change' }],
|
||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||
display_name: [{ required: true, message: '請輸入站台名稱', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
async function loadCompanies() {
|
||||
const res = await getCompanies()
|
||||
companies.value = res.data?.items || []
|
||||
}
|
||||
const showRolesDialog = ref(false)
|
||||
const rolesLoading = ref(false)
|
||||
const rolesSaving = ref(false)
|
||||
const selectedSiteKey = ref('')
|
||||
const selectedSiteLabel = ref('')
|
||||
const siteRoles = ref([])
|
||||
const roleOptions = ref([])
|
||||
const selectedRoleKeys = ref([])
|
||||
|
||||
const showMembersDialog = ref(false)
|
||||
const membersLoading = ref(false)
|
||||
const siteMembers = ref([])
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = false
|
||||
try {
|
||||
const [sitesRes] = await Promise.all([getSites(), loadCompanies()])
|
||||
const [sitesRes, companiesRes] = await Promise.all([getSites(), getCompanies()])
|
||||
sites.value = sitesRes.data?.items || []
|
||||
companies.value = companiesRes.data?.items || []
|
||||
} catch (err) {
|
||||
error.value = true
|
||||
errorMsg.value = err.response?.status === 422
|
||||
? '缺少管理員 API 認證,請檢查前端 .env.development'
|
||||
: '載入失敗,請稍後再試'
|
||||
errorMsg.value = err.response?.data?.detail || '載入站台失敗'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = { company_key: '', name: '' }
|
||||
function resetCreateForm() {
|
||||
createForm.value = { company_key: '', display_name: '', domain: '', status: 'active' }
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editForm.value = {
|
||||
site_key: row.site_key,
|
||||
company_key: row.company_key,
|
||||
name: row.name,
|
||||
display_name: row.display_name,
|
||||
domain: row.domain || '',
|
||||
status: row.status || 'active'
|
||||
}
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
function getCompanyName(companyKey) {
|
||||
const company = companies.value.find(c => c.company_key === companyKey)
|
||||
return company ? company.name : companyKey
|
||||
}
|
||||
|
||||
function resetEditForm() {
|
||||
editForm.value = { site_key: '', company_key: '', name: '', status: 'active' }
|
||||
editForm.value = { site_key: '', company_key: '', display_name: '', domain: '', status: 'active' }
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
const valid = await createFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
submitting.value = true
|
||||
creating.value = true
|
||||
try {
|
||||
const res = await createSite(form.value)
|
||||
ElMessage.success(`新增成功:${res.data?.site_key || ''}`)
|
||||
showDialog.value = false
|
||||
resetForm()
|
||||
const payload = {
|
||||
company_key: createForm.value.company_key,
|
||||
display_name: createForm.value.display_name,
|
||||
domain: createForm.value.domain || null,
|
||||
status: createForm.value.status
|
||||
}
|
||||
await createSite(payload)
|
||||
ElMessage.success('新增站台成功')
|
||||
showCreateDialog.value = false
|
||||
resetCreateForm()
|
||||
await load()
|
||||
} catch (err) {
|
||||
ElMessage.error('新增失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '新增站台失敗')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
savingEdit.value = true
|
||||
saving.value = true
|
||||
try {
|
||||
await updateSite(editForm.value.site_key, {
|
||||
const payload = {
|
||||
company_key: editForm.value.company_key,
|
||||
name: editForm.value.name,
|
||||
display_name: editForm.value.display_name,
|
||||
domain: editForm.value.domain || null,
|
||||
status: editForm.value.status
|
||||
})
|
||||
}
|
||||
await updateSite(editForm.value.site_key, payload)
|
||||
ElMessage.success('更新成功')
|
||||
showEditDialog.value = false
|
||||
await load()
|
||||
} catch (err) {
|
||||
ElMessage.error('更新失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '更新站台失敗')
|
||||
} finally {
|
||||
savingEdit.value = false
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`確認刪除站台 ${row.name}(${row.site_key})?此操作會清理相關授權。`,
|
||||
`確認刪除站台 ${row.display_name}(${row.site_key})?`,
|
||||
'刪除確認',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
@@ -179,7 +257,61 @@ async function handleDelete(row) {
|
||||
await load()
|
||||
} catch (err) {
|
||||
if (err === 'cancel') return
|
||||
ElMessage.error(err.response?.data?.detail || '刪除失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '刪除站台失敗')
|
||||
}
|
||||
}
|
||||
|
||||
async function openRoles(row) {
|
||||
selectedSiteKey.value = row.site_key
|
||||
selectedSiteLabel.value = `${row.company_display_name} / ${row.display_name}`
|
||||
showRolesDialog.value = true
|
||||
rolesLoading.value = true
|
||||
try {
|
||||
const [siteRolesRes, allRolesRes] = await Promise.all([
|
||||
getSiteRoles(row.site_key),
|
||||
getRoles({ limit: 500, offset: 0 })
|
||||
])
|
||||
siteRoles.value = siteRolesRes.data?.roles || []
|
||||
roleOptions.value = allRolesRes.data?.items || []
|
||||
selectedRoleKeys.value = siteRoles.value.map((role) => role.role_key)
|
||||
} catch (_err) {
|
||||
ElMessage.error('載入站台角色失敗')
|
||||
siteRoles.value = []
|
||||
roleOptions.value = []
|
||||
selectedRoleKeys.value = []
|
||||
} finally {
|
||||
rolesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveSiteRoles() {
|
||||
if (!selectedSiteKey.value) return
|
||||
rolesSaving.value = true
|
||||
try {
|
||||
const deduped = [...new Set(selectedRoleKeys.value)]
|
||||
const res = await setSiteRoles(selectedSiteKey.value, deduped)
|
||||
siteRoles.value = res.data?.roles || []
|
||||
selectedRoleKeys.value = siteRoles.value.map((role) => role.role_key)
|
||||
ElMessage.success('站台角色已更新')
|
||||
} catch (err) {
|
||||
ElMessage.error(err.response?.data?.detail || '儲存站台角色失敗')
|
||||
} finally {
|
||||
rolesSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openMembers(row) {
|
||||
selectedSiteLabel.value = `${row.company_display_name} / ${row.display_name}`
|
||||
showMembersDialog.value = true
|
||||
membersLoading.value = true
|
||||
try {
|
||||
const res = await getSiteMembers(row.site_key)
|
||||
siteMembers.value = res.data?.members || []
|
||||
} catch (_err) {
|
||||
ElMessage.error('載入站台會員失敗')
|
||||
siteMembers.value = []
|
||||
} finally {
|
||||
membersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,55 +2,49 @@
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-gray-800">系統管理</h2>
|
||||
<el-button type="primary" @click="showDialog = true" :icon="Plus">新增系統</el-button>
|
||||
<el-button type="primary" @click="showCreateDialog = true" :icon="Plus">新增系統</el-button>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
v-if="error"
|
||||
:title="errorMsg"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<el-alert v-if="error" :title="errorMsg" type="error" show-icon :closable="false" class="mb-4" />
|
||||
<el-skeleton v-if="loading" :rows="4" animated />
|
||||
|
||||
<el-table v-else :data="systems" stripe border class="w-full shadow-sm">
|
||||
<template #empty><el-empty description="目前無系統" /></template>
|
||||
<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="260">
|
||||
<el-table-column prop="name" label="系統名稱" min-width="180" />
|
||||
<el-table-column prop="idp_client_id" label="Keycloak Client ID" min-width="200" />
|
||||
<el-table-column prop="status" label="狀態" width="110" />
|
||||
<el-table-column label="操作" width="280">
|
||||
<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>
|
||||
<el-button size="small" @click="openRoles(row)">角色</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="showDialog" title="新增系統" @close="resetForm">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="名稱" prop="name">
|
||||
<el-input v-model="form.name" placeholder="行銷平台" />
|
||||
<el-dialog v-model="showCreateDialog" title="新增系統" width="620px" @close="resetCreateForm">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="rules" label-width="160px">
|
||||
<el-form-item label="系統名稱" prop="name"><el-input v-model="createForm.name" /></el-form-item>
|
||||
<el-form-item label="Keycloak Client ID" prop="idp_client_id"><el-input v-model="createForm.idp_client_id" /></el-form-item>
|
||||
<el-form-item label="狀態">
|
||||
<el-select v-model="createForm.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="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
||||
<el-button @click="showCreateDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="handleCreate">建立</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="showEditDialog" title="編輯系統" @close="resetEditForm">
|
||||
<el-form :model="editForm" label-width="100px">
|
||||
<el-form-item label="System Key">
|
||||
<el-input :model-value="editForm.system_key" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="名稱">
|
||||
<el-input v-model="editForm.name" />
|
||||
</el-form-item>
|
||||
<el-dialog v-model="showEditDialog" title="編輯系統" width="620px" @close="resetEditForm">
|
||||
<el-form :model="editForm" label-width="160px">
|
||||
<el-form-item label="System Key"><el-input :model-value="editForm.system_key" disabled /></el-form-item>
|
||||
<el-form-item label="系統名稱"><el-input v-model="editForm.name" /></el-form-item>
|
||||
<el-form-item label="Keycloak Client ID"><el-input v-model="editForm.idp_client_id" /></el-form-item>
|
||||
<el-form-item label="狀態">
|
||||
<el-select v-model="editForm.status" style="width: 100%">
|
||||
<el-option label="active" value="active" />
|
||||
@@ -60,34 +54,20 @@
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showEditDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="savingEdit" @click="handleEdit">儲存</el-button>
|
||||
<el-button type="primary" :loading="saving" @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="user_sub" label="User 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>
|
||||
<el-dialog v-model="showRolesDialog" :title="`系統角色:${selectedSystemLabel}`" width="980px">
|
||||
<el-table :data="systemRoles" border stripe v-loading="rolesLoading">
|
||||
<template #empty><el-empty description="此系統目前沒有角色" /></template>
|
||||
<el-table-column prop="role_key" label="Role Key" width="200" />
|
||||
<el-table-column prop="name" label="角色名稱" min-width="200" />
|
||||
<el-table-column prop="idp_role_name" label="Keycloak Role" min-width="210" />
|
||||
<el-table-column prop="status" label="狀態" width="110" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<el-button @click="showRelationDialog = false">關閉</el-button>
|
||||
<el-button @click="showRolesDialog = false">關閉</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
@@ -97,30 +77,30 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { getSystems, createSystem, updateSystem, deleteSystem, getSystemGroups, getSystemMembers } from '@/api/systems'
|
||||
import { getSystems, createSystem, updateSystem, deleteSystem, getSystemRoles } from '@/api/systems'
|
||||
|
||||
const systems = ref([])
|
||||
const loading = ref(false)
|
||||
const error = ref(false)
|
||||
const errorMsg = ref('')
|
||||
const showDialog = ref(false)
|
||||
const submitting = ref(false)
|
||||
const formRef = ref()
|
||||
const showEditDialog = ref(false)
|
||||
const savingEdit = ref(false)
|
||||
|
||||
const form = ref({ name: '' })
|
||||
const editForm = ref({ system_key: '', name: '', status: 'active' })
|
||||
const showCreateDialog = ref(false)
|
||||
const showEditDialog = ref(false)
|
||||
const creating = ref(false)
|
||||
const saving = ref(false)
|
||||
const createFormRef = ref()
|
||||
|
||||
const createForm = ref({ name: '', idp_client_id: '', status: 'active' })
|
||||
const editForm = ref({ system_key: '', name: '', idp_client_id: '', status: 'active' })
|
||||
const rules = {
|
||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||
name: [{ required: true, message: '請輸入系統名稱', trigger: 'blur' }],
|
||||
idp_client_id: [{ required: true, message: '請輸入 Keycloak Client ID', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const showRelationDialog = ref(false)
|
||||
const relationLoading = ref(false)
|
||||
const relationSystemKey = ref('')
|
||||
const relationTab = ref('groups')
|
||||
const relationGroups = ref([])
|
||||
const relationMembers = ref([])
|
||||
const showRolesDialog = ref(false)
|
||||
const selectedSystemLabel = ref('')
|
||||
const systemRoles = ref([])
|
||||
const rolesLoading = ref(false)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
@@ -130,93 +110,93 @@ async function load() {
|
||||
systems.value = res.data?.items || []
|
||||
} catch (err) {
|
||||
error.value = true
|
||||
errorMsg.value = err.response?.status === 422
|
||||
? '缺少管理員 API 認證,請檢查前端 .env.development'
|
||||
: '載入失敗,請稍後再試'
|
||||
errorMsg.value = err.response?.data?.detail || '載入系統失敗'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
form.value = { name: '' }
|
||||
function resetCreateForm() {
|
||||
createForm.value = { name: '', idp_client_id: '', status: 'active' }
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editForm.value = {
|
||||
system_key: row.system_key,
|
||||
name: row.name,
|
||||
idp_client_id: row.idp_client_id,
|
||||
status: row.status || 'active'
|
||||
}
|
||||
showEditDialog.value = true
|
||||
}
|
||||
|
||||
function resetEditForm() {
|
||||
editForm.value = { system_key: '', name: '', status: 'active' }
|
||||
editForm.value = { system_key: '', name: '', idp_client_id: '', status: 'active' }
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
const valid = await formRef.value.validate().catch(() => false)
|
||||
const valid = await createFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
submitting.value = true
|
||||
creating.value = true
|
||||
try {
|
||||
const res = await createSystem(form.value)
|
||||
ElMessage.success(`新增成功:${res.data?.system_key || ''}`)
|
||||
showDialog.value = false
|
||||
resetForm()
|
||||
await createSystem(createForm.value)
|
||||
ElMessage.success('新增系統成功')
|
||||
showCreateDialog.value = false
|
||||
resetCreateForm()
|
||||
await load()
|
||||
} catch (err) {
|
||||
ElMessage.error('新增失敗,請稍後再試')
|
||||
ElMessage.error(err.response?.data?.detail || '新增系統失敗')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEdit() {
|
||||
savingEdit.value = true
|
||||
saving.value = true
|
||||
try {
|
||||
await updateSystem(editForm.value.system_key, {
|
||||
name: editForm.value.name,
|
||||
idp_client_id: editForm.value.idp_client_id,
|
||||
status: editForm.value.status
|
||||
})
|
||||
ElMessage.success('更新成功')
|
||||
showEditDialog.value = false
|
||||
await load()
|
||||
} catch (err) {
|
||||
ElMessage.error('更新失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '更新系統失敗')
|
||||
} finally {
|
||||
savingEdit.value = false
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`確認刪除系統 ${row.name}(${row.system_key})?`, '刪除確認', { type: 'warning' })
|
||||
await ElMessageBox.confirm(
|
||||
`確認刪除系統 ${row.name}(${row.system_key})?`,
|
||||
'刪除確認',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
await deleteSystem(row.system_key)
|
||||
ElMessage.success('刪除成功')
|
||||
await load()
|
||||
} catch (err) {
|
||||
if (err === 'cancel') return
|
||||
ElMessage.error(err.response?.data?.detail || '刪除失敗')
|
||||
ElMessage.error(err.response?.data?.detail || '刪除系統失敗')
|
||||
}
|
||||
}
|
||||
|
||||
async function openRelations(row, tab) {
|
||||
relationSystemKey.value = row.system_key
|
||||
relationTab.value = tab
|
||||
showRelationDialog.value = true
|
||||
relationLoading.value = true
|
||||
async function openRoles(row) {
|
||||
selectedSystemLabel.value = `${row.name} (${row.system_key})`
|
||||
showRolesDialog.value = true
|
||||
rolesLoading.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('載入系統關聯資料失敗')
|
||||
const res = await getSystemRoles(row.system_key)
|
||||
systemRoles.value = res.data?.roles || []
|
||||
} catch (_err) {
|
||||
ElMessage.error('載入系統角色失敗')
|
||||
systemRoles.value = []
|
||||
} finally {
|
||||
relationLoading.value = false
|
||||
rolesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,439 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-6">權限管理</h2>
|
||||
|
||||
<!-- Grant / Revoke -->
|
||||
<el-tabs v-model="activeTab" type="border-card" class="shadow-sm">
|
||||
<!-- Grant Tab -->
|
||||
<el-tab-pane label="Grant 授權" name="grant">
|
||||
<el-form
|
||||
ref="grantFormRef"
|
||||
:model="grantForm"
|
||||
:rules="grantRules"
|
||||
label-width="130px"
|
||||
class="max-w-xl mt-4"
|
||||
@submit.prevent="handleGrant"
|
||||
>
|
||||
<el-form-item label="User Sub" prop="user_sub">
|
||||
<el-select v-model="grantForm.user_sub" filterable allow-create default-first-option placeholder="選擇會員或輸入 sub" style="width: 100%">
|
||||
<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="Email" prop="email">
|
||||
<el-input v-model="grantForm.email" placeholder="user@example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="顯示名稱" prop="display_name">
|
||||
<el-input v-model="grantForm.display_name" placeholder="User Name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Scope 類型" prop="scope_type">
|
||||
<el-select v-model="grantForm.scope_type" placeholder="選擇 Scope 類型">
|
||||
<el-option label="Site" value="site" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Scope ID" prop="scope_id">
|
||||
<el-select v-model="grantForm.scope_id" placeholder="選擇 Scope ID" filterable style="width: 100%">
|
||||
<el-option v-for="s in grantScopeOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="系統" prop="system">
|
||||
<el-select v-model="grantForm.system" placeholder="選擇系統" filterable style="width: 100%">
|
||||
<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="模組(選填)" prop="module">
|
||||
<el-select v-model="grantForm.module" placeholder="系統層(留空) 或選模組" clearable filterable style="width: 100%">
|
||||
<el-option v-for="m in grantModuleOptions" :key="m.value" :label="m.label" :value="m.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作" prop="action">
|
||||
<el-select v-model="grantForm.action" filterable allow-create default-first-option style="width: 100%">
|
||||
<el-option v-for="a in actionOptions" :key="a" :label="a" :value="a" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-alert
|
||||
v-if="grantError"
|
||||
:title="grantError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="grantSuccess"
|
||||
:title="grantSuccess"
|
||||
type="success"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="submit"
|
||||
:loading="grantLoading"
|
||||
>
|
||||
Grant 授權
|
||||
</el-button>
|
||||
<el-button @click="resetGrant">清除</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- Revoke Tab -->
|
||||
<el-tab-pane label="Revoke 撤銷" name="revoke">
|
||||
<el-form
|
||||
ref="revokeFormRef"
|
||||
:model="revokeForm"
|
||||
:rules="revokeRules"
|
||||
label-width="130px"
|
||||
class="max-w-xl mt-4"
|
||||
@submit.prevent="handleRevoke"
|
||||
>
|
||||
<el-form-item label="User Sub" prop="user_sub">
|
||||
<el-select v-model="revokeForm.user_sub" filterable allow-create default-first-option placeholder="選擇會員或輸入 sub" style="width: 100%">
|
||||
<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="Scope 類型" prop="scope_type">
|
||||
<el-select v-model="revokeForm.scope_type" placeholder="選擇 Scope 類型">
|
||||
<el-option label="Site" value="site" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="Scope ID" prop="scope_id">
|
||||
<el-select v-model="revokeForm.scope_id" placeholder="選擇 Scope ID" filterable style="width: 100%">
|
||||
<el-option v-for="s in revokeScopeOptions" :key="s.value" :label="s.label" :value="s.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="系統" prop="system">
|
||||
<el-select v-model="revokeForm.system" placeholder="選擇系統" filterable style="width: 100%">
|
||||
<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="模組(選填)" prop="module">
|
||||
<el-select v-model="revokeForm.module" placeholder="系統層(留空) 或選模組" clearable filterable style="width: 100%">
|
||||
<el-option v-for="m in revokeModuleOptions" :key="m.value" :label="m.label" :value="m.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作" prop="action">
|
||||
<el-select v-model="revokeForm.action" filterable allow-create default-first-option style="width: 100%">
|
||||
<el-option v-for="a in actionOptions" :key="a" :label="a" :value="a" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-alert
|
||||
v-if="revokeError"
|
||||
:title="revokeError"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
<el-alert
|
||||
v-if="revokeSuccess"
|
||||
:title="revokeSuccess"
|
||||
type="success"
|
||||
show-icon
|
||||
:closable="false"
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="danger"
|
||||
native-type="submit"
|
||||
:loading="revokeLoading"
|
||||
>
|
||||
Revoke 撤銷
|
||||
</el-button>
|
||||
<el-button @click="resetRevoke">清除</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-card class="mt-6 shadow-sm">
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="font-medium text-gray-700">已授權列表(直接授權)</span>
|
||||
<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="Site" value="site" />
|
||||
</el-select>
|
||||
<el-button :loading="listLoading" @click="loadDirectPermissionList">查詢</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table :data="directPermissions" stripe border class="w-full" v-loading="listLoading">
|
||||
<template #empty><el-empty description="目前沒有直接授權資料" /></template>
|
||||
<el-table-column prop="display_name" label="名稱" min-width="140" />
|
||||
<el-table-column prop="email" label="Email" min-width="200" />
|
||||
<el-table-column prop="user_sub" label="Sub" min-width="200" />
|
||||
<el-table-column prop="scope_type" label="Scope" width="90" />
|
||||
<el-table-column prop="scope_id" label="Scope ID" min-width="120" />
|
||||
<el-table-column prop="system" label="系統" width="100" />
|
||||
<el-table-column prop="module" label="模組" width="130" />
|
||||
<el-table-column prop="action" label="操作" width="100" />
|
||||
<el-table-column prop="created_at" label="建立時間" min-width="180" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="danger" size="small" @click="handleRevokeByRow(row)" :loading="revokeRowLoadingId === row.permission_id">撤銷</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { usePermissionStore } from '@/stores/permission'
|
||||
import { getSystems } from '@/api/systems'
|
||||
import { getModules } from '@/api/modules'
|
||||
import { getCompanies } from '@/api/companies'
|
||||
import { getSites } from '@/api/sites'
|
||||
import { getMembers } from '@/api/members'
|
||||
import { listDirectPermissions, revokeDirectPermissionById } from '@/api/permission-admin'
|
||||
|
||||
const permissionStore = usePermissionStore()
|
||||
|
||||
const activeTab = ref('grant')
|
||||
const systems = ref([])
|
||||
const modules = ref([])
|
||||
const companies = ref([])
|
||||
const sites = ref([])
|
||||
const members = ref([])
|
||||
const actionOptions = ['view', 'edit']
|
||||
const listFilters = reactive({ keyword: '', scope_type: '' })
|
||||
const listLoading = ref(false)
|
||||
const directPermissions = ref([])
|
||||
const revokeRowLoadingId = ref('')
|
||||
|
||||
// Grant
|
||||
const grantFormRef = ref()
|
||||
const grantLoading = ref(false)
|
||||
const grantError = ref('')
|
||||
const grantSuccess = ref('')
|
||||
|
||||
const grantForm = reactive({
|
||||
user_sub: '',
|
||||
email: '',
|
||||
display_name: '',
|
||||
scope_type: '',
|
||||
scope_id: '',
|
||||
system: '',
|
||||
module: '',
|
||||
action: ''
|
||||
})
|
||||
|
||||
const grantModuleOptions = computed(() => {
|
||||
if (!grantForm.system) return []
|
||||
return modules.value
|
||||
.filter(m => m.system_key === grantForm.system && !m.module_key.endsWith('.__system__'))
|
||||
.map(m => ({
|
||||
value: m.module_key.startsWith(`${grantForm.system}.`)
|
||||
? m.module_key.slice(grantForm.system.length + 1)
|
||||
: m.module_key,
|
||||
label: `${m.name} (${m.module_key})`
|
||||
}))
|
||||
})
|
||||
|
||||
const grantScopeOptions = computed(() => {
|
||||
if (grantForm.scope_type === 'site') {
|
||||
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const required = { required: true, message: '必填', trigger: 'blur' }
|
||||
const grantRules = {
|
||||
user_sub: [required],
|
||||
email: [required],
|
||||
display_name: [required],
|
||||
scope_type: [required],
|
||||
scope_id: [required],
|
||||
system: [required],
|
||||
action: [required]
|
||||
}
|
||||
|
||||
async function handleGrant() {
|
||||
const valid = await grantFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
grantLoading.value = true
|
||||
grantError.value = ''
|
||||
grantSuccess.value = ''
|
||||
try {
|
||||
const result = await permissionStore.grant({ ...grantForm })
|
||||
grantSuccess.value = `授權成功(ID: ${result.permission_id})`
|
||||
ElMessage.success('Grant 成功')
|
||||
await loadDirectPermissionList()
|
||||
} catch (err) {
|
||||
grantError.value = formatAdminError(err)
|
||||
} finally {
|
||||
grantLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetGrant() {
|
||||
grantFormRef.value?.resetFields()
|
||||
grantError.value = ''
|
||||
grantSuccess.value = ''
|
||||
}
|
||||
|
||||
// Revoke
|
||||
const revokeFormRef = ref()
|
||||
const revokeLoading = ref(false)
|
||||
const revokeError = ref('')
|
||||
const revokeSuccess = ref('')
|
||||
|
||||
const revokeForm = reactive({
|
||||
user_sub: '',
|
||||
scope_type: '',
|
||||
scope_id: '',
|
||||
system: '',
|
||||
module: '',
|
||||
action: ''
|
||||
})
|
||||
|
||||
const revokeModuleOptions = computed(() => {
|
||||
if (!revokeForm.system) return []
|
||||
return modules.value
|
||||
.filter(m => m.system_key === revokeForm.system && !m.module_key.endsWith('.__system__'))
|
||||
.map(m => ({
|
||||
value: m.module_key.startsWith(`${revokeForm.system}.`)
|
||||
? m.module_key.slice(revokeForm.system.length + 1)
|
||||
: m.module_key,
|
||||
label: `${m.name} (${m.module_key})`
|
||||
}))
|
||||
})
|
||||
|
||||
const revokeScopeOptions = computed(() => {
|
||||
if (revokeForm.scope_type === 'site') {
|
||||
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const revokeRules = {
|
||||
user_sub: [required],
|
||||
scope_type: [required],
|
||||
scope_id: [required],
|
||||
system: [required],
|
||||
action: [required]
|
||||
}
|
||||
|
||||
async function handleRevoke() {
|
||||
const valid = await revokeFormRef.value.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
revokeLoading.value = true
|
||||
revokeError.value = ''
|
||||
revokeSuccess.value = ''
|
||||
try {
|
||||
const result = await permissionStore.revoke({ ...revokeForm })
|
||||
revokeSuccess.value = `撤銷成功(共刪除 ${result.deleted} 筆)`
|
||||
ElMessage.success('Revoke 成功')
|
||||
await loadDirectPermissionList()
|
||||
} catch (err) {
|
||||
revokeError.value = formatAdminError(err)
|
||||
} finally {
|
||||
revokeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetRevoke() {
|
||||
revokeFormRef.value?.resetFields()
|
||||
revokeError.value = ''
|
||||
revokeSuccess.value = ''
|
||||
}
|
||||
|
||||
function formatAdminError(err) {
|
||||
const status = err.response?.status
|
||||
const detail = err.response?.data?.detail
|
||||
const map = {
|
||||
invalid_client: '無效的 Client Key',
|
||||
invalid_api_key: '無效的 API Key',
|
||||
client_expired: 'Client 已過期',
|
||||
origin_not_allowed: '來源 Origin 不允許',
|
||||
ip_not_allowed: 'IP 不在白名單',
|
||||
path_not_allowed: '路徑不允許',
|
||||
internal_secret_not_configured: '後端設定缺失(internal secret)',
|
||||
idp_admin_not_configured: '後端設定缺失(idp admin)',
|
||||
user_not_found: '找不到該使用者'
|
||||
}
|
||||
if (detail && map[detail]) return map[detail]
|
||||
if (detail) return `錯誤:${detail}`
|
||||
if (status === 401) return '認證失敗,請檢查 Client Key / API Key'
|
||||
if (status === 403) return '存取被拒(IP 或 Origin 限制)'
|
||||
if (status === 404) return '找不到該使用者'
|
||||
if (status === 503) return '後端設定不完整,請聯絡管理員'
|
||||
return '操作失敗,請稍後再試'
|
||||
}
|
||||
|
||||
async function loadCatalogs() {
|
||||
const [systemsRes, modulesRes, companiesRes, sitesRes, membersRes] = await Promise.all([
|
||||
getSystems(),
|
||||
getModules(),
|
||||
getCompanies(),
|
||||
getSites(),
|
||||
getMembers()
|
||||
])
|
||||
systems.value = systemsRes.data?.items || []
|
||||
modules.value = modulesRes.data?.items || []
|
||||
companies.value = companiesRes.data?.items || []
|
||||
sites.value = sitesRes.data?.items || []
|
||||
members.value = membersRes.data?.items || []
|
||||
}
|
||||
|
||||
async function loadDirectPermissionList() {
|
||||
listLoading.value = true
|
||||
try {
|
||||
const res = await listDirectPermissions({
|
||||
keyword: listFilters.keyword || undefined,
|
||||
scope_type: listFilters.scope_type || undefined,
|
||||
limit: 200,
|
||||
offset: 0
|
||||
})
|
||||
directPermissions.value = (res.data?.items || []).map(row => ({
|
||||
...row,
|
||||
created_at: row.created_at ? new Date(row.created_at).toLocaleString() : ''
|
||||
}))
|
||||
} catch (err) {
|
||||
ElMessage.error('載入權限列表失敗')
|
||||
} finally {
|
||||
listLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRevokeByRow(row) {
|
||||
revokeRowLoadingId.value = row.permission_id
|
||||
try {
|
||||
await revokeDirectPermissionById(row.permission_id)
|
||||
ElMessage.success('已撤銷該筆授權')
|
||||
await loadDirectPermissionList()
|
||||
} catch (err) {
|
||||
ElMessage.error('撤銷失敗')
|
||||
} finally {
|
||||
revokeRowLoadingId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => grantForm.scope_type, () => { grantForm.scope_id = '' })
|
||||
watch(() => grantForm.system, () => { grantForm.module = '' })
|
||||
watch(() => revokeForm.scope_type, () => { revokeForm.scope_id = '' })
|
||||
watch(() => revokeForm.system, () => { revokeForm.module = '' })
|
||||
|
||||
watch(() => grantForm.user_sub, (sub) => {
|
||||
const user = members.value.find(m => m.user_sub === sub)
|
||||
if (!user) return
|
||||
grantForm.email = user.email || ''
|
||||
grantForm.display_name = user.display_name || ''
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadCatalogs(), loadDirectPermissionList()])
|
||||
})
|
||||
</script>
|
||||
@@ -21,23 +21,20 @@
|
||||
Sub:<span class="font-mono">{{ snapshot.user_sub }}</span>
|
||||
</p>
|
||||
|
||||
<el-empty
|
||||
v-if="snapshot.permissions.length === 0"
|
||||
description="目前沒有任何權限"
|
||||
/>
|
||||
<el-empty v-if="snapshot.roles.length === 0" description="目前沒有任何角色" />
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
:data="snapshot.permissions"
|
||||
:data="snapshot.roles"
|
||||
stripe
|
||||
border
|
||||
class="w-full shadow-sm"
|
||||
>
|
||||
<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="120" />
|
||||
<el-table-column prop="action" label="操作" width="100" />
|
||||
<el-table-column prop="company_display_name" label="公司" min-width="150" />
|
||||
<el-table-column prop="site_display_name" label="站台" min-width="160" />
|
||||
<el-table-column prop="system_name" label="系統" min-width="150" />
|
||||
<el-table-column prop="role_name" label="角色" min-width="160" />
|
||||
<el-table-column prop="idp_role_name" label="Keycloak Role" min-width="180" />
|
||||
</el-table>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -25,24 +25,6 @@ const routes = [
|
||||
component: () => import('@/pages/permissions/PermissionSnapshotPage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/permissions',
|
||||
name: 'admin-permissions',
|
||||
component: () => import('@/pages/permissions/PermissionAdminPage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/systems',
|
||||
name: 'admin-systems',
|
||||
component: () => import('@/pages/admin/SystemsPage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/modules',
|
||||
name: 'admin-modules',
|
||||
component: () => import('@/pages/admin/ModulesPage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/companies',
|
||||
name: 'admin-companies',
|
||||
@@ -56,15 +38,21 @@ const routes = [
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/members',
|
||||
name: 'admin-members',
|
||||
component: () => import('@/pages/admin/MembersPage.vue'),
|
||||
path: '/admin/systems',
|
||||
name: 'admin-systems',
|
||||
component: () => import('@/pages/admin/SystemsPage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/permission-groups',
|
||||
name: 'admin-permission-groups',
|
||||
component: () => import('@/pages/admin/PermissionGroupsPage.vue'),
|
||||
path: '/admin/roles',
|
||||
name: 'admin-roles',
|
||||
component: () => import('@/pages/admin/RolesPage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/members',
|
||||
name: 'admin-members',
|
||||
component: () => import('@/pages/admin/MembersPage.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getSystems } from '@/api/systems'
|
||||
import { getModules } from '@/api/modules'
|
||||
import { getCompanies } from '@/api/companies'
|
||||
import { getSites } from '@/api/sites'
|
||||
|
||||
export const useAdminStore = defineStore('admin', () => {
|
||||
const systems = ref([])
|
||||
const modules = ref([])
|
||||
const companies = ref([])
|
||||
const sites = ref([])
|
||||
|
||||
async function loadAllData() {
|
||||
try {
|
||||
const [sysRes, modRes, comRes, siteRes] = await Promise.all([
|
||||
getSystems(),
|
||||
getModules(),
|
||||
getCompanies(),
|
||||
getSites()
|
||||
])
|
||||
systems.value = sysRes.data || []
|
||||
modules.value = modRes.data || []
|
||||
companies.value = comRes.data || []
|
||||
sites.value = siteRes.data || []
|
||||
} catch (err) {
|
||||
console.error('Error loading admin data:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
systems,
|
||||
modules,
|
||||
companies,
|
||||
sites,
|
||||
loadAllData
|
||||
}
|
||||
})
|
||||
@@ -1,7 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getMyPermissionSnapshot } from '@/api/me'
|
||||
import { grantPermission, revokePermission } from '@/api/permission-admin'
|
||||
|
||||
export const usePermissionStore = defineStore('permission', () => {
|
||||
const snapshot = ref(null)
|
||||
@@ -12,20 +11,8 @@ export const usePermissionStore = defineStore('permission', () => {
|
||||
return res.data
|
||||
}
|
||||
|
||||
async function grant(data) {
|
||||
const res = await grantPermission(data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
async function revoke(data) {
|
||||
const res = await revokePermission(data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
return {
|
||||
snapshot,
|
||||
fetchMySnapshot,
|
||||
grant,
|
||||
revoke
|
||||
fetchMySnapshot
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user