<template>
|
<div class="register">
|
<div class="reg-inner">
|
<div class="reg-back" @click="router.go(-1)">
|
<van-icon name="arrow-left" size="22" />
|
</div>
|
|
<div class="reg-logo-wrap">
|
<img :src="LOGO" alt="" class="reg-logo" />
|
</div>
|
<h1 class="reg-title">{{ $t('register') }}</h1>
|
<p class="reg-login">
|
{{ $t('hasAccount') }}
|
<span class="reg-login-link" @click="router.push('/login')">{{ $t('goLogin') }}</span>
|
</p>
|
<p class="reg-lang" @click="onRoute('/language')">{{ currentLocaleLabel }}</p>
|
|
<!-- Email -->
|
<div class="reg-field">
|
<input
|
v-model="username"
|
type="text"
|
class="reg-input"
|
:placeholder="$t('entryEmail')"
|
autocomplete="email"
|
/>
|
</div>
|
<!-- Verification Code -->
|
<div class="reg-field reg-field-code">
|
<input
|
v-model="verifyCode"
|
type="text"
|
class="reg-input"
|
:placeholder="$t('entryVerifyCode')"
|
maxlength="6"
|
/>
|
<span class="reg-send-code" @click="senCode">
|
{{ time > 0 ? `(${time})s` : $t('sendVerifyCode') }}
|
</span>
|
</div>
|
<!-- Password -->
|
<div class="reg-field reg-field-pwd">
|
<input
|
v-model="password"
|
:type="pwdVisible ? 'text' : 'password'"
|
class="reg-input"
|
:placeholder="$t('entryPassword')"
|
autocomplete="new-password"
|
/>
|
<span class="reg-eye" @click="pwdVisible = !pwdVisible">
|
<van-icon :name="pwdVisible ? 'eye-o' : 'closed-eye'" size="20" />
|
</span>
|
</div>
|
<!-- Confirm Password -->
|
<div class="reg-field reg-field-pwd">
|
<input
|
v-model="repassword"
|
:type="repwdVisible ? 'text' : 'password'"
|
class="reg-input"
|
:placeholder="$t('surePassword')"
|
autocomplete="new-password"
|
/>
|
<span class="reg-eye" @click="repwdVisible = !repwdVisible">
|
<van-icon :name="repwdVisible ? 'eye-o' : 'closed-eye'" size="20" />
|
</span>
|
</div>
|
<!-- Referral Code (optional) -->
|
<div class="reg-field">
|
<input
|
v-model="invitCode"
|
type="text"
|
class="reg-input"
|
:placeholder="`${$t('entryInvitCode')} (optional)`"
|
/>
|
</div>
|
|
<div class="reg-protocol">
|
<span class="reg-protocol-check" :class="{ checked: agree }" @click="agree = !agree"></span>
|
<span>{{ $t('readAgree') }}</span>
|
<span class="reg-protocol-link" @click.stop="router.push('/aboutUs?serviceTerm=23')">{{ $t('serviceConf') }}</span>
|
</div>
|
|
<button class="reg-btn reg-btn-primary" @click="register">{{ $t('register') }}</button>
|
</div>
|
|
<nationality-list ref="controlChildRef" :title="$t('selectArea')" @getName="getName" />
|
</div>
|
</template>
|
|
<script setup>
|
import { _sendVerifyCode } from "@/service/login.api";
|
import { _bindEmailRegister } from "@/service/user.api.js";
|
import { useUserStore } from '@/store/user';
|
import { GET_USERINFO } from '@/store/types.store';
|
import nationalityList from '../authentication/components/nationalityList.vue';
|
import { getStorage } from '@/utils/index';
|
import { useI18n } from 'vue-i18n';
|
import { LOGO } from "@/config";
|
import { useRouter } from 'vue-router';
|
import { ref, computed, onMounted, onUnmounted, reactive } from 'vue';
|
import { showToast } from "vant";
|
import store from '@/store/store';
|
|
const { t, locale } = useI18n();
|
const router = useRouter();
|
const userStore = useUserStore();
|
|
const localeLabels = { en: 'English', cn: '中文', Korean: '한국인', Japanese: 'やまと', de: 'Deutsch', French: 'Français', Italy: 'Italiano' };
|
const currentLocaleLabel = computed(() => localeLabels[locale.value] || locale.value || 'English');
|
|
const username = ref('');
|
const password = ref('');
|
const agree = ref(false);
|
const repassword = ref('');
|
const verifyCode = ref('');
|
const invitCode = ref('');
|
const pwdVisible = ref(false);
|
const repwdVisible = ref(false);
|
const time = ref(0);
|
const dialCode = ref(0);
|
const icon = ref('');
|
const controlChildRef = ref(null);
|
const state = reactive({ timer: null });
|
|
onMounted(() => {
|
const usercode = getStorage('usercode');
|
if (usercode) invitCode.value = usercode;
|
});
|
onUnmounted(() => {
|
if (state.timer) clearInterval(state.timer);
|
});
|
|
const onRoute = (path) => {
|
router.push(path);
|
};
|
|
const senCode = () => {
|
if (time.value > 0) return;
|
const email = username.value.trim();
|
if (!email || !/@/.test(email)) {
|
showToast(t('entryCorrectEmail'));
|
return;
|
}
|
_sendVerifyCode({ target: email }).then(() => {
|
time.value = 30;
|
state.timer = setInterval(() => {
|
if (time.value > 0) time.value--;
|
else {
|
clearInterval(state.timer);
|
state.timer = null;
|
}
|
}, 1000);
|
}).catch((err) => {
|
showToast(err?.msg || t('sendVerifyCode'));
|
});
|
};
|
|
const validatePassword = (pwd) => {
|
return /^(?=.*[A-Z])(?=.*[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]).{8,16}$/.test(pwd);
|
};
|
|
const register = () => {
|
const email = username.value.trim();
|
if (!email || !/@/.test(email)) {
|
showToast(t('entryCorrectEmail'));
|
return;
|
}
|
if (!verifyCode.value || verifyCode.value.length < 6) {
|
showToast(t('entryVerifyTips'));
|
return;
|
}
|
if (!password.value) {
|
showToast(t('entryPassword'));
|
return;
|
}
|
if (!validatePassword(password.value)) {
|
showToast(t('passwordTips'));
|
return;
|
}
|
if (repassword.value !== password.value) {
|
showToast(t('noSamePassword'));
|
if (!agree.value) {
|
showToast(t('agreeServiceCond'));
|
return;
|
}
|
return;
|
}
|
// 推荐码可选,无则传空
|
registerApi();
|
};
|
|
const registerApi = () => {
|
_bindEmailRegister({
|
username: username.value.trim(),
|
password: password.value,
|
type: '2',
|
verifcode: verifyCode.value,
|
usercode: invitCode.value.trim() || '',
|
safeword: password.value
|
}).then((res) => {
|
userStore[GET_USERINFO](res);
|
store.state.user.userInfo = res;
|
router.push('/login');
|
}).catch((err) => {
|
showToast(err?.msg || t('register'));
|
});
|
};
|
|
const onDownloadApp = () => {
|
// 可配置 APP 下载链接
|
const url = 'https://your-app-download-url.com';
|
if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
|
window.open(url, '_blank');
|
} else {
|
showToast(t('downloadAPP'));
|
}
|
};
|
|
const getName = (params) => {
|
icon.value = params.code;
|
dialCode.value = params.dialCode;
|
};
|
</script>
|
|
<style lang="scss" scoped>
|
.register {
|
position: relative;
|
min-height: 100vh;
|
background: #fff;
|
padding: 56px 24px 48px;
|
box-sizing: border-box;
|
}
|
.reg-inner {
|
max-width: 400px;
|
margin: 0 auto;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
|
.reg-back {
|
position: absolute;
|
left: 24px;
|
top: 24px;
|
color: #000;
|
cursor: pointer;
|
}
|
|
.reg-logo-wrap {
|
width: 72px;
|
height: 72px;
|
border-radius: 18px;
|
background: linear-gradient(135deg, #2c1a5c 0%, #5a37a5 100%);
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
margin-bottom: 24px;
|
}
|
.reg-logo {
|
width: 44px;
|
height: 44px;
|
object-fit: contain;
|
}
|
|
.reg-title {
|
font-size: 26px;
|
font-weight: 700;
|
color: #000;
|
margin: 0 0 8px;
|
}
|
.reg-login {
|
font-size: 14px;
|
color: #4a4a4a;
|
margin: 0 0 6px;
|
}
|
.reg-login-link {
|
color: #8a2be2;
|
cursor: pointer;
|
}
|
.reg-lang {
|
font-size: 13px;
|
color: #9b9b9b;
|
margin: 0 0 28px;
|
cursor: pointer;
|
}
|
|
/* 输入框:与登录页一致 */
|
.reg-field {
|
width: 100%;
|
margin-bottom: 16px;
|
position: relative;
|
}
|
.reg-field-code {
|
.reg-input { padding-right: 90px; }
|
}
|
.reg-field-pwd {
|
.reg-input { padding-right: 44px; }
|
}
|
.reg-input {
|
width: 100%;
|
height: 48px;
|
padding: 0 16px;
|
box-sizing: border-box;
|
font-size: 15px;
|
color: #333;
|
background: #f6f5fa;
|
border: none;
|
border-radius: 6px;
|
outline: none;
|
}
|
.reg-input::placeholder {
|
color: #9b9b9b;
|
}
|
.reg-send-code {
|
position: absolute;
|
right: 12px;
|
top: 50%;
|
transform: translateY(-50%);
|
font-size: 14px;
|
color: #8a2be2;
|
cursor: pointer;
|
}
|
.reg-eye {
|
position: absolute;
|
right: 16px;
|
top: 50%;
|
transform: translateY(-50%);
|
color: #6e6e6e;
|
.reg-protocol {
|
width: 100%;
|
display: flex;
|
align-items: center;
|
flex-wrap: wrap;
|
gap: 8px;
|
margin-bottom: 24px;
|
font-size: 13px;
|
color: #4a4a4a;
|
}
|
.reg-protocol-check {
|
width: 16px;
|
height: 16px;
|
flex-shrink: 0;
|
border: 1px solid #ccc;
|
border-radius: 4px;
|
cursor: pointer;
|
background: #fff;
|
display: inline-block;
|
}
|
.reg-protocol-check.checked {
|
background: #8a2be2;
|
border-color: #8a2be2;
|
}
|
.reg-protocol-link {
|
color: #8a2be2;
|
cursor: pointer;
|
}
|
|
cursor: pointer;
|
}
|
|
.reg-protocol {
|
width: 100%;
|
display: flex;
|
align-items: center;
|
flex-wrap: wrap;
|
gap: 8px;
|
margin-bottom: 24px;
|
font-size: 13px;
|
color: #4a4a4a;
|
}
|
.reg-protocol-check {
|
width: 16px;
|
height: 16px;
|
flex-shrink: 0;
|
border: 1px solid #ccc;
|
border-radius: 4px;
|
cursor: pointer;
|
background: #fff;
|
display: inline-block;
|
}
|
.reg-protocol-check.checked {
|
background: #8a2be2;
|
border-color: #8a2be2;
|
}
|
.reg-protocol-link {
|
color: #8a2be2;
|
cursor: pointer;
|
}
|
|
/* 主按钮:渐变,圆角 6px */
|
.reg-btn {
|
width: 100%;
|
height: 48px;
|
border: none;
|
border-radius: 6px;
|
font-size: 16px;
|
font-weight: 700;
|
cursor: pointer;
|
}
|
.reg-btn-primary {
|
background: linear-gradient(90deg, #a443cf, #5e2bc8);
|
color: #fff;
|
margin-bottom: 12px;
|
}
|
.reg-btn-outline {
|
background: #fff;
|
border: 1px solid #8a2be2;
|
color: #8a2be2;
|
}
|
</style>
|