feat(admin): add edit flows for all catalogs and member authentik sync

This commit is contained in:
Chris
2026-03-30 03:25:53 +08:00
parent 137861df1c
commit 0582b00f5f
13 changed files with 659 additions and 85 deletions

View File

@@ -2,3 +2,4 @@ import { adminHttp } from './http'
export const getCompanies = () => adminHttp.get('/admin/companies')
export const createCompany = (data) => adminHttp.post('/admin/companies', data)
export const updateCompany = (companyKey, data) => adminHttp.patch(`/admin/companies/${companyKey}`, data)

View File

@@ -1,3 +1,5 @@
import { adminHttp } from './http'
export const getMembers = () => adminHttp.get('/admin/members')
export const upsertMember = (data) => adminHttp.post('/admin/members/upsert', data)
export const updateMember = (authentikSub, data) => adminHttp.patch(`/admin/members/${authentikSub}`, data)

View File

@@ -2,3 +2,4 @@ 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)

View File

@@ -2,6 +2,7 @@ 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 addMemberToGroup = (groupKey, authentikSub) =>
adminHttp.post(`/admin/permission-groups/${groupKey}/members/${authentikSub}`)

View File

@@ -2,3 +2,4 @@ import { adminHttp } from './http'
export const getSites = () => adminHttp.get('/admin/sites')
export const createSite = (data) => adminHttp.post('/admin/sites', data)
export const updateSite = (siteKey, data) => adminHttp.patch(`/admin/sites/${siteKey}`, data)

View File

@@ -2,3 +2,4 @@ import { adminHttp } from './http'
export const getSystems = () => adminHttp.get('/admin/systems')
export const createSystem = (data) => adminHttp.post('/admin/systems', data)
export const updateSystem = (systemKey, data) => adminHttp.patch(`/admin/systems/${systemKey}`, data)

View File

