feat: unify invite flow with referral ref
This commit is contained in:
@@ -2,7 +2,7 @@ import { renderFaIcon, schemaPage, customPage } from './helpers'
|
||||
|
||||
/**
|
||||
* Users SPA keeps config-shaped pages.
|
||||
* Message / invite / ban / report queues live under users.php native menus.
|
||||
* Message / referral-code / ban / report queues live under users.php native menus.
|
||||
*/
|
||||
export const usersSection = {
|
||||
key: 'users',
|
||||
|
||||
@@ -68,22 +68,25 @@
|
||||
</n-card>
|
||||
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header"><h3>邀请奖励</h3></div>
|
||||
<n-form :model="cfg.invite_reward" label-placement="left" label-width="200" class="aksrt-form">
|
||||
<div class="aksrt-section-header"><h3>Ref 推广奖励</h3></div>
|
||||
<n-form :model="cfg.referral_reward" label-placement="left" label-width="200" class="aksrt-form">
|
||||
<n-form-item label="启用">
|
||||
<n-switch v-model:value="cfg.invite_reward.enabled" />
|
||||
<n-switch v-model:value="cfg.referral_reward.enabled" />
|
||||
</n-form-item>
|
||||
<n-form-item label="邀请人奖励">
|
||||
<n-input-number v-model:value="cfg.invite_reward.inviter_amount" :min="0" :max="10000" />
|
||||
<n-form-item label="推广人奖励">
|
||||
<n-input-number v-model:value="cfg.referral_reward.promoter_amount" :min="0" :max="10000" />
|
||||
</n-form-item>
|
||||
<n-form-item label="被邀请人奖励">
|
||||
<n-input-number v-model:value="cfg.invite_reward.invitee_amount" :min="0" :max="10000" />
|
||||
<n-form-item label="新用户奖励">
|
||||
<n-input-number v-model:value="cfg.referral_reward.referred_amount" :min="0" :max="10000" />
|
||||
</n-form-item>
|
||||
<n-form-item label="推广等级积分">
|
||||
<n-input-number v-model:value="cfg.referral_reward.level_points" :min="0" :max="10000" />
|
||||
</n-form-item>
|
||||
<n-form-item label="需邮箱验证后发放">
|
||||
<n-switch v-model:value="cfg.invite_reward.require_verify" />
|
||||
<n-switch v-model:value="cfg.referral_reward.require_verify" />
|
||||
</n-form-item>
|
||||
<n-form-item label="同 IP 当日上限">
|
||||
<n-input-number v-model:value="cfg.invite_reward.ip_daily_cap" :min="0" :max="100" />
|
||||
<n-input-number v-model:value="cfg.referral_reward.ip_daily_cap" :min="0" :max="100" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
@@ -120,14 +123,13 @@ const cfg = reactive({
|
||||
register_bonus: 10,
|
||||
daily_signin: { enabled: true, base: 1, streak: { 3: 2, 7: 5, 30: 20 } },
|
||||
comment_reward: { enabled: true, amount: 1, daily_cap: 5, min_length: 10, on_approved: true },
|
||||
invite_reward: {
|
||||
referral_reward: {
|
||||
enabled: true,
|
||||
inviter_amount: 5,
|
||||
invitee_amount: 2,
|
||||
promoter_amount: 5,
|
||||
referred_amount: 2,
|
||||
level_points: 0,
|
||||
require_verify: true,
|
||||
ip_daily_cap: 3,
|
||||
register_field: 'optional',
|
||||
reject_invalid: true,
|
||||
},
|
||||
download_default_cost: 0,
|
||||
})
|
||||
|
||||
@@ -121,7 +121,6 @@ const typeOptions = computed(() => optionsState.types.length
|
||||
: [
|
||||
{ label: '全部类型', value: '' },
|
||||
{ label: '订单返佣', value: 'order' },
|
||||
{ label: '邀请返佣', value: 'invite' },
|
||||
])
|
||||
|
||||
const columnChoices = [
|
||||
|
||||
@@ -92,7 +92,7 @@ const guides = [
|
||||
{ key: 'cdn', title: '静态资源与 CDN', desc: '核对前端资源地址与缓存策略', url: '#/global/common' },
|
||||
{ key: 'ip-geo', title: 'IP 归属地', desc: '验证 IP 查询服务与密钥配置', url: '#/extensions/ip' },
|
||||
{ key: 'payment', title: '支付与会员', desc: '核对网关、会员与充值配置', url: '#/payment/gateway' },
|
||||
{ key: 'credits', title: '积分与签到', desc: '检查积分规则、签到与邀请奖励', url: '#/users/signin' },
|
||||
{ key: 'credits', title: '积分与签到', desc: '检查积分规则、签到与推广奖励', url: '#/users/signin' },
|
||||
{ key: 'badge', title: '等级与徽章', desc: '维护等级、认证与徽章体系', url: '#/users/badge' },
|
||||
]
|
||||
|
||||
|
||||
@@ -1,512 +0,0 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader title="邀请码运营" desc="统一管理用户邀请码与独立邀请码,支持批量生成、批量禁用、备注追踪和筛选导出。" />
|
||||
|
||||
<div class="aksrt-invite-ops__stats">
|
||||
<n-card v-for="card in statCards" :key="card.key" size="small">
|
||||
<n-statistic :label="card.label" :value="stats[card.key] || 0" />
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<n-grid cols="1 s:1 m:2" :x-gap="16" :y-gap="16" responsive="screen">
|
||||
<n-grid-item>
|
||||
<n-card title="批量生成邀请码" size="small">
|
||||
<n-form :model="generateForm" label-placement="top">
|
||||
<n-grid cols="2" :x-gap="12">
|
||||
<n-grid-item>
|
||||
<n-form-item label="生成数量">
|
||||
<n-input-number v-model:value="generateForm.count" :min="1" :max="100" style="width: 100%;" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-form-item label="随机长度">
|
||||
<n-input-number v-model:value="generateForm.length" :min="4" :max="12" style="width: 100%;" />
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
<n-form-item label="前缀">
|
||||
<n-input v-model:value="generateForm.prefix" placeholder="AK" />
|
||||
</n-form-item>
|
||||
<n-form-item label="批次备注">
|
||||
<n-input
|
||||
v-model:value="generateForm.note"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 4 }"
|
||||
placeholder="例如:618 拉新活动 / 渠道合作批次"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" :loading="generating" @click="handleGenerate">批量生成</n-button>
|
||||
<n-button @click="loadData">刷新列表</n-button>
|
||||
</n-space>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
|
||||
<n-grid-item>
|
||||
<n-card title="批次输出" size="small">
|
||||
<n-alert type="info" :show-icon="false">
|
||||
独立邀请码会接入注册校验链路;导出前建议补齐批次备注,便于渠道回溯。
|
||||
</n-alert>
|
||||
<div v-if="generatedCodes.length" class="aksrt-invite-ops__generated">
|
||||
<n-tag type="success" round :bordered="false">批次 {{ generatedBatchId || '-' }}</n-tag>
|
||||
<n-input
|
||||
type="textarea"
|
||||
:value="generatedCodes.join('\n')"
|
||||
readonly
|
||||
:autosize="{ minRows: 7, maxRows: 10 }"
|
||||
/>
|
||||
</div>
|
||||
<n-empty v-else description="生成后的邀请码会显示在这里" />
|
||||
</n-card>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
|
||||
<n-card title="邀请码台账" size="small" class="aksrt-invite-ops__table-card">
|
||||
<n-space vertical :size="12">
|
||||
<n-grid cols="1 s:2 m:4" :x-gap="12" :y-gap="12" responsive="screen">
|
||||
<n-grid-item>
|
||||
<n-input v-model:value="filters.keyword" placeholder="搜索邀请码、批次、用户、备注" clearable />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-select v-model:value="filters.status" :options="statusOptions" placeholder="状态" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-select v-model:value="filters.source" :options="sourceOptions" placeholder="来源" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-date-picker
|
||||
v-model:value="filters.dateRange"
|
||||
type="daterange"
|
||||
clearable
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
|
||||
<n-space justify="space-between" wrap>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleSearch">筛选</n-button>
|
||||
<n-button @click="resetFilters">重置</n-button>
|
||||
<n-button :disabled="!checkedRows.length" @click="handleBulkAction('disable')">批量禁用</n-button>
|
||||
<n-button :disabled="!checkedRows.length" @click="handleBulkAction('delete')">批量删除</n-button>
|
||||
</n-space>
|
||||
<n-button quaternary @click="handleExport">导出当前筛选</n-button>
|
||||
</n-space>
|
||||
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:loading="loading"
|
||||
:bordered="false"
|
||||
:pagination="pagination"
|
||||
:row-key="(row) => row.row_key"
|
||||
:checked-row-keys="checkedRowKeys"
|
||||
@update:checked-row-keys="handleCheckedRowKeysChange"
|
||||
/>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { NButton, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import api from '@/api'
|
||||
|
||||
const message = useMessage()
|
||||
const dialog = useDialog()
|
||||
|
||||
const loading = ref(false)
|
||||
const generating = ref(false)
|
||||
const rows = ref([])
|
||||
const checkedRowKeys = ref([])
|
||||
const generatedCodes = ref([])
|
||||
const generatedBatchId = ref('')
|
||||
|
||||
const stats = reactive({
|
||||
total_codes: 0,
|
||||
active_codes: 0,
|
||||
used_codes: 0,
|
||||
disabled_codes: 0,
|
||||
user_codes: 0,
|
||||
standalone_codes: 0,
|
||||
total_invites: 0,
|
||||
total_rewarded: 0,
|
||||
})
|
||||
|
||||
const generateForm = reactive({
|
||||
count: 20,
|
||||
length: 6,
|
||||
prefix: 'AK',
|
||||
note: '',
|
||||
})
|
||||
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
status: '',
|
||||
source: '',
|
||||
dateRange: null,
|
||||
})
|
||||
|
||||
const pager = reactive({
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
itemCount: 0,
|
||||
})
|
||||
|
||||
const optionsState = reactive({
|
||||
statuses: [],
|
||||
sources: [],
|
||||
})
|
||||
|
||||
const statCards = [
|
||||
{ key: 'total_codes', label: '总邀请码' },
|
||||
{ key: 'active_codes', label: '当前可用' },
|
||||
{ key: 'used_codes', label: '已使用' },
|
||||
{ key: 'disabled_codes', label: '已禁用' },
|
||||
{ key: 'standalone_codes', label: '独立邀请码' },
|
||||
{ key: 'total_rewarded', label: '已完成兑现' },
|
||||
]
|
||||
|
||||
const statusOptions = computed(() => optionsState.statuses.length
|
||||
? optionsState.statuses
|
||||
: [
|
||||
{ label: '全部状态', value: '' },
|
||||
{ label: '可用', value: 'active' },
|
||||
{ label: '已使用', value: 'used' },
|
||||
{ label: '已禁用', value: 'disabled' },
|
||||
])
|
||||
|
||||
const sourceOptions = computed(() => optionsState.sources.length
|
||||
? optionsState.sources
|
||||
: [
|
||||
{ label: '全部来源', value: '' },
|
||||
{ label: '用户邀请码', value: 'user' },
|
||||
{ label: '独立邀请码', value: 'standalone' },
|
||||
])
|
||||
|
||||
const checkedRows = computed(() => rows.value.filter((row) => checkedRowKeys.value.includes(row.row_key)))
|
||||
|
||||
const pagination = computed(() => ({
|
||||
page: pager.page,
|
||||
pageSize: pager.pageSize,
|
||||
itemCount: pager.itemCount,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
onUpdatePage: (page) => {
|
||||
pager.page = page
|
||||
loadData()
|
||||
},
|
||||
onUpdatePageSize: (pageSize) => {
|
||||
pager.pageSize = pageSize
|
||||
pager.page = 1
|
||||
loadData()
|
||||
},
|
||||
}))
|
||||
|
||||
const columns = [
|
||||
{
|
||||
type: 'selection',
|
||||
disabled: (row) => !row.can_bulk,
|
||||
},
|
||||
{
|
||||
title: '来源',
|
||||
key: 'source_type',
|
||||
width: 120,
|
||||
render: (row) => h(NTag, { size: 'small', round: true, type: row.source_type === 'standalone' ? 'info' : 'default' }, () => row.source_label),
|
||||
},
|
||||
{
|
||||
title: '邀请码',
|
||||
key: 'invite_code',
|
||||
width: 180,
|
||||
ellipsis: { tooltip: true },
|
||||
},
|
||||
{
|
||||
title: '归属/使用人',
|
||||
key: 'owner',
|
||||
width: 180,
|
||||
render: (row) => row.user?.display_name || row.invitee?.display_name || '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status_label',
|
||||
width: 110,
|
||||
render: (row) => {
|
||||
const typeMap = { active: 'success', used: 'warning', disabled: 'error' }
|
||||
return h(NTag, { size: 'small', round: true, type: typeMap[row.status] || 'default' }, () => row.status_label)
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '注册状态',
|
||||
key: 'registration_status',
|
||||
width: 110,
|
||||
render: (row) => row.registration_status || '-',
|
||||
},
|
||||
{
|
||||
title: '批次备注',
|
||||
key: 'note',
|
||||
minWidth: 180,
|
||||
ellipsis: { tooltip: true },
|
||||
render: (row) => row.note || row.batch_id || '-',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'created_at',
|
||||
width: 170,
|
||||
},
|
||||
{
|
||||
title: '使用时间',
|
||||
key: 'used_at',
|
||||
width: 170,
|
||||
render: (row) => row.used_at || '-',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 170,
|
||||
render: (row) => h(NSpace, { size: 'small' }, () => {
|
||||
const actions = [
|
||||
h(NButton, {
|
||||
size: 'tiny',
|
||||
quaternary: true,
|
||||
type: 'warning',
|
||||
onClick: () => handleDisable(row),
|
||||
}, () => '禁用'),
|
||||
]
|
||||
|
||||
if (row.source_type === 'standalone') {
|
||||
actions.push(
|
||||
h(NButton, {
|
||||
size: 'tiny',
|
||||
quaternary: true,
|
||||
type: 'error',
|
||||
onClick: () => handleDelete(row),
|
||||
}, () => '删除'),
|
||||
)
|
||||
}
|
||||
|
||||
return actions
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
function formatDateValue(value) {
|
||||
const date = new Date(value)
|
||||
const year = date.getFullYear()
|
||||
const month = `${date.getMonth() + 1}`.padStart(2, '0')
|
||||
const day = `${date.getDate()}`.padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
function buildQuery(page = pager.page, pageSize = pager.pageSize) {
|
||||
const params = {
|
||||
page,
|
||||
per_page: pageSize,
|
||||
}
|
||||
|
||||
if (filters.keyword) params.keyword = filters.keyword
|
||||
if (filters.status) params.status = filters.status
|
||||
if (filters.source) params.source = filters.source
|
||||
if (Array.isArray(filters.dateRange) && filters.dateRange.length === 2) {
|
||||
params.date_from = formatDateValue(filters.dateRange[0])
|
||||
params.date_to = formatDateValue(filters.dateRange[1])
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.request('/invite/codes', { params: buildQuery() })
|
||||
const data = res?.data || {}
|
||||
rows.value = data.items || []
|
||||
pager.itemCount = data.total || 0
|
||||
Object.assign(stats, data.stats || {})
|
||||
optionsState.statuses = data.options?.statuses || []
|
||||
optionsState.sources = data.options?.sources || []
|
||||
checkedRowKeys.value = []
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载邀请码台账失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pager.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.keyword = ''
|
||||
filters.status = ''
|
||||
filters.source = ''
|
||||
filters.dateRange = null
|
||||
pager.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleCheckedRowKeysChange(keys) {
|
||||
checkedRowKeys.value = keys
|
||||
}
|
||||
|
||||
async function handleGenerate() {
|
||||
generating.value = true
|
||||
try {
|
||||
const res = await api.request('/invite/codes', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
count: generateForm.count,
|
||||
length: generateForm.length,
|
||||
prefix: generateForm.prefix,
|
||||
note: generateForm.note,
|
||||
},
|
||||
})
|
||||
generatedCodes.value = res?.data?.codes || []
|
||||
generatedBatchId.value = res?.data?.batch_id || ''
|
||||
message.success(res?.message || '邀请码已生成')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '生成失败')
|
||||
} finally {
|
||||
generating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDisable(row) {
|
||||
dialog.warning({
|
||||
title: '确认禁用',
|
||||
content: `确定要禁用邀请码 ${row.invite_code} 吗?`,
|
||||
positiveText: '禁用',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
if (row.source_type === 'user') {
|
||||
await api.request(`/invite/codes/${row.user?.id}/disable`, { method: 'POST' })
|
||||
} else {
|
||||
await api.request('/invite/codes/bulk', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'disable',
|
||||
targets: [{ source_type: 'standalone', code: row.invite_code }],
|
||||
},
|
||||
})
|
||||
}
|
||||
message.success('邀请码已禁用')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '操作失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleDelete(row) {
|
||||
dialog.warning({
|
||||
title: '确认删除',
|
||||
content: `确定要删除独立邀请码 ${row.invite_code} 吗?`,
|
||||
positiveText: '删除',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await api.request('/invite/codes/bulk', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
action: 'delete',
|
||||
targets: [{ source_type: 'standalone', code: row.invite_code }],
|
||||
},
|
||||
})
|
||||
message.success('邀请码已删除')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '删除失败')
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function handleBulkAction(action) {
|
||||
if (!checkedRows.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await api.request('/invite/codes/bulk', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
action,
|
||||
targets: checkedRows.value.map((row) => ({
|
||||
source_type: row.source_type,
|
||||
user_id: row.user?.id || 0,
|
||||
code: row.invite_code,
|
||||
})),
|
||||
},
|
||||
})
|
||||
message.success(action === 'delete' ? '批量删除完成' : '批量禁用完成')
|
||||
await loadData()
|
||||
} catch (error) {
|
||||
message.error(error?.message || '批量操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
try {
|
||||
const res = await api.request('/invite/codes', { params: buildQuery(1, 200) })
|
||||
const items = res?.data?.items || []
|
||||
const header = ['来源', '邀请码', '归属用户', '使用人', '状态', '注册状态', '批次', '备注', '创建时间', '使用时间']
|
||||
const lines = items.map((row) => [
|
||||
row.source_label || '',
|
||||
row.invite_code || '',
|
||||
row.user?.display_name || '',
|
||||
row.invitee?.display_name || '',
|
||||
row.status_label || '',
|
||||
row.registration_status || '',
|
||||
row.batch_id || '',
|
||||
row.note || '',
|
||||
row.created_at || '',
|
||||
row.used_at || '',
|
||||
])
|
||||
downloadCsv('invite-codes.csv', [header, ...lines])
|
||||
message.success('导出完成')
|
||||
} catch (error) {
|
||||
message.error(error?.message || '导出失败')
|
||||
}
|
||||
}
|
||||
|
||||
function downloadCsv(filename, rows) {
|
||||
const csv = `\uFEFF${rows.map((row) => row.map((cell) => `"${String(cell ?? '').replaceAll('"', '""')}"`).join(',')).join('\n')}`
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = filename
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-invite-ops__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.aksrt-invite-ops__generated {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.aksrt-invite-ops__table-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<UsersSectionPage
|
||||
title="签到奖励"
|
||||
desc="直接管理签到与邀请奖励的真实运行配置,保存后会立即影响前台签到、邀请链接和注册页邀请码行为。"
|
||||
desc="直接管理签到与 Ref 推广奖励,保存后会立即影响前台签到、推广链接和注册页推广码行为。"
|
||||
:links="[
|
||||
{ label: '注册登录', path: '/users/auth' },
|
||||
{ label: '用户积分', path: '/users/credits' },
|
||||
@@ -20,11 +20,11 @@
|
||||
<n-tag round :type="form.signin_enabled ? 'success' : 'default'">
|
||||
签到能力:{{ form.signin_enabled ? '已启用' : '已关闭' }}
|
||||
</n-tag>
|
||||
<n-tag round :type="form.invite_enabled ? 'success' : 'default'">
|
||||
邀请奖励:{{ form.invite_enabled ? '已启用' : '已关闭' }}
|
||||
<n-tag round :type="form.referral_reward_enabled ? 'success' : 'default'">
|
||||
推广奖励:{{ form.referral_reward_enabled ? '已启用' : '已关闭' }}
|
||||
</n-tag>
|
||||
<n-tag round :type="form.invite_registration_enabled ? 'info' : 'default'">
|
||||
邀请注册:{{ form.invite_registration_enabled ? '开启' : '关闭' }}
|
||||
<n-tag round :type="form.referral_registration_enabled ? 'info' : 'default'">
|
||||
推广码注册:{{ form.referral_registration_enabled ? '开启' : '关闭' }}
|
||||
</n-tag>
|
||||
<n-tag round :type="captchaSummary.register ? 'info' : 'default'">
|
||||
注册验证码:{{ captchaSummary.register ? '启用' : '未启用' }}
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
<n-card v-if="!form.credits_enabled" class="aksrt-section-card">
|
||||
<n-alert type="warning" :show-icon="false">
|
||||
当前积分系统处于关闭状态。即使保存签到和邀请奖励配置,前台也不会真正发放奖励,直到“用户积分”页重新启用积分系统。
|
||||
当前积分系统处于关闭状态。即使保存签到和推广奖励配置,前台也不会真正发放奖励,直到“用户积分”页重新启用积分系统。
|
||||
</n-alert>
|
||||
</n-card>
|
||||
</n-spin>
|
||||
@@ -75,27 +75,34 @@
|
||||
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>邀请奖励与注册页邀请码</h3>
|
||||
<h3>Ref 推广奖励与注册</h3>
|
||||
</div>
|
||||
|
||||
<n-form-item label="启用邀请奖励">
|
||||
<n-switch v-model:value="form.invite_enabled" />
|
||||
<n-form-item label="启用推广奖励">
|
||||
<n-switch v-model:value="form.referral_reward_enabled" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="开放邀请注册">
|
||||
<n-switch v-model:value="form.invite_registration_enabled" />
|
||||
<n-form-item label="Ref 码长度">
|
||||
<n-input-number v-model:value="form.referral_code_length" :min="4" :max="12" style="width: 180px;" />
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">关闭后,邀请链接入口、注册页邀请码字段和个人中心邀请模块会一起隐藏。</span>
|
||||
<span class="aksrt-hint-text">只影响新生成或管理员重新生成的推广码,已有 Ref 不会失效。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
|
||||
<template v-if="form.invite_registration_enabled">
|
||||
<n-form-item label="邀请码长度">
|
||||
<n-input-number v-model:value="form.invite_code_length" :min="4" :max="12" style="width: 180px;" />
|
||||
</n-form-item>
|
||||
<n-form-item label="首次触达窗口(天)">
|
||||
<n-input-number v-model:value="form.referral_window_days" :min="1" :max="365" style="width: 180px;" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="注册页邀请码字段">
|
||||
<n-radio-group v-model:value="form.invite_register_field">
|
||||
<n-form-item label="开放推广码注册">
|
||||
<n-switch v-model:value="form.referral_registration_enabled" />
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">此开关只控制注册页推广码字段;现金返佣和积分奖励仍可独立使用同一 Ref 关系。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
|
||||
<template v-if="form.referral_registration_enabled">
|
||||
<n-form-item label="注册页推广码字段">
|
||||
<n-radio-group v-model:value="form.referral_register_field">
|
||||
<n-space size="large">
|
||||
<n-radio value="hidden">隐藏</n-radio>
|
||||
<n-radio value="optional">选填</n-radio>
|
||||
@@ -104,21 +111,25 @@
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="无效邀请码处理" v-if="form.invite_register_field === 'required'">
|
||||
<n-switch v-model:value="form.invite_reject_invalid" />
|
||||
<n-form-item label="无效推广码处理" v-if="form.referral_register_field === 'required'">
|
||||
<n-switch v-model:value="form.referral_reject_invalid" />
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">开启后,必填模式下无效邀请码会直接拒绝注册。关闭则忽略邀请码错误并继续注册。</span>
|
||||
<span class="aksrt-hint-text">开启后,必填模式下无效推广码会直接拒绝注册。首次触达 Ref 始终优先于手填码。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
</template>
|
||||
|
||||
<template v-if="form.invite_enabled">
|
||||
<n-form-item label="邀请人奖励">
|
||||
<n-input-number v-model:value="form.inviter_amount" :min="0" :max="100000" style="width: 180px;" />
|
||||
<template v-if="form.referral_reward_enabled">
|
||||
<n-form-item label="推广人奖励">
|
||||
<n-input-number v-model:value="form.promoter_amount" :min="0" :max="100000" style="width: 180px;" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="被邀请人奖励">
|
||||
<n-input-number v-model:value="form.invitee_amount" :min="0" :max="100000" style="width: 180px;" />
|
||||
<n-form-item label="新用户奖励">
|
||||
<n-input-number v-model:value="form.referred_amount" :min="0" :max="100000" style="width: 180px;" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="推广等级积分">
|
||||
<n-input-number v-model:value="form.level_points" :min="0" :max="100000" style="width: 180px;" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="验证邮箱后发放">
|
||||
@@ -162,15 +173,17 @@ const form = reactive({
|
||||
signin_enabled: true,
|
||||
signin_base: 1,
|
||||
signin_streak_text: '3|2\n7|5\n30|20',
|
||||
invite_enabled: true,
|
||||
inviter_amount: 5,
|
||||
invitee_amount: 2,
|
||||
referral_reward_enabled: true,
|
||||
promoter_amount: 5,
|
||||
referred_amount: 2,
|
||||
level_points: 0,
|
||||
require_verify: true,
|
||||
ip_daily_cap: 3,
|
||||
invite_registration_enabled: true,
|
||||
invite_code_length: 6,
|
||||
invite_register_field: 'optional',
|
||||
invite_reject_invalid: true,
|
||||
referral_registration_enabled: true,
|
||||
referral_code_length: 12,
|
||||
referral_window_days: 30,
|
||||
referral_register_field: 'optional',
|
||||
referral_reject_invalid: true,
|
||||
})
|
||||
|
||||
const currencyLabel = computed(() => form.currency_name || '积分')
|
||||
@@ -226,7 +239,7 @@ async function load() {
|
||||
const authData = extractData(authRes)
|
||||
const captchaData = extractData(captchaRes)
|
||||
const dailySignin = creditData.daily_signin || {}
|
||||
const inviteReward = creditData.invite_reward || {}
|
||||
const referralReward = creditData.referral_reward || {}
|
||||
const enabledScenarios = Array.isArray(captchaData.enabled_scenarios)
|
||||
? captchaData.enabled_scenarios
|
||||
: []
|
||||
@@ -240,15 +253,17 @@ async function load() {
|
||||
form.signin_enabled = dailySignin.enabled !== false
|
||||
form.signin_base = Number(dailySignin.base || 0)
|
||||
form.signin_streak_text = formatStreak(dailySignin.streak || { 3: 2, 7: 5, 30: 20 })
|
||||
form.invite_enabled = inviteReward.enabled !== false
|
||||
form.inviter_amount = Number(inviteReward.inviter_amount || 0)
|
||||
form.invitee_amount = Number(inviteReward.invitee_amount || 0)
|
||||
form.require_verify = inviteReward.require_verify !== false
|
||||
form.ip_daily_cap = Number(inviteReward.ip_daily_cap || 0)
|
||||
form.invite_registration_enabled = authData.invite_registration_enabled !== false
|
||||
form.invite_code_length = Number(authData.invite_code_length || 6)
|
||||
form.invite_register_field = authData.invite_register_field || 'optional'
|
||||
form.invite_reject_invalid = authData.invite_reject_invalid !== false
|
||||
form.referral_reward_enabled = referralReward.enabled !== false
|
||||
form.promoter_amount = Number(referralReward.promoter_amount || 0)
|
||||
form.referred_amount = Number(referralReward.referred_amount || 0)
|
||||
form.level_points = Number(referralReward.level_points || 0)
|
||||
form.require_verify = referralReward.require_verify !== false
|
||||
form.ip_daily_cap = Number(referralReward.ip_daily_cap || 0)
|
||||
form.referral_registration_enabled = authData.referral_registration_enabled !== false
|
||||
form.referral_code_length = Number(authData.referral_code_length || 12)
|
||||
form.referral_window_days = Number(authData.referral_window_days || 30)
|
||||
form.referral_register_field = authData.referral_register_field || 'optional'
|
||||
form.referral_reject_invalid = authData.referral_reject_invalid !== false
|
||||
|
||||
captchaSummary.register = !!captchaData.enabled && !!captchaData.active_provider && sceneOn('register')
|
||||
captchaSummary.signin = !!captchaData.enabled && !!captchaData.active_provider && sceneOn('signin')
|
||||
@@ -270,24 +285,24 @@ async function handleSave() {
|
||||
base: Number(form.signin_base || 0),
|
||||
streak: parseStreakText(form.signin_streak_text),
|
||||
},
|
||||
invite_reward: {
|
||||
...(creditSnapshot.value.invite_reward || {}),
|
||||
enabled: form.invite_enabled,
|
||||
inviter_amount: Number(form.inviter_amount || 0),
|
||||
invitee_amount: Number(form.invitee_amount || 0),
|
||||
referral_reward: {
|
||||
...(creditSnapshot.value.referral_reward || {}),
|
||||
enabled: form.referral_reward_enabled,
|
||||
promoter_amount: Number(form.promoter_amount || 0),
|
||||
referred_amount: Number(form.referred_amount || 0),
|
||||
level_points: Number(form.level_points || 0),
|
||||
require_verify: form.require_verify,
|
||||
ip_daily_cap: Number(form.ip_daily_cap || 0),
|
||||
register_field: form.invite_register_field,
|
||||
reject_invalid: form.invite_reject_invalid,
|
||||
},
|
||||
}
|
||||
|
||||
const nextAuth = {
|
||||
...authSnapshot.value,
|
||||
invite_registration_enabled: form.invite_registration_enabled,
|
||||
invite_code_length: Number(form.invite_code_length || 6),
|
||||
invite_register_field: form.invite_register_field,
|
||||
invite_reject_invalid: form.invite_reject_invalid,
|
||||
referral_registration_enabled: form.referral_registration_enabled,
|
||||
referral_code_length: Number(form.referral_code_length || 12),
|
||||
referral_window_days: Number(form.referral_window_days || 30),
|
||||
referral_register_field: form.referral_register_field,
|
||||
referral_reject_invalid: form.referral_reject_invalid,
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
|
||||
@@ -80,7 +80,7 @@ const NATIVE_OPS_REDIRECTS = [
|
||||
{ path: '/payment/income', title: '创作分成运营', nativeAdminPath: 'admin.php?page=aksrt_income' },
|
||||
{ path: '/payment/rebate', title: '返佣管理', nativeAdminPath: 'admin.php?page=aksrt_rebate' },
|
||||
{ path: '/users/messages', title: '消息中心', nativeAdminPath: 'users.php?page=aksrt_messages' },
|
||||
{ path: '/users/invite-codes', title: '邀请码运营', nativeAdminPath: 'users.php?page=aksrt_invite_codes' },
|
||||
{ path: '/users/referral-codes', title: '推广码运营', nativeAdminPath: 'users.php?page=aksrt_referral_codes' },
|
||||
{ path: '/users/ban', title: '禁封管理', nativeAdminPath: 'users.php?page=aksrt_user_ban&tab=restrictions' },
|
||||
{ path: '/users/report', title: '禁封/举报', nativeAdminPath: 'users.php?page=aksrt_user_ban&tab=reports' },
|
||||
]
|
||||
|
||||
@@ -9,7 +9,6 @@ export const affiliateSchema = {
|
||||
affiliate_ratio_vip_1: 5,
|
||||
affiliate_ratio_vip_2: 5,
|
||||
affiliate_order_types: ['membership', 'content'],
|
||||
affiliate_touch_window_days: 30,
|
||||
affiliate_settlement_days: 7,
|
||||
affiliate_promotion_text: '',
|
||||
},
|
||||
@@ -24,7 +23,6 @@ export const affiliateSchema = {
|
||||
{ key: 'affiliate_ratio_vip_1', label: '一级会员佣金比例(%)', type: 'number', default: 5, min: 0, max: 100, dependsOn: { key: 'affiliate_enabled', value: true } },
|
||||
{ key: 'affiliate_ratio_vip_2', label: '二级会员佣金比例(%)', type: 'number', default: 5, min: 0, max: 100, dependsOn: { key: 'affiliate_enabled', value: true } },
|
||||
{ key: 'affiliate_order_types', label: '适用订单类型', type: 'checkbox', default: ['membership', 'content'], sanitize_type: 'array', options: [{ label: '会员订单', value: 'membership' }, { label: '付费内容', value: 'content' }, { label: '购买积分', value: 'credits' }], dependsOn: { key: 'affiliate_enabled', value: true } },
|
||||
{ key: 'affiliate_touch_window_days', label: '首次触达窗口(天)', type: 'number', default: 30, min: 1, max: 365, dependsOn: { key: 'affiliate_enabled', value: true } },
|
||||
{ key: 'affiliate_settlement_days', label: '结算观察期(天)', type: 'number', default: 7, min: 0, max: 90, dependsOn: { key: 'affiliate_enabled', value: true } },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -37,10 +37,13 @@ export const authSchema = {
|
||||
bind_reminder_expires: 24,
|
||||
mandatory_bind: 'close',
|
||||
email_set_captch: true,
|
||||
invit_code_s: false,
|
||||
invit_code_mode: 'open',
|
||||
invit_code_open_desc: '邀请码为选填,没有邀请码也可以注册',
|
||||
invit_code_must_desc: '请输入邀请码',
|
||||
referral_registration_enabled: true,
|
||||
referral_register_field: 'optional',
|
||||
referral_reject_invalid: true,
|
||||
referral_code_length: 12,
|
||||
referral_window_days: 30,
|
||||
referral_open_desc: '推广码为选填,没有推广码也可以注册',
|
||||
referral_required_desc: '请输入推广码',
|
||||
sign_page_background: [],
|
||||
sign_theme_mode: 'default',
|
||||
sign_show_header: true,
|
||||
@@ -115,14 +118,17 @@ export const authSchema = {
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'invite_inline',
|
||||
title: '邀请码注册',
|
||||
key: 'referral_inline',
|
||||
title: '推广码注册',
|
||||
icon: 'fas fa-ticket-alt',
|
||||
fields: [
|
||||
{ key: 'invit_code_s', label: '启用邀请码', type: 'switch', default: false, help: '与「邀请码」子页联动,此处开关控制注册表单是否显示邀请码字段' },
|
||||
{ key: 'invit_code_mode', label: '邀请模式', type: 'select', default: 'open', options: [{ label: '开放注册(邀请码选填)', value: 'open' }, { label: '必须邀请码', value: 'must' }], dependsOn: { key: 'invit_code_s', value: true } },
|
||||
{ key: 'invit_code_open_desc', label: '开放注册提示', type: 'input', default: '邀请码为选填,没有邀请码也可以注册', dependsOn: { key: 'invit_code_s', value: true } },
|
||||
{ key: 'invit_code_must_desc', label: '必填提示', type: 'input', default: '请输入邀请码', dependsOn: { key: 'invit_code_s', value: true } },
|
||||
{ key: 'referral_registration_enabled', label: '开放推广码注册', type: 'switch', default: true },
|
||||
{ key: 'referral_register_field', label: '注册页推广码字段', type: 'select', default: 'optional', options: [{ label: '隐藏', value: 'hidden' }, { label: '选填', value: 'optional' }, { label: '必填', value: 'required' }], dependsOn: { key: 'referral_registration_enabled', value: true } },
|
||||
{ key: 'referral_reject_invalid', label: '拒绝无效推广码', type: 'switch', default: true, dependsOn: { key: 'referral_registration_enabled', value: true } },
|
||||
{ key: 'referral_code_length', label: 'Ref 码长度', type: 'number', default: 12, min: 4, max: 12 },
|
||||
{ key: 'referral_window_days', label: '首次触达窗口(天)', type: 'number', default: 30, min: 1, max: 365 },
|
||||
{ key: 'referral_open_desc', label: '选填提示', type: 'input', default: '推广码为选填,没有推广码也可以注册', dependsOn: { key: 'referral_registration_enabled', value: true } },
|
||||
{ key: 'referral_required_desc', label: '必填提示', type: 'input', default: '请输入推广码', dependsOn: { key: 'referral_registration_enabled', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -17,7 +17,6 @@ export const creditsSchema = {
|
||||
points_free_comment_new: 1,
|
||||
points_free_comment_like: 1,
|
||||
points_free_followed: 1,
|
||||
points_earn_invite: 10,
|
||||
points_spend_comment: 0,
|
||||
points_spend_download: 0,
|
||||
points_spend_view_hidden: 0,
|
||||
@@ -59,7 +58,6 @@ export const creditsSchema = {
|
||||
{ key: 'points_free_comment_new', label: '发表评论', type: 'number', default: 1, min: 0, max: 100 },
|
||||
{ key: 'points_free_comment_like', label: '评论被赞', type: 'number', default: 1, min: 0, max: 100 },
|
||||
{ key: 'points_free_followed', label: '被关注', type: 'number', default: 1, min: 0, max: 100 },
|
||||
{ key: 'points_earn_invite', label: '邀请注册奖励', type: 'number', default: 10, min: 0, max: 1000, help: '邀请不计入免费日上限' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -6,7 +6,6 @@ import { authSchema } from './auth.js'
|
||||
import { oauthSchema } from './oauth.js'
|
||||
import { notificationsSchema } from './notifications.js'
|
||||
import { creditsSchema } from './credits.js'
|
||||
import { inviteSchema } from './invite.js'
|
||||
import { featuresSchema } from './features.js'
|
||||
import { signinSchema } from './signin.js'
|
||||
import { verifySchema } from './verify.js'
|
||||
@@ -21,7 +20,6 @@ export const usersSchemas = {
|
||||
oauth: oauthSchema,
|
||||
notifications: notificationsSchema,
|
||||
credits: creditsSchema,
|
||||
invite: inviteSchema,
|
||||
features: featuresSchema,
|
||||
signin: signinSchema,
|
||||
verify: verifySchema,
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
export const inviteSchema = {
|
||||
key: 'users.invite',
|
||||
title: '邀请码',
|
||||
icon: 'fas fa-ticket-alt',
|
||||
description: '邀请码生成、管理、奖励配置。',
|
||||
defaults: {
|
||||
invit_code_s: false,
|
||||
invit_code_open_desc: '邀请码为选填,没有邀请码也可以注册',
|
||||
invit_code_must_desc: '请输入邀请码',
|
||||
invit_code_mode: 'open',
|
||||
invit_code_length: 8,
|
||||
invit_code_prefix: '',
|
||||
invit_code_max_use: 1,
|
||||
invit_code_expiry: 0,
|
||||
invit_code_batch: 20,
|
||||
invit_reward_points: 0,
|
||||
invit_reward_level: 0,
|
||||
invit_reward_vip_days: 0,
|
||||
invit_invitee_reward: false,
|
||||
invit_code_show_count: true,
|
||||
invit_code_show_remain: true,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'basic',
|
||||
title: '邀请设置',
|
||||
icon: 'fas fa-cog',
|
||||
fields: [
|
||||
{ key: 'invit_code_s', label: '启用邀请码', type: 'switch', default: false },
|
||||
{ key: 'invit_code_open_desc', label: '开放注册提示', type: 'input', default: '邀请码为选填,没有邀请码也可以注册' },
|
||||
{ key: 'invit_code_must_desc', label: '必填提示', type: 'input', default: '请输入邀请码' },
|
||||
{ key: 'invit_code_mode', label: '邀请模式', type: 'select', default: 'open', options: [{ label: '开放注册(邀请码选填)', value: 'open' }, { label: '必须邀请码', value: 'must' }] },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'format',
|
||||
title: '码面设置',
|
||||
icon: 'fas fa-barcode',
|
||||
fields: [
|
||||
{ key: 'invit_code_length', label: '邀请码长度', type: 'number', default: 8, min: 4, max: 32 },
|
||||
{ key: 'invit_code_prefix', label: '邀请码前缀', type: 'input', default: '' },
|
||||
{ key: 'invit_code_max_use', label: '单码最大使用次数', type: 'number', default: 1, min: 1, max: 9999 },
|
||||
{ key: 'invit_code_expiry', label: '有效期(天)', type: 'number', default: 0, min: 0, help: '0为永久有效' },
|
||||
{ key: 'invit_code_batch', label: '批量生成数量', type: 'number', default: 20, min: 1, max: 500 },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'reward',
|
||||
title: '邀请奖励',
|
||||
icon: 'fas fa-gift',
|
||||
fields: [
|
||||
{ key: 'invit_reward_points', label: '邀请奖励积分', type: 'number', default: 0, min: 0, max: 1000 },
|
||||
{ key: 'invit_reward_level', label: '邀请奖励等级', type: 'number', default: 0, min: 0, max: 10 },
|
||||
{ key: 'invit_reward_vip_days', label: '奖励VIP天数', type: 'number', default: 0, min: 0, max: 3650 },
|
||||
{ key: 'invit_invitee_reward', label: '被邀请人奖励', type: 'switch', default: false },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'display',
|
||||
title: '展示设置',
|
||||
icon: 'fas fa-eye',
|
||||
fields: [
|
||||
{ key: 'invit_code_show_count', label: '显示已使用次数', type: 'switch', default: true },
|
||||
{ key: 'invit_code_show_remain', label: '显示剩余次数', type: 'switch', default: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user