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,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import api from '../api';
@@ -31,7 +31,13 @@ type CashbackRecord = {
};
const items = ref<CashbackRecord[]>([]);
const loading = ref(true);
const loading = ref(false);
const initialLoading = ref(true);
const page = ref(1);
const hasMore = ref(true);
const sentinel = ref<HTMLElement | null>(null);
let observer: IntersectionObserver | null = null;
const totalAmount = computed(() =>
items.value.reduce((sum, row) => sum + Math.abs(parseFloat(row.amount) || 0), 0),
@@ -61,25 +67,59 @@ function formatTime(v: string | null) {
});
}
async function fetchRecords() {
async function fetchRecords(p = 1) {
if (loading.value) return;
loading.value = true;
try {
const { data } = await api.get('/player/cashbacks');
items.value = data.data ?? [];
const { data } = await api.get('/player/cashbacks', { params: { page: p } });
const result = data.data ?? { items: [], total: 0, pageSize: 20 };
const newItems = result.items ?? [];
if (p === 1) {
items.value = newItems;
} else {
items.value = [...items.value, ...newItems];
}
const pageSize = result.pageSize ?? 20;
hasMore.value = newItems.length >= pageSize;
page.value = p;
} catch {
items.value = [];
if (p === 1) items.value = [];
} finally {
loading.value = false;
initialLoading.value = false;
}
}
useOnLocaleChange(fetchRecords);
const { pullDistance, spinning, progress } = usePullToRefresh({
onRefresh: fetchRecords,
useOnLocaleChange(() => {
items.value = [];
page.value = 1;
hasMore.value = true;
initialLoading.value = true;
fetchRecords(1);
});
onMounted(fetchRecords);
const { pullDistance, spinning, progress } = usePullToRefresh({
onRefresh: async () => { await fetchRecords(1); },
});
onMounted(() => {
fetchRecords(1);
observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore.value && !loading.value) {
fetchRecords(page.value + 1);
}
},
{ rootMargin: '200px' },
);
if (sentinel.value) observer.observe(sentinel.value);
});
onUnmounted(() => {
observer?.disconnect();
});
const pullIndicatorStyle = () => ({
height: `${pullDistance.value}px`,
@@ -144,6 +184,16 @@ const pullIndicatorStyle = () => ({
</article>
</div>
<div ref="sentinel" class="sentinel" />
<div v-if="loading && items.length > 0" class="load-more-spinner">
<GoldSpinner :size="24" />
</div>
<div v-else-if="!hasMore && items.length > 0" class="end-hint">
{{ t('common.no_more') }}
</div>
<div v-else class="empty">
<p class="empty-title">{{ t('cashback.empty') }}</p>
<p class="empty-hint">{{ t('cashback.empty_hint') }}</p>
@@ -351,4 +401,23 @@ const pullIndicatorStyle = () => ({
line-height: 1.5;
color: #666;
}
.sentinel {
height: 1px;
}
.load-more-spinner {
display: flex;
justify-content: center;
padding: 20px 0 8px;
}
.end-hint {
text-align: center;
font-size: 12px;
color: #555;
font-weight: 600;
padding: 16px 0 4px;
letter-spacing: 0.03em;
}
</style>