Files
wordpress-theme-aksrt/assets/admin/src/pages/CmsCredits.vue
T

276 lines
11 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.
<template>
<div class="aksrt-page-container">
<h2 class="aksrt-page-title">积分配置</h2>
<p class="aksrt-page-desc">
配置积分规则与发放策略。可自定义货币名称(如”金币”、”魔石”等)。用户余额与手工调整在 WordPress 原生”用户”菜单中管理。
</p>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>主开关 + 基础设置</h3></div>
<n-form :model="cfg" label-placement="left" label-width="200" class="aksrt-form">
<n-form-item label="启用积分系统">
<n-switch v-model:value="cfg.enabled" />
</n-form-item>
<n-form-item label="自定义货币名称">
<n-input v-model:value="cfg.currency_name" placeholder="积分" maxlength="20" />
<template #feedback>
<span class="aksrt-hint-text">自定义货币显示名称,如"金币"、"魔石"等。留空则使用默认名称"积分"。</span>
</template>
</n-form-item>
<n-form-item label="注册赠送积分">
<n-input-number v-model:value="cfg.register_bonus" :min="0" :max="10000" />
</n-form-item>
</n-form>
</n-card>
<n-card title="统一资产账本" class="aksrt-section-card">
<n-space vertical :size="12">
<n-space wrap>
<n-select v-model:value="ledgerFilters.asset" :options="assetOptions" clearable placeholder="资产" style="width: 130px" />
<n-input v-model:value="ledgerFilters.transaction_id" placeholder="交易号" clearable style="width: 130px" />
<n-input v-model:value="ledgerFilters.operation_key" placeholder="幂等键" clearable style="width: 240px" />
<n-input v-model:value="ledgerFilters.ref_type" placeholder="引用类型" clearable style="width: 130px" />
<n-input v-model:value="ledgerFilters.ref_key" placeholder="引用键" clearable style="width: 180px" />
<n-input v-model:value="ledgerFilters.actor_user_id" placeholder="操作者 ID" clearable style="width: 130px" />
<n-button type="primary" :loading="ledgerLoading" @click="searchLedger">查询</n-button>
<n-button @click="resetLedger">重置</n-button>
</n-space>
<n-data-table
:columns="ledgerColumns"
:data="ledgerRows"
:loading="ledgerLoading"
:row-key="row => row.id"
:scroll-x="1600"
size="small"
/>
<n-pagination
v-model:page="ledgerPage"
:page-count="ledgerPageCount"
:page-size="20"
@update:page="loadLedger"
/>
</n-space>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>每日签到</h3></div>
<n-form :model="cfg.daily_signin" label-placement="left" label-width="200" class="aksrt-form">
<n-form-item label="启用">
<n-switch v-model:value="cfg.daily_signin.enabled" />
</n-form-item>
<n-form-item label="基础奖励">
<n-input-number v-model:value="cfg.daily_signin.base" :min="0" :max="10000" />
</n-form-item>
<n-form-item label="连续奖励(JSON)">
<n-input
type="textarea"
v-model:value="streakJson"
placeholder='{"3":2,"7":5,"30":20}'
:autosize="{ minRows: 2 }"
/>
<template #feedback>
<span class="aksrt-hint-text">连续 N 天时额外发放的积分。</span>
</template>
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>评论奖励</h3></div>
<n-form :model="cfg.comment_reward" label-placement="left" label-width="200" class="aksrt-form">
<n-form-item label="启用">
<n-switch v-model:value="cfg.comment_reward.enabled" />
</n-form-item>
<n-form-item label="单次奖励">
<n-input-number v-model:value="cfg.comment_reward.amount" :min="0" :max="10000" />
</n-form-item>
<n-form-item label="每日上限">
<n-input-number v-model:value="cfg.comment_reward.daily_cap" :min="0" :max="100" />
</n-form-item>
<n-form-item label="最少字符数">
<n-input-number v-model:value="cfg.comment_reward.min_length" :min="0" :max="500" />
</n-form-item>
<n-form-item label="审核通过后发放">
<n-switch v-model:value="cfg.comment_reward.on_approved" />
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card">
<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.referral_reward.enabled" />
</n-form-item>
<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.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.referral_reward.require_verify" />
</n-form-item>
<n-form-item label="同 IP 当日上限">
<n-input-number v-model:value="cfg.referral_reward.ip_daily_cap" :min="0" :max="100" />
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header"><h3>下载消费(参考)</h3></div>
<n-form :model="cfg" label-placement="left" label-width="200" class="aksrt-form">
<n-form-item :label="`默认积分价参考(${cfg.currency_name || '积分'})`">
<n-input-number v-model:value="cfg.download_default_cost" :min="0" :max="100000" />
<template #feedback>
<span class="aksrt-hint-text">实际价格以文章「付费内容设置」的积分价或现金价为准;下载解锁走收银台订单,不按次扣费。</span>
</template>
</n-form-item>
</n-form>
</n-card>
<div class="aksrt-form-footer">
<n-button type="primary" :loading="saving" @click="saveCfg">保存配置</n-button>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api'
const message = useMessage()
// ---------- 配置 ----------
const cfg = reactive({
enabled: true,
currency_name: '',
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 },
referral_reward: {
enabled: true,
promoter_amount: 5,
referred_amount: 2,
level_points: 0,
require_verify: true,
ip_daily_cap: 3,
},
download_default_cost: 0,
})
const streakJson = ref('{}')
const saving = ref(false)
const ledgerLoading = ref(false)
const ledgerRows = ref([])
const ledgerPage = ref(1)
const ledgerPageCount = ref(1)
const ledgerFilters = reactive({
asset: null,
transaction_id: '',
operation_key: '',
ref_type: '',
ref_key: '',
actor_user_id: '',
})
const assetOptions = [
{ label: '积分', value: 'points' },
{ label: '余额', value: 'balance' },
]
const ledgerColumns = [
{ title: '交易号', key: 'transaction_label', width: 100, fixed: 'left' },
{ title: '幂等键', key: 'operation_key', width: 260, ellipsis: { tooltip: true } },
{ title: '资产', key: 'assets_label', width: 100 },
{ title: '原因', key: 'reason', width: 150, ellipsis: { tooltip: true } },
{ title: '业务引用', key: 'reference_label', width: 200, ellipsis: { tooltip: true } },
{ title: '操作者', key: 'actor_label', width: 100 },
{ title: '增减 / 手续费', key: 'delta_label', width: 180, ellipsis: { tooltip: true } },
{ title: '变动后余额', key: 'balance_label', width: 150, ellipsis: { tooltip: true } },
{ title: '备注', key: 'remark', width: 180, ellipsis: { tooltip: true } },
{ title: 'UTC 时间', key: 'created_at', width: 190 },
]
function formatAssetValue(asset, value) {
if (asset === 'balance') return typeof value === 'string' && /^[+-]?\d+\.\d{2}$/.test(value) ? value : '0.00'
const raw = String(value ?? '0')
return /^-?\d+$/.test(raw) ? raw : '0'
}
async function loadLedger() {
ledgerLoading.value = true
try {
const params = { page: ledgerPage.value, per_page: 20 }
Object.entries(ledgerFilters).forEach(([key, value]) => {
if (value !== null && String(value).trim() !== '') params[key] = value
})
const response = await api.getCreditLog(params)
const data = response?.data || response || {}
ledgerRows.value = (data.items || []).map((row) => {
const entries = Array.isArray(row.entries) ? row.entries : []
return {
...row,
transaction_label: `#${row.id}`,
assets_label: (row.assets || []).join(' / '),
reference_label: [row.ref_type, row.ref_key].filter(Boolean).join(':') || '—',
actor_label: row.actor_user_id ? `#${row.actor_user_id}` : '系统',
delta_label: entries.map((entry) => {
const value = formatAssetValue(entry.asset, entry.delta)
const signed = entry.asset === 'points' && !value.startsWith('-') && value !== '0' ? `+${value}` : value
return `${entry.asset}:${signed}`
}).join(';'),
balance_label: entries.map((entry) => `${entry.asset}:${formatAssetValue(entry.asset, entry.balance_after)}`).join(';'),
}
})
ledgerPageCount.value = Math.max(1, Math.ceil(Number(data.total || 0) / Number(data.per_page || 20)))
} catch (e) {
message.error(e.message || '加载账本失败')
} finally {
ledgerLoading.value = false
}
}
function searchLedger() {
ledgerPage.value = 1
loadLedger()
}
function resetLedger() {
Object.assign(ledgerFilters, { asset: null, transaction_id: '', operation_key: '', ref_type: '', ref_key: '', actor_user_id: '' })
searchLedger()
}
async function loadCfg() {
try {
const response = await api.getCreditSettings()
const data = response?.data || response || {}
Object.assign(cfg, data)
streakJson.value = JSON.stringify(cfg.daily_signin?.streak || {}, null, 0)
} catch (e) {
message.error(e.message || '加载失败')
}
}
async function saveCfg() {
saving.value = true
try {
let streak = {}
try { streak = JSON.parse(streakJson.value || '{}') } catch (_) { /* ignore */ }
cfg.daily_signin.streak = streak
await api.saveCreditSettings(cfg)
message.success('已保存')
} catch (e) {
message.error(e.message || '保存失败')
} finally {
saving.value = false
}
}
onMounted(() => {
loadCfg()
loadLedger()
})
</script>