yumi-admin/apps/src/views/operate/game-red-packet-record.vue
2026-03-31 17:40:10 +08:00

408 lines
12 KiB
Vue

<script lang="ts" setup>
import {
computed,
reactive,
ref,
watch } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import type { Dayjs } from 'dayjs';
import { flowGameRedPacket } from '#/api/legacy/room';
import { formatDate,
getAllowedSysOrigins } from '#/views/system/shared';
import {
Button,
Card,
DatePicker,
Descriptions,
DescriptionsItem,
Drawer,
Space,
Table,
Tag,
Timeline,
TimelineItem
} from 'antdv-next';
import RoomDetailsDrawer from '../app-system/components/room-details-drawer.vue';
import AccountInput from '#/components/account-input.vue';
import UserProfileLink from './components/user-profile-link.vue';
import RoomSearchInput from './components/room-search-input.vue';
defineOptions({ name: 'OperateGameRedPacketRecord' });
const { RangePicker } = DatePicker;
const RED_PACKET_TYPE_OPTIONS = [
{ label: '房间内', value: 'ROOM' },
{ label: '用户1V1', value: 'USER' },
{ label: '家族群聊', value: 'FAMILY_GROUP_CHAT' },
];
const STATUS_MAP: Record<string, string> = {
AVAILABLE: '等待领取',
NOT_RECEIVE: '已抢完',
REIMBURSE: '已处理退款',
NOT_REIMBURSE: '已处理退款, 抽成没有可退',
};
const accessStore = useAccessStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const loading = ref(false);
const loadingMore = ref(false);
const notMore = ref(false);
const list = ref<Array<Record<string, any>>>([]);
const rangeDate = ref<[Dayjs, Dayjs] | null>(null);
const detailOpen = ref(false);
const activeRow = ref<Record<string, any> | null>(null);
const roomDetailsOpen = ref(false);
const activeRoomId = ref('');
const query = reactive({
endCreateDate: '',
lastId: '',
limit: 20,
receiverId: '',
relationId: '',
startCreateDate: '',
sysOrigin: '',
type: 'ROOM',
});
const columns = [
{ dataIndex: 'userProfile', key: 'userProfile', title: '用户', width: 220 },
{ dataIndex: 'receiver', key: 'receiver', title: '接收用户', width: 220 },
{ dataIndex: 'roomProfile', key: 'roomProfile', title: '房间', width: 240 },
{ dataIndex: 'amount', key: 'amount', title: '金额', width: 120 },
{ dataIndex: 'quantity', key: 'quantity', title: '数量', width: 120 },
{ dataIndex: 'receiveUsers', key: 'receiveUsers', title: '已领取', width: 100 },
{ dataIndex: 'status', key: 'status', title: '状态', width: 140 },
{ dataIndex: 'createTime', key: 'createTime', title: '创建时间', width: 180 },
{ dataIndex: 'updateTime', key: 'updateTime', title: '修改时间', width: 180 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 100 },
];
watch(
sysOriginOptions,
(options) => {
if (!query.sysOrigin && options.length > 0) {
query.sysOrigin = String(options[0]?.value || '');
}
},
{ immediate: true },
);
watch(rangeDate, (value) => {
if (!value?.length) {
query.startCreateDate = '';
query.endCreateDate = '';
return;
}
query.startCreateDate = String(value[0].valueOf());
query.endCreateDate = String(value[1].valueOf());
});
function getReceiveUsersLength(row: Record<string, any>) {
return Array.isArray(row.receiveUsers) ? row.receiveUsers.length : 0;
}
async function loadData(reset = false, loadMore = false) {
if (reset) {
list.value = [];
query.lastId = '';
notMore.value = false;
}
if (loadMore) {
loadingMore.value = true;
} else {
loading.value = true;
}
try {
const result = await flowGameRedPacket({ ...query });
const next = result || [];
notMore.value = next.length <= 0;
if (!notMore.value) {
list.value = list.value.concat(next);
query.lastId = String(list.value[list.value.length - 1]?.id || '');
}
} finally {
loading.value = false;
loadingMore.value = false;
}
}
function handleSearch() {
void loadData(true);
}
function openRoomDetails(roomId?: number | string) {
if (!roomId) {
return;
}
activeRoomId.value = String(roomId);
roomDetailsOpen.value = true;
}
function openDetails(record: Record<string, any>) {
activeRow.value = record;
detailOpen.value = true;
}
function handleRoomSuccess(item: Record<string, any>) {
query.relationId = String(item?.id || '');
}
function handleRoomFail() {
query.relationId = '';
}
loadData(true);
</script>
<template>
<Page title="红包发送列表">
<Card>
<div class="toolbar">
<Space wrap>
<SysOriginSelect v-model:value="query.sysOrigin" style="width: 140px" @change="handleSearch"
:options="sysOriginOptions"
></SysOriginSelect>
<Select option-label-prop="label" v-model:value="query.type" style="width: 140px" @change="handleSearch"
:options="RED_PACKET_TYPE_OPTIONS.map((item) => ({ label: `${item.label}`, value: item.value as any }))"
/>
<RoomSearchInput
v-if="query.type === 'ROOM'"
:sys-origin="query.sysOrigin"
@fail="handleRoomFail"
@success="handleRoomSuccess"
/>
<AccountInput
v-else
v-model:value="query.relationId"
:sys-origin="query.sysOrigin"
placeholder="发送用户"
style="width: 260px"
/>
<AccountInput
v-if="query.type === 'USER'"
v-model:value="query.receiverId"
:sys-origin="query.sysOrigin"
placeholder="接收用户"
style="width: 260px"
/>
<RangePicker v-model:value="rangeDate" show-time />
<Button :loading="loading" type="primary" @click="handleSearch">
搜索
</Button>
</Space>
</div>
<Table
:columns="columns"
:data-source="list"
:loading="loading"
:pagination="false"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'userProfile'">
<UserProfileLink :profile="record.userProfile" />
</template>
<template v-else-if="column.key === 'receiver'">
<UserProfileLink
v-if="query.type === 'USER'"
:profile="record.designatedReceiverUser"
/>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'roomProfile'">
<div v-if="query.type === 'ROOM' && record.roomProfile" class="room-cell">
<img
:src="record.roomProfile.roomCover || 'https://dummyimage.com/48x48/e2e8f0/64748b&text=R'"
alt=""
class="room-cover"
>
<div class="room-copy">
<Button
size="small"
type="link"
@click="openRoomDetails(record.roomProfile?.id)"
>
{{ record.roomProfile?.roomName || '-' }}
</Button>
<div class="room-meta">
{{ record.roomProfile?.roomAccount || record.roomProfile?.id || '-' }}
</div>
</div>
</div>
<span v-else>-</span>
</template>
<template v-else-if="column.key === 'receiveUsers'">
{{ getReceiveUsersLength(record) }}
</template>
<template v-else-if="column.key === 'status'">
{{
getReceiveUsersLength(record) === Number(record.quantity || 0)
? STATUS_MAP.NOT_RECEIVE
: (STATUS_MAP[record.status] || record.status || '-')
}}
</template>
<template v-else-if="column.key === 'createTime'">
{{ formatDate(record.createTime) }}
</template>
<template v-else-if="column.key === 'updateTime'">
{{ formatDate(record.updateTime) }}
</template>
<template v-else-if="column.key === 'actions'">
<Button size="small" type="link" @click="openDetails(record)">
详情
</Button>
</template>
</template>
</Table>
<div v-if="query.lastId" class="load-more">
<span v-if="notMore">已加载全部</span>
<Button v-else :loading="loadingMore" @click="loadData(false, true)">加载更多</Button>
</div>
</Card>
<Drawer
:open="detailOpen"
destroy-on-close
title="详情"
width="720px"
@close="detailOpen = false"
>
<div v-if="activeRow" class="detail-stack">
<Card size="small" title="发送人">
<UserProfileLink :profile="activeRow.userProfile" />
</Card>
<Card v-if="activeRow.type === 'ROOM' && activeRow.roomProfile" size="small" title="发送房间">
<div class="room-cell">
<img
:src="activeRow.roomProfile.roomCover || 'https://dummyimage.com/48x48/e2e8f0/64748b&text=R'"
alt=""
class="room-cover"
>
<div class="room-copy">
<Button
size="small"
type="link"
@click="openRoomDetails(activeRow.roomProfile?.id)"
>
{{ activeRow.roomProfile?.roomName || '-' }}
</Button>
<div class="room-meta">
{{ activeRow.roomProfile?.roomAccount || activeRow.roomProfile?.id || '-' }}
</div>
</div>
</div>
</Card>
<Card size="small" title="基础信息">
<Descriptions :column="2" bordered size="small">
<DescriptionsItem label="ID">{{ activeRow.id || '-' }}</DescriptionsItem>
<DescriptionsItem label="类型">{{ activeRow.type || '-' }}</DescriptionsItem>
<DescriptionsItem label="金额">{{ activeRow.amount || 0 }}</DescriptionsItem>
<DescriptionsItem label="数量">{{ activeRow.quantity || 0 }}</DescriptionsItem>
<DescriptionsItem label="状态">
{{
getReceiveUsersLength(activeRow) === Number(activeRow.quantity || 0)
? STATUS_MAP.NOT_RECEIVE
: (STATUS_MAP[activeRow.status] || activeRow.status || '-')
}}
</DescriptionsItem>
<DescriptionsItem label="创建时间">
{{ formatDate(activeRow.createTime) }}
</DescriptionsItem>
</Descriptions>
</Card>
<Card size="small" title="领取用户">
<Timeline>
<TimelineItem
v-for="item in activeRow.receiveUsers || []"
:key="`${item.userProfile?.id}-${item.createTime}`"
>
<UserProfileLink :profile="item.userProfile" />
<div class="timeline-meta">
<Tag>领取: {{ item.amount || 0 }}</Tag>
<span>{{ formatDate(item.createTime) }}</span>
</div>
</TimelineItem>
</Timeline>
</Card>
<Card size="small" title="日志事件">
<Timeline>
<TimelineItem
v-for="(item, index) in activeRow.logs || []"
:key="`${index}-${item.createTime}`"
>
{{ item.content || '-' }}
<div class="timeline-meta">{{ formatDate(item.createTime) }}</div>
</TimelineItem>
</Timeline>
</Card>
</div>
</Drawer>
<RoomDetailsDrawer
:open="roomDetailsOpen"
:room-id="activeRoomId"
@close="roomDetailsOpen = false"
/>
</Page>
</template>
<style scoped>
.toolbar {
margin-bottom: 16px;
}
.room-cell {
align-items: center;
display: flex;
gap: 12px;
}
.room-cover {
border-radius: 12px;
height: 48px;
object-fit: cover;
width: 48px;
}
.room-meta {
color: #64748b;
font-size: 12px;
}
.load-more {
display: flex;
justify-content: center;
margin-top: 16px;
}
.detail-stack {
display: grid;
gap: 16px;
}
.timeline-meta {
color: #64748b;
display: flex;
gap: 8px;
margin-top: 8px;
}
</style>