feat(theme-4): PC 列表详情弹窗、实心面板与收件箱样式修复

投注/资金/充值/返水列表点击改为海军蓝弹窗,深链 query 自动打开;修复冻结字段 i18n 与悬浮客服按钮过白问题。
This commit is contained in:
mars
2026-07-10 17:25:41 +08:00
parent b420664795
commit 4a76c4a1bc
20 changed files with 1604 additions and 62 deletions

View File

@@ -1,9 +1,23 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useViewport } from '../composables/useViewport';
import MobileBetDetailView from './MobileBetDetailView.vue';
import DesktopBetDetailView from './desktop/DesktopBetDetailView.vue';
const route = useRoute();
const router = useRouter();
const { isDesktop } = useViewport();
onMounted(() => {
if (!isDesktop.value) return;
const betNo = route.params.betNo;
if (betNo) {
void router.replace({ path: '/bets', query: { betNo: String(betNo) } });
}
});
</script>
<template>
<DesktopBetDetailView v-if="isDesktop" />
<MobileBetDetailView v-else />

View File

@@ -159,7 +159,11 @@ onActivated(() => {
</button>
</div>
<MessageListPanel v-if="inboxEnabled" v-show="activeTab === 'messages'" />
<MessageListPanel
v-if="inboxEnabled"
v-show="activeTab === 'messages'"
@click-message="(id) => router.push(`/messages/${id}`)"
/>
<CustomerServicePanel v-if="activeTab === 'support'" />
<ConfirmDialog

View File

@@ -1,9 +1,24 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useViewport } from '../composables/useViewport';
import MobileRechargeDetailView from './MobileRechargeDetailView.vue';
import DesktopRechargeDetailView from './desktop/DesktopRechargeDetailView.vue';
const route = useRoute();
const router = useRouter();
const { isDesktop } = useViewport();
onMounted(() => {
if (!isDesktop.value) return;
const id = route.params.id;
if (id) {
void router.replace({
path: '/wallet/recharge/history',
query: { orderId: String(id) },
});
}
});
</script>
<template>

View File

@@ -1,9 +1,26 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useViewport } from '../composables/useViewport';
import MobileWalletTransactionDetailView from './MobileWalletTransactionDetailView.vue';
import DesktopWalletTransactionDetailView from './desktop/DesktopWalletTransactionDetailView.vue';
const route = useRoute();
const router = useRouter();
const { isDesktop } = useViewport();
onMounted(() => {
if (!isDesktop.value) return;
const transactionId = route.params.transactionId;
if (transactionId) {
void router.replace({
path: '/wallet/detail',
query: { transactionId: String(transactionId) },
});
}
});
</script>
<template>
<DesktopWalletTransactionDetailView v-if="isDesktop" />
<MobileWalletTransactionDetailView v-else />

View File

