feat(auth): 优化PC与移动端登录注册界面并支持邀请码锁定绑定
- 重构 PC 端 LoginView.vue 与移动端 MobileRegisterView.vue,统一契合平台金橙色主体色 - 识别邀请链接并自动填入邀请码,锁定输入框为只读并展示「已绑定邀请码」专属徽章,防止篡改 - 短链 /i/:code 与 /m/i/:code 统一重定向至登录注册页,移除冗余独立落地页 - PC 端页脚完全对齐主站并固定沉底,左侧定制官方品质、安全上号、资金担保平台核心卖点 - 验证方式:npm run check(Prettier + ESLint + vue-tsc)全数通过
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -27,7 +27,16 @@ const form = reactive({
|
|||||||
|
|
||||||
const { countDown, sending, handleSendCode } = useSmsCountdown()
|
const { countDown, sending, handleSendCode } = useSmsCountdown()
|
||||||
|
|
||||||
onMounted(loadCaptcha)
|
onMounted(() => {
|
||||||
|
if (route.query.tab === 'register' || route.query.invite_code || route.query.code) {
|
||||||
|
void router.replace({
|
||||||
|
path: '/m/register',
|
||||||
|
query: route.query,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void loadCaptcha()
|
||||||
|
})
|
||||||
|
|
||||||
function switchToPassword() {
|
function switchToPassword() {
|
||||||
loginMode.value = 'password'
|
loginMode.value = 'password'
|
||||||
|
|||||||
@@ -13,10 +13,15 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const session = useSessionStore()
|
const session = useSessionStore()
|
||||||
const siteStore = useDistributionSiteStore()
|
const siteStore = useDistributionSiteStore()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const agreed = ref(false)
|
const agreed = ref(false)
|
||||||
const captchaLoading = ref(false)
|
const captchaLoading = ref(false)
|
||||||
const captcha = ref<AuthCaptcha | null>(null)
|
const captcha = ref<AuthCaptcha | null>(null)
|
||||||
|
const isInviteLocked = ref(false)
|
||||||
|
const showPassword = ref(false)
|
||||||
|
const showConfirmPassword = ref(false)
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
phone: '',
|
phone: '',
|
||||||
captchaCode: '',
|
captchaCode: '',
|
||||||
@@ -28,7 +33,14 @@ const form = reactive({
|
|||||||
|
|
||||||
const { countDown, sending, handleSendCode } = useSmsCountdown()
|
const { countDown, sending, handleSendCode } = useSmsCountdown()
|
||||||
|
|
||||||
onMounted(loadCaptcha)
|
onMounted(async () => {
|
||||||
|
await loadCaptcha()
|
||||||
|
const qInviteCode = (route.query.invite_code || route.query.code) as string | undefined
|
||||||
|
if (qInviteCode && qInviteCode.trim()) {
|
||||||
|
form.inviteCode = qInviteCode.trim()
|
||||||
|
isInviteLocked.value = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
async function loadCaptcha() {
|
async function loadCaptcha() {
|
||||||
captchaLoading.value = true
|
captchaLoading.value = true
|
||||||
@@ -54,11 +66,20 @@ async function sendCode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleRegister() {
|
async function handleRegister() {
|
||||||
if (!agreed.value) {
|
if (!form.phone.trim()) {
|
||||||
showToast({
|
showToast({ message: '请输入手机号', icon: 'warning-o' })
|
||||||
message: '请先阅读并同意用户协议和隐私政策',
|
return
|
||||||
icon: 'warning-o',
|
}
|
||||||
})
|
if (!/^1[3-9]\d{9}$/.test(form.phone.trim())) {
|
||||||
|
showToast({ message: '请输入正确的11位手机号码', icon: 'warning-o' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.captchaCode.trim()) {
|
||||||
|
showToast({ message: '请输入图形验证码', icon: 'warning-o' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!form.code.trim()) {
|
||||||
|
showToast({ message: '请输入短信验证码', icon: 'warning-o' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!form.password) {
|
if (!form.password) {
|
||||||
@@ -73,14 +94,27 @@ async function handleRegister() {
|
|||||||
showToast({ message: '两次输入的密码不一致', icon: 'warning-o' })
|
showToast({ message: '两次输入的密码不一致', icon: 'warning-o' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!agreed.value) {
|
||||||
|
showToast({
|
||||||
|
message: '请先阅读并同意用户协议和隐私政策',
|
||||||
|
icon: 'warning-o',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
await session.register(form.phone, form.code, form.password)
|
await session.register(
|
||||||
|
form.phone.trim(),
|
||||||
|
form.code.trim(),
|
||||||
|
form.password,
|
||||||
|
form.inviteCode.trim() || undefined
|
||||||
|
)
|
||||||
|
showToast({ message: '注册成功', icon: 'success' })
|
||||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/m/profile'
|
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/m/profile'
|
||||||
await router.replace(redirect)
|
await router.replace(redirect)
|
||||||
} catch {
|
} catch (error) {
|
||||||
showToast({ message: '注册失败,请检查手机号和验证码', icon: 'cross' })
|
showToast({ message: readError(error, '注册失败,请核对信息'), icon: 'cross' })
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
@@ -90,12 +124,13 @@ async function handleRegister() {
|
|||||||
<template>
|
<template>
|
||||||
<main class="mobile-register-shell">
|
<main class="mobile-register-shell">
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<button class="back-btn" type="button" @click="router.back()">
|
<button class="back-btn" type="button" aria-label="返回上一页" @click="router.back()">
|
||||||
<van-icon name="arrow-left" :size="20" />
|
<van-icon name="arrow-left" :size="20" />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="auth-body">
|
<section class="auth-body">
|
||||||
|
<!-- 品牌信息 -->
|
||||||
<div class="brand-lockup">
|
<div class="brand-lockup">
|
||||||
<img v-if="siteStore.logoUrl" :src="siteStore.logoUrl" alt="" class="brand-logo-img" />
|
<img v-if="siteStore.logoUrl" :src="siteStore.logoUrl" alt="" class="brand-logo-img" />
|
||||||
<div v-else class="brand-logo">{{ siteStore.brandMark }}</div>
|
<div v-else class="brand-logo">{{ siteStore.brandMark }}</div>
|
||||||
@@ -103,30 +138,39 @@ async function handleRegister() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h1>注册</h1>
|
<div class="form-title-row">
|
||||||
|
<h1>注册账号</h1>
|
||||||
|
<RouterLink to="/m/login" class="title-link">已有账号?<b>去登录</b></RouterLink>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 手机号 -->
|
||||||
<label class="auth-input-row">
|
<label class="auth-input-row">
|
||||||
|
<van-icon name="phone-o" class="row-icon" />
|
||||||
<input
|
<input
|
||||||
v-model="form.phone"
|
v-model="form.phone"
|
||||||
type="tel"
|
type="tel"
|
||||||
maxlength="11"
|
maxlength="11"
|
||||||
inputmode="tel"
|
inputmode="tel"
|
||||||
placeholder="手机号"
|
placeholder="请输入手机号"
|
||||||
|
autocomplete="tel"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<!-- 图形验证码 -->
|
||||||
<label class="auth-input-row captcha-row">
|
<label class="auth-input-row captcha-row">
|
||||||
|
<van-icon name="shield-o" class="row-icon" />
|
||||||
<input
|
<input
|
||||||
v-model="form.captchaCode"
|
v-model="form.captchaCode"
|
||||||
type="text"
|
type="text"
|
||||||
maxlength="4"
|
maxlength="4"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
placeholder="图形验证码"
|
placeholder="请输入图形验证码"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
class="captcha-image-button"
|
class="captcha-image-button"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="captchaLoading"
|
:disabled="captchaLoading"
|
||||||
|
title="点击刷新验证码"
|
||||||
@click="loadCaptcha"
|
@click="loadCaptcha"
|
||||||
>
|
>
|
||||||
<img v-if="captcha" :src="captcha.image" alt="图形验证码" />
|
<img v-if="captcha" :src="captcha.image" alt="图形验证码" />
|
||||||
@@ -134,13 +178,15 @@ async function handleRegister() {
|
|||||||
</button>
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<!-- 短信验证码 -->
|
||||||
<label class="auth-input-row code-row">
|
<label class="auth-input-row code-row">
|
||||||
|
<van-icon name="comment-o" class="row-icon" />
|
||||||
<input
|
<input
|
||||||
v-model="form.code"
|
v-model="form.code"
|
||||||
type="text"
|
type="text"
|
||||||
inputmode="numeric"
|
inputmode="numeric"
|
||||||
maxlength="6"
|
maxlength="6"
|
||||||
placeholder="验证码"
|
placeholder="请输入短信验证码"
|
||||||
/>
|
/>
|
||||||
<span class="code-action">
|
<span class="code-action">
|
||||||
<van-button
|
<van-button
|
||||||
@@ -156,39 +202,66 @@ async function handleRegister() {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="auth-input-row">
|
<!-- 设置密码 -->
|
||||||
|
<label class="auth-input-row pwd-row">
|
||||||
|
<van-icon name="lock" class="row-icon" />
|
||||||
<input
|
<input
|
||||||
v-model="form.password"
|
v-model="form.password"
|
||||||
type="password"
|
:type="showPassword ? 'text' : 'password'"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
placeholder="设置密码(8-20 位)"
|
placeholder="设置密码(8-20 位)"
|
||||||
|
autocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pwd-toggle-btn"
|
||||||
|
aria-label="切换密码可见性"
|
||||||
|
@click="showPassword = !showPassword"
|
||||||
|
>
|
||||||
|
<van-icon :name="showPassword ? 'eye-o' : 'closed-eye'" size="18" />
|
||||||
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="auth-input-row">
|
<!-- 确认密码 -->
|
||||||
|
<label class="auth-input-row pwd-row">
|
||||||
|
<van-icon name="lock" class="row-icon" />
|
||||||
<input
|
<input
|
||||||
v-model="form.confirmPassword"
|
v-model="form.confirmPassword"
|
||||||
type="password"
|
:type="showConfirmPassword ? 'text' : 'password'"
|
||||||
maxlength="20"
|
maxlength="20"
|
||||||
placeholder="确认密码"
|
placeholder="确认登录密码"
|
||||||
|
autocomplete="new-password"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="pwd-toggle-btn"
|
||||||
|
aria-label="切换密码可见性"
|
||||||
|
@click="showConfirmPassword = !showConfirmPassword"
|
||||||
|
>
|
||||||
|
<van-icon :name="showConfirmPassword ? 'eye-o' : 'closed-eye'" size="18" />
|
||||||
|
</button>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="auth-input-row">
|
<!-- 邀请码 -->
|
||||||
<input v-model="form.inviteCode" maxlength="16" placeholder="邀请码(选填)" />
|
<label class="auth-input-row invite-row" :class="{ 'is-locked': isInviteLocked }">
|
||||||
|
<van-icon name="coupon-o" class="row-icon" />
|
||||||
|
<input
|
||||||
|
v-model="form.inviteCode"
|
||||||
|
maxlength="32"
|
||||||
|
:readonly="isInviteLocked"
|
||||||
|
:placeholder="isInviteLocked ? '' : '邀请码(选填)'"
|
||||||
|
/>
|
||||||
|
<span v-if="isInviteLocked" class="invite-locked-tag">已绑定邀请码</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="assist-row">
|
<!-- 协议勾选 -->
|
||||||
<span>验证码注册</span>
|
|
||||||
<RouterLink to="/m/login">已有账号?<b>去登录</b></RouterLink>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="agreement-row">
|
<div class="agreement-row">
|
||||||
<van-checkbox v-model="agreed" shape="square" icon-size="16px">
|
<van-checkbox v-model="agreed" shape="square" icon-size="16px" checked-color="#ff6a00">
|
||||||
我已阅读并同意《<b>用户协议</b>》和《<b>隐私政策</b>》
|
我已阅读并同意《<b>用户协议</b>》和《<b>隐私政策</b>》
|
||||||
</van-checkbox>
|
</van-checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 注册按钮 -->
|
||||||
<van-button
|
<van-button
|
||||||
type="primary"
|
type="primary"
|
||||||
block
|
block
|
||||||
@@ -200,7 +273,8 @@ async function handleRegister() {
|
|||||||
注册并登录
|
注册并登录
|
||||||
</van-button>
|
</van-button>
|
||||||
|
|
||||||
<RouterLink to="/m/login" class="secondary-entry"> 返回登录 </RouterLink>
|
<!-- 底部快捷跳转 -->
|
||||||
|
<RouterLink to="/m/login" class="secondary-entry"> 已有账号?去登录 </RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
@@ -209,9 +283,10 @@ async function handleRegister() {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.mobile-register-shell {
|
.mobile-register-shell {
|
||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
|
box-sizing: border-box;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at 50% 12%, rgba(20, 119, 255, 0.1), transparent 34%),
|
radial-gradient(circle at 50% 12%, rgba(255, 106, 0, 0.12), transparent 34%),
|
||||||
linear-gradient(180deg, #f8fbff 0%, #ffffff 70%, #f7fafc 100%);
|
linear-gradient(180deg, #fff7ed 0%, #ffffff 68%, #fffaf4 100%);
|
||||||
color: #111827;
|
color: #111827;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,20 +299,20 @@ async function handleRegister() {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
margin: 0;
|
padding: 0 16px;
|
||||||
padding: 0 18px;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.back-btn {
|
.back-btn {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 34px;
|
width: 36px;
|
||||||
height: 34px;
|
height: 36px;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: none;
|
background: rgba(255, 255, 255, 0.8);
|
||||||
color: #111827;
|
color: #1e293b;
|
||||||
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-body {
|
.auth-body {
|
||||||
@@ -246,113 +321,158 @@ async function handleRegister() {
|
|||||||
min-height: 100dvh;
|
min-height: 100dvh;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 50px 34px 22px;
|
padding: 56px 24px 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 品牌头部 */
|
||||||
.brand-lockup {
|
.brand-lockup {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin: 0 auto clamp(24px, 4vh, 38px);
|
margin: 0 auto clamp(20px, 3vh, 32px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-logo {
|
.brand-logo {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 46px;
|
width: 48px;
|
||||||
height: 46px;
|
height: 48px;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: linear-gradient(135deg, #1477ff, #2f8dff);
|
background: linear-gradient(135deg, #ff6a00, #ff8a1f);
|
||||||
box-shadow: 0 14px 32px rgba(20, 119, 255, 0.18);
|
box-shadow: 0 10px 26px rgba(255, 106, 0, 0.28);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 19px;
|
font-size: 20px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-logo-img {
|
.brand-logo-img {
|
||||||
width: 46px;
|
width: 48px;
|
||||||
height: 46px;
|
height: 48px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
box-shadow: 0 14px 32px rgba(23, 35, 61, 0.12);
|
box-shadow: 0 10px 26px rgba(23, 35, 61, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-lockup strong {
|
.brand-lockup strong {
|
||||||
display: block;
|
display: block;
|
||||||
color: #111827;
|
color: #0f172a;
|
||||||
font-size: 18px;
|
font-size: 19px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0.3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表单容器 */
|
||||||
.form-section {
|
.form-section {
|
||||||
width: min(100%, 420px);
|
width: min(100%, 400px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-section h1 {
|
.form-title-row {
|
||||||
margin: 0 0 20px;
|
display: flex;
|
||||||
color: #05070a;
|
align-items: baseline;
|
||||||
font-size: 22px;
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title-row h1 {
|
||||||
|
margin: 0;
|
||||||
|
color: #0f172a;
|
||||||
|
font-size: 24px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title-link {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #64748b;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-link b {
|
||||||
|
color: #ff6a00;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框行 Shell */
|
||||||
.auth-input-row {
|
.auth-input-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 12px;
|
||||||
padding: 0 16px;
|
padding: 0 14px;
|
||||||
border: 1px solid #e1e5eb;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
background: rgba(255, 255, 255, 0.88);
|
background: #ffffff;
|
||||||
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.03);
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.02);
|
||||||
|
transition:
|
||||||
|
border-color 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-input-row.code-row {
|
.auth-input-row:focus-within {
|
||||||
gap: 8px;
|
border-color: #ff6a00;
|
||||||
|
box-shadow: 0 0 0 3px rgba(255, 106, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-input-row.captcha-row {
|
.row-icon {
|
||||||
gap: 10px;
|
font-size: 17px;
|
||||||
padding-right: 4px;
|
color: #94a3b8;
|
||||||
|
margin-right: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-input-row input {
|
.auth-input-row input {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-input-row input::placeholder {
|
.auth-input-row input::placeholder {
|
||||||
color: #b6beca;
|
color: #94a3b8;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-action {
|
/* 密码明暗切换 */
|
||||||
display: flex;
|
.pwd-toggle-btn {
|
||||||
justify-content: flex-end;
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: #94a3b8;
|
||||||
|
padding: 4px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pwd-toggle-btn:active {
|
||||||
|
color: #ff6a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图形验证码 */
|
||||||
|
.captcha-row {
|
||||||
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-image-button {
|
.captcha-image-button {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: 0 0 116px;
|
flex: 0 0 106px;
|
||||||
width: 116px;
|
width: 106px;
|
||||||
height: 40px;
|
height: 38px;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid #dbe5f0;
|
border: 1px solid #e2e8f0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
color: #1477ff;
|
color: #ff6a00;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.captcha-image-button:disabled {
|
.captcha-image-button:disabled {
|
||||||
@@ -361,104 +481,125 @@ async function handleRegister() {
|
|||||||
|
|
||||||
.captcha-image-button img {
|
.captcha-image-button img {
|
||||||
display: block;
|
display: block;
|
||||||
width: 120px;
|
width: 100%;
|
||||||
height: 40px;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.code-btn {
|
/* 短信验证码 */
|
||||||
min-width: 82px;
|
.code-row {
|
||||||
height: 32px;
|
padding-right: 4px;
|
||||||
border-color: #1477ff !important;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: #1477ff !important;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.assist-row {
|
.code-action {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
justify-content: flex-end;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 14px;
|
|
||||||
margin: 12px 0 18px;
|
|
||||||
color: #111827;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.assist-row span {
|
.code-btn {
|
||||||
min-width: 0;
|
min-width: 86px;
|
||||||
color: #1477ff;
|
height: 36px;
|
||||||
|
border: 1px solid #fed7aa !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
background: #fff7ed !important;
|
||||||
|
color: #ff6a00 !important;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assist-row a {
|
.code-btn:disabled {
|
||||||
|
border-color: #e2e8f0 !important;
|
||||||
|
background: #f8fafc !important;
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
opacity: 0.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 锁定邀请码 */
|
||||||
|
.auth-input-row.is-locked {
|
||||||
|
background: #f8fafc;
|
||||||
|
border-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-input-row.is-locked input {
|
||||||
|
color: #0f172a;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invite-locked-tag {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: #22252b;
|
margin-left: 8px;
|
||||||
text-decoration: none;
|
padding: 3px 8px;
|
||||||
}
|
border-radius: 4px;
|
||||||
|
background: #ecfdf5;
|
||||||
.assist-row b {
|
border: 1px solid #a7f3d0;
|
||||||
color: #05070a;
|
color: #059669;
|
||||||
font-weight: 900;
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 协议条款勾选 */
|
||||||
.agreement-row {
|
.agreement-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0 0 18px;
|
margin: 6px 0 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agreement-row :deep(.van-checkbox__label) {
|
.agreement-row :deep(.van-checkbox__label) {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
color: #252b36;
|
color: #64748b;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agreement-row :deep(.van-checkbox__label b) {
|
.agreement-row :deep(.van-checkbox__label b) {
|
||||||
color: #1477ff;
|
color: #ff6a00;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 注册主按钮(金橙色渐变) */
|
||||||
.primary-button {
|
.primary-button {
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
border-radius: 10px !important;
|
border-radius: 12px !important;
|
||||||
background: #1477ff !important;
|
background: linear-gradient(135deg, #ff6a00, #ff8a1f) !important;
|
||||||
box-shadow: 0 12px 24px rgba(20, 119, 255, 0.18);
|
box-shadow: 0 10px 24px rgba(255, 106, 0, 0.28) !important;
|
||||||
font-size: 17px;
|
font-size: 16px;
|
||||||
font-weight: 900;
|
font-weight: 800;
|
||||||
|
color: #ffffff !important;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primary-button:active {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 次要入口 */
|
||||||
.secondary-entry {
|
.secondary-entry {
|
||||||
display: grid;
|
display: block;
|
||||||
min-height: 44px;
|
margin-top: 16px;
|
||||||
margin-top: 10px;
|
|
||||||
place-items: center;
|
|
||||||
border: 1px solid #e4e8ee;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: rgba(255, 255, 255, 0.82);
|
|
||||||
color: #1477ff;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 900;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-height: 760px) {
|
.secondary-entry:active {
|
||||||
|
color: #ff6a00;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: 740px) {
|
||||||
.auth-body {
|
.auth-body {
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding-top: 48px;
|
padding-top: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-lockup {
|
.brand-lockup {
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-section h1 {
|
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assist-row {
|
.form-title-row {
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,664 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import {
|
|
||||||
Lock,
|
|
||||||
Iphone,
|
|
||||||
Key,
|
|
||||||
View,
|
|
||||||
Hide,
|
|
||||||
Present,
|
|
||||||
WarningFilled,
|
|
||||||
Check,
|
|
||||||
} from '@element-plus/icons-vue'
|
|
||||||
import { fetchAuthCaptcha, sendSmsCode } from '@/features/auth/api/auth'
|
|
||||||
import { fetchInviteCodeInfo, type InviteCodeInfo } from '@/features/invitation/api/invitation'
|
|
||||||
import { useSessionStore } from '@/stores/session'
|
|
||||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
|
||||||
import { readError } from '@/shared/utils/error'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
const session = useSessionStore()
|
|
||||||
const siteStore = useDistributionSiteStore()
|
|
||||||
|
|
||||||
const inviteCode = computed(() => String(route.params.code || '').trim())
|
|
||||||
const loadingInfo = ref(true)
|
|
||||||
const codeInfo = ref<InviteCodeInfo | null>(null)
|
|
||||||
|
|
||||||
// 注册表单状态
|
|
||||||
const form = reactive({
|
|
||||||
phone: '',
|
|
||||||
captchaCode: '',
|
|
||||||
smsCode: '',
|
|
||||||
password: '',
|
|
||||||
confirmPassword: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const showPassword = ref(false)
|
|
||||||
const showConfirmPassword = ref(false)
|
|
||||||
const submitting = ref(false)
|
|
||||||
|
|
||||||
// 图形验证码状态
|
|
||||||
const captchaId = ref('')
|
|
||||||
const captchaImage = ref('')
|
|
||||||
const loadingCaptcha = ref(false)
|
|
||||||
|
|
||||||
// 短信倒计时
|
|
||||||
const smsCountdown = ref(0)
|
|
||||||
let timer: ReturnType<typeof setInterval> | null = null
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await Promise.all([loadCodeInfo(), refreshCaptcha()])
|
|
||||||
})
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (timer) clearInterval(timer)
|
|
||||||
})
|
|
||||||
|
|
||||||
async function loadCodeInfo() {
|
|
||||||
if (!inviteCode.value) {
|
|
||||||
loadingInfo.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
loadingInfo.value = true
|
|
||||||
try {
|
|
||||||
const res = await fetchInviteCodeInfo(inviteCode.value)
|
|
||||||
codeInfo.value = res
|
|
||||||
} catch (_err) {
|
|
||||||
ElMessage.warning('邀请码查询失败,请核对邀请链接')
|
|
||||||
} finally {
|
|
||||||
loadingInfo.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshCaptcha() {
|
|
||||||
loadingCaptcha.value = true
|
|
||||||
try {
|
|
||||||
const res = await fetchAuthCaptcha()
|
|
||||||
captchaId.value = res.captcha_id
|
|
||||||
captchaImage.value = res.image
|
|
||||||
} catch (_err) {
|
|
||||||
ElMessage.error('获取图形验证码失败,请点击刷新')
|
|
||||||
} finally {
|
|
||||||
loadingCaptcha.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSendSms() {
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
|
|
||||||
ElMessage.warning('请输入正确的11位手机号码')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.captchaCode) {
|
|
||||||
ElMessage.warning('请先输入图形验证码')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (smsCountdown.value > 0) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await sendSmsCode(form.phone, captchaId.value, form.captchaCode)
|
|
||||||
ElMessage.success('短信验证码已发送')
|
|
||||||
smsCountdown.value = 60
|
|
||||||
timer = setInterval(() => {
|
|
||||||
smsCountdown.value--
|
|
||||||
if (smsCountdown.value <= 0 && timer) {
|
|
||||||
clearInterval(timer)
|
|
||||||
timer = null
|
|
||||||
}
|
|
||||||
}, 1000)
|
|
||||||
} catch (err) {
|
|
||||||
ElMessage.error(readError(err, '发送验证码失败'))
|
|
||||||
await refreshCaptcha()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSubmit() {
|
|
||||||
if (!/^1[3-9]\d{9}$/.test(form.phone)) {
|
|
||||||
ElMessage.warning('请输入正确的11位手机号码')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.smsCode || form.smsCode.length !== 6) {
|
|
||||||
ElMessage.warning('请输入6位短信验证码')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!form.password || form.password.length < 8 || form.password.length > 20) {
|
|
||||||
ElMessage.warning('密码长度必须为8-20位')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (form.password !== form.confirmPassword) {
|
|
||||||
ElMessage.warning('两次输入的密码不一致')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
submitting.value = true
|
|
||||||
try {
|
|
||||||
await session.register(form.phone, form.smsCode, form.password, inviteCode.value)
|
|
||||||
|
|
||||||
ElMessageBox.confirm(
|
|
||||||
'账号注册成功并已绑定邀请人!根据平台规定,完成实名认证后方可正式激活会员特权与邀请返利资格。是否立即前往实名认证?',
|
|
||||||
'注册成功',
|
|
||||||
{
|
|
||||||
confirmButtonText: '去实名认证',
|
|
||||||
cancelButtonText: '稍后再说',
|
|
||||||
type: 'success',
|
|
||||||
distinguishCancelAndClose: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.then(() => {
|
|
||||||
router.replace('/realname')
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
router.replace('/')
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
ElMessage.error(readError(err, '注册失败'))
|
|
||||||
await refreshCaptcha()
|
|
||||||
} finally {
|
|
||||||
submitting.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="invite-register-page">
|
|
||||||
<div class="invite-card-container">
|
|
||||||
<!-- 顶部邀请人特权卡片 -->
|
|
||||||
<div class="invite-header-banner">
|
|
||||||
<div class="banner-badge">
|
|
||||||
<el-icon><Present /></el-icon>
|
|
||||||
<span>专属邀请专享特权</span>
|
|
||||||
</div>
|
|
||||||
<h1 class="banner-title">加入 {{ siteStore.siteName || '哈夫币' }}</h1>
|
|
||||||
<p class="banner-sub">安全便捷的游戏资产租赁与寄售平台 · 租客号主双边返利</p>
|
|
||||||
|
|
||||||
<div v-if="loadingInfo" class="inviter-box loading">
|
|
||||||
<span>正在核对邀请信息...</span>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="codeInfo && codeInfo.valid" class="inviter-box valid">
|
|
||||||
<div class="inviter-avatar">
|
|
||||||
<img v-if="codeInfo.inviter_avatar" :src="codeInfo.inviter_avatar" alt="avatar" />
|
|
||||||
<span v-else>{{ (codeInfo.inviter_nickname || '友').slice(0, 1) }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="inviter-meta">
|
|
||||||
<div class="inviter-name">
|
|
||||||
来自 <strong>{{ codeInfo.inviter_nickname || '平台认证号主' }}</strong> 的邀请
|
|
||||||
</div>
|
|
||||||
<div class="inviter-desc">
|
|
||||||
专属邀请码:<code class="code-pill">{{ inviteCode }}</code>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else class="inviter-box invalid">
|
|
||||||
<el-icon color="#e6a23c"><WarningFilled /></el-icon>
|
|
||||||
<span
|
|
||||||
>邀请码
|
|
||||||
<strong>{{ inviteCode }}</strong> 暂未生效或邀请人尚未实名,注册仍可正常完成</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 返利权益亮点条 -->
|
|
||||||
<div class="rebate-perks-row">
|
|
||||||
<div class="perk-item">
|
|
||||||
<span class="perk-rate">60%</span>
|
|
||||||
<span class="perk-label">首月高额分成</span>
|
|
||||||
</div>
|
|
||||||
<div class="perk-divider"></div>
|
|
||||||
<div class="perk-item">
|
|
||||||
<span class="perk-rate">30%</span>
|
|
||||||
<span class="perk-label">次月奖励分成</span>
|
|
||||||
</div>
|
|
||||||
<div class="perk-divider"></div>
|
|
||||||
<div class="perk-item">
|
|
||||||
<span class="perk-rate">15%</span>
|
|
||||||
<span class="perk-label">往后终身永久</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 注册表单卡片 -->
|
|
||||||
<div class="register-card-body">
|
|
||||||
<div class="form-title">创建新账号</div>
|
|
||||||
|
|
||||||
<form class="register-form" @submit.prevent="handleSubmit">
|
|
||||||
<!-- 手机号 -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label>手机号码</label>
|
|
||||||
<div class="input-wrapper">
|
|
||||||
<el-icon class="input-icon"><Iphone /></el-icon>
|
|
||||||
<input
|
|
||||||
v-model="form.phone"
|
|
||||||
type="tel"
|
|
||||||
maxlength="11"
|
|
||||||
placeholder="请输入11位手机号码"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 图形验证码 -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label>图形验证码</label>
|
|
||||||
<div class="input-wrapper captcha-wrapper">
|
|
||||||
<el-icon class="input-icon"><Key /></el-icon>
|
|
||||||
<input
|
|
||||||
v-model="form.captchaCode"
|
|
||||||
type="text"
|
|
||||||
maxlength="4"
|
|
||||||
placeholder="请输入图形验证码"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<div class="captcha-img-box" @click="refreshCaptcha">
|
|
||||||
<img v-if="captchaImage" :src="captchaImage" alt="captcha" />
|
|
||||||
<span v-else>点击获取</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 短信验证码 -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label>短信验证码</label>
|
|
||||||
<div class="input-wrapper sms-wrapper">
|
|
||||||
<el-icon class="input-icon"><Key /></el-icon>
|
|
||||||
<input
|
|
||||||
v-model="form.smsCode"
|
|
||||||
type="text"
|
|
||||||
maxlength="6"
|
|
||||||
placeholder="6位短信验证码"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="sms-btn"
|
|
||||||
:disabled="smsCountdown > 0"
|
|
||||||
@click="handleSendSms"
|
|
||||||
>
|
|
||||||
{{ smsCountdown > 0 ? `${smsCountdown}秒后重试` : '获取验证码' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 密码 -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label>登录密码</label>
|
|
||||||
<div class="input-wrapper">
|
|
||||||
<el-icon class="input-icon"><Lock /></el-icon>
|
|
||||||
<input
|
|
||||||
v-model="form.password"
|
|
||||||
:type="showPassword ? 'text' : 'password'"
|
|
||||||
placeholder="设置登录密码(8-20位)"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button type="button" class="pwd-toggle" @click="showPassword = !showPassword">
|
|
||||||
<el-icon><View v-if="showPassword" /><Hide v-else /></el-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 确认密码 -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label>确认密码</label>
|
|
||||||
<div class="input-wrapper">
|
|
||||||
<el-icon class="input-icon"><Lock /></el-icon>
|
|
||||||
<input
|
|
||||||
v-model="form.confirmPassword"
|
|
||||||
:type="showConfirmPassword ? 'text' : 'password'"
|
|
||||||
placeholder="再次输入登录密码"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="pwd-toggle"
|
|
||||||
@click="showConfirmPassword = !showConfirmPassword"
|
|
||||||
>
|
|
||||||
<el-icon><View v-if="showConfirmPassword" /><Hide v-else /></el-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 邀请码锁定提示 -->
|
|
||||||
<div class="invite-bind-notice">
|
|
||||||
<div class="bind-badge">
|
|
||||||
<el-icon color="#67c23a"><Check /></el-icon>
|
|
||||||
<span
|
|
||||||
>已绑定邀请码:<strong>{{ inviteCode }}</strong></span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<p class="bind-tip">注册成功后将自动建立推荐返利关联</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 提交按钮 -->
|
|
||||||
<button type="submit" class="submit-btn" :disabled="submitting">
|
|
||||||
{{ submitting ? '正在注册并绑定...' : '立即注册并领取返利特权' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="login-redirect">
|
|
||||||
已有账号?
|
|
||||||
<RouterLink to="/login" class="login-link">直接登录</RouterLink>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.invite-register-page {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: linear-gradient(135deg, #f0f4ff 0%, #e8edf9 100%);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 32px 16px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-card-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 480px;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 20px;
|
|
||||||
box-shadow: 0 12px 40px rgba(30, 58, 138, 0.08);
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid rgba(226, 232, 240, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-header-banner {
|
|
||||||
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%);
|
|
||||||
color: #ffffff;
|
|
||||||
padding: 28px 24px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
background: rgba(255, 255, 255, 0.18);
|
|
||||||
padding: 4px 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-sub {
|
|
||||||
margin: 8px 0 16px;
|
|
||||||
font-size: 13px;
|
|
||||||
opacity: 0.85;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inviter-box {
|
|
||||||
background: rgba(255, 255, 255, 0.12);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
text-align: left;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inviter-box.invalid {
|
|
||||||
background: rgba(234, 179, 8, 0.2);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inviter-avatar {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #ffffff;
|
|
||||||
color: #2563eb;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 16px;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inviter-avatar img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inviter-meta {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inviter-name {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inviter-desc {
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.8;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-pill {
|
|
||||||
background: rgba(255, 255, 255, 0.25);
|
|
||||||
padding: 1px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: monospace;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rebate-perks-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
background: #f8fafc;
|
|
||||||
padding: 12px 16px;
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.perk-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.perk-rate {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #dc2626;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.perk-label {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #64748b;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.perk-divider {
|
|
||||||
width: 1px;
|
|
||||||
height: 24px;
|
|
||||||
background: #cbd5e1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.register-card-body {
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.register-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #475569;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 0 12px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-wrapper:focus-within {
|
|
||||||
border-color: #2563eb;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-icon {
|
|
||||||
color: #94a3b8;
|
|
||||||
font-size: 16px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-wrapper input {
|
|
||||||
flex: 1;
|
|
||||||
height: 42px;
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #1e293b;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-img-box {
|
|
||||||
width: 96px;
|
|
||||||
height: 36px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: #e2e8f0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #64748b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.captcha-img-box img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sms-btn {
|
|
||||||
background: #2563eb;
|
|
||||||
color: #ffffff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sms-btn:disabled {
|
|
||||||
background: #94a3b8;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pwd-toggle {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #94a3b8;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-bind-notice {
|
|
||||||
background: #f0fdf4;
|
|
||||||
border: 1px solid #bbf7d0;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bind-badge {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #166534;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bind-tip {
|
|
||||||
margin: 4px 0 0 22px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: #15803d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn {
|
|
||||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
|
||||||
color: #ffffff;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
height: 44px;
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 6px;
|
|
||||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.25);
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn:hover:not(:disabled) {
|
|
||||||
opacity: 0.95;
|
|
||||||
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.35);
|
|
||||||
}
|
|
||||||
|
|
||||||
.submit-btn:disabled {
|
|
||||||
background: #94a3b8;
|
|
||||||
cursor: not-allowed;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-redirect {
|
|
||||||
text-align: center;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #64748b;
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-link {
|
|
||||||
color: #2563eb;
|
|
||||||
font-weight: 600;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-link:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -5,6 +5,7 @@ export const accountRoutes: RouteRecordRaw[] = [
|
|||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'login',
|
name: 'login',
|
||||||
component: () => import('@/features/auth/views/LoginView.vue'),
|
component: () => import('@/features/auth/views/LoginView.vue'),
|
||||||
|
meta: { layout: 'blank' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/orders/create',
|
path: '/orders/create',
|
||||||
|
|||||||
@@ -43,6 +43,16 @@ export const mobileRoutes: RouteRecordRaw[] = [
|
|||||||
component: () => import('@/features/auth/views/MobileRegisterView.vue'),
|
component: () => import('@/features/auth/views/MobileRegisterView.vue'),
|
||||||
meta: { layout: 'blank' },
|
meta: { layout: 'blank' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/m/i/:code',
|
||||||
|
name: 'mobile-invite-register',
|
||||||
|
redirect: to => ({
|
||||||
|
path: '/m/register',
|
||||||
|
query: {
|
||||||
|
invite_code: String(to.params.code || ''),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/m/messages',
|
path: '/m/messages',
|
||||||
name: 'mobile-messages',
|
name: 'mobile-messages',
|
||||||
|
|||||||
@@ -39,12 +39,17 @@ export const publicRoutes: RouteRecordRaw[] = [
|
|||||||
name: 'crash-detail',
|
name: 'crash-detail',
|
||||||
component: () => import('@/features/mohong/views/MohongDetailView.vue'),
|
component: () => import('@/features/mohong/views/MohongDetailView.vue'),
|
||||||
},
|
},
|
||||||
// 专属邀请注册落地页
|
// 专属邀请短链重定向
|
||||||
{
|
{
|
||||||
path: '/i/:code',
|
path: '/i/:code',
|
||||||
name: 'invite-register',
|
name: 'invite-register',
|
||||||
component: () => import('@/features/invitation/views/InviteRegisterView.vue'),
|
redirect: to => ({
|
||||||
meta: { layout: 'blank' },
|
path: '/login',
|
||||||
|
query: {
|
||||||
|
tab: 'register',
|
||||||
|
invite_code: String(to.params.code || ''),
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
// 旧路径兼容
|
// 旧路径兼容
|
||||||
{ path: '/mohong', redirect: '/crash' },
|
{ path: '/mohong', redirect: '/crash' },
|
||||||
|
|||||||
Reference in New Issue
Block a user