Merge remote-tracking branch 'origin/feature/0905_登录注册流程重构' into test

# Conflicts:
#	src/App.vue
#	src/layouts/components/siderBar/menu-list.ts
#	src/views/components/login/index.vue
This commit is contained in:
rd
2025-09-15 10:31:18 +08:00
44 changed files with 1574 additions and 788 deletions

View File

@ -0,0 +1,215 @@
<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" ref="inputRef" />
</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" :loading="submitLoading" @click="handleConfirm">确定</Button>
</div>
</Modal>
</template>
<script setup>
import { computed, 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 inputRef = ref();
const visible = ref(false);
const timer = ref();
const isLegalMobile = ref(false);
const hasGetCode = ref(false);
const submitLoading = 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;
nextTick(() => {
inputRef.value?.focus();
});
};
// 发送验证码
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) {
clearTimer();
}
}, 1000);
};
// 确定按钮点击
const handleConfirm = () => {
formRef.value.validate().then(async () => {
try {
submitLoading.value = true;
const { code } = await updateMobile(form.value);
if (code === 200) {
await store.getUserInfo();
message.success('修改成功!');
onClose();
}
} finally {
submitLoading.value = false;
}
});
};
const clearTimer = () => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
};
// 取消按钮点击
const onClose = () => {
form.value = {
mobile: '',
captcha: '',
};
countdown.value = 0;
visible.value = false;
hasGetCode.value = false;
isLegalMobile.value = false;
submitLoading.value = false;
formRef.value.resetFields();
formRef.value.clearValidate();
clearTimer();
};
// 暴露方法供外部调用
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,90 @@
<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" ref="inputRef" />
</FormItem>
</Form>
<div class="flex justify-end mt-20px">
<Button class="mr-16px" size="large" @click="onClose">取消</Button>
<Button type="primary" size="large" :loading="submitLoading" @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 inputRef = ref();
const visible = ref(false);
const submitLoading = ref(false);
const form = ref({
name: '',
});
// 打开弹窗
const open = (userName) => {
form.value.name = userName;
visible.value = true;
nextTick(() => {
inputRef.value?.focus();
});
};
// 确定按钮点击
const handleNicknameConfirm = async () => {
try {
submitLoading.value = true;
const { code } = await updateMyInfo(form.value);
if (code === 200) {
await store.getUserInfo();
message.success('修改成功!');
onClose();
}
} finally {
submitLoading.value = false;
}
};
// 取消按钮点击
const onClose = () => {
form.value.name = '';
visible.value = false;
submitLoading.valeu = 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,231 @@
<template>
<Modal
v-model:open="visible"
:title="title"
:width="412"
centered
:footer="null"
class="change-password-modal"
@cancel="onClose"
>
<div class="modal-content">
<p class="title-text">安全验证</p>
<template v-if="isCheckPass">
<p class="cts mt-36px !color-#211F24 mb-24px">验证成功请更改密码</p>
<Form ref="formRef" :model="formData" :rules="formRules" auto-label-width class="w-348 form-wrap">
<FormItem name="password" class="password-form-item">
<Input.Password v-model:value="formData.password" placeholder="新密码" size="large" class="!h-48px">
<template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template>
</Input.Password>
</FormItem>
<FormItem name="confirm_password" class="password-form-item">
<Input.Password
v-model:value="formData.confirm_password"
placeholder="密码确认"
size="large"
class="!h-48px"
>
<template #iconRender="visible">
<img :src="visible ? icon2 : icon1" width="20" height="20" class="cursor-pointer" />
</template>
</Input.Password>
</FormItem>
</Form>
<Button
type="primary"
:disabled="disabledSubmitBtn"
:loading="submitLoading"
class="w-full !h-48px mt-36px !rounded-8px"
@click="handleSubmit"
>完成更改
</Button>
</template>
<template v-else>
<p class="cts text-center mt-16px">为进一步保证您的账号安全确保为您本人操作</p>
<p class="cts mb-36px text-center">请先完成安全验证</p>
<p class="cts !color-#211F24 mb-24px">请输入发送至{{ showMobile }} 的短信验证码</p>
<div class="verify-code-wrap mb-24px">
<VerificationCode ref="verificationCodeRef" @success="onCheckPass" />
</div>
<p class="cts !text-12px !lh-20px !color-#939499" v-if="countdown > 0">
{{ countdown }} 秒后您可以再次发送验证码
</p>
<Button type="text" class="pl-0" v-if="hasGetCode && countdown === 0" @click="getCaptcha">重新发送</Button>
</template>
</div>
<PuzzleVerification
:show="isVerificationVisible"
@submit="handleVerificationSubmit"
@cancel="isVerificationVisible = false"
/>
</Modal>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Button, Form, FormItem, Input, message, Modal } from 'ant-design-vue';
import VerificationCode from '../verification-code/index.vue';
import PuzzleVerification from '@/views/login/components/PuzzleVerification.vue';
import { postUpdatePasswordCaptcha, postUpdatePassword } from '@/api/all/login';
import { postClearRateLimiter } from '@/api/all/common';
import icon1 from '@/assets/img/login/icon-close.png';
import icon2 from '@/assets/img/login/icon-open.png';
const emit = defineEmits(['success', 'cancel']);
const visible = ref(false);
const phone = ref('');
const isCheckPass = ref(false);
const formRef = ref(null);
const isVerificationVisible = ref(false);
// 验证码输入相关
const countdown = ref(0);
const timer = ref<number | null>(null);
const hasGetCode = ref(false);
const submitLoading = ref(false);
const verificationCodeRef = ref(null);
const formData = ref({
captcha: '',
password: '',
confirm_password: '',
});
const formRules = {
password: [
{
required: true,
validator: (_rule, value) => {
if (formData.value.confirm_password) {
formRef.value.validateFields('confirm_password');
}
return Promise.resolve();
},
trigger: ['blur'],
},
],
confirm_password: [
{
required: true,
validator: (_rule, value) => {
if (value !== formData.value.password) {
return Promise.reject('确认密码与设置的密码不同');
}
return Promise.resolve();
},
trigger: ['blur'],
},
],
};
const title = computed(() => (isCheckPass.value ? '更改密码' : '安全验证'));
const showMobile = computed(() => {
return phone.value?.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
});
const isPassPassword = computed(() => {
return (
formData.value.confirm_password &&
formData.value.password &&
formData.value.confirm_password === formData.value.password
);
});
const disabledSubmitBtn = computed(() => {
return !isPassPassword.value || !formData.value.password.trim();
});
const onCheckPass = (captcha) => {
isCheckPass.value = true;
formData.value.captcha = captcha;
};
const getCaptcha = async () => {
try {
const { code, message: msg } = await postUpdatePasswordCaptcha();
if (code === 200) {
startCountdown();
message.success(msg);
}
} catch (error) {
// 重置倒计时
countdown.value = 0;
clearInterval(timer.value);
if (error.status === 429) {
isVerificationVisible.value = true;
}
}
};
const handleVerificationSubmit = async () => {
isVerificationVisible.value = false;
verificationCodeRef.value?.init();
await postClearRateLimiter();
getCaptcha();
};
// 开始倒计时
const startCountdown = () => {
countdown.value = 60;
hasGetCode.value = true;
timer.value = window.setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearTimer();
}
}, 1000);
};
const clearTimer = () => {
if (timer.value) {
clearInterval(timer.value);
timer.value = null;
}
};
// 提交验证
const handleSubmit = async () => {
try {
submitLoading.value = true;
const { code } = await postUpdatePassword(formData.value);
if (code === 200) {
message.success('更改成功');
onClose();
}
} finally {
submitLoading.value = false;
}
};
const open = (mobile) => {
getCaptcha();
phone.value = mobile;
visible.value = true;
nextTick(() => {
verificationCodeRef.value?.init();
});
};
const onClose = () => {
visible.value = false;
phone.value = '';
countdown.value = 0;
hasGetCode.value = false;
submitLoading.value = false;
formRef.value?.resetFields();
formRef.value?.clearValidate();
clearTimer();
};
defineExpose({
open,
});
</script>
<style lang="scss">
@import './style.scss';
</style>

