feat(player): 新增桌面端 UI 与响应式双端路由架构

- 将原移动端页面重命名为 Mobile*,新增 22 个 Desktop* 视图与 DesktopShell 布局体系
- 路由入口改为 Wrapper 视图,通过 useViewport(≥1024px)自动切换移动/桌面端
- 新增投注单面板、盘口弹层、联赛侧栏、数据表格等桌面组件及独立样式令牌
- 扩展 betSlip store、串关筛选、冠军盘、Toast/悬浮信箱等交互与三语 i18n
- 公告/消息 Hub 拆分移动与桌面实现;API 投注/钱包/充值记录支持 pageSize 分页
This commit is contained in:
2026-06-29 18:01:39 +08:00
parent 9c5e8d6f5c
commit 8a1ba0fd95
116 changed files with 22249 additions and 8693 deletions

View File

@@ -6,6 +6,8 @@ import { usePlayerProfile } from '../../composables/usePlayerProfile';
import { formatMoney, parseAmount } from '../../utils/localeDisplay';
import { teamFlagUrl } from '../../utils/teamFlag';
import BetSuccessOverlay from '../BetSuccessOverlay.vue';
import ConfirmDialog from '../ConfirmDialog.vue';
import { buildBetPlaceConfirmMessage } from '../../utils/betPlaceConfirmMessage';
export interface OutrightPick {
selectionId: string;
@@ -51,6 +53,8 @@ const balance = ref(0);
const successBalance = ref(0);
const successStake = ref(0);
const showSuccess = ref(false);
const showPlaceConfirm = ref(false);
const placeConfirmMessage = ref('');
const currentOdds = ref('');
const currentOddsVersion = ref('');
const oddsDelta = ref<OddsDelta | null>(null);
@@ -209,19 +213,54 @@ function setMaxStake() {
if (balance.value > 0) setStake(balance.value);
}
async function submit() {
if (!props.pick || stake.value <= 0) return;
function validateSubmit(): boolean {
if (!props.pick || stake.value <= 0) return false;
if (stake.value > balance.value) {
error.value = t('bet.outright_insufficient');
return;
return false;
}
if (hasSuspendedSelection.value) {
error.value = t('bet.odds_suspended');
return;
return false;
}
return true;
}
function onSubmitClick() {
if (!props.pick) return;
if (!validateSubmit()) return;
if (hasPendingOddsChanges.value) {
acceptPendingOdds();
}
placeConfirmMessage.value = buildBetPlaceConfirmMessage(t, {
mode: 'single',
items: [
{
selectionId: props.pick.selectionId,
oddsVersion: currentOddsVersion.value,
matchId: '',
matchName: props.pick.eventTitle,
marketId: '',
marketName: props.pick.eventTitle,
selectionName: `${props.pick.teamName} · ${props.pick.eventTitle}`,
odds: oddsNum.value,
marketType: 'OUTRIGHT',
lineValue: null,
allowSingle: true,
allowParlay: false,
},
],
totalStake: stake.value,
totalReturn: estReturn.value,
formatMoney: (amount) => formatMoney(amount, locale.value),
getStake: () => stake.value,
getOdds: () => oddsNum.value,
});
showPlaceConfirm.value = true;
}
async function executeSubmit() {
if (!props.pick || stake.value <= 0) return;
loading.value = true;
error.value = '';
@@ -251,6 +290,11 @@ async function submit() {
}
}
async function confirmSubmit() {
showPlaceConfirm.value = false;
await executeSubmit();
}
function formatOdds(odds: string) {
const n = parseFloat(odds);
return Number.isFinite(n) ? n.toFixed(2) : odds;
@@ -326,7 +370,7 @@ function formatOdds(odds: string) {
type="button"
class="btn-confirm btn-gold-outline"
:disabled="loading || stake <= 0 || hasSuspendedSelection"
@click="submit"
@click="onSubmitClick"
>
{{ submitButtonLabel }}
</button>
@@ -364,6 +408,16 @@ function formatOdds(odds: string) {
</div>
<BetSuccessOverlay :show="showSuccess" @done="showSuccess = false" />
<ConfirmDialog
v-model:visible="showPlaceConfirm"
:title="t('bet.place_confirm_title')"
:message="placeConfirmMessage"
:confirm-text="t('bet.place_bet')"
:cancel-text="t('common.cancel')"
:loading="loading"
@confirm="confirmSubmit"
/>
</template>
<style scoped>