feat: 新增用户管理(增删改查/锁定解锁/密码修改重置)与首次登录强制改密

main
halo 2026-07-08 16:11:40 +08:00
parent 314067b91e
commit 0e4a38fd6f
10 changed files with 610 additions and 9 deletions

View File

@ -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: '密码重置并发送短信' }
]
},
{

23
mock-server/data/roles.js Normal file
View File

@ -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')}`
}

View File

@ -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')}`
}

View File

@ -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
}
})
})

View File

@ -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

View File

@ -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 })

View File

@ -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)
}

33
src/utils/validators.js Normal file
View File

@ -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()
}
}
}

View File

@ -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

View File

@ -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>