sync(theme-4): 同步 main 最新 API/Admin 与玩家端逻辑
从 main 同步站内信手动发送、媒体库选择、列表子路由重构等 API/Admin 改动;玩家端仅合并 ADMIN_CUSTOM 富文本、消息预览与充值截图压缩逻辑,保留 theme-4 样式。
This commit is contained in:
@@ -1,21 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, h } from 'vue';
|
||||
import { ref, watch, h, defineAsyncComponent } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox, ElDatePicker } from 'element-plus';
|
||||
import { useAdminLocale } from '../../composables/useAdminLocale';
|
||||
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;
|
||||
keyword: string;
|
||||
}>();
|
||||
|
||||
const MatchEventEditor = defineAsyncComponent(
|
||||
() => import('./MatchEventEditor.vue'),
|
||||
);
|
||||
const MatchMarketsPanel = defineAsyncComponent(
|
||||
() => import('./MatchMarketsPanel.vue'),
|
||||
);
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
leagueId: string;
|
||||
filterStatus?: string;
|
||||
keyword?: string;
|
||||
}>(),
|
||||
{
|
||||
filterStatus: '',
|
||||
keyword: '',
|
||||
},
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
changed: [];
|
||||
@@ -27,13 +39,15 @@ const router = useRouter();
|
||||
const archiveVisible = ref(false);
|
||||
const archiveMatchId = ref('');
|
||||
const archiveTitle = ref('');
|
||||
const manageDialogVisible = ref(false);
|
||||
const marketsDialogVisible = ref(false);
|
||||
const dialogMatchId = ref('');
|
||||
const dialogMatchTitle = ref('');
|
||||
const filterHasBets = ref(false);
|
||||
const orderBy = ref('default');
|
||||
|
||||
function onFilterChange() {
|
||||
matchPage.value = 1;
|
||||
void load();
|
||||
}
|
||||
const localKeyword = ref('');
|
||||
const localStatus = ref('');
|
||||
const kickoffRange = ref<[string, string] | null>(null);
|
||||
|
||||
const matches = ref<unknown[]>([]);
|
||||
const loading = ref(false);
|
||||
@@ -42,18 +56,67 @@ const matchPageSize = ref(10);
|
||||
const matchTotal = ref(0);
|
||||
let loadTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
watch(
|
||||
() => props.keyword,
|
||||
(value) => {
|
||||
localKeyword.value = value ?? '';
|
||||
scheduleLoad(true);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.filterStatus,
|
||||
(value) => {
|
||||
localStatus.value = value ?? '';
|
||||
scheduleLoad(true);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.leagueId,
|
||||
() => scheduleLoad(true),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function onSearch() {
|
||||
matchPage.value = 1;
|
||||
void load();
|
||||
}
|
||||
|
||||
function onFilterChange() {
|
||||
matchPage.value = 1;
|
||||
void load();
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
localKeyword.value = '';
|
||||
localStatus.value = '';
|
||||
kickoffRange.value = null;
|
||||
filterHasBets.value = false;
|
||||
orderBy.value = 'default';
|
||||
onFilterChange();
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const { data } = await api.get(`/admin/leagues/${props.leagueId}/matches`, {
|
||||
params: {
|
||||
status: props.filterStatus || undefined,
|
||||
keyword: props.keyword.trim() || undefined,
|
||||
status: localStatus.value || undefined,
|
||||
keyword: localKeyword.value.trim() || undefined,
|
||||
locale: locale.value,
|
||||
page: matchPage.value,
|
||||
pageSize: matchPageSize.value,
|
||||
hasBets: filterHasBets.value ? 'true' : undefined,
|
||||
orderBy: orderBy.value !== 'default' ? orderBy.value : undefined,
|
||||
startFrom: kickoffRange.value?.[0]
|
||||
? platformPickerDateTimeToIso(kickoffRange.value[0])
|
||||
: undefined,
|
||||
startTo: kickoffRange.value?.[1]
|
||||
? platformPickerDateTimeToIso(kickoffRange.value[1])
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
const payload = data.data as {
|
||||
@@ -75,6 +138,7 @@ async function load() {
|
||||
}
|
||||
|
||||
function scheduleLoad(resetPage = false) {
|
||||
if (!props.leagueId) return;
|
||||
if (resetPage) matchPage.value = 1;
|
||||
if (loadTimer) clearTimeout(loadTimer);
|
||||
loadTimer = setTimeout(() => {
|
||||
@@ -83,12 +147,6 @@ function scheduleLoad(resetPage = false) {
|
||||
}, 200);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.leagueId, props.filterStatus, props.keyword] as const,
|
||||
() => scheduleLoad(true),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function onMatchPageChange(page: number) {
|
||||
matchPage.value = page;
|
||||
void load();
|
||||
@@ -146,23 +204,28 @@ async function close(id: string) {
|
||||
notifyParent();
|
||||
}
|
||||
|
||||
function beforeLeaveList() {
|
||||
ensureLeagueExpanded(props.leagueId);
|
||||
function openManage(id: string, title?: string) {
|
||||
dialogMatchId.value = id;
|
||||
dialogMatchTitle.value = title?.trim() || `#${id}`;
|
||||
manageDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openManage(id: string) {
|
||||
beforeLeaveList();
|
||||
router.push(`/matches/${id}/edit`);
|
||||
function openMarkets(id: string, title?: string) {
|
||||
dialogMatchId.value = id;
|
||||
dialogMatchTitle.value = title?.trim() || `#${id}`;
|
||||
marketsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openMarkets(id: string) {
|
||||
beforeLeaveList();
|
||||
router.push(`/matches/${id}/markets`);
|
||||
function onManageSaved() {
|
||||
manageDialogVisible.value = false;
|
||||
notifyParent();
|
||||
}
|
||||
|
||||
function settle(id: string) {
|
||||
beforeLeaveList();
|
||||
router.push(`/settlement/${id}`);
|
||||
void router.push({
|
||||
path: `/settlement/${id}`,
|
||||
query: { returnTo: `/matches/leagues/${props.leagueId}` },
|
||||
});
|
||||
}
|
||||
|
||||
type TagType = '' | 'info' | 'success' | 'warning' | 'danger';
|
||||
@@ -339,14 +402,68 @@ defineExpose({ reload: load });
|
||||
<template>
|
||||
<div class="league-matches-panel">
|
||||
<div class="nested-panel-toolbar">
|
||||
<el-checkbox v-model="filterHasBets" size="default" @change="onFilterChange">
|
||||
{{ t('match.filter.has_bets') }}
|
||||
</el-checkbox>
|
||||
<el-select v-model="orderBy" size="small" style="width: 140px;" @change="onFilterChange">
|
||||
<el-option :label="t('match.sort.default')" value="default" />
|
||||
<el-option :label="t('match.sort.bet_count')" value="betCount" />
|
||||
<el-option :label="t('match.sort.total_stake')" value="totalStake" />
|
||||
</el-select>
|
||||
<div class="nested-panel-toolbar__filters">
|
||||
<div class="nested-panel-toolbar__field">
|
||||
<span class="nested-panel-toolbar__label">{{ t('common.keyword') }}</span>
|
||||
<el-input
|
||||
v-model="localKeyword"
|
||||
:placeholder="t('match.filter.keyword_ph')"
|
||||
clearable
|
||||
size="small"
|
||||
style="width: 168px"
|
||||
@keyup.enter="onSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="nested-panel-toolbar__field">
|
||||
<span class="nested-panel-toolbar__label">{{ t('common.status') }}</span>
|
||||
<el-select
|
||||
v-model="localStatus"
|
||||
:placeholder="t('common.all')"
|
||||
clearable
|
||||
size="small"
|
||||
style="width: 120px"
|
||||
@change="onFilterChange"
|
||||
>
|
||||
<el-option :label="t('match.status.DRAFT')" value="DRAFT" />
|
||||
<el-option :label="t('match.status.PUBLISHED')" value="PUBLISHED" />
|
||||
<el-option :label="t('match.status.CLOSED')" value="CLOSED" />
|
||||
<el-option :label="t('match.status.PENDING_SETTLEMENT')" value="PENDING_SETTLEMENT" />
|
||||
<el-option :label="t('match.status.SETTLED')" value="SETTLED" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="nested-panel-toolbar__field nested-panel-toolbar__field--range">
|
||||
<span class="nested-panel-toolbar__label">{{ t('match.field.kickoff') }}</span>
|
||||
<el-date-picker
|
||||
v-model="kickoffRange"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
:start-placeholder="t('match.filter.kickoff_from')"
|
||||
:end-placeholder="t('match.filter.kickoff_to')"
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 320px"
|
||||
@change="onFilterChange"
|
||||
/>
|
||||
</div>
|
||||
<el-button type="primary" size="small" @click="onSearch">
|
||||
{{ t('common.search') }}
|
||||
</el-button>
|
||||
<el-button size="small" @click="resetFilters">
|
||||
{{ t('common.reset') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="nested-panel-toolbar__extras">
|
||||
<el-checkbox v-model="filterHasBets" size="default" @change="onFilterChange">
|
||||
{{ t('match.filter.has_bets') }}
|
||||
</el-checkbox>
|
||||
<el-select v-model="orderBy" size="small" style="width: 148px;" @change="onFilterChange">
|
||||
<el-option :label="t('match.sort.default')" value="default" />
|
||||
<el-option :label="t('match.sort.kickoff_asc')" value="kickoffAsc" />
|
||||
<el-option :label="t('match.sort.kickoff_desc')" value="kickoffDesc" />
|
||||
<el-option :label="t('match.sort.bet_count')" value="betCount" />
|
||||
<el-option :label="t('match.sort.total_stake')" value="totalStake" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="matches" stripe row-key="id" class="nested-match-table">
|
||||
@@ -393,7 +510,7 @@ defineExpose({ reload: load });
|
||||
size="small"
|
||||
type="primary"
|
||||
:disabled="!canManage(row)"
|
||||
@click="openManage(matchId(row))"
|
||||
@click.stop="openManage(matchId(row), matchTitle(row))"
|
||||
>
|
||||
{{ t('matchEditor.manage_btn') }}
|
||||
</el-button>
|
||||
@@ -403,7 +520,7 @@ defineExpose({ reload: load });
|
||||
plain
|
||||
class="action-btn--markets"
|
||||
:disabled="!canManage(row)"
|
||||
@click="openMarkets(matchId(row))"
|
||||
@click.stop="openMarkets(matchId(row), matchTitle(row))"
|
||||
>
|
||||
{{ t('match.btn.markets') }}
|
||||
</el-button>
|
||||
@@ -482,6 +599,35 @@ defineExpose({ reload: load });
|
||||
:title="archiveTitle"
|
||||
@archived="onMatchArchived"
|
||||
/>
|
||||
<el-dialog
|
||||
v-model="manageDialogVisible"
|
||||
:title="`${t('matchEditor.title')} · ${dialogMatchTitle}`"
|
||||
width="920px"
|
||||
top="4vh"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
class="match-manage-dialog"
|
||||
>
|
||||
<MatchEventEditor
|
||||
v-if="manageDialogVisible && dialogMatchId"
|
||||
:match-id-prop="dialogMatchId"
|
||||
embedded
|
||||
@saved="onManageSaved"
|
||||
/>
|
||||
</el-dialog>
|
||||
<el-dialog
|
||||
v-model="marketsDialogVisible"
|
||||
:title="`${t('matchEditor.section_markets')} · ${dialogMatchTitle}`"
|
||||
width="min(1200px, 96vw)"
|
||||
top="3vh"
|
||||
destroy-on-close
|
||||
append-to-body
|
||||
class="match-markets-dialog"
|
||||
>
|
||||
<div v-if="marketsDialogVisible && dialogMatchId" class="match-markets-dialog__body">
|
||||
<MatchMarketsPanel :match-id="dialogMatchId" />
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -496,15 +642,41 @@ defineExpose({ reload: load });
|
||||
}
|
||||
.nested-panel-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px 16px;
|
||||
margin-bottom: 10px;
|
||||
padding: 6px 12px;
|
||||
padding: 8px 12px;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border-soft, #eaeaea);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.nested-panel-toolbar__filters,
|
||||
.nested-panel-toolbar__extras {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.nested-panel-toolbar__field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.nested-panel-toolbar__field--range {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nested-panel-toolbar__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.nested-pager {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -646,4 +818,9 @@ defineExpose({ reload: load });
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.match-markets-dialog__body {
|
||||
height: min(78vh, 900px);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user