feat: 前台匿名浏览、登录引导、客服入口与返水增强

前台:
- 未登录可浏览首页/赛事/赔率,下注等操作弹出登录引导(去登录/继续浏览)
- 顶部新增客服入口与 iframe 弹窗
- 登录页支持暂不登录返回浏览

API:
- 首页/赛事/冠军盘接口改为公开访问,支持 X-Locale 头
- JWT 守卫支持可选认证

返水:
- 注单新增 is_cashbacked 字段,发放时自动标记
- 预览展示玩家余额,明确平台直发不从代理扣款
- 后台注单列表与玩家历史展示回水状态

其他:
- 串关禁止同场重复选号(SAME_MATCH)
- 补充结算资金流分析文档

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 09:36:44 +08:00
parent 785fa4416d
commit 844727c82e
35 changed files with 1007 additions and 49 deletions

View File

@@ -10,6 +10,7 @@ import LocaleSwitcher from '../components/LocaleSwitcher.vue';
import { useAppLocale } from '../composables/useAppLocale';
import AnnouncementMarquee from '../components/AnnouncementMarquee.vue';
import BottomNavIcon from '../components/BottomNavIcon.vue';
import CustomerServiceModal from '../components/CustomerServiceModal.vue';
import { computed, onMounted, ref, watch } from 'vue';
import { usePlayerHome } from '../composables/usePlayerHome';
import { usePlayerProfile } from '../composables/usePlayerProfile';
@@ -44,6 +45,7 @@ const { announcements, load: loadPlayerHome } = usePlayerHome();
const { loadProfile } = usePlayerProfile();
const mainRef = ref<HTMLElement | null>(null);
const tabScrollTops = new Map<string, number>();
const customerServiceOpen = ref(false);
watch(locale, (next, prev) => {
if (prev && next !== prev) void loadPlayerHome(true);
@@ -66,8 +68,10 @@ onMounted(() => {
watch(
() => auth.token,
(token) => {
// 首页数据(公告、热门赛事)对所有人公开,始终加载
void loadPlayerHome();
// 个人资料仅登录用户需要
if (token) {
void loadPlayerHome();
void loadProfile();
}
},
@@ -80,9 +84,34 @@ watch(
<header v-if="showHeader" class="header">
<img src="/logo.png" alt="TheBet365" class="logo" />
<div class="header-actions">
<button
type="button"
class="support-btn"
:aria-label="t('support.open')"
@click="customerServiceOpen = true"
>
<svg class="support-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 class="support-label">{{ t('support.short') }}</span>
</button>
<LocaleSwitcher />
<CashBalanceChip v-if="auth.user" />
<UserAvatarMenu v-if="auth.user" />
<template v-if="auth.user">
<CashBalanceChip />
<UserAvatarMenu />
</template>
<button v-else type="button" class="login-btn" @click="auth.showLoginPrompt(route.fullPath)">
{{ t('auth.login') }}
</button>
</div>
</header>
@@ -127,6 +156,7 @@ watch(
</nav>
<BetSlipDrawer v-model="slip.drawerOpen" />
<CustomerServiceModal v-model="customerServiceOpen" />
</div>
</template>
@@ -168,6 +198,55 @@ watch(
.header-actions :deep(.avatar-btn) {
width: var(--header-chip-h);
}
.support-btn {
display: inline-flex;
align-items: center;
gap: 4px;
height: var(--header-chip-h, 36px);
padding: 0 10px;
border-radius: 6px;
border: 1px solid var(--border-gold-soft, rgba(200, 168, 78, 0.25));
background: rgba(200, 168, 78, 0.08);
color: var(--primary-light, #c8a84e);
font-size: 12px;
font-weight: 700;
cursor: pointer;
transition: background 0.2s;
}
.support-btn:active {
background: rgba(200, 168, 78, 0.16);
}
.support-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.support-label {
max-width: 48px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.login-btn {
padding: 6px 16px;
border-radius: 6px;
border: 1px solid var(--primary, #c8a84e);
background: transparent;
color: var(--primary-light, #c8a84e);
font-size: 13px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.login-btn:active {
background: rgba(200, 168, 78, 0.15);
}
.announce-strip {
flex-shrink: 0;
z-index: 105;