feat(admin): show recharge reward recipients

This commit is contained in:
zhx 2026-05-07 17:30:15 +08:00
parent 864502db1c
commit e5d2c94c04
2 changed files with 318 additions and 75 deletions

View File

@ -110,6 +110,15 @@ export async function saveResidentRechargeRewardConfig(data: Record<string, any>
); );
} }
export async function pageResidentRechargeRewardClaimRecords(
params: Record<string, any>,
) {
return requestClient.get<Record<string, any>>(
`${RECHARGE_REWARD_BASE}/claim-record/page`,
{ params },
);
}
export async function pageSpecifiedGiftWeeklyRankConfigs(params: Record<string, any>) { export async function pageSpecifiedGiftWeeklyRankConfigs(params: Record<string, any>) {
return requestClient.get<LegacyPageResult<Record<string, any>>>( return requestClient.get<LegacyPageResult<Record<string, any>>>(
`${SPECIFIED_GIFT_WEEKLY_RANK_BASE}/config/page`, `${SPECIFIED_GIFT_WEEKLY_RANK_BASE}/config/page`,

View File

@ -16,11 +16,14 @@ import {
Spin, Spin,
Switch, Switch,
Table, Table,
Tabs,
TabPane,
Tag, Tag,
} from 'antdv-next'; } from 'antdv-next';
import { import {
getResidentRechargeRewardConfig, getResidentRechargeRewardConfig,
pageResidentRechargeRewardClaimRecords,
saveResidentRechargeRewardConfig, saveResidentRechargeRewardConfig,
} from '#/api/legacy/resident-activity'; } from '#/api/legacy/resident-activity';
import ActivityResourceGroupSelectDrawer from '#/views/props/components/activity-resource-group-select-drawer.vue'; import ActivityResourceGroupSelectDrawer from '#/views/props/components/activity-resource-group-select-drawer.vue';
@ -66,7 +69,9 @@ const sysOriginOptions = computed(() => {
}); });
const selectedSysOrigin = ref(''); const selectedSysOrigin = ref('');
const activeTab = ref('config');
const loading = ref(false); const loading = ref(false);
const recordLoading = ref(false);
const saving = ref(false); const saving = ref(false);
const groupPickerOpen = ref(false); const groupPickerOpen = ref(false);
const pickerLevelIndex = ref(-1); const pickerLevelIndex = ref(-1);
@ -104,6 +109,31 @@ const statusMeta = computed(() => {
return { color: 'default', text: '已关闭' }; return { color: 'default', text: '已关闭' };
}); });
const recordQuery = reactive<Record<string, any>>({
cursor: 1,
limit: 20,
month: '',
userId: '',
});
const recordPage = reactive<Record<string, any>>({
current: 1,
month: '',
records: [],
size: 20,
timezone: '',
total: 0,
});
const recordColumns = [
{ dataIndex: 'user', key: 'user', title: '用户', width: 280 },
{ dataIndex: 'rechargeMonth', key: 'rechargeMonth', title: '统计月份', width: 120 },
{ dataIndex: 'rechargeAmount', key: 'rechargeAmount', title: '充值金额', width: 140 },
{ dataIndex: 'reachedLevels', key: 'reachedLevels', title: '达到档位', width: 300 },
{ dataIndex: 'claimedRewards', key: 'claimedRewards', title: '领取奖励', width: 420 },
{ dataIndex: 'lastRechargeTime', key: 'lastRechargeTime', title: '最后充值时间', width: 180 },
];
function toAmountCents(value: number | string | undefined) { function toAmountCents(value: number | string | undefined) {
const numeric = Number(value || 0); const numeric = Number(value || 0);
if (!Number.isFinite(numeric)) { if (!Number.isFinite(numeric)) {
@ -164,6 +194,35 @@ async function loadConfig() {
} }
} }
function normalizeRecordPage(result: null | Record<string, any> | undefined) {
const value = result || {};
recordPage.records = Array.isArray(value.records) ? value.records : [];
recordPage.total = Number(value.total || 0);
recordPage.current = Number(value.current || 1);
recordPage.size = Number(value.size || 20);
recordPage.month = String(value.month || '');
recordPage.timezone = String(value.timezone || '');
if (!recordQuery.month && recordPage.month) {
recordQuery.month = recordPage.month;
}
}
async function loadClaimRecords() {
if (!selectedSysOrigin.value) {
return;
}
recordLoading.value = true;
try {
const result = await pageResidentRechargeRewardClaimRecords({
...recordQuery,
sysOrigin: selectedSysOrigin.value,
});
normalizeRecordPage(result);
} finally {
recordLoading.value = false;
}
}
function addLevel() { function addLevel() {
const levels = form.levelConfigs as Array<Record<string, any>>; const levels = form.levelConfigs as Array<Record<string, any>>;
const maxLevel = Math.max(0, ...levels.map((item) => Number(item.level || 0))); const maxLevel = Math.max(0, ...levels.map((item) => Number(item.level || 0)));
@ -273,11 +332,55 @@ async function submitForm() {
}); });
message.success('保存成功'); message.success('保存成功');
await loadConfig(); await loadConfig();
if (activeTab.value === 'claims') {
await loadClaimRecords();
}
} finally { } finally {
saving.value = false; saving.value = false;
} }
} }
function searchClaimRecords() {
recordQuery.cursor = 1;
void loadClaimRecords();
}
function handleClaimTableChange(pagination: Record<string, any>) {
recordQuery.cursor = Number(pagination.current || 1);
recordQuery.limit = Number(pagination.pageSize || 20);
void loadClaimRecords();
}
function avatarBackgroundStyle(url: string) {
return {
backgroundImage: `url("${String(url || '').replaceAll('"', '%22')}")`,
};
}
function rewardText(item: Record<string, any>) {
const parts: string[] = [];
const rewardGold = Number(item.rewardGold || 0);
if (rewardGold > 0) {
parts.push(`${rewardGold} 金币`);
}
if (item.rewardGroupName || item.rewardGroupId) {
parts.push(item.rewardGroupName || `奖励组 ${item.rewardGroupId}`);
}
const rewardItems = Array.isArray(item.rewardItems) ? item.rewardItems : [];
if (rewardItems.length > 0) {
parts.push(
rewardItems
.map((reward: Record<string, any>) => {
const name = reward.name || reward.type || reward.content || '奖励';
const quantity = Number(reward.quantity || 0);
return quantity > 0 ? `${name} x${quantity}` : String(name);
})
.join('、'),
);
}
return parts.length > 0 ? parts.join(' / ') : '-';
}
watch( watch(
sysOriginOptions, sysOriginOptions,
(options) => { (options) => {
@ -292,11 +395,19 @@ watch(
selectedSysOrigin, selectedSysOrigin,
(value) => { (value) => {
if (value) { if (value) {
recordQuery.cursor = 1;
void loadConfig(); void loadConfig();
void loadClaimRecords();
} }
}, },
{ immediate: true }, { immediate: true },
); );
watch(activeTab, (value) => {
if (value === 'claims') {
void loadClaimRecords();
}
});
</script> </script>
<template> <template>
@ -318,6 +429,8 @@ watch(
<Button :loading="loading" @click="loadConfig">刷新</Button> <Button :loading="loading" @click="loadConfig">刷新</Button>
</div> </div>
<Tabs v-model:activeKey="activeTab">
<TabPane key="config" tab="奖励配置">
<div class="config-row"> <div class="config-row">
<Space align="center" wrap> <Space align="center" wrap>
<span class="field-label">启用</span> <span class="field-label">启用</span>
@ -397,6 +510,86 @@ watch(
<Button @click="addLevel">新增档位</Button> <Button @click="addLevel">新增档位</Button>
<Button :loading="saving" type="primary" @click="submitForm">保存配置</Button> <Button :loading="saving" type="primary" @click="submitForm">保存配置</Button>
</div> </div>
</TabPane>
<TabPane key="claims" tab="已领取人员">
<div class="toolbar">
<Input
v-model:value="recordQuery.month"
allow-clear
placeholder="YYYY-MM"
style="width: 140px"
/>
<Input
v-model:value="recordQuery.userId"
allow-clear
placeholder="用户长ID"
style="width: 180px"
/>
<Button :loading="recordLoading" type="primary" @click="searchClaimRecords">
查询
</Button>
</div>
<Table
:columns="recordColumns"
:data-source="recordPage.records"
:loading="recordLoading"
:pagination="{
current: recordPage.current,
pageSize: recordPage.size,
showSizeChanger: true,
total: recordPage.total,
}"
row-key="recordKey"
:scroll="{ x: 1440 }"
@change="handleClaimTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'user'">
<div class="user-cell">
<div
v-if="record.userAvatar"
class="user-avatar"
:style="avatarBackgroundStyle(record.userAvatar)"
/>
<div class="user-main">
<div class="user-name">{{ record.userNickname || '-' }}</div>
<div class="sub-text">
{{ record.account || record.userId || '-' }}
<span v-if="record.countryCode"> · {{ record.countryCode }}</span>
</div>
</div>
</div>
</template>
<template v-else-if="column.key === 'rechargeAmount'">
{{ record.rechargeAmount || '0.00' }}
</template>
<template v-else-if="column.key === 'reachedLevels'">
<Space wrap>
<Tag
v-for="item in record.reachedLevels || []"
:key="`${record.recordKey}-level-${item.level}`"
color="success"
>
Lv{{ item.level }} / {{ item.rechargeAmount }}
</Tag>
</Space>
</template>
<template v-else-if="column.key === 'claimedRewards'">
<div class="reward-summary">
<div
v-for="item in record.claimedRewards || []"
:key="`${record.recordKey}-reward-${item.level}`"
class="reward-line"
>
Lv{{ item.level }}{{ rewardText(item) }}
</div>
</div>
</template>
</template>
</Table>
</TabPane>
</Tabs>
</Card> </Card>
<ActivityResourceGroupSelectDrawer <ActivityResourceGroupSelectDrawer
@ -442,6 +635,47 @@ watch(
margin-top: 8px; margin-top: 8px;
} }
.user-cell {
align-items: center;
display: flex;
gap: 10px;
min-width: 0;
}
.user-avatar {
background-color: rgb(226 232 240);
background-position: center;
background-size: cover;
border-radius: 50%;
flex: 0 0 auto;
height: 36px;
width: 36px;
}
.user-main {
min-width: 0;
}
.user-name {
color: rgb(15 23 42);
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reward-summary {
display: flex;
flex-direction: column;
gap: 4px;
}
.reward-line {
color: rgb(15 23 42);
font-size: 13px;
line-height: 20px;
}
.actions { .actions {
display: flex; display: flex;
gap: 12px; gap: 12px;