feat(admin): add edit flows for all catalogs and member authentik sync
This commit is contained in:
@@ -2,3 +2,4 @@ import { adminHttp } from './http'
|
|||||||
|
|
||||||
export const getCompanies = () => adminHttp.get('/admin/companies')
|
export const getCompanies = () => adminHttp.get('/admin/companies')
|
||||||
export const createCompany = (data) => adminHttp.post('/admin/companies', data)
|
export const createCompany = (data) => adminHttp.post('/admin/companies', data)
|
||||||
|
export const updateCompany = (companyKey, data) => adminHttp.patch(`/admin/companies/${companyKey}`, data)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
import { adminHttp } from './http'
|
import { adminHttp } from './http'
|
||||||
|
|
||||||
export const getMembers = () => adminHttp.get('/admin/members')
|
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)
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ import { adminHttp } from './http'
|
|||||||
|
|
||||||
export const getModules = () => adminHttp.get('/admin/modules')
|
export const getModules = () => adminHttp.get('/admin/modules')
|
||||||
export const createModule = (data) => adminHttp.post('/admin/modules', data)
|
export const createModule = (data) => adminHttp.post('/admin/modules', data)
|
||||||
|
export const updateModule = (moduleKey, data) => adminHttp.patch(`/admin/modules/${moduleKey}`, data)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { adminHttp } from './http'
|
|||||||
|
|
||||||
export const getPermissionGroups = () => adminHttp.get('/admin/permission-groups')
|
export const getPermissionGroups = () => adminHttp.get('/admin/permission-groups')
|
||||||
export const createPermissionGroup = (data) => adminHttp.post('/admin/permission-groups', data)
|
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) =>
|
export const addMemberToGroup = (groupKey, authentikSub) =>
|
||||||
adminHttp.post(`/admin/permission-groups/${groupKey}/members/${authentikSub}`)
|
adminHttp.post(`/admin/permission-groups/${groupKey}/members/${authentikSub}`)
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ import { adminHttp } from './http'
|
|||||||
|
|
||||||
export const getSites = () => adminHttp.get('/admin/sites')
|
export const getSites = () => adminHttp.get('/admin/sites')
|
||||||
export const createSite = (data) => adminHttp.post('/admin/sites', data)
|
export const createSite = (data) => adminHttp.post('/admin/sites', data)
|
||||||
|
export const updateSite = (siteKey, data) => adminHttp.patch(`/admin/sites/${siteKey}`, data)
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ import { adminHttp } from './http'
|
|||||||
|
|
||||||
export const getSystems = () => adminHttp.get('/admin/systems')
|
export const getSystems = () => adminHttp.get('/admin/systems')
|
||||||
export const createSystem = (data) => adminHttp.post('/admin/systems', data)
|
export const createSystem = (data) => adminHttp.post('/admin/systems', data)
|
||||||
|
export const updateSystem = (systemKey, data) => adminHttp.patch(`/admin/systems/${systemKey}`, data)
|
||||||
|
|||||||
@@ -5,38 +5,48 @@
|
|||||||
<el-button type="primary" @click="showDialog = true" :icon="Plus">新增公司</el-button>
|
<el-button type="primary" @click="showDialog = true" :icon="Plus">新增公司</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-alert
|
<el-alert v-if="error" :title="errorMsg" type="error" show-icon :closable="false" class="mb-4" />
|
||||||
v-if="error"
|
|
||||||
:title="errorMsg"
|
|
||||||
type="error"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
class="mb-4"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-skeleton v-if="loading" :rows="4" animated />
|
<el-skeleton v-if="loading" :rows="4" animated />
|
||||||
|
|
||||||
<el-table v-else :data="companies" stripe border class="w-full shadow-sm">
|
<el-table v-else :data="companies" stripe border class="w-full shadow-sm">
|
||||||
<template #empty><el-empty description="目前無公司" /></template>
|
<template #empty><el-empty description="目前無公司" /></template>
|
||||||
<el-table-column prop="company_key" label="Company Key" width="200" />
|
<el-table-column prop="company_key" label="Company Key" width="220" />
|
||||||
<el-table-column prop="name" label="名稱" min-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="openEdit(row)">編輯</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<!-- 新增 Dialog -->
|
|
||||||
<el-dialog v-model="showDialog" title="新增公司" @close="resetForm">
|
<el-dialog v-model="showDialog" title="新增公司" @close="resetForm">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="Company Key" prop="company_key">
|
<el-form-item label="Company Key" prop="company_key"><el-input v-model="form.company_key" /></el-form-item>
|
||||||
<el-input v-model="form.company_key" placeholder="company-001" />
|
<el-form-item label="名稱" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="名稱" prop="name">
|
|
||||||
<el-input v-model="form.name" placeholder="公司名稱" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showDialog = false">取消</el-button>
|
<el-button @click="showDialog = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -44,7 +54,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
import { getCompanies, createCompany } from '@/api/companies'
|
import { getCompanies, createCompany, updateCompany } from '@/api/companies'
|
||||||
|
|
||||||
const companies = ref([])
|
const companies = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -52,9 +62,12 @@ const error = ref(false)
|
|||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
const showEditDialog = ref(false)
|
||||||
|
const savingEdit = ref(false)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
|
||||||
const form = ref({ company_key: '', name: '' })
|
const form = ref({ company_key: '', name: '' })
|
||||||
|
const editForm = ref({ company_key: '', name: '', status: 'active' })
|
||||||
const rules = {
|
const rules = {
|
||||||
company_key: [{ required: true, message: '請輸入 Company Key', trigger: 'blur' }],
|
company_key: [{ required: true, message: '請輸入 Company Key', trigger: 'blur' }],
|
||||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||||
@@ -80,6 +93,15 @@ function resetForm() {
|
|||||||
form.value = { company_key: '', name: '' }
|
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() {
|
async function handleCreate() {
|
||||||
const valid = await formRef.value.validate().catch(() => false)
|
const valid = await formRef.value.validate().catch(() => false)
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
@@ -91,11 +113,25 @@ async function handleCreate() {
|
|||||||
resetForm()
|
resetForm()
|
||||||
await load()
|
await load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error('新增失敗,請稍後再試')
|
ElMessage.error('新增失敗')
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
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)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,39 +2,95 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h2 class="text-xl font-bold text-gray-800">會員列表</h2>
|
<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>
|
</div>
|
||||||
|
|
||||||
<el-alert
|
<el-alert v-if="error" :title="errorMsg" type="error" show-icon :closable="false" class="mb-4" />
|
||||||
v-if="error"
|
|
||||||
:title="errorMsg"
|
|
||||||
type="error"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
class="mb-4"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-skeleton v-if="loading" :rows="4" animated />
|
<el-skeleton v-if="loading" :rows="4" animated />
|
||||||
|
|
||||||
<el-table v-else :data="members" stripe border class="w-full shadow-sm">
|
<el-table v-else :data="members" stripe border class="w-full shadow-sm">
|
||||||
<template #empty><el-empty description="目前無會員" /></template>
|
<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="authentik_sub" label="Authentik Sub" min-width="260" />
|
||||||
<el-table-column prop="email" label="Email" min-width="200" show-overflow-tooltip />
|
<el-table-column prop="email" label="Email" min-width="220" />
|
||||||
<el-table-column prop="display_name" label="顯示名稱" width="150" />
|
<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-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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
import { Refresh } from '@element-plus/icons-vue'
|
import { Refresh } from '@element-plus/icons-vue'
|
||||||
import { getMembers } from '@/api/members'
|
import { getMembers, upsertMember, updateMember } from '@/api/members'
|
||||||
|
|
||||||
const members = ref([])
|
const members = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(false)
|
const error = ref(false)
|
||||||
const errorMsg = ref('')
|
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() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = false
|
error.value = false
|
||||||
@@ -43,13 +99,80 @@ async function load() {
|
|||||||
members.value = res.data?.items || []
|
members.value = res.data?.items || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = true
|
error.value = true
|
||||||
errorMsg.value = err.response?.status === 422
|
errorMsg.value = err.response?.data?.detail || '載入失敗,請稍後再試'
|
||||||
? '缺少管理員 API 認證,請檢查前端 .env.development'
|
|
||||||
: '載入失敗,請稍後再試'
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
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)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,12 +18,17 @@
|
|||||||
|
|
||||||
<el-table v-else :data="modules" stripe border class="w-full shadow-sm">
|
<el-table v-else :data="modules" stripe border class="w-full shadow-sm">
|
||||||
<template #empty><el-empty description="目前無模組" /></template>
|
<template #empty><el-empty description="目前無模組" /></template>
|
||||||
<el-table-column prop="system_key" label="System Key" width="140" />
|
<el-table-column prop="system_key" label="System" width="140" />
|
||||||
<el-table-column prop="module_key" label="Module Key" width="160" />
|
<el-table-column prop="module_key" label="Module Key" width="180" />
|
||||||
<el-table-column prop="name" label="名稱" min-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>
|
</el-table>
|
||||||
|
|
||||||
<!-- 新增 Dialog -->
|
|
||||||
<el-dialog v-model="showDialog" title="新增模組" @close="resetForm">
|
<el-dialog v-model="showDialog" title="新增模組" @close="resetForm">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
<el-form-item label="System Key" prop="system_key">
|
<el-form-item label="System Key" prop="system_key">
|
||||||
@@ -41,6 +46,27 @@
|
|||||||
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -48,7 +74,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
import { getModules, createModule } from '@/api/modules'
|
import { getModules, createModule, updateModule } from '@/api/modules'
|
||||||
|
|
||||||
const modules = ref([])
|
const modules = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -57,8 +83,11 @@ const errorMsg = ref('')
|
|||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
const showEditDialog = ref(false)
|
||||||
|
const savingEdit = ref(false)
|
||||||
|
|
||||||
const form = ref({ system_key: '', module_key: '', name: '' })
|
const form = ref({ system_key: '', module_key: '', name: '' })
|
||||||
|
const editForm = ref({ module_key: '', name: '', status: 'active' })
|
||||||
const rules = {
|
const rules = {
|
||||||
system_key: [{ required: true, message: '請輸入 System Key', trigger: 'blur' }],
|
system_key: [{ required: true, message: '請輸入 System Key', trigger: 'blur' }],
|
||||||
module_key: [{ required: true, message: '請輸入 Module 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: '' }
|
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() {
|
async function handleCreate() {
|
||||||
const valid = await formRef.value.validate().catch(() => false)
|
const valid = await formRef.value.validate().catch(() => false)
|
||||||
if (!valid) return
|
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)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -16,6 +16,12 @@
|
|||||||
<template #empty><el-empty description="目前無群組" /></template>
|
<template #empty><el-empty description="目前無群組" /></template>
|
||||||
<el-table-column prop="group_key" label="Group Key" width="180" />
|
<el-table-column prop="group_key" label="Group Key" width="180" />
|
||||||
<el-table-column prop="name" label="群組名稱" min-width="200" />
|
<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>
|
</el-table>
|
||||||
</div>
|
</div>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
@@ -60,16 +66,34 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Scope ID">
|
<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>
|
||||||
<el-form-item label="系統">
|
<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>
|
||||||
<el-form-item label="模組(選填)">
|
<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>
|
||||||
<el-form-item label="操作">
|
<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-form-item>
|
<el-form-item>
|
||||||
<el-button
|
<el-button
|
||||||
@@ -113,22 +137,73 @@
|
|||||||
<el-button type="primary" :loading="creatingGroup" @click="handleCreateGroup">確認</el-button>
|
<el-button type="primary" :loading="creatingGroup" @click="handleCreateGroup">確認</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
getPermissionGroups,
|
getPermissionGroups,
|
||||||
createPermissionGroup,
|
createPermissionGroup,
|
||||||
|
updatePermissionGroup,
|
||||||
addMemberToGroup,
|
addMemberToGroup,
|
||||||
groupGrant,
|
groupGrant,
|
||||||
groupRevoke
|
groupRevoke
|
||||||
} from '@/api/permission-groups'
|
} 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 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
|
// Groups
|
||||||
const groups = ref([])
|
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
|
// Create Group
|
||||||
const showCreateGroup = ref(false)
|
const showCreateGroup = ref(false)
|
||||||
const creatingGroup = ref(false)
|
const creatingGroup = ref(false)
|
||||||
const createForm = reactive({ group_key: '', name: '' })
|
const createForm = reactive({ group_key: '', name: '' })
|
||||||
|
const showEditGroup = ref(false)
|
||||||
|
const savingGroup = ref(false)
|
||||||
|
const editGroupForm = reactive({ group_key: '', name: '', status: 'active' })
|
||||||
|
|
||||||
function resetCreateForm() {
|
function resetCreateForm() {
|
||||||
createForm.group_key = ''
|
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
|
// Add Member
|
||||||
const memberForm = reactive({ groupKey: '', authentikSub: '' })
|
const memberForm = reactive({ groupKey: '', authentikSub: '' })
|
||||||
const addingMember = ref(false)
|
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>
|
</script>
|
||||||
|
|||||||
@@ -5,42 +5,59 @@
|
|||||||
<el-button type="primary" @click="showDialog = true" :icon="Plus">新增站台</el-button>
|
<el-button type="primary" @click="showDialog = true" :icon="Plus">新增站台</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-alert
|
<el-alert v-if="error" :title="errorMsg" type="error" show-icon :closable="false" class="mb-4" />
|
||||||
v-if="error"
|
|
||||||
:title="errorMsg"
|
|
||||||
type="error"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
class="mb-4"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<el-skeleton v-if="loading" :rows="4" animated />
|
<el-skeleton v-if="loading" :rows="4" animated />
|
||||||
|
|
||||||
<el-table v-else :data="sites" stripe border class="w-full shadow-sm">
|
<el-table v-else :data="sites" stripe border class="w-full shadow-sm">
|
||||||
<template #empty><el-empty description="目前無站台" /></template>
|
<template #empty><el-empty description="目前無站台" /></template>
|
||||||
<el-table-column prop="site_key" label="Site Key" width="160" />
|
<el-table-column prop="site_key" label="Site Key" width="180" />
|
||||||
<el-table-column prop="company_key" label="Company Key" width="160" />
|
<el-table-column prop="company_key" label="Company" width="180" />
|
||||||
<el-table-column prop="name" label="名稱" min-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>
|
</el-table>
|
||||||
|
|
||||||
<!-- 新增 Dialog -->
|
|
||||||
<el-dialog v-model="showDialog" title="新增站台" @close="resetForm">
|
<el-dialog v-model="showDialog" title="新增站台" @close="resetForm">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
<el-form-item label="Site Key" prop="site_key">
|
<el-form-item label="Site Key" prop="site_key"><el-input v-model="form.site_key" /></el-form-item>
|
||||||
<el-input v-model="form.site_key" placeholder="site-001" />
|
<el-form-item label="Company" prop="company_key">
|
||||||
</el-form-item>
|
<el-select v-model="form.company_key" style="width: 100%" filterable>
|
||||||
<el-form-item label="Company Key" prop="company_key">
|
<el-option v-for="c in companies" :key="c.company_key" :label="`${c.name} (${c.company_key})`" :value="c.company_key" />
|
||||||
<el-input v-model="form.company_key" placeholder="company-001" />
|
</el-select>
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="名稱" prop="name">
|
|
||||||
<el-input v-model="form.name" placeholder="站台名稱" />
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="名稱" prop="name"><el-input v-model="form.name" /></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showDialog = false">取消</el-button>
|
<el-button @click="showDialog = false">取消</el-button>
|
||||||
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -48,29 +65,39 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
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 sites = ref([])
|
||||||
|
const companies = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref(false)
|
const error = ref(false)
|
||||||
const errorMsg = ref('')
|
const errorMsg = ref('')
|
||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
const showEditDialog = ref(false)
|
||||||
|
const savingEdit = ref(false)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
|
||||||
const form = ref({ site_key: '', company_key: '', name: '' })
|
const form = ref({ site_key: '', company_key: '', name: '' })
|
||||||
|
const editForm = ref({ site_key: '', company_key: '', name: '', status: 'active' })
|
||||||
const rules = {
|
const rules = {
|
||||||
site_key: [{ required: true, message: '請輸入 Site Key', trigger: 'blur' }],
|
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' }]
|
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCompanies() {
|
||||||
|
const res = await getCompanies()
|
||||||
|
companies.value = res.data?.items || []
|
||||||
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = false
|
error.value = false
|
||||||
try {
|
try {
|
||||||
const res = await getSites()
|
const [sitesRes] = await Promise.all([getSites(), loadCompanies()])
|
||||||
sites.value = res.data?.items || []
|
sites.value = sitesRes.data?.items || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = true
|
error.value = true
|
||||||
errorMsg.value = err.response?.status === 422
|
errorMsg.value = err.response?.status === 422
|
||||||
@@ -85,6 +112,20 @@ function resetForm() {
|
|||||||
form.value = { site_key: '', company_key: '', name: '' }
|
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() {
|
async function handleCreate() {
|
||||||
const valid = await formRef.value.validate().catch(() => false)
|
const valid = await formRef.value.validate().catch(() => false)
|
||||||
if (!valid) return
|
if (!valid) return
|
||||||
@@ -96,11 +137,29 @@ async function handleCreate() {
|
|||||||
resetForm()
|
resetForm()
|
||||||
await load()
|
await load()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ElMessage.error('新增失敗,請稍後再試')
|
ElMessage.error('新增失敗')
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
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)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,9 +20,14 @@
|
|||||||
<template #empty><el-empty description="目前無系統" /></template>
|
<template #empty><el-empty description="目前無系統" /></template>
|
||||||
<el-table-column prop="system_key" label="System Key" width="200" />
|
<el-table-column prop="system_key" label="System Key" width="200" />
|
||||||
<el-table-column prop="name" label="名稱" min-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>
|
</el-table>
|
||||||
|
|
||||||
<!-- 新增 Dialog -->
|
|
||||||
<el-dialog v-model="showDialog" title="新增系統" @close="resetForm">
|
<el-dialog v-model="showDialog" title="新增系統" @close="resetForm">
|
||||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="System Key" prop="system_key">
|
<el-form-item label="System Key" prop="system_key">
|
||||||
@@ -37,6 +42,27 @@
|
|||||||
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
<el-button type="primary" :loading="submitting" @click="handleCreate">確認</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -44,7 +70,7 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Plus } from '@element-plus/icons-vue'
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
import { getSystems, createSystem } from '@/api/systems'
|
import { getSystems, createSystem, updateSystem } from '@/api/systems'
|
||||||
|
|
||||||
const systems = ref([])
|
const systems = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -53,8 +79,11 @@ const errorMsg = ref('')
|
|||||||
const showDialog = ref(false)
|
const showDialog = ref(false)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
const showEditDialog = ref(false)
|
||||||
|
const savingEdit = ref(false)
|
||||||
|
|
||||||
const form = ref({ system_key: '', name: '' })
|
const form = ref({ system_key: '', name: '' })
|
||||||
|
const editForm = ref({ system_key: '', name: '', status: 'active' })
|
||||||
const rules = {
|
const rules = {
|
||||||
system_key: [{ required: true, message: '請輸入 System Key', trigger: 'blur' }],
|
system_key: [{ required: true, message: '請輸入 System Key', trigger: 'blur' }],
|
||||||
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
name: [{ required: true, message: '請輸入名稱', trigger: 'blur' }]
|
||||||
@@ -80,6 +109,19 @@ function resetForm() {
|
|||||||
form.value = { system_key: '', name: '' }
|
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() {
|
async function handleCreate() {
|
||||||
const valid = await formRef.value.validate().catch(() => false)
|
const valid = await formRef.value.validate().catch(() => false)
|
||||||
if (!valid) return
|
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)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -15,7 +15,9 @@
|
|||||||
@submit.prevent="handleGrant"
|
@submit.prevent="handleGrant"
|
||||||
>
|
>
|
||||||
<el-form-item label="Authentik Sub" prop="authentik_sub">
|
<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>
|
||||||
<el-form-item label="Email" prop="email">
|
<el-form-item label="Email" prop="email">
|
||||||
<el-input v-model="grantForm.email" placeholder="user@example.com" />
|
<el-input v-model="grantForm.email" placeholder="user@example.com" />
|
||||||
@@ -30,16 +32,24 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Scope ID" prop="scope_id">
|
<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>
|
||||||
<el-form-item label="系統" prop="system">
|
<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>
|
||||||
<el-form-item label="模組(選填)" prop="module">
|
<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>
|
||||||
<el-form-item label="操作" prop="action">
|
<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-form-item>
|
||||||
|
|
||||||
<el-alert
|
<el-alert
|
||||||
@@ -83,7 +93,9 @@
|
|||||||
@submit.prevent="handleRevoke"
|
@submit.prevent="handleRevoke"
|
||||||
>
|
>
|
||||||
<el-form-item label="Authentik Sub" prop="authentik_sub">
|
<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>
|
||||||
<el-form-item label="Scope 類型" prop="scope_type">
|
<el-form-item label="Scope 類型" prop="scope_type">
|
||||||
<el-select v-model="revokeForm.scope_type" placeholder="選擇 Scope 類型">
|
<el-select v-model="revokeForm.scope_type" placeholder="選擇 Scope 類型">
|
||||||
@@ -92,16 +104,24 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Scope ID" prop="scope_id">
|
<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>
|
||||||
<el-form-item label="系統" prop="system">
|
<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>
|
||||||
<el-form-item label="模組(選填)" prop="module">
|
<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>
|
||||||
<el-form-item label="操作" prop="action">
|
<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-form-item>
|
||||||
|
|
||||||
<el-alert
|
<el-alert
|
||||||
@@ -138,13 +158,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive } from 'vue'
|
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { usePermissionStore } from '@/stores/permission'
|
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 permissionStore = usePermissionStore()
|
||||||
|
|
||||||
const activeTab = ref('grant')
|
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
|
// Grant
|
||||||
const grantFormRef = ref()
|
const grantFormRef = ref()
|
||||||
@@ -163,6 +194,26 @@ const grantForm = reactive({
|
|||||||
action: ''
|
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 required = { required: true, message: '必填', trigger: 'blur' }
|
||||||
const grantRules = {
|
const grantRules = {
|
||||||
authentik_sub: [required],
|
authentik_sub: [required],
|
||||||
@@ -212,6 +263,26 @@ const revokeForm = reactive({
|
|||||||
action: ''
|
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 = {
|
const revokeRules = {
|
||||||
authentik_sub: [required],
|
authentik_sub: [required],
|
||||||
scope_type: [required],
|
scope_type: [required],
|
||||||
@@ -265,4 +336,33 @@ function formatAdminError(err) {
|
|||||||
if (status === 503) return '後端設定不完整,請聯絡管理員'
|
if (status === 503) return '後端設定不完整,請聯絡管理員'
|
||||||
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>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user