@@ -5,38 +5,48 @@
<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-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="companies" stripe border class="w-full shadow-sm">
<template #empty><el-empty description="目前無公司" /></template>
<el-table-column prop="company_key" label="Company Key" width="200" />
<el-table-column prop="name" label="名稱" min-width="180" />
<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 label="操作" width="120">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">編輯</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增 Dialog -->
<el-dialog v-model="showDialog" title="新增公司" @close="resetForm">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="Company Key" prop="company_key">
<el-input v-model="form.company_key" placeholder="company-001" />
</el-form-item>
<el-form-item label="名稱" prop="name">
<el-input v-model="form.name" placeholder="公司名稱" />
</el-form-item>
<el-form-item label="Company Key" prop="company_key"><el-input v-model="form.company_key" /></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>
</template>
</el-dialog>
<el-dialog v-model="showEditDialog" title="編輯公司" @close="resetEditForm">
<el-form :model="editForm" label-width="100px">
<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-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>
</div>
</template>
@@ -44,7 +54,7 @@
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getCompanies, createCompany } from '@/api/companies'
import { getCompanies, createCompany, updateCompany } from '@/api/companies'
const companies = ref([])
const loading = ref(false)
@@ -52,9 +62,12 @@ 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({ company_key: '', name: '', status: 'active' })
const rules = {
company_key: [{ required: true, message: '請輸入 Company Key', trigger: 'blur' }],
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
@@ -80,6 +93,15 @@ function resetForm() {
form.value = { company_key: '', name: '' }
}
function openEdit(row) {
editForm.value = { company_key: row.company_key, name: row.name, status: row.status || 'active' }
showEditDialog.value = true
}
function resetEditForm() {
editForm.value = { company_key: '', name: '', status: 'active' }
}
async function handleCreate() {
const valid = await formRef.value.validate().catch(() => false)
if (!valid) return
@@ -91,11 +113,25 @@ async function handleCreate() {
resetForm()
await load()
} catch (err) {
ElMessage.error('新增失敗,請稍後再試')
ElMessage.error('新增失敗')
} finally {
submitting.value = false
}
}
async function handleEdit() {
savingEdit.value = true
try {
await updateCompany(editForm.value.company_key, { name: editForm.value.name, status: editForm.value.status })
ElMessage.success('更新成功')
showEditDialog.value = false
await load()
} catch (err) {
ElMessage.error('更新失敗')
} finally {
savingEdit.value = false
}
}
onMounted(load)
</script>

View File

@@ -2,39 +2,95 @@
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-gray-800">會員列表</h2>
<el-button :loading="loading" @click="load" :icon="Refresh" size="small">重新整理</el-button>
<div class="flex gap-2">
<el-button type="primary" @click="showCreateDialog = true">新增會員</el-button>
<el-button :loading="loading" @click="load" :icon="Refresh" size="small">重新整理</el-button>
</div>
</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="members" stripe border class="w-full shadow-sm">
<template #empty><el-empty description="目前無會員" /></template>
<el-table-column prop="authentik_sub" label="Authentik Sub" min-width="240" show-overflow-tooltip />
<el-table-column prop="email" label="Email" min-width="200" show-overflow-tooltip />
<el-table-column prop="display_name" label="顯示名稱" width="150" />
<el-table-column prop="authentik_sub" label="Authentik Sub" min-width="260" />
<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">
<template #default="{ row }">{{ row.is_active ? '是' : '否' }}</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button size="small" @click="openEdit(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-form-item label="Authentik Sub" prop="authentik_sub"><el-input v-model="createForm.authentik_sub" /></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-switch v-model="createForm.is_active" /></el-form-item>
<el-form-item label="同步 Authentik"><el-switch v-model="createForm.sync_to_authentik" /></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="編輯會員" @close="resetEditForm">
<el-form :model="editForm" label-width="120px">
<el-form-item label="Authentik Sub"><el-input :model-value="editForm.authentik_sub" disabled /></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-switch v-model="editForm.is_active" /></el-form-item>
<el-form-item label="同步 Authentik"><el-switch v-model="editForm.sync_to_authentik" /></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>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { getMembers } from '@/api/members'
import { getMembers, upsertMember, updateMember } from '@/api/members'
const members = ref([])
const loading = ref(false)
const error = ref(false)
const errorMsg = ref('')
const showCreateDialog = ref(false)
const createFormRef = ref()
const creating = ref(false)
const createForm = ref({
authentik_sub: '',
email: '',
display_name: '',
is_active: true,
sync_to_authentik: true
})
const createRules = {
authentik_sub: [{ required: true, message: '請輸入 Authentik Sub', trigger: 'blur' }]
}
const showEditDialog = ref(false)
const saving = ref(false)
const editForm = ref({
authentik_sub: '',
email: '',
display_name: '',
is_active: true,
sync_to_authentik: true
})
async function load() {
loading.value = true
error.value = false
@@ -43,13 +99,80 @@ async function load() {
members.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 resetCreateForm() {
createForm.value = {
authentik_sub: '',
email: '',
display_name: '',
is_active: true,
sync_to_authentik: true
}
}
function openEdit(row) {
editForm.value = {
authentik_sub: row.authentik_sub,
email: row.email || '',
display_name: row.display_name || '',
is_active: !!row.is_active,
sync_to_authentik: true
}
showEditDialog.value = true
}
function resetEditForm() {
editForm.value = {
authentik_sub: '',
email: '',
display_name: '',
is_active: true,
sync_to_authentik: true
}
}
async function handleCreate() {
const valid = await createFormRef.value.validate().catch(() => false)
if (!valid) return
creating.value = true
try {
await upsertMember({ ...createForm.value })
ElMessage.success('新增會員成功')
showCreateDialog.value = false
resetCreateForm()
await load()
} catch (err) {
const detail = err.response?.data?.detail
ElMessage.error(detail || '新增會員失敗')
} finally {
creating.value = false
}
}
async function handleEdit() {
saving.value = true
try {
await updateMember(editForm.value.authentik_sub, {
email: editForm.value.email || null,
display_name: editForm.value.display_name || null,
is_active: editForm.value.is_active,
sync_to_authentik: editForm.value.sync_to_authentik
})
ElMessage.success('更新會員成功')
showEditDialog.value = false
await load()
} catch (err) {
const detail = err.response?.data?.detail
ElMessage.error(detail || '更新會員失敗')
} finally {
saving.value = false
}
}
onMounted(load)
</script>

View File

@@ -18,12 +18,17 @@
<el-table v-else :data="modules" stripe border class="w-full shadow-sm">
<template #empty><el-empty description="目前無模組" /></template>
<el-table-column prop="system_key" label="System Key" width="140" />
<el-table-column prop="module_key" label="Module Key" width="160" />
<el-table-column prop="system_key" label="System" width="140" />
<el-table-column prop="module_key" label="Module Key" width="180" />
<el-table-column prop="name" label="名稱" min-width="180" />
<el-table-column prop="status" label="狀態" width="120" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">編輯</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增 Dialog -->
<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">
@@ -41,6 +46,27 @@
<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>
</div>
</template>
@@ -48,7 +74,7 @@
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getModules, createModule } from '@/api/modules'
import { getModules, createModule, updateModule } from '@/api/modules'
const modules = ref([])
const loading = ref(false)
@@ -57,8 +83,11 @@ 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: '', module_key: '', name: '' })
const editForm = ref({ module_key: '', name: '', status: 'active' })
const rules = {
system_key: [{ required: true, message: '請輸入 System Key', trigger: 'blur' }],
module_key: [{ required: true, message: '請輸入 Module Key', trigger: 'blur' }],
@@ -85,6 +114,19 @@ function resetForm() {
form.value = { system_key: '', module_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
@@ -102,5 +144,22 @@ async function handleCreate() {
}
}
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
}
}
onMounted(load)
</script>

