feat(admin+api+player): 优胜赛结算态、禁止新增单场与钱包流水展示优化
- API/管理端:优胜赛 SETTLED 后禁止新增单场,列表与子页展示结算状态 - 玩家端:已结算 outright 只读展示并高亮冠军 - 管理端:结算后 stale 标记驱动列表刷新;财务流水时间与备注 i18n 优化 - shared:txDisplayAmount 与 LEAGUE_OUTRIGHT_SETTLED 错误码
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
export interface LeagueRowView {
|
||||
id: string;
|
||||
isPublished: boolean;
|
||||
isOutrightSettled?: boolean;
|
||||
isPublishing: boolean;
|
||||
labels: {
|
||||
edit: string;
|
||||
@@ -30,7 +31,12 @@ defineEmits<{
|
||||
<el-button size="small" type="primary" @click.stop="$emit('edit')">
|
||||
{{ row.labels.edit }}
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" @click.stop="$emit('createFixture')">
|
||||
<el-button
|
||||
v-if="!row.isOutrightSettled"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click.stop="$emit('createFixture')"
|
||||
>
|
||||
{{ row.labels.createFixture }}
|
||||
</el-button>
|
||||
<el-button
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useAuthStore } from '../stores/auth';
|
||||
import { useAdminLocale } from '../composables/useAdminLocale';
|
||||
import api from '../api';
|
||||
import AdminTableEmpty from './AdminTableEmpty.vue';
|
||||
import { formatAmount, formatAmountFull } from '../utils/format-amount';
|
||||
import { walletDepositMethodLabel, walletTxTypeKey } from '../utils/walletTx';
|
||||
import { formatAmountFull } from '../utils/format-amount';
|
||||
import { formatAdminDateTimeBrief, formatAdminDateTimeFull } from '../utils/format-datetime';
|
||||
import { walletDepositMethodLabel, walletTxTypeKey, txDisplayAmount, walletRemarkLabel } from '../utils/walletTx';
|
||||
|
||||
interface WalletTxRow {
|
||||
id: string;
|
||||
@@ -34,7 +35,7 @@ const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
}>();
|
||||
|
||||
const { t, locale, localeTag } = useAdminLocale();
|
||||
const { t, locale } = useAdminLocale();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const visible = computed({
|
||||
@@ -68,17 +69,6 @@ function depositMethodLabel(row: WalletTxRow) {
|
||||
return walletDepositMethodLabel(row, t);
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
function dateParams() {
|
||||
if (!dateRange.value?.length) return {};
|
||||
const [from, to] = dateRange.value;
|
||||
@@ -172,8 +162,12 @@ watch(
|
||||
<template #empty>
|
||||
<AdminTableEmpty />
|
||||
</template>
|
||||
<el-table-column :label="t('audit.col.time')" min-width="140">
|
||||
<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="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.transactionId }}</template>
|
||||
@@ -181,45 +175,27 @@ watch(
|
||||
<el-table-column :label="t('finance.col.tx_type')" min-width="80">
|
||||
<template #default="{ row }">{{ walletTypeLabel(row.transactionType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('finance.col.deposit_method')" min-width="100" show-overflow-tooltip>
|
||||
<el-table-column :label="t('finance.col.deposit_method')" min-width="72" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ depositMethodLabel(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('finance.col.balance_change')" min-width="96" align="right">
|
||||
<el-table-column :label="t('finance.col.balance_change')" min-width="110" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="formatAmountFull(row.amount)" placement="top">
|
||||
<span :class="parseFloat(row.amount) >= 0 ? 'amt-pos' : 'amt-neg'">
|
||||
{{ formatAmount(row.amount) }}
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<span :class="parseFloat(txDisplayAmount(row)) >= 0 ? 'amt-pos' : 'amt-neg'">
|
||||
{{ formatAmountFull(txDisplayAmount(row)) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('finance.col.balance_before')" min-width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="formatAmountFull(row.balanceBefore)" placement="top">
|
||||
<span>{{ formatAmount(row.balanceBefore) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-table-column :label="t('finance.col.balance_before')" min-width="110" align="right">
|
||||
<template #default="{ row }">{{ formatAmountFull(row.balanceBefore) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('finance.col.balance_after')" min-width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="formatAmountFull(row.balanceAfter)" placement="top">
|
||||
<span>{{ formatAmount(row.balanceAfter) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-table-column :label="t('finance.col.balance_after')" min-width="110" align="right">
|
||||
<template #default="{ row }">{{ formatAmountFull(row.balanceAfter) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('finance.col.frozen_before')" min-width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="formatAmountFull(row.frozenBefore)" placement="top">
|
||||
<span>{{ formatAmount(row.frozenBefore) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-table-column :label="t('finance.col.frozen_before')" min-width="110" align="right">
|
||||
<template #default="{ row }">{{ formatAmountFull(row.frozenBefore) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('finance.col.frozen_after')" min-width="90" align="right">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip :content="formatAmountFull(row.frozenAfter)" placement="top">
|
||||
<span>{{ formatAmount(row.frozenAfter) }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-table-column :label="t('finance.col.frozen_after')" min-width="110" align="right">
|
||||
<template #default="{ row }">{{ formatAmountFull(row.frozenAfter) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('finance.col.reference')" min-width="105" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
@@ -237,8 +213,8 @@ watch(
|
||||
<el-table-column :label="t('agent.credit_tx.col.operator')" min-width="85">
|
||||
<template #default="{ row }">{{ row.operatorUsername ?? '—' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('user.field.remark')" min-width="100" show-overflow-tooltip>
|
||||
<template #default="{ row }">{{ row.remark ?? '—' }}</template>
|
||||
<el-table-column :label="t('user.field.remark')" min-width="180">
|
||||
<template #default="{ row }">{{ walletRemarkLabel(row.remark, row.transactionType, t) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
@@ -231,6 +231,9 @@ export const adminPagesMs: Record<string, string> = {
|
||||
'finance.remark.admin_deposit': 'Deposit admin',
|
||||
'finance.remark.admin_withdraw': 'Pengeluaran admin',
|
||||
'finance.remark.initial_balance': 'Baki permulaan akaun',
|
||||
'finance.remark.revoke_deposit': 'Deposit diluluskan dibatalkan {orderNo}',
|
||||
'finance.remark.deposit_order': 'Pesanan deposit {orderNo}',
|
||||
'finance.remark.cashback_batch': 'Kumpulan cashback {batchNo}',
|
||||
'agent.col.no_records': 'Tiada rekod',
|
||||
'agent.btn.confirm_adjust': 'Sahkan',
|
||||
'agent.field.select_user': 'Pilih pengguna',
|
||||
@@ -278,6 +281,8 @@ export const adminPagesMs: Record<string, string> = {
|
||||
'match.hint.create_league': 'Kejohanan baharu tidak diterbitkan secara lalai; terbitkan untuk paparan pemain, kemudian kembangkan untuk tambah perlawanan.',
|
||||
'league.status.PUBLISHED': 'Diterbitkan',
|
||||
'league.status.UNPUBLISHED': 'Tidak diterbitkan',
|
||||
'league.status.OUTRIGHT_SETTLED': 'Kejohanan diselesaikan',
|
||||
'league.hint.outright_settled_no_fixture': 'Pasaran juara telah diselesaikan; perlawanan baharu tidak boleh ditambah.',
|
||||
'league.btn.unpublish': 'Nyahterbit',
|
||||
'league.confirm_unpublish': 'Pemain tidak lagi melihat kejohanan ini; anda masih boleh edit dan terbitkan semula di admin. Teruskan?',
|
||||
'msg.league_published': 'Kejohanan diterbitkan',
|
||||
|
||||
@@ -237,6 +237,9 @@ export const adminPagesZh: Record<string, string> = {
|
||||
'finance.remark.admin_deposit': '管理员上分',
|
||||
'finance.remark.admin_withdraw': '管理员下分',
|
||||
'finance.remark.initial_balance': '开户初始余额',
|
||||
'finance.remark.revoke_deposit': '撤销已通过充值 {orderNo}',
|
||||
'finance.remark.deposit_order': '充值订单 {orderNo}',
|
||||
'finance.remark.cashback_batch': '返水批次 {batchNo}',
|
||||
'agent.col.no_records': '暂无记录',
|
||||
'agent.btn.confirm_adjust': '确认调整',
|
||||
'agent.field.select_user': '选择用户',
|
||||
@@ -295,6 +298,8 @@ export const adminPagesZh: Record<string, string> = {
|
||||
'match.hint.create_league': '新建联赛默认为未发布,请在列表点击「发布」后玩家端可见;展开该行可添加单场。',
|
||||
'league.status.PUBLISHED': '已发布',
|
||||
'league.status.UNPUBLISHED': '未发布',
|
||||
'league.status.OUTRIGHT_SETTLED': '赛事已结算',
|
||||
'league.hint.outright_settled_no_fixture': '优胜赛已结算,不可再新增单场。',
|
||||
'league.btn.unpublish': '下架',
|
||||
'league.confirm_unpublish': '下架后玩家端将不再展示该联赛,管理端仍可编辑与重新发布。是否继续?',
|
||||
'msg.league_published': '联赛已发布',
|
||||
@@ -1336,6 +1341,9 @@ export const adminPagesEn: Record<string, string> = {
|
||||
'finance.remark.admin_deposit': 'Admin deposit',
|
||||
'finance.remark.admin_withdraw': 'Admin withdraw',
|
||||
'finance.remark.initial_balance': 'Initial account balance',
|
||||
'finance.remark.revoke_deposit': 'Revoked approved deposit {orderNo}',
|
||||
'finance.remark.deposit_order': 'Deposit order {orderNo}',
|
||||
'finance.remark.cashback_batch': 'Cashback batch {batchNo}',
|
||||
'agent.col.no_records': 'No records',
|
||||
'agent.btn.confirm_adjust': 'Confirm',
|
||||
'agent.field.select_user': 'Select user',
|
||||
@@ -1394,6 +1402,8 @@ export const adminPagesEn: Record<string, string> = {
|
||||
'match.hint.create_league': 'New tournaments are unpublished by default; use Publish in the list for player visibility, then expand to add fixtures.',
|
||||
'league.status.PUBLISHED': 'Published',
|
||||
'league.status.UNPUBLISHED': 'Unpublished',
|
||||
'league.status.OUTRIGHT_SETTLED': 'Tournament settled',
|
||||
'league.hint.outright_settled_no_fixture': 'Outright market is settled; new fixtures cannot be added.',
|
||||
'league.btn.unpublish': 'Unpublish',
|
||||
'league.confirm_unpublish': 'Players will no longer see this tournament; you can still edit and republish in admin. Continue?',
|
||||
'msg.league_published': 'Tournament published',
|
||||
|
||||
@@ -242,6 +242,9 @@ const adminPages: Record<string, string> = {
|
||||
'finance.remark.admin_deposit': 'Admin deposit',
|
||||
'finance.remark.admin_withdraw': 'Admin withdraw',
|
||||
'finance.remark.initial_balance': 'Initial account balance',
|
||||
'finance.remark.revoke_deposit': 'Revoked approved deposit {orderNo}',
|
||||
'finance.remark.deposit_order': 'Deposit order {orderNo}',
|
||||
'finance.remark.cashback_batch': 'Cashback batch {batchNo}',
|
||||
'agent.col.no_records': 'No records',
|
||||
'agent.btn.confirm_adjust': 'Confirm',
|
||||
'agent.field.select_user': 'Select user',
|
||||
@@ -300,6 +303,8 @@ const adminPages: Record<string, string> = {
|
||||
'match.hint.create_league': 'New tournaments are unpublished by default; use Publish in the list for player visibility, then expand to add fixtures.',
|
||||
'league.status.PUBLISHED': 'Published',
|
||||
'league.status.UNPUBLISHED': 'Unpublished',
|
||||
'league.status.OUTRIGHT_SETTLED': 'Tournament settled',
|
||||
'league.hint.outright_settled_no_fixture': 'Outright market is settled; new fixtures cannot be added.',
|
||||
'league.btn.unpublish': 'Unpublish',
|
||||
'league.confirm_unpublish': 'Players will no longer see this tournament; you can still edit and republish in admin. Continue?',
|
||||
'msg.league_published': 'Tournament published',
|
||||
|
||||
@@ -243,6 +243,9 @@ const adminPages: Record<string, string> = {
|
||||
'finance.remark.admin_deposit': '管理员上分',
|
||||
'finance.remark.admin_withdraw': '管理员下分',
|
||||
'finance.remark.initial_balance': '开户初始余额',
|
||||
'finance.remark.revoke_deposit': '撤销已通过充值 {orderNo}',
|
||||
'finance.remark.deposit_order': '充值订单 {orderNo}',
|
||||
'finance.remark.cashback_batch': '返水批次 {batchNo}',
|
||||
'agent.col.no_records': '暂无记录',
|
||||
'agent.btn.confirm_adjust': '确认调整',
|
||||
'agent.field.select_user': '选择用户',
|
||||
@@ -301,6 +304,8 @@ const adminPages: Record<string, string> = {
|
||||
'match.hint.create_league': '新建联赛默认为未发布,请在列表点击「发布」后玩家端可见;展开该行可添加单场。',
|
||||
'league.status.PUBLISHED': '已发布',
|
||||
'league.status.UNPUBLISHED': '未发布',
|
||||
'league.status.OUTRIGHT_SETTLED': '赛事已结算',
|
||||
'league.hint.outright_settled_no_fixture': '优胜赛已结算,不可再新增单场。',
|
||||
'league.btn.unpublish': '下架',
|
||||
'league.confirm_unpublish': '下架后玩家端将不再展示该联赛,管理端仍可编辑与重新发布。是否继续?',
|
||||
'msg.league_published': '联赛已发布',
|
||||
|
||||
13
apps/admin/src/utils/adminListStale.ts
Normal file
13
apps/admin/src/utils/adminListStale.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
const STALE_KEY = 'admin:list-stale';
|
||||
|
||||
/** 标记赛事相关列表需在下次激活时刷新(结算/预览等变更后端统计后调用) */
|
||||
export function markAdminListStale() {
|
||||
sessionStorage.setItem(STALE_KEY, '1');
|
||||
}
|
||||
|
||||
/** 若曾标记过 stale 则清除标记并返回 true */
|
||||
export function consumeAdminListStale(): boolean {
|
||||
if (sessionStorage.getItem(STALE_KEY) !== '1') return false;
|
||||
sessionStorage.removeItem(STALE_KEY);
|
||||
return true;
|
||||
}
|
||||
39
apps/admin/src/utils/format-datetime.ts
Normal file
39
apps/admin/src/utils/format-datetime.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { getAdminLocale } from '../i18n';
|
||||
import type { AdminLocale } from '../i18n/admin-messages';
|
||||
|
||||
function resolveLocale(locale?: AdminLocale): AdminLocale {
|
||||
return locale ?? getAdminLocale();
|
||||
}
|
||||
|
||||
/** 列表展示:年月日 */
|
||||
export function formatAdminDateTimeBrief(
|
||||
value: string | null | undefined,
|
||||
locale?: AdminLocale,
|
||||
): string {
|
||||
if (!value) return '—';
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return '—';
|
||||
return d.toLocaleString(resolveLocale(locale), {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
/** 悬停详情:含秒 */
|
||||
export function formatAdminDateTimeFull(
|
||||
value: string | null | undefined,
|
||||
locale?: AdminLocale,
|
||||
): string {
|
||||
if (!value) return '—';
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return '—';
|
||||
return d.toLocaleString(resolveLocale(locale), {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export { txDisplayAmount } from '@thebet365/shared';
|
||||
|
||||
export const TX_KEY_MAP: Record<string, string> = {
|
||||
MANUAL_DEPOSIT: 'finance.tx.deposit',
|
||||
ADMIN_DEPOSIT: 'finance.tx.admin_deposit',
|
||||
@@ -64,3 +66,45 @@ export function walletDepositMethodLabel(
|
||||
if (type === 'PLAYER_DEPOSIT') return t('finance.tx.player_deposit');
|
||||
return '—';
|
||||
}
|
||||
|
||||
const WALLET_REMARK_EXACT: 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',
|
||||
'Resettlement adjustment': 'finance.tx.resettle',
|
||||
};
|
||||
|
||||
/** 钱包流水备注:系统英文/中文模板按当前语言展示 */
|
||||
export function walletRemarkLabel(
|
||||
remark: string | null | undefined,
|
||||
transactionType: string,
|
||||
t: (key: string, params?: Record<string, unknown>) => string,
|
||||
): 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 exactKey = WALLET_REMARK_EXACT[raw];
|
||||
if (exactKey) return t(exactKey);
|
||||
|
||||
const revokeEn = raw.match(/^Revoke approved deposit\s+([A-Z0-9]+)$/i);
|
||||
if (revokeEn) return t('finance.remark.revoke_deposit', { orderNo: revokeEn[1] });
|
||||
|
||||
const revokeZh = raw.match(/^撤销已通过充值\s+([A-Z0-9]+)$/);
|
||||
if (revokeZh) return t('finance.remark.revoke_deposit', { orderNo: revokeZh[1] });
|
||||
|
||||
const depositOrder = raw.match(/^Deposit order\s+([A-Z0-9]+)$/i);
|
||||
if (depositOrder) return t('finance.remark.deposit_order', { orderNo: depositOrder[1] });
|
||||
|
||||
const cashbackBatch = raw.match(/^Cashback batch\s+(.+)$/i);
|
||||
if (cashbackBatch) return t('finance.remark.cashback_batch', { batchNo: cashbackBatch[1].trim() });
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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: '' };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user