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, onMounted } from 'vue';
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import api from '../api';
@@ -25,24 +25,58 @@ interface DepositOrder {
}
const items = ref<DepositOrder[]>([]);
const loading = ref(true);
const loading = ref(false);
const initialLoading = ref(true);
const page = ref(1);
const total = ref(0);
const hasMore = ref(true);
async function fetchOrders() {
const sentinel = ref<HTMLElement | null>(null);
let observer: IntersectionObserver | null = null;
async function fetchOrders(p = 1) {
if (loading.value) return;
loading.value = true;
try {
const { data } = await api.get('/player/deposit-orders', { params: { page: page.value } });
const result = data.data ?? { items: [], total: 0 };
items.value = result.items ?? [];
const { data } = await api.get('/player/deposit-orders', { 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];
}
total.value = result.total ?? 0;
const pageSize = result.pageSize ?? 20;
hasMore.value = newItems.length >= pageSize && items.value.length < total.value;
page.value = p;
} catch { /* */ } finally {
loading.value = false;
initialLoading.value = false;
}
}
const { pullDistance, spinning, progress } = usePullToRefresh({
onRefresh: fetchOrders,
onRefresh: async () => { await fetchOrders(1); },
});
onMounted(() => {
fetchOrders(1);
observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore.value && !loading.value) {
fetchOrders(page.value + 1);
}
},
{ rootMargin: '200px' },
);
if (sentinel.value) observer.observe(sentinel.value);
});
onUnmounted(() => {
observer?.disconnect();
});
function statusClass(s: string) {
@@ -123,6 +157,16 @@ onMounted(fetchOrders);
</div>
</div>
</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>
</template>
</div>
</template>
@@ -205,4 +249,23 @@ onMounted(fetchOrders);
border-left: 2px solid rgba(212, 175, 55, 0.3);
}
.reject-reason { margin-top: 8px; font-size: 12px; color: #f56c6c; background: #2a1515; padding: 6px 10px; border-radius: 6px; }
.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>