2026-07-30 17:59:43 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-08-01 12:17:33 +08:00
|
|
|
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
|
|
|
|
|
import { KeyRound, Pencil, Plus, ShieldCheck } from '@lucide/vue'
|
2026-07-31 15:34:35 +08:00
|
|
|
|
import {
|
2026-08-01 12:17:33 +08:00
|
|
|
|
assignPlatformInternalUserRoles,
|
|
|
|
|
|
changePlatformInternalUserStatus,
|
2026-07-31 15:34:35 +08:00
|
|
|
|
createPlatformInternalUser,
|
|
|
|
|
|
listPlatformInternalUsers,
|
|
|
|
|
|
listPlatformRoles,
|
2026-08-01 12:17:33 +08:00
|
|
|
|
resetPlatformInternalUserPassword,
|
|
|
|
|
|
updatePlatformInternalUser,
|
2026-07-31 15:34:35 +08:00
|
|
|
|
type PlatformInternalUser,
|
|
|
|
|
|
type PlatformRoleResource,
|
|
|
|
|
|
} from '@/api/platformIam'
|
|
|
|
|
|
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/api/http'
|
|
|
|
|
|
import ApiErrorMeta from '@/components/ApiErrorMeta.vue'
|
2026-07-30 17:59:43 +08:00
|
|
|
|
import StatusBadge from '@/components/StatusBadge.vue'
|
2026-08-01 12:17:33 +08:00
|
|
|
|
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
|
|
|
|
|
import QueryToolbar, { type QueryField, type QueryModel } from '@/components/common/QueryToolbar.vue'
|
2026-07-30 17:59:43 +08:00
|
|
|
|
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
|
|
|
|
|
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
2026-08-01 12:17:33 +08:00
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
2026-07-30 17:59:43 +08:00
|
|
|
|
|
|
|
|
|
|
const access = useAccessPreviewStore()
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const auth = useAuthStore()
|
2026-07-31 15:34:35 +08:00
|
|
|
|
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)
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const formRef = ref<FormInstance>()
|
|
|
|
|
|
const editingMembershipId = ref<string | null>(null)
|
|
|
|
|
|
const roleEditorOpen = ref(false)
|
|
|
|
|
|
const assigningRoles = ref(false)
|
|
|
|
|
|
const roleAssignmentUser = ref<PlatformInternalUser | null>(null)
|
|
|
|
|
|
const assignedRoleIds = ref<string[]>([])
|
|
|
|
|
|
const rowActionId = ref('')
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const page = ref(1)
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const pageSize = ref(10)
|
|
|
|
|
|
type InternalUserFilters = QueryModel & { keyword: string; status: string }
|
|
|
|
|
|
const filters = ref<InternalUserFilters>({ keyword: '', status: '' })
|
|
|
|
|
|
const appliedFilters = ref<InternalUserFilters>({ ...filters.value })
|
|
|
|
|
|
const queryFields: QueryField[] = [
|
|
|
|
|
|
{ key: 'keyword', type: 'input', placeholder: '登录账号、认证用户 ID、姓名或成员编号' },
|
|
|
|
|
|
{ key: 'status', type: 'select', placeholder: '全部状态', options: [{ label: '正常', value: 'ACTIVE' }, { label: '停用', value: 'DISABLED' }] },
|
|
|
|
|
|
]
|
|
|
|
|
|
const emptyForm = () => ({ authUserId: '', userType: 'INTERNAL', displayName: '', phone: '', email: '', memberNo: '', roleIds: [] as string[] })
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const form = reactive(emptyForm())
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const formRules = computed<FormRules>(() => ({
|
|
|
|
|
|
authUserId: [
|
|
|
|
|
|
{ required: !editingMembershipId.value, message: '请输入认证用户 ID', trigger: 'blur' },
|
|
|
|
|
|
{ pattern: /^[1-9]\d*$/, message: '认证用户 ID 必须为正整数', trigger: 'blur' },
|
|
|
|
|
|
],
|
|
|
|
|
|
displayName: [{ required: true, whitespace: true, message: '请输入姓名', trigger: 'blur' }],
|
|
|
|
|
|
userType: editingMembershipId.value ? [] : [{ required: true, message: '请选择用户类型', trigger: 'change' }],
|
|
|
|
|
|
memberNo: editingMembershipId.value ? [{ required: true, whitespace: true, message: '请输入成员编号', trigger: 'blur' }] : [],
|
|
|
|
|
|
email: [{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }],
|
|
|
|
|
|
}))
|
|
|
|
|
|
const editorLabel = computed(() => editingMembershipId.value ? '编辑内部用户' : '新增内部用户')
|
2026-07-30 17:59:43 +08:00
|
|
|
|
const { captureBaseline } = useUnsavedWorkRegistration({
|
|
|
|
|
|
key: 'internal-user-editor',
|
2026-08-01 12:17:33 +08:00
|
|
|
|
label: editorLabel,
|
2026-07-30 17:59:43 +08:00
|
|
|
|
isOpen: editorOpen,
|
|
|
|
|
|
model: form,
|
2026-08-01 12:17:33 +08:00
|
|
|
|
requiredPermission: computed(() => editingMembershipId.value ? 'platform:user:edit' : 'platform:user:create'),
|
2026-07-30 17:59:43 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
2026-07-31 15:34:35 +08:00
|
|
|
|
const canCreate = computed(() => access.can('platform:user:create'))
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const canEdit = computed(() => access.can('platform:user:edit'))
|
|
|
|
|
|
const canChangeStatus = computed(() => access.can('platform:user:status'))
|
|
|
|
|
|
const canResetPassword = computed(() => access.can('platform:user:reset-password'))
|
|
|
|
|
|
const canGrantRoles = computed(() => access.can('platform:user:grant-role'))
|
2026-07-31 15:34:35 +08:00
|
|
|
|
const roleNames = computed(() => new Map(roles.value.map((role) => [String(role.id), role.roleName])))
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const roleOptions = computed(() => roles.value.filter((role) => role.status === 'ACTIVE' || form.roleIds.includes(String(role.id)) || assignedRoleIds.value.includes(String(role.id))))
|
2026-07-31 15:34:35 +08:00
|
|
|
|
const filteredRows = computed(() => users.value.filter((item) => {
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const keyword = appliedFilters.value.keyword.trim().toLowerCase()
|
2026-07-31 15:34:35 +08:00
|
|
|
|
const matchesKeyword = !keyword || [item.displayName, item.username, item.memberNo ?? '', String(item.authUserId)]
|
|
|
|
|
|
.some((value) => value.toLowerCase().includes(keyword))
|
2026-08-01 12:17:33 +08:00
|
|
|
|
return matchesKeyword && (!appliedFilters.value.status || item.status === appliedFilters.value.status)
|
2026-07-30 17:59:43 +08:00
|
|
|
|
}))
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const pagedRows = computed(() => filteredRows.value.slice((page.value - 1) * pageSize.value, page.value * pageSize.value))
|
|
|
|
|
|
const userColumns: DataTableColumn[] = [
|
|
|
|
|
|
{ key: 'actions', label: '操作', width: 355, fixed: 'left', slotName: 'actions' },
|
|
|
|
|
|
{ prop: 'displayName', label: '姓名', minWidth: 150 },
|
|
|
|
|
|
{ prop: 'username', label: '登录账号', minWidth: 150 },
|
|
|
|
|
|
{ prop: 'authUserId', label: '认证用户 ID', minWidth: 145 },
|
|
|
|
|
|
{ prop: 'memberNo', label: '成员编号', minWidth: 170 },
|
|
|
|
|
|
{ prop: 'memberType', label: '成员类型', minWidth: 130 },
|
|
|
|
|
|
{ key: 'roles', label: '角色', minWidth: 190, slotName: 'roles' },
|
|
|
|
|
|
{ key: 'status', label: '状态', width: 100, slotName: 'status' },
|
|
|
|
|
|
]
|
2026-07-30 17:59:43 +08:00
|
|
|
|
|
2026-07-31 15:34:35 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 12:17:33 +08:00
|
|
|
|
function applyFilters() {
|
|
|
|
|
|
appliedFilters.value = { ...filters.value }
|
|
|
|
|
|
page.value = 1
|
|
|
|
|
|
void loadUsers()
|
|
|
|
|
|
}
|
2026-07-31 15:34:35 +08:00
|
|
|
|
function roleLabel(roleId: number | string) { return roleNames.value.get(String(roleId)) ?? `角色 #${roleId}` }
|
2026-08-01 12:17:33 +08:00
|
|
|
|
function openCreate() {
|
|
|
|
|
|
Object.assign(form, emptyForm())
|
|
|
|
|
|
editingMembershipId.value = null
|
|
|
|
|
|
captureBaseline()
|
|
|
|
|
|
editorOpen.value = true
|
|
|
|
|
|
nextTick(() => formRef.value?.clearValidate())
|
|
|
|
|
|
}
|
|
|
|
|
|
function openEdit(row: PlatformInternalUser) {
|
|
|
|
|
|
if (!canEdit.value) return
|
|
|
|
|
|
editingMembershipId.value = String(row.membershipId)
|
|
|
|
|
|
Object.assign(form, {
|
|
|
|
|
|
authUserId: String(row.authUserId),
|
|
|
|
|
|
userType: row.memberType,
|
|
|
|
|
|
displayName: row.displayName,
|
|
|
|
|
|
phone: row.phone ?? '',
|
|
|
|
|
|
email: row.email ?? '',
|
|
|
|
|
|
memberNo: row.memberNo ?? '',
|
|
|
|
|
|
roleIds: row.roleIds.map(String),
|
|
|
|
|
|
})
|
|
|
|
|
|
captureBaseline()
|
|
|
|
|
|
editorOpen.value = true
|
|
|
|
|
|
nextTick(() => formRef.value?.clearValidate())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function openRoleAssignment(row: PlatformInternalUser) {
|
|
|
|
|
|
if (!canGrantRoles.value) return
|
|
|
|
|
|
roleAssignmentUser.value = row
|
|
|
|
|
|
assignedRoleIds.value = row.roleIds.map(String)
|
|
|
|
|
|
roleEditorOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isCurrentUser(row: PlatformInternalUser) {
|
|
|
|
|
|
return String(row.userId) === auth.user?.id || String(row.authUserId) === auth.user?.id
|
|
|
|
|
|
}
|
2026-07-30 17:59:43 +08:00
|
|
|
|
|
2026-07-31 15:34:35 +08:00
|
|
|
|
async function saveUser() {
|
2026-08-01 12:17:33 +08:00
|
|
|
|
const valid = await formRef.value?.validate().catch(() => false)
|
|
|
|
|
|
if (!valid) return
|
2026-07-31 15:34:35 +08:00
|
|
|
|
saving.value = true
|
2026-07-30 17:59:43 +08:00
|
|
|
|
try {
|
2026-08-01 12:17:33 +08:00
|
|
|
|
if (editingMembershipId.value) {
|
|
|
|
|
|
await updatePlatformInternalUser(editingMembershipId.value, {
|
|
|
|
|
|
displayName: form.displayName.trim(),
|
|
|
|
|
|
phone: form.phone.trim(),
|
|
|
|
|
|
email: form.email.trim(),
|
|
|
|
|
|
memberNo: form.memberNo.trim(),
|
|
|
|
|
|
roleIds: form.roleIds,
|
|
|
|
|
|
})
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await createPlatformInternalUser({
|
|
|
|
|
|
authUserId: form.authUserId.trim(),
|
|
|
|
|
|
userType: form.userType,
|
|
|
|
|
|
displayName: form.displayName.trim(),
|
|
|
|
|
|
phone: form.phone.trim(),
|
|
|
|
|
|
email: form.email.trim(),
|
|
|
|
|
|
memberNo: form.memberNo.trim() || undefined,
|
|
|
|
|
|
roleIds: canGrantRoles.value ? form.roleIds : undefined,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-07-31 15:34:35 +08:00
|
|
|
|
editorOpen.value = false
|
2026-08-01 12:17:33 +08:00
|
|
|
|
ElMessage.success(editingMembershipId.value ? '内部用户已更新' : '内部用户已创建')
|
2026-07-31 15:34:35 +08:00
|
|
|
|
await loadUsers()
|
|
|
|
|
|
} catch (error) {
|
2026-08-01 12:17:33 +08:00
|
|
|
|
ElMessage.error(getApiErrorMessage(error, editingMembershipId.value ? '内部用户更新失败,请稍后重试' : '内部用户创建失败,请稍后重试'))
|
2026-07-31 15:34:35 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false
|
2026-07-30 17:59:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-01 12:17:33 +08:00
|
|
|
|
async function toggleStatus(row: PlatformInternalUser) {
|
|
|
|
|
|
const target = row.status === 'ACTIVE' ? '停用' : '启用'
|
|
|
|
|
|
try {
|
|
|
|
|
|
await ElMessageBox.confirm(`确认${target}内部用户“${row.displayName}”吗?`, '用户状态', { confirmButtonText: '确认', cancelButtonText: '取消' })
|
|
|
|
|
|
rowActionId.value = String(row.membershipId)
|
|
|
|
|
|
const updated = await changePlatformInternalUserStatus(row.membershipId, target === '启用' ? 'ACTIVE' : 'DISABLED')
|
|
|
|
|
|
Object.assign(row, updated)
|
|
|
|
|
|
ElMessage.success(`用户已${target}`)
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error === 'cancel' || error === 'close') return
|
|
|
|
|
|
ElMessage.error(getApiErrorMessage(error, '用户状态更新失败,请稍后重试'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
rowActionId.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function resetPassword(row: PlatformInternalUser) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { value } = await ElMessageBox.prompt(`为“${row.displayName}”设置新密码`, '重置密码', {
|
|
|
|
|
|
confirmButtonText: '确认重置',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
inputType: 'password',
|
|
|
|
|
|
inputPlaceholder: '8-64 位,必须包含字母和数字',
|
|
|
|
|
|
inputValidator: (password) => /^(?=.*[A-Za-z])(?=.*\d).{8,64}$/.test(password) || '密码需为 8-64 位,且同时包含字母和数字',
|
|
|
|
|
|
})
|
|
|
|
|
|
rowActionId.value = String(row.membershipId)
|
|
|
|
|
|
await resetPlatformInternalUserPassword(row.membershipId, value)
|
|
|
|
|
|
ElMessage.success('密码已重置')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
if (error === 'cancel' || error === 'close') return
|
|
|
|
|
|
ElMessage.error(getApiErrorMessage(error, '密码重置失败,请稍后重试'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
rowActionId.value = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function saveRoleAssignment() {
|
|
|
|
|
|
if (!roleAssignmentUser.value) return
|
|
|
|
|
|
assigningRoles.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const updated = await assignPlatformInternalUserRoles(roleAssignmentUser.value.membershipId, assignedRoleIds.value)
|
|
|
|
|
|
const index = users.value.findIndex((item) => String(item.membershipId) === String(updated.membershipId))
|
|
|
|
|
|
if (index >= 0) users.value[index] = updated
|
|
|
|
|
|
roleEditorOpen.value = false
|
|
|
|
|
|
ElMessage.success('用户角色已更新')
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
ElMessage.error(getApiErrorMessage(error, '用户角色更新失败,请稍后重试'))
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
assigningRoles.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 15:34:35 +08:00
|
|
|
|
onMounted(loadUsers)
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
|
<div class="business-page">
|
2026-07-31 15:34:35 +08:00
|
|
|
|
<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>
|
2026-08-03 11:45:45 +08:00
|
|
|
|
<QueryToolbar v-model="filters" class="surface-panel" ariaLabel="内部用户筛选" :fields="queryFields" @search="applyFilters" />
|
2026-07-30 17:59:43 +08:00
|
|
|
|
<section class="surface-panel table-panel">
|
2026-08-03 11:45:45 +08:00
|
|
|
|
<header class="panel-heading"><div><h2>内部用户 <span class="secondary-text">共 {{ filteredRows.length }} 条</span></h2><p>数据和维护操作均由平台 IAM 提供,角色关系按成员维度保存。</p></div><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增用户</el-button></header>
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<DataTable v-model:page="page" v-model:page-size="pageSize" :data="pagedRows" :columns="userColumns" :loading="loading" :total="filteredRows.length" row-key="membershipId" empty-text="没有符合条件的内部用户">
|
|
|
|
|
|
<template #actions="{ row }"><div v-if="canEdit || canChangeStatus || canResetPassword || canGrantRoles" class="table-actions"><el-button v-if="canEdit" link type="primary" :disabled="rowActionId === String((row as PlatformInternalUser).membershipId)" @click="openEdit(row as PlatformInternalUser)"><Pencil :size="15" />编辑</el-button><el-button v-if="canGrantRoles" link type="primary" :disabled="rowActionId === String((row as PlatformInternalUser).membershipId)" @click="openRoleAssignment(row as PlatformInternalUser)"><ShieldCheck :size="15" />分配角色</el-button><el-button v-if="canChangeStatus" link :type="(row as PlatformInternalUser).status === 'ACTIVE' ? 'danger' : 'success'" :loading="rowActionId === String((row as PlatformInternalUser).membershipId)" :disabled="isCurrentUser(row as PlatformInternalUser)" :title="isCurrentUser(row as PlatformInternalUser) ? '不能停用当前登录用户' : undefined" @click="toggleStatus(row as PlatformInternalUser)">{{ (row as PlatformInternalUser).status === 'ACTIVE' ? '停用' : '启用' }}</el-button><el-button v-if="canResetPassword" link type="warning" :loading="rowActionId === String((row as PlatformInternalUser).membershipId)" @click="resetPassword(row as PlatformInternalUser)"><KeyRound :size="15" />重置密码</el-button></div><span v-else class="secondary-text">仅查看</span></template>
|
|
|
|
|
|
<template #roles="{ row }"><div class="tag-list"><el-tag v-for="roleId in (row as PlatformInternalUser).roleIds" :key="roleId" effect="plain" size="small">{{ roleLabel(roleId) }}</el-tag><span v-if="(row as PlatformInternalUser).roleIds.length === 0" class="secondary-text">未分配</span></div></template>
|
|
|
|
|
|
<template #status="{ row }"><StatusBadge :status="(row as PlatformInternalUser).status === 'ACTIVE' ? '正常' : '停用'" /></template>
|
|
|
|
|
|
</DataTable>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</section>
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<el-drawer v-model="editorOpen" :title="editorLabel" size="560px" class="business-drawer editor-drawer" :modal="false" :close-on-click-modal="false">
|
|
|
|
|
|
<el-form ref="formRef" :model="form" :rules="formRules" label-position="top" class="drawer-form">
|
2026-07-31 15:34:35 +08:00
|
|
|
|
<div class="form-section"><h3>用户信息</h3><div class="form-grid two-cols">
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<el-form-item label="认证用户 ID" prop="authUserId" :required="!editingMembershipId"><el-input v-model="form.authUserId" :disabled="Boolean(editingMembershipId)" inputmode="numeric" placeholder="输入已有认证账号的数字 ID" /></el-form-item>
|
|
|
|
|
|
<el-form-item label="姓名" prop="displayName" required><el-input v-model="form.displayName" placeholder="如:张三" maxlength="64" /></el-form-item>
|
|
|
|
|
|
<el-form-item label="用户类型" prop="userType" :required="!editingMembershipId"><el-select v-model="form.userType" :disabled="Boolean(editingMembershipId)"><el-option label="平台内部用户" value="INTERNAL" /><el-option label="平台管理员" value="PLATFORM" /></el-select></el-form-item>
|
|
|
|
|
|
<el-form-item label="手机号"><el-input v-model="form.phone" placeholder="如:13800138000" inputmode="tel" /></el-form-item>
|
|
|
|
|
|
<el-form-item label="邮箱" prop="email"><el-input v-model="form.email" placeholder="如:name@example.com" inputmode="email" /></el-form-item>
|
|
|
|
|
|
<el-form-item v-if="editingMembershipId" label="成员编号" prop="memberNo" required><el-input v-model="form.memberNo" placeholder="如:PLATFORM-0001" maxlength="64" /></el-form-item>
|
|
|
|
|
|
<el-form-item v-if="canGrantRoles" label="角色"><el-select v-model="form.roleIds" multiple collapse-tags collapse-tags-tooltip placeholder="选择用户角色"><el-option v-for="role in roleOptions" :key="role.id" :label="role.roleName" :value="String(role.id)" :disabled="role.status !== 'ACTIVE'" /></el-select></el-form-item>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</div></div>
|
|
|
|
|
|
</el-form>
|
2026-08-01 12:17:33 +08:00
|
|
|
|
<template #footer><div class="drawer-footer"><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button v-if="editingMembershipId ? canEdit : canCreate" type="primary" :loading="saving" @click="saveUser">{{ editingMembershipId ? '保存修改' : '创建用户' }}</el-button></div></template>
|
|
|
|
|
|
</el-drawer>
|
|
|
|
|
|
<el-drawer v-model="roleEditorOpen" :title="roleAssignmentUser ? `分配角色:${roleAssignmentUser.displayName}` : '分配角色'" size="520px" 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><p class="secondary-text">保存后按后端契约全量替换当前用户的角色;不选择角色表示清空。</p><el-form-item label="角色"><el-select v-model="assignedRoleIds" multiple collapse-tags collapse-tags-tooltip placeholder="选择用户角色"><el-option v-for="role in roleOptions" :key="role.id" :label="role.roleName" :value="String(role.id)" :disabled="role.status !== 'ACTIVE'" /></el-select></el-form-item></div></el-form>
|
|
|
|
|
|
<template #footer><div class="drawer-footer"><el-button :disabled="assigningRoles" @click="roleEditorOpen = false">取消</el-button><el-button type="primary" :loading="assigningRoles" @click="saveRoleAssignment">保存角色</el-button></div></template>
|
2026-07-30 17:59:43 +08:00
|
|
|
|
</el-drawer>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|