sync(theme-3): 从 main 同步优胜赛结算态与钱包流水展示优化

- checkout shared/api/admin 自 d3c2114
- player: outright 结算态逻辑、wallet txAmountClass、i18n 新增 key(保留主题 CSS 变量)
This commit is contained in:
2026-06-23 13:51:07 +08:00
parent 5c1e06e873
commit 1239dd7ac2
36 changed files with 577 additions and 127 deletions

View File

@@ -8,8 +8,9 @@ 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();
import { formatAdminDateTimeBrief, formatAdminDateTimeFull } from '../utils/format-datetime';
import { walletTxTypeKey, walletRemarkLabel } from '../utils/walletTx';
const { t, locale } = useAdminLocale();
const auth = useAuthStore();
const route = useRoute();
const router = useRouter();
@@ -84,36 +85,8 @@ function transferTypeLabel(type: string) {
return key ? t(key) : type;
}
const TRANSFER_REMARK_KEYS: Record<string, string> = {
'Agent deposit': 'finance.remark.agent_deposit',
'Agent withdraw': 'finance.remark.agent_withdraw',
'代理上分': 'finance.remark.agent_deposit',
'代理下分': 'finance.remark.agent_withdraw',
'管理员上分': 'finance.remark.admin_deposit',
'管理员下分': 'finance.remark.admin_withdraw',
'开户初始余额': 'finance.remark.initial_balance',
};
function transferRemarkLabel(remark: string | null | undefined, transactionType: string) {
const raw = remark?.trim();
if (!raw) {
if (transactionType === 'MANUAL_DEPOSIT') return t('finance.remark.agent_deposit');
if (transactionType === 'MANUAL_WITHDRAW') return t('finance.remark.agent_withdraw');
return '—';
}
const key = TRANSFER_REMARK_KEYS[raw];
return key ? t(key) : raw;
}
function formatTime(v: string) {
return new Date(v).toLocaleString(localeTag.value, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});
return walletRemarkLabel(remark, transactionType, t);
}
function dateParams() {
@@ -339,8 +312,12 @@ watch(
<AdminTableEmpty />
</template>
<el-table-column type="index" :index="(i: number) => (creditPage - 1) * creditPageSize + i + 1" :label="t('common.seq')" width="70" align="center" />
<el-table-column :label="t('audit.col.time')" min-width="158">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
<el-table-column :label="t('audit.col.time')" min-width="88">
<template #default="{ row }">
<el-tooltip :content="formatAdminDateTimeFull(row.createdAt, locale)" placement="top">
<span>{{ formatAdminDateTimeBrief(row.createdAt, locale) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('user.col.username')" min-width="110">
<template #default="{ row }">
@@ -410,8 +387,12 @@ watch(
<AdminTableEmpty />
</template>
<el-table-column type="index" :index="(i: number) => (transferPage - 1) * transferPageSize + i + 1" :label="t('common.seq')" width="70" align="center" />
<el-table-column :label="t('audit.col.time')" min-width="158">
<template #default="{ row }">{{ formatTime(row.createdAt) }}</template>
<el-table-column :label="t('audit.col.time')" min-width="88">
<template #default="{ row }">
<el-tooltip :content="formatAdminDateTimeFull(row.createdAt, locale)" placement="top">
<span>{{ formatAdminDateTimeBrief(row.createdAt, locale) }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('finance.col.tx_id')" min-width="130" show-overflow-tooltip>
<template #default="{ row }">{{ row.transactionId }}</template>

View File

@@ -3,6 +3,7 @@ import { ref, computed, watch, onBeforeUnmount, onDeactivated } from 'vue';
defineOptions({ name: 'AdminMatches' });
import { useStaleListLifecycle } from '../composables/useStaleList';
import { consumeAdminListStale } from '../utils/adminListStale';
import { useRoute, useRouter } from 'vue-router';
import { useAdminLocale } from '../composables/useAdminLocale';
import { resolveFormError } from '../i18n/form-validation';
@@ -38,6 +39,7 @@ const isMatchChildRoute = computed(() =>
interface LeagueTableRow extends Record<string, unknown> {
id: string;
isPublished: boolean;
isOutrightSettled: boolean;
isPublishing: boolean;
labels: {
edit: string;
@@ -50,7 +52,7 @@ interface LeagueTableRow extends Record<string, unknown> {
displayNameZh: string;
displayNameEn: string;
displayStatusLabel: string;
displayStatusTagType: 'success' | 'info';
displayStatusTagType: 'success' | 'info' | 'warning';
displayMatchCount: number;
displayBetCount: number;
displayBetCountActive: boolean;
@@ -121,6 +123,7 @@ function mapLeagueRows(items: unknown[], publishingId = ''): LeagueTableRow[] {
const r = rowOf(item);
const id = String(r.id ?? '');
const published = Boolean(r.isPublished);
const outrightSettled = Boolean(r.isOutrightSettled);
const stats = r.betStats as
| { betCount?: number; totalStake?: string; pendingCount?: number }
| undefined;
@@ -129,13 +132,18 @@ function mapLeagueRows(items: unknown[], publishingId = ''): LeagueTableRow[] {
...r,
id,
isPublished: published,
isOutrightSettled: outrightSettled,
isPublishing: publishingId === id,
labels,
displaySeq: start + index + 1,
displayNameZh: String(r.leagueZh ?? '').trim() || '—',
displayNameEn: String(r.leagueEn ?? '').trim() || '—',
displayStatusLabel: published ? t('league.status.PUBLISHED') : t('league.status.UNPUBLISHED'),
displayStatusTagType: published ? 'success' : 'info',
displayStatusLabel: outrightSettled
? t('league.status.OUTRIGHT_SETTLED')
: published
? t('league.status.PUBLISHED')
: t('league.status.UNPUBLISHED'),
displayStatusTagType: outrightSettled ? 'warning' : published ? 'success' : 'info',
displayMatchCount: Number(r.matchCount ?? 0),
displayBetCount: betCount,
displayBetCountActive: betCount > 0,
@@ -163,6 +171,7 @@ function persistListUiState() {
type LoadOptions = { restore?: boolean };
async function load(options: LoadOptions = {}) {
consumeAdminListStale();
const saved = options.restore ? readMatchesListUiState() : null;
if (saved) {
page.value = saved.page;
@@ -189,6 +198,17 @@ function onSearch() {
void runLoad(true);
}
const MATCH_CHILD_ROUTE = /^\/matches\/leagues\/[^/]+/;
watch(
() => route.path,
(path, prevPath) => {
if (prevPath && MATCH_CHILD_ROUTE.test(prevPath) && path === '/matches') {
void load();
}
},
);
async function initialLoad() {
if (isMatchChildRoute.value) return;
const qStatus = route.query.status;

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, computed, onBeforeUnmount, onDeactivated } from 'vue';
import { ref, computed, onBeforeUnmount, onDeactivated, watch } from 'vue';
import { consumeAdminListStale } from '../utils/adminListStale';
defineOptions({ name: 'AdminMatchesOutrights' });
import { useStaleListLifecycle } from '../composables/useStaleList';
@@ -38,6 +39,7 @@ function persistListUiState() {
type LoadOptions = { restore?: boolean };
async function load(options: LoadOptions = {}) {
consumeAdminListStale();
const saved = options.restore ? readMatchesListUiState() : null;
if (saved) {
page.value = saved.page;
@@ -92,6 +94,17 @@ async function initialLoad() {
await resolveLeagueFromQuery();
}
const OUTRIGHT_CHILD_ROUTE = /^\/matches\/outrights\/leagues\/[^/]+/;
watch(
() => route.path,
(path, prevPath) => {
if (prevPath && OUTRIGHT_CHILD_ROUTE.test(prevPath) && path === '/matches/outrights') {
void load();
}
},
);
const { loading: listLoading, runLoad } = useStaleListLifecycle(() => leagues.value.length > 0, initialLoad);
onBeforeUnmount(persistListUiState);
onDeactivated(persistListUiState);
@@ -146,6 +159,9 @@ function leagueTitle(row: unknown) {
function outrightTeamCount(row: unknown) {
return Number(rowOf(row).outrightTeamCount ?? 0);
}
function outrightSettled(row: unknown) {
return Boolean(rowOf(row).isOutrightSettled);
}
</script>
<template>
@@ -204,6 +220,14 @@ function outrightTeamCount(row: unknown) {
<span class="league-en">{{ leagueNameEn(row) }}</span>
</template>
</el-table-column>
<el-table-column :label="t('common.status')" width="108" align="center">
<template #default="{ row }">
<el-tag v-if="outrightSettled(row)" size="small" type="warning" effect="plain">
{{ t('league.status.OUTRIGHT_SETTLED') }}
</el-tag>
<span v-else class="status-dash"></span>
</template>
</el-table-column>
<el-table-column :label="t('outright.col.teams_total')" width="120" align="center">
<template #default="{ row }">{{ outrightTeamCount(row) }}</template>
</el-table-column>
@@ -270,4 +294,7 @@ function outrightTeamCount(row: unknown) {
color: var(--text-muted);
font-size: 13px;
}
.status-dash {
color: var(--text-muted);
}
</style>

View File

@@ -23,7 +23,7 @@ import {
betTypeLabel,
betResultLabel,
} from '../utils/bet-labels';
import { adminSelectionLabel } from '../utils/adminSelectionLabel';
import { markAdminListStale } from '../utils/adminListStale';
import type { AdminMatchDetail } from './match-form';
import AdminSubNav from '../components/AdminSubNav.vue';
@@ -658,6 +658,7 @@ async function confirmResettle() {
resettleDialogVisible.value = false;
await loadMatch();
void loadSettlementHistory();
markAdminListStale();
} catch (e: unknown) {
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
} finally {
@@ -726,6 +727,7 @@ async function previewSettlement() {
previewPageSize.value = itemsPage.pageSize;
previewDialogVisible.value = true;
await loadMatch();
markAdminListStale();
} catch (e: unknown) {
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
} finally {
@@ -754,6 +756,7 @@ async function confirm() {
previewDialogVisible.value = false;
await loadMatch();
void loadSettlementHistory();
markAdminListStale();
} catch (e: unknown) {
ElMessage.error(settlementApiError(e, t('settlement.preview_failed')));
} finally {

View File

@@ -34,9 +34,15 @@ const leagueTitle = computed(() => {
const panelRef = ref<{ reload: () => void } | null>(null);
const createVisible = ref(false);
const createLoading = ref(false);
const isOutrightSettled = ref(false);
const form = ref<MatchCreateForm>(emptyMatchForm());
function onLeagueMeta(meta: { isOutrightSettled: boolean }) {
isOutrightSettled.value = meta.isOutrightSettled;
}
function openCreateFixture() {
if (isOutrightSettled.value) return;
form.value = emptyMatchForm();
form.value.leagueId = leagueId.value;
createVisible.value = true;
@@ -85,9 +91,10 @@ watch(leagueId, () => {
:subtitle="t('match.league_fixtures_subtitle')"
>
<template #extra>
<el-button type="primary" @click="openCreateFixture">
<el-button v-if="!isOutrightSettled" type="primary" @click="openCreateFixture">
{{ t('match.create_fixture_btn') }}
</el-button>
<span v-else class="settled-hint">{{ t('league.hint.outright_settled_no_fixture') }}</span>
</template>
</AdminSubNav>
@@ -97,6 +104,7 @@ watch(leagueId, () => {
:league-id="leagueId"
:filter-status="filterStatus"
:keyword="filterKeyword"
@league-meta="onLeagueMeta"
/>
</section>
@@ -254,6 +262,12 @@ watch(leagueId, () => {
font-weight: 700;
}
.settled-hint {
font-size: 13px;
color: var(--warning-text);
font-weight: 600;
}
@media (max-width: 760px) {
.teams-row {
grid-template-columns: 1fr;

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref, watch, h, defineAsyncComponent } from 'vue';
import { ref, watch, h, defineAsyncComponent, onActivated } from 'vue';
import { consumeAdminListStale } from '../../utils/adminListStale';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox, ElDatePicker } from 'element-plus';
import { useAdminLocale } from '../../composables/useAdminLocale';
@@ -32,6 +33,7 @@ const props = withDefaults(
const emit = defineEmits<{
changed: [];
'add-match': [];
'league-meta': [meta: { isOutrightSettled: boolean }];
}>();
const { t, locale } = useAdminLocale();
@@ -99,8 +101,8 @@ function resetFilters() {
onFilterChange();
}
async function load() {
loading.value = true;
async function load(options: { silent?: boolean } = {}) {
if (!options.silent) loading.value = true;
try {
const { data } = await api.get(`/admin/leagues/${props.leagueId}/matches`, {
params: {
@@ -124,19 +126,31 @@ async function load() {
total: number;
page: number;
pageSize: number;
league?: { isOutrightSettled?: boolean };
};
matches.value = payload.items;
matchTotal.value = payload.total;
emit('league-meta', {
isOutrightSettled: Boolean(payload.league?.isOutrightSettled),
});
matchPage.value = payload.page;
matchPageSize.value = payload.pageSize;
} catch (e: unknown) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? t('msg.load_matches_failed'));
} finally {
loading.value = false;
if (!options.silent) loading.value = false;
}
}
onActivated(() => {
if (!props.leagueId) return;
const stale = consumeAdminListStale();
if (stale || matches.value.length > 0) {
void load({ silent: !stale && matches.value.length > 0 });
}
});
function scheduleLoad(resetPage = false) {
if (!props.leagueId) return;
if (resetPage) matchPage.value = 1;

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { computed, ref, watch, onActivated } from 'vue';
import { consumeAdminListStale } from '../../utils/adminListStale';
import { useRouter, useRoute } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useAdminLocale } from '../../composables/useAdminLocale';
@@ -237,9 +238,9 @@ function goSettle() {
});
}
async function load() {
async function load(options: { silent?: boolean } = {}) {
if (!props.leagueId) return;
loading.value = true;
if (!options.silent) loading.value = true;
try {
const { data } = await api.get(`/admin/leagues/${props.leagueId}/outright`);
const payload = data.data as {
@@ -283,10 +284,18 @@ async function load() {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? t('msg.load_failed'));
} finally {
loading.value = false;
if (!options.silent) loading.value = false;
}
}
onActivated(() => {
if (!props.leagueId) return;
const stale = consumeAdminListStale();
if (stale || matchId.value || selections.value.length > 0) {
void load({ silent: !stale && Boolean(matchId.value || selections.value.length) });
}
});
function resetCustomTeamForm() {
customTeam.value = { teamCode: '', teamZh: '', teamEn: '', logoUrl: '' };
}