feat: 修改昵称,修改手机号

This commit is contained in:
rd
2025-09-11 18:16:05 +08:00
parent fd4c26f716
commit 465cb2fcad
9 changed files with 675 additions and 34 deletions

View File

@ -87,3 +87,8 @@ export const getMyEnterprises = () => {
export const getMyPrimaryEnterprise = () => {
return Http.get('/v1/me/primary-enterprise');
};
// 发送修改手机号验证码
export const postUpdateMobileCaptcha = (params = {}) => {
return Http.post('/v1/sms/update-mobile-captcha', params);
};

View File

@ -17,7 +17,7 @@
color: #211f24;
font-size: 16px;
font-style: normal;
font-weight: 400;
font-weight: 500;
justify-content: flex-start;
}
}

View File

@ -0,0 +1,200 @@
<template>
<Modal
class="change-mobile-modal"
centered
v-model:open="visible"
title="修改手机号"
@cancel="onClose"
:footer="null"
>
<Form
:model="form"
ref="formRef"
:rules="formRules"
labelAlign="right"
:labelCol="{ span: 5 }"
:wrapperCol="{ span: 19 }"
>
<FormItem label="手机号" name="mobile">
<Input v-model:value="form.mobile" placeholder="请输入新的手机号" size="large" />
</FormItem>
<FormItem label="获取验证码" name="captcha">
<Input v-model:value="form.captcha" placeholder="请输入验证码" size="large" :maxlength="6">
<template #suffix>
<div class="w-79px flex justify-center whitespace-nowrap">
<span
class="color-#939499 font-family-regular text-16px font-400 lh-24px cursor-not-allowed"
:class="{
'!color-#6D4CFE': isLegalMobile || countdown > 0,
'!cursor-pointer': canGetCaptcha,
}"
@click="getCode"
>{{ countdown > 0 ? `${countdown}s` : hasGetCode ? '重新发送' : '发送验证码' }}</span
>
</div>
</template>
</Input>
</FormItem>
</Form>
<div class="flex justify-end mt-20px">
<Button class="mr-16px" size="large" @click="onClose">取消</Button>
<Button type="primary" size="large" @click="handleConfirm">确定</Button>
</div>
</Modal>
</template>
<script setup>
import { computed, onUnmounted, ref } from 'vue';
import { Button, Form, FormItem, Input, message, Modal } from 'ant-design-vue';
import { postUpdateMobileCaptcha } from '@/api/all/login';
import { updateMobile } from '@/api/all';
import { useUserStore } from '@/stores';
const store = useUserStore();
const formRef = ref();
const visible = ref(false);
const timer = ref();
const isLegalMobile = ref(false);
const hasGetCode = ref(false);
const countdown = ref(0);
const formRules = {
mobile: [
{
required: true,
validator: (_rule, value) => {
if (!value) {
isLegalMobile.value = false;
return Promise.reject('手机号不能为空');
}
if (value && !/^1[3-9]\d{9}$/.test(value)) {
isLegalMobile.value = false;
return Promise.reject('手机号格式不正确');
} else {
isLegalMobile.value = true;
return Promise.resolve();
}
},
trigger: ['blur'],
},
],
captcha: [
{
required: true,
validator: (_rule, value) => {
if (!value) {
return Promise.reject('请输入验证码');
}
if (!/^\d{6}$/.test(value)) {
return Promise.reject('验证码必须是6位数字');
} else {
return Promise.resolve();
}
},
trigger: ['blur'],
},
],
};
const form = ref({
mobile: '',
captcha: '',
});
const canGetCaptcha = computed(() => {
return isLegalMobile.value && countdown.value === 0;
});
// 打开弹窗
const open = () => {
visible.value = true;
};
// 发送验证码
const getCode = async () => {
if (!canGetCaptcha.value) return;
formRef.value.validateFields('mobile').then(() => {
getCaptcha();
});
};
const getCaptcha = async () => {
try {
const { code, message: msg } = await postUpdateMobileCaptcha({ mobile: form.value.mobile });
if (code === 200) {
startCountdown();
message.success(msg);
}
} catch (error) {
// 重置倒计时
countdown.value = 0;
clearInterval(timer.value);
}
};
const startCountdown = () => {
countdown.value = 60;
hasGetCode.value = true;
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value);
timer.value = null;
}
}, 1000);
};
// 确定按钮点击
const handleConfirm = () => {
formRef.value.validate().then(async () => {
const { code } = await updateMobile(form.value);
if (code === 200) {
message.success('修改成功!');
store.getUserInfo();
visible.value = false;
}
});
};
// 取消按钮点击
const onClose = () => {
form.value = {
mobile: '',
captcha: '',
};
countdown.value = 0;
visible.value = false;
formRef.value.resetFields();
formRef.value.clearValidate();
};
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value);
}
});
// 暴露方法供外部调用
defineExpose({
open,
});
</script>
<style lang="scss">
.change-mobile-modal {
.ant-modal-header {
border-bottom: none !important;
}
.ant-modal-body {
padding: 20px 24px !important;
}
.ant-modal-footer {
height: 56px !important;
border-top: none !important;
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<Modal
class="change-name-modal"
v-model:open="visible"
centered
:width="480"
title="修改昵称"
:footer="null"
@cancel="onClose"
>
<Form :model="form" ref="formRef">
<FormItem label="昵称" name="name">
<Input v-model:value="form.name" placeholder="请输入新昵称" size="large" />
</FormItem>
</Form>
<div class="flex justify-end mt-20px">
<Button class="mr-16px" size="large" @click="onClose">取消</Button>
<Button type="primary" size="large" :loading="loading" @click="handleNicknameConfirm">确定</Button>
</div>
</Modal>
</template>
<script setup>
import { ref } from 'vue';
import { Button, Form, FormItem, Input, message, Modal } from 'ant-design-vue';
import { updateMyInfo } from '@/api/all';
import { useUserStore } from '@/stores';
const store = useUserStore();
const formRef = ref();
const visible = ref(false);
const loading = ref(false);
const form = ref({
name: '',
});
// 打开弹窗
const open = (userName) => {
form.value.name = userName;
visible.value = true;
};
// 确定按钮点击
const handleNicknameConfirm = async () => {
const { code } = await updateMyInfo(form.value);
if (code === 200) {
message.success('修改成功!');
store.getUserInfo();
visible.value = false;
}
};
// 取消按钮点击
const onClose = () => {
form.value.name = '';
visible.value = false;
};
// 暴露方法供外部调用
defineExpose({
open,
});
</script>
<style lang="scss">
.change-name-modal {
.ant-modal-header {
border-bottom: none !important;
}
.ant-modal-body {
padding: 20px 24px !important;
}
.ant-modal-footer {
height: 56px !important;
border-top: none !important;
}
}
</style>

View File

@ -0,0 +1,171 @@
<template>
<Modal v-model:open="visible" :title="title" :width="400" centered :footer="null" class="safety-verification-modal">
<div class="modal-content">
<div class="description">
<p class="title-text">安全验证</p>
<p class="desc-text">为进一步保证您的账号安全确保为您本人操作请先完成安全验证</p>
</div>
<div class="verify-section">
<div class="phone-info">
<span class="label">请输入发送至</span>
<span class="phone-number">{{ formattedPhone }}</span>
<span class="label">的短信验证码</span>
</div>
<div class="captcha-input">
<div class="input-group">
<Input
v-model:value="captcha"
type="password"
maxlength="6"
placeholder="请输入验证码"
@input="handleCaptchaInput"
@blur="handleCaptchaBlur"
@focus="handleCaptchaFocus"
class="captcha-input-field"
/>
</div>
</div>
<div class="countdown">
<span v-if="countdown > 0" class="countdown-text"> {{ countdown }}秒后可以再次发送验证码 </span>
<span v-else class="resend-btn" @click="sendCaptcha"> 发送验证码 </span>
</div>
</div>
<div class="action-buttons">
<Button type="primary" @click="handleSubmit" :disabled="!captcha || captcha.length !== 6"> 确认验证 </Button>
<Button @click="handleCancel">取消</Button>
</div>
</div>
</Modal>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Button, Input, message, Modal } from 'ant-design-vue';
const props = defineProps<{
onVerifySuccess?: (captcha: string) => void;
onVerifyCancel?: () => void;
}>();
const emit = defineEmits(['success', 'cancel']);
const visible = ref(false);
const phone = ref('');
// 格式化手机号显示隐藏中间4位
const formattedPhone = computed(() => {
if (!phone.value) return '';
return phone.value.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
});
// 验证码输入相关
const captcha = ref('');
const countdown = ref(0);
const timer = ref<number | null>(null);
// 处理验证码输入
const handleCaptchaInput = (value: string) => {
// 只允许数字输入且最多6位
const numericValue = value.replace(/[^0-9]/g, '');
captcha.value = numericValue.slice(0, 6);
};
// 处理焦点事件
const handleCaptchaFocus = () => {
// 可以在这里添加焦点效果
};
const handleCaptchaBlur = () => {
// 可以在这里添加失焦效果
};
// 发送验证码
const sendCaptcha = async () => {
if (countdown.value > 0) return;
try {
// 这里应该调用实际的API发送验证码
// 示例await sendCaptchaApi({ phone: phone.value });
// 模拟成功发送
message.success('验证码已发送,请注意查收');
// 开始倒计时
startCountdown();
} catch (error) {
message.error('发送失败,请重试');
}
};
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
timer.value = window.setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value as number);
timer.value = null;
}
}, 1000);
};
// 提交验证
const handleSubmit = async () => {
if (!captcha.value || captcha.value.length !== 6) {
message.error('请输入正确的6位验证码');
return;
}
try {
// 这里应该调用实际的API验证验证码
// 示例await verifyCaptchaApi({ phone: phone.value, captcha: captcha.value });
// 模拟验证成功
message.success('验证成功!');
// 触发成功回调
if (props.onVerifySuccess) {
props.onVerifySuccess(captcha.value);
}
// 关闭弹窗
emit('success', captcha.value);
close();
} catch (error) {
message.error('验证失败,请检查验证码是否正确');
}
};
// 取消验证
const handleCancel = () => {
close();
};
const open = (mobile) => {
phone.value = mobile;
visible.value = true;
};
const close = () => {
visible.value = false;
phone.value = '';
};
defineExpose({
open,
});
// 组件销毁时清理定时器
onUnmounted(() => {
if (timer.value) {
clearInterval(timer.value);
}
});
</script>
<style scoped lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,110 @@
.safety-verification-modal {
.modal-content {
padding: 24px;
text-align: center;
.description {
margin-bottom: 24px;
.title-text {
font-size: 18px;
font-weight: 500;
color: #211f24;
margin-bottom: 8px;
}
.desc-text {
font-size: 14px;
color: #737478;
line-height: 20px;
}
}
.verify-section {
margin-bottom: 24px;
.phone-info {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 16px;
.label {
font-size: 14px;
color: #737478;
}
.phone-number {
font-size: 14px;
font-weight: 500;
color: #211f24;
margin: 0 8px;
}
}
.captcha-input {
margin-bottom: 16px;
.input-group {
display: flex;
justify-content: center;
.captcha-input-field {
width: 200px;
height: 40px;
border-radius: 4px;
border: 1px solid var(--BG-400, rgba(215, 215, 217, 1));
&:focus {
border-color: var(--Brand-Brand-6, rgba(109, 76, 254, 1));
box-shadow: 0 2px 4px 0 rgba(109, 76, 254, 0.2);
}
}
}
}
.countdown {
font-size: 14px;
color: #737478;
margin-bottom: 16px;
.countdown-text {
color: #737478;
}
.resend-btn {
color: var(--Brand-Brand-6, rgba(109, 76, 254, 1));
cursor: pointer;
font-weight: 500;
transition: color 0.2s;
&:hover {
color: var(--Brand-Brand-7, rgba(95, 67, 238, 1));
}
}
}
}
.action-buttons {
display: flex;
justify-content: center;
gap: 16px;
.arco-btn {
min-width: 100px;
height: 40px;
}
.arco-btn-primary {
background-color: var(--Brand-Brand-6, rgba(109, 76, 254, 1));
border-color: var(--Brand-Brand-6, rgba(109, 76, 254, 1));
color: white;
&:hover {
background-color: var(--Brand-Brand-7, rgba(95, 67, 238, 1));
border-color: var(--Brand-Brand-7, rgba(95, 67, 238, 1));
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,26 +1,55 @@
<template>
<div class="bg-#fff rounded-16px w-100% p-36px">
<div class="bg-#fff rounded-16px w-100% p-36px person-wrap">
<p class="title mb-32px">个人信息</p>
<Table :dataSource="dataSource" :pagination="false" :showSorterTooltip="false" class="mt-8px">
<Table.Column title="用户信息" dataIndex="info">
<template #customRender="{ record }">
<div class="pt-3px pb-3px">
<Avatar :src="record.head_image" :size="32" />
{{ record.name || '-' }}
<icon-edit size="13" class="ml-8px" @click="openEditInfoModal" />
<div class="flex items-center">
<div class="mr-60px relative cursor-pointer" @click="openEditInfoModal">
<Avatar :src="dataSource.head_image" :size="100" />
<img :src="icon1" width="20" height="20" class="absolute right-5px bottom-4px" />
</div>
</template>
</Table.Column>
<Table.Column title="手机号" dataIndex="mobile">
<template #customRender="{ record }">
{{ record.mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }}
<icon-edit size="13" class="ml-8px" @click="openEditMobileModal" />
</template>
</Table.Column>
<template #emptyText>
<NoData />
</template>
</Table>
<div class="flex-1 flex h-68px">
<div class="flex flex-1">
<span class="cts mr-4p">昵称</span>
<span class="cts !color-#211F24 bold !font-500 mr-12px">{{ dataSource.name || '-' }}</span>
<Button type="text" size="small" class="!p-0 !h-22px m-0" @click="openChangeNameModal">编辑</Button>
</div>
<div class="flex-1">
<div class="flex items-center mb-24px">
<span class="cts mr-4px w-56px">手机号</span>
<span class="cts !color-#211F24 bold !font-500 mr-12px">{{ mobile }}</span>
<Button type="text" size="small" class="!p-0 !h-22px m-0" @click="openChangeMobileModal">更改</Button>
</div>
<div class="flex items-center">
<span class="cts mr-4px w-56px">密码</span>
<span
class="!bg-#211F24 bold !font-500 w-6px h-6px rounded-50% mr-2px"
v-for="(item, index) in new Array(10).fill(0)"
:key="index"
></span>
<Button type="text" size="small" class="!p-0 !h-22px ml-10px">更改</Button>
</div>
</div>
</div>
</div>
<!-- <Table :dataSource="dataSource" :pagination="false" :showSorterTooltip="false" class="mt-8px">-->
<!-- <Table.Column title="用户信息" dataIndex="info">-->
<!-- <template #customRender="{ record }">-->
<!-- <div class="pt-3px pb-3px">-->
<!-- <Avatar :src="record.head_image" :size="32" />-->
<!-- {{ record.name || '-' }}-->
<!-- <icon-edit size="13" class="ml-8px" @click="openEditInfoModal" />-->
<!-- </div>-->
<!-- </template>-->
<!-- </Table.Column>-->
<!-- <Table.Column title="手机号" dataIndex="mobile">-->
<!-- <template #customRender="{ record }">-->
<!-- {{ record.mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }}-->
<!-- <icon-edit size="13" class="ml-8px" @click="openEditMobileModal" />-->
<!-- </template>-->
<!-- </Table.Column>-->
<!-- <template #emptyText>-->
<!-- <NoData />-->
<!-- </template>-->
<!-- </Table>-->
<Modal v-model:open="infoVisible" centered title="修改用户信息" @ok="handleSubmitUserInfo">
<Form
class="form"
@ -80,21 +109,38 @@
@cancel="verificationVisible = false"
/>
</Modal>
<SafetyVerificationModal
ref="safetyVerificationModalRef"
@success="handleVerifySuccess"
@cancel="handleVerifyCancel"
/>
<ChangeNameModal ref="changeNameModalRef" />
<ChangeMobileModal ref="changeMobileModalRef" />
</div>
</template>
<script setup lang="ts">
import { Button, Form, FormItem, Input, Table, message, Avatar } from 'ant-design-vue';
import Container from '@/components/container.vue';
import { Avatar, Button, Form, FormItem, Input, message } from 'ant-design-vue';
import Modal from '@/components/modal.vue';
import PuzzleVerification from '@/views/login/components/PuzzleVerification.vue';
import { ref, reactive } from 'vue';
import SafetyVerificationModal from './components/safety-verification-modal/index.vue';
import ChangeNameModal from './components/change-name-modal/index.vue';
import ChangeMobileModal from './components/change-mobile-modal/index.vue';
import { reactive, ref } from 'vue';
import 'vue-cropper/dist/index.css';
import { VueCropper } from 'vue-cropper';
import { sendUpdateMobileCaptcha, updateMobile, fetchImageUploadFile, updateMyInfo } from '@/api/all';
import { fetchImageUploadFile, sendUpdateMobileCaptcha, updateMobile, updateMyInfo } from '@/api/all';
import axios from 'axios';
import { useUserStore } from '@/stores';
import icon1 from './img/icon1.png';
const store = useUserStore();
const safetyVerificationModalRef = ref(null);
const changeNameModalRef = ref(null);
const changeMobileModalRef = ref(null);
// const userInfo = computed(() => {
// return store.userInfo ?? {};
// });
@ -120,12 +166,16 @@ const formRef = ref();
const isSendCaptcha = ref(false);
const uploadInputRef = ref();
// console.log(userInfo.value)
const dataSource = computed(() => {
return !isEmpty(store.userInfo) ? [store.userInfo] : [];
return store.userInfo;
});
const mobile = computed(() => {
const _mobile = dataSource.value.mobile;
if (!_mobile) return '-';
return _mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
});
console.log(dataSource.value)
console.log(store.userInfo);
// 表单校验规则
const formRules = {
@ -199,13 +249,13 @@ async function handleFileChange(event: Event) {
userInfoForm.file_url = file_url;
}
}
function openEditImageModal() {
imageVisible.value = true;
}
function openEditMobileModal() {
mobileVisible.value = true;
}
const openChangeNameModal = () => {
changeNameModalRef.value.open(dataSource.name);
};
const openChangeMobileModal = () => {
changeMobileModalRef.value.open();
};
async function handleSubmitUserInfo() {
await updateMyInfo(userInfoForm);
@ -261,17 +311,23 @@ function getFileExtension(filename: string): string {
}
</script>
<style scoped lang="scss">
@import './style.scss';
</style>
<style scoped lang="scss">
.form {
margin-top: 13px;
:deep(.arco-row) {
align-items: center;
}
:deep(.arco-form-item-label) {
font-family: $font-family-medium;
font-weight: 400;
font-size: 14px;
margin: 0;
}
:deep(.arco-input-wrapper) {
background: white;
border: 1px solid var(--BG-400, rgba(215, 215, 217, 1));
@ -279,6 +335,7 @@ function getFileExtension(filename: string): string {
font-weight: 400;
font-size: 14px;
padding: 4px 12px;
input::placeholder {
font-family: $font-family-medium;
font-weight: 400;
@ -286,24 +343,29 @@ function getFileExtension(filename: string): string {
color: var(--Text-4, rgba(147, 148, 153, 1));
}
}
:deep(.arco-input-disabled) {
background: var(--BG-200, rgba(242, 243, 245, 1));
border: 1px solid var(--BG-400, rgba(215, 215, 217, 1));
.arco-input:disabled {
font-family: $font-family-medium;
font-weight: 400;
font-size: 14px;
}
}
:deep(.arco-input-focus) {
border: 1px solid var(--Brand-Brand-6, rgba(109, 76, 254, 1));
box-shadow: 0 2px 4px 0 rgba(109, 76, 254, 0.2);
}
}
.upload-button {
width: 104px;
height: 24px;
margin-left: 12px;
:deep(.arco-btn) {
width: 104px;
height: 24px;
@ -317,6 +379,7 @@ function getFileExtension(filename: string): string {
color: var(--Text-2, rgba(60, 64, 67, 1));
}
}
.title {
color: var(--Text-1, #211f24);
font-family: $font-family-medium;

View File

@ -0,0 +1,13 @@
.person-wrap {
.cts {
color: var(--Text-4, #939499);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
&.bold {
font-family: $font-family-medium;
}
}
}