修复用户管理表单交互和状态查询
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]"
|
v-model="formData[col.prop]"
|
||||||
:placeholder="$t(col.placeholder)"
|
:placeholder="$t(col.placeholder)"
|
||||||
:type="col.inputType || 'text'"
|
:type="col.inputType || 'text'"
|
||||||
|
:show-password="!!col.showPassword"
|
||||||
:autosize="col.autosize"
|
:autosize="col.autosize"
|
||||||
:clearable="col.clearable !== false"
|
:clearable="col.clearable !== false"
|
||||||
:disabled="!!col.disabled"
|
:disabled="!!col.disabled"
|
||||||
@@ -255,7 +256,13 @@ export default {
|
|||||||
const rules = this.rules || {}
|
const rules = this.rules || {}
|
||||||
const translated = {}
|
const translated = {}
|
||||||
for (const [field, validators] of Object.entries(rules)) {
|
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
|
return translated
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2344,6 +2344,7 @@
|
|||||||
"status": "Status",
|
"status": "Status",
|
||||||
"enable": "Enabled",
|
"enable": "Enabled",
|
||||||
"disable": "Disabled",
|
"disable": "Disabled",
|
||||||
|
"select_status": "Please select status",
|
||||||
"user_group": "User Group",
|
"user_group": "User Group",
|
||||||
"login_ip": "Last Login IP",
|
"login_ip": "Last Login IP",
|
||||||
"last_login_time": "Last Login Time",
|
"last_login_time": "Last Login Time",
|
||||||
@@ -2370,6 +2371,7 @@
|
|||||||
"password_not_match": "Passwords do not match",
|
"password_not_match": "Passwords do not match",
|
||||||
"password_length": "Length should be 6 to 64 characters",
|
"password_length": "Length should be 6 to 64 characters",
|
||||||
"username_length": "Length should be 3 to 20 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_delete_self": "Cannot delete own account",
|
||||||
"cannot_operate_self": "Cannot operate own account",
|
"cannot_operate_self": "Cannot operate own account",
|
||||||
"batch_delete": "Batch Delete",
|
"batch_delete": "Batch Delete",
|
||||||
|
|||||||
@@ -2344,6 +2344,7 @@
|
|||||||
"status": "状态",
|
"status": "状态",
|
||||||
"enable": "启用",
|
"enable": "启用",
|
||||||
"disable": "禁用",
|
"disable": "禁用",
|
||||||
|
"select_status": "请选择状态",
|
||||||
"user_group": "用户组",
|
"user_group": "用户组",
|
||||||
"login_ip": "上次登录IP",
|
"login_ip": "上次登录IP",
|
||||||
"last_login_time": "上次登录时间",
|
"last_login_time": "上次登录时间",
|
||||||
@@ -2370,6 +2371,7 @@
|
|||||||
"password_not_match": "两次输入的密码不一致",
|
"password_not_match": "两次输入的密码不一致",
|
||||||
"password_length": "长度在 6 到 64 个字符",
|
"password_length": "长度在 6 到 64 个字符",
|
||||||
"username_length": "长度在 3 到 20 个字符",
|
"username_length": "长度在 3 到 20 个字符",
|
||||||
|
"username_exists": "账号已存在,请更换账号",
|
||||||
"cannot_delete_self": "不能删除自己的账号",
|
"cannot_delete_self": "不能删除自己的账号",
|
||||||
"cannot_operate_self": "不能操作自己的账号",
|
"cannot_operate_self": "不能操作自己的账号",
|
||||||
"batch_delete": "批量删除",
|
"batch_delete": "批量删除",
|
||||||
|
|||||||
@@ -21,6 +21,18 @@
|
|||||||
@keyup.enter.native="onSearch"
|
@keyup.enter.native="onSearch"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</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-form-item>
|
||||||
<el-button type="primary" icon="el-icon-search" @click="onSearch">
|
<el-button type="primary" icon="el-icon-search" @click="onSearch">
|
||||||
{{ $t(key('search')) }}
|
{{ $t(key('search')) }}
|
||||||
@@ -46,7 +58,7 @@
|
|||||||
@selection-change="onSelect"
|
@selection-change="onSelect"
|
||||||
>
|
>
|
||||||
<template #col-status="{ row }">
|
<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" />
|
<i class="el-icon-circle-check" />
|
||||||
{{ $t(key('enable')) }}
|
{{ $t(key('enable')) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -79,7 +91,6 @@
|
|||||||
import { useTableColumns } from '@/composables/useTableColumns'
|
import { useTableColumns } from '@/composables/useTableColumns'
|
||||||
import { useTableButtons } from '@/composables/useTableButtons'
|
import { useTableButtons } from '@/composables/useTableButtons'
|
||||||
import { i18nMixin } from '@/composables/useI18n'
|
import { i18nMixin } from '@/composables/useI18n'
|
||||||
import { confirmMixin } from '@/composables/useConfirmHandle'
|
|
||||||
import { getRoleAll } from '@/api/system-administration/role'
|
import { getRoleAll } from '@/api/system-administration/role'
|
||||||
import {
|
import {
|
||||||
getUserList,
|
getUserList,
|
||||||
@@ -99,10 +110,43 @@ const ownUserId = () => localStorage.getItem('user_id')
|
|||||||
export default {
|
export default {
|
||||||
name: 'system-administration-user',
|
name: 'system-administration-user',
|
||||||
components: { PageTable, PageDialogForm },
|
components: { PageTable, PageDialogForm },
|
||||||
mixins: [i18nMixin('page.system_administration.user_management.user'), confirmMixin],
|
mixins: [i18nMixin('page.system_administration.user_management.user')],
|
||||||
data () {
|
data () {
|
||||||
const key = this.key.bind(this)
|
const key = this.key.bind(this)
|
||||||
const $t = this.$t.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 {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
@@ -112,12 +156,21 @@ export default {
|
|||||||
dialogTitle: '',
|
dialogTitle: '',
|
||||||
editId: '',
|
editId: '',
|
||||||
handleType: 'create',
|
handleType: 'create',
|
||||||
search: { username: '', nickname: '' },
|
search: { username: '', nickname: '', status: '' },
|
||||||
pagination: { current: 1, size: 10, total: 0 },
|
pagination: { current: 1, size: 10, total: 0 },
|
||||||
roleOptions: [],
|
roleOptions: [],
|
||||||
columns: [],
|
columns: [],
|
||||||
toolbarButtons: [],
|
toolbarButtons: [],
|
||||||
rowButtons: [],
|
rowButtons: [],
|
||||||
|
formData: {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
password_confirm: '',
|
||||||
|
role_id: '',
|
||||||
|
nickname: '',
|
||||||
|
pass_number: '',
|
||||||
|
status: '1'
|
||||||
|
},
|
||||||
baseFormCols: {
|
baseFormCols: {
|
||||||
create: [
|
create: [
|
||||||
[{ type: 'input', prop: 'username', label: key('username'), placeholder: key('enter_username'), clearable: true, style: { width: '90%' } }],
|
[{ type: 'input', prop: 'username', label: key('username'), placeholder: key('enter_username'), clearable: true, style: { width: '90%' } }],
|
||||||
@@ -139,7 +192,8 @@ export default {
|
|||||||
baseRules: {
|
baseRules: {
|
||||||
username: [
|
username: [
|
||||||
{ required: true, message: key('enter_username'), trigger: 'blur' },
|
{ 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: [
|
password: [
|
||||||
{ required: true, message: key('enter_password'), trigger: 'blur' },
|
{ required: true, message: key('enter_password'), trigger: 'blur' },
|
||||||
@@ -147,10 +201,14 @@ export default {
|
|||||||
],
|
],
|
||||||
password_confirm: [
|
password_confirm: [
|
||||||
{ required: true, message: key('enter_confirm_password'), trigger: 'blur' },
|
{ 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: [
|
role_id: [
|
||||||
{ required: true, message: key('select_user_group'), trigger: 'change' }
|
{ 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') {
|
if (this.handleType === 'edit') {
|
||||||
return {
|
return {
|
||||||
username: this.baseRules.username,
|
username: this.baseRules.username,
|
||||||
role_id: this.baseRules.role_id
|
role_id: this.baseRules.role_id,
|
||||||
|
status: this.baseRules.status
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.baseRules
|
return this.baseRules
|
||||||
@@ -255,10 +314,18 @@ export default {
|
|||||||
async initRoleOptions () {
|
async initRoleOptions () {
|
||||||
try {
|
try {
|
||||||
const res = await getRoleAll()
|
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 }))
|
this.roleOptions = data.map(item => ({ value: item.id, label: item.name }))
|
||||||
} catch { /* 忽略 */ }
|
} 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 () {
|
async fetchData () {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
try {
|
try {
|
||||||
@@ -267,10 +334,9 @@ export default {
|
|||||||
page_no: this.pagination.current,
|
page_no: this.pagination.current,
|
||||||
page_size: this.pagination.size
|
page_size: this.pagination.size
|
||||||
})
|
})
|
||||||
const list = Array.isArray(res) ? res : (res.data || [])
|
const data = this.normalizeResponse(res)
|
||||||
const total = Array.isArray(res) ? res.length : (res.count || 0)
|
this.tableData = data.list
|
||||||
this.tableData = list
|
this.pagination.total = data.total
|
||||||
this.pagination.total = total
|
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
}
|
}
|
||||||
@@ -280,7 +346,7 @@ export default {
|
|||||||
this.fetchData()
|
this.fetchData()
|
||||||
},
|
},
|
||||||
onReset () {
|
onReset () {
|
||||||
this.search = { username: '', nickname: '' }
|
this.search = { username: '', nickname: '', status: '' }
|
||||||
this.pagination.current = 1
|
this.pagination.current = 1
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
},
|
},
|
||||||
@@ -377,16 +443,34 @@ export default {
|
|||||||
} catch { /* 拦截器已处理 */ }
|
} catch { /* 拦截器已处理 */ }
|
||||||
}).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) {
|
async handleDelete (row) {
|
||||||
if (String(row.user_id) === ownUserId()) {
|
if (String(row.user_id) === ownUserId()) {
|
||||||
this.$message.warning(this.$t(this.key('cannot_delete_self')))
|
this.$message.warning(this.$t(this.key('cannot_delete_self')))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const cancelled = await this.$confirmAction(
|
const ok = await this.confirmAction(
|
||||||
{ message: this.key('confirm_delete'), title: this.key('prompt') },
|
this.key('confirm_delete'),
|
||||||
() => deleteUser({ id: [row.user_id] })
|
() => deleteUser({ id: [row.user_id] })
|
||||||
)
|
)
|
||||||
if (cancelled) return
|
if (!ok) return
|
||||||
this.$message.success(this.$t(this.key('operation_success')))
|
this.$message.success(this.$t(this.key('operation_success')))
|
||||||
this.pagination.current = Math.min(
|
this.pagination.current = Math.min(
|
||||||
this.pagination.current,
|
this.pagination.current,
|
||||||
@@ -405,11 +489,11 @@ export default {
|
|||||||
this.$message.warning(this.$t(this.key('select_rows_first')))
|
this.$message.warning(this.$t(this.key('select_rows_first')))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const cancelled = await this.$confirmAction(
|
const ok = await this.confirmAction(
|
||||||
{ message: this.key('confirm_batch_delete'), title: this.key('prompt') },
|
this.key('confirm_batch_delete'),
|
||||||
() => batchDeleteUser({ id: rows.map(row => row.user_id) })
|
() => batchDeleteUser({ id: rows.map(row => row.user_id) })
|
||||||
)
|
)
|
||||||
if (cancelled) return
|
if (!ok) return
|
||||||
this.$message.success(this.$t(this.key('operation_success')))
|
this.$message.success(this.$t(this.key('operation_success')))
|
||||||
this.fetchData()
|
this.fetchData()
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user