feat: 新增修改手机号、修改邮箱、修改密码、修改基本信息

This commit is contained in:
2024-04-27 15:05:00 +08:00
parent 136f07c8e4
commit 99498a3e54
14 changed files with 394 additions and 188 deletions

View File

@@ -7,6 +7,7 @@ export interface UserInfo {
email: string
phone: string
avatar: string
pwdResetTime: string
registrationDate: string
deptName: string
roles: string[]

View File

@@ -2,9 +2,9 @@ export * from './user'
export * from './role'
export * from './menu'
export * from './dept'
export * from '../monitor/log'
export * from './announcement'
export * from './dict'
export * from './file'
export * from './storage'
export * from './option'
export * from './announcement'
export * from './user-center'

View File

@@ -0,0 +1,39 @@
import http from '@/utils/http'
import type * as System from '@/apis/system/type'
const BASE_URL = '/system/user'
/** @desc 修改用户基本信息 */
export function updateUserBaseInfo(data: { nickname: string; gender: number }) {
return http.patch(`${BASE_URL}/basic/info`, data)
}
/** @desc 修改密码 */
export function updateUserPassword(data: { oldPassword: string; newPassword: string }) {
return http.patch(`${BASE_URL}/password`, data)
}
/** @desc 修改手机号 */
export function updateUserPhone(data: { newPhone: string; captcha: string; currentPassword: string }) {
return http.patch(`${BASE_URL}/phone`, data)
}
/** @desc 修改邮箱 */
export function updateUserEmail(data: { newEmail: string; captcha: string; currentPassword: string }) {
return http.patch(`${BASE_URL}/email`, data)
}
/** @desc 获取绑定的三方账号 */
export function listUserSocial() {
return http.get<System.BindSocialAccountRes[]>(`${BASE_URL}/social`)
}
/** @desc 绑定三方账号 */
export function bindSocialAccount(source: string, data: any) {
return http.post(`${BASE_URL}/social/${source}`, data)
}
/** @desc 解绑三方账号 */
export function unbindSocialAccount(source: string) {
return http.del(`${BASE_URL}/social/${source}`)
}

View File

@@ -37,25 +37,3 @@ export function exportUser(query: System.UserQuery) {
export function resetUserPwd(data: any, id: string) {
return http.patch(`${BASE_URL}/${id}/password`, data)
}
/** @desc 修改用户基础信息 */
export function updateUserBaseInfo(data: { nickname?: string; gender?: number }) {
return http.patch(`${BASE_URL}/basic/info`, data)
}
/** @desc 修改邮箱 */
export function updateUserEmail(data: { newEmail: string; captcha: string; currentPassword: string }) {
return http.patch(`${BASE_URL}/email`, data)
}
/**@desc 绑定三方账号 */
export function bindSocialAccount(source: string, data: any) {
return http.post(`${BASE_URL}/social/${source}`, data)
}
/**@desc 获取绑定的三方账号 */
export function getSocialAccount() {
return http.get<System.BindSocialAccountRes[]>(`${BASE_URL}/social`)
}
/**@desc 解绑三方账号 */
export function unbindSocialAccount(source: string) {
return http.del(`${BASE_URL}/social/${source}`)
}

View File

@@ -26,6 +26,7 @@ const storeSetup = () => {
email: '',
phone: '',
avatar: '',
pwdResetTime: '',
registrationDate: '',
deptName: '',
roles: [],

View File

@@ -313,6 +313,12 @@
.btn {
height: 28px;
width: 56px;
&:hover {
-webkit-box-shadow: 0 2px 3px rgba(0, 0, 0, .15);
box-shadow: 0 2px 3px rgba(0, 0, 0, .15);
border-color: rgb(var(--primary-5));
color: var(--color-text-2);
}
}
}
}

View File

@@ -74,8 +74,10 @@ const handleLogin = async () => {
}
}
const captchaTime = ref(60)
const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
@@ -84,15 +86,13 @@ const resetCaptcha = () => {
captchaDisable.value = false
}
const captchaBtnName = ref('获取验证码')
const captchaLoading = ref(false)
const captchaDisable = ref(false)
// 获取验证码
const onCaptcha = async () => {
try {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.validateField('email')
if (isInvalid) return
try {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
// await getEmailCaptcha({

View File

@@ -77,8 +77,10 @@ const handleLogin = async () => {
}
}
const captchaTime = ref(60)
const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
@@ -87,15 +89,13 @@ const resetCaptcha = () => {
captchaDisable.value = false
}
const captchaBtnName = ref('获取验证码')
const captchaLoading = ref(false)
const captchaDisable = ref(false)
// 获取验证码
const onCaptcha = async () => {
try {
if (captchaLoading.value) return
const isInvalid = await formRef.value?.validateField('phone')
if (isInvalid) return
try {
captchaLoading.value = true
captchaBtnName.value = '发送中...'
// await getSmsCaptcha({

View File

@@ -1,43 +0,0 @@
<template>
<div class="card">
<div class="card_header">
<slot name="header"></slot>
</div>
<div class="card_body">
<slot name="body"> </slot>
</div>
<slot name="footer"></slot>
</div>
</template>
<style scoped lang="scss">
.card {
width: 100%;
display: flex;
flex-direction: column;
border-radius: 8px;
background: var(--color-bg-1);
border: 1px solid var(--color-neutral-2);
.card_header {
padding: 18px 20px;
background: -webkit-gradient(
linear,
left top,
left bottom,
from(rgba(232, 244, 255, 0.5)),
to(hsla(0, 0%, 100%, 0))
);
font-size: 16px;
font-weight: 500;
line-height: 24px;
background: linear-gradient(180deg, rgba(232, 244, 255, 0.5), hsla(0, 0%, 100%, 0));
}
.card_body {
flex: 1;
padding: 15px 28px;
}
.card_footer {
padding: 15px 28px;
border-top: 1px solid var(--color-neutral-2);
}
}
</style>

View File

@@ -1,107 +1,184 @@
<template>
<a-modal v-model:visible="visible" :title="title" @before-ok="save" @cancel="handleCancel">
<a-form :model="form" ref="formRef">
<a-form-item
field="newPhone"
label="新手机号"
:rules="[{ required: true, match: Regexp.Phone, message: '请输入正确的手机号' }]"
v-if="verifyType === 'phone'"
<a-modal v-model:visible="visible" :title="title" @before-ok="save" @close="reset">
<GiForm ref="formRef" v-model="form" :options="options" :columns="columns">
<template #captcha>
<a-input v-model="form.captcha" placeholder="请输入验证码" :max-length="4" allow-clear style="flex: 1 1" />
<a-button
class="captcha-btn"
:loading="captchaLoading"
:disabled="captchaDisable"
size="large"
@click="onCaptcha"
>
<a-input v-model="form.newPhone" />
</a-form-item>
<a-form-item
field="email"
label="邮箱"
v-if="verifyType === 'email'"
:rules="[{ required: true, match: Regexp.Email, message: '请输入正确的邮箱' }]"
>
<a-input v-model="form.email" />
</a-form-item>
<a-form-item field="verifyCode" label="验证码" :rules="[{ required: true, message: '请输入正确的验证码' }]">
<a-input v-model="form.captcha" />
<a-button type="outline" @click="onSendCaptcha">发送验证码</a-button>
</a-form-item>
<a-form-item
field="currentPassword"
label="当前密码"
:rules="[
{ required: true, message: '请输入当前密码' },
{ match: Regexp.Password, message: '请输入格式的密码' }
]"
>
<a-input v-model="form.currentPassword" />
</a-form-item>
</a-form>
{{ captchaBtnName }}
</a-button>
</template>
</GiForm>
</a-modal>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { getSmsCaptcha, getEmailCaptcha, updateUserEmail } from '@/apis'
import { encryptByRsa } from '@/utils/encrypt'
// import { getSmsCaptcha, getEmailCaptcha, updateUserEmail, updateUserPhone } from '@/apis'
import { Message } from '@arco-design/web-vue'
// import { encryptByRsa } from '@/utils/encrypt'
import * as Regexp from '@/utils/regexp'
import { Message, type Modal } from '@arco-design/web-vue'
import { useUserStore } from '@/stores'
const userStore = useUserStore()
const visible = ref<boolean>(false)
const form = reactive({
import { type Columns, GiForm } from '@/components/GiForm'
import { useForm } from '@/hooks'
const verifyType = ref()
const title = computed(() => (verifyType.value === 'phone' ? '修改手机号' : '修改邮箱'))
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: 'newPhone',
type: 'input',
rules: [
{ required: true, message: '请输入手机号' },
{ match: Regexp.Phone, message: '请输入正确的手机号' }
],
hide: () => {
return verifyType.value !== 'phone'
}
},
{
label: '邮箱',
field: 'email',
type: 'input',
rules: [
{ required: true, message: '请输入邮箱' },
{ match: Regexp.Email, message: '请输入正确的邮箱' }
],
hide: () => {
return verifyType.value !== 'email'
}
},
{
label: '验证码',
field: 'captcha',
type: 'input',
rules: [{ required: true, message: '请输入验证码' }]
},
{
label: '当前密码',
field: 'currentPassword',
type: 'input',
rules: [{ required: true, message: '请输入当前密码' }]
}
]
const { form, resetForm } = useForm({
newPhone: '',
captcha: '',
currentPassword: '',
email: ''
})
const formRef = ref()
const verifyType = ref()
const title = computed(() => {
return verifyType.value === 'phone' ? '修改手机号' : '修改邮箱'
})
const onSendCaptcha = () => {
formRef.value.validateField(verifyType.value === 'phone' ? 'newPhone' : 'email', (validate) => {
if (!validate) {
// 发送验证码
if (verifyType.value === 'phone') {
//手机号
getSmsCaptcha({ phone: form.newPhone }).then((res) => {
console.log(res)
})
} else if (verifyType.value === 'email') {
//邮箱
getEmailCaptcha({ email: form.email }).then((res) => {
console.log(res)
})
}
}
})
// 重置
const reset = () => {
formRef.value?.formRef?.resetFields()
resetForm()
resetCaptcha()
}
const save: InstanceType<typeof Modal>['onBeforeOk'] = async () => {
const flag = await formRef.value?.validate()
if (flag) return false
const captchaTimer = ref()
const captchaTime = ref(60)
const captchaBtnName = ref('获取验证码')
const captchaDisable = ref(false)
// 重置验证码
const resetCaptcha = () => {
window.clearInterval(captchaTimer.value)
captchaTime.value = 60
captchaBtnName.value = '获取验证码'
captchaDisable.value = false
}
const captchaLoading = ref(false)
// 获取验证码
const onCaptcha = async () => {
const isInvalid = await formRef.value?.formRef?.validateField(verifyType.value === 'phone' ? 'newPhone' : 'email')
if (isInvalid) return false
// 发送验证码
try {
const res = await updateUserEmail({
newEmail: form.email,
captcha: form.captcha,
currentPassword: encryptByRsa(form.currentPassword) as string
})
if (res.code === 200) {
Message.success('修改成功')
visible.value = false
// 修改成功后,重新获取用户信息
userStore.getInfo()
return true
captchaLoading.value = true
captchaBtnName.value = '发送中...'
if (verifyType.value === 'phone') {
// await getSmsCaptcha({
// phone: form.newPhone
// })
} else if (verifyType.value === 'email') {
// await getEmailCaptcha({
// email: form.email
// })
}
captchaLoading.value = false
captchaDisable.value = true
captchaBtnName.value = `获取验证码(${(captchaTime.value -= 1)}s)`
// Message.success('发送成功')
Message.success('仅提供效果演示,实际使用请查看代码取消相关注释')
captchaTimer.value = window.setInterval(() => {
captchaTime.value -= 1
captchaBtnName.value = `获取验证码(${captchaTime.value}s)`
if (captchaTime.value <= 0) {
resetCaptcha()
}
}, 1000)
} catch (error) {
resetCaptcha()
} finally {
captchaLoading.value = false
}
}
const userStore = useUserStore()
// 保存
const save = async () => {
const isInvalid = await formRef.value?.formRef?.validate()
if (isInvalid) return false
try {
if (verifyType.value === 'phone') {
// await updateUserEmail({
// newEmail: form.email,
// captcha: form.captcha,
// currentPassword: encryptByRsa(form.currentPassword) as string
// })
} else if (verifyType.value === 'email') {
// await updateUserPhone({
// newPhone: form.email,
// captcha: form.captcha,
// currentPassword: encryptByRsa(form.currentPassword) as string
// })
}
Message.success('修改成功')
// 修改成功后,重新获取用户信息
await userStore.getInfo()
return true
} catch (error) {
return false
}
// return await saveApi()
}
const handleCancel = () => {
formRef.value?.resetFields()
visible.value = false
}
const visible = ref(false)
// 打开弹框
const open = (type: string) => {
verifyType.value = type
visible.value = true
}
defineExpose({
open
})
defineExpose({ open })
</script>
<style lang="scss" scoped>
.captcha-btn {
margin-left: 12px;
min-width: 98px;
border-radius: 4px;
}
</style>

View File

@@ -7,7 +7,7 @@
</div>
<div class="name">
<span style="margin-right: 10px">{{ userInfo.nickname }}</span>
<icon-edit :size="16" class="btn" @click="onEditNickName" />
<icon-edit :size="16" class="btn" @click="onUpdate" />
</div>
<div class="id">
<GiSvgIcon name="id" :size="16" />
@@ -43,22 +43,81 @@
</div>
<div class="footer">注册于 {{ userInfo.registrationDate }}</div>
</a-card>
<VerifyModel ref="verifyModelRef" />
<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 setup lang="ts">
import { updateUserBaseInfo } from '@/apis'
import VerifyModel from '../components/VerifyModel.vue'
import { updateUserBaseInfo, updateUserPassword } from '@/apis'
import { Message } from '@arco-design/web-vue'
import { type Columns, GiForm } from '@/components/GiForm'
import { useForm } from '@/hooks'
import { useUserStore } from '@/stores'
import { encryptByRsa } from '@/utils/encrypt'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
const onEditNickName = () => {
userStore.editNickNameVisible = true
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 openVerifyModel = (type: 'phone' | 'email') => {
verifyModelRef.value?.open(type)
const columns: Columns = [
{
label: '昵称',
field: 'nickname',
type: 'input',
rules: [{ required: true, message: '请输入昵称' }]
},
{
label: '性别',
field: 'gender',
type: 'radio-group',
options: [
{ label: '男', value: 1 },
{ label: '女', value: 2 },
{ label: '未知', value: 0, disabled: true }
],
rules: [{ required: true, message: '请选择性别' }]
}
]
const { form, resetForm } = useForm({
nickname: userInfo.value.nickname,
gender: userInfo.value.gender
})
// 重置
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
try {
await updateUserBaseInfo(form)
Message.success('修改成功')
// 修改成功后,重新获取用户信息
await userStore.getInfo()
return true
} catch (error) {
return false
}
}
</script>

View File

@@ -24,7 +24,7 @@
v-if="item.jumpMode == 'modal'"
class="btn"
:type="item.status ? 'secondary' : 'primary'"
@click="openVerifyModel(item.type, item.status)"
@click="onUpdate(item.type, item.status)"
>
{{ item.status ? '修改' : '绑定' }}
</a-button>
@@ -44,19 +44,15 @@
</template>
<script setup lang="ts">
import { socialAuth, getSocialAccount, unbindSocialAccount } from '@/apis'
import { socialAuth, listUserSocial, unbindSocialAccount } from '@/apis'
import type { ModeItem } from '../type'
import { useUserStore } from '@/stores'
import VerifyModel from '../components/VerifyModel.vue'
import { useUserStore } from '@/stores'
const userStore = useUserStore()
const userInfo = computed(() => userStore.userInfo)
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
const openVerifyModel = (type: 'phone' | 'email') => {
verifyModelRef.value?.open(type)
}
const socialList = ref<any>([])
const socialList = ref<any>([])
const modeList = ref<ModeItem[]>([])
modeList.value = [
{
@@ -94,12 +90,8 @@ modeList.value = [
status: socialList.value.some((el) => el == 'github')
}
]
const initData = () => {
getSocialAccount().then((res) => {
socialList.value = res.data.map((el) => el.source)
})
}
// 绑定
const onBinding = (type: string, status: boolean) => {
if (!status) {
socialAuth(type).then((res) => {
@@ -114,6 +106,19 @@ const onBinding = (type: string, status: boolean) => {
}
}
const verifyModelRef = ref<InstanceType<typeof VerifyModel>>()
// 修改
const onUpdate = (type: string) => {
verifyModelRef.value?.open(type)
}
// 初始化数据
const initData = () => {
listUserSocial().then((res) => {
socialList.value = res.data.map((el) => el.source)
})
}
onMounted(() => {
initData()
})

View File

@@ -19,7 +19,7 @@
</div>
</div>
<div class="btn-wrapper">
<a-switch disabled />
<a-switch disabled title="未开放" />
</div>
</div>
</div>

View File

@@ -9,7 +9,7 @@
<div class="info-desc">为了您的账号安全建议定期修改密码</div>
</div>
<div class="btn-wrapper">
<a-button class="btn">修改</a-button>
<a-button class="btn" @click="onUpdate">修改</a-button>
</div>
</div>
<div class="detail">
@@ -31,8 +31,91 @@
</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></script>
<script lang="ts" setup>
import { 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: '请输入当前密码' }],
hide: () => {
return userInfo.pwdResetTime
}
},
{
label: '新密码',
field: 'newPassword',
type: 'input-password',
rules: [{ required: true, message: '请输入新密码' }]
},
{
label: '确认新密码',
field: 'rePassword',
type: 'input-password',
rules: [{ required: true, message: '请再次输入新密码' }],
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
try {
await updateUserPassword({
oldPassword: encryptByRsa(form.oldPassword) || '',
newPassword: encryptByRsa(form.newPassword) || ''
})
Message.success('修改成功')
return true
} catch (error) {
return false
}
}
</script>
<style lang="scss" scoped></style>