feat(player): 重构 PC 端登录注册布局并优化注单栏与加载态

- 新增 DesktopAuthLayout 与 MouseRippleGrid,登录/注册/忘记密码采用管理端左右卡片布局(左侧大 Logo)

- 登录页补齐 GoldSpinner 全表单加载遮罩与三语 logging_in 文案

- 注单栏与 Hub 侧栏使用 cebian.webp 背景,左侧联赛栏保持纯色
This commit is contained in:
mars
2026-07-07 14:09:54 +08:00
parent ebcfaad264
commit cca74310ed
17 changed files with 998 additions and 336 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

View File

@@ -0,0 +1,284 @@
<script setup lang="ts">
/**
* MouseRippleGrid — interactive grid that only appears near the cursor.
* Mouse movement creates expanding ripple waves that distort the grid.
*
* Performance notes:
* - Skips points outside the max effective radius of all ripples + mouse
* - Caps devicePixelRatio at 2
* - Respects prefers-reduced-motion (renders nothing)
* - pointer-events:none so form interactions are never blocked
*/
import { ref, onMounted, onBeforeUnmount } from 'vue';
const canvasRef = ref<HTMLCanvasElement | null>(null);
let ctx: CanvasRenderingContext2D | null = null;
let raf = 0;
let width = 0;
let height = 0;
let dpr = 1;
let parent: HTMLElement | null = null;
let resizeObs: ResizeObserver | null = null;
let reducedMotion = false;
/* ---- Mouse state ---- */
let mouseX = -9999;
let mouseY = -9999;
let lastRippleX = -9999;
let lastRippleY = -9999;
let mouseInside = false;
let mouseFade = 0; // smoothed 0→1 for graceful fade
/* ---- Ripple data ---- */
interface Ripple {
x: number;
y: number;
born: number;
strength: number;
}
const ripples: Ripple[] = [];
/* ---- Tuning constants ---- */
const GRID = 34; // grid spacing px
const VISIBLE_R = 210; // mouse visibility radius
const RIPPLE_SPEED = 140; // px/s propagation
const RIPPLE_LIFE = 2000; // ms
const RIPPLE_SKIP_R = 380; // skip points farther than this from any ripple
const SIGMA = 42; // wave packet width
const MAX_DISPLACE = 14; // cap displacement px
function resize() {
const c = canvasRef.value;
if (!c || !c.parentElement) return;
parent = c.parentElement;
const rect = parent.getBoundingClientRect();
dpr = Math.min(window.devicePixelRatio || 1, 2);
width = rect.width;
height = rect.height;
c.width = Math.round(width * dpr);
c.height = Math.round(height * dpr);
c.style.width = width + 'px';
c.style.height = height + 'px';
ctx = c.getContext('2d');
if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function handleMove(e: MouseEvent) {
if (!parent) return;
const rect = parent.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
mouseInside = true;
const dx = mouseX - lastRippleX;
const dy = mouseY - lastRippleY;
if (dx * dx + dy * dy > 36) {
const dist = Math.sqrt(dx * dx + dy * dy);
ripples.push({
x: mouseX,
y: mouseY,
born: performance.now(),
strength: Math.min(dist * 0.12, 5),
});
lastRippleX = mouseX;
lastRippleY = mouseY;
if (ripples.length > 30) ripples.shift();
}
}
function handleEnter() {
mouseInside = true;
}
function handleLeave() {
mouseInside = false;
mouseX = -9999;
mouseY = -9999;
}
function frame() {
raf = requestAnimationFrame(frame);
if (!ctx) return;
const now = performance.now();
/* Smooth fade in/out when mouse enters/leaves */
const target = mouseInside ? 1 : 0;
mouseFade += (target - mouseFade) * 0.05;
/* Purge expired ripples */
for (let i = ripples.length - 1; i >= 0; i--) {
if (now - ripples[i].born > RIPPLE_LIFE) ripples.splice(i, 1);
}
/* Nothing to draw — clear and bail */
if (mouseFade < 0.004 && ripples.length === 0) {
ctx.clearRect(0, 0, width, height);
return;
}
ctx.clearRect(0, 0, width, height);
const cols = Math.ceil(width / GRID) + 2;
const rows = Math.ceil(height / GRID) + 2;
/* Precompute active ripple data for this frame */
type ARipple = { x: number; y: number; age: number; strength: number };
const active: ARipple[] = [];
for (const r of ripples) {
const age = (now - r.born) / RIPPLE_LIFE;
if (age < 1) active.push({ x: r.x, y: r.y, age, strength: r.strength });
}
/* ---- Pass 1: compute displaced positions + opacities ---- */
type Cell = { x: number; y: number; o: number };
const grid: Cell[][] = new Array(rows);
for (let row = 0; row < rows; row++) {
grid[row] = new Array(cols);
for (let col = 0; col < cols; col++) {
const bx = (col - 0.5) * GRID;
const by = (row - 0.5) * GRID;
/* Base opacity from mouse proximity */
let opacity = 0;
if (mouseFade > 0.004) {
const mdx = bx - mouseX;
const mdy = by - mouseY;
const mdist = Math.sqrt(mdx * mdx + mdy * mdy);
if (mdist < VISIBLE_R) {
const f = 1 - mdist / VISIBLE_R;
opacity = f * f * 0.28 * mouseFade;
}
}
/* Ripple displacement */
let dx = 0;
let dy = 0;
let rippleO = 0;
for (let i = 0; i < active.length; i++) {
const r = active[i];
const rdx = bx - r.x;
const rdy = by - r.y;
const rdist = Math.sqrt(rdx * rdx + rdy * rdy);
if (rdist > RIPPLE_SKIP_R) continue;
const wavefront = r.age * RIPPLE_SPEED * (RIPPLE_LIFE / 1000);
const distFromFront = rdist - wavefront;
const env = Math.exp(-(distFromFront * distFromFront) / (2 * SIGMA * SIGMA));
const osc = Math.cos(distFromFront * 0.11);
const decay = (1 - r.age) * Math.exp(-rdist / 190);
const amp = r.strength * decay * env * osc * 6;
if (rdist > 0.5) {
dx += (rdx / rdist) * amp;
dy += (rdy / rdist) * amp;
}
rippleO = Math.max(rippleO, decay * env * 0.45);
}
/* Cap displacement */
const dLen = Math.sqrt(dx * dx + dy * dy);
if (dLen > MAX_DISPLACE) {
dx = (dx / dLen) * MAX_DISPLACE;
dy = (dy / dLen) * MAX_DISPLACE;
}
opacity = Math.max(opacity, rippleO * mouseFade);
grid[row][col] = { x: bx + dx, y: by + dy, o: opacity };
}
}
/* ---- Pass 2: draw grid lines ---- */
ctx.lineWidth = 0.6;
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const p = grid[row][col];
if (p.o < 0.008) continue;
/* Line to right neighbour */
if (col < cols - 1) {
const n = grid[row][col + 1];
if (n.o > 0.008) {
const lo = Math.min(p.o, n.o) * 0.55;
ctx.strokeStyle = `rgba(212,175,55,${lo})`;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(n.x, n.y);
ctx.stroke();
}
}
/* Line to bottom neighbour */
if (row < rows - 1) {
const n = grid[row + 1][col];
if (n.o > 0.008) {
const lo = Math.min(p.o, n.o) * 0.55;
ctx.strokeStyle = `rgba(212,175,55,${lo})`;
ctx.beginPath();
ctx.moveTo(p.x, p.y);
ctx.lineTo(n.x, n.y);
ctx.stroke();
}
}
}
}
/* ---- Pass 3: draw intersection dots ---- */
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const p = grid[row][col];
if (p.o < 0.008) continue;
ctx.fillStyle = `rgba(240,216,117,${p.o})`;
ctx.beginPath();
ctx.arc(p.x, p.y, 1.3, 0, Math.PI * 2);
ctx.fill();
}
}
}
onMounted(() => {
reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
resize();
const c = canvasRef.value;
if (!c || !c.parentElement) return;
parent = c.parentElement;
parent.addEventListener('mousemove', handleMove);
parent.addEventListener('mouseenter', handleEnter);
parent.addEventListener('mouseleave', handleLeave);
resizeObs = new ResizeObserver(resize);
resizeObs.observe(parent);
if (!reducedMotion) {
raf = requestAnimationFrame(frame);
}
});
onBeforeUnmount(() => {
cancelAnimationFrame(raf);
if (parent) {
parent.removeEventListener('mousemove', handleMove);
parent.removeEventListener('mouseenter', handleEnter);
parent.removeEventListener('mouseleave', handleLeave);
}
resizeObs?.disconnect();
});
</script>
<template>
<canvas ref="canvasRef" class="ripple-grid" aria-hidden="true" />
</template>
<style scoped>
.ripple-grid {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 0;
}
</style>

View File

@@ -1181,7 +1181,7 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
height: 100%;
background: rgba(16, 16, 16, 0.95);
background: var(--desktop-sidebar-panel-bg);
}
.panel-head {

View File

@@ -0,0 +1,199 @@
<script setup lang="ts">
/**
* PC auth shell — admin-style card: large logo left, form slot right.
*/
import MouseRippleGrid from '../MouseRippleGrid.vue';
import LocaleSwitcher from '../LocaleSwitcher.vue';
withDefaults(
defineProps<{
/** Wider card for register / forgot-password forms */
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>
<MouseRippleGrid />
<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(135deg, #0a0a0a 0%, #111 50%, #0d0d0d 100%);
overflow: hidden;
}
.desktop-auth-page::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(ellipse 60% 60% at 50% 40%, rgba(212, 175, 55, 0.06) 0%, transparent 70%);
pointer-events: none;
}
.ambient-glow {
position: absolute;
border-radius: 50%;
filter: blur(60px);
pointer-events: none;
opacity: 0.12;
z-index: 0;
}
.ambient-glow--1 {
width: 320px;
height: 320px;
top: 10%;
left: 12%;
background: radial-gradient(circle, rgba(212, 175, 55, 0.5) 0%, transparent 70%);
animation: ambient-drift-1 12s ease-in-out infinite alternate;
}
.ambient-glow--2 {
width: 280px;
height: 280px;
bottom: 12%;
right: 12%;
background: radial-gradient(circle, rgba(240, 216, 117, 0.35) 0%, transparent 70%);
animation: ambient-drift-2 16s ease-in-out infinite alternate;
}
@keyframes ambient-drift-1 {
0% {
transform: translate(0, 0) scale(1);
}
100% {
transform: translate(30px, -20px) scale(1.1);
}
}
@keyframes ambient-drift-2 {
0% {
transform: translate(0, 0) scale(1);
}
100% {
transform: translate(-25px, 15px) scale(0.95);
}
}
.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(14, 14, 14, 0.92);
border: 1px solid rgba(212, 175, 55, 0.28);
border-radius: 12px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.45);
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: rgba(0, 0, 0, 0.25);
border-right: 1px solid rgba(212, 175, 55, 0.15);
}
.logo-large {
width: 100%;
max-width: 220px;
height: auto;
object-fit: contain;
filter: drop-shadow(0 4px 24px rgba(212, 175, 55, 0.25));
}
.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 rgba(212, 175, 55, 0.15);
}
.logo-large {
max-width: 180px;
}
.form-fields {
padding: 18px 20px 22px;
}
}
@media (prefers-reduced-motion: reduce) {
.ambient-glow--1,
.ambient-glow--2 {
animation: none;
}
}
</style>

View File

@@ -47,7 +47,7 @@ watch(
width: var(--desktop-right-betslip-w);
min-width: 0;
border-left: 1px solid var(--border);
background: var(--bg-card);
background: var(--desktop-sidebar-panel-bg);
display: flex;
flex-direction: column;
min-height: 0;

View File

@@ -172,6 +172,7 @@ export default {
},
auth:
{ login: 'Login',
logging_in: 'Logging in…',
register: 'Create Account',
logout: 'Log out',
username: 'Username',

View File

@@ -184,6 +184,7 @@ export default {
},
auth: {
login: 'Log Masuk',
logging_in: 'Sedang log masuk…',
register: 'Daftar Akaun',
logout: 'Log Keluar',
username: 'Nama Pengguna',

View File

@@ -172,6 +172,7 @@ export default {
},
auth: {
login: '登录',
logging_in: '登录中…',
register: '注册账号',
logout: '退出登录',
username: '账号',

View File

@@ -166,7 +166,7 @@
.desktop-hub-left {
border-right: 1px solid var(--border);
background: rgba(14, 14, 14, 0.6);
background: var(--desktop-sidebar-panel-bg);
overflow: hidden;
display: flex;
flex-direction: column;

View File

@@ -35,6 +35,7 @@
--desktop-bg: #050505;
--desktop-border: #262626;
--desktop-sidebar-bg: rgba(20, 20, 20, 0.95);
--desktop-sidebar-panel-bg: linear-gradient(rgba(16, 16, 16, 0.88), rgba(16, 16, 16, 0.88)), url('../../assets/images/cebian.webp') no-repeat center / cover;
/* Reset typography inside desktop */
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;

View File

@@ -9,6 +9,8 @@ import LocaleSwitcher from '../components/LocaleSwitcher.vue';
import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
import loginBg from '../assets/images/h5bg.webp';
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
const { t, locale } = useI18n();
const router = useRouter();
const route = useRoute();
@@ -98,11 +100,11 @@ 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>
<form @submit.prevent="submit" class="login-form ps-gold-frame">
<form @submit.prevent="submit" class="login-form" :class="{ 'ps-gold-frame': !props.desktop }">
<h2 class="form-title">{{ t('auth.forgot_password_title') }}</h2>
<div class="field">
@@ -203,6 +205,17 @@ 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-form {
width: 100%;
max-width: 320px;
@@ -212,6 +225,16 @@ function goLogin() {
padding: 12px;
}
.login-page--desktop .login-form {
max-width: none;
width: 100%;
gap: 10px;
padding: 0;
border: none !important;
background: transparent !important;
box-shadow: none !important;
}
.form-title {
margin: 0 0 2px;
font-size: 16px;
@@ -220,12 +243,24 @@ function goLogin() {
text-align: center;
}
.login-page--desktop .form-title {
margin: 0 0 8px;
font-size: 18px;
text-align: left;
color: var(--primary-light, #f0d875);
letter-spacing: 0.04em;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.login-page--desktop .field {
gap: 8px;
}
label {
font-size: 10px;
color: var(--text-muted);
@@ -233,6 +268,11 @@ label {
letter-spacing: 0.03em;
}
.login-page--desktop label {
font-size: 13px;
letter-spacing: 0.03em;
}
.field-input {
width: 100%;
height: 36px;
@@ -243,6 +283,7 @@ label {
color: var(--text);
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.field-input::placeholder {
@@ -254,6 +295,22 @@ label {
border-color: #555;
}
.login-page--desktop .field-input {
height: 46px;
font-size: 14px;
border-radius: 4px;
padding: 0 14px;
}
.login-page--desktop .field-input:focus {
border-color: var(--border-gold, #d4af37);
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.18);
}
.login-page--desktop .field-input:hover:not(:focus) {
border-color: rgba(255, 255, 255, 0.18);
}
.inline-row {
display: flex;
gap: 6px;
@@ -277,6 +334,19 @@ label {
font-weight: 600;
white-space: nowrap;
cursor: pointer;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.login-page--desktop .btn-secondary {
height: 46px;
font-size: 13px;
border-radius: 4px;
padding: 0 14px;
}
.login-page--desktop .btn-secondary:hover:not(:disabled) {
border-color: rgba(255, 255, 255, 0.2);
background: rgba(30, 30, 30, 0.8);
}
.btn-secondary:disabled {
@@ -290,6 +360,20 @@ label {
border-radius: 6px;
font-size: 14px;
font-weight: 800;
transition: background-color 0.2s ease, border-color 0.2s ease, opacity 0.2s ease;
}
.login-page--desktop .btn-login {
margin-top: 10px;
padding: 14px 18px;
border-radius: 4px;
font-size: 15px;
height: 50px;
}
.login-page--desktop .btn-login:hover:not(:disabled) {
background: rgba(212, 175, 55, 0.1);
border-color: var(--border-gold, #d4af37);
}
.btn-login:disabled {
@@ -306,6 +390,23 @@ label {
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: color 0.2s ease;
}
.login-page--desktop .btn-skip {
margin-top: 6px;
padding: 12px 16px;
border-radius: 4px;
border: 1px solid var(--border, #333);
background: rgba(20, 20, 20, 0.6);
font-size: 14px;
transition: border-color 0.2s ease, color 0.2s ease, background-color 0.2s ease;
}
.login-page--desktop .btn-skip:hover {
border-color: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.8);
background: rgba(30, 30, 30, 0.8);
}
.btn-skip:active {
@@ -319,4 +420,12 @@ label {
font-weight: 600;
line-height: 1.3;
}
.login-page--desktop .error {
font-size: 12px;
padding: 6px 10px;
border-radius: 4px;
background: rgba(255, 69, 58, 0.08);
border-left: 2px solid var(--danger, #ff453a);
}
</style>

View File

@@ -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/h5bg.webp';
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
type LoginMode = 'account' | 'phone';
const { t, locale } = useI18n();
@@ -85,11 +88,20 @@ 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>
<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 v-if="loginMode === 'account'" class="field">
<label>{{ t('auth.username') }}</label>
<input
@@ -98,6 +110,7 @@ function goRegister() {
required
autocomplete="username"
maxlength="32"
:disabled="loading"
:placeholder="t('auth.username_placeholder')"
/>
</div>
@@ -112,6 +125,7 @@ function goRegister() {
type="tel"
required
autocomplete="tel-national"
:disabled="loading"
:placeholder="t('auth.phone_local_placeholder')"
/>
</div>
@@ -119,28 +133,43 @@ function goRegister() {
<div class="field">
<label>{{ t('auth.password') }}</label>
<input v-model="password" class="field-input" type="password" required autocomplete="current-password" />
<input v-model="password" class="field-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>
<button type="button" class="btn-mode-switch" @click="switchLoginMode(loginMode === 'account' ? 'phone' : 'account')">
<button
type="button"
class="btn-mode-switch"
:disabled="loading"
@click="switchLoginMode(loginMode === 'account' ? 'phone' : 'account')"
>
{{ loginMode === 'account' ? t('auth.login_by_phone') : t('auth.login_by_account') }}
</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 type="button" class="btn-skip" @click="goRegister">
<button type="button" class="btn-skip" :disabled="loading" @click="goRegister">
{{ t('auth.go_register') }}
</button>
</form>
<button type="button" class="btn-skip-light" @click="continueBrowsing">
<button v-if="!props.desktop" type="button" class="btn-skip-light" @click="continueBrowsing">
{{ t('auth.continue_browsing') }}
</button>
</div>
@@ -170,13 +199,80 @@ function goRegister() {
background-repeat: no-repeat;
}
/* ===== Desktop mode: fields only (card shell in DesktopAuthLayout) ===== */
.login-page--desktop {
min-height: auto;
height: auto;
width: 100%;
align-items: stretch;
justify-content: flex-start;
padding: 0;
background-color: transparent;
overflow: visible;
}
.form-title {
margin: 0 0 8px;
font-size: 18px;
font-weight: 800;
color: var(--primary-light, #f0d875);
text-align: left;
letter-spacing: 0.04em;
}
.login-form {
position: relative;
width: 100%;
max-width: 320px;
display: flex;
flex-direction: column;
gap: 8px;
padding: 12px;
transition: border-color 0.25s ease, box-shadow 0.25s ease;
}
.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(8, 8, 8, 0.72);
backdrop-filter: blur(2px);
pointer-events: all;
}
.form-busy-text {
margin: 0;
font-size: 13px;
font-weight: 700;
color: rgba(240, 216, 117, 0.95);
letter-spacing: 0.04em;
}
.btn-inline-loading {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.login-page--desktop .login-form {
max-width: none;
width: 100%;
gap: 10px;
padding: 0;
border: none !important;
background: transparent !important;
box-shadow: none !important;
}
.field {
@@ -185,6 +281,10 @@ function goRegister() {
gap: 4px;
}
.login-page--desktop .field {
gap: 8px;
}
.field-input {
width: 100%;
height: 36px;
@@ -195,6 +295,7 @@ function goRegister() {
color: var(--text);
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.field-input:focus {
@@ -202,6 +303,23 @@ function goRegister() {
border-color: #555;
}
/* Desktop: visible focus ring per UX guideline */
.login-page--desktop .field-input {
height: 46px;
font-size: 14px;
border-radius: 4px;
padding: 0 14px;
}
.login-page--desktop .field-input:focus {
border-color: var(--border-gold, #D4AF37);
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.18);
}
.login-page--desktop .field-input:hover:not(:focus) {
border-color: rgba(255, 255, 255, 0.18);
}
.inline-row {
display: flex;
gap: 6px;
@@ -220,6 +338,11 @@ label {
letter-spacing: 0.04em;
}
.login-page--desktop label {
font-size: 13px;
letter-spacing: 0.03em;
}
.btn-mode-switch {
align-self: flex-start;
margin: -2px 0 0;
@@ -230,6 +353,11 @@ label {
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: color 0.2s ease;
}
.login-page--desktop .btn-mode-switch:hover {
color: rgba(240, 216, 117, 0.95);
}
.btn-forgot {
@@ -242,12 +370,17 @@ label {
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: color 0.2s ease;
}
.btn-forgot:active {
color: rgba(255, 255, 255, 0.75);
}
.login-page--desktop .btn-forgot:hover {
color: rgba(255, 255, 255, 0.75);
}
.btn-mode-switch:active {
color: rgba(240, 216, 117, 0.95);
}
@@ -259,6 +392,28 @@ label {
font-size: 14px;
font-weight: 800;
letter-spacing: 0.06em;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background-color 0.2s ease, border-color 0.2s ease, opacity 0.2s ease;
}
.login-page--desktop .btn-login {
margin-top: 10px;
padding: 14px 18px;
border-radius: 4px;
font-size: 15px;
height: 50px;
}
.login-page--desktop .btn-login:hover:not(:disabled) {
background: rgba(212, 175, 55, 0.1);
border-color: var(--border-gold, #D4AF37);
}
.login-page--desktop .btn-login:active:not(:disabled) {
transform: scale(0.98);
}
.btn-login:disabled {
@@ -266,6 +421,21 @@ label {
cursor: not-allowed;
}
.login-page--desktop .btn-login:disabled {
opacity: 0.55;
}
.btn-login.is-loading:disabled {
opacity: 1;
}
.btn-forgot:disabled,
.btn-mode-switch:disabled,
.btn-skip:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.btn-skip {
margin-top: 2px;
padding: 8px 14px;
@@ -275,12 +445,29 @@ label {
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: color 0.2s ease;
}
.btn-skip:active {
color: rgba(255, 255, 255, 0.75);
}
.login-page--desktop .btn-skip {
margin-top: 6px;
padding: 12px 16px;
border-radius: 4px;
border: 1px solid var(--border, #333);
background: rgba(20, 20, 20, 0.6);
font-size: 14px;
transition: border-color 0.2s ease, color 0.2s ease, background-color 0.2s ease;
}
.login-page--desktop .btn-skip:hover {
border-color: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.8);
background: rgba(30, 30, 30, 0.8);
}
.btn-skip-light {
position: absolute;
bottom: calc(20px + env(safe-area-inset-bottom));
@@ -304,6 +491,15 @@ label {
color: var(--danger);
font-size: 13px;
font-weight: 600;
animation: login-fade-in 0.25s ease;
}
.login-page--desktop .error {
font-size: 12px;
padding: 6px 10px;
border-radius: 4px;
background: rgba(255, 69, 58, 0.08);
border-left: 2px solid var(--danger, #FF453A);
}
.success {
@@ -312,5 +508,30 @@ label {
font-size: 12px;
font-weight: 600;
line-height: 1.4;
animation: login-fade-in 0.25s ease;
}
.login-page--desktop .success {
font-size: 12px;
padding: 6px 10px;
border-radius: 4px;
background: rgba(110, 231, 160, 0.08);
border-left: 2px solid #6ee7a0;
}
@keyframes login-fade-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>

View File

@@ -11,6 +11,8 @@ import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
import GoldSpinner from '../components/GoldSpinner.vue';
import loginBg from '../assets/images/h5bg.webp';
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
const { t, locale } = useI18n();
const { initFromUser } = useAppLocale();
const auth = useAuthStore();
@@ -101,11 +103,15 @@ 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>
<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>
@@ -203,7 +209,7 @@ const fieldError = () => {
{{ t('auth.have_account') }}
</button>
</form>
<button type="button" class="btn-skip-light" @click="continueBrowsing">
<button v-if="!props.desktop" type="button" class="btn-skip-light" @click="continueBrowsing">
{{ t('auth.continue_browsing') }}
</button>
</div>
@@ -233,6 +239,18 @@ const fieldError = () => {
background-repeat: no-repeat;
}
/* ===== Desktop mode: fields only (card shell in DesktopAuthLayout) ===== */
.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-form {
position: relative;
width: 100%;
@@ -243,6 +261,16 @@ const fieldError = () => {
padding: 12px;
}
.login-page--desktop .login-form {
max-width: none;
width: 100%;
gap: 10px;
padding: 0;
border: none !important;
background: transparent !important;
box-shadow: none !important;
}
.login-form.is-busy {
pointer-events: none;
}
@@ -285,12 +313,24 @@ const fieldError = () => {
text-align: center;
}
.login-page--desktop .form-title {
font-size: 18px;
margin: 0 0 8px;
text-align: left;
color: var(--primary-light, #f0d875);
letter-spacing: 0.04em;
}
.field {
display: flex;
flex-direction: column;
gap: 4px;
}
.login-page--desktop .field {
gap: 8px;
}
.field-optional {
opacity: 0.85;
}
@@ -302,12 +342,21 @@ label {
letter-spacing: 0.03em;
}
.login-page--desktop label {
font-size: 13px;
letter-spacing: 0.03em;
}
.optional-tag {
font-size: 10px;
font-weight: 500;
color: rgba(255, 255, 255, 0.4);
}
.login-page--desktop .optional-tag {
font-size: 12px;
}
.field-input {
width: 100%;
height: 36px;
@@ -318,6 +367,7 @@ label {
color: var(--text);
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.field-input::placeholder {
@@ -329,6 +379,23 @@ label {
border-color: #555;
}
/* Desktop: visible focus ring per UX guideline */
.login-page--desktop .field-input {
height: 46px;
font-size: 14px;
border-radius: 4px;
padding: 0 14px;
}
.login-page--desktop .field-input:focus {
border-color: var(--border-gold, #D4AF37);
box-shadow: 0 0 0 2px rgba(212, 175, 55, 0.18);
}
.login-page--desktop .field-input:hover:not(:focus) {
border-color: rgba(255, 255, 255, 0.18);
}
.inline-row {
display: flex;
gap: 6px;
@@ -352,6 +419,19 @@ label {
font-weight: 600;
white-space: nowrap;
cursor: pointer;
transition: border-color 0.2s ease, background-color 0.2s ease;
}
.login-page--desktop .btn-secondary {
height: 46px;
font-size: 13px;
border-radius: 4px;
padding: 0 14px;
}
.login-page--desktop .btn-secondary:hover:not(:disabled) {
border-color: rgba(255, 255, 255, 0.2);
background: rgba(30, 30, 30, 0.8);
}
.btn-secondary:disabled {
@@ -365,6 +445,28 @@ label {
border-radius: 6px;
font-size: 14px;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: background-color 0.2s ease, border-color 0.2s ease, opacity 0.2s ease;
}
.login-page--desktop .btn-login {
margin-top: 10px;
padding: 14px 18px;
border-radius: 4px;
font-size: 15px;
height: 50px;
}
.login-page--desktop .btn-login:hover:not(:disabled) {
background: rgba(212, 175, 55, 0.1);
border-color: var(--border-gold, #D4AF37);
}
.login-page--desktop .btn-login:active:not(:disabled) {
transform: scale(0.98);
}
.btn-login:disabled {
@@ -372,6 +474,10 @@ label {
cursor: not-allowed;
}
.login-page--desktop .btn-login:disabled {
opacity: 0.55;
}
.btn-login.is-loading:disabled {
opacity: 1;
}
@@ -385,12 +491,29 @@ label {
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: color 0.2s ease;
}
.btn-skip:active {
color: rgba(255, 255, 255, 0.75);
}
.login-page--desktop .btn-skip {
margin-top: 6px;
padding: 12px 16px;
border-radius: 4px;
border: 1px solid var(--border, #333);
background: rgba(20, 20, 20, 0.6);
font-size: 14px;
transition: border-color 0.2s ease, color 0.2s ease, background-color 0.2s ease;
}
.login-page--desktop .btn-skip:hover {
border-color: rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.8);
background: rgba(30, 30, 30, 0.8);
}
.btn-skip-light {
position: absolute;
bottom: calc(20px + env(safe-area-inset-bottom));
@@ -416,5 +539,30 @@ label {
font-size: 12px;
font-weight: 600;
line-height: 1.3;
animation: reg-fade-in 0.25s ease;
}
.login-page--desktop .error {
font-size: 12px;
padding: 6px 10px;
border-radius: 4px;
background: rgba(255, 69, 58, 0.08);
border-left: 2px solid var(--danger, #FF453A);
}
@keyframes reg-fade-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>

View File

@@ -1,99 +1,10 @@
<script setup lang="ts">
/**
* Desktop Forgot Password View
*/
import DesktopAuthLayout from '../../components/desktop/DesktopAuthLayout.vue';
import ForgotPasswordView from '../ForgotPasswordView.vue';
</script>
<template>
<div class="desktop-auth-shell">
<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);
}
.auth-brand {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0a0a0a 0%, #111 50%, #0d0d0d 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(212,175,55,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(212,175,55,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>

View File

@@ -150,7 +150,7 @@ onActivated(refreshUnreadCount);
width: 220px;
flex-shrink: 0;
border-right: 1px solid var(--desktop-border);
background: var(--desktop-sidebar-bg);
background: var(--desktop-sidebar-panel-bg);
padding: 10px;
}

View File

@@ -1,134 +1,10 @@
<script setup lang="ts">
/**
* Desktop Login View
* Reuses the existing LoginView form in a centered two-column auth layout.
*/
import DesktopAuthLayout from '../../components/desktop/DesktopAuthLayout.vue';
import LoginView from '../LoginView.vue';
</script>
<template>
<div class="desktop-auth-shell">
<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);
}
.auth-brand {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0a0a0a 0%, #111 50%, #0d0d0d 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(212,175,55,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(212,175,55,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(212,175,55,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>

View File

@@ -1,100 +1,10 @@
<script setup lang="ts">
/**
* Desktop Register View
* Reuses the existing RegisterView form in a centered desktop auth layout.
*/
import DesktopAuthLayout from '../../components/desktop/DesktopAuthLayout.vue';
import RegisterView from '../RegisterView.vue';
</script>
<template>
<div class="desktop-auth-shell">
<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);
}
.auth-brand {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #0a0a0a 0%, #111 50%, #0d0d0d 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(212,175,55,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(212,175,55,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>