重构
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user