346 lines
7.8 KiB
Vue
346 lines
7.8 KiB
Vue
<script setup lang="ts">
|
||
import { computed, onActivated, onMounted, ref } from 'vue';
|
||
import { useRouter } from 'vue-router';
|
||
import { useI18n } from 'vue-i18n';
|
||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||
import GoldSpinner from './GoldSpinner.vue';
|
||
import ConfirmDialog from './ConfirmDialog.vue';
|
||
import { useAuthStore } from '../stores/auth';
|
||
import { usePlayerMessages, type DepositMessagePayload, type PlayerMessage } from '../composables/usePlayerMessages';
|
||
|
||
const router = useRouter();
|
||
const { t, locale } = useI18n();
|
||
const auth = useAuthStore();
|
||
const {
|
||
messages,
|
||
loading,
|
||
listLoaded,
|
||
loadMessages,
|
||
refreshUnreadCount,
|
||
deleteMessage,
|
||
} = usePlayerMessages();
|
||
|
||
const page = ref(1);
|
||
const total = ref(0);
|
||
const deletingId = ref<string | null>(null);
|
||
const deleteConfirmVisible = ref(false);
|
||
const pendingDeleteId = ref<string | null>(null);
|
||
|
||
const hasMore = computed(() => messages.value.length < total.value);
|
||
|
||
function formatDate(createdAt?: string) {
|
||
if (!createdAt) return '';
|
||
return formatLocalMatchDateTime(createdAt, locale.value, { variant: 'full' });
|
||
}
|
||
|
||
function messageTitle(item: PlayerMessage) {
|
||
if (item.type === 'DEPOSIT_APPROVED') return t('messages.deposit_approved_title');
|
||
if (item.type === 'DEPOSIT_REJECTED') return t('messages.deposit_rejected_title');
|
||
return item.title;
|
||
}
|
||
|
||
function messagePreview(item: PlayerMessage) {
|
||
if (item.type === 'DEPOSIT_APPROVED' || item.type === 'DEPOSIT_REJECTED') {
|
||
const deposit = item.payload as DepositMessagePayload | null;
|
||
if (deposit?.orderNo) return deposit.orderNo;
|
||
}
|
||
return item.body.length > 80 ? `${item.body.slice(0, 80)}…` : item.body;
|
||
}
|
||
|
||
function openDetail(id: string) {
|
||
router.push(`/messages/${id}`);
|
||
}
|
||
|
||
async function fetchPage(nextPage: number, append = false) {
|
||
const result = await loadMessages(nextPage, append);
|
||
if (result) {
|
||
page.value = result.page;
|
||
total.value = result.total;
|
||
}
|
||
}
|
||
|
||
function tryLoad() {
|
||
if (!auth.token) return;
|
||
void fetchPage(1);
|
||
void refreshUnreadCount();
|
||
}
|
||
|
||
function goLogin() {
|
||
auth.showLoginPrompt('/messages');
|
||
}
|
||
|
||
function onDelete(id: string, event: Event) {
|
||
event.stopPropagation();
|
||
if (deletingId.value) return;
|
||
pendingDeleteId.value = id;
|
||
deleteConfirmVisible.value = true;
|
||
}
|
||
|
||
function onDeleteCancel() {
|
||
pendingDeleteId.value = null;
|
||
}
|
||
|
||
async function confirmDelete() {
|
||
const id = pendingDeleteId.value;
|
||
if (!id || deletingId.value) return;
|
||
deletingId.value = id;
|
||
try {
|
||
await deleteMessage(id);
|
||
total.value = Math.max(0, total.value - 1);
|
||
deleteConfirmVisible.value = false;
|
||
pendingDeleteId.value = null;
|
||
} finally {
|
||
deletingId.value = null;
|
||
}
|
||
}
|
||
|
||
onMounted(tryLoad);
|
||
onActivated(tryLoad);
|
||
</script>
|
||
|
||
<template>
|
||
<div class="message-list-panel">
|
||
<div v-if="!auth.token" class="guest-hint">
|
||
<p>{{ t('auth.login_required') }}</p>
|
||
<button type="button" class="login-link" @click="goLogin">{{ t('auth.go_login') }}</button>
|
||
</div>
|
||
|
||
<template v-else>
|
||
<div v-if="loading && !listLoaded" class="state">
|
||
<GoldSpinner :size="36" />
|
||
</div>
|
||
|
||
<div v-else-if="!messages.length" class="empty">
|
||
<p>{{ t('messages.empty') }}</p>
|
||
</div>
|
||
|
||
<div v-else class="list">
|
||
<div
|
||
v-for="item in messages"
|
||
:key="item.id"
|
||
class="list-row"
|
||
:class="{ unread: !item.isRead }"
|
||
>
|
||
<button type="button" class="row-body" @click="openDetail(item.id)">
|
||
<span class="row-dot" aria-hidden="true" />
|
||
<span class="row-main">
|
||
<span class="title-row">
|
||
<span class="title">{{ messageTitle(item) }}</span>
|
||
<span class="status-badge" :class="{ unread: !item.isRead }">
|
||
{{ item.isRead ? t('messages.status_read') : t('messages.status_unread') }}
|
||
</span>
|
||
</span>
|
||
<span class="preview">{{ messagePreview(item) }}</span>
|
||
<span v-if="item.createdAt" class="date">{{ formatDate(item.createdAt) }}</span>
|
||
</span>
|
||
<span class="chevron" aria-hidden="true">›</span>
|
||
</button>
|
||
<button
|
||
type="button"
|
||
class="delete-btn"
|
||
:aria-label="t('messages.delete')"
|
||
:disabled="deletingId === item.id"
|
||
@click="onDelete(item.id, $event)"
|
||
>
|
||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||
<path
|
||
d="M6 7h12M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2m2 0v12a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V7h12Z"
|
||
fill="none"
|
||
stroke="currentColor"
|
||
stroke-width="1.6"
|
||
stroke-linecap="round"
|
||
stroke-linejoin="round"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<button v-if="hasMore" type="button" class="load-more" :disabled="loading" @click="fetchPage(page + 1, true)">
|
||
{{ loading ? t('common.loading_more') : t('messages.load_more') }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<ConfirmDialog
|
||
v-model:visible="deleteConfirmVisible"
|
||
:title="t('messages.delete')"
|
||
:message="t('messages.delete_confirm')"
|
||
:confirm-text="t('messages.delete')"
|
||
danger
|
||
:loading="!!deletingId"
|
||
@confirm="confirmDelete"
|
||
@cancel="onDeleteCancel"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.message-list-panel {
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.guest-hint,
|
||
.state,
|
||
.empty {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 48px 16px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.login-link {
|
||
border: 1px solid var(--border-active);
|
||
border-radius: 8px;
|
||
padding: 8px 14px;
|
||
background: rgba(0, 61, 107, 0.06);
|
||
color: var(--primary);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.list-row {
|
||
display: flex;
|
||
align-items: stretch;
|
||
gap: 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.row-body {
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
min-width: 0;
|
||
padding: 14px 0;
|
||
border: none;
|
||
background: none;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.list-row.unread .title {
|
||
color: var(--primary-dark, #002847);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.row-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: transparent;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.list-row.unread .row-dot {
|
||
background: var(--primary);
|
||
box-shadow: 0 0 6px rgba(0, 61, 107, 0.35);
|
||
}
|
||
|
||
.row-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.title-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 15px;
|
||
color: #374151;
|
||
line-height: 1.35;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.status-badge {
|
||
flex-shrink: 0;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
color: var(--text-muted);
|
||
background: rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.status-badge.unread {
|
||
color: var(--primary);
|
||
background: rgba(0, 61, 107, 0.08);
|
||
}
|
||
|
||
.preview {
|
||
font-size: 13px;
|
||
color: var(--text-muted);
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.date {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.chevron {
|
||
color: var(--text-muted);
|
||
font-size: 20px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.delete-btn {
|
||
flex-shrink: 0;
|
||
align-self: center;
|
||
width: 36px;
|
||
height: 36px;
|
||
margin-left: 4px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: #9ca3af;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.delete-btn:active:not(:disabled) {
|
||
background: rgba(220, 38, 38, 0.08);
|
||
color: #dc2626;
|
||
}
|
||
|
||
.delete-btn svg {
|
||
width: 18px;
|
||
height: 18px;
|
||
}
|
||
|
||
.delete-btn:disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.load-more {
|
||
margin-top: 12px;
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 8px;
|
||
background: var(--bg-card);
|
||
color: var(--primary);
|
||
font-size: 13px;
|
||
}
|
||
</style>
|