From d59407d04c0a116d1b0485c8cb531e9157ba03a9 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 3 Apr 2026 01:56:22 +0800 Subject: [PATCH] feat: allow assigning sites directly from role page --- backend/app/api/admin_catalog.py | 21 +++++++++++ backend/app/repositories/site_roles_repo.py | 6 ++++ docs/FRONTEND_HANDOFF.md | 1 + frontend/src/api/roles.js | 1 + frontend/src/pages/admin/RolesPage.vue | 40 ++++++++++++++++++++- 5 files changed, 68 insertions(+), 1 deletion(-) diff --git a/backend/app/api/admin_catalog.py b/backend/app/api/admin_catalog.py index 9a019c4..ce1ac42 100644 --- a/backend/app/api/admin_catalog.py +++ b/backend/app/api/admin_catalog.py @@ -646,6 +646,27 @@ def list_role_sites(role_key: str, db: Session = Depends(get_db)) -> RoleSitesRe return RoleSitesResponse(role_key=role_key, sites=result) +@router.put("/roles/{role_key}/sites", response_model=RoleSitesResponse) +def assign_role_sites(role_key: str, payload: UserSiteAssignRequest, db: Session = Depends(get_db)) -> RoleSitesResponse: + roles_repo = RolesRepository(db) + sites_repo = SitesRepository(db) + site_roles_repo = SiteRolesRepository(db) + + role = roles_repo.get_by_key(role_key) + if not role: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="role_not_found") + + site_ids: list[str] = [] + for site_key in list(dict.fromkeys(payload.site_keys)): + site = sites_repo.get_by_key(site_key) + if not site: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"site_not_found:{site_key}") + site_ids.append(site.id) + + site_roles_repo.set_role_sites(role_id=role.id, site_ids=site_ids) + return list_role_sites(role_key=role_key, db=db) + + @router.get("/members", response_model=ListResponse) def list_members( db: Session = Depends(get_db), diff --git a/backend/app/repositories/site_roles_repo.py b/backend/app/repositories/site_roles_repo.py index 4771bdd..689138e 100644 --- a/backend/app/repositories/site_roles_repo.py +++ b/backend/app/repositories/site_roles_repo.py @@ -35,3 +35,9 @@ class SiteRolesRepository: for role_id in role_ids: self.db.add(SiteRole(site_id=site_id, role_id=role_id)) self.db.commit() + + def set_role_sites(self, *, role_id: str, site_ids: list[str]) -> None: + self.db.execute(delete(SiteRole).where(SiteRole.role_id == role_id)) + for site_id in site_ids: + self.db.add(SiteRole(site_id=site_id, role_id=role_id)) + self.db.commit() diff --git a/docs/FRONTEND_HANDOFF.md b/docs/FRONTEND_HANDOFF.md index a0a8b46..7f4b733 100644 --- a/docs/FRONTEND_HANDOFF.md +++ b/docs/FRONTEND_HANDOFF.md @@ -23,6 +23,7 @@ 4. 角色管理(DB 關聯為主) - 欄位:`role_key`, `system_key`, `name`, `description`, `status` - 關聯操作:指派到 Site(新增/刪除 `site_roles`) +- 需支援在「角色頁」直接多選站台並儲存(不必切到站台頁)。 5. 會員管理(CRUD) - 欄位:`user_sub`, `username`, `email`, `display_name`, `is_active`, `status` diff --git a/frontend/src/api/roles.js b/frontend/src/api/roles.js index a4c713c..063d891 100644 --- a/frontend/src/api/roles.js +++ b/frontend/src/api/roles.js @@ -5,3 +5,4 @@ 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`) +export const setRoleSites = (roleKey, siteKeys) => adminHttp.put(`/admin/roles/${roleKey}/sites`, { site_keys: siteKeys }) diff --git a/frontend/src/pages/admin/RolesPage.vue b/frontend/src/pages/admin/RolesPage.vue index 3acb1f7..25281d2 100644 --- a/frontend/src/pages/admin/RolesPage.vue +++ b/frontend/src/pages/admin/RolesPage.vue @@ -79,6 +79,18 @@ + + + + + + + @@ -86,6 +98,7 @@ @@ -96,8 +109,9 @@ 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 { getRoles, createRole, updateRole, deleteRole, getRoleSites, setRoleSites } from '@/api/roles' import { getSystems } from '@/api/systems' +import { getSites } from '@/api/sites' const roles = ref([]) const systems = ref([]) @@ -131,8 +145,12 @@ const rules = { const showSitesDialog = ref(false) const selectedRoleLabel = ref('') +const selectedRoleKey = ref('') const roleSites = ref([]) const sitesLoading = ref(false) +const savingSites = ref(false) +const siteOptions = ref([]) +const selectedSiteKeys = ref([]) async function load() { loading.value = true @@ -144,6 +162,8 @@ async function load() { ]) roles.value = rolesRes.data?.items || [] systems.value = systemsRes.data?.items || [] + const sitesRes = await getSites({ limit: 1000, offset: 0 }) + siteOptions.value = sitesRes.data?.items || [] } catch (err) { error.value = true errorMsg.value = err.response?.data?.detail || '載入角色失敗' @@ -240,19 +260,37 @@ async function handleDelete(row) { } async function openSites(row) { + selectedRoleKey.value = row.role_key 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 || [] + selectedSiteKeys.value = roleSites.value.map(item => item.site_key) } catch (_err) { ElMessage.error('載入角色站台失敗') roleSites.value = [] + selectedSiteKeys.value = [] } finally { sitesLoading.value = false } } +async function handleSaveRoleSites() { + if (!selectedRoleKey.value) return + savingSites.value = true + try { + await setRoleSites(selectedRoleKey.value, selectedSiteKeys.value) + const res = await getRoleSites(selectedRoleKey.value) + roleSites.value = res.data?.sites || [] + ElMessage.success('角色站台綁定已更新') + } catch (err) { + ElMessage.error(err.response?.data?.detail || '儲存角色站台失敗') + } finally { + savingSites.value = false + } +} + onMounted(load)