feat: 初始化

This commit is contained in:
George
2025-07-07 15:55:44 +08:00
commit 9b7bfcfe5a
969 changed files with 123036 additions and 0 deletions

View File

@ -0,0 +1,132 @@
.container {
padding: 0 32px;
::v-deep .uni-easyinput__placeholder-class {
font-size: 16px !important;
}
.welcomeTitle {
font-size: 28px;
font-weight: 700;
line-height: 64px;
margin: 12px 0 24px;
}
.validateStatusWrapper {
margin-top: 6px;
font-size: 12px;
.validateStatus {
display: flex;
column-gap: 5px;
align-items: center;
.verificationRes {
display: flex;
justify-content: center;
align-items: center;
width: 17px;
height: 17px;
.right {
width: 17px;
height: 17px;
}
.error {
width: 10px;
height: 10px;
}
}
.rightText {
color: #43cf7c;
}
.errorText {
color: #d43030;
}
}
}
.forgotPWDText {
display: flex;
column-gap: 6px;
align-items: center;
font-size: 18px;
font-weight: 700;
color: rgba(51, 51, 51, 1);
cursor: pointer;
}
.stepList {
display: flex;
align-items: center;
column-gap: 10px;
margin: 26px 0 24px;
padding-left: 5px;
color: #666666;
font-family: Arial !important;
.stepItem {
display: flex;
align-items: flex-end;
column-gap: 4px;
.numWrapper {
display: flex;
align-items: center;
column-gap: 4px;
.spaceLine {
width: 16px;
height: 1px;
background-color: #666666;
}
.num {
display: flex;
justify-content: center;
align-items: center;
width: 20px;
height: 20px;
border-radius: 20px;
border: 1px solid #666666;
box-sizing: border-box;
&.active {
color: #fff;
background-color: #0f3675;
border-color: #0f3675;
}
}
}
.stepTitle {
line-height: 16px;
}
}
}
.forgotPWDInputWrapper {
display: flex;
align-items: center;
height: 49px;
padding: 9px 23px 9px 15px;
border: 1px solid rgba(234, 236, 239, 1);
box-sizing: border-box;
.icon {
width: 27px;
}
.divider {
margin: 0 2px 0 22px;
width: 1px;
height: 30px;
background-color: rgba(220, 223, 229, 1);
}
.sendCodeBtn {
color: rgba(41, 187, 228, 1);
}
}
.success {
display: flex;
flex-direction: column;
align-items: center;
row-gap: 20px;
padding: 32px 0 50px;
.successIcon {
width: 48px;
height: 48px;
}
.successText {
color: rgba(51, 51, 51, 1);
line-height: 26px;
font-size: 18px;
}
}
}
::v-deep .uni-section .uni-section-header {
display: none;
}

View File