@@ -1,22 +1,50 @@
<script setup lang="ts">
import { computed, ref, onMounted, onActivated, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import api from '../../api';
import GoldSpinner from '../../components/GoldSpinner.vue';
import Pagination from '../../components/desktop/Pagination.vue';
import DesktopWalletSubNav from '../../components/desktop/DesktopWalletSubNav.vue';
import DesktopCashbackDetailModal from '../../components/desktop/DesktopCashbackDetailModal.vue';
import { formatMoney, formatMoneyCompact } from '../../utils/localeDisplay';
import { parseCashbackApiData, type CashbackRecord } from '../../utils/cashback';
const COL_COUNT = 6;
const { t, locale } = useI18n();
const route = useRoute();
const router = useRouter();
const detailModalVisible = ref(false);
const detailRecord = ref<CashbackRecord | null>(null);
const allItems = ref<CashbackRecord[]>([]);
const loading = ref(true);
const page = ref(1);
const pageSize = ref(20);
function openDetail(record: CashbackRecord) {
detailRecord.value = record;
detailModalVisible.value = true;
}
function openDetailFromQuery() {
const batchNo = route.query.batchNo;
if (typeof batchNo !== 'string' || !batchNo) return;
const match = allItems.value.find((row) => row.batchNo === batchNo);
if (match) {
openDetail(match);
const { batchNo: _removed, ...rest } = route.query;
void router.replace({ path: route.path, query: rest });
}
}
watch(() => route.query.batchNo, openDetailFromQuery);
watch(allItems, openDetailFromQuery);
watch(detailModalVisible, (visible) => {
if (!visible) detailRecord.value = null;
});
const total = computed(() => allItems.value.length);
const items = computed(() => {
@@ -75,8 +103,14 @@ watch(pageSize, () => {
page.value = 1;
});
onMounted(loadData);
onActivated(loadData);
onMounted(async () => {
await loadData();
openDetailFromQuery();
});
onActivated(async () => {
await loadData();
openDetailFromQuery();
});
</script>
<template>
@@ -113,7 +147,12 @@ onActivated(loadData);
</td>
</tr>
<template v-else-if="items.length">
<tr v-for="row in items" :key="row.id" class="hover-row">
<tr
v-for="row in items"
:key="row.id"
class="hover-row clickable-row"
@click="openDetail(row)"
>
<td>{{ formatPeriod(row.periodStart, row.periodEnd) }}</td>
<td>{{ formatMoneyCompact(row.effectiveStake, locale) }}</td>
<td>{{ formatRate(row.rate) }}</td>
@@ -140,6 +179,8 @@ onActivated(loadData);
/>
</div>
</section>
<DesktopCashbackDetailModal v-model:visible="detailModalVisible" :record="detailRecord" />
</div>
</template>

View File

@@ -1,18 +1,42 @@
<script setup lang="ts">
import { ref, onMounted, onActivated } from 'vue';
import { ref, onMounted, onActivated, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import api from '../../api';
import GoldSpinner from '../../components/GoldSpinner.vue';
import Pagination from '../../components/desktop/Pagination.vue';
import DesktopBetDetailModal from '../../components/desktop/DesktopBetDetailModal.vue';
import { type BetHistoryItem } from '../../components/BetHistoryCard.vue';
import { useOnLocaleChange } from '../../composables/useOnLocaleChange';
const COL_COUNT = 8;
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const detailModalVisible = ref(false);
const detailBetNo = ref<string | null>(null);
function openDetail(betNo: string) {
detailBetNo.value = betNo;
detailModalVisible.value = true;
}
function openDetailFromQuery() {
const betNo = route.query.betNo;
if (typeof betNo === 'string' && betNo) {
openDetail(betNo);
const { betNo: _removed, ...rest } = route.query;
void router.replace({ path: route.path, query: rest });
}
}
watch(() => route.query.betNo, openDetailFromQuery);
watch(detailModalVisible, (visible) => {
if (!visible) detailBetNo.value = null;
});
const items = ref<BetHistoryItem[]>([]);
const total = ref(0);
const page = ref(1);
@@ -62,7 +86,10 @@ useOnLocaleChange(() => {
});
onActivated(() => { void loadPage(page.value); });
onMounted(() => { void loadPage(1); });
onMounted(() => {
void loadPage(1);
openDetailFromQuery();
});
function statusClass(status: string) {
const map: Record<string, string> = {
@@ -154,7 +181,7 @@ function statusLabel(status: string) {
v-for="bet in items"
:key="bet.betNo"
class="hover-row clickable-row"
@click="router.push(`/bets/${bet.betNo}`)"
@click="openDetail(bet.betNo)"
>
<td class="id-cell">{{ bet.betNo }}</td>
<td>{{ bet.matchTitle || '-' }}</td>
@@ -187,6 +214,8 @@ function statusLabel(status: string) {
/>
</div>
</section>
<DesktopBetDetailModal v-model:visible="detailModalVisible" :bet-no="detailBetNo" />
</div>
</template>

View File

@@ -1,28 +1,49 @@
<script setup lang="ts">
import { ref, onMounted, onActivated } from 'vue';
import { ref, onMounted, onActivated, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { RouterLink, useRouter } from 'vue-router';
import { RouterLink, useRoute, useRouter } from 'vue-router';
import api from '../../api';
import GoldSpinner from '../../components/GoldSpinner.vue';
import Pagination from '../../components/desktop/Pagination.vue';
import DesktopWalletSubNav from '../../components/desktop/DesktopWalletSubNav.vue';
import DesktopRechargeDetailModal from '../../components/desktop/DesktopRechargeDetailModal.vue';
import { formatMoney } from '../../utils/localeDisplay';
import { enrichDepositOrder, findDepositOrderById, type PlayerDepositOrder } from '../../utils/depositOrderLookup';
const COL_COUNT = 5;
const { t, locale } = useI18n();
const route = useRoute();
const router = useRouter();
type RechargeOrder = {
id: string;
orderNo: string;
amount: string;
status: string;
methodType?: string;
bankName?: string | null;
paymentMethodName?: string | null;
createdAt: string;
};
const detailModalVisible = ref(false);
const detailOrder = ref<PlayerDepositOrder | null>(null);
async function openDetail(order: PlayerDepositOrder) {
detailOrder.value = await enrichDepositOrder(order);
detailModalVisible.value = true;
}
async function openDetailFromQuery() {
const orderId = route.query.orderId;
if (typeof orderId !== 'string' || !orderId) return;
const existing = items.value.find((order) => order.id === orderId);
const order = existing ?? await findDepositOrderById(orderId);
if (order) await openDetail(order);
const { orderId: _removed, ...rest } = route.query;
void router.replace({ path: route.path, query: rest });
}
watch(() => route.query.orderId, () => {
void openDetailFromQuery();
});
watch(detailModalVisible, (visible) => {
if (!visible) detailOrder.value = null;
});
type RechargeOrder = PlayerDepositOrder;
const items = ref<RechargeOrder[]>([]);
const loading = ref(true);
@@ -46,8 +67,14 @@ async function loadPage(p: number) {
}
}
onMounted(() => loadPage(1));
onActivated(() => loadPage(page.value));
onMounted(async () => {
await loadPage(1);
await openDetailFromQuery();
});
onActivated(async () => {
await loadPage(page.value);
await openDetailFromQuery();
});
function methodLabel(order: RechargeOrder) {
if (order.methodType === 'USDT') return 'USDT';
@@ -109,7 +136,7 @@ function statusLabel(status: string) {
v-for="order in items"
:key="order.id"
class="hover-row clickable-row"
@click="router.push(`/wallet/recharge/history/${order.id}`)"
@click="openDetail(order)"
>
<td class="id-cell">{{ order.orderNo || order.id }}</td>
<td>{{ methodLabel(order) }}</td>
@@ -139,5 +166,7 @@ function statusLabel(status: string) {
/>
</div>
</section>
<DesktopRechargeDetailModal v-model:visible="detailModalVisible" :order="detailOrder" />
</div>
</template>

View File

@@ -1,23 +1,39 @@
<script setup lang="ts">
import { ref, onMounted, onActivated } from 'vue';
import { ref, onMounted, onActivated, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { useRoute, useRouter } from 'vue-router';
import api from '../../api';
import GoldSpinner from '../../components/GoldSpinner.vue';
import DesktopTxDetailModal from '../../components/desktop/DesktopTxDetailModal.vue';
import { formatMoney } from '../../utils/localeDisplay';
import { txTypeKey, txDisplayType, txAmountClass, txSummaryLabel } from '../../utils/walletTx';
const { t, locale } = useI18n();
const route = useRoute();
const router = useRouter();
function goBack() {
if (window.history.state && window.history.state.back) {
router.back();
} else {
router.push('/wallet');
const detailModalVisible = ref(false);
const detailTxId = ref<string | null>(null);
function openDetail(txId: string) {
detailTxId.value = txId;
detailModalVisible.value = true;
}
function openDetailFromQuery() {
const transactionId = route.query.transactionId;
if (typeof transactionId === 'string' && transactionId) {
openDetail(transactionId);
const { transactionId: _removed, ...rest } = route.query;
void router.replace({ path: route.path, query: rest });
}
}
watch(() => route.query.transactionId, openDetailFromQuery);
watch(detailModalVisible, (visible) => {
if (!visible) detailTxId.value = null;
});
type Transaction = {
transactionType: string;
displayType?: string;
@@ -59,8 +75,14 @@ async function loadPage(p: number) {
}
}
onMounted(() => loadPage(1));
onActivated(() => loadPage(1));
onMounted(() => {
void loadPage(1);
openDetailFromQuery();
});
onActivated(() => {
void loadPage(1);
openDetailFromQuery();
});
</script>
<template>
@@ -94,7 +116,7 @@ onActivated(() => loadPage(1));
v-for="tx in items"
:key="tx.transactionId"
class="hover-row clickable-row"
@click="router.push(`/wallet/transactions/${tx.transactionId}`)"
@click="openDetail(tx.transactionId)"
>
<td class="type-cell">{{ txLabel(tx) }}</td>
<td class="note-cell">{{ txSummaryLabel(tx, t) || '-' }}</td>
@@ -118,6 +140,11 @@ onActivated(() => loadPage(1));
</template>
</div>
</main>
<DesktopTxDetailModal
v-model:visible="detailModalVisible"
:transaction-id="detailTxId"
/>
</div>
</template>
@@ -146,10 +173,10 @@ onActivated(() => loadPage(1));
flex-direction: column;
flex: 0 1 auto;
min-height: 0;
background: var(--bg-card);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.08);
background: var(--desktop-surface-solid);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow);
overflow: hidden;
}
.table-wrap { flex: 1; overflow-x: auto; overflow-y: auto; }
@@ -163,7 +190,7 @@ onActivated(() => loadPage(1));
font-size: 11px;
font-weight: 700;
color: var(--text-muted);
background: #0A2540;
background: var(--desktop-surface-header);
text-transform: uppercase;
letter-spacing: 0.06em;
border-bottom: 1px solid var(--desktop-border);

View File

@@ -109,11 +109,11 @@ function txLabel(tx: TxDetail): string {
<span class="row-val mono">{{ formatMoney(detail.balanceAfter, locale) }}</span>
</div>
<div v-if="parseFloat(detail.frozenBefore || '0') > 0 || parseFloat(detail.frozenAfter || '0') > 0" class="detail-row">
<span class="row-label">{{ t('wallet.frozen_before') || '冻结前' }}</span>
<span class="row-label">{{ t('wallet.detail_frozen_before') }}</span>
<span class="row-val mono">{{ formatMoney(detail.frozenBefore, locale) }}</span>
</div>
<div v-if="parseFloat(detail.frozenBefore || '0') > 0 || parseFloat(detail.frozenAfter || '0') > 0" class="detail-row">
<span class="row-label">{{ t('wallet.frozen_after') || '冻结后' }}</span>
<span class="row-label">{{ t('wallet.detail_frozen_after') }}</span>
<span class="row-val mono">{{ formatMoney(detail.frozenAfter, locale) }}</span>
</div>
<div class="detail-row">

View File

@@ -1,17 +1,25 @@
<script setup lang="ts">
import { ref, onMounted, onActivated } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter, RouterLink } from 'vue-router';
import { RouterLink } from 'vue-router';
import api from '../../api';
import GoldSpinner from '../../components/GoldSpinner.vue';
import Pagination from '../../components/desktop/Pagination.vue';
import DesktopWalletSubNav from '../../components/desktop/DesktopWalletSubNav.vue';
import DesktopTxDetailModal from '../../components/desktop/DesktopTxDetailModal.vue';
import { formatMoney } from '../../utils/localeDisplay';
import { txTypeKey, txDisplayType, txAmountClass, txSummaryLabel } from '../../utils/walletTx';
import { parseCashbackApiData, sumCashbackAmount } from '../../utils/cashback';
const { t, locale } = useI18n();
const router = useRouter();
const detailModalVisible = ref(false);
const detailTxId = ref<string | null>(null);
function openDetail(txId: string) {
detailTxId.value = txId;
detailModalVisible.value = true;
}
type Transaction = {
transactionType: string;
@@ -113,7 +121,7 @@ onActivated(() => fetchData(1));
v-for="tx in items"
:key="tx.transactionId"
class="hover-row clickable-row"
@click="router.push(`/wallet/transactions/${tx.transactionId}`)"
@click="openDetail(tx.transactionId)"
>
<td class="type-cell">{{ txLabel(tx) }}</td>
<td class="muted-cell">{{ txSubtitle(tx) || '-' }}</td>
@@ -139,6 +147,8 @@ onActivated(() => fetchData(1));
<span>{{ t('wallet.no_records') }}</span>
</div>
</section>
<DesktopTxDetailModal v-model:visible="detailModalVisible" :transaction-id="detailTxId" />
</div>
</template>