feat: 新增用户管理(增删改查/锁定解锁/密码修改重置)与首次登录强制改密
parent
314067b91e
commit
0e4a38fd6f
|
|
@ -18,7 +18,8 @@ export const menuTree = [
|
||||||
{ code: 'user:delete', name: '删除' },
|
{ code: 'user:delete', name: '删除' },
|
||||||
{ code: 'user:lock', name: '锁定' },
|
{ code: 'user:lock', name: '锁定' },
|
||||||
{ code: 'user:unlock', 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'],
|
roleIds: ['role_admin'],
|
||||||
status: 'enabled', // enabled | locked
|
status: 'enabled', // enabled | locked
|
||||||
failCount: 0,
|
failCount: 0,
|
||||||
|
mustChangePassword: false,
|
||||||
|
lastLoginAt: '',
|
||||||
createdAt: '2026-01-01 00:00:00'
|
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 })
|
if (code !== FIXED_SMS_CODE) return res.json({ code: 40003, msg: '验证码错误或已过期', data: null })
|
||||||
user.failCount = 0
|
user.failCount = 0
|
||||||
|
user.lastLoginAt = new Date().toISOString().slice(0, 19).replace('T', ' ')
|
||||||
|
|
||||||
const accessToken = crypto.randomUUID()
|
const accessToken = crypto.randomUUID()
|
||||||
const refreshToken = crypto.randomUUID()
|
const refreshToken = crypto.randomUUID()
|
||||||
|
|
@ -60,7 +61,8 @@ router.post('/login', (req, res) => {
|
||||||
accessToken,
|
accessToken,
|
||||||
userInfo: { id: user.id, username: user.username, realName: user.realName, orgId: user.orgId, roleIds: user.roleIds },
|
userInfo: { id: user.id, username: user.username, realName: user.realName, orgId: user.orgId, roleIds: user.roleIds },
|
||||||
menuTree,
|
menuTree,
|
||||||
buttonCodes: flattenButtons(menuTree)
|
buttonCodes: flattenButtons(menuTree),
|
||||||
|
mustChangePassword: !!user.mustChangePassword
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,35 @@
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
import crypto from 'crypto'
|
||||||
import { orgs, nextOrgId, buildOrgTree, collectDescendantIds } from '../data/orgs.js'
|
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'
|
import { requireAuth } from '../middleware/requireAuth.js'
|
||||||
|
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
router.use(requireAuth)
|
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) => {
|
router.get('/org/list', (req, res) => {
|
||||||
|
|
@ -96,4 +120,137 @@ router.delete('/org/:id', (req, res) => {
|
||||||
res.json({ code: 200, msg: 'ok', data: null })
|
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
|
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 createOrgApi = (data) => request.post('/system/org', data)
|
||||||
export const updateOrgApi = (id, data) => request.put(`/system/org/${id}`, data)
|
export const updateOrgApi = (id, data) => request.put(`/system/org/${id}`, data)
|
||||||
export const deleteOrgApi = (id) => request.delete(`/system/org/${id}`)
|
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,
|
userInfo: null,
|
||||||
menuTree: [],
|
menuTree: [],
|
||||||
buttonCodes: [],
|
buttonCodes: [],
|
||||||
routesReady: false
|
routesReady: false,
|
||||||
|
mustChangePassword: false
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
setAccessToken(token) {
|
setAccessToken(token) {
|
||||||
|
|
@ -17,11 +18,12 @@ export const useAuthStore = defineStore('auth', {
|
||||||
async login(payload) {
|
async login(payload) {
|
||||||
const res = await loginApi(payload)
|
const res = await loginApi(payload)
|
||||||
if (res.data.code === 200) {
|
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.setAccessToken(accessToken)
|
||||||
this.userInfo = userInfo
|
this.userInfo = userInfo
|
||||||
this.menuTree = menuTree
|
this.menuTree = menuTree
|
||||||
this.buttonCodes = buttonCodes
|
this.buttonCodes = buttonCodes
|
||||||
|
this.mustChangePassword = !!mustChangePassword
|
||||||
}
|
}
|
||||||
return res.data
|
return res.data
|
||||||
},
|
},
|
||||||
|
|
@ -43,11 +45,15 @@ export const useAuthStore = defineStore('auth', {
|
||||||
this.menuTree = []
|
this.menuTree = []
|
||||||
this.buttonCodes = []
|
this.buttonCodes = []
|
||||||
this.routesReady = false
|
this.routesReady = false
|
||||||
|
this.mustChangePassword = false
|
||||||
localStorage.removeItem('access_token')
|
localStorage.removeItem('access_token')
|
||||||
},
|
},
|
||||||
markRoutesReady() {
|
markRoutesReady() {
|
||||||
this.routesReady = true
|
this.routesReady = true
|
||||||
},
|
},
|
||||||
|
markPasswordChanged() {
|
||||||
|
this.mustChangePassword = false
|
||||||
|
},
|
||||||
hasButton(code) {
|
hasButton(code) {
|
||||||
return this.buttonCodes.includes(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-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-modal>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -68,6 +93,8 @@ import { sendSmsApi } from '@/api/auth'
|
||||||
import { getBaseUrl, setBaseUrlOverride } from '@/api/request'
|
import { getBaseUrl, setBaseUrlOverride } from '@/api/request'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { installDynamicRoutes, collectFirstPath } from '@/router/dynamic'
|
import { installDynamicRoutes, collectFirstPath } from '@/router/dynamic'
|
||||||
|
import { changeUserPasswordApi } from '@/api/system'
|
||||||
|
import { isValidPasswordStrength } from '@/utils/validators'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -81,6 +108,10 @@ const loginLoading = ref(false)
|
||||||
const baseUrlModalOpen = ref(false)
|
const baseUrlModalOpen = ref(false)
|
||||||
const baseUrlInput = ref(getBaseUrl())
|
const baseUrlInput = ref(getBaseUrl())
|
||||||
|
|
||||||
|
const forceChangePwdOpen = ref(false)
|
||||||
|
const changePwdLoading = ref(false)
|
||||||
|
const pwdForm = reactive({ newPassword: '', confirmPassword: '' })
|
||||||
|
|
||||||
async function handleSendSms() {
|
async function handleSendSms() {
|
||||||
if (!form.username || !form.password) {
|
if (!form.username || !form.password) {
|
||||||
message.warning('请先填写账号和密码')
|
message.warning('请先填写账号和密码')
|
||||||
|
|
@ -98,21 +129,52 @@ async function handleSendSms() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goToFirstPage() {
|
||||||
|
installDynamicRoutes(router, authStore.menuTree)
|
||||||
|
authStore.markRoutesReady()
|
||||||
|
const target = route.query.redirect || collectFirstPath(authStore.menuTree) || '/403'
|
||||||
|
router.replace(target)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogin() {
|
async function handleLogin() {
|
||||||
loginLoading.value = true
|
loginLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await authStore.login(form)
|
const res = await authStore.login(form)
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
installDynamicRoutes(router, authStore.menuTree)
|
if (authStore.mustChangePassword) {
|
||||||
authStore.markRoutesReady()
|
forceChangePwdOpen.value = true
|
||||||
const target = route.query.redirect || collectFirstPath(authStore.menuTree) || '/403'
|
} else {
|
||||||
router.replace(target)
|
goToFirstPage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loginLoading.value = false
|
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() {
|
function handleSaveBaseUrl() {
|
||||||
setBaseUrlOverride(baseUrlInput.value)
|
setBaseUrlOverride(baseUrlInput.value)
|
||||||
baseUrlModalOpen.value = false
|
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