重构
This commit is contained in:
@@ -2759,4 +2759,72 @@ function creditTypeLabel(type: string) {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mgr-top-tabs :deep(.el-tabs__item) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.list-panel-toolbar {
|
||||
border-bottom-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.expand-panel {
|
||||
background: #fbfaf7;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.expand-loading,
|
||||
.expand-section-title,
|
||||
.inner-tabs :deep(.el-tabs__item),
|
||||
.field-hint,
|
||||
.create-meta-label,
|
||||
.amount-full-hint,
|
||||
.text-muted,
|
||||
.list-settings-hint,
|
||||
.list-settings-unit,
|
||||
.section-title {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.inner-tabs :deep(.el-tabs__item.is-active) {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.inner-tabs :deep(.el-tabs__active-bar) {
|
||||
background-color: var(--primary);
|
||||
}
|
||||
|
||||
.create-meta-bar {
|
||||
border-color: var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.create-meta-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.c-green,
|
||||
.create-meta-row .c-green {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.invite-code-cell {
|
||||
color: var(--primary-link);
|
||||
}
|
||||
|
||||
.list-settings-block--danger {
|
||||
border-top-color: var(--danger-border);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.list-panel-toolbar,
|
||||
.list-panel-toolbar .list-chrome__grow,
|
||||
.list-panel-toolbar .list-chrome__actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.create-form-pair {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -547,4 +547,21 @@ function creditTypeLabel(type: string) {
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.field-hint,
|
||||
.section-title,
|
||||
.amount-full-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.detail-block {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.section-title--row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -384,4 +384,30 @@ async function openDetail(row: BetListRow) {
|
||||
color: #aaa;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-card,
|
||||
.data-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.bet-content-summary,
|
||||
.bet-no,
|
||||
.selections-title {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.bet-content-empty,
|
||||
.detail-league {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.bets-filter-form {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.bets-filter-form :deep(.el-form-item) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -738,4 +738,62 @@ onMounted(loadHistory);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tool-card,
|
||||
.preview-card,
|
||||
.data-card {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.rules-help-btn {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.rules-help-btn:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.rules-list,
|
||||
.detail-summary,
|
||||
.preview-title,
|
||||
.table-title,
|
||||
.pstat-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.rules-note,
|
||||
.preview-meta,
|
||||
.detail-meta,
|
||||
.pstat-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pstat {
|
||||
border-color: var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.amount-cell,
|
||||
.pstat-green {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.preview-head,
|
||||
.history-head {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.preview-actions,
|
||||
.detail-actions,
|
||||
.pager {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -670,6 +670,7 @@ void load();
|
||||
:required="form.status === 'ACTIVE'"
|
||||
>
|
||||
<div class="banner-upload-field">
|
||||
<p class="banner-size-hint">{{ t('content.upload.recommended_size') }}</p>
|
||||
<!-- Image preview -->
|
||||
<div v-if="tr.imageUrl" class="banner-preview">
|
||||
<img :src="tr.imageUrl" alt="" class="banner-preview-img" />
|
||||
@@ -822,10 +823,18 @@ void load();
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.banner-size-hint {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.banner-preview {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
aspect-ratio: 43 / 18;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #252525;
|
||||
@@ -834,8 +843,8 @@ void load();
|
||||
|
||||
.banner-preview-img {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -980,4 +989,106 @@ void load();
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.contents-page {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.type-hint,
|
||||
.batch-hint,
|
||||
.banner-size-hint,
|
||||
.schedule-line,
|
||||
.schedule-sep,
|
||||
.media-picker-loading,
|
||||
.media-picker-empty,
|
||||
.media-picker-name,
|
||||
.thumb-empty,
|
||||
.locale-head {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hidden-tip {
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.thumb,
|
||||
.banner-preview,
|
||||
.media-picker-thumb {
|
||||
background: #f4f0e8;
|
||||
}
|
||||
|
||||
.banner-preview,
|
||||
.locale-block {
|
||||
border-color: var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.banner-preview-remove {
|
||||
border-color: rgba(255, 255, 255, 0.36);
|
||||
background: rgba(31, 35, 32, 0.78);
|
||||
}
|
||||
|
||||
.banner-preview-remove:hover {
|
||||
background: rgba(159, 47, 45, 0.92);
|
||||
}
|
||||
|
||||
.banner-upload-btn,
|
||||
.banner-pick-btn {
|
||||
border-radius: 7px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.banner-upload-btn {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.banner-upload-btn:hover {
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.banner-pick-btn {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.banner-pick-btn:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.media-picker-card {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.media-picker-card:hover {
|
||||
border-color: #d5cfc3;
|
||||
box-shadow: 0 8px 22px rgba(56, 49, 37, 0.08);
|
||||
}
|
||||
|
||||
.media-picker-svg {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.filter-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.banner-preview,
|
||||
.banner-url-input {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -87,17 +87,17 @@ const mainTrendOption = computed(() =>
|
||||
[
|
||||
{
|
||||
name: t('dash.chart_stake'),
|
||||
color: '#d4d4d4',
|
||||
color: '#346538',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.stake)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_payout'),
|
||||
color: '#60a5fa',
|
||||
color: '#1f6c9f',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.payout)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_ggr'),
|
||||
color: '#a78bfa',
|
||||
color: '#956400',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.ggr)) ?? [],
|
||||
},
|
||||
],
|
||||
@@ -111,20 +111,20 @@ const distributionOption = computed(() => {
|
||||
const u = s.value?.users;
|
||||
const raw = s.value?.bets.todayByStatus ?? {};
|
||||
const betColors: Record<string, string> = {
|
||||
PENDING: '#fb923c',
|
||||
WON: '#d4d4d4',
|
||||
LOST: '#f87171',
|
||||
VOID: '#6b7280',
|
||||
REFUNDED: '#60a5fa',
|
||||
PENDING: '#956400',
|
||||
WON: '#346538',
|
||||
LOST: '#9f2f2d',
|
||||
VOID: '#8c867c',
|
||||
REFUNDED: '#1f6c9f',
|
||||
};
|
||||
|
||||
const matchSegs = m
|
||||
? [
|
||||
{ label: t('dash.match_draft'), value: m.draft, color: '#6b7280' },
|
||||
{ label: t('dash.match_published'), value: m.published, color: '#d4d4d4' },
|
||||
{ label: t('dash.match_closed'), value: m.closed, color: '#60a5fa' },
|
||||
{ label: t('dash.match_pending_settle'), value: m.pendingSettlement, color: '#fb923c' },
|
||||
{ label: t('dash.match_settled'), value: m.settled ?? 0, color: '#5eead4' },
|
||||
{ label: t('dash.match_draft'), value: m.draft, color: '#8c867c' },
|
||||
{ label: t('dash.match_published'), value: m.published, color: '#346538' },
|
||||
{ label: t('dash.match_closed'), value: m.closed, color: '#1f6c9f' },
|
||||
{ label: t('dash.match_pending_settle'), value: m.pendingSettlement, color: '#956400' },
|
||||
{ label: t('dash.match_settled'), value: m.settled ?? 0, color: '#477a4c' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
@@ -133,15 +133,15 @@ const distributionOption = computed(() => {
|
||||
.map((k) => ({
|
||||
label: betStatusLabel(k),
|
||||
value: raw[k].count,
|
||||
color: betColors[k] ?? '#888',
|
||||
color: betColors[k] ?? '#8c867c',
|
||||
}));
|
||||
|
||||
const userSegs = u
|
||||
? [
|
||||
{ label: t('dash.user_active'), value: u.playersActive, color: '#d4d4d4' },
|
||||
{ label: t('dash.user_suspended'), value: u.playersSuspended, color: '#f87171' },
|
||||
{ label: t('dash.user_direct'), value: u.playersDirect, color: '#60a5fa' },
|
||||
{ label: t('dash.user_agents'), value: u.agentsTotal, color: '#a78bfa' },
|
||||
{ label: t('dash.user_active'), value: u.playersActive, color: '#346538' },
|
||||
{ label: t('dash.user_suspended'), value: u.playersSuspended, color: '#9f2f2d' },
|
||||
{ label: t('dash.user_direct'), value: u.playersDirect, color: '#1f6c9f' },
|
||||
{ label: t('dash.user_agents'), value: u.agentsTotal, color: '#956400' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
@@ -428,4 +428,68 @@ const kpiSecondary = computed(() => {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Local light overrides for the legacy dashboard entry */
|
||||
.state-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.overview-board {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.dash-updated {
|
||||
color: #9b9489;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.kpi-cell {
|
||||
border-color: var(--border-soft);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.kpi-cell--link:hover,
|
||||
.kpi-cell--link:focus-visible {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
color: var(--text);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.kpi-sub {
|
||||
color: #9b9489;
|
||||
}
|
||||
|
||||
.kpi-delta {
|
||||
color: var(--text-muted);
|
||||
background: #f0ede6;
|
||||
}
|
||||
|
||||
.kpi-delta.up {
|
||||
color: var(--success-text);
|
||||
background: var(--success-bg);
|
||||
}
|
||||
|
||||
.kpi-delta.down {
|
||||
color: var(--danger-text);
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
.charts-stack {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.chart-main-caption {
|
||||
color: #9b9489;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -59,42 +59,41 @@ function switchTab(key: string) {
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
margin-bottom: 0;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
margin-bottom: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
position: relative;
|
||||
padding: 10px 20px;
|
||||
background: none;
|
||||
padding: 8px 18px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #888;
|
||||
border-radius: 6px;
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #ccc;
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #fff;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tab-btn.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: var(--primary, #409eff);
|
||||
border-radius: 2px 2px 0 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-deposit-manage :deep(.page-deposit-orders),
|
||||
@@ -108,4 +107,14 @@ function switchTab(key: string) {
|
||||
.page-deposit-manage :deep(.page-payment-methods .toolbar h2) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.tab-bar {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -421,4 +421,239 @@ onMounted(fetchList);
|
||||
.preview-box { position: relative; max-width: 90vw; max-height: 90vh; }
|
||||
.preview-image { max-width: 90vw; max-height: 85vh; object-fit: contain; border-radius: 8px; }
|
||||
.close-preview { position: absolute; top: -12px; right: -12px; background: #333; color: #fff; border: none; border-radius: 50%; width: 28px; height: 28px; font-size: 14px; cursor: pointer; }
|
||||
|
||||
.page-deposit-orders {
|
||||
padding: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.toolbar h2 {
|
||||
color: var(--text);
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.filters {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.filters select,
|
||||
.filters input {
|
||||
min-height: 34px;
|
||||
border-color: var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.filters select:focus,
|
||||
.filters input:focus {
|
||||
border-color: var(--primary);
|
||||
outline: 3px solid rgba(31, 35, 32, 0.08);
|
||||
}
|
||||
|
||||
.btn-search,
|
||||
.btn-primary {
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 7px;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
border-bottom-color: var(--border-soft);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #fbfaf7;
|
||||
color: var(--text-muted);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.time-cell,
|
||||
.remark-cell,
|
||||
.info-row span,
|
||||
.form-group label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.screenshot-thumb,
|
||||
.approve-screenshot {
|
||||
border-color: var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.badge,
|
||||
.status-pending,
|
||||
.status-approved,
|
||||
.status-rejected {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.badge-blue {
|
||||
background: var(--info-bg);
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
.badge-green {
|
||||
background: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.status-rejected {
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-sm:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-approve {
|
||||
border-color: var(--success-border);
|
||||
background: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.btn-reject {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.btn-reopen {
|
||||
border-color: var(--warning-border);
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.pagination button {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.pagination button:not(:disabled):hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
background: rgba(31, 35, 32, 0.28);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
box-shadow: 0 24px 64px rgba(56, 49, 37, 0.14);
|
||||
}
|
||||
|
||||
.dialog-box h3 {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.approve-check-hint {
|
||||
border-color: var(--warning-border);
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
border-color: var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-danger-full {
|
||||
border: 1px solid var(--danger-text);
|
||||
border-radius: 7px;
|
||||
background: var(--danger-text);
|
||||
}
|
||||
|
||||
.close-preview {
|
||||
background: var(--text);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.toolbar,
|
||||
.filters {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.filters > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
min-width: 0;
|
||||
width: calc(100vw - 32px);
|
||||
max-height: calc(100dvh - 32px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import api from '../api';
|
||||
import AdminTableEmpty from '../components/AdminTableEmpty.vue';
|
||||
import { formatAmount, formatAmountFull } from '../utils/format-amount';
|
||||
import { walletTxTypeKey } from '../utils/walletTx';
|
||||
const { t, locale, localeTag } = useAdminLocale();
|
||||
const auth = useAuthStore();
|
||||
const route = useRoute();
|
||||
@@ -75,9 +76,8 @@ function creditTypeLabel(type: string) {
|
||||
}
|
||||
|
||||
function transferTypeLabel(type: string) {
|
||||
if (type === 'MANUAL_DEPOSIT') return t('finance.tx.deposit');
|
||||
if (type === 'MANUAL_WITHDRAW') return t('finance.tx.withdraw');
|
||||
return type;
|
||||
const key = walletTxTypeKey(type);
|
||||
return key ? t(key) : type;
|
||||
}
|
||||
|
||||
const TRANSFER_REMARK_KEYS: Record<string, string> = {
|
||||
@@ -289,7 +289,12 @@ watch(
|
||||
<el-form-item :label="t('agent.col.credit_type')">
|
||||
<el-select v-model="transactionType" clearable :placeholder="t('common.all')" style="width: 110px">
|
||||
<el-option :label="t('finance.tx.deposit')" value="MANUAL_DEPOSIT" />
|
||||
<el-option :label="t('finance.tx.admin_deposit')" value="ADMIN_DEPOSIT" />
|
||||
<el-option :label="t('finance.tx.agent_deposit')" value="AGENT_DEPOSIT" />
|
||||
<el-option :label="t('finance.tx.player_deposit')" value="PLAYER_DEPOSIT" />
|
||||
<el-option :label="t('finance.tx.withdraw')" value="MANUAL_WITHDRAW" />
|
||||
<el-option :label="t('finance.tx.admin_withdraw')" value="ADMIN_WITHDRAW" />
|
||||
<el-option :label="t('finance.tx.agent_withdraw')" value="AGENT_WITHDRAW" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
@@ -311,4 +311,187 @@ label {
|
||||
font-weight: 600;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
/* Light operational login */
|
||||
.login-page {
|
||||
min-height: 100dvh;
|
||||
background-color: #f7f6f3;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.login-page::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(120deg, rgba(247, 246, 243, 0.94), rgba(247, 246, 243, 0.82)),
|
||||
rgba(247, 246, 243, 0.9);
|
||||
}
|
||||
|
||||
.login-mask {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.54), rgba(240, 237, 230, 0.82));
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 12px 0 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 24px 72px rgba(56, 49, 37, 0.14);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.form-lang {
|
||||
padding: 0 18px 12px;
|
||||
}
|
||||
|
||||
.form-body {
|
||||
grid-template-columns: minmax(210px, 270px) 1fr;
|
||||
}
|
||||
|
||||
.form-brand {
|
||||
background: #f4f0e8;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.logo-large {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
gap: 10px;
|
||||
padding: 24px 26px 26px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 8px;
|
||||
color: var(--text);
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #6c675f;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.field {
|
||||
height: 44px;
|
||||
background: #ffffff;
|
||||
border-color: var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.field::placeholder {
|
||||
color: #aaa49a;
|
||||
}
|
||||
|
||||
.field:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(31, 35, 32, 0.08);
|
||||
}
|
||||
|
||||
.field:-webkit-autofill,
|
||||
.field:-webkit-autofill:focus {
|
||||
-webkit-box-shadow: 0 0 0 1000px #ffffff inset;
|
||||
-webkit-text-fill-color: var(--text);
|
||||
caret-color: var(--text);
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
height: 44px;
|
||||
border-color: var(--primary);
|
||||
border-radius: 8px;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.btn-login:hover:not(:disabled) {
|
||||
border-color: var(--primary-light);
|
||||
background: var(--primary-light);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-login:disabled {
|
||||
opacity: 0.58;
|
||||
}
|
||||
|
||||
.quick-label {
|
||||
margin-top: 8px;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.quick-btns {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.quick-btn {
|
||||
min-height: 54px;
|
||||
border-color: var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.quick-btn:hover:not(:disabled) {
|
||||
background: var(--accent-hover);
|
||||
border-color: #d5cfc3;
|
||||
}
|
||||
|
||||
.quick-role {
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.quick-acc {
|
||||
color: var(--text);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.login-page {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.login-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 420px;
|
||||
min-height: 100dvh;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.form-brand {
|
||||
padding: 22px 24px 14px;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.form-fields {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.quick-btns {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
938
apps/admin/src/views/MarketTemplates.vue
Normal file
938
apps/admin/src/views/MarketTemplates.vue
Normal file
@@ -0,0 +1,938 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import api from '../api';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import MatchesSubNav from '../components/MatchesSubNav.vue';
|
||||
|
||||
const { t, locale } = useAdminLocale();
|
||||
|
||||
const LOCALES = [
|
||||
{ code: 'zh-CN', label: '中文' },
|
||||
{ code: 'en-US', label: 'English' },
|
||||
{ code: 'ms-MY', label: 'Bahasa Melayu' },
|
||||
] as const;
|
||||
|
||||
const UI_TEXT: Record<string, Record<string, string>> = {
|
||||
add: { 'zh-CN': '添加', 'en-US': 'Add', 'ms-MY': 'Tambah' },
|
||||
addMarket: { 'zh-CN': '增加盘口', 'en-US': 'Add market', 'ms-MY': 'Tambah pasaran' },
|
||||
close: { 'zh-CN': '关闭', 'en-US': 'Close', 'ms-MY': 'Tutup' },
|
||||
copyCreated: { 'zh-CN': '已复制盘口线', 'en-US': 'Market line duplicated', 'ms-MY': 'Garisan pasaran disalin' },
|
||||
copyMarket: { 'zh-CN': '复制盘口', 'en-US': 'Duplicate market', 'ms-MY': 'Salin pasaran' },
|
||||
copyMarketCreated: { 'zh-CN': '已复制盘口', 'en-US': 'Market duplicated', 'ms-MY': 'Pasaran disalin' },
|
||||
copyMarketShort: { 'zh-CN': '复制盘口', 'en-US': 'Duplicate', 'ms-MY': 'Salin pasaran' },
|
||||
copyLine: { 'zh-CN': '复制盘口线', 'en-US': 'Duplicate line', 'ms-MY': 'Salin garisan' },
|
||||
copyLineShort: { 'zh-CN': '复制线', 'en-US': 'Copy', 'ms-MY': 'Salin' },
|
||||
createTemplate: { 'zh-CN': '新建模板', 'en-US': 'New template', 'ms-MY': 'Templat baharu' },
|
||||
defaultTemplate: { 'zh-CN': '默认', 'en-US': 'Default', 'ms-MY': 'Lalai' },
|
||||
duplicateTemplate: { 'zh-CN': '复制模板', 'en-US': 'Duplicate template', 'ms-MY': 'Salin templat' },
|
||||
hidden: { 'zh-CN': '玩家端隐藏', 'en-US': 'Hidden on player', 'ms-MY': 'Disembunyikan pemain' },
|
||||
lineValue: { 'zh-CN': '盘口线', 'en-US': 'Line', 'ms-MY': 'Garisan' },
|
||||
marketDetails: { 'zh-CN': '模板详情', 'en-US': 'Template details', 'ms-MY': 'Butiran templat' },
|
||||
marketName: { 'zh-CN': '盘口名称', 'en-US': 'Market name', 'ms-MY': 'Nama pasaran' },
|
||||
marketToAdd: { 'zh-CN': '选择要添加的盘口', 'en-US': 'Choose a market to add', 'ms-MY': 'Pilih pasaran untuk ditambah' },
|
||||
marketUnavailable: { 'zh-CN': '暂不开放玩家端', 'en-US': 'Not available on player', 'ms-MY': 'Belum tersedia untuk pemain' },
|
||||
moveDown: { 'zh-CN': '下移', 'en-US': 'Move down', 'ms-MY': 'Turun' },
|
||||
moveUp: { 'zh-CN': '上移', 'en-US': 'Move up', 'ms-MY': 'Naik' },
|
||||
odds: { 'zh-CN': '赔率', 'en-US': 'Odds', 'ms-MY': 'Odds' },
|
||||
parlay: { 'zh-CN': '串关', 'en-US': 'Parlay', 'ms-MY': 'Parlay' },
|
||||
playerVisible: { 'zh-CN': '玩家端显示', 'en-US': 'Show on player', 'ms-MY': 'Papar pemain' },
|
||||
promoLabel: { 'zh-CN': '促销标签', 'en-US': 'Promo label', 'ms-MY': 'Label promosi' },
|
||||
remove: { 'zh-CN': '移除', 'en-US': 'Remove', 'ms-MY': 'Buang' },
|
||||
selectMarket: { 'zh-CN': '从盘口池选择', 'en-US': 'Select from market pool', 'ms-MY': 'Pilih daripada kumpulan pasaran' },
|
||||
selectTemplate: { 'zh-CN': '选择模板', 'en-US': 'Select template', 'ms-MY': 'Pilih templat' },
|
||||
setDefault: { 'zh-CN': '设为默认', 'en-US': 'Set default', 'ms-MY': 'Jadikan lalai' },
|
||||
single: { 'zh-CN': '单关', 'en-US': 'Single', 'ms-MY': 'Tunggal' },
|
||||
status: { 'zh-CN': '状态', 'en-US': 'Status', 'ms-MY': 'Status' },
|
||||
statusClosed: { 'zh-CN': '已关闭', 'en-US': 'Closed', 'ms-MY': 'Ditutup' },
|
||||
statusOpen: { 'zh-CN': '可售', 'en-US': 'Open', 'ms-MY': 'Dibuka' },
|
||||
statusSuspended: { 'zh-CN': '暂停', 'en-US': 'Suspended', 'ms-MY': 'Digantung' },
|
||||
templateDefaultSet: { 'zh-CN': '已设为默认模板', 'en-US': 'Default template updated', 'ms-MY': 'Templat lalai dikemas kini' },
|
||||
templateMarkets: { 'zh-CN': '模板盘口', 'en-US': 'Template markets', 'ms-MY': 'Pasaran templat' },
|
||||
templateName: { 'zh-CN': '模板名称', 'en-US': 'Template name', 'ms-MY': 'Nama templat' },
|
||||
};
|
||||
|
||||
type LocalizedText = Record<string, string>;
|
||||
|
||||
interface MarketDefinition {
|
||||
marketType: string;
|
||||
marketKey: string;
|
||||
period: string;
|
||||
sortOrder: number;
|
||||
defaultLineValue: number | null;
|
||||
allowSingle: boolean;
|
||||
allowParlay: boolean;
|
||||
showOnPlayer: boolean;
|
||||
settlementSupported: boolean;
|
||||
usesLineValue: boolean;
|
||||
nameI18n: LocalizedText;
|
||||
selectionTemplate: Array<{ code: string; name: string; nameI18n: LocalizedText; odds: number }>;
|
||||
}
|
||||
|
||||
interface SelectionRow {
|
||||
id?: string;
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
nameI18n: LocalizedText;
|
||||
odds: number;
|
||||
status: string;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
interface MarketRow {
|
||||
id?: string;
|
||||
marketType: string;
|
||||
marketKey: string;
|
||||
period: string;
|
||||
lineValue: number | null;
|
||||
paramsJson: Record<string, unknown> | null;
|
||||
status: string;
|
||||
allowSingle: boolean;
|
||||
allowParlay: boolean;
|
||||
showOnPlayer: boolean;
|
||||
sortOrder: number;
|
||||
promoLabelI18n: LocalizedText;
|
||||
nameI18n: LocalizedText;
|
||||
selections: SelectionRow[];
|
||||
}
|
||||
|
||||
interface MarketTemplate {
|
||||
id: string;
|
||||
name: string;
|
||||
nameI18n: LocalizedText;
|
||||
isDefault: boolean;
|
||||
status: string;
|
||||
sortOrder: number;
|
||||
items: MarketRow[];
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const definitions = ref<MarketDefinition[]>([]);
|
||||
const templates = ref<MarketTemplate[]>([]);
|
||||
const selectedId = ref('');
|
||||
const selectedIndex = ref(0);
|
||||
const addMarketVisible = ref(false);
|
||||
const selectedDefinitionType = ref('');
|
||||
const current = computed(() => templates.value.find((tpl) => tpl.id === selectedId.value) ?? null);
|
||||
const selectedItem = computed(() => current.value?.items[selectedIndex.value] ?? null);
|
||||
const definitionMap = computed(() => new Map(definitions.value.map((d) => [d.marketType, d])));
|
||||
const definitionOptions = computed(() => [...definitions.value].sort((a, b) => a.sortOrder - b.sortOrder));
|
||||
const displayLocales = computed(() => {
|
||||
const zhLabels: Record<string, string> = {
|
||||
'zh-CN': '中文',
|
||||
'en-US': '英语',
|
||||
'ms-MY': '马来语',
|
||||
};
|
||||
return LOCALES.map((loc) => ({
|
||||
code: loc.code,
|
||||
label: locale.value === 'zh-CN' ? zhLabels[loc.code] : loc.label,
|
||||
}));
|
||||
});
|
||||
|
||||
function localized(input?: Record<string, string> | null, fallback = ''): LocalizedText {
|
||||
const out: LocalizedText = {};
|
||||
for (const loc of LOCALES) out[loc.code] = input?.[loc.code]?.trim() || '';
|
||||
if (fallback && !out['zh-CN']) out['zh-CN'] = fallback;
|
||||
return out;
|
||||
}
|
||||
|
||||
function resolveText(map?: LocalizedText | null, fallback = '') {
|
||||
const order = Array.from(new Set([locale.value, 'en-US', 'zh-CN', 'ms-MY']));
|
||||
for (const loc of order) {
|
||||
const value = map?.[loc]?.trim();
|
||||
if (value) return value;
|
||||
}
|
||||
return Object.values(map ?? {}).find((value) => value?.trim()) || fallback;
|
||||
}
|
||||
|
||||
function ui(key: string) {
|
||||
return resolveText(UI_TEXT[key], key);
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
if (status === 'OPEN') return ui('statusOpen');
|
||||
if (status === 'SUSPENDED') return ui('statusSuspended');
|
||||
if (status === 'CLOSED') return ui('statusClosed');
|
||||
return status;
|
||||
}
|
||||
|
||||
function selectionTitle(selection: SelectionRow) {
|
||||
const score = /^SCORE_(\d+)_(\d+)$/.exec(selection.selectionCode);
|
||||
if (score) return `${score[1]}:${score[2]}`;
|
||||
return resolveText(selection.nameI18n, selection.selectionName || selection.selectionCode);
|
||||
}
|
||||
|
||||
function isScoreSelection(selection: SelectionRow) {
|
||||
return /^SCORE_\d+_\d+$/.test(selection.selectionCode);
|
||||
}
|
||||
|
||||
function marketTitle(market: MarketRow) {
|
||||
const line = isLineMarket(market) && market.lineValue != null ? ` ${market.lineValue}` : '';
|
||||
return `${resolveText(market.nameI18n, market.marketType)}${line}`;
|
||||
}
|
||||
|
||||
function marketMetaText(market: MarketRow) {
|
||||
const parts = [statusLabel(market.status)];
|
||||
if (isLineMarket(market) && market.lineValue != null) parts.push(`${ui('lineValue')} ${market.lineValue}`);
|
||||
if (!market.showOnPlayer) parts.push(ui('hidden'));
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function definitionLabel(def: MarketDefinition) {
|
||||
const label = resolveText(def.nameI18n, def.marketType);
|
||||
return def.settlementSupported ? label : `${label}(${ui('marketUnavailable')})`;
|
||||
}
|
||||
|
||||
function templateLabel(tpl: MarketTemplate) {
|
||||
const label = resolveText(tpl.nameI18n, tpl.name || '');
|
||||
return tpl.isDefault ? `${label}(${ui('defaultTemplate')})` : label;
|
||||
}
|
||||
|
||||
function mapItem(raw: MarketRow, index: number): MarketRow {
|
||||
return {
|
||||
id: raw.id,
|
||||
marketType: raw.marketType,
|
||||
marketKey: raw.marketKey || raw.marketType,
|
||||
period: raw.period,
|
||||
lineValue: raw.lineValue,
|
||||
paramsJson: raw.paramsJson ?? null,
|
||||
status: raw.status || 'OPEN',
|
||||
allowSingle: raw.allowSingle ?? true,
|
||||
allowParlay: raw.allowParlay ?? true,
|
||||
showOnPlayer: raw.showOnPlayer ?? true,
|
||||
sortOrder: raw.sortOrder ?? index,
|
||||
promoLabelI18n: localized(raw.promoLabelI18n),
|
||||
nameI18n: localized(raw.nameI18n, raw.marketType),
|
||||
selections: (raw.selections ?? []).map((s, sIndex) => ({
|
||||
id: s.id,
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: s.selectionName,
|
||||
nameI18n: localized(s.nameI18n, s.selectionName),
|
||||
odds: Number(s.odds),
|
||||
status: s.status || 'OPEN',
|
||||
sortOrder: s.sortOrder ?? sIndex,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function usesLineValue(marketType: string) {
|
||||
return definitionMap.value.get(marketType)?.usesLineValue ?? false;
|
||||
}
|
||||
|
||||
function isLineMarket(market: MarketRow) {
|
||||
return usesLineValue(market.marketType);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [{ data: defRes }, { data: tplRes }] = await Promise.all([
|
||||
api.get('/admin/market-definitions'),
|
||||
api.get('/admin/market-templates'),
|
||||
]);
|
||||
definitions.value = defRes.data ?? [];
|
||||
templates.value = (tplRes.data ?? []).map((tpl: MarketTemplate) => ({
|
||||
...tpl,
|
||||
nameI18n: localized(tpl.nameI18n, tpl.name),
|
||||
items: (tpl.items ?? []).map(mapItem),
|
||||
}));
|
||||
selectedId.value =
|
||||
selectedId.value && templates.value.some((tpl) => tpl.id === selectedId.value)
|
||||
? selectedId.value
|
||||
: templates.value.find((tpl) => tpl.isDefault)?.id || templates.value[0]?.id || '';
|
||||
selectedIndex.value = 0;
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(selectedId, () => {
|
||||
selectedIndex.value = 0;
|
||||
});
|
||||
|
||||
function openAddMarketDialog() {
|
||||
selectedDefinitionType.value = definitionOptions.value[0]?.marketType ?? '';
|
||||
addMarketVisible.value = true;
|
||||
}
|
||||
|
||||
function addSelectedMarket() {
|
||||
const def = definitionMap.value.get(selectedDefinitionType.value);
|
||||
if (!def) return;
|
||||
addItem(def);
|
||||
addMarketVisible.value = false;
|
||||
selectedDefinitionType.value = '';
|
||||
}
|
||||
|
||||
function addItem(def: MarketDefinition) {
|
||||
if (!current.value) return;
|
||||
current.value.items.push({
|
||||
marketType: def.marketType,
|
||||
marketKey: def.marketKey,
|
||||
period: def.period,
|
||||
lineValue: def.defaultLineValue,
|
||||
paramsJson: null,
|
||||
status: 'OPEN',
|
||||
allowSingle: def.allowSingle,
|
||||
allowParlay: def.allowParlay,
|
||||
showOnPlayer: def.showOnPlayer && def.settlementSupported,
|
||||
sortOrder: current.value.items.length,
|
||||
promoLabelI18n: localized(),
|
||||
nameI18n: localized(def.nameI18n, def.marketType),
|
||||
selections: def.selectionTemplate.map((s, index) => ({
|
||||
selectionCode: s.code,
|
||||
selectionName: s.name,
|
||||
nameI18n: localized(s.nameI18n, s.name),
|
||||
odds: Number(s.odds),
|
||||
status: 'OPEN',
|
||||
sortOrder: index,
|
||||
})),
|
||||
});
|
||||
selectedIndex.value = current.value.items.length - 1;
|
||||
}
|
||||
|
||||
function copyLocalizedName(map: LocalizedText) {
|
||||
const suffix: Record<string, string> = {
|
||||
'zh-CN': ' 副本',
|
||||
'en-US': ' Copy',
|
||||
'ms-MY': ' Salinan',
|
||||
};
|
||||
const next = localized(map, resolveText(map));
|
||||
for (const loc of LOCALES) {
|
||||
const base = next[loc.code] || resolveText(next);
|
||||
next[loc.code] = `${base}${suffix[loc.code]}`;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function clonePlain<T>(value: T): T {
|
||||
return value == null ? value : JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function nextCopiedLineValue(source: MarketRow) {
|
||||
const tpl = current.value;
|
||||
const used = new Set(
|
||||
(tpl?.items ?? [])
|
||||
.filter((item) => item.marketType === source.marketType && item.lineValue != null)
|
||||
.map((item) => Number(item.lineValue).toFixed(2)),
|
||||
);
|
||||
let candidate = Number(((source.lineValue ?? 0) + 0.25).toFixed(2));
|
||||
while (used.has(candidate.toFixed(2))) {
|
||||
candidate = Number((candidate + 0.25).toFixed(2));
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function duplicateItem(index: number) {
|
||||
const tpl = current.value;
|
||||
const source = tpl?.items[index];
|
||||
if (!tpl || !source) return;
|
||||
const shouldCopyLine = isLineMarket(source);
|
||||
const sourceCopy = clonePlain(source);
|
||||
const paramsCopy = clonePlain(source.paramsJson);
|
||||
const copy: MarketRow = {
|
||||
...sourceCopy,
|
||||
id: undefined,
|
||||
lineValue: shouldCopyLine ? nextCopiedLineValue(source) : null,
|
||||
paramsJson: shouldCopyLine
|
||||
? paramsCopy
|
||||
: { ...(paramsCopy ?? {}), copyId: `copy-${Date.now()}-${index}` },
|
||||
nameI18n: copyLocalizedName(source.nameI18n),
|
||||
sortOrder: tpl.items.length,
|
||||
selections: source.selections.map((selection) => ({ ...clonePlain(selection), id: undefined })),
|
||||
};
|
||||
tpl.items.splice(index + 1, 0, copy);
|
||||
tpl.items.forEach((item, i) => {
|
||||
item.sortOrder = i;
|
||||
});
|
||||
selectedIndex.value = index + 1;
|
||||
ElMessage.success(ui(shouldCopyLine ? 'copyCreated' : 'copyMarketCreated'));
|
||||
}
|
||||
|
||||
function moveItem(index: number, direction: -1 | 1) {
|
||||
const tpl = current.value;
|
||||
if (!tpl) return;
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= tpl.items.length) return;
|
||||
const [row] = tpl.items.splice(index, 1);
|
||||
tpl.items.splice(target, 0, row);
|
||||
tpl.items.forEach((item, i) => {
|
||||
item.sortOrder = i;
|
||||
});
|
||||
selectedIndex.value = target;
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
const tpl = current.value;
|
||||
if (!tpl) return;
|
||||
tpl.items.splice(index, 1);
|
||||
tpl.items.forEach((item, i) => {
|
||||
item.sortOrder = i;
|
||||
});
|
||||
selectedIndex.value = Math.min(selectedIndex.value, Math.max(0, tpl.items.length - 1));
|
||||
}
|
||||
|
||||
function payload(tpl: MarketTemplate) {
|
||||
return {
|
||||
name: tpl.name,
|
||||
nameI18n: localized(tpl.nameI18n, tpl.name),
|
||||
isDefault: tpl.isDefault,
|
||||
status: tpl.status,
|
||||
sortOrder: tpl.sortOrder,
|
||||
items: tpl.items.map((m, index) => ({
|
||||
id: m.id,
|
||||
marketType: m.marketType,
|
||||
marketKey: m.marketKey,
|
||||
lineKey: null,
|
||||
period: m.period,
|
||||
lineValue: usesLineValue(m.marketType) ? m.lineValue : null,
|
||||
paramsJson: m.paramsJson,
|
||||
status: m.status,
|
||||
allowSingle: m.allowSingle,
|
||||
allowParlay: m.allowParlay,
|
||||
showOnPlayer: m.showOnPlayer,
|
||||
sortOrder: index,
|
||||
promoLabelI18n: localized(m.promoLabelI18n),
|
||||
nameI18n: localized(m.nameI18n, m.marketType),
|
||||
selections: m.selections.map((s, sIndex) => ({
|
||||
id: s.id,
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: resolveText(s.nameI18n, s.selectionName),
|
||||
nameI18n: localized(s.nameI18n, s.selectionName),
|
||||
odds: Number(s.odds),
|
||||
status: s.status,
|
||||
sortOrder: sIndex,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async function createTemplate() {
|
||||
saving.value = true;
|
||||
try {
|
||||
const { data } = await api.post('/admin/market-templates', {
|
||||
name: '新盘口模板',
|
||||
nameI18n: { 'zh-CN': '新盘口模板', 'en-US': 'New Market Template', 'ms-MY': 'Templat Pasaran Baharu' },
|
||||
items: [],
|
||||
});
|
||||
await load();
|
||||
selectedId.value = data.data.id;
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateTemplate() {
|
||||
if (!current.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const { data } = await api.post(`/admin/market-templates/${current.value.id}/duplicate`);
|
||||
await load();
|
||||
selectedId.value = data.data.id;
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function setDefault() {
|
||||
if (!current.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await api.post(`/admin/market-templates/${current.value.id}/set-default`);
|
||||
ElMessage.success(ui('templateDefaultSet'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveTemplate() {
|
||||
if (!current.value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await api.put(`/admin/market-templates/${current.value.id}`, payload(current.value));
|
||||
ElMessage.success(t('msg.saved'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
load();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="template-page">
|
||||
<MatchesSubNav />
|
||||
<div class="template-toolbar">
|
||||
<el-select v-model="selectedId" class="template-select" :placeholder="ui('selectTemplate')">
|
||||
<el-option
|
||||
v-for="tpl in templates"
|
||||
:key="tpl.id"
|
||||
:label="templateLabel(tpl)"
|
||||
:value="tpl.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button size="small" @click="createTemplate">{{ ui('createTemplate') }}</el-button>
|
||||
<el-button size="small" :disabled="!current" @click="duplicateTemplate">{{ ui('duplicateTemplate') }}</el-button>
|
||||
<el-button size="small" :disabled="!current?.id || current?.isDefault" @click="setDefault">{{ ui('setDefault') }}</el-button>
|
||||
<el-button size="small" :disabled="!current" @click="openAddMarketDialog">{{ ui('addMarket') }}</el-button>
|
||||
<el-button size="small" type="primary" :loading="saving" :disabled="!current" @click="saveTemplate">
|
||||
{{ t('common.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="current" class="template-grid">
|
||||
<section class="items">
|
||||
<div class="section-title">{{ ui('templateMarkets') }}</div>
|
||||
<div class="template-name">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="current.nameI18n[loc.code]" size="small" />
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, index) in current.items"
|
||||
:key="item.id || `${item.marketType}-${index}`"
|
||||
class="item-card"
|
||||
:class="{ active: selectedIndex === index }"
|
||||
@click="selectedIndex = index"
|
||||
>
|
||||
<div class="item-main">
|
||||
<strong>{{ marketTitle(item) }}</strong>
|
||||
<span>{{ marketMetaText(item) }}</span>
|
||||
</div>
|
||||
<div class="item-actions" @click.stop>
|
||||
<el-button class="item-action-btn" size="small" text :title="ui('moveUp')" :disabled="index === 0" @click="moveItem(index, -1)">↑</el-button>
|
||||
<el-button class="item-action-btn" size="small" text :title="ui('moveDown')" :disabled="index === current.items.length - 1" @click="moveItem(index, 1)">↓</el-button>
|
||||
<el-button
|
||||
class="item-action-btn copy-btn"
|
||||
size="small"
|
||||
text
|
||||
:title="ui(isLineMarket(item) ? 'copyLine' : 'copyMarket')"
|
||||
@click="duplicateItem(index)"
|
||||
>
|
||||
{{ ui(isLineMarket(item) ? 'copyLineShort' : 'copyMarketShort') }}
|
||||
</el-button>
|
||||
<el-button class="item-action-btn" size="small" text type="danger" :title="ui('remove')" @click="removeItem(index)">{{ ui('remove') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="selectedItem" class="detail">
|
||||
<div class="detail-head">
|
||||
<div>
|
||||
<div class="section-title">{{ ui('marketDetails') }}</div>
|
||||
<strong>{{ marketTitle(selectedItem) }}</strong>
|
||||
</div>
|
||||
<el-switch v-model="selectedItem.showOnPlayer" :active-text="ui('playerVisible')" />
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<label>
|
||||
<span>{{ ui('status') }}</span>
|
||||
<el-select v-model="selectedItem.status" size="small">
|
||||
<el-option :label="ui('statusOpen')" value="OPEN" />
|
||||
<el-option :label="ui('statusSuspended')" value="SUSPENDED" />
|
||||
<el-option :label="ui('statusClosed')" value="CLOSED" />
|
||||
</el-select>
|
||||
</label>
|
||||
<label v-if="isLineMarket(selectedItem)">
|
||||
<span>{{ ui('lineValue') }}</span>
|
||||
<el-input-number v-model="selectedItem.lineValue" size="small" :step="0.25" controls-position="right" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ ui('single') }}</span>
|
||||
<el-switch v-model="selectedItem.allowSingle" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ ui('parlay') }}</span>
|
||||
<el-switch v-model="selectedItem.allowParlay" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="locale-block">
|
||||
<div class="block-title">{{ ui('marketName') }}</div>
|
||||
<div class="locale-grid">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="selectedItem.nameI18n[loc.code]" size="small" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="locale-block">
|
||||
<div class="block-title">{{ ui('promoLabel') }}</div>
|
||||
<div class="locale-grid">
|
||||
<label v-for="loc in displayLocales" :key="`promo-${loc.code}`">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="selectedItem.promoLabelI18n[loc.code]" size="small" clearable />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selection-list">
|
||||
<div
|
||||
v-for="sel in selectedItem.selections"
|
||||
:key="sel.selectionCode"
|
||||
class="selection-row"
|
||||
:class="{ 'selection-row--score': isScoreSelection(sel) }"
|
||||
>
|
||||
<div class="selection-meta">
|
||||
<strong :title="sel.selectionCode">{{ selectionTitle(sel) }}</strong>
|
||||
<label class="selection-field selection-field--status">
|
||||
<span>{{ ui('status') }}</span>
|
||||
<el-select v-model="sel.status" size="small">
|
||||
<el-option :label="ui('statusOpen')" value="OPEN" />
|
||||
<el-option :label="ui('statusSuspended')" value="SUSPENDED" />
|
||||
<el-option :label="ui('statusClosed')" value="CLOSED" />
|
||||
</el-select>
|
||||
</label>
|
||||
<label class="selection-field selection-field--odds">
|
||||
<span>{{ ui('odds') }}</span>
|
||||
<el-input-number v-model="sel.odds" size="small" :min="1.01" :step="0.01" controls-position="right" />
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="!isScoreSelection(sel)" class="locale-grid compact">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="sel.nameI18n[loc.code]" size="small" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="addMarketVisible" class="add-market-dialog" :title="ui('addMarket')" width="460px" destroy-on-close>
|
||||
<label class="add-market-field">
|
||||
<span>{{ ui('marketToAdd') }}</span>
|
||||
<el-select
|
||||
v-model="selectedDefinitionType"
|
||||
class="full-select"
|
||||
filterable
|
||||
:placeholder="ui('selectMarket')"
|
||||
>
|
||||
<el-option
|
||||
v-for="def in definitionOptions"
|
||||
:key="def.marketType"
|
||||
:label="definitionLabel(def)"
|
||||
:value="def.marketType"
|
||||
/>
|
||||
</el-select>
|
||||
</label>
|
||||
<template #footer>
|
||||
<el-button @click="addMarketVisible = false">{{ t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" :disabled="!selectedDefinitionType" @click="addSelectedMarket">
|
||||
{{ ui('add') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.template-page {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.template-toolbar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 0 12px;
|
||||
}
|
||||
|
||||
.template-select {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 420px) minmax(480px, 1fr);
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.items,
|
||||
.detail {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 8px;
|
||||
background: #111;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.block-title {
|
||||
color: #bbb;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.item-card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
border: 1px solid #282828;
|
||||
border-radius: 6px;
|
||||
background: #151515;
|
||||
color: #ddd;
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item-card span {
|
||||
display: block;
|
||||
margin-top: 3px;
|
||||
color: #777;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.item-card.active {
|
||||
border-color: #9f853d;
|
||||
background: #1a1710;
|
||||
}
|
||||
|
||||
.item-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-main strong,
|
||||
.detail-head strong {
|
||||
display: block;
|
||||
color: #f0f0f0;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.item-actions {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 30px 30px 46px 40px;
|
||||
gap: 2px;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.item-action-btn {
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
margin-left: 0 !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.template-name,
|
||||
.form-grid,
|
||||
.locale-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.locale-grid.compact {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.locale-block {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.selection-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selection-row {
|
||||
padding: 8px;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 6px;
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.selection-meta {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(90px, 1fr) 120px 132px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.selection-row--score .selection-meta {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.selection-meta strong {
|
||||
color: #ddd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.selection-field {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.selection-field > span {
|
||||
color: #f3c969;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.selection-field--status > span {
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
.selection-field--odds :deep(.el-input-number),
|
||||
.selection-field--odds :deep(.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-market-field {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.full-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.template-toolbar,
|
||||
.template-grid,
|
||||
.template-name,
|
||||
.form-grid,
|
||||
.locale-grid,
|
||||
.selection-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.template-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.items,
|
||||
.detail {
|
||||
min-height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.template-toolbar {
|
||||
padding: 12px 0 14px;
|
||||
}
|
||||
|
||||
.items,
|
||||
.detail,
|
||||
.item-card,
|
||||
.selection-row {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.items,
|
||||
.detail {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.block-title,
|
||||
.item-main strong,
|
||||
.detail-head strong,
|
||||
.selection-meta strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.item-card span,
|
||||
label,
|
||||
.selection-field > span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.item-card:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.item-card.active {
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.selection-row {
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.selection-field > span {
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.selection-field--status > span {
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.item-actions {
|
||||
grid-template-columns: repeat(4, minmax(30px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -408,6 +408,11 @@ function onLeagueArchived() {
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-chrome__actions">
|
||||
<el-button type="primary" @click.stop="openCreateLeague">
|
||||
{{ t('match.create_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="filterStatus" class="list-hint">{{ t('match.filter.status_hint') }}</p>
|
||||
</div>
|
||||
@@ -489,9 +494,6 @@ function onLeagueArchived() {
|
||||
<template #header>
|
||||
<div class="actions-col-header">
|
||||
<span class="actions-col-header__label">{{ t('common.actions') }}</span>
|
||||
<el-button type="primary" size="small" @click.stop="openCreateLeague">
|
||||
{{ t('match.create_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
@@ -596,6 +598,7 @@ function onLeagueArchived() {
|
||||
:placeholder="t('matchEditor.ph.kickoff')"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<p class="field-hint schedule-timezone-hint">{{ t('match.timezone.platform_hint') }}</p>
|
||||
</el-form-item>
|
||||
<div class="teams-row">
|
||||
<!-- Home Team Column -->
|
||||
@@ -686,19 +689,24 @@ function onLeagueArchived() {
|
||||
|
||||
.team-col-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #bbb;
|
||||
font-weight: 750;
|
||||
color: var(--text);
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 6px;
|
||||
border-bottom: 1px solid #2a2a2a;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.schedule-timezone-hint {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.edit-hint {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@@ -714,7 +722,7 @@ function onLeagueArchived() {
|
||||
|
||||
.matches-page :deep(.el-table__expanded-cell) {
|
||||
padding: 0 !important;
|
||||
background: #0a0a0a;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.list-panel :deep(.row-expandable) {
|
||||
@@ -734,7 +742,7 @@ function onLeagueArchived() {
|
||||
font-weight: 600;
|
||||
}
|
||||
.bet-stat-zero {
|
||||
color: #555;
|
||||
color: #aaa49a;
|
||||
}
|
||||
|
||||
.league-cell {
|
||||
@@ -751,13 +759,13 @@ function onLeagueArchived() {
|
||||
}
|
||||
|
||||
.league-en {
|
||||
color: #aaa;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.league-readonly {
|
||||
color: var(--green-text);
|
||||
font-weight: 500;
|
||||
color: var(--success-text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.actions-col-header {
|
||||
@@ -773,8 +781,8 @@ function onLeagueArchived() {
|
||||
|
||||
.actions-col-header__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #aaa;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -811,8 +819,8 @@ function onLeagueArchived() {
|
||||
gap: 4px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background: #fbfaf7;
|
||||
border: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.league-row-actions :deep(.el-button) {
|
||||
@@ -836,4 +844,50 @@ function onLeagueArchived() {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.list-chrome__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-col {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 8px;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.matchup-link,
|
||||
.bet-stat-active {
|
||||
color: var(--primary-link);
|
||||
}
|
||||
|
||||
.league-row-actions :deep(.el-button) {
|
||||
min-height: 26px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.list-chrome__actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.teams-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.actions-col-header {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.league-row-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -247,13 +247,13 @@ function isLeagueExpanded(id: string) {
|
||||
}
|
||||
.matches-page :deep(.el-table__expanded-cell) {
|
||||
padding: 0 !important;
|
||||
background: #0a0a0a;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
.list-panel :deep(.row-expandable) {
|
||||
cursor: pointer;
|
||||
}
|
||||
.matchup-link {
|
||||
color: var(--green-text);
|
||||
color: var(--primary-link);
|
||||
}
|
||||
.league-cell {
|
||||
display: flex;
|
||||
@@ -267,7 +267,7 @@ function isLeagueExpanded(id: string) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.league-en {
|
||||
color: #aaa;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -649,4 +649,223 @@ async function doUpload() {
|
||||
padding: 16px 20px;
|
||||
border-top: 1px solid #1e1e1e;
|
||||
}
|
||||
|
||||
.media-page {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.unused-badge {
|
||||
border-color: var(--warning-border);
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.tab-btn,
|
||||
.btn {
|
||||
border-radius: 7px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-ghost:hover:not(:disabled) {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
border-color: var(--primary-light);
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
padding: 2px 2px 8px;
|
||||
}
|
||||
|
||||
.file-card {
|
||||
border-color: var(--border);
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.file-card:hover {
|
||||
border-color: #d5cfc3;
|
||||
box-shadow: 0 10px 26px rgba(56, 49, 37, 0.08);
|
||||
}
|
||||
|
||||
.card-thumb,
|
||||
.media-picker-thumb {
|
||||
background: #f4f0e8;
|
||||
}
|
||||
|
||||
.svg-badge,
|
||||
.media-picker-svg {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.badge-used {
|
||||
border-color: var(--warning-border);
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.badge-unused {
|
||||
border-color: var(--border);
|
||||
background: #f0ede6;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-filename {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.card-meta,
|
||||
.card-date,
|
||||
.page-info,
|
||||
.state-center,
|
||||
.muted,
|
||||
.drop-hint,
|
||||
.sel-size,
|
||||
.upload-hint-text,
|
||||
.form-row label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.cat-tag {
|
||||
border-color: var(--border-soft);
|
||||
background: #f0ede6;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.act-btn {
|
||||
border-right-color: var(--border-soft);
|
||||
color: var(--text-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.act-btn:hover {
|
||||
background: var(--accent-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.act-delete:hover {
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.file-grid::-webkit-scrollbar-thumb {
|
||||
background: #d5cfc3;
|
||||
}
|
||||
|
||||
.file-grid::-webkit-scrollbar-thumb:hover {
|
||||
background: #bdb6a9;
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
background: rgba(31, 35, 32, 0.28);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 24px 64px rgba(56, 49, 37, 0.14);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
border-bottom-color: var(--border-soft);
|
||||
color: var(--text);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.dialog-close:hover {
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.drop-zone {
|
||||
border-color: #d5cfc3;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.drop-zone:hover,
|
||||
.drop-zone.drop-active {
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.sel-name {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.toolbar,
|
||||
.toolbar-right,
|
||||
.filter-tabs {
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tab-btn,
|
||||
.toolbar-right .btn {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.file-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(148px, 1fr));
|
||||
}
|
||||
|
||||
.dialog {
|
||||
max-height: calc(100dvh - 32px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -309,7 +309,11 @@ onMounted(fetchList);
|
||||
<input v-model.number="form.sortOrder" type="number" />
|
||||
</div>
|
||||
<div class="form-group row-checks">
|
||||
<label><input type="checkbox" v-model="form.isActive" /> {{ t('deposit.active') }}</label>
|
||||
<label class="check-option">
|
||||
<input type="checkbox" v-model="form.isActive" />
|
||||
<span class="check-option__box" aria-hidden="true"></span>
|
||||
<span>{{ t('deposit.active') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<button class="btn-cancel" @click="dialogVisible = false">{{ t('common.cancel') }}</button>
|
||||
@@ -365,4 +369,262 @@ onMounted(fetchList);
|
||||
.lang-row:last-child { margin-bottom: 0; }
|
||||
.lang-tag { flex-shrink: 0; font-size: 10px; font-weight: 700; color: #aaa; background: #252525; padding: 2px 6px; border-radius: 3px; min-width: 48px; text-align: center; }
|
||||
.lang-row input { flex: 1; padding: 5px 8px; border: 1px solid #333; border-radius: 3px; background: #0e0e0e; color: #ddd; font-size: 12px; box-sizing: border-box; }
|
||||
|
||||
.page-payment-methods {
|
||||
padding: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.toolbar h2 {
|
||||
color: var(--text);
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filter-select,
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.lang-row input {
|
||||
min-height: 34px;
|
||||
border-color: var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border: 1px solid var(--primary);
|
||||
border-radius: 7px;
|
||||
background: var(--primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--primary-light);
|
||||
}
|
||||
|
||||
.data-table {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
border-bottom-color: var(--border-soft);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #fbfaf7;
|
||||
color: var(--text-muted);
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.details-cell .sub,
|
||||
.form-group label,
|
||||
.lang-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.qr-thumb,
|
||||
.qr-preview {
|
||||
border: 1px solid var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.badge {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.badge-blue {
|
||||
background: var(--info-bg);
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
.badge-green {
|
||||
background: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.status-on {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.status-off {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.btn-sm:hover {
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.btn-toggle {
|
||||
border-color: var(--info-border);
|
||||
background: var(--info-bg);
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.dialog-overlay {
|
||||
background: rgba(31, 35, 32, 0.28);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
box-shadow: 0 24px 64px rgba(56, 49, 37, 0.14);
|
||||
}
|
||||
|
||||
.dialog-box h3 {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.row-checks label {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.row-checks .check-option {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 7px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fbfaf7;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.row-checks .check-option:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.row-checks .check-option input[type='checkbox'] {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.check-option__box {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex: 0 0 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #cfc7b9;
|
||||
border-radius: 5px;
|
||||
background: #ffffff;
|
||||
transition: background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.check-option__box::after {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 9px;
|
||||
border: solid #ffffff;
|
||||
border-width: 0 2px 2px 0;
|
||||
opacity: 0;
|
||||
transform: rotate(45deg) translateY(-1px);
|
||||
transition: opacity 0.12s ease;
|
||||
}
|
||||
|
||||
.row-checks .check-option input[type='checkbox']:checked + .check-option__box {
|
||||
border-color: var(--primary);
|
||||
background: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(31, 35, 32, 0.08);
|
||||
}
|
||||
|
||||
.row-checks .check-option input[type='checkbox']:checked + .check-option__box::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.row-checks .check-option input[type='checkbox']:focus-visible + .check-option__box {
|
||||
box-shadow: 0 0 0 3px rgba(31, 35, 32, 0.14);
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.lang-section {
|
||||
border-color: var(--border-soft);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.lang-tag {
|
||||
background: #f0ede6;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.toolbar {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.actions,
|
||||
.filter-select,
|
||||
.actions .btn-primary {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
min-width: 0;
|
||||
width: calc(100vw - 32px);
|
||||
max-height: calc(100dvh - 32px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, defineAsyncComponent } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { formatApiErrorMessage, isApiErrorCode } from '@thebet365/shared';
|
||||
import api from '../api';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
@@ -70,11 +71,82 @@ interface SettlementBetStats {
|
||||
const { t, locale, localeTag } = useAdminLocale();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const STAT_FACT_LABELS = {
|
||||
homeCorners: {
|
||||
'zh-CN': '主队角球',
|
||||
'en-US': 'Home corners',
|
||||
'ms-MY': 'Sepakan sudut tuan rumah',
|
||||
},
|
||||
awayCorners: {
|
||||
'zh-CN': '客队角球',
|
||||
'en-US': 'Away corners',
|
||||
'ms-MY': 'Sepakan sudut pelawat',
|
||||
},
|
||||
homeCards: {
|
||||
'zh-CN': '主队黄牌/红牌',
|
||||
'en-US': 'Home yellow/red cards',
|
||||
'ms-MY': 'Kad kuning/merah tuan rumah',
|
||||
},
|
||||
awayCards: {
|
||||
'zh-CN': '客队黄牌/红牌',
|
||||
'en-US': 'Away yellow/red cards',
|
||||
'ms-MY': 'Kad kuning/merah pelawat',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const loading = ref(false);
|
||||
const previewing = ref(false);
|
||||
const match = ref<AdminMatchDetail | null>(null);
|
||||
const score = ref({ htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 });
|
||||
const matchStats = ref<{
|
||||
homeCorners: number | null;
|
||||
awayCorners: number | null;
|
||||
homeYellowCards: number | null;
|
||||
awayYellowCards: number | null;
|
||||
homeRedCards: number | null;
|
||||
awayRedCards: number | null;
|
||||
homeCards: number | null;
|
||||
awayCards: number | null;
|
||||
}>(emptyMatchStats());
|
||||
|
||||
function emptyMatchStats() {
|
||||
return {
|
||||
homeCorners: null,
|
||||
awayCorners: null,
|
||||
homeYellowCards: null,
|
||||
awayYellowCards: null,
|
||||
homeRedCards: null,
|
||||
awayRedCards: null,
|
||||
homeCards: null,
|
||||
awayCards: null,
|
||||
};
|
||||
}
|
||||
|
||||
function cardTotalFromBreakdown(
|
||||
yellow: number | null,
|
||||
red: number | null,
|
||||
fallback: number | null,
|
||||
) {
|
||||
const hasBreakdown = yellow != null || red != null;
|
||||
if (!hasBreakdown) return fallback ?? null;
|
||||
if (yellow == null || red == null) return null;
|
||||
return yellow + red;
|
||||
}
|
||||
|
||||
function settlementStatsPayload() {
|
||||
const s = matchStats.value;
|
||||
return {
|
||||
...s,
|
||||
homeCards: cardTotalFromBreakdown(s.homeYellowCards, s.homeRedCards, s.homeCards),
|
||||
awayCards: cardTotalFromBreakdown(s.awayYellowCards, s.awayRedCards, s.awayCards),
|
||||
};
|
||||
}
|
||||
|
||||
function resetSettlementInputs() {
|
||||
score.value = { htHome: 0, htAway: 0, ftHome: 0, ftAway: 0 };
|
||||
matchStats.value = emptyMatchStats();
|
||||
}
|
||||
|
||||
const winnerTeamId = ref('');
|
||||
const outrightSelections = ref<
|
||||
Array<{ teamId: string; teamCode: string; teamZh: string; teamEn: string }>
|
||||
@@ -133,7 +205,7 @@ function buildSettlementPayload(): Record<string, unknown> | null {
|
||||
}
|
||||
return { winnerTeamId: Number(winnerTeamId.value) };
|
||||
}
|
||||
return { ...score.value };
|
||||
return { ...score.value, ...settlementStatsPayload() };
|
||||
}
|
||||
type PreviewItem = {
|
||||
betNo: string;
|
||||
@@ -162,7 +234,7 @@ const previewZeroHint = computed(() => {
|
||||
if (payout > 0 || refund > 0) return '';
|
||||
const pending = Number(p.pendingOtherMatches ?? 0);
|
||||
const wonLegs = Number(p.wonLegsOnMatch ?? 0);
|
||||
if (pending > 0 && wonLegs > 0) {
|
||||
if (pending > 0) {
|
||||
return t('settlement.preview_zero_parlay_hint', { pending: String(pending), legs: String(wonLegs) });
|
||||
}
|
||||
if (Number(p.lostOnThisMatch ?? 0) > 0) {
|
||||
@@ -380,8 +452,19 @@ async function loadMatch() {
|
||||
ftHome: detail.score.ftHome,
|
||||
ftAway: detail.score.ftAway,
|
||||
};
|
||||
matchStats.value = {
|
||||
homeCorners: detail.score.homeCorners ?? null,
|
||||
awayCorners: detail.score.awayCorners ?? null,
|
||||
homeYellowCards: detail.score.homeYellowCards ?? null,
|
||||
awayYellowCards: detail.score.awayYellowCards ?? null,
|
||||
homeRedCards: detail.score.homeRedCards ?? null,
|
||||
awayRedCards: detail.score.awayRedCards ?? null,
|
||||
homeCards: detail.score.homeCards ?? null,
|
||||
awayCards: detail.score.awayCards ?? null,
|
||||
};
|
||||
winnerTeamId.value = detail.score.winnerTeamId ?? '';
|
||||
} else {
|
||||
resetSettlementInputs();
|
||||
winnerTeamId.value = '';
|
||||
}
|
||||
if (detail.isOutright) {
|
||||
@@ -437,7 +520,21 @@ async function confirmResettle() {
|
||||
}
|
||||
|
||||
function settlementApiError(e: unknown, fallback: string) {
|
||||
const err = e as { response?: { data?: { error?: string; message?: string } } };
|
||||
const err = e as { response?: { data?: { code?: unknown; params?: Record<string, string | number>; error?: string; message?: string } } };
|
||||
const code = err.response?.data?.code;
|
||||
if (isApiErrorCode(code)) {
|
||||
const params = { ...(err.response?.data?.params ?? {}) };
|
||||
if (code === 'SETTLEMENT_FACTS_REQUIRED' && typeof params.fields === 'string') {
|
||||
params.fields = params.fields
|
||||
.split(',')
|
||||
.map((field) => {
|
||||
const labels = STAT_FACT_LABELS[field as keyof typeof STAT_FACT_LABELS];
|
||||
return labels?.[locale.value as keyof typeof labels] ?? field;
|
||||
})
|
||||
.join(locale.value === 'zh-CN' ? '、' : ', ');
|
||||
}
|
||||
return formatApiErrorMessage(code, locale.value, params);
|
||||
}
|
||||
const raw = err.response?.data?.error ?? err.response?.data?.message ?? fallback;
|
||||
if (raw === 'Score not recorded' || raw === 'Score not found') {
|
||||
return t('settlement.err_score_not_recorded');
|
||||
@@ -595,6 +692,69 @@ onMounted(() => {
|
||||
<el-input-number v-model="score.ftAway" :min="0" controls-position="right" style="width: 88px" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-block compact stat-block">
|
||||
<span class="score-title">{{ t('settlement.corners') }}</span>
|
||||
<div class="stat-inputs">
|
||||
<span class="stat-side">{{ t('settlement.home_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.homeCorners"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
<span class="stat-side">{{ t('settlement.away_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.awayCorners"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-block compact stat-block">
|
||||
<span class="score-title">{{ t('settlement.yellow_cards') }}</span>
|
||||
<div class="stat-inputs">
|
||||
<span class="stat-side">{{ t('settlement.home_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.homeYellowCards"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
<span class="stat-side">{{ t('settlement.away_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.awayYellowCards"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="score-block compact stat-block">
|
||||
<span class="score-title">{{ t('settlement.red_cards') }}</span>
|
||||
<div class="stat-inputs">
|
||||
<span class="stat-side">{{ t('settlement.home_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.homeRedCards"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
<span class="stat-side">{{ t('settlement.away_stat') }}</span>
|
||||
<el-input-number
|
||||
v-model="matchStats.awayRedCards"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
style="width: 80px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settle-actions">
|
||||
@@ -873,7 +1033,7 @@ onMounted(() => {
|
||||
flex-wrap: wrap;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.outright-settle-head {
|
||||
@@ -887,7 +1047,7 @@ onMounted(() => {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--green-text);
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.outright-winner-row {
|
||||
@@ -938,19 +1098,19 @@ onMounted(() => {
|
||||
|
||||
.team-name-inline {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #eee;
|
||||
font-weight: 750;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kickoff-inline {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kickoff-inline .meta-k {
|
||||
color: #777;
|
||||
color: #aaa49a;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@@ -971,6 +1131,22 @@ onMounted(() => {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stat-block {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.stat-inputs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-side {
|
||||
font-size: 11px;
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
@@ -997,7 +1173,7 @@ onMounted(() => {
|
||||
.leg-badge {
|
||||
margin-left: 4px;
|
||||
font-size: 10px;
|
||||
color: var(--gold-text, #dcc078);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.bet-content-cell {
|
||||
@@ -1083,7 +1259,7 @@ onMounted(() => {
|
||||
.vs {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--gold-text, #dcc078);
|
||||
color: var(--warning-text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1256,7 +1432,70 @@ onMounted(() => {
|
||||
|
||||
}
|
||||
|
||||
/* 智能比分弹窗样式(功能已关闭,保留便于恢复)
|
||||
.smart-hint { ... }
|
||||
*/
|
||||
.section-title,
|
||||
.preview-title,
|
||||
.preview-bar-title,
|
||||
.pstat-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.subsection-title,
|
||||
.bet-content-cell,
|
||||
.match-league,
|
||||
.score-title,
|
||||
.preview-metric-label,
|
||||
.preview-items-title,
|
||||
.period-tag,
|
||||
.empty-hint,
|
||||
.subsection-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stat-side,
|
||||
.leg-badge,
|
||||
.vs,
|
||||
.preview-metric-orange,
|
||||
.pstat-orange {
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.preview-metric-green,
|
||||
.pstat-green {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.mini-chart,
|
||||
.pstat {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.preview-zero-hint {
|
||||
border-color: var(--warning-border);
|
||||
background: var(--warning-bg);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.score-sep {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stats-table-block--bets .table-wrap {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.score-row,
|
||||
.preview-bar,
|
||||
.preview-metrics {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.preview-metric {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -411,4 +411,37 @@ onMounted(async () => {
|
||||
color: #888;
|
||||
padding: 24px 0;
|
||||
}
|
||||
|
||||
.intro-text,
|
||||
.intro-list,
|
||||
.summary-meta,
|
||||
.empty-card,
|
||||
.case-details-empty {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.summary-title,
|
||||
.table-title {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.case-details {
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 8px;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.case-details pre {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.summary-actions,
|
||||
.summary-stats,
|
||||
.table-head,
|
||||
.log-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -890,4 +890,22 @@ function statusLabel(s: string) {
|
||||
.reset-db-alert {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.field-hint,
|
||||
.amount-full-hint,
|
||||
.text-muted,
|
||||
.list-settings-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.list-settings-block--danger {
|
||||
border-top-color: var(--danger-border);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.settings-form :deep(.el-form-item) {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -104,5 +104,5 @@ function onSizeChange(size: number) {
|
||||
.page-title { font-size: 20px; font-weight: 700; color: #e0e0e0; }
|
||||
.page-desc { font-size: 13px; color: #3a3a3a; }
|
||||
.data-card { border-radius: 12px; }
|
||||
.bet-no { font-size: 12px; color: #ccc; font-family: ui-monospace, monospace; word-break: break-all; }
|
||||
.bet-no { font-size: 12px; color: var(--text); font-family: ui-monospace, monospace; word-break: break-all; }
|
||||
</style>
|
||||
|
||||
@@ -86,17 +86,17 @@ const mainTrendOption = computed(() =>
|
||||
[
|
||||
{
|
||||
name: t('dash.chart_stake'),
|
||||
color: '#d4d4d4',
|
||||
color: '#346538',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.stake)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_payout'),
|
||||
color: '#60a5fa',
|
||||
color: '#1f6c9f',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.payout)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_ggr'),
|
||||
color: '#a78bfa',
|
||||
color: '#956400',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.ggr)) ?? [],
|
||||
},
|
||||
],
|
||||
@@ -110,11 +110,11 @@ const distributionOption = computed(() => {
|
||||
const p = s.value?.players;
|
||||
const raw = s.value?.bets.todayByStatus ?? {};
|
||||
const betColors: Record<string, string> = {
|
||||
PENDING: '#fb923c',
|
||||
WON: '#d4d4d4',
|
||||
LOST: '#f87171',
|
||||
VOID: '#6b7280',
|
||||
REFUNDED: '#60a5fa',
|
||||
PENDING: '#956400',
|
||||
WON: '#346538',
|
||||
LOST: '#9f2f2d',
|
||||
VOID: '#8c867c',
|
||||
REFUNDED: '#1f6c9f',
|
||||
};
|
||||
|
||||
const creditSegs = c
|
||||
@@ -122,12 +122,12 @@ const distributionOption = computed(() => {
|
||||
{
|
||||
label: t('agent_dash.credit_available'),
|
||||
value: toNum(c.availableCredit),
|
||||
color: '#d4d4d4',
|
||||
color: '#346538',
|
||||
},
|
||||
{
|
||||
label: t('agent_dash.credit_used'),
|
||||
value: toNum(c.usedCredit),
|
||||
color: '#fb923c',
|
||||
color: '#956400',
|
||||
},
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
@@ -137,13 +137,13 @@ const distributionOption = computed(() => {
|
||||
.map((k) => ({
|
||||
label: betStatusLabel(k),
|
||||
value: raw[k].count,
|
||||
color: betColors[k] ?? '#888',
|
||||
color: betColors[k] ?? '#8c867c',
|
||||
}));
|
||||
|
||||
const playerSegs = p
|
||||
? [
|
||||
{ label: t('dash.user_active'), value: p.active, color: '#d4d4d4' },
|
||||
{ label: t('dash.user_suspended'), value: p.suspended, color: '#f87171' },
|
||||
{ label: t('dash.user_active'), value: p.active, color: '#346538' },
|
||||
{ label: t('dash.user_suspended'), value: p.suspended, color: '#9f2f2d' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
@@ -449,4 +449,78 @@ const kpiSecondary = computed(() => {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Agent dashboard light overrides */
|
||||
.state-card {
|
||||
border-color: var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
.state-title {
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.state-hint,
|
||||
.board-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.overview-board {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.dash-updated {
|
||||
color: #9b9489;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.kpi-cell {
|
||||
border-color: var(--border-soft);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.kpi-cell--link:hover,
|
||||
.kpi-cell--link:focus-visible {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
color: var(--text);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.kpi-sub {
|
||||
color: #9b9489;
|
||||
}
|
||||
|
||||
.kpi-delta {
|
||||
color: var(--text-muted);
|
||||
background: #f0ede6;
|
||||
}
|
||||
|
||||
.kpi-delta.up {
|
||||
color: var(--success-text);
|
||||
background: var(--success-bg);
|
||||
}
|
||||
|
||||
.kpi-delta.down {
|
||||
color: var(--danger-text);
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
.charts-stack {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.chart-main-caption {
|
||||
color: #9b9489;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1601,4 +1601,62 @@ function statusTagType(s: string) {
|
||||
.empty-hint { padding: 32px 0; color: #666; font-size: 13px; }
|
||||
.create-alert { margin-bottom: 16px; }
|
||||
.create-form { margin-top: 4px; }
|
||||
|
||||
.credit-strip {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.credit-label,
|
||||
.expand-loading,
|
||||
.expand-section-title,
|
||||
.expand-readonly-hint,
|
||||
.field-hint,
|
||||
.empty-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.credit-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.credit-value.c-green,
|
||||
.invite-code-cell {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.credit-divider {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.invite-prominent-btn {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.expand-panel {
|
||||
margin-top: 6px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.credit-strip {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.credit-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.inner-toolbar,
|
||||
.expand-pager {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -434,4 +434,48 @@ function statusLabel(status: string) {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.page-desc,
|
||||
.credit-label,
|
||||
.field-hint,
|
||||
.empty-hint {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.credit-strip,
|
||||
.data-card {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.credit-value {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.credit-value.c-green {
|
||||
color: var(--success-text);
|
||||
}
|
||||
|
||||
.credit-divider {
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page-header,
|
||||
.credit-strip {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.credit-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -38,17 +38,17 @@ const mainTrendOption = computed(() =>
|
||||
[
|
||||
{
|
||||
name: t('dash.chart_stake'),
|
||||
color: '#d4d4d4',
|
||||
color: '#346538',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.stake)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_payout'),
|
||||
color: '#60a5fa',
|
||||
color: '#1f6c9f',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.payout)) ?? [],
|
||||
},
|
||||
{
|
||||
name: t('dash.chart_ggr'),
|
||||
color: '#a78bfa',
|
||||
color: '#956400',
|
||||
values: s.value?.trend7d?.map((d) => toNum(d.ggr)) ?? [],
|
||||
},
|
||||
],
|
||||
@@ -61,20 +61,20 @@ const distributionOption = computed(() => {
|
||||
const m = s.value?.matches;
|
||||
const raw = s.value?.bets.todayByStatus ?? {};
|
||||
const betColors: Record<string, string> = {
|
||||
PENDING: '#fb923c',
|
||||
WON: '#d4d4d4',
|
||||
LOST: '#f87171',
|
||||
VOID: '#6b7280',
|
||||
REFUNDED: '#60a5fa',
|
||||
PENDING: '#956400',
|
||||
WON: '#346538',
|
||||
LOST: '#9f2f2d',
|
||||
VOID: '#8c867c',
|
||||
REFUNDED: '#1f6c9f',
|
||||
};
|
||||
|
||||
const matchSegs = m
|
||||
? [
|
||||
{ label: t('dash.match_draft'), value: m.draft, color: '#6b7280' },
|
||||
{ label: t('dash.match_published'), value: m.published, color: '#d4d4d4' },
|
||||
{ label: t('dash.match_closed'), value: m.closed, color: '#60a5fa' },
|
||||
{ label: t('dash.match_pending_settle'), value: m.pendingSettlement, color: '#fb923c' },
|
||||
{ label: t('dash.match_settled'), value: m.settled ?? 0, color: '#5eead4' },
|
||||
{ label: t('dash.match_draft'), value: m.draft, color: '#8c867c' },
|
||||
{ label: t('dash.match_published'), value: m.published, color: '#346538' },
|
||||
{ label: t('dash.match_closed'), value: m.closed, color: '#1f6c9f' },
|
||||
{ label: t('dash.match_pending_settle'), value: m.pendingSettlement, color: '#956400' },
|
||||
{ label: t('dash.match_settled'), value: m.settled ?? 0, color: '#477a4c' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
@@ -83,7 +83,7 @@ const distributionOption = computed(() => {
|
||||
.map((k) => ({
|
||||
label: betStatusLabel(k),
|
||||
value: raw[k].count,
|
||||
color: betColors[k] ?? '#888',
|
||||
color: betColors[k] ?? '#8c867c',
|
||||
}));
|
||||
|
||||
return buildTriplePieOption(
|
||||
|
||||
@@ -54,10 +54,10 @@ const userDistributionOption = computed(() => {
|
||||
const u = s.value?.users;
|
||||
const userSegs = u
|
||||
? [
|
||||
{ label: t('dash.user_active'), value: u.playersActive, color: '#d4d4d4' },
|
||||
{ label: t('dash.user_suspended'), value: u.playersSuspended, color: '#f87171' },
|
||||
{ label: t('dash.user_direct'), value: u.playersDirect, color: '#60a5fa' },
|
||||
{ label: t('dash.user_agents'), value: u.agentsTotal, color: '#a78bfa' },
|
||||
{ label: t('dash.user_active'), value: u.playersActive, color: '#346538' },
|
||||
{ label: t('dash.user_suspended'), value: u.playersSuspended, color: '#9f2f2d' },
|
||||
{ label: t('dash.user_direct'), value: u.playersDirect, color: '#1f6c9f' },
|
||||
{ label: t('dash.user_agents'), value: u.agentsTotal, color: '#956400' },
|
||||
].filter((x) => x.value > 0)
|
||||
: [];
|
||||
|
||||
@@ -167,9 +167,10 @@ function formatBetTime(v: string) {
|
||||
}
|
||||
|
||||
.recent-card {
|
||||
border-radius: 14px;
|
||||
border: 1px solid #1e1e1e;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.recent-card :deep(.el-card__body) {
|
||||
@@ -180,7 +181,7 @@ function formatBetTime(v: string) {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
|
||||
@@ -4,30 +4,31 @@
|
||||
|
||||
.state-card {
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(255, 69, 58, 0.04);
|
||||
border: 1px solid var(--danger-border);
|
||||
background: var(--danger-bg);
|
||||
text-align: center;
|
||||
padding: 8px 0 4px;
|
||||
}
|
||||
|
||||
.state-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #fca5a5;
|
||||
font-weight: 750;
|
||||
color: var(--danger-text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.state-hint {
|
||||
font-size: 13px;
|
||||
color: #737373;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.overview-board {
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: #111111;
|
||||
border: 1px solid var(--border);
|
||||
background: #ffffff;
|
||||
margin-bottom: 28px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.overview-board :deep(.el-card__body) {
|
||||
@@ -45,12 +46,12 @@
|
||||
|
||||
.board-hint {
|
||||
font-size: 12px;
|
||||
color: #737373;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.dash-updated {
|
||||
font-size: 11px;
|
||||
color: #525252;
|
||||
color: #9b9489;
|
||||
}
|
||||
|
||||
.kpi-grid {
|
||||
@@ -71,8 +72,8 @@
|
||||
.kpi-cell {
|
||||
padding: 12px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border-soft);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.kpi-cell.compact {
|
||||
@@ -86,25 +87,25 @@
|
||||
|
||||
.kpi-cell--link:hover,
|
||||
.kpi-cell--link:focus-visible {
|
||||
border-color: rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #737373;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
font-weight: 500;
|
||||
color: #f5f5f5;
|
||||
font-weight: 800;
|
||||
color: var(--text);
|
||||
line-height: 1.15;
|
||||
letter-spacing: -0.02em;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.kpi-value.sm {
|
||||
@@ -114,7 +115,7 @@
|
||||
.kpi-sub {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #525252;
|
||||
color: #9b9489;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@@ -122,29 +123,31 @@
|
||||
display: inline-block;
|
||||
margin-top: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
color: #737373;
|
||||
font-weight: 750;
|
||||
color: var(--text-muted);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
background: #f0ede6;
|
||||
}
|
||||
|
||||
.kpi-delta.up {
|
||||
color: #a3a3a3;
|
||||
color: var(--success-text);
|
||||
background: var(--success-bg);
|
||||
}
|
||||
|
||||
.kpi-delta.down {
|
||||
color: #f87171;
|
||||
color: var(--danger-text);
|
||||
background: var(--danger-bg);
|
||||
}
|
||||
|
||||
.charts-stack {
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-top: 1px solid var(--border-soft);
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.chart-main-caption {
|
||||
font-size: 11px;
|
||||
color: #525252;
|
||||
color: #9b9489;
|
||||
text-align: center;
|
||||
margin: -8px 0 8px;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
type BuiltinCountry,
|
||||
} from '../data/builtinCountries';
|
||||
import { FormValidationError } from '../i18n/form-validation';
|
||||
import {
|
||||
isoToPlatformPickerDateTime,
|
||||
platformPickerDateTimeToIso,
|
||||
} from '@thebet365/shared';
|
||||
|
||||
export interface MatchCreateForm {
|
||||
leagueId: string;
|
||||
@@ -23,7 +27,6 @@ export interface MatchCreateForm {
|
||||
awayTeamEn: string;
|
||||
awayTeamMs: string;
|
||||
isHot: boolean;
|
||||
correctScoreEnabled: boolean;
|
||||
displayOrder: number;
|
||||
matchName: string;
|
||||
stage: string;
|
||||
@@ -49,7 +52,6 @@ export function emptyMatchForm(): MatchCreateForm {
|
||||
awayTeamEn: '',
|
||||
awayTeamMs: '',
|
||||
isHot: false,
|
||||
correctScoreEnabled: true,
|
||||
displayOrder: 0,
|
||||
matchName: '',
|
||||
stage: '',
|
||||
@@ -64,17 +66,26 @@ export interface AdminMarketSelection {
|
||||
id: string;
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
nameI18n?: Record<string, string>;
|
||||
odds: number;
|
||||
status: string;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface AdminMarket {
|
||||
id: string;
|
||||
marketType: string;
|
||||
marketKey?: string;
|
||||
lineKey?: string | null;
|
||||
period: string;
|
||||
lineValue: number | null;
|
||||
paramsJson?: Record<string, unknown> | null;
|
||||
status: string;
|
||||
showOnPlayer: boolean;
|
||||
promoLabel: string;
|
||||
promoLabelI18n?: Record<string, string>;
|
||||
nameI18n?: Record<string, string>;
|
||||
sortOrder?: number;
|
||||
selections: AdminMarketSelection[];
|
||||
}
|
||||
|
||||
@@ -83,7 +94,6 @@ export type AdminMatchDetail = {
|
||||
status: string;
|
||||
isOutright: boolean;
|
||||
isHot: boolean;
|
||||
correctScoreEnabled: boolean;
|
||||
displayOrder: number;
|
||||
startTime: string;
|
||||
leagueId?: string;
|
||||
@@ -110,25 +120,25 @@ export type AdminMatchDetail = {
|
||||
htAway: number;
|
||||
ftHome: number;
|
||||
ftAway: number;
|
||||
homeCorners?: number | null;
|
||||
awayCorners?: number | null;
|
||||
homeYellowCards?: number | null;
|
||||
awayYellowCards?: number | null;
|
||||
homeRedCards?: number | null;
|
||||
awayRedCards?: number | null;
|
||||
homeCards?: number | null;
|
||||
awayCards?: number | null;
|
||||
winnerTeamId?: string | null;
|
||||
} | null;
|
||||
markets?: AdminMarket[];
|
||||
};
|
||||
|
||||
export function normalizeStartTimeForPicker(iso?: string): string {
|
||||
if (!iso?.trim()) return '';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso.slice(0, 19);
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
return isoToPlatformPickerDateTime(iso);
|
||||
}
|
||||
|
||||
export function normalizeStartTimeForApi(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return '';
|
||||
const d = new Date(trimmed);
|
||||
if (Number.isNaN(d.getTime())) return trimmed;
|
||||
return d.toISOString();
|
||||
return platformPickerDateTimeToIso(value);
|
||||
}
|
||||
|
||||
export function formFromDetail(d: AdminMatchDetail): MatchCreateForm {
|
||||
@@ -147,7 +157,6 @@ export function formFromDetail(d: AdminMatchDetail): MatchCreateForm {
|
||||
awayTeamEn: d.awayTeamEn,
|
||||
awayTeamMs: d.awayTeamMs ?? '',
|
||||
isHot: d.isHot,
|
||||
correctScoreEnabled: d.correctScoreEnabled ?? true,
|
||||
displayOrder: d.displayOrder ?? 0,
|
||||
matchName: d.matchName ?? '',
|
||||
stage: d.stage ?? '',
|
||||
@@ -244,7 +253,6 @@ export function buildPlatformPayload(form: MatchCreateForm) {
|
||||
awayTeamMs: form.awayTeamMs.trim() || undefined,
|
||||
startTime: normalizeStartTimeForApi(form.startTime),
|
||||
isHot: form.isHot,
|
||||
correctScoreEnabled: form.correctScoreEnabled,
|
||||
displayOrder: form.displayOrder,
|
||||
matchName: form.matchName.trim() || undefined,
|
||||
stage: form.stage.trim() || undefined,
|
||||
@@ -290,7 +298,6 @@ export function buildMatchUpdatePayload(form: MatchCreateForm) {
|
||||
awayTeamMs: form.awayTeamMs.trim() || undefined,
|
||||
startTime: normalizeStartTimeForApi(form.startTime),
|
||||
isHot: form.isHot,
|
||||
correctScoreEnabled: form.correctScoreEnabled,
|
||||
displayOrder: form.displayOrder,
|
||||
matchName: form.matchName.trim() || undefined,
|
||||
stage: form.stage.trim() || undefined,
|
||||
|
||||
@@ -7,6 +7,10 @@ import api from '../../api';
|
||||
import MatchArchiveDialog from '../../components/MatchArchiveDialog.vue';
|
||||
import { ensureLeagueExpanded } from '../../utils/matchesListState';
|
||||
import { formatAmount } from '../../utils/format-amount';
|
||||
import {
|
||||
formatPlatformMatchDateTime,
|
||||
platformPickerDateTimeToIso,
|
||||
} from '@thebet365/shared';
|
||||
const props = defineProps<{
|
||||
leagueId: string;
|
||||
filterStatus: string;
|
||||
@@ -180,7 +184,7 @@ function matchId(row: unknown) {
|
||||
return String(rowOf(row).id ?? '');
|
||||
}
|
||||
function matchTime(row: unknown) {
|
||||
return new Date(String(rowOf(row).startTime)).toLocaleString();
|
||||
return formatPlatformMatchDateTime(String(rowOf(row).startTime), locale.value);
|
||||
}
|
||||
function betCount(row: unknown) {
|
||||
return Number(rowOf(row).betCount ?? 0);
|
||||
@@ -260,6 +264,13 @@ async function promptReopenKickoff(): Promise<string | null> {
|
||||
},
|
||||
t('match.reopen_kickoff_hint'),
|
||||
),
|
||||
h(
|
||||
'p',
|
||||
{
|
||||
style: 'margin: 0 0 12px; font-size: 12px; color: var(--el-text-color-secondary)',
|
||||
},
|
||||
t('match.timezone.platform_hint'),
|
||||
),
|
||||
h(ElDatePicker, {
|
||||
modelValue: kickoff.value,
|
||||
'onUpdate:modelValue': (v: string) => {
|
||||
@@ -275,7 +286,8 @@ async function promptReopenKickoff(): Promise<string | null> {
|
||||
cancelButtonText: t('common.cancel'),
|
||||
beforeClose: (action, _instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
if (!kickoff.value || new Date(kickoff.value) <= new Date()) {
|
||||
const iso = kickoff.value ? platformPickerDateTimeToIso(kickoff.value) : '';
|
||||
if (!iso || Number.isNaN(new Date(iso).getTime()) || new Date(iso) <= new Date()) {
|
||||
ElMessage.warning(t('match.reopen_kickoff_invalid'));
|
||||
return;
|
||||
}
|
||||
@@ -283,7 +295,7 @@ async function promptReopenKickoff(): Promise<string | null> {
|
||||
done();
|
||||
},
|
||||
});
|
||||
return kickoff.value || null;
|
||||
return kickoff.value ? platformPickerDateTimeToIso(kickoff.value) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -316,6 +328,11 @@ defineExpose({ reload: load });
|
||||
|
||||
<template>
|
||||
<div class="league-matches-panel">
|
||||
<div class="nested-panel-toolbar">
|
||||
<el-button type="primary" size="small" @click.stop="emit('add-match')">
|
||||
{{ t('match.create_fixture_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table v-loading="loading" :data="matches" stripe row-key="id" class="nested-match-table">
|
||||
<el-table-column prop="id" label="ID" width="64" />
|
||||
<el-table-column :label="t('match.col.matchup')" min-width="180">
|
||||
@@ -328,7 +345,7 @@ defineExpose({ reload: load });
|
||||
<el-tag :type="matchStatusType(row)" size="small">{{ matchStatusLabel(row) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('match.col.kickoff')" min-width="150">
|
||||
<el-table-column :label="t('match.col.kickoff')" min-width="190">
|
||||
<template #default="{ row }">{{ matchTime(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('match.col.bet_count')" width="72" align="center">
|
||||
@@ -347,18 +364,15 @@ defineExpose({ reload: load });
|
||||
<span v-else class="bet-stat-zero">0</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="460" align="center">
|
||||
<el-table-column width="256" align="center">
|
||||
<template #header>
|
||||
<div class="actions-col-header">
|
||||
<span class="actions-col-header__label">{{ t('common.actions') }}</span>
|
||||
<el-button type="primary" size="small" @click.stop="emit('add-match')">
|
||||
{{ t('match.create_fixture_btn') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<div class="action-btns">
|
||||
<div class="action-group">
|
||||
<div class="action-row action-row--primary">
|
||||
<el-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@@ -371,13 +385,12 @@ defineExpose({ reload: load });
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
class="action-btn--markets"
|
||||
:disabled="!canManage(row)"
|
||||
@click="openMarkets(matchId(row))"
|
||||
>
|
||||
{{ t('match.btn.markets') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="action-group">
|
||||
<el-button
|
||||
v-if="canPublishRow(row)"
|
||||
size="small"
|
||||
@@ -394,6 +407,8 @@ defineExpose({ reload: load });
|
||||
>
|
||||
{{ t('match.btn.unpublish') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="action-row action-row--workflow">
|
||||
<el-button
|
||||
size="small"
|
||||
type="warning"
|
||||
@@ -419,15 +434,15 @@ defineExpose({ reload: load });
|
||||
>
|
||||
{{ settleButtonLabel(row) }}
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
@click="confirmDelete(row)"
|
||||
>
|
||||
{{ t('common.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
plain
|
||||
@click="confirmDelete(row)"
|
||||
>
|
||||
{{ t('common.delete') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -456,8 +471,17 @@ defineExpose({ reload: load });
|
||||
|
||||
<style scoped>
|
||||
.league-matches-panel {
|
||||
width: min(100%, calc(100vw - 272px));
|
||||
max-width: calc(100vw - 272px);
|
||||
padding: 10px 12px 12px;
|
||||
background: #0a0a0a;
|
||||
overflow: hidden;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
.nested-panel-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.nested-pager {
|
||||
display: flex;
|
||||
@@ -477,8 +501,8 @@ defineExpose({ reload: load });
|
||||
|
||||
.actions-col-header__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #aaa;
|
||||
font-weight: 700;
|
||||
color: var(--text-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -502,43 +526,70 @@ defineExpose({ reload: load });
|
||||
}
|
||||
.empty-hint {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.matchup-link {
|
||||
color: var(--green-text);
|
||||
color: var(--primary-link);
|
||||
}
|
||||
.bet-stat-active {
|
||||
color: var(--green-text);
|
||||
color: var(--success-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
.bet-stat-zero {
|
||||
color: #555;
|
||||
color: #aaa49a;
|
||||
}
|
||||
.action-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-rows: repeat(2, 26px);
|
||||
align-items: center;
|
||||
gap: 6px 8px;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.action-group {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
.action-row {
|
||||
display: inline-grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: max-content;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
min-width: 0;
|
||||
padding: 2px 4px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border-soft);
|
||||
}
|
||||
.action-row--primary {
|
||||
background: #fbfaf7;
|
||||
}
|
||||
.action-row--workflow {
|
||||
background: #ffffff;
|
||||
}
|
||||
.action-btns :deep(.el-button) {
|
||||
margin: 0 !important;
|
||||
min-width: 52px;
|
||||
padding: 4px 10px !important;
|
||||
min-width: 48px;
|
||||
height: 22px !important;
|
||||
min-height: 22px !important;
|
||||
padding: 0 9px !important;
|
||||
font-size: 12px !important;
|
||||
line-height: 1 !important;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.action-btns :deep(.action-btn--markets:not(.is-disabled):not(:disabled)) {
|
||||
background: var(--info-bg) !important;
|
||||
border-color: var(--info-border) !important;
|
||||
color: var(--info-text) !important;
|
||||
}
|
||||
|
||||
.action-btns :deep(.action-btn--markets:not(.is-disabled):not(:disabled):hover),
|
||||
.action-btns :deep(.action-btn--markets:not(.is-disabled):not(:disabled):focus) {
|
||||
background: #d5ecfb !important;
|
||||
border-color: #9fd0ed !important;
|
||||
color: #155b86 !important;
|
||||
}
|
||||
|
||||
.action-btns :deep(.el-button:not(.is-disabled):not(:disabled)) {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -547,4 +598,21 @@ defineExpose({ reload: load });
|
||||
.action-btns :deep(.el-button:disabled) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.league-matches-panel {
|
||||
width: calc(100vw - 32px);
|
||||
max-width: calc(100vw - 32px);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.nested-panel-toolbar,
|
||||
.nested-pager {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -901,7 +901,8 @@ watch(
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px 16px 16px;
|
||||
border-bottom: 1px solid #1a1a1a;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
.outright-odds-panel__head {
|
||||
display: flex;
|
||||
@@ -1027,8 +1028,8 @@ watch(
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
padding: 8px 10px 8px 8px;
|
||||
background: #111;
|
||||
border: 1px solid #262626;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
border-color 0.15s ease,
|
||||
@@ -1036,7 +1037,8 @@ watch(
|
||||
}
|
||||
|
||||
.team-row-wrap:hover .team-row {
|
||||
border-color: #3a3a3a;
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.team-row-wrap--batch-selected .team-row {
|
||||
@@ -1160,12 +1162,13 @@ watch(
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.team-row__odds {
|
||||
width: 72px;
|
||||
.team-row__odds,
|
||||
.team-row__odds.el-input-number {
|
||||
width: 86px;
|
||||
}
|
||||
|
||||
.team-row__odds :deep(.el-input-number) {
|
||||
width: 72px;
|
||||
.team-row__odds :deep(.el-input__wrapper) {
|
||||
padding-right: 28px !important;
|
||||
}
|
||||
|
||||
.add-teams-dialog__toolbar {
|
||||
@@ -1306,4 +1309,93 @@ watch(
|
||||
letter-spacing: 0.05em;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.outright-odds-panel__hint,
|
||||
.outright-odds-panel__batch-count,
|
||||
.outright-odds-panel__sort-label,
|
||||
.outright-odds-panel__empty,
|
||||
.team-row__rank,
|
||||
.team-row__meta,
|
||||
.team-row__odds-label,
|
||||
.add-teams-dialog__count,
|
||||
.add-teams-dialog__odds,
|
||||
.add-teams-dialog__empty,
|
||||
.add-teams-dialog__custom-hint,
|
||||
.add-team-pick__code {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.outright-odds-panel__workflow-hint {
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.outright-odds-panel__batch,
|
||||
.team-row,
|
||||
.add-team-pick {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.team-row-wrap:hover .team-row,
|
||||
.add-team-pick:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.team-row-wrap--batch-selected .team-row,
|
||||
.add-team-pick--selected {
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.team-list-scroll {
|
||||
scrollbar-color: #d5cfc3 transparent;
|
||||
}
|
||||
|
||||
.team-list-scroll::-webkit-scrollbar-thumb {
|
||||
background: #d5cfc3;
|
||||
}
|
||||
|
||||
.team-row__trash {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.team-row__trash:hover {
|
||||
background: var(--danger-bg);
|
||||
color: var(--danger-text);
|
||||
}
|
||||
|
||||
.team-row__flag {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.team-row__name,
|
||||
.add-team-pick__name {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.add-teams-dialog__actions {
|
||||
border-bottom-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.add-teams-dialog__custom :deep(.el-form-item__label) {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.team-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.team-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.outright-odds-panel__batch,
|
||||
.outright-odds-panel__sort {
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -203,8 +203,8 @@ function onOutrightArchived() {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: rgba(212, 175, 55, 0.04);
|
||||
border: 1px solid rgba(212, 175, 55, 0.14);
|
||||
background: var(--warning-bg);
|
||||
border: 1px solid var(--warning-border);
|
||||
}
|
||||
.panel-head {
|
||||
display: flex;
|
||||
@@ -222,11 +222,11 @@ function onOutrightArchived() {
|
||||
.panel-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: var(--green-text);
|
||||
color: var(--warning-text);
|
||||
}
|
||||
.panel-hint {
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.panel-actions {
|
||||
@@ -241,18 +241,18 @@ function onOutrightArchived() {
|
||||
.meta-line {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
color: var(--text);
|
||||
}
|
||||
.meta-warn {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: #e6a23c;
|
||||
color: var(--warning-text);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.meta-empty {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.preview-table {
|
||||
margin-top: 6px;
|
||||
|
||||
@@ -194,9 +194,9 @@ async function saveMeta() {
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="form-section form-section--schedule">
|
||||
<div class="section-label">{{ t('matchEditor.group.schedule') }}</div>
|
||||
<el-row :gutter="12">
|
||||
<el-row :gutter="12" class="schedule-fields">
|
||||
<el-col :xs="24" :sm="12">
|
||||
<el-form-item :label="t('match.field.kickoff')" required>
|
||||
<el-date-picker
|
||||
@@ -207,6 +207,7 @@ async function saveMeta() {
|
||||
:placeholder="t('matchEditor.ph.kickoff')"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<p class="field-hint schedule-timezone-hint">{{ t('match.timezone.platform_hint') }}</p>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12">
|
||||
@@ -234,11 +235,6 @@ async function saveMeta() {
|
||||
<el-switch v-model="form.isHot" size="small" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="6">
|
||||
<el-form-item :label="t('matchEditor.field.correct_score_enabled')">
|
||||
<el-switch v-model="form.correctScoreEnabled" size="small" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form>
|
||||
@@ -258,13 +254,15 @@ async function saveMeta() {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-bottom: 24px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #111;
|
||||
border: 1px solid #252525;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 12px 14px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
@@ -274,14 +272,14 @@ async function saveMeta() {
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #222;
|
||||
border-bottom: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--text);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.panel-foot {
|
||||
@@ -289,7 +287,7 @@ async function saveMeta() {
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid #222;
|
||||
border-top: 1px solid var(--border-soft);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
@@ -303,9 +301,9 @@ async function saveMeta() {
|
||||
.section-label {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--green-text);
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--success-text);
|
||||
letter-spacing: 0;
|
||||
text-transform: none;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -313,6 +311,44 @@ async function saveMeta() {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-section--schedule {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.schedule-fields {
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-form-item) {
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-form-item__label) {
|
||||
flex: 0 0 auto;
|
||||
width: auto !important;
|
||||
min-width: 64px;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
word-break: keep-all;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-form-item__content) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-date-editor),
|
||||
.schedule-fields :deep(.el-input),
|
||||
.schedule-fields :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-switch) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logo-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -339,7 +375,7 @@ async function saveMeta() {
|
||||
|
||||
.meta-form :deep(.el-input__inner),
|
||||
.meta-form :deep(.el-input-number .el-input__inner) {
|
||||
color: #fff !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.field-hint {
|
||||
@@ -349,6 +385,10 @@ async function saveMeta() {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.schedule-timezone-hint {
|
||||
margin: 4px 0 0;
|
||||
}
|
||||
|
||||
.league-readonly-grid {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -360,7 +400,7 @@ async function saveMeta() {
|
||||
height: 40px;
|
||||
object-fit: contain;
|
||||
border-radius: 6px;
|
||||
background: #1a1a1a;
|
||||
background: #f4f0e8;
|
||||
}
|
||||
|
||||
.league-readonly-names {
|
||||
@@ -374,7 +414,7 @@ async function saveMeta() {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #ddd;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.league-readonly-lang {
|
||||
@@ -382,4 +422,58 @@ async function saveMeta() {
|
||||
color: #8e8e93;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.panel-head,
|
||||
.panel-foot {
|
||||
border-color: var(--border-soft);
|
||||
}
|
||||
|
||||
.panel-title,
|
||||
.preview-title,
|
||||
.preview-bar-title,
|
||||
.league-readonly-line {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
color: var(--success-text);
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.logo-inline-label,
|
||||
.meta-form :deep(.el-form-item__label),
|
||||
.field-hint,
|
||||
.league-readonly-lang {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.meta-form :deep(.el-input__inner),
|
||||
.meta-form :deep(.el-input-number .el-input__inner) {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
.league-readonly-logo img {
|
||||
background: #f4f0e8;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.panel-head,
|
||||
.panel-foot,
|
||||
.league-readonly-grid {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-form-item) {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.schedule-fields :deep(.el-form-item__label) {
|
||||
width: auto !important;
|
||||
min-width: 0;
|
||||
padding-right: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,6 +17,12 @@ const loading = ref(false);
|
||||
const status = ref('DRAFT');
|
||||
const matchLabel = ref('');
|
||||
|
||||
function matchup(home: string, away: string) {
|
||||
const h = home.trim();
|
||||
const a = away.trim();
|
||||
return h && a ? `${h} vs ${a}` : '';
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!matchId.value) return;
|
||||
loading.value = true;
|
||||
@@ -29,11 +35,15 @@ async function load() {
|
||||
return;
|
||||
}
|
||||
status.value = detail.status;
|
||||
const home = detail.homeTeamZh || detail.homeTeamEn || detail.homeTeamCode || '';
|
||||
const away = detail.awayTeamZh || detail.awayTeamEn || detail.awayTeamCode || '';
|
||||
matchLabel.value =
|
||||
detail.matchName?.trim() ||
|
||||
(home && away ? `${home} vs ${away}` : `#${matchId.value}`);
|
||||
const zh = matchup(
|
||||
detail.homeTeamZh || detail.homeTeamCode || detail.homeTeamEn || '',
|
||||
detail.awayTeamZh || detail.awayTeamCode || detail.awayTeamEn || '',
|
||||
);
|
||||
const en = matchup(
|
||||
detail.homeTeamEn || detail.homeTeamCode || detail.homeTeamZh || '',
|
||||
detail.awayTeamEn || detail.awayTeamCode || detail.awayTeamZh || '',
|
||||
);
|
||||
matchLabel.value = zh && en && zh !== en ? `${zh} / ${en}` : zh || en || `#${matchId.value}`;
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
|
||||
@@ -46,7 +56,7 @@ watch(matchId, load, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="match-markets-page page-scroll">
|
||||
<div v-loading="loading" class="match-markets-page">
|
||||
<AdminSubNav
|
||||
:title="t('matchEditor.section_markets')"
|
||||
:subtitle="matchLabel"
|
||||
@@ -64,13 +74,21 @@ watch(matchId, load, { immediate: true });
|
||||
|
||||
<style scoped>
|
||||
.match-markets-page {
|
||||
padding: 0 4px 24px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 0 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #111;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,81 +1,249 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { useAdminLocale } from '../../composables/useAdminLocale';
|
||||
import api from '../../api';
|
||||
import type { AdminMatchDetail } from '../match-form';
|
||||
import { defaultSelectionName } from '../../utils/selectionDefaults';
|
||||
import { adminSelectionLabel } from '../../utils/adminSelectionLabel';
|
||||
|
||||
const props = defineProps<{
|
||||
matchId: string;
|
||||
}>();
|
||||
|
||||
const { t } = useAdminLocale();
|
||||
const { t, locale } = useAdminLocale();
|
||||
|
||||
const LOCALES = [
|
||||
{ code: 'zh-CN', label: '中文' },
|
||||
{ code: 'en-US', label: 'English' },
|
||||
{ code: 'ms-MY', label: 'Bahasa Melayu' },
|
||||
] as const;
|
||||
|
||||
const UI_TEXT: Record<string, Record<string, string>> = {
|
||||
add: { 'zh-CN': '添加', 'en-US': 'Add', 'ms-MY': 'Tambah' },
|
||||
addMarket: { 'zh-CN': '增加盘口', 'en-US': 'Add market', 'ms-MY': 'Tambah pasaran' },
|
||||
addScore: { 'zh-CN': '新增比分', 'en-US': 'Add score', 'ms-MY': 'Tambah skor' },
|
||||
cancel: { 'zh-CN': '取消', 'en-US': 'Cancel', 'ms-MY': 'Batal' },
|
||||
close: { 'zh-CN': '关闭', 'en-US': 'Close', 'ms-MY': 'Tutup' },
|
||||
copyCreated: { 'zh-CN': '已复制盘口线', 'en-US': 'Market line duplicated', 'ms-MY': 'Garisan pasaran disalin' },
|
||||
copyMarket: { 'zh-CN': '复制盘口', 'en-US': 'Duplicate market', 'ms-MY': 'Salin pasaran' },
|
||||
copyMarketCreated: { 'zh-CN': '已复制盘口', 'en-US': 'Market duplicated', 'ms-MY': 'Pasaran disalin' },
|
||||
copyMarketShort: { 'zh-CN': '复制盘口', 'en-US': 'Duplicate', 'ms-MY': 'Salin pasaran' },
|
||||
copyLine: { 'zh-CN': '复制盘口线', 'en-US': 'Duplicate line', 'ms-MY': 'Salin garisan' },
|
||||
copyLineShort: { 'zh-CN': '复制线', 'en-US': 'Copy', 'ms-MY': 'Salin' },
|
||||
defaultTemplate: { 'zh-CN': '默认', 'en-US': 'Default', 'ms-MY': 'Lalai' },
|
||||
fillScoreRange: { 'zh-CN': '补齐 0-4 比分', 'en-US': 'Fill 0-4 scores', 'ms-MY': 'Lengkapkan skor 0-4' },
|
||||
hidden: { 'zh-CN': '玩家端隐藏', 'en-US': 'Hidden on player', 'ms-MY': 'Disembunyikan pemain' },
|
||||
lineValue: { 'zh-CN': '盘口线', 'en-US': 'Line', 'ms-MY': 'Garisan' },
|
||||
loadTemplate: { 'zh-CN': '载入模板', 'en-US': 'Load template', 'ms-MY': 'Muat templat' },
|
||||
marketDetails: { 'zh-CN': '赛事详情', 'en-US': 'Match details', 'ms-MY': 'Butiran perlawanan' },
|
||||
marketName: { 'zh-CN': '盘口名称', 'en-US': 'Market name', 'ms-MY': 'Nama pasaran' },
|
||||
marketToAdd: { 'zh-CN': '选择要添加的盘口', 'en-US': 'Choose a market to add', 'ms-MY': 'Pilih pasaran untuk ditambah' },
|
||||
marketUnavailable: { 'zh-CN': '暂不开放玩家端', 'en-US': 'Not available on player', 'ms-MY': 'Belum tersedia untuk pemain' },
|
||||
matchMarkets: { 'zh-CN': '赛事盘口', 'en-US': 'Match markets', 'ms-MY': 'Pasaran perlawanan' },
|
||||
moveDown: { 'zh-CN': '下移', 'en-US': 'Move down', 'ms-MY': 'Turun' },
|
||||
moveUp: { 'zh-CN': '上移', 'en-US': 'Move up', 'ms-MY': 'Naik' },
|
||||
odds: { 'zh-CN': '赔率', 'en-US': 'Odds', 'ms-MY': 'Odds' },
|
||||
parlay: { 'zh-CN': '串关', 'en-US': 'Parlay', 'ms-MY': 'Parlay' },
|
||||
playerVisible: { 'zh-CN': '玩家端显示', 'en-US': 'Show on player', 'ms-MY': 'Papar pemain' },
|
||||
promoLabel: { 'zh-CN': '促销标签', 'en-US': 'Promo label', 'ms-MY': 'Label promosi' },
|
||||
saveAsTemplate: { 'zh-CN': '保存为模板', 'en-US': 'Save as template', 'ms-MY': 'Simpan templat' },
|
||||
saveTemplateTitle: { 'zh-CN': '保存为盘口模板', 'en-US': 'Save as market template', 'ms-MY': 'Simpan sebagai templat pasaran' },
|
||||
selectMarket: { 'zh-CN': '从盘口池选择', 'en-US': 'Select from market pool', 'ms-MY': 'Pilih daripada kumpulan pasaran' },
|
||||
selectTemplate: { 'zh-CN': '选择模板', 'en-US': 'Select template', 'ms-MY': 'Pilih templat' },
|
||||
single: { 'zh-CN': '单关', 'en-US': 'Single', 'ms-MY': 'Tunggal' },
|
||||
status: { 'zh-CN': '状态', 'en-US': 'Status', 'ms-MY': 'Status' },
|
||||
statusClosed: { 'zh-CN': '已关闭', 'en-US': 'Closed', 'ms-MY': 'Ditutup' },
|
||||
statusOpen: { 'zh-CN': '可售', 'en-US': 'Open', 'ms-MY': 'Dibuka' },
|
||||
statusSuspended: { 'zh-CN': '暂停', 'en-US': 'Suspended', 'ms-MY': 'Digantung' },
|
||||
templateLoaded: { 'zh-CN': '模板已载入', 'en-US': 'Template loaded', 'ms-MY': 'Templat dimuatkan' },
|
||||
templateNameInput: { 'zh-CN': '请输入模板名称', 'en-US': 'Enter template name', 'ms-MY': 'Masukkan nama templat' },
|
||||
templateSaved: { 'zh-CN': '模板已保存', 'en-US': 'Template saved', 'ms-MY': 'Templat disimpan' },
|
||||
unsaved: { 'zh-CN': '未保存', 'en-US': 'Unsaved', 'ms-MY': 'Belum disimpan' },
|
||||
};
|
||||
|
||||
type LocalizedText = Record<string, string>;
|
||||
|
||||
interface MarketDefinition {
|
||||
marketType: string;
|
||||
marketKey: string;
|
||||
period: string;
|
||||
sortOrder: number;
|
||||
defaultLineValue: number | null;
|
||||
allowSingle: boolean;
|
||||
allowParlay: boolean;
|
||||
showOnPlayer: boolean;
|
||||
settlementSupported: boolean;
|
||||
usesLineValue: boolean;
|
||||
renderType: 'standard' | 'correctScore' | 'unsupported';
|
||||
nameI18n: LocalizedText;
|
||||
selectionTemplate: Array<{
|
||||
code: string;
|
||||
name: string;
|
||||
nameI18n: LocalizedText;
|
||||
odds: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface SelectionRow {
|
||||
id: string;
|
||||
id?: string;
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
nameI18n: LocalizedText;
|
||||
odds: number;
|
||||
status: string;
|
||||
editOdds: number;
|
||||
sortOrder: number;
|
||||
}
|
||||
|
||||
interface MarketRow {
|
||||
id: string;
|
||||
id?: string;
|
||||
marketType: string;
|
||||
marketKey: string;
|
||||
period: string;
|
||||
lineValue: number | null;
|
||||
paramsJson: Record<string, unknown> | null;
|
||||
status: string;
|
||||
allowSingle: boolean;
|
||||
allowParlay: boolean;
|
||||
showOnPlayer: boolean;
|
||||
sortOrder: number;
|
||||
promoLabel: string;
|
||||
editPromoLabel: string;
|
||||
editLineValue: number | null;
|
||||
promoLabelI18n: LocalizedText;
|
||||
nameI18n: LocalizedText;
|
||||
selections: SelectionRow[];
|
||||
}
|
||||
|
||||
const DEFAULT_MARKET_TYPES = [
|
||||
'FT_1X2',
|
||||
'FT_HANDICAP',
|
||||
'FT_OVER_UNDER',
|
||||
'FT_ODD_EVEN',
|
||||
'HT_1X2',
|
||||
'HT_HANDICAP',
|
||||
'HT_OVER_UNDER',
|
||||
'FT_CORRECT_SCORE',
|
||||
'HT_CORRECT_SCORE',
|
||||
'SH_CORRECT_SCORE',
|
||||
];
|
||||
interface MarketTemplateSummary {
|
||||
id: string;
|
||||
name: string;
|
||||
nameI18n: LocalizedText;
|
||||
isDefault: boolean;
|
||||
status: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const savingMarketId = ref<string | null>(null);
|
||||
const saving = ref(false);
|
||||
const applying = ref(false);
|
||||
const definitions = ref<MarketDefinition[]>([]);
|
||||
const templates = ref<MarketTemplateSummary[]>([]);
|
||||
const selectedTemplateId = ref('');
|
||||
const markets = ref<MarketRow[]>([]);
|
||||
const selectedMarketIndex = ref(0);
|
||||
const scoreHome = ref(0);
|
||||
const scoreAway = ref(0);
|
||||
const addMarketVisible = ref(false);
|
||||
const selectedDefinitionType = ref('');
|
||||
|
||||
const definitionMap = computed(() => new Map(definitions.value.map((d) => [d.marketType, d])));
|
||||
const definitionOptions = computed(() => [...definitions.value].sort((a, b) => a.sortOrder - b.sortOrder));
|
||||
const selectedMarket = computed(() => markets.value[selectedMarketIndex.value] ?? markets.value[0] ?? null);
|
||||
const displayLocales = computed(() => {
|
||||
const zhLabels: Record<string, string> = {
|
||||
'zh-CN': '中文',
|
||||
'en-US': '英语',
|
||||
'ms-MY': '马来语',
|
||||
};
|
||||
return LOCALES.map((loc) => ({
|
||||
code: loc.code,
|
||||
label: locale.value === 'zh-CN' ? zhLabels[loc.code] : loc.label,
|
||||
}));
|
||||
});
|
||||
const dirty = ref(false);
|
||||
|
||||
function localized(input?: Record<string, string> | null, fallback = ''): LocalizedText {
|
||||
const out: LocalizedText = {};
|
||||
for (const loc of LOCALES) {
|
||||
const value = input?.[loc.code]?.trim();
|
||||
if (value) out[loc.code] = value;
|
||||
}
|
||||
if (!Object.keys(out).length && fallback) out['zh-CN'] = fallback;
|
||||
return out;
|
||||
}
|
||||
|
||||
function resolveText(map?: LocalizedText | null, fallback = '') {
|
||||
const order = Array.from(new Set([locale.value, 'en-US', 'zh-CN', 'ms-MY']));
|
||||
for (const loc of order) {
|
||||
const value = map?.[loc]?.trim();
|
||||
if (value) return value;
|
||||
}
|
||||
return Object.values(map ?? {}).find((value) => value?.trim()) || fallback;
|
||||
}
|
||||
|
||||
function ui(key: string) {
|
||||
return resolveText(UI_TEXT[key], key);
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
if (status === 'OPEN') return ui('statusOpen');
|
||||
if (status === 'SUSPENDED') return ui('statusSuspended');
|
||||
if (status === 'CLOSED') return ui('statusClosed');
|
||||
return status;
|
||||
}
|
||||
|
||||
function ensureLocaleKeys(map: LocalizedText, fallback = '') {
|
||||
for (const loc of LOCALES) {
|
||||
if (map[loc.code] == null) map[loc.code] = loc.code === 'zh-CN' ? fallback : '';
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function mapMarkets(detail: AdminMatchDetail) {
|
||||
markets.value = (detail.markets ?? []).map((m) => ({
|
||||
id: m.id,
|
||||
marketType: m.marketType,
|
||||
period: m.period,
|
||||
lineValue: m.lineValue,
|
||||
status: m.status,
|
||||
promoLabel: m.promoLabel,
|
||||
editPromoLabel: m.promoLabel,
|
||||
editLineValue: m.lineValue,
|
||||
selections: m.selections.map((s) => ({
|
||||
id: s.id,
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: s.selectionName,
|
||||
odds: s.odds,
|
||||
status: s.status,
|
||||
editOdds: s.odds,
|
||||
})),
|
||||
}));
|
||||
markets.value = (detail.markets ?? []).map((m, index) => {
|
||||
const def = definitionMap.value.get(m.marketType);
|
||||
const nameI18n = ensureLocaleKeys(localized(m.nameI18n, def ? resolveText(def.nameI18n, m.marketType) : m.marketType), m.marketType);
|
||||
const promoLabelI18n = ensureLocaleKeys(localized(m.promoLabelI18n, m.promoLabel || ''), m.promoLabel || '');
|
||||
return {
|
||||
id: m.id,
|
||||
marketType: m.marketType,
|
||||
marketKey: m.marketKey || m.marketType,
|
||||
period: m.period,
|
||||
lineValue: m.lineValue,
|
||||
paramsJson: m.paramsJson ?? null,
|
||||
status: m.status,
|
||||
allowSingle: true,
|
||||
allowParlay: true,
|
||||
showOnPlayer: m.showOnPlayer ?? true,
|
||||
sortOrder: m.sortOrder ?? index,
|
||||
promoLabel: m.promoLabel ?? '',
|
||||
promoLabelI18n,
|
||||
nameI18n,
|
||||
selections: m.selections.map((s, sIndex) => ({
|
||||
id: s.id,
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: s.selectionName,
|
||||
nameI18n: ensureLocaleKeys(localized(s.nameI18n, s.selectionName), s.selectionName),
|
||||
odds: s.odds,
|
||||
status: s.status,
|
||||
sortOrder: s.sortOrder ?? sIndex,
|
||||
})),
|
||||
};
|
||||
});
|
||||
selectedMarketIndex.value = Math.min(selectedMarketIndex.value, Math.max(0, markets.value.length - 1));
|
||||
dirty.value = false;
|
||||
}
|
||||
|
||||
function usesLineValue(marketType: string) {
|
||||
return definitionMap.value.get(marketType)?.usesLineValue ?? false;
|
||||
}
|
||||
|
||||
function isLineMarket(market: MarketRow) {
|
||||
return usesLineValue(market.marketType);
|
||||
}
|
||||
|
||||
async function loadDefinitionsAndTemplates() {
|
||||
const [{ data: defRes }, { data: tplRes }] = await Promise.all([
|
||||
api.get('/admin/market-definitions'),
|
||||
api.get('/admin/market-templates'),
|
||||
]);
|
||||
definitions.value = defRes.data ?? [];
|
||||
templates.value = tplRes.data ?? [];
|
||||
selectedTemplateId.value =
|
||||
templates.value.find((tpl) => tpl.isDefault)?.id || templates.value[0]?.id || '';
|
||||
}
|
||||
|
||||
async function load() {
|
||||
if (!props.matchId) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
await loadDefinitionsAndTemplates();
|
||||
const { data } = await api.get(`/admin/matches/${props.matchId}`);
|
||||
mapMarkets(data.data as AdminMatchDetail);
|
||||
} catch (e: unknown) {
|
||||
@@ -94,387 +262,846 @@ watch(
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function marketLabel(type: string) {
|
||||
const key = `matchEditor.market.${type}`;
|
||||
const v = t(key);
|
||||
return v !== key ? v : type;
|
||||
function markDirty() {
|
||||
dirty.value = true;
|
||||
}
|
||||
|
||||
function selectionCodeLabel(code: string, market?: MarketRow) {
|
||||
return adminSelectionLabel(t, code, {
|
||||
lineValue: market?.editLineValue ?? market?.lineValue,
|
||||
period: market?.period,
|
||||
function isCorrectScoreMarket(market: MarketRow) {
|
||||
return ['FT_CORRECT_SCORE', 'HT_CORRECT_SCORE', 'SH_CORRECT_SCORE'].includes(market.marketType);
|
||||
}
|
||||
|
||||
function marketTitle(market: MarketRow) {
|
||||
const line = isLineMarket(market) && market.lineValue != null ? ` ${market.lineValue}` : '';
|
||||
return `${resolveText(market.nameI18n, market.marketType)}${line}`;
|
||||
}
|
||||
|
||||
function definitionLabel(def: MarketDefinition) {
|
||||
const label = resolveText(def.nameI18n, def.marketType);
|
||||
return def.settlementSupported ? label : `${label}(${ui('marketUnavailable')})`;
|
||||
}
|
||||
|
||||
function templateLabel(tpl: MarketTemplateSummary) {
|
||||
const label = resolveText(tpl.nameI18n, tpl.name || '');
|
||||
return tpl.isDefault ? `${label}(${ui('defaultTemplate')})` : label;
|
||||
}
|
||||
|
||||
function marketMetaText(market: MarketRow) {
|
||||
const parts = [statusLabel(market.status)];
|
||||
if (isLineMarket(market) && market.lineValue != null) parts.push(`${ui('lineValue')} ${market.lineValue}`);
|
||||
if (!market.showOnPlayer) parts.push(ui('hidden'));
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
function selectionTitle(selection: SelectionRow) {
|
||||
const score = /^SCORE_(\d+)_(\d+)$/.exec(selection.selectionCode);
|
||||
if (score) return `${score[1]}:${score[2]}`;
|
||||
return resolveText(selection.nameI18n, selection.selectionName || selection.selectionCode);
|
||||
}
|
||||
|
||||
function isScoreSelection(selection: SelectionRow) {
|
||||
return /^SCORE_\d+_\d+$/.test(selection.selectionCode);
|
||||
}
|
||||
|
||||
function openAddMarketDialog() {
|
||||
selectedDefinitionType.value = definitionOptions.value[0]?.marketType ?? '';
|
||||
addMarketVisible.value = true;
|
||||
}
|
||||
|
||||
function addSelectedMarket() {
|
||||
const def = definitionMap.value.get(selectedDefinitionType.value);
|
||||
if (!def) return;
|
||||
addMarket(def);
|
||||
addMarketVisible.value = false;
|
||||
selectedDefinitionType.value = '';
|
||||
}
|
||||
|
||||
function addMarket(def: MarketDefinition) {
|
||||
const row: MarketRow = {
|
||||
marketType: def.marketType,
|
||||
marketKey: def.marketKey,
|
||||
period: def.period,
|
||||
lineValue: def.defaultLineValue,
|
||||
paramsJson: null,
|
||||
status: 'OPEN',
|
||||
allowSingle: def.allowSingle,
|
||||
allowParlay: def.allowParlay,
|
||||
showOnPlayer: def.showOnPlayer && def.settlementSupported,
|
||||
sortOrder: markets.value.length,
|
||||
promoLabel: '',
|
||||
promoLabelI18n: ensureLocaleKeys({}),
|
||||
nameI18n: ensureLocaleKeys(localized(def.nameI18n, def.marketType), def.marketType),
|
||||
selections: def.selectionTemplate.map((s, index) => ({
|
||||
selectionCode: s.code,
|
||||
selectionName: s.name,
|
||||
nameI18n: ensureLocaleKeys(localized(s.nameI18n, s.name), s.name),
|
||||
odds: Number(s.odds),
|
||||
status: 'OPEN',
|
||||
sortOrder: index,
|
||||
})),
|
||||
};
|
||||
markets.value.push(row);
|
||||
selectedMarketIndex.value = markets.value.length - 1;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function copyLocalizedName(map: LocalizedText) {
|
||||
const suffix: Record<string, string> = {
|
||||
'zh-CN': ' 副本',
|
||||
'en-US': ' Copy',
|
||||
'ms-MY': ' Salinan',
|
||||
};
|
||||
const next = ensureLocaleKeys(localized(map, resolveText(map)));
|
||||
for (const loc of LOCALES) {
|
||||
const base = next[loc.code] || resolveText(next);
|
||||
next[loc.code] = `${base}${suffix[loc.code]}`;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function clonePlain<T>(value: T): T {
|
||||
return value == null ? value : JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function nextCopiedLineValue(source: MarketRow) {
|
||||
const used = new Set(
|
||||
markets.value
|
||||
.filter((market) => market.marketType === source.marketType && market.lineValue != null)
|
||||
.map((market) => Number(market.lineValue).toFixed(2)),
|
||||
);
|
||||
let candidate = Number(((source.lineValue ?? 0) + 0.25).toFixed(2));
|
||||
while (used.has(candidate.toFixed(2))) {
|
||||
candidate = Number((candidate + 0.25).toFixed(2));
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function duplicateMarket(index: number) {
|
||||
const source = markets.value[index];
|
||||
if (!source) return;
|
||||
const shouldCopyLine = isLineMarket(source);
|
||||
const sourceCopy = clonePlain(source);
|
||||
const paramsCopy = clonePlain(source.paramsJson);
|
||||
const copy: MarketRow = {
|
||||
...sourceCopy,
|
||||
id: undefined,
|
||||
lineValue: shouldCopyLine ? nextCopiedLineValue(source) : null,
|
||||
paramsJson: shouldCopyLine
|
||||
? paramsCopy
|
||||
: { ...(paramsCopy ?? {}), copyId: `copy-${Date.now()}-${index}` },
|
||||
nameI18n: copyLocalizedName(source.nameI18n),
|
||||
sortOrder: markets.value.length,
|
||||
selections: source.selections.map((s) => ({ ...clonePlain(s), id: undefined })),
|
||||
};
|
||||
markets.value.splice(index + 1, 0, copy);
|
||||
selectedMarketIndex.value = index + 1;
|
||||
reorder();
|
||||
markDirty();
|
||||
ElMessage.success(ui(shouldCopyLine ? 'copyCreated' : 'copyMarketCreated'));
|
||||
}
|
||||
|
||||
function removeMarket(index: number) {
|
||||
markets.value.splice(index, 1);
|
||||
selectedMarketIndex.value = Math.min(selectedMarketIndex.value, Math.max(0, markets.value.length - 1));
|
||||
reorder();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function moveMarket(index: number, direction: -1 | 1) {
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= markets.value.length) return;
|
||||
const [row] = markets.value.splice(index, 1);
|
||||
markets.value.splice(target, 0, row);
|
||||
selectedMarketIndex.value = target;
|
||||
reorder();
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function reorder() {
|
||||
markets.value.forEach((m, index) => {
|
||||
m.sortOrder = index;
|
||||
});
|
||||
}
|
||||
|
||||
function hasLine(market: MarketRow) {
|
||||
return market.lineValue != null || market.editLineValue != null;
|
||||
function addScoreRange(market: MarketRow) {
|
||||
const existing = new Set(market.selections.map((s) => s.selectionCode));
|
||||
for (let home = 0; home <= 4; home += 1) {
|
||||
for (let away = 0; away <= 4; away += 1) {
|
||||
const code = `SCORE_${home}_${away}`;
|
||||
if (existing.has(code)) continue;
|
||||
market.selections.push({
|
||||
selectionCode: code,
|
||||
selectionName: `${home}-${away}`,
|
||||
nameI18n: ensureLocaleKeys({ 'zh-CN': `${home}-${away}`, 'en-US': `${home}-${away}`, 'ms-MY': `${home}-${away}` }),
|
||||
odds: 8,
|
||||
status: 'OPEN',
|
||||
sortOrder: market.selections.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
markDirty();
|
||||
}
|
||||
|
||||
const CORRECT_SCORE_TYPES = new Set([
|
||||
'FT_CORRECT_SCORE',
|
||||
'HT_CORRECT_SCORE',
|
||||
'SH_CORRECT_SCORE',
|
||||
]);
|
||||
|
||||
function isMultiRowMarket(market: MarketRow) {
|
||||
return CORRECT_SCORE_TYPES.has(market.marketType) || market.selections.length > 6;
|
||||
function addScore(market: MarketRow) {
|
||||
const home = Math.max(0, Number(scoreHome.value) || 0);
|
||||
const away = Math.max(0, Number(scoreAway.value) || 0);
|
||||
const code = `SCORE_${home}_${away}`;
|
||||
if (market.selections.some((s) => s.selectionCode === code)) return;
|
||||
market.selections.push({
|
||||
selectionCode: code,
|
||||
selectionName: `${home}-${away}`,
|
||||
nameI18n: ensureLocaleKeys({ 'zh-CN': `${home}-${away}`, 'en-US': `${home}-${away}`, 'ms-MY': `${home}-${away}` }),
|
||||
odds: 8,
|
||||
status: 'OPEN',
|
||||
sortOrder: market.selections.length,
|
||||
});
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function normPromo(value: string | null | undefined) {
|
||||
return (value ?? '').trim() || null;
|
||||
function removeSelection(market: MarketRow, index: number) {
|
||||
market.selections.splice(index, 1);
|
||||
market.selections.forEach((s, i) => {
|
||||
s.sortOrder = i;
|
||||
});
|
||||
markDirty();
|
||||
}
|
||||
|
||||
function normLine(value: number | null | undefined) {
|
||||
return value == null ? null : Number(value);
|
||||
function payloadMarkets() {
|
||||
return markets.value.map((m, index) => ({
|
||||
id: m.id,
|
||||
marketType: m.marketType,
|
||||
marketKey: m.marketKey,
|
||||
lineKey: null,
|
||||
period: m.period,
|
||||
lineValue: usesLineValue(m.marketType) ? m.lineValue : null,
|
||||
paramsJson: m.paramsJson,
|
||||
status: m.status,
|
||||
allowSingle: m.allowSingle,
|
||||
allowParlay: m.allowParlay,
|
||||
showOnPlayer: m.showOnPlayer,
|
||||
sortOrder: index,
|
||||
promoLabel: resolveText(m.promoLabelI18n, m.promoLabel),
|
||||
promoLabelI18n: localized(m.promoLabelI18n),
|
||||
nameI18n: localized(m.nameI18n, m.marketType),
|
||||
selections: m.selections.map((s, sIndex) => ({
|
||||
id: s.id,
|
||||
selectionCode: s.selectionCode,
|
||||
selectionName: resolveText(s.nameI18n, s.selectionName),
|
||||
nameI18n: localized(s.nameI18n, s.selectionName),
|
||||
odds: Number(s.odds),
|
||||
status: s.status,
|
||||
sortOrder: sIndex,
|
||||
})),
|
||||
}));
|
||||
}
|
||||
|
||||
function isMarketDirty(market: MarketRow) {
|
||||
if (normPromo(market.editPromoLabel) !== normPromo(market.promoLabel)) return true;
|
||||
if (normLine(market.editLineValue) !== normLine(market.lineValue)) return true;
|
||||
return market.selections.some((s) => Number(s.editOdds) !== Number(s.odds));
|
||||
}
|
||||
|
||||
async function generateTemplates() {
|
||||
loading.value = true;
|
||||
async function applyTemplate() {
|
||||
if (!selectedTemplateId.value) return;
|
||||
applying.value = true;
|
||||
try {
|
||||
await api.post(`/admin/matches/${props.matchId}/markets/templates`, {
|
||||
marketTypes: DEFAULT_MARKET_TYPES,
|
||||
await api.post(`/admin/matches/${props.matchId}/markets/apply-template`, {
|
||||
templateId: selectedTemplateId.value,
|
||||
});
|
||||
ElMessage.success(t('matchEditor.templates_generated'));
|
||||
ElMessage.success(ui('templateLoaded'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
applying.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveMarket(market: MarketRow) {
|
||||
const invalid = market.selections.find((s) => !s.editOdds || s.editOdds <= 1);
|
||||
async function saveAll() {
|
||||
const invalid = markets.value.flatMap((m) => m.selections).find((s) => !s.odds || s.odds <= 1);
|
||||
if (invalid) {
|
||||
ElMessage.warning(t('outright.err_odds_min'));
|
||||
return;
|
||||
}
|
||||
|
||||
savingMarketId.value = market.id;
|
||||
saving.value = true;
|
||||
try {
|
||||
await api.patch(`/admin/markets/${market.id}`, {
|
||||
promoLabel: market.editPromoLabel.trim() || null,
|
||||
lineValue: market.editLineValue,
|
||||
await api.put(`/admin/matches/${props.matchId}/markets/bulk`, {
|
||||
markets: payloadMarkets(),
|
||||
});
|
||||
await api.put(`/admin/matches/${props.matchId}/odds`, {
|
||||
updates: market.selections.map((s) => ({
|
||||
selectionId: s.id,
|
||||
odds: s.editOdds,
|
||||
})),
|
||||
});
|
||||
for (const s of market.selections) {
|
||||
const name = defaultSelectionName(s.selectionCode, {
|
||||
lineValue: market.editLineValue,
|
||||
period: market.period,
|
||||
});
|
||||
if (name !== s.selectionName) {
|
||||
await api.patch(`/admin/selections/${s.id}`, {
|
||||
selectionName: name,
|
||||
});
|
||||
}
|
||||
}
|
||||
ElMessage.success(t('msg.saved'));
|
||||
await load();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
savingMarketId.value = null;
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAsTemplate() {
|
||||
let value = '';
|
||||
try {
|
||||
const result = await ElMessageBox.prompt(ui('templateNameInput'), ui('saveTemplateTitle'), {
|
||||
confirmButtonText: t('common.save'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
inputValue: `${resolveText({ 'zh-CN': '自定义足球盘口模板', 'en-US': 'Custom football market template', 'ms-MY': 'Templat pasaran bola sepak tersuai' })}`,
|
||||
});
|
||||
value = result.value?.trim() || '';
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!value) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
await api.post('/admin/market-templates', {
|
||||
name: value,
|
||||
nameI18n: { 'zh-CN': value },
|
||||
items: payloadMarkets(),
|
||||
});
|
||||
ElMessage.success(ui('templateSaved'));
|
||||
await loadDefinitionsAndTemplates();
|
||||
} catch (e: unknown) {
|
||||
const err = e as { response?: { data?: { error?: string } } };
|
||||
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="match-markets-panel">
|
||||
<div class="panel-head">
|
||||
<span class="panel-title">{{ t('matchEditor.section_markets') }}</span>
|
||||
<el-button size="small" type="primary" plain @click="generateTemplates">
|
||||
{{ t('matchEditor.generate_templates') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<p v-if="!markets.length" class="empty-hint">{{ t('matchEditor.no_markets') }}</p>
|
||||
|
||||
<div v-else class="market-lines">
|
||||
<div
|
||||
v-for="market in markets"
|
||||
:key="market.id"
|
||||
class="market-line"
|
||||
:class="{ 'market-line--wrap': isMultiRowMarket(market) }"
|
||||
>
|
||||
<label class="field-promo-wrap">
|
||||
<span class="promo-label">{{ t('matchEditor.field.promo_label_optional') }}</span>
|
||||
<el-input
|
||||
v-model="market.editPromoLabel"
|
||||
size="small"
|
||||
class="field-promo"
|
||||
clearable
|
||||
<div v-loading="loading" class="market-editor">
|
||||
<div class="market-toolbar">
|
||||
<div class="template-tools">
|
||||
<el-select v-model="selectedTemplateId" size="small" class="template-select" :placeholder="ui('selectTemplate')">
|
||||
<el-option
|
||||
v-for="tpl in templates"
|
||||
:key="tpl.id"
|
||||
:label="templateLabel(tpl)"
|
||||
:value="tpl.id"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="market-line-head">
|
||||
<span class="market-label" :title="market.marketType">{{ marketLabel(market.marketType) }}</span>
|
||||
|
||||
<el-input-number
|
||||
v-if="hasLine(market)"
|
||||
v-model="market.editLineValue"
|
||||
size="small"
|
||||
class="field-line"
|
||||
:step="0.25"
|
||||
controls-position="right"
|
||||
/>
|
||||
|
||||
<el-button
|
||||
v-if="isMultiRowMarket(market)"
|
||||
size="small"
|
||||
:type="isMarketDirty(market) ? 'primary' : 'default'"
|
||||
class="btn-save"
|
||||
:disabled="!isMarketDirty(market)"
|
||||
:loading="savingMarketId === market.id"
|
||||
@click="saveMarket(market)"
|
||||
>
|
||||
{{ t('common.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="selections-wrap"
|
||||
:class="isMultiRowMarket(market) ? 'selections-grid' : 'selections-inline'"
|
||||
>
|
||||
<div v-for="sel in market.selections" :key="sel.id" class="sel-inline">
|
||||
<span class="sel-label" :title="sel.selectionCode">{{
|
||||
selectionCodeLabel(sel.selectionCode, market)
|
||||
}}</span>
|
||||
<el-input-number
|
||||
v-model="sel.editOdds"
|
||||
size="small"
|
||||
class="sel-odds"
|
||||
:min="1.01"
|
||||
:step="0.01"
|
||||
controls-position="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
v-if="!isMultiRowMarket(market)"
|
||||
size="small"
|
||||
:type="isMarketDirty(market) ? 'primary' : 'default'"
|
||||
class="btn-save"
|
||||
:disabled="!isMarketDirty(market)"
|
||||
:loading="savingMarketId === market.id"
|
||||
@click="saveMarket(market)"
|
||||
>
|
||||
</el-select>
|
||||
<el-button size="small" :loading="applying" @click="applyTemplate">{{ ui('loadTemplate') }}</el-button>
|
||||
<el-button size="small" @click="openAddMarketDialog">{{ ui('addMarket') }}</el-button>
|
||||
<el-button size="small" @click="saveAsTemplate">{{ ui('saveAsTemplate') }}</el-button>
|
||||
</div>
|
||||
<div class="save-tools">
|
||||
<el-tag v-if="dirty" size="small" type="warning">{{ ui('unsaved') }}</el-tag>
|
||||
<el-button size="small" type="primary" :loading="saving" @click="saveAll">
|
||||
{{ t('common.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-grid">
|
||||
<section class="market-list">
|
||||
<div class="section-title">{{ ui('matchMarkets') }}</div>
|
||||
<p v-if="!markets.length" class="empty-hint">{{ t('matchEditor.no_markets') }}</p>
|
||||
<div
|
||||
v-for="(market, index) in markets"
|
||||
:key="market.id || `${market.marketType}-${index}`"
|
||||
class="market-card"
|
||||
:class="{ active: selectedMarketIndex === index, hidden: !market.showOnPlayer }"
|
||||
@click="selectedMarketIndex = index"
|
||||
>
|
||||
<div class="card-main">
|
||||
<strong>{{ marketTitle(market) }}</strong>
|
||||
<span>{{ marketMetaText(market) }}</span>
|
||||
</div>
|
||||
<div class="card-actions" @click.stop>
|
||||
<el-button
|
||||
class="card-action-btn"
|
||||
size="small"
|
||||
text
|
||||
:title="ui('moveUp')"
|
||||
:disabled="index === 0"
|
||||
@click="moveMarket(index, -1)"
|
||||
>
|
||||
↑
|
||||
</el-button>
|
||||
<el-button
|
||||
class="card-action-btn"
|
||||
size="small"
|
||||
text
|
||||
:title="ui('moveDown')"
|
||||
:disabled="index === markets.length - 1"
|
||||
@click="moveMarket(index, 1)"
|
||||
>
|
||||
↓
|
||||
</el-button>
|
||||
<el-button
|
||||
class="card-action-btn copy-btn"
|
||||
size="small"
|
||||
text
|
||||
:title="ui(isLineMarket(market) ? 'copyLine' : 'copyMarket')"
|
||||
@click="duplicateMarket(index)"
|
||||
>
|
||||
{{ ui(isLineMarket(market) ? 'copyLineShort' : 'copyMarketShort') }}
|
||||
</el-button>
|
||||
<el-button class="card-action-btn" size="small" text type="danger" :title="ui('close')" @click="removeMarket(index)">
|
||||
{{ ui('close') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="selectedMarket" class="detail-panel">
|
||||
<div class="detail-head">
|
||||
<div>
|
||||
<div class="section-title">{{ ui('marketDetails') }}</div>
|
||||
<strong>{{ marketTitle(selectedMarket) }}</strong>
|
||||
</div>
|
||||
<el-switch
|
||||
v-model="selectedMarket.showOnPlayer"
|
||||
:active-text="ui('playerVisible')"
|
||||
@change="markDirty"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label>
|
||||
<span>{{ ui('status') }}</span>
|
||||
<el-select v-model="selectedMarket.status" size="small" @change="markDirty">
|
||||
<el-option :label="ui('statusOpen')" value="OPEN" />
|
||||
<el-option :label="ui('statusSuspended')" value="SUSPENDED" />
|
||||
<el-option :label="ui('statusClosed')" value="CLOSED" />
|
||||
</el-select>
|
||||
</label>
|
||||
<label v-if="isLineMarket(selectedMarket)">
|
||||
<span>{{ ui('lineValue') }}</span>
|
||||
<el-input-number
|
||||
v-model="selectedMarket.lineValue"
|
||||
size="small"
|
||||
:step="0.25"
|
||||
controls-position="right"
|
||||
@change="markDirty"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ ui('single') }}</span>
|
||||
<el-switch v-model="selectedMarket.allowSingle" @change="markDirty" />
|
||||
</label>
|
||||
<label>
|
||||
<span>{{ ui('parlay') }}</span>
|
||||
<el-switch v-model="selectedMarket.allowParlay" @change="markDirty" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="locale-block">
|
||||
<div class="block-title">{{ ui('marketName') }}</div>
|
||||
<div class="locale-grid">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="selectedMarket.nameI18n[loc.code]" size="small" @input="markDirty" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="locale-block">
|
||||
<div class="block-title">{{ ui('promoLabel') }}</div>
|
||||
<div class="locale-grid">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="selectedMarket.promoLabelI18n[loc.code]" size="small" clearable @input="markDirty" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isCorrectScoreMarket(selectedMarket)" class="score-tools">
|
||||
<el-button size="small" @click="addScoreRange(selectedMarket)">{{ ui('fillScoreRange') }}</el-button>
|
||||
<el-input-number v-model="scoreHome" size="small" :min="0" controls-position="right" />
|
||||
<span>-</span>
|
||||
<el-input-number v-model="scoreAway" size="small" :min="0" controls-position="right" />
|
||||
<el-button size="small" @click="addScore(selectedMarket)">{{ ui('addScore') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div class="selection-list">
|
||||
<div
|
||||
v-for="(sel, index) in selectedMarket.selections"
|
||||
:key="sel.id || `${sel.selectionCode}-${index}`"
|
||||
class="selection-row"
|
||||
:class="{ 'selection-row--score': isScoreSelection(sel) }"
|
||||
>
|
||||
<div class="selection-meta">
|
||||
<strong :title="sel.selectionCode">{{ selectionTitle(sel) }}</strong>
|
||||
<label class="selection-field selection-field--status">
|
||||
<span>{{ ui('status') }}</span>
|
||||
<el-select v-model="sel.status" size="small" @change="markDirty">
|
||||
<el-option :label="ui('statusOpen')" value="OPEN" />
|
||||
<el-option :label="ui('statusSuspended')" value="SUSPENDED" />
|
||||
<el-option :label="ui('statusClosed')" value="CLOSED" />
|
||||
</el-select>
|
||||
</label>
|
||||
<label class="selection-field selection-field--odds">
|
||||
<span>{{ ui('odds') }}</span>
|
||||
<el-input-number
|
||||
v-model="sel.odds"
|
||||
size="small"
|
||||
:min="1.01"
|
||||
:step="0.01"
|
||||
controls-position="right"
|
||||
@change="markDirty"
|
||||
/>
|
||||
</label>
|
||||
<el-button size="small" text type="danger" @click="removeSelection(selectedMarket, index)">{{ ui('close') }}</el-button>
|
||||
</div>
|
||||
<div v-if="!isScoreSelection(sel)" class="locale-grid compact">
|
||||
<label v-for="loc in displayLocales" :key="loc.code">
|
||||
<span>{{ loc.label }}</span>
|
||||
<el-input v-model="sel.nameI18n[loc.code]" size="small" @input="markDirty" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="addMarketVisible" class="add-market-dialog" :title="ui('addMarket')" width="460px" destroy-on-close>
|
||||
<label class="add-market-field">
|
||||
<span>{{ ui('marketToAdd') }}</span>
|
||||
<el-select
|
||||
v-model="selectedDefinitionType"
|
||||
class="full-select"
|
||||
filterable
|
||||
:placeholder="ui('selectMarket')"
|
||||
>
|
||||
<el-option
|
||||
v-for="def in definitionOptions"
|
||||
:key="def.marketType"
|
||||
:label="definitionLabel(def)"
|
||||
:value="def.marketType"
|
||||
/>
|
||||
</el-select>
|
||||
</label>
|
||||
<template #footer>
|
||||
<el-button @click="addMarketVisible = false">{{ ui('cancel') }}</el-button>
|
||||
<el-button type="primary" :disabled="!selectedDefinitionType" @click="addSelectedMarket">
|
||||
{{ ui('add') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.match-markets-panel {
|
||||
padding: 10px 12px 12px 16px;
|
||||
.market-editor {
|
||||
background: #0a0a0a;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
.market-toolbar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #ccc;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.market-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.market-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
border: 1px solid #252525;
|
||||
border-radius: 6px;
|
||||
background: #111;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.market-line--wrap {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.market-line--wrap .field-promo-wrap {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.field-promo-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.promo-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #8e8e93;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.field-promo {
|
||||
flex: 0 0 88px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.market-line--wrap .field-promo {
|
||||
flex: 1;
|
||||
max-width: 160px;
|
||||
}
|
||||
|
||||
.market-line-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.market-line:not(.market-line--wrap) .market-line-head {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.market-line--wrap .market-line-head {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.market-line--wrap .market-line-head .btn-save {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.market-label {
|
||||
flex: 0 0 76px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #e8e8e8;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.field-line {
|
||||
flex: 0 0 88px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.field-line :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selections-inline {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.selections-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selections-grid .sel-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.selections-grid .sel-label {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.selections-grid .sel-odds {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.selections-grid .sel-odds :deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sel-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #242424;
|
||||
background: #0d0d0d;
|
||||
}
|
||||
|
||||
.sel-label {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
.template-tools,
|
||||
.save-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.template-select {
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.editor-grid {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 420px) minmax(480px, 1fr);
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
padding: 10px 12px 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.market-list,
|
||||
.detail-panel {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 8px;
|
||||
background: #111;
|
||||
padding: 10px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.block-title {
|
||||
color: #b8b8b8;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--green-text);
|
||||
min-width: 14px;
|
||||
text-align: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
margin: 0;
|
||||
color: #777;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.market-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 8px;
|
||||
margin-bottom: 6px;
|
||||
border: 1px solid #282828;
|
||||
border-radius: 6px;
|
||||
background: #151515;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.market-card.active {
|
||||
border-color: #9f853d;
|
||||
background: #1a1710;
|
||||
}
|
||||
|
||||
.market-card.hidden {
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
.card-main {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-main strong,
|
||||
.detail-head strong {
|
||||
color: #f0f0f0;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sel-odds {
|
||||
width: 88px;
|
||||
.card-main span {
|
||||
color: #777;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.sel-odds :deep(.el-input-number) {
|
||||
.card-actions {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 30px 30px 46px 40px;
|
||||
gap: 2px;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.card-action-btn {
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
padding: 0;
|
||||
margin-left: 0 !important;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.detail-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-grid,
|
||||
.locale-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.locale-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.locale-grid.compact {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.locale-block {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.score-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.selection-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.selection-row {
|
||||
padding: 8px;
|
||||
border: 1px solid #262626;
|
||||
border-radius: 6px;
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.selection-meta {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(90px, 1fr) 120px 132px 70px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.selection-row--score .selection-meta {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.selection-meta strong {
|
||||
color: #ddd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.selection-field {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.selection-field > span {
|
||||
color: #f3c969;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.selection-field--status > span {
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
.selection-field--odds :deep(.el-input-number),
|
||||
.selection-field--odds :deep(.el-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
flex-shrink: 0;
|
||||
min-width: 52px;
|
||||
.add-market-field {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.btn-save.is-disabled,
|
||||
.btn-save:disabled {
|
||||
opacity: 0.45;
|
||||
.full-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.market-toolbar,
|
||||
.editor-grid,
|
||||
.form-grid,
|
||||
.locale-grid,
|
||||
.selection-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.template-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.market-list,
|
||||
.detail-panel {
|
||||
min-height: 320px;
|
||||
}
|
||||
}
|
||||
|
||||
.market-editor {
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.market-toolbar {
|
||||
border-bottom-color: var(--border);
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.market-list,
|
||||
.detail-panel,
|
||||
.market-card,
|
||||
.selection-row {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.market-list,
|
||||
.detail-panel {
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section-title,
|
||||
.block-title,
|
||||
.card-main strong,
|
||||
.detail-head strong,
|
||||
.selection-meta strong {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.empty-hint,
|
||||
.card-main span,
|
||||
label,
|
||||
.selection-field > span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.market-card:hover {
|
||||
border-color: #d5cfc3;
|
||||
background: var(--accent-hover);
|
||||
}
|
||||
|
||||
.market-card.active {
|
||||
border-color: var(--primary);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.selection-row {
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.selection-field > span {
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.selection-field--status > span {
|
||||
color: var(--info-text);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.market-toolbar {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.template-tools,
|
||||
.save-tools {
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -603,4 +603,33 @@ async function saveRow(row: SelectionRow) {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-color: var(--border);
|
||||
background: #ffffff;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.league-meta,
|
||||
.league-meta-k,
|
||||
.readonly-field {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.panel-foot.compact {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.settings-top,
|
||||
.panel-head.compact,
|
||||
.panel-foot.compact {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -356,26 +356,26 @@ onMounted(() => {
|
||||
.expand-line {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.expand-k {
|
||||
display: inline-block;
|
||||
min-width: 72px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.expand-warn {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
color: #c9a227;
|
||||
color: var(--warning-text);
|
||||
}
|
||||
|
||||
.expand-empty {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.preview-table {
|
||||
|
||||
Reference in New Issue
Block a user