增加首冲奖励

This commit is contained in:
zhx 2026-05-21 20:06:28 +08:00
parent 1db5607cb8
commit bc04695ac8
7 changed files with 985 additions and 7 deletions

View File

@ -9,6 +9,8 @@ const ROOM_TURNOVER_REWARD_BASE =
'/go/resident-activity/room-turnover-reward';
const VIP_BASE = '/go/resident-activity/vip';
const RECHARGE_REWARD_BASE = '/go/resident-activity/recharge-reward';
const FIRST_RECHARGE_REWARD_BASE =
'/go/resident-activity/first-recharge-reward';
const TASK_CENTER_BASE = '/go/resident-activity/task-center';
const VOICE_ROOM_RED_PACKET_BASE = '/go/resident-activity/voice-room-red-packet';
const VOICE_ROOM_ROCKET_BASE = '/go/resident-activity/voice-room-rocket';
@ -175,6 +177,31 @@ export async function pageResidentRechargeRewardClaimRecords(
);
}
export async function getResidentFirstRechargeRewardConfig(sysOrigin: string) {
return requestClient.get<Record<string, any>>(
`${FIRST_RECHARGE_REWARD_BASE}/config`,
{ params: { sysOrigin } },
);
}
export async function saveResidentFirstRechargeRewardConfig(
data: Record<string, any>,
) {
return requestClient.post<Record<string, any>>(
`${FIRST_RECHARGE_REWARD_BASE}/config/save`,
data,
);
}
export async function pageResidentFirstRechargeRewardGrantRecords(
params: Record<string, any>,
) {
return requestClient.get<Record<string, any>>(
`${FIRST_RECHARGE_REWARD_BASE}/grant-record/page`,
{ params },
);
}
export async function getResidentDailyTaskConfig(
sysOrigin: string,
taskCategory = 'DAILY',

View File

@ -42,6 +42,13 @@ const routes: RouteRecordRaw[] = [
component: () => import('#/views/resident-activity/recharge-reward-config.vue'),
meta: { title: '充值奖励配置' },
},
{
name: 'ResidentFirstRechargeRewardConfig',
path: 'first-recharge-reward/config',
component: () =>
import('#/views/resident-activity/first-recharge-reward-config.vue'),
meta: { title: '首冲奖励' },
},
{
name: 'ResidentDailyTaskConfig',
path: 'daily-task/config',

View File

@ -9,6 +9,7 @@ import { useAccessStore } from '@vben/stores';
import {
Button,
Card,
Image,
Input,
InputNumber,
message,
@ -26,6 +27,11 @@ import {
pageAppHomePopups,
saveAppHomePopup,
} from '#/api/legacy/app-system';
import {
getAccessImgUrl,
OSS_FILE_BUCKETS,
simpleUploadFile,
} from '#/api/legacy/oss';
import InlineFilterField from '#/views/_shared/inline-filter-field.vue';
import InlineFilterToolbar from '#/views/_shared/inline-filter-toolbar.vue';
import { getAllowedSysOrigins } from '#/views/system/shared';
@ -61,8 +67,10 @@ const sysOriginOptions = computed(() => {
const loading = ref(false);
const saving = ref(false);
const imageUploading = ref(false);
const formOpen = ref(false);
const formTitle = ref('新增弹窗');
const imageInputRef = ref<HTMLInputElement | null>(null);
const list = ref<AppHomePopupItem[]>([]);
const total = ref(0);
@ -148,6 +156,31 @@ function handleEdit(record: AppHomePopupItem) {
formOpen.value = true;
}
function pickImage() {
imageInputRef.value?.click();
}
async function handleImageUpload(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) {
return;
}
imageUploading.value = true;
try {
const result = await simpleUploadFile(file, OSS_FILE_BUCKETS.other);
form.image = getAccessImgUrl(result.name);
message.success('上传成功');
} finally {
imageUploading.value = false;
target.value = '';
}
}
function clearImage() {
form.image = '';
}
function validateForm() {
if (!String(form.popupKey || '').trim()) {
message.warning('请输入弹窗 key');
@ -340,7 +373,28 @@ watch(
</div>
<div class="form-item">
<label>图片</label>
<Input v-model:value="form.image" placeholder="https://..." />
<input
ref="imageInputRef"
accept="image/*"
class="hidden-input"
type="file"
@change="handleImageUpload"
/>
<div class="upload-row">
<Image
v-if="form.image"
:preview="false"
:src="form.image"
class="image-preview"
/>
<div v-else class="image-empty">未上传</div>
<Space>
<Button :loading="imageUploading" @click="pickImage">
{{ form.image ? '重新上传' : '上传图片' }}
</Button>
<Button v-if="form.image" @click="clearImage">清除</Button>
</Space>
</div>
</div>
<div class="form-item">
<label>跳转类型</label>
@ -403,5 +457,34 @@ watch(
color: hsl(var(--muted-foreground));
font-size: 13px;
}
</style>
.upload-row {
align-items: center;
display: flex;
gap: 12px;
}
.image-preview,
.image-empty {
border-radius: 8px;
height: 96px;
width: 160px;
}
.image-preview {
object-fit: cover;
}
.image-empty {
align-items: center;
background: hsl(var(--muted));
color: hsl(var(--muted-foreground));
display: flex;
font-size: 13px;
justify-content: center;
}
.hidden-input {
display: none;
}
</style>

View File

@ -49,7 +49,7 @@ const emit = defineEmits<{
success: [];
}>();
type ResourceKind = 'badge' | 'gift' | 'props' | 'vip';
type ResourceKind = 'badge' | 'currency' | 'gift' | 'props' | 'vip';
type TypeOption = {
icon: string;
@ -93,6 +93,7 @@ const sourceLoading = ref(false);
const resourceKeyword = ref('');
const currentResourcePage = ref(1);
const selectedType = ref('');
const goldAmount = ref<null | number>(null);
const form = reactive<{
id: number | string;
@ -121,6 +122,12 @@ const typeOptions = computed<TypeOption[]>(() => [
label: item.name,
value: String(item.value),
})),
{
icon: 'lucide:coins',
kind: 'currency',
label: '金币',
value: 'GOLD',
},
{
icon: 'lucide:award',
kind: 'badge',
@ -302,6 +309,9 @@ function getResourceKey(kind: ResourceKind, detailType: string, id: number | str
}
function getRewardKind(item: RewardGroupItem): ResourceKind {
if (item.type === 'GOLD') {
return 'currency';
}
if (item.type === 'GIFT') {
return 'gift';
}
@ -338,6 +348,9 @@ function getItemName(item: RewardGroupItem) {
}
function getQuantityLabel(item: RewardGroupItem) {
if (item.type === 'GOLD') {
return '金币';
}
if (item.type === 'GIFT') {
return '数量';
}
@ -348,6 +361,9 @@ function getQuantityLabel(item: RewardGroupItem) {
}
function getDefaultQuantity(resource: DisplayResource) {
if (resource.kind === 'currency') {
return 0;
}
if (resource.kind === 'gift') {
return 1;
}
@ -361,6 +377,9 @@ function getDefaultQuantity(resource: DisplayResource) {
}
function getRewardType(resource: DisplayResource) {
if (resource.kind === 'currency') {
return resource.type;
}
if (resource.kind === 'gift') {
return 'GIFT';
}
@ -396,6 +415,7 @@ function selectType(option: TypeOption) {
selectedType.value = option.value;
resourceKeyword.value = '';
currentResourcePage.value = 1;
goldAmount.value = null;
void loadOptions(option.value);
}
@ -403,6 +423,9 @@ async function loadOptions(type: string) {
if (!type || !form.sysOrigin) {
return;
}
if (type === 'GOLD') {
return;
}
const store = createTypeStore(type);
if (store.loaded || store.loading) {
return;
@ -493,6 +516,33 @@ function toggleResource(resource: DisplayResource) {
form.rewardConfigList.push(buildRewardItem(resource));
}
function addGoldItem() {
const amount = Number(goldAmount.value);
if (!Number.isFinite(amount) || amount <= 0) {
message.warning('请输入金币数量');
return;
}
const item: RewardGroupItem = {
amount,
content: amount,
detailType: 'GOLD',
name: '金币',
quantity: 0,
remark: '金币',
type: 'GOLD',
typeLabel: '金币',
};
const index = form.rewardConfigList.findIndex(
(reward) => reward.type === 'GOLD' && reward.detailType === 'GOLD',
);
if (index !== -1) {
form.rewardConfigList[index] = item;
} else {
form.rewardConfigList.push(item);
}
goldAmount.value = null;
}
function removeItem(index: number) {
form.rewardConfigList.splice(index, 1);
}
@ -522,9 +572,21 @@ function updateItemQuantity(index: number, value: number | string | null) {
if (!item) {
return;
}
if (item.type === 'GOLD') {
item.content = value ?? 0;
item.amount = value ?? 0;
return;
}
item.quantity = value ?? 0;
}
function getItemInputValue(item: RewardGroupItem) {
if (item.type === 'GOLD') {
return Number(item.content || 0);
}
return Number(item.quantity || 0);
}
function validateItems() {
for (const item of form.rewardConfigList) {
const quantity = Number(item.quantity);
@ -532,6 +594,14 @@ function validateItems() {
message.warning(`${getItemName(item)}${getQuantityLabel(item)}不能为空`);
return false;
}
if (item.type === 'GOLD') {
const amount = Number(item.content);
if (!Number.isFinite(amount) || amount <= 0) {
message.warning('金币数量必须大于 0');
return false;
}
continue;
}
if (item.type === 'GIFT' && quantity <= 0) {
message.warning(`${getItemName(item)} 的数量必须大于 0`);
return false;
@ -693,7 +763,22 @@ watch(filteredResources, () => {
</button>
</div>
<div class="resource-section">
<div v-if="selectedType === 'GOLD'" class="gold-editor">
<div class="gold-editor__title">添加金币</div>
<div class="gold-editor__row">
<InputNumber
v-model:value="goldAmount"
:controls="false"
:min="1"
:precision="0"
class="gold-editor__input"
placeholder="输入金币数量"
/>
<Button type="primary" @click="addGoldItem">加入资源组</Button>
</div>
</div>
<div v-else class="resource-section">
<div class="resource-head">
<div class="field-head">
<span>资源列表</span>
@ -804,7 +889,7 @@ watch(filteredResources, () => {
<InputNumber
:controls="false"
:precision="0"
:value="Number(item.quantity || 0)"
:value="getItemInputValue(item)"
class="quantity-input"
@change="(value: number | string | null) => updateItemQuantity(Number(index), value)"
/>
@ -965,6 +1050,35 @@ watch(filteredResources, () => {
gap: 8px;
}
.gold-editor {
align-content: start;
background: #fbfcfe;
border: 1px solid #edf0f3;
border-radius: 8px;
display: grid;
gap: 10px;
min-height: 420px;
padding: 14px;
}
.gold-editor__title {
color: #111827;
font-size: 15px;
font-weight: 700;
}
.gold-editor__row {
align-items: center;
display: grid;
gap: 8px;
grid-template-columns: minmax(240px, 360px) auto;
justify-content: start;
}
.gold-editor__input {
width: 100%;
}
.field-head,
.resource-head,
.selected-panel__head {

View File

@ -0,0 +1,640 @@
<script lang="ts" setup>
import { computed, reactive, ref, watch } from 'vue';
import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores';
import {
Button,
Card,
Input,
InputNumber,
message,
Modal,
Select,
Space,
Spin,
Switch,
Table,
Tabs,
TabPane,
Tag,
} from 'antdv-next';
import {
getResidentFirstRechargeRewardConfig,
pageResidentFirstRechargeRewardGrantRecords,
saveResidentFirstRechargeRewardConfig,
} from '#/api/legacy/resident-activity';
import ActivityResourceGroupSelectDrawer from '#/views/props/components/activity-resource-group-select-drawer.vue';
import RewardRow from '#/views/props/components/reward-row.vue';
import { getAllowedSysOrigins } from '#/views/system/shared';
defineOptions({ name: 'ResidentFirstRechargeRewardConfigPage' });
let localRowId = 0;
function createRowKey(seed?: number | string) {
localRowId += 1;
return `first-recharge-reward-${seed || localRowId}-${localRowId}`;
}
function createLevel(level = 1, rechargeAmount = 10) {
return {
enabled: true,
id: null,
level,
rechargeAmount,
rechargeAmountCents: 0,
rewardGroupId: null,
rewardGroupName: '',
rewardItems: [] as Array<Record<string, any>>,
rowKey: createRowKey(),
};
}
function defaultLevels() {
return [createLevel(1, 10), createLevel(2, 50), createLevel(3, 100)];
}
const accessStore = useAccessStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
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);
const form = reactive<Record<string, any>>({
configured: false,
enabled: false,
id: 0,
levelConfigs: defaultLevels(),
sysOrigin: '',
updateTime: '',
});
const levelColumns = [
{ dataIndex: 'enabled', key: 'enabled', title: '启用', width: 90 },
{ dataIndex: 'level', key: 'level', title: '档位', width: 110 },
{ dataIndex: 'rechargeAmount', key: 'rechargeAmount', title: '首冲金额', width: 180 },
{ dataIndex: 'rewardGroup', key: 'rewardGroup', title: '奖励资源组', width: 420 },
{ dataIndex: 'actions', key: 'actions', title: '操作', width: 100 },
];
const enabledLevelCount = computed(() =>
(form.levelConfigs || []).filter((item: Record<string, any>) => item.enabled !== false).length,
);
const statusMeta = computed(() => {
if (!form.configured) {
return { color: 'processing', text: '未配置' };
}
if (form.enabled) {
return { color: 'success', text: '已启用' };
}
return { color: 'default', text: '已关闭' };
});
const recordQuery = reactive<Record<string, any>>({
cursor: 1,
limit: 20,
status: '',
userId: '',
});
const recordPage = reactive<Record<string, any>>({
current: 1,
records: [],
size: 20,
total: 0,
});
const statusOptions = [
{ label: '全部状态', value: '' },
{ label: '发放中', value: 'PENDING' },
{ label: '已发放', value: 'SUCCESS' },
{ label: '发放失败', value: 'FAILED' },
];
const recordColumns = [
{ dataIndex: 'user', key: 'user', title: '用户', width: 260 },
{ dataIndex: 'rechargeAmount', key: 'rechargeAmount', title: '充值金额', width: 120 },
{ dataIndex: 'rechargeThreshold', key: 'rechargeThreshold', title: '达标金额', width: 120 },
{ dataIndex: 'level', key: 'level', title: '档位', width: 90 },
{ dataIndex: 'payPlatform', key: 'payPlatform', title: '充值方式', width: 180 },
{ dataIndex: 'sourceOrderId', key: 'sourceOrderId', title: '订单', width: 180 },
{ dataIndex: 'rewardGroup', key: 'rewardGroup', title: '奖励资源组', width: 360 },
{ dataIndex: 'status', key: 'status', title: '发放状态', width: 120 },
{ dataIndex: 'sentAt', key: 'sentAt', title: '发放时间', width: 180 },
{ dataIndex: 'lastError', key: 'lastError', title: '失败原因', width: 220 },
];
function toAmountCents(value: number | string | undefined) {
const numeric = Number(value || 0);
if (!Number.isFinite(numeric)) {
return 0;
}
return Math.round(numeric * 100);
}
function formatAmount(value: number | string | undefined) {
const numeric = Number(value || 0);
return Number.isFinite(numeric) ? numeric.toFixed(2) : '0.00';
}
function normalizeConfig(result: null | Record<string, any> | undefined) {
const value = result || {};
form.configured = Boolean(value.configured);
form.enabled = Boolean(value.enabled);
form.id = value.id || 0;
form.sysOrigin = String(value.sysOrigin || selectedSysOrigin.value || '');
form.updateTime = String(value.updateTime || '');
const levels = Array.isArray(value.levelConfigs) && value.levelConfigs.length > 0
? value.levelConfigs
: defaultLevels();
form.levelConfigs = levels
.map((item: Record<string, any>, index: number) => {
const rechargeAmount = Number(
item.rechargeAmount ?? Number(item.rechargeAmountCents || 0) / 100,
);
return {
enabled: item.enabled !== false,
id: item.id ?? null,
level: Number(item.level || index + 1),
rechargeAmount: Number.isFinite(rechargeAmount) ? rechargeAmount : 0,
rechargeAmountCents: Number(item.rechargeAmountCents || 0),
rewardGroupId: item.rewardGroupId ?? null,
rewardGroupName: String(item.rewardGroupName || ''),
rewardItems: Array.isArray(item.rewardItems) ? item.rewardItems : [],
rowKey: createRowKey(item.id || index),
};
})
.toSorted((left: Record<string, any>, right: Record<string, any>) =>
toAmountCents(left.rechargeAmount) - toAmountCents(right.rechargeAmount),
);
}
async function loadConfig() {
if (!selectedSysOrigin.value) {
return;
}
loading.value = true;
try {
const result = await getResidentFirstRechargeRewardConfig(selectedSysOrigin.value);
normalizeConfig(result);
} finally {
loading.value = false;
}
}
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);
}
async function loadGrantRecords() {
if (!selectedSysOrigin.value) {
return;
}
recordLoading.value = true;
try {
const result = await pageResidentFirstRechargeRewardGrantRecords({
...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)));
const maxAmount = Math.max(0, ...levels.map((item) => Number(item.rechargeAmount || 0)));
levels.push(createLevel(maxLevel + 1, maxAmount + 10));
}
function removeLevel(index: number) {
(form.levelConfigs as Array<Record<string, any>>).splice(index, 1);
}
function confirmRemoveLevel(index: number) {
Modal.confirm({
onOk: () => removeLevel(index),
title: '确认删除该首冲奖励档位?',
});
}
function openRewardPicker(index: number) {
pickerLevelIndex.value = index;
groupPickerOpen.value = true;
}
function clearRewardGroup(record: Record<string, any>) {
record.rewardGroupId = null;
record.rewardGroupName = '';
record.rewardItems = [];
}
function handleRewardGroupSelect(row: Record<string, any>) {
const levels = form.levelConfigs as Array<Record<string, any>>;
const target = levels[pickerLevelIndex.value];
if (!target) {
groupPickerOpen.value = false;
return;
}
target.rewardGroupId = row.id;
target.rewardGroupName = row.name;
target.rewardItems = Array.isArray(row.rewardConfigList) ? row.rewardConfigList : [];
groupPickerOpen.value = false;
}
function validateBeforeSave() {
const levels = form.levelConfigs as Array<Record<string, any>>;
if (levels.length === 0) {
message.warning('请至少配置一个首冲奖励档位');
return false;
}
const seenLevels = new Set<string>();
const seenAmounts = new Set<string>();
for (const item of levels) {
const level = Number(item.level || 0);
const amountCents = toAmountCents(item.rechargeAmount);
const rewardGroupId = Number(item.rewardGroupId || 0);
if (level <= 0) {
message.warning('档位必须大于 0');
return false;
}
if (amountCents <= 0) {
message.warning('首冲金额必须大于 0');
return false;
}
if (rewardGroupId <= 0) {
message.warning('每个档位都需要配置奖励资源组');
return false;
}
if (seenLevels.has(String(level))) {
message.warning('档位不能重复');
return false;
}
if (seenAmounts.has(String(amountCents))) {
message.warning('首冲金额不能重复');
return false;
}
seenLevels.add(String(level));
seenAmounts.add(String(amountCents));
}
if (form.enabled && enabledLevelCount.value === 0) {
message.warning('启用配置时至少要启用一个首冲奖励档位');
return false;
}
return true;
}
async function submitForm() {
if (!validateBeforeSave()) {
return;
}
saving.value = true;
try {
await saveResidentFirstRechargeRewardConfig({
enabled: Boolean(form.enabled),
id: form.id || 0,
levelConfigs: (form.levelConfigs || []).map((item: Record<string, any>) => ({
enabled: item.enabled !== false,
id: item.id || 0,
level: Number(item.level || 0),
rechargeAmount: formatAmount(item.rechargeAmount),
rechargeAmountCents: toAmountCents(item.rechargeAmount),
rewardGroupId: item.rewardGroupId ?? null,
rewardGroupName: String(item.rewardGroupName || ''),
})),
sysOrigin: selectedSysOrigin.value,
});
message.success('保存成功');
await loadConfig();
if (activeTab.value === 'records') {
await loadGrantRecords();
}
} finally {
saving.value = false;
}
}
function searchGrantRecords() {
recordQuery.cursor = 1;
void loadGrantRecords();
}
function handleGrantTableChange(pagination: Record<string, any>) {
recordQuery.cursor = Number(pagination.current || 1);
recordQuery.limit = Number(pagination.pageSize || 20);
void loadGrantRecords();
}
function avatarBackgroundStyle(url: string) {
return {
backgroundImage: `url("${String(url || '').replaceAll('"', '%22')}")`,
};
}
function grantStatusMeta(status: string) {
if (status === 'SUCCESS') {
return { color: 'success', text: '已发放' };
}
if (status === 'FAILED') {
return { color: 'error', text: '发放失败' };
}
return { color: 'processing', text: '发放中' };
}
watch(
sysOriginOptions,
(options) => {
if (!selectedSysOrigin.value) {
selectedSysOrigin.value = String(options[0]?.value || '');
}
},
{ immediate: true },
);
watch(
selectedSysOrigin,
(value) => {
if (value) {
recordQuery.cursor = 1;
void loadConfig();
void loadGrantRecords();
}
},
{ immediate: true },
);
watch(activeTab, (value) => {
if (value === 'records') {
void loadGrantRecords();
}
});
</script>
<template>
<Page auto-content-height>
<Spin :spinning="loading">
<Card :bordered="false" title="首冲奖励">
<div class="toolbar">
<Select
v-model:value="selectedSysOrigin"
:options="sysOriginOptions"
option-filter-prop="label"
option-label-prop="label"
placeholder="请选择系统"
show-search
style="width: 180px"
/>
<Tag :color="statusMeta.color">{{ statusMeta.text }}</Tag>
<Tag color="processing"> Google / 三方充值</Tag>
<Button :loading="loading" @click="loadConfig">刷新</Button>
</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="sub-text">更新时间{{ form.updateTime || '-' }}</span>
</Space>
</div>
<Table
:columns="levelColumns"
:data-source="form.levelConfigs"
:loading="loading"
:pagination="false"
row-key="rowKey"
:scroll="{ x: 900 }"
>
<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 === '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="records" tab="发奖记录">
<div class="toolbar">
<Input
v-model:value="recordQuery.userId"
allow-clear
placeholder="用户长ID"
style="width: 180px"
/>
<Select
v-model:value="recordQuery.status"
:options="statusOptions"
style="width: 140px"
/>
<Button :loading="recordLoading" type="primary" @click="searchGrantRecords">
查询
</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="id"
:scroll="{ x: 1810 }"
@change="handleGrantTableChange"
>
<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 === 'payPlatform'">
<div>{{ record.payPlatform || '-' }}</div>
<div class="sub-text">{{ record.paymentMethod || '-' }}</div>
</template>
<template v-else-if="column.key === 'rewardGroup'">
<div class="reward-group-name">
{{ record.rewardGroupName || '-' }} (ID: {{ record.rewardGroupId || '-' }})
</div>
<RewardRow v-if="record.rewardItems?.length" :list="record.rewardItems" />
</template>
<template v-else-if="column.key === 'status'">
<Tag :color="grantStatusMeta(record.status).color">
{{ grantStatusMeta(record.status).text }}
</Tag>
</template>
<template v-else-if="column.key === 'sentAt'">
{{ record.sentAt || '-' }}
</template>
<template v-else-if="column.key === 'lastError'">
<span class="error-text">{{ record.lastError || '-' }}</span>
</template>
</template>
</Table>
</TabPane>
</Tabs>
</Card>
<ActivityResourceGroupSelectDrawer
:open="groupPickerOpen"
:sys-origin="selectedSysOrigin"
@close="groupPickerOpen = false"
@select="handleRewardGroupSelect"
/>
</Spin>
</Page>
</template>
<style scoped>
.toolbar {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.config-row {
margin-bottom: 16px;
}
.field-label {
color: rgb(71 85 105);
font-size: 13px;
}
.sub-text {
color: rgb(100 116 139);
font-size: 12px;
}
.reward-group-cell {
min-width: 0;
}
.reward-group-name {
color: rgb(15 23 42);
font-size: 13px;
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;
}
.error-text {
color: rgb(185 28 28);
}
.actions {
display: flex;
gap: 12px;
justify-content: flex-end;
margin-top: 16px;
}
</style>

View File

@ -0,0 +1,54 @@
SET NAMES utf8mb4;
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000264,
(SELECT id FROM sys_menu WHERE alias = 'ResidentActivityManager' LIMIT 1),
'首冲奖励',
'resident-activity/first-recharge-reward-config/index',
2,
'form',
'0',
'0',
8,
0,
'first-recharge-reward/config',
'ResidentFirstRechargeRewardConfig'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'ResidentFirstRechargeRewardConfig'
);
UPDATE sys_menu
SET
parent_id = (
SELECT resident_menu.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'ResidentActivityManager'
LIMIT 1
) AS resident_menu
),
menu_name = '首冲奖励',
path = 'resident-activity/first-recharge-reward-config/index',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 8,
status = 0,
router = 'first-recharge-reward/config'
WHERE alias = 'ResidentFirstRechargeRewardConfig';

View File

@ -416,6 +416,59 @@ SET
router = 'recharge-reward/config'
WHERE alias = 'ResidentRechargeRewardConfig';
INSERT INTO sys_menu (
id,
parent_id,
menu_name,
path,
menu_type,
icon,
create_user,
update_user,
sort,
status,
router,
alias
)
SELECT
1700000264,
(SELECT id FROM sys_menu WHERE alias = 'ResidentActivityManager' LIMIT 1),
'首冲奖励',
'resident-activity/first-recharge-reward-config/index',
2,
'form',
'0',
'0',
8,
0,
'first-recharge-reward/config',
'ResidentFirstRechargeRewardConfig'
WHERE NOT EXISTS (
SELECT 1 FROM sys_menu WHERE alias = 'ResidentFirstRechargeRewardConfig'
);
UPDATE sys_menu
SET
parent_id = (
SELECT resident_menu.id
FROM (
SELECT id
FROM sys_menu
WHERE alias = 'ResidentActivityManager'
LIMIT 1
) AS resident_menu
),
menu_name = '首冲奖励',
path = 'resident-activity/first-recharge-reward-config/index',
menu_type = 2,
icon = 'form',
create_user = '0',
update_user = '0',
sort = 8,
status = 0,
router = 'first-recharge-reward/config'
WHERE alias = 'ResidentFirstRechargeRewardConfig';
INSERT INTO sys_menu (
id,
parent_id,
@ -439,7 +492,7 @@ SELECT
'form',
'0',
'0',
8,
9,
0,
'daily-task/config',
'ResidentDailyTaskConfig'
@ -464,7 +517,7 @@ SET
icon = 'form',
create_user = '0',
update_user = '0',
sort = 8,
sort = 9,
status = 0,
router = 'daily-task/config'
WHERE alias = 'ResidentDailyTaskConfig';