+
+
@@ -216,23 +262,26 @@ watch(
diff --git a/apps/player/src/views/AnnouncementListView.vue b/apps/player/src/views/AnnouncementListView.vue
index 84f7c58..26fa6db 100644
--- a/apps/player/src/views/AnnouncementListView.vue
+++ b/apps/player/src/views/AnnouncementListView.vue
@@ -1,179 +1,15 @@
-
-
-
-
-
-
-
-
-
{{ t('announcements.empty') }}
-
-
-
-
-
- {{ itemTitle(item) }}
- {{ itemPreview(item) }}
- {{ formatDate(item.createdAt) }}
-
- ›
-
-
-
+
-
-
diff --git a/apps/player/src/views/AnnouncementsHubView.vue b/apps/player/src/views/AnnouncementsHubView.vue
new file mode 100644
index 0000000..11c740a
--- /dev/null
+++ b/apps/player/src/views/AnnouncementsHubView.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/BetDetailView.vue b/apps/player/src/views/BetDetailView.vue
index 53e0b16..436662d 100644
--- a/apps/player/src/views/BetDetailView.vue
+++ b/apps/player/src/views/BetDetailView.vue
@@ -1,629 +1,10 @@
-
-
-
-
-
-
-
-
- ‹ {{ t('history.back') }}
-
-
-
-
-
-
{{ t('history.not_found') }}
-
-
-
-
- {{ statusLabel }}
- {{ returnAmount }}
-
- {{ statusKey === 'pending' ? t('history.est_return') : t('history.return') }}
-
-
-
-
-
-
-
- {{ bet.isParlay ? t('history.parlay_league') : (bet.leagueName || t('history.league_default')) }}
-
- {{ placedDateTime }}
-
- {{ matchTitle }}
-
-
-
-
-
-
- {{ t('history.my_pick') }}
- {{ myPick }}
-
-
-
-
- {{ t('history.ft') }}
- {{ bet.matchScore.ft }}
-
-
- {{ t('history.ht') }}
- {{ bet.matchScore.ht }}
-
-
-
{{ t('history.awaiting_result') }}
-
-
-
-
-
- {{ t('history.legs') }}
-
-
-
-
{{ i + 1 }}
-
-
{{ leg.matchTitle }}
-
- {{ leg.marketLabel }}: {{ translateSel(leg.selectionName) }}
-
-
- {{ t('history.ft') }} {{ leg.score.ft }}
- {{ t('history.ht') }} {{ leg.score.ht }}
-
-
-
-
- {{ formatOdds(leg.odds) }}
-
- {{ t(`history.status_${legStatusKey(leg.resultStatus)}`) }}
-
-
-
-
-
-
-
-
- {{ t('history.summary') }}
-
-
- {{ t('history.stake') }}
- {{ formatMoney(bet.stake, locale) }}
-
-
- {{ t('history.odds') }}
- {{ formatOdds(bet.totalOdds) }}
-
-
- {{ statusKey === 'pending' ? t('history.est_return') : t('history.return') }}
-
- {{ returnAmount }}
-
-
-
- {{ t('history.bet_no') }}
- {{ bet.betNo }}
-
-
-
-
-
+
+
-
-
diff --git a/apps/player/src/views/CashbackRecordsView.vue b/apps/player/src/views/CashbackRecordsView.vue
index 17d67fa..f310d3d 100644
--- a/apps/player/src/views/CashbackRecordsView.vue
+++ b/apps/player/src/views/CashbackRecordsView.vue
@@ -1,411 +1,10 @@
-
-
-
-
- ‹ {{ t('history.back') }}
-
-
-
-
-
-
-
-
-
- {{ t('bet.loading') }}
-
-
-
-
- {{ formatMoney(totalAmount, locale) }}
-
- {{ t('cashback.total_received') }}
-
- · {{ t('cashback.record_count', { n: items.length }) }}
-
-
-
-
- {{ t('cashback.ledger_hint') }}
-
- {{ t('cashback.list_title') }}
-
-
-
-
-
- {{ formatMoney(row.amount, locale) }}
- {{ formatPeriod(row.periodStart, row.periodEnd) }}
-
-
{{ formatRate(row.rate) }}
-
-
- {{ t('cashback.effective_stake') }} {{ formatMoneyCompact(row.effectiveStake, locale) }}
- {{ t('cashback.bet_count', { n: row.betCount }) }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('common.no_more') }}
-
-
-
-
{{ t('cashback.empty') }}
-
{{ t('cashback.empty_hint') }}
-
-
-
+
+
-
-
diff --git a/apps/player/src/views/FootballView.vue b/apps/player/src/views/FootballView.vue
index 442ec24..ed3cc1f 100644
--- a/apps/player/src/views/FootballView.vue
+++ b/apps/player/src/views/FootballView.vue
@@ -1,769 +1,12 @@
-
-
-
-
-
-
-
- ⚽
- {{ t('bet.tab_matches') }}
-
-
- 🏆
- {{ t('bet.tab_outright') }}
-
-
-
-
-
- ⌕
-
-
-
-
-
-
-
- {{ selectedLeagueName || t('bet.filter_leagues_all') }}
- ▼
-
-
-
-
-
-
-
-
-
- {{ t('bet.filter_status_all') }}
-
-
- {{ t('bet.tab_today') }}
-
-
- {{ t('bet.tab_early') }}
-
-
-
-
-
-
- {{ t('bet.filter_status_open') }}
-
-
-
-
-
-
-
-
-
-
-
- {{ t('search.results_count', { count: sortedFilteredMatches.length }) }}
-
-
-
- {{ normalizeLeagueName(match.leagueName) }}
-
-
-
-
-
-
-
-
-
-
{{ isSearchActive ? t('search.no_results') : t('bet.no_matches') }}
-
-
-
-
-
-
-
-
-
-
+
+
-
-
diff --git a/apps/player/src/views/ForgotPasswordWrapperView.vue b/apps/player/src/views/ForgotPasswordWrapperView.vue
new file mode 100644
index 0000000..aaadb56
--- /dev/null
+++ b/apps/player/src/views/ForgotPasswordWrapperView.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/apps/player/src/views/HomeView.vue b/apps/player/src/views/HomeView.vue
index 6b907ab..95fe20b 100644
--- a/apps/player/src/views/HomeView.vue
+++ b/apps/player/src/views/HomeView.vue
@@ -1,940 +1,12 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ match.homeTeamName }}
-
-
-
VS
-
-
-
- {{ match.awayTeamName }}
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('nav.bet') }}
- {{ t('home.bento_desc') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('recharge.title') }}
-
-
-
-
-
-
-
-
-
- {{ t('home.view_bet_history') }}
-
-
-
-
-
-
-
-
{{ match.homeTeamName }} vs {{ match.awayTeamName }}
-
{{ formatKickoff(match.startTime) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('home.view_all_matches') }} ›
-
-
-
-
-
-
{{ emptyMessage }}
-
-
+
+
-
-
diff --git a/apps/player/src/views/InboxHubView.vue b/apps/player/src/views/InboxHubView.vue
index 52600d5..135acdc 100644
--- a/apps/player/src/views/InboxHubView.vue
+++ b/apps/player/src/views/InboxHubView.vue
@@ -1,299 +1,10 @@
-
-
-
-
-
-
- {{ t('inbox_hub.tab_messages') }}
-
- {{ unreadCount > 99 ? '99+' : unreadCount }}
-
-
-
- {{ t('inbox_hub.tab_support') }}
-
-
-
-
-
-
- {{ t('messages.mark_all_read') }}
-
-
- {{ t('messages.delete_all') }}
-
-
-
-
-
-
-
-
+
-
-
diff --git a/apps/player/src/views/LoginWrapperView.vue b/apps/player/src/views/LoginWrapperView.vue
new file mode 100644
index 0000000..0ddd32e
--- /dev/null
+++ b/apps/player/src/views/LoginWrapperView.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/apps/player/src/views/MatchDetailView.vue b/apps/player/src/views/MatchDetailView.vue
index 752a301..25530f9 100644
--- a/apps/player/src/views/MatchDetailView.vue
+++ b/apps/player/src/views/MatchDetailView.vue
@@ -1,993 +1,12 @@
-
-
-
-
-
-
-
-
-
-
-
-
- {{ phaseLabel }}
- {{ phaseLabel }}
-
-
-
-
- {{ match.homeTeamName }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ match.awayTeamName }}
-
-
-
- {{ t('bet.kickoff_time') }}{{ kickoff }}
- {{ liveScoreText }}
-
-
-
-
-
{{ t('bet.result_stats_title') }}
- {{ match.homeTeamName }} / {{ match.awayTeamName }}
-
-
-
- {{ item.label }}
- {{ item.value }}
-
-
-
-
-
-
- {{ t('history.my_bets') || '我的投注' }}
-
-
-
-
-
-
-
-
- {{ phaseLabel }}
- {{ phaseLabel }}
-
- {{ marketStatusLabel(market.status) }}
-
-
-
-
-
-
-
-
-
+
+
-
-
diff --git a/apps/player/src/views/MessageDetailView.vue b/apps/player/src/views/MessageDetailView.vue
index 44eea2a..ef59a90 100644
--- a/apps/player/src/views/MessageDetailView.vue
+++ b/apps/player/src/views/MessageDetailView.vue
@@ -1,396 +1,10 @@
-
-
-
-
-
-
-
-
-
{{ t('messages.not_found') }}
-
{{ t('messages.view_all') }}
-
-
-
-
- {{ message.isRead ? t('messages.status_read') : t('messages.status_unread') }}
-
- {{ formatDate(message.createdAt) }}
- {{ messageTitle(message) }}
-
- {{ messageBody(message) }}
-
-
-
{{ t('messages.reject_reason') }}
-
{{ depositPayload.rejectReason }}
-
-
-
- {{ t('messages.content_promo_view') }}
-
-
-
- {{ t('messages.view_recharge_history') }}
-
-
-
-
-
+
-
-
diff --git a/apps/player/src/views/MessagesHubView.vue b/apps/player/src/views/MessagesHubView.vue
new file mode 100644
index 0000000..99bb078
--- /dev/null
+++ b/apps/player/src/views/MessagesHubView.vue
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileAnnouncementDetailView.vue b/apps/player/src/views/MobileAnnouncementDetailView.vue
new file mode 100644
index 0000000..9e12397
--- /dev/null
+++ b/apps/player/src/views/MobileAnnouncementDetailView.vue
@@ -0,0 +1,323 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('announcements.not_found') }}
+
{{ t('announcements.view_all') }}
+
+
+
+
+
+
+
+
+
{{ formatDate(item.createdAt) }}
+
{{ itemTitle(item) }}
+
+
+
+
{{ t('announcements.related_link') }}
+
{{ linkTarget }}
+
+ {{ t('announcements.go_link') }}
+
+
+ {{ t('announcements.open_link') }}
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileAnnouncementListView.vue b/apps/player/src/views/MobileAnnouncementListView.vue
new file mode 100644
index 0000000..84f7c58
--- /dev/null
+++ b/apps/player/src/views/MobileAnnouncementListView.vue
@@ -0,0 +1,179 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('announcements.empty') }}
+
+
+
+
+
+ {{ itemTitle(item) }}
+ {{ itemPreview(item) }}
+ {{ formatDate(item.createdAt) }}
+
+ ›
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileBetDetailView.vue b/apps/player/src/views/MobileBetDetailView.vue
new file mode 100644
index 0000000..53e0b16
--- /dev/null
+++ b/apps/player/src/views/MobileBetDetailView.vue
@@ -0,0 +1,629 @@
+
+
+
+
+
+
+
+
+
+
+ ‹ {{ t('history.back') }}
+
+
+
+
+
+
{{ t('history.not_found') }}
+
+
+
+
+ {{ statusLabel }}
+ {{ returnAmount }}
+
+ {{ statusKey === 'pending' ? t('history.est_return') : t('history.return') }}
+
+
+
+
+
+
+
+ {{ bet.isParlay ? t('history.parlay_league') : (bet.leagueName || t('history.league_default')) }}
+
+ {{ placedDateTime }}
+
+ {{ matchTitle }}
+
+
+
+
+
+
+ {{ t('history.my_pick') }}
+ {{ myPick }}
+
+
+
+
+ {{ t('history.ft') }}
+ {{ bet.matchScore.ft }}
+
+
+ {{ t('history.ht') }}
+ {{ bet.matchScore.ht }}
+
+
+
{{ t('history.awaiting_result') }}
+
+
+
+
+
+ {{ t('history.legs') }}
+
+
+
+
{{ i + 1 }}
+
+
{{ leg.matchTitle }}
+
+ {{ leg.marketLabel }}: {{ translateSel(leg.selectionName) }}
+
+
+ {{ t('history.ft') }} {{ leg.score.ft }}
+ {{ t('history.ht') }} {{ leg.score.ht }}
+
+
+
+
+ {{ formatOdds(leg.odds) }}
+
+ {{ t(`history.status_${legStatusKey(leg.resultStatus)}`) }}
+
+
+
+
+
+
+
+
+ {{ t('history.summary') }}
+
+
+ {{ t('history.stake') }}
+ {{ formatMoney(bet.stake, locale) }}
+
+
+ {{ t('history.odds') }}
+ {{ formatOdds(bet.totalOdds) }}
+
+
+ {{ statusKey === 'pending' ? t('history.est_return') : t('history.return') }}
+
+ {{ returnAmount }}
+
+
+
+ {{ t('history.bet_no') }}
+ {{ bet.betNo }}
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileCashbackRecordsView.vue b/apps/player/src/views/MobileCashbackRecordsView.vue
new file mode 100644
index 0000000..17d67fa
--- /dev/null
+++ b/apps/player/src/views/MobileCashbackRecordsView.vue
@@ -0,0 +1,411 @@
+
+
+
+
+
+
+ ‹ {{ t('history.back') }}
+
+
+
+
+
+
+
+
+
+ {{ t('bet.loading') }}
+
+
+
+
+ {{ formatMoney(totalAmount, locale) }}
+
+ {{ t('cashback.total_received') }}
+
+ · {{ t('cashback.record_count', { n: items.length }) }}
+
+
+
+
+ {{ t('cashback.ledger_hint') }}
+
+ {{ t('cashback.list_title') }}
+
+
+
+
+
+ {{ formatMoney(row.amount, locale) }}
+ {{ formatPeriod(row.periodStart, row.periodEnd) }}
+
+
{{ formatRate(row.rate) }}
+
+
+ {{ t('cashback.effective_stake') }} {{ formatMoneyCompact(row.effectiveStake, locale) }}
+ {{ t('cashback.bet_count', { n: row.betCount }) }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('common.no_more') }}
+
+
+
+
{{ t('cashback.empty') }}
+
{{ t('cashback.empty_hint') }}
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileFootballView.vue b/apps/player/src/views/MobileFootballView.vue
new file mode 100644
index 0000000..442ec24
--- /dev/null
+++ b/apps/player/src/views/MobileFootballView.vue
@@ -0,0 +1,769 @@
+
+
+
+
+
+
+
+
+
+
+ ⚽
+ {{ t('bet.tab_matches') }}
+
+
+ 🏆
+ {{ t('bet.tab_outright') }}
+
+
+
+
+
+ ⌕
+
+
+
+
+
+
+
+ {{ selectedLeagueName || t('bet.filter_leagues_all') }}
+ ▼
+
+
+
+
+
+
+
+
+
+ {{ t('bet.filter_status_all') }}
+
+
+ {{ t('bet.tab_today') }}
+
+
+ {{ t('bet.tab_early') }}
+
+
+
+
+
+
+ {{ t('bet.filter_status_open') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('search.results_count', { count: sortedFilteredMatches.length }) }}
+
+
+
+ {{ normalizeLeagueName(match.leagueName) }}
+
+
+
+
+
+
+
+
+
+
{{ isSearchActive ? t('search.no_results') : t('bet.no_matches') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileHomeView.vue b/apps/player/src/views/MobileHomeView.vue
new file mode 100644
index 0000000..6b907ab
--- /dev/null
+++ b/apps/player/src/views/MobileHomeView.vue
@@ -0,0 +1,940 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ match.homeTeamName }}
+
+
+
VS
+
+
+
+ {{ match.awayTeamName }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('nav.bet') }}
+ {{ t('home.bento_desc') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('recharge.title') }}
+
+
+
+
+
+
+
+
+
+ {{ t('home.view_bet_history') }}
+
+
+
+
+
+
+
+
{{ match.homeTeamName }} vs {{ match.awayTeamName }}
+
{{ formatKickoff(match.startTime) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('home.view_all_matches') }} ›
+
+
+
+
+
+
{{ emptyMessage }}
+
+
+
+
+
diff --git a/apps/player/src/views/MobileInboxHubView.vue b/apps/player/src/views/MobileInboxHubView.vue
new file mode 100644
index 0000000..52600d5
--- /dev/null
+++ b/apps/player/src/views/MobileInboxHubView.vue
@@ -0,0 +1,299 @@
+
+
+
+
+
+
+
+
+
+ {{ t('inbox_hub.tab_messages') }}
+
+ {{ unreadCount > 99 ? '99+' : unreadCount }}
+
+
+
+ {{ t('inbox_hub.tab_support') }}
+
+
+
+
+
+
+ {{ t('messages.mark_all_read') }}
+
+
+ {{ t('messages.delete_all') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileMatchDetailView.vue b/apps/player/src/views/MobileMatchDetailView.vue
new file mode 100644
index 0000000..752a301
--- /dev/null
+++ b/apps/player/src/views/MobileMatchDetailView.vue
@@ -0,0 +1,993 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ phaseLabel }}
+ {{ phaseLabel }}
+
+
+
+
+ {{ match.homeTeamName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ match.awayTeamName }}
+
+
+
+ {{ t('bet.kickoff_time') }}{{ kickoff }}
+ {{ liveScoreText }}
+
+
+
+
+
{{ t('bet.result_stats_title') }}
+ {{ match.homeTeamName }} / {{ match.awayTeamName }}
+
+
+
+ {{ item.label }}
+ {{ item.value }}
+
+
+
+
+
+
+ {{ t('history.my_bets') || '我的投注' }}
+
+
+
+
+
+
+
+
+ {{ phaseLabel }}
+ {{ phaseLabel }}
+
+ {{ marketStatusLabel(market.status) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileMessageDetailView.vue b/apps/player/src/views/MobileMessageDetailView.vue
new file mode 100644
index 0000000..44eea2a
--- /dev/null
+++ b/apps/player/src/views/MobileMessageDetailView.vue
@@ -0,0 +1,396 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('messages.not_found') }}
+
{{ t('messages.view_all') }}
+
+
+
+
+ {{ message.isRead ? t('messages.status_read') : t('messages.status_unread') }}
+
+ {{ formatDate(message.createdAt) }}
+ {{ messageTitle(message) }}
+
+ {{ messageBody(message) }}
+
+
+
{{ t('messages.reject_reason') }}
+
{{ depositPayload.rejectReason }}
+
+
+
+ {{ t('messages.content_promo_view') }}
+
+
+
+ {{ t('messages.view_recharge_history') }}
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileMyBetsView.vue b/apps/player/src/views/MobileMyBetsView.vue
new file mode 100644
index 0000000..f0930cc
--- /dev/null
+++ b/apps/player/src/views/MobileMyBetsView.vue
@@ -0,0 +1,225 @@
+
+
+
+
+
+
+
+
+
+
+ {{ t('bet.loading') }}
+
+
+
+
+
+
+
+ {{ t(f.label) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('common.no_more') }}
+
+
+
+ {{ t('history.empty') }}
+
+
+
+
+
diff --git a/apps/player/src/views/MobileOutrightDetailView.vue b/apps/player/src/views/MobileOutrightDetailView.vue
new file mode 100644
index 0000000..8e55099
--- /dev/null
+++ b/apps/player/src/views/MobileOutrightDetailView.vue
@@ -0,0 +1,166 @@
+
+
+
+
+
+ ← {{ t('bet.back') }}
+ {{ headTitle || t('bet.tab_outright') }}
+
+
+
+
+
+
+
+ {{ t('bet.outright_settled_hint') }}
+
+
+
+
+
+
+
{{ loadError || t('bet.no_outright') }}
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileProfileEditView.vue b/apps/player/src/views/MobileProfileEditView.vue
new file mode 100644
index 0000000..895f55a
--- /dev/null
+++ b/apps/player/src/views/MobileProfileEditView.vue
@@ -0,0 +1,564 @@
+
+
+
+
+
+
+
+
+
+ ← {{ t('profile.back') }}
+ {{ t('profile.edit') }}
+
+
+
+
+
+
+
+ {{ t('profile.avatar_change') }}
+
+
+
+
+
+
+
+
{{ message }}
+
{{ error }}
+
+
+
+
diff --git a/apps/player/src/views/MobileProfileView.vue b/apps/player/src/views/MobileProfileView.vue
new file mode 100644
index 0000000..8e4c194
--- /dev/null
+++ b/apps/player/src/views/MobileProfileView.vue
@@ -0,0 +1,745 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('common.load_failed') }}
+
{{ t('common.retry') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ THEBET365
+ MEMBER
+
+
+
+ +
+ {{ t('recharge.title') }}
+
+
+
+
+
{{ t('wallet.balance') }}
+
{{ balanceDisplay }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('wallet.view_all') }}
+
+
+
+
+
+ {{ t('recharge.title') }}
+
+
+
+
+
+ {{ t('wallet.view_cashbacks') }}
+
+
+
+
+
+ {{ t('recharge.history_title') }}
+
+
+
+
+
+
+
+
+
+
+ {{ t('profile.edit') }}
+
+ ›
+
+
+
+
+
+
+
+
+
+ {{ t('profile.rules_title') }}
+
+ ›
+
+
+
{{ t('profile.rules_p1') }}
+
{{ t('profile.rules_p2') }}
+
{{ t('profile.rules_p3') }}
+
{{ t('profile.rules_p4') }}
+
{{ t('profile.rules_p5') }}
+
+
+
+
+
+
+
+
+
+
{{ t('profile.language') }}
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+ {{ t('auth.logout') }}
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileRechargeHistoryView.vue b/apps/player/src/views/MobileRechargeHistoryView.vue
new file mode 100644
index 0000000..085a779
--- /dev/null
+++ b/apps/player/src/views/MobileRechargeHistoryView.vue
@@ -0,0 +1,843 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('recharge.no_orders') }}
+
+
+
+
+
+
{{ formatMoney(order.amount, locale) }}
+
+ {{ t('recharge.credited') }}: {{ formatMoney(order.approvedAmount, locale) }}
+
+
+ {{ order.paymentMethodName || '-' }}
+
+
+
+ {{ t('recharge.apply_time') }}
+ {{ formatOrderTime(order.createdAt) }}
+
+
+ {{ t('recharge.review_time') }}
+ {{ formatOrderTime(order.reviewedAt) }}
+
+
+
+ {{ orderNoteLine(order) }}
+
+
+
+ {{ t('recharge.audit_summary', { count: auditStepCount(order) }) }}
+
+ {{ t('recharge.view_detail') }} ›
+
+
+ {{ t('recharge.view_detail') }} ›
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('common.no_more') }}
+
+
+
+
+
+
+
✕
+
+
{{ t('recharge.order_detail') }}
+
+
+
+
+ {{ selectedOrder.methodType }}
+
+
+ {{ statusLabel(selectedOrder.status) }}
+
+
+
{{ formatMoney(selectedOrder.amount, locale) }}
+
+ {{ t('recharge.credited') }}: {{ formatMoney(selectedOrder.approvedAmount, locale) }}
+
+
{{ selectedOrder.paymentMethodName || '-' }}
+
+ {{ t('recharge.apply_time') }}
+ {{ formatOrderTime(selectedOrder.createdAt) }}
+
+
+ {{ t('recharge.review_time') }}
+ {{ formatOrderTime(selectedOrder.reviewedAt) }}
+
+
+ {{ orderNoteLine(selectedOrder) }}
+
+
+
+
+
{{ t('recharge.audit_title') }}
+
+
+
+
+
+
+
+
+ {{ auditActionLabel(entry.log.action) }}
+ {{ formatAuditTime(entry.log.createdAt) }}
+
+
{{ entry.actor }}
+
+ {{ t('recharge.audit_credited') }} {{ formatMoney(entry.log.approvedAmount, locale) }}
+
+
+ {{ t('recharge.reject_reason') }}
+ {{ entry.remark.text }}
+
+
+ {{ auditNoteDisplayText(entry.remark) }}
+
+
+
+
+
+
+
+ {{ t('recharge.reapply') }}
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileRechargeView.vue b/apps/player/src/views/MobileRechargeView.vue
new file mode 100644
index 0000000..74e2b2a
--- /dev/null
+++ b/apps/player/src/views/MobileRechargeView.vue
@@ -0,0 +1,683 @@
+
+
+
+
+
+
+
+
+
+
+
+
✓
+
{{ t('recharge.submitted') }}
+
{{ orderNo }}
+
{{ t('recharge.pending_review') }}
+
{{ t('recharge.back_to_history') }}
+
{{ t('recharge.new_recharge') }}
+
+
+
+ {{ t('recharge.reapply_hint') }}
+
+ {{ t('recharge.bank_transfer') }}
+ USDT
+
+
+
+
+ {{ m.displayName || m.bankName || m.usdtAddress }}
+ {{ m.accountHolder }}
+
+
+ {{ t('recharge.no_methods') }}
+
+
+
+
+
+
+
+
{{ selectedMethod.bankName }}
+
+
+
{{ formatCardNumber(selectedMethod.accountNumber) }}
+
+
+
+
+
+
+
+
+
+ {{ t('recharge.account_holder') }}
+ {{ selectedMethod.accountHolder }}
+
+
DEBIT
+
+
+
+
+
+
+
+
{{ t('recharge.usdt_address') }}
+
+ {{ selectedMethod.usdtAddress }}
+
+
+
+
+
+
+
+
+
+
+ {{ t('recharge.amount_label') }}
+
+
+
+
+
+
+ {{ t('recharge.submitting') }}...
+ {{ t('recharge.submit') }}
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileWalletDetailView.vue b/apps/player/src/views/MobileWalletDetailView.vue
new file mode 100644
index 0000000..a5f680b
--- /dev/null
+++ b/apps/player/src/views/MobileWalletDetailView.vue
@@ -0,0 +1,391 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('bet.loading') }}
+
+
+
+
+
+
+
+ {{ t(f.label) }}
+
+
+
+
+
+
+ {{ txLabel(tx) }}
+ {{ txSubtitle(tx) }}
+ {{ new Date(tx.createdAt).toLocaleString() }}
+
+
+
+ {{ formatMoney(tx.amount, locale) }}
+
+ ›
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('common.no_more') }}
+
+
+
+ {{ t('wallet.no_records') }}
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileWalletTransactionDetailView.vue b/apps/player/src/views/MobileWalletTransactionDetailView.vue
new file mode 100644
index 0000000..c2c5eb1
--- /dev/null
+++ b/apps/player/src/views/MobileWalletTransactionDetailView.vue
@@ -0,0 +1,388 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('wallet.detail_not_found') }}
+
+
+
+ {{ displayTypeLabel }}
+ {{ summaryText }}
+ {{ formatMoney(tx.amount, locale) }}
+ {{ formattedTime }}
+
+
+
+ {{ t('wallet.detail_summary') }}
+
+
+ {{ t('wallet.detail_amount') }}
+ {{ formatMoney(tx.amount, locale) }}
+
+
+ {{ t('wallet.detail_balance_before') }}
+ {{ formatMoney(tx.balanceBefore, locale) }}
+
+
+ {{ t('wallet.detail_balance_after') }}
+ {{ formatMoney(tx.balanceAfter, locale) }}
+
+
+
+ {{ t('wallet.detail_frozen_before') }}
+ {{ formatMoney(tx.frozenBefore, locale) }}
+
+
+ {{ t('wallet.detail_frozen_after') }}
+ {{ formatMoney(tx.frozenAfter, locale) }}
+
+
+
+
+
+
+ {{ t('wallet.detail_reference') }}
+
+
+ {{ t('wallet.detail_reference_type') }}
+ {{ referenceLabel }}
+
+
+ {{ t('wallet.detail_reference_id') }}
+ {{ tx.referenceId }}
+
+
+ {{ t('wallet.detail_remark') }}
+
+
+
+
+ {{ t('wallet.detail_bet_link') }} · {{ tx.betNo }}
+
+
+ {{ t('wallet.detail_cashback_link') }} · {{ cashbackBatchNo }}
+
+
+
+
+ {{ t('wallet.detail_tx_id') }}
+ {{ tx.transactionId }}
+
+
+
+
+
+
diff --git a/apps/player/src/views/MobileWalletView.vue b/apps/player/src/views/MobileWalletView.vue
new file mode 100644
index 0000000..8df0075
--- /dev/null
+++ b/apps/player/src/views/MobileWalletView.vue
@@ -0,0 +1,588 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('bet.loading') }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('wallet.view_cashbacks') }}
+
+
+
+
+
+ {{ t('wallet.view_all') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ txLabel(tx) }}
+
+ {{ txSubtitle(tx) }}
+
+ {{ new Date(tx.createdAt).toLocaleString() }}
+
+
+
+
+
+
+
+ {{ formatMoney(tx.amount, locale) }}
+
+
+
+ ›
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('wallet.no_records') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/MyBetsView.vue b/apps/player/src/views/MyBetsView.vue
index f0930cc..84273e6 100644
--- a/apps/player/src/views/MyBetsView.vue
+++ b/apps/player/src/views/MyBetsView.vue
@@ -1,225 +1,10 @@
-
-
-
-
-
-
-
-
- {{ t('bet.loading') }}
-
-
-
-
-
-
-
- {{ t(f.label) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('common.no_more') }}
-
-
-
- {{ t('history.empty') }}
-
-
+
+
-
-
diff --git a/apps/player/src/views/OutrightDetailView.vue b/apps/player/src/views/OutrightDetailView.vue
new file mode 100644
index 0000000..3208a44
--- /dev/null
+++ b/apps/player/src/views/OutrightDetailView.vue
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/apps/player/src/views/ProfileEditView.vue b/apps/player/src/views/ProfileEditView.vue
index 895f55a..ade1308 100644
--- a/apps/player/src/views/ProfileEditView.vue
+++ b/apps/player/src/views/ProfileEditView.vue
@@ -1,564 +1,10 @@
-
-
-
-
-
-
-
- ← {{ t('profile.back') }}
- {{ t('profile.edit') }}
-
-
-
-
-
-
-
- {{ t('profile.avatar_change') }}
-
-
-
-
-
-
-
-
{{ message }}
-
{{ error }}
-
+
+
-
-
diff --git a/apps/player/src/views/ProfileView.vue b/apps/player/src/views/ProfileView.vue
index 8e4c194..c738428 100644
--- a/apps/player/src/views/ProfileView.vue
+++ b/apps/player/src/views/ProfileView.vue
@@ -1,745 +1,10 @@
-
-
-
-
-
-
-
-
-
-
-
-
{{ t('common.load_failed') }}
-
{{ t('common.retry') }}
-
-
-
-
-
-
-
-
-
-
-
-
- THEBET365
- MEMBER
-
-
-
- +
- {{ t('recharge.title') }}
-
-
-
-
-
{{ t('wallet.balance') }}
-
{{ balanceDisplay }}
-
-
-
-
-
-
-
-
-
-
-
- {{ t('wallet.view_all') }}
-
-
-
-
-
- {{ t('recharge.title') }}
-
-
-
-
-
- {{ t('wallet.view_cashbacks') }}
-
-
-
-
-
- {{ t('recharge.history_title') }}
-
-
-
-
-
-
-
-
-
-
- {{ t('profile.edit') }}
-
- ›
-
-
-
-
-
-
-
-
-
- {{ t('profile.rules_title') }}
-
- ›
-
-
-
{{ t('profile.rules_p1') }}
-
{{ t('profile.rules_p2') }}
-
{{ t('profile.rules_p3') }}
-
{{ t('profile.rules_p4') }}
-
{{ t('profile.rules_p5') }}
-
-
-
-
-
-
-
-
-
-
{{ t('profile.language') }}
-
-
-
-
- {{ item.label }}
-
-
-
-
-
-
- {{ t('auth.logout') }}
-
-
-
+
+
-
-
diff --git a/apps/player/src/views/RechargeHistoryView.vue b/apps/player/src/views/RechargeHistoryView.vue
index 085a779..a71b00c 100644
--- a/apps/player/src/views/RechargeHistoryView.vue
+++ b/apps/player/src/views/RechargeHistoryView.vue
@@ -1,843 +1,10 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('recharge.no_orders') }}
-
-
-
-
-
-
{{ formatMoney(order.amount, locale) }}
-
- {{ t('recharge.credited') }}: {{ formatMoney(order.approvedAmount, locale) }}
-
-
- {{ order.paymentMethodName || '-' }}
-
-
-
- {{ t('recharge.apply_time') }}
- {{ formatOrderTime(order.createdAt) }}
-
-
- {{ t('recharge.review_time') }}
- {{ formatOrderTime(order.reviewedAt) }}
-
-
-
- {{ orderNoteLine(order) }}
-
-
-
- {{ t('recharge.audit_summary', { count: auditStepCount(order) }) }}
-
- {{ t('recharge.view_detail') }} ›
-
-
- {{ t('recharge.view_detail') }} ›
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('common.no_more') }}
-
-
-
-
-
-
-
✕
-
-
{{ t('recharge.order_detail') }}
-
-
-
-
- {{ selectedOrder.methodType }}
-
-
- {{ statusLabel(selectedOrder.status) }}
-
-
-
{{ formatMoney(selectedOrder.amount, locale) }}
-
- {{ t('recharge.credited') }}: {{ formatMoney(selectedOrder.approvedAmount, locale) }}
-
-
{{ selectedOrder.paymentMethodName || '-' }}
-
- {{ t('recharge.apply_time') }}
- {{ formatOrderTime(selectedOrder.createdAt) }}
-
-
- {{ t('recharge.review_time') }}
- {{ formatOrderTime(selectedOrder.reviewedAt) }}
-
-
- {{ orderNoteLine(selectedOrder) }}
-
-
-
-
-
{{ t('recharge.audit_title') }}
-
-
-
-
-
-
-
-
- {{ auditActionLabel(entry.log.action) }}
- {{ formatAuditTime(entry.log.createdAt) }}
-
-
{{ entry.actor }}
-
- {{ t('recharge.audit_credited') }} {{ formatMoney(entry.log.approvedAmount, locale) }}
-
-
- {{ t('recharge.reject_reason') }}
- {{ entry.remark.text }}
-
-
- {{ auditNoteDisplayText(entry.remark) }}
-
-
-
-
-
-
-
- {{ t('recharge.reapply') }}
-
-
-
-
-
+
+
-
-
diff --git a/apps/player/src/views/RechargeView.vue b/apps/player/src/views/RechargeView.vue
index 74e2b2a..8c97723 100644
--- a/apps/player/src/views/RechargeView.vue
+++ b/apps/player/src/views/RechargeView.vue
@@ -1,683 +1,10 @@
-
-
-
-
-
-
-
-
-
-
✓
-
{{ t('recharge.submitted') }}
-
{{ orderNo }}
-
{{ t('recharge.pending_review') }}
-
{{ t('recharge.back_to_history') }}
-
{{ t('recharge.new_recharge') }}
-
-
-
- {{ t('recharge.reapply_hint') }}
-
- {{ t('recharge.bank_transfer') }}
- USDT
-
-
-
-
- {{ m.displayName || m.bankName || m.usdtAddress }}
- {{ m.accountHolder }}
-
-
- {{ t('recharge.no_methods') }}
-
-
-
-
-
-
-
-
{{ selectedMethod.bankName }}
-
-
-
{{ formatCardNumber(selectedMethod.accountNumber) }}
-
-
-
-
-
-
-
-
-
- {{ t('recharge.account_holder') }}
- {{ selectedMethod.accountHolder }}
-
-
DEBIT
-
-
-
-
-
-
-
-
{{ t('recharge.usdt_address') }}
-
- {{ selectedMethod.usdtAddress }}
-
-
-
-
-
-
-
-
-
-
- {{ t('recharge.amount_label') }}
-
-
-
-
-
-
- {{ t('recharge.submitting') }}...
- {{ t('recharge.submit') }}
-
-
-
+
+
-
-
diff --git a/apps/player/src/views/RegisterWrapperView.vue b/apps/player/src/views/RegisterWrapperView.vue
new file mode 100644
index 0000000..eacf3de
--- /dev/null
+++ b/apps/player/src/views/RegisterWrapperView.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
diff --git a/apps/player/src/views/WalletDetailView.vue b/apps/player/src/views/WalletDetailView.vue
index a5f680b..81ef4a5 100644
--- a/apps/player/src/views/WalletDetailView.vue
+++ b/apps/player/src/views/WalletDetailView.vue
@@ -1,391 +1,10 @@
-
-
-
-
-
-
-
-
-
-
- {{ t('bet.loading') }}
-
-
-
-
-
-
-
- {{ t(f.label) }}
-
-
-
-
-
-
- {{ txLabel(tx) }}
- {{ txSubtitle(tx) }}
- {{ new Date(tx.createdAt).toLocaleString() }}
-
-
-
- {{ formatMoney(tx.amount, locale) }}
-
- ›
-
-
-
-
-
-
-
-
-
-
-
- {{ t('common.no_more') }}
-
-
-
- {{ t('wallet.no_records') }}
-
-
-
+
+
-
-
diff --git a/apps/player/src/views/WalletTransactionDetailView.vue b/apps/player/src/views/WalletTransactionDetailView.vue
index c2c5eb1..d2616df 100644
--- a/apps/player/src/views/WalletTransactionDetailView.vue
+++ b/apps/player/src/views/WalletTransactionDetailView.vue
@@ -1,388 +1,10 @@
-
-
-
-
-
-
-
-
-
-
-
-
{{ t('wallet.detail_not_found') }}
-
-
-
- {{ displayTypeLabel }}
- {{ summaryText }}
- {{ formatMoney(tx.amount, locale) }}
- {{ formattedTime }}
-
-
-
- {{ t('wallet.detail_summary') }}
-
-
- {{ t('wallet.detail_amount') }}
- {{ formatMoney(tx.amount, locale) }}
-
-
- {{ t('wallet.detail_balance_before') }}
- {{ formatMoney(tx.balanceBefore, locale) }}
-
-
- {{ t('wallet.detail_balance_after') }}
- {{ formatMoney(tx.balanceAfter, locale) }}
-
-
-
- {{ t('wallet.detail_frozen_before') }}
- {{ formatMoney(tx.frozenBefore, locale) }}
-
-
- {{ t('wallet.detail_frozen_after') }}
- {{ formatMoney(tx.frozenAfter, locale) }}
-
-
-
-
-
-
- {{ t('wallet.detail_reference') }}
-
-
- {{ t('wallet.detail_reference_type') }}
- {{ referenceLabel }}
-
-
- {{ t('wallet.detail_reference_id') }}
- {{ tx.referenceId }}
-
-
- {{ t('wallet.detail_remark') }}
-
-
-
-
- {{ t('wallet.detail_bet_link') }} · {{ tx.betNo }}
-
-
- {{ t('wallet.detail_cashback_link') }} · {{ cashbackBatchNo }}
-
-
-
-
- {{ t('wallet.detail_tx_id') }}
- {{ tx.transactionId }}
-
-
-
+
+
-
-
diff --git a/apps/player/src/views/WalletView.vue b/apps/player/src/views/WalletView.vue
index 8df0075..39a6ffc 100644
--- a/apps/player/src/views/WalletView.vue
+++ b/apps/player/src/views/WalletView.vue
@@ -1,588 +1,10 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ t('bet.loading') }}
-
-
-
-
-
-
-
-
-
-
-
- {{ t('wallet.view_cashbacks') }}
-
-
-
-
-
- {{ t('wallet.view_all') }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ txLabel(tx) }}
-
- {{ txSubtitle(tx) }}
-
- {{ new Date(tx.createdAt).toLocaleString() }}
-
-
-
-
-
-
-
- {{ formatMoney(tx.amount, locale) }}
-
-
-
- ›
-
-
-
-
-
-
-
-
-
-
-
- {{ t('wallet.no_records') }}
-
-
-
-
-
-
-
+
+
-
-
-
-
-
diff --git a/apps/player/src/views/desktop/DesktopAnnouncementDetailView.vue b/apps/player/src/views/desktop/DesktopAnnouncementDetailView.vue
new file mode 100644
index 0000000..7e14209
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopAnnouncementDetailView.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopAnnouncementListView.vue b/apps/player/src/views/desktop/DesktopAnnouncementListView.vue
new file mode 100644
index 0000000..91a9bac
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopAnnouncementListView.vue
@@ -0,0 +1,327 @@
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('announcements.empty') }}
+
+
+
+
+
+
+
+
+
+ {{ typeLabel(item) }}
+ {{ formatDate(item.createdAt) }}
+
+
{{ itemTitle(item) }}
+
{{ itemPreview(item) }}
+
{{ t('announcements.read_more') }} ›
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopAnnouncementsHubShell.vue b/apps/player/src/views/desktop/DesktopAnnouncementsHubShell.vue
new file mode 100644
index 0000000..52ff171
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopAnnouncementsHubShell.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+ {{ t('announcements.select_hint') }}
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopBetDetailView.vue b/apps/player/src/views/desktop/DesktopBetDetailView.vue
new file mode 100644
index 0000000..b68236f
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopBetDetailView.vue
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('common.load_failed') }}
+
{{ t('common.retry') }}
+
+
+
+
+
+
+
+
+
{{ t('bet.selections') || '投注选项' }}
+
+
{{ sel.matchName || t('bet.match') }}
+
+ {{ sel.market || sel.marketName || '' }}
+ {{ sel.selectionName || '' }}
+ @{{ sel.odds }}
+
+
+
+
+
+ {{ t('common.not_found') || '未找到记录' }}
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopCashbackRecordsView.vue b/apps/player/src/views/desktop/DesktopCashbackRecordsView.vue
new file mode 100644
index 0000000..23d8827
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopCashbackRecordsView.vue
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('cashback.period') }}
+ {{ t('cashback.effective_stake') }}
+ {{ t('cashback.rate') }}
+ {{ t('cashback.amount') }}
+ {{ t('cashback.batch_no') }}
+ {{ t('wallet.time') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ formatPeriod(row.periodStart, row.periodEnd) }}
+ {{ formatMoneyCompact(row.effectiveStake, locale) }}
+ {{ formatRate(row.rate) }}
+ {{ formatMoney(row.amount, locale) }}
+ {{ row.batchNo }}
+ {{ formatTime(row.confirmedAt ?? row.createdAt) }}
+
+
+
+
+ {{ t('cashback.empty') }}
+ {{ t('cashback.empty_hint') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopFootballView.vue b/apps/player/src/views/desktop/DesktopFootballView.vue
new file mode 100644
index 0000000..295f87b
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopFootballView.vue
@@ -0,0 +1,372 @@
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopForgotPasswordView.vue b/apps/player/src/views/desktop/DesktopForgotPasswordView.vue
new file mode 100644
index 0000000..349f769
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopForgotPasswordView.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
THEBET365
+
重置您的登录密码
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopHomeView.vue b/apps/player/src/views/desktop/DesktopHomeView.vue
new file mode 100644
index 0000000..6792a46
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopHomeView.vue
@@ -0,0 +1,355 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ match.leagueName }}
+ LIVE
+
+
+
+
+
+ {{ match.homeTeamName }}
+
+
VS
+
+
+ {{ match.awayTeamName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopInboxHubView.vue b/apps/player/src/views/desktop/DesktopInboxHubView.vue
new file mode 100644
index 0000000..a066ac0
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopInboxHubView.vue
@@ -0,0 +1,264 @@
+
+
+
+
+
+
+
+ {{ t(hubTitleKey) }}
+
+
+ {{ t('messages.tab_inbox') }}
+ {{ unreadInList }}
+
+
+
+ {{ t('messages.tab_support') }}
+
+
+
+
+
+
+
+
+ $router.push('/messages/' + id)" />
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopLoginView.vue b/apps/player/src/views/desktop/DesktopLoginView.vue
new file mode 100644
index 0000000..440440d
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopLoginView.vue
@@ -0,0 +1,134 @@
+
+
+
+
+
+
+
+
THEBET365
+
专业体育赛事投注平台
+
+
+ ⚡
+ 实时赔率更新
+
+
+ 🔒
+ 安全稳定平台
+
+
+ 💰
+ 快速充值提款
+
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopMatchDetailView.vue b/apps/player/src/views/desktop/DesktopMatchDetailView.vue
new file mode 100644
index 0000000..c16cf77
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopMatchDetailView.vue
@@ -0,0 +1,771 @@
+
+
+
+
+
+
+ ← {{ t('bet.back') || '返回' }}
+ /
+ {{ match?.leagueName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ match.homeTeamName }}
+
+
+
+
+ {{ match.score?.ftHome }}
+ -
+ {{ match.score?.ftAway }}
+
+
VS
+
{{ kickoff }}
+
+
+
+
+ {{ match.awayTeamName }}
+
+
+
+
+
+
{{ t('bet.result_stats_title') || '赛果数据' }}
+
+
+ {{ item.label }}
+ {{ item.value }}
+
+
+
+
+
+
{{ t('bet.filter_market') }}
+
+
+ {{ t('bet.time_all') }}
+
+
+ {{ option.label }}
+
+
+
+
+
+
+
+
+ {{ marketLabel(market) }}
+ {{ t('bet.market_status_closed') }}
+
+
+ toggleSelection(market.selections.find(s => s.id === id)!, market, ev)"
+ />
+
+
+
+
+
+
+
{{ t('bet.correct_score') }}
+
+
+
+ {{ marketLabel(market) }}
+ {{ t('bet.market_status_closed') }}
+
+
toggleSelection(market.selections.find(s => s.id === id)!, market, ev)"
+ />
+
+
+
+
+
+
{{ hasMarketFilter ? t('bet.no_markets_filtered') : (t('bet.no_markets') || '暂无可用盘口') }}
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopMessagesHubShell.vue b/apps/player/src/views/desktop/DesktopMessagesHubShell.vue
new file mode 100644
index 0000000..2b8f2d4
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopMessagesHubShell.vue
@@ -0,0 +1,182 @@
+
+
+
+
+
+
+ {{ t(hubTitleKey) }}
+
+
+ {{ t('messages.tab_inbox') }}
+ {{ unreadInList }}
+
+
+
+ {{ t('messages.tab_support') }}
+
+
+
+ $router.push('/messages/' + id)" />
+
+
+
+
+
+
+ {{ t('messages.select_hint') }}
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopMyBetsView.vue b/apps/player/src/views/desktop/DesktopMyBetsView.vue
new file mode 100644
index 0000000..6e41db8
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopMyBetsView.vue
@@ -0,0 +1,194 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('bet.bet_no') }}
+ {{ t('bet.match') }}
+ {{ t('bet.selection') }}
+ {{ t('bet.odds') }}
+ {{ t('bet.stake') }}
+ {{ t('bet.potential_payout') }}
+ {{ t('common.status') }}
+ {{ t('bet.placed_at') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ bet.betNo }}
+
+ {{ bet.matchTitle || '-' }}
+ {{ bet.pickLabel || '-' }}
+ {{ bet.totalOdds ?? '-' }}
+ {{ bet.stake }}
+ {{ bet.potentialReturn ?? '-' }}
+
+
+ {{ statusLabel(bet.status) }}
+
+
+ {{ bet.placedAt ? new Date(bet.placedAt).toLocaleString() : '-' }}
+
+
+
+
+ {{ t('history.empty') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopOutrightDetailView.vue b/apps/player/src/views/desktop/DesktopOutrightDetailView.vue
new file mode 100644
index 0000000..4122aa0
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopOutrightDetailView.vue
@@ -0,0 +1,334 @@
+
+
+
+
+
+ ← {{ t('bet.back') }}
+ /
+ {{ t('bet.tab_outright') }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ headTitle }}
+ {{ t('bet.outright_settled') }}
+
+
+ {{ event.leagueName }}
+
+
{{ t('bet.outright_teams_count', { n: teamCount }) }}
+
{{ t('bet.outright_settled_hint') }}
+
+
+
+
+
+
+
+
+
+
+
{{ loadError || t('bet.no_outright') }}
+
{{ t('bet.back') }}
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopProfileEditView.vue b/apps/player/src/views/desktop/DesktopProfileEditView.vue
new file mode 100644
index 0000000..6694077
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopProfileEditView.vue
@@ -0,0 +1,353 @@
+
+
+
+
+
+
+
+
+
+
+
{{ t('profile.avatar') || '头像' }}
+
+
+
+
+ {{ t('profile.change_avatar') || '更换头像' }}
+
+
+
+
+
+
+
{{ t('profile.basic_info') || '基础信息' }}
+
+
{{ message }}
+
{{ error }}
+
+
+
+
{{ t('profile.username') || '用户名' }}
+
+
{{ t('profile.username_locked') || '用户名不可修改' }}
+
+
+
+ {{ t('profile.phone') || '手机号' }}
+
+
+
+
+ Email
+
+
+
+
+
+
+ {{ t('common.save') || '保存' }}
+
+
+
+
+
+
+ {{ t('profile.change_password') || '修改密码' }}
+
+ {{ passwordChangeOpen ? (t('common.collapse') || '收起') : (t('common.expand') || '展开') }}
+
+
+
+
+
+
+
+ { avatarModalOpen = false; if (key !== undefined) setAvatarKey(key); }"
+ />
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopProfileView.vue b/apps/player/src/views/desktop/DesktopProfileView.vue
new file mode 100644
index 0000000..022aead
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopProfileView.vue
@@ -0,0 +1,439 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('common.load_failed') }}
+
{{ t('common.retry') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ THEBET365
+ MEMBER
+
+
+
+ +
+ {{ t('recharge.title') }}
+
+
+
+
+
{{ t('wallet.available') || '可用余额' }}
+
{{ balanceDisplay }}
+
+
+
+
+
+
+
+
+
+
+ {{ t('profile.username') || '用户名' }}
+ {{ profileRaw?.username }}
+
+
+ {{ t('profile.phone') || '手机号' }}
+ {{ profileRaw?.preferences?.phone || '-' }}
+
+
+ {{ t('profile.edit') }}
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+ {{ t('auth.logout') }}
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopRechargeHistoryView.vue b/apps/player/src/views/desktop/DesktopRechargeHistoryView.vue
new file mode 100644
index 0000000..6fef90b
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopRechargeHistoryView.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('recharge.order_no') }}
+ {{ t('recharge.method') }}
+ {{ t('wallet.amount') }}
+ {{ t('common.status') }}
+ {{ t('wallet.time') }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ order.orderNo || order.id }}
+ {{ methodLabel(order) }}
+ {{ formatMoney(order.amount, locale) }}
+
+
+ {{ statusLabel(order.status) }}
+
+
+ {{ new Date(order.createdAt).toLocaleString() }}
+
+
+
+
+ {{ t('recharge.no_orders') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopRechargeView.vue b/apps/player/src/views/desktop/DesktopRechargeView.vue
new file mode 100644
index 0000000..568d804
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopRechargeView.vue
@@ -0,0 +1,580 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('recharge.success_title') }}
+
{{ t('recharge.order_no') }}: {{ orderNo }}
+
{{ t('recharge.success_hint') }}
+
+ {{ t('recharge.history_title') }}
+
+
+
+
+
+
+
+ {{ t('recharge.bank_transfer') || '银行转账' }}
+
+
+ USDT
+
+
+
+
+
+
+
{{ t('recharge.choose_method') || '选择收款账户' }}
+
+ {{ t('recharge.no_methods') || '暂无可用收款账户' }}
+
+
+ {{ m.displayName || m.bankName || 'USDT' }}
+ {{ m.accountNumber || m.usdtAddress || '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopRegisterView.vue b/apps/player/src/views/desktop/DesktopRegisterView.vue
new file mode 100644
index 0000000..3df109b
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopRegisterView.vue
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+
+
THEBET365
+
加入我们,开始精彩体育投注
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopWalletDetailView.vue b/apps/player/src/views/desktop/DesktopWalletDetailView.vue
new file mode 100644
index 0000000..cd85ee2
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopWalletDetailView.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('wallet.type') || '类型' }}
+ {{ t('wallet.note') || '备注' }}
+ {{ t('wallet.amount') || '金额' }}
+ {{ t('wallet.time') || '时间' }}
+
+
+
+
+
+ {{ txLabel(tx) }}
+ {{ txSummaryLabel(tx, t) || '-' }}
+ {{ formatMoney(tx.amount, locale) }}
+ {{ new Date(tx.createdAt).toLocaleString() }}
+ ›
+
+
+
+
+
+
+
+ {{ t('common.load_more') || '加载更多' }}
+
+
+
{{ t('common.no_more') }}
+
+
+ {{ t('wallet.no_records') }}
+
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopWalletTransactionDetailView.vue b/apps/player/src/views/desktop/DesktopWalletTransactionDetailView.vue
new file mode 100644
index 0000000..1dcaab4
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopWalletTransactionDetailView.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('common.load_failed') }}
+
{{ t('common.retry') }}
+
+
+
+
+ {{ formatMoney(detail.amount, locale) }}
+
+
{{ txLabel(detail) }}
+
+
+
+ {{ t('wallet.type') || '类型' }}
+ {{ txLabel(detail) }}
+
+
+ {{ t('wallet.note') || '备注' }}
+ {{ txSummaryLabel(detail, t) || '-' }}
+
+
+ {{ t('wallet.balance_before') || '变动前余额' }}
+ {{ formatMoney(detail.balanceBefore, locale) }}
+
+
+ {{ t('wallet.balance_after') || '变动后余额' }}
+ {{ formatMoney(detail.balanceAfter, locale) }}
+
+
+ {{ t('wallet.frozen_before') || '冻结前' }}
+ {{ formatMoney(detail.frozenBefore, locale) }}
+
+
+ {{ t('wallet.frozen_after') || '冻结后' }}
+ {{ formatMoney(detail.frozenAfter, locale) }}
+
+
+ {{ t('wallet.time') || '时间' }}
+ {{ new Date(detail.createdAt).toLocaleString() }}
+
+
+ {{ t('wallet.transaction_id') || '流水号' }}
+ {{ detail.transactionId }}
+
+
+
+
+
{{ t('common.not_found') || '未找到记录' }}
+
+
+
+
+
+
diff --git a/apps/player/src/views/desktop/DesktopWalletView.vue b/apps/player/src/views/desktop/DesktopWalletView.vue
new file mode 100644
index 0000000..e1f267b
--- /dev/null
+++ b/apps/player/src/views/desktop/DesktopWalletView.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t('wallet.type') || '类型' }}
+ {{ t('wallet.note') || '备注' }}
+ {{ t('wallet.amount') || '金额' }}
+ {{ t('wallet.time') || '时间' }}
+
+
+
+
+ {{ txLabel(tx) }}
+ {{ txSubtitle(tx) || '-' }}
+ {{ formatMoney(tx.amount, locale) }}
+ {{ new Date(tx.createdAt).toLocaleString() }}
+
+
+
+
+
+
+
+
+
+ {{ t('wallet.no_records') }}
+
+
+
+
+
+
diff --git a/apps/player/vite.config.ts b/apps/player/vite.config.ts
index 1704b59..94b8af0 100644
--- a/apps/player/vite.config.ts
+++ b/apps/player/vite.config.ts
@@ -1,9 +1,9 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';
-import { resolveApiDevTarget } from '../../scripts/dev-api-target.mjs';
+import { resolveDevApiTarget } from '../../scripts/resolve-dev-api-target.mjs';
-const apiTarget = resolveApiDevTarget();
+const devApiTarget = resolveDevApiTarget();
export default defineConfig({
plugins: [vue()],
@@ -38,10 +38,11 @@ export default defineConfig({
},
publicDir: resolve(__dirname, '../../packages/shared/public'),
server: {
+ host: true,
port: 5173,
proxy: {
- '/api': { target: apiTarget, changeOrigin: true },
- '/uploads': { target: apiTarget, changeOrigin: true },
+ '/api': { target: devApiTarget, changeOrigin: true },
+ '/uploads': { target: devApiTarget, changeOrigin: true },
},
},
});
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 7f71f6a..bcb5714 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -75,7 +75,7 @@ services:
- thebet365
player:
- image: thebet365-player:${IMAGE_TAG:-latest}
+ image: thebet365-player:${PLAYER_IMAGE_TAG:-main}
build:
context: .
dockerfile: docker/player/Dockerfile
diff --git a/docker-compose.themes.yml b/docker-compose.themes.yml
new file mode 100644
index 0000000..c0a6dc6
--- /dev/null
+++ b/docker-compose.themes.yml
@@ -0,0 +1,59 @@
+# 四套主题扩展 — 与 docker-compose.prod.yml 合并使用:
+# docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d
+#
+# 关联文档:docs/四套主题Docker部署任务.md
+
+services:
+ player2:
+ image: thebet365-player:${PLAYER2_IMAGE_TAG:-theme-2}
+ container_name: thebet365-player2
+ depends_on:
+ api:
+ condition: service_healthy
+ ports:
+ - '${BIND_ADDR:-127.0.0.1}:${PLAYER2_PORT:-8083}:80'
+ healthcheck:
+ test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1/ || exit 1']
+ interval: 10s
+ timeout: 5s
+ retries: 6
+ start_period: 10s
+ restart: unless-stopped
+ networks:
+ - thebet365
+
+ player3:
+ image: thebet365-player:${PLAYER3_IMAGE_TAG:-theme-3}
+ container_name: thebet365-player3
+ depends_on:
+ api:
+ condition: service_healthy
+ ports:
+ - '${BIND_ADDR:-127.0.0.1}:${PLAYER3_PORT:-8084}:80'
+ healthcheck:
+ test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1/ || exit 1']
+ interval: 10s
+ timeout: 5s
+ retries: 6
+ start_period: 10s
+ restart: unless-stopped
+ networks:
+ - thebet365
+
+ player4:
+ image: thebet365-player:${PLAYER4_IMAGE_TAG:-theme-4}
+ container_name: thebet365-player4
+ depends_on:
+ api:
+ condition: service_healthy
+ ports:
+ - '${BIND_ADDR:-127.0.0.1}:${PLAYER4_PORT:-8085}:80'
+ healthcheck:
+ test: ['CMD-SHELL', 'wget -q -O /dev/null http://127.0.0.1/ || exit 1']
+ interval: 10s
+ timeout: 5s
+ retries: 6
+ start_period: 10s
+ restart: unless-stopped
+ networks:
+ - thebet365
diff --git a/docs/docker/build-and-export-all-themes.bat b/docs/docker/build-and-export-all-themes.bat
new file mode 100644
index 0000000..94a48d0
--- /dev/null
+++ b/docs/docker/build-and-export-all-themes.bat
@@ -0,0 +1,13 @@
+@echo off
+REM Thin launcher for build-and-export-all-themes.ps1
+REM CMD reads batch files line-by-line from disk; git checkout to theme-* removes
+REM this file from the working tree. PowerShell loads the script into memory first.
+REM
+REM Usage:
+REM docs\docker\build-and-export-all-themes.bat
+REM docs\docker\build-and-export-all-themes.bat --use-cache
+REM docs\docker\build-and-export-all-themes.bat --export-only
+REM docs\docker\build-and-export-all-themes.bat --skip-api-admin
+
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0build-and-export-all-themes.ps1" %*
+exit /b %ERRORLEVEL%
diff --git a/docs/docker/build-and-export-all-themes.ps1 b/docs/docker/build-and-export-all-themes.ps1
new file mode 100644
index 0000000..5460292
--- /dev/null
+++ b/docs/docker/build-and-export-all-themes.ps1
@@ -0,0 +1,285 @@
+# 四套主题 player 一键打包(自动切换 Git 分支)
+# PowerShell 会在启动时将整份脚本载入内存,git checkout 后仍可继续执行。
+# 用法(项目根目录或本目录均可):
+# .\docs\docker\build-and-export-all-themes.ps1
+# .\docs\docker\build-and-export-all-themes.ps1 -UseCache
+# .\docs\docker\build-and-export-all-themes.ps1 -ExportOnly
+# .\docs\docker\build-and-export-all-themes.ps1 -SkipApiAdmin
+# 或通过 bat 启动(参数支持 --use-cache 等 CMD 风格):
+# docs\docker\build-and-export-all-themes.bat --use-cache
+
+param(
+ [switch]$UseCache,
+ [switch]$ExportOnly,
+ [switch]$SkipApiAdmin,
+ [switch]$SkipBundle,
+ [switch]$Help
+)
+
+$ErrorActionPreference = "Stop"
+
+foreach ($arg in $args) {
+ switch ($arg.ToLowerInvariant()) {
+ "--use-cache" { $UseCache = $true; continue }
+ "--export-only" { $ExportOnly = $true; continue }
+ "--skip-api-admin" { $SkipApiAdmin = $true; continue }
+ "--skip-bundle" { $SkipBundle = $true; continue }
+ "-h" { $Help = $true; continue }
+ "--help" { $Help = $true; continue }
+ "/?" { $Help = $true; continue }
+ default { throw "Unknown argument: $arg" }
+ }
+}
+
+function Show-Help {
+ Write-Host @"
+
+Usage: docs\docker\build-and-export-all-themes.ps1 [options]
+
+ -UseCache, --use-cache Use Docker build cache (default: --no-cache)
+ -ExportOnly, --export-only Skip build, export existing images only
+ -SkipApiAdmin, --skip-api-admin
+ Only build 4 player themes, skip api/admin
+ -SkipBundle, --skip-bundle Skip thebet365-full-themes-latest.tar (6-in-1 bundle)
+ -Help, -h, --help Show this help
+
+Output (project root):
+ thebet365-player-main.tar
+ thebet365-player-theme-2.tar
+ thebet365-player-theme-3.tar
+ thebet365-player-theme-4.tar
+ thebet365-images-latest.tar (api + admin, unless -SkipApiAdmin)
+ thebet365-full-themes-latest.tar (api + admin + 4 player themes, unless -SkipBundle)
+
+"@
+}
+
+if ($Help) {
+ Show-Help
+ exit 0
+}
+
+$Root = (Resolve-Path (Join-Path $PSScriptRoot "../..")).Path
+
+$Themes = @(
+ @{ Branch = "main"; Tag = "main" },
+ @{ Branch = "theme-2"; Tag = "theme-2" },
+ @{ Branch = "theme-3"; Tag = "theme-3" },
+ @{ Branch = "theme-4"; Tag = "theme-4" }
+)
+
+if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
+ throw "docker not found. Please start Docker Desktop first."
+}
+
+if (-not (Test-Path (Join-Path $Root "docs\docker\build-and-export-images.bat"))) {
+ throw "Missing build script: docs\docker\build-and-export-images.bat"
+}
+
+function Invoke-Git {
+ param([Parameter(ValueFromRemainingArguments = $true)][string[]]$GitArgs)
+
+ $prevErrorAction = $ErrorActionPreference
+ $ErrorActionPreference = "SilentlyContinue"
+ try {
+ & git @GitArgs 2>$null | Out-Null
+ return $LASTEXITCODE
+ } finally {
+ $ErrorActionPreference = $prevErrorAction
+ }
+}
+
+function Get-GitOutput {
+ param([Parameter(ValueFromRemainingArguments = $true)][string[]]$GitArgs)
+
+ $prevErrorAction = $ErrorActionPreference
+ $ErrorActionPreference = "SilentlyContinue"
+ try {
+ return ((& git @GitArgs 2>$null | Out-String).Trim())
+ } finally {
+ $ErrorActionPreference = $prevErrorAction
+ }
+}
+
+Set-Location $Root
+
+$OriginalBranch = Get-GitOutput rev-parse --abbrev-ref HEAD
+if ([string]::IsNullOrWhiteSpace($OriginalBranch)) {
+ throw "Cannot determine current Git branch. Run this script from the repo root."
+}
+Write-Host "[INFO] Current branch: $OriginalBranch"
+
+$DidStash = $false
+$diffExit = Invoke-Git diff --quiet
+$cachedExit = Invoke-Git diff --cached --quiet
+if ($diffExit -ne 0 -or $cachedExit -ne 0) {
+ Write-Host "[INFO] Stashing local changes before branch switching..."
+ $stashExit = Invoke-Git stash push -m "build-all-themes auto-stash"
+ if ($stashExit -eq 0) {
+ $DidStash = $true
+ Write-Host "[INFO] Changes stashed."
+ } else {
+ Write-Warning "git stash failed. Branch switching may fail if there are conflicts."
+ }
+}
+
+$FailedThemes = [System.Collections.Generic.List[string]]::new()
+$SuccessCount = 0
+
+function Invoke-BuildExport {
+ param(
+ [string]$Service = "",
+ [string]$Tag
+ )
+
+ $argList = @()
+ if (-not [string]::IsNullOrWhiteSpace($Service)) {
+ $argList += "--service"
+ $argList += $Service
+ }
+ $argList += "--tag"
+ $argList += $Tag
+ if ($UseCache) { $argList += "--use-cache" }
+ if ($ExportOnly) { $argList += "--export-only" }
+
+ Set-Location $Root
+ $batPath = Join-Path $Root "docs\docker\build-and-export-images.bat"
+ $argText = ($argList | ForEach-Object {
+ if ($_ -match '\s') { "`"$_`"" } else { $_ }
+ }) -join ' '
+ & cmd.exe /c "`"$batPath`" $argText"
+ return $LASTEXITCODE
+}
+
+function Invoke-PlayerThemeBuild {
+ param(
+ [string]$Branch,
+ [string]$Tag
+ )
+
+ Write-Host ""
+ Write-Host "----------------------------------------------------------------"
+ Write-Host "[INFO] Theme: $Branch (image tag: $Tag)"
+ Write-Host "----------------------------------------------------------------"
+
+ Set-Location $Root
+ $checkoutExit = Invoke-Git checkout $Branch
+ if ($checkoutExit -ne 0) {
+ Write-Host "[ERROR] Cannot switch to branch $Branch -- skipping"
+ $script:FailedThemes.Add($Branch) | Out-Null
+ return
+ }
+ Write-Host "[INFO] Switched to $Branch"
+
+ Set-Location $Root
+ if ((Invoke-BuildExport -Service "player" -Tag $Tag) -ne 0) {
+ Write-Host "[ERROR] Player build failed for $Branch"
+ $script:FailedThemes.Add($Branch) | Out-Null
+ return
+ }
+
+ Write-Host "[OK] thebet365-player:$Tag exported successfully"
+ $script:SuccessCount++
+}
+
+foreach ($theme in $Themes) {
+ Invoke-PlayerThemeBuild -Branch $theme.Branch -Tag $theme.Tag
+}
+
+Write-Host ""
+Write-Host "[INFO] Restoring original branch: $OriginalBranch"
+Set-Location $Root
+if ((Invoke-Git checkout $OriginalBranch) -eq 0) {
+ Write-Host "[INFO] Restored to $OriginalBranch"
+} else {
+ Write-Warning "Could not restore branch $OriginalBranch. Run: git checkout $OriginalBranch"
+}
+
+if ($DidStash) {
+ Write-Host "[INFO] Restoring stashed changes..."
+ if ((Invoke-Git stash pop) -eq 0) {
+ Write-Host "[INFO] Stash restored."
+ } else {
+ Write-Warning "git stash pop failed. Run: git stash pop"
+ }
+}
+
+if (-not $SkipApiAdmin) {
+ Write-Host ""
+ Write-Host "[INFO] Building api + admin (tag: latest) on branch main..."
+ Set-Location $Root
+ if ((Invoke-Git checkout main) -ne 0) {
+ Write-Host "[ERROR] Cannot switch to branch main for api/admin build"
+ $FailedThemes.Add("api/admin") | Out-Null
+ } else {
+ Set-Location $Root
+ if ((Invoke-BuildExport -Service "api-admin" -Tag "latest") -ne 0) {
+ Write-Host "[ERROR] api/admin build failed"
+ $FailedThemes.Add("api/admin") | Out-Null
+ } else {
+ $SuccessCount++
+ }
+ }
+
+ Write-Host ""
+ Write-Host "[INFO] Restoring original branch: $OriginalBranch"
+ Set-Location $Root
+ Invoke-Git checkout $OriginalBranch | Out-Null
+}
+
+if (-not $SkipBundle -and -not $SkipApiAdmin -and $FailedThemes.Count -eq 0) {
+ Write-Host ""
+ Write-Host "[INFO] Exporting full bundle (api + admin + 4 player themes)..."
+ Set-Location $Root
+ if ((Invoke-BuildExport -Service "full-themes" -Tag "latest") -ne 0) {
+ Write-Host "[ERROR] Full bundle export failed"
+ $FailedThemes.Add("full-bundle") | Out-Null
+ } else {
+ Write-Host "[OK] thebet365-full-themes-latest.tar exported successfully"
+ $SuccessCount++
+ }
+}
+
+Write-Host ""
+Write-Host "================================================================"
+Write-Host " BUILD SUMMARY"
+Write-Host "================================================================"
+Write-Host " Succeeded: $SuccessCount task(s)"
+
+if ($FailedThemes.Count -gt 0) {
+ Write-Host " Failed: $($FailedThemes -join ' ')"
+ Write-Host ""
+ Write-Host " Tip: Retry failed themes manually:"
+ Write-Host " git checkout
"
+ Write-Host " docs\docker\build-and-export-images.bat --service player --tag "
+ Write-Host "================================================================"
+ exit 1
+}
+
+Write-Host ""
+Write-Host " All done! Output files in project root:"
+Write-Host " thebet365-player-main.tar"
+Write-Host " thebet365-player-theme-2.tar"
+Write-Host " thebet365-player-theme-3.tar"
+Write-Host " thebet365-player-theme-4.tar"
+if (-not $SkipApiAdmin) {
+ Write-Host " thebet365-images-latest.tar (api + admin)"
+}
+if (-not $SkipBundle -and -not $SkipApiAdmin) {
+ Write-Host " thebet365-full-themes-latest.tar (api + admin + 4 themes, upload this only)"
+}
+Write-Host ""
+Write-Host " Server import (single bundle):"
+Write-Host " docker load -i thebet365-full-themes-latest.tar"
+Write-Host " docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d"
+Write-Host ""
+Write-Host " Or import separate tars:"
+if (-not $SkipApiAdmin) {
+ Write-Host " docker load -i thebet365-images-latest.tar"
+}
+Write-Host " docker load -i thebet365-player-main.tar"
+Write-Host " docker load -i thebet365-player-theme-2.tar"
+Write-Host " docker load -i thebet365-player-theme-3.tar"
+Write-Host " docker load -i thebet365-player-theme-4.tar"
+Write-Host "================================================================"
+exit 0
diff --git a/docs/docker/build-and-export-images.bat b/docs/docker/build-and-export-images.bat
index 2563057..ea6e690 100644
--- a/docs/docker/build-and-export-images.bat
+++ b/docs/docker/build-and-export-images.bat
@@ -8,6 +8,9 @@ REM docs\docker\build-and-export-images.bat --service admin
REM docs\docker\build-and-export-images.bat --tag v1.2.3
REM docs\docker\build-and-export-images.bat --use-cache
REM docs\docker\build-and-export-images.bat --export-only
+REM
+REM 四套主题玩家端一键打包(自动切分支,推荐):
+REM docs\docker\build-and-export-all-themes.bat
cd /d "%~dp0..\.."
@@ -66,7 +69,7 @@ goto parse_args
echo.
echo Usage: docs\docker\build-and-export-images.bat [options]
echo.
-echo --service SVC api ^| player ^| admin ^| all (default: all)
+echo --service SVC api ^| player ^| admin ^| api-admin ^| full-themes ^| all (default: all)
echo --tag TAG Image tag (default: latest)
echo --use-cache Use Docker build cache
echo --export-only Skip build, export existing images only
@@ -78,6 +81,9 @@ echo docs\docker\build-and-export-images.bat --service admin
echo docs\docker\build-and-export-admin.bat
echo docs\docker\build-and-export-admin.bat --export-only
echo.
+echo 四套主题玩家端批量打包(自动切分支,推荐):
+echo docs\docker\build-and-export-all-themes.bat
+echo.
exit /b 0
:args_done
@@ -111,8 +117,7 @@ if errorlevel 1 exit /b 1
if not defined OUTPUT call :default_output
set "OUTPUT_PATH=%OUTPUT%"
-echo %OUTPUT_PATH%| findstr /r "^[A-Za-z]:\\" >nul
-if errorlevel 1 set "OUTPUT_PATH=%CD%\%OUTPUT%"
+if /i not "%OUTPUT:~1,1%"==":" set "OUTPUT_PATH=%CD%\%OUTPUT%"
where docker >nul 2>&1
if errorlevel 1 (
@@ -120,7 +125,7 @@ if errorlevel 1 (
exit /b 1
)
-if "%EXPORT_ONLY%"=="0" goto do_build
+if "%EXPORT_ONLY%"=="0" if /i not "%SERVICE%"=="full-themes" goto do_build
goto do_export
:do_build
@@ -129,6 +134,8 @@ set "IMAGE_TAG=%TAG%"
if "%NO_CACHE%"=="1" goto build_no_cache
if /i "%SERVICE%"=="all" (
docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build api player admin
+) else if /i "%SERVICE%"=="api-admin" (
+ docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build api admin
) else (
docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build %SERVICE%
)
@@ -137,6 +144,8 @@ goto build_done
:build_no_cache
if /i "%SERVICE%"=="all" (
docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build --no-cache api player admin
+) else if /i "%SERVICE%"=="api-admin" (
+ docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build --no-cache api admin
) else (
docker compose -f "%COMPOSE_FILE%" --env-file "%ENV_FILE%" build --no-cache %SERVICE%
)
@@ -151,6 +160,10 @@ if errorlevel 1 (
echo ==^> Exporting %SERVICE% to %OUTPUT_PATH%
if /i "%SERVICE%"=="all" (
docker save thebet365-api:%TAG% thebet365-player:%TAG% thebet365-admin:%TAG% -o "%OUTPUT_PATH%"
+) else if /i "%SERVICE%"=="api-admin" (
+ docker save thebet365-api:%TAG% thebet365-admin:%TAG% -o "%OUTPUT_PATH%"
+) else if /i "%SERVICE%"=="full-themes" (
+ docker save thebet365-api:%TAG% thebet365-admin:%TAG% thebet365-player:main thebet365-player:theme-2 thebet365-player:theme-3 thebet365-player:theme-4 -o "%OUTPUT_PATH%"
) else (
docker save thebet365-%SERVICE%:%TAG% -o "%OUTPUT_PATH%"
)
@@ -181,16 +194,34 @@ for %%F in ("%OUTPUT_PATH%") do set /a SIZE_MB=%%~zF/1048576
echo git_dirty=!GIT_DIRTY!
echo tar=!TAR_NAME!
echo tar_sha256=!TAR_SHA256!
- if /i not "!SERVICE!"=="all" (
- echo image=thebet365-!SERVICE!:%TAG%
- echo image_id=!SVC_IMAGE_ID!
- ) else (
+ if /i "!SERVICE!"=="all" (
echo api_image=thebet365-api:%TAG%
echo api_image_id=!API_IMAGE_ID!
echo player_image=thebet365-player:%TAG%
echo player_image_id=!PLAYER_IMAGE_ID!
echo admin_image=thebet365-admin:%TAG%
echo admin_image_id=!ADMIN_IMAGE_ID!
+ ) else if /i "!SERVICE!"=="api-admin" (
+ echo api_image=thebet365-api:%TAG%
+ echo api_image_id=!API_IMAGE_ID!
+ echo admin_image=thebet365-admin:%TAG%
+ echo admin_image_id=!ADMIN_IMAGE_ID!
+ ) else if /i "!SERVICE!"=="full-themes" (
+ echo api_image=thebet365-api:%TAG%
+ echo api_image_id=!API_IMAGE_ID!
+ echo admin_image=thebet365-admin:%TAG%
+ echo admin_image_id=!ADMIN_IMAGE_ID!
+ echo player_main_image=thebet365-player:main
+ echo player_main_image_id=!PLAYER_MAIN_IMAGE_ID!
+ echo player_theme_2_image=thebet365-player:theme-2
+ echo player_theme_2_image_id=!PLAYER_THEME2_IMAGE_ID!
+ echo player_theme_3_image=thebet365-player:theme-3
+ echo player_theme_3_image_id=!PLAYER_THEME3_IMAGE_ID!
+ echo player_theme_4_image=thebet365-player:theme-4
+ echo player_theme_4_image_id=!PLAYER_THEME4_IMAGE_ID!
+ ) else (
+ echo image=thebet365-!SERVICE!:%TAG%
+ echo image_id=!SVC_IMAGE_ID!
)
)
@@ -201,6 +232,12 @@ echo Server load and recreate:
if /i "%SERVICE%"=="all" (
echo docker load -i !TAR_NAME!
echo docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate api player admin
+) else if /i "%SERVICE%"=="api-admin" (
+ echo docker load -i !TAR_NAME!
+ echo docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate api admin
+) else if /i "%SERVICE%"=="full-themes" (
+ echo docker load -i !TAR_NAME!
+ echo docker compose -f docker-compose.prod.yml -f docker-compose.themes.yml --env-file .env.docker up -d
) else (
echo docker load -i !TAR_NAME!
echo docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate %SERVICE%
@@ -217,6 +254,10 @@ exit /b 0
:default_output
if /i "%SERVICE%"=="all" (
set "OUTPUT=thebet365-images-%TAG%.tar"
+) else if /i "%SERVICE%"=="api-admin" (
+ set "OUTPUT=thebet365-images-%TAG%.tar"
+) else if /i "%SERVICE%"=="full-themes" (
+ set "OUTPUT=thebet365-full-themes-%TAG%.tar"
) else (
set "OUTPUT=thebet365-%SERVICE%-%TAG%.tar"
)
@@ -224,10 +265,13 @@ exit /b 0
:validate_service
if /i "%SERVICE%"=="all" exit /b 0
+if /i "%SERVICE%"=="api-admin" exit /b 0
+if /i "%SERVICE%"=="full-themes" exit /b 0
if /i "%SERVICE%"=="api" exit /b 0
if /i "%SERVICE%"=="player" exit /b 0
if /i "%SERVICE%"=="admin" exit /b 0
-echo ERROR: invalid --service: %SERVICE% (use api, player, admin, or all)
+echo ERROR: invalid --service: %SERVICE% (use api, player, admin, api-admin, full-themes, or all)
+echo TIP: 四套主题一键打包请使用 docs\docker\build-and-export-all-themes.bat
exit /b 1
:validate_tag
@@ -266,10 +310,19 @@ set "API_IMAGE_ID="
set "PLAYER_IMAGE_ID="
set "ADMIN_IMAGE_ID="
set "SVC_IMAGE_ID="
+set "PLAYER_MAIN_IMAGE_ID="
+set "PLAYER_THEME2_IMAGE_ID="
+set "PLAYER_THEME3_IMAGE_ID="
+set "PLAYER_THEME4_IMAGE_ID="
for /f "delims=" %%I in ('docker image inspect thebet365-api:%TAG% --format "{{.Id}}" 2^>nul') do set "API_IMAGE_ID=%%I"
for /f "delims=" %%I in ('docker image inspect thebet365-player:%TAG% --format "{{.Id}}" 2^>nul') do set "PLAYER_IMAGE_ID=%%I"
for /f "delims=" %%I in ('docker image inspect thebet365-admin:%TAG% --format "{{.Id}}" 2^>nul') do set "ADMIN_IMAGE_ID=%%I"
-if /i not "%SERVICE%"=="all" (
+if /i "%SERVICE%"=="full-themes" (
+ for /f "delims=" %%I in ('docker image inspect thebet365-player:main --format "{{.Id}}" 2^>nul') do set "PLAYER_MAIN_IMAGE_ID=%%I"
+ for /f "delims=" %%I in ('docker image inspect thebet365-player:theme-2 --format "{{.Id}}" 2^>nul') do set "PLAYER_THEME2_IMAGE_ID=%%I"
+ for /f "delims=" %%I in ('docker image inspect thebet365-player:theme-3 --format "{{.Id}}" 2^>nul') do set "PLAYER_THEME3_IMAGE_ID=%%I"
+ for /f "delims=" %%I in ('docker image inspect thebet365-player:theme-4 --format "{{.Id}}" 2^>nul') do set "PLAYER_THEME4_IMAGE_ID=%%I"
+) else if /i not "%SERVICE%"=="all" (
for /f "delims=" %%I in ('docker image inspect thebet365-%SERVICE%:%TAG% --format "{{.Id}}" 2^>nul') do set "SVC_IMAGE_ID=%%I"
)
exit /b 0
diff --git a/docs/docker/build-and-export-images.ps1 b/docs/docker/build-and-export-images.ps1
index 02653cf..c8d00d9 100644
--- a/docs/docker/build-and-export-images.ps1
+++ b/docs/docker/build-and-export-images.ps1
@@ -1,13 +1,17 @@
-# 构建 api / player / admin 生产镜像并导出为 tar
-# 用法(在项目根目录或本目录执行均可):
+# ?? api / player / admin ???????? tar
+# ??????????????????:
# .\docs\docker\build-and-export-images.ps1
# .\docs\docker\build-and-export-images.ps1 -Service admin
# .\docs\docker\build-and-export-images.ps1 -Tag v1.2.3
# .\docs\docker\build-and-export-images.ps1 -UseCache
# .\docs\docker\build-and-export-images.ps1 -ExportOnly
+#
+# ????????????????????????????????????
+# docs\docker\build-and-export-all-themes.bat
+
param(
- [ValidateSet("all", "api", "player", "admin")]
+ [ValidateSet("all", "api", "player", "admin", "api-admin")]
[string]$Service = "all",
[string]$Tag = $env:IMAGE_TAG,
[switch]$UseCache,
@@ -27,27 +31,27 @@ function Get-DefaultTag { "latest" }
function Get-DefaultOutput {
param([string]$Svc, [string]$ImageTag)
- if ($Svc -eq "all") { return "thebet365-images-$ImageTag.tar" }
+ if ($Svc -eq "all" -or $Svc -eq "api-admin") { return "thebet365-images-$ImageTag.tar" }
return "thebet365-$Svc-$ImageTag.tar"
}
function Assert-ImageTag {
param([string]$Value)
if ($Value -notmatch '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$') {
- throw "镜像 tag 不合法: $Value"
+ throw "?? tag ???? $Value"
}
}
if (-not (Test-Path $ComposeFile)) {
- throw "未找到 $ComposeFile(当前目录: $Root)"
+ throw "????$ComposeFile?????? $Root??
}
if (-not (Test-Path $EnvFile)) {
if (Test-Path ".env.docker.example") {
- Write-Warning "未找到 .env.docker,使用 .env.docker.example(生产请复制并修改密钥)"
+ Write-Warning "????.env.docker????.env.docker.example????????????"
$EnvFile = ".env.docker.example"
} else {
- throw "未找到 .env.docker 或 .env.docker.example"
+ throw "????.env.docker ??.env.docker.example"
}
}
@@ -56,7 +60,11 @@ if ([string]::IsNullOrWhiteSpace($Tag)) {
}
Assert-ImageTag $Tag
-$BuildServices = if ($Service -eq "all") { $AllServices } else { @($Service) }
+$BuildServices = switch ($Service) {
+ "all" { $AllServices }
+ "api-admin" { @("api", "admin") }
+ default { @($Service) }
+}
$SaveImages = $BuildServices | ForEach-Object { "thebet365-${_}:${Tag}" }
if ([string]::IsNullOrWhiteSpace($Output)) {
@@ -64,7 +72,7 @@ if ([string]::IsNullOrWhiteSpace($Output)) {
}
if (-not $ExportOnly) {
- Write-Host "==> 构建镜像: $($BuildServices -join ', ') (tag: $Tag)"
+ Write-Host "==> ????: $($BuildServices -join ', ') (tag: $Tag)"
$buildArgs = @(
"compose", "-f", $ComposeFile, "--env-file", $EnvFile, "build"
)
@@ -76,7 +84,7 @@ if (-not $ExportOnly) {
try {
$env:IMAGE_TAG = $Tag
& docker @buildArgs
- if ($LASTEXITCODE -ne 0) { throw "docker build 失败,退出码 $LASTEXITCODE" }
+ if ($LASTEXITCODE -ne 0) { throw "docker build ?????? $LASTEXITCODE" }
} finally {
$env:IMAGE_TAG = $oldImageTag
}
@@ -84,9 +92,9 @@ if (-not $ExportOnly) {
$OutputPath = if ([System.IO.Path]::IsPathRooted($Output)) { $Output } else { Join-Path $Root $Output }
-Write-Host "==> 导出镜像 ($Service) -> $OutputPath"
+Write-Host "==> ???? ($Service) -> $OutputPath"
& docker save @SaveImages -o $OutputPath
-if ($LASTEXITCODE -ne 0) { throw "docker save 失败,退出码 $LASTEXITCODE" }
+if ($LASTEXITCODE -ne 0) { throw "docker save ?????? $LASTEXITCODE" }
$manifestPath = if ($OutputPath.EndsWith(".tar")) {
$OutputPath.Substring(0, $OutputPath.Length - 4) + ".manifest.txt"
@@ -117,7 +125,7 @@ $manifestLines = @(
)
foreach ($svc in $BuildServices) {
$imageId = (& docker image inspect "thebet365-${svc}:${Tag}" --format "{{.Id}}").Trim()
- if ($Service -eq "all") {
+ if ($Service -eq "all" -or $Service -eq "api-admin") {
$manifestLines += "${svc}_image=thebet365-${svc}:${Tag}"
$manifestLines += "${svc}_image_id=$imageId"
} else {
@@ -128,13 +136,17 @@ foreach ($svc in $BuildServices) {
$manifestLines | Set-Content -Encoding UTF8 $manifestPath
$sizeMb = [math]::Round((Get-Item $OutputPath).Length / 1MB, 2)
-Write-Host "完成: $OutputPath (${sizeMb} MB)"
+Write-Host "??: $OutputPath (${sizeMb} MB)"
Write-Host "Manifest: $manifestPath"
-$recreate = if ($Service -eq "all") { "api player admin" } else { $Service }
+$recreate = switch ($Service) {
+ "all" { "api player admin" }
+ "api-admin" { "api admin" }
+ default { $Service }
+}
Write-Host @"
-服务器导入并重建:
+????????:
docker load -i $([System.IO.Path]::GetFileName($OutputPath))
docker compose -f docker-compose.prod.yml --env-file .env.docker up -d --force-recreate $recreate
"@
diff --git a/docs/docker/build-and-export-images.sh b/docs/docker/build-and-export-images.sh
index 39c9d63..5b04613 100755
--- a/docs/docker/build-and-export-images.sh
+++ b/docs/docker/build-and-export-images.sh
@@ -25,7 +25,7 @@ default_tag() { echo "latest"; }
default_output() {
local svc="$1" image_tag="$2"
- if [[ "$svc" == "all" ]]; then
+ if [[ "$svc" == "all" || "$svc" == "api-admin" ]]; then
echo "thebet365-images-${image_tag}.tar"
else
echo "thebet365-${svc}-${image_tag}.tar"
@@ -39,8 +39,8 @@ validate_tag() {
validate_service() {
case "$1" in
- all|api|player|admin) ;;
- *) echo "错误: --service 无效: $1(可选 api / player / admin / all)" >&2; exit 1 ;;
+ all|api|player|admin|api-admin) ;;
+ *) echo "错误: --service 无效: $1(可选 api / player / admin / api-admin / all)" >&2; exit 1 ;;
esac
}
@@ -49,7 +49,7 @@ usage() {
用法: docs/docker/build-and-export-images.sh [选项]
选项:
- --service SVC api | player | admin | all(默认 all)
+ --service SVC api | player | admin | api-admin | all(默认 all)
--tag TAG 镜像 tag(默认 latest)
--use-cache 构建时使用 Docker 缓存(默认 --no-cache)
--export-only 跳过构建,仅导出已有指定 tag 镜像
@@ -105,6 +105,8 @@ OUTPUT="${OUTPUT:-$(default_output "$SERVICE" "$TAG")}"
if [[ "$SERVICE" == "all" ]]; then
BUILD_SERVICES=(api player admin)
+elif [[ "$SERVICE" == "api-admin" ]]; then
+ BUILD_SERVICES=(api admin)
else
BUILD_SERVICES=("$SERVICE")
fi
@@ -161,7 +163,7 @@ fi
echo "tar=$(basename "$OUTPUT_PATH")"
echo "tar_sha256=$CHECKSUM"
for svc in "${BUILD_SERVICES[@]}"; do
- if [[ "$SERVICE" == "all" ]]; then
+ if [[ "$SERVICE" == "all" || "$SERVICE" == "api-admin" ]]; then
echo "${svc}_image=thebet365-${svc}:$TAG"
echo "${svc}_image_id=$(docker image inspect "thebet365-${svc}:$TAG" --format '{{.Id}}')"
else
@@ -182,6 +184,8 @@ echo "Manifest: $MANIFEST_PATH"
RECREATE="$SERVICE"
if [[ "$SERVICE" == "all" ]]; then
RECREATE="api player admin"
+elif [[ "$SERVICE" == "api-admin" ]]; then
+ RECREATE="api admin"
fi
cat <nul') do set "ORIGINAL_BRANCH=%%B"
+if not defined ORIGINAL_BRANCH (
+ echo ERROR: Cannot determine current Git branch.
+ exit /b 1
+)
+
+echo [INFO] Switching to branch %BRANCH%...
+git checkout %BRANCH% >nul 2>&1
+if errorlevel 1 (
+ echo [ERROR] Cannot switch to branch %BRANCH%
+ exit /b 1
+)
+
+call docs\docker\build-and-export-images.bat --service player --tag %TAG% %*
+set "BUILD_STATUS=%ERRORLEVEL%"
+
+echo [INFO] Restoring original branch: %ORIGINAL_BRANCH%
+git checkout %ORIGINAL_BRANCH% >nul 2>&1
+
+exit /b %BUILD_STATUS%
diff --git a/docs/docker/build-and-export-player-theme-2.bat b/docs/docker/build-and-export-player-theme-2.bat
new file mode 100644
index 0000000..acfa6fc
--- /dev/null
+++ b/docs/docker/build-and-export-player-theme-2.bat
@@ -0,0 +1,30 @@
+@echo off
+REM Build and export theme-2 player theme only.
+REM Automatically switches to 'theme-2' branch, builds, and restores original branch.
+REM Usage: docs\docker\build-and-export-player-theme-2.bat [options]
+
+cd /d "%~dp0..\.."
+set "BRANCH=theme-2"
+set "TAG=theme-2"
+
+set "ORIGINAL_BRANCH="
+for /f "delims=" %%B in ('git rev-parse --abbrev-ref HEAD 2^>nul') do set "ORIGINAL_BRANCH=%%B"
+if not defined ORIGINAL_BRANCH (
+ echo ERROR: Cannot determine current Git branch.
+ exit /b 1
+)
+
+echo [INFO] Switching to branch %BRANCH%...
+git checkout %BRANCH% >nul 2>&1
+if errorlevel 1 (
+ echo [ERROR] Cannot switch to branch %BRANCH%
+ exit /b 1
+)
+
+call docs\docker\build-and-export-images.bat --service player --tag %TAG% %*
+set "BUILD_STATUS=%ERRORLEVEL%"
+
+echo [INFO] Restoring original branch: %ORIGINAL_BRANCH%
+git checkout %ORIGINAL_BRANCH% >nul 2>&1
+
+exit /b %BUILD_STATUS%
diff --git a/docs/docker/build-and-export-player-theme-3.bat b/docs/docker/build-and-export-player-theme-3.bat
new file mode 100644
index 0000000..b17d4b2
--- /dev/null
+++ b/docs/docker/build-and-export-player-theme-3.bat
@@ -0,0 +1,30 @@
+@echo off
+REM Build and export theme-3 player theme only.
+REM Automatically switches to 'theme-3' branch, builds, and restores original branch.
+REM Usage: docs\docker\build-and-export-player-theme-3.bat [options]
+
+cd /d "%~dp0..\.."
+set "BRANCH=theme-3"
+set "TAG=theme-3"
+
+set "ORIGINAL_BRANCH="
+for /f "delims=" %%B in ('git rev-parse --abbrev-ref HEAD 2^>nul') do set "ORIGINAL_BRANCH=%%B"
+if not defined ORIGINAL_BRANCH (
+ echo ERROR: Cannot determine current Git branch.
+ exit /b 1
+)
+
+echo [INFO] Switching to branch %BRANCH%...
+git checkout %BRANCH% >nul 2>&1
+if errorlevel 1 (
+ echo [ERROR] Cannot switch to branch %BRANCH%
+ exit /b 1
+)
+
+call docs\docker\build-and-export-images.bat --service player --tag %TAG% %*
+set "BUILD_STATUS=%ERRORLEVEL%"
+
+echo [INFO] Restoring original branch: %ORIGINAL_BRANCH%
+git checkout %ORIGINAL_BRANCH% >nul 2>&1
+
+exit /b %BUILD_STATUS%
diff --git a/docs/docker/build-and-export-player-theme-4.bat b/docs/docker/build-and-export-player-theme-4.bat
new file mode 100644
index 0000000..2e20b6d
--- /dev/null
+++ b/docs/docker/build-and-export-player-theme-4.bat
@@ -0,0 +1,30 @@
+@echo off
+REM Build and export theme-4 player theme only.
+REM Automatically switches to 'theme-4' branch, builds, and restores original branch.
+REM Usage: docs\docker\build-and-export-player-theme-4.bat [options]
+
+cd /d "%~dp0..\.."
+set "BRANCH=theme-4"
+set "TAG=theme-4"
+
+set "ORIGINAL_BRANCH="
+for /f "delims=" %%B in ('git rev-parse --abbrev-ref HEAD 2^>nul') do set "ORIGINAL_BRANCH=%%B"
+if not defined ORIGINAL_BRANCH (
+ echo ERROR: Cannot determine current Git branch.
+ exit /b 1
+)
+
+echo [INFO] Switching to branch %BRANCH%...
+git checkout %BRANCH% >nul 2>&1
+if errorlevel 1 (
+ echo [ERROR] Cannot switch to branch %BRANCH%
+ exit /b 1
+)
+
+call docs\docker\build-and-export-images.bat --service player --tag %TAG% %*
+set "BUILD_STATUS=%ERRORLEVEL%"
+
+echo [INFO] Restoring original branch: %ORIGINAL_BRANCH%
+git checkout %ORIGINAL_BRANCH% >nul 2>&1
+
+exit /b %BUILD_STATUS%
diff --git a/docs/docker/镜像构建与导出.md b/docs/docker/镜像构建与导出.md
index 3717c7b..e709d50 100644
--- a/docs/docker/镜像构建与导出.md
+++ b/docs/docker/镜像构建与导出.md
@@ -14,8 +14,14 @@
|------|----------|
| `docs/docker/build-and-export-images.bat` | Windows(CMD,构建全部) |
| `docs/docker/build-and-export-api.bat` | Windows(仅 api,可双击) |
-| `docs/docker/build-and-export-player.bat` | Windows(仅 player,可双击) |
+| `docs/docker/build-and-export-player.bat` | Windows(仅单分支 player,可双击) |
+| `docs/docker/build-and-export-player-main.bat` | Windows(仅主站/暗金主题,自动切分支) |
+| `docs/docker/build-and-export-player-theme-2.bat` | Windows(仅 theme-2 蓝白主题,自动切分支) |
+| `docs/docker/build-and-export-player-theme-3.bat` | Windows(仅 theme-3 移动主题,自动切分支) |
+| `docs/docker/build-and-export-player-theme-4.bat` | Windows(仅 theme-4 海军蓝极简,自动切分支) |
| `docs/docker/build-and-export-admin.bat` | Windows(仅 admin,可双击) |
+| `docs/docker/build-and-export-all-themes.ps1` | Windows(**四套主题 player 一键打包**,自动切分支,推荐) |
+| `docs/docker/build-and-export-all-themes.bat` | Windows(同上,薄包装启动器,双击可用) |
| `docs/docker/build-and-export-images.ps1` | Windows(PowerShell) |
| `docs/docker/build-and-export-images.sh` | Linux / macOS / Git Bash |
@@ -118,6 +124,81 @@ docs\docker\build-and-export-images.bat --export-only
---
+## 三、四套主题玩家端打包
+
+> 关联文档:[四套主题Docker部署任务.md](../四套主题Docker部署任务.md)「阶段 C」
+
+### 一键打包(推荐)
+
+脚本会自动:切到各主题分支 → 构建 player 镜像 → 导出 tar → 切回原分支 → 构建 api/admin。
+
+> **为何用 PowerShell?** 本脚本仅存在于 `main` 分支;执行过程中会 `git checkout theme-*`,CMD 批处理会从磁盘逐行读取,切分支后脚本文件消失会导致中断。PowerShell 启动时已将整份脚本载入内存,可安全跨分支执行。`.bat` 仅为薄包装,内部转调 `.ps1`。
+
+```powershell
+cd C:\path\to\thebet365
+.\docs\docker\build-and-export-all-themes.ps1
+```
+
+或双击 / CMD:
+
+```bat
+docs\docker\build-and-export-all-themes.bat
+```
+
+可选参数(PowerShell 与 bat 均支持):
+
+| 参数 | 说明 |
+|------|------|
+| `-UseCache` / `--use-cache` | 使用 Docker 层缓存,加快重复构建速度 |
+| `-ExportOnly` / `--export-only` | 跳过构建,仅导出本地已有镜像 |
+| `-SkipApiAdmin` / `--skip-api-admin` | 只打四套 player,跳过 api/admin |
+| `-SkipBundle` / `--skip-bundle` | 跳过六合一包,仅保留分散 tar |
+
+### 产物
+
+| tar 文件 | 加载后镜像名 |
+|----------|----------------|
+| `thebet365-player-main.tar` | `thebet365-player:main` |
+| `thebet365-player-theme-2.tar` | `thebet365-player:theme-2` |
+| `thebet365-player-theme-3.tar` | `thebet365-player:theme-3` |
+| `thebet365-player-theme-4.tar` | `thebet365-player:theme-4` |
+| `thebet365-images-latest.tar` | `thebet365-api:latest` + `thebet365-admin:latest` |
+| **`thebet365-full-themes-latest.tar`** | **上面全部 6 个镜像(上传这一个即可)** |
+
+也可单独导出六合一包(本地镜像已齐时):
+
+```bat
+docs\docker\build-and-export-images.bat --service full-themes --export-only --tag latest
+```
+
+### 分支要求
+
+- 执行前确保 `main`、`theme-2`、`theme-3`、`theme-4` 四个分支在本地均已拉取
+- 工作区若有未提交变更,脚本会给出 WARN;建议先 `git stash` 后再运行
+- 若某一分支切换失败,该分支会被跳过并标记失败,其余分支仍继续构建
+
+### 手动逐一打包(有明确失败时)
+
+```bat
+git checkout main
+docs\docker\build-and-export-images.bat --service player --tag main
+
+git checkout theme-2
+docs\docker\build-and-export-images.bat --service player --tag theme-2
+
+git checkout theme-3
+docs\docker\build-and-export-images.bat --service player --tag theme-3
+
+git checkout theme-4
+docs\docker\build-and-export-images.bat --service player --tag theme-4
+
+REM 最后打 api + admin
+git checkout main
+docs\docker\build-and-export-images.bat --tag latest
+```
+
+---
+
## 四、构建产物
| 镜像名 | 说明 |
diff --git a/docs/四套主题Docker部署任务.md b/docs/四套主题Docker部署任务.md
index 29bc27b..a1032e1 100644
--- a/docs/四套主题Docker部署任务.md
+++ b/docs/四套主题Docker部署任务.md
@@ -37,11 +37,11 @@
### 阶段 A — 仓库改动(一次性)
-- [ ] **A1** 新增 `docker-compose.themes.yml`(player2 / player3 / player4 服务定义)
-- [ ] **A2** 更新 `.env.docker.example`(`PLAYER2_*` ~ `PLAYER4_*`、`CORS_ORIGINS` 示例)
-- [ ] **A3** 更新 `docker-compose.prod.yml` 中 `player` 使用 `PLAYER_IMAGE_TAG`(与 `IMAGE_TAG` 解耦,见下文片段)
-- [ ] **A4**(可选)在 `docs/Docker部署指南.md` 增加「多主题」小节链接到本文
-- [ ] **A5**(可选)在 `AGENTS.md` 文档索引增加本文链接
+- [x] **A1** 新增 `docker-compose.themes.yml`(player2 / player3 / player4 服务定义)
+- [x] **A2** 更新 `.env.docker.example`(`PLAYER2_*` ~ `PLAYER4_*`、`CORS_ORIGINS` 示例)
+- [x] **A3** 更新 `docker-compose.prod.yml` 中 `player` 使用 `PLAYER_IMAGE_TAG`(与 `IMAGE_TAG` 解耦,见下文片段)
+- [x] **A4**(可选)在 `docs/Docker部署指南.md` 增加「多主题」小节链接到本文
+- [x] **A5**(可选)在 `AGENTS.md` 文档索引增加本文链接
### 阶段 B — 各主题分支同步 main 功能(发版前每个 theme 分支各做一遍)
@@ -276,7 +276,18 @@ pnpm dev:player # http://localhost:5173 目视确认仍是本主题皮
## 六、构建与上传镜像
-### 6.1 四个 player 镜像(Windows CMD)
+### 6.1 四个 player 镜像
+
+**一键打包(推荐,仅 `main` 分支提供脚本):**
+
+```powershell
+cd C:\path\to\thebet365
+.\docs\docker\build-and-export-all-themes.ps1 -SkipApiAdmin
+```
+
+或 `docs\docker\build-and-export-all-themes.bat --skip-api-admin`(内部转调 PowerShell,避免切分支后 bat 从磁盘消失)。
+
+**手动逐步(Windows CMD):**
```bat
cd C:\path\to\thebet365
diff --git a/scripts/ensure-port-free.mjs b/scripts/ensure-port-free.mjs
index fddbc29..fe84f38 100644
--- a/scripts/ensure-port-free.mjs
+++ b/scripts/ensure-port-free.mjs
@@ -1,7 +1,7 @@
import { execSync } from 'node:child_process';
-import { resolveApiDevPort } from './dev-api-target.mjs';
+import { resolveDevApiPort } from './resolve-dev-api-target.mjs';
-const port = process.argv[2] ?? resolveApiDevPort();
+const port = process.argv[2] ?? resolveDevApiPort();
function killWindows(portNum) {
let out = '';
diff --git a/scripts/resolve-dev-api-target.mjs b/scripts/resolve-dev-api-target.mjs
new file mode 100644
index 0000000..8402062
--- /dev/null
+++ b/scripts/resolve-dev-api-target.mjs
@@ -0,0 +1,17 @@
+import { existsSync, readFileSync } from 'node:fs';
+import { resolve, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const envPath = resolve(root, 'apps/api/.env');
+
+/** 读取 apps/api/.env 的 PORT,供 Vite 代理与 dev 脚本共用 */
+export function resolveDevApiPort(fallback = '3000') {
+ if (!existsSync(envPath)) return fallback;
+ const match = readFileSync(envPath, 'utf8').match(/^PORT=(\d+)/m);
+ return match?.[1] ?? fallback;
+}
+
+export function resolveDevApiTarget(fallbackPort = '3000') {
+ return `http://127.0.0.1:${resolveDevApiPort(fallbackPort)}`;
+}
diff --git a/skills-lock.json b/skills-lock.json
index e0caf0f..310fcb9 100644
--- a/skills-lock.json
+++ b/skills-lock.json
@@ -61,12 +61,24 @@
"skillPath": "skills/minimalist-skill/SKILL.md",
"computedHash": "08873a3131d3be27bef9bf3304b310b16b44ca6e3561aebe532797be3443f6bd"
},
+ "receiving-code-review": {
+ "source": "obra/superpowers",
+ "sourceType": "github",
+ "skillPath": "skills/receiving-code-review/SKILL.md",
+ "computedHash": "3f56080356c62e4f74a183d7371686babc661e41a952dc67a97c2c76fe8a9329"
+ },
"redesign-existing-projects": {
"source": "leonxlnx/taste-skill",
"sourceType": "github",
"skillPath": "skills/redesign-skill/SKILL.md",
"computedHash": "b405eee0e0e80fc243f731d9aa368bca307e356db7e6157d27101d369dac6726"
},
+ "requesting-code-review": {
+ "source": "obra/superpowers",
+ "sourceType": "github",
+ "skillPath": "skills/requesting-code-review/SKILL.md",
+ "computedHash": "cde520e9118d7e6b74b5ab0123cff2f68d9c07bddb3f82c997753b6126600aed"
+ },
"stitch-design-taste": {
"source": "leonxlnx/taste-skill",
"sourceType": "github",