Files
admin-web/src/modules/iam/pages/InternalUsersView.vue
T

479 lines
25 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.
<script setup lang="ts">
import '@/styles/adminDensity.css'
import '../iamDensity.css'
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import { Plus } from '@lucide/vue'
import {
changePlatformManagedUserStatus,
createPlatformManagedUser,
getPlatformUserOptions,
listSharedReceivingOverseasWarehouseOptions,
listPlatformManagedUsers,
resetPlatformManagedUserPassword,
updatePlatformManagedUser,
type PlatformManagedUser,
type PlatformUserOptions,
type SharedReceivingOverseasWarehouseOption,
} from '@/modules/iam/api/platformIam'
import { getApiErrorDetails, getApiErrorMessage, type ApiErrorDetails } from '@/shared/api/http'
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
import StatusBadge from '@/shared/components/StatusBadge.vue'
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
import AdminPage from '@/shared/components/AdminPage.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import InlineHelp from '@/shared/components/InlineHelp.vue'
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
import { useAccessPreviewStore } from '@/core/access/accessPreview'
import { useAuthStore } from '@/core/auth/auth'
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
const access = useAccessPreviewStore()
const auth = useAuthStore()
const users = ref<PlatformManagedUser[]>([])
const options = ref<PlatformUserOptions>({ tenants: [], roles: [] })
const overseasWarehouses = ref<SharedReceivingOverseasWarehouseOption[]>([])
const overseasWarehousesLoading = ref(false)
const overseasWarehousesError = ref<ApiErrorDetails | null>(null)
const domesticWarehouses = ref<PlatformWarehouseSummaryResource[]>([])
const domesticWarehousesLoading = ref(false)
const domesticWarehousesError = ref<ApiErrorDetails | null>(null)
const loading = ref(false)
const saving = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const editorOpen = ref(false)
const formRef = ref<FormInstance>()
const editingUser = ref<PlatformManagedUser | null>(null)
const rowActionId = ref('')
let loadRequestId = 0
let warehouseRequestId = 0
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
type UserFilters = QueryModel & { keyword: string; status: string }
const filters = ref<UserFilters>({ keyword: '', status: '' })
const appliedFilters = ref<UserFilters>({ ...filters.value })
const queryFields = computed<QueryField[]>(() => [
{
key: 'keyword',
type: 'input',
label: '用户关键词',
placeholder: '姓名、手机号、邮箱或员工编号',
props: { maxlength: 128, 'aria-label': '用户关键词' },
},
{
key: 'status',
type: 'select',
label: '状态',
placeholder: '全部状态',
props: { 'aria-label': '用户状态' },
options: [
{ label: '正常', value: 'ACTIVE' },
{ label: '禁用', value: 'DISABLED' },
],
},
])
const emptyForm = () => ({
username: '',
displayName: '',
phone: '',
email: '',
tenantId: '0',
roleIds: [] as string[],
overseasWarehouseId: '',
domesticWarehouseId: '',
password: '',
confirmPassword: '',
status: 'ACTIVE' as 'ACTIVE' | 'DISABLED',
resetPassword: false,
})
const form = reactive(emptyForm())
const canCreate = computed(() => access.can('platform:user:create') && access.can('platform:user:grant-role'))
const canEdit = computed(() => access.can('platform:user:edit'))
const canGrantRoles = computed(() => access.can('platform:user:grant-role'))
const canChangeStatus = computed(() => access.can('platform:user:status'))
const canResetPassword = computed(() => access.can('platform:user:reset-password'))
const editorLabel = computed(() => editingUser.value ? `编辑用户 · ${editingUser.value.displayName?.trim() || editingUser.value.username?.trim() || '—'}` : '新增用户')
useUnsavedWorkRegistration({
key: 'platform-user-editor',
label: editorLabel,
isOpen: editorOpen,
model: form,
requiredPermission: computed(() => editingUser.value ? 'platform:user:edit' : 'platform:user:create'),
})
const availableRoles = computed(() => options.value.roles.filter((role) =>
String(role.tenantId) === '0'
&& (role.status === 'ACTIVE' || form.roleIds.includes(String(role.id))),
))
const overseasPdaRoleIds = computed(() => new Set(
availableRoles.value.filter((role) => role.roleCode === 'OVERSEAS_WAREHOUSE_PDA_OPERATOR').map((role) => String(role.id)),
))
const hasOverseasPdaRole = computed(() => form.roleIds.some((roleId) => overseasPdaRoleIds.value.has(roleId)))
const domesticPdaRoleIds = computed(() => new Set(
availableRoles.value.filter((role) => role.roleCode === 'CN_FIRSTLEG_PDA_OPERATOR').map((role) => String(role.id)),
))
const hasDomesticPdaRole = computed(() => form.roleIds.some((roleId) => domesticPdaRoleIds.value.has(roleId)))
const selectedRoleNames = computed(() => form.roleIds.map((roleId) => availableRoles.value.find((role) => String(role.id) === roleId)?.name || '—').join('、') || '—')
const selectedOverseasWarehouse = computed(() => overseasWarehouses.value.find((warehouse) => warehouse.id === form.overseasWarehouseId)?.warehouseName || '—')
const selectedDomesticWarehouse = computed(() => domesticWarehouses.value.find((warehouse) => String(warehouse.id) === form.domesticWarehouseId)?.warehouseName || '—')
const formRules = computed<FormRules>(() => ({
username: editingUser.value ? [] : [
{ required: true, whitespace: true, message: '请输入用户名', trigger: 'blur' },
{ pattern: /^[A-Za-z0-9_.@-]{4,64}$/, message: '用户名为 4-64 位字母、数字或 _ . @ -', trigger: 'blur' },
],
displayName: editingUser.value && !canEdit.value ? [] : [{ required: true, whitespace: true, message: '请输入姓名', trigger: 'blur' }],
phone: editingUser.value && !canEdit.value ? [] : [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1\d{10}$/, message: '请输入正确的 11 位手机号', trigger: 'blur' },
],
email: editingUser.value && !canEdit.value ? [] : [{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }],
roleIds: editingUser.value && !canEdit.value ? [] : [{ type: 'array', required: true, min: 1, message: '请选择角色', trigger: 'change' }],
overseasWarehouseId: !hasOverseasPdaRole.value || (editingUser.value && !canEdit.value) ? [] : [
{ required: true, message: '请选择实际海外收货仓', trigger: 'change' },
],
domesticWarehouseId: !hasDomesticPdaRole.value || (editingUser.value && !canEdit.value) ? [] : [
{ required: true, message: '请选择国内作业仓', trigger: 'change' },
],
password: editingUser.value && !form.resetPassword ? [] : [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ pattern: /^(?=.*[A-Za-z])(?=.*\d).{8,64}$/, message: '密码为 8-64 位,且必须包含字母和数字', trigger: 'blur' },
],
confirmPassword: editingUser.value && !form.resetPassword ? [] : [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{ validator: (_rule, value, callback) => value === form.password ? callback() : callback(new Error('两次输入的密码不一致')), trigger: 'blur' },
],
}))
const userColumns: DataTableColumn[] = [
{ key: 'actions', label: '操作', width: 170, fixed: 'left', slotName: 'actions' },
{ key: 'username', prop: 'username', label: '用户名', minWidth: 145 },
{ key: 'displayName', prop: 'displayName', label: '姓名', minWidth: 130 },
{ key: 'roles', label: '角色', minWidth: 180, slotName: 'roles' },
{ key: 'status', label: '状态', width: 100, slotName: 'status' },
{ key: 'lastLoginAt', label: '最后登录', minWidth: 170, slotName: 'lastLoginAt' },
]
function userQuery() {
return {
page: page.value,
size: pageSize.value,
...(appliedFilters.value.keyword.trim() ? { keyword: appliedFilters.value.keyword.trim() } : {}),
...(appliedFilters.value.status ? { status: appliedFilters.value.status as PlatformManagedUser['status'] } : {}),
}
}
async function loadUsers() {
const requestId = ++loadRequestId
loading.value = true
loadError.value = null
try {
const result = await listPlatformManagedUsers(userQuery())
if (requestId !== loadRequestId) return
users.value = result.records
total.value = result.total
} catch (error) {
if (requestId !== loadRequestId) return
users.value = []
total.value = 0
loadError.value = getApiErrorDetails(error, '用户列表加载失败,请稍后重试')
} finally {
if (requestId === loadRequestId) loading.value = false
}
}
async function loadInitialData() {
const requestId = ++loadRequestId
loading.value = true
loadError.value = null
try {
const [result, optionRecords] = await Promise.all([
listPlatformManagedUsers(userQuery()),
getPlatformUserOptions(),
])
options.value = {
tenants: optionRecords.tenants.filter((tenant) => String(tenant.id) === '0'),
roles: optionRecords.roles.filter((role) => String(role.tenantId) === '0'),
}
if (requestId !== loadRequestId) return
users.value = result.records
total.value = result.total
} catch (error) {
if (requestId !== loadRequestId) return
users.value = []
total.value = 0
loadError.value = getApiErrorDetails(error, '内部用户列表加载失败,请稍后重试')
} finally {
if (requestId === loadRequestId) loading.value = false
}
}
function openCreate() {
Object.assign(form, emptyForm())
editingUser.value = null
editorOpen.value = true
nextTick(() => formRef.value?.clearValidate())
}
function openEdit(row: PlatformManagedUser) {
editingUser.value = row
Object.assign(form, {
username: row.username,
displayName: row.displayName,
phone: row.phone ?? '',
email: row.email ?? '',
tenantId: '0',
roleIds: row.roleIds.map(String),
overseasWarehouseId: row.overseasWarehouseId == null ? '' : String(row.overseasWarehouseId),
domesticWarehouseId: row.domesticWarehouseId == null ? '' : String(row.domesticWarehouseId),
password: '',
confirmPassword: '',
status: row.status,
resetPassword: false,
})
editorOpen.value = true
nextTick(() => formRef.value?.clearValidate())
}
async function loadOverseasWarehouses() {
const requestId = ++warehouseRequestId
overseasWarehousesLoading.value = true
overseasWarehousesError.value = null
try {
const records = await listSharedReceivingOverseasWarehouseOptions()
if (requestId !== warehouseRequestId) return
overseasWarehouses.value = records
} catch (error) {
if (requestId !== warehouseRequestId) return
overseasWarehouses.value = []
overseasWarehousesError.value = getApiErrorDetails(error, '共享海外收货仓加载失败,请重试')
} finally {
if (requestId === warehouseRequestId) overseasWarehousesLoading.value = false
}
}
async function loadDomesticWarehouses() {
domesticWarehousesLoading.value = true
domesticWarehousesError.value = null
try {
domesticWarehouses.value = await listAllPlatformWarehouses({ warehouseType: 'DOMESTIC', status: 'ACTIVE' })
} catch (error) {
domesticWarehouses.value = []
domesticWarehousesError.value = getApiErrorDetails(error, '国内作业仓加载失败,请重试')
} finally {
domesticWarehousesLoading.value = false
}
}
function isCurrentUser(row: PlatformManagedUser | null) {
if (!row) return false
return String(row.userId) === auth.user?.id || String(row.authUserId) === auth.user?.id
}
function cannotDisable(row: PlatformManagedUser | null) {
return Boolean(row?.protectedAccount) || isCurrentUser(row)
}
async function saveUser() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
saving.value = true
try {
if (!editingUser.value) {
await createPlatformManagedUser({
username: form.username.trim(),
displayName: form.displayName.trim(),
phone: form.phone.trim(),
email: form.email.trim(),
tenantId: '0',
roleIds: form.roleIds,
overseasWarehouseId: hasOverseasPdaRole.value ? form.overseasWarehouseId : null,
...(hasDomesticPdaRole.value ? { domesticWarehouseId: form.domesticWarehouseId } : {}),
password: form.password,
status: form.status,
})
ElMessage.success('用户添加成功')
} else {
const row = editingUser.value
if (canEdit.value) {
await updatePlatformManagedUser(row.membershipId, {
displayName: form.displayName.trim(),
phone: form.phone.trim(),
email: form.email.trim(),
tenantId: '0',
roleIds: form.roleIds,
overseasWarehouseId: hasOverseasPdaRole.value ? form.overseasWarehouseId : null,
...(hasDomesticPdaRole.value ? { domesticWarehouseId: form.domesticWarehouseId } : {}),
})
}
if (canChangeStatus.value && form.status !== row.status) {
await changePlatformManagedUserStatus(row.membershipId, form.status)
}
if (canResetPassword.value && form.resetPassword) {
await resetPlatformManagedUserPassword(row.membershipId, form.password)
}
ElMessage.success(form.resetPassword ? '用户信息已更新,密码已重置' : '用户信息已更新')
}
editorOpen.value = false
await loadUsers()
} catch (error) {
ElMessage.error(getApiErrorMessage(error, editingUser.value ? '用户更新失败,请稍后重试' : '用户添加失败,请稍后重试'))
} finally {
saving.value = false
}
}
async function toggleStatus(row: PlatformManagedUser) {
const targetStatus = row.status === 'ACTIVE' ? 'DISABLED' : 'ACTIVE'
const targetLabel = targetStatus === 'ACTIVE' ? '启用' : '禁用'
try {
await ElMessageBox.confirm(`确认${targetLabel}用户“${row.displayName}”吗?`, `${targetLabel}用户 · ${row.displayName?.trim() || row.username?.trim() || '—'}`, { customClass: 'admin-density-confirm',
confirmButtonText: `确认${targetLabel}`,
cancelButtonText: '取消',
type: targetStatus === 'DISABLED' ? 'warning' : 'info',
})
rowActionId.value = String(row.membershipId)
const updated = await changePlatformManagedUserStatus(row.membershipId, targetStatus)
Object.assign(row, updated)
ElMessage.success(`用户已${targetLabel}`)
} catch (error) {
if (error === 'cancel' || error === 'close') return
ElMessage.error(getApiErrorMessage(error, `用户${targetLabel}失败,请稍后重试`))
} finally {
rowActionId.value = ''
}
}
function formatLogin(value: string | null) {
if (!value) return '—'
return value.replace('T', ' ').slice(0, 16)
}
function applyFilters() {
appliedFilters.value = { ...filters.value }
if (page.value === 1) void loadUsers()
else page.value = 1
}
watch([page, pageSize], () => void loadUsers())
watch(hasOverseasPdaRole, (selected) => {
if (!selected) {
form.overseasWarehouseId = ''
overseasWarehousesError.value = null
return
}
void loadOverseasWarehouses()
})
watch(hasDomesticPdaRole, (selected) => {
if (!selected) {
form.domesticWarehouseId = ''
domesticWarehousesError.value = null
return
}
void loadDomesticWarehouses()
})
onMounted(loadInitialData)
</script>
<template>
<AdminPage class="admin-density iam-density" as="div">
<ListPageLayout aria-label="内部用户管理">
<section :aria-busy="loading"><DataTable appearance="integrated" :stripe="false" column-settings-mode="show" :results-visible="!loadError" v-model:page="page" v-model:page-size="pageSize" :data="users" :columns="userColumns" preference-key="internal-users-table"
:loading="loading" :total="total" aria-label="内部用户列表" row-key="membershipId" empty-text="没有符合筛选条件的内部用户">
<template #toolbar-start><h1>用户管理</h1></template>
<template #filters>
<QueryToolbar
v-model="filters"
ariaLabel="用户筛选"
:fields="queryFields"
preference-key="internal-users-query"
@search="applyFilters"
/>
</template>
<template #notices><template v-if="loadError">
<LoadErrorState title="内部用户列表暂时无法加载" :message="loadError.message" :loading="loading" @retry="loadInitialData">
<template #details><ApiErrorMeta :details="loadError" /></template>
</LoadErrorState>
</template></template>
<template #toolbar-actions><el-button v-if="canCreate" type="primary" @click="openCreate"><Plus :size="16" />新增用户</el-button></template>
<template #actions="{ row }"><div class="table-actions density-actions row-actions admin-text-actions">
<el-button class="density-action" v-if="canEdit || canChangeStatus || canResetPassword" size="small" data-tone="neutral" @click="openEdit(row as PlatformManagedUser)">
编辑
</el-button>
<el-button class="density-action" v-if="canChangeStatus" size="small" :data-tone="(row as PlatformManagedUser).status === 'ACTIVE' ? 'danger' : 'success'"
:loading="rowActionId === String((row as PlatformManagedUser).membershipId)"
:disabled="cannotDisable(row as PlatformManagedUser)"
:title="(row as PlatformManagedUser).protectedAccount ? '系统管理员不能被禁用' : isCurrentUser(row as PlatformManagedUser) ? '不能禁用当前登录用户' : undefined"
@click="toggleStatus(row as PlatformManagedUser)">
{{ (row as PlatformManagedUser).status === 'ACTIVE' ? '禁用' : '启用' }}
</el-button>
</div></template>
<template #roles="{ row }">
<div class="tag-list">
<el-tag v-for="role in (row as PlatformManagedUser).roleNames" :key="role" effect="plain" size="small">{{ role }}</el-tag>
<span v-if="!(row as PlatformManagedUser).roleNames.length" class="secondary-text">未分配</span>
</div>
</template>
<template #status="{ row }"><StatusBadge :status="(row as PlatformManagedUser).status === 'ACTIVE' ? '正常' : '禁用'" /></template>
<template #lastLoginAt="{ row }">{{ formatLogin((row as PlatformManagedUser).lastLoginAt) }}</template>
<template #pagination-left="{ total: tableTotal }"><span>共 {{ tableTotal }} 条</span></template>
</DataTable></section>
</ListPageLayout>
<AppDialog v-model="editorOpen" :title="editorLabel" width="min(650px, calc(100vw - 28px))" class="internal-user-editor-dialog admin-density-dialog iam-density" fullscreenable destroy-on-close :confirm-loading="saving" :confirm-text="editingUser ? '保存修改' : '确认添加'" @confirm="saveUser">
<el-form class="density-form density-dialog-sections" ref="formRef" :model="form" :rules="formRules" label-position="left" label-width="100px" :disabled="saving">
<section class="density-dialog-section" data-tone="work" aria-label="用户基本资料">
<h3 class="density-dialog-section__title">用户资料</h3>
<table class="density-table density-table--editable user-editor-table"><tbody>
<tr><td><el-form-item label="用户名" prop="username" required><span v-if="editingUser">{{form.username || '—'}}</span><el-input v-else v-model="form.username" placeholder="登录用户名" maxlength="64" /></el-form-item></td><td><el-form-item label="姓名" prop="displayName" required><span v-if="editingUser && !canEdit">{{form.displayName || '—'}}</span><el-input v-else v-model="form.displayName" placeholder="真实姓名" maxlength="64" /></el-form-item></td></tr>
<tr><td><el-form-item label="手机号" prop="phone" required><span v-if="editingUser && !canEdit">{{form.phone || '—'}}</span><el-input v-else v-model="form.phone" placeholder="手机号码" inputmode="tel" maxlength="11" /></el-form-item></td><td><el-form-item label="邮箱" prop="email"><span v-if="editingUser && !canEdit">{{form.email || '—'}}</span><el-input v-else v-model="form.email" placeholder="邮箱地址" inputmode="email" maxlength="128" /></el-form-item></td></tr>
<tr><td colspan="2"><el-form-item label="角色" prop="roleIds" required><span v-if="editingUser && (!canEdit || !canGrantRoles || editingUser.protectedAccount)">{{selectedRoleNames}}</span><el-select v-else v-model="form.roleIds" multiple collapse-tags collapse-tags-tooltip placeholder="选择角色"><el-option v-for="role in availableRoles" :key="role.id" :label="role.name" :value="String(role.id)" :disabled="role.status !== 'ACTIVE'" /></el-select></el-form-item></td></tr>
<tr><td :colspan="editingUser ? 1 : 2"><el-form-item label="状态" prop="status"><span v-if="editingUser && (!canChangeStatus || cannotDisable(editingUser))">{{form.status === 'ACTIVE' ? '正常' : '禁用'}}</span><el-select v-else v-model="form.status"><el-option label="正常" value="ACTIVE" /><el-option label="禁用" value="DISABLED" /></el-select></el-form-item></td><td v-if="editingUser"><el-form-item label="最后登录"><span>{{formatLogin(editingUser.lastLoginAt)}}</span></el-form-item></td></tr>
</tbody></table>
</section>
<section v-if="hasOverseasPdaRole || hasDomesticPdaRole" class="density-dialog-section" data-tone="work" aria-label="用户作业仓库">
<h3 class="density-dialog-section__title">作业仓库</h3>
<table class="density-table density-table--editable"><tbody>
<tr v-if="hasOverseasPdaRole"><td><el-form-item label="实际海外收货仓" prop="overseasWarehouseId" label-width="145px" required>
<template #label>实际海外收货仓 <InlineHelp title="海外仓作业范围">该人员仅在绑定的海外仓作业,海外 PDA 不提供仓库切换。</InlineHelp></template>
<span v-if="editingUser && !canEdit">{{selectedOverseasWarehouse}}</span><el-select v-else v-model="form.overseasWarehouseId" filterable :loading="overseasWarehousesLoading" placeholder="请选择平台共享海外收货仓"><el-option v-for="warehouse in overseasWarehouses" :key="warehouse.id" :label="`${warehouse.warehouseName}(${warehouse.warehouseCode} · ${warehouse.countryCode})`" :value="warehouse.id" /></el-select>
<LoadErrorState v-if="overseasWarehousesError" compact title="共享海外收货仓加载失败" :message="overseasWarehousesError.message" :loading="overseasWarehousesLoading" @retry="loadOverseasWarehouses"><template #details><ApiErrorMeta :details="overseasWarehousesError" /></template></LoadErrorState>
</el-form-item></td></tr>
<tr v-if="hasDomesticPdaRole"><td><el-form-item label="国内作业仓" prop="domesticWarehouseId" label-width="145px" required>
<template #label>国内作业仓 <InlineHelp title="国内仓作业范围">该人员只会出现在此仓库的国内头程 PDA 任务候选中。</InlineHelp></template>
<span v-if="editingUser && !canEdit">{{selectedDomesticWarehouse}}</span><el-select v-else v-model="form.domesticWarehouseId" filterable :loading="domesticWarehousesLoading" placeholder="请选择国内作业仓"><el-option v-for="warehouse in domesticWarehouses" :key="String(warehouse.id)" :label="`${warehouse.warehouseName}(${warehouse.warehouseCode})`" :value="String(warehouse.id)" /></el-select>
<LoadErrorState v-if="domesticWarehousesError" compact title="国内作业仓加载失败" :message="domesticWarehousesError.message" :loading="domesticWarehousesLoading" @retry="loadDomesticWarehouses"><template #details><ApiErrorMeta :details="domesticWarehousesError" /></template></LoadErrorState>
</el-form-item></td></tr>
</tbody></table>
</section>
<section v-if="!editingUser || canResetPassword" class="density-dialog-section" data-tone="work" aria-label="用户登录密码">
<h3 class="density-dialog-section__title">登录密码 <InlineHelp title="密码要求">密码为 8-64 位,并同时包含字母和数字。</InlineHelp></h3>
<table class="density-table density-table--editable user-editor-table"><tbody>
<tr v-if="editingUser"><td colspan="2"><el-checkbox v-model="form.resetPassword">同时重置密码</el-checkbox></td></tr>
<tr v-if="!editingUser || form.resetPassword"><td><el-form-item :label="editingUser ? '新密码' : '初始密码'" prop="password" required><el-input v-model="form.password" type="password" show-password autocomplete="new-password" placeholder="8-64 位,包含字母和数字" /></el-form-item></td><td><el-form-item label="确认密码" prop="confirmPassword" required><el-input v-model="form.confirmPassword" type="password" show-password autocomplete="new-password" placeholder="再次输入密码" /></el-form-item></td></tr>
</tbody></table>
</section>
</el-form>
</AppDialog>
</AdminPage>
</template>
<style scoped>
.user-editor-table { table-layout: fixed; }
.user-editor-table td:not([colspan="2"]) { width: 50%; }
:global(.internal-user-editor-dialog.app-dialog.admin-density-dialog.el-dialog:not(.is-fullscreen)) { max-height: min(760px, calc(100vh - 32px)); }
@media (max-width: 720px) { .user-editor-table tr { display: grid; } .user-editor-table td:not([colspan="2"]) { width: auto; } }
</style>