修复bug
This commit is contained in:
@@ -1,128 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { KeyRound, Pencil, Plus, RotateCcw, Search, UserRoundCog } from '@lucide/vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus, RotateCcw, Search } from '@lucide/vue'
|
||||
import {
|
||||
createPlatformInternalUser,
|
||||
listPlatformInternalUsers,
|
||||
listPlatformRoles,
|
||||
type PlatformInternalUser,
|
||||
type PlatformRoleResource,
|
||||
} from '@/api/platformIam'
|
||||
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
||||
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
||||
import MetricStrip from '@/components/MetricStrip.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
||||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||||
import { useAdminDataStore } from '@/stores/adminData'
|
||||
import type { InternalUser } from '@/types/admin'
|
||||
|
||||
const store = useAdminDataStore()
|
||||
const access = useAccessPreviewStore()
|
||||
const users = ref<PlatformInternalUser[]>([])
|
||||
const roles = ref<PlatformRoleResource[]>([])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const loadError = ref<ApiErrorDetails | null>(null)
|
||||
const editorOpen = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = 10
|
||||
const editorOpen = ref(false)
|
||||
const filters = reactive({ keyword: '', department: '', roleId: '', status: '' })
|
||||
const emptyForm = (): Omit<InternalUser, 'lastLogin' | 'createdAt'> => ({ id: '', account: '', name: '', department: '', phone: '', email: '', roleIds: [], status: '正常' })
|
||||
const filters = reactive({ keyword: '', status: '' })
|
||||
const emptyForm = () => ({ authUserId: '', userType: 'PLATFORM_STAFF', displayName: '', phone: '', email: '' })
|
||||
const form = reactive(emptyForm())
|
||||
const editorLabel = computed(() => form.id ? '编辑内部用户' : '新增内部用户')
|
||||
const requiredPermission = computed(() => 'system:user:write')
|
||||
const { captureBaseline } = useUnsavedWorkRegistration({
|
||||
key: 'internal-user-editor',
|
||||
label: editorLabel,
|
||||
label: computed(() => '新增内部用户'),
|
||||
isOpen: editorOpen,
|
||||
model: form,
|
||||
requiredPermission,
|
||||
requiredPermission: computed(() => 'platform:user:create'),
|
||||
})
|
||||
|
||||
const roleName = (id: string) => store.roles.find((role) => role.id === id)?.name || id
|
||||
const filteredRows = computed(() => store.users.filter((item) => {
|
||||
const canCreate = computed(() => access.can('platform:user:create'))
|
||||
const roleNames = computed(() => new Map(roles.value.map((role) => [String(role.id), role.roleName])))
|
||||
const filteredRows = computed(() => users.value.filter((item) => {
|
||||
const keyword = filters.keyword.trim().toLowerCase()
|
||||
const matchesKeyword = !keyword || [item.account, item.name, item.phone, item.email].some((value) => value.toLowerCase().includes(keyword))
|
||||
return matchesKeyword && (!filters.department || item.department === filters.department) && (!filters.roleId || item.roleIds.includes(filters.roleId)) && (!filters.status || item.status === filters.status)
|
||||
const matchesKeyword = !keyword || [item.displayName, item.username, item.memberNo ?? '', String(item.authUserId)]
|
||||
.some((value) => value.toLowerCase().includes(keyword))
|
||||
return matchesKeyword && (!filters.status || item.status === filters.status)
|
||||
}))
|
||||
const pagedRows = computed(() => filteredRows.value.slice((page.value - 1) * pageSize, page.value * pageSize))
|
||||
const departments = computed(() => [...new Set(store.users.map((item) => item.department))])
|
||||
const metrics = computed(() => [
|
||||
{ label: '内部用户', value: store.users.length, note: '平台员工账号' },
|
||||
{ label: '正常账号', value: store.activeUsers, note: '当前可登录' },
|
||||
{ label: '已停用', value: store.users.filter((item) => item.status === '停用').length, note: '无法登录平台' },
|
||||
{ label: '角色覆盖', value: store.roles.length, note: '已配置角色' },
|
||||
{ label: '内部用户', value: users.value.length, note: 'IAM 返回的用户' },
|
||||
{ label: '正常账号', value: users.value.filter((item) => item.status === 'ACTIVE').length, note: '当前有效' },
|
||||
{ label: '停用账号', value: users.value.filter((item) => item.status !== 'ACTIVE').length, note: '只读展示' },
|
||||
])
|
||||
|
||||
watch(filters, () => { page.value = 1 })
|
||||
|
||||
function resetFilters() { Object.assign(filters, { keyword: '', department: '', roleId: '', status: '' }) }
|
||||
async function loadUsers() {
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
users.value = []
|
||||
try {
|
||||
const [userRecords, roleRecords] = await Promise.all([listPlatformInternalUsers(), listPlatformRoles()])
|
||||
users.value = userRecords
|
||||
roles.value = roleRecords
|
||||
} catch (error) {
|
||||
loadError.value = getApiErrorDetails(error, '内部用户列表加载失败,请稍后重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetFilters() { Object.assign(filters, { keyword: '', status: '' }) }
|
||||
function roleLabel(roleId: number | string) { return roleNames.value.get(String(roleId)) ?? `角色 #${roleId}` }
|
||||
function openCreate() { Object.assign(form, emptyForm()); captureBaseline(); editorOpen.value = true }
|
||||
function openEdit(row: InternalUser) { Object.assign(form, { ...row, roleIds: [...row.roleIds] }); captureBaseline(); editorOpen.value = true }
|
||||
|
||||
function saveUser() {
|
||||
if (!form.account.trim() || !form.name.trim() || !form.department.trim() || form.roleIds.length === 0) {
|
||||
ElMessage.warning('请填写账号、姓名、部门并至少分配一个角色')
|
||||
async function saveUser() {
|
||||
if (!form.authUserId.trim() || !form.displayName.trim() || !form.userType.trim()) {
|
||||
ElMessage.warning('请填写认证用户 ID、姓名和用户类型')
|
||||
return
|
||||
}
|
||||
if (!form.id && store.users.some((item) => item.account === form.account)) {
|
||||
ElMessage.error('该登录账号已存在,请更换账号')
|
||||
return
|
||||
}
|
||||
store.saveUser({ ...form, id: form.id || undefined, roleIds: [...form.roleIds] })
|
||||
editorOpen.value = false
|
||||
ElMessage.success(form.id ? '用户资料已更新' : '内部用户已创建')
|
||||
}
|
||||
|
||||
async function resetPassword(row: InternalUser) {
|
||||
saving.value = true
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认重置“${row.name}”的登录密码吗?`, '重置密码', { confirmButtonText: '确认重置', cancelButtonText: '取消', type: 'warning' })
|
||||
ElMessage.success('密码已重置,请通知用户首次登录后修改密码')
|
||||
} catch {
|
||||
// 用户取消,不需要反馈。
|
||||
await createPlatformInternalUser({ ...form, authUserId: form.authUserId.trim() })
|
||||
editorOpen.value = false
|
||||
ElMessage.success('内部用户已创建')
|
||||
await loadUsers()
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorMessage(error, '内部用户创建失败,请稍后重试'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleStatus(row: InternalUser) {
|
||||
if (row.account === 'admin') { ElMessage.warning('系统管理员账号不能停用'); return }
|
||||
const target = row.status === '正常' ? '停用' : '正常'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认${target === '停用' ? '停用' : '启用'}“${row.name}”的账号吗?`, '账号状态', { confirmButtonText: '确认', cancelButtonText: '取消' })
|
||||
store.setUserStatus(row.id, target)
|
||||
ElMessage.success(`账号已${target === '正常' ? '启用' : '停用'}`)
|
||||
} catch {
|
||||
// 用户取消,不需要反馈。
|
||||
}
|
||||
}
|
||||
onMounted(loadUsers)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="business-page">
|
||||
<MetricStrip :items="metrics" />
|
||||
<el-alert type="info" :closable="false" show-icon title="成员列表已返回当前角色关系;编辑、启停、重置密码及角色分配操作尚未接入本页面。" />
|
||||
<el-alert v-if="loadError" type="error" :closable="false" show-icon role="alert">
|
||||
<template #title><div class="api-error-alert-title"><span>{{ loadError.message }}</span><ApiErrorMeta :details="loadError" /></div><el-button link type="danger" @click="loadUsers">重新加载</el-button></template>
|
||||
</el-alert>
|
||||
<section class="surface-panel filter-panel" aria-label="内部用户筛选">
|
||||
<div class="filter-fields">
|
||||
<el-input v-model="filters.keyword" clearable placeholder="账号、姓名、手机或邮箱" aria-label="用户关键词"><template #prefix><Search :size="16" /></template></el-input>
|
||||
<el-select v-model="filters.department" clearable placeholder="全部部门" aria-label="用户部门"><el-option v-for="item in departments" :key="item" :label="item" :value="item" /></el-select>
|
||||
<el-select v-model="filters.roleId" clearable placeholder="全部角色" aria-label="用户角色"><el-option v-for="item in store.roles" :key="item.id" :label="item.name" :value="item.id" /></el-select>
|
||||
<el-select v-model="filters.status" clearable placeholder="全部状态" aria-label="用户状态"><el-option label="正常" value="正常" /><el-option label="停用" value="停用" /></el-select>
|
||||
<el-input v-model="filters.keyword" clearable placeholder="登录账号、认证用户 ID、姓名或成员编号" aria-label="用户关键词"><template #prefix><Search :size="16" /></template></el-input>
|
||||
<el-select v-model="filters.status" clearable placeholder="全部状态" aria-label="用户状态"><el-option label="正常" value="ACTIVE" /><el-option label="停用" value="INACTIVE" /></el-select>
|
||||
</div>
|
||||
<div class="filter-actions"><el-button @click="resetFilters"><RotateCcw :size="16" />重置</el-button><el-button v-if="access.can('system:user:write')" type="primary" @click="openCreate"><Plus :size="16" />新增用户</el-button></div>
|
||||
<div class="filter-actions"><el-button @click="resetFilters"><RotateCcw :size="16" />重置</el-button><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增用户</el-button></div>
|
||||
</section>
|
||||
|
||||
<section class="surface-panel table-panel">
|
||||
<header class="panel-heading"><div><h2>内部用户</h2><p>维护平台员工账号,并通过角色控制菜单和操作权限。</p></div></header>
|
||||
<el-table :data="pagedRows" row-key="id" empty-text="没有符合条件的内部用户">
|
||||
<el-table-column label="操作" width="250" fixed="left"><template #default="{ row }"><div v-if="access.can('system:user:write')" class="table-actions"><el-button link type="primary" @click="openEdit(row as InternalUser)"><Pencil :size="15" />编辑</el-button><el-button link type="primary" @click="resetPassword(row as InternalUser)"><KeyRound :size="15" />重置密码</el-button><el-button link :type="row.status === '正常' ? 'danger' : 'success'" :disabled="row.account === 'admin'" @click="toggleStatus(row as InternalUser)">{{ row.status === '正常' ? '停用' : '启用' }}</el-button></div></template></el-table-column>
|
||||
<el-table-column label="用户" min-width="190"><template #default="{ row }"><div class="user-cell"><span>{{ row.name.slice(0,1) }}</span><div><strong>{{ row.name }}</strong><small>{{ row.account }}</small></div></div></template></el-table-column>
|
||||
<el-table-column prop="department" label="部门" width="110" />
|
||||
<el-table-column label="角色" min-width="190"><template #default="{ row }"><div class="tag-list"><el-tag v-for="roleId in row.roleIds" :key="roleId" effect="plain" size="small">{{ roleName(roleId) }}</el-tag></div></template></el-table-column>
|
||||
<el-table-column label="联系方式" min-width="190"><template #default="{ row }"><div class="primary-cell"><strong>{{ row.phone }}</strong><span>{{ row.email }}</span></div></template></el-table-column>
|
||||
<el-table-column prop="lastLogin" label="最后登录" width="150" />
|
||||
<el-table-column label="状态" width="88"><template #default="{ row }"><StatusBadge :status="row.status" /></template></el-table-column>
|
||||
<header class="panel-heading"><div><h2>内部用户</h2><p>数据来自平台 IAM,未开放的操作不会在前端模拟执行。</p></div></header>
|
||||
<el-table v-loading="loading" :data="pagedRows" row-key="membershipId" empty-text="没有符合条件的内部用户">
|
||||
<el-table-column label="操作" width="100" fixed="left"><template #default><span class="secondary-text">仅查看</span></template></el-table-column>
|
||||
<el-table-column prop="displayName" label="姓名" min-width="150" />
|
||||
<el-table-column prop="username" label="登录账号" min-width="150" />
|
||||
<el-table-column prop="authUserId" label="认证用户 ID" min-width="145" />
|
||||
<el-table-column prop="memberNo" label="成员编号" min-width="170" />
|
||||
<el-table-column prop="memberType" label="成员类型" min-width="130" />
|
||||
<el-table-column label="角色" min-width="190"><template #default="{ row }"><div class="tag-list"><el-tag v-for="roleId in row.roleIds" :key="roleId" effect="plain" size="small">{{ roleLabel(roleId) }}</el-tag><span v-if="row.roleIds.length === 0" class="secondary-text">未分配</span></div></template></el-table-column>
|
||||
<el-table-column label="状态" width="100"><template #default="{ row }"><StatusBadge :status="row.status === 'ACTIVE' ? '正常' : '停用'" /></template></el-table-column>
|
||||
</el-table>
|
||||
<footer class="table-footer"><span>显示 {{ pagedRows.length }} 条,共 {{ filteredRows.length }} 条</span><el-pagination v-model:current-page="page" :page-size="pageSize" :total="filteredRows.length" layout="prev, pager, next" /></footer>
|
||||
</section>
|
||||
|
||||
<el-drawer v-model="editorOpen" :title="form.id ? '编辑内部用户' : '新增内部用户'" size="620px" class="business-drawer editor-drawer" :modal="false" :close-on-click-modal="false">
|
||||
<el-drawer v-model="editorOpen" title="新增内部用户" size="560px" class="business-drawer editor-drawer" :modal="false" :close-on-click-modal="false">
|
||||
<el-form label-position="top" class="drawer-form">
|
||||
<div class="form-section"><h3>账号信息</h3><div class="form-grid two-cols">
|
||||
<el-form-item label="登录账号" required><el-input v-model="form.account" :disabled="Boolean(form.id)" placeholder="英文、数字或下划线" /></el-form-item>
|
||||
<el-form-item label="姓名" required><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="所属部门" required><el-select v-model="form.department" filterable allow-create placeholder="请选择或输入部门"><el-option v-for="item in departments" :key="item" :label="item" :value="item" /></el-select></el-form-item>
|
||||
<el-form-item label="账号状态"><el-radio-group v-model="form.status"><el-radio-button label="正常" value="正常" /><el-radio-button label="停用" value="停用" /></el-radio-group></el-form-item>
|
||||
<div class="form-section"><h3>用户信息</h3><div class="form-grid two-cols">
|
||||
<el-form-item label="认证用户 ID" required><el-input v-model="form.authUserId" placeholder="已存在的认证账号 ID" /></el-form-item>
|
||||
<el-form-item label="姓名" required><el-input v-model="form.displayName" maxlength="64" /></el-form-item>
|
||||
<el-form-item label="用户类型" required><el-input v-model="form.userType" maxlength="32" /></el-form-item>
|
||||
<el-form-item label="手机号"><el-input v-model="form.phone" /></el-form-item>
|
||||
<el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item>
|
||||
</div></div>
|
||||
<div class="form-section"><h3>联系方式</h3><div class="form-grid two-cols"><el-form-item label="手机号"><el-input v-model="form.phone" /></el-form-item><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></div></div>
|
||||
<div class="form-section"><h3>角色分配</h3><el-form-item label="平台角色" required><el-select v-model="form.roleIds" multiple collapse-tags placeholder="至少选择一个角色"><el-option v-for="item in store.roles.filter((role) => role.status === '启用')" :key="item.id" :label="item.name" :value="item.id" /></el-select></el-form-item><p class="form-help"><UserRoundCog :size="16" />用户最终权限为所分配角色权限的合集。</p></div>
|
||||
</el-form>
|
||||
<template #footer><div class="drawer-footer"><el-button @click="editorOpen = false">取消</el-button><el-button v-if="access.can('system:user:write')" type="primary" @click="saveUser">保存用户</el-button></div></template>
|
||||
<template #footer><div class="drawer-footer"><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button v-if="canCreate" type="primary" :loading="saving" @click="saveUser">创建用户</el-button></div></template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user