refactor: 优化个人中心部分代码

This commit is contained in:
2024-05-09 22:33:14 +08:00
parent c64cf9da34
commit eb11cae635
13 changed files with 197 additions and 234 deletions

View File

@@ -8,7 +8,7 @@ export interface UserInfo {
phone: string phone: string
avatar: string avatar: string
pwdResetTime: string pwdResetTime: string
passwordExpired: boolean pwdExpired: boolean
registrationDate: string registrationDate: string
deptName: string deptName: string
roles: string[] roles: string[]

View File

@@ -19,12 +19,12 @@ export function updateUserPassword(data: { oldPassword: string; newPassword: str
} }
/** @desc 修改手机号 */ /** @desc 修改手机号 */
export function updateUserPhone(data: { newPhone: string; captcha: string; currentPassword: string }) { export function updateUserPhone(data: { phone: string; captcha: string; oldPassword: string }) {
return http.patch(`${BASE_URL}/phone`, data) return http.patch(`${BASE_URL}/phone`, data)
} }
/** @desc 修改邮箱 */ /** @desc 修改邮箱 */
export function updateUserEmail(data: { newEmail: string; captcha: string; currentPassword: string }) { export function updateUserEmail(data: { email: string; captcha: string; oldPassword: string }) {
return http.patch(`${BASE_URL}/email`, data) return http.patch(`${BASE_URL}/email`, data)
} }

View File

Before

Width:  |  Height:  |  Size: 846 B

After

Width:  |  Height:  |  Size: 846 B

View File

@@ -51,7 +51,7 @@
</a-row> </a-row>
<template #content> <template #content>
<a-doption @click="router.push('/setting/profile')"> <a-doption @click="router.push('/setting/profile')">
<span>账号管理</span> <span>个人中心</span>
</a-doption> </a-doption>
<a-divider :margin="0" /> <a-divider :margin="0" />
<a-doption @click="logout"> <a-doption @click="logout">
@@ -98,16 +98,14 @@ const logout = () => {
} }
}) })
} }
onMounted(() => {
checkPasswordExpired()
})
const checkPasswordExpired = () => { const checkPasswordExpired = () => {
if (!userStore.passwordExpiredShow || !userStore.userInfo.passwordExpired) { if (!userStore.pwdExpiredShow || !userStore.userInfo.pwdExpired) {
return return
} }
Modal.confirm({ Modal.confirm({
title: '提示', title: '提示',
content: '密码已过期,是否去修改', content: '密码已过期,需要跳转到修改密码页面',
hideCancel: false, hideCancel: false,
closable: true, closable: true,
onBeforeOk: async () => { onBeforeOk: async () => {
@@ -120,10 +118,14 @@ const checkPasswordExpired = () => {
}, },
onCancel: () => { onCancel: () => {
// 当前登录会话不再提示 // 当前登录会话不再提示
userStore.passwordExpiredShow = false userStore.pwdExpiredShow = false
} }
}) })
} }
onMounted(() => {
checkPasswordExpired()
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -61,7 +61,7 @@ export const constantRoutes: RouteRecordRaw[] = [
path: '/setting/profile', path: '/setting/profile',
name: 'SettingProfile', name: 'SettingProfile',
component: () => import('@/views/setting/profile/index.vue'), component: () => import('@/views/setting/profile/index.vue'),
meta: { title: '账号管理', showInTabs: false } meta: { title: '个人中心', showInTabs: false }
} }
] ]
} }

View File

