feat(player): theme-2 PC auth layout and correct-score quick bet popover
This commit is contained in:
169
apps/player/src/components/desktop/DesktopAuthLayout.vue
Normal file
169
apps/player/src/components/desktop/DesktopAuthLayout.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* PC auth shell — admin-style card: large logo left, form slot right (theme-2).
|
||||
*/
|
||||
import LocaleSwitcher from '../LocaleSwitcher.vue';
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
wide?: boolean;
|
||||
}>(),
|
||||
{ wide: false },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-auth-page">
|
||||
<div class="ambient-glow ambient-glow--1" aria-hidden="true"></div>
|
||||
<div class="ambient-glow ambient-glow--2" aria-hidden="true"></div>
|
||||
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-card" :class="{ 'auth-card--wide': wide }">
|
||||
<div class="form-lang">
|
||||
<LocaleSwitcher compact />
|
||||
</div>
|
||||
<div class="form-body">
|
||||
<aside class="form-brand">
|
||||
<img src="/logo.png" alt="TheBet365" class="logo-large" />
|
||||
</aside>
|
||||
<div class="form-fields">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-auth-page {
|
||||
position: relative;
|
||||
min-height: 100dvh;
|
||||
background: linear-gradient(160deg, #f5f7fa 0%, #e8edf2 55%, #f5f7fa 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktop-auth-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 70% 60% at 50% 20%, rgba(0, 102, 204, 0.08) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ambient-glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(70px);
|
||||
pointer-events: none;
|
||||
opacity: 0.35;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.ambient-glow--1 {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
top: 8%;
|
||||
left: 10%;
|
||||
background: radial-gradient(circle, rgba(0, 91, 159, 0.22) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.ambient-glow--2 {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
bottom: 10%;
|
||||
right: 8%;
|
||||
background: radial-gradient(circle, rgba(0, 102, 204, 0.16) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
.auth-wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100dvh;
|
||||
padding: 24px 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--shadow-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-card--wide {
|
||||
max-width: 820px;
|
||||
}
|
||||
|
||||
.form-lang {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 14px 18px 0;
|
||||
}
|
||||
|
||||
.form-body {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(200px, 260px) 1fr;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.form-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28px 20px;
|
||||
background: linear-gradient(180deg, rgba(0, 102, 204, 0.05) 0%, rgba(232, 237, 242, 0.65) 100%);
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.logo-large {
|
||||
width: 100%;
|
||||
max-width: 220px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 6px 20px rgba(0, 91, 159, 0.18));
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 22px 24px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.auth-wrap {
|
||||
align-items: flex-start;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.auth-card,
|
||||
.auth-card--wide {
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.form-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-brand {
|
||||
padding: 20px 24px 12px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.logo-large {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
padding: 18px 20px 22px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { groupCorrectScoreSelections, type CsSelection } from '../../utils/correctScoreLayout';
|
||||
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
|
||||
|
||||
const props = defineProps<{
|
||||
marketType: string;
|
||||
@@ -24,6 +25,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { cancelClose } = useDesktopBetPopover();
|
||||
|
||||
const columns = computed(() =>
|
||||
groupCorrectScoreSelections(
|
||||
@@ -48,7 +50,11 @@ function formatOdds(odds: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="cs-panel" :class="{ 'cs-panel--locked': locked, 'cs-panel--dense': dense }">
|
||||
<div
|
||||
class="cs-panel"
|
||||
:class="{ 'cs-panel--locked': locked, 'cs-panel--dense': dense }"
|
||||
@mouseenter="cancelClose"
|
||||
>
|
||||
<div class="cols-head">
|
||||
<span>{{ t('bet.col_home') }}</span>
|
||||
<span>{{ t('bet.col_draw') }}</span>
|
||||
@@ -65,6 +71,7 @@ function formatOdds(odds: string) {
|
||||
:class="{ selected: isSelected(sel.id), 'score-card--locked': locked }"
|
||||
:data-bet-selection-id="sel.id"
|
||||
:disabled="locked"
|
||||
@mouseenter="onPick(sel, $event)"
|
||||
@click="onPick(sel, $event)"
|
||||
>
|
||||
<span class="score-line">{{ sel.scoreDisplay }}</span>
|
||||
|
||||
@@ -164,6 +164,7 @@ export default {
|
||||
},
|
||||
auth:
|
||||
{ login: 'Login',
|
||||
logging_in: 'Logging in…',
|
||||
register: 'Create Account',
|
||||
logout: 'Log out',
|
||||
username: 'Username',
|
||||
@@ -218,6 +219,7 @@ export default {
|
||||
forgot_password: 'Forgot password?',
|
||||
forgot_password_title: 'Reset password',
|
||||
reset_password_btn: 'Reset password',
|
||||
resetting_password: 'Resetting…',
|
||||
reset_success: 'Password reset. Please sign in with your new password. Contact your agent if you cannot log in.',
|
||||
reset_failed: 'Reset failed. Please try again.',
|
||||
phone_not_registered: 'This phone number is not registered',
|
||||
|
||||
@@ -177,6 +177,7 @@ export default {
|
||||
},
|
||||
auth: {
|
||||
login: 'Log Masuk',
|
||||
logging_in: 'Sedang log masuk…',
|
||||
register: 'Daftar Akaun',
|
||||
logout: 'Log Keluar',
|
||||
username: 'Nama Pengguna',
|
||||
@@ -231,6 +232,7 @@ export default {
|
||||
forgot_password: 'Lupa kata laluan?',
|
||||
forgot_password_title: 'Tetapkan semula kata laluan',
|
||||
reset_password_btn: 'Tetapkan semula',
|
||||
resetting_password: 'Sedang tetapkan semula…',
|
||||
reset_success: 'Kata laluan ditetapkan semula. Sila log masuk dengan kata laluan baharu. Hubungi ejen anda jika tidak dapat log masuk.',
|
||||
reset_failed: 'Gagal menetapkan semula. Sila cuba lagi.',
|
||||
phone_not_registered: 'Nombor telefon ini belum didaftarkan',
|
||||
|
||||
@@ -164,6 +164,7 @@ export default {
|
||||
},
|
||||
auth: {
|
||||
login: '登录',
|
||||
logging_in: '登录中…',
|
||||
register: '注册账号',
|
||||
logout: '退出登录',
|
||||
username: '账号',
|
||||
@@ -218,6 +219,7 @@ export default {
|
||||
forgot_password: '忘记密码?',
|
||||
forgot_password_title: '重置密码',
|
||||
reset_password_btn: '重置密码',
|
||||
resetting_password: '重置中…',
|
||||
reset_success: '密码已重置,请使用新密码登录。若无法登录请联系代理。',
|
||||
reset_failed: '重置失败,请重试',
|
||||
phone_not_registered: '该手机号未注册',
|
||||
|
||||
@@ -7,8 +7,11 @@ import { useSmsCode } from '../composables/useSmsCode';
|
||||
import api from '../api';
|
||||
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
||||
import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import loginBg from '../assets/images/zt2.webp';
|
||||
|
||||
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -98,12 +101,16 @@ function goLogin() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page" :style="{ backgroundImage: `url(${loginBg})` }">
|
||||
<div class="login-lang">
|
||||
<div class="login-page" :class="{ 'login-page--desktop': props.desktop }" :style="props.desktop ? undefined : { backgroundImage: `url(${loginBg})` }">
|
||||
<div v-if="!props.desktop" class="login-lang">
|
||||
<LocaleSwitcher compact />
|
||||
</div>
|
||||
<div class="login-stack">
|
||||
<form @submit.prevent="submit" class="login-form ps-gold-frame">
|
||||
<form @submit.prevent="submit" class="login-form" :class="{ 'ps-gold-frame': !props.desktop, 'is-busy': loading }">
|
||||
<div v-if="loading" class="form-busy-overlay" aria-hidden="true">
|
||||
<GoldSpinner :size="40" :active="true" />
|
||||
<p class="form-busy-text">{{ t('auth.resetting_password') }}</p>
|
||||
</div>
|
||||
<h2 class="form-title">{{ t('auth.forgot_password_title') }}</h2>
|
||||
|
||||
<div class="field">
|
||||
@@ -116,6 +123,7 @@ function goLogin() {
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel-national"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.phone_local_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
@@ -130,12 +138,13 @@ function goLogin() {
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.sms_code_placeholder')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
:disabled="sending || countdown > 0 || !phone.trim()"
|
||||
:disabled="sending || countdown > 0 || !phone.trim() || loading"
|
||||
@click="sendCode"
|
||||
>
|
||||
{{ countdown > 0 ? `${countdown}s` : t('auth.send_sms') }}
|
||||
@@ -152,6 +161,7 @@ function goLogin() {
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.password_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
@@ -165,15 +175,26 @@ function goLogin() {
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="fieldError()" class="error">{{ fieldError() }}</p>
|
||||
|
||||
<button type="submit" class="btn-login btn-gold-outline" :disabled="loading">
|
||||
{{ t('auth.reset_password_btn') }}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-login btn-gold-outline"
|
||||
:class="{ 'is-loading': loading }"
|
||||
:disabled="loading"
|
||||
:aria-busy="loading"
|
||||
>
|
||||
<span v-if="loading" class="btn-inline-loading">
|
||||
<GoldSpinner :size="18" :active="true" />
|
||||
{{ t('auth.resetting_password') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.reset_password_btn') }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn-skip" @click="goLogin">
|
||||
<button type="button" class="btn-skip" :disabled="loading" @click="goLogin">
|
||||
{{ t('auth.back_to_login') }}
|
||||
</button>
|
||||
</form>
|
||||
@@ -205,6 +226,22 @@ function goLogin() {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.login-page--desktop {
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-stack {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-stack {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -213,6 +250,7 @@ function goLogin() {
|
||||
}
|
||||
|
||||
.login-form {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -220,6 +258,48 @@ function goLogin() {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.login-form.is-busy {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-busy-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border-radius: inherit;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(4px);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.form-busy-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.btn-inline-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-form {
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 16px;
|
||||
@@ -228,6 +308,56 @@ function goLogin() {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-page--desktop .form-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-page--desktop .field-input,
|
||||
.login-page--desktop .btn-secondary {
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login {
|
||||
margin-top: 10px;
|
||||
min-height: 50px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip:hover:not(:disabled) {
|
||||
border-color: var(--border-active);
|
||||
background: var(--surface-accent);
|
||||
}
|
||||
|
||||
.login-page--desktop .error {
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border-left: 2px solid var(--danger);
|
||||
}
|
||||
|
||||
.btn-login.is-loading:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -8,8 +8,11 @@ import { useAppLocale } from '../composables/useAppLocale';
|
||||
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
||||
import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
|
||||
import RobotVerify from '../components/RobotVerify.vue';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import loginBg from '../assets/images/zt2.webp';
|
||||
|
||||
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
|
||||
|
||||
type LoginMode = 'account' | 'phone';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
@@ -85,17 +88,27 @@ function goRegister() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page" :style="{ backgroundImage: `url(${loginBg})` }">
|
||||
<div class="login-lang">
|
||||
<div class="login-page" :class="{ 'login-page--desktop': props.desktop }" :style="props.desktop ? undefined : { backgroundImage: `url(${loginBg})` }">
|
||||
<div v-if="!props.desktop" class="login-lang">
|
||||
<LocaleSwitcher compact />
|
||||
</div>
|
||||
<div class="login-stack">
|
||||
<form @submit.prevent="submit" class="login-form ps-gold-frame">
|
||||
<form
|
||||
@submit.prevent="submit"
|
||||
class="login-form"
|
||||
:class="{ 'ps-gold-frame': !props.desktop, 'is-busy': loading }"
|
||||
>
|
||||
<div v-if="loading" class="form-busy-overlay" aria-hidden="true">
|
||||
<GoldSpinner :size="40" :active="true" />
|
||||
<p class="form-busy-text">{{ t('auth.logging_in') }}</p>
|
||||
</div>
|
||||
<h2 v-if="props.desktop" class="form-title">{{ t('auth.login') }}</h2>
|
||||
<div class="auth-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="auth-tab"
|
||||
:class="{ active: loginMode === 'account' }"
|
||||
:disabled="loading"
|
||||
@click="switchLoginMode('account')"
|
||||
>
|
||||
{{ t('auth.login_by_account') }}
|
||||
@@ -104,6 +117,7 @@ function goRegister() {
|
||||
type="button"
|
||||
class="auth-tab"
|
||||
:class="{ active: loginMode === 'phone' }"
|
||||
:disabled="loading"
|
||||
@click="switchLoginMode('phone')"
|
||||
>
|
||||
{{ t('auth.login_by_phone') }}
|
||||
@@ -118,6 +132,7 @@ function goRegister() {
|
||||
required
|
||||
autocomplete="username"
|
||||
maxlength="32"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.username_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
@@ -132,6 +147,7 @@ function goRegister() {
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel-national"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.phone_local_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
@@ -139,25 +155,38 @@ function goRegister() {
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.password') }}</label>
|
||||
<input v-model="password" class="field-input ps-gold-input" type="password" required autocomplete="current-password" />
|
||||
<input v-model="password" class="field-input ps-gold-input" type="password" required autocomplete="current-password" :disabled="loading" />
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-forgot" @click="goForgotPassword">
|
||||
<button type="button" class="btn-forgot" :disabled="loading" @click="goForgotPassword">
|
||||
{{ t('auth.forgot_password') }}
|
||||
</button>
|
||||
|
||||
<RobotVerify ref="captchaRef" />
|
||||
<p v-if="resetSuccess" class="success">{{ t('auth.reset_success') }}</p>
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<button type="submit" class="btn-login btn-gold-outline" :disabled="loading">
|
||||
{{ t('auth.login') }}
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-login btn-gold-outline"
|
||||
:class="{ 'is-loading': loading }"
|
||||
:disabled="loading"
|
||||
:aria-busy="loading"
|
||||
>
|
||||
<span v-if="loading" class="btn-inline-loading">
|
||||
<GoldSpinner :size="18" :active="true" />
|
||||
{{ t('auth.logging_in') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.login') }}</span>
|
||||
</button>
|
||||
<button v-if="props.desktop" type="button" class="btn-skip" :disabled="loading" @click="goRegister">
|
||||
{{ t('auth.go_register') }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="login-actions">
|
||||
<button type="button" class="btn-skip" @click="goRegister">
|
||||
<div v-if="!props.desktop" class="login-actions">
|
||||
<button type="button" class="btn-skip" :disabled="loading" @click="goRegister">
|
||||
{{ t('auth.go_register') }}
|
||||
</button>
|
||||
<button type="button" class="btn-skip-light" @click="continueBrowsing">
|
||||
<button type="button" class="btn-skip-light" :disabled="loading" @click="continueBrowsing">
|
||||
{{ t('auth.continue_browsing') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -189,6 +218,22 @@ function goRegister() {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.login-page--desktop {
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-stack {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-stack {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -200,6 +245,7 @@ function goRegister() {
|
||||
}
|
||||
|
||||
.login-form {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -207,6 +253,118 @@ function goRegister() {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.login-form.is-busy {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-busy-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border-radius: inherit;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(4px);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.form-busy-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.btn-inline-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-form {
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
text-align: left;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-page--desktop .field-input {
|
||||
height: 46px;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.login-page--desktop .auth-tabs {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login {
|
||||
margin-top: 10px;
|
||||
min-height: 50px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip:hover:not(:disabled) {
|
||||
border-color: var(--border-active);
|
||||
background: var(--surface-accent);
|
||||
}
|
||||
|
||||
.login-page--desktop .error {
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border-left: 2px solid var(--danger);
|
||||
}
|
||||
|
||||
.login-page--desktop .success {
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(5, 150, 105, 0.08);
|
||||
border-left: 2px solid var(--success);
|
||||
}
|
||||
|
||||
.btn-login.is-loading:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.auth-tab:disabled,
|
||||
.btn-forgot:disabled,
|
||||
.btn-skip:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -11,6 +11,8 @@ import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import loginBg from '../assets/images/zt2.webp';
|
||||
|
||||
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { initFromUser } = useAppLocale();
|
||||
const auth = useAuthStore();
|
||||
@@ -101,12 +103,16 @@ const fieldError = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-page" :style="{ backgroundImage: `url(${loginBg})` }">
|
||||
<div class="login-lang">
|
||||
<div class="login-page" :class="{ 'login-page--desktop': props.desktop }" :style="props.desktop ? undefined : { backgroundImage: `url(${loginBg})` }">
|
||||
<div v-if="!props.desktop" class="login-lang">
|
||||
<LocaleSwitcher compact />
|
||||
</div>
|
||||
<div class="login-stack">
|
||||
<form @submit.prevent="submit" class="login-form ps-gold-frame" :class="{ 'is-busy': loading }">
|
||||
<form
|
||||
@submit.prevent="submit"
|
||||
class="login-form"
|
||||
:class="{ 'ps-gold-frame': !props.desktop, 'is-busy': loading }"
|
||||
>
|
||||
<div v-if="loading" class="form-busy-overlay" aria-hidden="true">
|
||||
<GoldSpinner :size="40" :active="true" />
|
||||
<p class="form-busy-text">{{ t('auth.registering') }}</p>
|
||||
@@ -200,8 +206,11 @@ const fieldError = () => {
|
||||
</span>
|
||||
<span v-else>{{ t('auth.register_btn') }}</span>
|
||||
</button>
|
||||
<button v-if="props.desktop" type="button" class="btn-skip" :disabled="loading" @click="goLogin">
|
||||
{{ t('auth.have_account') }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="login-actions">
|
||||
<div v-if="!props.desktop" class="login-actions">
|
||||
<button type="button" class="btn-skip" :disabled="loading" @click="goLogin">
|
||||
{{ t('auth.have_account') }}
|
||||
</button>
|
||||
@@ -237,6 +246,22 @@ const fieldError = () => {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.login-page--desktop {
|
||||
min-height: auto;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
align-items: stretch;
|
||||
justify-content: flex-start;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-stack {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-stack {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -298,6 +323,60 @@ const fieldError = () => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-form {
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.login-page--desktop .form-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-page--desktop .field-input,
|
||||
.login-page--desktop .btn-secondary {
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login {
|
||||
margin-top: 10px;
|
||||
min-height: 50px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-skip:hover:not(:disabled) {
|
||||
border-color: var(--border-active);
|
||||
background: var(--surface-accent);
|
||||
}
|
||||
|
||||
.login-page--desktop .error {
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border-left: 2px solid var(--danger);
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 16px;
|
||||
|
||||
@@ -1,121 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Desktop Forgot Password View
|
||||
*/
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import DesktopAuthLayout from '../../components/desktop/DesktopAuthLayout.vue';
|
||||
import ForgotPasswordView from '../ForgotPasswordView.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-auth-shell">
|
||||
<RouterLink to="/login" class="auth-back-link">{{ t('common.back') }}</RouterLink>
|
||||
<div class="auth-brand">
|
||||
<div class="brand-inner">
|
||||
<img src="/logo.png" alt="TheBet365" class="brand-logo" />
|
||||
<h1 class="brand-name">THEBET365</h1>
|
||||
<p class="brand-tagline">重置您的登录密码</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-card-wrap">
|
||||
<div class="auth-card">
|
||||
<ForgotPasswordView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopAuthLayout wide>
|
||||
<ForgotPasswordView desktop />
|
||||
</DesktopAuthLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-auth-shell {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: var(--desktop-bg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auth-back-link {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 24px;
|
||||
z-index: 2;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.auth-back-link:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-card) 50%, var(--bg-card) 100%);
|
||||
border-right: 1px solid var(--desktop-border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-brand::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 60% 60% at 50% 40%, rgba(0, 102, 204,0.06) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 16px rgba(0, 102, 204,0.3));
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--primary-light);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-card-wrap {
|
||||
width: 480px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
background: var(--desktop-bg);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,156 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Desktop Login View
|
||||
* Reuses the existing LoginView form in a centered two-column auth layout.
|
||||
*/
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import DesktopAuthLayout from '../../components/desktop/DesktopAuthLayout.vue';
|
||||
import LoginView from '../LoginView.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-auth-shell">
|
||||
<RouterLink to="/" class="auth-back-link">{{ t('common.back') }}</RouterLink>
|
||||
<div class="auth-brand">
|
||||
<div class="brand-inner">
|
||||
<img src="/logo.png" alt="TheBet365" class="brand-logo" />
|
||||
<h1 class="brand-name">THEBET365</h1>
|
||||
<p class="brand-tagline">专业体育赛事投注平台</p>
|
||||
<div class="brand-features">
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon">⚡</span>
|
||||
<span>实时赔率更新</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon">🔒</span>
|
||||
<span>安全稳定平台</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<span class="feature-icon">💰</span>
|
||||
<span>快速充值提款</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-card-wrap">
|
||||
<div class="auth-card">
|
||||
<LoginView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopAuthLayout>
|
||||
<LoginView desktop />
|
||||
</DesktopAuthLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-auth-shell {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: var(--desktop-bg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auth-back-link {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 24px;
|
||||
z-index: 2;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.auth-back-link:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-card) 50%, var(--bg-card) 100%);
|
||||
border-right: 1px solid var(--desktop-border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-brand::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 60% 60% at 50% 40%, rgba(0, 102, 204,0.06) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 16px rgba(0, 102, 204,0.3));
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--primary-light);
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 12px rgba(0, 102, 204,0.25);
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.brand-features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.auth-card-wrap {
|
||||
width: 480px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
background: var(--desktop-bg);
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,122 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Desktop Register View
|
||||
* Reuses the existing RegisterView form in a centered desktop auth layout.
|
||||
*/
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import DesktopAuthLayout from '../../components/desktop/DesktopAuthLayout.vue';
|
||||
import RegisterView from '../RegisterView.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="desktop-auth-shell">
|
||||
<RouterLink to="/" class="auth-back-link">{{ t('common.back') }}</RouterLink>
|
||||
<div class="auth-brand">
|
||||
<div class="brand-inner">
|
||||
<img src="/logo.png" alt="TheBet365" class="brand-logo" />
|
||||
<h1 class="brand-name">THEBET365</h1>
|
||||
<p class="brand-tagline">加入我们,开始精彩体育投注</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-card-wrap">
|
||||
<div class="auth-card">
|
||||
<RegisterView />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DesktopAuthLayout wide>
|
||||
<RegisterView desktop />
|
||||
</DesktopAuthLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.desktop-auth-shell {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
background: var(--desktop-bg);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.auth-back-link {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 24px;
|
||||
z-index: 2;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.auth-back-link:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.auth-brand {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-card) 50%, var(--bg-card) 100%);
|
||||
border-right: 1px solid var(--desktop-border);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-brand::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 60% 60% at 50% 40%, rgba(0, 102, 204,0.06) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.brand-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 16px rgba(0, 102, 204,0.3));
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--primary-light);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.brand-tagline {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-card-wrap {
|
||||
width: 520px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
background: var(--desktop-bg);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user