feat(frontend): migrate admin UI to role-site model and clean legacy pages

This commit is contained in:
Chris
2026-04-03 00:18:39 +08:00
parent bcf4a43769
commit c9d05531f8
22 changed files with 789 additions and 1489 deletions

View File

@@ -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 }) {

View File

@@ -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}`)

View File

@@ -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`)

View File

@@ -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`)

View File

@@ -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}`)

View File

@@ -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
View 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`)

View File

@@ -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`)

View File

@@ -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`)

View File

@@ -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')

View File

@@ -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 || '刪除公司失敗')
}
}

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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 固定為 siteaction 只允許 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>

View 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>

View File

@@ -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
}
}

View File

@@ -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-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>
</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>
<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
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 }
},
{

View File

@@ -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
}
})

View File

@@ -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
}
})