View File

@@ -16,6 +16,12 @@
<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="200" />
<el-table-column prop="status" label="狀態" width="120" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button size="small" @click="openEditGroup(row)">編輯</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
@@ -60,16 +66,34 @@
</el-select>
</el-form-item>
<el-form-item label="Scope ID">
<el-input v-model="groupPermForm.scope_id" placeholder="company_key or site_key" />
<el-select v-model="groupPermForm.scope_id" placeholder="選擇 Scope ID" filterable style="width: 100%">
<el-option
v-for="s in scopeOptions"
:key="s.value"
:label="s.label"
:value="s.value"
/>
</el-select>
</el-form-item>
<el-form-item label="系統">
<el-input v-model="groupPermForm.system" placeholder="mkt" />
<el-select v-model="groupPermForm.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="模組(選填)">
<el-input v-model="groupPermForm.module" placeholder="campaign" clearable />
<el-select v-model="groupPermForm.module" placeholder="系統層(留空) 或選模組" clearable filterable style="width: 100%">
<el-option
v-for="m in filteredModuleOptions"
:key="m.value"
:label="m.label"
:value="m.value"
/>
</el-select>
</el-form-item>
<el-form-item label="操作">
<el-input v-model="groupPermForm.action" placeholder="view" />
<el-select v-model="groupPermForm.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-form-item>
<el-button
@@ -113,22 +137,73 @@
<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 } from 'vue'
import { ref, reactive, onMounted, computed, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import {
getPermissionGroups,
createPermissionGroup,
updatePermissionGroup,
addMemberToGroup,
groupGrant,
groupRevoke
} from '@/api/permission-groups'
import { getSystems } from '@/api/systems'
import { getModules } from '@/api/modules'
import { getCompanies } from '@/api/companies'
import { getSites } from '@/api/sites'
const activeTab = ref('groups')
const systems = ref([])
const modules = ref([])
const companies = ref([])
const sites = ref([])
const actionOptions = ['view', 'edit', 'manage', 'admin']
const filteredModuleOptions = computed(() => {
if (!groupPermForm.system) return []
return modules.value
.filter(m => m.system_key === groupPermForm.system && !m.module_key.endsWith('.__system__'))
.map(m => ({
value: m.module_key.split('.', 2)[1] || m.module_key,
label: `${m.name} (${m.module_key})`
}))
})
const scopeOptions = computed(() => {
if (groupPermForm.scope_type === 'company') {
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
}
if (groupPermForm.scope_type === 'site') {
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
}
return []
})
// Groups
const groups = ref([])
@@ -150,10 +225,26 @@ async function loadGroups() {
}
}
async function loadCatalogs() {
const [systemsRes, modulesRes, companiesRes, sitesRes] = await Promise.all([
getSystems(),
getModules(),
getCompanies(),
getSites()
])
systems.value = systemsRes.data?.items || []
modules.value = modulesRes.data?.items || []
companies.value = companiesRes.data?.items || []
sites.value = sitesRes.data?.items || []
}
// Create Group
const showCreateGroup = ref(false)
const creatingGroup = ref(false)
const createForm = reactive({ group_key: '', name: '' })
const showEditGroup = ref(false)
const savingGroup = ref(false)
const editGroupForm = reactive({ group_key: '', name: '', status: 'active' })
function resetCreateForm() {
createForm.group_key = ''
@@ -179,6 +270,36 @@ async function handleCreateGroup() {
}
}
function openEditGroup(row) {
editGroupForm.group_key = row.group_key
editGroupForm.name = row.name
editGroupForm.status = row.status || 'active'
showEditGroup.value = true
}
function resetEditGroupForm() {
editGroupForm.group_key = ''
editGroupForm.name = ''
editGroupForm.status = 'active'
}
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
}
}
// Add Member
const memberForm = reactive({ groupKey: '', authentikSub: '' })
const addingMember = ref(false)
@@ -245,5 +366,15 @@ async function handleGroupRevoke() {
}
}
onMounted(loadGroups)
watch(() => groupPermForm.scope_type, () => {
groupPermForm.scope_id = ''
})
watch(() => groupPermForm.system, () => {
groupPermForm.module = ''
})
onMounted(async () => {
await Promise.all([loadGroups(), loadCatalogs()])
})
</script>

