Merge pull request 'Worktree identity zibll p1' (#181) from worktree-identity-zibll-p1 into main
Reviewed-on: CYL/wordpress-theme-aksrt#181
This commit is contained in:
@@ -71,7 +71,7 @@ export const usersSection = {
|
||||
component: 'UsersVerify',
|
||||
status: 'available',
|
||||
title: '身份认证',
|
||||
desc: '身份认证字段与展示配置(审核队列在 用户 → 身份认证)。',
|
||||
desc: '认证展示、申请门槛与审核策略(审核队列在 用户 → 身份认证)。',
|
||||
canonicalDomain: 'users',
|
||||
section: 'verify',
|
||||
}),
|
||||
@@ -95,7 +95,7 @@ export const usersSection = {
|
||||
component: 'UsersBadge',
|
||||
status: 'available',
|
||||
title: '用户徽章',
|
||||
desc: '徽章规则、授予条件和前台展示。',
|
||||
desc: '勋章分类、自动授予条件(get_type)与前台佩戴展示。存储键 user_medal_*。',
|
||||
canonicalDomain: 'users',
|
||||
section: 'badge',
|
||||
}),
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<n-alert type="warning" :show-icon="true" style="margin: 12px 0 16px;">
|
||||
转账/提现(balance_transfer、points_transfer、balance_withdraw)默认 <b>exclude all</b>,与 Zibll 一致:未在矩阵中为角色/等级/VIP/认证打开前,前台入口会隐藏或拒绝。
|
||||
medal_manually_set 控制谁可授予徽章;管理员 manage_options 始终可通过。
|
||||
</n-alert>
|
||||
|
||||
|
||||
<n-spin :show="loading" style="width: 100%; min-height: 240px;">
|
||||
<n-card class="aksrt-section-card" v-if="summary.cap_count">
|
||||
<div class="aksrt-section-header">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader
|
||||
title="用户徽章"
|
||||
desc="维护徽章字典(含可选 icon_url)、展示数量。授予/收回在 WP 用户资料页;用户前台可佩戴徽章。"
|
||||
desc="配置勋章分类、自动授予条件(get_type / get_val)与公开展示。路由 /users/badge,存储键 user_medal_*。"
|
||||
>
|
||||
<template #actions>
|
||||
<n-space>
|
||||
@@ -15,24 +15,18 @@
|
||||
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>当前联动摘要</h3>
|
||||
<h3>当前摘要</h3>
|
||||
</div>
|
||||
<n-space>
|
||||
<n-tag round :type="form.user_badge_enabled ? 'success' : 'default'">
|
||||
徽章系统:{{ form.user_badge_enabled ? '已启用' : '已关闭' }}
|
||||
<n-tag round :type="form.user_medal_s ? 'success' : 'default'">
|
||||
徽章系统:{{ form.user_medal_s ? '已启用' : '已关闭' }}
|
||||
</n-tag>
|
||||
<n-tag round :type="form.user_badge_public_display ? 'info' : 'default'">
|
||||
公开展示:{{ form.user_badge_public_display ? '开启' : '关闭' }}
|
||||
</n-tag>
|
||||
<n-tag round type="info">
|
||||
最多显示:{{ form.user_badge_limit }} 枚
|
||||
</n-tag>
|
||||
<n-tag round :type="notificationSummary.dynamicBadge ? 'success' : 'default'">
|
||||
通知红点联动:{{ notificationSummary.dynamicBadge ? '启用' : '关闭' }}
|
||||
</n-tag>
|
||||
<n-tag round type="info">
|
||||
字典条目:{{ badgeCount }}
|
||||
<n-tag round :type="form.user_medal_public_display ? 'info' : 'default'">
|
||||
公开展示:{{ form.user_medal_public_display ? '开启' : '关闭' }}
|
||||
</n-tag>
|
||||
<n-tag round type="info">最多显示:{{ form.user_medal_limit }} 枚</n-tag>
|
||||
<n-tag round type="info">分类:{{ form.user_medal_args.length }}</n-tag>
|
||||
<n-tag round type="info">条目:{{ itemCount }}</n-tag>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
@@ -41,43 +35,77 @@
|
||||
<h3>徽章展示</h3>
|
||||
</div>
|
||||
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
||||
<n-form-item label="启用徽章系统">
|
||||
<n-switch v-model:value="form.user_badge_enabled" />
|
||||
<n-form-item label="启用徽章/勋章系统">
|
||||
<n-switch v-model:value="form.user_medal_s" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="公开页显示徽章" v-if="form.user_badge_enabled">
|
||||
<n-switch v-model:value="form.user_badge_public_display" />
|
||||
<n-form-item v-if="form.user_medal_s" label="公开页显示徽章">
|
||||
<n-switch v-model:value="form.user_medal_public_display" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="最多显示数量" v-if="form.user_badge_enabled">
|
||||
<n-input-number v-model:value="form.user_badge_limit" :min="1" :max="8" style="width: 180px;" />
|
||||
<n-form-item v-if="form.user_medal_s" label="最多显示数量">
|
||||
<n-input-number v-model:value="form.user_medal_limit" :min="1" :max="8" style="width: 180px;" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>徽章字典</h3>
|
||||
<n-card v-if="form.user_medal_s" class="aksrt-section-card">
|
||||
<div class="aksrt-section-header" style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h3>徽章明细(分类 → 条目)</h3>
|
||||
<n-button size="small" @click="addCategory">添加分类</n-button>
|
||||
</div>
|
||||
|
||||
<n-empty v-if="!form.user_medal_args.length" description="暂无分类,请添加" />
|
||||
|
||||
<div v-for="(cat, ci) in form.user_medal_args" :key="'cat-' + ci" class="medal-cat-block">
|
||||
<n-card size="small" embedded>
|
||||
<n-space vertical :size="12">
|
||||
<n-space align="center">
|
||||
<n-input v-model:value="cat.cat_name" placeholder="分类名称" style="width: 220px;" />
|
||||
<n-button size="small" @click="addItem(ci)">添加条目</n-button>
|
||||
<n-button size="small" quaternary type="error" @click="removeCategory(ci)">删除分类</n-button>
|
||||
</n-space>
|
||||
|
||||
<n-empty v-if="!(cat.items && cat.items.length)" description="该分类暂无条目" />
|
||||
|
||||
<div v-for="(item, ii) in cat.items || []" :key="'item-' + ci + '-' + ii" class="medal-item-row">
|
||||
<n-grid cols="1 s:2 m:3" :x-gap="12" :y-gap="8" responsive="screen">
|
||||
<n-grid-item>
|
||||
<n-input v-model:value="item.slug" placeholder="slug" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-input v-model:value="item.name" placeholder="名称" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-select v-model:value="item.get_type" :options="getTypeOptions" placeholder="获取条件" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-input-number
|
||||
v-model:value="item.get_val"
|
||||
:min="0"
|
||||
:disabled="item.get_type === 'manually_add'"
|
||||
placeholder="阈值"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-input v-model:value="item.icon" placeholder="图标 URL" />
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-input v-model:value="item.desc" placeholder="描述" />
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
<div style="margin-top: 8px; text-align: right;">
|
||||
<n-button size="tiny" quaternary type="error" @click="removeItem(ci, ii)">删除条目</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
||||
<n-form-item label="徽章列表">
|
||||
<n-input
|
||||
v-model:value="form.user_badge_dictionary"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 5, maxRows: 10 }"
|
||||
placeholder="founder|创始成员|warning staff|站务团队|info|https://example.com/medal.png"
|
||||
:disabled="!form.user_badge_enabled"
|
||||
/>
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">每行:`slug|显示文案|色调|图标URL(可选)`。色调:neutral/info/success/warning/brand。授予在用户资料页;前台「我的徽章」可佩戴。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<n-card class="aksrt-section-card">
|
||||
<n-alert type="info" :show-icon="false">
|
||||
字典定义合法徽章;`aksrt_user_badges` 存已授予 slug;`aksrt_wear_badge` 为当前佩戴。公开页/评论通过 identity chips 展示(有 icon_url 时为勋章图)。
|
||||
前台已授予徽章保存在用户 meta aksrt_user_badges;佩戴为 aksrt_wear_badge(slug)。
|
||||
字典来自 user_medal_args;运行时 AKSRT_User_Badges 读取 identity badge.dictionary 派生列表。
|
||||
</n-alert>
|
||||
</n-card>
|
||||
|
||||
@@ -94,31 +122,25 @@ import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import api from '@/api'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import { MEDAL_GET_TYPES, defaultMedalArgs } from '@/schema/sections/users/badge.js'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const getTypeOptions = MEDAL_GET_TYPES
|
||||
|
||||
const form = reactive({
|
||||
user_badge_enabled: false,
|
||||
user_badge_public_display: true,
|
||||
user_badge_limit: 3,
|
||||
user_badge_dictionary: 'founder|创始成员|warning\nstaff|站务团队|info',
|
||||
user_medal_s: false,
|
||||
user_medal_public_display: true,
|
||||
user_medal_limit: 3,
|
||||
user_medal_args: defaultMedalArgs(),
|
||||
})
|
||||
|
||||
const notificationSummary = reactive({
|
||||
dynamicBadge: false,
|
||||
})
|
||||
|
||||
const badgeCount = computed(() => {
|
||||
return String(form.user_badge_dictionary || '')
|
||||
.split(/\r?\n/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.length
|
||||
})
|
||||
const itemCount = computed(() =>
|
||||
form.user_medal_args.reduce((n, c) => n + (Array.isArray(c.items) ? c.items.length : 0), 0),
|
||||
)
|
||||
|
||||
function go(path) {
|
||||
router.push(path)
|
||||
@@ -128,23 +150,70 @@ function extractData(response) {
|
||||
return response?.data ?? response ?? {}
|
||||
}
|
||||
|
||||
function emptyItem() {
|
||||
return {
|
||||
slug: '',
|
||||
name: '',
|
||||
desc: '',
|
||||
icon: '',
|
||||
get_type: 'manually_add',
|
||||
get_val: 0,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeArgs(raw) {
|
||||
if (!Array.isArray(raw)) {
|
||||
return defaultMedalArgs()
|
||||
}
|
||||
return raw.map((cat) => ({
|
||||
cat_name: String(cat?.cat_name || cat?.name || '未命名').trim() || '未命名',
|
||||
items: Array.isArray(cat?.items)
|
||||
? cat.items.map((it) => ({
|
||||
slug: String(it?.slug || '').trim(),
|
||||
name: String(it?.name || '').trim(),
|
||||
desc: String(it?.desc || '').trim(),
|
||||
icon: String(it?.icon || it?.icon_url || '').trim(),
|
||||
get_type: String(it?.get_type || 'manually_add').trim() || 'manually_add',
|
||||
get_val: Math.max(0, Number(it?.get_val ?? 0) || 0),
|
||||
}))
|
||||
: [],
|
||||
}))
|
||||
}
|
||||
|
||||
function addCategory() {
|
||||
form.user_medal_args.push({ cat_name: '新分类', items: [emptyItem()] })
|
||||
}
|
||||
|
||||
function removeCategory(ci) {
|
||||
form.user_medal_args.splice(ci, 1)
|
||||
}
|
||||
|
||||
function addItem(ci) {
|
||||
if (!form.user_medal_args[ci].items) {
|
||||
form.user_medal_args[ci].items = []
|
||||
}
|
||||
form.user_medal_args[ci].items.push(emptyItem())
|
||||
}
|
||||
|
||||
function removeItem(ci, ii) {
|
||||
form.user_medal_args[ci].items.splice(ii, 1)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [schemaRes, notificationRes] = await Promise.all([
|
||||
api.getSchemaPage('users', 'badge'),
|
||||
api.getNotificationSettings(),
|
||||
])
|
||||
|
||||
const schemaRes = await api.getSchemaPage('users', 'badge')
|
||||
const schemaData = extractData(schemaRes)
|
||||
const notificationData = extractData(notificationRes)
|
||||
|
||||
form.user_badge_enabled = !!schemaData.user_badge_enabled
|
||||
form.user_badge_public_display = schemaData.user_badge_public_display !== false
|
||||
form.user_badge_limit = Number(schemaData.user_badge_limit || 3)
|
||||
form.user_badge_dictionary = schemaData.user_badge_dictionary || 'founder|创始成员|warning\nstaff|站务团队|info'
|
||||
|
||||
notificationSummary.dynamicBadge = !!notificationData.user_badge_enabled
|
||||
form.user_medal_s = !!(schemaData.user_medal_s ?? schemaData.user_badge_enabled)
|
||||
form.user_medal_public_display =
|
||||
schemaData.user_medal_public_display !== undefined
|
||||
? schemaData.user_medal_public_display !== false
|
||||
: schemaData.user_badge_public_display !== false
|
||||
form.user_medal_limit = Number(
|
||||
schemaData.user_medal_limit ?? schemaData.user_badge_limit ?? 3,
|
||||
)
|
||||
form.user_medal_args = normalizeArgs(schemaData.user_medal_args ?? null)
|
||||
} catch (error) {
|
||||
message.error(error.message || '加载用户徽章配置失败')
|
||||
} finally {
|
||||
@@ -155,12 +224,23 @@ async function load() {
|
||||
async function handleSave() {
|
||||
saving.value = true
|
||||
try {
|
||||
const args = normalizeArgs(form.user_medal_args)
|
||||
for (const cat of args) {
|
||||
for (const it of cat.items) {
|
||||
if (!it.slug || !it.name || !it.get_type) {
|
||||
message.error('每个条目需要 slug、名称和获取条件')
|
||||
saving.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
await api.saveSchemaPage('users', 'badge', {
|
||||
user_badge_enabled: form.user_badge_enabled,
|
||||
user_badge_public_display: form.user_badge_public_display,
|
||||
user_badge_limit: Number(form.user_badge_limit || 3),
|
||||
user_badge_dictionary: form.user_badge_dictionary,
|
||||
user_medal_s: !!form.user_medal_s,
|
||||
user_medal_public_display: !!form.user_medal_public_display,
|
||||
user_medal_limit: Number(form.user_medal_limit || 3),
|
||||
user_medal_args: args,
|
||||
})
|
||||
form.user_medal_args = args
|
||||
message.success('用户徽章配置已保存')
|
||||
} catch (error) {
|
||||
message.error(error.message || '保存失败')
|
||||
@@ -171,3 +251,15 @@ async function handleSave() {
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.medal-cat-block {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.medal-item-row {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--n-border-color, #efeff5);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader
|
||||
title="用户等级"
|
||||
desc="配置等级来源、展示前缀和阈值规则。前台公开主页会按照这里的规则输出等级身份芯片。"
|
||||
desc="配置等级开关、最高等级、升级经验阈值与经验获取矩阵(对齐 Zibll 用户等级 / 经验值)。"
|
||||
>
|
||||
<template #actions>
|
||||
<n-space>
|
||||
@@ -14,80 +14,64 @@
|
||||
|
||||
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>当前联动摘要</h3>
|
||||
</div>
|
||||
<div class="aksrt-section-header"><h3>当前摘要</h3></div>
|
||||
<n-space>
|
||||
<n-tag round :type="form.user_level_enabled ? 'success' : 'default'">
|
||||
等级系统:{{ form.user_level_enabled ? '已启用' : '已关闭' }}
|
||||
</n-tag>
|
||||
<n-tag round :type="creditsSummary.enabled ? 'info' : 'warning'">
|
||||
积分系统:{{ creditsSummary.enabled ? '已启用' : '未启用' }}
|
||||
</n-tag>
|
||||
<n-tag round type="info">
|
||||
当前货币:{{ creditsSummary.currencyName }}
|
||||
</n-tag>
|
||||
<n-tag round :type="form.user_level_source === 'credits_balance' ? 'success' : 'warning'">
|
||||
来源:{{ form.user_level_source === 'credits_balance' ? '积分余额' : '手动 Meta' }}
|
||||
</n-tag>
|
||||
<n-tag round type="info">
|
||||
阈值条目:{{ thresholdCount }}
|
||||
<n-tag round :type="form.user_level_s ? 'success' : 'default'">
|
||||
等级系统:{{ form.user_level_s ? '已启用' : '已关闭' }}
|
||||
</n-tag>
|
||||
<n-tag round type="info">最高等级:{{ form.user_level_max }}</n-tag>
|
||||
<n-tag round type="info">日经验上限:{{ form.user_integral_opt.day_max }}</n-tag>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>等级显示</h3>
|
||||
</div>
|
||||
<div class="aksrt-section-header"><h3>等级显示</h3></div>
|
||||
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
||||
<n-form-item label="启用等级系统">
|
||||
<n-switch v-model:value="form.user_level_enabled" />
|
||||
<n-form-item label="启用用户等级">
|
||||
<n-switch v-model:value="form.user_level_s" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="公开页显示等级" v-if="form.user_level_enabled">
|
||||
<n-form-item v-if="form.user_level_s" label="公开页显示等级">
|
||||
<n-switch v-model:value="form.user_level_public_display" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="等级前缀" v-if="form.user_level_enabled">
|
||||
<n-input v-model:value="form.user_level_prefix" maxlength="12" placeholder="Lv." />
|
||||
<n-form-item v-if="form.user_level_s" label="等级前缀">
|
||||
<n-input v-model:value="form.user_level_prefix" maxlength="12" style="width: 180px;" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="form.user_level_s" label="最高等级">
|
||||
<n-input-number v-model:value="form.user_level_max" :min="3" :max="30" style="width: 180px;" @update:value="onMaxChange" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="form.user_level_s" label="升级通知">
|
||||
<n-switch v-model:value="form.user_level_notify" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>等级规则</h3>
|
||||
</div>
|
||||
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
||||
<n-form-item label="等级来源">
|
||||
<n-radio-group v-model:value="form.user_level_source" :disabled="!form.user_level_enabled">
|
||||
<n-space size="large">
|
||||
<n-radio value="credits_balance">积分余额</n-radio>
|
||||
<n-radio value="manual_meta">手动 Meta</n-radio>
|
||||
</n-space>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="等级阈值">
|
||||
<n-input
|
||||
v-model:value="form.user_level_thresholds"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 5, maxRows: 10 }"
|
||||
placeholder="1|0 2|30 3|100 4|300"
|
||||
:disabled="!form.user_level_enabled"
|
||||
/>
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">每行一个等级,格式 `等级|所需积分`。手动 Meta 模式下仍建议保留完整字典,方便后续切换回自动计算。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<n-card class="aksrt-section-card">
|
||||
<n-alert type="info" :show-icon="false">
|
||||
手动模式读取 `aksrt_user_level` user meta;自动模式则根据当前积分余额和上方阈值表实时计算等级,不需要单独写库。
|
||||
<n-card v-if="form.user_level_s" class="aksrt-section-card">
|
||||
<div class="aksrt-section-header"><h3>升级所需经验(累计)</h3></div>
|
||||
<n-alert type="info" :show-icon="false" style="margin-bottom: 12px;">
|
||||
Lv.1 通常为 0。数值为升到该级所需的累计经验。修改最高等级后会自动补齐/截断行。
|
||||
</n-alert>
|
||||
<n-data-table :columns="levelColumns" :data="levelRows" :bordered="false" size="small" />
|
||||
</n-card>
|
||||
|
||||
<n-card v-if="form.user_level_s" class="aksrt-section-card">
|
||||
<div class="aksrt-section-header"><h3>经验值得分</h3></div>
|
||||
<n-form :model="form" label-placement="left" label-width="180" class="aksrt-form">
|
||||
<n-form-item label="每日上限">
|
||||
<n-input-number v-model:value="form.user_integral_opt.day_max" :min="0" :max="10000" style="width: 180px;" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-divider />
|
||||
<div v-for="group in integralGroups" :key="group" style="margin-bottom: 16px;">
|
||||
<h4 style="margin: 0 0 8px;">{{ group }}</h4>
|
||||
<n-grid cols="1 s:2 m:3" :x-gap="12" :y-gap="8" responsive="screen">
|
||||
<n-grid-item v-for="row in INTEGRAL_CATALOG.filter((r) => r.group === group)" :key="row.key">
|
||||
<div class="integral-field">
|
||||
<div class="integral-field__label">{{ row.label }}</div>
|
||||
<n-input-number v-model:value="form.user_integral_opt[row.key]" :min="0" :max="1000" style="width: 100%;" />
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<div class="aksrt-form-actions">
|
||||
@@ -98,38 +82,78 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { NInput, NInputNumber, useMessage } from 'naive-ui'
|
||||
import api from '@/api'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import {
|
||||
INTEGRAL_CATALOG,
|
||||
defaultIntegralOpt,
|
||||
defaultLevelOpt,
|
||||
} from '@/schema/sections/users/level.js'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
const form = reactive({
|
||||
user_level_enabled: false,
|
||||
user_level_s: true,
|
||||
user_level_public_display: true,
|
||||
user_level_source: 'credits_balance',
|
||||
user_level_prefix: 'Lv.',
|
||||
user_level_thresholds: '1|0\n2|30\n3|100\n4|300',
|
||||
user_level_max: 10,
|
||||
user_level_opt: defaultLevelOpt(10),
|
||||
user_integral_opt: defaultIntegralOpt(),
|
||||
user_level_notify: true,
|
||||
})
|
||||
|
||||
const creditsSummary = reactive({
|
||||
enabled: true,
|
||||
currencyName: '积分',
|
||||
const integralGroups = computed(() => [...new Set(INTEGRAL_CATALOG.map((r) => r.group))])
|
||||
|
||||
const levelRows = computed(() => {
|
||||
const max = Math.max(3, Math.min(30, Number(form.user_level_max) || 10))
|
||||
const rows = []
|
||||
for (let i = 1; i <= max; i++) {
|
||||
rows.push({
|
||||
level: i,
|
||||
name: form.user_level_opt['name_' + i] ?? form.user_level_prefix + i,
|
||||
upgrade: Number(form.user_level_opt['upgrade_integral_' + i] ?? 0),
|
||||
})
|
||||
}
|
||||
return rows
|
||||
})
|
||||
|
||||
const thresholdCount = computed(() => {
|
||||
return String(form.user_level_thresholds || '')
|
||||
.split(/\r?\n/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.length
|
||||
})
|
||||
const levelColumns = [
|
||||
{ title: '等级', key: 'level', width: 80 },
|
||||
{
|
||||
title: '名称',
|
||||
key: 'name',
|
||||
render: (row) =>
|
||||
h(NInput, {
|
||||
value: form.user_level_opt['name_' + row.level] ?? '',
|
||||
size: 'small',
|
||||
onUpdateValue: (v) => {
|
||||
form.user_level_opt['name_' + row.level] = v
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '升级所需累计经验',
|
||||
key: 'upgrade',
|
||||
width: 200,
|
||||
render: (row) =>
|
||||
h(NInputNumber, {
|
||||
value: Number(form.user_level_opt['upgrade_integral_' + row.level] ?? 0),
|
||||
min: 0,
|
||||
max: 9999999,
|
||||
size: 'small',
|
||||
style: 'width: 160px;',
|
||||
onUpdateValue: (v) => {
|
||||
form.user_level_opt['upgrade_integral_' + row.level] = Number(v || 0)
|
||||
},
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
function go(path) {
|
||||
router.push(path)
|
||||
@@ -139,25 +163,74 @@ function extractData(response) {
|
||||
return response?.data ?? response ?? {}
|
||||
}
|
||||
|
||||
function onMaxChange(val) {
|
||||
const max = Math.max(3, Math.min(30, Number(val) || 10))
|
||||
form.user_level_max = max
|
||||
const next = { ...form.user_level_opt }
|
||||
const filled = defaultLevelOpt(max)
|
||||
for (let i = 1; i <= max; i++) {
|
||||
if (next['upgrade_integral_' + i] === undefined) {
|
||||
next['upgrade_integral_' + i] = filled['upgrade_integral_' + i]
|
||||
}
|
||||
if (next['name_' + i] === undefined) {
|
||||
next['name_' + i] = form.user_level_prefix + i
|
||||
}
|
||||
}
|
||||
// drop extras
|
||||
for (const k of Object.keys(next)) {
|
||||
const m = k.match(/_(\d+)$/)
|
||||
if (m && Number(m[1]) > max) {
|
||||
delete next[k]
|
||||
}
|
||||
}
|
||||
form.user_level_opt = next
|
||||
}
|
||||
|
||||
function normalizeIntegral(raw) {
|
||||
const base = defaultIntegralOpt()
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return base
|
||||
}
|
||||
for (const row of INTEGRAL_CATALOG) {
|
||||
if (raw[row.key] !== undefined) {
|
||||
base[row.key] = Math.max(0, Number(raw[row.key]) || 0)
|
||||
}
|
||||
}
|
||||
if (raw.day_max !== undefined) {
|
||||
base.day_max = Math.max(0, Number(raw.day_max) || 0)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
function normalizeLevelOpt(raw, max) {
|
||||
const base = defaultLevelOpt(max)
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return base
|
||||
}
|
||||
for (let i = 1; i <= max; i++) {
|
||||
if (raw['upgrade_integral_' + i] !== undefined) {
|
||||
base['upgrade_integral_' + i] = Math.max(0, Number(raw['upgrade_integral_' + i]) || 0)
|
||||
}
|
||||
if (raw['name_' + i] !== undefined) {
|
||||
base['name_' + i] = String(raw['name_' + i] || '')
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [schemaRes, creditRes] = await Promise.all([
|
||||
api.getSchemaPage('users', 'level'),
|
||||
api.getCreditSettings(),
|
||||
])
|
||||
|
||||
const schemaData = extractData(schemaRes)
|
||||
const creditData = extractData(creditRes)
|
||||
|
||||
form.user_level_enabled = !!schemaData.user_level_enabled
|
||||
form.user_level_public_display = schemaData.user_level_public_display !== false
|
||||
form.user_level_source = schemaData.user_level_source || 'credits_balance'
|
||||
form.user_level_prefix = schemaData.user_level_prefix || 'Lv.'
|
||||
form.user_level_thresholds = schemaData.user_level_thresholds || '1|0\n2|30\n3|100\n4|300'
|
||||
|
||||
creditsSummary.enabled = creditData.enabled !== false
|
||||
creditsSummary.currencyName = creditData.currency_name || '积分'
|
||||
const schemaRes = await api.getSchemaPage('users', 'level')
|
||||
const d = extractData(schemaRes)
|
||||
const enabled = d.user_level_s !== undefined ? !!d.user_level_s : !!d.user_level_enabled
|
||||
form.user_level_s = enabled
|
||||
form.user_level_public_display = d.user_level_public_display !== false
|
||||
form.user_level_prefix = d.user_level_prefix || 'Lv.'
|
||||
form.user_level_max = Math.max(3, Math.min(30, Number(d.user_level_max || 10)))
|
||||
form.user_level_notify = d.user_level_notify !== false
|
||||
form.user_level_opt = normalizeLevelOpt(d.user_level_opt, form.user_level_max)
|
||||
form.user_integral_opt = normalizeIntegral(d.user_integral_opt)
|
||||
} catch (error) {
|
||||
message.error(error.message || '加载用户等级配置失败')
|
||||
} finally {
|
||||
@@ -168,12 +241,23 @@ async function load() {
|
||||
async function handleSave() {
|
||||
saving.value = true
|
||||
try {
|
||||
await api.saveSchemaPage('users', 'level', {
|
||||
user_level_enabled: form.user_level_enabled,
|
||||
user_level_public_display: form.user_level_public_display,
|
||||
user_level_source: form.user_level_source,
|
||||
user_level_prefix: form.user_level_prefix,
|
||||
user_level_thresholds: form.user_level_thresholds,
|
||||
const max = Math.max(3, Math.min(30, Number(form.user_level_max) || 10))
|
||||
const payload = {
|
||||
user_level_s: !!form.user_level_s,
|
||||
user_level_enabled: !!form.user_level_s,
|
||||
user_level_public_display: !!form.user_level_public_display,
|
||||
user_level_prefix: form.user_level_prefix || 'Lv.',
|
||||
user_level_max: max,
|
||||
user_level_notify: !!form.user_level_notify,
|
||||
user_level_source: 'integral',
|
||||
user_level_opt: normalizeLevelOpt(form.user_level_opt, max),
|
||||
user_integral_opt: normalizeIntegral(form.user_integral_opt),
|
||||
}
|
||||
await api.saveSchemaPage('users', 'level', payload)
|
||||
Object.assign(form, {
|
||||
user_level_max: max,
|
||||
user_level_opt: payload.user_level_opt,
|
||||
user_integral_opt: payload.user_integral_opt,
|
||||
})
|
||||
message.success('用户等级配置已保存')
|
||||
} catch (error) {
|
||||
@@ -185,3 +269,11 @@ async function handleSave() {
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.integral-field__label {
|
||||
font-size: 12px;
|
||||
margin-bottom: 4px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -54,6 +54,43 @@
|
||||
<n-form-item label="启用人工审核台">
|
||||
<n-switch v-model:value="form.user_verify_require_manual_review" :disabled="!form.user_verify_enabled" />
|
||||
</n-form-item>
|
||||
<n-form-item label="申请类型选项">
|
||||
<n-input
|
||||
v-model:value="form.user_verify_apply_option"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 6 }"
|
||||
:disabled="!form.user_verify_enabled"
|
||||
placeholder="slug|显示名称,每行一条"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="允许用户前台申请">
|
||||
<n-switch v-model:value="form.user_verify_allow_apply" :disabled="!form.user_verify_enabled" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="!form.user_verify_allow_apply" label="停用申请说明">
|
||||
<n-input
|
||||
v-model:value="form.user_verify_disable_desc"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 2, maxRows: 6 }"
|
||||
:disabled="!form.user_verify_enabled"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="form.user_verify_allow_apply" label="申请要求等级">
|
||||
<n-input-number
|
||||
v-model:value="form.user_verify_limit_level"
|
||||
:min="0"
|
||||
:max="99"
|
||||
:disabled="!form.user_verify_enabled"
|
||||
style="width: 180px;"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="form.user_verify_allow_apply" label="申请介绍">
|
||||
<n-input
|
||||
v-model:value="form.user_verify_desc"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 3, maxRows: 8 }"
|
||||
:disabled="!form.user_verify_enabled"
|
||||
/>
|
||||
</n-form-item>
|
||||
<div class="aksrt-form-actions">
|
||||
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
|
||||
</div>
|
||||
@@ -118,6 +155,11 @@ const form = reactive({
|
||||
user_verify_badge_text: '已认证',
|
||||
user_verify_accept_email_verified: true,
|
||||
user_verify_require_manual_review: false,
|
||||
user_verify_allow_apply: true,
|
||||
user_verify_disable_desc: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
|
||||
user_verify_limit_level: 2,
|
||||
user_verify_desc: '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
|
||||
user_verify_apply_option: 'real_name|实名认证\norganization|企业认证',
|
||||
})
|
||||
|
||||
const authSummary = reactive({
|
||||
@@ -276,6 +318,14 @@ async function load() {
|
||||
form.user_verify_badge_text = schemaData.user_verify_badge_text || '已认证'
|
||||
form.user_verify_accept_email_verified = schemaData.user_verify_accept_email_verified !== false
|
||||
form.user_verify_require_manual_review = !!schemaData.user_verify_require_manual_review
|
||||
form.user_verify_allow_apply = schemaData.user_verify_allow_apply !== false
|
||||
form.user_verify_disable_desc = schemaData.user_verify_disable_desc
|
||||
|| '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员'
|
||||
form.user_verify_limit_level = Number(schemaData.user_verify_limit_level ?? 2)
|
||||
form.user_verify_desc = schemaData.user_verify_desc
|
||||
|| '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。'
|
||||
form.user_verify_apply_option = schemaData.user_verify_apply_option
|
||||
|| 'real_name|实名认证\norganization|企业认证'
|
||||
authSummary.emailVerificationEnabled = !!authData.registration_email_verification_enabled
|
||||
|
||||
await loadRequests()
|
||||
@@ -311,6 +361,11 @@ async function handleSave() {
|
||||
user_verify_badge_text: form.user_verify_badge_text,
|
||||
user_verify_accept_email_verified: form.user_verify_accept_email_verified,
|
||||
user_verify_require_manual_review: form.user_verify_require_manual_review,
|
||||
user_verify_allow_apply: form.user_verify_allow_apply,
|
||||
user_verify_disable_desc: form.user_verify_disable_desc,
|
||||
user_verify_limit_level: Number(form.user_verify_limit_level || 0),
|
||||
user_verify_desc: form.user_verify_desc,
|
||||
user_verify_apply_option: form.user_verify_apply_option,
|
||||
})
|
||||
message.success('身份认证配置已保存')
|
||||
} catch (error) {
|
||||
|
||||
@@ -49,10 +49,11 @@ export const pageSchema = {
|
||||
post_article_video_cover_scale: 56,
|
||||
item_heading_bold: false,
|
||||
post_button_toptext: '喜欢就支持一下吧',
|
||||
post_rewards_enabled: false,
|
||||
post_rewards_text: '',
|
||||
post_rewards_image: '',
|
||||
post_rewards_btn_text: '赞赏',
|
||||
post_rewards_s: false,
|
||||
post_rewards_text: '赞赏',
|
||||
post_author_card_limit: 6,
|
||||
post_author_card_orderby: 'views',
|
||||
post_author_card_style: 'card',
|
||||
article_maxheight_kg: false,
|
||||
article_maxheight: 1000,
|
||||
},
|
||||
@@ -71,6 +72,17 @@ export const pageSchema = {
|
||||
{ key: 'breadcrumbs_single_text', label: '末项显示「正文」', type: 'switch', default: true, dependsOn: { key: 'breadcrumbs_single_s', value: true }, help: '关闭后末项显示文章标题' },
|
||||
{ key: 'post_prevnext_s', label: '上一篇/下一篇', type: 'switch', default: true },
|
||||
{ key: 'post_authordesc_s', label: '作者简介', type: 'switch', default: true },
|
||||
{ key: 'post_author_card_limit', label: '作者卡文章数', type: 'number', default: 6, min: 0, max: 12, dependsOn: { key: 'post_authordesc_s', value: true } },
|
||||
{ key: 'post_author_card_orderby', label: '作者卡文章排序', type: 'select', default: 'views', options: [
|
||||
{ label: '浏览量', value: 'views' },
|
||||
{ label: '发布时间', value: 'date' },
|
||||
{ label: '更新时间', value: 'modified' },
|
||||
{ label: '评论数', value: 'comment_count' },
|
||||
], dependsOn: { key: 'post_authordesc_s', value: true } },
|
||||
{ key: 'post_author_card_style', label: '作者卡文章样式', type: 'radio', default: 'card', options: [
|
||||
{ label: '卡片', value: 'card' },
|
||||
{ label: '列表', value: 'list' },
|
||||
], dependsOn: { key: 'post_authordesc_s', value: true } },
|
||||
{ key: 'post_single_hide_time_s', label: '隐藏文章时间', type: 'switch', default: false },
|
||||
],
|
||||
},
|
||||
@@ -137,10 +149,8 @@ export const pageSchema = {
|
||||
{ key: 'post_copyright_enabled', label: '版权信息', type: 'switch', default: true },
|
||||
{ key: 'post_copyright_text', label: '版权默认文案', type: 'textarea', default: '', help: '留空则自动生成"本文由 XX 原创发布"' },
|
||||
{ key: 'post_button_toptext', label: '按钮上方文案', type: 'input', default: '喜欢就支持一下吧', help: '显示在点赞/打赏/分享按钮上方' },
|
||||
{ key: 'post_rewards_enabled', label: '打赏', type: 'switch', default: false },
|
||||
{ key: 'post_rewards_btn_text', label: '打赏按钮文案', type: 'input', default: '赞赏', dependsOn: { key: 'post_rewards_enabled', value: true } },
|
||||
{ key: 'post_rewards_text', label: '打赏说明', type: 'input', default: '', dependsOn: { key: 'post_rewards_enabled', value: true } },
|
||||
{ key: 'post_rewards_image', label: '打赏二维码 URL', type: 'input', default: '', dependsOn: { key: 'post_rewards_enabled', value: true } },
|
||||
{ key: 'post_rewards_s', label: '用户打赏', type: 'switch', default: false, help: '开启后用户可配置微信/支付宝收款码;无码则不显示按钮。' },
|
||||
{ key: 'post_rewards_text', label: '打赏按钮文案', type: 'input', default: '赞赏', dependsOn: { key: 'post_rewards_s', value: true } },
|
||||
{ key: 'article_maxheight_kg', label: '正文限高', type: 'switch', default: false, help: '开启后长文默认折叠,可展开全文' },
|
||||
{ key: 'article_maxheight', label: '限高像素', type: 'number', default: 1000, min: 200, max: 5000, dependsOn: { key: 'article_maxheight_kg', value: true } },
|
||||
],
|
||||
|
||||
@@ -1,13 +1,75 @@
|
||||
/**
|
||||
* User medal catalog (SPA route remains /users/badge).
|
||||
* Storage keys: user_medal_* (Zibll user_medal_s / user_medal_args parity).
|
||||
*/
|
||||
|
||||
export const MEDAL_GET_TYPES = [
|
||||
{ label: '注册天数', value: 'registration_time' },
|
||||
{ label: '累计签到天数', value: 'checkin_all_day' },
|
||||
{ label: '发布文章数', value: 'new_post' },
|
||||
{ label: '论坛发帖数', value: 'bbs_post' },
|
||||
{ label: '评论数', value: 'comment' },
|
||||
{ label: '粉丝数', value: 'followed' },
|
||||
{ label: '文章获赞', value: 'like' },
|
||||
{ label: '评论获赞', value: 'comment_like' },
|
||||
{ label: '回复被采纳', value: 'bbs_adopt' },
|
||||
{ label: '神评次数', value: 'bbs_comment_hot' },
|
||||
{ label: '热帖次数', value: 'bbs_post_hot' },
|
||||
{ label: '精华帖数', value: 'bbs_essence' },
|
||||
{ label: '帖子评分累计', value: 'bbs_score' },
|
||||
{ label: '内容阅读量', value: 'views' },
|
||||
{ label: '创作分成累计(元)', value: 'pay_income' },
|
||||
{ label: '推广佣金累计(元)', value: 'pay_rebate' },
|
||||
{ label: '仅手动授予', value: 'manually_add' },
|
||||
]
|
||||
|
||||
export const defaultMedalArgs = () => [
|
||||
{
|
||||
cat_name: '成长',
|
||||
items: [
|
||||
{
|
||||
slug: 'rookie',
|
||||
name: '初出茅庐',
|
||||
desc: '注册时间超过30天',
|
||||
icon: '',
|
||||
get_type: 'registration_time',
|
||||
get_val: 30,
|
||||
},
|
||||
{
|
||||
slug: 'checkin-30',
|
||||
name: '签到达人',
|
||||
desc: '累计签到30天',
|
||||
icon: '',
|
||||
get_type: 'checkin_all_day',
|
||||
get_val: 30,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
cat_name: '限定',
|
||||
items: [
|
||||
{
|
||||
slug: 'annual-star',
|
||||
name: '年度人气达人',
|
||||
desc: '活动限定徽章',
|
||||
icon: '',
|
||||
get_type: 'manually_add',
|
||||
get_val: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const badgeSchema = {
|
||||
key: 'users.badge',
|
||||
title: '用户徽章',
|
||||
icon: 'fas fa-award',
|
||||
description: '配置徽章字典、公开展示数量和前台挂点,具体授予逻辑后续扩展。',
|
||||
description: '配置勋章分类、自动授予条件与前台展示。路由仍为 /users/badge;存储键为 user_medal_*。',
|
||||
defaults: {
|
||||
user_badge_enabled: false,
|
||||
user_badge_public_display: true,
|
||||
user_badge_limit: 3,
|
||||
user_badge_dictionary: 'founder|创始成员|warning\nstaff|站务团队|info',
|
||||
user_medal_s: false,
|
||||
user_medal_public_display: true,
|
||||
user_medal_limit: 3,
|
||||
user_medal_args: defaultMedalArgs(),
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
@@ -15,23 +77,37 @@ export const badgeSchema = {
|
||||
title: '徽章展示',
|
||||
icon: 'fas fa-medal',
|
||||
fields: [
|
||||
{ key: 'user_badge_enabled', label: '启用徽章系统', type: 'switch', default: false },
|
||||
{ key: 'user_badge_public_display', label: '公开页显示徽章', type: 'switch', default: true, dependsOn: { key: 'user_badge_enabled', value: true } },
|
||||
{ key: 'user_badge_limit', label: '最多显示数量', type: 'number', default: 3, min: 1, max: 8, dependsOn: { key: 'user_badge_enabled', value: true } },
|
||||
{ key: 'user_medal_s', label: '启用徽章/勋章系统', type: 'switch', default: false },
|
||||
{
|
||||
key: 'user_medal_public_display',
|
||||
label: '公开页显示徽章',
|
||||
type: 'switch',
|
||||
default: true,
|
||||
dependsOn: { key: 'user_medal_s', value: true },
|
||||
},
|
||||
{
|
||||
key: 'user_medal_limit',
|
||||
label: '最多显示数量',
|
||||
type: 'number',
|
||||
default: 3,
|
||||
min: 1,
|
||||
max: 8,
|
||||
dependsOn: { key: 'user_medal_s', value: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'dictionary',
|
||||
title: '徽章字典',
|
||||
icon: 'fas fa-bookmark',
|
||||
key: 'catalog',
|
||||
title: '徽章明细',
|
||||
icon: 'fas fa-trophy',
|
||||
fields: [
|
||||
{
|
||||
key: 'user_badge_dictionary',
|
||||
label: '徽章列表',
|
||||
type: 'textarea',
|
||||
default: 'founder|创始成员|warning\nstaff|站务团队|info',
|
||||
help: '每行一个徽章:slug|显示文案|色调(neutral/info/success/warning/brand)|图标URL(可选)。有 icon_url 时前台显示勋章图;用户 meta aksrt_wear_badge 指定当前佩戴一枚。',
|
||||
dependsOn: { key: 'user_badge_enabled', value: true },
|
||||
key: 'user_medal_args',
|
||||
label: '分类与条目',
|
||||
type: 'json',
|
||||
default: defaultMedalArgs(),
|
||||
help: '由自定义页编辑:分类 → 条目(slug/名称/描述/图标/get_type/get_val)。仅手动授予用 manually_add。',
|
||||
dependsOn: { key: 'user_medal_s', value: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,33 +1,82 @@
|
||||
/**
|
||||
* User level + integral XP (Zibll user_level_s / user_level_opt / user_integral_opt).
|
||||
*/
|
||||
|
||||
export const INTEGRAL_CATALOG = [
|
||||
{ key: 'sign_up', label: '首次注册', default: 20, group: '用户' },
|
||||
{ key: 'sign_in', label: '每日登录', default: 5, group: '用户' },
|
||||
{ key: 'followed', label: '被关注', default: 5, group: '用户' },
|
||||
{ key: 'post_new', label: '发布文章', default: 5, group: '文章' },
|
||||
{ key: 'post_like', label: '文章获赞', default: 1, group: '文章' },
|
||||
{ key: 'post_favorite', label: '文章被收藏', default: 2, group: '文章' },
|
||||
{ key: 'comment_new', label: '发表评论', default: 2, group: '文章' },
|
||||
{ key: 'comment_like', label: '评论获赞', default: 1, group: '文章' },
|
||||
{ key: 'bbs_posts_new', label: '发布帖子', default: 3, group: '论坛' },
|
||||
{ key: 'bbs_score_extra', label: '帖子被加分', default: 1, group: '论坛' },
|
||||
{ key: 'bbs_essence', label: '帖子评为精华', default: 2, group: '论坛' },
|
||||
{ key: 'bbs_posts_hot', label: '帖子成为热门', default: 2, group: '论坛' },
|
||||
{ key: 'bbs_plate_new', label: '创建版块', default: 2, group: '论坛' },
|
||||
{ key: 'bbs_plate_hot', label: '版块成为热门', default: 2, group: '论坛' },
|
||||
{ key: 'bbs_adopt', label: '回答被采纳', default: 2, group: '论坛' },
|
||||
{ key: 'bbs_comment_hot', label: '评论成为神评', default: 2, group: '论坛' },
|
||||
]
|
||||
|
||||
export function defaultLevelOpt(max = 10) {
|
||||
const opt = {}
|
||||
for (let i = 1; i <= max; i++) {
|
||||
// Rough Zibll-like curve: LV1=0, then grows
|
||||
opt['upgrade_integral_' + i] = i === 1 ? 0 : Math.round(20 * Math.pow(i - 1, 1.6))
|
||||
opt['name_' + i] = 'Lv.' + i
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
export function defaultIntegralOpt() {
|
||||
const opt = { day_max: 100 }
|
||||
for (const row of INTEGRAL_CATALOG) {
|
||||
opt[row.key] = row.default
|
||||
}
|
||||
return opt
|
||||
}
|
||||
|
||||
export const levelSchema = {
|
||||
key: 'users.level',
|
||||
title: '用户等级',
|
||||
icon: 'fas fa-level-up-alt',
|
||||
description: '配置等级展示前缀和等级阈值,当前可基于积分余额自动计算。',
|
||||
description: '等级与经验值:最高等级、升级阈值、经验获取矩阵(对齐 Zibll user_level / user_integral)。',
|
||||
defaults: {
|
||||
user_level_enabled: false,
|
||||
user_level_s: true,
|
||||
user_level_public_display: true,
|
||||
user_level_source: 'credits_balance',
|
||||
user_level_prefix: 'Lv.',
|
||||
user_level_thresholds: '1|0\n2|30\n3|100\n4|300',
|
||||
user_level_max: 10,
|
||||
user_level_opt: defaultLevelOpt(10),
|
||||
user_integral_opt: defaultIntegralOpt(),
|
||||
user_level_notify: true,
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
key: 'display',
|
||||
title: '等级显示',
|
||||
icon: 'fas fa-signal',
|
||||
fields: [
|
||||
{ key: 'user_level_enabled', label: '启用等级系统', type: 'switch', default: false },
|
||||
{ key: 'user_level_public_display', label: '公开页显示等级', type: 'switch', default: true, dependsOn: { key: 'user_level_enabled', value: true } },
|
||||
{ key: 'user_level_prefix', label: '等级前缀', type: 'input', default: 'Lv.', dependsOn: { key: 'user_level_enabled', value: true } },
|
||||
{ key: 'user_level_s', label: '启用用户等级', type: 'switch', default: true },
|
||||
{ key: 'user_level_public_display', label: '公开页显示等级', type: 'switch', default: true, dependsOn: { key: 'user_level_s', value: true } },
|
||||
{ key: 'user_level_prefix', label: '等级前缀', type: 'input', default: 'Lv.', dependsOn: { key: 'user_level_s', value: true } },
|
||||
{ key: 'user_level_max', label: '最高等级', type: 'number', default: 10, min: 3, max: 30, dependsOn: { key: 'user_level_s', value: true } },
|
||||
{ key: 'user_level_notify', label: '升级通知', type: 'switch', default: true, dependsOn: { key: 'user_level_s', value: true } },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'rules',
|
||||
title: '等级规则',
|
||||
icon: 'fas fa-list-ol',
|
||||
key: 'level_opt',
|
||||
title: '等级升级阈值',
|
||||
fields: [
|
||||
{ key: 'user_level_source', label: '等级来源', type: 'radio', default: 'credits_balance', options: [{ label: '积分余额', value: 'credits_balance' }, { label: '手动 Meta', value: 'manual_meta' }], dependsOn: { key: 'user_level_enabled', value: true } },
|
||||
{ key: 'user_level_thresholds', label: '等级阈值', type: 'textarea', default: '1|0\n2|30\n3|100\n4|300', help: '每行一个等级,格式:等级|所需积分,例如 3|100', dependsOn: { key: 'user_level_enabled', value: true } },
|
||||
{ key: 'user_level_opt', label: '升级经验配置', type: 'json', default: defaultLevelOpt(10), help: 'upgrade_integral_N 为升到 N 级所需累计经验' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'integral',
|
||||
title: '经验值得分',
|
||||
fields: [
|
||||
{ key: 'user_integral_opt', label: '经验值获取', type: 'json', default: defaultIntegralOpt(), help: 'day_max + 各行为得分;对应功能未开则将分值设 0' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -2,13 +2,20 @@ export const verifySchema = {
|
||||
key: 'users.verify',
|
||||
title: '身份认证',
|
||||
icon: 'fas fa-id-card',
|
||||
description: '配置公开认证徽标、审核策略,以及是否复用现有邮箱验证状态。',
|
||||
description: '配置公开认证徽标、申请门槛与审核策略(审核队列在 用户 → 身份认证 原生菜单)。',
|
||||
defaults: {
|
||||
user_verify_enabled: true,
|
||||
user_verify_public_display: true,
|
||||
user_verify_badge_text: '已认证',
|
||||
user_verify_accept_email_verified: true,
|
||||
user_verify_require_manual_review: false,
|
||||
user_verify_allow_apply: true,
|
||||
user_verify_disable_desc:
|
||||
'由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
|
||||
user_verify_limit_level: 2,
|
||||
user_verify_desc:
|
||||
'认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
|
||||
user_verify_apply_option: 'real_name|实名认证\norganization|企业认证',
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
@@ -16,9 +23,21 @@ export const verifySchema = {
|
||||
title: '公开展示',
|
||||
icon: 'fas fa-eye',
|
||||
fields: [
|
||||
{ key: 'user_verify_enabled', label: '启用身份认证结构', type: 'switch', default: true },
|
||||
{ key: 'user_verify_public_display', label: '公开页显示认证徽标', type: 'switch', default: true, dependsOn: { key: 'user_verify_enabled', value: true } },
|
||||
{ key: 'user_verify_badge_text', label: '认证徽标文案', type: 'input', default: '已认证', dependsOn: { key: 'user_verify_public_display', value: true } },
|
||||
{ key: 'user_verify_enabled', label: '启用身份认证', type: 'switch', default: true },
|
||||
{
|
||||
key: 'user_verify_public_display',
|
||||
label: '公开页显示认证徽标',
|
||||
type: 'switch',
|
||||
default: true,
|
||||
dependsOn: { key: 'user_verify_enabled', value: true },
|
||||
},
|
||||
{
|
||||
key: 'user_verify_badge_text',
|
||||
label: '认证徽标文案',
|
||||
type: 'input',
|
||||
default: '已认证',
|
||||
dependsOn: { key: 'user_verify_public_display', value: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -26,8 +45,66 @@ export const verifySchema = {
|
||||
title: '审核流程',
|
||||
icon: 'fas fa-tasks',
|
||||
fields: [
|
||||
{ key: 'user_verify_accept_email_verified', label: '邮箱验证通过视为已认证', type: 'switch', default: true, dependsOn: { key: 'user_verify_enabled', value: true } },
|
||||
{ key: 'user_verify_require_manual_review', label: '保留人工审核位', type: 'switch', default: false, dependsOn: { key: 'user_verify_enabled', value: true } },
|
||||
{
|
||||
key: 'user_verify_accept_email_verified',
|
||||
label: '邮箱验证通过视为已认证',
|
||||
type: 'switch',
|
||||
default: true,
|
||||
dependsOn: { key: 'user_verify_enabled', value: true },
|
||||
},
|
||||
{
|
||||
key: 'user_verify_require_manual_review',
|
||||
label: '保留人工审核位',
|
||||
type: 'switch',
|
||||
default: false,
|
||||
dependsOn: { key: 'user_verify_enabled', value: true },
|
||||
},
|
||||
{
|
||||
key: 'user_verify_apply_option',
|
||||
label: '申请类型选项',
|
||||
type: 'textarea',
|
||||
default: 'real_name|实名认证\norganization|企业认证',
|
||||
help: '每行:slug|显示名称',
|
||||
dependsOn: { key: 'user_verify_enabled', value: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'apply',
|
||||
title: '申请设置',
|
||||
icon: 'fas fa-file-signature',
|
||||
fields: [
|
||||
{
|
||||
key: 'user_verify_allow_apply',
|
||||
label: '允许用户前台申请',
|
||||
type: 'switch',
|
||||
default: true,
|
||||
dependsOn: { key: 'user_verify_enabled', value: true },
|
||||
},
|
||||
{
|
||||
key: 'user_verify_disable_desc',
|
||||
label: '停用申请说明',
|
||||
type: 'textarea',
|
||||
default: '由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员',
|
||||
dependsOn: { key: 'user_verify_allow_apply', value: false },
|
||||
},
|
||||
{
|
||||
key: 'user_verify_limit_level',
|
||||
label: '申请要求等级',
|
||||
type: 'number',
|
||||
default: 2,
|
||||
min: 0,
|
||||
max: 99,
|
||||
help: '0 表示不限制;依赖用户等级功能',
|
||||
dependsOn: { key: 'user_verify_allow_apply', value: true },
|
||||
},
|
||||
{
|
||||
key: 'user_verify_desc',
|
||||
label: '申请介绍',
|
||||
type: 'textarea',
|
||||
default: '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。',
|
||||
dependsOn: { key: 'user_verify_allow_apply', value: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
+28
-74
@@ -1,8 +1,5 @@
|
||||
/**
|
||||
* Reward (打赏) — dedicated module.
|
||||
*
|
||||
* Handles the reward modal: amount selection, custom amount input,
|
||||
* QR code display, and open/close interaction.
|
||||
* Reward (打赏) — per-user QR modal openers.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
@@ -10,95 +7,52 @@
|
||||
'use strict';
|
||||
|
||||
function initReward() {
|
||||
var btn = document.getElementById('action-reward');
|
||||
if (!btn) return;
|
||||
function closeAll() {
|
||||
document.querySelectorAll('.reward-modal.is-open').forEach(function (m) {
|
||||
m.classList.remove('is-open');
|
||||
m.setAttribute('aria-hidden', 'true');
|
||||
});
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
var modal = document.getElementById('reward-modal');
|
||||
if (!modal) return;
|
||||
|
||||
var closeBtn = modal.querySelector('.reward-modal__close');
|
||||
var overlay = modal.querySelector('.reward-modal__overlay');
|
||||
var amountBtns = modal.querySelectorAll('.reward-amounts__btn');
|
||||
var customWrap = modal.querySelector('.reward-custom');
|
||||
var customInput = modal ? modal.querySelector('.reward-custom__input') : null;
|
||||
var customConfirm = modal ? modal.querySelector('.reward-custom__confirm') : null;
|
||||
var selectedAmount = 0;
|
||||
|
||||
/* ---- open / close ---- */
|
||||
function openReward() {
|
||||
function openForUser(userId) {
|
||||
var id = String(userId || '');
|
||||
var modal = id
|
||||
? document.getElementById('reward-modal-' + id)
|
||||
: document.querySelector('.reward-modal');
|
||||
if (!modal) return;
|
||||
closeAll();
|
||||
modal.classList.add('is-open');
|
||||
modal.setAttribute('aria-hidden', 'false');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeReward() {
|
||||
modal.classList.remove('is-open');
|
||||
document.body.style.overflow = '';
|
||||
// reset selection
|
||||
selectedAmount = 0;
|
||||
amountBtns.forEach(function (b) { b.classList.remove('is-active'); });
|
||||
if (customInput) customInput.value = '';
|
||||
}
|
||||
|
||||
btn.addEventListener('click', openReward);
|
||||
document.querySelectorAll('[data-aksrt-reward-open]').forEach(function (el) {
|
||||
if (el === btn) return;
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
openReward();
|
||||
openForUser(el.getAttribute('data-user-id') || '');
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-aksrt-reward-close]').forEach(function (el) {
|
||||
el.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
closeAll();
|
||||
});
|
||||
});
|
||||
if (closeBtn) closeBtn.addEventListener('click', closeReward);
|
||||
if (overlay) overlay.addEventListener('click', closeReward);
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && modal.classList.contains('is-open')) closeReward();
|
||||
if (e.key === 'Escape') closeAll();
|
||||
});
|
||||
|
||||
/* ---- amount selection ---- */
|
||||
amountBtns.forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
amountBtns.forEach(function (x) { x.classList.remove('is-active'); });
|
||||
b.classList.add('is-active');
|
||||
selectedAmount = parseInt(b.dataset.amount || '0', 10);
|
||||
// clear custom input when preset is chosen
|
||||
if (customInput) customInput.value = '';
|
||||
});
|
||||
});
|
||||
|
||||
/* ---- custom amount ---- */
|
||||
if (customInput) {
|
||||
customInput.addEventListener('focus', function () {
|
||||
amountBtns.forEach(function (x) { x.classList.remove('is-active'); });
|
||||
selectedAmount = 0;
|
||||
});
|
||||
customInput.addEventListener('input', function () {
|
||||
// strip non-digits
|
||||
customInput.value = customInput.value.replace(/[^\d]/g, '');
|
||||
});
|
||||
}
|
||||
|
||||
if (customConfirm) {
|
||||
customConfirm.addEventListener('click', function () {
|
||||
var val = customInput ? parseInt(customInput.value, 10) : 0;
|
||||
if (val > 0) {
|
||||
selectedAmount = val;
|
||||
amountBtns.forEach(function (x) { x.classList.remove('is-active'); });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- register with AksrtFrontend lifecycle ---- */
|
||||
if (window.AksrtFrontend && typeof window.AksrtFrontend.register === 'function') {
|
||||
window.AksrtFrontend.register('aksrt-reward', function () {
|
||||
initReward();
|
||||
});
|
||||
} else if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initReward);
|
||||
} else {
|
||||
// fallback: run on DOMContentLoaded
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initReward);
|
||||
} else {
|
||||
initReward();
|
||||
}
|
||||
initReward();
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -370,6 +370,9 @@ if (class_exists('AKSRT_User_Badges')) {
|
||||
if (class_exists('AKSRT_User_Badges_Admin')) {
|
||||
AKSRT_User_Badges_Admin::get_instance()->init();
|
||||
}
|
||||
if (class_exists('AKSRT_User_Level')) {
|
||||
AKSRT_User_Level::get_instance()->init();
|
||||
}
|
||||
|
||||
// WordPress 原生侧栏:支付/订单运营中心(zibll zibpay 对齐)
|
||||
if (is_admin() && class_exists('AKSRT_Admin_Pay_Menus')) {
|
||||
|
||||
@@ -45,6 +45,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-coupon-order.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-credits.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-null.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-identity.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-rewards.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/menu-decoration.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/class-aksrt-nav-walker.php';
|
||||
require_once AKSRT_DIR . '/inc/helpers/aksrt-transactions.php';
|
||||
@@ -221,6 +222,7 @@ require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-credits-admin.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-balance-admin.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges-admin.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-level.php';
|
||||
|
||||
// 加载数据表迁移器(CMS / 积分 / 下载 token)
|
||||
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php';
|
||||
|
||||
@@ -75,6 +75,17 @@ class AKSRT_Admin_Schema_Post_List {
|
||||
['key' => 'breadcrumbs_single_text', 'label' => '末项显示「正文」', 'type' => 'switch', 'default' => true, 'depends_on' => 'breadcrumbs_single_s', 'description' => '关闭后末项显示文章标题'],
|
||||
['key' => 'post_prevnext_s', 'label' => '上一篇/下一篇', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_authordesc_s', 'label' => '作者简介', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_author_card_limit', 'label' => '作者卡文章数', 'type' => 'number', 'default' => 6, 'min' => 0, 'max' => 12],
|
||||
['key' => 'post_author_card_orderby', 'label' => '作者卡文章排序', 'type' => 'select', 'default' => 'views', 'options' => [
|
||||
['label' => '浏览量', 'value' => 'views'],
|
||||
['label' => '发布时间', 'value' => 'date'],
|
||||
['label' => '更新时间', 'value' => 'modified'],
|
||||
['label' => '评论数', 'value' => 'comment_count'],
|
||||
]],
|
||||
['key' => 'post_author_card_style', 'label' => '作者卡文章样式', 'type' => 'radio', 'default' => 'card', 'options' => [
|
||||
['label' => '卡片', 'value' => 'card'],
|
||||
['label' => '列表', 'value' => 'list'],
|
||||
]],
|
||||
['key' => 'post_single_hide_time_s', 'label' => '隐藏文章时间', 'type' => 'switch', 'default' => false],
|
||||
]],
|
||||
['key' => 'cover', 'title' => '封面展示', 'fields' => [
|
||||
@@ -127,8 +138,8 @@ class AKSRT_Admin_Schema_Post_List {
|
||||
['key' => 'post_copyright_enabled', 'label' => '版权信息', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'post_copyright_text', 'label' => '版权默认文案', 'type' => 'textarea', 'default' => '', 'description' => '留空则自动生成"本文由 XX 原创发布"'],
|
||||
['key' => 'post_wechat_share', 'label' => '微信分享', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_rewards_enabled', 'label' => '打赏功能', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_rewards_text', 'label' => '打赏文案', 'type' => 'input', 'default' => ''],
|
||||
['key' => 'post_rewards_s', 'label' => '用户打赏', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'post_rewards_text', 'label' => '打赏按钮文案', 'type' => 'input', 'default' => '赞赏'],
|
||||
['key' => 'post_button_toptext', 'label' => '按钮上方文案', 'type' => 'input', 'default' => '喜欢就支持一下吧'],
|
||||
]],
|
||||
['key' => 'content', 'title' => '内容处理', 'fields' => [
|
||||
|
||||
@@ -580,7 +580,7 @@ class AKSRT_Admin_Schema_Users {
|
||||
'verify' => [
|
||||
'key' => 'verify',
|
||||
'title' => '身份认证',
|
||||
'version' => '1.0.0',
|
||||
'version' => '1.1.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '公开展示', 'fields' => [
|
||||
@@ -591,69 +591,95 @@ class AKSRT_Admin_Schema_Users {
|
||||
['key' => 'workflow', 'title' => '审核流程', 'fields' => [
|
||||
['key' => 'user_verify_accept_email_verified', 'label' => '邮箱验证通过视为已认证', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_verify_require_manual_review', 'label' => '保留人工审核位', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_verify_apply_option', 'label' => '申请选项', 'type' => 'textarea', 'default' => "real_name|实名认证\norganization|企业认证"],
|
||||
['key' => 'user_verify_apply_option', 'label' => '申请类型选项', 'type' => 'textarea', 'default' => "real_name|实名认证\norganization|企业认证", 'desc' => '每行:slug|显示名称'],
|
||||
]],
|
||||
['key' => 'restrict', 'title' => '限制设置', 'fields' => [
|
||||
['key' => 'user_verify_disable_desc', 'label' => '未认证限制说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'user_verify_limit_level', 'label' => '限制等级', 'type' => 'number', 'default' => 0, 'min' => 0, 'max' => 100],
|
||||
['key' => 'user_verify_desc', 'label' => '认证说明', 'type' => 'textarea', 'default' => ''],
|
||||
['key' => 'apply', 'title' => '申请设置', 'fields' => [
|
||||
['key' => 'user_verify_allow_apply', 'label' => '允许用户前台申请', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_verify_disable_desc', 'label' => '停用申请说明', 'type' => 'textarea', 'default' => "由于系统正在维护,暂时无法申请身份认证\n详情请联系管理员"],
|
||||
['key' => 'user_verify_limit_level', 'label' => '申请要求等级', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 99, 'desc' => '0 表示不限制;依赖用户等级'],
|
||||
['key' => 'user_verify_desc', 'label' => '申请介绍', 'type' => 'textarea', 'default' => '认证用户会在昵称后显示认证徽章,同时享受多种优先待遇。申请请确保信息属实。'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'level' => [
|
||||
'key' => 'level',
|
||||
'title' => '用户等级',
|
||||
'version' => '1.0.0',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '等级显示', 'fields' => [
|
||||
['key' => 'user_level_enabled', 'label' => '启用等级系统', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_level_s', 'label' => '启用用户等级', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_level_enabled', 'label' => '启用等级系统(别名)', 'type' => 'switch', 'default' => true, 'help' => '与 user_level_s 同步写入'],
|
||||
['key' => 'user_level_public_display', 'label' => '公开页显示等级', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_level_prefix', 'label' => '等级前缀', 'type' => 'input', 'default' => 'Lv.'],
|
||||
['key' => 'user_level_color', 'label' => '等级颜色', 'type' => 'color', 'default' => '#f04494'],
|
||||
['key' => 'user_level_icon', 'label' => '等级图标', 'type' => 'icon', 'default' => 'fas fa-star'],
|
||||
]],
|
||||
['key' => 'rules', 'title' => '等级规则', 'fields' => [
|
||||
['key' => 'user_level_source', 'label' => '等级来源', 'type' => 'radio', 'default' => 'credits_balance'],
|
||||
['key' => 'user_level_thresholds', 'label' => '等级阈值', 'type' => 'textarea', 'default' => "1|0\n2|30\n3|100\n4|300"],
|
||||
['key' => 'user_level_max', 'label' => '最高等级', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 99],
|
||||
['key' => 'user_level_decay', 'label' => '等级衰减', 'type' => 'switch', 'default' => false, 'help' => '长期不活跃自动降级'],
|
||||
['key' => 'user_level_decay_days', 'label' => '衰减周期(天)', 'type' => 'number', 'default' => 90, 'min' => 7, 'max' => 365],
|
||||
]],
|
||||
['key' => 'privilege', 'title' => '等级特权', 'fields' => [
|
||||
['key' => 'user_level_download_limit', 'label' => '下载限制加成', 'type' => 'switch', 'default' => false, 'help' => '高等级用户每日下载次数增加'],
|
||||
['key' => 'user_level_upload_limit', 'label' => '上传限制加成', 'type' => 'switch', 'default' => false, 'help' => '高等级用户可上传更大文件'],
|
||||
['key' => 'user_level_comment_cooldown', 'label' => '评论冷却缩短', 'type' => 'switch', 'default' => false, 'help' => '高等级用户评论间隔更短'],
|
||||
['key' => 'user_level_custom_title', 'label' => '自定义头衔', 'type' => 'switch', 'default' => false, 'help' => '允许高等级用户自定义头衔'],
|
||||
]],
|
||||
['key' => 'notify', 'title' => '升级通知', 'fields' => [
|
||||
['key' => 'user_level_max', 'label' => '最高等级', 'type' => 'number', 'default' => 10, 'min' => 3, 'max' => 30],
|
||||
['key' => 'user_level_notify', 'label' => '升级通知', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_level_notify_style', 'label' => '通知方式', 'type' => 'select', 'default' => 'toast', 'options' => [
|
||||
['label' => '弹窗提示', 'value' => 'toast'],
|
||||
['label' => '站内信', 'value' => 'message'],
|
||||
['label' => '两者都有', 'value' => 'both'],
|
||||
['key' => 'user_level_source', 'label' => '等级来源', 'type' => 'radio', 'default' => 'integral', 'options' => [
|
||||
['label' => '经验值', 'value' => 'integral'],
|
||||
['label' => '手动 Meta', 'value' => 'manual_meta'],
|
||||
]],
|
||||
]],
|
||||
['key' => 'level_opt', 'title' => '升级阈值', 'fields' => [
|
||||
['key' => 'user_level_opt', 'label' => '等级参数', 'type' => 'json', 'default' => [], 'help' => 'upgrade_integral_N / name_N'],
|
||||
]],
|
||||
['key' => 'integral', 'title' => '经验值得分', 'fields' => [
|
||||
['key' => 'user_integral_opt', 'label' => '经验值配置', 'type' => 'json', 'default' => [], 'help' => 'day_max + 行为得分键'],
|
||||
]],
|
||||
],
|
||||
],
|
||||
'badge' => [
|
||||
'key' => 'badge',
|
||||
'title' => '用户徽章',
|
||||
'version' => '1.0.0',
|
||||
'version' => '2.0.0',
|
||||
'option_key' => 'aksrt_settings',
|
||||
'groups' => [
|
||||
['key' => 'display', 'title' => '徽章展示', 'fields' => [
|
||||
['key' => 'user_badge_enabled', 'label' => '启用徽章系统', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_badge_public_display', 'label' => '公开页显示徽章', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_badge_limit', 'label' => '最多显示数量', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 8],
|
||||
['key' => 'user_medal_s', 'label' => '启用徽章/勋章系统', 'type' => 'switch', 'default' => false],
|
||||
['key' => 'user_medal_public_display', 'label' => '公开页显示徽章', 'type' => 'switch', 'default' => true],
|
||||
['key' => 'user_medal_limit', 'label' => '最多显示数量', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 8],
|
||||
]],
|
||||
['key' => 'dictionary', 'title' => '徽章字典', 'fields' => [
|
||||
['key' => 'catalog', 'title' => '徽章明细', 'fields' => [
|
||||
[
|
||||
'key' => 'user_badge_dictionary',
|
||||
'label' => '徽章列表',
|
||||
'type' => 'textarea',
|
||||
'default' => "founder|创始成员|warning\nstaff|站务团队|info",
|
||||
'help' => '每行:slug|显示文案|色调(neutral/info/success/warning/brand)|图标URL(可选)。配置 icon_url 后前台显示勋章图;用户 meta aksrt_wear_badge 指定当前佩戴。',
|
||||
'key' => 'user_medal_args',
|
||||
'label' => '分类与条目',
|
||||
'type' => 'json',
|
||||
'default' => [
|
||||
[
|
||||
'cat_name' => '成长',
|
||||
'items' => [
|
||||
[
|
||||
'slug' => 'rookie',
|
||||
'name' => '初出茅庐',
|
||||
'desc' => '注册时间超过30天',
|
||||
'icon' => '',
|
||||
'get_type' => 'registration_time',
|
||||
'get_val' => 30,
|
||||
],
|
||||
[
|
||||
'slug' => 'checkin-30',
|
||||
'name' => '签到达人',
|
||||
'desc' => '累计签到30天',
|
||||
'icon' => '',
|
||||
'get_type' => 'checkin_all_day',
|
||||
'get_val' => 30,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'cat_name' => '限定',
|
||||
'items' => [
|
||||
[
|
||||
'slug' => 'annual-star',
|
||||
'name' => '年度人气达人',
|
||||
'desc' => '活动限定徽章',
|
||||
'icon' => '',
|
||||
'get_type' => 'manually_add',
|
||||
'get_val' => 0,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'help' => '结构化目录:分类 → 条目(slug/name/desc/icon/get_type/get_val)。由 SPA UsersBadge 编辑。',
|
||||
],
|
||||
]],
|
||||
],
|
||||
|
||||
@@ -180,9 +180,12 @@ class AKSRT_Points_Free {
|
||||
* @return list<array{key:string,title:string,desc:string,points:int,enabled:bool}>
|
||||
*/
|
||||
public static function task_list_for_user(int $user_id = 0): array {
|
||||
unset($user_id); // reserved for per-user progress later
|
||||
$cfg = self::settings();
|
||||
$list = [];
|
||||
$login_done = false;
|
||||
if ($user_id > 0) {
|
||||
$login_done = (string) get_user_meta($user_id, self::META_LOGIN, true) === self::today_key();
|
||||
}
|
||||
foreach (self::catalog() as $key => $meta) {
|
||||
$pts = (int) ($cfg['tasks'][$key] ?? 0);
|
||||
// Hide sign_up from daily task list (one-time); still grant on register.
|
||||
@@ -192,12 +195,21 @@ class AKSRT_Points_Free {
|
||||
if ($pts <= 0) {
|
||||
continue;
|
||||
}
|
||||
$status = 'todo';
|
||||
if (in_array($key, ['login', 'sign_in'], true) && $login_done) {
|
||||
$status = 'done_today';
|
||||
} elseif (in_array($key, ['post_like', 'post_favorite', 'followed', 'comment_like', 'bbs_essence', 'bbs_adopt', 'bbs_posts_hot', 'bbs_comment_hot', 'bbs_score_extra'], true)) {
|
||||
$status = 'passive';
|
||||
}
|
||||
$list[] = [
|
||||
'key' => $key,
|
||||
'title' => $meta[0],
|
||||
'desc' => $meta[1],
|
||||
'points' => $pts,
|
||||
'enabled' => true,
|
||||
'key' => $key,
|
||||
'title' => $meta[0],
|
||||
'desc' => $meta[1],
|
||||
'points' => $pts,
|
||||
'enabled' => true,
|
||||
'status' => $status,
|
||||
'progress' => null,
|
||||
'target' => null,
|
||||
];
|
||||
}
|
||||
return $list;
|
||||
|
||||
@@ -404,4 +404,136 @@ class AKSRT_User_Badges {
|
||||
'message' => __('已收回徽章', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Auto-grant medals whose get_type metrics meet get_val (skip manually_add).
|
||||
*/
|
||||
public static function auto_evaluate(int $user_id): void {
|
||||
if ($user_id <= 0 || !self::enabled() || !class_exists('AKSRT_Settings')) {
|
||||
return;
|
||||
}
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
$medal = is_array($settings['medal'] ?? null) ? $settings['medal'] : [];
|
||||
$items = is_array($medal['items_by_slug'] ?? null) ? $medal['items_by_slug'] : [];
|
||||
if (!$items) {
|
||||
// Fall back to dictionary only — no get_type → nothing to auto-grant.
|
||||
return;
|
||||
}
|
||||
$owned = self::get_owned($user_id);
|
||||
$owned_map = array_fill_keys($owned, true);
|
||||
foreach ($items as $slug => $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug === '' || !empty($owned_map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$type = sanitize_key((string) ($item['get_type'] ?? 'manually_add'));
|
||||
if ($type === '' || $type === 'manually_add') {
|
||||
continue;
|
||||
}
|
||||
$need = max(0, (float) ($item['get_val'] ?? 0));
|
||||
$have = (float) self::metric_value($user_id, $type);
|
||||
if ($have >= $need) {
|
||||
self::grant($user_id, $slug, 0);
|
||||
$owned_map[$slug] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Numeric metric for a medal get_type.
|
||||
*/
|
||||
public static function metric_value(int $user_id, string $type): float {
|
||||
switch ($type) {
|
||||
case 'registration_time':
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return 0.0;
|
||||
}
|
||||
$reg = get_date_from_gmt($user->user_registered, 'Y-m-d H:i:s');
|
||||
$now = current_time('Y-m-d H:i:s');
|
||||
return max(0.0, (strtotime($now) - strtotime($reg)) / 86400);
|
||||
|
||||
case 'checkin_all_day':
|
||||
// Prefer dedicated check-in total meta if present.
|
||||
$v = get_user_meta($user_id, 'aksrt_checkin_all_day', true);
|
||||
if ($v === '' || $v === false) {
|
||||
$v = get_user_meta($user_id, 'checkin_all_day', true);
|
||||
}
|
||||
return max(0.0, (float) $v);
|
||||
|
||||
case 'new_post':
|
||||
return (float) count_user_posts($user_id, 'post', true);
|
||||
|
||||
case 'bbs_post':
|
||||
$types = post_type_exists('forum_topic') ? 'forum_topic' : (post_type_exists('topic') ? 'topic' : '');
|
||||
return $types !== '' ? (float) count_user_posts($user_id, $types, true) : 0.0;
|
||||
|
||||
case 'comment':
|
||||
$counts = get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']);
|
||||
return (float) $counts;
|
||||
|
||||
case 'followed':
|
||||
$meta = get_user_meta($user_id, 'followed-user', true);
|
||||
if (!is_array($meta)) {
|
||||
$meta = get_user_meta($user_id, 'aksrt_followed_users', true);
|
||||
}
|
||||
return is_array($meta) ? (float) count($meta) : 0.0;
|
||||
|
||||
case 'like':
|
||||
return (float) get_user_meta($user_id, 'aksrt_posts_like_sum', true);
|
||||
|
||||
case 'comment_like':
|
||||
return (float) get_user_meta($user_id, 'aksrt_comment_like_sum', true);
|
||||
|
||||
case 'bbs_adopt':
|
||||
return (float) get_user_meta($user_id, 'aksrt_bbs_adopt_count', true);
|
||||
|
||||
case 'bbs_comment_hot':
|
||||
return (float) get_user_meta($user_id, 'aksrt_bbs_comment_hot_count', true);
|
||||
|
||||
case 'bbs_post_hot':
|
||||
return (float) get_user_meta($user_id, 'aksrt_bbs_post_hot_count', true);
|
||||
|
||||
case 'bbs_essence':
|
||||
return (float) get_user_meta($user_id, 'aksrt_bbs_essence_count', true);
|
||||
|
||||
case 'bbs_score':
|
||||
return (float) get_user_meta($user_id, 'aksrt_bbs_score_sum', true);
|
||||
|
||||
case 'views':
|
||||
return (float) get_user_meta($user_id, 'aksrt_posts_views_sum', true);
|
||||
|
||||
case 'pay_income':
|
||||
return (float) get_user_meta($user_id, 'aksrt_pay_income_sum', true);
|
||||
|
||||
case 'pay_rebate':
|
||||
return (float) get_user_meta($user_id, 'aksrt_pay_rebate_sum', true);
|
||||
|
||||
default:
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user_login
|
||||
* @param WP_User $user
|
||||
*/
|
||||
public function on_login($user_login, $user): void {
|
||||
$user_id = $user instanceof WP_User ? (int) $user->ID : 0;
|
||||
if ($user_id > 0) {
|
||||
self::auto_evaluate($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_current_user_probe(): void {
|
||||
$uid = get_current_user_id();
|
||||
if ($uid > 0) {
|
||||
self::auto_evaluate($uid);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -279,22 +279,24 @@ class AKSRT_User_Home {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{source:string,thresholds:array<int,int>} $level_settings
|
||||
* @param array{source?:string,thresholds?:array<int,int>} $level_settings
|
||||
*/
|
||||
private function get_user_level(int $user_id, array $level_settings): int {
|
||||
if (($level_settings['source'] ?? 'credits_balance') === 'manual_meta') {
|
||||
$source = (string) ($level_settings['source'] ?? 'integral');
|
||||
if ($source === 'manual_meta') {
|
||||
return max(0, (int) get_user_meta($user_id, 'aksrt_user_level', true));
|
||||
}
|
||||
|
||||
if (class_exists('AKSRT_User_Level')) {
|
||||
return AKSRT_User_Level::get_level($user_id);
|
||||
}
|
||||
// Fallback: credits_balance thresholds (legacy) if integral class missing.
|
||||
$score = function_exists('aksrt_user_credits') ? (int) aksrt_user_credits($user_id) : 0;
|
||||
$level = 0;
|
||||
|
||||
foreach ((array) ($level_settings['thresholds'] ?? []) as $candidate => $threshold) {
|
||||
if ($score >= (int) $threshold) {
|
||||
$level = (int) $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $level;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
/**
|
||||
* User level + integral XP — Zibll user_level / user_integral parity.
|
||||
*
|
||||
* Meta:
|
||||
* aksrt_level_integral int total XP
|
||||
* aksrt_user_level int resolved level
|
||||
* aksrt_level_integral_detail array reason => [value, integral]
|
||||
* aksrt_level_integral_date_detail array Y-m-d => int
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Level {
|
||||
|
||||
public const META_INTEGRAL = 'aksrt_level_integral';
|
||||
public const META_LEVEL = 'aksrt_user_level';
|
||||
public const META_DETAIL = 'aksrt_level_integral_detail';
|
||||
public const META_DATE_DETAIL = 'aksrt_level_integral_date_detail';
|
||||
public const META_TODAY_FOLLOW = 'aksrt_level_integral_followed';
|
||||
|
||||
/** @var self|null */
|
||||
private static $instance = null;
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('user_register', [$this, 'on_register'], 20, 1);
|
||||
add_action('wp_login', [$this, 'on_login'], 20, 2);
|
||||
add_action('aksrt_user_signed_in', [$this, 'on_sign_in'], 20, 1);
|
||||
add_action('publish_post', [$this, 'on_publish_post'], 20, 2);
|
||||
add_action('comment_post', [$this, 'on_comment_post'], 20, 3);
|
||||
add_action('aksrt_user_followed', [$this, 'on_followed'], 20, 2);
|
||||
add_action('aksrt_post_liked', [$this, 'on_post_like'], 20, 2);
|
||||
add_action('aksrt_comment_liked', [$this, 'on_comment_like'], 20, 2);
|
||||
add_action('aksrt_post_favorited', [$this, 'on_post_favorite'], 20, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{enabled:bool,public_display:bool,prefix:string,max:int,source:string,level_opt:array<string,mixed>,integral_opt:array<string,mixed>,notify:bool}
|
||||
*/
|
||||
public static function settings(): array {
|
||||
$all = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_all() : [];
|
||||
$enabled = !empty($all['user_level_s']) || !empty($all['user_level_enabled']);
|
||||
$max = max(3, min(30, (int) ($all['user_level_max'] ?? 10)));
|
||||
$source = (string) ($all['user_level_source'] ?? 'integral');
|
||||
if (!in_array($source, ['integral', 'manual_meta'], true)) {
|
||||
$source = 'integral';
|
||||
}
|
||||
$level_opt = $all['user_level_opt'] ?? [];
|
||||
if (is_string($level_opt)) {
|
||||
$decoded = json_decode($level_opt, true);
|
||||
$level_opt = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
if (!is_array($level_opt)) {
|
||||
$level_opt = [];
|
||||
}
|
||||
$integral_opt = $all['user_integral_opt'] ?? [];
|
||||
if (is_string($integral_opt)) {
|
||||
$decoded = json_decode($integral_opt, true);
|
||||
$integral_opt = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
if (!is_array($integral_opt)) {
|
||||
$integral_opt = [];
|
||||
}
|
||||
$prefix = trim((string) ($all['user_level_prefix'] ?? 'Lv.'));
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'public_display' => !isset($all['user_level_public_display']) || !empty($all['user_level_public_display']),
|
||||
'prefix' => $prefix !== '' ? $prefix : 'Lv.',
|
||||
'max' => $max,
|
||||
'source' => $source,
|
||||
'level_opt' => $level_opt,
|
||||
'integral_opt' => $integral_opt,
|
||||
'notify' => !isset($all['user_level_notify']) || !empty($all['user_level_notify']),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Catalog of integral earn keys (defaults).
|
||||
*
|
||||
* @return array<string,int>
|
||||
*/
|
||||
public static function integral_defaults(): array {
|
||||
return [
|
||||
'day_max' => 100,
|
||||
'sign_up' => 20,
|
||||
'sign_in' => 5,
|
||||
'followed' => 5,
|
||||
'post_new' => 5,
|
||||
'post_like' => 1,
|
||||
'post_favorite' => 2,
|
||||
'comment_new' => 2,
|
||||
'comment_like' => 1,
|
||||
'bbs_posts_new' => 3,
|
||||
'bbs_score_extra' => 1,
|
||||
'bbs_essence' => 2,
|
||||
'bbs_posts_hot' => 2,
|
||||
'bbs_plate_new' => 2,
|
||||
'bbs_plate_hot' => 2,
|
||||
'bbs_adopt' => 2,
|
||||
'bbs_comment_hot' => 2,
|
||||
];
|
||||
}
|
||||
|
||||
public static function task_points(string $key): int {
|
||||
$cfg = self::settings();
|
||||
$opt = $cfg['integral_opt'];
|
||||
$def = self::integral_defaults();
|
||||
if (array_key_exists($key, $opt)) {
|
||||
return max(0, (int) $opt[$key]);
|
||||
}
|
||||
return max(0, (int) ($def[$key] ?? 0));
|
||||
}
|
||||
|
||||
public static function day_max(): int {
|
||||
$cfg = self::settings();
|
||||
$opt = $cfg['integral_opt'];
|
||||
if (array_key_exists('day_max', $opt)) {
|
||||
return max(0, (int) $opt['day_max']);
|
||||
}
|
||||
return 100;
|
||||
}
|
||||
|
||||
public static function get_integral(int $user_id): int {
|
||||
if ($user_id <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return max(0, (int) get_user_meta($user_id, self::META_INTEGRAL, true));
|
||||
}
|
||||
|
||||
public static function get_level(int $user_id): int {
|
||||
if ($user_id <= 0) {
|
||||
return 0;
|
||||
}
|
||||
$cfg = self::settings();
|
||||
if (!$cfg['enabled']) {
|
||||
return 0;
|
||||
}
|
||||
if ($cfg['source'] === 'manual_meta') {
|
||||
return max(0, (int) get_user_meta($user_id, self::META_LEVEL, true));
|
||||
}
|
||||
return self::resolve_level(self::get_integral($user_id), $cfg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{max:int,level_opt:array} $cfg
|
||||
*/
|
||||
public static function resolve_level(int $integral, ?array $cfg = null): int {
|
||||
$cfg = $cfg ?? self::settings();
|
||||
$max = (int) $cfg['max'];
|
||||
$opt = $cfg['level_opt'];
|
||||
$level = 1;
|
||||
for ($i = 1; $i <= $max; $i++) {
|
||||
$need = (int) ($opt['upgrade_integral_' . $i] ?? ($i === 1 ? 0 : PHP_INT_MAX));
|
||||
if ($integral >= $need) {
|
||||
$level = $i;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return max(0, min($max, $level));
|
||||
}
|
||||
|
||||
/**
|
||||
* Grant integral for a task key. Respects day_max.
|
||||
*
|
||||
* @return int|false points granted or false
|
||||
*/
|
||||
public static function add(int $user_id, string $key, int $points = 0) {
|
||||
if ($user_id <= 0) {
|
||||
return false;
|
||||
}
|
||||
$cfg = self::settings();
|
||||
if (!$cfg['enabled'] || $cfg['source'] !== 'integral') {
|
||||
return false;
|
||||
}
|
||||
$key = sanitize_key($key);
|
||||
if ($key === '' || $key === 'day_max') {
|
||||
return false;
|
||||
}
|
||||
if ($points <= 0) {
|
||||
$points = self::task_points($key);
|
||||
}
|
||||
if ($points <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$day_max = self::day_max();
|
||||
$today = gmdate('Y-m-d', current_time('timestamp'));
|
||||
$date_detail = get_user_meta($user_id, self::META_DATE_DETAIL, true);
|
||||
$date_detail = is_array($date_detail) ? $date_detail : [];
|
||||
$today_sum = max(0, (int) ($date_detail[$today] ?? 0));
|
||||
if ($day_max > 0 && $today_sum >= $day_max) {
|
||||
return false;
|
||||
}
|
||||
if ($day_max > 0) {
|
||||
$points = min($points, $day_max - $today_sum);
|
||||
}
|
||||
if ($points <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$before = self::get_integral($user_id);
|
||||
$after = $before + $points;
|
||||
update_user_meta($user_id, self::META_INTEGRAL, $after);
|
||||
|
||||
$date_detail[$today] = $today_sum + $points;
|
||||
// Keep last 90 days.
|
||||
if (count($date_detail) > 90) {
|
||||
krsort($date_detail);
|
||||
$date_detail = array_slice($date_detail, 0, 90, true);
|
||||
}
|
||||
update_user_meta($user_id, self::META_DATE_DETAIL, $date_detail);
|
||||
|
||||
$detail = get_user_meta($user_id, self::META_DETAIL, true);
|
||||
$detail = is_array($detail) ? $detail : [];
|
||||
$prev = is_array($detail[$key] ?? null) ? $detail[$key] : ['value' => 0, 'integral' => 0];
|
||||
$detail[$key] = [
|
||||
'value' => (int) ($prev['value'] ?? 0) + $points,
|
||||
'integral' => (int) ($prev['integral'] ?? 0) + $points,
|
||||
];
|
||||
update_user_meta($user_id, self::META_DETAIL, $detail);
|
||||
|
||||
$old_level = (int) get_user_meta($user_id, self::META_LEVEL, true);
|
||||
$new_level = self::resolve_level($after, $cfg);
|
||||
if ($new_level !== $old_level) {
|
||||
update_user_meta($user_id, self::META_LEVEL, $new_level);
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param int $new_level
|
||||
* @param int $old_level
|
||||
*/
|
||||
do_action('aksrt_user_level_changed', $user_id, $new_level, $old_level);
|
||||
} else {
|
||||
update_user_meta($user_id, self::META_LEVEL, $new_level);
|
||||
}
|
||||
|
||||
return $points;
|
||||
}
|
||||
|
||||
public function on_register(int $user_id): void {
|
||||
self::add($user_id, 'sign_up');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $user_login
|
||||
* @param WP_User $user
|
||||
*/
|
||||
public function on_login($user_login, $user): void {
|
||||
$user_id = $user instanceof WP_User ? (int) $user->ID : 0;
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
// Once per calendar day for login XP.
|
||||
$today = gmdate('Y-m-d', current_time('timestamp'));
|
||||
$flag = (string) get_user_meta($user_id, '_aksrt_integral_sign_in_day', true);
|
||||
if ($flag === $today) {
|
||||
return;
|
||||
}
|
||||
$granted = self::add($user_id, 'sign_in');
|
||||
if ($granted) {
|
||||
update_user_meta($user_id, '_aksrt_integral_sign_in_day', $today);
|
||||
}
|
||||
}
|
||||
|
||||
public function on_sign_in(int $user_id): void {
|
||||
// Dedicated check-in hook can also grant if separate from login; reuse sign_in key once/day.
|
||||
$this->on_login('', get_userdata($user_id) ?: new WP_User($user_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $post_id
|
||||
* @param WP_Post $post
|
||||
*/
|
||||
public function on_publish_post($post_id, $post): void {
|
||||
if (!$post instanceof WP_Post || $post->post_type !== 'post' || $post->post_status !== 'publish') {
|
||||
return;
|
||||
}
|
||||
// Only when transitioning to publish first time.
|
||||
if ((string) get_post_meta($post_id, '_aksrt_integral_post_new', true) === '1') {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0) {
|
||||
return;
|
||||
}
|
||||
if (self::add($author, 'post_new')) {
|
||||
update_post_meta($post_id, '_aksrt_integral_post_new', '1');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $comment_id
|
||||
* @param int|string $approved
|
||||
* @param array $commentdata
|
||||
*/
|
||||
public function on_comment_post($comment_id, $approved, $commentdata): void {
|
||||
if ((string) $approved !== '1' && $approved !== 1) {
|
||||
return;
|
||||
}
|
||||
$user_id = (int) ($commentdata['user_id'] ?? 0);
|
||||
if ($user_id <= 0) {
|
||||
$user_id = (int) get_comment($comment_id)->user_id;
|
||||
}
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::add($user_id, 'comment_new');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $follower_id
|
||||
* @param int $followed_id
|
||||
*/
|
||||
public function on_followed($follower_id, $followed_id): void {
|
||||
$followed_id = (int) $followed_id;
|
||||
if ($followed_id <= 0) {
|
||||
return;
|
||||
}
|
||||
self::add($followed_id, 'followed');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $post_id
|
||||
* @param int $user_id liker
|
||||
*/
|
||||
public function on_post_like($post_id, $user_id): void {
|
||||
$post = get_post((int) $post_id);
|
||||
if (!$post instanceof WP_Post) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0 || $author === (int) $user_id) {
|
||||
return;
|
||||
}
|
||||
self::add($author, 'post_like');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $comment_id
|
||||
* @param int $user_id
|
||||
*/
|
||||
public function on_comment_like($comment_id, $user_id): void {
|
||||
$c = get_comment((int) $comment_id);
|
||||
if (!$c) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $c->user_id;
|
||||
if ($author <= 0 || $author === (int) $user_id) {
|
||||
return;
|
||||
}
|
||||
self::add($author, 'comment_like');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $post_id
|
||||
* @param int $user_id
|
||||
*/
|
||||
public function on_post_favorite($post_id, $user_id): void {
|
||||
$post = get_post((int) $post_id);
|
||||
if (!$post instanceof WP_Post) {
|
||||
return;
|
||||
}
|
||||
$author = (int) $post->post_author;
|
||||
if ($author <= 0 || $author === (int) $user_id) {
|
||||
return;
|
||||
}
|
||||
self::add($author, 'post_favorite');
|
||||
}
|
||||
}
|
||||
@@ -237,10 +237,20 @@ class AKSRT_Settings_Post_Display {
|
||||
$authordesc = !isset($settings['post_authordesc_s']) || !empty($settings['post_authordesc_s']);
|
||||
$hide_time = !empty($settings['post_single_hide_time_s']);
|
||||
$share_enabled = !isset($settings['share_s']) || !empty($settings['share_s']);
|
||||
$rewards_enabled = !empty($settings['post_rewards_enabled']) || !empty($settings['post_rewards_s']);
|
||||
$rewards_image = (string) ($settings['post_rewards_image'] ?? $settings['post_rewards_qrcode'] ?? $settings['post_rewards_img'] ?? '');
|
||||
$rewards_enabled = !empty($settings['post_rewards_s']) || !empty($settings['post_rewards_enabled']);
|
||||
$rewards_text = sanitize_text_field((string) ($settings['post_rewards_text'] ?? ''));
|
||||
$rewards_btn_text = sanitize_text_field((string) ($settings['post_rewards_btn_text'] ?? '赞赏'));
|
||||
if ($rewards_text === '') {
|
||||
$rewards_text = '赞赏';
|
||||
}
|
||||
$author_card_limit = max(0, min(12, (int) ($settings['post_author_card_limit'] ?? 6)));
|
||||
$author_card_orderby = sanitize_key((string) ($settings['post_author_card_orderby'] ?? 'views'));
|
||||
if (!in_array($author_card_orderby, ['views', 'date', 'modified', 'comment_count'], true)) {
|
||||
$author_card_orderby = 'views';
|
||||
}
|
||||
$author_card_style = sanitize_key((string) ($settings['post_author_card_style'] ?? 'card'));
|
||||
if (!in_array($author_card_style, ['card', 'list'], true)) {
|
||||
$author_card_style = 'card';
|
||||
}
|
||||
$maxheight_enabled = !empty($settings['article_maxheight_kg'])
|
||||
|| !empty($settings['post_article_maxheight_kg'])
|
||||
|| !empty($settings['post_maxheight_kg']);
|
||||
@@ -318,11 +328,15 @@ class AKSRT_Settings_Post_Display {
|
||||
// Canonical + legacy share.
|
||||
'share_enabled' => $share_enabled,
|
||||
'share_s' => $share_enabled,
|
||||
// Rewards (Zibll post_rewards_s).
|
||||
// Author card posts (Zibll user_card_box defaults).
|
||||
'author_card_limit' => $author_card_limit,
|
||||
'author_card_orderby' => $author_card_orderby,
|
||||
'author_card_style' => $author_card_style,
|
||||
// Rewards (Zibll post_rewards_s) — per-user QR only.
|
||||
'rewards_enabled' => $rewards_enabled,
|
||||
'rewards_image' => $rewards_image,
|
||||
'post_rewards_s' => $rewards_enabled,
|
||||
'rewards_text' => $rewards_text,
|
||||
'rewards_btn_text' => $rewards_btn_text !== '' ? $rewards_btn_text : '赞赏',
|
||||
'rewards_btn_text' => $rewards_text,
|
||||
// Content max height (Zibll article_maxheight_kg / article_maxheight).
|
||||
'maxheight_enabled' => $maxheight_enabled,
|
||||
'maxheight' => $maxheight,
|
||||
|
||||
@@ -115,11 +115,12 @@ class AKSRT_Settings_User_Runtime {
|
||||
}
|
||||
|
||||
/**
|
||||
* User identity, level, badge, and report governance settings.
|
||||
* User identity, level, medal, and report governance settings.
|
||||
*
|
||||
* @return array{
|
||||
* verify:array{enabled:bool,public_display:bool,badge_text:string,accept_email_verified:bool,require_manual_review:bool},
|
||||
* verify:array{enabled:bool,public_display:bool,badge_text:string,accept_email_verified:bool,require_manual_review:bool,allow_apply:bool,disable_desc:string,limit_level:int,desc:string,apply_option:string},
|
||||
* level:array{enabled:bool,public_display:bool,source:string,label_prefix:string,thresholds:array<int,int>},
|
||||
* medal:array{enabled:bool,public_display:bool,limit:int,categories:array<int,array{cat_name:string,items:array<int,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int}>}>,items_by_slug:array<string,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int,cat_name:string}>},
|
||||
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array<int,array{slug:string,label:string,tone:string,icon_url?:string}>},
|
||||
* report:array{enabled:bool,public_entry:bool,public_notice:bool,ban_notice_text:string,categories:array<int,array{slug:string,label:string}>}
|
||||
* }
|
||||
@@ -129,6 +130,7 @@ class AKSRT_Settings_User_Runtime {
|
||||
$verify_badge_text = trim((string) ($settings['user_verify_badge_text'] ?? ''));
|
||||
$level_prefix = trim((string) ($settings['user_level_prefix'] ?? ''));
|
||||
$ban_notice_text = trim((string) ($settings['user_report_ban_notice_text'] ?? ''));
|
||||
$medal = self::parse_medal_catalog($settings);
|
||||
|
||||
return [
|
||||
'verify' => [
|
||||
@@ -137,22 +139,37 @@ class AKSRT_Settings_User_Runtime {
|
||||
'badge_text' => $verify_badge_text !== '' ? $verify_badge_text : __('已认证', 'aksrt'),
|
||||
'accept_email_verified' => !isset($settings['user_verify_accept_email_verified']) || !empty($settings['user_verify_accept_email_verified']),
|
||||
'require_manual_review' => !empty($settings['user_verify_require_manual_review']),
|
||||
'allow_apply' => !isset($settings['user_verify_allow_apply']) || !empty($settings['user_verify_allow_apply']),
|
||||
'disable_desc' => (string) ($settings['user_verify_disable_desc'] ?? ''),
|
||||
'limit_level' => max(0, (int) ($settings['user_verify_limit_level'] ?? 0)),
|
||||
'desc' => (string) ($settings['user_verify_desc'] ?? ''),
|
||||
'apply_option' => (string) ($settings['user_verify_apply_option'] ?? ''),
|
||||
],
|
||||
'level' => [
|
||||
'enabled' => !empty($settings['user_level_enabled']),
|
||||
'enabled' => !empty($settings['user_level_enabled']) || !empty($settings['user_level_s']),
|
||||
'public_display' => !isset($settings['user_level_public_display']) || !empty($settings['user_level_public_display']),
|
||||
'source' => (function() use ($settings) {
|
||||
$v = $settings['user_level_source'] ?? 'credits_balance';
|
||||
return in_array($v, ['credits_balance', 'manual_meta'], true) ? $v : 'credits_balance';
|
||||
'source' => (static function () use ($settings) {
|
||||
$v = $settings['user_level_source'] ?? 'integral';
|
||||
return in_array($v, ['credits_balance', 'manual_meta', 'integral'], true) ? $v : 'integral';
|
||||
})(),
|
||||
'label_prefix' => $level_prefix !== '' ? $level_prefix : 'Lv.',
|
||||
'thresholds' => self::parse_level_thresholds((string) ($settings['user_level_thresholds'] ?? "1|0\n2|30\n3|100\n4|300")),
|
||||
'max' => max(3, min(30, (int) ($settings['user_level_max'] ?? 10))),
|
||||
],
|
||||
'medal' => $medal,
|
||||
// Alias for existing call sites that still read badge.*; dictionary is derived from medal catalog.
|
||||
'badge' => [
|
||||
'enabled' => !empty($settings['user_badge_enabled']),
|
||||
'public_display' => !isset($settings['user_badge_public_display']) || !empty($settings['user_badge_public_display']),
|
||||
'limit' => max(1, min(8, (int) ($settings['user_badge_limit'] ?? 3))),
|
||||
'dictionary' => self::parse_badge_dictionary((string) ($settings['user_badge_dictionary'] ?? "founder|创始成员|warning\nstaff|站务团队|info")),
|
||||
'enabled' => $medal['enabled'],
|
||||
'public_display' => $medal['public_display'],
|
||||
'limit' => $medal['limit'],
|
||||
'dictionary' => array_values(array_map(static function (array $item): array {
|
||||
return [
|
||||
'slug' => $item['slug'],
|
||||
'label' => $item['name'],
|
||||
'tone' => 'info',
|
||||
'icon_url' => $item['icon'],
|
||||
];
|
||||
}, $medal['items_by_slug'])),
|
||||
],
|
||||
'report' => [
|
||||
'enabled' => !empty($settings['user_report_enabled']),
|
||||
@@ -207,6 +224,100 @@ class AKSRT_Settings_User_Runtime {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse structured user_medal_args catalog (rewrite; no badge dictionary dual-read).
|
||||
*
|
||||
* @param array<string,mixed> $settings
|
||||
* @return array{enabled:bool,public_display:bool,limit:int,categories:array,items_by_slug:array}
|
||||
*/
|
||||
public static function parse_medal_catalog(array $settings): array {
|
||||
$enabled = !empty($settings['user_medal_s']);
|
||||
$public = !isset($settings['user_medal_public_display']) || !empty($settings['user_medal_public_display']);
|
||||
$limit = max(1, min(8, (int) ($settings['user_medal_limit'] ?? 3)));
|
||||
|
||||
$allowed_types = [
|
||||
'registration_time',
|
||||
'checkin_all_day',
|
||||
'new_post',
|
||||
'bbs_post',
|
||||
'comment',
|
||||
'followed',
|
||||
'like',
|
||||
'comment_like',
|
||||
'bbs_adopt',
|
||||
'bbs_comment_hot',
|
||||
'bbs_post_hot',
|
||||
'bbs_essence',
|
||||
'bbs_score',
|
||||
'views',
|
||||
'pay_income',
|
||||
'pay_rebate',
|
||||
'manually_add',
|
||||
];
|
||||
|
||||
$raw = $settings['user_medal_args'] ?? null;
|
||||
if (is_string($raw) && $raw !== '') {
|
||||
$decoded = json_decode($raw, true);
|
||||
$raw = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
if (!is_array($raw)) {
|
||||
$raw = [];
|
||||
}
|
||||
|
||||
$categories = [];
|
||||
$items_by_slug = [];
|
||||
|
||||
foreach ($raw as $cat) {
|
||||
if (!is_array($cat)) {
|
||||
continue;
|
||||
}
|
||||
$cat_name = sanitize_text_field((string) ($cat['cat_name'] ?? $cat['name'] ?? ''));
|
||||
if ($cat_name === '') {
|
||||
$cat_name = __('未命名', 'aksrt');
|
||||
}
|
||||
$items_out = [];
|
||||
$items = is_array($cat['items'] ?? null) ? $cat['items'] : [];
|
||||
foreach ($items as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$slug = sanitize_key((string) ($item['slug'] ?? ''));
|
||||
$name = sanitize_text_field((string) ($item['name'] ?? ''));
|
||||
if ($slug === '' || $name === '') {
|
||||
continue;
|
||||
}
|
||||
$get_type = sanitize_key((string) ($item['get_type'] ?? 'manually_add'));
|
||||
if (!in_array($get_type, $allowed_types, true)) {
|
||||
$get_type = 'manually_add';
|
||||
}
|
||||
$icon = esc_url_raw((string) ($item['icon'] ?? $item['icon_url'] ?? ''));
|
||||
$row = [
|
||||
'slug' => $slug,
|
||||
'name' => $name,
|
||||
'desc' => sanitize_text_field((string) ($item['desc'] ?? '')),
|
||||
'icon' => $icon,
|
||||
'get_type' => $get_type,
|
||||
'get_val' => max(0, (int) ($item['get_val'] ?? 0)),
|
||||
'cat_name' => $cat_name,
|
||||
];
|
||||
$items_out[] = $row;
|
||||
$items_by_slug[$slug] = $row;
|
||||
}
|
||||
$categories[] = [
|
||||
'cat_name' => $cat_name,
|
||||
'items' => $items_out,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'public_display' => $public,
|
||||
'limit' => $limit,
|
||||
'categories' => $categories,
|
||||
'items_by_slug' => $items_by_slug,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,int>
|
||||
*/
|
||||
@@ -238,41 +349,13 @@ class AKSRT_Settings_User_Runtime {
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Removed — medal catalog is structured user_medal_args. Method kept empty stub intentionally deleted.
|
||||
* @return array<int,array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
*/
|
||||
public static function parse_badge_dictionary(string $raw): array {
|
||||
$lines = preg_split('/[\r\n]+/', $raw, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$allowed_tones = ['neutral', 'info', 'success', 'warning', 'brand'];
|
||||
$items = [];
|
||||
|
||||
foreach ((array) $lines as $line) {
|
||||
$parts = array_map('trim', explode('|', (string) $line));
|
||||
if (count($parts) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$slug = sanitize_key((string) $parts[0]);
|
||||
$label = sanitize_text_field((string) $parts[1]);
|
||||
$tone = isset($parts[2]) ? sanitize_key((string) $parts[2]) : 'neutral';
|
||||
$icon_url = isset($parts[3]) ? esc_url_raw((string) $parts[3]) : '';
|
||||
|
||||
if ($slug === '' || $label === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!in_array($tone, $allowed_tones, true)) {
|
||||
$tone = 'neutral';
|
||||
}
|
||||
|
||||
$items[] = [
|
||||
'slug' => $slug,
|
||||
'label' => $label,
|
||||
'tone' => $tone,
|
||||
'icon_url' => $icon_url,
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
// Rewrite policy: no dual-read of pipe dictionary. Callers must use parse_medal_catalog / get_user_identity_settings()['medal'].
|
||||
unset($raw);
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -835,28 +835,59 @@ if (!function_exists('aksrt_comment_action_permissions')) :
|
||||
* @return array{can_edit:bool,can_delete:bool,can_moderate:bool,can_pin:bool}
|
||||
*/
|
||||
function aksrt_comment_action_permissions($comment): array {
|
||||
$comment = $comment instanceof WP_Comment ? $comment : get_comment($comment);
|
||||
$none = ['can_edit' => false, 'can_delete' => false, 'can_moderate' => false, 'can_pin' => false];
|
||||
if (!($comment instanceof WP_Comment) || !is_user_logged_in()) {
|
||||
return $none;
|
||||
$comment = $comment instanceof WP_Comment ? $comment : get_comment($comment);
|
||||
$none = ['can_edit' => false, 'can_delete' => false, 'can_moderate' => false, 'can_pin' => false];
|
||||
if (!($comment instanceof WP_Comment) || !is_user_logged_in()) {
|
||||
return $none;
|
||||
}
|
||||
$uid = get_current_user_id();
|
||||
$is_wp_mod = current_user_can('moderate_comments') || current_user_can('manage_options');
|
||||
$is_owner = (int) $comment->user_id > 0 && (int) $comment->user_id === $uid;
|
||||
$post_id = (int) $comment->comment_post_ID;
|
||||
$post = $post_id > 0 ? get_post($post_id) : null;
|
||||
$is_post_author = $post instanceof WP_Post && (int) $post->post_author === $uid;
|
||||
$ctx = $post_id > 0 ? $post_id : null;
|
||||
|
||||
$can = static function (string $cap) use ($uid, $ctx): bool {
|
||||
if (function_exists('aksrt_user_can')) {
|
||||
return aksrt_user_can($cap, $uid, $ctx);
|
||||
}
|
||||
if (class_exists('AKSRT_Settings_Capabilities')) {
|
||||
return AKSRT_Settings_Capabilities::user_can($cap, $uid, $ctx);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
$edit_window = 10;
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$display = AKSRT_Settings::get_comment_display_settings();
|
||||
$edit_window = (int) ($display['edit_window'] ?? 10);
|
||||
}
|
||||
$within_window = $edit_window > 0
|
||||
&& (time() - strtotime((string) $comment->comment_date_gmt)) < ($edit_window * MINUTE_IN_SECONDS);
|
||||
|
||||
$can_edit_own = $is_owner && $within_window && $can('comment_edit');
|
||||
$can_edit_others = (!$is_owner) && (
|
||||
($is_post_author && $can('comment_edit_my_post'))
|
||||
|| $can('comment_edit_other')
|
||||
);
|
||||
$can_delete_own = $is_owner && $can('comment_delete');
|
||||
$can_delete_others = (!$is_owner) && (
|
||||
($is_post_author && $can('comment_delete_my_post'))
|
||||
|| $can('comment_delete_other')
|
||||
);
|
||||
$can_pin = (int) $comment->comment_parent === 0 && (
|
||||
($is_post_author && $can('comment_set_topping_my_post'))
|
||||
|| $can('comment_set_topping_other')
|
||||
);
|
||||
|
||||
return [
|
||||
'can_edit' => $is_wp_mod || $can_edit_own || $can_edit_others,
|
||||
'can_delete' => $is_wp_mod || $can_delete_own || $can_delete_others,
|
||||
'can_moderate' => $is_wp_mod || $can('comment_audit_other') || ($is_post_author && $can('comment_audit_my_post')),
|
||||
'can_pin' => $is_wp_mod || $can_pin,
|
||||
];
|
||||
}
|
||||
$is_moderator = current_user_can('moderate_comments');
|
||||
$is_owner = (int) $comment->user_id > 0 && (int) $comment->user_id === get_current_user_id();
|
||||
// Owner self-edit window (minutes); moderators bypass the window.
|
||||
$edit_window = 10;
|
||||
if (class_exists('AKSRT_Settings')) {
|
||||
$display = AKSRT_Settings::get_comment_display_settings();
|
||||
$edit_window = (int) ($display['edit_window'] ?? 10);
|
||||
}
|
||||
$within_window = $edit_window > 0
|
||||
&& (time() - strtotime((string) $comment->comment_date_gmt)) < ($edit_window * MINUTE_IN_SECONDS);
|
||||
return [
|
||||
'can_edit' => $is_moderator || ($is_owner && $within_window),
|
||||
'can_delete' => $is_moderator || $is_owner,
|
||||
'can_moderate' => $is_moderator,
|
||||
'can_pin' => $is_moderator && (int) $comment->comment_parent === 0,
|
||||
];
|
||||
}
|
||||
endif;
|
||||
if (!function_exists('aksrt_is_comment_pinned')) :
|
||||
/**
|
||||
|
||||
@@ -91,6 +91,119 @@ if (!function_exists('aksrt_render_user_identity_chips')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_get_user_name')) {
|
||||
/**
|
||||
* Zibll zib_get_user_name parity: name link + auth + wear medal + level + optional follow.
|
||||
*
|
||||
* @param array|int $args User id or args array.
|
||||
*/
|
||||
function aksrt_get_user_name($args = []): string {
|
||||
$defaults = [
|
||||
'id' => 0,
|
||||
'class' => 'flex ac flex1',
|
||||
'name_class' => '',
|
||||
'vip' => false,
|
||||
'medal' => true,
|
||||
'auth' => true,
|
||||
'level' => true,
|
||||
'follow' => false,
|
||||
];
|
||||
if (is_numeric($args)) {
|
||||
$defaults['id'] = (int) $args;
|
||||
$args = $defaults;
|
||||
} else {
|
||||
$args = wp_parse_args(is_array($args) ? $args : [], $defaults);
|
||||
}
|
||||
$user_id = (int) $args['id'];
|
||||
if ($user_id <= 0) {
|
||||
$user_id = get_current_user_id();
|
||||
}
|
||||
if ($user_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$home = function_exists('get_author_posts_url') ? get_author_posts_url($user_id) : '';
|
||||
$display = $user->display_name;
|
||||
$vip_html = '';
|
||||
if (!empty($args['vip']) && function_exists('aksrt_render_vip_badge')) {
|
||||
$vip_html = (string) aksrt_render_vip_badge($user_id, 'sm');
|
||||
}
|
||||
$name_class = trim('display-name text-ellipsis ' . (string) $args['name_class']);
|
||||
$name_link = '<a class="' . esc_attr($name_class) . '" href="' . esc_url($home) . '">' . $vip_html . esc_html($display) . '</a>';
|
||||
|
||||
$auth_html = '';
|
||||
$medal_html = '';
|
||||
$level_html = '';
|
||||
$signals = aksrt_get_user_identity_signals($user_id);
|
||||
foreach ($signals as $signal) {
|
||||
$type = (string) ($signal['type'] ?? '');
|
||||
$label = trim((string) ($signal['label'] ?? ''));
|
||||
if ($label === '') {
|
||||
continue;
|
||||
}
|
||||
$icon_url = esc_url((string) ($signal['icon_url'] ?? ''));
|
||||
$title = (string) ($signal['title'] ?? $label);
|
||||
if ($type === 'verify' && !empty($args['auth']) && $auth_html === '') {
|
||||
$auth_html = '<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--success" title="' . esc_attr($title) . '"><i class="fa fa-check-circle" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
|
||||
} elseif ($type === 'level' && !empty($args['level']) && $level_html === '') {
|
||||
$level_html = '<span class="badg badg-sm ml3 aksrt-identity-chip aksrt-identity-chip--info" title="' . esc_attr($title) . '"><i class="fa fa-layer-group" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
|
||||
} elseif ($type === 'badge' && !empty($args['medal']) && $medal_html === '' && !empty($signal['wear'])) {
|
||||
if ($icon_url !== '') {
|
||||
$medal_html = '<img class="medal-icon img-icon ml3 aksrt-identity-medal is-wear" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
|
||||
} else {
|
||||
$medal_html = '<span class="badg badg-sm ml3 aksrt-identity-chip is-wear" title="' . esc_attr($title) . '"><i class="fa fa-award" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
// If medal requested but no wear flag, use first badge with icon.
|
||||
if (!empty($args['medal']) && $medal_html === '') {
|
||||
foreach ($signals as $signal) {
|
||||
if ((string) ($signal['type'] ?? '') !== 'badge') {
|
||||
continue;
|
||||
}
|
||||
$label = trim((string) ($signal['label'] ?? ''));
|
||||
$icon_url = esc_url((string) ($signal['icon_url'] ?? ''));
|
||||
$title = (string) ($signal['title'] ?? $label);
|
||||
if ($label === '') {
|
||||
continue;
|
||||
}
|
||||
if ($icon_url !== '') {
|
||||
$medal_html = '<img class="medal-icon img-icon ml3 aksrt-identity-medal" src="' . $icon_url . '" alt="' . esc_attr($label) . '" title="' . esc_attr($title) . '" loading="lazy" decoding="async" />';
|
||||
} else {
|
||||
$medal_html = '<span class="badg badg-sm ml3 aksrt-identity-chip" title="' . esc_attr($title) . '"><i class="fa fa-award" aria-hidden="true"></i> ' . esc_html($label) . '</span>';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$follow_html = '';
|
||||
if (!empty($args['follow']) && get_current_user_id() !== $user_id) {
|
||||
if (is_user_logged_in()) {
|
||||
$follow_html = '<button type="button" class="focus-color ml10 follow flex0 but" data-aksrt-follow="' . (int) $user_id . '"><i class="fa fa-heart em09" aria-hidden="true"></i> ' . esc_html__('关注', 'aksrt') . '</button>';
|
||||
} else {
|
||||
$login = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url();
|
||||
$follow_html = '<a class="focus-color ml10 follow flex0 but" href="' . esc_url($login) . '"><i class="fa fa-heart em09" aria-hidden="true"></i> ' . esc_html__('关注', 'aksrt') . '</a>';
|
||||
}
|
||||
}
|
||||
|
||||
// Order: name → auth → wear medal → level → follow (Zibll).
|
||||
$html = '<name class="' . esc_attr((string) $args['class']) . '">' . $name_link . $auth_html . $medal_html . $level_html . $follow_html . '</name>';
|
||||
/**
|
||||
* Filter rendered user name chrome.
|
||||
*
|
||||
* @param string $html
|
||||
* @param int $user_id
|
||||
* @param array $args
|
||||
*/
|
||||
return (string) apply_filters('aksrt_user_show_name', $html, $user_id, $args);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_is_blog_comment_hot')) {
|
||||
/**
|
||||
* "神评" for ordinary post comments when likes cross threshold.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
/**
|
||||
* Per-user tip (打赏) helpers — Zibll post_rewards_s + user QR parity.
|
||||
*
|
||||
* Meta: aksrt_rewards_weixin, aksrt_rewards_alipay (attachment id or URL).
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
if (!function_exists('aksrt_rewards_enabled')) {
|
||||
function aksrt_rewards_enabled(): bool {
|
||||
if (!class_exists('AKSRT_Settings') || !method_exists('AKSRT_Settings', 'get_post_page_settings')) {
|
||||
return false;
|
||||
}
|
||||
$s = AKSRT_Settings::get_post_page_settings();
|
||||
return !empty($s['rewards_enabled']) || !empty($s['post_rewards_s']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_get_user_rewards_img_urls')) {
|
||||
/**
|
||||
* @return array{weixin:string,alipay:string}
|
||||
*/
|
||||
function aksrt_get_user_rewards_img_urls(int $user_id): array {
|
||||
$out = ['weixin' => '', 'alipay' => ''];
|
||||
if ($user_id <= 0) {
|
||||
return $out;
|
||||
}
|
||||
foreach (['weixin' => 'aksrt_rewards_weixin', 'alipay' => 'aksrt_rewards_alipay'] as $key => $meta) {
|
||||
$raw = get_user_meta($user_id, $meta, true);
|
||||
if (is_numeric($raw) && (int) $raw > 0) {
|
||||
$url = wp_get_attachment_image_url((int) $raw, 'full');
|
||||
$out[$key] = is_string($url) ? $url : '';
|
||||
} else {
|
||||
$out[$key] = esc_url_raw((string) $raw);
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_user_has_rewards_qr')) {
|
||||
function aksrt_user_has_rewards_qr(int $user_id): bool {
|
||||
$urls = aksrt_get_user_rewards_img_urls($user_id);
|
||||
return $urls['weixin'] !== '' || $urls['alipay'] !== '';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_get_rewards_button')) {
|
||||
function aksrt_get_rewards_button(int $user_id, string $class = 'but jb-blue ml10 pw-1em'): string {
|
||||
if ($user_id <= 0 || !aksrt_rewards_enabled() || !aksrt_user_has_rewards_qr($user_id)) {
|
||||
return '';
|
||||
}
|
||||
$label = '赞赏';
|
||||
if (class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_post_page_settings')) {
|
||||
$s = AKSRT_Settings::get_post_page_settings();
|
||||
$label = (string) ($s['rewards_btn_text'] ?? $s['rewards_text'] ?? $label);
|
||||
if ($label === '') {
|
||||
$label = '赞赏';
|
||||
}
|
||||
}
|
||||
return '<button type="button" class="' . esc_attr($class) . ' aksrt-reward-open" data-aksrt-reward-open data-user-id="' . (int) $user_id . '"><i class="fa fa-gift em09" aria-hidden="true"></i> ' . esc_html($label) . '</button>';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('aksrt_render_rewards_modal_for_user')) {
|
||||
function aksrt_render_rewards_modal_for_user(int $user_id): void {
|
||||
static $done = [];
|
||||
if ($user_id <= 0 || isset($done[$user_id])) {
|
||||
return;
|
||||
}
|
||||
if (!aksrt_rewards_enabled() || !aksrt_user_has_rewards_qr($user_id)) {
|
||||
return;
|
||||
}
|
||||
$done[$user_id] = true;
|
||||
$urls = aksrt_get_user_rewards_img_urls($user_id);
|
||||
$label = '赞赏';
|
||||
if (class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_post_page_settings')) {
|
||||
$s = AKSRT_Settings::get_post_page_settings();
|
||||
$label = (string) ($s['rewards_text'] ?? $label);
|
||||
if ($label === '') {
|
||||
$label = __('赞赏', 'aksrt');
|
||||
}
|
||||
}
|
||||
?>
|
||||
<div class="reward-modal" id="reward-modal-<?php echo (int) $user_id; ?>" data-user-id="<?php echo (int) $user_id; ?>" aria-hidden="true">
|
||||
<div class="reward-modal__overlay" data-aksrt-reward-close></div>
|
||||
<div class="reward-modal__dialog" role="dialog" aria-modal="true">
|
||||
<button type="button" class="reward-modal__close" data-aksrt-reward-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fas fa-times" aria-hidden="true"></i></button>
|
||||
<div class="reward-modal__content">
|
||||
<h3><?php echo esc_html($label); ?></h3>
|
||||
<div class="reward-modal__qrcodes flex jc ac hh">
|
||||
<?php if ($urls['weixin'] !== '') : ?>
|
||||
<div class="reward-modal__qrcode text-center mr10">
|
||||
<img src="<?php echo esc_url($urls['weixin']); ?>" alt="<?php esc_attr_e('微信收款码', 'aksrt'); ?>" loading="lazy" decoding="async" />
|
||||
<div class="em09 muted-2-color mt6"><?php esc_html_e('微信', 'aksrt'); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($urls['alipay'] !== '') : ?>
|
||||
<div class="reward-modal__qrcode text-center">
|
||||
<img src="<?php echo esc_url($urls['alipay']); ?>" alt="<?php esc_attr_e('支付宝收款码', 'aksrt'); ?>" loading="lazy" decoding="async" />
|
||||
<div class="em09 muted-2-color mt6"><?php esc_html_e('支付宝', 'aksrt'); ?></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,23 @@ if (!function_exists('aksrt_single_render_actions')) :
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!empty($settings['rewards_enabled']) && !empty($settings['rewards_image'])) : ?>
|
||||
<?php
|
||||
$reward_author = (int) get_the_author_meta('ID');
|
||||
$reward_btn = ($reward_author > 0 && function_exists('aksrt_get_rewards_button'))
|
||||
? aksrt_get_rewards_button($reward_author, 'action action-rewards')
|
||||
: '';
|
||||
if ($reward_btn === '' && !empty($settings['rewards_enabled']) && function_exists('aksrt_user_has_rewards_qr') && $reward_author > 0 && aksrt_user_has_rewards_qr($reward_author)) {
|
||||
$label = esc_html((string) ($settings['rewards_btn_text'] ?? __('赞赏', 'aksrt')));
|
||||
$reward_btn = '<button type="button" class="action action-rewards" data-aksrt-reward-open data-user-id="' . (int) $reward_author . '"><i class="fas fa-gift" aria-hidden="true"></i><span class="action-btn__label">' . $label . '</span></button>';
|
||||
}
|
||||
if ($reward_btn !== '') :
|
||||
echo $reward_btn; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
if (function_exists('aksrt_render_rewards_modal_for_user')) {
|
||||
aksrt_render_rewards_modal_for_user($reward_author);
|
||||
}
|
||||
endif;
|
||||
// legacy placeholder to keep structure; actual button echoed above
|
||||
if (false) : ?>
|
||||
<button type="button" class="action action-rewards" id="action-reward">
|
||||
<i class="fas fa-gift" aria-hidden="true"></i>
|
||||
<span class="action-btn__label"><?php echo esc_html($settings['rewards_btn_text'] ?? __('赞赏', 'aksrt')); ?></span>
|
||||
@@ -331,21 +347,59 @@ if (!function_exists('aksrt_single_render_author_card')) :
|
||||
$author_url = get_author_posts_url($author_id);
|
||||
$author_name = get_the_author();
|
||||
$bio = trim((string) get_the_author_meta('description'));
|
||||
$identity_chips = function_exists('aksrt_render_user_identity_chips')
|
||||
$name_html = function_exists('aksrt_get_user_name')
|
||||
? aksrt_get_user_name([
|
||||
'id' => $author_id,
|
||||
'class' => 'user-name flex jc ac',
|
||||
'vip' => true,
|
||||
'medal' => true,
|
||||
'auth' => true,
|
||||
'level' => true,
|
||||
'follow' => false,
|
||||
])
|
||||
: '';
|
||||
$identity_chips = ($name_html === '' && function_exists('aksrt_render_user_identity_chips'))
|
||||
? aksrt_render_user_identity_chips($author_id, 'card')
|
||||
: '';
|
||||
$avatar = function_exists('aksrt_get_avatar_html')
|
||||
? aksrt_get_avatar_html($author_id, 80, ['class' => 'avatar avatar-lg'])
|
||||
: get_avatar($author_id, 80, '', esc_attr($author_name), ['class' => 'avatar avatar-lg']);
|
||||
$recent = get_posts([
|
||||
'author' => $author_id,
|
||||
'post_type' => 'post',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 6,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
'no_found_rows' => true,
|
||||
]);
|
||||
$page_s = class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_post_page_settings')
|
||||
? AKSRT_Settings::get_post_page_settings()
|
||||
: [];
|
||||
$card_limit = max(0, min(12, (int) ($page_s['author_card_limit'] ?? 6)));
|
||||
$card_orderby = sanitize_key((string) ($page_s['author_card_orderby'] ?? 'views'));
|
||||
$card_style = sanitize_key((string) ($page_s['author_card_style'] ?? 'card'));
|
||||
$recent = [];
|
||||
if ($card_limit > 0) {
|
||||
$q = [
|
||||
'author' => $author_id,
|
||||
'post_type' => 'post',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => $card_limit,
|
||||
'ignore_sticky_posts' => true,
|
||||
'no_found_rows' => true,
|
||||
];
|
||||
$cur = get_the_ID();
|
||||
if ($cur) {
|
||||
$q['post__not_in'] = [(int) $cur];
|
||||
}
|
||||
if ($card_orderby === 'views') {
|
||||
$q['meta_key'] = 'views';
|
||||
$q['orderby'] = 'meta_value_num';
|
||||
$q['order'] = 'DESC';
|
||||
} elseif ($card_orderby === 'comment_count') {
|
||||
$q['orderby'] = 'comment_count';
|
||||
$q['order'] = 'DESC';
|
||||
} elseif ($card_orderby === 'modified') {
|
||||
$q['orderby'] = 'modified';
|
||||
$q['order'] = 'DESC';
|
||||
} else {
|
||||
$q['orderby'] = 'date';
|
||||
$q['order'] = 'DESC';
|
||||
}
|
||||
$recent = get_posts($q);
|
||||
}
|
||||
$is_self = is_user_logged_in() && get_current_user_id() === $author_id;
|
||||
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink());
|
||||
$aksrt_pm_on = class_exists('AKSRT_Settings') && !empty(AKSRT_Settings::private_message_settings()['enabled']);
|
||||
@@ -362,17 +416,34 @@ if (!function_exists('aksrt_single_render_author_card')) :
|
||||
<div class="user-content">
|
||||
<div class="user-avatar"><?php echo $avatar; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
|
||||
<div class="user-info mt20 mb10">
|
||||
<div class="user-name flex jc ac">
|
||||
<a class="display-name" href="<?php echo esc_url($author_url); ?>"><?php echo esc_html($author_name); ?></a>
|
||||
<?php if ($identity_chips !== '') : ?>
|
||||
<?php echo $identity_chips; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php if ($name_html !== '') : ?>
|
||||
<?php echo $name_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php else : ?>
|
||||
<div class="user-name flex jc ac">
|
||||
<a class="display-name" href="<?php echo esc_url($author_url); ?>"><?php echo esc_html($author_name); ?></a>
|
||||
<?php if ($identity_chips !== '') : ?>
|
||||
<?php echo $identity_chips; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php if ($bio !== '') : ?>
|
||||
<div class="user-desc mt10 muted-2-color em09"><?php echo esc_html($bio); ?></div>
|
||||
<?php endif; ?>
|
||||
<div class="user-btns mt20">
|
||||
<?php if (!$is_self) : ?>
|
||||
<?php if ($is_self) : ?>
|
||||
<?php
|
||||
$submit = function_exists('aksrt_get_page_url_by_template')
|
||||
? (string) aksrt_get_page_url_by_template('templates/pages/page-submit.php')
|
||||
: '';
|
||||
$center = function_exists('aksrt_get_user_center_url')
|
||||
? (string) aksrt_get_user_center_url()
|
||||
: $author_url;
|
||||
if ($submit !== '') :
|
||||
?>
|
||||
<a class="but pw-1em mr6 jb-pink" href="<?php echo esc_url($submit); ?>"><?php esc_html_e('发布文章', 'aksrt'); ?></a>
|
||||
<?php endif; ?>
|
||||
<a class="but pw-1em mr6 jb-blue" href="<?php echo esc_url($center); ?>"><?php esc_html_e('用户中心', 'aksrt'); ?></a>
|
||||
<?php else : ?>
|
||||
<?php if (is_user_logged_in()) : ?>
|
||||
<button type="button" class="but pw-1em mr6 c-red follow" data-aksrt-follow="<?php echo (int) $author_id; ?>">
|
||||
<i class="fa fa-heart em09" aria-hidden="true"></i> <span><?php esc_html_e('关注', 'aksrt'); ?></span>
|
||||
@@ -382,34 +453,56 @@ if (!function_exists('aksrt_single_render_author_card')) :
|
||||
<i class="fa fa-heart em09" aria-hidden="true"></i> <span><?php esc_html_e('关注', 'aksrt'); ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if (is_user_logged_in() && $aksrt_pm_on && $aksrt_msg_url !== '') : ?>
|
||||
<a class="but pw-1em mr6 c-blue" href="<?php echo esc_url(add_query_arg('msg_type', 'conversation', $aksrt_msg_url)); ?>">
|
||||
<i class="fa fa-comment em09" aria-hidden="true"></i> <?php esc_html_e('私信', 'aksrt'); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
$tip = function_exists('aksrt_get_rewards_button') ? aksrt_get_rewards_button($author_id, 'but pw-1em mr6 c-yellow') : '';
|
||||
if ($tip !== '') {
|
||||
echo $tip; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
if (function_exists('aksrt_render_rewards_modal_for_user')) {
|
||||
aksrt_render_rewards_modal_for_user($author_id);
|
||||
}
|
||||
}
|
||||
?>
|
||||
<a class="but pw-1em mr6 jb-blue" href="<?php echo esc_url($author_url); ?>"><?php esc_html_e('个人主页', 'aksrt'); ?></a>
|
||||
<?php endif; ?>
|
||||
<?php if (!$is_self && is_user_logged_in() && $aksrt_pm_on && $aksrt_msg_url !== '') : ?>
|
||||
<a class="but pw-1em mr6 c-blue" href="<?php echo esc_url(add_query_arg('msg_type', 'conversation', $aksrt_msg_url)); ?>">
|
||||
<i class="fa fa-comment em09" aria-hidden="true"></i> <?php esc_html_e('私信', 'aksrt'); ?>
|
||||
</a>
|
||||
<?php elseif (!$is_self && $rewards_on) : ?>
|
||||
<button type="button" class="but pw-1em mr6 c-yellow" data-aksrt-reward-open>
|
||||
<i class="fa fa-gift em09" aria-hidden="true"></i> <?php esc_html_e('赞赏', 'aksrt'); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<a class="but pw-1em mr6 jb-blue" href="<?php echo esc_url($author_url); ?>"><?php esc_html_e('个人主页', 'aksrt'); ?></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!empty($recent)) : ?>
|
||||
<div class="user-card-posts mt10">
|
||||
<ul class="no-thumb">
|
||||
<?php foreach ($recent as $rp) : ?>
|
||||
<?php if ($rp instanceof WP_Post) : ?>
|
||||
<li><a class="icon-circle" href="<?php echo esc_url(get_permalink($rp)); ?>"><?php echo esc_html(get_the_title($rp)); ?></a></li>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php if ($card_style === 'card') : ?>
|
||||
<div class="more-posts-mini flex hh">
|
||||
<?php foreach ($recent as $rp) : ?>
|
||||
<?php if ($rp instanceof WP_Post) :
|
||||
$thumb = get_the_post_thumbnail_url($rp, 'medium');
|
||||
?>
|
||||
<a class="item mr10 mb10" href="<?php echo esc_url(get_permalink($rp)); ?>" style="display:block;width:46%;">
|
||||
<?php if ($thumb) : ?>
|
||||
<img class="fit-cover radius8" src="<?php echo esc_url($thumb); ?>" alt="" loading="lazy" decoding="async" style="width:100%;aspect-ratio:16/10;object-fit:cover;" />
|
||||
<?php endif; ?>
|
||||
<div class="em09 mt6 text-ellipsis"><?php echo esc_html(get_the_title($rp)); ?></div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<ul class="no-thumb">
|
||||
<?php foreach ($recent as $rp) : ?>
|
||||
<?php if ($rp instanceof WP_Post) : ?>
|
||||
<li><a class="icon-circle" href="<?php echo esc_url(get_permalink($rp)); ?>"><?php echo esc_html(get_the_title($rp)); ?></a></li>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="user-card-posts mt10 muted-2-color em09 text-center"><?php esc_html_e('暂无更多文章', 'aksrt'); ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif;
|
||||
?> </div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
@@ -167,8 +167,10 @@ $pane = isset($points_tasks_pane) ? sanitize_key((string) $points_tasks_pane)
|
||||
<?php
|
||||
$task_key = sanitize_key((string) ($task['key'] ?? ''));
|
||||
$action = $aksrt_points_task_action($task_key);
|
||||
$status = sanitize_key((string) ($task['status'] ?? 'todo'));
|
||||
$done = in_array($status, ['done_today', 'done'], true);
|
||||
?>
|
||||
<div class="border-bottom padding-h10 uc-points-tasks__row" data-task-key="<?php echo esc_attr($task_key); ?>">
|
||||
<div class="border-bottom padding-h10 uc-points-tasks__row<?php echo $done ? ' is-done' : ''; ?>" data-task-key="<?php echo esc_attr($task_key); ?>" data-status="<?php echo esc_attr($status); ?>">
|
||||
<div class="flex jsb ac">
|
||||
<div class="flex1 mr20">
|
||||
<div class="font-bold mb6"><?php echo esc_html((string) $task['title']); ?></div>
|
||||
|
||||
@@ -48,10 +48,23 @@ $aksrt_msg_url = function_exists('aksrt_get_messages_page_url') ? aksrt_get_mess
|
||||
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink());
|
||||
$is_self = is_user_logged_in() && get_current_user_id() === $author_id;
|
||||
|
||||
// Identity chips: VIP + verify/level/badges (shared helper).
|
||||
$identity_chips = function_exists('aksrt_render_user_identity_chips')
|
||||
? aksrt_render_user_identity_chips($author_id, 'author')
|
||||
: '';
|
||||
// Identity: prefer aksrt_get_user_name (Zibll order); fall back to chips.
|
||||
$identity_chips = '';
|
||||
$name_html = '';
|
||||
if (function_exists('aksrt_get_user_name')) {
|
||||
$name_html = aksrt_get_user_name([
|
||||
'id' => $author_id,
|
||||
'class' => 'name flex ac flex1',
|
||||
'name_class' => '',
|
||||
'vip' => true,
|
||||
'medal' => true,
|
||||
'auth' => true,
|
||||
'level' => true,
|
||||
'follow' => false,
|
||||
]);
|
||||
} elseif (function_exists('aksrt_render_user_identity_chips')) {
|
||||
$identity_chips = aksrt_render_user_identity_chips($author_id, 'author');
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="<?php echo esc_attr($classes); ?>">
|
||||
@@ -67,12 +80,16 @@ $identity_chips = function_exists('aksrt_render_user_identity_chips')
|
||||
</a>
|
||||
<div class="user-right flex flex1 ac jsb ml10">
|
||||
<div class="flex1">
|
||||
<span class="name flex ac flex1">
|
||||
<a class="display-name text-ellipsis" href="<?php echo esc_url($author_url); ?>"><?php echo esc_html($author_name); ?></a>
|
||||
<?php if ($identity_chips !== '') : ?>
|
||||
<?php echo $identity_chips; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php if ($name_html !== '') : ?>
|
||||
<?php echo $name_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php else : ?>
|
||||
<span class="name flex ac flex1">
|
||||
<a class="display-name text-ellipsis" href="<?php echo esc_url($author_url); ?>"><?php echo esc_html($author_name); ?></a>
|
||||
<?php if ($identity_chips !== '') : ?>
|
||||
<?php echo $identity_chips; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
|
||||
<?php endif; ?>
|
||||
</span>
|
||||
<?php endif; ?>
|
||||
<div class="px12-sm muted-2-color text-ellipsis">
|
||||
<?php if ($show_time) : ?>
|
||||
<span class="icon-circle" title="<?php echo esc_attr($abs_fmt); ?>" data-toggle="tooltip"><?php echo esc_html($time_text); ?></span>
|
||||
|
||||
Reference in New Issue
Block a user