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:
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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>
|
||||
BIN
src/views/components/management/person/img/icon1.png
Normal file
BIN
src/views/components/management/person/img/icon1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@ -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>
|
||||
|
||||
22
src/views/components/management/person/style.scss
Normal file
22
src/views/components/management/person/style.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -98,43 +98,32 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Checkbox,
|
||||
Modal,
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Space,
|
||||
message,
|
||||
Typography,
|
||||
Card,
|
||||
List,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
const { Link } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
import { Button, Checkbox, Form, FormItem, Input, message, Tabs, Typography } from 'ant-design-vue';
|
||||
import PuzzleVerification from '../PuzzleVerification.vue';
|
||||
import SelectAccountModal from '../select-account-modal/index.vue';
|
||||
import { fetchLoginCaptCha, fetchAuthorizationsCaptcha, fetchProfileInfo, postLoginPassword } from '@/api/all/login';
|
||||
import { fetchAuthorizationsCaptcha, fetchLoginCaptCha, fetchProfileInfo, postLoginPassword } from '@/api/all/login';
|
||||
import { postClearRateLimiter } from '@/api/all/common';
|
||||
import { joinEnterpriseByInviteCode } from '@/api/all';
|
||||
import { ref, reactive, onUnmounted, computed } from 'vue';
|
||||
import { computed, onUnmounted, reactive, ref } from 'vue';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useEnterpriseStore } from '@/stores/modules/enterprise';
|
||||
import { handleUserLogin, goUserLogin } from '@/utils/user';
|
||||
import router from '@/router';
|
||||
import { handleUserLogin } from '@/utils/user';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import icon1 from '@/assets/img/login/icon-close.png';
|
||||
import icon2 from '@/assets/img/login/icon-open.png';
|
||||
|
||||
const { Link } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const setPageType = inject('setPageType');
|
||||
|
||||
const formRef = ref();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const enterpriseStore = useEnterpriseStore();
|
||||
|
||||
const formRef = ref();
|
||||
const countdown = ref(0);
|
||||
let timer = ref();
|
||||
const isLogin = ref(true);
|
||||
@ -277,7 +266,7 @@ const onTabChange = () => {
|
||||
const getProfileInfo = async () => {
|
||||
const { code, data } = await fetchProfileInfo();
|
||||
if (code === 200) {
|
||||
let enterprises = data['enterprises'];
|
||||
const enterprises = data['enterprises'];
|
||||
mobileNumber.value = data['mobile'];
|
||||
accounts.value = enterprises;
|
||||
|
||||
@ -289,13 +278,14 @@ const getProfileInfo = async () => {
|
||||
// 多个企业时候需要弹窗让用户选择企业
|
||||
selectAccountModalRef.value.open();
|
||||
}
|
||||
} else {
|
||||
router.push({ name: 'Trial' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
console.log('handleSubmit', disabledSubmitBtn.value);
|
||||
if (disabledSubmitBtn.value) return;
|
||||
|
||||
try {
|
||||
@ -312,7 +302,6 @@ const handleSubmit = async () => {
|
||||
const _fn = isCaptchaLogin.value ? fetchAuthorizationsCaptcha : postLoginPassword;
|
||||
const { code, data, message: errorInfo } = await _fn(loginForm);
|
||||
|
||||
console.log(code, errorInfo);
|
||||
if (code === 10001) {
|
||||
errMsg.value = errorInfo;
|
||||
return;
|
||||
|
||||
@ -85,44 +85,39 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="js">
|
||||
import {
|
||||
Checkbox,
|
||||
Modal,
|
||||
Button,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
Space,
|
||||
message,
|
||||
Typography,
|
||||
Card,
|
||||
List,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
const { Link } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
import { Button, Checkbox, Form, FormItem, Input, message, Tabs, Typography } from 'ant-design-vue';
|
||||
import PuzzleVerification from '../PuzzleVerification.vue';
|
||||
import SelectAccountModal from '../select-account-modal/index.vue';
|
||||
import { postClearRateLimiter } from '@/api/all/common';
|
||||
import { postRegisterCaptcha,postForgetPasswordCaptcha, fetchProfileInfo, postRegister, postForgetPassword } from '@/api/all/login';
|
||||
import {
|
||||
fetchProfileInfo,
|
||||
postForgetPassword,
|
||||
postForgetPasswordCaptcha,
|
||||
postRegister,
|
||||
postRegisterCaptcha,
|
||||
} from '@/api/all/login';
|
||||
import { joinEnterpriseByInviteCode } from '@/api/all';
|
||||
import { ref, reactive, onUnmounted, computed } from 'vue';
|
||||
import { computed, onUnmounted, ref } from 'vue';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useEnterpriseStore } from '@/stores/modules/enterprise';
|
||||
import { handleUserLogin, goUserLogin } from '@/utils/user';
|
||||
import router from '@/router';
|
||||
import { handleUserLogin } from '@/utils/user';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import icon1 from '@/assets/img/login/icon-close.png';
|
||||
import icon2 from '@/assets/img/login/icon-open.png';
|
||||
|
||||
const { Link } = Typography;
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
const setPageType = inject('setPageType');
|
||||
const pageType = inject('pageType');
|
||||
|
||||
const formRef = ref();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const enterpriseStore = useEnterpriseStore();
|
||||
|
||||
const formRef = ref();
|
||||
const countdown = ref(0);
|
||||
let timer = ref();
|
||||
const isLogin = ref(true);
|
||||
@ -173,9 +168,9 @@ const formRules = {
|
||||
// if (value.length < 6) {
|
||||
// return Promise.reject('密码长度不能小于6位');
|
||||
// }
|
||||
if(formData.value.confirm_password) {
|
||||
formRef.value.validateFields('confirm_password');
|
||||
}
|
||||
if (formData.value.confirm_password) {
|
||||
formRef.value.validateFields('confirm_password');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
trigger: ['blur'],
|
||||
@ -268,7 +263,7 @@ const getCaptcha = async () => {
|
||||
const fn = isResetPassword.value ? postForgetPasswordCaptcha : postRegisterCaptcha;
|
||||
const { code, message: msg } = await fn({ mobile: formData.value.mobile });
|
||||
if (code === 200) {
|
||||
startCountdown()
|
||||
startCountdown();
|
||||
message.success(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
@ -293,19 +288,21 @@ const handleVerificationSubmit = async () => {
|
||||
const getProfileInfo = async () => {
|
||||
const { code, data } = await fetchProfileInfo();
|
||||
if (code === 200) {
|
||||
let enterprises = data['enterprises'];
|
||||
const enterprises = data['enterprises'];
|
||||
mobileNumber.value = data['mobile'];
|
||||
accounts.value = enterprises;
|
||||
|
||||
if (enterprises.length > 0) {
|
||||
enterpriseStore.setEnterpriseInfo(data.enterprises[0]);
|
||||
if (enterprises.length === 1) {
|
||||
setTimeout(() => {
|
||||
handleUserLogin();
|
||||
}, 1500);
|
||||
setTimeout(() => {
|
||||
handleUserLogin();
|
||||
}, 1500);
|
||||
} else {
|
||||
selectAccountModalRef.value.open();
|
||||
}
|
||||
} else {
|
||||
router.push({ name: 'Trial' });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -335,13 +332,14 @@ const handleSubmit = async () => {
|
||||
|
||||
if (code === 200) {
|
||||
message.success(isResetPassword.value ? '重置成功' : '注册成功');
|
||||
// 注册成功后跳转登录页
|
||||
if(!isResetPassword.value) {
|
||||
setTimeout(() => {
|
||||
setPageType('loginForm');
|
||||
}, 1500);
|
||||
return;
|
||||
};
|
||||
// 注册成功后跳转登录页
|
||||
if (!isResetPassword.value) {
|
||||
setTimeout(() => {
|
||||
setPageType('loginForm');
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
;
|
||||
|
||||
userStore.setToken(data.access_token);
|
||||
|
||||
@ -371,7 +369,7 @@ const startCountdown = () => {
|
||||
timer.value = setInterval(() => {
|
||||
countdown.value--;
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer.value );
|
||||
clearInterval(timer.value);
|
||||
timer.value = null;
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="highlight-textarea-container">
|
||||
<a-textarea
|
||||
<TextArea
|
||||
ref="textareaWrapRef"
|
||||
v-model="inputValue"
|
||||
v-model:value="inputValue"
|
||||
placeholder="请输入作品描述"
|
||||
:disabled="disabled"
|
||||
show-word-limit
|
||||
:max-length="1000"
|
||||
showCount
|
||||
:maxlength="1000"
|
||||
size="large"
|
||||
class="textarea-input h-full w-full"
|
||||
@input="handleInput"
|
||||
@ -25,7 +25,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Input } from 'ant-design-vue';
|
||||
const {TextArea} = Input
|
||||
|
||||
const { TextArea } = Input;
|
||||
import { ref, computed, watch, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
|
||||
import { escapeRegExp } from './constants';
|
||||
|
||||
@ -69,8 +70,8 @@ watch(
|
||||
);
|
||||
|
||||
// 处理输入事件
|
||||
const handleInput = (value: string) => {
|
||||
emit('update:modelValue', value);
|
||||
const handleInput = (e) => {
|
||||
emit('update:modelValue', e.currentTarget.value);
|
||||
};
|
||||
|
||||
const escapeHtml = (str: string): string => {
|
||||
@ -109,14 +110,12 @@ const generateHighlightedHtml = (): string => {
|
||||
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
|
||||
const color = levelStyle?.color || '#F64B31';
|
||||
|
||||
return `<span class="text-14px font-400 lh-22px" style="color: ${color};">${escapeHtml(match)}</span>`;
|
||||
return `<span class="text-14px font-400 lh-22px font-family-regular" style="color: ${color};">${escapeHtml(match)}</span>`;
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nativeTextarea =
|
||||
(textareaWrapRef.value?.$el || textareaWrapRef.value)?.querySelector?.('textarea.arco-textarea') ||
|
||||
document.querySelector('.textarea-input .arco-textarea');
|
||||
nativeTextarea = textareaWrapRef.value?.$el?.querySelector?.('textarea.ant-input');
|
||||
|
||||
if (nativeTextarea) {
|
||||
nativeTextarea.addEventListener('scroll', handleTextareaScroll);
|
||||
@ -173,8 +172,9 @@ const handleCompositionUpdate = () => {
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400px;
|
||||
border: 1px solid #d7d7d9;
|
||||
font-weight: 400;
|
||||
font-family: $font-family-regular;
|
||||
//border: 1px solid #d7d7d9;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
white-space: pre-wrap;
|
||||
@ -192,43 +192,43 @@ const handleCompositionUpdate = () => {
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
@include textarea-padding;
|
||||
|
||||
&.focus {
|
||||
border-color: rgb(var(--primary-6)) !important;
|
||||
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-textarea-wrapper) {
|
||||
:deep(.ant-input-textarea) {
|
||||
@include textarea-style;
|
||||
|
||||
.arco-textarea {
|
||||
textarea {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
background-color: transparent !important;
|
||||
color: transparent;
|
||||
caret-color: #211f24 !important;
|
||||
resize: none;
|
||||
@include textarea-padding;
|
||||
overflow-y: auto;
|
||||
}
|
||||
&.ant-input-disabled {
|
||||
background: #f2f3f5 !important;
|
||||
-webkit-text-fill-color: rgba(0, 0, 0, 0.25) !important;
|
||||
}
|
||||
|
||||
.arco-textarea-word-limit {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.arco-textarea-disabled {
|
||||
.arco-textarea {
|
||||
background: #f2f3f5;
|
||||
&.ant-input-textarea-show-count {
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
font-size: 12px;
|
||||
right: 10px;
|
||||
bottom: 6px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处于中文输入法合成态时,显示真实文本并隐藏高亮层
|
||||
:deep(.textarea-input.composing .arco-textarea) {
|
||||
:deep(.ant-input-textarea.composing textarea) {
|
||||
color: #211f24 !important;
|
||||
-webkit-text-fill-color: #211f24 !important;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
}
|
||||
.card-item {
|
||||
border-radius: 8px;
|
||||
// border: 1px solid var(--BG-300, #e6e6e8);
|
||||
border: 1px solid transparent;
|
||||
background: var(--BG-white, #fff);
|
||||
padding: 12px 16px 16px;
|
||||
position: relative;
|
||||
@ -103,7 +103,7 @@
|
||||
padding-top: 8px;
|
||||
}
|
||||
&.checked {
|
||||
border: 1px solid var(--Brand-6, #6d4cfe);
|
||||
border-color: #6d4cfe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<Layout class="flex justify-center items-center trial-wrap">
|
||||
<Layout.Header class="header-wrap">
|
||||
<Layout.Header class="header-wrap cursor-pointer" @click="onLogoClick">
|
||||
<div class="h-full px-24px">
|
||||
<div class="w-full h-full relative flex justify-between">
|
||||
<div class="flex items-center cursor-pointer" @click="handleUserHome">
|
||||
<div class="flex items-center">
|
||||
<img src="@/assets/img/icon-logo.png" alt="" width="96" height="24" />
|
||||
</div>
|
||||
<RightSide />
|
||||
@ -13,7 +13,7 @@
|
||||
<Layout class="flex trial-content items-center">
|
||||
<div class="w-800px">
|
||||
<!-- 未建联 -->
|
||||
<section class="w-full" v-if="status === 1">
|
||||
<section class="w-full" v-if="!primary_enterprise">
|
||||
<div class="rounded-16px mb-16px bg-#fff px-24px py-16px flex justify-between">
|
||||
<div class="flex items-center">
|
||||
<span class="cts !text-18px !lh-26px !color-#000 mr-8px">申请试用</span>
|
||||
@ -25,18 +25,18 @@
|
||||
<div class="rounded-16px mb-16px bg-#fff p-24px">
|
||||
<p class="cts !text-16px !lh-24px !color-#000 mb-32px">基本信息</p>
|
||||
<Form ref="formRef" :model="formData" :rules="formRules" layout="vertical" class="w-full form-wrap">
|
||||
<FormItem name="name" label="联系人">
|
||||
<FormItem name="contact" label="联系人">
|
||||
<Input
|
||||
v-model:value="formData.name"
|
||||
v-model:value="formData.contact"
|
||||
placeholder="请输入您的姓名"
|
||||
size="large"
|
||||
allowClear
|
||||
class="w-500px"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem name="company_name" label="公司名称">
|
||||
<FormItem name="name" label="公司名称">
|
||||
<Input
|
||||
v-model:value="formData.company_name"
|
||||
v-model:value="formData.name"
|
||||
placeholder="请输入您的公司名称,个人用户写无"
|
||||
size="large"
|
||||
allowClear
|
||||
@ -65,7 +65,7 @@
|
||||
</section>
|
||||
|
||||
<!-- 建立商务联系中 -->
|
||||
<section class="w-full" v-if="status === 2">
|
||||
<section class="w-full" v-if="enterpriseStore.enterpriseInfo?.audit_status === 1">
|
||||
<div class="rounded-16px bg-#fff px-36px pt-80px pb-60px flex flex-col items-center">
|
||||
<img :src="icon2" width="96" height="96" class="mb-8px" />
|
||||
<p class="cts !text-18px !lh-26px mb-8px">您的试用申请已提交</p>
|
||||
@ -87,7 +87,7 @@
|
||||
</section>
|
||||
|
||||
<!-- 试用到期 -->
|
||||
<section class="w-full" v-if="status === 3">
|
||||
<section class="w-full" v-if="enterpriseStore.enterpriseInfo?.subscribe_status === 4">
|
||||
<div class="rounded-16px bg-#fff px-36px pt-80px pb-60px flex flex-col items-center">
|
||||
<img :src="icon1" width="96" height="96" class="mb-8px" />
|
||||
<p class="cts !text-18px !lh-26px mb-8px">试用已到期</p>
|
||||
@ -113,26 +113,36 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Layout, Form, Button, FormItem, Input, Steps } from 'ant-design-vue';
|
||||
import { Button, Form, FormItem, Input, Layout, Steps } from 'ant-design-vue';
|
||||
import RightSide from '@/layouts/components/navbar/components/right-side/index.vue';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
import { postCreateEnterprises } from '@/api/all/login';
|
||||
import { exactFormatTime } from '@/utils/tools';
|
||||
import { handleUserHome } from '@/utils/user';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useEnterpriseStore } from '@/stores/modules/enterprise';
|
||||
|
||||
import icon1 from './img/icon-info.png';
|
||||
import icon2 from './img/icon-check.png';
|
||||
|
||||
type Status = 1 | 2 | 3;
|
||||
// 0-未开通,1-已开通,2-试用中,3-已到期,4-试用结束
|
||||
type Status = 0 | 1 | 2 | 3 | 4;
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const enterpriseStore = useEnterpriseStore();
|
||||
|
||||
const formRef = ref();
|
||||
const submitting = ref(false);
|
||||
const status = ref<Status>(1);
|
||||
// const status = ref<Status>(1);
|
||||
const formData = ref({
|
||||
mobile: '',
|
||||
contact: '',
|
||||
name: '',
|
||||
company_name: '',
|
||||
});
|
||||
const formRules = {
|
||||
name: {
|
||||
contact: {
|
||||
required: true,
|
||||
trigger: ['blur'],
|
||||
|
||||
@ -144,7 +154,7 @@ const formRules = {
|
||||
}
|
||||
},
|
||||
},
|
||||
company_name: {
|
||||
name: {
|
||||
required: true,
|
||||
trigger: ['blur'],
|
||||
|
||||
@ -170,11 +180,14 @@ const formRules = {
|
||||
},
|
||||
};
|
||||
|
||||
const primary_enterprise = computed(() => userStore.userInfo?.primary_enterprise);
|
||||
const hasOpenEnterprise = computed(() => enterpriseStore.isOpenEnterprise);
|
||||
|
||||
const trialingStepsItems = computed(() => {
|
||||
return [
|
||||
{
|
||||
title: '提交申请',
|
||||
description: '2025/09/01 12:00:00',
|
||||
description: exactFormatTime(enterpriseStore.enterpriseInfo?.created_at),
|
||||
},
|
||||
{
|
||||
title: '人工审核',
|
||||
@ -190,7 +203,7 @@ const trialEndStepsItems = computed(() => {
|
||||
return [
|
||||
{
|
||||
title: '提交申请',
|
||||
description: '2025/09/01 12:00:00',
|
||||
description: exactFormatTime(enterpriseStore.enterpriseInfo?.created_at),
|
||||
},
|
||||
{
|
||||
title: '人工审核',
|
||||
@ -203,12 +216,30 @@ const trialEndStepsItems = computed(() => {
|
||||
];
|
||||
});
|
||||
|
||||
const onLogoClick = () => {
|
||||
if (hasOpenEnterprise.value) {
|
||||
handleUserHome();
|
||||
} else {
|
||||
router.push({
|
||||
name: 'Trial',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
submitting.value = true;
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
console.log('验证通过');
|
||||
.then(async () => {
|
||||
const { code } = await postCreateEnterprises(formData.value);
|
||||
if (code === 200) {
|
||||
await userStore.getUserInfo();
|
||||
|
||||
const { primary_enterprise } = userStore.userInfo;
|
||||
if (primary_enterprise) {
|
||||
enterpriseStore.setEnterpriseInfo(primary_enterprise);
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
submitting.value = false;
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="highlight-textarea-container">
|
||||
<a-textarea
|
||||
<TextArea
|
||||
ref="textareaWrapRef"
|
||||
v-model="inputValue"
|
||||
v-model:value="inputValue"
|
||||
placeholder="请输入作品描述"
|
||||
:disabled="disabled"
|
||||
show-word-limit
|
||||
:max-length="1000"
|
||||
showCount
|
||||
:maxlength="1000"
|
||||
size="large"
|
||||
class="textarea-input h-full w-full"
|
||||
@input="handleInput"
|
||||
@ -25,7 +25,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Input } from 'ant-design-vue';
|
||||
const {TextArea} = Input
|
||||
|
||||
const { TextArea } = Input;
|
||||
import { ref, computed, watch, defineProps, defineEmits, onMounted, onUnmounted } from 'vue';
|
||||
import { escapeRegExp } from './constants';
|
||||
|
||||
@ -69,8 +70,8 @@ watch(
|
||||
);
|
||||
|
||||
// 处理输入事件
|
||||
const handleInput = (value: string) => {
|
||||
emit('update:modelValue', value);
|
||||
const handleInput = (e) => {
|
||||
emit('update:modelValue', e.currentTarget.value);
|
||||
};
|
||||
|
||||
const escapeHtml = (str: string): string => {
|
||||
@ -109,14 +110,12 @@ const generateHighlightedHtml = (): string => {
|
||||
const levelStyle = props.levelMap?.get(wordInfo.risk_level);
|
||||
const color = levelStyle?.color || '#F64B31';
|
||||
|
||||
return `<span class="text-14px font-400 lh-22px" style="color: ${color};">${escapeHtml(match)}</span>`;
|
||||
return `<span class="text-14px font-400 lh-22px font-family-regular" style="color: ${color};">${escapeHtml(match)}</span>`;
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nativeTextarea =
|
||||
(textareaWrapRef.value?.$el || textareaWrapRef.value)?.querySelector?.('textarea.arco-textarea') ||
|
||||
document.querySelector('.textarea-input .arco-textarea');
|
||||
nativeTextarea = textareaWrapRef.value?.$el?.querySelector?.('textarea.ant-input');
|
||||
|
||||
if (nativeTextarea) {
|
||||
nativeTextarea.addEventListener('scroll', handleTextareaScroll);
|
||||
@ -173,8 +172,9 @@ const handleCompositionUpdate = () => {
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
font-weight: 400px;
|
||||
border: 1px solid #d7d7d9;
|
||||
font-weight: 400;
|
||||
font-family: $font-family-regular;
|
||||
//border: 1px solid #d7d7d9;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
white-space: pre-wrap;
|
||||
@ -192,43 +192,43 @@ const handleCompositionUpdate = () => {
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
@include textarea-padding;
|
||||
|
||||
&.focus {
|
||||
border-color: rgb(var(--primary-6)) !important;
|
||||
box-shadow: 0 0 0 0 var(--color-primary-light-2) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-textarea-wrapper) {
|
||||
:deep(.ant-input-textarea) {
|
||||
@include textarea-style;
|
||||
|
||||
.arco-textarea {
|
||||
textarea {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
background-color: transparent !important;
|
||||
color: transparent;
|
||||
caret-color: #211f24 !important;
|
||||
resize: none;
|
||||
@include textarea-padding;
|
||||
overflow-y: auto;
|
||||
}
|
||||
&.ant-input-disabled {
|
||||
background: #f2f3f5 !important;
|
||||
-webkit-text-fill-color: rgba(0, 0, 0, 0.25) !important;
|
||||
}
|
||||
|
||||
.arco-textarea-word-limit {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&.arco-textarea-disabled {
|
||||
.arco-textarea {
|
||||
background: #f2f3f5;
|
||||
&.ant-input-textarea-show-count {
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
font-size: 12px;
|
||||
right: 10px;
|
||||
bottom: 6px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处于中文输入法合成态时,显示真实文本并隐藏高亮层
|
||||
:deep(.textarea-input.composing .arco-textarea) {
|
||||
:deep(.ant-input-textarea.composing textarea) {
|
||||
color: #211f24 !important;
|
||||
-webkit-text-fill-color: #211f24 !important;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user