View File

@ -0,0 +1,62 @@
.change-password-modal {
.ant-modal-content {
border-radius: 16px;
.ant-modal-header {
border-radius: 16px 16px 0 0;
border-bottom: none !important;
.ant-modal-title {
display: none;
}
}
.ant-modal-body {
padding: 0 32px 8px !important;
position: relative;
top: -24px;
display: flex;
flex-direction: column;
justify-content: center;
.cts {
color: var(--Text-3, #737478);
font-family: $font-family-regular;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px; /* 157.143% */
}
.title-text {
color: var(--Text-1, #211F24);
text-align: center;
font-family: $font-family-medium;
font-size: 24px;
font-style: normal;
font-weight: 500;
line-height: 32px;
}
.ant-form {
.ant-form-item {
&:not(:last-child) {
margin-bottom: 24px !important;
}
.ant-input-affix-wrapper {
border-radius: 8px !important;
}
}
}
}
.ant-modal-footer {
height: 56px !important;
border-top: none !important;
}
}
}

View File

@ -0,0 +1,156 @@
<template>
<div class="verify-code-container">
<!-- 验证码输入区域 -->
<div class="code-inputs">
<Input
v-for="(item, index) in codeArray"
ref="inputRef"
:key="index"
:value="item.value"
:class="{ error: item.error, fill: item.value }"
@input="handleInput(index, $event.target.value)"
@focus="handleFocus(index)"
@blur="handleBlur(index)"
:maxlength="1"
placeholder=""
/>
</div>
<!-- 错误提示 -->
<p v-if="errorMessage" class="error-message">
{{ errorMessage }}
</p>
</div>
</template>
<script setup>
import { Input } from 'ant-design-vue';
import { ref, watch } from 'vue';
import { postCheckUpdatePasswordCaptcha } from '@/api/all/login';
const emits = defineEmits(['success']);
const codeArray = ref([]);
const activeIndex = ref(0); // 当前激活的输入框索引
const errorMessage = ref('');
const inputRef = ref(null);
// 处理输入事件
const handleInput = (index, value) => {
if (!value || !/^\d$/.test(value)) return;
codeArray.value[index].value = value;
if (index < 5) {
activeIndex.value = index + 1;
inputRef.value?.[activeIndex.value]?.focus?.();
}
};
const handleFocus = (index) => {
activeIndex.value = index;
};
const handleBlur = (index) => {
const _value = codeArray.value[index].value;
if (!_value || !/^\d$/.test(_value)) {
codeArray.value[index].error = true;
} else {
if (errorMessage.value) {
codeArray.value.forEach((item) => {
item.error = false;
});
}
errorMessage.value = '';
codeArray.value[index].error = false;
}
};
// 验证验证码逻辑
const validateCode = async () => {
const captcha = codeArray.value.map((item) => item.value).join('');
const { code } = await postCheckUpdatePasswordCaptcha({ captcha });
if (code === 200) {
emits('success', captcha);
} else {
activeIndex.value = 0;
inputRef.value?.[activeIndex.value]?.focus?.();
errorMessage.value = '验证码错误,请检查后重试';
codeArray.value.forEach((item) => {
item.error = true;
item.value = '';
});
}
};
const init = () => {
activeIndex.value = 0;
errorMessage.value = '';
inputRef.value = null;
codeArray.value = new Array(6).fill().map(() => ({ value: '', error: false }));
nextTick(() => {
inputRef.value?.[activeIndex.value]?.focus?.();
});
};
// 监听输入变化
watch(
() => codeArray.value,
(newVal) => {
if (newVal.every((item) => item.value)) {
validateCode();
}
},
{ deep: true },
);
defineExpose({ init });
</script>
<style scoped lang="scss">
.verify-code-container {
.code-inputs {
display: flex;
gap: 12px;
.ant-input {
width: 48px;
height: 48px;
border-radius: 8px !important;
text-align: center;
font-family: $font-family-medium;
font-size: 24px !important;
font-style: normal;
font-weight: 500;
line-height: 32px;
border: 1px solid #d7d7d9;
outline: none;
caret-color: #6d4cfe;
&:hover {
border-color: #6d4cfe !important;
}
&.fill {
color: #6d4cfe;
}
&.error {
border-color: #f64b31 !important;
}
}
}
}
.error-message {
color: var(--Functional-Red-6, #f64b31);
font-family: $font-family-regular;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 20px;
margin-top: 4px;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,333 +1,126 @@
<template>
<div class="bg-#fff rounded-8px w-100% py-0 px-20px pb-24px">
<div class="title-row">
<span class="title">个人信息</span>
</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"
:rules="rules"
:model="userInfoForm"
:label-col-props="{ span: 3, offset: 0 }"
:wrapper-col-props="{ span: 21, offset: 0 }"
<div class="bg-#fff rounded-16px w-100% p-36px person-wrap">
<p class="title mb-32px">个人信息</p>
<div class="flex items-center">
<Upload
action="/"
:showUploadList="false"
accept="image/*"
v-if="dataSource"
:customRequest="handleUpload"
class="mr-60px relative cursor-pointer"
>
<FormItem name="head_image" label="头像">
<Avatar :src="dataSource.head_image" :size="100" v-if="dataSource.head_image" />
<div v-else class="w-100 h-100 rounded-50% bg-#6D4CFE flex items-center justify-center">
<span class="color-#FFF text-46px font-400">{{ dataSource.mobile?.slice(-3) }}</span>
</div>
<img :src="icon1" width="20" height="20" class="absolute right-5px bottom-4px" />
</Upload>
<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">{{ mobileLabel }}</span>
<Button type="text" size="small" class="!p-0 !h-22px m-0" @click="openChangeMobileModal">更改</Button>
</div>
<div class="flex items-center">
<Avatar :src="userInfoForm.file_url" :size="48" />
<span class="upload-button" @click="triggerFileInput">
<input
ref="uploadInputRef"
accept="image/*"
type="file"
style="display: none"
@change="handleFileChange"
/>
<Button><icon-upload />上传新头像</Button>
</span>
<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" @click="openChangePasswordModal">更改</Button>
</div>
</FormItem>
<FormItem name="name" label="昵称">
<Input v-model:value="userInfoForm.name" placeholder="请输入昵称" />
</FormItem>
</Form>
</Modal>
<Modal v-model:open="imageVisible" centered title="头像裁剪">
<VueCropper></VueCropper>
</Modal>
<Modal v-model:open="mobileVisible" centered title="修改手机号" @ok="handleUpdateMobile">
<Form
ref="formRef"
:model="form"
class="form"
:rules="formRules"
labelAlign="right"
:labelCol="{ span: 4 }"
:wrapperCol="{ span: 20 }"
>
<FormItem required name="mobile" label="新手机号">
<Input v-model:value="form.mobile" size="small" placeholder="请输入新的手机号" />
</FormItem>
<FormItem required name="captcha" label="获取验证码">
<Input v-model:value="form.captcha" size="small" placeholder="请输入验证码">
<template #suffix>
<span v-if="countdown <= 0" @click="sendCaptcha">发送验证码</span>
<span v-else>{{ countdown }}s</span>
</template>
</Input>
</FormItem>
</Form>
<PuzzleVerification
:show="verificationVisible"
@submit="handleVerificationSubmit"
@cancel="verificationVisible = false"
/>
</Modal>
</div>
</div>
</div>
<ChangePasswordModal ref="changePasswordModalRef" />
<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 Modal from '@/components/modal.vue';
import PuzzleVerification from '@/views/login/components/PuzzleVerification.vue';
import { ref, reactive } from 'vue';
import 'vue-cropper/dist/index.css';
import { VueCropper } from 'vue-cropper';
import { sendUpdateMobileCaptcha, updateMobile, fetchImageUploadFile, updateMyInfo } from '@/api/all';
import { Avatar, Button, Upload, message } from 'ant-design-vue';
import ChangePasswordModal from './components/change-password-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 axios from 'axios';
import { fetchImageUploadFile, updateMyInfo } from '@/api/all';
import { useUserStore } from '@/stores';
import icon1 from './img/icon1.png';
const store = useUserStore();
const userInfo = computed(() => {
return store.userInfo ?? {};
});
const columns = [
{
title: '用户信息',
slotName: 'info',
},
{
title: '手机号',
slotName: 'mobile',
},
];
const infoVisible = ref(false);
const imageVisible = ref(false);
const mobileVisible = ref(false);
const verificationVisible = ref(false);
const timer = ref();
const countdown = ref(0);
const changePasswordModalRef = ref(null);
const changeNameModalRef = ref(null);
const changeMobileModalRef = ref(null);
const formRef = ref();
const isSendCaptcha = ref(false);
const uploadInputRef = ref();
const dataSource = computed(() => {
return userInfo.value ? [userInfo.value] : [];
return store.userInfo;
});
const mobileLabel = computed(() => {
const _mobile = dataSource.value.mobile;
if (!_mobile) return '-';
return _mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
});
// 表单校验规则
const formRules = {
mobile: [
{
validator: (_rule: any, value: string) => {
if (!value) {
return Promise.reject('请填写手机号');
}
if (!/^1[3-9]\d{9}$/.test(value)) {
return Promise.reject('手机号格式不正确');
}
return Promise.resolve();
},
required: true,
trigger: ['blur', 'change'],
},
],
captcha: [
{
required: true,
validator: (rule, value) => {
if (!value) {
return Promise.reject('请填写验证码');
}
if (!/^\d{6}$/.test(value)) {
return Promise.reject('验证码必须是6位数字');
}
return Promise.resolve();
},
// const form = reactive({
// mobile: '',
// captcha: '',
// });
trigger: ['blur', 'change'],
},
],
const openChangeNameModal = () => {
changeNameModalRef.value.open(dataSource.name);
};
const openChangeMobileModal = () => {
changeMobileModalRef.value.open();
};
const openChangePasswordModal = () => {
changePasswordModalRef.value.open(dataSource.value.mobile);
};
const form = reactive({
mobile: '',
captcha: '',
});
const userInfoForm = reactive({
name: '',
head_image: '',
file_url: '',
});
function triggerFileInput() {
uploadInputRef.value.click();
}
function openEditInfoModal() {
infoVisible.value = true;
}
async function handleFileChange(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
const fileExtension = getFileExtension(file.name);
const { data } = await fetchImageUploadFile({
suffix: fileExtension,
});
const { upload_url, file_name, file_url } = data;
const blob = new Blob([file], { type: file.type });
await axios.put(upload_url, blob, {
headers: { 'Content-Type': file.type },
});
userInfoForm.head_image = file_name;
userInfoForm.file_url = file_url;
}
}
function openEditImageModal() {
imageVisible.value = true;
}
function openEditMobileModal() {
mobileVisible.value = true;
}
async function handleSubmitUserInfo() {
await updateMyInfo(userInfoForm);
message.success('修改成功!');
}
async function sendCaptcha() {
try {
const result = await formRef.value.validateField('mobile');
if (result === true || result === undefined) {
verificationVisible.value = true;
isSendCaptcha.value = true;
}
message.error('请填写正确的手机号!');
} catch (error) {
console.log('手机号验证失败:', error);
}
}
function beginCountdown() {
timer.value = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer.value);
timer.value = null;
}
}, 1000);
}
async function handleVerificationSubmit() {
await sendUpdateMobileCaptcha({ mobile: form.mobile });
message.success('发送成功');
verificationVisible.value = false;
countdown.value = 60;
beginCountdown();
}
async function handleUpdateMobile() {
if (!isSendCaptcha.value) {
message.error('请先获取验证码!');
return false;
}
const res = await formRef.value.validate();
if (res === true || res === undefined) {
await updateMobile(form);
message.success('修改成功!');
}
}
function getFileExtension(filename: string): string {
const getFileExtension: string = (filename: string) => {
const match = filename.match(/\.([^.]+)$/);
return match ? match[1].toLowerCase() : '';
}
};
const handleUpload = async (option) => {
const { file } = option;
try {
if (file) {
const fileExtension = getFileExtension(file.name);
const { data } = await fetchImageUploadFile({
suffix: fileExtension,
});
const { upload_url, file_url } = data;
const blob = new Blob([file], { type: file.type });
await axios.put(upload_url, blob, {
headers: { 'Content-Type': file.type },
});
const { code } = await updateMyInfo({ head_image: file_url });
if (code === 200) {
message.success('修改成功');
store.userInfo.head_image = file_url;
}
}
} catch (error) {
message.error('上传失败');
}
};
</script>
<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));
font-family: $font-family-medium;
font-weight: 400;
font-size: 14px;
padding: 4px 12px;
input::placeholder {
font-family: $font-family-medium;
font-weight: 400;
font-size: 14px;
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;
border-radius: 4px;
padding: 2px 12px;
border: 1px solid var(--BG-500, rgba(177, 178, 181, 1));
font-family: $font-family-medium;
font-weight: 400;
font-size: 12px;
vertical-align: middle;
color: var(--Text-2, rgba(60, 64, 67, 1));
}
}
.title-row {
display: flex;
height: 64px;
padding: 10px 0 2px 0;
align-items: center;
.title {
color: var(--Text-1, #211f24);
font-family: $font-family-medium;
font-size: 18px;
font-style: normal;
font-weight: 400;
line-height: 24px; /* 150% */
}
}
@import './style.scss';
</style>

View File

@ -0,0 +1,22 @@
.person-wrap {
.title {
color: var(--Text-1, #211f24);
font-family: $font-family-medium;
font-size: 18px;
font-style: normal;
font-weight: 500;
line-height: 26px; /* 150% */
}
.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;
}
}
}