View File

@@ -5,42 +5,59 @@
<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-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="sites" stripe border class="w-full shadow-sm">
<template #empty><el-empty description="目前無站台" /></template>
<el-table-column prop="site_key" label="Site Key" width="160" />
<el-table-column prop="company_key" label="Company Key" width="160" />
<el-table-column prop="site_key" label="Site Key" width="180" />
<el-table-column prop="company_key" label="Company" width="180" />
<el-table-column prop="name" label="名稱" min-width="180" />
<el-table-column prop="status" label="狀態" width="120" />
<el-table-column label="操作" width="120">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">編輯</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增 Dialog -->
<el-dialog v-model="showDialog" title="新增站台" @close="resetForm">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="Site Key" prop="site_key">
<el-input v-model="form.site_key" placeholder="site-001" />
</el-form-item>
<el-form-item label="Company Key" prop="company_key">
<el-input v-model="form.company_key" placeholder="company-001" />
</el-form-item>
<el-form-item label="名稱" prop="name">
<el-input v-model="form.name" placeholder="站台名稱" />
<el-form-item label="Site Key" prop="site_key"><el-input v-model="form.site_key" /></el-form-item>
<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-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>
</template>
</el-dialog>
<el-dialog v-model="showEditDialog" title="編輯站台" @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-select>
</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>
</div>
</template>
@@ -48,29 +65,39 @@
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getSites, createSite } from '@/api/sites'
import { getSites, createSite, updateSite } from '@/api/sites'
import { getCompanies } from '@/api/companies'
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({ site_key: '', company_key: '', name: '' })
const editForm = ref({ site_key: '', company_key: '', name: '', status: 'active' })
const rules = {
site_key: [{ required: true, message: '請輸入 Site Key', trigger: 'blur' }],
company_key: [{ required: true, message: '請輸入 Company Key', trigger: 'blur' }],
company_key: [{ required: true, message: '請選擇公司', trigger: 'change' }],
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
}
async function loadCompanies() {
const res = await getCompanies()
companies.value = res.data?.items || []
}
async function load() {
loading.value = true
error.value = false
try {
const res = await getSites()
sites.value = res.data?.items || []
const [sitesRes] = await Promise.all([getSites(), loadCompanies()])
sites.value = sitesRes.data?.items || []
} catch (err) {
error.value = true
errorMsg.value = err.response?.status === 422
@@ -85,6 +112,20 @@ function resetForm() {
form.value = { site_key: '', company_key: '', name: '' }
}
function openEdit(row) {
editForm.value = {
site_key: row.site_key,
company_key: row.company_key,
name: row.name,
status: row.status || 'active'
}
showEditDialog.value = true
}
function resetEditForm() {
editForm.value = { site_key: '', company_key: '', name: '', status: 'active' }
}
async function handleCreate() {
const valid = await formRef.value.validate().catch(() => false)
if (!valid) return
@@ -96,11 +137,29 @@ async function handleCreate() {
resetForm()
await load()
} catch (err) {
ElMessage.error('新增失敗,請稍後再試')
ElMessage.error('新增失敗')
} finally {
submitting.value = false
}
}
async function handleEdit() {
savingEdit.value = true
try {
await updateSite(editForm.value.site_key, {
company_key: editForm.value.company_key,
name: editForm.value.name,
status: editForm.value.status
})
ElMessage.success('更新成功')
showEditDialog.value = false
await load()
} catch (err) {
ElMessage.error('更新失敗')
} finally {
savingEdit.value = false
}
}
onMounted(load)
</script>

View File

@@ -20,9 +20,14 @@
<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="120">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">編輯</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增 Dialog -->
<el-dialog v-model="showDialog" title="新增系統" @close="resetForm">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="System Key" prop="system_key">
@@ -37,6 +42,27 @@
<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="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-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>
</div>
</template>
@@ -44,7 +70,7 @@
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { getSystems, createSystem } from '@/api/systems'
import { getSystems, createSystem, updateSystem } from '@/api/systems'
const systems = ref([])
const loading = ref(false)
@@ -53,8 +79,11 @@ 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({ system_key: '', name: '', status: 'active' })
const rules = {
system_key: [{ required: true, message: '請輸入 System Key', trigger: 'blur' }],
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
@@ -80,6 +109,19 @@ function resetForm() {
form.value = { system_key: '', name: '' }
}
function openEdit(row) {
editForm.value = {
system_key: row.system_key,
name: row.name,
status: row.status || 'active'
}
showEditDialog.value = true
}
function resetEditForm() {
editForm.value = { system_key: '', name: '', status: 'active' }
}
async function handleCreate() {
const valid = await formRef.value.validate().catch(() => false)
if (!valid) return
@@ -97,5 +139,22 @@ async function handleCreate() {
}
}
async function handleEdit() {
savingEdit.value = true
try {
await updateSystem(editForm.value.system_key, {
name: editForm.value.name,
status: editForm.value.status
})
ElMessage.success('更新成功')
showEditDialog.value = false
await load()
} catch (err) {
ElMessage.error('更新失敗')
} finally {
savingEdit.value = false
}
}
onMounted(load)
</script>

View File

@@ -15,7 +15,9 @@
@submit.prevent="handleGrant"
>
<el-form-item label="Authentik Sub" prop="authentik_sub">
<el-input v-model="grantForm.authentik_sub" placeholder="authentik-sub-xxx" />
<el-select v-model="grantForm.authentik_sub" filterable allow-create default-first-option placeholder="選擇會員或輸入 sub" style="width: 100%">
<el-option v-for="m in members" :key="m.authentik_sub" :label="`${m.display_name || m.email || '(no-name)'} (${m.authentik_sub})`" :value="m.authentik_sub" />
</el-select>
</el-form-item>
<el-form-item label="Email" prop="email">
<el-input v-model="grantForm.email" placeholder="user@example.com" />
@@ -30,16 +32,24 @@
</el-select>
</el-form-item>
<el-form-item label="Scope ID" prop="scope_id">
<el-input v-model="grantForm.scope_id" placeholder="company_key or site_key" />
<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-input v-model="grantForm.system" placeholder="mkt" />
<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-input v-model="grantForm.module" placeholder="campaign空值代表系統層" clearable />
<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-input v-model="grantForm.action" placeholder="view" />
<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
@@ -83,7 +93,9 @@
@submit.prevent="handleRevoke"
>
<el-form-item label="Authentik Sub" prop="authentik_sub">
<el-input v-model="revokeForm.authentik_sub" placeholder="authentik-sub-xxx" />
<el-select v-model="revokeForm.authentik_sub" filterable allow-create default-first-option placeholder="選擇會員或輸入 sub" style="width: 100%">
<el-option v-for="m in members" :key="m.authentik_sub" :label="`${m.display_name || m.email || '(no-name)'} (${m.authentik_sub})`" :value="m.authentik_sub" />
</el-select>
</el-form-item>
<el-form-item label="Scope 類型" prop="scope_type">
<el-select v-model="revokeForm.scope_type" placeholder="選擇 Scope 類型">
@@ -92,16 +104,24 @@
</el-select>
</el-form-item>
<el-form-item label="Scope ID" prop="scope_id">
<el-input v-model="revokeForm.scope_id" placeholder="company_key or site_key" />
<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-input v-model="revokeForm.system" placeholder="mkt" />
<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-input v-model="revokeForm.module" placeholder="campaign空值代表系統層" clearable />
<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-input v-model="revokeForm.action" placeholder="view" />
<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
@@ -138,13 +158,24 @@
</template>
<script setup>
import { ref, reactive } from 'vue'
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'
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', 'manage', 'admin']
// Grant
const grantFormRef = ref()
@@ -163,6 +194,26 @@ const grantForm = reactive({
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.split('.', 2)[1] || m.module_key,
label: `${m.name} (${m.module_key})`
}))
})
const grantScopeOptions = computed(() => {
if (grantForm.scope_type === 'company') {
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
}
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 = {
authentik_sub: [required],
@@ -212,6 +263,26 @@ const revokeForm = reactive({
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.split('.', 2)[1] || m.module_key,
label: `${m.name} (${m.module_key})`
}))
})
const revokeScopeOptions = computed(() => {
if (revokeForm.scope_type === 'company') {
return companies.value.map(c => ({ value: c.company_key, label: `${c.name} (${c.company_key})` }))
}
if (revokeForm.scope_type === 'site') {
return sites.value.map(s => ({ value: s.site_key, label: `${s.name} (${s.site_key})` }))
}
return []
})
const revokeRules = {
authentik_sub: [required],
scope_type: [required],
@@ -265,4 +336,33 @@ function formatAdminError(err) {
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 || []
}
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.authentik_sub, (sub) => {
const user = members.value.find(m => m.authentik_sub === sub)
if (!user) return
grantForm.email = user.email || ''
grantForm.display_name = user.display_name || ''
})
onMounted(loadCatalogs)
</script>