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

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { useRouter, useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '../stores/auth';
import { useAppLocale } from '../composables/useAppLocale';
@@ -12,6 +12,7 @@ const { t } = useI18n();
const { initFromUser } = useAppLocale();
const auth = useAuthStore();
const router = useRouter();
const route = useRoute();
const captchaRef = ref<InstanceType<typeof RobotVerify> | null>(null);
const username = ref('');
const password = ref('');
@@ -29,13 +30,19 @@ async function submit() {
try {
await auth.login(username.value, password.value);
initFromUser(auth.user?.locale);
router.push('/');
const redirectTo = (route.query.redirect as string) || '/';
router.push(redirectTo);
} catch (e: unknown) {
error.value = (e as { response?: { data?: { error?: string } } })?.response?.data?.error || 'Login failed';
} finally {
loading.value = false;
}
}
function continueBrowsing() {
const redirect = (route.query.redirect as string) || '/';
router.push(redirect);
}
</script>
<template>
@@ -53,6 +60,9 @@ async function submit() {
<button type="submit" class="btn-login btn-gold-outline" :disabled="loading">
{{ t('auth.login') }}
</button>
<button type="button" class="btn-skip" @click="continueBrowsing">
{{ t('auth.continue_browsing') }}
</button>
</form>
</div>
</template>
@@ -111,6 +121,21 @@ label {
cursor: not-allowed;
}
.btn-skip {
margin-top: 2px;
padding: 8px 14px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.55);
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.btn-skip:active {
color: rgba(255, 255, 255, 0.75);
}
.error {
color: var(--danger);
font-size: 13px;

View File

@@ -5,6 +5,7 @@ import { useI18n } from 'vue-i18n';
import api from '../api';
import { formatMoney } from '../utils/localeDisplay';
import { useBetSlipStore } from '../stores/betSlip';
import { useAuthStore } from '../stores/auth';
import TeamEmblem from '../components/TeamEmblem.vue';
import { DETAIL_MARKET_TYPES, MARKET_I18N_KEY } from '../utils/marketCatalog';
import MatchBetGuide from '../components/match-detail/MatchBetGuide.vue';
@@ -26,6 +27,11 @@ const route = useRoute();
const router = useRouter();
const { t, locale } = useI18n();
const slip = useBetSlipStore();
const auth = useAuthStore();
function goLogin() {
auth.showLoginPrompt(route.fullPath);
}
interface Market {
id: string;
@@ -96,7 +102,7 @@ const myBets = ref<MyBet[]>([]);
const loadingMyBets = ref(false);
async function loadMyBets() {
if (!match.value) return;
if (!match.value || !auth.token) return;
loadingMyBets.value = true;
try {
const { data } = await api.get('/player/bets?page=1');
@@ -241,6 +247,10 @@ async function confirmCorrectScoreBets() {
async function placeCorrectScoreBets(marketType: string) {
if (!bettingOpen.value) return;
if (!auth.token) {
goLogin();
return;
}
const market = marketsByType.value.get(marketType);
if (!market || !match.value) return;
const entries = market.selections.filter((s) => (correctScoreStakes.value[s.id] ?? 0) > 0);
@@ -325,6 +335,10 @@ function onPickSelection(selId: string, marketType: string) {
}
function openBetSlipDrawer() {
if (!auth.token) {
goLogin();
return;
}
slip.openDrawer();
}

View File

@@ -80,7 +80,7 @@ async function changeLocale(code: string) {
function logout() {
auth.logout();
router.push('/login');
router.push('/');
}
</script>