Files
member-frontend/src/pages/admin/MembersPage.vue

311 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div>
<div class="flex items-center justify-between mb-6">
<h2 class="text-xl font-bold text-gray-800">會員列表</h2>
<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-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="user_sub" label="User Sub" min-width="260" />
<el-table-column prop="username" label="Username" min-width="150" />
<el-table-column prop="email" label="Email" min-width="220" />
<el-table-column prop="display_name" label="顯示名稱" min-width="170" />
<el-table-column label="啟用" width="80">
<template #default="{ row }">{{ row.is_active ? '是' : '否' }}</template>
</el-table-column>
<el-table-column label="操作" width="360">
<template #default="{ row }">
<el-button size="small" @click="openEdit(row)">編輯</el-button>
<el-button size="small" @click="openRoles(row)">角色</el-button>
<el-button size="small" type="warning" @click="handleResetPassword(row)">重設密碼</el-button>
<el-button size="small" type="danger" @click="handleDelete(row)">刪除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="showCreateDialog" title="新增會員" width="760px" @close="resetCreateForm">
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="130px">
<el-form-item label="Username" prop="username"><el-input v-model="createForm.username" /></el-form-item>
<el-form-item label="Email" prop="email"><el-input v-model="createForm.email" /></el-form-item>
<el-form-item label="顯示名稱"><el-input v-model="createForm.display_name" /></el-form-item>
<el-form-item label="所屬站台">
<el-select v-model="createForm.site_keys" multiple filterable clearable style="width: 100%">
<el-option
v-for="site in siteOptions"
:key="site.site_key"
:label="`${site.company_display_name} / ${site.display_name} (${site.site_key})`"
:value="site.site_key"
/>
</el-select>
</el-form-item>
<el-form-item label="啟用"><el-switch v-model="createForm.is_active" /></el-form-item>
<el-form-item label="同步 Keycloak"><el-switch v-model="createForm.sync_to_idp" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="showCreateDialog = false">取消</el-button>
<el-button type="primary" :loading="creating" @click="handleCreate">建立</el-button>
</template>
</el-dialog>
<el-dialog v-model="showEditDialog" title="編輯會員" width="760px" @close="resetEditForm">
<el-form :model="editForm" label-width="130px">
<el-form-item label="User Sub"><el-input :model-value="editForm.user_sub" disabled /></el-form-item>
<el-form-item label="Username"><el-input v-model="editForm.username" /></el-form-item>
<el-form-item label="Email"><el-input v-model="editForm.email" /></el-form-item>
<el-form-item label="顯示名稱"><el-input v-model="editForm.display_name" /></el-form-item>
<el-form-item label="所屬站台">
<el-select v-model="editForm.site_keys" multiple filterable clearable style="width: 100%">
<el-option
v-for="site in siteOptions"
:key="site.site_key"
:label="`${site.company_display_name} / ${site.display_name} (${site.site_key})`"
:value="site.site_key"
/>
</el-select>
</el-form-item>
<el-form-item label="啟用"><el-switch v-model="editForm.is_active" /></el-form-item>
<el-form-item label="同步 Keycloak"><el-switch v-model="editForm.sync_to_idp" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="showEditDialog = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleEdit">儲存</el-button>
</template>
</el-dialog>
<el-dialog v-model="showRolesDialog" :title="`會員角色:${selectedUserLabel}`" width="1080px">
<el-table :data="effectiveRoles" border stripe v-loading="rolesLoading">
<template #empty><el-empty description="此會員目前沒有角色" /></template>
<el-table-column prop="company_display_name" label="公司" min-width="160" />
<el-table-column prop="site_display_name" label="站台" min-width="170" />
<el-table-column prop="system_name" label="系統" min-width="150" />
<el-table-column prop="role_name" label="角色" min-width="160" />
<el-table-column prop="idp_role_name" label="Keycloak Role" min-width="190" />
</el-table>
<template #footer>
<el-button @click="showRolesDialog = false">關閉</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Refresh } from '@element-plus/icons-vue'
import { getSites } from '@/api/sites'
import {
getMembers,
createMember,
updateMember,
deleteMember,
resetMemberPassword,
getMemberSites,
setMemberSites,
getMemberRoles
} from '@/api/members'
const members = ref([])
const siteOptions = ref([])
const loading = ref(false)
const error = ref(false)
const errorMsg = ref('')
const showCreateDialog = ref(false)
const creating = ref(false)
const createFormRef = ref()
const createForm = ref({
username: '',
email: '',
display_name: '',
site_keys: [],
is_active: true,
sync_to_idp: true
})
const createRules = {
username: [{ required: true, message: '請輸入 Username', trigger: 'blur' }],
email: [{ required: true, message: '請輸入 Email', trigger: 'blur' }]
}
const showEditDialog = ref(false)
const saving = ref(false)
const editForm = ref({
user_sub: '',
username: '',
email: '',
display_name: '',
site_keys: [],
is_active: true,
sync_to_idp: true
})
const showRolesDialog = ref(false)
const selectedUserLabel = ref('')
const effectiveRoles = ref([])
const rolesLoading = ref(false)
async function loadCatalogs() {
const res = await getSites({ limit: 500, offset: 0 })
siteOptions.value = res.data?.items || []
}
async function load() {
loading.value = true
error.value = false
try {
const [membersRes] = await Promise.all([getMembers(), loadCatalogs()])
members.value = membersRes.data?.items || []
} catch (err) {
error.value = true
errorMsg.value = err.response?.data?.detail || '載入會員失敗'
} finally {
loading.value = false
}
}
function resetCreateForm() {
createForm.value = {
username: '',
email: '',
display_name: '',
site_keys: [],
is_active: true,
sync_to_idp: true
}
}
async function handleCreate() {
const valid = await createFormRef.value.validate().catch(() => false)
if (!valid) return
creating.value = true
try {
const payload = {
username: createForm.value.username || null,
email: createForm.value.email || null,
display_name: createForm.value.display_name || null,
is_active: createForm.value.is_active,
sync_to_idp: createForm.value.sync_to_idp
}
const res = await createMember(payload)
const userSub = res.data?.user_sub
if (userSub) {
await setMemberSites(userSub, createForm.value.site_keys || [])
}
ElMessage.success('新增會員成功')
showCreateDialog.value = false
resetCreateForm()
await load()
} catch (err) {
ElMessage.error(err.response?.data?.detail || '新增會員失敗')
} finally {
creating.value = false
}
}
async function openEdit(row) {
editForm.value = {
user_sub: row.user_sub,
username: row.username || '',
email: row.email || '',
display_name: row.display_name || '',
site_keys: [],
is_active: !!row.is_active,
sync_to_idp: true
}
try {
const res = await getMemberSites(row.user_sub)
editForm.value.site_keys = (res.data?.sites || []).map((site) => site.site_key)
} catch (_err) {
ElMessage.warning('讀取會員站台失敗,仍可編輯基本資料')
}
showEditDialog.value = true
}
function resetEditForm() {
editForm.value = {
user_sub: '',
username: '',
email: '',
display_name: '',
site_keys: [],
is_active: true,
sync_to_idp: true
}
}
async function handleEdit() {
saving.value = true
try {
await updateMember(editForm.value.user_sub, {
username: editForm.value.username || null,
email: editForm.value.email || null,
display_name: editForm.value.display_name || null,
is_active: editForm.value.is_active,
sync_to_idp: editForm.value.sync_to_idp
})
await setMemberSites(editForm.value.user_sub, editForm.value.site_keys || [])
ElMessage.success('更新會員成功')
showEditDialog.value = false
await load()
} catch (err) {
ElMessage.error(err.response?.data?.detail || '更新會員失敗')
} finally {
saving.value = false
}
}
async function handleResetPassword(row) {
try {
const res = await resetMemberPassword(row.user_sub)
const tempPassword = res.data?.temporary_password
if (tempPassword) {
await navigator.clipboard.writeText(tempPassword)
ElMessage.success('已重設密碼,臨時密碼已複製')
return
}
ElMessage.success('已重設密碼')
} catch (err) {
ElMessage.error(err.response?.data?.detail || '重設密碼失敗')
}
}
async function handleDelete(row) {
try {
await ElMessageBox.confirm(
`確認刪除會員 ${row.display_name || row.email || row.username || row.user_sub}`,
'刪除確認',
{ type: 'warning' }
)
await deleteMember(row.user_sub, true)
ElMessage.success('刪除成功')
await load()
} catch (err) {
if (err === 'cancel') return
ElMessage.error(err.response?.data?.detail || '刪除會員失敗')
}
}
async function openRoles(row) {
selectedUserLabel.value = `${row.display_name || row.username || row.user_sub}`
showRolesDialog.value = true
rolesLoading.value = true
try {
const res = await getMemberRoles(row.user_sub)
effectiveRoles.value = res.data?.roles || []
} catch (_err) {
ElMessage.error('載入會員角色失敗')
effectiveRoles.value = []
} finally {
rolesLoading.value = false
}
}
onMounted(load)
</script>