@ -0,0 +1,368 @@
<template>
<NavBar />
<view class="container">
<view class="welcomeTitle">{{ $t('login.pageDescription') }}</view>
<view class="forgotPWDText" @click="backToLogin">
<uni-icons type="left" size="20"></uni-icons>
<text>{{ $t('common.forgotPassword') }}</text>
</view>
<view class="stepList">
<view v-for="(step, index) in stepList" :key="step.title" class="stepItem">
<view class="numWrapper">
<view v-if="index > 0" class="spaceLine"></view>
<view :class="['num', active === index ? 'active' : '']">
{{ index + 1 }}
</view>
</view>
<view class="stepTitle">{{ step.title }}</view>
</view>
</view>
<view v-show="active === 0">
<uni-forms ref="verifyIdentityForm" :modelValue="verifyIdentityFormData" :rules="verifyIdentityFormRules">
<uni-forms-item name="email">
<view class="forgotPWDInputWrapper">
<image src="/static/email.png" mode="aspectFit" class="icon"></image>
<view class="divider"></view>
<uni-easyinput
trim="all"
primaryColor="#29BBE4"
:input-border="false"
v-model="verifyIdentityFormData.email"
:placeholder="$t('form.registerEmail.placeholder')"
></uni-easyinput>
</view>
</uni-forms-item>
<uni-forms-item name="code">
<view class="forgotPWDInputWrapper">
<image src="/static/send.png" mode="aspectFit" class="icon"></image>
<view class="divider"></view>
<uni-easyinput
trim="all"
primaryColor="#29BBE4"
:input-border="false"
v-model="verifyIdentityFormData.code"
:placeholder="$t('form.captcha.placeholder')"
></uni-easyinput>
<view class="sendCodeBtn" @click="handleSendCode">
<text v-if="!sendBtnDisabled">
{{ $t('form.verificationCode.send') }}
</text>
<text v-if="sendBtnLoading">{{ $t('form.verificationCode.sending') }}</text>
<text v-else-if="sendBtnDisabled">{{ sendBtnWaiting }}</text>
</view>
</view>
</uni-forms-item>
</uni-forms>
</view>
<view v-show="active === 1">
<uni-forms ref="changePWDForm" :modelValue="changePWDFormData" :rules="changePWDFormRules">
<uni-forms-item name="password">
<view class="forgotPWDInputWrapper">
<image src="/static/pwd.png" mode="aspectFit" class="icon" style="width: 24px"></image>
<view class="divider" style="margin-left: 16px"></view>
<uni-easyinput
:input-border="false"
type="password"
trim="all"
primaryColor="#29BBE4"
v-model="changePWDFormData.password"
:placeholder="$t('form.newPassword.placeholder')"
@input="passwordInputChange"
></uni-easyinput>
</view>
<view v-if="pwdValidating" class="validateStatusWrapper">
<view v-for="(status, index) in validateStatuses" :key="index" class="validateStatus">
<view class="verificationRes">
<image :src="status.valid ? '/static/right.png' : '/static/error.png'" :class="status.valid ? 'right' : 'error'" mode="aspectFit"></image>
</view>
<view :class="status.valid ? 'rightText' : 'errorText'">{{ status.text }}</view>
</view>
</view>
</uni-forms-item>
<uni-forms-item name="confirmPassword">
<view class="forgotPWDInputWrapper">
<image src="/static/confirmPwd.png" mode="aspectFit" class="icon" style="width: 24px"></image>
<view class="divider" style="margin-left: 16px"></view>
<uni-easyinput
:input-border="false"
trim="all"
type="password"
primaryColor="#29BBE4"
v-model="changePWDFormData.confirmPassword"
:placeholder="$t('form.confirmPassword.placeholder')"
@input="confirmPasswordInputChange"
></uni-easyinput>
</view>
<view v-if="confirmPwdValidating" class="validateStatusWrapper">
<view v-for="(status, index) in confirmValidateStatuses" :key="index" class="validateStatus">
<view class="verificationRes">
<image :src="status.valid ? '/static/right.png' : '/static/error.png'" :class="status.valid ? 'right' : 'error'" mode="aspectFit"></image>
</view>
<view :class="status.valid ? 'rightText' : 'errorText'">{{ status.text }}</view>
</view>
</view>
</uni-forms-item>
</uni-forms>
</view>
<view v-show="active === 2" class="success">
<image src="/static/success.png" mode="aspectFit" class="successIcon"></image>
<text class="successText">{{ $t('forgotPassword.resetPwdSuccess') }}</text>
</view>
<button class="primaryButton" hover-class="btnHover" type="button" @click="nextStep">
{{ active > 1 ? $t('forgotPassword.backToLogin') : $t('form.next') }}
</button>
</view>
</template>
<script>
import { sendCode, checkCode, saveNewPwd } from '@/services/user/forgotPassword';
import { UserLanguage, patterns } from '@/utils/const';
export default {
data() {
return {
active: 0,
stepList: [
{
title: this.$t('forgotPassword.step1')
},
{
title: this.$t('forgotPassword.step2')
},
{
title: this.$t('forgotPassword.step3')
}
],
sendBtnDisabled: false,
sendBtnLoading: false,
sendBtnWaiting: '',
sign: undefined,
authenticationPassed: false,
pwdValidating: false,
confirmPwdValidating: false,
validateStatuses: [
{ valid: false, text: this.$t('form.password.pattern1') },
{ valid: false, text: this.$t('form.password.pattern2') },
{ valid: false, text: this.$t('form.password.pattern3') },
{ valid: false, text: this.$t('form.password.pattern4') }
],
confirmValidateStatuses: [
{ valid: false, text: this.$t('form.password.pattern1') },
{ valid: false, text: this.$t('form.password.pattern2') },
{ valid: false, text: this.$t('form.password.pattern3') },
{ valid: false, text: this.$t('form.password.pattern4') }
],
verifyIdentityFormData: {
email: undefined,
code: undefined
},
verifyIdentityFormRules: {
email: {
rules: [
{
required: true,
errorMessage: this.$t('form.registerEmail.placeholder')
}
]
},
code: {
rules: [
{
required: true,
errorMessage: this.$t('form.captcha.required')
}
]
}
},
changePWDFormData: {
password: undefined,
confirmPassword: undefined
},
changePWDFormRules: {
password: {
rules: [
{
required: true,
errorMessage: this.$t('form.password.required')
},
{
validateFunction: (rule, value, data, callback) => {
// 异步需要返回 Promise 对象
return new Promise((resolve, reject) => {
setTimeout(() => {
if (this.validateStatuses.every((item) => item.valid)) {
// 通过返回 resolve
resolve();
} else {
// 不通过返回 reject(new Error('错误信息'))
reject(new Error(this.$t('form.password.invalid')));
}
}, 0);
});
}
}
]
},
confirmPassword: {
rules: [
{
required: true,
errorMessage: this.$t('form.confirmPassword.required')
},
{
validateFunction: (rule, value, data, callback) => {
// 异步需要返回 Promise 对象
return new Promise((resolve, reject) => {
setTimeout(() => {
if (this.validateStatuses.every((item) => item.valid)) {
// 通过返回 resolve
resolve();
} else {
// 不通过返回 reject(new Error('错误信息'))
reject(new Error(this.$t('form.password.invalid')));
}
}, 0);
});
}
},
{
validateFunction: (rule, value, data, callback) => {
// 异步需要返回 Promise 对象
return new Promise((resolve, reject) => {
setTimeout(() => {
if (this.changePWDFormData.password === this.changePWDFormData.confirmPassword) {
// 通过返回 resolve
resolve();
} else {
// 不通过返回 reject(new Error('错误信息'))
reject(new Error(this.$t('form.confirmPassword.invalid')));
}
}, 0);
});
}
}
]
}
}
};
},
computed: {},
onLoad() {},
onShow() {},
methods: {
backToLogin() {
uni.redirectTo({ url: '/pages/login/index' });
},
async handleSendCode() {
this.$refs.verifyIdentityForm.validateField('email').then(async (fields) => {
if (this.sendBtnDisabled) return;
this.sendBtnDisabled = true;
this.sendBtnLoading = true;
const res = await sendCode({ email: fields.email, qcc_language: UserLanguage });
this.sendBtnLoading = false;
this.sendBtnWaiting = 60;
let timer = setInterval(() => {
if (this.sendBtnWaiting > 1) {
this.sendBtnWaiting -= 1;
} else {
this.sendBtnDisabled = false;
clearInterval(timer);
}
}, 1000);
if (res && res.code === 0) {
this.$cusModal.showModal({
type: 'message',
status: 'success',
contentText: this.$t('form.verificationCode.sendSuccess')
});
} else {
this.$cusModal.showModal({
type: 'message',
status: 'warning',
contentText: res.msg ?? this.$t('common.error.sysError')
});
}
});
},
async verifyIdentity() {
let success = false;
await this.$refs.verifyIdentityForm.validate().then(async (fields) => {
const res = await checkCode(fields);
if (res && res.code === 0) {
this.sign = res.data;
success = true;
} else {
this.$cusModal.showModal({
type: 'message',
status: 'warning',
contentText: res.msg ?? this.$t('common.error.sysError')
});
this.sign = undefined;
success = false;
}
});
return success;
},
passwordInputChange(e) {
if (!this.pwdValidating) {
this.pwdValidating = true;
}
this.validateStatuses.forEach((item, index) => {
item.valid = patterns[`passwordPattern${index + 1}`].test(e);
});
},
confirmPasswordInputChange(e) {
if (!this.confirmPwdValidating) {
this.confirmPwdValidating = true;
}
this.confirmValidateStatuses.forEach((item, index) => {
item.valid = patterns[`passwordPattern${index + 1}`].test(e);
});
},
async handleChangePWD() {
let success = false;
await this.$refs.changePWDForm.validate().then(async (fields) => {
delete fields.confirmPassword;
const res = await saveNewPwd({ ...fields, sign: this.sign, email: this.verifyIdentityFormData.email });
if (res && res.code === 0) {
success = true;
} else {
this.$cusModal.showModal({
type: 'message',
status: 'warning',
contentText: res.msg ?? this.$t('common.error.sysError')
});
success = false;
}
});
return success;
},
async nextStep() {
switch (this.active) {
case 0:
const valid = await this.verifyIdentity();
if (valid) {
this.active += 1;
}
break;
case 1:
const ok = await this.handleChangePWD();
if (ok) {
this.active += 1;
}
break;
case 2:
this.backToLogin();
break;
defaultf;
}
}
}
};
</script>
<style lang="scss" scoped>
@import './index.scss';
.is-input-border {
border: none;
}
</style>