feat: 开户备注、账单展示优化与后台代理管理增强

- 新增初始上分备注(日常上分/开户赠金/自定义)及前后台校验与展示

- 优化钱包流水类型与备注显示,区分管理员/代理/玩家上下分

- 修复登录后语言被后端覆盖的问题,登录时同步当前语言到服务端

- 后台代理/玩家表格操作栏重构,充值订单增加备注列

- 前台个人中心、充值、账单与验证码组件体验优化

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 17:23:58 +08:00
parent 10485ecfaf
commit 03e72ca9b2
46 changed files with 3721 additions and 1059 deletions

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ref, watch, h } from 'vue';
import { useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { ElMessage, ElMessageBox, ElDatePicker } from 'element-plus';
import { useAdminLocale } from '../../composables/useAdminLocale';
import api from '../../api';
import { ensureLeagueExpanded } from '../../utils/matchesListState';
@@ -165,9 +165,79 @@ function canPublishRow(row: unknown) {
function canCloseRow(row: unknown) {
return matchStatus(row) === 'PUBLISHED';
}
function canReopenRow(row: unknown) {
return matchStatus(row) === 'CLOSED';
}
function canSettleRow(row: unknown) {
return matchStatus(row) !== 'DRAFT';
}
function settleButtonLabel(row: unknown) {
return matchStatus(row) === 'SETTLED' ? t('common.resettle') : t('common.settle');
}
function kickoffPassed(row: unknown) {
return new Date(String(rowOf(row).startTime)) <= new Date();
}
async function promptReopenKickoff(): Promise<string | null> {
const kickoff = ref('');
try {
await ElMessageBox({
title: t('match.reopen_kickoff_title'),
message: () =>
h('div', { class: 'reopen-kickoff-prompt' }, [
h(
'p',
{
style: 'margin: 0 0 12px; font-size: 13px; color: var(--el-text-color-secondary)',
},
t('match.reopen_kickoff_hint'),
),
h(ElDatePicker, {
modelValue: kickoff.value,
'onUpdate:modelValue': (v: string) => {
kickoff.value = v;
},
type: 'datetime',
valueFormat: 'YYYY-MM-DDTHH:mm:ss',
style: 'width: 100%',
}),
]),
showCancelButton: true,
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
beforeClose: (action, _instance, done) => {
if (action === 'confirm') {
if (!kickoff.value || new Date(kickoff.value) <= new Date()) {
ElMessage.warning(t('match.reopen_kickoff_invalid'));
return;
}
}
done();
},
});
return kickoff.value || null;
} catch {
return null;
}
}
async function reopenRow(row: unknown) {
const id = matchId(row);
let startTime: string | undefined;
if (kickoffPassed(row)) {
const picked = await promptReopenKickoff();
if (!picked) return;
startTime = picked;
}
try {
await api.post(`/admin/matches/${id}/reopen`, startTime ? { startTime } : {});
ElMessage.success(t('msg.reopened'));
notifyParent();
} catch (e) {
const err = e as { response?: { data?: { error?: string } } };
ElMessage.error(err.response?.data?.error ?? t('msg.save_failed'));
}
}
async function confirmDelete(row: unknown) {
const id = matchId(row);
@@ -271,13 +341,22 @@ defineExpose({ reload: load });
>
{{ t('common.close_betting') }}
</el-button>
<el-button
size="small"
type="success"
plain
:disabled="!canReopenRow(row)"
@click="reopenRow(row)"
>
{{ t('common.reopen_betting') }}
</el-button>
<el-button
size="small"
type="primary"
:disabled="!canSettleRow(row)"
@click="settle(matchId(row))"
>
{{ t('common.settle') }}
{{ settleButtonLabel(row) }}
</el-button>
</div>
<el-button