@@ -27,7 +27,7 @@ const storeSetup = () => {
phone: '', phone: '',
avatar: '', avatar: '',
pwdResetTime: '', pwdResetTime: '',
passwordExpired: false, pwdExpired: false,
registrationDate: '', registrationDate: '',
deptName: '', deptName: '',
roles: [], roles: [],
@@ -37,7 +37,7 @@ const storeSetup = () => {
const avatar = computed(() => userInfo.avatar) const avatar = computed(() => userInfo.avatar)
const token = ref(getToken() || '') const token = ref(getToken() || '')
const passwordExpiredShow = ref<boolean>(true) const pwdExpiredShow = ref<boolean>(true)
const roles = ref<string[]>([]) // 当前用户角色 const roles = ref<string[]>([]) // 当前用户角色
const permissions = ref<string[]>([]) // 当前角色权限标识集合 const permissions = ref<string[]>([]) // 当前角色权限标识集合
@@ -91,7 +91,7 @@ const storeSetup = () => {
const logoutCallBack = async () => { const logoutCallBack = async () => {
roles.value = [] roles.value = []
permissions.value = [] permissions.value = []
passwordExpiredShow.value = true pwdExpiredShow.value = true
resetToken() resetToken()
resetRouter() resetRouter()
} }
@@ -114,7 +114,7 @@ const storeSetup = () => {
token, token,
roles, roles,
permissions, permissions,
passwordExpiredShow, pwdExpiredShow,
accountLogin, accountLogin,
emailLogin, emailLogin,
phoneLogin, phoneLogin,
@@ -127,5 +127,5 @@ const storeSetup = () => {
} }
export const useUserStore = defineStore('user', storeSetup, { export const useUserStore = defineStore('user', storeSetup, {
persist: { paths: ['token', 'roles', 'permissions', 'passwordExpiredShow'], storage: localStorage } persist: { paths: ['token', 'roles', 'permissions', 'pwdExpiredShow'], storage: localStorage }
}) })

View File

@@ -19,15 +19,21 @@
<script setup lang="ts"> <script setup lang="ts">
// import { getSmsCaptcha, getEmailCaptcha, updateUserEmail, updateUserPhone } from '@/apis' // import { getSmsCaptcha, getEmailCaptcha, updateUserEmail, updateUserPhone } from '@/apis'
import { updateUserPassword } from '@/apis'
import { Message } from '@arco-design/web-vue' import { Message } from '@arco-design/web-vue'
// import { encryptByRsa } from '@/utils/encrypt' import { encryptByRsa } from '@/utils/encrypt'
import { useUserStore } from '@/stores' import { useUserStore } from '@/stores'
import { type Columns, GiForm } from '@/components/GiForm' import { type Columns, GiForm } from '@/components/GiForm'
import { useForm } from '@/hooks' import { useForm } from '@/hooks'
import * as Regexp from '@/utils/regexp' import * as Regexp from '@/utils/regexp'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const verifyType = ref() const verifyType = ref()
const title = computed(() => (verifyType.value === 'phone' ? '修改手机号' : '修改邮箱')) const title = computed(
() => `修改${verifyType.value === 'phone' ? '手机号' : verifyType.value === 'email' ? '邮箱' : '密码'}`
)
const formRef = ref<InstanceType<typeof GiForm>>() const formRef = ref<InstanceType<typeof GiForm>>()
const options: Options = { const options: Options = {
@@ -39,7 +45,7 @@ const options: Options = {
const columns: Columns = [ const columns: Columns = [
{ {
label: '手机号', label: '手机号',
field: 'newPhone', field: 'phone',
type: 'input', type: 'input',
rules: [ rules: [
{ required: true, message: '请输入手机号' }, { required: true, message: '请输入手机号' },
@@ -65,21 +71,50 @@ const columns: Columns = [
label: '验证码', label: '验证码',
field: 'captcha', field: 'captcha',
type: 'input', type: 'input',
rules: [{ required: true, message: '请输入验证码' }] rules: [{ required: true, message: '请输入验证码' }],
hide: () => {
return !['phone', 'email'].includes(verifyType.value)
}
}, },
{ {
label: '当前密码', label: '当前密码',
field: 'currentPassword', field: 'oldPassword',
type: 'input', type: 'input-password',
rules: [{ required: true, message: '请输入当前密码' }] rules: [{ required: true, message: '请输入当前密码' }],
hide: () => {
return !userInfo.value.pwdResetTime
}
},
{
label: '新密码',
field: 'newPassword',
type: 'input-password',
rules: [{ required: true, message: '请输入新密码' }],
hide: () => {
return verifyType.value !== 'password'
}
},
{
label: '确认新密码',
field: 'rePassword',
type: 'input-password',
rules: [{ required: true, message: '请再次输入新密码' }],
props: {
placeholder: '请再次输入新密码'
},
hide: () => {
return verifyType.value !== 'password'
}
} }
] ]
const { form, resetForm } = useForm({ const { form, resetForm } = useForm({
newPhone: '', phone: '',
email: '',
captcha: '', captcha: '',
currentPassword: '', oldPassword: '',
email: '' newPassword: '',
rePassword: ''
}) })
// 重置 // 重置
@@ -112,7 +147,7 @@ const onCaptcha = async () => {
captchaBtnName.value = '发送中...' captchaBtnName.value = '发送中...'
if (verifyType.value === 'phone') { if (verifyType.value === 'phone') {
// await getSmsCaptcha({ // await getSmsCaptcha({
// phone: form.newPhone // phone: form.phone
// }) // })
} else if (verifyType.value === 'email') { } else if (verifyType.value === 'email') {
// await getEmailCaptcha({ // await getEmailCaptcha({
@@ -138,24 +173,36 @@ const onCaptcha = async () => {
} }
} }
const userStore = useUserStore()
// 保存 // 保存
const save = async () => { const save = async () => {
const isInvalid = await formRef.value?.formRef?.validate() const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false if (isInvalid) return false
try { try {
if (verifyType.value === 'phone') { if (verifyType.value === 'phone') {
// await updateUserEmail({ // await updateUserPhone({
// newEmail: form.email, // phone: form.phone,
// captcha: form.captcha, // captcha: form.captcha,
// currentPassword: encryptByRsa(form.currentPassword) as string // oldPassword: encryptByRsa(form.oldPassword) as string
// }) // })
} else if (verifyType.value === 'email') { } else if (verifyType.value === 'email') {
// await updateUserPhone({ // await updateUserEmail({
// newPhone: form.email, // email: form.email,
// captcha: form.captcha, // captcha: form.captcha,
// currentPassword: encryptByRsa(form.currentPassword) as string // oldPassword: encryptByRsa(form.oldPassword) as string
// }) // })
} else if (verifyType.value === 'password') {
if (form.newPassword !== form.rePassword) {
Message.error('两次新密码不一致')
return false
}
if (form.newPassword === form.oldPassword) {
Message.error('新密码与旧密码不能相同')
return false
}
await updateUserPassword({
oldPassword: encryptByRsa(form.oldPassword) || '',
newPassword: encryptByRsa(form.newPassword) || ''
})
} }
Message.success('修改成功') Message.success('修改成功')
// 修改成功后,重新获取用户信息 // 修改成功后,重新获取用户信息

View File

@@ -1,178 +0,0 @@
<template>
<a-card title="密码策略" bordered class="gradient-card">
<div class="item">
<div class="icon-wrapper"><GiSvgIcon name="password" :size="26" /></div>
<div class="info">
<div class="info-top">
<span class="label">登录密码</span>
</div>
<div class="info-desc">为了您的账号安全建议定期修改密码</div>
</div>
<div class="btn-wrapper">
<a-button class="btn" @click="onUpdate">修改</a-button>
</div>
</div>
<div class="detail">
<div class="sub-text-wrapper">
<div class="sub-text">
密码至少包含
<span class="sub-text-value">大写字母</span>
<span class="sub-text-value">小写字母</span>
<span class="sub-text-value">数字</span>
<span class="sub-text-value" v-if="securityConfig.password_special_char.value == 1">特殊字符</span>
</div>
<div class="sub-text" v-if="securityConfig.password_contain_name.value == 1">
密码不能包含<span class="sub-text-value">正反序用户名</span>
</div>
<div class="sub-text">
密码长度至少
<span class="sub-text-value">
{{ securityConfig.password_min_length.value }}
</span>
</div>
<div class="sub-text">
<div v-if="securityConfig.password_expiration_days.value == 0">未设置密码有效期</div>
<div v-else>
密码有效期
<span class="sub-text-value">
{{ securityConfig.password_expiration_days.value }}
</span>
</div>
</div>
<div class="sub-text">
连续密码错误可重试
<span class="sub-text-value">
{{ securityConfig.password_error_count.value }}
</span>
</div>
<div class="sub-text">
超过错误密码重试次数账号将被锁定
<span class="sub-text-value">
{{ securityConfig.password_lock_minutes.value }}
</span>
分钟
</div>
</div>
</div>
</a-card>
<a-modal v-model:visible="visible" title="修改密码" @before-ok="save" @close="reset">
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns" />
</a-modal>
</template>
<script lang="ts" setup>
import { listOption, type OptionResp, type SecurityConfigResp, updateUserPassword } from '@/apis'
import { Message } from '@arco-design/web-vue'
import { encryptByRsa } from '@/utils/encrypt'
import { type Columns, GiForm } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useUserStore } from '@/stores'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const formRef = ref<InstanceType<typeof GiForm>>()
const options: Options = {
form: {},
col: { xs: 24, sm: 24, md: 24, lg: 24, xl: 24, xxl: 24 },
btns: { hide: true }
}
const columns: Columns = [
{
label: '当前密码',
field: 'oldPassword',
type: 'input-password',
rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }],
hide: () => {
return userInfo.pwdResetTime
}
},
{
label: '新密码',
field: 'newPassword',
type: 'input-password',
rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }]
},
{
label: '确认新密码',
field: 'rePassword',
type: 'input-password',
rules: [{ required: true, message: '密码长度不正确', maxLength: 32, minLength: 6 }],
props: {
placeholder: '请再次输入新密码'
}
}
]
const { form, resetForm } = useForm({
oldPassword: '',
newPassword: '',
rePassword: ''
})
// 重置
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
}
const visible = ref(false)
// 修改
const onUpdate = async () => {
reset()
visible.value = true
}
// 保存
const save = async () => {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
if (form.newPassword !== form.rePassword) {
Message.error('两次新密码不一致')
return false
}
if (form.newPassword === form.oldPassword) {
Message.error('新密码与旧密码不能相同')
return false
}
try {
await updateUserPassword({
oldPassword: encryptByRsa(form.oldPassword) || '',
newPassword: encryptByRsa(form.newPassword) || ''
})
Message.success('修改成功')
return true
} catch (error) {
return false
}
}
const securityConfig = ref<SecurityConfigResp>({
password_contain_name: {},
password_error_count: {},
password_expiration_days: {},
password_lock_minutes: {},
password_min_length: {},
password_special_char: {},
password_update_interval: {}
})
// 查询列表数据
const getDataList = async () => {
const { data } = await listOption({ code: Object.keys(securityConfig.value) })
securityConfig.value = data.reduce((obj: SecurityConfigResp, option: OptionResp) => {
obj[option.code] = { ...option, value: parseInt(option.value) }
return obj
}, {})
}
onMounted(() => {
getDataList()
})
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,109 @@
<template>
<a-card title="安全设置" bordered class="gradient-card">
<div v-for="item in modeList" :key="item.title">
<div class="item">
<div class="icon-wrapper"><GiSvgIcon :name="item.icon" :size="26" /></div>
<div class="info">
<div class="info-top">
<span class="label">{{ item.title }}</span>
<span class="bind">
<icon-check-circle-fill v-if="item.status" :size="14" class="success" />
<icon-exclamation-circle-fill v-else :size="14" class="warning" />
<span style="font-size: 12px" :class="item.status ? 'success' : 'warning'">{{ item.statusString }}</span>
</span>
</div>
<div class="info-desc">
<span class="value">{{ item.value }}</span>
{{ item.subtitle }}
</div>
</div>
<div class="btn-wrapper">
<a-button
v-if="item.jumpMode == 'modal'"
class="btn"
:type="item.status ? 'secondary' : 'primary'"
@click="onUpdate(item.type, item.status)"
>
{{ ['password'].includes(item.type) || item.status ? '修改' : '绑定' }}
</a-button>
</div>
</div>
</div>
</a-card>
<VerifyModel ref="verifyModelRef" />
</template>
<script lang="ts" setup>
import { listOption, type OptionResp, type SecurityConfigResp } from '@/apis'
import type { ModeItem } from '../type'
import VerifyModel from '../components/VerifyModel.vue'
import { useUserStore } from '@/stores'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const modeList = ref<ModeItem[]>([])
modeList.value = [
{
title: '安全手机',
icon: 'phone-color',
value: `${userInfo.value.phone + ' ' || '手机号'}`,
subtitle: `可用于身份验证、密码找回、通知接收`,
type: 'phone',
jumpMode: 'modal',
status: !!userInfo.value.phone,
statusString: userInfo.value.phone ? '已绑定' : '未绑定'
},
{
title: '安全邮箱',
icon: 'email-color',
value: `${userInfo.value.email + ' ' || '邮箱'}`,
subtitle: `可用于身份验证、密码找回、通知接收`,
type: 'email',
jumpMode: 'modal',
status: !!userInfo.value.email,
statusString: userInfo.value.email ? '已绑定' : '未绑定'
},
{
title: '登录密码',
icon: 'password-color',
subtitle: userInfo.value.pwdResetTime ? `为了您的账号安全,建议定期修改密码` : '请设置密码,可通过账号+密码登录',
type: 'password',
jumpMode: 'modal',
status: !!userInfo.value.pwdResetTime,
statusString: userInfo.value.pwdResetTime ? '已设置' : '未设置'
}
]
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
// 修改
const onUpdate = (type: string) => {
verifyModelRef.value?.open(type)
}
const securityConfig = ref<SecurityConfigResp>({
password_contain_name: {},
password_error_count: {},
password_expiration_days: {},
password_lock_minutes: {},
password_min_length: {},
password_special_char: {},
password_update_interval: {}
})
// 查询列表数据
const getDataList = async () => {
const { data } = await listOption({ code: Object.keys(securityConfig.value) })
securityConfig.value = data.reduce((obj: SecurityConfigResp, option: OptionResp) => {
obj[option.code] = { ...option, value: parseInt(option.value) }
return obj
}, {})
}
onMounted(() => {
getDataList()
})
</script>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<a-card title="登录方式" bordered class="gradient-card"> <a-card title="第三方账号" bordered class="gradient-card">
<div v-for="item in modeList" :key="item.title"> <div v-for="item in modeList" :key="item.title">
<div class="item"> <div class="item">
<div class="icon-wrapper"><GiSvgIcon :name="item.icon" :size="26" /></div> <div class="icon-wrapper"><GiSvgIcon :name="item.icon" :size="26" /></div>
@@ -55,24 +55,6 @@ const userInfo = computed(() => userStore.userInfo)
const socialList = ref<any>([]) const socialList = ref<any>([])
const modeList = ref<ModeItem[]>([]) const modeList = ref<ModeItem[]>([])
modeList.value = [ modeList.value = [
{
title: '绑定手机',
icon: 'phone-color',
value: `${userInfo.value.phone + ' ' || '绑定后,'}`,
subtitle: `可通过手机验证码快捷登录`,
type: 'phone',
jumpMode: 'modal',
status: !!userInfo.value.phone
},
{
title: '绑定邮箱',
icon: 'email-color',
value: `${userInfo.value.email + ' ' || '绑定后,'}`,
subtitle: `可通过邮箱验证码进行登录`,
type: 'email',
jumpMode: 'modal',
status: !!userInfo.value.email
},
{ {
title: '绑定 Gitee', title: '绑定 Gitee',
icon: 'gitee', icon: 'gitee',

View File

@@ -17,9 +17,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import LeftBox from './LeftBox.vue' import LeftBox from './BasicInfo.vue'
import RightBox from './RightBox.vue' import RightBox from './Social.vue'
import PasswordPolicy from './PasswordPolicy.vue' import PasswordPolicy from './Security.vue'
defineOptions({ name: 'SettingProfile' }) defineOptions({ name: 'SettingProfile' })
</script> </script>

View File

@@ -6,4 +6,5 @@ export interface ModeItem {
type: 'phone' | 'email' | 'gitee' | 'github' type: 'phone' | 'email' | 'gitee' | 'github'
jumpMode?: 'link' | 'modal' jumpMode?: 'link' | 'modal'
status: boolean status: boolean
statusString: string
} }