diff --git a/apps/admin/src/App.vue b/apps/admin/src/App.vue index 8cbaf7c..17202e8 100644 --- a/apps/admin/src/App.vue +++ b/apps/admin/src/App.vue @@ -92,16 +92,42 @@ html, body, #app { overflow: hidden; } -/* 隐藏滚动条,表格区域仍可滚动 */ -* { +/* 页面级隐藏滚动条;可滚动区域保留细滚动条便于发现溢出内容 */ +html, body { scrollbar-width: none; -ms-overflow-style: none; } -*::-webkit-scrollbar { +html::-webkit-scrollbar, +body::-webkit-scrollbar { width: 0; height: 0; display: none; } +.admin-list-page .table-wrap, +.dashboard-page, +.page-scroll, +.settlement-page, +.nav { + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.18) transparent; +} +.admin-list-page .table-wrap::-webkit-scrollbar, +.dashboard-page::-webkit-scrollbar, +.page-scroll::-webkit-scrollbar, +.settlement-page::-webkit-scrollbar, +.nav::-webkit-scrollbar { + width: 6px; + height: 6px; + display: block; +} +.admin-list-page .table-wrap::-webkit-scrollbar-thumb, +.dashboard-page::-webkit-scrollbar-thumb, +.page-scroll::-webkit-scrollbar-thumb, +.settlement-page::-webkit-scrollbar-thumb, +.nav::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.16); + border-radius: 3px; +} /* 管理端列表页:占满主区域,表头固定、表体滚动,底部分页 */ .admin-list-page { @@ -113,7 +139,9 @@ html, body, #app { } .admin-list-page > .page-toolbar, .admin-list-page > .filter-card, -.admin-list-page > .tool-card { +.admin-list-page > .tool-card, +.admin-list-page > .list-chrome, +.admin-list-page > .list-settings { flex-shrink: 0; } .admin-list-page > .page-toolbar { @@ -121,27 +149,180 @@ html, body, #app { justify-content: flex-end; align-items: center; gap: 8px; - margin-bottom: 16px; + margin: 0 0 8px; } .admin-list-page > .tool-card { - margin-bottom: 16px; + margin-bottom: 8px; } .admin-list-page > .filter-card { - margin-bottom: 16px; + margin-bottom: 8px; } -.admin-list-page > .data-card { +.admin-list-page > .list-chrome { + margin-bottom: 8px; + display: flex; + flex-direction: column; + gap: 6px; +} +.list-chrome__row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: nowrap; + padding: 8px 10px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.05); + --list-chrome-control-h: 32px; + --el-component-size: 32px; +} +.list-chrome__left { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + gap: 12px; + flex-wrap: nowrap; +} +.list-chrome__filters { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px 12px; + flex: 1; + min-width: 0; +} +.list-chrome__field { + display: inline-flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} +.list-chrome__label { + flex-shrink: 0; + font-size: 11px; + font-weight: 600; + color: var(--text-muted); + letter-spacing: 0.04em; + line-height: 1; + white-space: nowrap; +} +.list-chrome__grow { + flex: 1; + min-width: 0; + display: flex; + flex-wrap: wrap; + align-items: center; + row-gap: 8px; +} +.list-chrome__grow.el-form--inline { + display: flex !important; + align-items: center; +} +.list-chrome__left .matches-subnav--embedded { + align-self: center; + height: var(--list-chrome-control-h); +} +.list-chrome__actions { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; +} +.list-chrome :deep(.el-form-item) { + margin-bottom: 0 !important; + margin-right: 12px; + display: inline-flex; + align-items: center; + vertical-align: middle; +} +.list-chrome :deep(.el-form-item__label) { + display: inline-flex; + align-items: center; + height: var(--list-chrome-control-h); + line-height: 1; + padding: 0 8px 0 0; + margin-bottom: 0 !important; +} +.list-chrome :deep(.el-form-item__content) { + display: inline-flex; + align-items: center; + line-height: 1; +} +.list-chrome :deep(.el-input__wrapper), +.list-chrome :deep(.el-select__wrapper) { + height: var(--list-chrome-control-h) !important; + min-height: var(--list-chrome-control-h) !important; + box-sizing: border-box; + padding-top: 0; + padding-bottom: 0; +} +.list-chrome :deep(.el-input__inner) { + height: calc(var(--list-chrome-control-h) - 2px); + line-height: calc(var(--list-chrome-control-h) - 2px); +} +.list-chrome :deep(.el-button:not(.is-link)) { + height: var(--list-chrome-control-h) !important; + min-height: var(--list-chrome-control-h) !important; + padding-top: 0 !important; + padding-bottom: 0 !important; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + line-height: 1; +} +.list-chrome :deep(.el-form-item:last-child) { + margin-right: 0; +} +.admin-list-page > .list-settings { + margin-bottom: 8px; +} +.list-settings :deep(.el-collapse-item__header) { + height: 36px; + line-height: 36px; + padding: 0 10px; + font-size: 13px; + font-weight: 600; + background: rgba(255, 255, 255, 0.02); + border-color: rgba(255, 255, 255, 0.05); +} +.list-settings :deep(.el-collapse-item__wrap) { + border-color: rgba(255, 255, 255, 0.05); +} +.list-settings :deep(.el-collapse-item__content) { + padding: 8px 10px 10px; +} +.list-settings-block + .list-settings-block { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} +.list-settings-title { + font-size: 12px; + font-weight: 600; + color: #aaa; + margin-bottom: 6px; +} +.admin-list-page > .data-card, +.admin-list-page > .list-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; - border-radius: 12px; + border-radius: 10px; +} +.admin-list-page > .list-panel { + border: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(18, 18, 18, 0.85); + padding: 0 10px 10px; } .admin-list-page > .data-card .el-card__body { flex: 1; min-height: 0; display: flex; flex-direction: column; - padding-bottom: 16px; + padding: 0 12px 12px; } .admin-list-page .table-wrap { flex: 1; @@ -154,11 +335,19 @@ html, body, #app { .admin-list-page .table-wrap .el-table th.el-table__cell .cell { white-space: nowrap; } +.admin-list-page .list-hint { + flex-shrink: 0; + margin: 0; + padding: 6px 0 8px; + font-size: 11px; + color: #666; + line-height: 1.4; +} .admin-list-page .pager { flex-shrink: 0; display: flex; justify-content: flex-end; - margin-top: 16px; + margin-top: 8px; padding-top: 0; } @@ -259,6 +448,60 @@ body { } .el-button--warning { background: rgba(251,191,36,0.1) !important; border-color: rgba(251,191,36,0.35) !important; color: #fbbf24 !important; } .el-button--danger { background: rgba(255,69,58,0.1) !important; border-color: rgba(255,69,58,0.35) !important; color: #ff453a !important; } +.el-button--primary.is-plain { + background: rgba(36, 143, 84, 0.12) !important; + border-color: var(--green-border) !important; + color: var(--green-text) !important; + box-shadow: none !important; +} +.el-button--primary.is-plain:hover { + background: rgba(36, 143, 84, 0.22) !important; + border-color: rgba(120, 230, 170, 0.45) !important; + color: #d4fde5 !important; +} +.el-button--danger.is-plain { + background: rgba(255, 69, 58, 0.08) !important; + border-color: rgba(255, 69, 58, 0.35) !important; + color: #ff6b62 !important; + box-shadow: none !important; +} +.el-button--danger.is-plain:hover { + background: rgba(255, 69, 58, 0.16) !important; + border-color: rgba(255, 120, 110, 0.5) !important; + color: #ff8a82 !important; +} +.el-button.is-text, +.el-button.is-link.el-button--default { + color: var(--green-text) !important; + background: transparent !important; + border-color: transparent !important; + box-shadow: none !important; +} +.el-button.is-text:hover, +.el-button.is-link.el-button--default:hover { + color: #d4fde5 !important; + background: rgba(36, 143, 84, 0.1) !important; +} +.el-button--primary.is-plain { + background: rgba(36, 143, 84, 0.12) !important; + border-color: var(--green-border) !important; + color: var(--green-text) !important; +} +.el-button--primary.is-plain:hover { + background: rgba(36, 143, 84, 0.22) !important; + border-color: rgba(120, 230, 170, 0.45) !important; + color: #d4fde5 !important; +} +.el-button--danger.is-plain { + background: rgba(255, 69, 58, 0.08) !important; + border-color: rgba(255, 69, 58, 0.35) !important; + color: #ff6961 !important; +} +.el-button--danger.is-plain:hover { + background: rgba(255, 69, 58, 0.18) !important; + border-color: rgba(255, 120, 110, 0.5) !important; + color: #ff8a82 !important; +} .el-tag { border-radius: 4px !important; font-size: 11px !important; font-weight: 600 !important; } .el-tag--success { diff --git a/apps/admin/src/components/AdminSubNav.vue b/apps/admin/src/components/AdminSubNav.vue new file mode 100644 index 0000000..a3f27bf --- /dev/null +++ b/apps/admin/src/components/AdminSubNav.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/apps/admin/src/components/AdminTableEmpty.vue b/apps/admin/src/components/AdminTableEmpty.vue new file mode 100644 index 0000000..d86b262 --- /dev/null +++ b/apps/admin/src/components/AdminTableEmpty.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/apps/admin/src/components/MatchesSubNav.vue b/apps/admin/src/components/MatchesSubNav.vue new file mode 100644 index 0000000..4ca52d3 --- /dev/null +++ b/apps/admin/src/components/MatchesSubNav.vue @@ -0,0 +1,93 @@ + + + + + diff --git a/apps/admin/src/data/builtinCountries.ts b/apps/admin/src/data/builtinCountries.ts index 2515a57..24e23e5 100644 --- a/apps/admin/src/data/builtinCountries.ts +++ b/apps/admin/src/data/builtinCountries.ts @@ -155,6 +155,20 @@ export function countryOptionLabel(c: BuiltinCountry, locale: string): string { return `${countryDisplayName(c, locale)} (${c.code})`; } +/** 冠军盘队伍行:按后台语言只显示一种队名 */ +export function teamRowDisplayName( + row: { teamCode: string; teamZh: string; teamEn: string }, + locale: string, +): string { + const builtin = getBuiltinCountry(row.teamCode); + if (builtin) return countryDisplayName(builtin, locale); + if (locale === 'en-US') return row.teamEn || row.teamZh || row.teamCode; + if (locale === 'ms-MY') { + return NATIONAL_TEAM_MS[row.teamCode] ?? row.teamEn ?? row.teamZh ?? row.teamCode; + } + return row.teamZh || row.teamEn || row.teamCode; +} + function countryNameMs(c: BuiltinCountry): string { return NATIONAL_TEAM_MS[c.code] ?? c.nameEn; } diff --git a/apps/admin/src/i18n/admin-messages.ts b/apps/admin/src/i18n/admin-messages.ts index cfba5e8..5087abe 100644 --- a/apps/admin/src/i18n/admin-messages.ts +++ b/apps/admin/src/i18n/admin-messages.ts @@ -29,7 +29,7 @@ const zh: Record = { 'login.captcha_ph': '验证码', 'login.captcha_refresh': '点击刷新', - 'nav.dashboard': '控制台', + 'nav.dashboard': '概览', 'nav.users': '玩家管理', 'nav.agents': '代理管理', 'nav.matches': '赛事管理', @@ -41,6 +41,12 @@ const zh: Record = { 'nav.players': '直属玩家', 'nav.subAgents': '下级代理', 'nav.myBets': '注单查询', + 'nav.open_menu': '打开菜单', + 'nav.close_menu': '关闭菜单', + 'breadcrumb.settlement': '赛事结算', + 'breadcrumb.match_edit': '编辑赛事', + 'breadcrumb.match_markets': '盘口管理', + 'breadcrumb.outright_edit': '编辑优胜冠军', 'role.admin': '系统管理员', 'role.agent': '代理账号', 'logout': '退出', @@ -57,10 +63,12 @@ const zh: Record = { 'common.delete': '删除', 'common.cancel': '取消', 'common.confirm': '确定', + 'common.retry': '重试', 'common.status': '状态', 'common.type': '类型', 'common.keyword': '关键词', 'common.actions': '操作', + 'common.more': '更多', 'common.loading': '加载中…', 'common.no_data': '暂无数据', 'common.yesterday': '昨日', @@ -73,7 +81,7 @@ const zh: Record = { 'common.platform_direct': '平台直属', 'common.updated_at': '更新于', - 'dash.title': '控制台', + 'dash.title': '概览', 'dash.desc': '平台整体运行概况', 'dash.board_title': '整体概览', 'dash.board_hint': '一屏查看经营趋势与平台分布', @@ -85,6 +93,7 @@ const zh: Record = { 'dash.kpi_new_players': '今日新增 {n} 人', 'dash.kpi_pending': '待结算', 'dash.kpi_pending_sub': '{bets} 单 · {matches} 场赛事', + 'dash.load_error_hint': '无法加载概览数据,请检查网络或稍后重试。', 'dash.kpi_wallet': '玩家余额', 'dash.kpi_credit': '代理授信', 'dash.trend_caption': '近 7 日经营趋势(金额折线 + 注单柱)', @@ -114,6 +123,8 @@ const zh: Record = { 'page.users.desc': '创建玩家、查看余额与投注概况,支持上分与状态管理', 'page.agents.title': '代理管理', 'page.agents.desc': '创建一级代理、调整授信额度、查看直属玩家与额度占用', + 'nav.matches.fixtures': '赛事配置', + 'nav.matches.outrights': '优胜赛配置(盘口)', 'page.matches.title': '赛事管理', 'page.matches.desc': '草稿可编辑、删除;已发布可改开赛时间与热门', 'page.bets.title': '注单管理', @@ -182,7 +193,7 @@ const en: Record = { 'login.captcha_ph': 'Captcha', 'login.captcha_refresh': 'Click to refresh', - 'nav.dashboard': 'Dashboard', + 'nav.dashboard': 'Overview', 'nav.users': 'Players', 'nav.agents': 'Agents', 'nav.matches': 'Matches', @@ -194,6 +205,12 @@ const en: Record = { 'nav.players': 'My Players', 'nav.subAgents': 'Sub-Agents', 'nav.myBets': 'Bet Search', + 'nav.open_menu': 'Open menu', + 'nav.close_menu': 'Close menu', + 'breadcrumb.settlement': 'Settlement', + 'breadcrumb.match_edit': 'Edit match', + 'breadcrumb.match_markets': 'Markets', + 'breadcrumb.outright_edit': 'Edit outright', 'role.admin': 'Administrator', 'role.agent': 'Agent', 'logout': 'Logout', @@ -210,10 +227,12 @@ const en: Record = { 'common.delete': 'Delete', 'common.cancel': 'Cancel', 'common.confirm': 'OK', + 'common.retry': 'Retry', 'common.status': 'Status', 'common.type': 'Type', 'common.keyword': 'Keyword', 'common.actions': 'Actions', + 'common.more': 'More', 'common.loading': 'Loading…', 'common.no_data': 'No data', 'common.yesterday': 'Yesterday', @@ -226,7 +245,7 @@ const en: Record = { 'common.platform_direct': 'Platform direct', 'common.updated_at': 'Updated', - 'dash.title': 'Dashboard', + 'dash.title': 'Overview', 'dash.desc': 'Platform overview', 'dash.board_title': 'Overview', 'dash.board_hint': 'Trends and distribution at a glance', @@ -238,6 +257,7 @@ const en: Record = { 'dash.kpi_new_players': '{n} new today', 'dash.kpi_pending': 'Pending settlement', 'dash.kpi_pending_sub': '{bets} bets · {matches} matches', + 'dash.load_error_hint': 'Could not load overview data. Check your connection and try again.', 'dash.kpi_wallet': 'Player balance', 'dash.kpi_credit': 'Agent credit', 'dash.trend_caption': 'Last 7 days (amount lines + bet bars)', @@ -267,6 +287,8 @@ const en: Record = { 'page.users.desc': 'Create players, balances, stakes, top-ups, and status', 'page.agents.title': 'Agents', 'page.agents.desc': 'Tier-1 agents, credit limits, players, and usage', + 'nav.matches.fixtures': 'Fixtures', + 'nav.matches.outrights': 'Outright odds', 'page.matches.title': 'Matches', 'page.matches.desc': 'Edit/delete drafts; adjust kickoff and featured when published', 'page.bets.title': 'Bets', @@ -335,7 +357,7 @@ const ms: Record = { 'login.captcha_ph': 'Captcha', 'login.captcha_refresh': 'Klik untuk muat semula', - 'nav.dashboard': 'Papan pemuka', + 'nav.dashboard': 'Gambaran', 'nav.users': 'Pemain', 'nav.agents': 'Ejen', 'nav.matches': 'Perlawanan', @@ -347,6 +369,12 @@ const ms: Record = { 'nav.players': 'Pemain saya', 'nav.subAgents': 'Sub-ejen', 'nav.myBets': 'Carian pertaruhan', + 'nav.open_menu': 'Buka menu', + 'nav.close_menu': 'Tutup menu', + 'breadcrumb.settlement': 'Penyelesaian', + 'breadcrumb.match_edit': 'Edit perlawanan', + 'breadcrumb.match_markets': 'Pasaran', + 'breadcrumb.outright_edit': 'Edit juara', 'role.admin': 'Pentadbir', 'role.agent': 'Ejen', 'logout': 'Log keluar', @@ -363,10 +391,12 @@ const ms: Record = { 'common.delete': 'Padam', 'common.cancel': 'Batal', 'common.confirm': 'OK', + 'common.retry': 'Cuba lagi', 'common.status': 'Status', 'common.type': 'Jenis', 'common.keyword': 'Kata kunci', 'common.actions': 'Tindakan', + 'common.more': 'Lagi', 'common.loading': 'Memuatkan…', 'common.no_data': 'Tiada data', 'common.yesterday': 'Semalam', @@ -379,7 +409,7 @@ const ms: Record = { 'common.platform_direct': 'Terus platform', 'common.updated_at': 'Dikemas kini', - 'dash.title': 'Papan pemuka', + 'dash.title': 'Gambaran', 'dash.desc': 'Gambaran keseluruhan platform', 'dash.board_title': 'Gambaran', 'dash.board_hint': 'Trend dan taburan sepintas lalu', @@ -391,6 +421,7 @@ const ms: Record = { 'dash.kpi_new_players': '{n} baharu hari ini', 'dash.kpi_pending': 'Menunggu penyelesaian', 'dash.kpi_pending_sub': '{bets} pertaruhan · {matches} perlawanan', + 'dash.load_error_hint': 'Tidak dapat memuatkan data gambaran. Semak sambungan dan cuba lagi.', 'dash.kpi_wallet': 'Baki pemain', 'dash.kpi_credit': 'Kredit ejen', 'dash.trend_caption': '7 hari lepas (garis jumlah + palang pertaruhan)', @@ -420,6 +451,8 @@ const ms: Record = { 'page.users.desc': 'Cipta pemain, baki, stake, tambah baki dan status', 'page.agents.title': 'Ejen', 'page.agents.desc': 'Ejen peringkat 1, had kredit, pemain dan penggunaan', + 'nav.matches.fixtures': 'Konfigurasi perlawanan', + 'nav.matches.outrights': 'Odds juara', 'page.matches.title': 'Perlawanan', 'page.matches.desc': 'Edit/padam draf; laraskan masa mula dan pilihan utama bila diterbitkan', 'page.bets.title': 'Pertaruhan', diff --git a/apps/admin/src/i18n/admin-pages-ms.ts b/apps/admin/src/i18n/admin-pages-ms.ts index 25ec70d..fcead04 100644 --- a/apps/admin/src/i18n/admin-pages-ms.ts +++ b/apps/admin/src/i18n/admin-pages-ms.ts @@ -60,6 +60,7 @@ export const adminPagesMs: Record = { 'user.btn.hide_password': 'Sembunyi', 'user.ph.reset_password': 'Biarkan kosong untuk kekalkan; nilai baharu boleh dilihat', 'user.ph.reset_password_short': 'Biarkan kosong', + 'user.page_settings': 'Tetapan global', 'user.global_settings': 'Kata laluan & akaun (global)', 'user.global_settings_hint': 'Kawal sama ada semua pemain boleh ubah kata laluan/nama akaun dalam app', 'user.section.password_mgmt': 'Pengurusan kata laluan', @@ -128,6 +129,7 @@ export const adminPagesMs: Record = { 'match.btn.markets': 'Pasaran', 'match.filter.keyword_ph': 'Nama kejohanan / kod pasukan', 'match.col.league': 'Kejohanan', + 'match.col.league_en': 'Liga (EN)', 'match.col.fixture_count': 'Perlawanan', 'match.col.bet_count': 'Pertaruhan', 'match.col.total_stake': 'Jumlah stake', @@ -148,6 +150,8 @@ export const adminPagesMs: Record = { 'match.field.lang_en': 'EN', 'match.field.lang_ms': 'MS', 'match.field.kickoff': 'Masa mula', + 'match.field.home_team': 'Pasukan tuan rumah', + 'match.field.away_team': 'Pasukan pelawat', 'match.field.home_en': 'Tuan rumah (EN)', 'match.field.home_zh': 'Tuan rumah (ZH)', 'match.field.home_ms': 'Tuan rumah (MS)', @@ -158,7 +162,14 @@ export const adminPagesMs: Record = { 'match.hint.create_draft': 'Disimpan sebagai draf; kembangkan kejohanan dan terbitkan setiap perlawanan tunggal.', 'match.hint.create_league': 'Cipta kejohanan dahulu, kemudian kembangkan untuk tambah perlawanan tunggal.', 'match.hint.edit_published': 'Diterbitkan: edit masa mula, pilihan utama, nama paparan; tertutup/selesai dikunci.', - 'match.expand_league_hint': 'Kembangkan kejohanan untuk senarai perlawanan; klik Pasaran untuk halaman tetapan odds (sama seperti aplikasi pemain).', + 'match.expand_league_hint': 'Kembangkan liga untuk urus perlawanan; odds juara di tab Odds juara.', + 'match.expand_outright_hint': 'Kembangkan liga untuk sunting odds juara; pasukan perlawanan disegerakkan auto, boleh tambah pasukan belum dijadualkan.', + 'outright.odds_only_hint': 'Pasukan daripada perlawanan disegerakkan auto; boleh tambah pasukan manual dan sunting odds di sini.', + 'outright.col.teams_from_fixtures': 'Pasukan (daripada perlawanan)', + 'outright.col.teams_total': 'Pasukan odds juara', + 'outright.empty_no_teams': 'Tiada pasukan — tambah perlawanan di Konfigurasi atau klik Tambah pasukan.', + 'match.outright.setup': 'Sediakan', + 'match.outright.section_hint': 'Pasaran juara untuk liga ini; perlawanan disenaraikan di bawah', 'match.expand_markets_hint': 'Klik Pasaran pada perlawanan tunggal untuk halaman pasaran berasingan.', 'match.no_fixtures': 'Tiada perlawanan tunggal di bawah kejohanan ini.', 'match.ph.league_ms': 'Piala Dunia 2027', @@ -173,6 +184,7 @@ export const adminPagesMs: Record = { 'bet.col.agent': 'Ejen', 'bet.col.selection': 'Pilihan', 'bet.col.content': 'Kandungan taruhan', + 'bet.content.bet_counts': '{singles} tunggal · {parlays} parlay', 'bet.col.match': 'Perlawanan', 'bet.legs_more': '+{n} lagi…', 'bet.col.selection_count': 'Bil. pilihan', @@ -319,6 +331,8 @@ export const adminPagesMs: Record = { 'err.password_mismatch': 'Kata laluan tidak sepadan', 'err.credit_negative': 'Had kredit tidak boleh negatif', 'err.kickoff_required': 'Sila isi masa mula', + 'err.team_country_required': 'Pilih pasukan tuan rumah dan pelawat', + 'err.team_country_required': 'Pilih pasukan tuan rumah dan pelawat', 'err.teams_required': 'Isi nama pasukan tuan rumah dan pelawat (ZH atau EN)', 'err.teams_same': 'Pasukan tuan rumah dan pelawat mesti berbeza', 'err.league_required': 'Sila isi nama liga', @@ -485,6 +499,37 @@ export const adminPagesMs: Record = { 'outright.field.title_en': 'Tajuk (EN)', 'outright.field.title_ms': 'Tajuk (MS)', 'outright.btn.create_event': 'Acara juara baharu', + 'outright.fixtures_sync_hint': 'Pasukan daripada perlawanan liga; hanya laraskan odds dan status terbit.', + 'outright.empty_no_fixtures': 'Tiada perlawanan dalam liga ini — tambah di Konfigurasi perlawanan dahulu.', + 'outright.btn.add_team': 'Tambah pasukan', + 'outright.add.filter_fixture': 'Pasukan sedia ada', + 'outright.add.filter_all': 'Semua terbina dalam', + 'outright.add.select_all': 'Pilih semua', + 'outright.add.clear_selection': 'Kosongkan pilihan', + 'outright.add.selected_count': '{n} dipilih', + 'outright.add.empty_fixture': 'Tiada pasukan perlawanan untuk ditambah (pasukan dalam perlawanan tetapi belum dalam pasaran juara)', + 'outright.add.empty_all': 'Semua pasukan terbina dalam sudah dalam pasaran juara', + 'outright.add.default_odds': 'Odds lalai', + 'outright.add.search_ph': 'Cari nama atau kod', + 'outright.add.err_none': 'Sila pilih sekurang-kurangnya satu pasukan', + 'outright.batch.mode': 'Urus kelompok', + 'outright.batch.exit': 'Keluar kelompok', + 'outright.batch.apply_odds': 'Guna odds', + 'outright.batch.remove': 'Buang terpilih', + 'outright.batch.confirm_remove': 'Buang {n} pasukan terpilih?', + 'outright.batch.err_none': 'Sila pilih pasukan dahulu', + 'outright.batch.apply_ok': 'Odds {n} pasukan dikemas kini — simpan semua odds', + 'outright.batch.remove_ok': '{n} pasukan dibuang', + 'outright.batch.remove_partial': '{ok} berjaya, {fail} gagal', + 'outright.sort.label': 'Susun', + 'outright.sort.rank': 'Kedudukan', + 'outright.sort.name': 'Nama pasukan', + 'outright.sort.code': 'Kod', + 'outright.sort.odds': 'Odds (semasa)', + 'outright.sort.saved_odds': 'Odds (disimpan)', + 'outright.sort.asc': 'Menaik', + 'outright.sort.desc': 'Menurun', + 'msg.outright_teams_added': '{n} pasukan ditambah ({skipped} dilangkau)', 'msg.load_matches_failed': 'Gagal memuatkan perlawanan', 'msg.cashback_issued': 'Rebat telah dikeluarkan', 'msg.freeze_confirm_title': '{action} akaun', diff --git a/apps/admin/src/i18n/admin-pages.ts b/apps/admin/src/i18n/admin-pages.ts index 1bf6d83..95a42e4 100644 --- a/apps/admin/src/i18n/admin-pages.ts +++ b/apps/admin/src/i18n/admin-pages.ts @@ -60,6 +60,7 @@ export const adminPagesZh: Record = { 'user.btn.hide_password': '隐藏', 'user.ph.reset_password': '留空则不修改;填写后将更新并可查看', 'user.ph.reset_password_short': '留空不修改', + 'user.page_settings': '全局设置', 'user.global_settings': '密码与账号管理(全局)', 'user.global_settings_hint': '控制所有玩家是否可在 App 内改密码、改账号名', 'user.section.password_mgmt': '密码管理', @@ -127,7 +128,9 @@ export const adminPagesZh: Record = { 'match.create_fixture_btn': '+ 新增单场', 'match.btn.markets': '盘口', 'match.filter.keyword_ph': '赛事名 / 球队代码', + 'match.filter.status_hint': '仅筛选展开后的单场列表与「单场」列计数,不会隐藏新建的空联赛', 'match.col.league': '赛事', + 'match.col.league_en': '联赛(英文)', 'match.col.fixture_count': '单场', 'match.col.bet_count': '注单数', 'match.col.total_stake': '总投注额', @@ -148,6 +151,8 @@ export const adminPagesZh: Record = { 'match.field.lang_en': 'EN', 'match.field.lang_ms': 'MS', 'match.field.kickoff': '开赛时间', + 'match.field.home_team': '主队', + 'match.field.away_team': '客队', 'match.field.home_en': '主队(英)', 'match.field.home_zh': '主队(中)', 'match.field.home_ms': '主队(马来)', @@ -158,7 +163,14 @@ export const adminPagesZh: Record = { 'match.hint.create_draft': '创建后为草稿,请展开赛事后在单场行点击「发布」并生成盘口。', 'match.hint.create_league': '创建赛事(联赛)后,展开该行可添加多场单场比赛。', 'match.hint.edit_published': '已发布:可修改开赛时间、热门及显示名称;封盘/已结算后不可编辑。', - 'match.expand_league_hint': '展开赛事查看单场列表;点击「盘口」进入单独页面设置盘口与赔率(与玩家端按联赛分组一致)。', + 'match.expand_league_hint': '展开联赛可管理单场赛事;夺冠盘口请在「优胜赛配置(盘口)」中设置赔率。', + 'match.expand_outright_hint': '展开联赛可编辑夺冠赔率;单场球队会自动同步,也可手动补充尚未赛程的球队。', + 'outright.odds_only_hint': '单场赛程中的球队会自动加入;可手动添加尚未参赛的球队,并在此调整赔率。', + 'outright.col.teams_from_fixtures': '参赛球队(来自单场)', + 'outright.col.teams_total': '冠军盘球队', + 'outright.empty_no_teams': '暂无球队,请先在「赛事配置」添加单场或点击「添加队伍」。', + 'match.outright.setup': '配置', + 'match.outright.section_hint': '按联赛配置冠军盘,与下方单场列表同属本联赛', 'match.expand_markets_hint': '在单场列表点击「盘口」进入单独页面设置盘口与赔率。', 'match.no_fixtures': '该赛事下暂无单场。', 'match.ph.league_ms': '2027 世界杯', @@ -173,6 +185,7 @@ export const adminPagesZh: Record = { 'bet.col.agent': '所属代理', 'bet.col.selection': '选项', 'bet.col.content': '投注内容', + 'bet.content.bet_counts': '{singles}单 · {parlays}串', 'bet.col.match': '赛事', 'bet.legs_more': '还有 {n} 项…', 'bet.col.selection_count': '投注项数', @@ -319,6 +332,7 @@ export const adminPagesZh: Record = { 'err.password_mismatch': '两次密码不一致', 'err.credit_negative': '授信额度不能为负', 'err.kickoff_required': '请填写开赛时间', + 'err.team_country_required': '请选择主客队', 'err.teams_required': '请填写主客队名称(中文、英文或马来文至少一项)', 'err.teams_same': '主客队不能相同,请填写不同的队名', 'err.league_required': '请填写联赛名称(中文、英文或马来文至少一项)', @@ -518,7 +532,37 @@ export const adminPagesZh: Record = { 'outright.col.player_visible': '玩家端', 'outright.col.league_en': '联赛(英文)', 'outright.expand_no_teams': '暂无队伍,请进入编辑页添加', + 'outright.fixtures_sync_hint': '参赛队伍来自本联赛单场赛程,仅可调整赔率与发布状态。', + 'outright.empty_no_fixtures': '该联赛暂无单场,请先在「赛事配置」中添加比赛。', 'outright.btn.add_team': '添加队伍', + 'outright.add.filter_fixture': '已有队伍', + 'outright.add.filter_all': '全部内置', + 'outright.add.select_all': '全选', + 'outright.add.clear_selection': '取消全选', + 'outright.add.selected_count': '已选 {n} 支', + 'outright.add.empty_fixture': '暂无待添加的参赛球队(单场中已有且未在冠军盘的球队会显示在此)', + 'outright.add.empty_all': '所有内置球队均已加入冠军盘', + 'outright.add.default_odds': '默认赔率', + 'outright.add.search_ph': '搜索队名或代码', + 'outright.add.err_none': '请至少选择一支球队', + 'outright.batch.mode': '批量管理', + 'outright.batch.exit': '退出批量', + 'outright.batch.apply_odds': '应用赔率', + 'outright.batch.remove': '批量移除', + 'outright.batch.confirm_remove': '确定移除选中的 {n} 支队伍?', + 'outright.batch.err_none': '请先选择队伍', + 'outright.batch.apply_ok': '已更新 {n} 支队伍的赔率,请点击「保存全部赔率」', + 'outright.batch.remove_ok': '已移除 {n} 支队伍', + 'outright.batch.remove_partial': '成功 {ok} 支,失败 {fail} 支', + 'outright.sort.label': '排序', + 'outright.sort.rank': '排名', + 'outright.sort.name': '队名', + 'outright.sort.code': '代码', + 'outright.sort.odds': '赔率(当前)', + 'outright.sort.saved_odds': '赔率(已保存)', + 'outright.sort.asc': '升序', + 'outright.sort.desc': '降序', + 'msg.outright_teams_added': '已添加 {n} 支球队(跳过 {skipped} 支)', 'outright.btn.create_event': '新建冠军赛事', 'outright.btn.import_wc2026': '导入世界杯 48 强', 'outright.btn.apply_canonical': '应用世界杯基准表', @@ -617,6 +661,7 @@ export const adminPagesEn: Record = { 'user.btn.hide_password': 'Hide', 'user.ph.reset_password': 'Leave empty to keep; new value will be viewable', 'user.ph.reset_password_short': 'Leave empty to keep', + 'user.page_settings': 'Global settings', 'user.global_settings': 'Password & account (global)', 'user.global_settings_hint': 'Controls whether all players can change password or username in the app', 'user.section.password_mgmt': 'Password management', @@ -684,7 +729,9 @@ export const adminPagesEn: Record = { 'match.create_fixture_btn': '+ Add fixture', 'match.btn.markets': 'Markets', 'match.filter.keyword_ph': 'Tournament / team code', + 'match.filter.status_hint': 'Filters fixtures inside a league and the fixture count column; empty leagues stay visible', 'match.col.league': 'Tournament', + 'match.col.league_en': 'League (EN)', 'match.col.fixture_count': 'Fixtures', 'match.col.bet_count': 'Bets', 'match.col.total_stake': 'Total stake', @@ -705,6 +752,8 @@ export const adminPagesEn: Record = { 'match.field.lang_en': 'EN', 'match.field.lang_ms': 'MS', 'match.field.kickoff': 'Kickoff time', + 'match.field.home_team': 'Home team', + 'match.field.away_team': 'Away team', 'match.field.home_en': 'Home (EN)', 'match.field.home_zh': 'Home (ZH)', 'match.field.home_ms': 'Home (MS)', @@ -715,7 +764,14 @@ export const adminPagesEn: Record = { 'match.hint.create_draft': 'Saved as draft; expand the tournament and publish each fixture to open markets.', 'match.hint.create_league': 'Create a tournament first, then expand it to add fixtures.', 'match.hint.edit_published': 'Published: edit kickoff, featured, display names; closed/settled are locked.', - 'match.expand_league_hint': 'Expand a tournament to see fixtures; use Markets for a dedicated odds page (same grouping as player app).', + 'match.expand_league_hint': 'Expand a league to manage fixtures; set winner odds under Outright odds.', + 'match.expand_outright_hint': 'Expand a league to edit winner odds; fixture teams sync automatically, and you can add teams not yet on the schedule.', + 'outright.odds_only_hint': 'Teams from fixtures are added automatically; add extra teams manually and edit winner odds here.', + 'outright.col.teams_from_fixtures': 'Teams (from fixtures)', + 'outright.col.teams_total': 'Outright teams', + 'outright.empty_no_teams': 'No teams yet — add fixtures under Fixtures or click Add team.', + 'match.outright.setup': 'Set up', + 'match.outright.section_hint': 'Winner market for this league; fixtures are listed below', 'match.expand_markets_hint': 'Click Markets on a fixture to open the dedicated markets page.', 'match.no_fixtures': 'No fixtures under this tournament yet.', 'match.ph.league_ms': 'World Cup 2027', @@ -730,6 +786,7 @@ export const adminPagesEn: Record = { 'bet.col.agent': 'Agent', 'bet.col.selection': 'Pick', 'bet.col.content': 'Selections', + 'bet.content.bet_counts': '{singles} single · {parlays} parlay', 'bet.col.match': 'Match', 'bet.legs_more': '+{n} more…', 'bet.col.selection_count': 'Legs', @@ -876,6 +933,7 @@ export const adminPagesEn: Record = { 'err.password_mismatch': 'Passwords do not match', 'err.credit_negative': 'Credit limit cannot be negative', 'err.kickoff_required': 'Kickoff time is required', + 'err.team_country_required': 'Select home and away teams', 'err.teams_required': 'Enter home and away team names (ZH or EN)', 'err.teams_same': 'Home and away teams must be different', 'err.league_required': 'League name is required', @@ -1076,7 +1134,37 @@ export const adminPagesEn: Record = { 'outright.col.player_visible': 'Player', 'outright.col.league_en': 'League (EN)', 'outright.expand_no_teams': 'No teams — open Edit to add', + 'outright.fixtures_sync_hint': 'Teams come from league fixtures; adjust odds and publish status only.', + 'outright.empty_no_fixtures': 'No fixtures in this league — add matches under Fixtures first.', 'outright.btn.add_team': 'Add team', + 'outright.add.filter_fixture': 'From fixtures', + 'outright.add.filter_all': 'All built-in', + 'outright.add.select_all': 'Select all', + 'outright.add.clear_selection': 'Clear selection', + 'outright.add.selected_count': '{n} selected', + 'outright.add.empty_fixture': 'No fixture teams to add (teams in matches but not yet on the outright market)', + 'outright.add.empty_all': 'All built-in teams are already on the outright market', + 'outright.add.default_odds': 'Default odds', + 'outright.add.search_ph': 'Search name or code', + 'outright.add.err_none': 'Select at least one team', + 'outright.batch.mode': 'Batch manage', + 'outright.batch.exit': 'Exit batch', + 'outright.batch.apply_odds': 'Apply odds', + 'outright.batch.remove': 'Remove selected', + 'outright.batch.confirm_remove': 'Remove {n} selected team(s)?', + 'outright.batch.err_none': 'Select teams first', + 'outright.batch.apply_ok': 'Updated odds for {n} team(s) — click Save all odds', + 'outright.batch.remove_ok': 'Removed {n} team(s)', + 'outright.batch.remove_partial': '{ok} removed, {fail} failed', + 'outright.sort.label': 'Sort', + 'outright.sort.rank': 'Rank', + 'outright.sort.name': 'Team name', + 'outright.sort.code': 'Code', + 'outright.sort.odds': 'Odds (current)', + 'outright.sort.saved_odds': 'Odds (saved)', + 'outright.sort.asc': 'Ascending', + 'outright.sort.desc': 'Descending', + 'msg.outright_teams_added': 'Added {n} team(s) ({skipped} skipped)', 'outright.btn.create_event': 'New outright event', 'outright.btn.import_wc2026': 'Import WC 2026 (48)', 'outright.btn.apply_canonical': 'Apply WC baseline', diff --git a/apps/admin/src/layouts/ManageLayout.vue b/apps/admin/src/layouts/ManageLayout.vue index 3e8ac84..ed7a2a8 100644 --- a/apps/admin/src/layouts/ManageLayout.vue +++ b/apps/admin/src/layouts/ManageLayout.vue @@ -1,22 +1,25 @@