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

@@ -3,8 +3,8 @@ import { ref, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import api from '../api';
import { formatMoney } from '../utils/localeDisplay';
import { txTypeKey } from '../utils/walletTx';
import { formatMoney, formatMoneyCompact } from '../utils/localeDisplay';
import { txTypeKey, txDisplayType, txSummaryLabel } from '../utils/walletTx';
import GoldSpinner from '../components/GoldSpinner.vue';
import { usePullToRefresh } from '../composables/usePullToRefresh';
@@ -13,22 +13,31 @@ const { t, locale } = useI18n();
type Transaction = {
transactionType: string;
displayType?: string;
summary?: string | null;
summaryKind?: 'opening_bonus' | null;
referenceType?: string | null;
amount: string;
createdAt: string;
transactionId: string;
};
const items = ref<Transaction[]>([]);
const cashbackTotal = ref<string>('0');
const loading = ref(true);
const PREVIEW_COUNT = 15;
function txLabel(type: string): string {
const key = txTypeKey(type);
function txLabel(tx: Transaction): string {
const key = txTypeKey(txDisplayType(tx));
if (key) {
const translated = t(key);
if (translated !== key) return translated;
}
return type;
return tx.transactionType;
}
function txSubtitle(tx: Transaction): string {
return txSummaryLabel(tx, t);
}
function goDetail(tx: Transaction) {
@@ -36,20 +45,21 @@ function goDetail(tx: Transaction) {
router.push(`/wallet/transactions/${tx.transactionId}`);
}
function goWalletDetail() {
router.push('/wallet/detail');
}
function goCashbacks() {
router.push('/wallet/cashbacks');
}
async function fetchTransactions() {
async function fetchData() {
loading.value = true;
try {
const { data } = await api.get('/player/wallet/transactions', { params: { page: 1 } });
const result = data.data ?? { items: [] };
const [txRes, cbRes] = await Promise.all([
api.get('/player/wallet/transactions', { params: { page: 1 } }),
api.get('/player/cashbacks').catch(() => ({ data: { data: { items: [], totalAmount: '0' } } })),
]);
const result = txRes.data.data ?? { items: [] };
items.value = (result.items ?? []).slice(0, PREVIEW_COUNT);
const cbData = cbRes.data?.data;
cashbackTotal.value = cbData?.totalAmount ?? cbData?.items?.reduce((s: number, r: { amount: string }) => s + Math.abs(parseFloat(r.amount) || 0), 0)?.toString() ?? '0';
} catch {
/* ignore */
} finally {
@@ -58,10 +68,10 @@ async function fetchTransactions() {
}
const { pullDistance, spinning, progress } = usePullToRefresh({
onRefresh: fetchTransactions,
onRefresh: fetchData,
});
onMounted(fetchTransactions);
onMounted(fetchData);
const pullIndicatorStyle = () => ({
height: `${pullDistance.value}px`,
@@ -92,7 +102,7 @@ const pullIndicatorStyle = () => ({
v-if="items.length"
type="button"
class="more-link"
@click="goWalletDetail"
@click="router.push('/wallet/detail')"
>{{ t('wallet.view_all') }} &#x203A;</button>
</div>
@@ -105,7 +115,10 @@ const pullIndicatorStyle = () => ({
@click="goDetail(tx)"
>
<div class="tx-main">
<span class="tx-type">{{ txLabel(tx.transactionType) }}</span>
<div class="tx-text">
<span class="tx-type">{{ txLabel(tx) }}</span>
<span v-if="txSubtitle(tx)" class="tx-summary">{{ txSubtitle(tx) }}</span>
</div>
<span :class="parseFloat(tx.amount) >= 0 ? 'pos' : 'neg'">
{{ formatMoney(tx.amount, locale) }}
</span>
@@ -184,6 +197,22 @@ const pullIndicatorStyle = () => ({
gap: 12px;
}
.tx-text {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.tx-summary {
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.tx-meta {
display: flex;
justify-content: space-between;