修复用户管理表单交互和状态查询
This commit is contained in:
22
docs/用户管理模块表单UX验证.md
Normal file
22
docs/用户管理模块表单UX验证.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# 用户管理模块表单 UX 验证
|
||||
|
||||
## Findings
|
||||
|
||||
- **High** `src/views/system-administration/user-management/user/index.vue`: 已修复新增/编辑弹窗表单模型未在 `data()` 中声明的问题。该问题会导致 Vue 2 根级动态属性不具备可靠响应式,输入框和下拉框在弹窗中表现为无法正常录入或选择。
|
||||
- **Medium** `src/views/system-administration/user-management/user/index.vue`: 已补充状态查询项,查询参数会随 `username`、`nickname` 一起传给列表接口,用于按启用/停用筛选用户。
|
||||
- **Medium** `src/views/system-administration/user-management/user/index.vue`: 已将账号重复校验、确认密码一致性校验接入 Element 表单规则,错误会出现在对应字段附近,而不是只依赖提交后的全局提示。
|
||||
- **Medium** `src/views/system-administration/user-management/user/index.vue`: 当前用户数据模型没有手机号、邮箱字段,接口封装也没有对应字段或独立校验接口。因此手机号/邮箱格式校验无法在不扩展前后端契约的情况下落地。建议后续先确认字段设计,再补充表单项、列表列、接口字段和格式规则。
|
||||
- **Low** `src/components/page-dialog-form/index.vue`: 已补充 `show-password` 透传,并修复自定义 validator 没有 `message` 时被无条件翻译的问题,避免后续自定义表单校验不稳定。
|
||||
|
||||
## Evidence
|
||||
|
||||
- Browser: source-only。当前页面依赖登录态和后端接口,未做真实浏览器端新增用户提交。
|
||||
- Source checks: 用户弹窗字段状态、表单规则、状态查询、权限按钮声明已核对。
|
||||
- Build checks: 已通过 `eslint`、locale JSON 解析和生产构建验证语法与编译兼容性。
|
||||
- Confidence limits: 账号重复检查复用现有列表接口,并按用户名精确匹配;如果后端列表接口只做模糊查询或分页限制异常,最终仍需后端唯一约束兜底。
|
||||
|
||||
## Suggested Shape
|
||||
|
||||
- 保持“账号/密码/确认密码/用户组/姓名/出入证编号/状态”的当前录入顺序,必填项优先,状态默认启用。
|
||||
- 若业务确认需要手机号/邮箱,应放在姓名之后、出入证编号之前,并使用字段级格式提示。
|
||||
- 删除、批量删除、重置密码、启停用等操作保持二次确认;接口失败时不应显示成功提示。
|
||||
@@ -66,6 +66,7 @@
|
||||
v-model="formData[col.prop]"
|
||||
:placeholder="$t(col.placeholder)"
|
||||
:type="col.inputType || 'text'"
|
||||
:show-password="!!col.showPassword"
|
||||
:autosize="col.autosize"
|
||||
:clearable="col.clearable !== false"
|
||||
:disabled="!!col.disabled"
|
||||
@@ -255,7 +256,13 @@ export default {
|
||||
const rules = this.rules || {}
|
||||
const translated = {}
|
||||
for (const [field, validators] of Object.entries(rules)) {
|
||||
translated[field] = validators.map(v => ({ ...v, message: this.$t(v.message) }))
|
||||
translated[field] = validators.map(v => {
|
||||
const rule = { ...v }
|
||||
if (rule.message) {
|
||||
rule.message = this.$t(rule.message)
|
||||
}
|
||||
return rule
|
||||
})
|
||||
}
|
||||
return translated
|
||||
}
|
||||
|
||||
@@ -2344,6 +2344,7 @@
|
||||
"status": "Status",
|
||||
"enable": "Enabled",
|
||||
"disable": "Disabled",
|
||||
"select_status": "Please select status",
|
||||
"user_group": "User Group",
|
||||
"login_ip": "Last Login IP",
|
||||
"last_login_time": "Last Login Time",
|
||||
@@ -2370,6 +2371,7 @@
|
||||
"password_not_match": "Passwords do not match",
|
||||
"password_length": "Length should be 6 to 64 characters",
|
||||
"username_length": "Length should be 3 to 20 characters",
|
||||
"username_exists": "Username already exists. Please use another username",
|
||||
"cannot_delete_self": "Cannot delete own account",
|
||||
"cannot_operate_self": "Cannot operate own account",
|
||||
"batch_delete": "Batch Delete",
|
||||
|
||||
@@ -2344,6 +2344,7 @@
|
||||
"status": "状态",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"select_status": "请选择状态",
|
||||
"user_group": "用户组",
|
||||
"login_ip": "上次登录IP",
|
||||
"last_login_time": "上次登录时间",
|
||||
@@ -2370,6 +2371,7 @@
|
||||
"password_not_match": "两次输入的密码不一致",
|
||||
"password_length": "长度在 6 到 64 个字符",
|
||||
"username_length": "长度在 3 到 20 个字符",
|
||||
"username_exists": "账号已存在,请更换账号",
|
||||
"cannot_delete_self": "不能删除自己的账号",
|
||||
"cannot_operate_self": "不能操作自己的账号",
|
||||
"batch_delete": "批量删除",
|
||||
|
||||
@@ -21,6 +21,18 @@
|
||||
@keyup.enter.native="onSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t(key('status'))">
|
||||
<el-select
|
||||
v-model="search.status"
|
||||
:placeholder="$t(key('select_status'))"
|
||||
clearable
|
||||
style="width:140px"
|
||||
@change="onSearch"
|
||||
>
|
||||
<el-option value="1" :label="$t(key('enable'))" />
|
||||
<el-option value="0" :label="$t(key('disable'))" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="el-icon-search" @click="onSearch">
|
||||
{{ $t(key('search')) }}
|
||||
@@ -46,7 +58,7 @@
|
||||
@selection-change="onSelect"
|
||||
>
|
||||
<template #col-status="{ row }">
|
||||
<span v-if="row.status === 1" style="color: #67c23a;">
|
||||
<span v-if="String(row.status) === '1'" style="color: #67c23a;">
|
||||
<i class="el-icon-circle-check" />
|
||||
{{ $t(key('enable')) }}
|
||||
</span>
|
||||
@@ -79,7 +91,6 @@
|
||||
import { useTableColumns } from '@/composables/useTableColumns'
|
||||
import { useTableButtons } from '@/composables/useTableButtons'
|
||||
import { i18nMixin } from '@/composables/useI18n'
|
||||
import { confirmMixin } from '@/composables/useConfirmHandle'
|
||||
import { getRoleAll } from '@/api/system-administration/role'
|
||||
import {
|
||||
getUserList,
|
||||
@@ -99,10 +110,43 @@ const ownUserId = () => localStorage.getItem('user_id')
|
||||
export default {
|
||||
name: 'system-administration-user',
|
||||
components: { PageTable, PageDialogForm },
|
||||
mixins: [i18nMixin('page.system_administration.user_management.user'), confirmMixin],
|
||||
mixins: [i18nMixin('page.system_administration.user_management.user')],
|
||||
data () {
|
||||
const key = this.key.bind(this)
|
||||
const $t = this.$t.bind(this)
|
||||
const normalizeList = res => {
|
||||
const data = res && res.data !== undefined ? res.data : res
|
||||
if (Array.isArray(data)) return data
|
||||
if (data && Array.isArray(data.list)) return data.list
|
||||
if (data && Array.isArray(data.data)) return data.data
|
||||
if (data && data.data && Array.isArray(data.data.data)) return data.data.data
|
||||
return []
|
||||
}
|
||||
const validateUsernameUnique = async (rule, value, callback) => {
|
||||
const username = String(value || '').trim()
|
||||
if (!username || username.length < 3) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getUserList({ username, page_no: 1, page_size: 10000 })
|
||||
const exists = normalizeList(res).some(item => {
|
||||
const sameName = String(item.username || '') === username
|
||||
const sameUser = this.handleType === 'edit' && String(item.user_id) === String(this.editId)
|
||||
return sameName && !sameUser
|
||||
})
|
||||
callback(exists ? new Error($t(key('username_exists'))) : undefined)
|
||||
} catch {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
const validatePasswordConfirm = (rule, value, callback) => {
|
||||
if (this.handleType === 'create' && value && value !== this.formData.password) {
|
||||
callback(new Error($t(key('password_not_match'))))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
return {
|
||||
loading: false,
|
||||
submitting: false,
|
||||
@@ -112,12 +156,21 @@ export default {
|
||||
dialogTitle: '',
|
||||
editId: '',
|
||||
handleType: 'create',
|
||||
search: { username: '', nickname: '' },
|
||||
search: { username: '', nickname: '', status: '' },
|
||||
pagination: { current: 1, size: 10, total: 0 },
|
||||
roleOptions: [],
|
||||
columns: [],
|
||||
toolbarButtons: [],
|
||||
rowButtons: [],
|
||||
formData: {
|
||||
username: '',
|
||||
password: '',
|
||||
password_confirm: '',
|
||||
role_id: '',
|
||||
nickname: '',
|
||||
pass_number: '',
|
||||
status: '1'
|
||||
},
|
||||
baseFormCols: {
|
||||
create: [
|
||||
[{ type: 'input', prop: 'username', label: key('username'), placeholder: key('enter_username'), clearable: true, style: { width: '90%' } }],
|
||||
@@ -139,7 +192,8 @@ export default {
|
||||
baseRules: {
|
||||
username: [
|
||||
{ required: true, message: key('enter_username'), trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: key('username_length'), trigger: 'blur' }
|
||||
{ min: 3, max: 20, message: key('username_length'), trigger: 'blur' },
|
||||
{ validator: validateUsernameUnique, trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: key('enter_password'), trigger: 'blur' },
|
||||
@@ -147,10 +201,14 @@ export default {
|
||||
],
|
||||
password_confirm: [
|
||||
{ required: true, message: key('enter_confirm_password'), trigger: 'blur' },
|
||||
{ min: 6, max: 64, message: key('password_length'), trigger: 'blur' }
|
||||
{ min: 6, max: 64, message: key('password_length'), trigger: 'blur' },
|
||||
{ validator: validatePasswordConfirm, trigger: ['blur', 'change'] }
|
||||
],
|
||||
role_id: [
|
||||
{ required: true, message: key('select_user_group'), trigger: 'change' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: key('select_status'), trigger: 'change' }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -168,7 +226,8 @@ export default {
|
||||
if (this.handleType === 'edit') {
|
||||
return {
|
||||
username: this.baseRules.username,
|
||||
role_id: this.baseRules.role_id
|
||||
role_id: this.baseRules.role_id,
|
||||
status: this.baseRules.status
|
||||
}
|
||||
}
|
||||
return this.baseRules
|
||||
@@ -255,10 +314,18 @@ export default {
|
||||
async initRoleOptions () {
|
||||
try {
|
||||
const res = await getRoleAll()
|
||||
const data = Array.isArray(res) ? res : (res.data || [])
|
||||
const data = this.normalizeResponse(res).list
|
||||
this.roleOptions = data.map(item => ({ value: item.id, label: item.name }))
|
||||
} catch { /* 忽略 */ }
|
||||
},
|
||||
normalizeResponse (res) {
|
||||
const data = res && res.data !== undefined ? res.data : res
|
||||
if (Array.isArray(data)) return { list: data, total: data.length }
|
||||
if (data && Array.isArray(data.list)) return { list: data.list, total: Number(data.count || data.total || data.list.length) }
|
||||
if (data && Array.isArray(data.data)) return { list: data.data, total: Number(data.count || data.total || data.data.length) }
|
||||
if (data && data.data && Array.isArray(data.data.data)) return { list: data.data.data, total: Number(data.data.count || data.data.total || data.data.data.length) }
|
||||
return { list: [], total: 0 }
|
||||
},
|
||||
async fetchData () {
|
||||
this.loading = true
|
||||
try {
|
||||
@@ -267,10 +334,9 @@ export default {
|
||||
page_no: this.pagination.current,
|
||||
page_size: this.pagination.size
|
||||
})
|
||||
const list = Array.isArray(res) ? res : (res.data || [])
|
||||
const total = Array.isArray(res) ? res.length : (res.count || 0)
|
||||
this.tableData = list
|
||||
this.pagination.total = total
|
||||
const data = this.normalizeResponse(res)
|
||||
this.tableData = data.list
|
||||
this.pagination.total = data.total
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
@@ -280,7 +346,7 @@ export default {
|
||||
this.fetchData()
|
||||
},
|
||||
onReset () {
|
||||
this.search = { username: '', nickname: '' }
|
||||
this.search = { username: '', nickname: '', status: '' }
|
||||
this.pagination.current = 1
|
||||
this.fetchData()
|
||||
},
|
||||
@@ -377,16 +443,34 @@ export default {
|
||||
} catch { /* 拦截器已处理 */ }
|
||||
}).catch(() => {})
|
||||
},
|
||||
async confirmAction (message, action) {
|
||||
try {
|
||||
await this.$confirm(
|
||||
this.$t(message),
|
||||
this.$t(this.key('prompt')),
|
||||
{
|
||||
confirmButtonText: this.$t(this.key('confirm')),
|
||||
cancelButtonText: this.$t(this.key('cancel')),
|
||||
type: 'warning',
|
||||
closeOnClickModal: false
|
||||
}
|
||||
)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
await action()
|
||||
return true
|
||||
},
|
||||
async handleDelete (row) {
|
||||
if (String(row.user_id) === ownUserId()) {
|
||||
this.$message.warning(this.$t(this.key('cannot_delete_self')))
|
||||
return
|
||||
}
|
||||
const cancelled = await this.$confirmAction(
|
||||
{ message: this.key('confirm_delete'), title: this.key('prompt') },
|
||||
const ok = await this.confirmAction(
|
||||
this.key('confirm_delete'),
|
||||
() => deleteUser({ id: [row.user_id] })
|
||||
)
|
||||
if (cancelled) return
|
||||
if (!ok) return
|
||||
this.$message.success(this.$t(this.key('operation_success')))
|
||||
this.pagination.current = Math.min(
|
||||
this.pagination.current,
|
||||
@@ -405,11 +489,11 @@ export default {
|
||||
this.$message.warning(this.$t(this.key('select_rows_first')))
|
||||
return
|
||||
}
|
||||
const cancelled = await this.$confirmAction(
|
||||
{ message: this.key('confirm_batch_delete'), title: this.key('prompt') },
|
||||
const ok = await this.confirmAction(
|
||||
this.key('confirm_batch_delete'),
|
||||
() => batchDeleteUser({ id: rows.map(row => row.user_id) })
|
||||
)
|
||||
if (cancelled) return
|
||||
if (!ok) return
|
||||
this.$message.success(this.$t(this.key('operation_success')))
|
||||
this.fetchData()
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user