2026-04-27 23:07:50 +08:00

714 lines
20 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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,
Space,
Spin,
Switch,
Tag,
TextArea,
} from 'antdv-next';
import {
getResidentInviteConfig,
saveResidentInviteConfig,
} from '#/api/legacy/resident-activity';
import ActivityResourceGroupSelectDrawer from '#/views/props/components/activity-resource-group-select-drawer.vue';
import { getAllowedSysOrigins } from '#/views/system/shared';
defineOptions({ name: 'ResidentInviteConfigPage' });
function createReward() {
return {
goldAmount: 0,
id: null,
rewardGroupId: null,
rewardGroupName: '',
};
}
function createRule(threshold = 1) {
return {
enabled: true,
goldAmount: 0,
id: null,
rewardGroupId: null,
rewardGroupName: '',
threshold,
};
}
const accessStore = useAccessStore();
const sysOriginOptions = computed(() => {
const options = getAllowedSysOrigins(accessStore.accessCodes || []);
return options.length > 0 ? options : getAllowedSysOrigins([]);
});
const loading = ref(false);
const saving = ref(false);
const groupPickerOpen = ref(false);
const pickerTarget = reactive({
listKey: '',
rowIndex: -1,
});
const form = reactive<Record<string, any>>({
antiCheat: {
sameDeviceOnce: true,
sameIpOnce: true,
},
downloadUrl: '',
enabled: false,
inviteeBindReward: createReward(),
inviteeRechargeRules: [],
inviterBindReward: createReward(),
inviterTotalRechargeRules: [],
inviterValidCountRules: [],
serviceContact: '',
shareDesc: '',
shareTitle: '',
sysOrigin: '',
timezone: 'Asia/Riyadh',
});
const statusMeta = computed(() => (
form.enabled
? { color: 'success', text: '已启用' }
: { color: 'default', text: '已关闭' }
));
const summaryCards = computed(() => [
{
label: '邀请方首邀奖励',
value: formatRewardSummary(form.inviterBindReward),
},
{
label: '被邀请方注册礼包',
value: `${formatRewardSummary(form.inviteeBindReward)} / H5 固定展示 +1000 金币`,
},
{
label: '被邀请人充值阶梯',
value: formatRuleSummary('inviteeRechargeRules'),
},
{
label: '邀请方任务阶梯',
value: [
`人数 ${formatRuleSummary('inviterValidCountRules')}`,
`充值 ${formatRuleSummary('inviterTotalRechargeRules')}`,
].join(' / '),
},
]);
function formatRewardSummary(reward: Record<string, any>) {
const parts: string[] = [];
const goldAmount = Number(reward?.goldAmount || 0);
if (goldAmount > 0) {
parts.push(`${goldAmount} 金币`);
}
if (reward?.rewardGroupName) {
parts.push(String(reward.rewardGroupName));
}
return parts.length > 0 ? parts.join(' + ') : '未配置';
}
function formatRuleSummary(listKey: string) {
const list = (form[listKey] || []) as Array<Record<string, any>>;
if (list.length === 0) {
return '0 个阶梯';
}
const enabledCount = list.filter((item) => item.enabled !== false).length;
return `${enabledCount}/${list.length} 启用`;
}
function normalizeReward(value: null | Record<string, any> | undefined) {
return {
goldAmount: Number(value?.goldAmount || 0),
id: value?.id ?? null,
rewardGroupId: value?.rewardGroupId ?? null,
rewardGroupName: value?.rewardGroupName || '',
};
}
function normalizeRuleList(list: Array<Record<string, any>> | null | undefined) {
return (list || []).map((item) => ({
enabled: item?.enabled !== false,
goldAmount: Number(item?.goldAmount || 0),
id: item?.id ?? null,
rewardGroupId: item?.rewardGroupId ?? null,
rewardGroupName: item?.rewardGroupName || '',
threshold: Number(item?.threshold || 0),
}));
}
function normalizeConfig(result: null | Record<string, any> | undefined) {
const value = result ?? {};
form.sysOrigin = String(value.sysOrigin || form.sysOrigin || sysOriginOptions.value[0]?.value || '');
form.enabled = Boolean(value.enabled);
form.downloadUrl = String(value.downloadUrl || '');
form.shareTitle = String(value.shareTitle || '');
form.shareDesc = String(value.shareDesc || '');
form.serviceContact = String(value.serviceContact || '');
form.timezone = String(value.timezone || 'Asia/Riyadh');
form.antiCheat.sameIpOnce = value?.antiCheat?.sameIpOnce !== false;
form.antiCheat.sameDeviceOnce = value?.antiCheat?.sameDeviceOnce !== false;
form.inviterBindReward = normalizeReward(value.inviterBindReward);
form.inviteeBindReward = normalizeReward(value.inviteeBindReward);
form.inviteeRechargeRules = normalizeRuleList(value.inviteeRechargeRules);
form.inviterValidCountRules = normalizeRuleList(value.inviterValidCountRules);
form.inviterTotalRechargeRules = normalizeRuleList(value.inviterTotalRechargeRules);
}
async function loadData() {
if (!form.sysOrigin) {
return;
}
loading.value = true;
try {
const result = await getResidentInviteConfig(form.sysOrigin);
normalizeConfig(result);
} finally {
loading.value = false;
}
}
watch(
sysOriginOptions,
(options) => {
if (!form.sysOrigin) {
form.sysOrigin = String(options[0]?.value || '');
}
},
{ immediate: true },
);
watch(
() => form.sysOrigin,
(value) => {
if (value) {
void loadData();
}
},
{ immediate: true },
);
function addRule(listKey: string) {
const list = form[listKey] as Array<Record<string, any>>;
const nextThreshold = list.length > 0 ? Number(list[list.length - 1]?.threshold || 0) + 1 : 1;
list.push(createRule(nextThreshold));
}
function normalizeRowIndex(index: number | string) {
return typeof index === 'number' ? index : Number(index || 0);
}
function removeRule(listKey: string, index: number | string) {
(form[listKey] as Array<Record<string, any>>).splice(normalizeRowIndex(index), 1);
}
function openRewardPicker(listKey: string, rowIndex: number | string = -1) {
pickerTarget.listKey = listKey;
pickerTarget.rowIndex = normalizeRowIndex(rowIndex);
groupPickerOpen.value = true;
}
function clearRewardGroup(listKey: string, rowIndex: number | string = -1) {
const normalizedRowIndex = normalizeRowIndex(rowIndex);
const target = normalizedRowIndex >= 0
? (form[listKey] as Array<Record<string, any>>)[normalizedRowIndex]
: form[listKey];
target.rewardGroupId = null;
target.rewardGroupName = '';
}
function handleRewardGroupSelect(row: Record<string, any>) {
const target = pickerTarget.rowIndex >= 0
? (form[pickerTarget.listKey] as Array<Record<string, any>>)[pickerTarget.rowIndex]
: form[pickerTarget.listKey];
target.rewardGroupId = row.id;
target.rewardGroupName = row.name;
groupPickerOpen.value = false;
}
async function submitForm() {
saving.value = true;
try {
await saveResidentInviteConfig({
antiCheat: { ...form.antiCheat },
downloadUrl: String(form.downloadUrl || '').trim(),
enabled: Boolean(form.enabled),
inviteeBindReward: { ...form.inviteeBindReward },
inviteeRechargeRules: (form.inviteeRechargeRules || []).map((item: Record<string, any>) => ({
enabled: Boolean(item.enabled),
goldAmount: Number(item.goldAmount || 0),
id: item.id ?? null,
rewardGroupId: item.rewardGroupId ?? null,
threshold: Number(item.threshold || 0),
})),
inviterBindReward: { ...form.inviterBindReward },
inviterTotalRechargeRules: (form.inviterTotalRechargeRules || []).map((item: Record<string, any>) => ({
enabled: Boolean(item.enabled),
goldAmount: Number(item.goldAmount || 0),
id: item.id ?? null,
rewardGroupId: item.rewardGroupId ?? null,
threshold: Number(item.threshold || 0),
})),
inviterValidCountRules: (form.inviterValidCountRules || []).map((item: Record<string, any>) => ({
enabled: Boolean(item.enabled),
goldAmount: Number(item.goldAmount || 0),
id: item.id ?? null,
rewardGroupId: item.rewardGroupId ?? null,
threshold: Number(item.threshold || 0),
})),
serviceContact: String(form.serviceContact || '').trim(),
shareDesc: String(form.shareDesc || '').trim(),
shareTitle: String(form.shareTitle || '').trim(),
sysOrigin: form.sysOrigin,
timezone: String(form.timezone || '').trim(),
});
message.success('保存成功');
await loadData();
} finally {
saving.value = false;
}
}
function handleSubmit() {
Modal.confirm({
async onOk() {
await submitForm();
},
title: '确认覆盖当前系统的整套邀请活动配置?',
});
}
const rewardSections = [
{ key: 'inviteeRechargeRules', title: '被邀请人充值达标奖励', thresholdLabel: '充值金币阈值' },
{ key: 'inviterValidCountRules', title: '邀请有效人数任务', thresholdLabel: '邀请人数阈值' },
{ key: 'inviterTotalRechargeRules', title: '邀请名下累计充值任务', thresholdLabel: '累计充值金币阈值' },
];
</script>
<template>
<Page auto-content-height title="邀请活动配置">
<div class="invite-config-page">
<Card :bordered="false" class="overview-card">
<div class="toolbar">
<Space align="end" wrap>
<div class="toolbar-item">
<div class="field-label">系统</div>
<SysOriginSelect
v-model:value="form.sysOrigin"
:options="sysOriginOptions"
style="width: 180px"
/>
</div>
<Tag :color="statusMeta.color">
{{ statusMeta.text }}
</Tag>
</Space>
<Button :loading="saving" type="primary" @click="handleSubmit">
保存配置
</Button>
</div>
<div class="summary-grid">
<div
v-for="item in summaryCards"
:key="item.label"
class="summary-item"
>
<div class="summary-label">{{ item.label }}</div>
<div class="summary-value" :title="item.value">{{ item.value }}</div>
</div>
</div>
</Card>
<Spin :spinning="loading">
<div class="content-grid">
<Card :bordered="false" class="config-card basic-card" title="基础设置">
<div class="form-grid">
<div class="field compact-field">
<div class="field-label">活动开关</div>
<Switch v-model:checked="form.enabled" />
</div>
<div class="field">
<div class="field-label">时区</div>
<Input v-model:value="form.timezone" />
</div>
<div class="field field-span-2">
<div class="field-label">下载地址</div>
<Input v-model:value="form.downloadUrl" />
</div>
<div class="field">
<div class="field-label">分享标题</div>
<Input v-model:value="form.shareTitle" />
</div>
<div class="field">
<div class="field-label">客服联系方式</div>
<Input v-model:value="form.serviceContact" />
</div>
<div class="field field-span-2">
<div class="field-label">分享描述</div>
<TextArea
v-model:value="form.shareDesc"
:auto-size="{ minRows: 2, maxRows: 4 }"
/>
</div>
<div class="anti-cheat field-span-2">
<div class="switch-row">
<span class="field-label"> IP 只计一次</span>
<Switch v-model:checked="form.antiCheat.sameIpOnce" />
</div>
<div class="switch-row">
<span class="field-label">同设备只计一次</span>
<Switch v-model:checked="form.antiCheat.sameDeviceOnce" />
</div>
</div>
</div>
</Card>
<div class="reward-card-list">
<Card :bordered="false" class="config-card" title="邀请方首邀奖励">
<div class="reward-row">
<div class="field amount-field">
<div class="field-label">金币</div>
<InputNumber
v-model:value="form.inviterBindReward.goldAmount"
:min="0"
style="width: 100%"
/>
</div>
<div class="field field-grow">
<div class="field-label">奖励组</div>
<div class="reward-group-box">
<Tag v-if="form.inviterBindReward.rewardGroupName" color="blue">
{{ form.inviterBindReward.rewardGroupName }}
</Tag>
<span v-else class="placeholder">未选择奖励组</span>
</div>
</div>
<Space class="reward-actions" wrap>
<Button @click="openRewardPicker('inviterBindReward')">选择奖励组</Button>
<Button @click="clearRewardGroup('inviterBindReward')">清空</Button>
</Space>
</div>
</Card>
<Card :bordered="false" class="config-card" title="被邀请方注册礼包可不填H5 固定展示 +1000 金币)">
<div class="reward-row">
<div class="field amount-field">
<div class="field-label">金币</div>
<InputNumber
v-model:value="form.inviteeBindReward.goldAmount"
:min="0"
style="width: 100%"
/>
</div>
<div class="field field-grow">
<div class="field-label">奖励组</div>
<div class="reward-group-box">
<Tag v-if="form.inviteeBindReward.rewardGroupName" color="blue">
{{ form.inviteeBindReward.rewardGroupName }}
</Tag>
<span v-else class="placeholder">未选择奖励组</span>
</div>
</div>
<Space class="reward-actions" wrap>
<Button @click="openRewardPicker('inviteeBindReward')">选择奖励组</Button>
<Button @click="clearRewardGroup('inviteeBindReward')">清空</Button>
</Space>
</div>
</Card>
</div>
</div>
<div class="rule-section-list">
<Card
v-for="section in rewardSections"
:key="section.key"
:bordered="false"
class="config-card"
:title="section.title"
>
<template #extra>
<Button type="dashed" @click="addRule(section.key)">新增阶梯</Button>
</template>
<div v-if="(form[section.key] || []).length === 0" class="empty-box">
暂无配置
</div>
<div
v-for="(item, index) in form[section.key]"
:key="`${section.key}-${index}`"
class="rule-item"
>
<div class="field amount-field">
<div class="field-label">{{ section.thresholdLabel }}</div>
<InputNumber v-model:value="item.threshold" :min="1" style="width: 100%" />
</div>
<div class="field amount-field">
<div class="field-label">金币</div>
<InputNumber v-model:value="item.goldAmount" :min="0" style="width: 100%" />
</div>
<div class="field field-grow">
<div class="field-label">奖励组</div>
<div class="reward-group-box">
<Tag v-if="item.rewardGroupName" color="blue">
{{ item.rewardGroupName }}
</Tag>
<span v-else class="placeholder">未选择奖励组</span>
</div>
</div>
<div class="field switch-field">
<div class="field-label">启用</div>
<Switch v-model:checked="item.enabled" />
</div>
<Space class="rule-actions" wrap>
<Button @click="openRewardPicker(section.key, index)">选择奖励组</Button>
<Button @click="clearRewardGroup(section.key, index)">清空</Button>
<Button danger @click="removeRule(section.key, index)">删除</Button>
</Space>
</div>
</Card>
</div>
</Spin>
</div>
<ActivityResourceGroupSelectDrawer
:open="groupPickerOpen"
:sys-origin="form.sysOrigin"
@close="groupPickerOpen = false"
@select="handleRewardGroupSelect"
/>
</Page>
</template>
<style scoped>
.invite-config-page {
display: flex;
flex-direction: column;
gap: 16px;
}
.overview-card,
.config-card {
border-radius: 8px;
}
.toolbar {
display: flex;
gap: 16px;
align-items: flex-end;
justify-content: space-between;
margin-bottom: 16px;
}
.toolbar-item {
display: flex;
flex-direction: column;
gap: 8px;
}
.field-label {
color: #475569;
font-size: 13px;
font-weight: 600;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.summary-item {
min-width: 0;
padding: 12px;
background: #f8fafc;
border: 1px solid #eef2f7;
border-radius: 6px;
}
.summary-label {
color: #64748b;
font-size: 12px;
}
.summary-value {
margin-top: 6px;
overflow: hidden;
color: #0f172a;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.content-grid {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(360px, 0.85fr);
gap: 16px;
align-items: start;
margin-top: 16px;
}
.reward-card-list,
.rule-section-list {
display: grid;
gap: 16px;
}
.rule-section-list {
margin-top: 16px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.field {
min-width: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.compact-field {
align-items: flex-start;
}
.field-span-2 {
grid-column: span 2;
}
.anti-cheat {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.switch-row {
min-height: 48px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
background: #f8fafc;
border: 1px solid #eef2f7;
border-radius: 6px;
gap: 12px;
}
.reward-row {
display: grid;
grid-template-columns: 160px minmax(220px, 1fr) auto;
gap: 12px;
align-items: end;
}
.rule-item {
display: grid;
grid-template-columns: 160px 160px minmax(240px, 1fr) 88px auto;
gap: 12px;
align-items: end;
padding: 12px;
background: #fbfdff;
border: 1px solid #eef2f7;
border-radius: 6px;
}
.rule-item + .rule-item {
margin-top: 12px;
}
.amount-field {
width: 160px;
}
.field-grow {
min-width: 0;
}
.switch-field {
width: 88px;
}
.reward-group-box {
min-height: 32px;
display: flex;
align-items: center;
min-width: 0;
padding: 0 11px;
overflow: hidden;
background: #fff;
border: 1px solid #d9d9d9;
border-radius: 6px;
}
.placeholder {
color: #94a3b8;
}
.reward-actions,
.rule-actions {
justify-content: flex-end;
}
.empty-box {
color: #94a3b8;
padding: 12px 0;
}
@media (max-width: 1280px) {
.content-grid {
grid-template-columns: 1fr;
}
.rule-item {
grid-template-columns: 160px 160px minmax(240px, 1fr);
}
.switch-field,
.rule-actions {
width: auto;
grid-column: span 1;
}
}
@media (max-width: 900px) {
.toolbar {
align-items: stretch;
flex-direction: column;
}
.summary-grid,
.form-grid,
.anti-cheat,
.reward-row,
.rule-item {
grid-template-columns: 1fr;
}
.field-span-2 {
grid-column: span 1;
}
.amount-field,
.switch-field {
width: 100%;
}
}
</style>