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>) {
return requestClient.get<LegacyPageResult<Record<string, any>>>(
`${SPECIFIED_GIFT_WEEKLY_RANK_BASE}/config/page`,

View File

@ -16,11 +16,14 @@ import {
Spin,
Switch,
Table,
Tabs,
TabPane,
Tag,
} from 'antdv-next';
import {
getResidentRechargeRewardConfig,
pageResidentRechargeRewardClaimRecords,
saveResidentRechargeRewardConfig,
} from '#/api/legacy/resident-activity';
import ActivityResourceGroupSelectDrawer from '#/views/props/components/activity-resource-group-select-drawer.vue';
@ -66,7 +69,9 @@ const sysOriginOptions = computed(() => {
});
const selectedSysOrigin = ref('');
const activeTab = ref('config');
const loading = ref(false);
const recordLoading = ref(false);
const saving = ref(false);
const groupPickerOpen = ref(false);
const pickerLevelIndex = ref(-1);
@ -104,6 +109,31 @@ const statusMeta = computed(() => {
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) {
const numeric = Number(value || 0);
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() {
const levels = form.levelConfigs as Array<Record<string, any>>;
const maxLevel = Math.max(0, ...levels.map((item) => Number(item.level || 0)));
@ -273,11 +332,55 @@ async function submitForm() {
});
message.success('保存成功');
await loadConfig();
if (activeTab.value === 'claims') {
await loadClaimRecords();
}
} finally {
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(
sysOriginOptions,
(options) => {
@ -292,11 +395,19 @@ watch(
selectedSysOrigin,
(value) => {
if (value) {
recordQuery.cursor = 1;
void loadConfig();
void loadClaimRecords();
}
},
{ immediate: true },
);
watch(activeTab, (value) => {
if (value === 'claims') {
void loadClaimRecords();
}
});
</script>
<template>
@ -318,85 +429,167 @@ watch(
<Button :loading="loading" @click="loadConfig">刷新</Button>
</div>
<div class="config-row">
<Space align="center" wrap>
<span class="field-label">启用</span>
<Switch v-model:checked="form.enabled" />
<span class="field-label">统计时区</span>
<Input v-model:value="form.timezone" style="width: 180px" />
<span class="sub-text">更新时间{{ form.updateTime || '-' }}</span>
</Space>
</div>
<Tabs v-model:activeKey="activeTab">
<TabPane key="config" tab="奖励配置">
<div class="config-row">
<Space align="center" wrap>
<span class="field-label">启用</span>
<Switch v-model:checked="form.enabled" />
<span class="field-label">统计时区</span>
<Input v-model:value="form.timezone" style="width: 180px" />
<span class="sub-text">更新时间{{ form.updateTime || '-' }}</span>
</Space>
</div>
<Table
:columns="levelColumns"
:data-source="form.levelConfigs"
:loading="loading"
:pagination="false"
row-key="rowKey"
:scroll="{ x: 1020 }"
>
<template #bodyCell="{ column, index, record }">
<template v-if="column.key === 'enabled'">
<Switch v-model:checked="record.enabled" />
</template>
<template v-else-if="column.key === 'level'">
<InputNumber
v-model:value="record.level"
:min="1"
:precision="0"
style="width: 90px"
/>
</template>
<template v-else-if="column.key === 'rechargeAmount'">
<InputNumber
v-model:value="record.rechargeAmount"
:min="0.01"
:precision="2"
<Table
:columns="levelColumns"
:data-source="form.levelConfigs"
:loading="loading"
:pagination="false"
row-key="rowKey"
:scroll="{ x: 1020 }"
>
<template #bodyCell="{ column, index, record }">
<template v-if="column.key === 'enabled'">
<Switch v-model:checked="record.enabled" />
</template>
<template v-else-if="column.key === 'level'">
<InputNumber
v-model:value="record.level"
:min="1"
:precision="0"
style="width: 90px"
/>
</template>
<template v-else-if="column.key === 'rechargeAmount'">
<InputNumber
v-model:value="record.rechargeAmount"
:min="0.01"
:precision="2"
style="width: 140px"
/>
</template>
<template v-else-if="column.key === 'rewardGold'">
<InputNumber
v-model:value="record.rewardGold"
:min="0"
:precision="0"
style="width: 140px"
/>
</template>
<template v-else-if="column.key === 'rewardGroup'">
<div class="reward-group-cell">
<Space wrap>
<Button size="small" @click="openRewardPicker(index)">
{{ record.rewardGroupId ? '更换奖励组' : '选择奖励组' }}
</Button>
<Button
v-if="record.rewardGroupId"
danger
size="small"
type="link"
@click="clearRewardGroup(record)"
>
清空
</Button>
</Space>
<div v-if="record.rewardGroupId" class="reward-group-name">
{{ record.rewardGroupName || '-' }} (ID: {{ record.rewardGroupId }})
</div>
<div v-else class="sub-text">未选择奖励组</div>
<RewardRow v-if="record.rewardItems?.length" :list="record.rewardItems" />
</div>
</template>
<template v-else-if="column.key === 'actions'">
<Button danger type="link" @click="confirmRemoveLevel(index)">删除</Button>
</template>
</template>
</Table>
<div class="actions">
<Button @click="addLevel">新增档位</Button>
<Button :loading="saving" type="primary" @click="submitForm">保存配置</Button>
</div>
</TabPane>
<TabPane key="claims" tab="已领取人员">
<div class="toolbar">
<Input
v-model:value="recordQuery.month"
allow-clear
placeholder="YYYY-MM"
style="width: 140px"
/>
</template>
<template v-else-if="column.key === 'rewardGold'">
<InputNumber
v-model:value="record.rewardGold"
:min="0"
:precision="0"
style="width: 140px"
<Input
v-model:value="recordQuery.userId"
allow-clear
placeholder="用户长ID"
style="width: 180px"
/>
</template>
<template v-else-if="column.key === 'rewardGroup'">
<div class="reward-group-cell">
<Space wrap>
<Button size="small" @click="openRewardPicker(index)">
{{ record.rewardGroupId ? '更换奖励组' : '选择奖励组' }}
</Button>
<Button
v-if="record.rewardGroupId"
danger
size="small"
type="link"
@click="clearRewardGroup(record)"
>
清空
</Button>
</Space>
<div v-if="record.rewardGroupId" class="reward-group-name">
{{ record.rewardGroupName || '-' }} (ID: {{ record.rewardGroupId }})
</div>
<div v-else class="sub-text">未选择奖励组</div>
<RewardRow v-if="record.rewardItems?.length" :list="record.rewardItems" />
</div>
</template>
<template v-else-if="column.key === 'actions'">
<Button danger type="link" @click="confirmRemoveLevel(index)">删除</Button>
</template>
</template>
</Table>
<div class="actions">
<Button @click="addLevel">新增档位</Button>
<Button :loading="saving" type="primary" @click="submitForm">保存配置</Button>
</div>
<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>
<ActivityResourceGroupSelectDrawer
@ -442,6 +635,47 @@ watch(
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 {
display: flex;
gap: 12px;