From c9d05531f8b8cab5605fc860aac07a83c21aef97 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 3 Apr 2026 00:18:39 +0800 Subject: [PATCH] feat(frontend): migrate admin UI to role-site model and clean legacy pages --- src/App.vue | 15 +- src/api/companies.js | 2 +- src/api/members.js | 12 +- src/api/modules.js | 8 - src/api/permission-admin.js | 6 - src/api/permission-groups.js | 22 - src/api/roles.js | 7 + src/api/sites.js | 5 +- src/api/systems.js | 5 +- src/pages/AuthCallbackPage.vue | 4 +- src/pages/admin/CompaniesPage.vue | 119 +++-- src/pages/admin/MembersPage.vue | 206 +++++--- src/pages/admin/ModulesPage.vue | 241 ---------- src/pages/admin/PermissionGroupsPage.vue | 371 --------------- src/pages/admin/RolesPage.vue | 269 +++++++++++ src/pages/admin/SitesPage.vue | 258 +++++++--- src/pages/admin/SystemsPage.vue | 182 ++++---- src/pages/permissions/PermissionAdminPage.vue | 439 ------------------ .../permissions/PermissionSnapshotPage.vue | 17 +- src/router/index.js | 36 +- src/stores/admin.js | 39 -- src/stores/permission.js | 15 +- 22 files changed, 789 insertions(+), 1489 deletions(-) delete mode 100644 src/api/modules.js delete mode 100644 src/api/permission-admin.js delete mode 100644 src/api/permission-groups.js create mode 100644 src/api/roles.js delete mode 100644 src/pages/admin/ModulesPage.vue delete mode 100644 src/pages/admin/PermissionGroupsPage.vue create mode 100644 src/pages/admin/RolesPage.vue delete mode 100644 src/pages/permissions/PermissionAdminPage.vue delete mode 100644 src/stores/admin.js diff --git a/src/App.vue b/src/App.vue index 04b74c2..4a038d1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,22 +2,16 @@
- -
member.ose.tw
- -
-
@@ -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 }) { diff --git a/src/api/companies.js b/src/api/companies.js index a5d7268..5db9bd3 100644 --- a/src/api/companies.js +++ b/src/api/companies.js @@ -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}`) diff --git a/src/api/members.js b/src/api/members.js index 06e426d..615853d 100644 --- a/src/api/members.js +++ b/src/api/members.js @@ -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`) diff --git a/src/api/modules.js b/src/api/modules.js deleted file mode 100644 index 44ee34d..0000000 --- a/src/api/modules.js +++ /dev/null @@ -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`) diff --git a/src/api/permission-admin.js b/src/api/permission-admin.js deleted file mode 100644 index cc4d23f..0000000 --- a/src/api/permission-admin.js +++ /dev/null @@ -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}`) diff --git a/src/api/permission-groups.js b/src/api/permission-groups.js deleted file mode 100644 index ecb075e..0000000 --- a/src/api/permission-groups.js +++ /dev/null @@ -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) diff --git a/src/api/roles.js b/src/api/roles.js new file mode 100644 index 0000000..a4c713c --- /dev/null +++ b/src/api/roles.js @@ -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`) diff --git a/src/api/sites.js b/src/api/sites.js index 9556165..cda7309 100644 --- a/src/api/sites.js +++ b/src/api/sites.js @@ -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`) diff --git a/src/api/systems.js b/src/api/systems.js index 486a96f..653a561 100644 --- a/src/api/systems.js +++ b/src/api/systems.js @@ -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`) diff --git a/src/pages/AuthCallbackPage.vue b/src/pages/AuthCallbackPage.vue index ca07e7c..7a10c8b 100644 --- a/src/pages/AuthCallbackPage.vue +++ b/src/pages/AuthCallbackPage.vue @@ -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') diff --git a/src/pages/admin/CompaniesPage.vue b/src/pages/admin/CompaniesPage.vue index faa91b4..0b8661f 100644 --- a/src/pages/admin/CompaniesPage.vue +++ b/src/pages/admin/CompaniesPage.vue @@ -2,7 +2,7 @@

公司管理

- 新增公司 + 新增公司
@@ -11,31 +11,40 @@ - - + + + - - - + + + + + + + + + + - - + + - + + @@ -45,16 +54,17 @@ - + - - - + + + + - - + + - - - + + + @@ -67,6 +78,20 @@ 儲存 + + + + + + + + + + + +
@@ -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 } } diff --git a/src/pages/admin/ModulesPage.vue b/src/pages/admin/ModulesPage.vue deleted file mode 100644 index 05426f9..0000000 --- a/src/pages/admin/ModulesPage.vue +++ /dev/null @@ -1,241 +0,0 @@ - - - diff --git a/src/pages/admin/PermissionGroupsPage.vue b/src/pages/admin/PermissionGroupsPage.vue deleted file mode 100644 index f8525db..0000000 --- a/src/pages/admin/PermissionGroupsPage.vue +++ /dev/null @@ -1,371 +0,0 @@ - - - diff --git a/src/pages/admin/RolesPage.vue b/src/pages/admin/RolesPage.vue new file mode 100644 index 0000000..4cfe50b --- /dev/null +++ b/src/pages/admin/RolesPage.vue @@ -0,0 +1,269 @@ + + + diff --git a/src/pages/admin/SitesPage.vue b/src/pages/admin/SitesPage.vue index 1eb39ae..87fa5f1 100644 --- a/src/pages/admin/SitesPage.vue +++ b/src/pages/admin/SitesPage.vue @@ -2,7 +2,7 @@

站台管理

- 新增站台 + 新增站台
@@ -11,43 +11,62 @@ - - - - - - + + + + + - - - - - + + + + + + + + + + + + + - - + - - - + + + - + + @@ -57,7 +76,53 @@ + + + +
+ + + +
+ + + + + + + +
+ + + + + + + + + + + + +
@@ -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 } } diff --git a/src/pages/admin/SystemsPage.vue b/src/pages/admin/SystemsPage.vue index f5d7fd9..ad7c8eb 100644 --- a/src/pages/admin/SystemsPage.vue +++ b/src/pages/admin/SystemsPage.vue @@ -2,55 +2,49 @@

系統管理

- 新增系統 + 新增系統
- - + - - - + + + + - - - - + + + + + + + + + - - - - - - - - + + + + + @@ -60,34 +54,20 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + +
@@ -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 } } diff --git a/src/pages/permissions/PermissionAdminPage.vue b/src/pages/permissions/PermissionAdminPage.vue deleted file mode 100644 index b2f32e8..0000000 --- a/src/pages/permissions/PermissionAdminPage.vue +++ /dev/null @@ -1,439 +0,0 @@ - - - diff --git a/src/pages/permissions/PermissionSnapshotPage.vue b/src/pages/permissions/PermissionSnapshotPage.vue index 69b543a..5a3b1af 100644 --- a/src/pages/permissions/PermissionSnapshotPage.vue +++ b/src/pages/permissions/PermissionSnapshotPage.vue @@ -21,23 +21,20 @@ Sub:{{ snapshot.user_sub }}

- + - - - - - + + + + +
diff --git a/src/router/index.js b/src/router/index.js index b776b21..621d25e 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -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 } }, { diff --git a/src/stores/admin.js b/src/stores/admin.js deleted file mode 100644 index f802f47..0000000 --- a/src/stores/admin.js +++ /dev/null @@ -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 - } -}) diff --git a/src/stores/permission.js b/src/stores/permission.js index b529e25..b07c6f5 100644 --- a/src/stores/permission.js +++ b/src/stores/permission.js @@ -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 } })