feat: 新增用户管理(增删改查/锁定解锁/密码修改重置)与首次登录强制改密
parent
314067b91e
commit
0e4a38fd6f
|
|
@ -18,7 +18,8 @@ export const menuTree = [
|
|||
{ code: 'user:delete', name: '删除' },
|
||||
{ code: 'user:lock', name: '锁定' },
|
||||
{ code: 'user:unlock', name: '解锁' },
|
||||
{ code: 'user:resetPwd', name: '密码重置' }
|
||||
{ code: 'user:changePwd', name: '密码修改' },
|
||||
{ code: 'user:resetPwd', name: '密码重置并发送短信' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
import { menuTree, flattenButtons } from './menu.js'
|
||||
|
||||
// 角色数据以扁平列表存储;role_admin 为内置系统管理员角色,不可删除/改名
|
||||
export const roles = [
|
||||
{
|
||||
id: 'role_admin',
|
||||
name: '【内置】系统管理员',
|
||||
orgId: 'org001',
|
||||
builtin: true,
|
||||
status: 'enabled',
|
||||
menuIds: ['system', 'sys_user', 'sys_role', 'sys_org'],
|
||||
buttonCodes: flattenButtons(menuTree),
|
||||
dataScope: 'all',
|
||||
createdAt: '2026-01-01 00:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
let seq = roles.length
|
||||
|
||||
export function nextRoleId() {
|
||||
seq += 1
|
||||
return `role${String(seq).padStart(3, '0')}`
|
||||
}
|
||||
|
|
@ -9,6 +9,16 @@ export const users = [
|
|||
roleIds: ['role_admin'],
|
||||
status: 'enabled', // enabled | locked
|
||||
failCount: 0,
|
||||
mustChangePassword: false,
|
||||
lastLoginAt: '',
|
||||
createdAt: '2026-01-01 00:00:00'
|
||||
}
|
||||
]
|
||||
|
||||
let seq = users.length
|
||||
|
||||
export function nextUserId() {
|
||||
seq += 1
|
||||
return `u${String(seq).padStart(3, '0')}`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ router.post('/login', (req, res) => {
|
|||
}
|
||||
if (code !== FIXED_SMS_CODE) return res.json({ code: 40003, msg: '验证码错误或已过期', data: null })
|
||||
user.failCount = 0
|
||||
user.lastLoginAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
|
||||
const accessToken = crypto.randomUUID()
|
||||
const refreshToken = crypto.randomUUID()
|
||||
|
|
@ -60,7 +61,8 @@ router.post('/login', (req, res) => {
|
|||
accessToken,
|
||||
userInfo: { id: user.id, username: user.username, realName: user.realName, orgId: user.orgId, roleIds: user.roleIds },
|
||||
menuTree,
|
||||
buttonCodes: flattenButtons(menuTree)
|
||||
buttonCodes: flattenButtons(menuTree),
|
||||
mustChangePassword: !!user.mustChangePassword
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,35 @@
|
|||
import express from 'express'
|
||||
import crypto from 'crypto'
|
||||
import { orgs, nextOrgId, buildOrgTree, collectDescendantIds } from '../data/orgs.js'
|
||||
import { users } from '../data/users.js'
|
||||
import { users, nextUserId } from '../data/users.js'
|
||||
import { roles } from '../data/roles.js'
|
||||
import { requireAuth } from '../middleware/requireAuth.js'
|
||||
|
||||
const router = express.Router()
|
||||
router.use(requireAuth)
|
||||
|
||||
function orgName(orgId) {
|
||||
return orgs.find((o) => o.id === orgId)?.name || ''
|
||||
}
|
||||
|
||||
function roleNames(roleIds) {
|
||||
return (roleIds || []).map((id) => roles.find((r) => r.id === id)?.name).filter(Boolean)
|
||||
}
|
||||
|
||||
function nowStr() {
|
||||
return new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||
}
|
||||
|
||||
function genInitialPassword() {
|
||||
const upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'
|
||||
const lower = 'abcdefghijkmnpqrstuvwxyz'
|
||||
const digit = '0123456789'
|
||||
const all = upper + lower + digit
|
||||
let pwd = upper[Math.floor(Math.random() * upper.length)] + digit[Math.floor(Math.random() * digit.length)]
|
||||
for (let i = 0; i < 6; i += 1) pwd += all[Math.floor(Math.random() * all.length)]
|
||||
return pwd
|
||||
}
|
||||
|
||||
// ---------------- 机构管理 ----------------
|
||||
|
||||
router.get('/org/list', (req, res) => {
|
||||
|
|
@ -96,4 +120,137 @@ router.delete('/org/:id', (req, res) => {
|
|||
res.json({ code: 200, msg: 'ok', data: null })
|
||||
})
|
||||
|
||||
// ---------------- 用户管理 ----------------
|
||||
|
||||
router.get('/user/list', (req, res) => {
|
||||
const { orgId, username, phone, status, roleId, page = 1, pageSize = 10 } = req.query || {}
|
||||
let list = users
|
||||
if (orgId) list = list.filter((u) => u.orgId === orgId)
|
||||
if (username) list = list.filter((u) => u.username.includes(username))
|
||||
if (phone) list = list.filter((u) => u.phone.includes(phone))
|
||||
if (status) list = list.filter((u) => u.status === status)
|
||||
if (roleId) list = list.filter((u) => u.roleIds.includes(roleId))
|
||||
|
||||
const total = list.length
|
||||
const start = (Number(page) - 1) * Number(pageSize)
|
||||
const pageList = list.slice(start, start + Number(pageSize)).map((u) => ({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
realName: u.realName,
|
||||
phone: u.phone,
|
||||
orgId: u.orgId,
|
||||
orgName: orgName(u.orgId),
|
||||
roleIds: u.roleIds,
|
||||
roleNames: roleNames(u.roleIds),
|
||||
status: u.status,
|
||||
createdAt: u.createdAt,
|
||||
lastLoginAt: u.lastLoginAt
|
||||
}))
|
||||
res.json({ code: 200, msg: 'ok', data: { list: pageList, total } })
|
||||
})
|
||||
|
||||
router.post('/user', (req, res) => {
|
||||
const { username, phone, realName, orgId, roleIds } = req.body || {}
|
||||
if (!username) return res.json({ code: 40010, msg: '请输入用户名', data: null })
|
||||
if (!/^1\d{10}$/.test(phone || '')) return res.json({ code: 40011, msg: '请输入正确的11位手机号', data: null })
|
||||
if (users.some((u) => u.phone === phone)) return res.json({ code: 40012, msg: '该手机号已被注册', data: null })
|
||||
if (!orgId || !orgs.some((o) => o.id === orgId)) return res.json({ code: 40013, msg: '请选择有效的所属机构', data: null })
|
||||
if (!roleIds?.length) return res.json({ code: 40014, msg: '请选择用户角色', data: null })
|
||||
|
||||
const initialPassword = genInitialPassword()
|
||||
const user = {
|
||||
id: nextUserId(),
|
||||
username,
|
||||
password: initialPassword,
|
||||
realName: realName || '',
|
||||
phone,
|
||||
orgId,
|
||||
roleIds,
|
||||
status: 'enabled',
|
||||
failCount: 0,
|
||||
mustChangePassword: true,
|
||||
lastLoginAt: '',
|
||||
createdAt: nowStr()
|
||||
}
|
||||
users.push(user)
|
||||
res.json({ code: 200, msg: 'ok', data: { id: user.id, initialPassword } })
|
||||
})
|
||||
|
||||
router.put('/user/:id', (req, res) => {
|
||||
const { id } = req.params
|
||||
const { phone, realName, orgId, roleIds } = req.body || {}
|
||||
const user = users.find((u) => u.id === id)
|
||||
if (!user) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
||||
if (!/^1\d{10}$/.test(phone || '')) return res.json({ code: 40011, msg: '请输入正确的11位手机号', data: null })
|
||||
if (users.some((u) => u.id !== id && u.phone === phone)) {
|
||||
return res.json({ code: 40012, msg: '该手机号已被注册', data: null })
|
||||
}
|
||||
if (!orgId || !orgs.some((o) => o.id === orgId)) return res.json({ code: 40013, msg: '请选择有效的所属机构', data: null })
|
||||
if (!roleIds?.length) return res.json({ code: 40014, msg: '请选择用户角色', data: null })
|
||||
|
||||
user.phone = phone
|
||||
user.realName = realName || ''
|
||||
user.orgId = orgId
|
||||
user.roleIds = roleIds
|
||||
res.json({ code: 200, msg: 'ok', data: null })
|
||||
})
|
||||
|
||||
router.delete('/user/:id', (req, res) => {
|
||||
const { id } = req.params
|
||||
const index = users.findIndex((u) => u.id === id)
|
||||
if (index === -1) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
||||
users.splice(index, 1)
|
||||
res.json({ code: 200, msg: 'ok', data: null })
|
||||
})
|
||||
|
||||
router.put('/user/:id/lock', (req, res) => {
|
||||
const user = users.find((u) => u.id === req.params.id)
|
||||
if (!user) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
||||
user.status = 'locked'
|
||||
res.json({ code: 200, msg: 'ok', data: null })
|
||||
})
|
||||
|
||||
router.put('/user/:id/unlock', (req, res) => {
|
||||
const user = users.find((u) => u.id === req.params.id)
|
||||
if (!user) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
||||
user.status = 'enabled'
|
||||
user.failCount = 0
|
||||
res.json({ code: 200, msg: 'ok', data: null })
|
||||
})
|
||||
|
||||
router.put('/user/:id/password', (req, res) => {
|
||||
const { newPassword } = req.body || {}
|
||||
const user = users.find((u) => u.id === req.params.id)
|
||||
if (!user) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
||||
const strongEnough = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$/.test(newPassword || '')
|
||||
if (!strongEnough) return res.json({ code: 40015, msg: '密码需8位以上,且包含大小写字母和数字', data: null })
|
||||
user.password = newPassword
|
||||
user.mustChangePassword = false
|
||||
res.json({ code: 200, msg: 'ok', data: null })
|
||||
})
|
||||
|
||||
router.put('/user/:id/password/reset', (req, res) => {
|
||||
const user = users.find((u) => u.id === req.params.id)
|
||||
if (!user) return res.json({ code: 40404, msg: '用户不存在', data: null })
|
||||
user.password = '123456'
|
||||
user.mustChangePassword = true
|
||||
console.log(`[mock-server] 密码已重置,短信通知已发送至 ${user.phone}: 初始密码 123456`)
|
||||
res.json({ code: 200, msg: 'ok', data: null })
|
||||
})
|
||||
|
||||
// ---------------- 角色管理(简版,供用户管理下拉使用;完整 CRUD 见后续路由) ----------------
|
||||
|
||||
router.get('/role/list', (req, res) => {
|
||||
const { status } = req.query || {}
|
||||
let list = roles
|
||||
if (status) list = list.filter((r) => r.status === status)
|
||||
res.json({
|
||||
code: 200,
|
||||
msg: 'ok',
|
||||
data: list.map((r) => ({ id: r.id, name: r.name, orgId: r.orgId, status: r.status, builtin: r.builtin }))
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,3 +5,16 @@ export const fetchOrgListApi = (params) => request.get('/system/org/list', { par
|
|||
export const createOrgApi = (data) => request.post('/system/org', data)
|
||||
export const updateOrgApi = (id, data) => request.put(`/system/org/${id}`, data)
|
||||
export const deleteOrgApi = (id) => request.delete(`/system/org/${id}`)
|
||||
|
||||
// ---------------- 用户管理 ----------------
|
||||
export const fetchUserListApi = (params) => request.get('/system/user/list', { params })
|
||||
export const createUserApi = (data) => request.post('/system/user', data)
|
||||
export const updateUserApi = (id, data) => request.put(`/system/user/${id}`, data)
|
||||
export const deleteUserApi = (id) => request.delete(`/system/user/${id}`)
|
||||
export const lockUserApi = (id) => request.put(`/system/user/${id}/lock`)
|
||||
export const unlockUserApi = (id) => request.put(`/system/user/${id}/unlock`)
|
||||
export const changeUserPasswordApi = (id, newPassword) => request.put(`/system/user/${id}/password`, { newPassword })
|
||||
export const resetUserPasswordApi = (id) => request.put(`/system/user/${id}/password/reset`)
|
||||
|
||||
// ---------------- 角色管理 ----------------
|
||||
export const fetchRoleListApi = (params) => request.get('/system/role/list', { params })
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ export const useAuthStore = defineStore('auth', {
|
|||
userInfo: null,
|
||||
menuTree: [],
|
||||
buttonCodes: [],
|
||||
routesReady: false
|
||||
routesReady: false,
|
||||
mustChangePassword: false
|
||||
}),
|
||||
actions: {
|
||||
setAccessToken(token) {
|
||||
|
|
@ -17,11 +18,12 @@ export const useAuthStore = defineStore('auth', {
|
|||
async login(payload) {
|
||||
const res = await loginApi(payload)
|
||||
if (res.data.code === 200) {
|
||||
const { accessToken, userInfo, menuTree, buttonCodes } = res.data.data
|
||||
const { accessToken, userInfo, menuTree, buttonCodes, mustChangePassword } = res.data.data
|
||||
this.setAccessToken(accessToken)
|
||||
this.userInfo = userInfo
|
||||
this.menuTree = menuTree
|
||||
this.buttonCodes = buttonCodes
|
||||
this.mustChangePassword = !!mustChangePassword
|
||||
}
|
||||
return res.data
|
||||
},
|
||||
|
|
@ -43,11 +45,15 @@ export const useAuthStore = defineStore('auth', {
|
|||
this.menuTree = []
|
||||
this.buttonCodes = []
|
||||
this.routesReady = false
|
||||
this.mustChangePassword = false
|
||||
localStorage.removeItem('access_token')
|
||||
},
|
||||
markRoutesReady() {
|
||||
this.routesReady = true
|
||||
},
|
||||
markPasswordChanged() {
|
||||
this.mustChangePassword = false
|
||||
},
|
||||
hasButton(code) {
|
||||
return this.buttonCodes.includes(code)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
const PHONE_REGEX = /^1\d{10}$/
|
||||
// 密码强度:8 位以上,同时包含大写字母、小写字母、数字
|
||||
const PASSWORD_REGEX = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$/
|
||||
|
||||
export function isValidPhone(phone) {
|
||||
return PHONE_REGEX.test(phone || '')
|
||||
}
|
||||
|
||||
export function isValidPasswordStrength(password) {
|
||||
return PASSWORD_REGEX.test(password || '')
|
||||
}
|
||||
|
||||
export function phoneValidatorRule() {
|
||||
return {
|
||||
validator: (_rule, value) => {
|
||||
if (!value) return Promise.reject(new Error('请输入手机号'))
|
||||
if (!isValidPhone(value)) return Promise.reject(new Error('请输入正确的11位手机号'))
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function passwordStrengthValidatorRule() {
|
||||
return {
|
||||
validator: (_rule, value) => {
|
||||
if (!value) return Promise.reject(new Error('请输入密码'))
|
||||
if (!isValidPasswordStrength(value)) {
|
||||
return Promise.reject(new Error('密码需8位以上,且包含大小写字母和数字'))
|
||||
}
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,31 @@
|
|||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:open="forceChangePwdOpen"
|
||||
title="首次登录,请设置新密码"
|
||||
:closable="false"
|
||||
:mask-closable="false"
|
||||
:keyboard="false"
|
||||
:confirm-loading="changePwdLoading"
|
||||
@ok="handleForceChangePassword"
|
||||
>
|
||||
<a-alert
|
||||
type="warning"
|
||||
show-icon
|
||||
message="检测到您使用的是初始密码,为保障账号安全,请先设置新密码后再继续操作"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<a-form layout="vertical" :model="pwdForm">
|
||||
<a-form-item label="新密码" required>
|
||||
<a-input-password v-model:value="pwdForm.newPassword" placeholder="8位以上,含大小写字母和数字" />
|
||||
</a-form-item>
|
||||
<a-form-item label="确认新密码" required>
|
||||
<a-input-password v-model:value="pwdForm.confirmPassword" placeholder="请再次输入新密码" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -68,6 +93,8 @@ import { sendSmsApi } from '@/api/auth'
|
|||
import { getBaseUrl, setBaseUrlOverride } from '@/api/request'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { installDynamicRoutes, collectFirstPath } from '@/router/dynamic'
|
||||
import { changeUserPasswordApi } from '@/api/system'
|
||||
import { isValidPasswordStrength } from '@/utils/validators'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
|
@ -81,6 +108,10 @@ const loginLoading = ref(false)
|
|||
const baseUrlModalOpen = ref(false)
|
||||
const baseUrlInput = ref(getBaseUrl())
|
||||
|
||||
const forceChangePwdOpen = ref(false)
|
||||
const changePwdLoading = ref(false)
|
||||
const pwdForm = reactive({ newPassword: '', confirmPassword: '' })
|
||||
|
||||
async function handleSendSms() {
|
||||
if (!form.username || !form.password) {
|
||||
message.warning('请先填写账号和密码')
|
||||
|
|
@ -98,21 +129,52 @@ async function handleSendSms() {
|
|||
}
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
loginLoading.value = true
|
||||
try {
|
||||
const res = await authStore.login(form)
|
||||
if (res.code === 200) {
|
||||
function goToFirstPage() {
|
||||
installDynamicRoutes(router, authStore.menuTree)
|
||||
authStore.markRoutesReady()
|
||||
const target = route.query.redirect || collectFirstPath(authStore.menuTree) || '/403'
|
||||
router.replace(target)
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
loginLoading.value = true
|
||||
try {
|
||||
const res = await authStore.login(form)
|
||||
if (res.code === 200) {
|
||||
if (authStore.mustChangePassword) {
|
||||
forceChangePwdOpen.value = true
|
||||
} else {
|
||||
goToFirstPage()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleForceChangePassword() {
|
||||
if (!isValidPasswordStrength(pwdForm.newPassword)) {
|
||||
message.error('密码需8位以上,且包含大小写字母和数字')
|
||||
return
|
||||
}
|
||||
if (pwdForm.newPassword !== pwdForm.confirmPassword) {
|
||||
message.error('两次输入的密码不一致')
|
||||
return
|
||||
}
|
||||
changePwdLoading.value = true
|
||||
try {
|
||||
const res = await changeUserPasswordApi(authStore.userInfo.id, pwdForm.newPassword)
|
||||
if (res.data.code === 200) {
|
||||
message.success('密码修改成功')
|
||||
authStore.markPasswordChanged()
|
||||
forceChangePwdOpen.value = false
|
||||
goToFirstPage()
|
||||
}
|
||||
} finally {
|
||||
changePwdLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveBaseUrl() {
|
||||
setBaseUrlOverride(baseUrlInput.value)
|
||||
baseUrlModalOpen.value = false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,294 @@
|
|||
<template>
|
||||
<div class="user-list-page">
|
||||
<ProTable
|
||||
ref="proTableRef"
|
||||
:columns="columns"
|
||||
:fetch-data="loadUserList"
|
||||
:initial-search="{ orgId: undefined, username: '', phone: '', status: undefined, roleId: undefined }"
|
||||
>
|
||||
<template #search="{ form }">
|
||||
<a-form-item label="所属机构">
|
||||
<OrgTreeSelect v-model="form.orgId" style="width: 200px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="用户名称">
|
||||
<a-input v-model:value="form.username" placeholder="请输入用户名称" allow-clear style="width: 160px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号">
|
||||
<a-input v-model:value="form.phone" placeholder="请输入手机号" allow-clear style="width: 160px" />
|
||||
</a-form-item>
|
||||
<a-form-item label="用户状态">
|
||||
<a-select v-model:value="form.status" placeholder="请选择状态" allow-clear style="width: 140px">
|
||||
<a-select-option value="enabled">正常</a-select-option>
|
||||
<a-select-option value="locked">已锁定</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="用户角色">
|
||||
<a-select v-model:value="form.roleId" placeholder="请选择角色" allow-clear style="width: 160px">
|
||||
<a-select-option v-for="r in roleOptions" :key="r.id" :value="r.id">{{ r.name }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<a-button v-permission="'user:add'" type="primary" @click="openCreateModal">
|
||||
<PlusOutlined /> 新增用户
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<template v-if="column.dataIndex === 'index'">{{ index + 1 }}</template>
|
||||
<template v-else-if="column.dataIndex === 'roleNames'">{{ record.roleNames.join('、') }}</template>
|
||||
<template v-else-if="column.dataIndex === 'status'">
|
||||
<StatusTag :value="record.status" />
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<a-space wrap>
|
||||
<a v-permission="'user:edit'" @click="openEditModal(record)">修改</a>
|
||||
<a-popconfirm title="确定删除该用户吗?" @confirm="handleDelete(record)">
|
||||
<a v-permission="'user:delete'" class="danger-link">删除</a>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm v-if="record.status === 'enabled'" title="确定锁定该用户吗?" @confirm="handleLock(record)">
|
||||
<a v-permission="'user:lock'">锁定</a>
|
||||
</a-popconfirm>
|
||||
<a-popconfirm v-else title="确定解锁该用户吗?" @confirm="handleUnlock(record)">
|
||||
<a v-permission="'user:unlock'">解锁</a>
|
||||
</a-popconfirm>
|
||||
<a v-permission="'user:changePwd'" @click="openChangePwdModal(record)">密码修改</a>
|
||||
<a-popconfirm title="确定重置密码并短信通知该用户吗?" @confirm="handleResetPwd(record)">
|
||||
<a v-permission="'user:resetPwd'">密码重置并发送短信</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ProTable>
|
||||
|
||||
<a-modal
|
||||
v-model:open="modalOpen"
|
||||
:title="modalMode === 'create' ? '新增用户' : '修改用户'"
|
||||
:confirm-loading="submitting"
|
||||
@ok="handleSubmit"
|
||||
>
|
||||
<a-form ref="formRef" layout="vertical" :model="form" :rules="rules">
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input v-model:value="form.username" :disabled="modalMode === 'edit'" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="真实姓名" name="realName">
|
||||
<a-input v-model:value="form.realName" placeholder="请输入真实姓名(非必填)" />
|
||||
</a-form-item>
|
||||
<a-form-item label="手机号" name="phone">
|
||||
<a-input v-model:value="form.phone" placeholder="请输入11位手机号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="所属机构" name="orgId">
|
||||
<OrgTreeSelect v-model="form.orgId" />
|
||||
</a-form-item>
|
||||
<a-form-item label="用户角色" name="roleIds">
|
||||
<a-select v-model:value="form.roleIds" mode="multiple" placeholder="请选择用户角色(仅显示已启用角色)">
|
||||
<a-select-option v-for="r in roleOptions" :key="r.id" :value="r.id">{{ r.name }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:open="changePwdOpen" title="密码修改" :confirm-loading="changePwdLoading" @ok="handleChangePassword">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="新密码">
|
||||
<a-input-password v-model:value="newPassword" placeholder="8位以上,含大小写字母和数字" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import ProTable from '@/components/ProTable.vue'
|
||||
import StatusTag from '@/components/StatusTag.vue'
|
||||
import OrgTreeSelect from '@/components/OrgTreeSelect.vue'
|
||||
import {
|
||||
fetchUserListApi,
|
||||
createUserApi,
|
||||
updateUserApi,
|
||||
deleteUserApi,
|
||||
lockUserApi,
|
||||
unlockUserApi,
|
||||
changeUserPasswordApi,
|
||||
resetUserPasswordApi,
|
||||
fetchRoleListApi
|
||||
} from '@/api/system'
|
||||
import { isValidPasswordStrength } from '@/utils/validators'
|
||||
|
||||
const columns = [
|
||||
{ title: '序号', dataIndex: 'index', width: 60 },
|
||||
{ title: '用户名', dataIndex: 'username' },
|
||||
{ title: '真实姓名', dataIndex: 'realName' },
|
||||
{ title: '用户角色', dataIndex: 'roleNames' },
|
||||
{ title: '所属机构', dataIndex: 'orgName' },
|
||||
{ title: '用户状态', dataIndex: 'status' },
|
||||
{ title: '创建时间', dataIndex: 'createdAt' },
|
||||
{ title: '最后登录', dataIndex: 'lastLoginAt' },
|
||||
{ title: '操作', dataIndex: 'action', width: 260 }
|
||||
]
|
||||
|
||||
const proTableRef = ref(null)
|
||||
const roleOptions = ref([])
|
||||
|
||||
async function loadRoleOptions() {
|
||||
const res = await fetchRoleListApi({ status: 'enabled' })
|
||||
if (res.data.code === 200) roleOptions.value = res.data.data
|
||||
}
|
||||
|
||||
async function loadUserList(params) {
|
||||
const res = await fetchUserListApi(params)
|
||||
if (res.data.code === 200) return res.data.data
|
||||
return { list: [], total: 0 }
|
||||
}
|
||||
|
||||
onMounted(loadRoleOptions)
|
||||
|
||||
// ---------------- 新增/修改弹窗 ----------------
|
||||
const modalOpen = ref(false)
|
||||
const modalMode = ref('create')
|
||||
const submitting = ref(false)
|
||||
const formRef = ref(null)
|
||||
const form = reactive({ id: '', username: '', realName: '', phone: '', orgId: undefined, roleIds: [] })
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名' }],
|
||||
phone: [{ required: true, message: '请输入手机号' }],
|
||||
orgId: [{ required: true, message: '请选择所属机构' }],
|
||||
roleIds: [{ required: true, type: 'array', message: '请选择用户角色' }]
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
modalMode.value = 'create'
|
||||
form.id = ''
|
||||
form.username = ''
|
||||
form.realName = ''
|
||||
form.phone = ''
|
||||
form.orgId = undefined
|
||||
form.roleIds = []
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
function openEditModal(record) {
|
||||
modalMode.value = 'edit'
|
||||
form.id = record.id
|
||||
form.username = record.username
|
||||
form.realName = record.realName
|
||||
form.phone = record.phone
|
||||
form.orgId = record.orgId
|
||||
form.roleIds = [...record.roleIds]
|
||||
modalOpen.value = true
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
if (modalMode.value === 'create') {
|
||||
const res = await createUserApi({
|
||||
username: form.username,
|
||||
realName: form.realName,
|
||||
phone: form.phone,
|
||||
orgId: form.orgId,
|
||||
roleIds: form.roleIds
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
modalOpen.value = false
|
||||
proTableRef.value.reload()
|
||||
Modal.success({
|
||||
title: '新增成功',
|
||||
content: `已生成初始密码:${res.data.data.initialPassword}(请妥善告知用户,首次登录需强制修改密码)`
|
||||
})
|
||||
}
|
||||
} else {
|
||||
const res = await updateUserApi(form.id, {
|
||||
realName: form.realName,
|
||||
phone: form.phone,
|
||||
orgId: form.orgId,
|
||||
roleIds: form.roleIds
|
||||
})
|
||||
if (res.data.code === 200) {
|
||||
message.success('修改成功')
|
||||
modalOpen.value = false
|
||||
proTableRef.value.reload()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(record) {
|
||||
const res = await deleteUserApi(record.id)
|
||||
if (res.data.code === 200) {
|
||||
message.success('删除成功')
|
||||
proTableRef.value.reload()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLock(record) {
|
||||
const res = await lockUserApi(record.id)
|
||||
if (res.data.code === 200) {
|
||||
message.success('锁定成功')
|
||||
proTableRef.value.reload()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUnlock(record) {
|
||||
const res = await unlockUserApi(record.id)
|
||||
if (res.data.code === 200) {
|
||||
message.success('解锁成功')
|
||||
proTableRef.value.reload()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResetPwd(record) {
|
||||
const res = await resetUserPasswordApi(record.id)
|
||||
if (res.data.code === 200) {
|
||||
message.success('密码已重置为初始密码,并已短信通知用户')
|
||||
proTableRef.value.reload()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 密码修改弹窗 ----------------
|
||||
const changePwdOpen = ref(false)
|
||||
const changePwdLoading = ref(false)
|
||||
const newPassword = ref('')
|
||||
const changePwdTarget = ref(null)
|
||||
|
||||
function openChangePwdModal(record) {
|
||||
changePwdTarget.value = record
|
||||
newPassword.value = ''
|
||||
changePwdOpen.value = true
|
||||
}
|
||||
|
||||
async function handleChangePassword() {
|
||||
if (!isValidPasswordStrength(newPassword.value)) {
|
||||
message.error('密码需8位以上,且包含大小写字母和数字')
|
||||
return
|
||||
}
|
||||
changePwdLoading.value = true
|
||||
try {
|
||||
const res = await changeUserPasswordApi(changePwdTarget.value.id, newPassword.value)
|
||||
if (res.data.code === 200) {
|
||||
message.success('密码修改成功')
|
||||
changePwdOpen.value = false
|
||||
}
|
||||
} finally {
|
||||
changePwdLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.danger-link {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue