Compare commits
31 Commits
4a76c4a1bc
...
theme-3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9abfbfcedd | ||
|
|
4ccb0cde6d | ||
|
|
e7e2b77906 | ||
| fee34feee8 | |||
| dd7ca2f4e4 | |||
| 233e9c891a | |||
| a379f9e522 | |||
| aa1a82e8e3 | |||
| ce28f1719b | |||
| b91aa74be1 | |||
| a9f067744d | |||
| 1239dd7ac2 | |||
| 5c1e06e873 | |||
| d314b3c94d | |||
| d8e08aaed0 | |||
| f92c5e8a59 | |||
| 2ed072f2f3 | |||
| 6b8dde5924 | |||
| 3eace49899 | |||
| e4d6d5c048 | |||
| 7b5a752b8f | |||
| 2487724692 | |||
| 533d748605 | |||
| 58f0876017 | |||
| 03c625f7b1 | |||
| 2b70ceb77b | |||
| 6881d325d5 | |||
| 0d430857c7 | |||
| 78322578d3 | |||
| d2c0b575b7 | |||
| 4a93fcfce0 |
@@ -4,7 +4,8 @@ JWT_SECRET=change-me-in-production-use-long-random-string
|
||||
JWT_PLAYER_EXPIRES=24h
|
||||
JWT_ADMIN_EXPIRES=2h
|
||||
JWT_AGENT_EXPIRES=8h
|
||||
PORT=3000
|
||||
# 本地开发建议 3100:Windows Hyper-V 常保留 2960–3059,3000 会 EACCES
|
||||
PORT=3100
|
||||
NODE_ENV=development
|
||||
UPLOAD_DIR=
|
||||
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
> 给 AI 编程助手(Cursor 等)看的项目速查手册,浓缩开发约定与易错点。
|
||||
> 人类日常开发请优先看 `README.md` 与 `docs/`。
|
||||
|
||||
如果你是AI请每次读了这个文件就在已有的次数上+1
|
||||
|
||||
当前被读次数: 3
|
||||
|
||||
## 目录
|
||||
|
||||
|
||||
@@ -162,7 +157,7 @@
|
||||
| --------- | ----------- | ------------------------------------------------------------------- |
|
||||
| `main` | 暗金主题 | 生产默认发版分支 |
|
||||
| `theme-2` | Pinnacle 蓝白 | 玩家 UI 与 main 分叉,**禁止** `git checkout main -- apps/player/...` 整文件覆盖 |
|
||||
| `theme-3` | 统一移动端视觉 | 同 theme-2 |
|
||||
| `theme-3` | Dusk Coral 珊瑚玻璃(移动 + PC双端) | 同 theme-2 |
|
||||
| `theme-4` | 海军蓝暗色极简 | 含悬浮客服等独有组件 |
|
||||
|
||||
|
||||
|
||||
2
apps/admin/components.d.ts
vendored
2
apps/admin/components.d.ts
vendored
@@ -37,8 +37,6 @@ declare module 'vue' {
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "node ../../scripts/ensure-port-free.mjs 3000 && nest start --watch",
|
||||
"dev": "node ../../scripts/ensure-port-free.mjs && nest start --watch",
|
||||
"start": "node dist/main",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
|
||||
@@ -190,7 +190,7 @@ export class PlayerController {
|
||||
this.content.listActive('BANNER', locale),
|
||||
this.content.listActiveAnnouncements(locale),
|
||||
this.matches.listPublished(locale, undefined, { includeMarkets: true }),
|
||||
this.matches.listUpcomingPublished(locale, { includeMarkets: true }),
|
||||
this.matches.listUpcomingPublished(locale),
|
||||
this.systemConfig.getInboxFeatureEnabled(),
|
||||
]);
|
||||
const timeZone = safeTimeZone(headerTimeZone);
|
||||
|
||||
@@ -1595,11 +1595,10 @@ export class MatchesService {
|
||||
/** 未来 N 天内开赛的已发布赛事(按开赛时间升序,不限 isHot) */
|
||||
async listUpcomingPublished(
|
||||
locale = 'en-US',
|
||||
options?: { limit?: number; days?: number; includeMarkets?: boolean },
|
||||
options?: { limit?: number; days?: number },
|
||||
) {
|
||||
const limit = options?.limit ?? 50;
|
||||
const days = options?.days ?? 3;
|
||||
const includeMarkets = options?.includeMarkets ?? false;
|
||||
const now = new Date();
|
||||
const end = new Date(now);
|
||||
end.setDate(end.getDate() + days);
|
||||
@@ -1626,14 +1625,14 @@ export class MatchesService {
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
score: true,
|
||||
markets: includeMarkets ? this.playerMarketInclude : this.playerMarketStatusInclude,
|
||||
markets: this.playerMarketInclude,
|
||||
},
|
||||
orderBy: [{ startTime: 'asc' }, { displayOrder: 'asc' }],
|
||||
take: limit,
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
matches.map((m) => this.enrichMatch(m, locale, { omitMarkets: !includeMarkets })),
|
||||
matches.map((m) => this.enrichMatch(m, locale, { omitMarkets: false })),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#001A33" />
|
||||
<meta name="theme-color" content="#0F2027" />
|
||||
<link rel="icon" type="image/png" href="/logo.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/logo.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<title>TheBet365</title>
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: #001A33; }
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: #F5F7FA; }
|
||||
#app-loading {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -18,7 +18,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #001A33;
|
||||
background: #F5F7FA;
|
||||
z-index: 9999;
|
||||
}
|
||||
#app-loading svg {
|
||||
@@ -34,11 +34,40 @@
|
||||
<body>
|
||||
<div id="app-loading">
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="24" cy="24" r="20" stroke="rgba(255,255,255,0.14)" stroke-width="3" fill="none" />
|
||||
<circle cx="24" cy="24" r="20" stroke="rgba(255,255,255,0.72)" stroke-width="3" fill="none" stroke-linecap="round" stroke-dasharray="31.4 94.2" />
|
||||
<defs>
|
||||
<linearGradient id="gs-track" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#003D6B" stop-opacity="0.18" />
|
||||
<stop offset="50%" stop-color="#005B9F" stop-opacity="0.25" />
|
||||
<stop offset="100%" stop-color="#003D6B" stop-opacity="0.18" />
|
||||
</linearGradient>
|
||||
<linearGradient id="gs-arc" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#E8F4FD" />
|
||||
<stop offset="22%" stop-color="#7CB9E8" />
|
||||
<stop offset="50%" stop-color="#005B9F" />
|
||||
<stop offset="78%" stop-color="#003D6B" />
|
||||
<stop offset="100%" stop-color="#002847" />
|
||||
</linearGradient>
|
||||
<filter id="gs-glow">
|
||||
<feGaussianBlur stdDeviation="1.2" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<radialGradient id="gs-dot" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#E8F4FD" />
|
||||
<stop offset="60%" stop-color="#7CB9E8" />
|
||||
<stop offset="100%" stop-color="#005B9F" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<circle cx="24" cy="24" r="20" stroke="url(#gs-track)" stroke-width="3" fill="none" />
|
||||
<circle cx="24" cy="24" r="20" stroke="url(#gs-arc)" stroke-width="3" fill="none" stroke-linecap="round" stroke-dasharray="31.4 94.2" filter="url(#gs-glow)" />
|
||||
<circle cx="24" cy="4" r="2.5" fill="url(#gs-dot)" filter="url(#gs-glow)">
|
||||
<animate attributeName="r" values="2.5;3.2;2.5" dur="1.2s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
44
apps/player/scripts/check-desktop-i18n.mjs
Normal file
44
apps/player/scripts/check-desktop-i18n.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
function flatten(obj, prefix = '') {
|
||||
const out = {};
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
const key = prefix ? `${prefix}.${k}` : k;
|
||||
if (v && typeof v === 'object' && !Array.isArray(v)) Object.assign(out, flatten(v, key));
|
||||
else out[key] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractKeysFromDir(dir) {
|
||||
const keys = new Set();
|
||||
const re = /\bt\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
||||
function walk(d) {
|
||||
for (const ent of fs.readdirSync(d, { withFileTypes: true })) {
|
||||
const p = path.join(d, ent.name);
|
||||
if (ent.isDirectory()) walk(p);
|
||||
else if (ent.name.endsWith('.vue')) {
|
||||
const src = fs.readFileSync(p, 'utf8');
|
||||
let m;
|
||||
while ((m = re.exec(src))) keys.add(m[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(dir);
|
||||
return keys;
|
||||
}
|
||||
|
||||
const zhMod = await import('../src/i18n/zh-CN.ts');
|
||||
const zh = flatten(zhMod.default);
|
||||
const dirs = ['src/views/desktop', 'src/components/desktop'].map((d) => path.join(root, d));
|
||||
const used = new Set();
|
||||
for (const d of dirs) extractKeysFromDir(d).forEach((k) => used.add(k));
|
||||
|
||||
const missing = [...used].filter((k) => !(k in zh)).sort();
|
||||
console.log(`Missing keys (${missing.length}):`);
|
||||
missing.forEach((k) => console.log(` ${k}`));
|
||||
@@ -1,19 +1,19 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 750 280" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="750" y2="280" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1A1A1A"/>
|
||||
<stop offset="0.5" stop-color="#252015"/>
|
||||
<stop offset="1" stop-color="#000000"/>
|
||||
<stop stop-color="#1A3038"/>
|
||||
<stop offset="0.5" stop-color="#203A43"/>
|
||||
<stop offset="1" stop-color="#0F2027"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="gold" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop stop-color="#FFFFFF"/>
|
||||
<stop offset="1" stop-color="rgba(255,255,255,0.72)"/>
|
||||
<stop stop-color="#FFB980"/>
|
||||
<stop offset="1" stop-color="#F4A261"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="750" height="280" fill="url(#bg)"/>
|
||||
<circle cx="620" cy="80" r="120" fill="#FFFFFF" opacity="0.06"/>
|
||||
<circle cx="680" cy="200" r="80" fill="#FFFFFF" opacity="0.08"/>
|
||||
<circle cx="620" cy="80" r="120" fill="#F4A261" opacity="0.06"/>
|
||||
<circle cx="680" cy="200" r="80" fill="#F4A261" opacity="0.08"/>
|
||||
<text x="40" y="110" font-family="Arial, sans-serif" font-size="42" font-weight="800" fill="url(#gold)">TheBet365</text>
|
||||
<text x="40" y="160" font-family="Arial, sans-serif" font-size="22" fill="#8E8E93">足球赛事火热进行中</text>
|
||||
<rect x="40" y="190" width="140" height="4" fill="#FFFFFF" opacity="0.5" rx="2"/>
|
||||
<text x="40" y="160" font-family="Arial, sans-serif" font-size="22" fill="#8FA3AD">足球赛事火热进行中</text>
|
||||
<rect x="40" y="190" width="140" height="4" fill="#F4A261" opacity="0.5" rx="2"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1018 B |
@@ -1,8 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="none" aria-hidden="true">
|
||||
<circle cx="60" cy="60" r="48" stroke="#FFFFFF" stroke-width="4" opacity="0.5"/>
|
||||
<circle cx="60" cy="60" r="22" stroke="rgba(255,255,255,0.72)" stroke-width="4"/>
|
||||
<path d="M60 8 L68 32 L60 44 L52 32 Z" fill="#FFFFFF"/>
|
||||
<path d="M60 112 L68 88 L60 76 L52 88 Z" fill="#FFFFFF"/>
|
||||
<path d="M8 60 L32 52 L44 60 L32 68 Z" fill="#FFFFFF"/>
|
||||
<path d="M112 60 L88 68 L76 60 L88 52 Z" fill="#FFFFFF"/>
|
||||
<circle cx="60" cy="60" r="48" stroke="#F4A261" stroke-width="4" opacity="0.5"/>
|
||||
<circle cx="60" cy="60" r="22" stroke="#FFB980" stroke-width="4"/>
|
||||
<path d="M60 8 L68 32 L60 44 L52 32 Z" fill="#F4A261"/>
|
||||
<path d="M60 112 L68 88 L60 76 L52 88 Z" fill="#F4A261"/>
|
||||
<path d="M8 60 L32 52 L44 60 L32 68 Z" fill="#F4A261"/>
|
||||
<path d="M112 60 L88 68 L76 60 L88 52 Z" fill="#F4A261"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 504 B After Width: | Height: | Size: 489 B |
BIN
apps/player/src/assets/images/h5bg2.webp
Normal file
BIN
apps/player/src/assets/images/h5bg2.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 697 KiB |
@@ -56,6 +56,7 @@ function goDetail() {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
@@ -69,7 +70,7 @@ function goDetail() {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-elevated);
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.marquee-bar.embedded .marquee-badge {
|
||||
@@ -87,9 +88,9 @@ function goDetail() {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--text);
|
||||
background: var(--surface-active);
|
||||
border: 1px solid var(--border-strong);
|
||||
color: #FFFFFF;
|
||||
background: var(--gradient-primary);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -112,7 +113,7 @@ function goDetail() {
|
||||
padding-right: 80px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -121,3 +122,5 @@ function goDetail() {
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@ const { visible, message } = useAppToast();
|
||||
max-width: min(90vw, 360px);
|
||||
padding: 10px 18px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(22, 22, 22, 0.96);
|
||||
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
|
||||
border: 1px solid var(--border-active);
|
||||
background: var(--bg-card-elevated);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow: var(--shadow-lg);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -44,12 +44,12 @@ const { visible, scrollToTop } = useBackToTop(toRef(props, 'scrollEl'));
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: var(--bg-body);
|
||||
color: var(--primary-light);
|
||||
box-shadow: var(--shadow);
|
||||
border: none;
|
||||
background: var(--gradient-primary);
|
||||
color: #FFFFFF;
|
||||
box-shadow: var(--shadow-glow), 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.back-top-btn.above-nav {
|
||||
@@ -57,7 +57,7 @@ const { visible, scrollToTop } = useBackToTop(toRef(props, 'scrollEl'));
|
||||
}
|
||||
|
||||
.back-top-btn:active {
|
||||
background: var(--surface-accent);
|
||||
opacity: 0.9;
|
||||
transform: scale(0.96);
|
||||
}
|
||||
|
||||
|
||||
@@ -125,14 +125,23 @@ onUnmounted(stopAutoPlay);
|
||||
class="slide"
|
||||
@click="onBannerClick(banner)"
|
||||
>
|
||||
<img
|
||||
v-if="imageUrl(banner)"
|
||||
:src="imageUrl(banner)"
|
||||
:alt="title(banner)"
|
||||
class="slide-img"
|
||||
:loading="i === 0 ? 'eager' : 'lazy'"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<template v-if="imageUrl(banner)">
|
||||
<img
|
||||
:src="imageUrl(banner)"
|
||||
alt=""
|
||||
class="slide-backdrop"
|
||||
aria-hidden="true"
|
||||
:loading="i === 0 ? 'eager' : 'lazy'"
|
||||
@error="onImgError"
|
||||
/>
|
||||
<img
|
||||
:src="imageUrl(banner)"
|
||||
:alt="title(banner)"
|
||||
class="slide-img"
|
||||
:loading="i === 0 ? 'eager' : 'lazy'"
|
||||
@error="onImgError"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="slide-fallback">{{ title(banner) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -159,65 +168,106 @@ onUnmounted(stopAutoPlay);
|
||||
<style scoped>
|
||||
.carousel {
|
||||
position: relative;
|
||||
margin: 0 -16px 14px;
|
||||
margin: -12px -16px 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.media {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.viewport {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: clamp(120px, 34vw, 150px);
|
||||
}
|
||||
|
||||
.track {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
transition: transform 0.45s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.slide {
|
||||
position: relative;
|
||||
flex: 0 0 100%;
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0B0F1A, #141B2D);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slide-img {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
.slide::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(15, 32, 39, 0.12), rgba(15, 32, 39, 0.03) 48%, rgba(15, 32, 39, 0.12));
|
||||
box-shadow: inset 0 -1px 0 rgba(244, 162, 97, 0.08);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slide-backdrop {
|
||||
position: absolute;
|
||||
inset: -18px;
|
||||
width: calc(100% + 36px);
|
||||
height: calc(100% + 36px);
|
||||
object-fit: cover;
|
||||
filter: blur(16px);
|
||||
opacity: 0.2;
|
||||
transform: scale(1.04);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.slide-img {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.slide-fallback {
|
||||
height: 180px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(212, 175, 55, 0.25), var(--secondary));
|
||||
background: linear-gradient(135deg, rgba(244, 162, 97, 0.08), #141B2D);
|
||||
font-size: 22px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.media .nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: var(--primary-light);
|
||||
background: var(--bg-glass);
|
||||
color: var(--primary);
|
||||
border: 1px solid var(--border);
|
||||
font-size: 22px;
|
||||
padding-bottom: 4px;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.nav.prev { left: 8px; }
|
||||
@@ -225,29 +275,30 @@ onUnmounted(stopAutoPlay);
|
||||
|
||||
.media .dots {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
bottom: 8px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
border: 2px solid transparent;
|
||||
background: rgba(148, 163, 184, 0.3);
|
||||
border: 1.5px solid transparent;
|
||||
padding: 0;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.dot.active {
|
||||
width: 22px;
|
||||
border-radius: 6px;
|
||||
background: var(--gradient-gold);
|
||||
border-color: var(--primary-light);
|
||||
box-shadow: none;
|
||||
width: 18px;
|
||||
border-radius: 5px;
|
||||
background: var(--gradient-primary);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 8px rgba(244, 162, 97, 0.4);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -65,9 +65,9 @@ function close() {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: var(--bg-card);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid #E5E7EB;
|
||||
background: #FFFFFF;
|
||||
color: #0F2027;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
@@ -79,7 +79,7 @@ function close() {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 205;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -93,11 +93,11 @@ function close() {
|
||||
max-width: 340px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: var(--radius);
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 8px;
|
||||
padding: 16px 14px 14px;
|
||||
box-shadow: var(--shadow), var(--glow-gold);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.close-x {
|
||||
@@ -107,15 +107,15 @@ function close() {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-muted);
|
||||
background: #F5F7FA;
|
||||
color: #6B7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: #0F2027;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
padding-right: 24px;
|
||||
@@ -126,14 +126,14 @@ function close() {
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
color: #6B7280;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.guide-body :deep(.flow-name) {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: #0F2027;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
@@ -152,21 +152,24 @@ function close() {
|
||||
|
||||
.guide-body :deep(.flow-block) {
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--border);
|
||||
border-top: 1px solid #E5E7EB;
|
||||
}
|
||||
|
||||
.guide-body :deep(.rules-link) {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--border);
|
||||
border-top: 1px dashed #E5E7EB;
|
||||
}
|
||||
|
||||
.btn-ok {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
background: #F4A261;
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -149,15 +149,16 @@ function goDetail() {
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bet-card:active {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@@ -178,9 +179,9 @@ function goDetail() {
|
||||
.cashback-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
background: var(--surface-accent);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
color: var(--accent);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
border: 1px solid rgba(244, 162, 97, 0.2);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
@@ -188,9 +189,9 @@ function goDetail() {
|
||||
.bet-type-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
background: var(--surface-accent);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
color: var(--primary);
|
||||
background: rgba(244, 162, 97, 0.06);
|
||||
border: 1px solid rgba(244, 162, 97, 0.15);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
letter-spacing: 0.02em;
|
||||
@@ -202,7 +203,6 @@ function goDetail() {
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.title {
|
||||
@@ -234,8 +234,8 @@ function goDetail() {
|
||||
.odds-badge {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
background: var(--surface-accent);
|
||||
color: var(--warning);
|
||||
background: rgba(255, 179, 0, 0.08);
|
||||
border-radius: 4px;
|
||||
padding: 1px 6px;
|
||||
flex-shrink: 0;
|
||||
@@ -262,13 +262,13 @@ function goDetail() {
|
||||
}
|
||||
|
||||
.return-amt.highlight { color: var(--success); }
|
||||
.return-amt.pending { color: var(--primary-light); }
|
||||
.return-amt.pending { color: var(--warning); }
|
||||
.return-amt.lost { color: var(--danger); }
|
||||
|
||||
.chevron {
|
||||
flex-shrink: 0;
|
||||
font-size: 18px;
|
||||
color: var(--neutral);
|
||||
color: var(--border);
|
||||
margin-left: 2px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -637,7 +637,7 @@ watch(
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
@@ -646,13 +646,13 @@ watch(
|
||||
.drawer {
|
||||
width: 100%;
|
||||
max-height: 88vh;
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-card);
|
||||
border-radius: 16px 16px 0 0;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
box-shadow: 0 -8px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.drawer-head {
|
||||
@@ -703,7 +703,7 @@ watch(
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: var(--bg-card);
|
||||
background: rgba(244, 162, 97, 0.04);
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
@@ -711,7 +711,7 @@ watch(
|
||||
|
||||
.balance-bar strong {
|
||||
justify-self: end;
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -737,7 +737,7 @@ watch(
|
||||
min-height: 38px;
|
||||
border-radius: 6px 6px 0 0;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-input);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
@@ -745,8 +745,8 @@ watch(
|
||||
|
||||
.slip-tab.active {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface-active);
|
||||
color: var(--text);
|
||||
background: rgba(244, 162, 97, 0.06);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.tab-count {
|
||||
@@ -758,7 +758,7 @@ watch(
|
||||
margin-left: 6px;
|
||||
border-radius: 9px;
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
color: #FFFFFF;
|
||||
font-size: 11px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -791,7 +791,7 @@ watch(
|
||||
|
||||
.slip-item--single {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface-active);
|
||||
background: rgba(244, 162, 97, 0.03);
|
||||
}
|
||||
|
||||
.item-main {
|
||||
@@ -852,12 +852,12 @@ watch(
|
||||
min-height: 48px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-strong);
|
||||
color: var(--text);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--primary);
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
box-shadow: none;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn-add-parlay span {
|
||||
@@ -868,7 +868,7 @@ watch(
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -905,16 +905,16 @@ watch(
|
||||
margin: 0 0 10px;
|
||||
padding: 10px 11px;
|
||||
border-radius: 6px;
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
border: 1px solid rgba(239, 68, 68, 0.25);
|
||||
color: rgba(239, 68, 68, 0.95);
|
||||
background: rgba(255, 77, 79, 0.12);
|
||||
border: 1px solid rgba(255, 77, 79, 0.24);
|
||||
color: var(--danger);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.parlay-meta {
|
||||
margin: 8px 2px 10px;
|
||||
color: var(--text-muted);
|
||||
color: var(--primary);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -939,7 +939,7 @@ watch(
|
||||
|
||||
.stake-head strong,
|
||||
.return strong {
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
}
|
||||
@@ -1024,10 +1024,11 @@ watch(
|
||||
|
||||
.quick-stakes button {
|
||||
min-height: 50px;
|
||||
background: var(--bg-card);
|
||||
background: var(--bg-card-elevated);
|
||||
color: var(--text);
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.return {
|
||||
@@ -1046,9 +1047,9 @@ watch(
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-card);
|
||||
border-top: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.drawer-foot--split {
|
||||
@@ -1059,8 +1060,8 @@ watch(
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
background: var(--gradient-primary);
|
||||
color: #FFFFFF;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
@@ -34,27 +34,27 @@ const stats = computed(() => {
|
||||
|
||||
<template>
|
||||
<div class="stats-panel">
|
||||
<div class="stats-row main">
|
||||
<div class="stat-cell">
|
||||
<div class="stats-row">
|
||||
<div class="stat-item">
|
||||
<span class="stat-val">{{ stats.total }}</span>
|
||||
<span class="stat-label">{{ t('history.stats_total') }}</span>
|
||||
</div>
|
||||
<div class="stat-cell won">
|
||||
<div class="stat-item won">
|
||||
<span class="stat-val">{{ stats.won }}</span>
|
||||
<span class="stat-label">{{ t('history.stats_won') }}</span>
|
||||
</div>
|
||||
<div class="stat-cell lost">
|
||||
<div class="stat-item lost">
|
||||
<span class="stat-val">{{ stats.lost }}</span>
|
||||
<span class="stat-label">{{ t('history.stats_lost') }}</span>
|
||||
</div>
|
||||
<div class="stat-cell pending">
|
||||
<div class="stat-item pending">
|
||||
<span class="stat-val">{{ stats.pending }}</span>
|
||||
<span class="stat-label">{{ t('history.stats_pending') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="stats.push > 0" class="push-row">
|
||||
<span class="stat-label">{{ t('history.stats_push') }}</span>
|
||||
<span class="stat-val push">{{ stats.push }}</span>
|
||||
<div class="stat-item push">
|
||||
<span class="stat-val">{{ stats.push }}</span>
|
||||
<span class="stat-label">{{ t('history.stats_push') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-bar" :class="{ empty: stats.total === 0 }" aria-hidden="true">
|
||||
<template v-if="stats.total > 0">
|
||||
@@ -64,14 +64,14 @@ const stats = computed(() => {
|
||||
<div v-if="stats.push > 0" class="bar-seg push" :style="{ flex: stats.push }" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="stats-row money">
|
||||
<div class="money-cell">
|
||||
<span class="stat-label">{{ t('history.stats_stake') }}</span>
|
||||
<div class="stats-row secondary">
|
||||
<div class="stat-item">
|
||||
<span class="stat-val small">{{ formatMoney(stats.totalStake, locale) }}</span>
|
||||
<span class="stat-label">{{ t('history.stats_stake') }}</span>
|
||||
</div>
|
||||
<div class="money-cell accent">
|
||||
<span class="stat-label">{{ t('history.stats_return') }}</span>
|
||||
<div class="stat-item">
|
||||
<span class="stat-val small gold">{{ formatMoney(stats.totalReturn, locale) }}</span>
|
||||
<span class="stat-label">{{ t('history.stats_return') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,133 +79,79 @@ const stats = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
.stats-panel {
|
||||
position: relative;
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
border-radius: var(--radius);
|
||||
padding: 8px 10px;
|
||||
box-shadow: var(--shadow-card-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
height: 1px;
|
||||
background: var(--gradient-card-shine);
|
||||
pointer-events: none;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 14px 12px 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stats-row.main {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
.stats-row.secondary {
|
||||
margin-bottom: 0;
|
||||
margin-top: 8px;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.stats-row.money {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.stat-cell,
|
||||
.money-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.money-cell {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.money-cell.accent {
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.push-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-val {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.stat-val.small {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stat-val.gold {
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.stat-val.push {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stat-cell.won .stat-val { color: var(--success); }
|
||||
.stat-cell.lost .stat-val { color: var(--danger); }
|
||||
.stat-cell.pending .stat-val { color: var(--primary-light); }
|
||||
.stat-item.won .stat-val { color: var(--success); }
|
||||
.stat-item.lost .stat-val { color: var(--danger); }
|
||||
.stat-item.pending .stat-val { color: var(--warning); }
|
||||
.stat-item.push .stat-val { color: var(--text-muted); }
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.03em;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
letter-spacing: 0.04em;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
height: 3px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
gap: 1px;
|
||||
margin-top: 6px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.stats-bar.empty {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.bar-seg {
|
||||
border-radius: 1px;
|
||||
border-radius: 2px;
|
||||
transition: flex 0.3s ease;
|
||||
}
|
||||
|
||||
.bar-seg.won { background: var(--success); }
|
||||
.bar-seg.lost { background: var(--danger); }
|
||||
.bar-seg.pending { background: var(--primary-light); }
|
||||
.bar-seg.push { background: var(--neutral); }
|
||||
.bar-seg.pending { background: var(--warning); }
|
||||
.bar-seg.push { background: var(--text-muted); }
|
||||
</style>
|
||||
|
||||
@@ -16,7 +16,7 @@ interface Particle {
|
||||
color: string; shape: number;
|
||||
}
|
||||
|
||||
const COLORS = ['#FFFFFF', '#FFD700', '#F0C040', '#C8A02E', '#E6C84C', '#B8960C'];
|
||||
const COLORS = ['#F4A261', '#FFB980', '#FFDDD2', '#F4A261', '#E76F51', '#FFDDD2'];
|
||||
let w = 0;
|
||||
let h = 0;
|
||||
|
||||
@@ -111,8 +111,8 @@ onUnmounted(stop);
|
||||
<canvas ref="canvasRef" class="confetti-canvas" />
|
||||
<div class="success-card">
|
||||
<svg class="check" viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle class="check-circle" cx="26" cy="26" r="24" fill="none" stroke="currentColor" stroke-width="3" />
|
||||
<path class="check-mark" d="M14 27l7 7 16-16" fill="none" stroke="currentColor" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle class="check-circle" cx="26" cy="26" r="24" fill="none" stroke="#10B981" stroke-width="3" />
|
||||
<path class="check-mark" d="M14 27l7 7 16-16" fill="none" stroke="#10B981" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<p class="success-text">{{ t('bet.place_success') }}</p>
|
||||
</div>
|
||||
@@ -124,8 +124,10 @@ onUnmounted(stop);
|
||||
.bet-success-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 300;
|
||||
background: rgba(0, 0, 0, 0.82);
|
||||
z-index: 5000;
|
||||
background: rgba(15, 32, 39, 0.88);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -151,7 +153,6 @@ onUnmounted(stop);
|
||||
.check {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.check-circle {
|
||||
@@ -167,11 +168,12 @@ onUnmounted(stop);
|
||||
}
|
||||
|
||||
.success-text {
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.04em;
|
||||
animation: text-up 0.4s 0.4s ease both;
|
||||
text-shadow: 0 0 20px rgba(244, 162, 97, 0.4);
|
||||
}
|
||||
|
||||
@keyframes card-pop {
|
||||
|
||||
@@ -2,28 +2,17 @@
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { playerAvatarUrl, randomAvatarKey } from '@thebet365/shared';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
import { usePlayerProfile } from '../composables/usePlayerProfile';
|
||||
|
||||
const props = withDefaults(defineProps<{ hideAvatar?: boolean }>(), { hideAvatar: false });
|
||||
|
||||
const { locale, t } = useI18n();
|
||||
const { profileRaw, refreshProfile, avatarUrl } = usePlayerProfile();
|
||||
const auth = useAuthStore();
|
||||
const { profileRaw, refreshProfile } = usePlayerProfile();
|
||||
const router = useRouter();
|
||||
const open = ref(false);
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
|
||||
const wallet = computed(() => profileRaw.value?.wallet ?? null);
|
||||
|
||||
const displayAvatarUrl = computed(() => {
|
||||
if (avatarUrl.value) return avatarUrl.value;
|
||||
const seed = auth.user?.username;
|
||||
return seed ? playerAvatarUrl(randomAvatarKey(seed)) : null;
|
||||
});
|
||||
|
||||
function amountValue(value: unknown): number {
|
||||
if (value == null) return 0;
|
||||
if (typeof value === 'number') return Number.isFinite(value) ? value : 0;
|
||||
@@ -85,11 +74,7 @@ onUnmounted(() => {
|
||||
<span class="chip-label">{{ t('wallet.cash_balance') }}</span>
|
||||
<span class="chip-amount">{{ available }}</span>
|
||||
</span>
|
||||
|
||||
<!-- Integrated Avatar image (hidden when hideAvatar is set, e.g. desktop top nav) -->
|
||||
<span v-if="!props.hideAvatar" class="chip-avatar-wrap">
|
||||
<img v-if="displayAvatarUrl" :src="displayAvatarUrl" alt="" class="chip-avatar-img" />
|
||||
</span>
|
||||
<span class="chevron" :class="{ open }">▾</span>
|
||||
</button>
|
||||
|
||||
<div v-if="open" class="cash-panel">
|
||||
@@ -125,15 +110,16 @@ onUnmounted(() => {
|
||||
.cash-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 4px;
|
||||
height: 36px;
|
||||
padding: 0 6px 0 10px;
|
||||
padding: 0 8px 0 8px;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-card);
|
||||
box-sizing: border-box;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.chip-body {
|
||||
@@ -145,8 +131,8 @@ onUnmounted(() => {
|
||||
|
||||
.chip-label {
|
||||
font-size: 8px;
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -157,32 +143,21 @@ onUnmounted(() => {
|
||||
|
||||
.chip-amount {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-weight: 800;
|
||||
color: var(--accent-light);
|
||||
white-space: nowrap;
|
||||
line-height: 1.15;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Integrated Avatar Styling */
|
||||
.chip-avatar-wrap {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-body);
|
||||
.chevron {
|
||||
font-size: 10px;
|
||||
color: var(--primary-light);
|
||||
transition: transform 0.2s;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.chip-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
.chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.cash-panel {
|
||||
@@ -192,12 +167,10 @@ onUnmounted(() => {
|
||||
width: min(260px, 78vw);
|
||||
padding: 12px 14px;
|
||||
z-index: 130;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--dropdown-bg);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-card-elevated);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.cash-panel::before {
|
||||
@@ -209,9 +182,9 @@ onUnmounted(() => {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
padding: 6px 0;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.panel-row.muted {
|
||||
@@ -219,8 +192,8 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.panel-row.highlight {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
color: var(--accent-light);
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -234,18 +207,18 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
padding: 8px 0;
|
||||
margin-top: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--primary);
|
||||
background: transparent;
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.panel-recharge-btn:active {
|
||||
opacity: 0.88;
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
|
||||
@@ -51,7 +51,7 @@ function onConfirm() {
|
||||
@click.self="close"
|
||||
>
|
||||
<div
|
||||
class="confirm-modal"
|
||||
class="confirm-modal glass-card"
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
:aria-labelledby="title ? 'confirm-dialog-title' : undefined"
|
||||
@@ -95,26 +95,22 @@ function onConfirm() {
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px));
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
background: rgba(15, 32, 39, 0.42);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.confirm-modal {
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
background: var(--dropdown-bg, #0A2540);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 12px;
|
||||
padding: 22px 18px 16px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.confirm-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
color: var(--primary-light);
|
||||
text-align: center;
|
||||
line-height: 1.35;
|
||||
}
|
||||
@@ -123,7 +119,7 @@ function onConfirm() {
|
||||
margin: 0 0 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-muted);
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -135,7 +131,7 @@ function onConfirm() {
|
||||
.confirm-btn {
|
||||
flex: 1;
|
||||
min-height: 44px;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -149,19 +145,21 @@ function onConfirm() {
|
||||
|
||||
.confirm-btn.cancel {
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
background: rgba(15, 32, 39, 0.45);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.confirm-btn.confirm {
|
||||
border: none;
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
background: var(--gradient-primary);
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 4px 14px rgba(231, 111, 81, 0.35);
|
||||
}
|
||||
|
||||
.confirm-btn.confirm.danger {
|
||||
background: rgba(239, 68, 68, 0.85);
|
||||
background: var(--danger);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 14px rgba(239, 68, 68, 0.35);
|
||||
}
|
||||
|
||||
.confirm-fade-enter-active,
|
||||
|
||||
@@ -46,6 +46,7 @@ const iframeSrc = computed(() => {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 -16px;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
@@ -54,6 +55,6 @@ const iframeSrc = computed(() => {
|
||||
width: 100%;
|
||||
min-height: calc(100dvh - 140px);
|
||||
border: 0;
|
||||
background: var(--bg-body);
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -216,10 +216,8 @@ onMounted(() => {
|
||||
position: absolute;
|
||||
bottom: 64px;
|
||||
right: 0;
|
||||
background: var(--dropdown-bg);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: #1c3742;
|
||||
border: 1px solid rgba(244, 162, 97, 0.25);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
@@ -244,7 +242,7 @@ onMounted(() => {
|
||||
bottom: -6px;
|
||||
right: 22px;
|
||||
border-width: 6px 6px 0 6px;
|
||||
border-color: var(--dropdown-bg) transparent transparent transparent;
|
||||
border-color: #1c3742 transparent transparent transparent;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -252,7 +250,7 @@ onMounted(() => {
|
||||
bottom: -7px;
|
||||
right: 21px;
|
||||
border-width: 7px 7px 0 7px;
|
||||
border-color: var(--border-gold-soft) transparent transparent transparent;
|
||||
border-color: rgba(244, 162, 97, 0.25) transparent transparent transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -274,8 +272,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
color: var(--primary-light);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
@@ -320,22 +318,21 @@ onMounted(() => {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
background: #0A3558;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, background 0.2s, border-color 0.2s;
|
||||
transition: transform 0.2s, background 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fab-btn:hover {
|
||||
transform: scale(1.05);
|
||||
background: #0F4068;
|
||||
border-color: rgba(255, 255, 255, 0.28);
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.fab-active {
|
||||
@@ -357,7 +354,7 @@ onMounted(() => {
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #0A3558;
|
||||
border: 2px solid var(--bg-card);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -368,10 +365,10 @@ onMounted(() => {
|
||||
width: 380px;
|
||||
height: 600px;
|
||||
max-height: calc(100vh - 120px);
|
||||
background: var(--dropdown-bg);
|
||||
border: 1px solid var(--border-strong);
|
||||
background: #1a323c;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -383,8 +380,8 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #001A33;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #162d36;
|
||||
height: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -401,7 +398,7 @@ onMounted(() => {
|
||||
|
||||
.popup-title .tab-badge {
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
color: var(--bg-body);
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
@@ -444,8 +441,7 @@ onMounted(() => {
|
||||
.popup-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
background: #0A2540;
|
||||
background: #1a323c;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -454,6 +450,7 @@ onMounted(() => {
|
||||
.panel-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* Transitions */
|
||||
|
||||
@@ -1,114 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
size?: number;
|
||||
|
||||
progress?: number;
|
||||
|
||||
active?: boolean;
|
||||
|
||||
}>();
|
||||
|
||||
|
||||
|
||||
const rotation = computed(() => (props.progress ?? 0) * 360);
|
||||
|
||||
const wrapperStyle = computed(() => {
|
||||
|
||||
if (props.active) return {};
|
||||
|
||||
return {
|
||||
|
||||
transform: `rotate(${rotation.value}deg)`,
|
||||
|
||||
transition: 'transform 0.05s linear',
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<svg
|
||||
|
||||
:class="['gold-spinner', { 'is-active': active }]"
|
||||
|
||||
:class="['spinner', { 'is-active': active }]"
|
||||
:width="size ?? 48"
|
||||
|
||||
:height="size ?? 48"
|
||||
|
||||
viewBox="0 0 48 48"
|
||||
|
||||
fill="none"
|
||||
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
:style="!active ? wrapperStyle : undefined"
|
||||
|
||||
>
|
||||
|
||||
<circle cx="24" cy="24" r="20" stroke="rgba(255,255,255,0.14)" stroke-width="3" fill="none" />
|
||||
|
||||
<defs>
|
||||
<linearGradient id="sp-track" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#F4A261" stop-opacity="0.15" />
|
||||
<stop offset="50%" stop-color="#FFB980" stop-opacity="0.22" />
|
||||
<stop offset="100%" stop-color="#F4A261" stop-opacity="0.15" />
|
||||
</linearGradient>
|
||||
<linearGradient id="sp-arc" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#FFDDD2" />
|
||||
<stop offset="22%" stop-color="#FFB980" />
|
||||
<stop offset="50%" stop-color="#F4A261" />
|
||||
<stop offset="78%" stop-color="#E76F51" />
|
||||
<stop offset="100%" stop-color="#F4A261" />
|
||||
</linearGradient>
|
||||
<filter id="sp-glow">
|
||||
<feGaussianBlur stdDeviation="1.2" result="blur" />
|
||||
<feMerge>
|
||||
<feMergeNode in="blur" />
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<radialGradient id="sp-dot" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#FFDDD2" />
|
||||
<stop offset="60%" stop-color="#FFB980" />
|
||||
<stop offset="100%" stop-color="#F4A261" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
<circle cx="24" cy="24" r="20" stroke="url(#sp-track)" stroke-width="3" fill="none" />
|
||||
<circle
|
||||
|
||||
cx="24"
|
||||
|
||||
cy="24"
|
||||
|
||||
r="20"
|
||||
|
||||
stroke="rgba(255,255,255,0.72)"
|
||||
|
||||
stroke="url(#sp-arc)"
|
||||
stroke-width="3"
|
||||
|
||||
fill="none"
|
||||
|
||||
stroke-linecap="round"
|
||||
|
||||
stroke-dasharray="31.4 94.2"
|
||||
|
||||
filter="url(#sp-glow)"
|
||||
/>
|
||||
|
||||
<circle cx="24" cy="4" r="2.5" fill="url(#sp-dot)" filter="url(#sp-glow)">
|
||||
<animate
|
||||
v-if="active"
|
||||
attributeName="r"
|
||||
values="2.5;3.2;2.5"
|
||||
dur="1.2s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
<circle v-if="active" cx="36" cy="13.5" r="1.8" fill="#FFDDD2" opacity="0.6">
|
||||
<animate attributeName="opacity" values="0.6;0.15;0.6" dur="0.9s" repeatCount="indefinite" begin="0.15s" />
|
||||
</circle>
|
||||
<circle v-if="active" cx="34" cy="34" r="1.5" fill="#FFB980" opacity="0.4">
|
||||
<animate attributeName="opacity" values="0.4;0.1;0.4" dur="1.1s" repeatCount="indefinite" begin="0.35s" />
|
||||
</circle>
|
||||
<circle v-if="active" cx="14" cy="34" r="1.5" fill="#FFB980" opacity="0.4">
|
||||
<animate attributeName="opacity" values="0.4;0.1;0.4" dur="1s" repeatCount="indefinite" begin="0.55s" />
|
||||
</circle>
|
||||
</svg>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.gold-spinner {
|
||||
|
||||
.spinner {
|
||||
display: block;
|
||||
|
||||
will-change: transform;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.gold-spinner.is-active {
|
||||
|
||||
animation: gs-spin 0.8s linear infinite;
|
||||
|
||||
.spinner.is-active {
|
||||
animation: sp-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes gs-spin {
|
||||
|
||||
@keyframes sp-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</style>
|
||||
@@ -130,7 +130,7 @@ watch(totalPages, () => {
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: rgba(201, 162, 39, 0.05);
|
||||
}
|
||||
|
||||
.header-title {
|
||||
@@ -158,8 +158,8 @@ watch(totalPages, () => {
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-hover);
|
||||
color: var(--primary-light);
|
||||
background: var(--secondary);
|
||||
color: var(--gold);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -228,7 +228,7 @@ watch(totalPages, () => {
|
||||
}
|
||||
|
||||
.chevron {
|
||||
color: var(--primary-light);
|
||||
color: var(--gold);
|
||||
font-size: 18px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="league-block" :class="{ expanded }">
|
||||
<section class="league-block">
|
||||
<button type="button" class="league-row" :aria-expanded="expanded" @click="emit('toggle')">
|
||||
<span class="toggle-icon" :class="{ open: expanded }" aria-hidden="true">
|
||||
<span class="toggle-mark">{{ expanded ? '−' : '+' }}</span>
|
||||
@@ -57,9 +57,9 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
<span class="league-info">
|
||||
<span class="league-title">*{{ leagueName }}</span>
|
||||
<span class="league-stats">
|
||||
<span class="stat-item">{{ totalCount }} {{ t('bet.match_unit') }}</span>
|
||||
<span v-if="liveCount > 0" class="stat-item stat-live">{{ liveCount }} {{ t('bet.status_live') }}</span>
|
||||
<span v-if="openCount > 0" class="stat-item stat-open">{{ openCount }} {{ t('bet.status_open') }}</span>
|
||||
<span class="stat-item">{{ totalCount }}{{ t('bet.match_unit') }}</span>
|
||||
<span v-if="liveCount > 0" class="stat-item stat-live">{{ liveCount }}{{ t('bet.status_live') }}</span>
|
||||
<span v-if="openCount > 0" class="stat-item stat-open">{{ openCount }}{{ t('bet.status_open') }}</span>
|
||||
</span>
|
||||
</span>
|
||||
<img
|
||||
@@ -88,26 +88,11 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
.league-block {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
border-radius: var(--radius);
|
||||
background: var(--gradient-card);
|
||||
box-shadow: var(--shadow-card-sm);
|
||||
margin-bottom: 10px;
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.league-block::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 8%;
|
||||
right: 8%;
|
||||
height: 1px;
|
||||
background: var(--gradient-card-shine);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.league-row {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -116,7 +101,7 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.02) 100%);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
text-align: left;
|
||||
@@ -127,8 +112,7 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
|
||||
opacity: 0.25;
|
||||
background: linear-gradient(135deg, rgba(244, 162, 97, 0.02) 0%, transparent 60%);
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -142,17 +126,17 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--primary);
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toggle-mark {
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-top: -1px;
|
||||
}
|
||||
@@ -167,7 +151,7 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
|
||||
.league-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
min-width: 0;
|
||||
@@ -187,7 +171,7 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
}
|
||||
|
||||
.stat-open {
|
||||
color: #6ee78a;
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.league-saishi {
|
||||
@@ -202,14 +186,15 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
||||
}
|
||||
|
||||
.match-panel {
|
||||
background: linear-gradient(180deg, rgba(0, 20, 40, 0.35) 0%, rgba(0, 26, 51, 0.2) 100%);
|
||||
padding: 12px 8px 8px;
|
||||
padding: 8px 0 4px;
|
||||
background: var(--bg-card-elevated);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.match-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -27,6 +27,6 @@ const src = computed(() => `/flags/${countryCode.value}.svg`);
|
||||
flex-shrink: 0;
|
||||
border-radius: 2px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 0 0 1px var(--border);
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -81,7 +81,7 @@ onUnmounted(() => {
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-card-elevated);
|
||||
color: var(--primary-light);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
@@ -89,6 +89,7 @@ onUnmounted(() => {
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.locale-switch.compact .locale-trigger {
|
||||
@@ -105,12 +106,10 @@ onUnmounted(() => {
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
list-style: none;
|
||||
background: var(--dropdown-bg);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--bg-card-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.locale-option {
|
||||
@@ -128,7 +127,7 @@ onUnmounted(() => {
|
||||
|
||||
.locale-option:hover,
|
||||
.locale-option.active {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,7 @@ function continueBrowsing() {
|
||||
<Teleport to="body">
|
||||
<Transition name="fade">
|
||||
<div v-if="auth.loginPromptVisible" class="login-overlay" @click.self="continueBrowsing">
|
||||
<div class="login-modal">
|
||||
<div class="login-modal auth-card">
|
||||
<button type="button" class="close-btn" :aria-label="t('auth.continue_browsing')" @click="continueBrowsing">
|
||||
✕
|
||||
</button>
|
||||
@@ -34,7 +34,7 @@ function continueBrowsing() {
|
||||
<p class="login-hint">{{ t('auth.login_hint') }}</p>
|
||||
|
||||
<div class="login-actions">
|
||||
<button type="button" class="login-submit" @click="goLogin">
|
||||
<button type="button" class="login-submit btn-primary" @click="goLogin">
|
||||
{{ t('auth.go_login') }}
|
||||
</button>
|
||||
<button type="button" class="login-secondary" @click="continueBrowsing">
|
||||
@@ -55,51 +55,54 @@ function continueBrowsing() {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
padding: 20px;
|
||||
background: rgba(15, 32, 39, 0.42);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.login-modal {
|
||||
position: relative;
|
||||
width: 90%;
|
||||
max-width: 360px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: var(--radius);
|
||||
padding: 28px 24px 24px;
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
padding: 28px 22px 22px;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 14px;
|
||||
top: 10px;
|
||||
right: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-size: 18px;
|
||||
color: var(--text-dim);
|
||||
font-size: 17px;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
padding: 6px;
|
||||
line-height: 1;
|
||||
border-radius: 6px;
|
||||
transition: color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
color: var(--text);
|
||||
.close-btn:active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 18px;
|
||||
font-size: 19px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
margin: 0 0 6px;
|
||||
margin: 0 0 8px;
|
||||
text-align: center;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.login-hint {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
margin: 0 0 24px;
|
||||
margin: 0 0 22px;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
@@ -109,47 +112,48 @@ function continueBrowsing() {
|
||||
}
|
||||
|
||||
.login-submit {
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
background: var(--gradient-accent);
|
||||
color: var(--text);
|
||||
min-height: 46px;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
min-height: 44px;
|
||||
transition: opacity 0.2s;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.login-submit:active {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.login-secondary {
|
||||
padding: 10px;
|
||||
padding: 11px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
background: rgba(15, 32, 39, 0.45);
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
min-height: 40px;
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
min-height: 42px;
|
||||
backdrop-filter: blur(8px);
|
||||
transition: color 0.2s, border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.login-secondary:active {
|
||||
color: var(--text);
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.25s ease;
|
||||
transition: opacity 0.22s ease;
|
||||
}
|
||||
|
||||
.fade-enter-active .login-modal,
|
||||
.fade-leave-active .login-modal {
|
||||
transition: transform 0.22s ease, opacity 0.22s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-from .login-modal,
|
||||
.fade-leave-to .login-modal {
|
||||
opacity: 0;
|
||||
transform: scale(0.96) translateY(8px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, getCurrentInstance } from 'vue';
|
||||
import { ref, computed, getCurrentInstance, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { teamFlagUrl } from '../utils/teamFlag';
|
||||
@@ -44,6 +44,13 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{ bet: [id: string] }>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const slip = useBetSlipStore();
|
||||
const { openAt, visible: popoverVisible, pendingItem, cancelClose, scheduleClose, isActiveForCard } =
|
||||
useDesktopBetPopover();
|
||||
|
||||
const cardRootId = `bet-card-${props.match.id}-${getCurrentInstance()?.uid ?? 0}`;
|
||||
const isCardEngaged = computed(() => isActiveForCard(cardRootId));
|
||||
|
||||
function formatKickoff(startTime: string) {
|
||||
return formatLocalMatchDateTime(startTime, locale.value, {
|
||||
@@ -78,22 +85,14 @@ const liveScoreText = computed(() => {
|
||||
return `${s.ftHome} - ${s.ftAway}`;
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const slip = useBetSlipStore();
|
||||
const { openAt, visible: popoverVisible, pendingItem, cancelClose, scheduleClose, isActiveForCard } = useDesktopBetPopover();
|
||||
|
||||
const cardRootId = `bet-card-${props.match.id}-${getCurrentInstance()?.uid ?? 0}`;
|
||||
|
||||
const isCardEngaged = computed(() => isActiveForCard(cardRootId));
|
||||
|
||||
function getHoveredSide() {
|
||||
if (!popoverVisible.value || !pendingItem.value) return '';
|
||||
if (!isActiveForCard(cardRootId)) return '';
|
||||
if (String(pendingItem.value.matchId) !== String(props.match.id)) return '';
|
||||
|
||||
|
||||
const selId = pendingItem.value.selectionId;
|
||||
for (const market of props.match.markets ?? []) {
|
||||
const sel = market.selections?.find((s: any) => s.id === selId);
|
||||
const sel = market.selections?.find((s: { id: string }) => s.id === selId);
|
||||
if (!sel) continue;
|
||||
const code = (sel.selectionCode || '').toUpperCase();
|
||||
if (code === 'HOME') return 'home';
|
||||
@@ -102,7 +101,7 @@ function getHoveredSide() {
|
||||
if (code === 'OVER') return 'home';
|
||||
if (code === 'UNDER') return 'away';
|
||||
if (code === 'ODD' || code === 'EVEN') return 'draw';
|
||||
const selIndex = market.selections?.findIndex((s: any) => s.id === selId) ?? -1;
|
||||
const selIndex = market.selections?.findIndex((s: { id: string }) => s.id === selId) ?? -1;
|
||||
if (selIndex === -1) continue;
|
||||
if (market.marketType === 'FT_1X2') {
|
||||
if (selIndex === 0) return 'home';
|
||||
@@ -116,24 +115,39 @@ function getHoveredSide() {
|
||||
return '';
|
||||
}
|
||||
|
||||
const activeMarketType = ref<string>('');
|
||||
function pickQuickBetMarkets(markets: any[] | undefined) {
|
||||
const supported = ['FT_1X2', 'FT_HANDICAP', 'FT_OVER_UNDER'];
|
||||
const byType = new Map<string, any>();
|
||||
for (const m of markets ?? []) {
|
||||
if (!supported.includes(m.marketType)) continue;
|
||||
if (!Array.isArray(m.selections) || m.selections.length === 0) continue;
|
||||
if (!byType.has(m.marketType)) byType.set(m.marketType, m);
|
||||
}
|
||||
return supported.map((type) => byType.get(type)).filter(Boolean);
|
||||
}
|
||||
|
||||
const activeMarketType = ref('');
|
||||
|
||||
watch(
|
||||
() => props.match.id,
|
||||
() => { activeMarketType.value = ''; },
|
||||
);
|
||||
|
||||
const currentMarketType = computed(() => {
|
||||
if (activeMarketType.value) return activeMarketType.value;
|
||||
const available = getAvailableMarketsForMatch(props.match);
|
||||
if (available.some((m: any) => m.marketType === 'FT_1X2')) {
|
||||
return 'FT_1X2';
|
||||
const available = pickQuickBetMarkets(props.match.markets);
|
||||
if (activeMarketType.value && available.some((m) => m.marketType === activeMarketType.value)) {
|
||||
return activeMarketType.value;
|
||||
}
|
||||
return available[0]?.marketType || 'FT_1X2';
|
||||
if (available.some((m) => m.marketType === 'FT_1X2')) return 'FT_1X2';
|
||||
return available[0]?.marketType ?? '';
|
||||
});
|
||||
|
||||
function isSelected(id: string) {
|
||||
return slip.isInSlip(id);
|
||||
}
|
||||
|
||||
function getAvailableMarketsForMatch(match: any) {
|
||||
const supported = ['FT_1X2', 'FT_HANDICAP', 'FT_OVER_UNDER'];
|
||||
return (match.markets ?? []).filter((m: any) => supported.includes(m.marketType));
|
||||
function getAvailableMarketsForMatch(match: typeof props.match) {
|
||||
return pickQuickBetMarkets(match.markets);
|
||||
}
|
||||
|
||||
function getMarketTabLabel(type: string) {
|
||||
@@ -143,37 +157,14 @@ function getMarketTabLabel(type: string) {
|
||||
if (type === 'FT_OVER_UNDER') return t('bet.market_ft_ou');
|
||||
return type;
|
||||
})();
|
||||
return raw.replace(/全场\s*|FT\s*|Penuh\s*/ig, '').trim();
|
||||
return raw.replace(/全场\s*|FT\s*|Penuh\s*/gi, '').trim();
|
||||
}
|
||||
|
||||
function getSelLabel(sel: any, marketType: string, lineValue?: string | number | null) {
|
||||
function getSelLabel(sel: { selectionCode?: string; selectionName: string }, _marketType: string, lineValue?: string | number | null) {
|
||||
return resolveSelectionLabel(t, sel.selectionCode || '', sel.selectionName || '', { lineValue });
|
||||
}
|
||||
|
||||
function handleOddsClick(match: any, market: any, selId: string, event?: MouseEvent) {
|
||||
if (isMarketLocked(match, market)) return;
|
||||
const sel = market.selections?.find((s: any) => s.id === selId);
|
||||
if (!sel || !event) return;
|
||||
|
||||
const item = {
|
||||
selectionId: sel.id,
|
||||
oddsVersion: String(sel.oddsVersion),
|
||||
matchId: match.id,
|
||||
matchName: `${match.homeTeamName} vs ${match.awayTeamName}`,
|
||||
marketId: market.id,
|
||||
marketName: market.marketDisplayName?.trim() || market.marketType || '',
|
||||
selectionName: getSelLabel(sel, market.marketType, market.lineValue),
|
||||
odds: parseFloat(sel.odds),
|
||||
marketType: market.marketType,
|
||||
lineValue: market.lineValue != null && market.lineValue !== '' ? parseFloat(String(market.lineValue)) : null,
|
||||
allowSingle: market.allowSingle,
|
||||
allowParlay: market.allowParlay,
|
||||
};
|
||||
|
||||
openAt(event, item);
|
||||
}
|
||||
|
||||
function isMarketLocked(match: any, market?: { status: string; allowSingle?: boolean; allowParlay?: boolean }) {
|
||||
function isMarketLocked(match: any, market?: { status?: string; allowSingle?: boolean; allowParlay?: boolean }) {
|
||||
if (!market) return true;
|
||||
if (match.bettingOpen === false) return true;
|
||||
const status = (market.status ?? 'OPEN').toUpperCase();
|
||||
@@ -183,7 +174,32 @@ function isMarketLocked(match: any, market?: { status: string; allowSingle?: boo
|
||||
|
||||
function getActiveMarket() {
|
||||
const type = currentMarketType.value;
|
||||
return props.match.markets?.find((m: any) => m.marketType === type);
|
||||
if (!type) return undefined;
|
||||
return pickQuickBetMarkets(props.match.markets).find((m) => m.marketType === type);
|
||||
}
|
||||
|
||||
function handleOddsClick(match: any, market: any, selId: string, event?: MouseEvent) {
|
||||
if (isMarketLocked(match, market)) return;
|
||||
const sel = market.selections?.find((s: { id: string }) => s.id === selId);
|
||||
if (!sel || !event) return;
|
||||
|
||||
openAt(event, {
|
||||
selectionId: sel.id,
|
||||
oddsVersion: String(sel.oddsVersion),
|
||||
matchId: match.id,
|
||||
matchName: `${match.homeTeamName} vs ${match.awayTeamName}`,
|
||||
marketId: market.id,
|
||||
marketName: market.marketDisplayName?.trim() || market.marketType || '',
|
||||
selectionName: getSelLabel(sel, market.marketType, market.lineValue),
|
||||
odds: parseFloat(sel.odds),
|
||||
marketType: market.marketType,
|
||||
lineValue:
|
||||
market.lineValue != null && market.lineValue !== ''
|
||||
? parseFloat(String(market.lineValue))
|
||||
: null,
|
||||
allowSingle: market.allowSingle,
|
||||
allowParlay: market.allowParlay,
|
||||
});
|
||||
}
|
||||
|
||||
function goMatchDetail() {
|
||||
@@ -200,12 +216,13 @@ function goMatchDetail() {
|
||||
'no-quick-bet': noQuickBet,
|
||||
'match-card--engaged': isCardEngaged,
|
||||
}"
|
||||
@click="emit('bet', match.id)"
|
||||
@click="goMatchDetail"
|
||||
@mouseenter="cancelClose"
|
||||
@mouseleave="scheduleClose()"
|
||||
>
|
||||
<span v-if="phase === 'open'" class="status-tag status-tag--open">{{ t('bet.status_open') }}</span>
|
||||
<span v-else-if="phase === 'settled'" class="status-tag status-tag--settled">{{ phaseLabel }}</span>
|
||||
<span v-else class="status-tag status-tag--pending">{{ phaseLabel }}</span>
|
||||
|
||||
<div class="teams-row" :class="getHoveredSide()">
|
||||
<div class="team">
|
||||
@@ -241,18 +258,23 @@ function goMatchDetail() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 静止状态:下注按钮 -->
|
||||
<button
|
||||
type="button"
|
||||
class="bet-btn"
|
||||
:class="bettingOpen ? 'btn-gold-outline' : 'bet-btn--view'"
|
||||
@click.stop="emit('bet', match.id)"
|
||||
@click.stop="goMatchDetail"
|
||||
>
|
||||
{{ bettingOpen ? t('bet.place_bet_short') : t('bet.view_match') }}
|
||||
</button>
|
||||
|
||||
<!-- Morphing Quick Bet panel (Desktop Only, disabled when noQuickBet=true) -->
|
||||
<div v-if="!noQuickBet" class="card-quick-bet" @click.stop>
|
||||
<div class="quick-bet-tabs" v-if="getAvailableMarketsForMatch(match).length">
|
||||
<!-- Hover 快速下注区 -->
|
||||
<div
|
||||
v-if="!noQuickBet && getAvailableMarketsForMatch(match).length"
|
||||
class="card-quick-bet"
|
||||
@click.stop
|
||||
>
|
||||
<div class="quick-bet-tabs">
|
||||
<button
|
||||
v-for="m in getAvailableMarketsForMatch(match)"
|
||||
:key="m.marketType"
|
||||
@@ -263,7 +285,7 @@ function goMatchDetail() {
|
||||
>
|
||||
{{ getMarketTabLabel(m.marketType) }}
|
||||
</button>
|
||||
<span class="quick-bet-all-link" @click="goMatchDetail">{{ t('bet.all') || '全部' }} →</span>
|
||||
<span class="quick-bet-all-link" @click="goMatchDetail">{{ t('bet.all') }} →</span>
|
||||
</div>
|
||||
|
||||
<div class="quick-bet-odds">
|
||||
@@ -273,13 +295,16 @@ function goMatchDetail() {
|
||||
compact
|
||||
:locked="isMarketLocked(match, getActiveMarket())"
|
||||
:line-value="getActiveMarket()?.lineValue"
|
||||
:selections="getActiveMarket()?.selections || []"
|
||||
:selections="(getActiveMarket()?.selections || []).map((s: any) => ({
|
||||
...s,
|
||||
odds: String(s.odds),
|
||||
}))"
|
||||
:is-selected="isSelected"
|
||||
@pick="(selId, ev) => handleOddsClick(match, getActiveMarket()!, selId, ev)"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="quick-bet-no-odds">
|
||||
{{ t('bet.no_market_odds') || '暂无盘口' }}
|
||||
{{ t('bet.no_market_odds') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -289,40 +314,31 @@ function goMatchDetail() {
|
||||
<style scoped>
|
||||
.match-card {
|
||||
position: relative;
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 12px 10px 10px;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 10px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
min-height: 130px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-card-sm);
|
||||
box-shadow: var(--shadow);
|
||||
transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s;
|
||||
}
|
||||
|
||||
.match-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 12%;
|
||||
right: 12%;
|
||||
height: 1px;
|
||||
background: var(--gradient-card-shine);
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(244, 162, 97, 0.03) 0%, transparent 50%, rgba(231, 111, 81, 0.03) 100%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.match-card--phase {
|
||||
background: linear-gradient(
|
||||
165deg,
|
||||
rgba(255, 255, 255, 0.07) 0%,
|
||||
rgba(255, 255, 255, 0.03) 30%,
|
||||
rgba(0, 20, 40, 0.55) 100%
|
||||
);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.teams-row {
|
||||
@@ -352,14 +368,13 @@ function goMatchDetail() {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
line-height: 1.3;
|
||||
line-height: 1.2;
|
||||
word-break: break-word;
|
||||
text-align: center;
|
||||
padding: 0 6px;
|
||||
padding: 0 4px;
|
||||
align-self: stretch;
|
||||
letter-spacing: -0.02em;
|
||||
transition: font-size 0.25s ease;
|
||||
}
|
||||
|
||||
@@ -368,19 +383,18 @@ function goMatchDetail() {
|
||||
}
|
||||
|
||||
.team-flag {
|
||||
width: 72px;
|
||||
height: 48px;
|
||||
width: 56px;
|
||||
height: 38px;
|
||||
object-fit: cover;
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
transition: transform 0.25s ease, opacity 0.2s ease, height 0.25s ease, width 0.25s ease, margin 0.25s ease;
|
||||
transition: transform 0.25s ease, opacity 0.2s ease, height 0.25s ease, width 0.25s ease;
|
||||
}
|
||||
|
||||
.team-flag.flag-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
object-fit: contain;
|
||||
transition: transform 0.25s ease, opacity 0.2s ease, height 0.25s ease, width 0.25s ease, margin 0.25s ease;
|
||||
}
|
||||
|
||||
.center-col {
|
||||
@@ -389,38 +403,28 @@ function goMatchDetail() {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
min-width: 56px;
|
||||
gap: 4px;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.kickoff {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.2;
|
||||
line-height: 1.15;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
max-width: 96px;
|
||||
max-width: 80px;
|
||||
overflow-wrap: anywhere;
|
||||
transition: opacity 0.2s ease, height 0.2s ease, margin 0.2s ease;
|
||||
}
|
||||
|
||||
.live-score {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1;
|
||||
letter-spacing: 0.02em;
|
||||
transition: font-size 0.25s ease;
|
||||
}
|
||||
|
||||
.live-score,
|
||||
.vs {
|
||||
font-size: 22px;
|
||||
font-size: 18px;
|
||||
font-weight: 900;
|
||||
color: var(--text);
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--primary);
|
||||
line-height: 1;
|
||||
transition: font-size 0.25s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
@@ -430,47 +434,52 @@ function goMatchDetail() {
|
||||
z-index: 3;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 0 6px 0 8px;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.status-tag--open {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(255, 179, 0, 0.15);
|
||||
color: #FFB300;
|
||||
border-bottom: 1px solid rgba(255, 179, 0, 0.3);
|
||||
border-left: 1px solid rgba(255, 179, 0, 0.3);
|
||||
}
|
||||
|
||||
.status-tag--settled {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-dim);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.status-tag--pending {
|
||||
background: rgba(255, 179, 0, 0.15);
|
||||
color: #FFB300;
|
||||
border-bottom: 1px solid rgba(255, 179, 0, 0.3);
|
||||
border-left: 1px solid rgba(255, 179, 0, 0.3);
|
||||
}
|
||||
|
||||
.bet-btn {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: auto;
|
||||
min-width: 80px;
|
||||
padding: 5px 24px;
|
||||
min-width: 72px;
|
||||
padding: 4px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.04em;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, height 0.25s ease, padding 0.25s ease, min-width 0.25s ease, min-height 0.25s ease, margin 0.25s ease;
|
||||
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.bet-btn--view {
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* --- Morphing Quick Bet panel --- */
|
||||
/* ── Hover 快速下注区 ── */
|
||||
.card-quick-bet {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
@@ -488,13 +497,14 @@ function goMatchDetail() {
|
||||
|
||||
@media (hover: hover) {
|
||||
.match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) {
|
||||
border-color: var(--border-gold);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border-color: var(--border-active);
|
||||
box-shadow: 0 4px 14px rgba(244, 162, 97, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .teams-row {
|
||||
transform: translateY(0);
|
||||
padding: 0 40px;
|
||||
padding: 0 28px;
|
||||
}
|
||||
|
||||
.match-card:not(.no-quick-bet):is(:hover, .match-card--engaged) .team {
|
||||
@@ -545,11 +555,12 @@ function goMatchDetail() {
|
||||
}
|
||||
|
||||
.match-card.no-quick-bet:hover {
|
||||
border-color: var(--border-gold);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border-color: var(--border-active);
|
||||
box-shadow: 0 4px 14px rgba(244, 162, 97, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Quick Bet Tabs ── */
|
||||
.quick-bet-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -570,13 +581,10 @@ function goMatchDetail() {
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.quick-bet-tab-btn:hover {
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.quick-bet-tab-btn:hover,
|
||||
.quick-bet-tab-btn.active {
|
||||
color: var(--primary-light);
|
||||
background: var(--border-gold-soft);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
}
|
||||
|
||||
.quick-bet-all-link {
|
||||
@@ -586,11 +594,6 @@ function goMatchDetail() {
|
||||
color: var(--primary-light);
|
||||
cursor: pointer;
|
||||
padding: 2px 4px;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.quick-bet-all-link:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.quick-bet-odds {
|
||||
@@ -603,7 +606,6 @@ function goMatchDetail() {
|
||||
.quick-bet-odds :deep(.odds-btn) {
|
||||
min-height: 26px !important;
|
||||
padding: 1px 4px !important;
|
||||
gap: 1px !important;
|
||||
}
|
||||
|
||||
.quick-bet-odds :deep(.odds) {
|
||||
@@ -619,34 +621,37 @@ function goMatchDetail() {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* --- VS Selection Highlight: scale selected team --- */
|
||||
/* ── 悬浮侧队高亮动效 ── */
|
||||
.teams-row .team {
|
||||
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease;
|
||||
}
|
||||
|
||||
.teams-row.home .team:first-child {
|
||||
transform: scale(1.08);
|
||||
filter: brightness(1.3) saturate(1.2);
|
||||
filter: brightness(1.08) saturate(1.15);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.teams-row.home .team:last-child {
|
||||
opacity: 0.35 !important;
|
||||
transform: scale(0.94);
|
||||
filter: grayscale(0.6) brightness(0.7);
|
||||
filter: grayscale(0.6) brightness(0.5);
|
||||
}
|
||||
|
||||
.teams-row.away .team:last-child {
|
||||
transform: scale(1.08);
|
||||
filter: brightness(1.3) saturate(1.2);
|
||||
filter: brightness(1.08) saturate(1.15);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.teams-row.away .team:first-child {
|
||||
opacity: 0.35 !important;
|
||||
transform: scale(0.94);
|
||||
filter: grayscale(0.6) brightness(0.7);
|
||||
filter: grayscale(0.6) brightness(0.5);
|
||||
}
|
||||
|
||||
.teams-row.draw .team {
|
||||
transform: scale(1.05);
|
||||
filter: brightness(1.15) saturate(1.1);
|
||||
filter: brightness(1.08) saturate(1.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||||
import GoldSpinner from './GoldSpinner.vue';
|
||||
@@ -12,6 +13,7 @@ const emit = defineEmits<{
|
||||
'click-message': [id: string];
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const auth = useAuthStore();
|
||||
const {
|
||||
@@ -196,10 +198,10 @@ onActivated(tryLoad);
|
||||
|
||||
.login-link {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 8px 14px;
|
||||
background: var(--surface-active);
|
||||
color: var(--text);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -223,7 +225,7 @@ onActivated(tryLoad);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
padding: 14px 0;
|
||||
padding: 14px 4px 14px 0;
|
||||
border: none;
|
||||
background: none;
|
||||
text-align: left;
|
||||
@@ -231,7 +233,7 @@ onActivated(tryLoad);
|
||||
}
|
||||
|
||||
.list-row.unread .title {
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -254,7 +256,7 @@ onActivated(tryLoad);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 15px;
|
||||
color: var(--text-muted);
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.35;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -272,8 +274,8 @@ onActivated(tryLoad);
|
||||
}
|
||||
|
||||
.status-badge.unread {
|
||||
color: var(--text);
|
||||
background: var(--surface-active);
|
||||
color: var(--primary);
|
||||
background: rgba(244, 162, 97, 0.12);
|
||||
}
|
||||
|
||||
.preview {
|
||||
@@ -310,7 +312,7 @@ onActivated(tryLoad);
|
||||
|
||||
.delete-btn:active:not(:disabled) {
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: rgba(239, 68, 68, 0.95);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.delete-btn svg {
|
||||
@@ -328,9 +330,9 @@ onActivated(tryLoad);
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,284 +0,0 @@
|
||||
<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(255,255,255,${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(255,255,255,${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(255,255,255,${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>
|
||||
@@ -131,13 +131,14 @@ onUnmounted(() => {
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.country-trigger:active {
|
||||
@@ -166,12 +167,10 @@ onUnmounted(() => {
|
||||
z-index: 60;
|
||||
width: min(260px, calc(100vw - 48px));
|
||||
padding: 4px;
|
||||
background: var(--dropdown-bg);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 6px;
|
||||
box-shadow: var(--shadow);
|
||||
background: var(--bg-card-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.country-search {
|
||||
@@ -180,19 +179,19 @@ onUnmounted(() => {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.country-search::placeholder {
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.country-search:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.country-menu {
|
||||
@@ -215,7 +214,7 @@ onUnmounted(() => {
|
||||
|
||||
.country-option:hover,
|
||||
.country-option.active {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.country-option .country-iso {
|
||||
|
||||
@@ -55,7 +55,7 @@ function confirm() {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 210;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -69,11 +69,11 @@ function confirm() {
|
||||
max-width: 360px;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-card-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 16px 14px 14px;
|
||||
box-shadow: var(--shadow), var(--glow-gold);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.close-x {
|
||||
@@ -84,7 +84,7 @@ function confirm() {
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
@@ -95,7 +95,7 @@ function confirm() {
|
||||
margin: 0 28px 12px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.actions {
|
||||
@@ -115,11 +115,13 @@ function confirm() {
|
||||
|
||||
.btn-cancel {
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
border: none;
|
||||
background: var(--gradient-primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -88,14 +88,14 @@ function select(key: string) {
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.picker-search:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.picker-grid {
|
||||
@@ -115,15 +115,15 @@ function select(key: string) {
|
||||
padding: 8px 4px 6px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-card);
|
||||
cursor: pointer;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.picker-item.active {
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35);
|
||||
background: var(--surface-accent);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 1px rgba(244, 162, 97, 0.18);
|
||||
background: rgba(244, 162, 97, 0.04);
|
||||
}
|
||||
|
||||
.picker-photo {
|
||||
@@ -132,7 +132,7 @@ function select(key: string) {
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.picker-name {
|
||||
|
||||
@@ -48,8 +48,8 @@ function drawCaptcha() {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const bg = cssVar('--captcha-canvas-bg', '#0d0d0d');
|
||||
const charBase = cssVar('--captcha-char', '#f0d875');
|
||||
const bg = cssVar('--captcha-canvas-bg', '#162D36');
|
||||
const charBase = cssVar('--captcha-char', '#F4A261');
|
||||
const noiseRgb = cssVar('--captcha-noise-rgb', '255, 255, 255');
|
||||
|
||||
ctx.fillStyle = bg;
|
||||
@@ -79,7 +79,7 @@ function drawCaptcha() {
|
||||
ctx.translate(x, y);
|
||||
ctx.rotate((Math.random() - 0.5) * 0.35);
|
||||
ctx.font = `bold ${15 + Math.random() * 4}px 'Courier New', monospace`;
|
||||
ctx.fillStyle = withAlpha(charBase, 0.85 + Math.random() * 0.15);
|
||||
ctx.fillStyle = `rgba(240, 216, 117, ${0.85 + Math.random() * 0.15})`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(code.value[i], 0, 0);
|
||||
@@ -161,13 +161,13 @@ defineExpose({ validate, refresh });
|
||||
align-items: stretch;
|
||||
height: 36px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-body);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-input);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.captcha-row:focus-within {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.hp-field {
|
||||
@@ -195,7 +195,7 @@ defineExpose({ validate, refresh });
|
||||
}
|
||||
|
||||
.captcha-input::placeholder {
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: none;
|
||||
|
||||
@@ -1,517 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onUnmounted, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { useAppLocale } from '../composables/useAppLocale';
|
||||
import LocaleFlag from './LocaleFlag.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void;
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const { locales, setLocale } = useAppLocale();
|
||||
|
||||
const showRulesModal = ref(false);
|
||||
const showLanguageModal = ref(false);
|
||||
|
||||
function close() {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
|
||||
async function changeLocale(code: string) {
|
||||
await setLocale(code);
|
||||
showLanguageModal.value = false;
|
||||
}
|
||||
|
||||
function goEdit() {
|
||||
close();
|
||||
router.push('/profile/edit');
|
||||
}
|
||||
|
||||
function logout() {
|
||||
close();
|
||||
auth.logout();
|
||||
router.push('/');
|
||||
}
|
||||
|
||||
const isAnyOpen = computed(() => props.modelValue || showRulesModal.value || showLanguageModal.value);
|
||||
|
||||
watch(
|
||||
isAnyOpen,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-drawer-wrapper" :class="{ open: modelValue }">
|
||||
<div class="settings-drawer-backdrop" @click="close" />
|
||||
|
||||
<div class="settings-drawer-panel">
|
||||
<!-- Header -->
|
||||
<div class="drawer-header">
|
||||
<h2 class="drawer-title">{{ t('profile.settings') }}</h2>
|
||||
<button type="button" class="drawer-close-btn" @click="close" :aria-label="t('common.cancel')">
|
||||
<svg class="close-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="drawer-content">
|
||||
<section class="settings-group">
|
||||
<!-- Modify Profile -->
|
||||
<button type="button" class="settings-cell" @click="goEdit">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<circle cx="12" cy="8" r="3.5" />
|
||||
<path d="M5 20c0-3.5 3.1-6 7-6s7 2.5 7 6" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('profile.edit') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</button>
|
||||
|
||||
<!-- Betting Rules -->
|
||||
<button type="button" class="settings-cell" @click="showRulesModal = true">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path d="M7 4h10v16H7z" />
|
||||
<path d="M10 8h6M10 12h6M10 16h4" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('profile.rules_title') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</button>
|
||||
|
||||
<!-- Language Selector -->
|
||||
<button type="button" class="settings-cell" @click="showLanguageModal = true">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M3 12h18M12 3c2.5 2.8 4 6 4 9s-1.5 6.2-4 9M12 3c-2.5 2.8-4 6-4 9s1.5 6.2 4 9" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('profile.language') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- Logout Button -->
|
||||
<button type="button" class="logout-btn" @click="logout">
|
||||
{{ t('auth.logout') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Betting Rules Modal -->
|
||||
<Teleport to="body">
|
||||
<Transition name="modal-fade">
|
||||
<div v-if="showRulesModal" class="modal-overlay" @click.self="showRulesModal = false">
|
||||
<div class="modal-card">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{ t('profile.rules_title') }}</h3>
|
||||
<button type="button" class="modal-close-btn" @click="showRulesModal = false" :aria-label="t('common.cancel')">
|
||||
<svg class="close-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body rules-modal-body">
|
||||
<p>{{ t('profile.rules_p1') }}</p>
|
||||
<p>{{ t('profile.rules_p2') }}</p>
|
||||
<p>{{ t('profile.rules_p3') }}</p>
|
||||
<p>{{ t('profile.rules_p4') }}</p>
|
||||
<p>{{ t('profile.rules_p5') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- Language Selector Modal -->
|
||||
<Teleport to="body">
|
||||
<Transition name="modal-fade">
|
||||
<div v-if="showLanguageModal" class="modal-overlay" @click.self="showLanguageModal = false">
|
||||
<div class="modal-card modal-card--small">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">{{ t('profile.language') }}</h3>
|
||||
<button type="button" class="modal-close-btn" @click="showLanguageModal = false" :aria-label="t('common.cancel')">
|
||||
<svg class="close-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="lang-list">
|
||||
<button
|
||||
v-for="item in locales"
|
||||
:key="item.code"
|
||||
type="button"
|
||||
class="lang-item"
|
||||
:class="{ active: locale === item.code }"
|
||||
@click="changeLocale(item.code)"
|
||||
>
|
||||
<div class="lang-item-main">
|
||||
<LocaleFlag :locale="item.code" :size="16" />
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
<svg v-if="locale === item.code" class="check-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-drawer-wrapper {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 150;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
transition: visibility 0.3s ease;
|
||||
}
|
||||
|
||||
.settings-drawer-wrapper.open {
|
||||
pointer-events: auto;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.settings-drawer-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.settings-drawer-wrapper.open .settings-drawer-backdrop {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settings-drawer-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 70%;
|
||||
background: #001a33;
|
||||
border-left: 1px solid var(--border);
|
||||
box-shadow: -4px 0 24px rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-drawer-wrapper.open .settings-drawer-panel {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--dropdown-bg);
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.drawer-close-btn {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.drawer-close-btn:active {
|
||||
background: var(--surface-active);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.drawer-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-card-sm);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.settings-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
background: none;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-cell:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.settings-cell:active {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.cell-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cell-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.cell-label {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.cell-chevron {
|
||||
color: var(--text-muted);
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
font-weight: 300;
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
color: var(--danger);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
transition: background 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.logout-btn:active {
|
||||
background: rgba(255, 69, 58, 0.08);
|
||||
}
|
||||
|
||||
/* Modal Overlays & Cards */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
padding-bottom: calc(20px + env(safe-area-inset-bottom, 0px));
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
width: 100%;
|
||||
max-width: 340px;
|
||||
background: var(--dropdown-bg, #0A2540);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: 12px;
|
||||
padding: 20px 18px 22px;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-card--small {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
padding: 0;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-close-btn:active {
|
||||
background: var(--surface-active);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.rules-modal-body {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-muted);
|
||||
overflow-y: auto;
|
||||
max-height: 50vh;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.rules-modal-body p {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.rules-modal-body p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Language List inside Modal */
|
||||
.lang-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.lang-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.lang-item:active {
|
||||
background: var(--surface-active);
|
||||
}
|
||||
|
||||
.lang-item.active {
|
||||
border-color: var(--primary);
|
||||
color: var(--text);
|
||||
background: var(--surface-accent);
|
||||
}
|
||||
|
||||
.lang-item-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--success, #34C759);
|
||||
}
|
||||
|
||||
/* Modal Transitions */
|
||||
.modal-fade-enter-active,
|
||||
.modal-fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-fade-enter-active .modal-card,
|
||||
.modal-fade-leave-active .modal-card {
|
||||
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.modal-fade-enter-from,
|
||||
.modal-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-fade-enter-from .modal-card,
|
||||
.modal-fade-leave-to .modal-card {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
</style>
|
||||
@@ -23,7 +23,7 @@ withDefaults(
|
||||
letter-spacing: 0.06em;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
opacity: 0.2;
|
||||
opacity: 0.15;
|
||||
max-width: 92%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -43,11 +43,11 @@ withDefaults(
|
||||
}
|
||||
|
||||
.status-watermark.won {
|
||||
color: var(--success);
|
||||
color: rgba(16, 185, 129, 0.12);
|
||||
}
|
||||
|
||||
.status-watermark.lost {
|
||||
color: var(--danger);
|
||||
color: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.status-watermark.push {
|
||||
@@ -55,14 +55,14 @@ withDefaults(
|
||||
}
|
||||
|
||||
.status-watermark.pending {
|
||||
color: var(--primary-light);
|
||||
color: rgba(255, 179, 0, 0.12);
|
||||
}
|
||||
|
||||
.status-watermark.closed {
|
||||
color: var(--primary);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.status-watermark.settled {
|
||||
color: var(--text-muted);
|
||||
color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@ const props = withDefaults(
|
||||
teamCode?: string;
|
||||
teamName?: string;
|
||||
logoUrl?: string | null;
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}>(),
|
||||
{ size: 'md' },
|
||||
);
|
||||
@@ -62,7 +62,7 @@ watch(
|
||||
/* 队徽(非国旗):圆角 + 投影 */
|
||||
.team-emblem--logo {
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.team-emblem--sm {
|
||||
@@ -80,11 +80,6 @@ watch(
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.team-emblem--xl {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
/* 国旗:横向比例 + 铺满 */
|
||||
.team-emblem:not(.team-emblem--logo) {
|
||||
object-fit: cover;
|
||||
@@ -107,11 +102,6 @@ watch(
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.team-emblem--xl:not(.team-emblem--logo) {
|
||||
width: 80px;
|
||||
height: 54px;
|
||||
}
|
||||
|
||||
/* 队徽:正方形容器 + 完整显示 */
|
||||
.team-emblem--logo {
|
||||
object-fit: contain;
|
||||
@@ -139,8 +129,4 @@ watch(
|
||||
.team-emblem--lg.team-emblem--placeholder {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.team-emblem--xl.team-emblem--placeholder {
|
||||
font-size: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -85,8 +85,8 @@ function logout() {
|
||||
height: 36px;
|
||||
box-sizing: border-box;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: var(--bg-card);
|
||||
border: 2px solid var(--primary-light);
|
||||
background: linear-gradient(145deg, var(--primary-dark), var(--primary-light));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -94,6 +94,7 @@ function logout() {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
box-shadow: 0 0 12px rgba(244, 162, 97, 0.3);
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
@@ -106,7 +107,7 @@ function logout() {
|
||||
.avatar-letter {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.avatar-menu {
|
||||
@@ -115,12 +116,12 @@ function logout() {
|
||||
right: 0;
|
||||
min-width: 168px;
|
||||
padding: 8px 0;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--dropdown-bg);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-card-elevated);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
z-index: 140;
|
||||
}
|
||||
|
||||
@@ -144,7 +145,7 @@ function logout() {
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.menu-item.recharge {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}>(),
|
||||
{
|
||||
size: 'md',
|
||||
},
|
||||
);
|
||||
|
||||
const sizeClass = computed(() => `vs-badge--${props.size}`);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
class="vs-badge"
|
||||
:class="sizeClass"
|
||||
role="img"
|
||||
aria-label="VS"
|
||||
>
|
||||
VS
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.vs-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
||||
font-weight: 700;
|
||||
font-variant: small-caps;
|
||||
letter-spacing: 0.1em;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
line-height: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vs-badge--sm {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.11em;
|
||||
}
|
||||
|
||||
.vs-badge--md {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.12em;
|
||||
}
|
||||
|
||||
.vs-badge--lg {
|
||||
font-size: 22px;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,171 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { RouterLink } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
|
||||
const props = defineProps<{
|
||||
availableBalance: unknown;
|
||||
cashbackTotal?: string;
|
||||
frozenBalance?: unknown;
|
||||
compact?: boolean;
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const balanceDisplay = computed(() => formatMoney(props.availableBalance, locale.value));
|
||||
|
||||
const balanceSizeClass = computed(() => {
|
||||
const len = balanceDisplay.value.length;
|
||||
if (len <= 10) return 'wallet-balance--xl';
|
||||
if (len <= 13) return 'wallet-balance--lg';
|
||||
if (len <= 16) return 'wallet-balance--md';
|
||||
if (len <= 19) return 'wallet-balance--sm';
|
||||
return 'wallet-balance--xs';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="wallet-card" :class="{ 'wallet-card--compact': compact }">
|
||||
<div class="wallet-card-head">
|
||||
<span class="wallet-kicker">{{ t('wallet.cash_balance') }}</span>
|
||||
<RouterLink to="/wallet/recharge" class="wallet-recharge">
|
||||
{{ t('recharge.title') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<p class="wallet-balance" :class="balanceSizeClass">{{ balanceDisplay }}</p>
|
||||
|
||||
<div class="wallet-metrics">
|
||||
<div class="wallet-metric">
|
||||
<span class="wallet-metric-label">{{ t('wallet.stats_cashback') }}</span>
|
||||
<span class="wallet-metric-value">{{ formatMoney(cashbackTotal ?? '0', locale) }}</span>
|
||||
</div>
|
||||
<div class="wallet-metric">
|
||||
<span class="wallet-metric-label">{{ t('wallet.unsettled') }}</span>
|
||||
<span class="wallet-metric-value">{{ formatMoney(frozenBalance, locale) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.wallet-card {
|
||||
position: relative;
|
||||
margin-bottom: var(--space-section);
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: var(--gradient-card);
|
||||
box-shadow: var(--shadow-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wallet-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
height: 1px;
|
||||
background: var(--gradient-card-shine);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wallet-card--compact {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.wallet-card-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wallet-kicker {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.wallet-recharge {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.wallet-recharge:active {
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.wallet-balance {
|
||||
margin: 0 0 10px;
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.03em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--text);
|
||||
line-height: 1.08;
|
||||
}
|
||||
|
||||
.wallet-balance--xl { font-size: 34px; }
|
||||
.wallet-balance--lg { font-size: 30px; }
|
||||
.wallet-balance--md { font-size: 26px; }
|
||||
.wallet-balance--sm { font-size: 22px; }
|
||||
.wallet-balance--xs { font-size: 19px; }
|
||||
|
||||
.wallet-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.wallet-metric {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.wallet-metric:not(:first-child) {
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.wallet-metric:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.wallet-metric:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.wallet-metric-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-dim);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.wallet-metric-value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,264 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { formatMoneyCompact, parseAmount } from '../utils/localeDisplay';
|
||||
|
||||
|
||||
|
||||
interface Transaction {
|
||||
|
||||
transactionType: string;
|
||||
|
||||
amount: string;
|
||||
|
||||
frozenBefore?: string;
|
||||
|
||||
frozenAfter?: string;
|
||||
|
||||
createdAt: string;
|
||||
|
||||
transactionId?: string;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
|
||||
items: Transaction[];
|
||||
|
||||
cashbackTotal?: string;
|
||||
|
||||
}>(), {
|
||||
|
||||
cashbackTotal: '0',
|
||||
|
||||
});
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
|
||||
|
||||
const CASHBACK_TYPES = new Set(['CASHBACK', 'CASHBACK_DEPOSIT']);
|
||||
|
||||
|
||||
|
||||
const stats = computed(() => {
|
||||
|
||||
let income = 0;
|
||||
|
||||
let expense = 0;
|
||||
|
||||
let cashback = 0;
|
||||
|
||||
|
||||
|
||||
for (const tx of props.items) {
|
||||
|
||||
const amt = parseAmount(tx.amount);
|
||||
|
||||
const isCb = CASHBACK_TYPES.has(tx.transactionType.toUpperCase());
|
||||
|
||||
if (isCb) {
|
||||
|
||||
cashback += Math.abs(amt);
|
||||
|
||||
}
|
||||
|
||||
if (amt >= 0) income += amt;
|
||||
|
||||
else expense += Math.abs(amt);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Use server-side cashback total if provided (more accurate across all pages)
|
||||
if (props.cashbackTotal !== '0') {
|
||||
|
||||
cashback = Math.abs(parseAmount(props.cashbackTotal));
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const net = income - expense;
|
||||
|
||||
|
||||
|
||||
return { income, expense, net, cashback };
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<div class="wallet-stats-panel">
|
||||
|
||||
<div class="stats-grid">
|
||||
|
||||
<div class="stat-cell">
|
||||
|
||||
<div class="stats-row">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('wallet.stats_income') }}</span>
|
||||
|
||||
<span class="stat-val income">{{ formatMoneyCompact(stats.income, locale) }}</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="stat-cell">
|
||||
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('wallet.stats_expense') }}</span>
|
||||
|
||||
<span class="stat-val expense">{{ formatMoneyCompact(stats.expense, locale) }}</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="stat-cell">
|
||||
|
||||
<span class="stat-label">{{ t('wallet.stats_net') }}</span>
|
||||
|
||||
<span class="stat-val" :class="stats.net >= 0 ? 'income' : 'muted'">
|
||||
|
||||
{{ formatMoneyCompact(stats.net, locale) }}
|
||||
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="stat-cell">
|
||||
|
||||
<span class="stat-label">{{ t('wallet.stats_cashback') }}</span>
|
||||
|
||||
<span class="stat-val">{{ formatMoneyCompact(stats.cashback, locale) }}</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="stats-divider" />
|
||||
<div class="stats-row">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('wallet.stats_net') }}</span>
|
||||
<span class="stat-val" :class="stats.net >= 0 ? 'income' : 'expense'">
|
||||
{{ formatMoneyCompact(stats.net, locale) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">{{ t('wallet.stats_cashback') }}</span>
|
||||
<span class="stat-val cashback">{{ formatMoneyCompact(stats.cashback, locale) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.wallet-stats-panel {
|
||||
|
||||
position: relative;
|
||||
|
||||
background: var(--gradient-card);
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
|
||||
border-radius: var(--radius);
|
||||
|
||||
padding: 14px 16px;
|
||||
|
||||
margin-bottom: var(--space-section);
|
||||
|
||||
box-shadow: var(--shadow-card-sm);
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.wallet-stats-panel::before {
|
||||
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
|
||||
left: 10%;
|
||||
|
||||
right: 10%;
|
||||
|
||||
height: 1px;
|
||||
|
||||
background: var(--gradient-card-shine);
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.stats-grid {
|
||||
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
gap: 12px 16px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.stat-cell {
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 4px;
|
||||
|
||||
min-width: 0;
|
||||
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
min-width: 0;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
color: var(--text-dim);
|
||||
|
||||
letter-spacing: 0;
|
||||
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.03em;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.stat-val {
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
color: var(--text);
|
||||
|
||||
letter-spacing: -0.02em;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.stat-val.income { color: var(--success); }
|
||||
.stat-val.expense { color: var(--danger); }
|
||||
.stat-val.cashback { color: var(--warning); }
|
||||
|
||||
.stat-val.expense { color: var(--text-muted); }
|
||||
|
||||
.stat-val.muted { color: var(--text-muted); }
|
||||
|
||||
.stats-divider {
|
||||
height: 1px;
|
||||
margin: 6px 8px;
|
||||
background: var(--border);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ const menuItems = computed(() => {
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.nav-item.active .dot {
|
||||
@@ -111,7 +111,7 @@ const menuItems = computed(() => {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: var(--tertiary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import { useAuthStore } from '../../stores/auth';
|
||||
import { formatMoney, parseAmount } from '../../utils/localeDisplay';
|
||||
import api from '../../api';
|
||||
import { usePlayerProfile } from '../../composables/usePlayerProfile';
|
||||
import BetSuccessOverlay from '../BetSuccessOverlay.vue';
|
||||
import ConfirmDialog from '../ConfirmDialog.vue';
|
||||
import { useAppToast } from '../../composables/useAppToast';
|
||||
import GoldSpinner from '../GoldSpinner.vue';
|
||||
import { type BetHistoryItem } from '../BetHistoryCard.vue';
|
||||
import { buildBetPlaceConfirmMessage } from '../../utils/betPlaceConfirmMessage';
|
||||
@@ -24,6 +24,7 @@ const slip = useBetSlipStore();
|
||||
const auth = useAuthStore();
|
||||
const { refreshProfile } = usePlayerProfile();
|
||||
const { requestLocate } = useDesktopBetLocate();
|
||||
const { showToast } = useAppToast();
|
||||
|
||||
const activeTab = ref<PanelTab>('single');
|
||||
const transitionName = ref('list-slide');
|
||||
@@ -38,8 +39,6 @@ const loading = ref(false);
|
||||
const balanceLoading = ref(false);
|
||||
const balance = ref<number | null>(null);
|
||||
const error = ref('');
|
||||
const success = ref('');
|
||||
const showSuccess = ref(false);
|
||||
const showPlaceConfirm = ref(false);
|
||||
const placeConfirmMessage = ref('');
|
||||
const pendingSingleItem = ref<SlipItem | null>(null);
|
||||
@@ -640,7 +639,6 @@ function onPlaceBetClick() {
|
||||
async function executePlaceSingleItem(item: SlipItem) {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
|
||||
try {
|
||||
await api.post('/player/bets/single', {
|
||||
@@ -650,12 +648,9 @@ async function executePlaceSingleItem(item: SlipItem) {
|
||||
requestId: genId(),
|
||||
});
|
||||
slip.removeItem(item.selectionId);
|
||||
success.value = t('bet.place_success');
|
||||
showSuccess.value = true;
|
||||
await Promise.all([loadBalance(), refreshProfile()]);
|
||||
setTimeout(() => {
|
||||
if (showSuccess.value) onSuccessDone();
|
||||
}, 2200);
|
||||
showToast(t('bet.place_success'));
|
||||
syncStakeInputFromSlip();
|
||||
} catch (e: unknown) {
|
||||
error.value =
|
||||
(e as { response?: { data?: { error?: string } } })?.response?.data?.error ||
|
||||
@@ -668,7 +663,6 @@ async function executePlaceSingleItem(item: SlipItem) {
|
||||
async function executePlaceBet() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
|
||||
try {
|
||||
if (activeTab.value === 'parlay') {
|
||||
@@ -692,15 +686,12 @@ async function executePlaceBet() {
|
||||
}
|
||||
slip.clearSingle();
|
||||
}
|
||||
success.value = t('bet.place_success');
|
||||
showSuccess.value = true;
|
||||
if (activeTab.value === 'history') {
|
||||
void loadHistory(true);
|
||||
}
|
||||
await Promise.all([loadBalance(), refreshProfile()]);
|
||||
setTimeout(() => {
|
||||
if (showSuccess.value) onSuccessDone();
|
||||
}, 2200);
|
||||
showToast(t('bet.place_success'));
|
||||
syncStakeInputFromSlip();
|
||||
} catch (e: unknown) {
|
||||
error.value =
|
||||
(e as { response?: { data?: { error?: string } } })?.response?.data?.error ||
|
||||
@@ -721,13 +712,6 @@ async function confirmPlaceBet() {
|
||||
await executePlaceBet();
|
||||
}
|
||||
|
||||
function onSuccessDone() {
|
||||
showSuccess.value = false;
|
||||
error.value = '';
|
||||
success.value = '';
|
||||
syncStakeInputFromSlip();
|
||||
}
|
||||
|
||||
async function loadBalance() {
|
||||
if (!auth.token) {
|
||||
balance.value = null;
|
||||
@@ -1125,7 +1109,6 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="panel-error-msg">{{ error }}</p>
|
||||
<p v-if="success" class="panel-success-msg">{{ success }}</p>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -1170,9 +1153,6 @@ onUnmounted(() => {
|
||||
:loading="loading"
|
||||
@confirm="confirmPlaceBet"
|
||||
/>
|
||||
|
||||
<!-- Success Overlay -->
|
||||
<BetSuccessOverlay :show="showSuccess" @done="onSuccessDone" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1240,7 +1220,7 @@ onUnmounted(() => {
|
||||
.clear-slip-btn:hover {
|
||||
color: var(--primary-light);
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: rgba(244, 162, 97, 0.06);
|
||||
}
|
||||
|
||||
.balance-info {
|
||||
@@ -1298,7 +1278,7 @@ onUnmounted(() => {
|
||||
|
||||
.badge {
|
||||
background: var(--primary);
|
||||
color: #001A33;
|
||||
color: #fff;
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
padding: 1px 4px;
|
||||
@@ -1307,7 +1287,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.odds-warning {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
background: rgba(244, 162, 97, 0.12);
|
||||
border-bottom: 1px solid var(--border-gold-soft);
|
||||
color: var(--primary-light);
|
||||
font-size: 11px;
|
||||
@@ -1350,7 +1330,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.slip-card {
|
||||
background: #0A2540;
|
||||
background: rgba(22, 45, 54, 0.4);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 7px 8px;
|
||||
@@ -1498,7 +1478,7 @@ onUnmounted(() => {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #002244;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
padding: 0 6px;
|
||||
@@ -1563,32 +1543,32 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.card-single-btn {
|
||||
border: 1px solid rgba(255, 255, 255, 0.55);
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
border: 1px solid rgba(244, 162, 97, 0.55);
|
||||
background: rgba(244, 162, 97, 0.14);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.card-single-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
background: rgba(244, 162, 97, 0.22);
|
||||
border-color: var(--border-gold);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-parlay-btn {
|
||||
border: 1px dashed rgba(255, 255, 255, 0.45);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px dashed rgba(244, 162, 97, 0.45);
|
||||
background: rgba(244, 162, 97, 0.04);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.card-parlay-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
border-color: var(--border-gold-soft);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-parlay-btn.active {
|
||||
border-style: solid;
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
border-color: rgba(244, 162, 97, 0.35);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
@@ -1605,7 +1585,7 @@ onUnmounted(() => {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
background: rgba(244, 162, 97, 0.16);
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
font-weight: 800;
|
||||
@@ -1635,7 +1615,7 @@ onUnmounted(() => {
|
||||
.panel-warning {
|
||||
font-size: 11px;
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
@@ -1649,7 +1629,7 @@ onUnmounted(() => {
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
padding: 4px 6px;
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@@ -1658,7 +1638,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.stake-control {
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
@@ -1669,7 +1649,7 @@ onUnmounted(() => {
|
||||
.stake-input-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #002244;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 0 10px;
|
||||
@@ -1713,7 +1693,7 @@ onUnmounted(() => {
|
||||
|
||||
.chip-btn {
|
||||
flex: 1;
|
||||
background: #0A2540;
|
||||
background: rgba(22, 45, 54, 0.45); border: 1px solid var(--border);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
@@ -1735,17 +1715,10 @@ onUnmounted(() => {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.panel-success-msg {
|
||||
color: #2ecc71;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.panel-bottom-fixed {
|
||||
flex-shrink: 0;
|
||||
border-top: 1px solid var(--border);
|
||||
background: rgba(10, 37, 64, 0.98);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.panel-summary {
|
||||
@@ -1753,7 +1726,6 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
@@ -1798,7 +1770,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.add-parlay-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: rgba(244, 162, 97, 0.05);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -1807,8 +1779,8 @@ onUnmounted(() => {
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #3D2800;
|
||||
background: var(--gradient-primary);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
@@ -1829,7 +1801,7 @@ onUnmounted(() => {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
@@ -1839,7 +1811,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.history-view-all-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
background: rgba(244, 162, 97, 0.14);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -1869,7 +1841,7 @@ onUnmounted(() => {
|
||||
.scope-chip.active {
|
||||
color: var(--primary-light);
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.history-loading {
|
||||
@@ -1889,7 +1861,7 @@ onUnmounted(() => {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: #0A2540;
|
||||
background: rgba(22, 45, 54, 0.45); border: 1px solid var(--border);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
@@ -1897,7 +1869,7 @@ onUnmounted(() => {
|
||||
|
||||
.history-card:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: rgba(244, 162, 97, 0.04);
|
||||
}
|
||||
|
||||
.history-card-top {
|
||||
@@ -1941,7 +1913,7 @@ onUnmounted(() => {
|
||||
|
||||
.history-status.hist-pending {
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
}
|
||||
|
||||
.history-match {
|
||||
|
||||
@@ -60,7 +60,7 @@ const { t } = useI18n();
|
||||
.data-table-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
background: rgba(12, 40, 66, 0.95);
|
||||
background: var(--bg-card); backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
@@ -73,7 +73,7 @@ const { t } = useI18n();
|
||||
}
|
||||
|
||||
th {
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-muted);
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
/**
|
||||
* PC auth shell — admin-style card: large logo left, form slot right.
|
||||
*/
|
||||
import MouseRippleGrid from '../MouseRippleGrid.vue';
|
||||
import LocaleSwitcher from '../LocaleSwitcher.vue';
|
||||
|
||||
withDefaults(
|
||||
@@ -18,7 +17,6 @@ withDefaults(
|
||||
<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 }">
|
||||
@@ -42,7 +40,7 @@ withDefaults(
|
||||
.desktop-auth-page {
|
||||
position: relative;
|
||||
min-height: 100dvh;
|
||||
background: linear-gradient(135deg, #001A33 0%, #001A33 50%, #002244 100%);
|
||||
background: linear-gradient(135deg, #0f2027 0%, #203a43 50%, #0f2027 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -50,7 +48,7 @@ withDefaults(
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 60% 60% at 50% 40%, rgba(255, 255, 255, 0.06) 0%, transparent 70%);
|
||||
background: radial-gradient(ellipse 60% 60% at 50% 40%, rgba(244, 162, 97, 0.06) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -59,7 +57,7 @@ withDefaults(
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
pointer-events: none;
|
||||
opacity: 0.12;
|
||||
opacity: 0.15;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@@ -68,7 +66,7 @@ withDefaults(
|
||||
height: 320px;
|
||||
top: 10%;
|
||||
left: 12%;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.5) 0%, transparent 70%);
|
||||
background: radial-gradient(circle, rgba(244, 162, 97, 0.5) 0%, transparent 70%);
|
||||
animation: ambient-drift-1 12s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@@ -77,7 +75,7 @@ withDefaults(
|
||||
height: 280px;
|
||||
bottom: 12%;
|
||||
right: 12%;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.35) 0%, transparent 70%);
|
||||
background: radial-gradient(circle, rgba(231, 111, 81, 0.35) 0%, transparent 70%);
|
||||
animation: ambient-drift-2 16s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@@ -111,41 +109,15 @@ withDefaults(
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
background: linear-gradient(
|
||||
165deg,
|
||||
rgba(255, 255, 255, 0.06) 0%,
|
||||
rgba(255, 255, 255, 0.03) 28%,
|
||||
rgba(10, 37, 64, 0.55) 65%,
|
||||
rgba(0, 20, 40, 0.75) 100%
|
||||
);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
background: rgba(22, 45, 54, 0.82);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-radius: 12px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.12),
|
||||
0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.38);
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.auth-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.28) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.auth-card--wide {
|
||||
@@ -153,8 +125,6 @@ withDefaults(
|
||||
}
|
||||
|
||||
.form-lang {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 14px 18px 0;
|
||||
@@ -171,12 +141,8 @@ withDefaults(
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28px 20px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.03) 0%,
|
||||
rgba(0, 26, 51, 0.3) 100%
|
||||
);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-right: 1px solid rgba(244, 162, 97, 0.15);
|
||||
}
|
||||
|
||||
.logo-large {
|
||||
@@ -184,14 +150,14 @@ withDefaults(
|
||||
max-width: 220px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 4px 24px rgba(255, 255, 255, 0.25));
|
||||
filter: drop-shadow(0 4px 24px rgba(244, 162, 97, 0.25));
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 22px 28px 28px;
|
||||
padding: 22px 24px 24px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@@ -212,7 +178,7 @@ withDefaults(
|
||||
.form-brand {
|
||||
padding: 20px 24px 12px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 1px solid rgba(244, 162, 97, 0.15);
|
||||
}
|
||||
|
||||
.logo-large {
|
||||
|
||||
@@ -255,7 +255,7 @@ onUnmounted(stopAutoPlay);
|
||||
height: 100%;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(180deg, #0A2540 0%, #001A33 100%);
|
||||
background: linear-gradient(180deg, var(--secondary) 0%, var(--bg-body) 100%);
|
||||
box-shadow: 0 10px 32px rgba(0, 0, 0, 0.45);
|
||||
transition: box-shadow 0.35s ease, filter 0.35s ease, transform 0.25s ease;
|
||||
display: flex;
|
||||
@@ -276,8 +276,8 @@ onUnmounted(stopAutoPlay);
|
||||
filter: none;
|
||||
box-shadow:
|
||||
0 16px 40px rgba(0, 0, 0, 0.5),
|
||||
0 0 28px rgba(255, 255, 255, 0.22),
|
||||
0 0 56px rgba(255, 255, 255, 0.1);
|
||||
0 0 28px rgba(244, 162, 97, 0.22),
|
||||
0 0 56px rgba(244, 162, 97, 0.1);
|
||||
}
|
||||
|
||||
.coverflow-slide.is-active:hover .slide-card {
|
||||
@@ -290,16 +290,16 @@ onUnmounted(stopAutoPlay);
|
||||
transform: scale(1);
|
||||
box-shadow:
|
||||
0 16px 40px rgba(0, 0, 0, 0.5),
|
||||
0 0 22px rgba(255, 255, 255, 0.16),
|
||||
0 0 48px rgba(255, 255, 255, 0.08);
|
||||
0 0 22px rgba(244, 162, 97, 0.16),
|
||||
0 0 48px rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.008);
|
||||
box-shadow:
|
||||
0 18px 44px rgba(0, 0, 0, 0.52),
|
||||
0 0 32px rgba(255, 255, 255, 0.26),
|
||||
0 0 60px rgba(255, 255, 255, 0.12);
|
||||
0 0 32px rgba(244, 162, 97, 0.26),
|
||||
0 0 60px rgba(244, 162, 97, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,13 +318,13 @@ onUnmounted(stopAutoPlay);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.12), #0A2540);
|
||||
background: linear-gradient(135deg, rgba(244, 162, 97, 0.12), var(--secondary));
|
||||
}
|
||||
|
||||
.fallback-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
color: rgba(244, 162, 97, 0.35);
|
||||
}
|
||||
|
||||
.click-hint {
|
||||
@@ -338,7 +338,7 @@ onUnmounted(stopAutoPlay);
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
background: rgba(0, 26, 51, 0.35);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ onUnmounted(stopAutoPlay);
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.45);
|
||||
border: 1px solid rgba(244, 162, 97, 0.45);
|
||||
animation: click-ring-pulse 2.8s ease-out infinite;
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ onUnmounted(stopAutoPlay);
|
||||
position: relative;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
color: rgba(244, 162, 97, 0.88);
|
||||
animation: click-icon-bob 2.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@@ -409,7 +409,7 @@ onUnmounted(stopAutoPlay);
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 26, 51, 0.65);
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
backdrop-filter: blur(8px);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
|
||||
@@ -175,16 +175,16 @@ function statusLabel(status: string) {
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 48px 20px;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
padding: 8px 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid var(--primary);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -193,7 +193,7 @@ function statusLabel(status: string) {
|
||||
.detail-card {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #0A2540;
|
||||
background: #1a323c;
|
||||
}
|
||||
|
||||
.detail-card:last-child {
|
||||
@@ -210,7 +210,7 @@ function statusLabel(status: string) {
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ function statusLabel(status: string) {
|
||||
|
||||
.status-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
||||
.status-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
||||
.status-pending { background: rgba(255, 255, 255, 0.12); color: rgba(255, 255, 255, 0.78); }
|
||||
.status-pending { background: rgba(244, 162, 97, 0.12); color: var(--primary-light); }
|
||||
.status-push { background: rgba(100, 100, 100, 0.12); color: #aaa; }
|
||||
.status-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||
|
||||
@@ -243,7 +243,7 @@ function statusLabel(status: string) {
|
||||
|
||||
.amount-label {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
@@ -252,17 +252,17 @@ function statusLabel(status: string) {
|
||||
.amount-val {
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
color: var(--text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.amount-val.gold { color: rgba(255, 255, 255, 0.78); }
|
||||
.amount-val.date { font-size: 13px; color: rgba(255, 255, 255, 0.56); font-weight: 600; }
|
||||
.amount-val.gold { color: var(--primary-light); }
|
||||
.amount-val.date { font-size: 13px; color: var(--text-muted); font-weight: 600; }
|
||||
|
||||
.card-title {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 14px;
|
||||
@@ -280,7 +280,7 @@ function statusLabel(status: string) {
|
||||
.sel-match {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
color: var(--text);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@@ -290,9 +290,9 @@ function statusLabel(status: string) {
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sel-name { color: rgba(255, 255, 255, 0.94); font-weight: 700; }
|
||||
.sel-odds { color: rgba(255, 255, 255, 0.78); font-weight: 800; margin-left: auto; }
|
||||
.sel-name { color: var(--text); font-weight: 700; }
|
||||
.sel-odds { color: var(--primary-light); font-weight: 800; margin-left: auto; }
|
||||
</style>
|
||||
|
||||
@@ -73,7 +73,7 @@ watch(
|
||||
transform: translateY(-50%);
|
||||
box-shadow:
|
||||
0 2px 10px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 3px rgba(12, 40, 66, 0.92);
|
||||
0 0 0 3px rgba(22, 45, 54, 0.92);
|
||||
transition:
|
||||
transform 0.22s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background 0.2s,
|
||||
@@ -95,10 +95,10 @@ watch(
|
||||
.rail-toggle:hover {
|
||||
border-color: var(--border-gold);
|
||||
background: var(--primary);
|
||||
color: #001A33;
|
||||
color: #fff;
|
||||
box-shadow:
|
||||
0 4px 14px rgba(0, 0, 0, 0.6),
|
||||
0 0 0 3px rgba(255, 255, 255, 0.15);
|
||||
0 0 0 3px rgba(244, 162, 97, 0.15);
|
||||
}
|
||||
|
||||
.rail-toggle:active {
|
||||
@@ -153,8 +153,8 @@ watch(
|
||||
height: 20px;
|
||||
padding: 0 5px;
|
||||
border-radius: 10px;
|
||||
background: var(--primary, rgba(255, 255, 255, 0.92));
|
||||
color: #001A33;
|
||||
background: var(--primary, #F4A261);
|
||||
color: var(--bg-body);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
line-height: 20px;
|
||||
|
||||
@@ -4,7 +4,7 @@ import DesktopDetailModalShell from './DesktopDetailModalShell.vue';
|
||||
import { formatMoney, formatMoneyCompact } from '../../utils/localeDisplay';
|
||||
import type { CashbackRecord } from '../../utils/cashback';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
record: CashbackRecord | null;
|
||||
}>();
|
||||
@@ -82,14 +82,14 @@ function formatTime(value: string | null) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 20px;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #001A33;
|
||||
background: #162d36;
|
||||
}
|
||||
|
||||
.amount-hero {
|
||||
@@ -97,7 +97,7 @@ function formatTime(value: string | null) {
|
||||
font-weight: 800;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
color: var(--primary-light);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
@@ -106,12 +106,12 @@ function formatTime(value: string | null) {
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.detail-rows {
|
||||
padding: 4px 0 12px;
|
||||
background: #0A2540;
|
||||
background: #1a323c;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
@@ -129,7 +129,7 @@ function formatTime(value: string | null) {
|
||||
width: 108px;
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -137,11 +137,11 @@ function formatTime(value: string | null) {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.row-val.muted {
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,7 +43,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
<header class="detail-modal-header">
|
||||
<h2 class="detail-modal-title">{{ title }}</h2>
|
||||
<button type="button" class="detail-modal-close" :aria-label="t('support.close')" @click="close">
|
||||
×
|
||||
✕
|
||||
</button>
|
||||
</header>
|
||||
<div class="detail-modal-body">
|
||||
@@ -64,7 +64,8 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(0, 26, 51, 0.96);
|
||||
background: #0f2027;
|
||||
background: rgba(15, 32, 39, 0.96);
|
||||
}
|
||||
|
||||
.detail-modal {
|
||||
@@ -75,8 +76,8 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 12px;
|
||||
background: #0A2540;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.55);
|
||||
background: #1a323c;
|
||||
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.detail-modal-header {
|
||||
@@ -86,7 +87,7 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #001A33;
|
||||
background: #162d36;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -94,38 +95,38 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.detail-modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
color: var(--text-muted);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
padding: 0 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.detail-modal-close:hover {
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.detail-modal-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
background: #0A2540;
|
||||
background: #1a323c;
|
||||
}
|
||||
|
||||
.detail-modal-fade-enter-active,
|
||||
.detail-modal-fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
transition: opacity 0.18s ease;
|
||||
}
|
||||
|
||||
.detail-modal-fade-enter-active .detail-modal,
|
||||
.detail-modal-fade-leave-active .detail-modal {
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.detail-modal-fade-enter-from,
|
||||
@@ -135,7 +136,6 @@ onUnmounted(() => document.removeEventListener('keydown', onKeydown));
|
||||
|
||||
.detail-modal-fade-enter-from .detail-modal,
|
||||
.detail-modal-fade-leave-to .detail-modal {
|
||||
transform: scale(0.97) translateY(8px);
|
||||
opacity: 0;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { usePlayerProfile } from '../../composables/usePlayerProfile';
|
||||
import ConfirmDialog from '../ConfirmDialog.vue';
|
||||
import { buildBetPlaceConfirmMessage } from '../../utils/betPlaceConfirmMessage';
|
||||
import { useAppToast } from '../../composables/useAppToast';
|
||||
import BetSuccessOverlay from '../BetSuccessOverlay.vue';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { visible, anchorX, anchorY, pendingItem, placement, close, cancelClose, scheduleClose } = useDesktopBetPopover();
|
||||
@@ -26,7 +25,6 @@ const error = ref('');
|
||||
const showPlaceConfirm = ref(false);
|
||||
const placeConfirmMessage = ref('');
|
||||
const MIN_STAKE = 5;
|
||||
const showSuccess = ref(false);
|
||||
const confirmingItem = ref<any>(null);
|
||||
|
||||
watch(showPlaceConfirm, (val) => {
|
||||
@@ -138,7 +136,7 @@ async function executePlaceNow(item = confirmingItem.value || pendingItem.value)
|
||||
await refreshProfile();
|
||||
showPlaceConfirm.value = false;
|
||||
close();
|
||||
showSuccess.value = true;
|
||||
showToast(t('bet.place_success'));
|
||||
} catch (e: unknown) {
|
||||
error.value =
|
||||
(e as { response?: { data?: { error?: string } } })?.response?.data?.error ||
|
||||
@@ -251,22 +249,20 @@ function addToParlayList() {
|
||||
@confirm="confirmPlaceNow"
|
||||
/>
|
||||
|
||||
<BetSuccessOverlay :show="showSuccess" @done="showSuccess = false" />
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bet-popover {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
width: 190px;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
background: rgba(14, 44, 70, 0.9);
|
||||
width: 172px;
|
||||
padding: 6px 7px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-active);
|
||||
background: var(--bg-card-elevated);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.bet-popover::after,
|
||||
@@ -284,7 +280,7 @@ function addToParlayList() {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 0 6px 6px 6px;
|
||||
border-color: transparent transparent rgba(14, 44, 70, 0.9) transparent;
|
||||
border-color: transparent transparent var(--bg-card-elevated) transparent;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -293,7 +289,7 @@ function addToParlayList() {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 0 7px 7px 7px;
|
||||
border-color: transparent transparent rgba(255, 255, 255, 0.2) transparent;
|
||||
border-color: transparent transparent var(--border-active) transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -302,7 +298,7 @@ function addToParlayList() {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 6px 6px 0 6px;
|
||||
border-color: rgba(14, 44, 70, 0.9) transparent transparent transparent;
|
||||
border-color: var(--bg-card-elevated) transparent transparent transparent;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@@ -311,7 +307,7 @@ function addToParlayList() {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border-width: 7px 7px 0 7px;
|
||||
border-color: rgba(255, 255, 255, 0.2) transparent transparent transparent;
|
||||
border-color: var(--border-active) transparent transparent transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -342,8 +338,8 @@ function addToParlayList() {
|
||||
padding: 4px 6px;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
background: rgba(244, 162, 97, 0.04);
|
||||
border: 1px solid rgba(244, 162, 97, 0.15);
|
||||
}
|
||||
|
||||
.pick {
|
||||
@@ -375,13 +371,13 @@ function addToParlayList() {
|
||||
margin-bottom: 5px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #001A33;
|
||||
background: var(--bg-body);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.stake-input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(244, 162, 97, 0.3);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -426,8 +422,8 @@ function addToParlayList() {
|
||||
padding: 5px;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #3d2800;
|
||||
background: var(--gradient-primary);
|
||||
color: #ffffff;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
cursor: pointer;
|
||||
@@ -447,8 +443,8 @@ function addToParlayList() {
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(244, 162, 97, 0.25);
|
||||
background: rgba(244, 162, 97, 0.03);
|
||||
color: var(--primary-light);
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
@@ -457,6 +453,6 @@ function addToParlayList() {
|
||||
}
|
||||
|
||||
.btn-outline:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -107,19 +107,21 @@ const topSelections = computed(() => {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 140px;
|
||||
border: 1px solid rgba(140, 140, 140, 0.35);
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(180deg, #0A2540 0%, #002244 100%);
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: var(--shadow);
|
||||
transition: border-color 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
.outright-event-card:hover {
|
||||
border-color: var(--border-gold);
|
||||
box-shadow: var(--shadow-gold);
|
||||
border-color: var(--border-active);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.card-clickable-area {
|
||||
@@ -167,7 +169,7 @@ const topSelections = computed(() => {
|
||||
.settled-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: #c9a227;
|
||||
color: var(--primary);
|
||||
border: 1px solid rgba(201, 162, 39, 0.45);
|
||||
border-radius: 999px;
|
||||
padding: 1px 7px;
|
||||
@@ -281,7 +283,7 @@ const topSelections = computed(() => {
|
||||
.odds-btn:hover {
|
||||
background: var(--border-gold-soft);
|
||||
border-color: var(--border-gold);
|
||||
box-shadow: 0 0 8px rgba(255, 255, 255, 0.25);
|
||||
box-shadow: 0 0 8px rgba(244, 162, 97, 0.25);
|
||||
}
|
||||
|
||||
.selection-name {
|
||||
|
||||
@@ -225,14 +225,14 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 20px;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #0A2540;
|
||||
background: #1a323c;
|
||||
}
|
||||
|
||||
.detail-card:last-child {
|
||||
@@ -249,7 +249,7 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
|
||||
.status-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
||||
.status-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
||||
.status-pending { background: rgba(255, 255, 255, 0.12); color: rgba(255, 255, 255, 0.78); }
|
||||
.status-pending { background: rgba(244, 162, 97, 0.12); color: var(--primary-light); }
|
||||
.status-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||
|
||||
.amount-row {
|
||||
@@ -281,7 +281,7 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
|
||||
.amount-label {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
@@ -290,13 +290,13 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
.amount-val {
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
color: var(--text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.amount-val.gold { color: rgba(255, 255, 255, 0.78); }
|
||||
.amount-val.gold { color: var(--primary-light); }
|
||||
.amount-val.method { font-size: 14px; font-weight: 700; }
|
||||
.amount-val.date { font-size: 13px; color: rgba(255, 255, 255, 0.56); font-weight: 600; }
|
||||
.amount-val.date { font-size: 13px; color: var(--text-muted); font-weight: 600; }
|
||||
|
||||
.reject-reason {
|
||||
padding: 10px 12px;
|
||||
@@ -310,17 +310,17 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
|
||||
.order-remark {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.reapply-btn {
|
||||
align-self: flex-start;
|
||||
padding: 9px 24px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -329,7 +329,7 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
.card-title {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 14px;
|
||||
@@ -350,7 +350,7 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.56);
|
||||
background: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.audit-line {
|
||||
@@ -368,11 +368,11 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.audit-step-title { font-size: 13px; font-weight: 700; color: rgba(255, 255, 255, 0.94); }
|
||||
.audit-step-time { font-size: 11px; color: rgba(255, 255, 255, 0.56); white-space: nowrap; }
|
||||
.audit-step-actor { margin: 3px 0 0; font-size: 12px; color: rgba(255, 255, 255, 0.56); }
|
||||
.audit-step-title { font-size: 13px; font-weight: 700; color: var(--text); }
|
||||
.audit-step-time { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
|
||||
.audit-step-actor { margin: 3px 0 0; font-size: 12px; color: var(--text-muted); }
|
||||
.audit-step-credited { margin: 4px 0 0; font-size: 12px; font-weight: 600; color: #7eb87a; }
|
||||
.audit-step-note { margin: 4px 0 0; font-size: 12px; color: rgba(255, 255, 255, 0.56); line-height: 1.45; }
|
||||
.audit-step-note { margin: 4px 0 0; font-size: 12px; color: var(--text-muted); line-height: 1.45; }
|
||||
.audit-step-box {
|
||||
margin-top: 4px;
|
||||
padding: 8px 10px;
|
||||
@@ -384,9 +384,9 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
.audit-step-box--reject { border: 1px solid rgba(255, 69, 58, 0.22); }
|
||||
.audit-step-box-label { font-size: 10px; font-weight: 600; color: #c07070; }
|
||||
.audit-step-box-text { font-size: 12px; color: #b08888; }
|
||||
.audit-step--submitted .audit-dot { background: rgba(255, 255, 255, 0.92); }
|
||||
.audit-step--submitted .audit-dot { background: var(--primary); }
|
||||
.audit-step--approved .audit-dot { background: #4cd964; }
|
||||
.audit-step--rejected .audit-dot { background: #ff453a; }
|
||||
.audit-step--revoked .audit-dot { background: #888; }
|
||||
.audit-step--reopened .audit-dot { background: rgba(255, 255, 255, 0.68); }
|
||||
.audit-step--reopened .audit-dot { background: var(--primary); }
|
||||
</style>
|
||||
|
||||
@@ -74,7 +74,7 @@ const activeTab = computed(() => {
|
||||
<div class="nav-right">
|
||||
<LocaleSwitcher />
|
||||
<template v-if="auth.user">
|
||||
<CashBalanceChip hide-avatar />
|
||||
<CashBalanceChip />
|
||||
<UserAvatarMenu />
|
||||
</template>
|
||||
<div v-else class="auth-buttons">
|
||||
@@ -114,7 +114,7 @@ const activeTab = computed(() => {
|
||||
height: 38px;
|
||||
width: auto;
|
||||
display: block;
|
||||
filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.2));
|
||||
filter: drop-shadow(0 0 4px rgba(244, 162, 97, 0.2));
|
||||
}
|
||||
|
||||
.site-name {
|
||||
@@ -122,10 +122,10 @@ const activeTab = computed(() => {
|
||||
font-weight: 800;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
letter-spacing: 0.5px;
|
||||
background: var(--gradient-gold);
|
||||
background: var(--gradient-primary);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 8px rgba(255, 255, 255, 0.15);
|
||||
text-shadow: 0 0 8px rgba(244, 162, 97, 0.25);
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ const activeTab = computed(() => {
|
||||
height: 3px;
|
||||
background: var(--primary);
|
||||
border-radius: 3px 3px 0 0;
|
||||
box-shadow: 0 -1px 8px rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 -1px 8px rgba(244, 162, 97, 0.5);
|
||||
}
|
||||
|
||||
.badge {
|
||||
@@ -204,7 +204,7 @@ const activeTab = computed(() => {
|
||||
padding: 6px 18px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: rgba(244, 162, 97, 0.05);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
@@ -212,15 +212,15 @@ const activeTab = computed(() => {
|
||||
}
|
||||
|
||||
.login-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
background: rgba(244, 162, 97, 0.12);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
padding: 7px 18px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #3D2800;
|
||||
background: var(--gradient-primary);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
@@ -144,11 +144,11 @@ function goCashbackDetail() {
|
||||
<span class="row-val mono">{{ formatMoney(detail.balanceAfter, locale) }}</span>
|
||||
</div>
|
||||
<div v-if="showFrozen(detail)" class="detail-row">
|
||||
<span class="row-label">{{ t('wallet.detail_frozen_before') }}</span>
|
||||
<span class="row-label">{{ t('wallet.frozen_before') }}</span>
|
||||
<span class="row-val mono">{{ formatMoney(detail.frozenBefore, locale) }}</span>
|
||||
</div>
|
||||
<div v-if="showFrozen(detail)" class="detail-row">
|
||||
<span class="row-label">{{ t('wallet.detail_frozen_after') }}</span>
|
||||
<span class="row-label">{{ t('wallet.frozen_after') }}</span>
|
||||
<span class="row-val mono">{{ formatMoney(detail.frozenAfter, locale) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
@@ -183,16 +183,16 @@ function goCashbackDetail() {
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 48px 20px;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
padding: 8px 20px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border: 1px solid var(--primary);
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -201,7 +201,7 @@ function goCashbackDetail() {
|
||||
.hero {
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #001A33;
|
||||
background: #162d36;
|
||||
}
|
||||
|
||||
.amount-hero {
|
||||
@@ -212,19 +212,19 @@ function goCashbackDetail() {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.amount-hero.pos { color: rgba(255, 255, 255, 0.78); }
|
||||
.amount-hero.neg { color: rgba(255, 255, 255, 0.62); }
|
||||
.amount-hero.pos { color: var(--primary-light); }
|
||||
.amount-hero.neg { color: var(--danger); }
|
||||
|
||||
.type-label {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.detail-rows {
|
||||
padding: 4px 0;
|
||||
background: #0A2540;
|
||||
background: #1a323c;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
@@ -242,7 +242,7 @@ function goCashbackDetail() {
|
||||
width: 108px;
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -250,12 +250,12 @@ function goCashbackDetail() {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
color: var(--text);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.row-val.muted {
|
||||
color: rgba(255, 255, 255, 0.56);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -270,16 +270,16 @@ function goCashbackDetail() {
|
||||
gap: 8px;
|
||||
padding: 12px 20px 18px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #0A2540;
|
||||
background: #1a323c;
|
||||
}
|
||||
|
||||
.link-btn {
|
||||
width: 100%;
|
||||
padding: 9px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -287,6 +287,6 @@ function goCashbackDetail() {
|
||||
}
|
||||
|
||||
.link-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(244, 162, 97, 0.14);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -197,7 +197,7 @@ const handleLeagueClick = (leagueId: string) => {
|
||||
|
||||
.sidebar-search-input {
|
||||
width: 100%;
|
||||
background: #002244 !important;
|
||||
background: var(--bg-body) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
color: var(--text);
|
||||
padding: 6px 10px;
|
||||
@@ -207,7 +207,7 @@ const handleLeagueClick = (leagueId: string) => {
|
||||
|
||||
.sidebar-search-input:focus {
|
||||
border-color: var(--border-gold-soft) !important;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1) !important;
|
||||
box-shadow: 0 0 0 2px rgba(244, 162, 97, 0.1) !important;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
@@ -231,7 +231,7 @@ const handleLeagueClick = (leagueId: string) => {
|
||||
|
||||
.time-btn {
|
||||
flex: 1;
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
padding: 5px 2px;
|
||||
@@ -248,7 +248,7 @@ const handleLeagueClick = (leagueId: string) => {
|
||||
}
|
||||
|
||||
.time-btn.active {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
@@ -300,7 +300,7 @@ const handleLeagueClick = (leagueId: string) => {
|
||||
}
|
||||
|
||||
.league-item.active {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: rgba(244, 162, 97, 0.04);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@@ -310,7 +310,7 @@ const handleLeagueClick = (leagueId: string) => {
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background: #002244;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.checkbox.checked {
|
||||
@@ -325,7 +325,7 @@ const handleLeagueClick = (leagueId: string) => {
|
||||
left: 4px;
|
||||
width: 3px;
|
||||
height: 6px;
|
||||
border: solid #001A33;
|
||||
border: solid var(--bg-body);
|
||||
border-width: 0 2px 2px 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
@@ -349,7 +349,7 @@ const handleLeagueClick = (leagueId: string) => {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
background: #181818;
|
||||
background: rgba(22, 45, 54, 0.4); border: 1px solid var(--border);
|
||||
padding: 1px 5px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ watch(currentMatchId, scrollToActive);
|
||||
|
||||
.sidebar-search-input {
|
||||
width: 100%;
|
||||
background: #002244 !important;
|
||||
background: var(--bg-body) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
color: var(--text);
|
||||
padding: 6px 10px;
|
||||
@@ -171,7 +171,7 @@ watch(currentMatchId, scrollToActive);
|
||||
|
||||
.sidebar-search-input:focus {
|
||||
border-color: var(--border-gold-soft) !important;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1) !important;
|
||||
box-shadow: 0 0 0 2px rgba(244, 162, 97, 0.1) !important;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
@@ -201,7 +201,7 @@ watch(currentMatchId, scrollToActive);
|
||||
|
||||
.time-btn {
|
||||
flex: 1;
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
padding: 5px 2px;
|
||||
@@ -218,7 +218,7 @@ watch(currentMatchId, scrollToActive);
|
||||
}
|
||||
|
||||
.time-btn.active {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
@@ -324,7 +324,7 @@ watch(currentMatchId, scrollToActive);
|
||||
.matches-list :deep(.match-card) {
|
||||
padding: 10px 8px;
|
||||
min-height: unset;
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
gap: 6px;
|
||||
@@ -337,7 +337,7 @@ watch(currentMatchId, scrollToActive);
|
||||
|
||||
.matches-list :deep(.match-card.active) {
|
||||
border-color: var(--primary) !important;
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
box-shadow: 0 0 8px rgba(255, 255, 255, 0.25);
|
||||
background: rgba(244, 162, 97, 0.08) !important;
|
||||
box-shadow: 0 0 8px rgba(244, 162, 97, 0.25);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -126,7 +126,7 @@ watch(currentOutrightId, scrollToActive);
|
||||
|
||||
.sidebar-search-input {
|
||||
width: 100%;
|
||||
background: #002244 !important;
|
||||
background: var(--bg-body) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
color: var(--text);
|
||||
padding: 6px 10px;
|
||||
@@ -136,7 +136,7 @@ watch(currentOutrightId, scrollToActive);
|
||||
|
||||
.sidebar-search-input:focus {
|
||||
border-color: var(--border-gold-soft) !important;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1) !important;
|
||||
box-shadow: 0 0 0 2px rgba(244, 162, 97, 0.1) !important;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
@@ -188,7 +188,7 @@ watch(currentOutrightId, scrollToActive);
|
||||
|
||||
.outright-card {
|
||||
padding: 12px 10px;
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
@@ -205,8 +205,8 @@ watch(currentOutrightId, scrollToActive);
|
||||
|
||||
.outright-card.active {
|
||||
border-color: var(--primary) !important;
|
||||
background: rgba(255, 255, 255, 0.08) !important;
|
||||
box-shadow: 0 0 8px rgba(255, 255, 255, 0.25);
|
||||
background: rgba(244, 162, 97, 0.08) !important;
|
||||
box-shadow: 0 0 8px rgba(244, 162, 97, 0.25);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
@@ -251,15 +251,15 @@ watch(currentOutrightId, scrollToActive);
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
background: #0A2540;
|
||||
background: rgba(22, 45, 54, 0.45); border: 1px solid var(--border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.outright-card.active .selections-badge {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: rgba(244, 162, 97, 0.15);
|
||||
color: var(--primary-light);
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(244, 162, 97, 0.3);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -120,7 +120,7 @@ function onPageSizeChange(e: Event) {
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -131,13 +131,13 @@ function onPageSizeChange(e: Event) {
|
||||
.page-btn:hover:not(:disabled) {
|
||||
border-color: var(--border-gold-soft);
|
||||
color: var(--text);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: rgba(244, 162, 97, 0.05);
|
||||
}
|
||||
|
||||
.page-btn.active {
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
color: #001A33;
|
||||
color: var(--bg-body);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ function onPageSizeChange(e: Event) {
|
||||
|
||||
.page-size-select {
|
||||
height: 32px;
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
border-radius: 6px;
|
||||
|
||||
@@ -78,9 +78,9 @@ function selectSport(id: SportCategory, enabled: boolean) {
|
||||
|
||||
.sport-category.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.sport-category.disabled {
|
||||
|
||||
@@ -96,18 +96,18 @@ function formatOdds(odds: string) {
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 210;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
background: var(--gradient-card);
|
||||
background: var(--bg-card);
|
||||
width: 100%;
|
||||
max-height: 80vh;
|
||||
border-radius: 16px 16px 0 0;
|
||||
border-top: 1px solid var(--border-gold-soft);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -118,15 +118,14 @@ function formatOdds(odds: string) {
|
||||
overflow-y: auto;
|
||||
padding: 10px 14px 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.drawer-foot {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
|
||||
background: var(--bg-card);
|
||||
border-top: 1px solid var(--border-gold-soft);
|
||||
box-shadow: 0 -6px 20px rgba(0, 0, 0, 0.45);
|
||||
background: var(--bg-elevated);
|
||||
border-top: 1px solid var(--border);
|
||||
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
@@ -135,7 +134,7 @@ function formatOdds(odds: string) {
|
||||
align-items: center;
|
||||
padding: 14px 14px 12px;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-elevated);
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 2;
|
||||
}
|
||||
@@ -143,10 +142,11 @@ function formatOdds(odds: string) {
|
||||
.drawer-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
@@ -158,15 +158,16 @@ function formatOdds(odds: string) {
|
||||
|
||||
.slip-item {
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.item-sel {
|
||||
@@ -176,7 +177,7 @@ function formatOdds(odds: string) {
|
||||
}
|
||||
|
||||
.odds {
|
||||
color: var(--primary-light);
|
||||
color: var(--warning);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@@ -195,7 +196,7 @@ function formatOdds(odds: string) {
|
||||
}
|
||||
|
||||
.summary-row strong {
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
@@ -206,7 +207,7 @@ function formatOdds(odds: string) {
|
||||
}
|
||||
|
||||
.return strong {
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n';
|
||||
import { groupCorrectScoreSelections, type CsSelection } from '../../utils/correctScoreLayout';
|
||||
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
marketType: string;
|
||||
selections: Array<{
|
||||
@@ -86,6 +85,8 @@ function formatOdds(odds: string) {
|
||||
<style scoped>
|
||||
.cs-panel {
|
||||
padding: 6px 8px 0;
|
||||
background: var(--bg-card-elevated);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.cs-panel--locked {
|
||||
@@ -121,6 +122,7 @@ function formatOdds(odds: string) {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
/* ── Score Card ── */
|
||||
.score-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -129,36 +131,42 @@ function formatOdds(odds: string) {
|
||||
gap: 3px;
|
||||
min-height: 42px;
|
||||
padding: 6px 3px;
|
||||
background: var(--bg-elevated, rgba(255,255,255,0.08));
|
||||
border: 1px solid var(--border, rgba(255,255,255,0.1));
|
||||
border-radius: 5px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.score-card:hover:not(.score-card--locked) {
|
||||
border-color: var(--primary-light);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.score-card.selected {
|
||||
border-color: var(--border-gold, rgba(255,255,255,0.2));
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15), 0 0 10px rgba(255, 255, 255, 0.06);
|
||||
border-color: var(--primary);
|
||||
border-width: 2px;
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.score-card.selected .score-line {
|
||||
color: var(--text);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.score-card.selected .odds {
|
||||
color: #ffffff;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.score-card--locked {
|
||||
background: var(--bg-elevated);
|
||||
border-color: var(--border);
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.score-card--locked .score-line,
|
||||
.score-card--locked .odds {
|
||||
color: var(--text-muted, rgba(255,255,255,0.4));
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.score-line {
|
||||
@@ -170,9 +178,10 @@ function formatOdds(odds: string) {
|
||||
.odds {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light, rgba(255,255,255,0.68));
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
/* ── Dense 紧凑模式 ── */
|
||||
.cs-panel--dense {
|
||||
padding: 4px 6px 6px;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n';
|
||||
import { resolveSelectionLabel } from '../../utils/selectionLabel';
|
||||
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
selections: {
|
||||
id: string;
|
||||
@@ -15,7 +14,7 @@ const props = defineProps<{
|
||||
}[];
|
||||
isSelected: (id: string) => boolean;
|
||||
compact?: boolean;
|
||||
/** PC 行内横排:左标签右赔率,适合详情页全宽行 */
|
||||
/** PC 行内横排:左标签右赔率,适合详情页全宽 */
|
||||
horizontal?: boolean;
|
||||
/** PC 卡片内:上标签下赔率,列数随选项数量 */
|
||||
desktopCard?: boolean;
|
||||
@@ -80,10 +79,12 @@ const panelStyle = computed(() =>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.wrap {
|
||||
padding: 6px 8px 8px;
|
||||
background: var(--bg-input);
|
||||
background: var(--bg-card-elevated);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.wrap.compact {
|
||||
@@ -99,6 +100,22 @@ const panelStyle = computed(() =>
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.wrap.desktopCard {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.wrap.locked {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
/* ── 基础网格 ── */
|
||||
.panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.wrap.compact .panel:not(.horizontal) {
|
||||
gap: 2px;
|
||||
}
|
||||
@@ -109,6 +126,7 @@ const panelStyle = computed(() =>
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* ── Horizontal 行内布局 ── */
|
||||
.panel.horizontal {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -138,11 +156,7 @@ const panelStyle = computed(() =>
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wrap.desktopCard {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* ── Desktop Card 紧凑布局 ── */
|
||||
.panel.desktop-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--market-cols, 3), minmax(0, 1fr));
|
||||
@@ -151,14 +165,14 @@ const panelStyle = computed(() =>
|
||||
}
|
||||
|
||||
.panel.desktop-card .odds-btn {
|
||||
min-height: 38px;
|
||||
padding: 4px 6px;
|
||||
gap: 2px;
|
||||
min-height: 34px;
|
||||
padding: 3px 4px;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.panel.desktop-card .odds-btn .label {
|
||||
font-size: 9px;
|
||||
line-height: 1.2;
|
||||
font-size: 8px;
|
||||
line-height: 1.15;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -166,20 +180,11 @@ const panelStyle = computed(() =>
|
||||
}
|
||||
|
||||
.panel.desktop-card .odds-btn .odds {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.wrap.locked {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
/* ── Odds Button ── */
|
||||
.odds-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -188,52 +193,54 @@ const panelStyle = computed(() =>
|
||||
gap: 2px;
|
||||
min-height: 44px;
|
||||
padding: 6px 4px;
|
||||
border-radius: 4px;
|
||||
background: var(--gradient-card-soft);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
text-align: center;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.odds-btn:hover:not(.odds-btn--locked) {
|
||||
border-color: var(--border-strong);
|
||||
border-color: var(--primary-light);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.odds-btn.selected {
|
||||
border-color: var(--border-strong);
|
||||
background: var(--surface-active);
|
||||
box-shadow: inset 0 0 0 1px var(--border-gold);
|
||||
border-color: var(--primary);
|
||||
border-width: 2px;
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.odds-btn.selected .label {
|
||||
color: var(--text);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.odds-btn.selected .odds {
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.odds-btn--locked {
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-elevated);
|
||||
border-color: var(--border);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.odds-btn--locked .label,
|
||||
.odds-btn--locked .odds {
|
||||
color: var(--text-dim);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 9px;
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.15;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.odds {
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
color: var(--warning);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps<{
|
||||
label: string;
|
||||
promoLabel?: string;
|
||||
statusLabel?: string;
|
||||
expanded: boolean;
|
||||
hasMarket: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ toggle: [] }>();
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<button
|
||||
type="button"
|
||||
class="row"
|
||||
:class="{ expanded, disabled: !hasMarket }"
|
||||
:disabled="!hasMarket"
|
||||
@click="emit('toggle')"
|
||||
>
|
||||
<span class="row-label">{{ label }}</span>
|
||||
<span v-if="promoLabel" class="row-promo">{{ promoLabel }}</span>
|
||||
<span v-if="statusLabel" class="row-status">{{ statusLabel }}</span>
|
||||
</div>
|
||||
<span v-else-if="!hasMarket" class="row-muted">{{ t('bet.market_closed') }}</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -21,22 +35,36 @@ defineProps<{
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 13px 12px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
transition: background 0.15s ease;
|
||||
border-radius: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row::before {
|
||||
.row:not(.disabled):active {
|
||||
background: rgba(244, 162, 97, 0.07);
|
||||
}
|
||||
|
||||
.row.expanded {
|
||||
background: rgba(244, 162, 97, 0.07);
|
||||
}
|
||||
|
||||
.row.expanded::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent 0%, var(--primary) 30%, var(--primary-light) 50%, var(--primary) 70%, transparent 100%);
|
||||
opacity: 0.6;
|
||||
height: 2.5px;
|
||||
background: var(--primary);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.row.disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.row-label {
|
||||
@@ -44,7 +72,7 @@ defineProps<{
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
color: var(--text);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
@@ -52,11 +80,11 @@ defineProps<{
|
||||
flex-shrink: 0;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
color: var(--warning);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--surface-accent);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(255, 179, 0, 0.1);
|
||||
border: 1px solid rgba(255, 179, 0, 0.3);
|
||||
}
|
||||
|
||||
.row-status {
|
||||
@@ -69,4 +97,14 @@ defineProps<{
|
||||
background: rgba(240, 180, 41, 0.12);
|
||||
border: 1px solid rgba(240, 180, 41, 0.35);
|
||||
}
|
||||
|
||||
.row.expanded .row-label {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.row-muted {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -258,16 +258,24 @@ function formatOdds(odds: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="modal-fade">
|
||||
<div v-if="open && pick" class="overlay" @click.self="close">
|
||||
<div class="modal" role="dialog" aria-modal="true">
|
||||
<button type="button" class="close-x" :aria-label="t('bet.cancel')" @click="close">✕</button>
|
||||
<button type="button" class="close-x" :aria-label="t('bet.cancel')" @click="close">
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M1 1L13 13M13 1L1 13" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
|
||||
</button>
|
||||
|
||||
<template v-if="step === 'form'">
|
||||
<p class="hint">{{ t('bet.outright_enter_stake') }}</p>
|
||||
<div class="modal-header">
|
||||
<p class="hint">{{ t('bet.outright_enter_stake') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="hero">
|
||||
<img v-if="flagUrl" :src="flagUrl" alt="" class="flag" />
|
||||
<p class="team">{{ pick.teamName }}</p>
|
||||
<div class="hero-info">
|
||||
<p class="team">{{ pick.teamName }}</p>
|
||||
<p class="event-title">{{ pick.eventTitle }}</p>
|
||||
</div>
|
||||
<span
|
||||
class="odds-badge"
|
||||
:class="{
|
||||
@@ -277,16 +285,16 @@ function formatOdds(odds: string) {
|
||||
}"
|
||||
>
|
||||
<template v-if="oddsDelta && !oddsDelta.suspended">
|
||||
@ {{ oddsDelta.oldOdds.toFixed(2) }} → {{ oddsDelta.newOdds.toFixed(2) }}
|
||||
<span class="odds-old">{{ oddsDelta.oldOdds.toFixed(2) }}</span>
|
||||
<span class="odds-arrow">→</span>
|
||||
<span class="odds-new">{{ oddsDelta.newOdds.toFixed(2) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
@ {{ formatOdds(currentOdds || pick.odds) }}
|
||||
{{ formatOdds(currentOdds || pick.odds) }}
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="event-title">{{ pick.eventTitle }}</p>
|
||||
|
||||
<p v-if="oddsWarningText" class="odds-warning">{{ oddsWarningText }}</p>
|
||||
|
||||
<div class="balance-row">
|
||||
@@ -294,27 +302,33 @@ function formatOdds(odds: string) {
|
||||
<span class="balance-value">{{ balanceText }}</span>
|
||||
</div>
|
||||
|
||||
<label class="stake-label">{{ t('bet.stake_label') }}</label>
|
||||
<input
|
||||
v-model.number="stake"
|
||||
type="number"
|
||||
class="stake-input"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
inputmode="decimal"
|
||||
:placeholder="t('bet.stake_placeholder')"
|
||||
/>
|
||||
<div class="stake-section">
|
||||
<label class="stake-label">{{ t('bet.stake_label') }}</label>
|
||||
<div class="stake-input-wrap">
|
||||
<input
|
||||
v-model.number="stake"
|
||||
type="number"
|
||||
class="stake-input"
|
||||
min="0.01"
|
||||
step="0.01"
|
||||
inputmode="decimal"
|
||||
:placeholder="t('bet.stake_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="quick-stakes">
|
||||
<button type="button" class="chip" @click="setStake(50)">50</button>
|
||||
<button type="button" class="chip" @click="setStake(100)">100</button>
|
||||
<button type="button" class="chip" @click="setStake(500)">500</button>
|
||||
<button type="button" class="chip" @click="setMaxStake">{{ t('bet.stake_max') }}</button>
|
||||
<div class="quick-stakes">
|
||||
<button type="button" class="chip" @click="setStake(50)">50</button>
|
||||
<button type="button" class="chip" @click="setStake(100)">100</button>
|
||||
<button type="button" class="chip" @click="setStake(500)">500</button>
|
||||
<button type="button" class="chip" @click="setStake(1000)">1000</button>
|
||||
<button type="button" class="chip chip-max" @click="setMaxStake">{{ t('bet.stake_max') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="est-return">
|
||||
{{ t('history.est_return') }}:<span class="est-value">{{ estReturnText }}</span>
|
||||
</p>
|
||||
<div class="est-return">
|
||||
<span class="est-label">{{ t('history.est_return') }}</span>
|
||||
<span class="est-value">{{ estReturnText }}</span>
|
||||
</div>
|
||||
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
|
||||
@@ -324,73 +338,97 @@ function formatOdds(odds: string) {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-confirm btn-gold-outline"
|
||||
class="btn-confirm"
|
||||
:disabled="loading || stake <= 0 || hasSuspendedSelection"
|
||||
@click="submit"
|
||||
>
|
||||
<span v-if="loading" class="btn-spinner"></span>
|
||||
{{ submitButtonLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="success-icon">✓</div>
|
||||
<div class="success-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M5 12.5L10 17.5L19 7.5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</div>
|
||||
<h3 class="success-title">{{ t('bet.outright_success') }}</h3>
|
||||
|
||||
<div class="hero hero--compact">
|
||||
<img v-if="flagUrl" :src="flagUrl" alt="" class="flag" />
|
||||
<p class="team">{{ pick.teamName }}</p>
|
||||
<span class="odds-badge">@ {{ formatOdds(pick.odds) }}</span>
|
||||
<div class="hero-info">
|
||||
<p class="team">{{ pick.teamName }}</p>
|
||||
<p class="event-title">{{ pick.eventTitle }}</p>
|
||||
</div>
|
||||
<span class="odds-badge odds-badge--success">{{ formatOdds(pick.odds) }}</span>
|
||||
</div>
|
||||
|
||||
<p class="event-title">{{ pick.eventTitle }}</p>
|
||||
|
||||
<div class="summary">
|
||||
<div class="summary-row">
|
||||
<span>{{ t('bet.outright_stake_amount') }}</span>
|
||||
<span>{{ formatMoney(successStake, locale) }}</span>
|
||||
<span class="summary-label">{{ t('bet.outright_stake_amount') }}</span>
|
||||
<span class="summary-value">{{ formatMoney(successStake, locale) }}</span>
|
||||
</div>
|
||||
<div class="summary-row">
|
||||
<span>{{ t('bet.outright_balance') }}</span>
|
||||
<span class="balance-value">{{ formatMoney(successBalance, locale) }}</span>
|
||||
<span class="summary-label">{{ t('bet.outright_balance') }}</span>
|
||||
<span class="summary-value balance-value">{{ formatMoney(successBalance, locale) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-done btn-gold-outline" @click="close">
|
||||
<button type="button" class="btn-done" @click="close">
|
||||
{{ t('bet.outright_done') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<BetSuccessOverlay :show="showSuccess" @done="showSuccess = false" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Transition ── */
|
||||
.modal-fade-enter-active {
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
.modal-fade-enter-active .modal {
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.25s ease;
|
||||
}
|
||||
.modal-fade-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
.modal-fade-enter-from .modal {
|
||||
transform: scale(0.92) translateY(12px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* ── Overlay ── */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
background: rgba(0, 0, 0, 0.72);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(4px);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
/* ── Modal ── */
|
||||
.modal {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px 16px 16px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 20px 16px 16px;
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow), var(--glow-gold);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
/* ── Close button ── */
|
||||
.close-x {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
@@ -398,31 +436,54 @@ function formatOdds(odds: string) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.15s, background 0.15s;
|
||||
}
|
||||
|
||||
.close-x:hover {
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.modal-header {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ── Hero ── */
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 12px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 8px;
|
||||
background: rgba(244, 162, 97, 0.04);
|
||||
border: 1px solid rgba(244, 162, 97, 0.1);
|
||||
}
|
||||
|
||||
.hero--compact {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.hero-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.flag {
|
||||
@@ -430,25 +491,41 @@ function formatOdds(odds: string) {
|
||||
height: 32px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.team {
|
||||
font-size: 20px;
|
||||
font-weight: 900;
|
||||
color: var(--primary-light);
|
||||
color: var(--text);
|
||||
letter-spacing: 0.02em;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* ── Odds badge ── */
|
||||
.odds-badge {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
color: var(--text);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 12px;
|
||||
background: var(--warning);
|
||||
border: 1px solid rgba(255, 179, 0, 0.3);
|
||||
color: #FFFFFF;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
border-radius: 4px;
|
||||
border-radius: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.odds-old {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.65;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.odds-arrow {
|
||||
opacity: 0.7;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.odds-badge--up {
|
||||
@@ -466,6 +543,12 @@ function formatOdds(odds: string) {
|
||||
border-color: rgba(255, 184, 77, 0.35);
|
||||
}
|
||||
|
||||
.odds-badge--success {
|
||||
background: rgba(22, 101, 52, 0.85);
|
||||
border-color: rgba(110, 231, 160, 0.3);
|
||||
}
|
||||
|
||||
/* ── Odds warning ── */
|
||||
.odds-warning {
|
||||
margin: 0 0 10px;
|
||||
padding: 8px 10px;
|
||||
@@ -477,23 +560,23 @@ function formatOdds(odds: string) {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
/* ── Event title ── */
|
||||
.event-title {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.4;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* ── Balance ── */
|
||||
.balance-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
margin-bottom: 12px;
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
@@ -505,7 +588,12 @@ function formatOdds(odds: string) {
|
||||
.balance-value {
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* ── Stake section ── */
|
||||
.stake-section {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.stake-label {
|
||||
@@ -517,66 +605,90 @@ function formatOdds(odds: string) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.stake-input-wrap {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stake-input {
|
||||
width: 100%;
|
||||
padding: 11px 12px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.stake-input:focus {
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.25);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(244, 162, 97, 0.15);
|
||||
}
|
||||
|
||||
.stake-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ── Quick stakes ── */
|
||||
.quick-stakes {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
flex: 1;
|
||||
padding: 6px 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 2px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
background: var(--bg-card-elevated);
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.chip:active {
|
||||
border-color: var(--border-gold-soft);
|
||||
color: var(--primary-light);
|
||||
background: var(--surface-accent);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
background: rgba(244, 162, 97, 0.06);
|
||||
}
|
||||
|
||||
.chip-max {
|
||||
color: var(--primary-light);
|
||||
border-color: rgba(244, 162, 97, 0.2);
|
||||
}
|
||||
|
||||
/* ── Est return ── */
|
||||
.est-return {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 6px;
|
||||
background: rgba(244, 162, 97, 0.06);
|
||||
border: 1px solid rgba(244, 162, 97, 0.12);
|
||||
}
|
||||
|
||||
.est-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.est-value {
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-weight: 800;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* ── Error ── */
|
||||
.error {
|
||||
color: var(--danger);
|
||||
font-size: 12px;
|
||||
@@ -584,69 +696,115 @@ function formatOdds(odds: string) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* ── Actions ── */
|
||||
.actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr;
|
||||
grid-template-columns: 1fr 1.3fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
padding: 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-body);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.btn-cancel:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
background: var(--gradient-primary);
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
transition: opacity 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
.btn-confirm:hover:not(:disabled) {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.btn-confirm:active:not(:disabled) {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.btn-confirm:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.btn-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: btn-spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes btn-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ── Success ── */
|
||||
.success-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 0 auto 8px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
margin: 0 auto 10px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--success);
|
||||
background: rgba(46, 204, 113, 0.12);
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: var(--success);
|
||||
font-size: 24px;
|
||||
font-weight: 900;
|
||||
line-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
margin-bottom: 10px;
|
||||
color: var(--text);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
margin: 12px 0 14px;
|
||||
padding: 10px;
|
||||
background: var(--bg-body);
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.summary-row + .summary-row {
|
||||
margin-top: 4px;
|
||||
padding-top: 8px;
|
||||
@@ -656,8 +814,214 @@ function formatOdds(odds: string) {
|
||||
.btn-done {
|
||||
width: 100%;
|
||||
padding: 11px;
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
background: var(--gradient-primary);
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
transition: opacity 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
.btn-done:hover {
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.btn-done:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════
|
||||
Desktop / PC styles (≥ 768px)
|
||||
════════════════════════════════════════════ */
|
||||
@media (min-width: 768px) {
|
||||
.overlay {
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
max-width: 420px;
|
||||
border-radius: 14px;
|
||||
padding: 24px 24px 20px;
|
||||
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(244, 162, 97, 0.06);
|
||||
}
|
||||
|
||||
.close-x {
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Hero — horizontal layout on desktop */
|
||||
.hero {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.flag {
|
||||
width: 52px;
|
||||
height: 36px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.hero-info {
|
||||
flex: 1;
|
||||
align-items: flex-start;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.team {
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.event-title {
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.odds-badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
padding: 5px 14px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.odds-warning {
|
||||
margin-bottom: 12px;
|
||||
padding: 9px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.balance-row {
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.stake-section {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.stake-label {
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stake-input-wrap {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.stake-input {
|
||||
padding: 12px 14px;
|
||||
font-size: 20px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.quick-stakes {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
padding: 8px 4px;
|
||||
font-size: 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chip:hover {
|
||||
border-color: rgba(244, 162, 97, 0.4);
|
||||
color: var(--primary-light);
|
||||
background: rgba(244, 162, 97, 0.06);
|
||||
}
|
||||
|
||||
.est-return {
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.est-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.est-value {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-cancel,
|
||||
.btn-confirm {
|
||||
padding: 12px;
|
||||
font-size: 15px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Success state desktop */
|
||||
.success-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.success-icon svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 18px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
margin: 14px 0 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn-done {
|
||||
padding: 13px;
|
||||
font-size: 15px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -55,7 +55,7 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="event-block" :class="{ expanded }">
|
||||
<section class="event-block">
|
||||
<button type="button" class="event-head" :class="{ 'is-expanded': expanded, 'is-settled': isSettled }" :aria-expanded="expanded" @click="emit('toggle')">
|
||||
<span class="toggle-icon" :class="{ open: expanded }">
|
||||
<span class="toggle-mark">{{ expanded ? '−' : '+' }}</span>
|
||||
@@ -97,9 +97,10 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
isolation: isolate;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-card);
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.event-head {
|
||||
@@ -110,7 +111,7 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: var(--bg-elevated);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
text-align: left;
|
||||
@@ -122,7 +123,7 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
|
||||
opacity: 0.25;
|
||||
opacity: 0.04;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -132,8 +133,8 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -141,11 +142,11 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
}
|
||||
|
||||
.toggle-icon.open {
|
||||
border-color: var(--primary-light);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.toggle-mark {
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-size: 17px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
@@ -170,8 +171,8 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
flex-shrink: 0;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
border: 1px solid color-mix(in srgb, var(--primary-light) 45%, transparent);
|
||||
color: var(--warning);
|
||||
border: 1px solid color-mix(in srgb, var(--warning) 45%, transparent);
|
||||
border-radius: 999px;
|
||||
padding: 1px 7px;
|
||||
line-height: 1.4;
|
||||
@@ -180,7 +181,7 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
.event-title {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
@@ -195,7 +196,6 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.event-saishi {
|
||||
@@ -212,8 +212,7 @@ const isSettled = computed(() => props.event.bettingOpen === false || props.even
|
||||
max-height: 408px;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 12px 8px 8px;
|
||||
background: var(--bg-card);
|
||||
padding: 8px 2px 2px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ onUnmounted(() => {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 8px 4px 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
@@ -94,18 +94,12 @@ onUnmounted(() => {
|
||||
gap: 4px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.option-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.06) 0%, transparent 50%, rgba(0, 51, 102, 0.08) 100%);
|
||||
pointer-events: none;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.option-card:active {
|
||||
border-color: var(--border-gold-soft);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 1px rgba(244, 162, 97, 0.12);
|
||||
}
|
||||
|
||||
.option-card--disabled {
|
||||
@@ -115,11 +109,12 @@ onUnmounted(() => {
|
||||
|
||||
.option-card--disabled:active {
|
||||
border-color: var(--border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.option-card--winner {
|
||||
border-color: color-mix(in srgb, var(--primary-light) 75%, transparent);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary-light) 25%, transparent);
|
||||
border-color: color-mix(in srgb, var(--warning) 75%, transparent);
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--warning) 25%, transparent);
|
||||
}
|
||||
|
||||
.flag {
|
||||
@@ -142,7 +137,7 @@ onUnmounted(() => {
|
||||
.name {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--text);
|
||||
line-height: 1.2;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
@@ -153,6 +148,6 @@ onUnmounted(() => {
|
||||
.odds {
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--warning);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -174,7 +174,7 @@ function closeModal() {
|
||||
|
||||
<style scoped>
|
||||
.outright-panel {
|
||||
padding: 4px 0 0;
|
||||
padding: 4px 12px 0;
|
||||
}
|
||||
|
||||
.panel-summary {
|
||||
@@ -190,11 +190,11 @@ function closeModal() {
|
||||
margin: 0 0 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--primary-light) 8%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--primary-light) 22%, transparent);
|
||||
background: color-mix(in srgb, var(--warning) 8%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--warning) 22%, transparent);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-light);
|
||||
color: var(--warning);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,35 +10,32 @@ const placement = ref<'top' | 'bottom'>('bottom');
|
||||
let closeTimer: number | undefined = undefined;
|
||||
|
||||
export function useDesktopBetPopover() {
|
||||
function openAt(event: MouseEvent, item: SlipItem) {
|
||||
function openAt(event: MouseEvent | undefined, item: SlipItem) {
|
||||
if (!event) return;
|
||||
cancelClose();
|
||||
|
||||
|
||||
const el = (event.currentTarget || event.target) as HTMLElement;
|
||||
const btn = el?.closest?.('button') || el;
|
||||
const rect = btn?.getBoundingClientRect?.();
|
||||
if (!rect) return;
|
||||
|
||||
const popW = 190;
|
||||
const popH = 170;
|
||||
const pad = 12; // increased pad to leave space for the arrow pointer
|
||||
const popW = 172;
|
||||
const popH = 155;
|
||||
const pad = 12;
|
||||
|
||||
// Calculate center X of the button
|
||||
const btnCenterX = rect.left + rect.width / 2;
|
||||
let x = btnCenterX - popW / 2;
|
||||
|
||||
// Default: place below button (center bottom)
|
||||
let y = rect.bottom + pad;
|
||||
placement.value = 'bottom';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const spaceBelow = window.innerHeight - rect.bottom;
|
||||
if (spaceBelow < popH + pad) {
|
||||
// Not enough space below, place above button (center top)
|
||||
y = rect.top - popH - pad;
|
||||
placement.value = 'top';
|
||||
}
|
||||
|
||||
// Keep within viewport boundaries
|
||||
x = Math.max(pad, Math.min(x, window.innerWidth - popW - pad));
|
||||
y = Math.max(pad, Math.min(y, window.innerHeight - popH - pad));
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { ref, onUnmounted } from 'vue';
|
||||
|
||||
export function useHoverDropdown(delay = 200) {
|
||||
const open = ref(false);
|
||||
let closeTimer: number | undefined;
|
||||
|
||||
function cancelClose() {
|
||||
if (closeTimer !== undefined) {
|
||||
window.clearTimeout(closeTimer);
|
||||
closeTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function openHover() {
|
||||
cancelClose();
|
||||
open.value = true;
|
||||
}
|
||||
|
||||
function scheduleClose() {
|
||||
cancelClose();
|
||||
closeTimer = window.setTimeout(() => {
|
||||
open.value = false;
|
||||
closeTimer = undefined;
|
||||
}, delay);
|
||||
}
|
||||
|
||||
function closeNow() {
|
||||
cancelClose();
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
cancelClose();
|
||||
});
|
||||
|
||||
return {
|
||||
open,
|
||||
openHover,
|
||||
scheduleClose,
|
||||
closeNow,
|
||||
};
|
||||
}
|
||||
@@ -27,11 +27,19 @@ export default {
|
||||
copy: 'Copy',
|
||||
copy_success: 'Copied',
|
||||
},
|
||||
pagination: {
|
||||
total: '{total} total',
|
||||
per_page: '{size} / page',
|
||||
nav: {
|
||||
home: 'Home',
|
||||
bet: 'Bet',
|
||||
bet_history: 'History',
|
||||
wallet: 'Wallet',
|
||||
profile: 'Profile',
|
||||
sports: 'Sports',
|
||||
football: 'Football',
|
||||
account_hub: 'Bets & Wallet',
|
||||
search: 'Search teams, leagues…',
|
||||
announcements: 'Announcements',
|
||||
sports_more_soon: 'More sports coming soon',
|
||||
},
|
||||
nav: { home: 'Home', sports: 'Sports', football: 'Football', bet: 'Bet', bet_history: 'History', wallet: 'Transactions', account_hub: 'Bets & Wallet', profile: 'Profile', search: 'Search teams, leagues…', announcements: 'Announcements', sports_more_soon: 'More sports coming soon' },
|
||||
home: {
|
||||
hot_matches: 'Hot matches',
|
||||
hot_tab: 'Hot',
|
||||
@@ -39,8 +47,7 @@ export default {
|
||||
no_matches: 'No matches',
|
||||
upcoming_empty: 'No matches kicking off in the next 3 days',
|
||||
announcement_badge: 'Notice',
|
||||
announcement_default:
|
||||
'Welcome to TheBet365 · Football events are live · Bet responsibly',
|
||||
announcement_default: 'Welcome to TheBet365 · Football events are live · Bet responsibly',
|
||||
banner_prev: 'Previous slide',
|
||||
banner_next: 'Next slide',
|
||||
banner_slide: 'Slide {n}',
|
||||
@@ -64,7 +71,6 @@ export default {
|
||||
empty: 'No announcements yet',
|
||||
not_found: 'This announcement is unavailable',
|
||||
view_all: 'View all announcements',
|
||||
select_hint: 'Select an announcement to read',
|
||||
type_notice: 'Notice',
|
||||
type_ticker: 'Ticker',
|
||||
type_banner: 'Banner',
|
||||
@@ -72,6 +78,7 @@ export default {
|
||||
go_link: 'Go to page',
|
||||
open_link: 'Open link',
|
||||
back: 'Back',
|
||||
select_hint: 'Select an announcement to read',
|
||||
center_desc: 'Platform notices, promotions, and maintenance updates',
|
||||
total_count: '{count} total',
|
||||
read_more: 'Read more',
|
||||
@@ -96,7 +103,6 @@ export default {
|
||||
not_found: 'Message not found',
|
||||
view_all: 'Back to inbox',
|
||||
back: 'Back',
|
||||
select_hint: 'Select a message to read',
|
||||
mark_all_read: 'Mark all read',
|
||||
load_more: 'Load more',
|
||||
delete: 'Delete',
|
||||
@@ -116,6 +122,7 @@ export default {
|
||||
view_recharge_history: 'View recharge history',
|
||||
unread_badge: '{count} unread',
|
||||
open_inbox: 'Open inbox',
|
||||
select_hint: 'Select a message to read',
|
||||
tab_inbox: 'Inbox',
|
||||
tab_support: 'Support',
|
||||
},
|
||||
@@ -155,9 +162,6 @@ export default {
|
||||
filter_lost: 'Lost',
|
||||
filter_pending: 'Pending',
|
||||
filter_push: 'Push',
|
||||
filter_status_group: 'Settlement',
|
||||
filter_push_group: 'Special',
|
||||
filter_push_hint: 'Push: stake refunded on a draw line',
|
||||
stats_total: 'Total',
|
||||
stats_won: 'Won',
|
||||
stats_lost: 'Lost',
|
||||
@@ -169,10 +173,12 @@ export default {
|
||||
profit: 'Profit',
|
||||
return_incl_stake: 'Return {amount} (incl. stake)',
|
||||
stake_to_return: '{stake} × {odds} = {amount}',
|
||||
filter_status_group: 'Settlement',
|
||||
filter_push_group: 'Special',
|
||||
filter_push_hint: 'Push: stake refunded on a draw line',
|
||||
},
|
||||
auth:
|
||||
{ login: 'Login',
|
||||
logging_in: 'Logging in…',
|
||||
auth: {
|
||||
login: 'Login',
|
||||
register: 'Create Account',
|
||||
logout: 'Log out',
|
||||
username: 'Username',
|
||||
@@ -231,6 +237,7 @@ export default {
|
||||
reset_failed: 'Reset failed. Please try again.',
|
||||
phone_not_registered: 'This phone number is not registered',
|
||||
back_to_login: 'Back to login',
|
||||
logging_in: 'Logging in…',
|
||||
},
|
||||
support: {
|
||||
short: 'Support',
|
||||
@@ -291,6 +298,9 @@ export default {
|
||||
detail_bet_link: 'View Bet',
|
||||
detail_tx_id: 'Transaction ID',
|
||||
detail_not_found: 'Transaction not found',
|
||||
all_records: 'All Records',
|
||||
frozen_before: 'Frozen Before',
|
||||
frozen_after: 'Frozen After',
|
||||
ref_bet: 'Bet',
|
||||
ref_deposit: 'Deposit',
|
||||
ref_withdraw: 'Withdraw',
|
||||
@@ -316,7 +326,6 @@ export default {
|
||||
},
|
||||
recharge: {
|
||||
title: 'Recharge',
|
||||
scan_to_pay: 'Scan QR code to pay',
|
||||
history: 'History',
|
||||
history_title: 'Recharge History',
|
||||
bank_transfer: 'Bank Transfer',
|
||||
@@ -370,6 +379,7 @@ export default {
|
||||
reapply: 'Re-apply',
|
||||
reapply_hint: 'You are resubmitting this order. Transfer again and upload a new screenshot.',
|
||||
back_to_history: 'Back to history',
|
||||
scan_to_pay: 'Scan QR code to pay',
|
||||
order_no: 'Order no.',
|
||||
method: 'Method',
|
||||
amount: 'Amount',
|
||||
@@ -401,38 +411,11 @@ export default {
|
||||
stake: 'Stake',
|
||||
place_bet: 'Place Bet',
|
||||
place_bet_short: 'Bet',
|
||||
quick_bet: 'Quick Bet',
|
||||
popular_teams: 'Popular Teams',
|
||||
view_all_markets: 'View All Markets',
|
||||
all: 'All',
|
||||
parlay: 'Parlay',
|
||||
tab_matches: 'Matches',
|
||||
tab_outright: 'Outright',
|
||||
sport_football: 'Football',
|
||||
sport_basketball: 'Basketball',
|
||||
sport_tennis: 'Tennis',
|
||||
sport_coming_soon: 'Coming soon',
|
||||
tab_today: 'Today',
|
||||
tab_early: 'Early',
|
||||
filter_time: 'Time',
|
||||
filter_league: 'Leagues',
|
||||
filter_market: 'Markets',
|
||||
time_all: 'All',
|
||||
time_today: 'Today',
|
||||
time_early: 'Early',
|
||||
market_all: 'All markets',
|
||||
market_handicap_ou: 'Handicap / O-U',
|
||||
correct_score: 'Correct score',
|
||||
no_markets: 'No markets available',
|
||||
no_markets_filtered: 'No markets match the current filter',
|
||||
no_market_odds: 'No odds available',
|
||||
popover_title: 'Confirm bet',
|
||||
place_now: 'Bet now',
|
||||
add_to_single: 'Add to singles',
|
||||
add_to_parlay: 'Add to parlay',
|
||||
slip_parlay_added_short: 'In parlay · View',
|
||||
add_to_single_list: 'Add to singles',
|
||||
add_to_parlay_list: 'Add to parlay',
|
||||
filter_status_all: 'All',
|
||||
filter_status_open: 'Open',
|
||||
filter_status_settled: 'Settled',
|
||||
@@ -467,7 +450,6 @@ export default {
|
||||
outright_load_more: 'Load more',
|
||||
outright_settled: 'Settled',
|
||||
outright_settled_hint: 'This event is settled. Odds and results are view-only.',
|
||||
outright_view_all: 'View All Teams',
|
||||
cancel: 'Cancel',
|
||||
parlay_max_legs: 'Parlay allows up to 5 legs',
|
||||
parlay_block_outright: 'Outright cannot be parlayed',
|
||||
@@ -545,37 +527,24 @@ export default {
|
||||
slip_pick_hint: 'Tap to add/remove from slip; gold border = selected',
|
||||
picked_tag: 'Selected',
|
||||
pick_added: 'Added to bet slip',
|
||||
added_to_single: 'Added to singles list',
|
||||
added_to_parlay: 'Added to parlay list',
|
||||
pick_removed: 'Removed from bet slip',
|
||||
slip_bar_ready: '1 selection',
|
||||
slip_bar_go: 'Bet slip',
|
||||
cs_top_hint: '① Enter stake ② Tap Confirm bet above',
|
||||
slip_empty_hint: 'Tap odds to open the bet drawer',
|
||||
slip_clear: 'Clear all',
|
||||
slip_remove: 'Remove',
|
||||
slip_locate: 'Locate odds',
|
||||
slip_locate_aria: 'Scroll to this selection in the odds list',
|
||||
slip_est_return: 'Est. max payout',
|
||||
slip_parlay_odds: 'Combined odds {odds}',
|
||||
slip_review_title: 'Review selection',
|
||||
slip_balance: 'Account balance',
|
||||
slip_tab_single: 'Single',
|
||||
slip_tab_parlay: 'Parlay',
|
||||
slip_tab_history: 'History',
|
||||
slip_history_all: 'All',
|
||||
slip_history_match: 'This match',
|
||||
slip_history_view_all: 'View all',
|
||||
slip_history_load_more: 'Load more',
|
||||
slip_parlay_empty_hint: 'Pick a selection, then tap Add to parlay',
|
||||
slip_add_parlay: 'Add to parlay',
|
||||
slip_parlay_only_hint: 'This market is parlay-only. Tap Add to parlay to bet.',
|
||||
slip_parlay_same_match: 'Parlay selections cannot be from the same match. Remove the same-match leg first.',
|
||||
slip_parlay_count: '{n} parlay leg(s)',
|
||||
slip_total_stake: 'Total stake',
|
||||
slip_place_single: 'Single bet',
|
||||
slip_place_all: 'Bet all',
|
||||
slip_parlay_added_btn: 'Added',
|
||||
slip_currency: 'Amount',
|
||||
slip_min: 'Min',
|
||||
slip_min_error: 'Minimum stake is {amount}',
|
||||
@@ -584,15 +553,53 @@ export default {
|
||||
accept_changes_place: 'Accept changes & place bet',
|
||||
odds_was: 'Was',
|
||||
odds_now: 'Now',
|
||||
place_confirm_title: 'Confirm bet',
|
||||
place_confirm_single:
|
||||
"Selection: {pick} ({'@'}{odds})\nStake: {stake}\nEst. return: {return}\n\nPlace this bet?",
|
||||
place_confirm_singles:
|
||||
'{count} single bet(s)\nTotal stake: {stake}\nEst. return: {return}\n\nPlace all?',
|
||||
place_confirm_parlay:
|
||||
'Parlay ({count} legs), total odds {odds}\nStake: {stake}\nEst. return: {return}\n\nPlace parlay?',
|
||||
place_success: 'Bet placed',
|
||||
place_failed: 'Bet failed',
|
||||
quick_bet: 'Quick Bet',
|
||||
popular_teams: 'Popular Teams',
|
||||
view_all_markets: 'View All Markets',
|
||||
all: 'All',
|
||||
sport_football: 'Football',
|
||||
sport_basketball: 'Basketball',
|
||||
sport_tennis: 'Tennis',
|
||||
sport_coming_soon: 'Coming soon',
|
||||
filter_time: 'Time',
|
||||
filter_league: 'Leagues',
|
||||
filter_market: 'Markets',
|
||||
time_all: 'All',
|
||||
time_today: 'Today',
|
||||
time_early: 'Early',
|
||||
market_all: 'All markets',
|
||||
market_handicap_ou: 'Handicap / O-U',
|
||||
correct_score: 'Correct score',
|
||||
no_markets: 'No markets available',
|
||||
no_markets_filtered: 'No markets match the current filter',
|
||||
no_market_odds: 'No odds available',
|
||||
popover_title: 'Confirm bet',
|
||||
place_now: 'Bet now',
|
||||
add_to_single: 'Add to singles',
|
||||
add_to_parlay: 'Add to parlay',
|
||||
slip_parlay_added_short: 'In parlay · View',
|
||||
add_to_single_list: 'Add to singles',
|
||||
add_to_parlay_list: 'Add to parlay',
|
||||
outright_view_all: 'View All Teams',
|
||||
added_to_single: 'Added to singles list',
|
||||
added_to_parlay: 'Added to parlay list',
|
||||
slip_clear: 'Clear all',
|
||||
slip_locate: 'Locate odds',
|
||||
slip_locate_aria: 'Scroll to this selection in the odds list',
|
||||
slip_tab_history: 'History',
|
||||
slip_history_all: 'All',
|
||||
slip_history_match: 'This match',
|
||||
slip_history_view_all: 'View all',
|
||||
slip_history_load_more: 'Load more',
|
||||
slip_place_single: 'Single bet',
|
||||
slip_place_all: 'Bet all',
|
||||
slip_parlay_added_btn: 'Added',
|
||||
place_confirm_title: 'Confirm bet',
|
||||
place_confirm_single: 'Selection: {pick} ({\'@\'}{odds})\nStake: {stake}\nEst. return: {return}\n\nPlace this bet?',
|
||||
place_confirm_singles: '{count} single bet(s)\nTotal stake: {stake}\nEst. return: {return}\n\nPlace all?',
|
||||
place_confirm_parlay: 'Parlay ({count} legs), total odds {odds}\nStake: {stake}\nEst. return: {return}\n\nPlace parlay?',
|
||||
bet_no: 'Bet no.',
|
||||
match: 'Match',
|
||||
selection: 'Selection',
|
||||
@@ -604,15 +611,6 @@ export default {
|
||||
},
|
||||
profile: {
|
||||
edit: 'Edit Profile',
|
||||
edit_title: 'Edit profile',
|
||||
settings: 'Settings',
|
||||
save_success: 'Saved successfully',
|
||||
basic_info: 'Basic info',
|
||||
change_avatar: 'Change avatar',
|
||||
username: 'Username',
|
||||
username_locked: 'Username cannot be changed',
|
||||
account: 'Account',
|
||||
quick_nav: 'Quick links',
|
||||
language: 'Language',
|
||||
avatar: 'Avatar',
|
||||
avatar_change: 'Change avatar',
|
||||
@@ -657,5 +655,17 @@ export default {
|
||||
rules_p3: 'Results use admin-entered half-time and full-time scores; payouts apply after settlement preview is confirmed.',
|
||||
rules_p4: 'If this text conflicts with site notices, the latest notice and market rules prevail.',
|
||||
rules_p5: 'How to bet: open any match, tap the ? icon on the top right.',
|
||||
edit_title: 'Edit profile',
|
||||
save_success: 'Saved successfully',
|
||||
basic_info: 'Basic info',
|
||||
change_avatar: 'Change avatar',
|
||||
username: 'Username',
|
||||
username_locked: 'Username cannot be changed',
|
||||
account: 'Account',
|
||||
quick_nav: 'Quick links',
|
||||
},
|
||||
pagination: {
|
||||
total: '{total} total',
|
||||
per_page: '{size} / page',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -27,19 +27,15 @@ export default {
|
||||
copy: 'Salin',
|
||||
copy_success: 'Berjaya disalin',
|
||||
},
|
||||
pagination: {
|
||||
total: '{total} jumlah',
|
||||
per_page: '{size} / halaman',
|
||||
},
|
||||
nav: {
|
||||
home: 'Laman Utama',
|
||||
sports: 'Sukan',
|
||||
football: 'Bola Sepak',
|
||||
bet: 'Pertaruhan',
|
||||
bet_history: 'Sejarah',
|
||||
wallet: 'Transaksi',
|
||||
account_hub: 'Pertaruhan & Bil',
|
||||
wallet: 'Bil',
|
||||
profile: 'Profil',
|
||||
sports: 'Sukan',
|
||||
football: 'Bola Sepak',
|
||||
account_hub: 'Pertaruhan & Bil',
|
||||
search: 'Cari pasukan, liga…',
|
||||
announcements: 'Pengumuman',
|
||||
sports_more_soon: 'Lebih banyak sukan akan datang',
|
||||
@@ -51,8 +47,7 @@ export default {
|
||||
no_matches: 'Tiada perlawanan',
|
||||
upcoming_empty: 'Tiada perlawanan dalam 3 hari akan datang',
|
||||
announcement_badge: 'Notis',
|
||||
announcement_default:
|
||||
'Selamat datang ke TheBet365 · Perlawanan bola sepak sedang berlangsung · Bertaruh secara bertanggungjawab',
|
||||
announcement_default: 'Selamat datang ke TheBet365 · Perlawanan bola sepak sedang berlangsung · Bertaruh secara bertanggungjawab',
|
||||
banner_prev: 'Slaid sebelumnya',
|
||||
banner_next: 'Slaid seterusnya',
|
||||
banner_slide: 'Slaid {n}',
|
||||
@@ -76,7 +71,6 @@ export default {
|
||||
empty: 'Tiada pengumuman',
|
||||
not_found: 'Pengumuman tidak tersedia',
|
||||
view_all: 'Lihat semua pengumuman',
|
||||
select_hint: 'Pilih pengumuman untuk dibaca',
|
||||
type_notice: 'Notis',
|
||||
type_ticker: 'Ticker',
|
||||
type_banner: 'Banner',
|
||||
@@ -84,6 +78,7 @@ export default {
|
||||
go_link: 'Pergi ke halaman',
|
||||
open_link: 'Buka pautan',
|
||||
back: 'Kembali',
|
||||
select_hint: 'Pilih pengumuman untuk dibaca',
|
||||
center_desc: 'Notis platform, promosi dan penyelenggaraan',
|
||||
total_count: '{count} jumlah',
|
||||
read_more: 'Baca lagi',
|
||||
@@ -108,7 +103,6 @@ export default {
|
||||
not_found: 'Mesej tidak dijumpai',
|
||||
view_all: 'Kembali ke peti mesej',
|
||||
back: 'Kembali',
|
||||
select_hint: 'Pilih mesej untuk dibaca',
|
||||
mark_all_read: 'Tanda semua dibaca',
|
||||
load_more: 'Muat lagi',
|
||||
delete: 'Padam',
|
||||
@@ -128,6 +122,7 @@ export default {
|
||||
view_recharge_history: 'Lihat sejarah deposit',
|
||||
unread_badge: '{count} belum dibaca',
|
||||
open_inbox: 'Buka peti mesej',
|
||||
select_hint: 'Pilih mesej untuk dibaca',
|
||||
tab_inbox: 'Peti masuk',
|
||||
tab_support: 'Sokongan',
|
||||
},
|
||||
@@ -167,9 +162,6 @@ export default {
|
||||
filter_lost: 'Kalah',
|
||||
filter_pending: 'Menunggu',
|
||||
filter_push: 'Seri',
|
||||
filter_status_group: 'Penyelesaian',
|
||||
filter_push_group: 'Khas',
|
||||
filter_push_hint: 'Seri: jumlah dikembalikan pada garis seri',
|
||||
stats_total: 'Jumlah',
|
||||
stats_won: 'Menang',
|
||||
stats_lost: 'Kalah',
|
||||
@@ -181,10 +173,12 @@ export default {
|
||||
profit: 'Keuntungan',
|
||||
return_incl_stake: 'Pulangan {amount} (termasuk jumlah)',
|
||||
stake_to_return: '{stake} × {odds} = {amount}',
|
||||
filter_status_group: 'Penyelesaian',
|
||||
filter_push_group: 'Khas',
|
||||
filter_push_hint: 'Seri: jumlah dikembalikan pada garis seri',
|
||||
},
|
||||
auth: {
|
||||
login: 'Log Masuk',
|
||||
logging_in: 'Sedang log masuk…',
|
||||
register: 'Daftar Akaun',
|
||||
logout: 'Log Keluar',
|
||||
username: 'Nama Pengguna',
|
||||
@@ -243,6 +237,7 @@ export default {
|
||||
reset_failed: 'Gagal menetapkan semula. Sila cuba lagi.',
|
||||
phone_not_registered: 'Nombor telefon ini belum didaftarkan',
|
||||
back_to_login: 'Kembali ke log masuk',
|
||||
logging_in: 'Sedang log masuk…',
|
||||
},
|
||||
support: {
|
||||
short: 'Sokongan',
|
||||
@@ -303,6 +298,9 @@ export default {
|
||||
detail_bet_link: 'Lihat Pertaruhan',
|
||||
detail_tx_id: 'ID Transaksi',
|
||||
detail_not_found: 'Rekod tidak dijumpai',
|
||||
all_records: 'Semua Rekod',
|
||||
frozen_before: 'Beku Sebelum',
|
||||
frozen_after: 'Beku Selepas',
|
||||
ref_bet: 'Pertaruhan',
|
||||
ref_deposit: 'Deposit',
|
||||
ref_withdraw: 'Pengeluaran',
|
||||
@@ -328,7 +326,6 @@ export default {
|
||||
},
|
||||
recharge: {
|
||||
title: 'Topup',
|
||||
scan_to_pay: 'Imbas kod QR untuk bayar',
|
||||
history: 'Sejarah',
|
||||
history_title: 'Sejarah Topup',
|
||||
bank_transfer: 'Pindahan Bank',
|
||||
@@ -382,6 +379,7 @@ export default {
|
||||
reapply: 'Mohon semula',
|
||||
reapply_hint: 'Anda menghantar semula pesanan ini. Buat pindahan baharu dan muat naik tangkapan skrin baharu.',
|
||||
back_to_history: 'Kembali ke sejarah',
|
||||
scan_to_pay: 'Imbas kod QR untuk bayar',
|
||||
order_no: 'No. pesanan',
|
||||
method: 'Kaedah',
|
||||
amount: 'Amaun',
|
||||
@@ -413,38 +411,11 @@ export default {
|
||||
stake: 'Jumlah',
|
||||
place_bet: 'Letak Pertaruhan',
|
||||
place_bet_short: 'Pertaruhan',
|
||||
quick_bet: 'Pertaruhan Pantas',
|
||||
popular_teams: 'Pasukan Popular',
|
||||
view_all_markets: 'Lihat Semua Pasaran',
|
||||
all: 'Semua',
|
||||
parlay: 'Berganda',
|
||||
tab_matches: 'Perlawanan',
|
||||
tab_outright: 'Juara',
|
||||
sport_football: 'Bola Sepak',
|
||||
sport_basketball: 'Bola Keranjang',
|
||||
sport_tennis: 'Tenis',
|
||||
sport_coming_soon: 'Akan datang',
|
||||
tab_today: 'Hari Ini',
|
||||
tab_early: 'Awal',
|
||||
filter_time: 'Masa',
|
||||
filter_league: 'Liga',
|
||||
filter_market: 'Pasaran',
|
||||
time_all: 'Semua',
|
||||
time_today: 'Hari ini',
|
||||
time_early: 'Awal',
|
||||
market_all: 'Semua pasaran',
|
||||
market_handicap_ou: 'Handicap / O-U',
|
||||
correct_score: 'Skor tepat',
|
||||
no_markets: 'Tiada pasaran tersedia',
|
||||
no_markets_filtered: 'Tiada pasaran sepadan dengan penapis semasa',
|
||||
no_market_odds: 'Tiada odds pasaran',
|
||||
popover_title: 'Sahkan pertaruhan',
|
||||
place_now: 'Pertaruhan sekarang',
|
||||
add_to_single: 'Tambah tunggal',
|
||||
add_to_parlay: 'Tambah parlay',
|
||||
slip_parlay_added_short: 'Dalam parlay · Lihat',
|
||||
add_to_single_list: 'Tambah ke senarai tunggal',
|
||||
add_to_parlay_list: 'Tambah ke berganda',
|
||||
filter_status_all: 'Semua',
|
||||
filter_status_open: 'Buka',
|
||||
filter_status_settled: 'Selesai',
|
||||
@@ -479,7 +450,6 @@ export default {
|
||||
outright_load_more: 'Muat lagi',
|
||||
outright_settled: 'Selesai',
|
||||
outright_settled_hint: 'Acara ini telah diselesaikan. Hanya paparan odds dan keputusan.',
|
||||
outright_view_all: 'Lihat Semua Pasukan',
|
||||
cancel: 'Batal',
|
||||
parlay_max_legs: 'Maksimum 5 pilihan parlay',
|
||||
parlay_block_outright: 'Outright tidak boleh parlay',
|
||||
@@ -557,37 +527,24 @@ export default {
|
||||
slip_pick_hint: 'Ketik untuk tambah/buang; sisi emas = dipilih',
|
||||
picked_tag: 'Dipilih',
|
||||
pick_added: 'Ditambah ke slip',
|
||||
added_to_single: 'Ditambah ke senarai tunggal',
|
||||
added_to_parlay: 'Ditambah ke senarai parlay',
|
||||
pick_removed: 'Dikeluarkan dari slip',
|
||||
slip_bar_ready: '1 pilihan',
|
||||
slip_bar_go: 'Buka slip',
|
||||
cs_top_hint: '① Isi jumlah ② Ketik Sahkan di atas',
|
||||
slip_empty_hint: 'Ketik odds untuk buka laci pertaruhan',
|
||||
slip_clear: 'Kosongkan semua',
|
||||
slip_remove: 'Buang',
|
||||
slip_locate: 'Lokasi odds',
|
||||
slip_locate_aria: 'Tatal ke pilihan ini dalam senarai odds',
|
||||
slip_est_return: 'Anggaran bayaran maksimum',
|
||||
slip_parlay_odds: 'Odds gabungan {odds}',
|
||||
slip_review_title: 'Semak pilihan',
|
||||
slip_balance: 'Baki akaun',
|
||||
slip_tab_single: 'Tunggal',
|
||||
slip_tab_parlay: 'Parlay',
|
||||
slip_tab_history: 'Sejarah',
|
||||
slip_history_all: 'Semua',
|
||||
slip_history_match: 'Perlawanan ini',
|
||||
slip_history_view_all: 'Lihat semua',
|
||||
slip_history_load_more: 'Muat lagi',
|
||||
slip_parlay_empty_hint: 'Pilih satu odds, kemudian ketik Tambah ke parlay',
|
||||
slip_add_parlay: 'Tambah ke parlay',
|
||||
slip_parlay_only_hint: 'Pasaran ini hanya parlay. Ketik Tambah ke parlay untuk bertaruh.',
|
||||
slip_parlay_same_match: 'Pilihan parlay tidak boleh daripada perlawanan sama. Buang pilihan perlawanan sama dahulu.',
|
||||
slip_parlay_count: '{n} pilihan parlay',
|
||||
slip_total_stake: 'Jumlah pertaruhan',
|
||||
slip_place_single: 'Taruhan tunggal',
|
||||
slip_place_all: 'Taruhan semua',
|
||||
slip_parlay_added_btn: 'Ditambah',
|
||||
slip_currency: 'Amaun',
|
||||
slip_min: 'Min',
|
||||
slip_min_error: 'Jumlah minimum ialah {amount}',
|
||||
@@ -596,15 +553,53 @@ export default {
|
||||
accept_changes_place: 'Terima perubahan & pertaruh',
|
||||
odds_was: 'Asal',
|
||||
odds_now: 'Baharu',
|
||||
place_confirm_title: 'Sahkan pertaruhan',
|
||||
place_confirm_single:
|
||||
"Pilihan: {pick} ({'@'}{odds})\nJumlah: {stake}\nAnggaran pulangan: {return}\n\nSahkan pertaruhan ini?",
|
||||
place_confirm_singles:
|
||||
'{count} pertaruhan tunggal\nJumlah keseluruhan: {stake}\nAnggaran pulangan: {return}\n\nSahkan semua?',
|
||||
place_confirm_parlay:
|
||||
'Parlay {count} perlawanan, odds {odds}\nJumlah: {stake}\nAnggaran pulangan: {return}\n\nSahkan parlay?',
|
||||
place_success: 'Pertaruhan berjaya',
|
||||
place_failed: 'Pertaruhan gagal',
|
||||
quick_bet: 'Pertaruhan Pantas',
|
||||
popular_teams: 'Pasukan Popular',
|
||||
view_all_markets: 'Lihat Semua Pasaran',
|
||||
all: 'Semua',
|
||||
sport_football: 'Bola Sepak',
|
||||
sport_basketball: 'Bola Keranjang',
|
||||
sport_tennis: 'Tenis',
|
||||
sport_coming_soon: 'Akan datang',
|
||||
filter_time: 'Masa',
|
||||
filter_league: 'Liga',
|
||||
filter_market: 'Pasaran',
|
||||
time_all: 'Semua',
|
||||
time_today: 'Hari ini',
|
||||
time_early: 'Awal',
|
||||
market_all: 'Semua pasaran',
|
||||
market_handicap_ou: 'Handicap / O-U',
|
||||
correct_score: 'Skor tepat',
|
||||
no_markets: 'Tiada pasaran tersedia',
|
||||
no_markets_filtered: 'Tiada pasaran sepadan dengan penapis semasa',
|
||||
no_market_odds: 'Tiada odds pasaran',
|
||||
popover_title: 'Sahkan pertaruhan',
|
||||
place_now: 'Pertaruhan sekarang',
|
||||
add_to_single: 'Tambah tunggal',
|
||||
add_to_parlay: 'Tambah parlay',
|
||||
slip_parlay_added_short: 'Dalam parlay · Lihat',
|
||||
add_to_single_list: 'Tambah ke senarai tunggal',
|
||||
add_to_parlay_list: 'Tambah ke berganda',
|
||||
outright_view_all: 'Lihat Semua Pasukan',
|
||||
added_to_single: 'Ditambah ke senarai tunggal',
|
||||
added_to_parlay: 'Ditambah ke senarai parlay',
|
||||
slip_clear: 'Kosongkan semua',
|
||||
slip_locate: 'Lokasi odds',
|
||||
slip_locate_aria: 'Tatal ke pilihan ini dalam senarai odds',
|
||||
slip_tab_history: 'Sejarah',
|
||||
slip_history_all: 'Semua',
|
||||
slip_history_match: 'Perlawanan ini',
|
||||
slip_history_view_all: 'Lihat semua',
|
||||
slip_history_load_more: 'Muat lagi',
|
||||
slip_place_single: 'Taruhan tunggal',
|
||||
slip_place_all: 'Taruhan semua',
|
||||
slip_parlay_added_btn: 'Ditambah',
|
||||
place_confirm_title: 'Sahkan pertaruhan',
|
||||
place_confirm_single: 'Pilihan: {pick} ({\'@\'}{odds})\nJumlah: {stake}\nAnggaran pulangan: {return}\n\nSahkan pertaruhan ini?',
|
||||
place_confirm_singles: '{count} pertaruhan tunggal\nJumlah keseluruhan: {stake}\nAnggaran pulangan: {return}\n\nSahkan semua?',
|
||||
place_confirm_parlay: 'Parlay {count} perlawanan, odds {odds}\nJumlah: {stake}\nAnggaran pulangan: {return}\n\nSahkan parlay?',
|
||||
bet_no: 'No. pertaruhan',
|
||||
match: 'Perlawanan',
|
||||
selection: 'Pilihan',
|
||||
@@ -616,15 +611,6 @@ export default {
|
||||
},
|
||||
profile: {
|
||||
edit: 'Edit Profil',
|
||||
edit_title: 'Edit profil',
|
||||
settings: 'Tetapan',
|
||||
save_success: 'Berjaya disimpan',
|
||||
basic_info: 'Maklumat asas',
|
||||
change_avatar: 'Tukar avatar',
|
||||
username: 'Nama pengguna',
|
||||
username_locked: 'Nama pengguna tidak boleh diubah',
|
||||
account: 'Akaun',
|
||||
quick_nav: 'Pautan pantas',
|
||||
language: 'Bahasa',
|
||||
avatar: 'Avatar',
|
||||
avatar_change: 'Tukar avatar',
|
||||
@@ -669,5 +655,17 @@ export default {
|
||||
rules_p3: 'Keputusan berdasarkan skor separuh masa/penuh yang dimasukkan admin; bayaran selepas pratonton disahkan.',
|
||||
rules_p4: 'Jika bercanggah dengan notis laman, ikut notis terkini dan peraturan pasaran.',
|
||||
rules_p5: 'Langkah operasi: buka butiran perlawanan, ketik ikon ? di atas kanan.',
|
||||
edit_title: 'Edit profil',
|
||||
save_success: 'Berjaya disimpan',
|
||||
basic_info: 'Maklumat asas',
|
||||
change_avatar: 'Tukar avatar',
|
||||
username: 'Nama pengguna',
|
||||
username_locked: 'Nama pengguna tidak boleh diubah',
|
||||
account: 'Akaun',
|
||||
quick_nav: 'Pautan pantas',
|
||||
},
|
||||
pagination: {
|
||||
total: '{total} jumlah',
|
||||
per_page: '{size} / halaman',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -27,11 +27,19 @@ export default {
|
||||
copy: '复制',
|
||||
copy_success: '复制成功',
|
||||
},
|
||||
pagination: {
|
||||
total: '共 {total} 条',
|
||||
per_page: '{size} / 页',
|
||||
nav: {
|
||||
home: '主页',
|
||||
bet: '投注',
|
||||
bet_history: '历史投注',
|
||||
wallet: '账单',
|
||||
profile: '我的',
|
||||
sports: '体育赛事',
|
||||
football: '足球',
|
||||
account_hub: '投注账单',
|
||||
search: '搜索球队、联赛…',
|
||||
announcements: '公告',
|
||||
sports_more_soon: '更多体育,敬请期待',
|
||||
},
|
||||
nav: { home: '主页', sports: '体育赛事', football: '足球', bet: '投注', bet_history: '历史投注', wallet: '资金明细', account_hub: '投注账单', profile: '我的', search: '搜索球队、联赛…', announcements: '公告', sports_more_soon: '更多体育,敬请期待' },
|
||||
home: {
|
||||
hot_matches: '热门赛事',
|
||||
hot_tab: '热门',
|
||||
@@ -39,8 +47,7 @@ export default {
|
||||
no_matches: '暂无赛事',
|
||||
upcoming_empty: '未来 3 天内暂无赛事',
|
||||
announcement_badge: '公告',
|
||||
announcement_default:
|
||||
'欢迎光临 TheBet365 · 足球赛事火热进行中 · 理性投注,量力而行',
|
||||
announcement_default: '欢迎光临 TheBet365 · 足球赛事火热进行中 · 理性投注,量力而行',
|
||||
banner_prev: '上一张',
|
||||
banner_next: '下一张',
|
||||
banner_slide: '第 {n} 张',
|
||||
@@ -64,7 +71,6 @@ export default {
|
||||
empty: '暂无公告',
|
||||
not_found: '公告不存在或已下线',
|
||||
view_all: '查看全部公告',
|
||||
select_hint: '请选择一条公告查看详情',
|
||||
type_notice: '公告',
|
||||
type_ticker: '跑马灯',
|
||||
type_banner: 'Banner',
|
||||
@@ -72,6 +78,7 @@ export default {
|
||||
go_link: '前往页面',
|
||||
open_link: '打开链接',
|
||||
back: '返回',
|
||||
select_hint: '请选择一条公告查看详情',
|
||||
center_desc: '平台公告、活动与维护通知一览',
|
||||
total_count: '共 {count} 条',
|
||||
read_more: '查看详情',
|
||||
@@ -96,7 +103,6 @@ export default {
|
||||
not_found: '消息不存在',
|
||||
view_all: '返回消息列表',
|
||||
back: '返回',
|
||||
select_hint: '请选择一条消息查看详情',
|
||||
mark_all_read: '全部已读',
|
||||
load_more: '加载更多',
|
||||
delete: '删除',
|
||||
@@ -116,6 +122,7 @@ export default {
|
||||
view_recharge_history: '查看充值记录',
|
||||
unread_badge: '{count} 条未读',
|
||||
open_inbox: '打开消息中心',
|
||||
select_hint: '请选择一条消息查看详情',
|
||||
tab_inbox: '收件箱',
|
||||
tab_support: '客服',
|
||||
},
|
||||
@@ -154,10 +161,7 @@ export default {
|
||||
filter_won: '已赢',
|
||||
filter_lost: '已输',
|
||||
filter_pending: '待定',
|
||||
filter_push: '走水',
|
||||
filter_status_group: '结算状态',
|
||||
filter_push_group: '特殊结算',
|
||||
filter_push_hint: '走水:盘口平局,本金退回',
|
||||
filter_push: '走盘',
|
||||
stats_total: '总投注',
|
||||
stats_won: '赢',
|
||||
stats_lost: '输',
|
||||
@@ -169,10 +173,12 @@ export default {
|
||||
profit: '净赢',
|
||||
return_incl_stake: '回报 {amount}(含本金)',
|
||||
stake_to_return: '{stake} × {odds} = {amount}',
|
||||
filter_status_group: '结算状态',
|
||||
filter_push_group: '特殊结算',
|
||||
filter_push_hint: '走水:盘口平局,本金退回',
|
||||
},
|
||||
auth: {
|
||||
login: '登录',
|
||||
logging_in: '登录中…',
|
||||
register: '注册账号',
|
||||
logout: '退出登录',
|
||||
username: '账号',
|
||||
@@ -231,6 +237,7 @@ export default {
|
||||
reset_failed: '重置失败,请重试',
|
||||
phone_not_registered: '该手机号未注册',
|
||||
back_to_login: '返回登录',
|
||||
logging_in: '登录中…',
|
||||
},
|
||||
support: {
|
||||
short: '客服',
|
||||
@@ -291,6 +298,9 @@ export default {
|
||||
detail_bet_link: '查看注单',
|
||||
detail_tx_id: '流水号',
|
||||
detail_not_found: '账单不存在',
|
||||
all_records: '全部记录',
|
||||
frozen_before: '变动前冻结',
|
||||
frozen_after: '变动后冻结',
|
||||
ref_bet: '投注',
|
||||
ref_deposit: '充值',
|
||||
ref_withdraw: '提款',
|
||||
@@ -316,7 +326,6 @@ export default {
|
||||
},
|
||||
recharge: {
|
||||
title: '充值',
|
||||
scan_to_pay: '请扫描二维码完成支付',
|
||||
history: '记录',
|
||||
history_title: '充值记录',
|
||||
bank_transfer: '银行转账',
|
||||
@@ -370,6 +379,7 @@ export default {
|
||||
reapply: '重新申请',
|
||||
reapply_hint: '将在原订单上重新提交,请重新转账并上传新的截图。',
|
||||
back_to_history: '返回充值记录',
|
||||
scan_to_pay: '请扫描二维码完成支付',
|
||||
order_no: '订单号',
|
||||
method: '方式',
|
||||
amount: '充值金额',
|
||||
@@ -401,38 +411,11 @@ export default {
|
||||
stake: '投注金额',
|
||||
place_bet: '确认下注',
|
||||
place_bet_short: '下注',
|
||||
quick_bet: '快速下注',
|
||||
popular_teams: '热门队伍',
|
||||
view_all_markets: '查看全部玩法',
|
||||
all: '全部',
|
||||
parlay: '串关',
|
||||
tab_matches: '球赛',
|
||||
tab_outright: '优胜冠军',
|
||||
sport_football: '足球',
|
||||
sport_basketball: '篮球',
|
||||
sport_tennis: '网球',
|
||||
sport_coming_soon: '即将上线',
|
||||
tab_today: '今日',
|
||||
tab_early: '早盘',
|
||||
filter_time: '时间筛选',
|
||||
filter_league: '联赛',
|
||||
filter_market: '玩法筛选',
|
||||
time_all: '全部',
|
||||
time_today: '今天',
|
||||
time_early: '早盘',
|
||||
market_all: '所有盘口',
|
||||
market_handicap_ou: '让球 / 大小',
|
||||
correct_score: '波胆',
|
||||
no_markets: '暂无可用盘口',
|
||||
no_markets_filtered: '当前筛选下暂无盘口',
|
||||
no_market_odds: '暂无盘口赔率',
|
||||
popover_title: '确认投注',
|
||||
place_now: '立即下注',
|
||||
add_to_single: '加入单关',
|
||||
add_to_parlay: '加入串关',
|
||||
slip_parlay_added_short: '已加入串关 · 点击查看',
|
||||
add_to_single_list: '加入单关列表',
|
||||
add_to_parlay_list: '加入串关列表',
|
||||
filter_status_all: '全部',
|
||||
filter_status_open: '待开赛',
|
||||
filter_status_settled: '已结束',
|
||||
@@ -467,7 +450,6 @@ export default {
|
||||
outright_load_more: '加载更多',
|
||||
outright_settled: '已结算',
|
||||
outright_settled_hint: '本赛事已结算,仅可查看赔率与冠军结果',
|
||||
outright_view_all: '查看全部队伍',
|
||||
cancel: '取消',
|
||||
parlay_max_legs: '串关最多 5 项',
|
||||
parlay_block_outright: '冠军盘不可串关',
|
||||
@@ -545,37 +527,24 @@ export default {
|
||||
slip_pick_hint: '点选项加入投注单;金边表示已选,再点一次可取消',
|
||||
picked_tag: '已选',
|
||||
pick_added: '已加入投注单',
|
||||
added_to_single: '已加入单关列表',
|
||||
added_to_parlay: '已加入串关列表',
|
||||
pick_removed: '已从投注单移除',
|
||||
slip_bar_ready: '已选一项',
|
||||
slip_bar_go: '投注单',
|
||||
cs_top_hint: '① 在比分格填金额 ② 点上方「确认下注」',
|
||||
slip_empty_hint: '点击赔率加入单关或串关列表',
|
||||
slip_clear: '清空',
|
||||
slip_empty_hint: '点击赔率打开投注抽屉',
|
||||
slip_remove: '移除',
|
||||
slip_locate: '定位到盘口',
|
||||
slip_locate_aria: '定位到该选项在盘口列表中的位置',
|
||||
slip_est_return: '预计最大领取金额',
|
||||
slip_parlay_odds: '组合赔率 {odds}',
|
||||
slip_review_title: '确认下注内容',
|
||||
slip_balance: '账户余额',
|
||||
slip_tab_single: '单注',
|
||||
slip_tab_parlay: '串关',
|
||||
slip_tab_history: '历史',
|
||||
slip_history_all: '全部',
|
||||
slip_history_match: '本场赛事',
|
||||
slip_history_view_all: '查看全部',
|
||||
slip_history_load_more: '加载更多',
|
||||
slip_parlay_empty_hint: '先选择一个下注项,再点「加入串关」',
|
||||
slip_add_parlay: '加入串关',
|
||||
slip_parlay_only_hint: '此盘口不支持单关,请点「加入串关」后再投注',
|
||||
slip_parlay_same_match: '串关选项不得为同一场比赛,请先移除同场选项后再加入。',
|
||||
slip_parlay_count: '{n} 项串关',
|
||||
slip_total_stake: '总投注金额',
|
||||
slip_place_single: '单独下注',
|
||||
slip_place_all: '全部下注',
|
||||
slip_parlay_added_btn: '已加入',
|
||||
slip_currency: '金额',
|
||||
slip_min: '最低',
|
||||
slip_min_error: '最低投注金额为 {amount}',
|
||||
@@ -584,15 +553,53 @@ export default {
|
||||
accept_changes_place: '接受变更并下注',
|
||||
odds_was: '原赔率',
|
||||
odds_now: '新赔率',
|
||||
place_confirm_title: '确认下注',
|
||||
place_confirm_single:
|
||||
"选项:{pick}({'@'}{odds})\n投注金额:{stake}\n预计派彩:{return}\n\n确认提交此注单?",
|
||||
place_confirm_singles:
|
||||
'共 {count} 笔单关\n总投注:{stake}\n预计派彩:{return}\n\n确认提交?',
|
||||
place_confirm_parlay:
|
||||
'串关 {count} 场,总赔率 {odds}\n投注金额:{stake}\n预计派彩:{return}\n\n确认提交?',
|
||||
place_success: '下注成功',
|
||||
place_failed: '下注失败',
|
||||
quick_bet: '快速下注',
|
||||
popular_teams: '热门队伍',
|
||||
view_all_markets: '查看全部玩法',
|
||||
all: '全部',
|
||||
sport_football: '足球',
|
||||
sport_basketball: '篮球',
|
||||
sport_tennis: '网球',
|
||||
sport_coming_soon: '即将上线',
|
||||
filter_time: '时间筛选',
|
||||
filter_league: '联赛',
|
||||
filter_market: '玩法筛选',
|
||||
time_all: '全部',
|
||||
time_today: '今天',
|
||||
time_early: '早盘',
|
||||
market_all: '所有盘口',
|
||||
market_handicap_ou: '让球 / 大小',
|
||||
correct_score: '波胆',
|
||||
no_markets: '暂无可用盘口',
|
||||
no_markets_filtered: '当前筛选下暂无盘口',
|
||||
no_market_odds: '暂无盘口赔率',
|
||||
popover_title: '确认投注',
|
||||
place_now: '立即下注',
|
||||
add_to_single: '加入单关',
|
||||
add_to_parlay: '加入串关',
|
||||
slip_parlay_added_short: '已加入串关 · 点击查看',
|
||||
add_to_single_list: '加入单关列表',
|
||||
add_to_parlay_list: '加入串关列表',
|
||||
outright_view_all: '查看全部队伍',
|
||||
added_to_single: '已加入单关列表',
|
||||
added_to_parlay: '已加入串关列表',
|
||||
slip_clear: '清空',
|
||||
slip_locate: '定位到盘口',
|
||||
slip_locate_aria: '定位到该选项在盘口列表中的位置',
|
||||
slip_tab_history: '历史',
|
||||
slip_history_all: '全部',
|
||||
slip_history_match: '本场赛事',
|
||||
slip_history_view_all: '查看全部',
|
||||
slip_history_load_more: '加载更多',
|
||||
slip_place_single: '单独下注',
|
||||
slip_place_all: '全部下注',
|
||||
slip_parlay_added_btn: '已加入',
|
||||
place_confirm_title: '确认下注',
|
||||
place_confirm_single: '选项:{pick}({\'@\'}{odds})\n投注金额:{stake}\n预计派彩:{return}\n\n确认提交此注单?',
|
||||
place_confirm_singles: '共 {count} 笔单关\n总投注:{stake}\n预计派彩:{return}\n\n确认提交?',
|
||||
place_confirm_parlay: '串关 {count} 场,总赔率 {odds}\n投注金额:{stake}\n预计派彩:{return}\n\n确认提交?',
|
||||
bet_no: '注单号',
|
||||
match: '比赛',
|
||||
selection: '投注选项',
|
||||
@@ -604,15 +611,6 @@ export default {
|
||||
},
|
||||
profile: {
|
||||
edit: '修改资料',
|
||||
edit_title: '修改资料',
|
||||
settings: '设置',
|
||||
save_success: '保存成功',
|
||||
basic_info: '基础信息',
|
||||
change_avatar: '更换头像',
|
||||
username: '用户名',
|
||||
username_locked: '用户名不可修改',
|
||||
account: '账户信息',
|
||||
quick_nav: '快捷入口',
|
||||
language: '语言',
|
||||
avatar: '选择头像',
|
||||
avatar_change: '修改头像',
|
||||
@@ -657,5 +655,17 @@ export default {
|
||||
rules_p3: '赛果由平台根据官方录入的半场/全场比分结算,结算预览经确认后入账。',
|
||||
rules_p4: '若本说明与后台公告冲突,以最新公告及实际盘口规则为准。',
|
||||
rules_p5: '操作步骤:进入任意赛事详情,点右上角「?」查看玩法说明。',
|
||||
edit_title: '修改资料',
|
||||
save_success: '保存成功',
|
||||
basic_info: '基础信息',
|
||||
change_avatar: '更换头像',
|
||||
username: '用户名',
|
||||
username_locked: '用户名不可修改',
|
||||
account: '账户信息',
|
||||
quick_nav: '快捷入口',
|
||||
},
|
||||
pagination: {
|
||||
total: '共 {total} 条',
|
||||
per_page: '{size} / 页',
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -1,194 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView, RouterLink, useRoute, useRouter } from 'vue-router';
|
||||
import { RouterView, RouterLink, useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { useBetSlipStore } from '../stores/betSlip';
|
||||
import CashBalanceChip from '../components/CashBalanceChip.vue';
|
||||
import UserAvatarMenu from '../components/UserAvatarMenu.vue';
|
||||
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
||||
import { useAppLocale } from '../composables/useAppLocale';
|
||||
import AnnouncementMarquee from '../components/AnnouncementMarquee.vue';
|
||||
import BottomNavIcon from '../components/BottomNavIcon.vue';
|
||||
import BackToTopButton from '../components/BackToTopButton.vue';
|
||||
import SettingsDrawer from '../components/SettingsDrawer.vue';
|
||||
import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
||||
import { useViewport } from '../composables/useViewport';
|
||||
import DesktopShell from '../components/desktop/DesktopShell.vue';
|
||||
|
||||
const BetSlipDrawer = defineAsyncComponent(() => import('../components/BetSlipDrawer.vue'));
|
||||
const { isDesktop } = useViewport();
|
||||
|
||||
import { usePlayerHome } from '../composables/usePlayerHome';
|
||||
import { useInboxFeature } from '../composables/useInboxFeature';
|
||||
import { usePlayerProfile } from '../composables/usePlayerProfile';
|
||||
import { startPresencePing, stopPresencePing } from '../composables/usePresencePing';
|
||||
import { useDepositNotifications } from '../composables/useDepositNotifications';
|
||||
import { usePlayerMessages } from '../composables/usePlayerMessages';
|
||||
import { useInboxFeature } from '../composables/useInboxFeature';
|
||||
import { startPresencePing, stopPresencePing } from '../composables/usePresencePing';
|
||||
|
||||
const { isDesktop } = useViewport();
|
||||
const { t, locale } = useI18n();
|
||||
const auth = useAuthStore();
|
||||
const { initFromUser } = useAppLocale();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const slip = useBetSlipStore();
|
||||
const showSettings = ref(false);
|
||||
const floatBtnRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const btnX = ref<number | null>(null);
|
||||
const btnY = ref<number | null>(null);
|
||||
const isDragging = ref(false);
|
||||
const isDocked = ref(false);
|
||||
const dockSide = ref<'left' | 'right'>('right');
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let initialX = 0;
|
||||
let initialY = 0;
|
||||
let hasMoved = false;
|
||||
let wasDockedBeforeInteraction = false;
|
||||
|
||||
const btnStyle = computed(() => {
|
||||
if (btnX.value === null || btnY.value === null) {
|
||||
return {
|
||||
right: '16px',
|
||||
bottom: '80px',
|
||||
};
|
||||
}
|
||||
return {
|
||||
left: `${btnX.value}px`,
|
||||
top: `${btnY.value}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const DOCK_OFFSET = 24;
|
||||
const SNAP_THRESHOLD = 16; // 仅当按钮贴近å±<C3A5>幕边缘时æ‰<C3A6>å<EFBFBD>¸é™„
|
||||
const DOCK_SNAP_THRESHOLD = 20; // px â€?按钮边缘è·<C3A8>å±<C3A5>幕边缘å°<C3A5>于æ¤å€¼æ‰<C3A6>å<EFBFBD>¸é™„
|
||||
const FLOAT_BTN_SIZE = 48;
|
||||
const FLOAT_BTN_EDGE_INSET = 8;
|
||||
|
||||
function undockToVisualPosition() {
|
||||
if (!isDocked.value || btnX.value === null) return;
|
||||
if (dockSide.value === 'right') {
|
||||
btnX.value += DOCK_OFFSET;
|
||||
} else {
|
||||
btnX.value -= DOCK_OFFSET;
|
||||
}
|
||||
isDocked.value = false;
|
||||
}
|
||||
|
||||
function snapToEdge(side: 'left' | 'right') {
|
||||
const screenWidth = window.innerWidth;
|
||||
dockSide.value = side;
|
||||
btnX.value = side === 'left' ? FLOAT_BTN_EDGE_INSET : screenWidth - FLOAT_BTN_SIZE - FLOAT_BTN_EDGE_INSET;
|
||||
isDocked.value = true;
|
||||
}
|
||||
|
||||
function onPointerDown(e: PointerEvent) {
|
||||
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
||||
if (isDragging.value) return;
|
||||
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
try {
|
||||
el.setPointerCapture(e.pointerId);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
isDragging.value = true;
|
||||
hasMoved = false;
|
||||
wasDockedBeforeInteraction = isDocked.value;
|
||||
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
|
||||
// å<>¸é™„æ€<C3A6>下 transform 会把按钮推出å±<C3A5>幕,先还原到å<C2B0>¯è§<C3A8>ä½<C3A4>置,å<C592>¦åˆ™å‡ ä¹Žæ— æ³•è§¦å<C2A6>‘拖拽
|
||||
undockToVisualPosition();
|
||||
|
||||
const btnSize = FLOAT_BTN_SIZE;
|
||||
initialX = btnX.value !== null ? btnX.value : window.innerWidth - btnSize - 16;
|
||||
initialY = btnY.value !== null ? btnY.value : window.innerHeight - btnSize - 80;
|
||||
|
||||
btnX.value = initialX;
|
||||
btnY.value = initialY;
|
||||
}
|
||||
|
||||
function onPointerMove(e: PointerEvent) {
|
||||
if (!isDragging.value) return;
|
||||
|
||||
const dx = e.clientX - startX;
|
||||
const dy = e.clientY - startY;
|
||||
|
||||
if (!hasMoved && (Math.abs(dx) > 5 || Math.abs(dy) > 5)) {
|
||||
hasMoved = true;
|
||||
}
|
||||
|
||||
if (hasMoved) {
|
||||
const newX = initialX + dx;
|
||||
const newY = initialY + dy;
|
||||
|
||||
const btnSize = 48;
|
||||
const minX = -24;
|
||||
const maxX = window.innerWidth - 24;
|
||||
const minY = 60;
|
||||
const maxY = window.innerHeight - btnSize - 60;
|
||||
|
||||
btnX.value = Math.max(minX, Math.min(newX, maxX));
|
||||
btnY.value = Math.max(minY, Math.min(newY, maxY));
|
||||
}
|
||||
}
|
||||
|
||||
function openInboxHub() {
|
||||
if (!auth.token) {
|
||||
auth.showLoginPrompt(hubRoute.value);
|
||||
return;
|
||||
}
|
||||
|
||||
void router.push(hubRoute.value);
|
||||
}
|
||||
|
||||
function onPointerUp(e: PointerEvent) {
|
||||
if (!isDragging.value) return;
|
||||
isDragging.value = false;
|
||||
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
try {
|
||||
el.releasePointerCapture(e.pointerId);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (hasMoved) {
|
||||
if (btnX.value !== null && btnY.value !== null) {
|
||||
const screenWidth = window.innerWidth;
|
||||
const btnSize = 48;
|
||||
const snapThreshold = SNAP_THRESHOLD;
|
||||
|
||||
const distLeft = btnX.value;
|
||||
const distRight = screenWidth - (btnX.value + btnSize);
|
||||
|
||||
if (distLeft <= snapThreshold) {
|
||||
snapToEdge('left');
|
||||
} else if (distRight <= snapThreshold) {
|
||||
snapToEdge('right');
|
||||
} else {
|
||||
// Float freely in place �no folding
|
||||
isDocked.value = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (wasDockedBeforeInteraction) {
|
||||
snapToEdge(dockSide.value);
|
||||
}
|
||||
openInboxHub();
|
||||
}
|
||||
|
||||
if (btnX.value !== null && btnY.value !== null) {
|
||||
localStorage.setItem('float_cs_x', String(btnX.value));
|
||||
localStorage.setItem('float_cs_y', String(btnY.value));
|
||||
localStorage.setItem('float_cs_side', dockSide.value);
|
||||
localStorage.setItem('float_cs_docked', isDocked.value ? '1' : '0');
|
||||
}
|
||||
}
|
||||
|
||||
const { inboxEnabled, hubRoute, hubOpenLabelKey } = useInboxFeature();
|
||||
|
||||
const isDetailPage = computed(() => {
|
||||
const p = route.path;
|
||||
@@ -198,6 +38,7 @@ const isDetailPage = computed(() => {
|
||||
p.startsWith('/bets/') ||
|
||||
p.startsWith('/wallet/') ||
|
||||
p.startsWith('/messages') ||
|
||||
p.startsWith('/announcements/') ||
|
||||
p === '/profile/edit' ||
|
||||
p === '/profile/cashbacks'
|
||||
);
|
||||
@@ -205,7 +46,7 @@ const isDetailPage = computed(() => {
|
||||
|
||||
const showHeader = computed(() => !isDetailPage.value);
|
||||
const showAnnouncement = computed(
|
||||
() => !isDetailPage.value && !route.path.startsWith('/profile') && !route.path.startsWith('/announcements')
|
||||
() => !isDetailPage.value && !route.path.startsWith('/profile') && !route.path.startsWith('/announcements'),
|
||||
);
|
||||
|
||||
const showBottomNav = computed(() => {
|
||||
@@ -225,13 +66,10 @@ const showBottomNav = computed(() => {
|
||||
return false;
|
||||
});
|
||||
const { announcements, announcementItems, load: loadPlayerHome } = usePlayerHome();
|
||||
const primaryAnnouncementId = computed(() => announcementItems.value[0]?.id ?? '');
|
||||
const { loadProfile, refreshProfile, bindProfileVisibilityRefresh } = usePlayerProfile();
|
||||
const { startPolling, stopPolling } = useDepositNotifications();
|
||||
const { unreadCount, refreshUnreadCount, resetMessagesState } = usePlayerMessages();
|
||||
const { inboxEnabled, hubRoute, hubOpenLabelKey } = useInboxFeature();
|
||||
const showFloatCsBtn = computed(() => !route.path.startsWith('/messages'));
|
||||
const primaryAnnouncementId = computed(() => announcementItems.value[0]?.id ?? '');
|
||||
|
||||
const mainRef = ref<HTMLElement | null>(null);
|
||||
const tabScrollTops = new Map<string, number>();
|
||||
|
||||
@@ -254,29 +92,14 @@ const balanceRefreshPaths = ['/profile', '/wallet', '/bets'];
|
||||
onMounted(() => {
|
||||
if (auth.user?.locale) void initFromUser(auth.user.locale);
|
||||
bindProfileVisibilityRefresh();
|
||||
|
||||
const savedX = localStorage.getItem('float_cs_x');
|
||||
const savedY = localStorage.getItem('float_cs_y');
|
||||
const savedSide = localStorage.getItem('float_cs_side');
|
||||
const savedDocked = localStorage.getItem('float_cs_docked');
|
||||
if (savedX !== null && savedY !== null) {
|
||||
btnX.value = Number(savedX);
|
||||
btnY.value = Number(savedY);
|
||||
}
|
||||
if (savedSide === 'left' || savedSide === 'right') {
|
||||
dockSide.value = savedSide;
|
||||
}
|
||||
if (savedDocked !== null) {
|
||||
isDocked.value = savedDocked === '1';
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => auth.token,
|
||||
(token) => {
|
||||
// 首页数æ<EFBFBD>®ï¼ˆå…¬å‘Šã€<EFBFBD>çƒé—¨èµ›äº‹ï¼‰å¯¹æ‰€æœ‰äººå…¬å¼€ï¼Œå§‹ç»ˆåŠ è½?
|
||||
// 首页数据(公告、热门赛事)对所有人公开,始终加载
|
||||
void loadPlayerHome(true);
|
||||
// 个人资料仅登录用户需�
|
||||
// 个人资料仅登录用户需要
|
||||
if (token) {
|
||||
void loadProfile(true);
|
||||
startPolling();
|
||||
@@ -309,22 +132,36 @@ watch(
|
||||
<DesktopShell v-if="isDesktop" />
|
||||
<div v-else class="layout">
|
||||
<header v-if="showHeader" class="header">
|
||||
<img src="/logo.png" alt="TheBet365" class="logo" />
|
||||
<svg class="logo" viewBox="0 0 130 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="TheBet365">
|
||||
<text x="0" y="18" font-family="'Inter','SF Pro Display',system-ui,sans-serif" font-size="11" font-weight="800" fill="#94A3B8" letter-spacing="0.14em">THE</text>
|
||||
<text x="32" y="18" font-family="'Inter','SF Pro Display',system-ui,sans-serif" font-size="18" font-weight="900" fill="#FFB980" letter-spacing="0.01em">BET</text>
|
||||
<text x="74" y="18" font-family="'Inter','SF Pro Display',system-ui,sans-serif" font-size="18" font-weight="900" fill="#FFDDD2" letter-spacing="0.01em">365</text>
|
||||
</svg>
|
||||
<div class="header-actions">
|
||||
<LocaleSwitcher v-if="!auth.user" />
|
||||
<RouterLink
|
||||
:to="hubRoute"
|
||||
class="hub-btn"
|
||||
:aria-label="inboxEnabled && unreadCount ? t('messages.unread_badge', { count: unreadCount }) : t(hubOpenLabelKey)"
|
||||
>
|
||||
<svg class="hub-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M12 3C7.03 3 3 6.58 3 11c0 2.02.9 3.86 2.38 5.24L4 21l4.2-1.02A10.8 10.8 0 0 0 12 19c4.97 0 9-3.58 9-8s-4.03-8-9-8Z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="9" cy="11" r="1" fill="currentColor" />
|
||||
<circle cx="12" cy="11" r="1" fill="currentColor" />
|
||||
<circle cx="15" cy="11" r="1" fill="currentColor" />
|
||||
</svg>
|
||||
<span v-if="auth.user && inboxEnabled && unreadCount > 0" class="hub-badge">{{ unreadCount > 99 ? '99+' : unreadCount }}</span>
|
||||
</RouterLink>
|
||||
<div class="header-divider" />
|
||||
<LocaleSwitcher />
|
||||
<template v-if="auth.user">
|
||||
<CashBalanceChip />
|
||||
<button
|
||||
type="button"
|
||||
class="settings-trigger-btn"
|
||||
:aria-label="t('profile.settings')"
|
||||
@click="showSettings = true"
|
||||
>
|
||||
<svg class="settings-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.1a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
</button>
|
||||
<UserAvatarMenu />
|
||||
</template>
|
||||
<button v-else type="button" class="login-btn" @click="auth.showLoginPrompt(route.fullPath)">
|
||||
{{ t('auth.login') }}
|
||||
@@ -340,7 +177,7 @@ watch(
|
||||
/>
|
||||
</div>
|
||||
|
||||
<main ref="mainRef" :class="['main', { 'has-nav': showBottomNav, 'main--sub-page': isDetailPage }]">
|
||||
<main ref="mainRef" :class="['main', { 'has-nav': showBottomNav, 'main--detail': isDetailPage }]">
|
||||
<RouterView v-slot="{ Component, route: viewRoute }">
|
||||
<KeepAlive v-if="viewRoute.meta.keepAlive" :max="10">
|
||||
<component :is="Component" :key="viewRoute.path" />
|
||||
@@ -379,35 +216,6 @@ watch(
|
||||
<BackToTopButton :scroll-el="mainRef" :above-nav="showBottomNav" />
|
||||
|
||||
<BetSlipDrawer v-model="slip.drawerOpen" />
|
||||
<SettingsDrawer v-model="showSettings" />
|
||||
|
||||
<!-- Draggable Floating Customer Service Button -->
|
||||
<div
|
||||
v-if="showFloatCsBtn"
|
||||
ref="floatBtnRef"
|
||||
class="float-cs-btn"
|
||||
:class="{ dragging: isDragging, docked: isDocked, 'dock-left': dockSide === 'left', 'dock-right': dockSide === 'right' }"
|
||||
:style="btnStyle"
|
||||
@pointerdown="onPointerDown"
|
||||
@pointermove="onPointerMove"
|
||||
@pointerup="onPointerUp"
|
||||
@pointercancel="onPointerUp"
|
||||
@touchstart.prevent
|
||||
:aria-label="inboxEnabled && unreadCount ? t('messages.unread_badge', { count: unreadCount }) : t(hubOpenLabelKey)"
|
||||
>
|
||||
<svg class="cs-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M12 3C7.03 3 3 6.58 3 11c0 2.02.9 3.86 2.38 5.24L4 21l4.2-1.02A10.8 10.8 0 0 0 12 19c4.97 0 9-3.58 9-8s-4.03-8-9-8Z"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="9" cy="11" r="1" fill="currentColor" />
|
||||
<circle cx="12" cy="11" r="1" fill="currentColor" />
|
||||
<circle cx="15" cy="11" r="1" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -419,178 +227,121 @@ watch(
|
||||
min-height: 100dvh;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
padding-top: calc(var(--app-top-gap) + env(safe-area-inset-top, 0px));
|
||||
}
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
padding: 6px var(--space-page-x);
|
||||
min-height: var(--header-h);
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
height: 48px;
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 110;
|
||||
}
|
||||
.logo {
|
||||
height: 36px; width: auto; display: block;
|
||||
height: 22px;
|
||||
width: auto;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header-actions {
|
||||
--header-chip-h: 36px;
|
||||
--header-chip-h: 32px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
.header-actions :deep(.locale-switch:not(.compact)),
|
||||
.header-actions :deep(.cash-chip) {
|
||||
.header-actions :deep(.cash-chip),
|
||||
.header-actions :deep(.avatar-btn) {
|
||||
height: var(--header-chip-h);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header-actions :deep(.avatar-btn) {
|
||||
width: var(--header-chip-h);
|
||||
}
|
||||
|
||||
.float-cs-btn {
|
||||
position: fixed;
|
||||
z-index: 135;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: var(--dropdown-bg, #0A2540);
|
||||
border: 1px solid var(--border-strong);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
.header-divider {
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: var(--border);
|
||||
flex-shrink: 0;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.hub-btn {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.15);
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
touch-action: none;
|
||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.float-cs-btn:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.float-cs-btn.dragging {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.float-cs-btn.docked {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.float-cs-btn.docked:hover {
|
||||
opacity: 1;
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.float-cs-btn.docked:active {
|
||||
opacity: 1;
|
||||
transform: translateX(0) !important;
|
||||
}
|
||||
|
||||
.float-cs-btn.docked.dock-left {
|
||||
transform: translateX(-24px);
|
||||
}
|
||||
|
||||
.float-cs-btn.docked.dock-right {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
/* Expand touch target to bypass edge gesture intercept */
|
||||
.float-cs-btn::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
bottom: -12px;
|
||||
left: -24px;
|
||||
right: -24px;
|
||||
background: transparent;
|
||||
gap: 0;
|
||||
width: var(--header-chip-h, 32px);
|
||||
height: var(--header-chip-h, 32px);
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
pointer-events: auto;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card-elevated);
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
flex-shrink: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.hub-btn:active {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--border-active);
|
||||
}
|
||||
|
||||
.cs-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
pointer-events: none;
|
||||
.hub-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cs-badge {
|
||||
.hub-badge {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
border-radius: 999px;
|
||||
background: rgba(239, 68, 68, 0.95);
|
||||
background: var(--danger, #EF4444);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 0 2px var(--bg-body);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cs-badge.badge-left {
|
||||
left: -2px;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.cs-badge.badge-right {
|
||||
right: -2px;
|
||||
left: auto;
|
||||
box-shadow: 0 0 0 2px var(--bg-card-elevated);
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
padding: 6px 14px;
|
||||
padding: 6px 18px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-strong);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
border: none;
|
||||
background: var(--gradient-primary);
|
||||
color: #FFFFFF;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s;
|
||||
transition: opacity 0.15s, transform 0.15s;
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
|
||||
.login-btn:active {
|
||||
background: var(--surface-active);
|
||||
opacity: 0.9;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.settings-trigger-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--header-chip-h, 36px);
|
||||
height: var(--header-chip-h, 36px);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s, border-color 0.18s;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settings-trigger-btn:active {
|
||||
background: var(--surface-active);
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.announce-strip {
|
||||
flex-shrink: 0;
|
||||
z-index: 105;
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.main {
|
||||
@@ -599,26 +350,23 @@ watch(
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: var(--space-section) var(--space-page-x) 0;
|
||||
padding: 10px 14px 0;
|
||||
}
|
||||
|
||||
.main.main--sub-page {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.main.has-nav {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.main.main--detail {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
display: flex;
|
||||
background: var(--glass);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-top: 1px solid var(--border);
|
||||
box-shadow: none;
|
||||
z-index: 100;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
min-height: var(--nav-h);
|
||||
}
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
@@ -626,16 +374,17 @@ watch(
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 6px 2px 8px;
|
||||
gap: 3px;
|
||||
padding: 7px 2px 8px;
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
letter-spacing: 0.01em;
|
||||
position: relative;
|
||||
transition: color 0.18s;
|
||||
transition: color 0.2s;
|
||||
line-height: 1.2;
|
||||
min-width: 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
@@ -647,16 +396,24 @@ watch(
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
text-shadow: 0 0 8px rgba(255, 185, 128, 0.4);
|
||||
}
|
||||
|
||||
.nav-item.active :deep(.nav-icon) {
|
||||
color: var(--text);
|
||||
filter: none;
|
||||
color: var(--primary-light);
|
||||
filter: drop-shadow(0 0 6px rgba(255, 185, 128, 0.5));
|
||||
}
|
||||
|
||||
.nav-item.active::after {
|
||||
display: none;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 18%;
|
||||
right: 18%;
|
||||
height: 2.5px;
|
||||
background: var(--gradient-primary);
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -12,11 +12,6 @@ import {
|
||||
type PlayerLocale,
|
||||
} from './i18n/index.ts';
|
||||
|
||||
/** iOS Safari:阻止双指捏合缩放,配合 viewport 保持页面比例稳定 */
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('gesturestart', (event) => event.preventDefault());
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
const initialLocale = readStoredLocale();
|
||||
const initialMessages = await loadLocaleMessages(initialLocale);
|
||||
|
||||
@@ -1,241 +1,149 @@
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
/* ── 海军 · 白色强调(替代红色) ── */
|
||||
--primary: rgba(255, 255, 255, 0.92);
|
||||
--primary-dark: rgba(255, 255, 255, 0.78);
|
||||
--primary-light: rgba(255, 255, 255, 0.68);
|
||||
--primary-muted: rgba(255, 255, 255, 0.52);
|
||||
--on-primary: #001A33;
|
||||
--secondary: #0A3558;
|
||||
--tertiary: #001A33;
|
||||
--neutral: rgba(255, 255, 255, 0.42);
|
||||
--bg-body: #001A33;
|
||||
--dropdown-bg: #0A2540;
|
||||
--bg-card: rgba(255, 255, 255, 0.045);
|
||||
--bg-hover: rgba(255, 255, 255, 0.06);
|
||||
--bg-elevated: rgba(255, 255, 255, 0.08);
|
||||
--bg-input: rgba(255, 255, 255, 0.06);
|
||||
--captcha-canvas-bg: #002244;
|
||||
--captcha-char: #ffffff;
|
||||
/* ── 蓉色珊瑚 — Dusk Coral Theme ── */
|
||||
--primary: #F4A261;
|
||||
--primary-dark: #E76F51;
|
||||
--primary-light: #FFB980;
|
||||
--primary-glow: #FFDDD2;
|
||||
--accent: #E76F51;
|
||||
--accent-light: #FFDDD2;
|
||||
--gradient-primary: linear-gradient(135deg, #E76F51, #F4A261);
|
||||
--gradient-accent: linear-gradient(135deg, #F4A261, #FFDDD2);
|
||||
--gradient-hot: linear-gradient(135deg, #E76F51, #F4A261, #FFDDD2);
|
||||
--gradient-success: linear-gradient(135deg, #2C5364, #203A43);
|
||||
--gradient-hero: linear-gradient(135deg, #0F2027, #203A43, #0F2027);
|
||||
|
||||
--secondary: #203A43;
|
||||
--tertiary: #1A3038;
|
||||
--neutral: #8FA3AD;
|
||||
|
||||
--bg-body: #0F2027;
|
||||
--glass-bg: rgba(22, 45, 54, 0.82);
|
||||
--glass-border: rgba(255, 255, 255, 0.1);
|
||||
--glass-border-accent: rgba(244, 162, 97, 0.32);
|
||||
--glass-glow: 0 0 20px rgba(0, 0, 0, 0.18);
|
||||
--bg-card: var(--glass-bg);
|
||||
--bg-card-elevated: rgba(28, 55, 66, 0.88);
|
||||
--bg-hover: rgba(44, 83, 100, 0.55);
|
||||
--bg-elevated: rgba(28, 55, 66, 0.88);
|
||||
--bg-input: #162D36;
|
||||
--captcha-canvas-bg: #162D36;
|
||||
--captcha-char: #F4A261;
|
||||
--captcha-noise-rgb: 255, 255, 255;
|
||||
--surface-active: rgba(255, 255, 255, 0.1);
|
||||
--surface-accent: rgba(255, 255, 255, 0.08);
|
||||
--text: rgba(255, 255, 255, 0.94);
|
||||
--text-muted: rgba(255, 255, 255, 0.56);
|
||||
--text-dim: rgba(255, 255, 255, 0.38);
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
--border-strong: rgba(255, 255, 255, 0.16);
|
||||
--border-gold: rgba(255, 255, 255, 0.2);
|
||||
--border-gold-soft: rgba(255, 255, 255, 0.18);
|
||||
--bg-glass: rgba(15, 32, 39, 0.85);
|
||||
|
||||
--text: #FFFFFF;
|
||||
--text-secondary: #D4E4EB;
|
||||
--text-muted: #8FA3AD;
|
||||
--text-dim: #5E7A86;
|
||||
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
--border-active: #F4A261;
|
||||
--border-w: 1px;
|
||||
--border-w-thick: 1px;
|
||||
--danger: rgba(255, 255, 255, 0.62);
|
||||
--success: #34C759;
|
||||
|
||||
--danger: #EF4444;
|
||||
--success: #4ADE80;
|
||||
--warning: #FBBF24;
|
||||
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 12px 32px rgba(0, 0, 0, 0.22);
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.16);
|
||||
--shadow-card: inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 8px 28px rgba(0, 0, 0, 0.22);
|
||||
--shadow-card-sm: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 4px 18px rgba(0, 0, 0, 0.18);
|
||||
--shadow-gold: none;
|
||||
--glow-gold: none;
|
||||
--gradient-accent: rgba(255, 255, 255, 0.88);
|
||||
--gradient-gold: var(--gradient-accent);
|
||||
--gradient-gold-border: var(--gradient-accent);
|
||||
--gradient-card: linear-gradient(
|
||||
165deg,
|
||||
rgba(255, 255, 255, 0.11) 0%,
|
||||
rgba(255, 255, 255, 0.055) 24%,
|
||||
rgba(10, 53, 88, 0.32) 58%,
|
||||
rgba(0, 20, 40, 0.62) 100%
|
||||
);
|
||||
--gradient-card-soft: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.08) 0%,
|
||||
rgba(255, 255, 255, 0.03) 40%,
|
||||
rgba(0, 26, 51, 0.45) 100%
|
||||
);
|
||||
--gradient-card-shine: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.28) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
--glass: rgba(0, 26, 51, 0.78);
|
||||
--space-page-x: 20px;
|
||||
--space-toolbar-top: 8px;
|
||||
--space-section: 16px;
|
||||
--space-card: 16px;
|
||||
--header-h: 48px;
|
||||
--nav-h: 52px;
|
||||
--app-top-gap: 8px;
|
||||
--radius-lg: 16px;
|
||||
|
||||
--shadow: 0 2px 10px rgba(0, 0, 0, 0.32);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.38);
|
||||
--shadow-lg: 0 8px 28px rgba(0, 0, 0, 0.45);
|
||||
--shadow-glow: 0 0 20px rgba(244, 162, 97, 0.2);
|
||||
--shadow-glow-accent: 0 0 20px rgba(231, 111, 81, 0.2);
|
||||
}
|
||||
|
||||
/** 描边按钮 */
|
||||
.btn-gold-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-strong);
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-gold-outline:active:not(:disabled) {
|
||||
background: var(--surface-active);
|
||||
border-color: var(--border-strong);
|
||||
/* Odds / betting accent — coral orange */
|
||||
:root {
|
||||
--odds-accent: #F4A261;
|
||||
--odds-accent-light: #FFB980;
|
||||
--odds-accent-soft: rgba(244, 162, 97, 0.1);
|
||||
}
|
||||
|
||||
/** 选中态:中性浅底 */
|
||||
.tab-gold-active {
|
||||
background: var(--bg-elevated) !important;
|
||||
border-color: var(--border-strong) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
/* ── Global resets ── */
|
||||
html, body, #app { height: 100%; min-height: 100%; }
|
||||
|
||||
/* 登录等关键区域 */
|
||||
.ps-gold-frame {
|
||||
position: relative;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12) !important;
|
||||
border-radius: var(--radius);
|
||||
background: var(--gradient-card) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: var(--shadow-card-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ps-gold-frame::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
height: 1px;
|
||||
background: var(--gradient-card-shine);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ps-gold-frame::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ps-gold-input {
|
||||
background: var(--bg-input) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
box-shadow: none;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.ps-gold-input::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.ps-gold-input:focus {
|
||||
border-color: var(--border-strong) !important;
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.08) !important;
|
||||
}
|
||||
|
||||
/* 主按钮 · 扁平 CTA(Stripe 式) */
|
||||
.btn-primary {
|
||||
position: relative;
|
||||
padding: 14px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
width: 100%;
|
||||
letter-spacing: -0.01em;
|
||||
text-transform: none;
|
||||
transition: opacity 0.15s ease, transform 0.12s ease;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.btn-primary:active:not(:disabled) {
|
||||
transform: scale(0.985);
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 筛选 Tab · iOS 分段风格 */
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 0 0 var(--space-section);
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.filter-tabs::-webkit-scrollbar { display: none; }
|
||||
|
||||
.filter-tab {
|
||||
flex-shrink: 0;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s, border-color 0.18s, color 0.18s;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
background: var(--surface-active);
|
||||
border-color: var(--border-gold);
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
text-size-adjust: 100%;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif;
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
min-height: 100%;
|
||||
min-height: -webkit-fill-available;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background-color: var(--bg-body);
|
||||
background-image: none;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -2;
|
||||
pointer-events: none;
|
||||
background-image: url('./assets/images/h5bg2.webp');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 240px;
|
||||
background-size: 84% auto;
|
||||
opacity: 0.55;
|
||||
filter: saturate(0.9);
|
||||
}
|
||||
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(ellipse 120% 60% at 50% -10%, rgba(244, 162, 97, 0.08), transparent 55%),
|
||||
linear-gradient(rgba(15, 32, 39, 0.72), rgba(15, 32, 39, 0.82));
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100%;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
a { color: inherit; text-decoration: none; }
|
||||
button { cursor: pointer; border: none; font-family: inherit; }
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
/* ── Scrollbar ── */
|
||||
::-webkit-scrollbar { width: 4px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(244, 162, 97, 0.3); border-radius: 4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(244, 162, 97, 0.5); }
|
||||
|
||||
/* ── Input ── */
|
||||
input {
|
||||
font-family: inherit;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 13px 16px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
transition: border-color 0.18s, box-shadow 0.18s;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
input::placeholder { color: var(--text-dim); }
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.08);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(244, 162, 97, 0.15);
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
@@ -244,96 +152,168 @@ input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-text-fill-color: var(--text);
|
||||
caret-color: var(--text);
|
||||
box-shadow: 0 0 0 1000px #0A2540 inset;
|
||||
box-shadow: 0 0 0 1000px var(--bg-input) inset;
|
||||
border: 1px solid var(--border);
|
||||
transition: background-color 99999s ease-out 0s;
|
||||
}
|
||||
|
||||
.odds-btn {
|
||||
background: var(--gradient-card-soft);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
/* ── Primary button (coral gradient CTA) ── */
|
||||
.btn-primary {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 48px;
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--gradient-primary);
|
||||
color: #FFFFFF;
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0.02em;
|
||||
width: 100%;
|
||||
transition: opacity 0.15s, transform 0.1s, box-shadow 0.2s;
|
||||
box-shadow: 0 4px 15px rgba(231, 111, 81, 0.35);
|
||||
}
|
||||
.btn-primary:hover { opacity: 0.9; box-shadow: 0 4px 20px rgba(244, 162, 97, 0.4); }
|
||||
.btn-primary:active:not(:disabled) {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Outline button ── */
|
||||
.btn-gold-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--primary);
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn-gold-outline:active:not(:disabled) {
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
border-color: var(--primary-light);
|
||||
}
|
||||
|
||||
/* ── Active tab ── */
|
||||
.tab-gold-active {
|
||||
background: rgba(244, 162, 97, 0.1) !important;
|
||||
border-color: var(--primary) !important;
|
||||
color: var(--primary-light) !important;
|
||||
}
|
||||
|
||||
/* ── Card frame (glass) ── */
|
||||
.glass-card,
|
||||
.card,
|
||||
.ps-gold-frame {
|
||||
position: relative;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.ps-gold-frame::before,
|
||||
.ps-gold-frame::after { display: none; }
|
||||
|
||||
/* Common list / panel surfaces */
|
||||
.bet-card,
|
||||
.order-card,
|
||||
.list-card,
|
||||
.match-card,
|
||||
.option-card,
|
||||
.my-bet-card,
|
||||
.score-card,
|
||||
.success-card,
|
||||
.avatar-card,
|
||||
.form-card,
|
||||
.wallet-stats-panel,
|
||||
.bet-stats-panel,
|
||||
.stats-panel,
|
||||
.confirm-modal,
|
||||
.qa-item,
|
||||
.qe-item,
|
||||
.league-block,
|
||||
.drawer,
|
||||
.slip-item,
|
||||
.hero,
|
||||
.section,
|
||||
.recharge-section,
|
||||
.panel-card {
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
box-shadow: var(--shadow-md);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.ps-gold-input {
|
||||
background: var(--bg-input) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
box-shadow: none;
|
||||
color: var(--text);
|
||||
padding: 11px 14px;
|
||||
}
|
||||
.ps-gold-input::placeholder { color: var(--text-dim); }
|
||||
.ps-gold-input:focus {
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: 0 0 0 3px rgba(244, 162, 97, 0.15) !important;
|
||||
}
|
||||
|
||||
/* ── Odds button ── */
|
||||
.odds-btn {
|
||||
background: var(--bg-card-elevated);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
text-align: center;
|
||||
min-width: 78px;
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
transition: border-color 0.18s, background 0.18s, transform 0.12s;
|
||||
transition: border-color 0.2s, box-shadow 0.2s, transform 0.15s, background 0.2s;
|
||||
}
|
||||
|
||||
.odds-btn:active { transform: scale(0.98); }
|
||||
|
||||
.odds-btn:active { transform: scale(0.96); }
|
||||
.odds-btn.selected {
|
||||
border-color: var(--border-strong);
|
||||
background: var(--bg-elevated);
|
||||
box-shadow: none;
|
||||
border: 1px solid var(--primary);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
box-shadow: 0 0 12px rgba(244, 162, 97, 0.12);
|
||||
}
|
||||
|
||||
.odds-btn.selected::before,
|
||||
.odds-btn.selected::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.odds-btn.selected .label,
|
||||
.odds-btn.selected .value {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.odds-btn.selected::after { display: none; }
|
||||
|
||||
.odds-btn .label {
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.odds-btn .value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-shadow: none;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--primary);
|
||||
margin-top: 2px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.odds-btn.selected .value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* ── Card ── */
|
||||
.card {
|
||||
position: relative;
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-card);
|
||||
margin-bottom: var(--space-section);
|
||||
box-shadow: var(--shadow-card-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 8%;
|
||||
right: 8%;
|
||||
height: 1px;
|
||||
background: var(--gradient-card-shine);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
padding: 0;
|
||||
border-left: none;
|
||||
color: var(--text);
|
||||
letter-spacing: -0.02em;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/* ── Section title ── */
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
padding: 4px 0 4px 12px;
|
||||
border-left: 3px solid var(--primary);
|
||||
color: var(--text);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* ── Empty / loading state ── */
|
||||
.state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -343,47 +323,94 @@ input:-webkit-autofill:active {
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Sub-page toolbar: full-bleed under .main horizontal padding */
|
||||
.sub-toolbar {
|
||||
box-sizing: border-box;
|
||||
margin-left: calc(-1 * var(--space-page-x));
|
||||
margin-right: calc(-1 * var(--space-page-x));
|
||||
width: calc(100% + 2 * var(--space-page-x));
|
||||
padding: var(--space-toolbar-top) var(--space-page-x) 8px;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.sub-toolbar--sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.sub-toolbar--glass {
|
||||
background: var(--bg-body);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Top-left diagonal ribbon for open-match status */
|
||||
.status-ribbon {
|
||||
/* ── Auth pages (login / register / forgot) ── */
|
||||
.auth-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: -28px;
|
||||
width: 100px;
|
||||
transform: rotate(-45deg);
|
||||
background: var(--success);
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
z-index: 3;
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
transparent 0%,
|
||||
transparent 32%,
|
||||
rgba(15, 32, 39, 0.25) 58%,
|
||||
rgba(15, 32, 39, 0.72) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.auth-page::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(165deg, rgba(244, 162, 97, 0.14) 0%, transparent 42%);
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: var(--glass-bg) !important;
|
||||
border: 1px solid var(--glass-border-accent) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
box-shadow: var(--shadow-lg), 0 0 28px rgba(244, 162, 97, 0.1) !important;
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.auth-card label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.auth-card .field-input {
|
||||
background: rgba(15, 32, 39, 0.55);
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.auth-card .field-input:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(244, 162, 97, 0.18);
|
||||
}
|
||||
|
||||
.auth-card .btn-login {
|
||||
background: var(--gradient-primary);
|
||||
border: none;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 16px rgba(231, 111, 81, 0.38);
|
||||
}
|
||||
|
||||
.auth-card .btn-login:active:not(:disabled) {
|
||||
opacity: 0.92;
|
||||
transform: scale(0.985);
|
||||
}
|
||||
|
||||
.auth-card .btn-secondary {
|
||||
background: rgba(15, 32, 39, 0.5);
|
||||
border-color: var(--border);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.auth-page .btn-skip-light {
|
||||
border-color: var(--border);
|
||||
background: rgba(22, 45, 54, 0.45);
|
||||
backdrop-filter: blur(8px);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.auth-page .btn-skip-light:active {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body::before {
|
||||
background-image: var(--gradient-hero) !important;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
.desktop-shell input:focus {
|
||||
border-color: var(--border-gold) !important;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2) !important;
|
||||
box-shadow: 0 0 0 2px rgba(244, 162, 97, 0.2) !important;
|
||||
}
|
||||
|
||||
/* Button hover overrides */
|
||||
@@ -55,7 +55,7 @@
|
||||
transition: background-color 0.2s, border-color 0.2s;
|
||||
}
|
||||
.desktop-shell .btn-gold-outline:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
border-color: var(--border-gold);
|
||||
}
|
||||
.desktop-shell .btn-gold-outline:active {
|
||||
@@ -79,7 +79,7 @@
|
||||
}
|
||||
|
||||
.desktop-skeleton {
|
||||
background: linear-gradient(90deg, #0A2540 25%, rgba(255, 255, 255, 0.06) 37%, #0A2540 63%);
|
||||
background: linear-gradient(90deg, var(--tertiary) 25%, var(--secondary) 37%, var(--tertiary) 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: desktop-skeleton-glow 1.4s ease infinite;
|
||||
}
|
||||
@@ -87,10 +87,10 @@
|
||||
@keyframes bet-locate-pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
||||
box-shadow: 0 0 0 0 rgba(244, 162, 97, 0);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.45);
|
||||
box-shadow: 0 0 0 3px rgba(244, 162, 97, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #001A33;
|
||||
/* background-image removed for theme-4 */
|
||||
background-color: var(--desktop-bg);
|
||||
background-image: var(--gradient-hero);
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-attachment: fixed;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktop-header-wrap {
|
||||
flex-shrink: 0;
|
||||
height: var(--desktop-header-h);
|
||||
background: #001A33;
|
||||
background: var(--bg-glass);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
z-index: 120;
|
||||
display: flex;
|
||||
@@ -28,7 +29,7 @@
|
||||
flex-shrink: 0;
|
||||
z-index: 110;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #001A33;
|
||||
background: rgba(15, 32, 39, 0.9);
|
||||
}
|
||||
|
||||
.desktop-shell.betslip-hidden {
|
||||
@@ -69,7 +70,8 @@
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(0, 26, 51, 0.9);
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.desktop-layout-betting {
|
||||
@@ -82,7 +84,8 @@
|
||||
|
||||
.desktop-betting-left {
|
||||
border-right: 1px solid var(--border);
|
||||
background: rgba(10, 37, 64, 0.8);
|
||||
background: rgba(15, 32, 39, 0.6);
|
||||
backdrop-filter: blur(10px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -175,7 +178,7 @@
|
||||
.desktop-hub-right {
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
background: rgba(15, 32, 39, 0.3);
|
||||
}
|
||||
|
||||
/* Auth Layout: Centered container */
|
||||
@@ -193,8 +196,10 @@
|
||||
width: 100%;
|
||||
max-width: 440px;
|
||||
padding: 30px;
|
||||
background: rgba(10, 37, 64, 0.95);
|
||||
background: var(--bg-card);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 18px;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
@@ -58,7 +58,7 @@
|
||||
}
|
||||
|
||||
.desktop-records-action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
background: rgba(244, 162, 97, 0.14);
|
||||
border-color: var(--border-gold);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
|
||||
.desktop-records-filter-chip {
|
||||
padding: 7px 16px;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
@@ -114,13 +114,13 @@
|
||||
|
||||
.desktop-records-filter-chip:hover {
|
||||
color: #fff;
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(244, 162, 97, 0.25);
|
||||
}
|
||||
|
||||
.desktop-records-filter-chip.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
border-color: rgba(244, 162, 97, 0.35);
|
||||
}
|
||||
|
||||
.desktop-wallet-subnav {
|
||||
@@ -133,7 +133,7 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
@@ -149,8 +149,8 @@
|
||||
|
||||
.desktop-wallet-subnav-link.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.28);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
border-color: rgba(244, 162, 97, 0.28);
|
||||
}
|
||||
|
||||
.desktop-records-panel {
|
||||
@@ -306,7 +306,7 @@
|
||||
|
||||
.desktop-records-status.status-won { background: rgba(52, 199, 89, 0.12); color: #4cd964; }
|
||||
.desktop-records-status.status-lost { background: rgba(255, 69, 58, 0.12); color: #ff453a; }
|
||||
.desktop-records-status.status-pending { background: rgba(255, 255, 255, 0.12); color: var(--primary-light); }
|
||||
.desktop-records-status.status-pending { background: rgba(244, 162, 97, 0.12); color: var(--primary-light); }
|
||||
.desktop-records-status.status-push { background: rgba(100, 100, 100, 0.12); color: #aaa; }
|
||||
.desktop-records-status.status-cancelled { background: rgba(100, 100, 100, 0.1); color: #777; }
|
||||
.desktop-records-status.status-default { background: rgba(100, 100, 100, 0.1); color: #888; }
|
||||
|
||||
@@ -1,36 +1,37 @@
|
||||
/* Teleport 弹窗挂在 body 下,须用 :root 暴露实心色 */
|
||||
/* Teleport 弹窗挂在 body 下,须用 :root 暴露实心色,不能只在 .desktop-shell 内定义 */
|
||||
:root {
|
||||
--desktop-surface-solid: #0A2540;
|
||||
--desktop-surface-elevated: #0A3558;
|
||||
--desktop-surface-header: #001A33;
|
||||
--desktop-border: rgba(255, 255, 255, 0.1);
|
||||
--desktop-surface-solid: #1a323c;
|
||||
--desktop-surface-elevated: #1c3742;
|
||||
--desktop-surface-header: #162d36;
|
||||
--desktop-border: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.desktop-shell {
|
||||
/* Color Palette overrides */
|
||||
--primary: rgba(255, 255, 255, 0.92);
|
||||
--primary-dark: rgba(255, 255, 255, 0.78);
|
||||
--primary-light: rgba(255, 255, 255, 0.68);
|
||||
--secondary: #0A3558;
|
||||
--tertiary: #001A33;
|
||||
--neutral: #8E8E93;
|
||||
--bg-body: #001A33;
|
||||
--bg-card: rgba(255, 255, 255, 0.045);
|
||||
--bg-hover: rgba(255, 255, 255, 0.06);
|
||||
--bg-elevated: rgba(255, 255, 255, 0.08);
|
||||
--text: rgba(255, 255, 255, 0.94);
|
||||
--text-muted: rgba(255, 255, 255, 0.56);
|
||||
--border: rgba(255, 255, 255, 0.1);
|
||||
--border-gold: rgba(255, 255, 255, 0.2);
|
||||
--border-gold-soft: rgba(255, 255, 255, 0.18);
|
||||
/* Color Palette overrides (theme-3 Dusk Coral style) */
|
||||
--primary: #F4A261;
|
||||
--primary-dark: #E76F51;
|
||||
--primary-light: #FFB980;
|
||||
--secondary: #203A43;
|
||||
--tertiary: #1A3038;
|
||||
--neutral: #8FA3AD;
|
||||
--bg-body: #0F2027;
|
||||
--bg-card: rgba(22, 45, 54, 0.82);
|
||||
--bg-hover: rgba(44, 83, 100, 0.55);
|
||||
--bg-elevated: rgba(28, 55, 66, 0.88);
|
||||
--text: #FFFFFF;
|
||||
--text-muted: #8FA3AD;
|
||||
--border: rgba(255, 255, 255, 0.08);
|
||||
--border-gold: #F4A261;
|
||||
--border-gold-soft: rgba(244, 162, 97, 0.25);
|
||||
--border-w: 1px;
|
||||
--border-w-thick: 1px;
|
||||
--danger: rgba(255, 255, 255, 0.62);
|
||||
--danger: #EF4444;
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 12px 32px rgba(0, 0, 0, 0.22);
|
||||
--shadow-gold: none;
|
||||
--glow-gold: none;
|
||||
--shadow: 0 4px 16px rgba(0, 0, 0, 0.38);
|
||||
--shadow-gold: 0 0 20px rgba(244, 162, 97, 0.2);
|
||||
--glow-gold: 0 0 6px rgba(244, 162, 97, 0.15);
|
||||
--odds-accent: #F4A261;
|
||||
|
||||
/* PC Dimensioning */
|
||||
--desktop-header-h: 56px;
|
||||
@@ -40,16 +41,17 @@
|
||||
--desktop-right-betslip-collapsed-w: 40px;
|
||||
|
||||
/* Desktop specific added vars */
|
||||
--desktop-bg: #001A33;
|
||||
--desktop-border: rgba(255, 255, 255, 0.1);
|
||||
--desktop-sidebar-bg: rgba(10, 37, 64, 0.95);
|
||||
--desktop-sidebar-panel-bg: linear-gradient(rgba(0, 26, 51, 0.88), rgba(0, 26, 51, 0.88));
|
||||
--desktop-surface-solid: #0A2540;
|
||||
--desktop-surface-elevated: #0A3558;
|
||||
--desktop-surface-header: #001A33;
|
||||
--desktop-bg: #0F2027;
|
||||
--desktop-border: rgba(255, 255, 255, 0.08);
|
||||
--desktop-sidebar-bg: rgba(22, 45, 54, 0.82);
|
||||
--desktop-sidebar-panel-bg: linear-gradient(135deg, rgba(22, 45, 54, 0.95), rgba(15, 32, 39, 0.98));
|
||||
--desktop-surface-solid: #1a323c;
|
||||
--desktop-surface-elevated: #1c3742;
|
||||
--desktop-surface-header: #162d36;
|
||||
--gradient-hero: linear-gradient(135deg, #0F2027, #203A43, #0F2027);
|
||||
|
||||
/* Reset typography inside desktop */
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
color: var(--text);
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
export type WalletTransactionStats = {
|
||||
netAmount?: string;
|
||||
byType?: Array<{ transactionType?: string; totalAmount?: string }>;
|
||||
};
|
||||
|
||||
const CASHBACK_TYPES = new Set(['CASHBACK', 'CASHBACK_DEPOSIT']);
|
||||
|
||||
/** Sum wallet credits from cashback transaction types (matches WalletDetailView fallback). */
|
||||
export function sumCashbackFromStats(stats: WalletTransactionStats | null | undefined): number {
|
||||
let sum = 0;
|
||||
for (const group of stats?.byType ?? []) {
|
||||
const type = group.transactionType?.toUpperCase() ?? '';
|
||||
if (CASHBACK_TYPES.has(type)) {
|
||||
sum += Math.abs(parseFloat(group.totalAmount ?? '0') || 0);
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { useSmsCode } from '../composables/useSmsCode';
|
||||
import api from '../api';
|
||||
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
||||
import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
|
||||
import loginBg from '../assets/images/h5bg.webp';
|
||||
import loginBg from '../assets/images/h5bg2.webp';
|
||||
|
||||
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
|
||||
|
||||
@@ -100,109 +100,84 @@ function goLogin() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="login-page"
|
||||
:class="{ 'login-page--desktop': props.desktop }"
|
||||
:style="props.desktop ? undefined : { backgroundImage: `url(${loginBg})` }"
|
||||
>
|
||||
<div class="login-page auth-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"
|
||||
:class="{ 'ps-gold-frame': !props.desktop }"
|
||||
>
|
||||
<h2 class="form-title">{{ t('auth.forgot_password_title') }}</h2>
|
||||
<form @submit.prevent="submit" class="login-form ps-gold-frame auth-card">
|
||||
<h2 class="form-title">{{ t('auth.forgot_password_title') }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.phone') }}</label>
|
||||
<div class="inline-row">
|
||||
<PhoneCountrySelect v-model="countryIso" />
|
||||
<input
|
||||
v-model="phone"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel-national"
|
||||
:placeholder="t('auth.phone_local_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.sms_code') }}</label>
|
||||
<div class="inline-row">
|
||||
<input
|
||||
v-model="smsCode"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
:placeholder="t('auth.sms_code_placeholder')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
:disabled="sending || countdown > 0 || !phone.trim()"
|
||||
@click="sendCode"
|
||||
>
|
||||
{{ countdown > 0 ? `${countdown}s` : t('auth.send_sms') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.password') }}</label>
|
||||
<div class="field">
|
||||
<label>{{ t('auth.phone') }}</label>
|
||||
<div class="inline-row">
|
||||
<PhoneCountrySelect v-model="countryIso" />
|
||||
<input
|
||||
v-model="password"
|
||||
v-model="phone"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
type="password"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
:placeholder="t('auth.password_placeholder')"
|
||||
autocomplete="tel-national"
|
||||
:placeholder="t('auth.phone_local_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.confirm_password') }}</label>
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
/>
|
||||
</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>
|
||||
|
||||
<button v-if="props.desktop" type="button" class="btn-skip btn-skip--desktop" @click="goLogin">
|
||||
{{ t('auth.back_to_login') }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div v-if="!props.desktop" class="login-actions">
|
||||
<button type="button" class="btn-skip" @click="goLogin">
|
||||
{{ t('auth.back_to_login') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.sms_code') }}</label>
|
||||
<div class="inline-row">
|
||||
<input
|
||||
v-model="smsCode"
|
||||
class="field-input"
|
||||
inputmode="numeric"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
:placeholder="t('auth.sms_code_placeholder')"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-secondary"
|
||||
:disabled="sending || countdown > 0 || !phone.trim()"
|
||||
@click="sendCode"
|
||||
>
|
||||
{{ countdown > 0 ? `${countdown}s` : t('auth.send_sms') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.password') }}</label>
|
||||
<input
|
||||
v-model="password"
|
||||
class="field-input"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
:placeholder="t('auth.password_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.confirm_password') }}</label>
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
class="field-input"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="fieldError()" class="error">{{ fieldError() }}</p>
|
||||
|
||||
<button type="submit" class="btn-login btn-primary" :disabled="loading">
|
||||
{{ t('auth.reset_password_btn') }}
|
||||
</button>
|
||||
<button type="button" class="btn-skip" @click="goLogin">
|
||||
{{ t('auth.back_to_login') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -211,7 +186,7 @@ function goLogin() {
|
||||
position: absolute;
|
||||
top: max(12px, env(safe-area-inset-top));
|
||||
right: 16px;
|
||||
z-index: 10;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
@@ -230,90 +205,24 @@ function goLogin() {
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.login-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 34, 68, 0.35) 0%,
|
||||
rgba(0, 34, 68, 0.78) 52%,
|
||||
rgba(0, 34, 68, 0.94) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ===== 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-page--desktop::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-stack {
|
||||
.login-form {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-stack {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.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-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0 14px 4px;
|
||||
padding: 22px 20px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 16px;
|
||||
margin: 0 0 4px;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-page--desktop .form-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
text-align: left;
|
||||
color: var(--primary-light);
|
||||
letter-spacing: 0.04em;
|
||||
text-align: center;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -322,59 +231,26 @@ function goLogin() {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.field-input::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-gold-soft);
|
||||
}
|
||||
|
||||
/* 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-strong);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.login-page--desktop .field-input:hover:not(:focus) {
|
||||
border-color: var(--border-gold-soft);
|
||||
}
|
||||
|
||||
.inline-row {
|
||||
@@ -394,31 +270,12 @@ label {
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--primary);
|
||||
font-size: 11px;
|
||||
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: var(--border-gold-soft);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.btn-secondary:not(:disabled):active {
|
||||
border-color: var(--border-gold);
|
||||
background: var(--surface-accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
@@ -432,28 +289,6 @@ 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: var(--surface-accent);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-login:disabled {
|
||||
@@ -461,40 +296,19 @@ label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.btn-skip {
|
||||
margin-top: 2px;
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-skip:active {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.btn-skip--desktop {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-input);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-skip--desktop:hover:not(:disabled) {
|
||||
border-color: var(--border-gold-soft);
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.error {
|
||||
@@ -505,22 +319,92 @@ label {
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* ===== 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-page--desktop .login-form {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.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(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(244, 162, 97, 0.18);
|
||||
}
|
||||
.login-page--desktop .field-input:hover:not(:focus) {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.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(244, 162, 97, 0.1);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.login-page--desktop .btn-login:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.login-page--desktop .btn-login:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.login-page--desktop .btn-skip {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(15, 32, 39, 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);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
border-left: 2px solid var(--danger, #FF453A);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,8 +8,7 @@ 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';
|
||||
import loginBg from '../assets/images/h5bg2.webp';
|
||||
|
||||
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
|
||||
|
||||
@@ -88,125 +87,64 @@ function goRegister() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="login-page"
|
||||
:class="{ 'login-page--desktop': props.desktop }"
|
||||
:style="props.desktop ? undefined : { backgroundImage: `url(${loginBg})` }"
|
||||
>
|
||||
<div class="login-page auth-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"
|
||||
: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" :class="{ 'auth-tabs--desktop': props.desktop }">
|
||||
<button
|
||||
type="button"
|
||||
class="auth-tab"
|
||||
:class="{ active: loginMode === 'account' }"
|
||||
@click="switchLoginMode('account')"
|
||||
>
|
||||
{{ t('auth.login_by_account') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="auth-tab"
|
||||
:class="{ active: loginMode === 'phone' }"
|
||||
@click="switchLoginMode('phone')"
|
||||
>
|
||||
{{ t('auth.login_by_phone') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loginMode === 'account'" class="field">
|
||||
<label>{{ t('auth.username') }}</label>
|
||||
<input
|
||||
v-model="account"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
required
|
||||
autocomplete="username"
|
||||
maxlength="32"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.username_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else class="field">
|
||||
<label>{{ t('auth.phone') }}</label>
|
||||
<div class="inline-row">
|
||||
<PhoneCountrySelect v-model="countryIso" />
|
||||
<input
|
||||
v-model="phone"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel-national"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.phone_local_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.password') }}</label>
|
||||
<input
|
||||
v-model="password"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
: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 btn-skip--desktop" :disabled="loading" @click="goRegister">
|
||||
{{ t('auth.go_register') }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div v-if="!props.desktop" class="login-actions">
|
||||
<button type="button" class="btn-skip" @click="goRegister">
|
||||
{{ t('auth.go_register') }}
|
||||
</button>
|
||||
<button type="button" class="btn-skip-light" @click="continueBrowsing">
|
||||
{{ t('auth.continue_browsing') }}
|
||||
</button>
|
||||
<form @submit.prevent="submit" class="login-form ps-gold-frame auth-card">
|
||||
<div v-if="loginMode === 'account'" class="field">
|
||||
<label>{{ t('auth.username') }}</label>
|
||||
<input
|
||||
v-model="account"
|
||||
class="field-input"
|
||||
required
|
||||
autocomplete="username"
|
||||
maxlength="32"
|
||||
:placeholder="t('auth.username_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="field">
|
||||
<label>{{ t('auth.phone') }}</label>
|
||||
<div class="inline-row">
|
||||
<PhoneCountrySelect v-model="countryIso" />
|
||||
<input
|
||||
v-model="phone"
|
||||
class="field-input"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel-national"
|
||||
:placeholder="t('auth.phone_local_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.password') }}</label>
|
||||
<input v-model="password" class="field-input" type="password" required autocomplete="current-password" />
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-forgot" @click="goForgotPassword">
|
||||
{{ t('auth.forgot_password') }}
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn-mode-switch" @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-primary" :disabled="loading">
|
||||
{{ t('auth.login') }}
|
||||
</button>
|
||||
<button type="button" class="btn-skip" @click="goRegister">
|
||||
{{ t('auth.go_register') }}
|
||||
</button>
|
||||
</form>
|
||||
<button v-if="!props.desktop" type="button" class="btn-skip-light" @click="continueBrowsing">
|
||||
{{ t('auth.continue_browsing') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -215,7 +153,7 @@ function goRegister() {
|
||||
position: absolute;
|
||||
top: max(12px, env(safe-area-inset-top));
|
||||
right: 16px;
|
||||
z-index: 10;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
@@ -227,169 +165,22 @@ function goRegister() {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 34vh 20px max(20px, env(safe-area-inset-bottom));
|
||||
padding: 34vh 20px calc(15vh + max(28px, env(safe-area-inset-bottom)));
|
||||
background-color: var(--bg-body);
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.login-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 34, 68, 0.35) 0%,
|
||||
rgba(0, 34, 68, 0.78) 52%,
|
||||
rgba(0, 34, 68, 0.94) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ===== 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-page--desktop::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-stack {
|
||||
.login-form {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-stack {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 14px;
|
||||
transition: border-color 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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(0, 26, 51, 0.82);
|
||||
backdrop-filter: blur(2px);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.form-busy-text {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.btn-inline-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0 14px 4px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
text-align: left;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.auth-tabs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
padding: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.auth-tabs--desktop {
|
||||
margin-bottom: 6px;
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.auth-tab {
|
||||
flex: 1;
|
||||
min-height: 34px;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.auth-tab.active {
|
||||
border-color: var(--border-gold);
|
||||
color: var(--text);
|
||||
background: var(--surface-accent);
|
||||
}
|
||||
|
||||
.login-page--desktop .auth-tab {
|
||||
min-height: 40px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-page--desktop .auth-tab.active {
|
||||
border-color: var(--border-strong);
|
||||
padding: 22px 20px;
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -398,47 +189,20 @@ function goRegister() {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.field-input::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-gold-soft);
|
||||
}
|
||||
|
||||
/* 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-strong);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.login-page--desktop .field-input:hover:not(:focus) {
|
||||
border-color: var(--border-gold-soft);
|
||||
}
|
||||
|
||||
.inline-row {
|
||||
@@ -454,14 +218,20 @@ function goRegister() {
|
||||
|
||||
label {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
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;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--primary-light);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-forgot {
|
||||
@@ -474,20 +244,14 @@ label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-forgot:active {
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-forgot:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-forgot:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
.btn-mode-switch:active {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
@@ -497,28 +261,6 @@ 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: var(--surface-accent);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-login:disabled {
|
||||
@@ -526,80 +268,46 @@ label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.btn-login.is-loading:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-skip {
|
||||
padding: 6px 14px;
|
||||
margin-top: 2px;
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-skip:active {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.btn-skip--desktop {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-input);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-skip--desktop:hover:not(:disabled) {
|
||||
border-color: var(--border-gold-soft);
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.btn-skip:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.btn-skip-light {
|
||||
padding: 6px 16px;
|
||||
border: none;
|
||||
position: absolute;
|
||||
bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-skip-light:active {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.error {
|
||||
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);
|
||||
}
|
||||
|
||||
.success {
|
||||
@@ -608,30 +316,97 @@ label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
animation: login-fade-in 0.25s ease;
|
||||
}
|
||||
|
||||
/* ===== 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-page--desktop .login-form {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
.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(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(244, 162, 97, 0.18);
|
||||
}
|
||||
.login-page--desktop .field-input:hover:not(:focus) {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.login-page--desktop .btn-mode-switch:hover {
|
||||
color: var(--primary-light);
|
||||
}
|
||||
.login-page--desktop .btn-forgot:hover {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
.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(244, 162, 97, 0.1);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.login-page--desktop .btn-login:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.login-page--desktop .btn-login:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.login-page--desktop .btn-skip {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(15, 32, 39, 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);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.login-page--desktop .success {
|
||||
font-size: 12px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
background: rgba(52, 199, 89, 0.08);
|
||||
background: rgba(110, 231, 160, 0.08);
|
||||
border-left: 2px solid var(--success);
|
||||
}
|
||||
|
||||
@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>
|
||||
|
||||
@@ -10,6 +10,8 @@ import { sanitizeAnnouncementHtml, stripHtml } from '../utils/html';
|
||||
|
||||
const FALLBACK_IMG = '/uploads/banners/welcome.svg';
|
||||
|
||||
const { embedded = false } = defineProps<{ embedded?: boolean }>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
@@ -29,7 +31,13 @@ const item = computed<PlayerContentItem | null>(() => {
|
||||
const isBanner = computed(() => item.value?.contentType === 'BANNER');
|
||||
|
||||
function goBack() {
|
||||
router.back();
|
||||
if (embedded) return;
|
||||
const prev = (router.options.history.state as { back?: string } | null)?.back;
|
||||
if (prev && prev !== route.fullPath) {
|
||||
router.back();
|
||||
return;
|
||||
}
|
||||
void router.push('/announcements');
|
||||
}
|
||||
|
||||
function goList() {
|
||||
@@ -105,19 +113,23 @@ watch(announcementId, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="announce-detail">
|
||||
<header class="page-header">
|
||||
<div class="announce-detail" :class="{ 'is-embedded': embedded }">
|
||||
<header v-if="!embedded" class="page-header">
|
||||
<button type="button" class="back-btn" :aria-label="t('announcements.back')" @click="goBack">‹</button>
|
||||
<h1>{{ t('announcements.detail_title') }}</h1>
|
||||
</header>
|
||||
|
||||
<header v-else class="page-header page-header--hub">
|
||||
<h1>{{ t('announcements.detail_title') }}</h1>
|
||||
</header>
|
||||
|
||||
<div v-if="loading && !item" class="state">
|
||||
<GoldSpinner :size="36" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="!item" class="empty">
|
||||
<p>{{ t('announcements.not_found') }}</p>
|
||||
<button type="button" class="link-btn" @click="goList">{{ t('announcements.view_all') }}</button>
|
||||
<button v-if="!embedded" type="button" class="link-btn" @click="goList">{{ t('announcements.view_all') }}</button>
|
||||
</div>
|
||||
|
||||
<article v-else class="detail-article">
|
||||
@@ -176,7 +188,7 @@ watch(announcementId, () => {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
@@ -185,7 +197,7 @@ watch(announcementId, () => {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.state,
|
||||
@@ -200,10 +212,10 @@ watch(announcementId, () => {
|
||||
|
||||
.link-btn {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 8px 14px;
|
||||
background: var(--surface-active);
|
||||
color: var(--text);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -243,14 +255,14 @@ watch(announcementId, () => {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
color: #d0d0d0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.detail-body :deep(p) {
|
||||
@@ -266,7 +278,7 @@ watch(announcementId, () => {
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 14px 0;
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.detail-body :deep(ul),
|
||||
@@ -304,9 +316,9 @@ watch(announcementId, () => {
|
||||
width: 100%;
|
||||
padding: 11px 14px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--gradient-primary);
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -315,6 +327,30 @@ watch(announcementId, () => {
|
||||
.link-action--outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.announce-detail.is-embedded {
|
||||
padding: 0 4px 24px;
|
||||
}
|
||||
|
||||
.is-embedded .page-header--hub {
|
||||
padding: 0 0 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.is-embedded .page-header--hub h1 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.is-embedded .detail-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.is-embedded .detail-body-wrap {
|
||||
padding-top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePlayerHome } from '../composables/usePlayerHome';
|
||||
import { stripHtml } from '../utils/html';
|
||||
|
||||
const { embedded = false } = defineProps<{ embedded?: boolean }>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const { announcementItems, loading, load } = usePlayerHome();
|
||||
|
||||
const items = computed(() => announcementItems.value);
|
||||
|
||||
function isActive(id: string) {
|
||||
return embedded && String(route.params.id ?? '') === id;
|
||||
}
|
||||
|
||||
function onBack() {
|
||||
if (embedded) {
|
||||
void router.push('/');
|
||||
return;
|
||||
}
|
||||
goBack();
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
router.back();
|
||||
}
|
||||
@@ -45,9 +60,16 @@ onActivated(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="announce-page">
|
||||
<div class="announce-page" :class="{ 'is-embedded': embedded }">
|
||||
<header class="page-header">
|
||||
<button type="button" class="back-btn" :aria-label="t('announcements.back')" @click="goBack">‹</button>
|
||||
<button
|
||||
type="button"
|
||||
class="back-btn"
|
||||
:aria-label="t('announcements.back')"
|
||||
@click="onBack"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<h1>{{ t('announcements.title') }}</h1>
|
||||
</header>
|
||||
|
||||
@@ -65,6 +87,7 @@ onActivated(() => {
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="list-row"
|
||||
:class="{ 'is-active': isActive(item.id) }"
|
||||
@click="openDetail(item.id)"
|
||||
>
|
||||
<span class="row-main">
|
||||
@@ -99,16 +122,17 @@ onActivated(() => {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.state,
|
||||
@@ -175,4 +199,101 @@ onActivated(() => {
|
||||
color: var(--text-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Desktop hub sidebar */
|
||||
.announce-page.is-embedded {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 16px 16px 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.is-embedded .page-header {
|
||||
flex-shrink: 0;
|
||||
padding: 0 4px 12px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.is-embedded .page-header h1 {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.is-embedded .back-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.is-embedded .list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
margin: 0 -4px;
|
||||
padding: 0 4px 16px;
|
||||
}
|
||||
|
||||
.is-embedded .list-row {
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 12px 12px;
|
||||
margin-bottom: 4px;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
border-radius: 8px;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.is-embedded .list-row:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.is-embedded .list-row.is-active {
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
border-color: var(--border-active);
|
||||
}
|
||||
|
||||
.is-embedded .list-row.is-active .title {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.is-embedded .list-row.is-active .chevron {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.is-embedded .title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.is-embedded .preview {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.is-embedded .date {
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.is-embedded .chevron {
|
||||
margin-top: 2px;
|
||||
font-size: 16px;
|
||||
align-self: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -142,9 +142,9 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
</div>
|
||||
|
||||
<!-- back bar -->
|
||||
<header class="top-bar sub-toolbar">
|
||||
<div class="top-bar">
|
||||
<button class="back-btn" @click="router.back()">‹ {{ t('history.back') }}</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="state">
|
||||
<GoldSpinner :size="36" />
|
||||
@@ -275,7 +275,7 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
padding: 4px 0 8px;
|
||||
@@ -293,9 +293,9 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── hero ── */
|
||||
/* ─ hero ── */
|
||||
.hero {
|
||||
border-radius: var(--radius);
|
||||
border-radius: 12px;
|
||||
padding: 20px 20px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -305,44 +305,32 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero.won {
|
||||
background: linear-gradient(135deg, rgba(46, 204, 113, 0.18), rgba(46, 204, 113, 0.06));
|
||||
border: 1px solid rgba(46, 204, 113, 0.35);
|
||||
}
|
||||
.hero.lost {
|
||||
background: linear-gradient(135deg, var(--surface-accent), rgba(255, 255, 255, 0.06));
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
.hero.pending {
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
}
|
||||
.hero.push {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.hero.won { background: rgba(16, 185, 129, 0.1); border: 1px solid rgba(16, 185, 129, 0.25); }
|
||||
.hero.lost { background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.25); }
|
||||
.hero.pending { background: rgba(255, 179, 0, 0.1); border: 1px solid rgba(255, 179, 0, 0.25); }
|
||||
.hero.push { background: var(--bg-card); border: 1px solid var(--border); }
|
||||
|
||||
.hero-status {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: none;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.hero.won .hero-status { color: var(--success); }
|
||||
.hero.lost .hero-status { color: var(--primary); }
|
||||
.hero.pending .hero-status { color: var(--primary-light); }
|
||||
.hero.lost .hero-status { color: var(--danger); }
|
||||
.hero.pending .hero-status { color: var(--warning); }
|
||||
.hero.push .hero-status { color: var(--text-muted); }
|
||||
|
||||
.hero-return {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
font-size: 36px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.01em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.hero.won .hero-return { color: var(--success); text-shadow: none; }
|
||||
.hero.lost .hero-return { color: var(--primary); }
|
||||
.hero.pending .hero-return { color: var(--primary-light); }
|
||||
.hero.won .hero-return { color: var(--success); }
|
||||
.hero.lost .hero-return { color: var(--danger); }
|
||||
.hero.pending .hero-return { color: var(--warning); }
|
||||
.hero.push .hero-return { color: var(--text-muted); }
|
||||
|
||||
.hero-return-label {
|
||||
@@ -356,9 +344,10 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
.section {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-card) 16px;
|
||||
margin-bottom: var(--space-section);
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section-head {
|
||||
@@ -376,15 +365,15 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
|
||||
.placed-time {
|
||||
font-size: 11px;
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.match-name {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
font-weight: 900;
|
||||
color: var(--text);
|
||||
line-height: 1.35;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -442,35 +431,35 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
.score-period {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: none;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.score-value.muted {
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.no-score {
|
||||
font-size: 11.5px;
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* section title */
|
||||
.section-title {
|
||||
font-size: 11px;
|
||||
font-size: 10.5px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: none;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@@ -488,12 +477,12 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
gap: 10px;
|
||||
background: var(--bg-body);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.leg-row.won { border-color: rgba(46, 204, 113, 0.35); }
|
||||
.leg-row.lost { border-color: rgba(255, 255, 255, 0.35); }
|
||||
.leg-row.won { border-color: rgba(16, 185, 129, 0.3); }
|
||||
.leg-row.lost { border-color: rgba(239, 68, 68, 0.3); }
|
||||
|
||||
.leg-left {
|
||||
display: flex;
|
||||
@@ -508,7 +497,7 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: var(--bg-card);
|
||||
background: var(--secondary);
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
@@ -518,8 +507,8 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.leg-num.won { background: rgba(46, 204, 113, 0.18); color: var(--success); }
|
||||
.leg-num.lost { background: rgba(255, 255, 255, 0.18); color: var(--primary); }
|
||||
.leg-num.won { background: rgba(16, 185, 129, 0.15); color: var(--success); }
|
||||
.leg-num.lost { background: rgba(239, 68, 68, 0.15); color: var(--danger); }
|
||||
|
||||
.leg-body {
|
||||
display: flex;
|
||||
@@ -553,7 +542,7 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
}
|
||||
|
||||
.leg-scores .muted-score {
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.leg-right {
|
||||
@@ -566,23 +555,23 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
|
||||
.leg-odds-val {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
font-weight: 900;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.leg-result-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: none;
|
||||
padding: 2px 8px;
|
||||
font-size: 9px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.leg-result-badge.won { color: var(--success); background: rgba(46, 204, 113, 0.12); }
|
||||
.leg-result-badge.lost { color: var(--primary); background: rgba(255, 255, 255, 0.1); }
|
||||
.leg-result-badge.push { color: var(--text-muted); background: var(--bg-card); }
|
||||
.leg-result-badge.pending { color: var(--neutral); background: var(--bg-body); }
|
||||
.leg-result-badge.won { color: var(--success); background: rgba(16, 185, 129, 0.12); }
|
||||
.leg-result-badge.lost { color: var(--danger); background: rgba(239, 68, 68, 0.12); }
|
||||
.leg-result-badge.push { color: var(--text-muted); background: var(--secondary); }
|
||||
.leg-result-badge.pending { color: var(--text-muted); background: var(--bg-body); }
|
||||
|
||||
/* summary */
|
||||
.summary-rows {
|
||||
@@ -598,7 +587,7 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -607,34 +596,35 @@ const matchPhaseText = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
}
|
||||
|
||||
.sum-row.muted {
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.odds-val {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
color: var(--warning);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.amt-won {
|
||||
color: var(--success);
|
||||
font-weight: 700;
|
||||
font-weight: 900;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.amt-pending {
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
color: var(--warning);
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.amt-lost {
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
color: var(--danger);
|
||||
font-weight: 900;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.bet-no-val {
|
||||
font-family: monospace;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -117,11 +117,11 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
<template>
|
||||
<div class="cashback-page">
|
||||
<header class="top-bar sub-toolbar">
|
||||
<div class="top-bar">
|
||||
<button type="button" class="back-btn" @click="router.back()">
|
||||
‹ {{ t('history.back') }}
|
||||
</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div class="pull-indicator" :style="pullIndicatorStyle()">
|
||||
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
|
||||
@@ -202,7 +202,7 @@ const pullIndicatorStyle = () => ({
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
padding: 4px 0 8px;
|
||||
@@ -243,18 +243,18 @@ const pullIndicatorStyle = () => ({
|
||||
gap: 4px;
|
||||
padding: 18px 16px 16px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, rgba(244, 162, 97, 0.15), var(--bg-card));
|
||||
border: 1px solid var(--border);
|
||||
text-align: center;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.hero-amount {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
line-height: 1.2;
|
||||
text-shadow: 0 0 20px var(--surface-accent);
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
@@ -273,11 +273,8 @@ const pullIndicatorStyle = () => ({
|
||||
}
|
||||
|
||||
.list-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.record-row {
|
||||
@@ -290,7 +287,7 @@ const pullIndicatorStyle = () => ({
|
||||
}
|
||||
|
||||
.record-row--highlight {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
box-shadow: inset 3px 0 0 var(--primary);
|
||||
}
|
||||
|
||||
@@ -318,7 +315,7 @@ const pullIndicatorStyle = () => ({
|
||||
.row-amount {
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.row-period {
|
||||
@@ -331,9 +328,9 @@ const pullIndicatorStyle = () => ({
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
background: var(--surface-accent);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
color: var(--warning);
|
||||
background: rgba(255, 179, 0, 0.12);
|
||||
border: 1px solid rgba(255, 179, 0, 0.3);
|
||||
border-radius: 999px;
|
||||
padding: 3px 10px;
|
||||
}
|
||||
@@ -356,7 +353,7 @@ const pullIndicatorStyle = () => ({
|
||||
padding-top: 8px;
|
||||
border-top: 1px dashed var(--border);
|
||||
font-size: 11px;
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.row-batch {
|
||||
@@ -387,7 +384,7 @@ const pullIndicatorStyle = () => ({
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sentinel {
|
||||
@@ -403,7 +400,7 @@ const pullIndicatorStyle = () => ({
|
||||
.end-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
padding: 16px 0 4px;
|
||||
letter-spacing: 0.03em;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onActivated, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { usePlayerMatches } from '../composables/usePlayerMatches';
|
||||
import LeagueAccordionItem from '../components/LeagueAccordionItem.vue';
|
||||
import MatchBetCard from '../components/MatchBetCard.vue';
|
||||
import OutrightPanel from '../components/outright/OutrightPanel.vue';
|
||||
import emptyMatchesImg from '../assets/images/empty-matches.svg';
|
||||
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
|
||||
@@ -59,8 +60,10 @@ interface LeagueGroup {
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const mainTab = ref<MainTab>('matches');
|
||||
const searchQuery = ref('');
|
||||
const filterState = ref({
|
||||
time: 'all' as 'all' | 'today' | 'early',
|
||||
status: 'open' as 'all' | 'open' | 'settled',
|
||||
@@ -96,10 +99,23 @@ function normalizeLeagueName(name: string): string {
|
||||
.trim();
|
||||
}
|
||||
|
||||
function syncSearchFromRoute() {
|
||||
const q = route.query.search;
|
||||
searchQuery.value = typeof q === 'string' ? q : '';
|
||||
}
|
||||
|
||||
function matchesSearchKeyword(m: Match, keyword: string) {
|
||||
if (!keyword) return true;
|
||||
const haystack = `${m.homeTeamName} ${m.awayTeamName} ${m.leagueName}`.toLowerCase();
|
||||
return haystack.includes(keyword);
|
||||
}
|
||||
|
||||
const filteredMatches = computed(() => {
|
||||
if (mainTab.value !== 'matches') return [];
|
||||
const now = filterNow.value;
|
||||
const keyword = searchQuery.value.trim().toLowerCase();
|
||||
return matches.value.filter((m) => {
|
||||
if (!matchesSearchKeyword(m, keyword)) return false;
|
||||
let timeMatch = true;
|
||||
if (filterState.value.time === 'today') {
|
||||
timeMatch = isInTodayMatchWindow(m.startTime, now);
|
||||
@@ -196,6 +212,16 @@ function buildLeagueGroups(source: Match[]): LeagueGroup[] {
|
||||
|
||||
const leagueGroups = computed(() => buildLeagueGroups(filteredMatches.value));
|
||||
|
||||
const isSearchActive = computed(() => searchQuery.value.trim().length > 0);
|
||||
|
||||
const sortedFilteredMatches = computed(() =>
|
||||
[...filteredMatches.value].sort(
|
||||
(a, b) =>
|
||||
(a.displayOrder ?? 0) - (b.displayOrder ?? 0) ||
|
||||
new Date(a.startTime).getTime() - new Date(b.startTime).getTime(),
|
||||
),
|
||||
);
|
||||
|
||||
const expandedLeagues = ref(new Set<string>());
|
||||
|
||||
watch(leagueGroups, (groups) => {
|
||||
@@ -226,9 +252,15 @@ function selectMainTab(tab: MainTab) {
|
||||
onActivated(() => {
|
||||
filterNow.value = new Date();
|
||||
filterState.value.time = 'all';
|
||||
syncSearchFromRoute();
|
||||
void loadSummary(true, true);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query.search,
|
||||
() => syncSearchFromRoute(),
|
||||
);
|
||||
|
||||
function goMatch(id: string) {
|
||||
router.push(`/match/${id}`);
|
||||
}
|
||||
@@ -247,7 +279,7 @@ function goMatch(id: string) {
|
||||
<button
|
||||
type="button"
|
||||
class="main-tab"
|
||||
:class="{ active: mainTab === 'matches' }"
|
||||
:class="{ active: mainTab === 'matches', 'tab-gold-active': mainTab === 'matches' }"
|
||||
@click="selectMainTab('matches')"
|
||||
>
|
||||
<span class="tab-icon">⚽</span>
|
||||
@@ -256,7 +288,7 @@ function goMatch(id: string) {
|
||||
<button
|
||||
type="button"
|
||||
class="main-tab"
|
||||
:class="{ active: mainTab === 'outright' }"
|
||||
:class="{ active: mainTab === 'outright', 'tab-gold-active': mainTab === 'outright' }"
|
||||
@click="selectMainTab('outright')"
|
||||
>
|
||||
<span class="tab-icon">🏆</span>
|
||||
@@ -265,11 +297,22 @@ function goMatch(id: string) {
|
||||
</div>
|
||||
|
||||
<div :class="['tab-panel', { 'tab-panel--hidden': mainTab !== 'matches' }]">
|
||||
<div class="search-bar">
|
||||
<span class="search-icon" aria-hidden="true">⌕</span>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
:placeholder="t('search.placeholder')"
|
||||
enterkeyhint="search"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="filters-bar">
|
||||
<div class="phase-filter">
|
||||
<div class="league-dropdown">
|
||||
<button type="button" class="dropdown-trigger" @click="dropdownOpen = !dropdownOpen">
|
||||
<span class="dropdown-text">{{ selectedLeagueName || t('bet.filter_leagues_all') }}</span>
|
||||
<span>{{ selectedLeagueName || t('bet.filter_leagues_all') }}</span>
|
||||
<span class="arrow-icon" :class="{ open: dropdownOpen }">▼</span>
|
||||
</button>
|
||||
<div v-if="dropdownOpen" class="dropdown-backdrop" @click="dropdownOpen = false"></div>
|
||||
@@ -340,7 +383,22 @@ function goMatch(id: string) {
|
||||
</div>
|
||||
<template v-else>
|
||||
<div>
|
||||
<div v-if="leagueGroups.length" class="league-list">
|
||||
<div v-if="isSearchActive && sortedFilteredMatches.length" class="search-results">
|
||||
<p class="search-results-meta">
|
||||
{{ t('search.results_count', { count: sortedFilteredMatches.length }) }}
|
||||
</p>
|
||||
<div class="search-match-list">
|
||||
<article
|
||||
v-for="match in sortedFilteredMatches"
|
||||
:key="match.id"
|
||||
class="search-result-item"
|
||||
>
|
||||
<p class="search-result-league">{{ normalizeLeagueName(match.leagueName) }}</p>
|
||||
<MatchBetCard :match="match" @bet="goMatch" />
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!isSearchActive && leagueGroups.length" class="league-list">
|
||||
<LeagueAccordionItem
|
||||
v-for="group in leagueGroups"
|
||||
:key="group.leagueId"
|
||||
@@ -355,7 +413,7 @@ function goMatch(id: string) {
|
||||
</div>
|
||||
<div v-else class="empty">
|
||||
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
|
||||
<p>{{ t('bet.no_matches') }}</p>
|
||||
<p>{{ isSearchActive ? t('search.no_results') : t('bet.no_matches') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -382,6 +440,7 @@ function goMatch(id: string) {
|
||||
}
|
||||
|
||||
.bet-page {
|
||||
margin: 0 -16px;
|
||||
padding-bottom: 8px;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
@@ -406,13 +465,8 @@ function goMatch(id: string) {
|
||||
|
||||
.main-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
margin: 0 0 var(--space-section);
|
||||
padding: 3px;
|
||||
background: var(--gradient-card-soft);
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
gap: 8px;
|
||||
padding: 0 12px 12px;
|
||||
}
|
||||
|
||||
.main-tab {
|
||||
@@ -421,23 +475,23 @@ function goMatch(id: string) {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 9px 8px;
|
||||
border-radius: calc(var(--radius-sm) - 3px);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 10px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
box-shadow: none;
|
||||
transition: background 0.18s, color 0.18s;
|
||||
box-shadow: var(--shadow);
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
}
|
||||
|
||||
.main-tab.active {
|
||||
font-weight: 600;
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text);
|
||||
box-shadow: inset 0 0 0 1px var(--border);
|
||||
font-weight: 700;
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
border-color: var(--primary);
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
@@ -449,35 +503,20 @@ function goMatch(id: string) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
padding: 0 0 var(--space-section);
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.time-tab.active,
|
||||
.status-toggle-btn.active {
|
||||
color: var(--text);
|
||||
background: var(--surface-active);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.time-tab.active {
|
||||
box-shadow: inset 0 0 0 1px var(--border-gold);
|
||||
gap: 12px;
|
||||
padding: 0 16px 12px;
|
||||
}
|
||||
|
||||
.phase-filter {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.right-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-tabs {
|
||||
@@ -485,11 +524,10 @@ function goMatch(id: string) {
|
||||
align-items: center;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
height: 28px;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-tab {
|
||||
@@ -500,51 +538,54 @@ function goMatch(id: string) {
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: calc(var(--radius-sm) - 2px);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.time-tab.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(244, 162, 97, 0.15);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.right-filters .divider {
|
||||
width: 1px;
|
||||
height: 14px;
|
||||
height: 12px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.status-toggle-btn {
|
||||
padding: 0 10px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
background: transparent;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-toggle-btn.active {
|
||||
border-color: var(--border-gold);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
|
||||
color: var(--primary-light);
|
||||
background: rgba(244, 162, 97, 0.15);
|
||||
border-color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* League Dropdown Selector */
|
||||
.league-dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
@@ -553,24 +594,14 @@ function goMatch(id: string) {
|
||||
gap: 6px;
|
||||
padding: 0 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
height: 28px;
|
||||
height: 24px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
transition: border-color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.dropdown-text {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-trigger .arrow-icon {
|
||||
@@ -597,48 +628,41 @@ function goMatch(id: string) {
|
||||
top: calc(100% + 4px);
|
||||
left: 0;
|
||||
z-index: 99;
|
||||
min-width: 170px;
|
||||
max-width: 240px;
|
||||
min-width: 140px;
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
background: var(--dropdown-bg);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 6px 0;
|
||||
background: var(--bg-card-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 8px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.dropdown-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item:active {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.dropdown-item.active {
|
||||
color: var(--text);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: var(--primary-light);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.league-list {
|
||||
padding: 4px 0 0;
|
||||
padding: 4px 12px 0;
|
||||
}
|
||||
|
||||
.state,
|
||||
@@ -663,4 +687,72 @@ function goMatch(id: string) {
|
||||
.outright-tab {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0 12px 8px;
|
||||
padding: 0 10px;
|
||||
height: 32px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: var(--primary-light);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.search-bar input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.search-results {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.search-results-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.search-match-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.search-result-league {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-light);
|
||||
line-height: 1.3;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -66,35 +66,37 @@ function formatKickoff(startTime: string) {
|
||||
|
||||
<BannerCarousel :banners="banners" :fallback-to="bannerFallbackTo" />
|
||||
|
||||
<div class="hot-tabs" role="tablist" :aria-label="t('home.hot_matches')">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="hot-tab"
|
||||
:class="{ active: activeTab === 'hot' }"
|
||||
:aria-selected="activeTab === 'hot'"
|
||||
@click="activeTab = 'hot'"
|
||||
<section class="hot-panel">
|
||||
<div class="hot-tabs" role="tablist" :aria-label="t('home.hot_matches')">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="hot-tab"
|
||||
:class="{ active: activeTab === 'hot' }"
|
||||
:aria-selected="activeTab === 'hot'"
|
||||
@click="activeTab = 'hot'"
|
||||
>
|
||||
{{ t('home.hot_tab') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="hot-tab"
|
||||
:class="{ active: activeTab === 'upcoming' }"
|
||||
:aria-selected="activeTab === 'upcoming'"
|
||||
@click="activeTab = 'upcoming'"
|
||||
>
|
||||
{{ t('home.upcoming_tab') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(match, index) in displayedMatches"
|
||||
:key="match.id"
|
||||
class="match-card"
|
||||
:class="{ 'match-card--live-anim': activeTab === 'hot' && index < 3 }"
|
||||
@click="goMatch(match.id)"
|
||||
>
|
||||
{{ t('home.hot_tab') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="hot-tab"
|
||||
:class="{ active: activeTab === 'upcoming' }"
|
||||
:aria-selected="activeTab === 'upcoming'"
|
||||
@click="activeTab = 'upcoming'"
|
||||
>
|
||||
{{ t('home.upcoming_tab') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="(match, index) in displayedMatches"
|
||||
:key="match.id"
|
||||
class="match-card"
|
||||
:class="{ 'match-card--live-anim': activeTab === 'hot' && index < 3 }"
|
||||
@click="goMatch(match.id)"
|
||||
>
|
||||
<div class="match-info">
|
||||
<div class="match-teams">{{ match.homeTeamName }} vs {{ match.awayTeamName }}</div>
|
||||
<div class="match-time">{{ formatKickoff(match.startTime) }}</div>
|
||||
@@ -110,11 +112,11 @@ function formatKickoff(startTime: string) {
|
||||
<svg class="hz-lightning" viewBox="0 0 72 28" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient :id="`hzBoltGrad-${match.id}`" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#5eb8ff" stop-opacity="0.2" />
|
||||
<stop offset="35%" stop-color="#b8ecff" stop-opacity="1" />
|
||||
<stop offset="50%" stop-color="#ffffff" stop-opacity="1" />
|
||||
<stop offset="65%" stop-color="#ffd080" stop-opacity="1" />
|
||||
<stop offset="100%" stop-color="#ff9040" stop-opacity="0.2" />
|
||||
<stop offset="0%" stop-color="#FFB980" stop-opacity="0.2" />
|
||||
<stop offset="35%" stop-color="#FFDDD2" stop-opacity="1" />
|
||||
<stop offset="50%" stop-color="#FFFFFF" stop-opacity="1" />
|
||||
<stop offset="65%" stop-color="#FFDDD2" stop-opacity="1" />
|
||||
<stop offset="100%" stop-color="#FFDDD2" stop-opacity="0.2" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
@@ -140,10 +142,11 @@ function formatKickoff(startTime: string) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!loading && !displayedMatches.length" class="empty">
|
||||
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
|
||||
<p>{{ emptyMessage }}</p>
|
||||
</div>
|
||||
<div v-if="!loading && !displayedMatches.length" class="empty">
|
||||
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
|
||||
<p>{{ emptyMessage }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -156,34 +159,153 @@ function formatKickoff(startTime: string) {
|
||||
transition: height 0.15s ease;
|
||||
}
|
||||
|
||||
.hot-panel {
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hot-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 14px 0;
|
||||
gap: 0;
|
||||
margin-bottom: 0;
|
||||
background: rgba(15, 32, 39, 0.55);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.hot-tab {
|
||||
flex: 1;
|
||||
min-height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
padding: 12px 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.hot-tab.active {
|
||||
font-weight: 700;
|
||||
background: var(--surface-active);
|
||||
border-color: var(--primary);
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
.hot-tab:active {
|
||||
opacity: 0.92;
|
||||
.quick-entries {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-areas:
|
||||
"a a b b"
|
||||
"c d e f";
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.qe-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 4px 10px;
|
||||
border-radius: var(--radius-lg);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.qe-item:active {
|
||||
transform: scale(0.96);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.qe-item:nth-child(1) { grid-area: a; }
|
||||
.qe-item:nth-child(2) { grid-area: b; }
|
||||
.qe-item:nth-child(3) { grid-area: c; }
|
||||
.qe-item:nth-child(4) { grid-area: d; }
|
||||
.qe-item:nth-child(5) { grid-area: e; }
|
||||
.qe-item:nth-child(6) { grid-area: f; }
|
||||
|
||||
.qe-item--wide {
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
padding: 14px 12px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.qe-item.qe-item--wide .qe-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.qe-item.qe-item--wide .qe-label {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.qe-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qe-icon--bet {
|
||||
background: rgba(244, 162, 97, 0.15);
|
||||
color: #FFB980;
|
||||
}
|
||||
|
||||
.qe-icon--recharge {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.qe-icon--bill {
|
||||
background: rgba(255, 179, 0, 0.15);
|
||||
color: #FFB300;
|
||||
}
|
||||
|
||||
.qe-icon--history {
|
||||
background: rgba(231, 111, 81, 0.15);
|
||||
color: #E76F51;
|
||||
}
|
||||
|
||||
.qe-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
margin: 0 0 8px;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid var(--primary);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.hot-panel .match-card {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
.match-card {
|
||||
@@ -192,32 +314,32 @@ function formatKickoff(startTime: string) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
padding: 14px 16px;
|
||||
min-height: 72px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: none;
|
||||
background: var(--bg-card);
|
||||
gap: 8px;
|
||||
margin-bottom: 0;
|
||||
padding: 10px 12px;
|
||||
min-height: 60px;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.2s;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hot-panel .match-card:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.match-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
|
||||
opacity: 0.25;
|
||||
background: linear-gradient(135deg, rgba(244, 162, 97, 0.06) 0%, transparent 60%);
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.match-card:active {
|
||||
opacity: 0.92;
|
||||
transform: scale(0.995);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.match-info,
|
||||
@@ -232,14 +354,15 @@ function formatKickoff(startTime: string) {
|
||||
}
|
||||
|
||||
.match-teams {
|
||||
font-weight: 800;
|
||||
margin-bottom: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.match-time {
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -248,15 +371,15 @@ function formatKickoff(startTime: string) {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
gap: 3px;
|
||||
max-width: 46%;
|
||||
}
|
||||
|
||||
.vs-arena {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 64px;
|
||||
height: 52px;
|
||||
width: 52px;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -277,7 +400,7 @@ function formatKickoff(startTime: string) {
|
||||
stroke-width: 2.2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 0 4px rgba(120, 210, 255, 0.95)) drop-shadow(0 0 8px rgba(255, 180, 80, 0.55));
|
||||
filter: drop-shadow(0 0 4px rgba(244, 162, 97, 0.6)) drop-shadow(0 0 8px rgba(255, 185, 128, 0.35));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -302,11 +425,11 @@ function formatKickoff(startTime: string) {
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(94, 184, 255, 0) 0%,
|
||||
rgba(184, 236, 255, 0.95) 28%,
|
||||
#fff 50%,
|
||||
rgba(255, 208, 128, 0.95) 72%,
|
||||
rgba(255, 144, 64, 0) 100%
|
||||
rgba(255, 185, 128, 0) 0%,
|
||||
rgba(244, 162, 97, 0.7) 28%,
|
||||
#F4A261 50%,
|
||||
rgba(255, 185, 128, 0.7) 72%,
|
||||
rgba(109, 40, 217, 0) 100%
|
||||
);
|
||||
opacity: 0;
|
||||
filter: blur(0.4px);
|
||||
@@ -320,10 +443,10 @@ function formatKickoff(startTime: string) {
|
||||
.vs-img {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 48px;
|
||||
width: 40px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 0 3px rgba(212, 175, 55, 0.35));
|
||||
filter: drop-shadow(0 0 3px rgba(244, 162, 97, 0.25));
|
||||
}
|
||||
|
||||
.match-card--live-anim .vs-img {
|
||||
@@ -411,21 +534,21 @@ function formatKickoff(startTime: string) {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.82;
|
||||
filter: drop-shadow(0 0 2px rgba(212, 175, 55, 0.3));
|
||||
filter: drop-shadow(0 0 2px rgba(244, 162, 97, 0.2));
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
filter:
|
||||
drop-shadow(0 0 3px rgba(255, 230, 140, 0.7))
|
||||
drop-shadow(0 0 6px rgba(212, 175, 55, 0.35));
|
||||
drop-shadow(0 0 3px rgba(244, 162, 97, 0.5))
|
||||
drop-shadow(0 0 6px rgba(255, 185, 128, 0.25));
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.vs-img {
|
||||
animation: none;
|
||||
filter: drop-shadow(0 0 3px rgba(212, 175, 55, 0.35));
|
||||
filter: drop-shadow(0 0 3px rgba(244, 162, 97, 0.25));
|
||||
}
|
||||
|
||||
.hz-path,
|
||||
@@ -438,13 +561,14 @@ function formatKickoff(startTime: string) {
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 40px 20px;
|
||||
padding: 28px 16px;
|
||||
font-weight: 600;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
margin-bottom: 14px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -206,7 +206,7 @@ onActivated(() => {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
@@ -216,7 +216,7 @@ onActivated(() => {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.hub-top-bar {
|
||||
@@ -224,6 +224,8 @@ onActivated(() => {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.hub-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -250,7 +252,7 @@ onActivated(() => {
|
||||
}
|
||||
|
||||
.hub-tab.active {
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
}
|
||||
|
||||
@@ -263,7 +265,7 @@ onActivated(() => {
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
background: var(--primary);
|
||||
color: var(--on-primary);
|
||||
color: var(--bg-body);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
@@ -280,9 +282,9 @@ onActivated(() => {
|
||||
flex: 1;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-active);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
color: var(--primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
@@ -296,6 +298,6 @@ onActivated(() => {
|
||||
.action-btn.danger {
|
||||
border-color: rgba(239, 68, 68, 0.35);
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
color: rgba(239, 68, 68, 0.95);
|
||||
color: var(--danger);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -14,8 +14,8 @@ import CorrectScorePanel from '../components/match-detail/CorrectScorePanel.vue'
|
||||
import { isCorrectScoreMarket, parseScoreCode } from '../utils/correctScoreLayout';
|
||||
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import vsImg from '../assets/images/vs.png';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import VsBadge from '../components/VsBadge.vue';
|
||||
import { matchPhaseLabel, type MatchPhase } from '../utils/matchPhase';
|
||||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||||
|
||||
@@ -48,7 +48,6 @@ interface Selection {
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
selectionDisplayName?: string;
|
||||
status?: string;
|
||||
odds: string;
|
||||
oddsVersion: string;
|
||||
}
|
||||
@@ -290,7 +289,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
|
||||
</div>
|
||||
|
||||
<header class="toolbar sub-toolbar sub-toolbar--sticky sub-toolbar--glass">
|
||||
<header class="toolbar">
|
||||
<button type="button" class="icon-btn" :aria-label="t('bet.back')" @click="router.back()">←</button>
|
||||
<span class="toolbar-title">{{ match?.leagueName ?? '' }}</span>
|
||||
<div class="toolbar-actions">
|
||||
@@ -312,8 +311,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
</div>
|
||||
<template v-else-if="match">
|
||||
<section class="match-hero" :class="{ 'match-hero--phase': matchPhase !== 'open' }">
|
||||
<span v-if="matchPhase === 'open'" class="status-ribbon">{{ t('bet.status_open') }}</span>
|
||||
<span v-else-if="matchPhase === 'settled'" class="status-tag status-tag--settled">{{ phaseLabel }}</span>
|
||||
<span v-if="matchPhase === 'settled'" class="status-tag status-tag--settled">{{ phaseLabel }}</span>
|
||||
<span v-else-if="matchPhase === 'closed_pending'" class="status-tag status-tag--pending">{{ phaseLabel }}</span>
|
||||
<div class="hero-teams">
|
||||
<!-- home -->
|
||||
@@ -327,7 +325,32 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
<span class="hero-name">{{ match.homeTeamName }}</span>
|
||||
</div>
|
||||
|
||||
<VsBadge size="lg" />
|
||||
<!-- VS arena with lightning -->
|
||||
<div class="vs-arena">
|
||||
<svg class="hz-lightning" viewBox="0 0 72 28" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="hzDetailGrad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#FFB980" stop-opacity="0.2" />
|
||||
<stop offset="35%" stop-color="#FFDDD2" stop-opacity="1" />
|
||||
<stop offset="50%" stop-color="#FFFFFF" stop-opacity="1" />
|
||||
<stop offset="65%" stop-color="#FFDDD2" stop-opacity="1" />
|
||||
<stop offset="100%" stop-color="#FFDDD2" stop-opacity="0.2" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path
|
||||
class="hz-path hz-path-main"
|
||||
stroke="url(#hzDetailGrad)"
|
||||
d="M1 14 H16 L20 5 L24 23 L28 9 L32 14 H40 L44 6 L48 22 L52 12 L56 14 H71"
|
||||
/>
|
||||
<path
|
||||
class="hz-path hz-path-sub"
|
||||
stroke="url(#hzDetailGrad)"
|
||||
d="M3 19 H14 L18 15 L22 19 H50 L54 16 L58 19 H69"
|
||||
/>
|
||||
</svg>
|
||||
<span class="hz-beam" aria-hidden="true" />
|
||||
<img :src="vsImg" alt="" class="vs-img" />
|
||||
</div>
|
||||
|
||||
<!-- away -->
|
||||
<div class="hero-team">
|
||||
@@ -360,7 +383,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
|
||||
<!-- 我的投注 -->
|
||||
<section v-if="myBets.length" class="my-bets-section">
|
||||
<h3 class="my-bets-title">{{ t('history.my_bets') || '我的投注' }}</h3>
|
||||
<h3 class="my-bets-title">{{ t('history.my_bets') }}</h3>
|
||||
<div class="my-bets-list">
|
||||
<div v-for="bet in myBets" :key="bet.betNo" class="my-bet-card" @click="router.push(`/bets/${bet.betNo}`)">
|
||||
<div class="bet-header">
|
||||
@@ -389,6 +412,8 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
:label="marketLabel(market)"
|
||||
:promo-label="market.promoLabel?.trim() || ''"
|
||||
:status-label="bettingOpen && !isMarketOpen(market) ? marketStatusLabel(market.status) : ''"
|
||||
:has-market="true"
|
||||
:expanded="true"
|
||||
/>
|
||||
<div class="market-panel-wrap" :class="{ locked: isMarketLocked(market) }">
|
||||
<span v-if="!bettingOpen && matchPhase === 'settled'" class="market-status-tag market-status-tag--settled">{{ phaseLabel }}</span>
|
||||
@@ -427,9 +452,10 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
|
||||
<style scoped>
|
||||
.detail-page {
|
||||
--detail-gutter-x: 0;
|
||||
padding-top: var(--space-toolbar-top);
|
||||
margin: 0 -16px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 16px;
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.pull-indicator {
|
||||
@@ -444,19 +470,22 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 12px 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-page > .sub-toolbar {
|
||||
padding-top: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
margin-bottom: 8px;
|
||||
background: var(--bg-glass);
|
||||
border-bottom: 1px solid var(--border);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -475,8 +504,8 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
color: var(--primary-light);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
@@ -488,92 +517,77 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* ── match hero ── */
|
||||
/* -- match hero -- */
|
||||
.match-hero {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
margin: 0 calc(-1 * (var(--space-page-x) - var(--detail-gutter-x))) 12px;
|
||||
margin: 0 12px 10px;
|
||||
padding: 14px 12px 16px;
|
||||
overflow: hidden;
|
||||
border-radius: var(--radius);
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.match-hero::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
height: 1px;
|
||||
background: var(--gradient-card-shine);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.match-hero--phase {
|
||||
opacity: 0.98;
|
||||
}
|
||||
|
||||
/* ── hero status tag (top-left ribbon) ── */
|
||||
/* -- hero status tag (top-right corner) -- */
|
||||
.status-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: auto;
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 4px 12px 4px 10px;
|
||||
border-radius: 0 0 10px 0;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 4px 14px;
|
||||
border-radius: 0 6px 0 10px;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.status-tag--settled {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 20, 40, 0.75) 100%);
|
||||
background: var(--secondary);
|
||||
color: var(--text-muted);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.status-tag--pending {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(10, 53, 88, 0.6) 100%);
|
||||
color: var(--text-muted);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(255, 179, 0, 0.1);
|
||||
color: #FFB300;
|
||||
border-bottom: 1px solid rgba(255, 179, 0, 0.25);
|
||||
border-left: 1px solid rgba(255, 179, 0, 0.25);
|
||||
}
|
||||
|
||||
/* ── market panel smaller status tag ── */
|
||||
/* -- market panel smaller status tag -- */
|
||||
.market-status-tag {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 5;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 0 4px 0 8px;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.market-status-tag--settled {
|
||||
background: linear-gradient(180deg, #2a2a3a 0%, #1a1a28 100%);
|
||||
color: #8a9ab8;
|
||||
border-bottom: 1px solid rgba(120, 140, 180, 0.3);
|
||||
border-left: 1px solid rgba(120, 140, 180, 0.3);
|
||||
background: var(--secondary);
|
||||
color: var(--text-muted);
|
||||
border-bottom: 1px solid var(--border);
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.market-status-tag--pending {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.22) 0%, rgba(0, 51, 102, 0.95) 100%);
|
||||
color: var(--primary-light);
|
||||
border-bottom: 1px solid var(--border-gold-soft);
|
||||
border-left: 1px solid var(--border-gold-soft);
|
||||
background: rgba(255, 179, 0, 0.1);
|
||||
color: #FFB300;
|
||||
border-bottom: 1px solid rgba(255, 179, 0, 0.25);
|
||||
border-left: 1px solid rgba(255, 179, 0, 0.25);
|
||||
}
|
||||
|
||||
.market-status-tag--suspended {
|
||||
@@ -585,7 +599,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
|
||||
.market-status-tag--closed {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(40, 40, 55, 0.95) 100%);
|
||||
color: var(--text-muted);
|
||||
color: #8a9ab8;
|
||||
border-bottom: 1px solid rgba(120, 140, 180, 0.3);
|
||||
border-left: 1px solid rgba(120, 140, 180, 0.3);
|
||||
}
|
||||
@@ -613,17 +627,16 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin: 8px 0 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
font-size: 28px;
|
||||
font-weight: 900;
|
||||
color: var(--primary-light);
|
||||
text-align: center;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.result-stats-section {
|
||||
margin: 0 calc(-1 * (var(--space-page-x) - var(--detail-gutter-x))) 12px;
|
||||
padding: var(--space-card);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
margin: 0 10px 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-card);
|
||||
box-shadow: var(--shadow);
|
||||
@@ -641,8 +654,8 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.result-stats-head span {
|
||||
@@ -686,8 +699,8 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.hero-teams {
|
||||
@@ -710,8 +723,8 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
|
||||
.hero-name {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
text-align: center;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
@@ -720,16 +733,124 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* VS arena -- blue themed */
|
||||
.vs-arena {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 140px;
|
||||
height: 126px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hz-lightning {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.hz-path {
|
||||
fill: none;
|
||||
stroke-width: 2.2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
filter: drop-shadow(0 0 4px rgba(244, 162, 97, 0.6)) drop-shadow(0 0 8px rgba(255, 185, 128, 0.35));
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hz-path-main {
|
||||
animation: hz-strike-main 2.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.hz-path-sub {
|
||||
stroke-width: 1.6;
|
||||
animation: hz-strike-sub 2.6s ease-in-out infinite;
|
||||
animation-delay: 0.12s;
|
||||
}
|
||||
|
||||
.hz-beam {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
height: 2px;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 185, 128, 0) 0%,
|
||||
rgba(244, 162, 97, 0.7) 28%,
|
||||
#F4A261 50%,
|
||||
rgba(255, 185, 128, 0.7) 72%,
|
||||
rgba(109, 40, 217, 0) 100%
|
||||
);
|
||||
opacity: 0;
|
||||
filter: blur(0.4px);
|
||||
animation: hz-beam-flash 2.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.vs-img {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
width: 126px;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@keyframes hz-strike-main {
|
||||
0%, 72%, 100% { opacity: 0; }
|
||||
74% { opacity: 1; }
|
||||
75% { opacity: 0.25; }
|
||||
76% { opacity: 0.95; }
|
||||
78% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes hz-strike-sub {
|
||||
0%, 74%, 100% { opacity: 0; }
|
||||
76% { opacity: 0.85; }
|
||||
77% { opacity: 0.2; }
|
||||
78% { opacity: 0.7; }
|
||||
80% { opacity: 0; }
|
||||
}
|
||||
|
||||
@keyframes hz-beam-flash {
|
||||
0%, 71%, 100% { opacity: 0; transform: translateY(-50%) scaleX(0.6); }
|
||||
73% { opacity: 0.85; transform: translateY(-50%) scaleX(1); }
|
||||
75% { opacity: 0.15; transform: translateY(-50%) scaleX(0.95); }
|
||||
76% { opacity: 0.75; transform: translateY(-50%) scaleX(1); }
|
||||
78% { opacity: 0; transform: translateY(-50%) scaleX(1.05); }
|
||||
}
|
||||
|
||||
@keyframes vs-glow {
|
||||
0%, 100% {
|
||||
opacity: 0.82;
|
||||
filter: drop-shadow(0 0 2px rgba(244, 162, 97, 0.2));
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
filter: drop-shadow(0 0 3px rgba(244, 162, 97, 0.5)) drop-shadow(0 0 6px rgba(255, 185, 128, 0.25));
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hz-path, .hz-beam { animation: none; opacity: 0; }
|
||||
}
|
||||
|
||||
.markets-section {
|
||||
margin: 0 calc(-1 * (var(--space-page-x) - var(--detail-gutter-x)));
|
||||
padding: 0 0 12px;
|
||||
padding: 0 10px 8px;
|
||||
}
|
||||
|
||||
.market-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 6px 0;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.market-group {
|
||||
@@ -742,8 +863,8 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
}
|
||||
|
||||
.market-group.open {
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: none;
|
||||
border-color: var(--border);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.market-group :deep(.row) {
|
||||
@@ -759,12 +880,12 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
width: calc(100% - 16px);
|
||||
margin: 0 8px 10px;
|
||||
padding: 9px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: var(--surface-accent);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.state {
|
||||
@@ -780,16 +901,16 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
padding: 2px 0 6px;
|
||||
}
|
||||
|
||||
/* ── 我的投注 ── */
|
||||
/* -- my bets -- */
|
||||
.my-bets-section {
|
||||
margin: 0 calc(-1 * (var(--space-page-x) - var(--detail-gutter-x))) 12px;
|
||||
padding: 0 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.my-bets-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
color: var(--text);
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
@@ -806,10 +927,11 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.my-bet-card:active {
|
||||
background: var(--bg-hover);
|
||||
background: var(--bg-body);
|
||||
}
|
||||
|
||||
.bet-header {
|
||||
@@ -822,7 +944,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
.bet-type {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -839,18 +961,18 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
}
|
||||
|
||||
.bet-status-won {
|
||||
background: rgba(61, 184, 101, 0.15);
|
||||
color: #2ECC71;
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10B981;
|
||||
}
|
||||
|
||||
.bet-status-lost {
|
||||
background: rgba(224, 80, 80, 0.15);
|
||||
color: #e05050;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #EF4444;
|
||||
}
|
||||
|
||||
.bet-status-pending {
|
||||
background: rgba(232, 200, 74, 0.15);
|
||||
color: #e8c84a;
|
||||
background: rgba(255, 179, 0, 0.1);
|
||||
color: #FFB300;
|
||||
}
|
||||
|
||||
.bet-footer {
|
||||
@@ -866,7 +988,7 @@ function onPickSelection(selId: string, marketId: string) {
|
||||
|
||||
.bet-return {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -15,13 +15,26 @@ import {
|
||||
} from '../composables/usePlayerMessages';
|
||||
import { useInboxFeature } from '../composables/useInboxFeature';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
embedded?: boolean;
|
||||
/** 悬浮信箱内嵌时使用,不走路由 */
|
||||
id?: string;
|
||||
}>(),
|
||||
{ embedded: false },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{ back: [] }>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { t, locale } = useI18n();
|
||||
const { loadMessageDetail, markMessageRead, deleteMessage } = usePlayerMessages();
|
||||
const { hubRoute } = useInboxFeature();
|
||||
|
||||
const messageId = computed(() => String(route.params.id ?? ''));
|
||||
const messageId = computed(() => props.id ?? String(route.params.id ?? ''));
|
||||
const isPopupEmbedded = computed(() => props.embedded && Boolean(props.id));
|
||||
const isHubEmbedded = computed(() => props.embedded && !props.id);
|
||||
const message = ref<PlayerMessage | null>(null);
|
||||
const loading = ref(false);
|
||||
const error = ref(false);
|
||||
@@ -49,11 +62,23 @@ const contentPromoId = computed(() => {
|
||||
});
|
||||
|
||||
function goBack() {
|
||||
if (isPopupEmbedded.value) {
|
||||
emit('back');
|
||||
return;
|
||||
}
|
||||
if (isHubEmbedded.value) {
|
||||
void router.push(hubRoute.value);
|
||||
return;
|
||||
}
|
||||
router.back();
|
||||
}
|
||||
|
||||
function goList() {
|
||||
router.push(hubRoute.value);
|
||||
if (isPopupEmbedded.value) {
|
||||
emit('back');
|
||||
return;
|
||||
}
|
||||
void router.push(hubRoute.value);
|
||||
}
|
||||
|
||||
function viewContentPromo() {
|
||||
@@ -124,7 +149,11 @@ async function confirmDelete() {
|
||||
try {
|
||||
await deleteMessage(messageId.value);
|
||||
deleteConfirmVisible.value = false;
|
||||
router.replace(hubRoute.value);
|
||||
if (isPopupEmbedded.value) {
|
||||
emit('back');
|
||||
} else {
|
||||
void router.replace(hubRoute.value);
|
||||
}
|
||||
} finally {
|
||||
deleting.value = false;
|
||||
}
|
||||
@@ -144,8 +173,8 @@ watch(messageId, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="message-detail">
|
||||
<header class="page-header">
|
||||
<div class="message-detail" :class="{ 'is-embedded': embedded, 'is-hub-embedded': isHubEmbedded, 'is-popup-embedded': isPopupEmbedded }">
|
||||
<header v-if="!isHubEmbedded" class="page-header">
|
||||
<button type="button" class="back-btn" :aria-label="t('messages.back')" @click="goBack">‹</button>
|
||||
<h1>{{ t('messages.detail_title') }}</h1>
|
||||
<button
|
||||
@@ -160,13 +189,32 @@ watch(messageId, () => {
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<header v-else class="page-header page-header--hub">
|
||||
<button type="button" class="back-btn back-btn--hub" :aria-label="t('messages.back')" @click="goBack">
|
||||
‹ {{ t('messages.back') }}
|
||||
</button>
|
||||
<h1>{{ t('messages.detail_title') }}</h1>
|
||||
<button
|
||||
v-if="message"
|
||||
type="button"
|
||||
class="delete-header-btn"
|
||||
:aria-label="t('messages.delete')"
|
||||
:disabled="deleting"
|
||||
@click="onDelete"
|
||||
>
|
||||
{{ t('messages.delete') }}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div v-if="loading && !message" class="state">
|
||||
<GoldSpinner :size="36" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="error || !message" class="empty">
|
||||
<p>{{ t('messages.not_found') }}</p>
|
||||
<button type="button" class="link-btn" @click="goList">{{ t('messages.view_all') }}</button>
|
||||
<button v-if="!isHubEmbedded" type="button" class="link-btn" @click="goList">
|
||||
{{ isPopupEmbedded ? t('common.back') : t('messages.view_all') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<article v-else class="detail-article">
|
||||
@@ -241,9 +289,18 @@ watch(messageId, () => {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
color: var(--primary);
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.back-btn--hub {
|
||||
width: auto;
|
||||
min-width: 36px;
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
@@ -251,15 +308,15 @@ watch(messageId, () => {
|
||||
flex: 1;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.delete-header-btn {
|
||||
border: 1px solid rgba(239, 68, 68, 0.35);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 6px 10px;
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
color: rgba(239, 68, 68, 0.95);
|
||||
color: var(--danger);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -275,13 +332,13 @@ watch(messageId, () => {
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
color: var(--text-muted);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.status-badge.unread {
|
||||
color: var(--text);
|
||||
background: var(--surface-active);
|
||||
color: var(--primary);
|
||||
background: rgba(244, 162, 97, 0.12);
|
||||
}
|
||||
|
||||
.state,
|
||||
@@ -296,10 +353,10 @@ watch(messageId, () => {
|
||||
|
||||
.link-btn {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 8px 14px;
|
||||
background: var(--surface-active);
|
||||
color: var(--text);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@@ -319,14 +376,14 @@ watch(messageId, () => {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.45;
|
||||
color: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1.75;
|
||||
color: #d0d0d0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.rich-body :deep(p) {
|
||||
@@ -352,8 +409,7 @@ watch(messageId, () => {
|
||||
}
|
||||
|
||||
.rich-body :deep(a) {
|
||||
color: var(--primary-light);
|
||||
text-decoration: underline;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.reason-box {
|
||||
@@ -367,7 +423,7 @@ watch(messageId, () => {
|
||||
.reason-label {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: rgba(239, 68, 68, 0.95);
|
||||
color: var(--danger);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -375,7 +431,7 @@ watch(messageId, () => {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
color: var(--text);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
@@ -383,11 +439,38 @@ watch(messageId, () => {
|
||||
width: 100%;
|
||||
padding: 11px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-active);
|
||||
color: var(--text);
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
color: var(--primary);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.is-hub-embedded {
|
||||
padding: 0 4px 24px;
|
||||
}
|
||||
|
||||
.is-hub-embedded .page-header--hub {
|
||||
padding: 0 0 16px;
|
||||
}
|
||||
|
||||
.is-hub-embedded .page-header--hub h1 {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.is-popup-embedded {
|
||||
padding: 12px 14px 16px;
|
||||
}
|
||||
|
||||
.is-popup-embedded .page-header {
|
||||
padding: 0 0 12px;
|
||||
}
|
||||
|
||||
.is-popup-embedded .page-header h1 {
|
||||
font-size: 15px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -112,7 +112,7 @@ const pullIndicatorStyle = () => ({
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<BetStatsPanel class="stats-panel" :items="items" />
|
||||
<BetStatsPanel :items="items" />
|
||||
|
||||
<div class="filter-tabs">
|
||||
<button
|
||||
@@ -128,9 +128,7 @@ const pullIndicatorStyle = () => ({
|
||||
</div>
|
||||
|
||||
<template v-if="items.length">
|
||||
<div class="bet-list">
|
||||
<BetHistoryCard v-for="bet in items" :key="bet.betNo" :bet="bet" />
|
||||
</div>
|
||||
|
||||
<div ref="sentinel" class="sentinel" />
|
||||
|
||||
@@ -153,16 +151,6 @@ const pullIndicatorStyle = () => ({
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.bet-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-section);
|
||||
}
|
||||
|
||||
.pull-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -173,8 +161,8 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--space-section);
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
@@ -184,21 +172,20 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
.filter-tab {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 16px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: var(--text);
|
||||
background: var(--surface-accent);
|
||||
color: var(--primary);
|
||||
background: rgba(244, 162, 97, 0.15);
|
||||
border-color: var(--primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.state {
|
||||
|
||||
@@ -149,7 +149,7 @@ onUnmounted(() => {
|
||||
border: 1px solid rgba(201, 162, 39, 0.22);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #c9a227;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.teams-grid {
|
||||
|
||||
@@ -185,7 +185,7 @@ function back() {
|
||||
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
|
||||
</div>
|
||||
|
||||
<header class="page-head sub-toolbar">
|
||||
<header class="page-head">
|
||||
<button type="button" class="back-btn" @click="back">← {{ t('profile.back') }}</button>
|
||||
<h2 class="page-title">{{ t('profile.edit') }}</h2>
|
||||
</header>
|
||||
@@ -308,7 +308,7 @@ function back() {
|
||||
|
||||
<style scoped>
|
||||
.edit-page {
|
||||
padding: 0 0 20px;
|
||||
padding: 8px 0 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
@@ -338,7 +338,7 @@ function back() {
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@@ -346,7 +346,7 @@ function back() {
|
||||
.form-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.avatar-card {
|
||||
@@ -361,12 +361,12 @@ function back() {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--border-gold-soft);
|
||||
border: 2px solid var(--border);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--gradient-card);
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
@@ -379,15 +379,15 @@ function back() {
|
||||
.avatar-change-btn {
|
||||
padding: 7px 18px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: var(--surface-accent);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid var(--primary);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: var(--space-card);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@@ -471,8 +471,8 @@ function back() {
|
||||
border: none;
|
||||
border-left: 1px solid var(--border);
|
||||
border-radius: 0 6px 6px 0;
|
||||
background: var(--surface-accent);
|
||||
color: var(--primary-light);
|
||||
background: var(--bg-body);
|
||||
color: var(--primary);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
@@ -512,8 +512,8 @@ label {
|
||||
|
||||
.field-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15);
|
||||
border-color: var(--border-active);
|
||||
box-shadow: 0 0 0 2px rgba(244, 162, 97, 0.2);
|
||||
}
|
||||
|
||||
.field-input:-webkit-autofill,
|
||||
@@ -525,7 +525,7 @@ label {
|
||||
}
|
||||
|
||||
.field-input::placeholder {
|
||||
color: var(--neutral);
|
||||
color: rgba(148, 163, 184, 0.5);
|
||||
}
|
||||
|
||||
.readonly {
|
||||
@@ -555,7 +555,7 @@ label {
|
||||
}
|
||||
|
||||
.msg.ok {
|
||||
color: var(--primary-light);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.msg.err {
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onActivated } from 'vue';
|
||||
import { ref, onMounted, onActivated, computed } from 'vue';
|
||||
import { useRouter, RouterLink } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api from '../api';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
import LocaleFlag from '../components/LocaleFlag.vue';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import { useAppLocale } from '../composables/useAppLocale';
|
||||
import { usePlayerProfile } from '../composables/usePlayerProfile';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import { parseCashbackApiData, sumCashbackAmount } from '../utils/cashback';
|
||||
import { sumCashbackFromStats } from '../utils/walletStats';
|
||||
import WalletBalanceCard from '../components/WalletBalanceCard.vue';
|
||||
import walletBg from '../assets/images/wallet-bg.webp';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const { initFromUser } = useAppLocale();
|
||||
const { locales, setLocale, initFromUser } = useAppLocale();
|
||||
const { profileRaw, refreshProfile } = usePlayerProfile();
|
||||
|
||||
const loading = ref(true);
|
||||
const error = ref(false);
|
||||
const rulesExpanded = ref(false);
|
||||
const cashbackTotal = ref('0');
|
||||
|
||||
const displayAmount = ref(0);
|
||||
@@ -47,7 +49,14 @@ async function fetchCashbackTotal() {
|
||||
// Fallback: sum wallet cashback credits when batch detail API is unavailable.
|
||||
try {
|
||||
const { data } = await api.get('/player/wallet/transactions/stats');
|
||||
cashbackTotal.value = sumCashbackFromStats(data.data).toString();
|
||||
const byType = data.data?.byType ?? [];
|
||||
let sum = 0;
|
||||
for (const g of byType) {
|
||||
if (['CASHBACK', 'CASHBACK_DEPOSIT'].includes(g.transactionType?.toUpperCase())) {
|
||||
sum += Math.abs(parseFloat(g.totalAmount ?? '0'));
|
||||
}
|
||||
}
|
||||
cashbackTotal.value = sum.toString();
|
||||
} catch {
|
||||
// Keep default value
|
||||
}
|
||||
@@ -70,6 +79,28 @@ const pullIndicatorStyle = () => ({
|
||||
height: `${pullDistance.value}px`,
|
||||
opacity: Math.min(pullDistance.value / 48, 1),
|
||||
});
|
||||
|
||||
async function changeLocale(code: string) {
|
||||
await setLocale(code);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
auth.logout();
|
||||
router.push('/');
|
||||
}
|
||||
|
||||
const balanceDisplay = computed(() =>
|
||||
formatMoney(profileRaw.value?.wallet?.availableBalance, locale.value),
|
||||
);
|
||||
|
||||
const balanceAmountClass = computed(() => {
|
||||
const len = balanceDisplay.value.length;
|
||||
if (len <= 10) return 'balance-amount--xl';
|
||||
if (len <= 13) return 'balance-amount--lg';
|
||||
if (len <= 16) return 'balance-amount--md';
|
||||
if (len <= 19) return 'balance-amount--sm';
|
||||
return 'balance-amount--xs';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -91,47 +122,138 @@ const pullIndicatorStyle = () => ({
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<WalletBalanceCard
|
||||
:available-balance="profileRaw?.wallet?.availableBalance"
|
||||
:cashback-total="cashbackTotal"
|
||||
:frozen-balance="profileRaw?.wallet?.frozenBalance"
|
||||
/>
|
||||
<div class="wallet-banner">
|
||||
<img class="wallet-banner-img" :src="walletBg" alt="" />
|
||||
<div class="wallet-banner-scrim" aria-hidden="true" />
|
||||
|
||||
<section class="settings-group settings-group--wallet">
|
||||
<RouterLink to="/wallet/detail" class="settings-cell settings-cell--accent-entry">
|
||||
<div class="card-overlay">
|
||||
<div class="bank-card-top">
|
||||
<div class="bank-card-top-left">
|
||||
<img src="/logo.png" alt="TheBet365" class="brand-logo" />
|
||||
<div class="bank-card-brand-text">
|
||||
<span class="brand-name">THEBET365</span>
|
||||
<span class="bank-card-type">MEMBER</span>
|
||||
</div>
|
||||
</div>
|
||||
<RouterLink to="/wallet/recharge" class="recharge-btn">
|
||||
<span class="recharge-icon">+</span>
|
||||
<span>{{ t('recharge.title') }}</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<div class="bank-card-balance">
|
||||
<span class="bank-card-label">{{ t('wallet.balance') }}</span>
|
||||
<p class="bank-card-number balance-amount" :class="balanceAmountClass">{{ balanceDisplay }}</p>
|
||||
</div>
|
||||
|
||||
<div class="bank-card-footer">
|
||||
<div class="bank-card-field">
|
||||
<span class="bank-card-label">{{ t('wallet.card_holder') }}</span>
|
||||
<span class="bank-card-holder">{{ profileRaw?.username }}</span>
|
||||
</div>
|
||||
<div class="bank-card-field bank-card-field--center">
|
||||
<span class="bank-card-label">{{ t('cashback.total_received') }}</span>
|
||||
<span class="bank-card-stat">{{ formatMoney(cashbackTotal, locale) }}</span>
|
||||
</div>
|
||||
<div class="bank-card-field bank-card-field--right">
|
||||
<span class="bank-card-label">{{ t('wallet.unsettled') }}</span>
|
||||
<span class="bank-card-stat">{{ formatMoney(profileRaw?.wallet?.frozenBalance, locale) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quick-actions">
|
||||
<RouterLink to="/wallet/detail" class="qa-item">
|
||||
<span class="qa-icon qa-icon--wallet">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="24" height="24"><path d="M21 12V7H5a2 2 0 010-4h14v4"/><path d="M3 5v14a2 2 0 002 2h16v-5"/><path d="M18 12a2 2 0 100 4 2 2 0 000-4z"/></svg>
|
||||
</span>
|
||||
<span class="qa-label">{{ t('wallet.view_all') }}</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/wallet/recharge" class="qa-item">
|
||||
<span class="qa-icon qa-icon--recharge">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="24" height="24"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>
|
||||
</span>
|
||||
<span class="qa-label">{{ t('recharge.title') }}</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/profile/cashbacks" class="qa-item">
|
||||
<span class="qa-icon qa-icon--cashback">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="24" height="24"><circle cx="12" cy="12" r="8"/><path d="M12 8v4l2.5 2.5"/></svg>
|
||||
</span>
|
||||
<span class="qa-label">{{ t('wallet.view_cashbacks') }}</span>
|
||||
</RouterLink>
|
||||
<RouterLink to="/wallet/recharge/history" class="qa-item">
|
||||
<span class="qa-icon qa-icon--history">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="24" height="24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6M9 13h6M9 17h4"/></svg>
|
||||
</span>
|
||||
<span class="qa-label">{{ t('recharge.history_title') }}</span>
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<section class="settings-group">
|
||||
<RouterLink to="/profile/edit" class="settings-cell">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path d="M9 5H5a2 2 0 00-2 2v12a2 2 0 002 2h14a2 2 0 002-2V9" />
|
||||
<path d="M9 5a2 2 0 012-2h2a2 2 0 012 2v0M9 12h6M9 16h4" />
|
||||
<circle cx="12" cy="8" r="3.5" />
|
||||
<path d="M5 20c0-3.5 3.1-6 7-6s7 2.5 7 6" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('wallet.view_all') }}</span>
|
||||
<span class="cell-label">{{ t('profile.edit') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</RouterLink>
|
||||
|
||||
<RouterLink to="/profile/cashbacks" class="settings-cell settings-cell--accent-entry">
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="8" />
|
||||
<path d="M12 8v4l2.5 2.5" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('wallet.view_cashbacks') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</RouterLink>
|
||||
<div class="rules-cell">
|
||||
<button
|
||||
type="button"
|
||||
class="settings-cell rules-toggle"
|
||||
:aria-expanded="rulesExpanded"
|
||||
@click="rulesExpanded = !rulesExpanded"
|
||||
>
|
||||
<span class="cell-main">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path d="M7 4h10v16H7z" />
|
||||
<path d="M10 8h6M10 12h6M10 16h4" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('profile.rules_title') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" :class="{ open: rulesExpanded }" aria-hidden="true">›</span>
|
||||
</button>
|
||||
<div v-show="rulesExpanded" class="rules-body">
|
||||
<p>{{ t('profile.rules_p1') }}</p>
|
||||
<p>{{ t('profile.rules_p2') }}</p>
|
||||
<p>{{ t('profile.rules_p3') }}</p>
|
||||
<p>{{ t('profile.rules_p4') }}</p>
|
||||
<p>{{ t('profile.rules_p5') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RouterLink to="/wallet/recharge/history" class="settings-cell settings-cell--accent-entry">
|
||||
<span class="cell-main">
|
||||
<div class="settings-cell settings-cell--stack">
|
||||
<div class="cell-head">
|
||||
<svg class="cell-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
|
||||
<path d="M12 8v8M8 12h8" />
|
||||
<circle cx="12" cy="12" r="8" />
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M3 12h18M12 3c2.5 2.8 4 6 4 9s-1.5 6.2-4 9M12 3c-2.5 2.8-4 6-4 9s1.5 6.2 4 9" />
|
||||
</svg>
|
||||
<span class="cell-label">{{ t('recharge.history_title') }}</span>
|
||||
</span>
|
||||
<span class="cell-chevron" aria-hidden="true">›</span>
|
||||
</RouterLink>
|
||||
|
||||
<span class="cell-label">{{ t('profile.language') }}</span>
|
||||
</div>
|
||||
<div class="lang-segment" role="group" :aria-label="t('profile.language')">
|
||||
<button
|
||||
v-for="item in locales"
|
||||
:key="item.code"
|
||||
type="button"
|
||||
class="lang-opt"
|
||||
:class="{ active: locale === item.code }"
|
||||
@click="changeLocale(item.code)"
|
||||
>
|
||||
<LocaleFlag :locale="item.code" :size="14" />
|
||||
<span>{{ item.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button type="button" class="logout-btn" @click="logout">
|
||||
{{ t('auth.logout') }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
@@ -146,7 +268,7 @@ const pullIndicatorStyle = () => ({
|
||||
}
|
||||
|
||||
.profile-page {
|
||||
padding: 8px 0 12px;
|
||||
padding: 6px 0 10px;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
@@ -183,49 +305,320 @@ const pullIndicatorStyle = () => ({
|
||||
}
|
||||
|
||||
.retry-btn:active {
|
||||
background: var(--surface-active);
|
||||
background: rgba(244, 162, 97, 0.15);
|
||||
}
|
||||
|
||||
.wallet-banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
aspect-ratio: 1.75 / 1;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.wallet-banner::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.wallet-banner-img {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
object-position: center center;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.wallet-banner-scrim {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
#0F2027 0%,
|
||||
#F4A261 50%,
|
||||
#203A43 100%
|
||||
);
|
||||
}
|
||||
|
||||
.card-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 14px 10px;
|
||||
line-height: 1.3;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.bank-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bank-card-top-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.bank-card-brand-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0.14em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bank-card-type {
|
||||
font-size: 8px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.14em;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-style: italic;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bank-card-label {
|
||||
display: block;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.recharge-btn {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 5px 10px;
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: #FFFFFF;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.recharge-btn:active {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.recharge-icon {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bank-card-balance {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
padding: 1px 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.bank-card-number {
|
||||
margin: 0;
|
||||
font-family: 'SF Mono', 'Consolas', 'Courier New', monospace;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #FFFFFF;
|
||||
line-height: 1.1;
|
||||
letter-spacing: 0.04em;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.balance-amount--xl { font-size: clamp(22px, 5.6vw, 27px); }
|
||||
.balance-amount--lg { font-size: clamp(19px, 5vw, 24px); }
|
||||
.balance-amount--md { font-size: clamp(16px, 4.4vw, 20px); }
|
||||
.balance-amount--sm { font-size: clamp(14px, 3.8vw, 17px); }
|
||||
.balance-amount--xs { font-size: clamp(12px, 3.2vw, 14px); }
|
||||
|
||||
.bank-card-footer {
|
||||
flex-shrink: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.bank-card-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bank-card-field--center { text-align: center; }
|
||||
.bank-card-field--right { text-align: right; }
|
||||
|
||||
.bank-card-holder {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #FFFFFF;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
line-height: 1.2;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.bank-card-stat {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #FFFFFF;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-areas:
|
||||
"a a b b"
|
||||
"c c d d";
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.qa-item:nth-child(1) { grid-area: a; }
|
||||
.qa-item:nth-child(2) { grid-area: b; }
|
||||
.qa-item:nth-child(3) { grid-area: c; }
|
||||
.qa-item:nth-child(4) { grid-area: d; }
|
||||
|
||||
.qa-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 10px;
|
||||
border-radius: var(--radius-lg);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qa-item:active {
|
||||
transform: scale(0.96);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.qa-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.qa-icon--wallet {
|
||||
background: rgba(244, 162, 97, 0.15);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.qa-icon--recharge {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.qa-icon--cashback {
|
||||
background: rgba(255, 179, 0, 0.15);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.qa-icon--history {
|
||||
background: rgba(244, 162, 97, 0.15);
|
||||
color: #F4A261;
|
||||
}
|
||||
|
||||
.qa-icon--inbox {
|
||||
background: rgba(14, 165, 233, 0.15);
|
||||
color: #0EA5E9;
|
||||
}
|
||||
|
||||
.qa-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
position: relative;
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-card-sm);
|
||||
}
|
||||
|
||||
.settings-group::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
height: 1px;
|
||||
background: var(--gradient-card-shine);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.settings-group--wallet {
|
||||
margin-bottom: var(--space-section);
|
||||
}
|
||||
|
||||
.settings-group--profile {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.settings-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
min-height: 48px;
|
||||
padding: 0 16px;
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
background: none;
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
@@ -239,34 +632,12 @@ const pullIndicatorStyle = () => ({
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.settings-cell--accent-entry {
|
||||
position: relative;
|
||||
min-height: 50px;
|
||||
border-color: var(--border);
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.settings-cell--accent-entry:active {
|
||||
background: var(--surface-active);
|
||||
}
|
||||
|
||||
.settings-cell--accent-entry .cell-label {
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.settings-cell--accent-entry .cell-chevron {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.settings-cell--stack {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px 14px;
|
||||
gap: 8px;
|
||||
padding: 10px 14px 12px;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
@@ -284,10 +655,6 @@ const pullIndicatorStyle = () => ({
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.settings-cell--accent-entry .cell-icon {
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.cell-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -317,7 +684,7 @@ const pullIndicatorStyle = () => ({
|
||||
}
|
||||
|
||||
.rules-cell .rules-body {
|
||||
padding: 0 16px 14px;
|
||||
padding: 0 14px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -332,37 +699,41 @@ const pullIndicatorStyle = () => ({
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
min-height: 34px;
|
||||
padding: 0 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
min-height: 30px;
|
||||
min-width: 0;
|
||||
padding: 0 5px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lang-opt.active {
|
||||
border-color: var(--primary);
|
||||
color: var(--text);
|
||||
background: var(--surface-accent);
|
||||
color: var(--primary);
|
||||
background: rgba(244, 162, 97, 0.12);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
margin-top: var(--space-section);
|
||||
min-height: 44px;
|
||||
padding: 0 16px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
margin-top: 10px;
|
||||
min-height: 40px;
|
||||
padding: 0 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--danger);
|
||||
background: var(--bg-card);
|
||||
color: var(--danger);
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logout-btn:active {
|
||||
background: rgba(255, 69, 58, 0.08);
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
}
|
||||
|
||||
.rules-body {
|
||||
|
||||
@@ -324,11 +324,11 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
.audit-step-box--reject { border: 1px solid rgba(245,108,108,0.22); }
|
||||
.audit-step-box-label { font-size: 10px; font-weight: 500; color: #c07070; }
|
||||
.audit-step-box-text { font-size: 11px; color: #b08888; }
|
||||
.audit-step--submitted .audit-dot { background: #c9a227; }
|
||||
.audit-step--submitted .audit-dot { background: var(--primary); }
|
||||
.audit-step--approved .audit-dot { background: #4cd964; }
|
||||
.audit-step--rejected .audit-dot { background: #ff453a; }
|
||||
.audit-step--revoked .audit-dot { background: #888; }
|
||||
.audit-step--reopened .audit-dot { background: #c9a227; }
|
||||
.audit-step--reopened .audit-dot { background: var(--primary); }
|
||||
|
||||
.reapply-btn {
|
||||
display: block;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import api from '../api';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
import { enrichDepositOrder, findDepositOrderById } from '../utils/depositOrderLookup';
|
||||
import {
|
||||
auditActionTone,
|
||||
auditActorSecondary,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from '../utils/depositAuditDisplay';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
interface DepositAuditLog {
|
||||
@@ -85,7 +87,7 @@ const { pullDistance, spinning, progress } = usePullToRefresh({
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrders(1);
|
||||
void fetchOrders(1).then(() => openDetailFromQuery());
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasMore.value && !loading.value) {
|
||||
@@ -97,6 +99,10 @@ onMounted(() => {
|
||||
if (sentinel.value) observer.observe(sentinel.value);
|
||||
});
|
||||
|
||||
watch(() => route.query.orderId, () => {
|
||||
void openDetailFromQuery();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
observer?.disconnect();
|
||||
});
|
||||
@@ -121,14 +127,24 @@ function goRecharge() {
|
||||
router.push('/wallet/recharge');
|
||||
}
|
||||
|
||||
function openDetail(order: DepositOrder) {
|
||||
selectedOrder.value = order;
|
||||
async function openDetail(order: DepositOrder) {
|
||||
selectedOrder.value = await enrichDepositOrder(order);
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
selectedOrder.value = null;
|
||||
}
|
||||
|
||||
async function openDetailFromQuery() {
|
||||
const orderId = route.query.orderId;
|
||||
if (typeof orderId !== 'string' || !orderId) return;
|
||||
const existing = items.value.find((order) => order.id === orderId);
|
||||
const order = existing ?? await findDepositOrderById(orderId);
|
||||
if (order) await openDetail(order);
|
||||
const { orderId: _removed, ...rest } = route.query;
|
||||
void router.replace({ path: route.path, query: rest });
|
||||
}
|
||||
|
||||
function reapply(order: DepositOrder) {
|
||||
const query: Record<string, string> = {
|
||||
orderId: order.id,
|
||||
@@ -226,11 +242,11 @@ function auditStepCount(order: DepositOrder) {
|
||||
|
||||
<template>
|
||||
<div class="recharge-history-page">
|
||||
<header class="page-header sub-toolbar">
|
||||
<div class="page-header">
|
||||
<button class="back-btn" @click="goBack">‹</button>
|
||||
<h2>{{ t('recharge.history_title') }}</h2>
|
||||
<button class="recharge-btn" @click="goRecharge">+ {{ t('recharge.title') }}</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pull-indicator"
|
||||
@@ -409,26 +425,23 @@ function auditStepCount(order: DepositOrder) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.recharge-history-page { padding: 0 0 24px; }
|
||||
.page-header { display: flex; align-items: center; justify-content: space-between; }
|
||||
.page-header h2 { margin: 0; font-size: 17px; font-weight: 700; }
|
||||
.back-btn { background: none; border: none; color: var(--primary-light); font-size: 24px; cursor: pointer; padding: 0 8px; }
|
||||
.recharge-btn { background: none; border: none; color: var(--primary-light); font-size: 13px; cursor: pointer; font-weight: 600; }
|
||||
.recharge-history-page { padding: 0 16px 24px; }
|
||||
.page-header { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; }
|
||||
.page-header h2 { margin: 0; font-size: 17px; font-weight: 700; color: var(--text); }
|
||||
.back-btn { background: none; border: none; color: var(--primary); font-size: 24px; cursor: pointer; padding: 0 8px; }
|
||||
.recharge-btn { background: none; border: none; color: var(--primary); font-size: 13px; cursor: pointer; font-weight: 600; }
|
||||
.state { display: flex; justify-content: center; padding: 48px; }
|
||||
.empty { text-align: center; color: var(--text-muted); padding: 48px 16px; font-weight: 600; }
|
||||
.pull-indicator { display: flex; align-items: center; justify-content: center; overflow: hidden; transition: height 0.15s ease; }
|
||||
|
||||
.order-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.order-card {
|
||||
background: var(--gradient-card);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border-radius: var(--radius);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.order-card:active {
|
||||
opacity: 0.92;
|
||||
@@ -438,14 +451,14 @@ function auditStepCount(order: DepositOrder) {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.45), transparent);
|
||||
background: linear-gradient(90deg, transparent, #F4A261, transparent);
|
||||
}
|
||||
.order-card.rejected {
|
||||
background: var(--bg-card);
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
.order-card.rejected::before {
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.35), transparent);
|
||||
background: linear-gradient(90deg, transparent, #EF4444, transparent);
|
||||
}
|
||||
.order-card.rejected .order-amount {
|
||||
background: none;
|
||||
@@ -454,50 +467,50 @@ function auditStepCount(order: DepositOrder) {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.order-card.rejected .info-label {
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.order-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
|
||||
.method-badge { padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 700; }
|
||||
.method-badge.bank { background: rgba(30, 58, 95, 0.6); color: #66b1ff; }
|
||||
.method-badge.usdt { background: rgba(26, 58, 42, 0.6); color: #67c23a; }
|
||||
.method-badge.bank { background: rgba(244, 162, 97, 0.15); color: var(--primary-light); }
|
||||
.method-badge.usdt { background: rgba(16, 185, 129, 0.15); color: var(--success); }
|
||||
.status-badge { font-size: 12px; font-weight: 700; }
|
||||
.status-pending { color: var(--primary-light); }
|
||||
.status-pending { color: var(--warning); }
|
||||
.status-approved { color: var(--success); }
|
||||
.status-rejected { color: var(--primary); }
|
||||
.status-rejected { color: var(--danger); }
|
||||
.order-body { }
|
||||
.order-amount {
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
margin-bottom: 4px;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
.approved-amount { font-size: 12px; color: var(--success); margin-bottom: 6px; font-weight: 600; }
|
||||
.order-info-row { margin-bottom: 8px; }
|
||||
.info-label { font-size: 13px; color: var(--text-muted); font-weight: 600; }
|
||||
.info-label { font-size: 13px; color: var(--primary); font-weight: 600; }
|
||||
.order-times { display: flex; flex-direction: column; gap: 4px; margin-bottom: 6px; }
|
||||
.time-row { display: flex; justify-content: space-between; align-items: center; }
|
||||
.time-label { font-size: 11px; color: var(--text-muted); }
|
||||
.time-value { font-size: 11px; color: var(--neutral); font-variant-numeric: tabular-nums; }
|
||||
.time-value { font-size: 11px; color: var(--text); font-variant-numeric: tabular-nums; }
|
||||
.order-remark {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
background: var(--surface-accent);
|
||||
color: var(--primary);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
margin-top: 6px;
|
||||
border-left: 2px solid var(--border-gold-soft);
|
||||
border-left: 2px solid var(--primary);
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
.reject-reason {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #c07070;
|
||||
color: var(--danger);
|
||||
background: transparent;
|
||||
padding: 6px 0 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-top: 1px solid rgba(245, 108, 108, 0.18);
|
||||
border-top: 1px solid rgba(239, 68, 68, 0.25);
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -517,7 +530,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
}
|
||||
.card-detail-link {
|
||||
font-size: 11px;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -530,7 +543,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
background: none;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -548,7 +561,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
.end-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
padding: 16px 0 4px;
|
||||
letter-spacing: 0.03em;
|
||||
@@ -558,7 +571,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 200;
|
||||
background: rgba(0, 34, 68, 0.72);
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
@@ -571,14 +584,12 @@ function auditStepCount(order: DepositOrder) {
|
||||
max-width: 480px;
|
||||
max-height: min(88vh, 720px);
|
||||
overflow-y: auto;
|
||||
background: var(--dropdown-bg);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-bottom: none;
|
||||
border-radius: 14px 14px 0 0;
|
||||
padding: 16px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.35);
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.detail-close {
|
||||
@@ -608,8 +619,6 @@ function auditStepCount(order: DepositOrder) {
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-body);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -624,7 +633,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
@@ -672,12 +681,12 @@ function auditStepCount(order: DepositOrder) {
|
||||
.detail-summary .reject-reason {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--primary-light);
|
||||
color: var(--danger);
|
||||
background: transparent;
|
||||
padding: 6px 0 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.25);
|
||||
border-top: 1px solid rgba(239, 68, 68, 0.2);
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -725,7 +734,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--neutral);
|
||||
background: var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -763,7 +772,7 @@ function auditStepCount(order: DepositOrder) {
|
||||
|
||||
.audit-step-time {
|
||||
font-size: 10px;
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
@@ -804,36 +813,36 @@ function auditStepCount(order: DepositOrder) {
|
||||
}
|
||||
|
||||
.audit-step-box--reject {
|
||||
background: var(--bg-body);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: transparent;
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.audit-step-box-label {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--primary-light);
|
||||
color: var(--danger);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.audit-step-box-text {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.audit-step--submitted .audit-dot {
|
||||
background: var(--primary-light);
|
||||
background: var(--primary);
|
||||
}
|
||||
.audit-step--approved .audit-dot {
|
||||
background: var(--success);
|
||||
}
|
||||
.audit-step--rejected .audit-dot {
|
||||
background: var(--primary);
|
||||
background: var(--danger);
|
||||
}
|
||||
.audit-step--revoked .audit-dot {
|
||||
background: var(--neutral);
|
||||
background: var(--text-muted);
|
||||
}
|
||||
.audit-step--reopened .audit-dot {
|
||||
background: var(--primary-light);
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.modal-reapply-btn {
|
||||
|
||||
@@ -250,11 +250,11 @@ onMounted(fetchMethods);
|
||||
|
||||
<template>
|
||||
<div class="recharge-page">
|
||||
<header class="page-header sub-toolbar">
|
||||
<div class="page-header">
|
||||
<button class="back-btn" @click="goBack">‹</button>
|
||||
<h2>{{ t('recharge.title') }}</h2>
|
||||
<button class="history-btn" @click="goHistory">{{ t('recharge.history') }}</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="state">
|
||||
<GoldSpinner :size="36" />
|
||||
@@ -379,14 +379,15 @@ onMounted(fetchMethods);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.recharge-page { padding: 0 0 24px; }
|
||||
.recharge-page { padding: 0 12px 24px; }
|
||||
|
||||
.page-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 8px 0 12px;
|
||||
}
|
||||
.page-header h2 { margin: 0; font-size: 16px; font-weight: 700; }
|
||||
.back-btn { background: none; border: none; color: var(--primary-light); font-size: 22px; cursor: pointer; padding: 0 6px; }
|
||||
.history-btn { background: none; border: none; color: var(--primary-light); font-size: 12px; cursor: pointer; font-weight: 600; }
|
||||
.page-header h2 { margin: 0; font-size: 16px; font-weight: 700; color: var(--text); }
|
||||
.back-btn { background: none; border: none; color: var(--primary); font-size: 22px; cursor: pointer; padding: 0 6px; }
|
||||
.history-btn { background: none; border: none; color: var(--primary); font-size: 12px; cursor: pointer; font-weight: 600; }
|
||||
|
||||
.state { display: flex; justify-content: center; padding: 48px; }
|
||||
|
||||
@@ -396,34 +397,24 @@ onMounted(fetchMethods);
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: #ffd0d0;
|
||||
background: rgba(245, 108, 108, 0.12);
|
||||
border: 1px solid rgba(245, 108, 108, 0.25);
|
||||
color: var(--danger);
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.type-tabs {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
display: flex; margin-bottom: 12px;
|
||||
border-radius: 6px; overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
flex: 1; padding: 8px; border: none;
|
||||
background: var(--bg-card);
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
color: var(--text-muted); font-weight: 700; font-size: 13px;
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.tab.active {
|
||||
background: var(--surface-accent);
|
||||
color: var(--text);
|
||||
box-shadow: inset 0 0 0 1px var(--primary);
|
||||
background: var(--primary); color: #FFFFFF;
|
||||
}
|
||||
|
||||
.methods-list {
|
||||
@@ -438,8 +429,8 @@ onMounted(fetchMethods);
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.method-pill.selected {
|
||||
border-color: var(--primary);
|
||||
background: var(--surface-accent);
|
||||
border-color: var(--border-active);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
}
|
||||
.pill-name { font-weight: 700; font-size: 12px; color: var(--text); }
|
||||
.pill-sub { font-size: 10px; color: var(--text-muted); }
|
||||
@@ -459,15 +450,9 @@ onMounted(fetchMethods);
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 100% 0%, rgba(255, 255, 255, 0.18), transparent 55%),
|
||||
radial-gradient(ellipse 50% 50% at 0% 100%, var(--surface-accent), transparent 50%),
|
||||
var(--gradient-card);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
box-shadow:
|
||||
0 8px 24px rgba(0, 0, 0, 0.35),
|
||||
0 2px 8px var(--surface-accent),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
background: linear-gradient(135deg, #0F2027 0%, #F4A261 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.bank-card-shine {
|
||||
@@ -476,9 +461,9 @@ onMounted(fetchMethods);
|
||||
background: linear-gradient(
|
||||
115deg,
|
||||
transparent 30%,
|
||||
rgba(255, 255, 255, 0.04) 45%,
|
||||
rgba(255, 255, 255, 0.08) 50%,
|
||||
rgba(255, 255, 255, 0.04) 55%,
|
||||
rgba(255, 255, 255, 0.03) 45%,
|
||||
rgba(255, 255, 255, 0.06) 50%,
|
||||
rgba(255, 255, 255, 0.03) 55%,
|
||||
transparent 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
@@ -487,7 +472,7 @@ onMounted(fetchMethods);
|
||||
.bank-card-deco {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -496,7 +481,7 @@ onMounted(fetchMethods);
|
||||
height: 180px;
|
||||
right: -60px;
|
||||
bottom: -80px;
|
||||
background: radial-gradient(circle, var(--surface-accent), transparent 70%);
|
||||
background: radial-gradient(circle, rgba(255, 185, 128, 0.15), transparent 70%);
|
||||
}
|
||||
|
||||
.bank-card-deco--2 {
|
||||
@@ -504,7 +489,7 @@ onMounted(fetchMethods);
|
||||
height: 100px;
|
||||
right: 40px;
|
||||
bottom: 20px;
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
border-color: rgba(255, 185, 128, 0.12);
|
||||
}
|
||||
|
||||
.bank-card-top,
|
||||
@@ -524,10 +509,9 @@ onMounted(fetchMethods);
|
||||
.bank-card-bank {
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
color: #FFFFFF;
|
||||
text-align: right;
|
||||
letter-spacing: 0.5px;
|
||||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||
line-height: 1.3;
|
||||
max-width: 58%;
|
||||
word-break: break-all;
|
||||
@@ -546,8 +530,7 @@ onMounted(fetchMethods);
|
||||
font-size: clamp(15px, 4.2vw, 19px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.14em;
|
||||
color: var(--primary-light);
|
||||
text-shadow: 0 0 12px rgba(240, 216, 117, 0.25);
|
||||
color: #FFFFFF;
|
||||
word-break: break-all;
|
||||
line-height: 1.3;
|
||||
}
|
||||
@@ -557,9 +540,9 @@ onMounted(fetchMethods);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(0, 34, 68, 0.45);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #FFFFFF;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -574,7 +557,7 @@ onMounted(fetchMethods);
|
||||
|
||||
.bank-card-copy:active {
|
||||
opacity: 0.65;
|
||||
background: var(--surface-accent);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.bank-card-bottom {
|
||||
@@ -596,15 +579,14 @@ onMounted(fetchMethods);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.bank-card-holder-name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: #FFFFFF;
|
||||
letter-spacing: 0.04em;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
@@ -613,21 +595,20 @@ onMounted(fetchMethods);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--primary-light);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.method-info {
|
||||
background: var(--bg-body);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-card);
|
||||
border-radius: 8px; padding: 10px 12px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.info-row {
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.info-row:last-child { border-bottom: none; }
|
||||
.info-label { font-size: 11px; color: var(--text-muted); flex-shrink: 0; }
|
||||
@@ -635,15 +616,16 @@ onMounted(fetchMethods);
|
||||
font-size: 13px; font-weight: 600;
|
||||
word-break: break-all; text-align: right;
|
||||
max-width: 60%;
|
||||
color: var(--text);
|
||||
}
|
||||
.copyable {
|
||||
cursor: pointer; color: var(--primary-light);
|
||||
cursor: pointer; color: var(--primary);
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
}
|
||||
.copyable:active { opacity: 0.6; }
|
||||
.copy-icon { width: 14px; height: 14px; flex-shrink: 0; }
|
||||
.qr-container { display: flex; justify-content: center; padding: 8px 0 4px; }
|
||||
.qr-image { width: 140px; height: 140px; object-fit: contain; border-radius: 6px; background: #fff; padding: 6px; }
|
||||
.qr-image { width: 140px; height: 140px; object-fit: contain; border-radius: 6px; background: #fff; padding: 6px; border: 1px solid var(--border); }
|
||||
|
||||
.form-section { margin-bottom: 12px; }
|
||||
.form-section label {
|
||||
@@ -656,46 +638,46 @@ onMounted(fetchMethods);
|
||||
color: var(--text); font-size: 16px; font-weight: 700;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.amount-input:focus { border-color: var(--border-gold-soft); outline: none; }
|
||||
.amount-input:focus { border-color: var(--border-active); outline: none; box-shadow: 0 0 0 2px rgba(244, 162, 97, 0.2); }
|
||||
|
||||
.upload-area {
|
||||
border: 1px dashed var(--border-gold-soft);
|
||||
border: 1px dashed var(--primary);
|
||||
border-radius: 6px; padding: 16px;
|
||||
text-align: center; position: relative;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer;
|
||||
background: var(--bg-body);
|
||||
background: rgba(244, 162, 97, 0.06);
|
||||
}
|
||||
.upload-area input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
|
||||
.upload-hint { font-size: 12px; color: var(--text-muted); }
|
||||
.compress-hint { font-size: 12px; color: var(--primary-light); }
|
||||
.compress-hint { font-size: 12px; color: var(--primary); }
|
||||
.screenshot-preview { position: relative; display: inline-block; }
|
||||
.screenshot-preview img { max-width: 100%; max-height: 160px; border-radius: 6px; }
|
||||
.remove-btn {
|
||||
position: absolute; top: 4px; right: 4px;
|
||||
background: rgba(0,0,0,0.7); border: none; color: #fff;
|
||||
background: rgba(0, 0, 0, 0.6); border: none; color: #fff;
|
||||
width: 20px; height: 20px; border-radius: 50%;
|
||||
cursor: pointer; font-size: 11px;
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
width: 100%; padding: 12px;
|
||||
background: var(--gradient-accent);
|
||||
color: #fff; border: none; border-radius: 6px;
|
||||
background: var(--primary);
|
||||
color: #FFFFFF; border: none; border-radius: 6px;
|
||||
font-size: 14px; font-weight: 800;
|
||||
cursor: pointer; margin-top: 8px;
|
||||
box-shadow: none;
|
||||
box-shadow: var(--shadow-glow);
|
||||
}
|
||||
.btn-submit:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
|
||||
.success-state { text-align: center; padding: 40px 16px; }
|
||||
.success-icon { font-size: 40px; color: var(--success); margin-bottom: 10px; }
|
||||
.success-state h3 { margin: 0 0 6px; font-size: 16px; color: var(--text); }
|
||||
.order-no { font-family: monospace; color: var(--primary-light); font-size: 13px; margin: 4px 0; }
|
||||
.order-no { font-family: monospace; color: var(--primary); font-size: 13px; margin: 4px 0; }
|
||||
.success-hint { font-size: 12px; color: var(--text-muted); margin-bottom: 20px; }
|
||||
.btn-primary {
|
||||
background: var(--gradient-accent);
|
||||
color: #fff; border: none; border-radius: 6px;
|
||||
background: var(--primary);
|
||||
color: #FFFFFF; border: none; border-radius: 6px;
|
||||
padding: 10px 20px; font-weight: 700; font-size: 13px; cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -155,9 +155,9 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
<template>
|
||||
<div class="wallet-detail-page">
|
||||
<header class="top-bar sub-toolbar">
|
||||
<div class="top-bar">
|
||||
<button class="back-btn" type="button" @click="router.back()">‹ {{ t('history.back') }}</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="pull-indicator"
|
||||
@@ -172,7 +172,7 @@ const pullIndicatorStyle = () => ({
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<WalletStatsPanel class="stats-panel" :items="items" :cashback-total="cashbackTotal" />
|
||||
<WalletStatsPanel :items="items" :cashback-total="cashbackTotal" />
|
||||
|
||||
<div class="filter-tabs">
|
||||
<button
|
||||
@@ -195,15 +195,17 @@ const pullIndicatorStyle = () => ({
|
||||
class="tx-row"
|
||||
@click="goDetail(tx)"
|
||||
>
|
||||
<div class="tx-col-left">
|
||||
<span class="tx-type">{{ txLabel(tx) }}</span>
|
||||
<span v-if="txSubtitle(tx)" class="tx-summary">{{ txSubtitle(tx) }}</span>
|
||||
<span class="tx-time">{{ new Date(tx.createdAt).toLocaleString() }}</span>
|
||||
</div>
|
||||
<div class="tx-col-right">
|
||||
<div class="tx-main">
|
||||
<div class="tx-text">
|
||||
<span class="tx-type">{{ txLabel(tx) }}</span>
|
||||
<span v-if="txSubtitle(tx)" class="tx-summary">{{ txSubtitle(tx) }}</span>
|
||||
</div>
|
||||
<span :class="txAmountClass(tx.amount)">
|
||||
{{ formatMoney(tx.amount, locale) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="tx-meta">
|
||||
<span class="tx-time">{{ new Date(tx.createdAt).toLocaleString() }}</span>
|
||||
<span class="tx-arrow">›</span>
|
||||
</div>
|
||||
</button>
|
||||
@@ -231,10 +233,6 @@ const pullIndicatorStyle = () => ({
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-panel {
|
||||
margin-bottom: var(--space-section);
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -242,9 +240,9 @@ const pullIndicatorStyle = () => ({
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
color: var(--primary);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
font-weight: 700;
|
||||
padding: 4px 0 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -262,8 +260,8 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--space-section);
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
@@ -273,21 +271,20 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
.filter-tab {
|
||||
flex-shrink: 0;
|
||||
padding: 8px 16px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
color: var(--text);
|
||||
background: var(--bg-elevated);
|
||||
border-color: var(--border-strong);
|
||||
font-weight: 600;
|
||||
color: #FFFFFF;
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.state {
|
||||
@@ -308,21 +305,14 @@ const pullIndicatorStyle = () => ({
|
||||
|
||||
.tx-list {
|
||||
margin-bottom: 0;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tx-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
padding: 12px 14px;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: transparent;
|
||||
@@ -334,22 +324,21 @@ const pullIndicatorStyle = () => ({
|
||||
}
|
||||
|
||||
.tx-row:active {
|
||||
background: var(--bg-hover);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.tx-col-left {
|
||||
.tx-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.tx-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tx-col-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tx-summary {
|
||||
@@ -361,12 +350,19 @@ const pullIndicatorStyle = () => ({
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.tx-type { font-weight: 600; color: var(--text); letter-spacing: -0.01em; }
|
||||
.pos { color: var(--success); font-weight: 600; font-size: 14px; font-variant-numeric: tabular-nums; }
|
||||
.neg { color: var(--text); font-weight: 600; font-size: 14px; font-variant-numeric: tabular-nums; }
|
||||
.zero { color: var(--text-muted); font-weight: 600; font-size: 14px; font-variant-numeric: tabular-nums; }
|
||||
.tx-time { font-size: 11px; color: var(--text-dim); }
|
||||
.tx-arrow { font-size: 15px; color: var(--text-dim); font-weight: 400; line-height: 1; }
|
||||
.tx-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tx-type { font-weight: 700; color: var(--text); }
|
||||
.pos { color: var(--accent-light); font-weight: 800; font-size: 15px; }
|
||||
.neg { color: var(--danger); font-weight: 700; }
|
||||
.zero { color: var(--text-muted); font-weight: 700; font-size: 15px; }
|
||||
.tx-time { font-size: 11px; color: var(--text-muted); }
|
||||
.tx-arrow { font-size: 16px; color: var(--text-muted); font-weight: 700; line-height: 1; }
|
||||
|
||||
.sentinel {
|
||||
height: 1px;
|
||||
|
||||
@@ -138,9 +138,9 @@ function goCashbackDetail() {
|
||||
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
|
||||
</div>
|
||||
|
||||
<header class="top-bar sub-toolbar">
|
||||
<div class="top-bar">
|
||||
<button class="back-btn" type="button" @click="router.back()">‹ {{ t('history.back') }}</button>
|
||||
</header>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="state">
|
||||
<GoldSpinner :size="36" />
|
||||
@@ -235,7 +235,7 @@ function goCashbackDetail() {
|
||||
.back-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
padding: 4px 0 8px;
|
||||
@@ -254,7 +254,7 @@ function goCashbackDetail() {
|
||||
}
|
||||
|
||||
.hero {
|
||||
border-radius: var(--radius);
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -264,16 +264,17 @@ function goCashbackDetail() {
|
||||
text-align: center;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.hero.pos {
|
||||
border-color: rgba(46, 204, 113, 0.35);
|
||||
background: linear-gradient(135deg, rgba(46, 204, 113, 0.16), rgba(46, 204, 113, 0.05));
|
||||
border-color: rgba(244, 162, 97, 0.3);
|
||||
background: linear-gradient(135deg, rgba(244, 162, 97, 0.15), var(--bg-card));
|
||||
}
|
||||
|
||||
.hero.neg {
|
||||
border-color: rgba(255, 255, 255, 0.35);
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.05));
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
background: linear-gradient(135deg, rgba(239, 68, 68, 0.15), var(--bg-card));
|
||||
}
|
||||
|
||||
.hero.zero {
|
||||
@@ -292,7 +293,7 @@ function goCashbackDetail() {
|
||||
.hero-summary {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
color: var(--text-muted);
|
||||
max-width: 100%;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -303,22 +304,23 @@ function goCashbackDetail() {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.hero.pos .hero-amount { color: var(--success); }
|
||||
.hero.neg .hero-amount { color: var(--primary); }
|
||||
.hero.pos .hero-amount { color: var(--primary); }
|
||||
.hero.neg .hero-amount { color: var(--danger); }
|
||||
.hero.zero .hero-amount { color: var(--text-muted); }
|
||||
|
||||
.hero-time {
|
||||
font-size: 11px;
|
||||
color: var(--neutral);
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-card) 16px;
|
||||
margin-bottom: var(--space-section);
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@@ -351,13 +353,14 @@ function goCashbackDetail() {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.pos { color: var(--success) !important; }
|
||||
.neg { color: var(--primary) !important; }
|
||||
.pos { color: var(--primary) !important; }
|
||||
.neg { color: var(--danger) !important; }
|
||||
.zero { color: var(--text-muted) !important; }
|
||||
|
||||
.mono {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.remark {
|
||||
@@ -370,9 +373,9 @@ function goCashbackDetail() {
|
||||
margin-top: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: var(--surface-accent);
|
||||
color: var(--primary-light);
|
||||
border: 1px solid var(--primary);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
@@ -381,7 +384,7 @@ function goCashbackDetail() {
|
||||
.tx-id {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
color: var(--text-secondary);
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -1,588 +1,263 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, onActivated, onMounted } from 'vue';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
import { formatMoney } from '../utils/localeDisplay';
|
||||
|
||||
import { formatMoney, formatMoneyCompact } from '../utils/localeDisplay';
|
||||
import { txTypeKey, txDisplayType, txAmountClass, txSummaryLabel } from '../utils/walletTx';
|
||||
|
||||
import { parseCashbackApiData, sumCashbackAmount } from '../utils/cashback';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
|
||||
|
||||
type Transaction = {
|
||||
|
||||
transactionType: string;
|
||||
|
||||
displayType?: string;
|
||||
|
||||
summary?: string | null;
|
||||
|
||||
summaryKind?: 'opening_bonus' | null;
|
||||
|
||||
referenceType?: string | null;
|
||||
|
||||
amount: string;
|
||||
|
||||
frozenBefore?: string;
|
||||
|
||||
frozenAfter?: string;
|
||||
|
||||
createdAt: string;
|
||||
|
||||
transactionId: string;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const items = ref<Transaction[]>([]);
|
||||
|
||||
const cashbackTotal = ref<string>('0');
|
||||
const loading = ref(true);
|
||||
|
||||
const PREVIEW_COUNT = 15;
|
||||
|
||||
|
||||
|
||||
function txLabel(tx: Transaction): string {
|
||||
|
||||
const key = txTypeKey(txDisplayType(tx));
|
||||
|
||||
if (key) {
|
||||
|
||||
const translated = t(key);
|
||||
|
||||
if (translated !== key) return translated;
|
||||
|
||||
}
|
||||
|
||||
return tx.transactionType;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function txSubtitle(tx: Transaction): string {
|
||||
|
||||
return txSummaryLabel(tx, t);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function goDetail(tx: Transaction) {
|
||||
|
||||
if (!tx.transactionId) return;
|
||||
|
||||
router.push(`/wallet/transactions/${tx.transactionId}`);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function goCashbacks() {
|
||||
|
||||
router.push('/wallet/cashbacks');
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function fetchData() {
|
||||
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
|
||||
const { data } = await api.get('/player/wallet/transactions', { params: { page: 1 } });
|
||||
|
||||
const result = data.data ?? { items: [] };
|
||||
|
||||
const [txRes, cbRes] = await Promise.all([
|
||||
api.get('/player/wallet/transactions', { params: { page: 1 } }),
|
||||
api.get('/player/cashbacks').catch(() => ({ data: { data: [] } })),
|
||||
]);
|
||||
const result = txRes.data.data ?? { items: [] };
|
||||
items.value = (result.items ?? []).slice(0, PREVIEW_COUNT);
|
||||
|
||||
const cbRows = parseCashbackApiData(cbRes.data?.data);
|
||||
cashbackTotal.value = sumCashbackAmount(cbRows).toString();
|
||||
} catch {
|
||||
|
||||
/* ignore */
|
||||
|
||||
} finally {
|
||||
|
||||
loading.value = false;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const { pullDistance, spinning, progress } = usePullToRefresh({
|
||||
|
||||
onRefresh: fetchData,
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
onMounted(fetchData);
|
||||
|
||||
onActivated(fetchData);
|
||||
|
||||
|
||||
|
||||
const pullIndicatorStyle = () => ({
|
||||
|
||||
height: `${pullDistance.value}px`,
|
||||
|
||||
opacity: Math.min(pullDistance.value / 48, 1),
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<template>
|
||||
|
||||
<div class="wallet-page">
|
||||
|
||||
<div
|
||||
|
||||
class="pull-indicator"
|
||||
|
||||
:style="pullIndicatorStyle()"
|
||||
|
||||
>
|
||||
|
||||
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div v-if="loading" class="state">
|
||||
|
||||
<GoldSpinner :size="36" />
|
||||
|
||||
<span class="loading-text">{{ t('bet.loading') }}</span>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<template v-else>
|
||||
|
||||
<div class="wallet-links">
|
||||
|
||||
<button type="button" class="wallet-link" @click="goCashbacks">
|
||||
|
||||
{{ t('wallet.view_cashbacks') }}
|
||||
|
||||
<div class="top-bar">
|
||||
<button type="button" class="more-link secondary" @click="goCashbacks">
|
||||
{{ t('wallet.view_cashbacks_detail') }} ›
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
||||
v-if="items.length"
|
||||
|
||||
type="button"
|
||||
|
||||
class="wallet-link wallet-link--primary"
|
||||
|
||||
class="more-link"
|
||||
@click="router.push('/wallet/detail')"
|
||||
|
||||
>
|
||||
|
||||
{{ t('wallet.view_all') }}
|
||||
|
||||
</button>
|
||||
|
||||
>{{ t('wallet.view_all') }} ›</button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div v-if="items.length" class="tx-list">
|
||||
|
||||
<button
|
||||
|
||||
v-for="tx in items"
|
||||
|
||||
:key="tx.transactionId"
|
||||
|
||||
type="button"
|
||||
|
||||
class="tx-row"
|
||||
|
||||
@click="goDetail(tx)"
|
||||
|
||||
>
|
||||
|
||||
<div class="tx-col-left">
|
||||
|
||||
<span class="tx-type">{{ txLabel(tx) }}</span>
|
||||
|
||||
<span v-if="txSubtitle(tx)" class="tx-summary">{{ txSubtitle(tx) }}</span>
|
||||
|
||||
<span class="tx-time">{{ new Date(tx.createdAt).toLocaleString() }}</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tx-col-right">
|
||||
|
||||
<div class="tx-main">
|
||||
<div class="tx-text">
|
||||
<span class="tx-type">{{ txLabel(tx) }}</span>
|
||||
<span v-if="txSubtitle(tx)" class="tx-summary">{{ txSubtitle(tx) }}</span>
|
||||
</div>
|
||||
<span :class="txAmountClass(tx.amount)">
|
||||
|
||||
{{ formatMoney(tx.amount, locale) }}
|
||||
|
||||
</span>
|
||||
|
||||
<span class="tx-arrow" aria-hidden="true">›</span>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="tx-meta">
|
||||
<span class="tx-time">{{ new Date(tx.createdAt).toLocaleString() }}</span>
|
||||
<span class="tx-arrow">›</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div v-if="!items.length" class="empty">
|
||||
|
||||
{{ t('wallet.no_records') }}
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
|
||||
.wallet-page {
|
||||
|
||||
padding-bottom: 24px;
|
||||
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.pull-indicator {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
transition: height 0.15s ease;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.state {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
color: var(--text-muted);
|
||||
|
||||
padding: 56px 20px;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.loading-text {
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.wallet-links {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
margin: 0 0 12px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.wallet-link {
|
||||
|
||||
background: none;
|
||||
|
||||
border: none;
|
||||
|
||||
padding: 4px 0;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
color: var(--text-muted);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.wallet-link--primary {
|
||||
|
||||
color: var(--text);
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.wallet-link:active {
|
||||
|
||||
opacity: 0.72;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tx-list {
|
||||
|
||||
margin-bottom: 0;
|
||||
|
||||
background: var(--gradient-card);
|
||||
|
||||
border: 1px solid rgba(255, 255, 255, 0.11);
|
||||
|
||||
border-radius: var(--radius);
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
box-shadow: var(--shadow-card-sm);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tx-row {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
||||
text-align: left;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
padding: 14px 16px;
|
||||
|
||||
font-size: 13px;
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
background: transparent;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tx-row:last-child {
|
||||
|
||||
border-bottom: none;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tx-row:active {
|
||||
|
||||
background: var(--bg-hover);
|
||||
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tx-col-left {
|
||||
|
||||
.tx-main {
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 3px;
|
||||
|
||||
min-width: 0;
|
||||
|
||||
flex: 1;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tx-col-right {
|
||||
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
gap: 6px;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
|
||||
.tx-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tx-summary {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--text-dim);
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tx-type {
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: var(--text);
|
||||
|
||||
letter-spacing: -0.01em;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.pos {
|
||||
|
||||
color: var(--success);
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.neg {
|
||||
|
||||
color: var(--text);
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.zero {
|
||||
|
||||
color: var(--text-muted);
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tx-time {
|
||||
|
||||
font-size: 11px;
|
||||
|
||||
color: var(--text-dim);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.tx-arrow {
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
color: var(--text-dim);
|
||||
|
||||
font-weight: 400;
|
||||
|
||||
line-height: 1;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.empty {
|
||||
|
||||
text-align: center;
|
||||
|
||||
color: var(--text-muted);
|
||||
|
||||
padding: 40px 16px;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
</style>
|
||||
.tx-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.tx-type { font-weight: 700; color: var(--text); }
|
||||
.pos { color: var(--accent-light); font-weight: 800; font-size: 14px; }
|
||||
.neg { color: var(--danger); font-weight: 700; }
|
||||
.zero { color: var(--text-muted); font-weight: 700; font-size: 14px; }
|
||||
.tx-time { font-size: 11px; color: var(--text-muted); }
|
||||
.tx-arrow { font-size: 16px; color: var(--text-muted); font-weight: 700; line-height: 1; }
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 4px 1px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.top-bar .more-link.secondary {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.more-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--primary);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.more-link:active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.empty { text-align: center; color: var(--text-muted); padding: 28px 14px; font-weight: 600; }
|
||||
</style>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useSmsCode } from '../composables/useSmsCode';
|
||||
import LocaleSwitcher from '../components/LocaleSwitcher.vue';
|
||||
import PhoneCountrySelect from '../components/PhoneCountrySelect.vue';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import loginBg from '../assets/images/h5bg.webp';
|
||||
import loginBg from '../assets/images/h5bg2.webp';
|
||||
|
||||
const props = withDefaults(defineProps<{ desktop?: boolean }>(), { desktop: false });
|
||||
|
||||
@@ -103,156 +103,111 @@ const fieldError = () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="login-page"
|
||||
:class="{ 'login-page--desktop': props.desktop }"
|
||||
:style="props.desktop ? undefined : { backgroundImage: `url(${loginBg})` }"
|
||||
>
|
||||
<div class="login-page auth-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"
|
||||
: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>
|
||||
</div>
|
||||
|
||||
<h2 class="form-title">{{ t('auth.register') }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.username') }}</label>
|
||||
<input
|
||||
v-model="account"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
required
|
||||
autocomplete="username"
|
||||
maxlength="32"
|
||||
minlength="7"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.username_register_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.phone') }}</label>
|
||||
<div class="inline-row">
|
||||
<PhoneCountrySelect v-model="countryIso" />
|
||||
<input
|
||||
v-model="phone"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel-national"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.phone_local_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.sms_code') }}</label>
|
||||
<div class="inline-row">
|
||||
<input
|
||||
v-model="smsCode"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
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() || loading"
|
||||
@click="sendCode"
|
||||
>
|
||||
<span v-if="sending" class="btn-inline-loading">
|
||||
<GoldSpinner :size="14" :active="true" />
|
||||
{{ t('auth.sending_sms') }}
|
||||
</span>
|
||||
<span v-else>{{ countdown > 0 ? `${countdown}s` : t('auth.send_sms') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.password') }}</label>
|
||||
<input
|
||||
v-model="password"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.confirm_password') }}</label>
|
||||
<input
|
||||
v-model="confirmPassword"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
type="password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
minlength="8"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field field-optional">
|
||||
<label>{{ t('auth.invite_code') }} <span class="optional-tag">{{ t('auth.optional') }}</span></label>
|
||||
<input
|
||||
v-model="inviteCode"
|
||||
class="field-input"
|
||||
:class="{ 'ps-gold-input': !props.desktop }"
|
||||
autocomplete="off"
|
||||
:disabled="loading"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="fieldError()" class="error">{{ fieldError() }}</p>
|
||||
|
||||
<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.registering') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.register_btn') }}</span>
|
||||
</button>
|
||||
|
||||
<button v-if="props.desktop" type="button" class="btn-skip btn-skip--desktop" :disabled="loading" @click="goLogin">
|
||||
{{ t('auth.have_account') }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div v-if="!props.desktop" class="login-actions">
|
||||
<button type="button" class="btn-skip" :disabled="loading" @click="goLogin">
|
||||
{{ t('auth.have_account') }}
|
||||
</button>
|
||||
<button type="button" class="btn-skip-light" :disabled="loading" @click="continueBrowsing">
|
||||
{{ t('auth.continue_browsing') }}
|
||||
</button>
|
||||
<form @submit.prevent="submit" class="login-form ps-gold-frame auth-card" :class="{ '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>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="form-title">{{ t('auth.register') }}</h2>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.username') }}</label>
|
||||
<input
|
||||
v-model="account"
|
||||
class="field-input"
|
||||
required
|
||||
autocomplete="username"
|
||||
maxlength="32"
|
||||
minlength="7"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.username_register_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.phone') }}</label>
|
||||
<div class="inline-row">
|
||||
<PhoneCountrySelect v-model="countryIso" />
|
||||
<input
|
||||
v-model="phone"
|
||||
class="field-input"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel-national"
|
||||
:disabled="loading"
|
||||
:placeholder="t('auth.phone_local_placeholder')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.sms_code') }}</label>
|
||||
<div class="inline-row">
|
||||
<input
|
||||
v-model="smsCode"
|
||||
class="field-input"
|
||||
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() || loading"
|
||||
@click="sendCode"
|
||||
>
|
||||
<span v-if="sending" class="btn-inline-loading">
|
||||
<GoldSpinner :size="14" :active="true" />
|
||||
{{ t('auth.sending_sms') }}
|
||||
</span>
|
||||
<span v-else>{{ countdown > 0 ? `${countdown}s` : t('auth.send_sms') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.password') }}</label>
|
||||
<input v-model="password" class="field-input" type="password" required autocomplete="new-password" minlength="8" :disabled="loading" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>{{ t('auth.confirm_password') }}</label>
|
||||
<input v-model="confirmPassword" class="field-input" type="password" required autocomplete="new-password" minlength="8" :disabled="loading" />
|
||||
</div>
|
||||
|
||||
<div class="field field-optional">
|
||||
<label>{{ t('auth.invite_code') }} <span class="optional-tag">{{ t('auth.optional') }}</span></label>
|
||||
<input v-model="inviteCode" class="field-input" autocomplete="off" :disabled="loading" />
|
||||
</div>
|
||||
|
||||
<p v-if="fieldError()" class="error">{{ fieldError() }}</p>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-login btn-primary"
|
||||
:class="{ 'is-loading': loading }"
|
||||
:disabled="loading"
|
||||
:aria-busy="loading"
|
||||
>
|
||||
<span v-if="loading" class="btn-inline-loading">
|
||||
<GoldSpinner :size="18" :active="true" />
|
||||
{{ t('auth.registering') }}
|
||||
</span>
|
||||
<span v-else>{{ t('auth.register_btn') }}</span>
|
||||
</button>
|
||||
<button type="button" class="btn-skip" :disabled="loading" @click="goLogin">
|
||||
{{ t('auth.have_account') }}
|
||||
</button>
|
||||
</form>
|
||||
<button v-if="!props.desktop" type="button" class="btn-skip-light" @click="continueBrowsing">
|
||||
{{ t('auth.continue_browsing') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -261,7 +216,7 @@ const fieldError = () => {
|
||||
position: absolute;
|
||||
top: max(12px, env(safe-area-inset-top));
|
||||
right: 16px;
|
||||
z-index: 10;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.login-page {
|
||||
@@ -273,74 +228,22 @@ const fieldError = () => {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 30vh 16px max(20px, env(safe-area-inset-bottom));
|
||||
padding: 30vh 16px calc(10vh + max(20px, env(safe-area-inset-bottom)));
|
||||
background-color: var(--bg-body);
|
||||
background-size: cover;
|
||||
background-position: center top;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.login-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 34, 68, 0.35) 0%,
|
||||
rgba(0, 34, 68, 0.78) 52%,
|
||||
rgba(0, 34, 68, 0.94) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ===== 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-page--desktop::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-stack {
|
||||
.login-form {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-stack {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.login-page--desktop .login-form {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
padding: 22px 20px;
|
||||
}
|
||||
|
||||
.login-form.is-busy {
|
||||
@@ -357,8 +260,8 @@ const fieldError = () => {
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
border-radius: inherit;
|
||||
background: rgba(0, 26, 51, 0.82);
|
||||
backdrop-filter: blur(2px);
|
||||
background: rgba(22, 45, 54, 0.88);
|
||||
backdrop-filter: blur(4px);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@@ -366,7 +269,7 @@ const fieldError = () => {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@@ -377,28 +280,13 @@ const fieldError = () => {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.login-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0 14px 4px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
margin: 0 0 2px;
|
||||
font-size: 16px;
|
||||
margin: 0 0 4px;
|
||||
font-size: 17px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-page--desktop .form-title {
|
||||
font-size: 18px;
|
||||
margin: 0 0 8px;
|
||||
text-align: left;
|
||||
color: var(--primary-light);
|
||||
letter-spacing: 0.04em;
|
||||
text-align: center;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -407,73 +295,36 @@ const fieldError = () => {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-optional {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.optional-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.login-page--desktop .optional-tag {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.field-input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
height: 40px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.field-input::placeholder {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--border-gold-soft);
|
||||
}
|
||||
|
||||
/* 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-strong);
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.login-page--desktop .field-input:hover:not(:focus) {
|
||||
border-color: var(--border-gold-soft);
|
||||
}
|
||||
|
||||
.inline-row {
|
||||
@@ -493,32 +344,12 @@ label {
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-card);
|
||||
color: var(--text);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--primary);
|
||||
font-size: 11px;
|
||||
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: var(--border-gold-soft);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.btn-secondary:not(:disabled).active,
|
||||
.btn-secondary:active:not(:disabled) {
|
||||
border-color: var(--border-gold);
|
||||
background: var(--surface-accent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
@@ -532,28 +363,6 @@ 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: var(--surface-accent);
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-login:disabled {
|
||||
@@ -561,65 +370,43 @@ label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-page--desktop .btn-login:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.btn-login.is-loading:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-skip {
|
||||
padding: 6px 14px;
|
||||
margin-top: 2px;
|
||||
padding: 8px 14px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
color: var(--primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-skip:active {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.btn-skip--desktop {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-input);
|
||||
font-size: 14px;
|
||||
transition: border-color 0.2s ease, color 0.2s ease, background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-skip--desktop:hover:not(:disabled) {
|
||||
border-color: var(--border-gold-soft);
|
||||
color: var(--text);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.btn-skip:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.btn-skip-light {
|
||||
padding: 6px 16px;
|
||||
position: absolute;
|
||||
bottom: calc(20px + env(safe-area-inset-bottom));
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-skip-light:active {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.error {
|
||||
@@ -628,30 +415,100 @@ label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
animation: reg-fade-in 0.25s ease;
|
||||
}
|
||||
|
||||
/* ===== 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-page--desktop .login-form {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
padding: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.login-page--desktop .field {
|
||||
gap: 8px;
|
||||
}
|
||||
.login-page--desktop .field-optional {
|
||||
opacity: 0.85;
|
||||
}
|
||||
.login-page--desktop label {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.login-page--desktop .optional-tag {
|
||||
font-size: 12px;
|
||||
}
|
||||
.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(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(244, 162, 97, 0.18);
|
||||
}
|
||||
.login-page--desktop .field-input:hover:not(:focus) {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.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(244, 162, 97, 0.1);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.login-page--desktop .btn-login:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.login-page--desktop .btn-login:disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.login-page--desktop .btn-skip {
|
||||
margin-top: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(15, 32, 39, 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);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
border-left: 2px solid var(--danger, #FF453A);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -135,7 +135,7 @@ onActivated(() => {
|
||||
padding: 18px 20px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, rgba(10, 42, 68, 0.95) 42%);
|
||||
background: linear-gradient(135deg, rgba(244, 162, 97, 0.08) 0%, rgba(18, 18, 18, 0.95) 42%);
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
@@ -147,8 +147,8 @@ onActivated(() => {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #0A2540;
|
||||
color: var(--primary-light);
|
||||
background: var(--bg-elevated);
|
||||
color: var(--gold);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
@@ -158,7 +158,7 @@ onActivated(() => {
|
||||
|
||||
.back-btn:hover {
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
@@ -202,7 +202,7 @@ onActivated(() => {
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
@@ -238,7 +238,7 @@ onActivated(() => {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: rgba(10, 42, 68, 0.96);
|
||||
background: var(--bg-card);
|
||||
text-align: left;
|
||||
transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
@@ -251,7 +251,7 @@ onActivated(() => {
|
||||
|
||||
.card-cover {
|
||||
aspect-ratio: 16 / 7;
|
||||
background: #001A33;
|
||||
background: var(--bg-body);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -281,11 +281,11 @@ onActivated(() => {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 7px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
background: rgba(244, 162, 97, 0.12);
|
||||
border: 1px solid rgba(244, 162, 97, 0.22);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--primary-light);
|
||||
color: var(--gold);
|
||||
}
|
||||
|
||||
.card-date {
|
||||
|
||||
@@ -193,7 +193,7 @@ function translateSel(name: string): string {
|
||||
.status-badge { display: inline-block; padding: 4px 14px; border-radius: 999px; font-size: 12px; font-weight: 700; align-self: flex-start; }
|
||||
.status-won { background: rgba(52,199,89,0.12); color: #4cd964; }
|
||||
.status-lost { background: rgba(255,69,58,0.12); color: #ff453a; }
|
||||
.status-pending { background: rgba(255, 255, 255,0.12); color: var(--primary-light); }
|
||||
.status-pending { background: rgba(244, 162, 97,0.12); color: var(--primary-light); }
|
||||
.status-push { background: rgba(100,100,100,0.12); color: #aaa; }
|
||||
.status-default { background: rgba(100,100,100,0.1); color: #888; }
|
||||
|
||||
|
||||
@@ -190,9 +190,9 @@ onActivated(async () => {
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
border: 1px solid rgba(244, 162, 97, 0.22);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
|
||||
@@ -108,7 +108,7 @@ const leagueGroups = computed(() => {
|
||||
const filteredOutrightEvents = computed(() => {
|
||||
if (mainTab.value !== 'outright') return [];
|
||||
const keyword = searchQuery.value.trim().toLowerCase();
|
||||
return outrightEvents.value.filter((e) => {
|
||||
return outrightEvents.value.filter((e: OutrightEvent) => {
|
||||
if (keyword) {
|
||||
const haystack = `${e.title} ${e.leagueName || ''}`.toLowerCase();
|
||||
if (!haystack.includes(keyword)) return false;
|
||||
@@ -238,7 +238,7 @@ void loadParlayMatches(true);
|
||||
{{
|
||||
t('bet.outright_events_summary', {
|
||||
events: filteredOutrightEvents.length,
|
||||
teams: filteredOutrightEvents.reduce((n, e) => n + e.selections.length, 0),
|
||||
teams: filteredOutrightEvents.reduce((n: number, e: OutrightEvent) => n + e.selections.length, 0),
|
||||
})
|
||||
}}
|
||||
</p>
|
||||
@@ -303,7 +303,7 @@ void loadParlayMatches(true);
|
||||
|
||||
.main-tab.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ void loadParlayMatches(true);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: rgba(244, 162, 97, 0.06);
|
||||
border-left: 3px solid var(--primary);
|
||||
border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
:line-value="m.lineValue"
|
||||
:selections="m.selections || []"
|
||||
:is-selected="isSelected"
|
||||
@pick="(selId, ev) => handleOddsClick(featuredMatch!, m, selId, ev)"
|
||||
@pick="(selId, event) => handleOddsClick(featuredMatch!, m, selId, event)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!getAvailableMarketsForMatch(featuredMatch).length" class="no-market-odds">
|
||||
@@ -351,7 +351,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
:line-value="getActiveMarket(match)?.lineValue"
|
||||
:selections="getActiveMarket(match)?.selections || []"
|
||||
:is-selected="isSelected"
|
||||
@pick="(selId, ev) => handleOddsClick(match, getActiveMarket(match)!, selId, ev)"
|
||||
@pick="(selId, event) => handleOddsClick(match, getActiveMarket(match)!, selId, event)"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="no-market-odds">
|
||||
@@ -463,7 +463,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
:line-value="getActiveMarket(match)?.lineValue"
|
||||
:selections="getActiveMarket(match)?.selections || []"
|
||||
:is-selected="isSelected"
|
||||
@pick="(selId, ev) => handleOddsClick(match, getActiveMarket(match)!, selId, ev)"
|
||||
@pick="(selId, event) => handleOddsClick(match, getActiveMarket(match)!, selId, event)"
|
||||
/>
|
||||
</template>
|
||||
<div v-else class="no-market-odds">
|
||||
@@ -584,14 +584,14 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(90deg,
|
||||
rgba(255, 255, 255, 0.02),
|
||||
rgba(255, 255, 255, 0.14),
|
||||
rgba(255, 255, 255, 0.02));
|
||||
rgba(244, 162, 97, 0.02),
|
||||
rgba(244, 162, 97, 0.14),
|
||||
rgba(244, 162, 97, 0.02));
|
||||
border: 1px solid var(--border-gold-soft);
|
||||
display: flex;
|
||||
align-items: stretch; /* let child fill full height */
|
||||
padding: 0; /* no padding — child spans edge to edge */
|
||||
box-shadow: inset 0 0 12px rgba(255, 255, 255, 0.08);
|
||||
box-shadow: inset 0 0 12px rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
/* force the embedded button to fill the strip completely */
|
||||
@@ -621,7 +621,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
}
|
||||
|
||||
.content-section {
|
||||
background: var(--gradient-card);
|
||||
background: var(--bg-card); backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 16px 18px;
|
||||
@@ -640,7 +640,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
height: 18px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(180deg, var(--primary), var(--primary-light));
|
||||
box-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
|
||||
box-shadow: 0 0 8px rgba(244, 162, 97, 0.6);
|
||||
}
|
||||
|
||||
.st-text {
|
||||
@@ -693,10 +693,10 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
padding: 18px 20px;
|
||||
border-radius: 12px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04) 50%, rgba(0, 26, 51, 0.2)),
|
||||
linear-gradient(135deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04) 50%, rgba(15, 10, 0, 0.1)),
|
||||
url('../../assets/images/card-bg.webp') center/cover no-repeat;
|
||||
border: 1px solid var(--border-gold);
|
||||
box-shadow: inset 0 0 20px rgba(255, 255, 255, 0.04);
|
||||
box-shadow: var(--shadow-gold), inset 0 0 20px rgba(244, 162, 97, 0.06);
|
||||
margin-bottom: 14px;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.25s, transform 0.15s;
|
||||
@@ -709,13 +709,13 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
position: absolute;
|
||||
top: 0; right: 0;
|
||||
width: 140px; height: 140px;
|
||||
background: radial-gradient(circle at top right, rgba(255, 255, 255, 0.18), transparent 65%);
|
||||
background: radial-gradient(circle at top right, rgba(244, 162, 97, 0.18), transparent 65%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.featured-match:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 24px rgba(255, 255, 255, 0.08), inset 0 0 20px rgba(255, 255, 255, 0.04);
|
||||
box-shadow: 0 0 24px rgba(244, 162, 97, 0.25), inset 0 0 20px rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.featured-top {
|
||||
@@ -781,7 +781,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
color: var(--primary-light);
|
||||
text-shadow: 0 0 12px rgba(255, 255, 255, 0.5);
|
||||
text-shadow: 0 0 12px rgba(244, 162, 97, 0.5);
|
||||
}
|
||||
|
||||
.featured-markets {
|
||||
@@ -790,8 +790,8 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
gap: 10px;
|
||||
margin: 14px -20px -18px;
|
||||
padding: 14px 20px 18px;
|
||||
background: rgba(0, 26, 51, 0.6);
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg-body);
|
||||
border-top: 1px solid rgba(244, 162, 97, 0.2);
|
||||
}
|
||||
|
||||
.featured-market-block {
|
||||
@@ -828,8 +828,8 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
}
|
||||
|
||||
.match-card:is(:hover, .match-card--engaged) {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border-color: var(--border-gold);
|
||||
box-shadow: var(--shadow-gold);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -1008,7 +1008,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
}
|
||||
|
||||
.side-card {
|
||||
background: var(--gradient-card);
|
||||
background: var(--bg-card); backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
@@ -1220,7 +1220,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
transform: translateY(8px) scale(0.95);
|
||||
transition: opacity 0.35s cubic-bezier(0.25, 0.8, 0.25, 1), transform 0.35s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
pointer-events: none;
|
||||
background: var(--glass);
|
||||
background: rgba(14, 14, 14, 0.96);
|
||||
border-radius: 8px 0 0 8px;
|
||||
}
|
||||
|
||||
@@ -1283,8 +1283,8 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
transform: scale(2.8);
|
||||
opacity: 0;
|
||||
text-shadow:
|
||||
0 0 10px rgba(255, 255, 255, 0.8),
|
||||
0 0 20px rgba(255, 255, 255, 0.4);
|
||||
0 0 10px rgba(244, 162, 97, 0.8),
|
||||
0 0 20px rgba(244, 162, 97, 0.4);
|
||||
transition: transform 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275) 0.1s, opacity 0.3s ease 0.1s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -1297,11 +1297,11 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
|
||||
@keyframes vs-clash-impact {
|
||||
0% {
|
||||
text-shadow: 0 0 25px rgba(255, 255, 255, 1);
|
||||
text-shadow: 0 0 25px rgba(244, 162, 97, 1);
|
||||
transform: scale(1.25);
|
||||
}
|
||||
100% {
|
||||
text-shadow: 0 0 8px rgba(255, 255, 255, 0.7);
|
||||
text-shadow: 0 0 8px rgba(244, 162, 97, 0.7);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
@@ -1313,7 +1313,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
|
||||
.side-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 4px 20px rgba(244, 162, 97, 0.12);
|
||||
}
|
||||
|
||||
/* --- 8. 推荐列表 item 左侧金点指示 hover --- */
|
||||
@@ -1323,7 +1323,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
}
|
||||
|
||||
.side-recommend-item:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: rgba(244, 162, 97, 0.06);
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
@@ -1346,11 +1346,11 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
right: calc(100% + 6px);
|
||||
transform: translateY(-50%) translateX(8px);
|
||||
width: 260px;
|
||||
background: var(--dropdown-bg);
|
||||
background: rgba(12, 12, 12, 0.97);
|
||||
border: 1px solid var(--border-gold);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 20px rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 0 20px rgba(244, 162, 97, 0.12);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
@@ -1439,7 +1439,7 @@ function getHoveredSideForMatch(match: PlayerHomeMatch | null, cardRootId: strin
|
||||
font-weight: 900;
|
||||
font-style: italic;
|
||||
color: var(--primary-light);
|
||||
text-shadow: 0 0 8px rgba(255, 255, 255, 0.6);
|
||||
text-shadow: 0 0 8px rgba(244, 162, 97, 0.6);
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transform: scale(2.2);
|
||||
|
||||
@@ -185,7 +185,7 @@ onActivated(refreshUnreadCount);
|
||||
|
||||
.hub-tab.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.hub-tab.active .dot { background: var(--primary); box-shadow: 0 0 8px var(--primary); }
|
||||
@@ -194,7 +194,7 @@ onActivated(refreshUnreadCount);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: var(--tertiary);
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
@@ -260,6 +260,5 @@ onActivated(refreshUnreadCount);
|
||||
.hub-panel {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -350,7 +350,7 @@ function getHoveredSide() {
|
||||
@mouseleave="scheduleClose()"
|
||||
>
|
||||
<div class="team-side home">
|
||||
<TeamEmblem size="xl" :team-code="match.homeTeamCode" :team-name="match.homeTeamName" :logo-url="match.homeTeamLogoUrl" />
|
||||
<TeamEmblem size="lg" :team-code="match.homeTeamCode" :team-name="match.homeTeamName" :logo-url="match.homeTeamLogoUrl" />
|
||||
<span class="name">{{ match.homeTeamName }}</span>
|
||||
</div>
|
||||
|
||||
@@ -365,7 +365,7 @@ function getHoveredSide() {
|
||||
</div>
|
||||
|
||||
<div class="team-side away">
|
||||
<TeamEmblem size="xl" :team-code="match.awayTeamCode" :team-name="match.awayTeamName" :logo-url="match.awayTeamLogoUrl" />
|
||||
<TeamEmblem size="lg" :team-code="match.awayTeamCode" :team-name="match.awayTeamName" :logo-url="match.awayTeamLogoUrl" />
|
||||
<span class="name">{{ match.awayTeamName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -427,7 +427,7 @@ function getHoveredSide() {
|
||||
:line-value="market.lineValue"
|
||||
:selections="market.selections"
|
||||
:is-selected="isSelected"
|
||||
@pick="(id, ev) => toggleSelection(market.selections.find(s => s.id === id)!, market, ev)"
|
||||
@pick="(id, event) => toggleSelection(market.selections.find(s => s.id === id)!, market, event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -455,7 +455,7 @@ function getHoveredSide() {
|
||||
:selections="market.selections"
|
||||
:locked="isMarketLocked(market)"
|
||||
:is-selected="isSelected"
|
||||
@pick="(id, ev) => toggleSelection(market.selections.find(s => s.id === id)!, market, ev)"
|
||||
@pick="(id, event) => toggleSelection(market.selections.find(s => s.id === id)!, market, event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -490,7 +490,7 @@ function getHoveredSide() {
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
background: var(--desktop-sidebar-panel-bg);
|
||||
background: var(--bg-body);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -541,7 +541,7 @@ function getHoveredSide() {
|
||||
linear-gradient(135deg, rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.04) 50%, rgba(15, 10, 0, 0.1)),
|
||||
url('../../assets/images/card-bg.webp') center/cover no-repeat;
|
||||
border: 1px solid var(--border-gold);
|
||||
box-shadow: var(--shadow-gold), inset 0 0 20px rgba(255, 255, 255, 0.06);
|
||||
box-shadow: var(--shadow-gold), inset 0 0 20px rgba(244, 162, 97, 0.06);
|
||||
border-radius: 12px;
|
||||
padding: 18px 24px;
|
||||
overflow: hidden;
|
||||
@@ -586,7 +586,7 @@ function getHoveredSide() {
|
||||
font-size: 32px;
|
||||
font-weight: 900;
|
||||
color: var(--primary-light);
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
|
||||
text-shadow: 0 0 10px rgba(244, 162, 97, 0.3);
|
||||
}
|
||||
|
||||
.versus-row .divider {
|
||||
@@ -606,7 +606,7 @@ function getHoveredSide() {
|
||||
}
|
||||
|
||||
.result-stats-card {
|
||||
background: rgba(12, 40, 66, 0.8);
|
||||
background: var(--bg-card); backdrop-filter: blur(12px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
@@ -627,7 +627,7 @@ function getHoveredSide() {
|
||||
}
|
||||
|
||||
.stat-pill {
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
@@ -646,7 +646,7 @@ function getHoveredSide() {
|
||||
padding: 6px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: rgba(10, 42, 68, 0.85);
|
||||
background: var(--bg-card);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@@ -696,9 +696,9 @@ function getHoveredSide() {
|
||||
|
||||
.market-filter-chip.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
border-color: var(--border-gold-soft);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.markets-score-section {
|
||||
@@ -738,7 +738,7 @@ function getHoveredSide() {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: rgba(10, 42, 68, 0.98);
|
||||
background: var(--bg-card);
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
@@ -752,7 +752,7 @@ function getHoveredSide() {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@@ -779,7 +779,7 @@ function getHoveredSide() {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background: rgba(10, 42, 68, 0.98);
|
||||
background: var(--bg-card);
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
@@ -793,7 +793,7 @@ function getHoveredSide() {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #0A2540;
|
||||
background: var(--bg-elevated);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
|
||||
@@ -128,7 +128,7 @@ onActivated(refreshUnreadCount);
|
||||
|
||||
.hub-tab.active {
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: rgba(244, 162, 97, 0.08);
|
||||
}
|
||||
|
||||
.hub-tab.active .dot {
|
||||
@@ -140,7 +140,7 @@ onActivated(refreshUnreadCount);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: var(--tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ const activePick = ref<OutrightPick | null>(null);
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const event = computed<OutrightEvent | null>(() =>
|
||||
events.value.find((e) => e.id === String(route.params.id)) ?? null,
|
||||
events.value.find((e: OutrightEvent) => e.id === String(route.params.id)) ?? null,
|
||||
);
|
||||
|
||||
const headTitle = computed(() => {
|
||||
@@ -182,7 +182,7 @@ onUnmounted(stopPolling);
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
background: #0c0c0c;
|
||||
background: var(--bg-body);
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -237,7 +237,7 @@ onUnmounted(stopPolling);
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
background: rgba(10, 42, 68, 0.98);
|
||||
background: rgba(18, 18, 18, 0.98);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ onUnmounted(stopPolling);
|
||||
.settled-tag {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: #c9a227;
|
||||
color: var(--primary);
|
||||
border: 1px solid rgba(201, 162, 39, 0.45);
|
||||
border-radius: 999px;
|
||||
padding: 1px 7px;
|
||||
@@ -298,7 +298,7 @@ onUnmounted(stopPolling);
|
||||
margin: 8px 0 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #c9a227;
|
||||
color: var(--primary);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
|
||||
@@ -309,7 +309,7 @@ async function onAvatarSelect(key: string | null) {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: #0A2540;
|
||||
background: var(--secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -347,7 +347,7 @@ async function onAvatarSelect(key: string | null) {
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--desktop-border);
|
||||
background: #001A33;
|
||||
background: var(--bg-glass); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -371,7 +371,7 @@ async function onAvatarSelect(key: string | null) {
|
||||
padding: 9px 24px;
|
||||
border-radius: 6px;
|
||||
background: var(--primary);
|
||||
color: #001A33;
|
||||
color: var(--bg-body);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
border: none;
|
||||
|
||||
@@ -276,7 +276,7 @@ const balanceDisplay = computed(() =>
|
||||
.bank-card-type {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
color: rgba(244, 162, 97, 0.8);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -286,8 +286,8 @@ const balanceDisplay = computed(() =>
|
||||
gap: 4px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 14px;
|
||||
background: rgba(0, 26, 51, 0.45);
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
border: 1px solid rgba(244, 162, 97, 0.35);
|
||||
color: var(--primary-light);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
@@ -296,7 +296,7 @@ const balanceDisplay = computed(() =>
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.recharge-btn:hover { background: rgba(255, 255, 255, 0.15); }
|
||||
.recharge-btn:hover { background: rgba(244, 162, 97, 0.15); }
|
||||
|
||||
.bank-card-balance {
|
||||
flex: 1;
|
||||
@@ -396,7 +396,7 @@ const balanceDisplay = computed(() =>
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.edit-btn:hover { background: rgba(255, 255, 255,0.08); }
|
||||
.edit-btn:hover { background: rgba(244, 162, 97,0.08); }
|
||||
|
||||
.lang-group { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
|
||||
@@ -418,7 +418,7 @@ const balanceDisplay = computed(() =>
|
||||
.lang-opt.active {
|
||||
border-color: var(--border-gold-soft);
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255,0.08);
|
||||
background: rgba(244, 162, 97,0.08);
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
|
||||
@@ -311,7 +311,7 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
.status-badge { display: inline-block; padding: 4px 14px; border-radius: 999px; font-size: 12px; font-weight: 700; align-self: flex-start; }
|
||||
.status-won { background: rgba(52,199,89,0.12); color: #4cd964; }
|
||||
.status-lost { background: rgba(255,69,58,0.12); color: #ff453a; }
|
||||
.status-pending { background: rgba(255, 255, 255,0.12); color: var(--primary-light); }
|
||||
.status-pending { background: rgba(244, 162, 97,0.12); color: var(--primary-light); }
|
||||
.status-default { background: rgba(100,100,100,0.1); color: #888; }
|
||||
|
||||
.amount-row { display: flex; gap: 32px; padding-top: 4px; flex-wrap: wrap; }
|
||||
@@ -379,7 +379,7 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #555;
|
||||
background: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -425,5 +425,5 @@ function auditNoteDisplayText(remark: { kind: 'note'; text: string }) {
|
||||
.audit-step--approved .audit-dot { background: #4cd964; }
|
||||
.audit-step--rejected .audit-dot { background: #ff453a; }
|
||||
.audit-step--revoked .audit-dot { background: #888; }
|
||||
.audit-step--reopened .audit-dot { background: #c9a227; }
|
||||
.audit-step--reopened .audit-dot { background: var(--primary); }
|
||||
</style>
|
||||
|
||||
@@ -158,8 +158,8 @@ onActivated(fetchMethods);
|
||||
<div v-else-if="success" class="success-card">
|
||||
<div class="success-icon-wrap">
|
||||
<svg class="check-svg" viewBox="0 0 52 52" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle class="check-circle" cx="26" cy="26" r="24" fill="none" stroke="rgba(255, 255, 255, 0.92)" stroke-width="3" />
|
||||
<path class="check-mark" d="M14 27l7 7 16-16" fill="none" stroke="rgba(255, 255, 255, 0.92)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<circle class="check-circle" cx="26" cy="26" r="24" fill="none" stroke="#F4A261" stroke-width="3" />
|
||||
<path class="check-mark" d="M14 27l7 7 16-16" fill="none" stroke="#F4A261" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2>{{ t('recharge.success_title') || '充值申请已提交' }}</h2>
|
||||
@@ -403,7 +403,7 @@ onActivated(fetchMethods);
|
||||
background: rgba(25, 25, 25, 0.65);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(244, 162, 97, 0.12);
|
||||
border-radius: 16px;
|
||||
text-align: center;
|
||||
max-width: 480px;
|
||||
@@ -434,7 +434,7 @@ onActivated(fetchMethods);
|
||||
}
|
||||
|
||||
.success-details {
|
||||
background: rgba(0, 26, 51, 0.25);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
width: 100%;
|
||||
@@ -479,8 +479,8 @@ onActivated(fetchMethods);
|
||||
.action-btn {
|
||||
padding: 10px 28px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #3d2800;
|
||||
background: var(--gradient-primary);
|
||||
color: #ffffff;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
border: none;
|
||||
@@ -488,12 +488,12 @@ onActivated(fetchMethods);
|
||||
transition: opacity 0.2s;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 4px 12px rgba(244, 162, 97, 0.15);
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 4px 16px rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(244, 162, 97, 0.3);
|
||||
}
|
||||
|
||||
/* Recharge Box Card */
|
||||
@@ -501,7 +501,7 @@ onActivated(fetchMethods);
|
||||
background: rgba(25, 25, 25, 0.65);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(244, 162, 97, 0.12);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
|
||||
@@ -510,7 +510,7 @@ onActivated(fetchMethods);
|
||||
/* Segment Tabs */
|
||||
.type-tabs {
|
||||
display: flex;
|
||||
background: rgba(0, 26, 51, 0.35);
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
gap: 4px;
|
||||
@@ -537,9 +537,9 @@ onActivated(fetchMethods);
|
||||
}
|
||||
|
||||
.type-tab.active {
|
||||
color: #3d2800;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow: 0 2px 8px rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
background: var(--gradient-primary);
|
||||
box-shadow: 0 2px 8px rgba(244, 162, 97, 0.2);
|
||||
}
|
||||
|
||||
/* Columns */
|
||||
@@ -585,13 +585,13 @@ onActivated(fetchMethods);
|
||||
}
|
||||
|
||||
.method-card:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(244, 162, 97, 0.3);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.method-card.selected {
|
||||
border-color: var(--border-gold-soft);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
background: rgba(244, 162, 97, 0.06);
|
||||
}
|
||||
|
||||
.method-card-header {
|
||||
@@ -604,7 +604,7 @@ onActivated(fetchMethods);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
color: var(--primary-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -619,7 +619,7 @@ onActivated(fetchMethods);
|
||||
|
||||
.method-card.selected .method-icon-wrap {
|
||||
background: var(--primary);
|
||||
color: #3d2800;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
@@ -645,7 +645,7 @@ onActivated(fetchMethods);
|
||||
}
|
||||
|
||||
.bank-detail-card {
|
||||
background: rgba(0, 26, 51, 0.2);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 8px;
|
||||
padding: 16px 18px;
|
||||
@@ -678,8 +678,8 @@ onActivated(fetchMethods);
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: rgba(244, 162, 97, 0.1);
|
||||
border: 1px solid rgba(244, 162, 97, 0.25);
|
||||
border-radius: 4px;
|
||||
color: var(--primary-light);
|
||||
font-size: 10px;
|
||||
@@ -691,7 +691,7 @@ onActivated(fetchMethods);
|
||||
}
|
||||
|
||||
.copy-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(244, 162, 97, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -775,7 +775,7 @@ onActivated(fetchMethods);
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 26, 51, 0.25);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
@@ -807,9 +807,9 @@ onActivated(fetchMethods);
|
||||
}
|
||||
|
||||
.preset-btn:hover {
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(244, 162, 97, 0.3);
|
||||
color: var(--primary-light);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: rgba(244, 162, 97, 0.05);
|
||||
}
|
||||
|
||||
.preview-wrap {
|
||||
@@ -833,7 +833,7 @@ onActivated(fetchMethods);
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
background: var(--tertiary);
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
@@ -851,7 +851,7 @@ onActivated(fetchMethods);
|
||||
gap: 8px;
|
||||
width: 180px;
|
||||
height: 120px;
|
||||
border: 1.5px dashed rgba(255, 255, 255, 0.3);
|
||||
border: 1.5px dashed rgba(244, 162, 97, 0.3);
|
||||
background: rgba(255, 255, 255, 0.01);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
@@ -860,7 +860,7 @@ onActivated(fetchMethods);
|
||||
|
||||
.upload-zone:hover {
|
||||
border-color: var(--primary);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: rgba(244, 162, 97, 0.04);
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
@@ -880,20 +880,20 @@ onActivated(fetchMethods);
|
||||
gap: 8px;
|
||||
padding: 12px 36px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #3d2800;
|
||||
background: var(--gradient-primary);
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
align-self: flex-start;
|
||||
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.15);
|
||||
box-shadow: 0 4px 12px rgba(244, 162, 97, 0.15);
|
||||
}
|
||||
|
||||
.submit-btn:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
box-shadow: 0 4px 16px rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 4px 16px rgba(244, 162, 97, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
|
||||
@@ -34,6 +34,14 @@ watch(detailModalVisible, (visible) => {
|
||||
if (!visible) detailTxId.value = null;
|
||||
});
|
||||
|
||||
function goBack() {
|
||||
if (window.history.state && window.history.state.back) {
|
||||
router.back();
|
||||
} else {
|
||||
router.push('/wallet');
|
||||
}
|
||||
}
|
||||
|
||||
type Transaction = {
|
||||
transactionType: string;
|
||||
displayType?: string;
|
||||
|
||||
@@ -109,11 +109,11 @@ function txLabel(tx: TxDetail): string {
|
||||
<span class="row-val mono">{{ formatMoney(detail.balanceAfter, locale) }}</span>
|
||||
</div>
|
||||
<div v-if="parseFloat(detail.frozenBefore || '0') > 0 || parseFloat(detail.frozenAfter || '0') > 0" class="detail-row">
|
||||
<span class="row-label">{{ t('wallet.detail_frozen_before') }}</span>
|
||||
<span class="row-label">{{ t('wallet.frozen_before') || '冻结前' }}</span>
|
||||
<span class="row-val mono">{{ formatMoney(detail.frozenBefore, locale) }}</span>
|
||||
</div>
|
||||
<div v-if="parseFloat(detail.frozenBefore || '0') > 0 || parseFloat(detail.frozenAfter || '0') > 0" class="detail-row">
|
||||
<span class="row-label">{{ t('wallet.detail_frozen_after') }}</span>
|
||||
<span class="row-label">{{ t('wallet.frozen_after') || '冻结后' }}</span>
|
||||
<span class="row-val mono">{{ formatMoney(detail.frozenAfter, locale) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
|
||||
@@ -148,7 +148,10 @@ onActivated(() => fetchData(1));
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<DesktopTxDetailModal v-model:visible="detailModalVisible" :transaction-id="detailTxId" />
|
||||
<DesktopTxDetailModal
|
||||
v-model:visible="detailModalVisible"
|
||||
:transaction-id="detailTxId"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 四套主题 Docker 生产部署任务
|
||||
|
||||
> **目标**:在同一台服务器、**同一套 Docker Compose 栈**内,同时运行 4 套玩家端主题(4 个 player 容器 + 1 套共用 api/admin/db)。
|
||||
> **适用分支**:`main`(暗金)、`theme-2`(Pinnacle 蓝白)、`theme-3`(统一移动端视觉)、`theme-4`(海军蓝暗色极简)。
|
||||
> **适用分支**:`main`(暗金)、`theme-2`(Pinnacle 蓝白)、`theme-3`(Dusk Coral 移动 + PC双端)、`theme-4`(海军蓝暗色极简)。
|
||||
> **关联文档**:[Docker部署指南.md](./Docker部署指南.md)、[docker/镜像构建与导出.md](./docker/镜像构建与导出.md)、[AGENTS.md](../AGENTS.md)「主题分支」章节。
|
||||
|
||||
---
|
||||
@@ -475,7 +475,7 @@ location / {
|
||||
"themes": [
|
||||
{ "id": "main", "label": "暗金主站", "baseUrl": "https://www.example.com", "enabled": true, "isDefault": true },
|
||||
{ "id": "theme-2", "label": "Pinnacle 蓝白", "baseUrl": "https://theme2.example.com", "enabled": true, "isDefault": false },
|
||||
{ "id": "theme-3", "label": "统一移动端", "baseUrl": "https://theme3.example.com", "enabled": true, "isDefault": false },
|
||||
{ "id": "theme-3", "label": "Dusk Coral 移动 + PC双端", "baseUrl": "https://theme3.example.com", "enabled": true, "isDefault": false },
|
||||
{ "id": "theme-4", "label": "海军蓝极简", "baseUrl": "https://theme4.example.com", "enabled": true, "isDefault": false }
|
||||
]
|
||||
}
|
||||
|
||||
156
docs/玩家端缺失功能分析.md
Normal file
156
docs/玩家端缺失功能分析.md
Normal file
@@ -0,0 +1,156 @@
|
||||
## 玩家端缺失功能分析
|
||||
|
||||
> 基于 PRD v1.2 要求 + 行业通用标准,对比当前代码实际实现情况
|
||||
> 分析日期:2026-06-17
|
||||
|
||||
---
|
||||
|
||||
### 一、PRD 明确要求但尚未完整实现的功能
|
||||
|
||||
#### 1. 首页「今日赛事」板块
|
||||
|
||||
PRD 4.4 节明确要求首页同时展示「热门赛事」和「今日赛事」两个板块。当前 HomeView.vue 只渲染了 `hotMatches`,缺少按当天开赛时间筛选的「今日赛事」列表。后端 `/player/home` 接口实际已返回 `todayMatches` 数据,前端未消费。
|
||||
|
||||
#### 2. 公告详情页
|
||||
|
||||
PRD 17.2 节将公告定义为"可点击查看详情的完整内容",与走马灯(短文案滚动)是两种不同的内容形态。当前只有 AnnouncementMarquee 走马灯组件,点击后没有跳转到公告详情页查看完整正文。
|
||||
|
||||
#### 3. 投注规则独立页面
|
||||
|
||||
PRD 3.3 节要求"投注规则说明必须三语,减少争议"。当前规则内容硬编码在 ProfileView.vue 的 5 段 i18n 翻译文本中(rules_p1 ~ rules_p5),不可由后台动态维护,也没有独立的规则页面。建议做成后台可编辑、前台可独立访问的规则中心页面。
|
||||
|
||||
---
|
||||
|
||||
### 二、业务闭环缺失的关键功能
|
||||
|
||||
#### 4. 提款 / 出金
|
||||
|
||||
当前已实现充值(Recharge)全流程——银行转账、USDT、截图上传、审核、入账。但完全没有提款/出金功能。玩家赢了钱却无法主动申请提款,这在业务闭环上是最大的缺口。至少需要:提款申请页面、提款方式选择、金额输入、提款记录与状态追踪。
|
||||
|
||||
#### 5. 注单结算/派彩通知
|
||||
|
||||
玩家下注后,赛事结算完成时没有任何主动通知机制。玩家只能反复刷"我的投注"才知道结果。需要:注单结算后推送通知(站内消息或 WebSocket 实时推送),至少在下注成功页和注单列表中有明确的"等待开奖"与"已开奖"状态区分。
|
||||
|
||||
#### 6. 充值/提款状态通知
|
||||
|
||||
充值订单审核通过或被拒绝后,玩家没有收到即时通知。目前只能在充值历史页面手动查看状态变化。
|
||||
|
||||
---
|
||||
|
||||
### 三、体验层面的缺失功能
|
||||
|
||||
#### 7. 赛事 / 球队搜索
|
||||
|
||||
当前赛事浏览路径只有:联赛筛选 → 时间筛选 → 列表滚动。当赛事数量多时,玩家无法通过关键词搜索球队名或联赛名快速定位比赛。需要在赛事列表页或首页增加搜索框。
|
||||
|
||||
#### 8. 投注单清空确认
|
||||
|
||||
BetSlipDrawer 中清空投注单没有二次确认弹窗,容易误操作导致已选择的投注项丢失。
|
||||
|
||||
#### 9. 投注单赔率实时更新提示
|
||||
|
||||
PRD 7.3 节要求"赔率已变化,拒绝下注,提示重新确认"。当前提交时校验赔率版本会报错,但在投注单停留期间没有实时检测赔率变化并主动标记(如高亮变化项、显示新旧赔率对比),玩家只有在点提交时才知道赔率变了。
|
||||
|
||||
#### 10. 盘口封盘倒计时
|
||||
|
||||
赛事列表和详情页没有显示距离封盘的倒计时。玩家不知道赛事何时关闭投注,影响下注决策。
|
||||
|
||||
#### 11. 快捷金额按钮自定义
|
||||
|
||||
PRD 4.3 节桌面端布局提到"快捷投注金额"。当前 BetSlipDrawer 中有固定的快捷金额按钮(Min/5、+50、+100、Max),但不能由后台配置或让玩家自定义。
|
||||
|
||||
#### 12. 我的投注 - 按赛事/时间筛选
|
||||
|
||||
MyBetsView 目前支持按状态筛选(全部/赢/输/待结算/走水),缺少按赛事、按时间范围筛选的能力,注单多了之后查找困难。
|
||||
|
||||
#### 13. 注单详情页 - 缺少分享/截图功能
|
||||
|
||||
玩家无法分享或保存注单截图。在社交平台传播场景中,注单截图是常见的拉新素材。
|
||||
|
||||
---
|
||||
|
||||
### 四、安全与合规相关缺失
|
||||
|
||||
#### 14. 负责任博彩 / 自我限额
|
||||
|
||||
PRD 18.1 节提到玩家每日投注上限和派彩上限由后台配置,但前台没有向玩家展示当前限额使用情况(如"今日已投注 X / 上限 Y"),也没有自我设置存款限额、投注限额、冷静期等负责任博彩工具。对于面向合法市场的平台,这通常是合规要求。
|
||||
|
||||
#### 15. 登录安全 - 缺少二次验证
|
||||
|
||||
玩家端登录只有密码 + 人机验证(RobotVerify),没有可选的短信验证码二次验证或 TOTP,安全性偏弱。
|
||||
|
||||
#### 16. 会话管理
|
||||
|
||||
玩家无法查看当前活跃会话(哪些设备在登录),也无法远程踢出其他设备的会话。
|
||||
|
||||
---
|
||||
|
||||
### 五、运营与增长相关缺失
|
||||
|
||||
#### 17. 邀请好友入口
|
||||
|
||||
后端已有完整的邀请码系统(生成邀请码、设置返水比例、邀请列表、撤销),但玩家前台没有"邀请好友"入口页面,无法查看自己的邀请码、邀请链接、邀请记录和奖励。
|
||||
|
||||
#### 18. 活动 / 优惠专区
|
||||
|
||||
没有活动页面或优惠专区。运营无法通过前台展示限时活动、新人奖励、充值优惠等信息。目前只能依赖 Banner 和公告,缺乏独立的活动落地页。
|
||||
|
||||
#### 19. VIP / 等级体系
|
||||
|
||||
没有玩家等级体系。成熟平台通常根据投注量、充值额等划分 VIP 等级,不同等级享受不同返水比例、专属客服、提款加速等权益。
|
||||
|
||||
---
|
||||
|
||||
### 六、数据辅助功能缺失
|
||||
|
||||
#### 20. 赛事数据 / 历史战绩
|
||||
|
||||
赛事详情页只展示盘口和赔率,没有历史交锋记录、球队近期战绩、联赛积分榜等辅助数据。这些数据可以帮助玩家做投注决策,也是提升用户粘性的手段。
|
||||
|
||||
#### 21. 投注统计 / 个人报表
|
||||
|
||||
虽然有 BetStatsPanel 显示注单数/赢/输/待结算的基础统计,但缺少更丰富的个人投注报表:按时间段统计、按玩法统计、盈亏趋势图、返水收入统计等。
|
||||
|
||||
---
|
||||
|
||||
### 七、其他体验优化建议
|
||||
|
||||
| 编号 | 功能 | 说明 |
|
||||
|---|---|---|
|
||||
| 22 | 赛事收藏 / 关注 | 玩家可以收藏感兴趣的赛事,快速筛选查看 |
|
||||
| 23 | 多赔率格式切换 | PRD 二期提到支持 Decimal/Malay/HK/Indo/American 格式,当前只有十进制 |
|
||||
| 24 | PWA / 添加到主屏幕 | 移动端 H5 支持 PWA 安装提示,提升留存 |
|
||||
| 25 | 网络状态提示 | 弱网或断网时显示明确提示,避免下注失败无感知 |
|
||||
| 26 | 暗色/亮色主题切换 | 当前只有深色主题,没有亮色模式 |
|
||||
| 27 | 系统公告弹窗 | 维护、重大赛事等场景的全屏弹窗通知,目前只有走马灯 |
|
||||
| 28 | 盘口排序偏好 | 允许玩家调整盘口分组默认排序(如把让球放在独赢前面) |
|
||||
|
||||
---
|
||||
|
||||
### 优先级建议
|
||||
|
||||
**P0 - 必须补齐(影响业务闭环):**
|
||||
- 提款功能(#4)
|
||||
- 邀请好友入口(#17)— 后端已就绪,只差前端页面
|
||||
- 注单结算通知(#5)
|
||||
|
||||
**P1 - 高优先级(影响核心体验):**
|
||||
- 今日赛事板块(#1)— 后端数据已有,前端未消费
|
||||
- 赛事搜索(#7)
|
||||
- 投注规则独立页面(#3)
|
||||
- 公告详情页(#2)
|
||||
- 负责任博彩 / 限额展示(#14)
|
||||
|
||||
**P2 - 中优先级(体验优化):**
|
||||
- 盘口封盘倒计时(#10)
|
||||
- 赔率变化实时标记(#9)
|
||||
- 注单筛选增强(#12)
|
||||
- 投注单清空确认(#8)
|
||||
- 充值/提款状态通知(#6)
|
||||
|
||||
**P3 - 低优先级(长期迭代):**
|
||||
- VIP 等级体系(#19)
|
||||
- 活动专区(#18)
|
||||
- 赛事数据/历史战绩(#20)
|
||||
- 多赔率格式(#23)
|
||||
- PWA(#24)
|
||||
@@ -16,7 +16,7 @@
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #001A33;
|
||||
background: linear-gradient(180deg, #15120A 0%, #0A0A16 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: #001A33;
|
||||
background: linear-gradient(180deg, #15120A 0%, #0A0A16 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -46,8 +46,8 @@
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.15);
|
||||
border-top-color: rgba(255, 255, 255, 0.72);
|
||||
border: 2px solid rgba(200, 168, 78, 0.3);
|
||||
border-top-color: #c8a84e;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@@ -4,16 +4,25 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
/** 本地 dev:与 apps/api/.env 的 PORT 对齐,供 player/admin Vite 代理使用 */
|
||||
export function resolveApiDevTarget() {
|
||||
if (process.env.VITE_DEV_API_TARGET) return process.env.VITE_DEV_API_TARGET;
|
||||
if (process.env.API_DEV_TARGET) return process.env.API_DEV_TARGET;
|
||||
/** 本地 dev 默认端口;3000 在部分 Windows(Hyper-V 保留 2960–3059)上会 EACCES */
|
||||
const DEFAULT_DEV_API_PORT = '3100';
|
||||
|
||||
let port = '3000';
|
||||
/** 与 apps/api/.env 的 PORT 对齐 */
|
||||
export function resolveApiDevPort() {
|
||||
if (process.env.PORT) return String(process.env.PORT);
|
||||
|
||||
let port = DEFAULT_DEV_API_PORT;
|
||||
const envPath = resolve(repoRoot, 'apps/api/.env');
|
||||
if (existsSync(envPath)) {
|
||||
const match = readFileSync(envPath, 'utf8').match(/^PORT=(\d+)\s*$/m);
|
||||
if (match) port = match[1];
|
||||
}
|
||||
return `http://localhost:${port}`;
|
||||
return port;
|
||||
}
|
||||
|
||||
/** 供 player/admin Vite 代理使用 */
|
||||
export function resolveApiDevTarget() {
|
||||
if (process.env.VITE_DEV_API_TARGET) return process.env.VITE_DEV_API_TARGET;
|
||||
if (process.env.API_DEV_TARGET) return process.env.API_DEV_TARGET;
|
||||
return `http://localhost:${resolveApiDevPort()}`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { execSync } from 'node:child_process';
|
||||
import { resolveApiDevPort } from './dev-api-target.mjs';
|
||||
|
||||
const port = process.argv[2] ?? '3000';
|
||||
const port = process.argv[2] ?? resolveApiDevPort();
|
||||
|
||||
function killWindows(portNum) {
|
||||
let out = '';
|
||||
|
||||
Reference in New Issue
Block a user