Merge pull request 'feat(users): govern profiles and access' (#6) from worktree/fix-user-function into main

Reviewed-on: #6
This commit is contained in:
2026-08-12 16:30:52 +00:00
67 changed files with 780 additions and 1127 deletions
@@ -7,37 +7,15 @@ export const permissionsSection = {
route: '/permissions',
children: [
customPage({
key: 'permissions-comment',
name: 'PermissionsComment',
label: '评论权限',
route: '/permissions/comment',
component: 'CommentPermissions',
title: '评论权限',
desc: '游客评论、频率限制、敏感词和审核策略。',
key: 'permissions-capabilities',
name: 'PermissionsCapabilities',
label: '基本权限',
route: '/permissions/capabilities',
component: 'PermissionsCapabilities',
title: '基本权限 / 能力矩阵',
desc: '统一管理用户、投稿、评论、上传和下载的准入能力。',
canonicalDomain: 'permissions',
section: 'comment',
}),
customPage({
key: 'permissions-attachment',
name: 'PermissionsAttachment',
label: '附件权限',
route: '/permissions/attachment',
component: 'AttachmentPermissions',
title: '附件权限',
desc: '上传角色、文件类型、大小限制和审核状态。',
canonicalDomain: 'permissions',
section: 'attachment',
}),
customPage({
key: 'permissions-download',
name: 'PermissionsDownload',
label: '下载权限',
route: '/permissions/download',
component: 'DownloadPermissions',
title: '下载权限',
desc: '登录要求、积分要求、角色要求和 VIP 特权。',
canonicalDomain: 'permissions',
section: 'download',
section: 'capabilities',
}),
schemaPage({
key: 'permissions-login',
@@ -52,23 +30,23 @@ export const permissionsSection = {
schemaPage({
key: 'permissions-moderation',
name: 'PermissionsModeration',
label: '审核和风控',
label: '内容审核',
route: '/permissions/moderation',
title: '审核和风控',
title: '内容审核',
desc: '评论审核、反垃圾、限流与风控配置。',
canonicalDomain: 'permissions',
section: 'moderation',
}),
customPage({
key: 'permissions-capabilities',
name: 'PermissionsCapabilities',
label: '基本权限',
route: '/permissions/capabilities',
component: 'PermissionsCapabilities',
title: '基本权限 / 能力矩阵',
desc: '统一管理用户治理、投稿和评论的角色能力矩阵。',
key: 'permissions-attachment',
name: 'PermissionsAttachment',
label: '附件配置',
route: '/permissions/attachment',
component: 'AttachmentPermissions',
title: '附件配置',
desc: '管理文件大小、类型、安全限制和审核策略。',
canonicalDomain: 'permissions',
section: 'capabilities',
section: 'attachment',
}),
],
}
+1 -21
View File
@@ -6,7 +6,7 @@
>
<template #actions>
<n-space>
<n-button quaternary @click="go('/permissions/download')">下载权限</n-button>
<n-button quaternary @click="go('/permissions/capabilities')">基本权限</n-button>
<n-button quaternary @click="go('/payment/gateway')">收款接口</n-button>
</n-space>
</template>
@@ -18,15 +18,9 @@
<h3>当前联动摘要</h3>
</div>
<n-space>
<n-tag round :type="form.download_login ? 'warning' : 'default'">
登录门槛:{{ form.download_login ? '已启用' : '已关闭' }}
</n-tag>
<n-tag round :type="form.download_credit ? 'warning' : 'default'">
积分购买模式:{{ form.download_credit ? '已启用' : '已关闭' }}
</n-tag>
<n-tag round :type="form.download_captcha ? 'warning' : 'default'">
下载验证码:{{ form.download_captcha ? '已启用' : '已关闭' }}
</n-tag>
<n-tag round type="info">
默认积分价:{{ form.download_default_cost }} {{ currencyName }}
</n-tag>
@@ -44,10 +38,6 @@
<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.download_login" />
</n-form-item>
<n-form-item label="允许积分购买模式">
<n-switch v-model:value="form.download_credit" />
<template #feedback>
@@ -55,10 +45,6 @@
</template>
</n-form-item>
<n-form-item label="下载启用验证码">
<n-switch v-model:value="form.download_captcha" />
</n-form-item>
<n-form-item :label="`默认积分价(${currencyName})`">
<n-input-number v-model:value="form.download_default_cost" :min="0" :max="100000" style="width: 180px;" />
<template #feedback>
@@ -140,9 +126,7 @@ const creditSnapshot = ref({})
const overview = ref({})
const form = reactive({
download_login: true,
download_credit: true,
download_captcha: false,
download_default_cost: 0,
payment_paid_post_types: 'post',
payment_vip_free_download: false,
@@ -185,9 +169,7 @@ async function load() {
creditSnapshot.value = creditData
overview.value = extractData(overviewRes)
form.download_login = featureData.download_login !== false
form.download_credit = featureData.download_credit !== false
form.download_captcha = !!featureData.download_captcha
form.download_default_cost = Number(creditData.download_default_cost || 0)
form.payment_paid_post_types = schemaData.payment_paid_post_types || 'post'
form.payment_vip_free_download = !!schemaData.payment_vip_free_download
@@ -206,9 +188,7 @@ async function handleSave() {
try {
const nextFeatures = {
...featureSnapshot.value,
download_login: form.download_login,
download_credit: form.download_credit,
download_captcha: form.download_captcha,
}
const nextCredits = {
...creditSnapshot.value,
-9
View File
@@ -24,12 +24,6 @@
</template>
</n-form-item>
<n-form-item v-if="basic.comment_enabled" label="必须注册才能评论">
<n-switch v-model:value="basic.comment_requires_registration">
<template #checked>是</template>
<template #unchecked>否</template>
</n-switch>
</n-form-item>
</n-form>
</n-card>
@@ -123,7 +117,6 @@ const saving = ref(false)
// 后端 /settings/basic 返回基础字段;auth 相关字段由 /settings/auth 独立维护。
const basic = reactive({
comment_enabled: true,
comment_requires_registration: false,
icp_number: '',
police_number: '',
lazyload_enabled: true,
@@ -139,7 +132,6 @@ async function loadAll() {
if (basicRes?.code === 'success' && basicRes.data) {
const d = basicRes.data
basic.comment_enabled = d.comment_enabled !== false
basic.comment_requires_registration = !!d.comment_requires_registration
basic.icp_number = d.icp_number || ''
basic.police_number = d.police_number || ''
basic.lazyload_enabled = d.lazyload_enabled !== false
@@ -159,7 +151,6 @@ async function handleSave() {
try {
const basicPayload = {
comment_enabled: basic.comment_enabled,
comment_requires_registration: basic.comment_requires_registration,
icp_number: basic.icp_number,
police_number: basic.police_number,
lazyload_enabled: basic.lazyload_enabled,
@@ -2,11 +2,10 @@
<div class="aksrt-page-container">
<PageHeader
title="附件治理"
desc="统一管理全站上传行为:头像、封面、投稿图片、评论图片、背景图和资源附件。"
desc="管理各类上传文件的大小、安全限制和审核策略;准入权限请在“基本权限”中配置。"
>
<template #actions>
<n-space>
<n-button quaternary @click="go('/permissions/download')">下载权限</n-button>
<n-button quaternary @click="goModeration">审核策略</n-button>
</n-space>
</template>
@@ -34,31 +33,10 @@
<!-- Avatar & Cover -->
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>头像与封面</h3>
<h3>用户资料卡背景图</h3>
</div>
<n-form :model="form" label-placement="left" label-width="200" class="aksrt-form">
<n-form-item label="允许用户上传头像">
<n-switch v-model:value="form.allow_avatar_upload" />
<template #feedback>
<span class="aksrt-hint-text">关闭后用户使用项目内置默认头像,且不能自行更换。</span>
</template>
</n-form-item>
<n-form-item label="允许用户上传封面图">
<n-switch v-model:value="form.allow_cover_upload" />
<template #feedback>
<span class="aksrt-hint-text">控制用户中心公开主页封面图上传入口。</span>
</template>
</n-form-item>
<n-form-item label="允许用户上传背景图">
<n-switch v-model:value="form.allow_background_upload" />
<template #feedback>
<span class="aksrt-hint-text">控制用户中心公开可用的背景图上传入口。</span>
</template>
</n-form-item>
<n-form-item label="头像/封面大小上限 (MB)">
<n-form-item label="头像/用户资料卡背景图大小上限 (MB)">
<n-input-number v-model:value="form.avatar_max_size" :min="1" :max="10" style="max-width:160px;" />
</n-form-item>
</n-form>
@@ -70,24 +48,10 @@
<h3>内容上传</h3>
</div>
<n-form :model="form" label-placement="left" label-width="200" class="aksrt-form">
<n-form-item label="允许投稿插入图片">
<n-switch v-model:value="form.allow_submit_images" />
<template #feedback>
<span class="aksrt-hint-text">前台投稿编辑器中允许插入图片。</span>
</template>
</n-form-item>
<n-form-item label="投稿图片大小上限 (MB)">
<n-input-number v-model:value="form.submit_image_max_size" :min="1" :max="20" style="max-width:160px;" />
</n-form-item>
<n-form-item label="允许评论插入图片">
<n-switch v-model:value="form.allow_comment_images" />
<template #feedback>
<span class="aksrt-hint-text">评论工具栏中允许插入图片链接或上传图片。</span>
</template>
</n-form-item>
<n-form-item label="评论图片大小上限 (MB)">
<n-input-number v-model:value="form.comment_image_max_size" :min="1" :max="10" style="max-width:160px;" />
</n-form-item>
@@ -100,13 +64,6 @@
<h3>资源附件</h3>
</div>
<n-form :model="form" label-placement="left" label-width="200" class="aksrt-form">
<n-form-item label="允许资源附件上传">
<n-switch v-model:value="form.allow_resource_upload" />
<template #feedback>
<span class="aksrt-hint-text">后台资源编辑器中允许上传附件文件。</span>
</template>
</n-form-item>
<n-form-item label="资源附件大小上限 (MB)">
<n-input-number v-model:value="form.resource_max_size" :min="1" :max="200" style="max-width:160px;" />
</n-form-item>
@@ -158,27 +115,19 @@ const saving = ref(false)
const moderationSnapshot = ref({})
const form = reactive({
allow_avatar_upload: true,
allow_cover_upload: true,
allow_background_upload: false,
avatar_max_size: 2,
allow_submit_images: true,
submit_image_max_size: 5,
allow_comment_images: false,
comment_image_max_size: 2,
allow_resource_upload: true,
resource_max_size: 50,
ai_moderate_background: true,
ai_moderate_submit: false,
})
const summaryItems = computed(() => [
{ key: 'avatar', icon: 'fas fa-user-circle', label: '头像上传', enabled: form.allow_avatar_upload },
{ key: 'cover', icon: 'fas fa-image', label: '封面上传', enabled: form.allow_cover_upload },
{ key: 'background', icon: 'fas fa-photo-video', label: '背景图上传', enabled: form.allow_background_upload },
{ key: 'submit', icon: 'fas fa-edit', label: '投稿图片', enabled: form.allow_submit_images },
{ key: 'comment', icon: 'fas fa-comment-dots', label: '评论图片', enabled: form.allow_comment_images },
{ key: 'resource', icon: 'fas fa-file-archive', label: '资源附件', enabled: form.allow_resource_upload },
{ key: 'profile', icon: 'fas fa-id-card', label: '资料卡图片', enabled: true },
{ key: 'submit', icon: 'fas fa-edit', label: '投稿图片', enabled: true },
{ key: 'comment', icon: 'fas fa-comment-dots', label: '评论图片', enabled: true },
{ key: 'resource', icon: 'fas fa-file-archive', label: '资源附件', enabled: true },
])
function go(path) {
@@ -199,15 +148,9 @@ async function load() {
const moderation = moderationRes?.data ?? moderationRes ?? {}
// Map permission fields
form.allow_avatar_upload = permission.allow_avatar_upload !== false
form.allow_cover_upload = permission.allow_cover_upload !== false
form.allow_background_upload = !!permission.allow_background_upload
form.avatar_max_size = Number(permission.avatar_max_size || 2)
form.allow_submit_images = permission.allow_submit_images !== false
form.submit_image_max_size = Number(permission.submit_image_max_size || 5)
form.allow_comment_images = !!permission.allow_comment_images
form.comment_image_max_size = Number(permission.comment_image_max_size || 2)
form.allow_resource_upload = permission.allow_resource_upload !== false
form.resource_max_size = Number(permission.resource_max_size || 50)
form.ai_moderate_background = moderation.ai_moderate_background !== false
@@ -226,15 +169,9 @@ async function handleSave() {
try {
await Promise.all([
api.savePermissionsAttachmentSettings({
allow_avatar_upload: form.allow_avatar_upload,
allow_cover_upload: form.allow_cover_upload,
allow_background_upload: form.allow_background_upload,
avatar_max_size: form.avatar_max_size,
allow_submit_images: form.allow_submit_images,
submit_image_max_size: form.submit_image_max_size,
allow_comment_images: form.allow_comment_images,
comment_image_max_size: form.comment_image_max_size,
allow_resource_upload: form.allow_resource_upload,
resource_max_size: form.resource_max_size,
}),
api.savePermissionsModerationSettings({
@@ -1,154 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader
title="评论权限"
desc="管理评论入口的登录要求与回复权限,并查看当前验证码与审核策略状态。"
>
<template #actions>
<n-space>
<n-button quaternary @click="go('/permissions/login')">人机验证</n-button>
<n-button quaternary @click="go('/permissions/moderation')">审核策略</n-button>
</n-space>
</template>
</PageHeader>
<n-spin :show="loading" style="width:100%; min-height:200px;">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>当前联动状态</h3>
</div>
<n-space>
<n-tag :type="status.commentCaptchaEnabled ? 'success' : 'default'" round>
评论验证码:{{ status.commentCaptchaEnabled ? '已启用' : '未启用' }}
</n-tag>
<n-tag :type="status.moderationEnabled ? 'warning' : 'default'" round>
审核链路:{{ status.moderationEnabled ? '已启用' : '未启用' }}
</n-tag>
<n-tag :type="status.rateLimitEnabled ? 'warning' : 'default'" round>
评论限流:{{ status.rateLimitEnabled ? '已启用' : '未启用' }}
</n-tag>
<n-tag :type="status.keywordRuleCount > 0 ? 'error' : 'default'" round>
敏感词:{{ status.keywordRuleCount }} 条
</n-tag>
</n-space>
</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-switch v-model:value="form.comment_require_login" />
<template #feedback>
<span class="aksrt-hint-text">开启后,游客只能浏览评论,不能直接提交评论。</span>
</template>
</n-form-item>
<n-form-item label="允许楼中回复">
<n-switch v-model:value="form.comment_allow_reply" />
<template #feedback>
<span class="aksrt-hint-text">关闭后,前台评论列表将不再展示回复按钮。</span>
</template>
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card">
<n-alert type="info" :show-icon="false">
评论排序、分页方式、头像大小和空状态文案继续在“文章&列表 -> 评论设置”中维护;本页只负责准入与治理联动。
</n-alert>
</n-card>
<div class="aksrt-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
</div>
</n-spin>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
const router = useRouter()
const message = useMessage()
const loading = ref(true)
const saving = ref(false)
const form = reactive({
comment_require_login: false,
comment_allow_reply: true,
})
const status = reactive({
commentCaptchaEnabled: false,
moderationEnabled: false,
rateLimitEnabled: false,
keywordRuleCount: 0,
})
function go(path) {
router.push(path)
}
function splitKeywords(value) {
return String(value || '')
.split(/\r?\n/)
.map((item) => item.trim())
.filter(Boolean)
}
async function load() {
loading.value = true
try {
const [commentData, captchaRes, moderationRes] = await Promise.all([
api.getSchemaPage('post_list', 'comment'),
api.getPermissionsLoginSettings(),
api.getPermissionsModerationSettings(),
])
const comment = commentData?.data ?? commentData ?? {}
const captcha = captchaRes?.data ?? captchaRes ?? {}
const moderation = moderationRes?.data ?? moderationRes ?? {}
const enabledScenarios = Array.isArray(captcha.enabled_scenarios) ? captcha.enabled_scenarios : null
const disabledScenarios = Array.isArray(captcha.disabled_scenarios) ? captcha.disabled_scenarios : []
const commentSceneOn = enabledScenarios
? enabledScenarios.includes('comment')
: !disabledScenarios.includes('comment')
form.comment_require_login = !!comment.comment_require_login
form.comment_allow_reply = comment.comment_allow_reply !== false
status.commentCaptchaEnabled = !!captcha.enabled && !!captcha.active_provider && commentSceneOn
status.moderationEnabled = !!moderation.enabled
status.rateLimitEnabled = !!moderation.rate_limit_enabled
status.keywordRuleCount = splitKeywords(moderation.blocked_keywords).length
} catch (error) {
message.error(error.message || '加载评论权限失败')
} finally {
loading.value = false
}
}
async function handleSave() {
saving.value = true
try {
await api.saveSchemaPage('post_list', 'comment', {
comment_require_login: form.comment_require_login,
comment_allow_reply: form.comment_allow_reply,
})
message.success('评论权限已保存')
} catch (error) {
message.error(error.message || '保存失败')
} finally {
saving.value = false
}
}
onMounted(load)
</script>
@@ -1,161 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader
title="下载权限"
desc="管理资源下载的登录、积分购买模式与验证码门槛。付费解锁走收银台订单,下载 token 不再按次扣积分。"
>
<template #actions>
<n-space>
<n-button quaternary @click="go('/payment/paid')">资源下载页</n-button>
<n-button quaternary @click="go('/permissions/login')">人机验证</n-button>
</n-space>
</template>
</PageHeader>
<n-spin :show="loading" style="width:100%; min-height:200px;">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>当前门槛摘要</h3>
</div>
<n-space>
<n-tag :type="form.download_login ? 'warning' : 'default'" round>
登录要求:{{ form.download_login ? '已启用' : '已关闭' }}
</n-tag>
<n-tag :type="form.download_credit ? 'warning' : 'default'" round>
积分购买模式:{{ form.download_credit ? '已启用' : '已关闭' }}
</n-tag>
<n-tag :type="form.download_captcha ? 'warning' : 'default'" round>
下载验证码:{{ form.download_captcha ? '已启用' : '已关闭' }}
</n-tag>
<n-tag type="info" round>默认积分价参考:{{ defaultCost }}</n-tag>
<n-tag type="info" round>Token TTL:10 分钟</n-tag>
</n-space>
</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-switch v-model:value="form.download_login" />
<template #feedback>
<span class="aksrt-hint-text">关闭后,单个资源的登录要求仍可通过资源元字段单独覆盖。</span>
</template>
</n-form-item>
<n-form-item label="允许积分购买模式">
<n-switch v-model:value="form.download_credit" />
<template #feedback>
<span class="aksrt-hint-text">开启后,文章 pay_modo=积分 可走收银台一次购买解锁;下载 token 不再按次扣积分。关闭时积分模式资源按免费/需登录策略处理。</span>
</template>
</n-form-item>
<n-form-item label="下载启用验证码">
<n-switch v-model:value="form.download_captcha" />
<template #feedback>
<span class="aksrt-hint-text">具体验证码服务商与场景白名单仍在「人机验证」页维护。</span>
</template>
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card">
<n-alert type="info" :show-icon="false">
实际价格以文章「付费内容设置」为准;本页只维护全局访问门槛。VIP 白名单与收银台策略见「支付 &amp; 付费」相关页面。
</n-alert>
</n-card>
<div class="aksrt-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
</div>
</n-spin>
</div>
</template>
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
const router = useRouter()
const message = useMessage()
const loading = ref(true)
const saving = ref(false)
const defaultCost = ref(0)
const featureSnapshot = ref({
resource_cpt_enabled: true,
search_include_resource: true,
download_login: true,
download_credit: true,
download_captcha: false,
})
const form = reactive({
download_login: true,
download_credit: true,
download_captcha: false,
})
function go(path) {
router.push(path)
}
async function load() {
loading.value = true
try {
const [featureRes, creditRes] = await Promise.all([
api.getCmsFeatures(),
api.getCreditSettings(),
])
const features = featureRes?.cms_features ?? featureRes?.data?.cms_features ?? {}
const creditSettings = creditRes?.data ?? creditRes ?? {}
featureSnapshot.value = {
resource_cpt_enabled: features.resource_cpt_enabled !== false,
search_include_resource: features.search_include_resource !== false,
download_login: features.download_login !== false,
download_credit: features.download_credit !== false,
download_captcha: !!features.download_captcha,
}
form.download_login = featureSnapshot.value.download_login
form.download_credit = featureSnapshot.value.download_credit
form.download_captcha = featureSnapshot.value.download_captcha
defaultCost.value = Number(creditSettings.download_default_cost || 0)
} catch (error) {
message.error(error.message || '加载下载权限失败')
} finally {
loading.value = false
}
}
async function handleSave() {
saving.value = true
try {
await api.saveCmsFeatures({
...featureSnapshot.value,
download_login: form.download_login,
download_credit: form.download_credit,
download_captcha: form.download_captcha,
})
featureSnapshot.value = {
...featureSnapshot.value,
download_login: form.download_login,
download_credit: form.download_credit,
download_captcha: form.download_captcha,
}
message.success('下载权限已保存')
} catch (error) {
message.error(error.message || '保存失败')
} finally {
saving.value = false
}
}
onMounted(load)
</script>
@@ -12,8 +12,7 @@
</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 始终可通过。
每项能力都通过“所有人、已登录、等级、VIP、已认证”五列治理;管理员(manage_options)始终可通过。
</n-alert>
+20 -112
View File
@@ -1,49 +1,30 @@
<template>
<div class="aksrt-page-container">
<PageHeader
title="用户功能"
desc="统一配置用户中心和公开主页的功能模块,让前台页面和后台字段使用同一份契约。"
>
<PageHeader title="用户功能" desc="用户中心的资料、背景图与公开资料均使用统一的能力矩阵和隐私规则。">
<template #actions>
<n-space>
<n-button quaternary @click="go('/permissions/capabilities')">基本权限</n-button>
<n-button quaternary @click="go('/page-display/user-home')">公开主页</n-button>
<n-button quaternary @click="go('/permissions/attachment')">附件权限</n-button>
</n-space>
</template>
</PageHeader>
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
<n-spin :show="loading" style="width: 100%; min-height: 180px;">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>当前联动摘要</h3>
</div>
<n-space>
<n-tag type="info" round>固定入口:个人信息 / 账号设置 / 消息通知</n-tag>
<n-tag round :type="form.user_cover_upload ? 'success' : 'default'">
背景图编辑位:{{ form.user_cover_upload ? '已展示' : '已隐藏' }}
</n-tag>
<n-tag round :type="backgroundUploadAllowed ? 'success' : 'warning'">
背景图上传权限:{{ backgroundUploadAllowed ? '已开放' : '未开放' }}
</n-tag>
<n-tag round :type="homeSummary.sidebar ? 'success' : 'default'">
公开主页侧栏:{{ homeSummary.sidebar ? '开启' : '关闭' }}
</n-tag>
<div class="aksrt-section-header"><h3>固定资料与操作入口</h3></div>
<n-space vertical size="large">
<n-alert type="info" :show-icon="false">
个人资料固定为昵称、个人签名、隐私设置、性别、个人网站、QQ、微信和 GitHub。公开资料由访问者身份过滤,原始社交资料不会下发给无权访客。
</n-alert>
<n-alert type="info" :show-icon="false">
背景图操作入口始终显示在资料卡右下角的三点菜单中;“上传头像”和“上传用户资料卡背景图”由基本权限矩阵控制。
</n-alert>
</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 label-placement="left" label-width="170" class="aksrt-form">
<n-form-item label="固定面板">
<n-space>
<n-tag round>个人信息</n-tag>
<n-tag round>账号设置</n-tag>
<n-tag round>消息通知</n-tag>
</n-space>
</n-form-item>
<n-form-item label="可切换业务面板">
<n-checkbox-group v-model:value="form.dynamicTabs">
<n-space vertical>
@@ -53,40 +34,10 @@
<n-checkbox value="credits">账户余额 / 签到</n-checkbox>
</n-space>
</n-checkbox-group>
<template #feedback>
<span class="aksrt-hint-text">“个人信息”始终保留在首位;积分面板仅在积分系统启用时显示。</span>
</template>
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<h3>公开资料编辑位</h3>
</div>
<n-form :model="form" label-placement="left" label-width="170" class="aksrt-form">
<n-form-item label="背景图编辑位">
<n-switch v-model:value="form.user_cover_upload" />
<template #feedback>
<span class="aksrt-hint-text">这里只控制前台入口显示;真正的上传权限仍由“附件权限”页治理。</span>
</template>
</n-form-item>
<n-form-item label="公开社交链接">
<n-switch v-model:value="form.user_social_links" />
<template #feedback>
<span class="aksrt-hint-text">关闭后,用户中心联系方式编辑区和公开主页联系卡片会一起隐藏。</span>
</template>
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card">
<n-alert type="info" :show-icon="false">
公开主页的封面、侧栏、关注按钮和公开 Tab 仍在“页面&显示 / 用户主页”维护;本页负责“用户中心能否编辑哪些内容”。
</n-alert>
</n-card>
<div class="aksrt-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存配置</n-button>
</div>
@@ -103,61 +54,22 @@ import PageHeader from '@/components/PageHeader.vue'
const router = useRouter()
const message = useMessage()
const loading = ref(true)
const saving = ref(false)
const backgroundUploadAllowed = ref(false)
const homeSummary = reactive({
tabs: [],
sidebar: true,
})
const form = reactive({
dynamicTabs: ['posts', 'comments', 'favorites'],
user_cover_upload: true,
user_social_links: true,
})
function go(path) {
router.push(path)
}
function extractData(response) {
return response?.data ?? response ?? {}
}
const form = reactive({ dynamicTabs: ['posts', 'comments', 'favorites'] })
function go(path) { router.push(path) }
function extractData(response) { return response?.data ?? response ?? {} }
function parseTabs(value) {
const items = Array.isArray(value)
? value
: String(value || '')
.split(/[\s,]+/)
.filter(Boolean)
return items
.map((item) => String(item).trim())
.filter((item) => ['posts', 'comments', 'favorites', 'credits'].includes(item))
const values = Array.isArray(value) ? value : String(value || '').split(/[\s,]+/)
return values.map((value) => String(value).trim()).filter((value) => ['posts', 'comments', 'favorites', 'credits'].includes(value))
}
async function load() {
loading.value = true
try {
const [featureRes, homeRes, permissionRes] = await Promise.all([
api.getSchemaPage('users', 'features'),
api.getSchemaPage('page_display', 'user-home'),
api.getPermissionsAttachmentSettings(),
])
const featureData = extractData(featureRes)
const homeData = extractData(homeRes)
const permissionData = extractData(permissionRes)
form.dynamicTabs = parseTabs(featureData.user_center_tabs || 'profile,posts,comments,favorites')
form.user_cover_upload = featureData.user_cover_upload !== false
form.user_social_links = featureData.user_social_links !== false
homeSummary.tabs = parseTabs(homeData.user_home_tabs || 'posts,comments,favorites')
homeSummary.sidebar = homeData.user_home_sidebar !== false
backgroundUploadAllowed.value = !!permissionData.allow_background_upload
const data = extractData(await api.getSchemaPage('users', 'features'))
form.dynamicTabs = parseTabs(data.user_center_tabs || 'profile,posts,comments,favorites')
} catch (error) {
message.error(error.message || '加载用户功能配置失败')
} finally {
@@ -168,11 +80,7 @@ async function load() {
async function handleSave() {
saving.value = true
try {
await api.saveSchemaPage('users', 'features', {
user_center_tabs: ['profile', ...form.dynamicTabs].join(','),
user_cover_upload: form.user_cover_upload,
user_social_links: form.user_social_links,
})
await api.saveSchemaPage('users', 'features', { user_center_tabs: ['profile', ...form.dynamicTabs].join(',') })
message.success('用户功能配置已保存')
} catch (error) {
message.error(error.message || '保存失败')
@@ -10,10 +10,8 @@ export const corePageLoaders = {
BannerManager: () => import('@/pages/content/BannerManager.vue'),
PostPageSettings: () => import('@/pages/post-list/PostPageSettings.vue'),
CommentSettings: () => import('@/pages/post-list/CommentSettings.vue'),
CommentPermissions: () => import('@/pages/permissions/CommentPermissions.vue'),
PermissionsCapabilities: () => import('@/pages/permissions/PermissionsCapabilities.vue'),
AttachmentPermissions: () => import('@/pages/permissions/AttachmentPermissions.vue'),
DownloadPermissions: () => import('@/pages/permissions/DownloadPermissions.vue'),
CmsCredits: () => import('@/pages/CmsCredits.vue'),
ResourceDownload: () => import('@/pages/ResourceDownload.vue'),
ThemeInfoPage: () => import('@/pages/theme/ThemeInfoPage.vue'),
@@ -15,7 +15,6 @@ export const orderSchema = {
payment_order_max_minutes: 30,
pay_mark: '付费',
pay_box_position: 'top',
pay_free_logged_show: false,
payment_show_ip: false,
payment_show_device: false,
payment_show_order_no: true,
@@ -58,7 +57,6 @@ export const orderSchema = {
fields: [
{ key: 'pay_mark', label: '付费标记', type: 'input', default: '付费' },
{ key: 'pay_box_position', label: '付费框位置', type: 'radio', default: 'top', options: [{ label: '文章框顶部(封面后)', value: 'box_top' }, { label: '正文顶部', value: 'top' }, { label: '正文底部', value: 'bottom' }, { label: '文章框底部(相关前)', value: 'box_bottom' }] },
{ key: 'pay_free_logged_show', label: '登录用户显示免费', type: 'switch', default: false },
{ key: 'payment_show_ip', label: '显示用户IP', type: 'switch', default: false },
{ key: 'payment_show_device', label: '显示设备信息', type: 'switch', default: false },
{ key: 'payment_show_order_no', label: '显示订单号', type: 'switch', default: true },
@@ -16,7 +16,6 @@ export const paidSchema = {
pay_box_position: 'top',
payment_download_enable: false,
payment_down_alone_page: false,
pay_free_logged_show: false,
payment_download_remote_s: true,
payment_download_verify_nonce: true,
payment_download_user_log: 50,
@@ -56,7 +55,6 @@ export const paidSchema = {
{ key: 'pay_show_user', label: '显示购买用户', type: 'switch', default: true },
{ key: 'pay_show_paycount', label: '显示购买次数', type: 'switch', default: true },
{ key: 'pay_box_position', label: '付费框位置', type: 'radio', default: 'top', options: [{ label: '文章框顶部(封面后)', value: 'box_top' }, { label: '正文顶部', value: 'top' }, { label: '正文底部', value: 'bottom' }, { label: '文章框底部(相关前)', value: 'box_bottom' }] },
{ key: 'pay_free_logged_show', label: '免费资源需登录后下载', type: 'switch', default: false, help: '开启后,价格为 0 的下载资源也要求登录才可下载(对齐 zibll pay_free_logged_show)。' },
],
},
{
@@ -209,35 +209,6 @@ export const permissionsSchemas = {
],
},
/**
* Comment permissions
*/
comment: {
key: 'permissions.comment',
title: '评论权限',
icon: 'fas fa-comment-dots',
description: '控制评论功能的开关和权限要求。',
defaults: {
comment_allow: true,
comment_require_login: false,
comment_nested_depth: 3,
comment_allow_reply: true,
},
groups: [
{
key: 'basic',
title: '评论基础',
icon: 'fas fa-cog',
fields: [
{ key: 'comment_allow', label: '允许评论', type: 'switch', help: '全局开启评论功能' },
{ key: 'comment_require_login', label: '需登录评论', type: 'switch', help: '只有登录用户才能评论' },
{ key: 'comment_nested_depth', label: '嵌套层级', type: 'slider', min: 1, max: 5, default: 3 },
{ key: 'comment_allow_reply', label: '允许回复', type: 'switch' },
],
},
],
},
/**
* Attachment permissions
*/
@@ -247,27 +218,18 @@ export const permissionsSchemas = {
icon: 'fas fa-paperclip',
description: '控制文件上传和附件下载权限。',
defaults: {
allow_avatar_upload: true,
allow_cover_upload: true,
allow_background_upload: false,
avatar_max_size: 2,
allow_submit_images: true,
submit_image_max_size: 5,
allow_comment_images: false,
comment_image_max_size: 2,
allow_resource_upload: true,
resource_max_size: 50,
},
groups: [
{
key: 'profile',
title: '头像与封面',
title: '用户资料卡背景图',
icon: 'fas fa-user-circle',
fields: [
{ key: 'allow_avatar_upload', label: '允许用户上传头像', type: 'switch' },
{ key: 'allow_cover_upload', label: '允许用户上传封面图', type: 'switch' },
{ key: 'allow_background_upload', label: '允许用户上传背景图', type: 'switch' },
{ key: 'avatar_max_size', label: '头像/封面大小上限 (MB)', type: 'number', min: 1, max: 10 },
{ key: 'avatar_max_size', label: '头像/用户资料卡背景图大小上限 (MB)', type: 'number', min: 1, max: 10 },
],
},
{
@@ -275,9 +237,7 @@ export const permissionsSchemas = {
title: '内容上传',
icon: 'fas fa-images',
fields: [
{ key: 'allow_submit_images', label: '允许投稿插入图片', type: 'switch' },
{ key: 'submit_image_max_size', label: '投稿图片大小上限 (MB)', type: 'number', min: 1, max: 20 },
{ key: 'allow_comment_images', label: '允许评论插入图片', type: 'switch' },
{ key: 'comment_image_max_size', label: '评论图片大小上限 (MB)', type: 'number', min: 1, max: 10 },
],
},
@@ -286,23 +246,10 @@ export const permissionsSchemas = {
title: '资源附件',
icon: 'fas fa-file-archive',
fields: [
{ key: 'allow_resource_upload', label: '允许资源附件上传', type: 'switch' },
{ key: 'resource_max_size', label: '资源附件大小上限 (MB)', type: 'number', min: 1, max: 200 },
],
},
],
},
/**
* Download permissions — live UI is DownloadPermissions.vue via cms_features.
* Keep empty schema so SchemaPage cannot mis-write aksrt_settings.
*/
download: {
key: 'permissions.download',
title: '下载权限',
icon: 'fas fa-download',
description: '下载门槛由 DownloadPermissions 页维护(cms_features);人机验证服务商在「人机验证」页。',
defaults: {},
groups: [],
},
};
@@ -11,14 +11,11 @@ export const commentSchema = {
comment_paging_ajax_ias_max: 3,
comment_nested_depth: 3,
comment_avatar_size: 48,
comment_require_login: false,
comment_allow_reply: true,
comment_close_comments: false,
comment_edit_window: 10,
comment_smilie: true,
comment_code: true,
comment_img: true,
comment_upload_img: false,
comment_like_s: true,
comment_hot_threshold: 10,
comment_quick_s: true,
@@ -56,11 +53,9 @@ export const commentSchema = {
},
{
key: 'permissions',
title: '评论权限',
title: '评论设置',
icon: 'fas fa-lock',
fields: [
{ key: 'comment_require_login', label: '评论需要登录', type: 'switch', default: false },
{ key: 'comment_allow_reply', label: '允许回复', type: 'switch', default: true },
{ key: 'comment_close_comments', label: '关闭评论', type: 'switch', default: false },
{ key: 'comment_edit_window', label: '作者自助编辑时限 (分钟)', type: 'number', default: 10, min: 0, max: 1440 },
],
@@ -5,8 +5,6 @@ export const featuresSchema = {
description: '配置用户中心和个人主页的功能模块。',
defaults: {
user_center_tabs: 'profile,level,posts,comments,favorites,vip,credits,revenue,auth',
user_cover_upload: true,
user_social_links: true,
user_desc_std: '这家伙很懒…',
user_city_s: true,
user_city_type: 'province',
@@ -25,8 +23,6 @@ export const featuresSchema = {
icon: 'fas fa-user-shield',
fields: [
{ key: 'user_center_tabs', label: '用户中心 Tab', type: 'input', default: 'profile,level,comments,favorites,vip,credits,revenue,auth', help: '逗号分隔。level=我的等级;creator=创作中心;revenue=推广收益中心;auth=官方认证;等级、VIP、订单、创作中心、收益中心和认证会按功能开关自动注入' },
{ key: 'user_cover_upload', label: '封面上传', type: 'switch', default: true },
{ key: 'user_social_links', label: '社交链接', type: 'switch', default: true },
{ key: 'user_desc_std', label: '默认签名', type: 'input', default: '这家伙很懒…', help: '用户未设置个人简介时显示的默认文案' },
{ key: 'user_city_s', label: '显示城市(IP 归属地)', type: 'switch', default: true, help: '根据用户登录 IP 在用户主页与用户中心显示归属地;需同时在「扩展 → IP归属地」启用并勾选对应展示位置' },
{
+43 -12
View File
@@ -96,34 +96,65 @@
pointer-events: none;
}
.uc-header__cover-upload {
.aksrt-cover-menu {
position: absolute;
bottom: 12px;
right: 12px;
z-index: 2;
}
.aksrt-cover-menu__trigger {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
justify-content: center;
width: 34px;
height: 34px;
padding: 0;
border: none;
border-radius: 6px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.25);
color: #fff;
font-size: 12px;
cursor: pointer;
backdrop-filter: blur(4px);
transition: background 0.2s;
transition: background .2s;
}
.uc-header__cover-upload:hover {
.aksrt-cover-menu__trigger:hover,
.aksrt-cover-menu__trigger:focus-visible {
background: rgba(0, 0, 0, 0.45);
outline: 2px solid #fff;
outline-offset: 2px;
}
.uc-header__cover-upload:disabled {
opacity: 0.6;
cursor: not-allowed;
.aksrt-cover-menu__panel {
position: absolute;
right: 0;
bottom: calc(100% + 8px);
display: grid;
min-width: 140px;
padding: 6px;
border-radius: 10px;
background: rgba(20, 24, 31, .94);
box-shadow: 0 12px 32px rgba(0,0,0,.22);
backdrop-filter: blur(12px);
}
.uc-header__cover-upload i {
font-size: 11px;
.aksrt-cover-menu__panel [role="menuitem"] {
display: flex;
align-items: center;
gap: 8px;
min-height: 36px;
padding: 0 10px;
border: 0;
border-radius: 6px;
color: #fff;
background: transparent;
font: inherit;
font-size: 13px;
text-align: left;
text-decoration: none;
cursor: pointer;
}
.aksrt-cover-menu__panel [role="menuitem"]:hover,
.aksrt-cover-menu__panel [role="menuitem"]:focus-visible { background: rgba(255,255,255,.13); outline: none; }
.profile-form__fields { display: grid; gap: 14px; }
.profile-form__fields label { display: grid; gap: 6px; font-size: 13px; color: var(--color-ink, #24292f); }
.uc-header__content {
display: flex;
+22 -3
View File
@@ -44,7 +44,7 @@
.aksrt-author-hero__cover-metas {
position: absolute;
right: 14px;
right: 58px;
bottom: 12px;
z-index: 2;
display: flex;
@@ -589,8 +589,27 @@
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-top: .35rem;
margin-top: .15rem;
}
.aksrt-author-hero__identity .aksrt-ip-geo-badge {
margin: 2px 0;
display: inline-flex;
align-items: center;
height: 26px;
box-sizing: border-box;
margin: 0;
padding: 0 8px;
border-radius: 999px;
font-size: 12px;
line-height: 26px;
}
.aksrt-author-hero__identity .aksrt-medal-entry,
.aksrt-author-hero__identity .badg { height: 26px; box-sizing: border-box; margin: 0; padding: 0 8px; border-radius: 999px; font-size: 12px; line-height: 26px; }
.aksrt-profile-details-modal { position: fixed; inset: 0; z-index: 10001; display: grid; place-items: center; padding: 20px; }
.aksrt-profile-details-modal__backdrop { position: absolute; inset: 0; background: rgba(10, 15, 22, .52); }
.aksrt-profile-details-modal__dialog { position: relative; width: min(100%, 420px); padding: 24px; border-radius: 16px; background: var(--color-card, #fff); box-shadow: 0 24px 64px rgba(0,0,0,.24); }
.aksrt-profile-details-modal__dialog h2 { margin: 0 28px 16px 0; font-size: 18px; }
.aksrt-profile-details-modal__dialog dl { display: grid; gap: 12px; margin: 0; }
.aksrt-profile-details-modal__dialog dl div { display: grid; grid-template-columns: 84px 1fr; gap: 12px; }
.aksrt-profile-details-modal__dialog dt { color: var(--color-muted, #6b7280); }
.aksrt-profile-details-modal__dialog dd { margin: 0; overflow-wrap: anywhere; }
.aksrt-profile-details-modal__close { position: absolute; top: 12px; right: 12px; width: 32px; height: 32px; border: 0; border-radius: 50%; background: transparent; font-size: 24px; cursor: pointer; }
+42
View File
@@ -258,8 +258,50 @@
});
}
function initCoverMenus() {
document.querySelectorAll('[data-aksrt-menu]').forEach(function (menu) {
if (menu.dataset.aksrtMenuBound === '1') return;
menu.dataset.aksrtMenuBound = '1';
var trigger = menu.querySelector('.aksrt-cover-menu__trigger');
var panel = menu.querySelector('.aksrt-cover-menu__panel');
if (!trigger || !panel) return;
function close() { panel.hidden = true; trigger.setAttribute('aria-expanded', 'false'); }
function open() {
document.querySelectorAll('[data-aksrt-menu]').forEach(function (otherMenu) {
if (otherMenu === menu) return;
var otherPanel = otherMenu.querySelector('.aksrt-cover-menu__panel');
var otherTrigger = otherMenu.querySelector('.aksrt-cover-menu__trigger');
if (otherPanel) otherPanel.hidden = true;
if (otherTrigger) otherTrigger.setAttribute('aria-expanded', 'false');
});
panel.hidden = false;
trigger.setAttribute('aria-expanded', 'true');
var first = panel.querySelector('[role="menuitem"]');
if (first) first.focus();
}
trigger.addEventListener('click', function () { panel.hidden ? open() : close(); });
menu.addEventListener('keydown', function (event) { if (event.key === 'Escape') { close(); trigger.focus(); } });
document.addEventListener('pointerdown', function (event) { if (!menu.contains(event.target)) close(); });
});
}
function initProfileDetailsModal() {
var modal = document.getElementById('aksrt-profile-details-modal');
if (!modal) return;
var dialog = modal.querySelector('.aksrt-profile-details-modal__dialog');
var returnFocus = null;
function close() { modal.hidden = true; if (returnFocus) returnFocus.focus(); }
document.querySelectorAll('[data-aksrt-profile-details]').forEach(function (button) {
button.addEventListener('click', function () { returnFocus = button; modal.hidden = false; if (dialog) dialog.focus(); });
});
modal.querySelectorAll('[data-aksrt-modal-close]').forEach(function (button) { button.addEventListener('click', close); });
document.addEventListener('keydown', function (event) { if (event.key === 'Escape' && !modal.hidden) close(); });
}
function init() {
initPublicDashboards();
initCoverMenus();
initProfileDetailsModal();
initCoverUpload();
initAvatarUpload();
}
+84 -35
View File
@@ -390,27 +390,41 @@
}
},
openMedia: function (opts, cb) {
opts = opts || {};
if (!window.wp || !wp.media) {
alert('Media library unavailable');
return;
uploadFile: function (kind, file) {
var upload = (cfg.uploads || {})[kind] || null;
if (!upload || !upload.url || !file) {
return Promise.reject(new Error(i18n.error || 'Upload unavailable'));
}
var frame = wp.media({
title: opts.title || i18n.pickImage || 'Select',
button: { text: opts.button || 'OK' },
multiple: !!opts.multiple,
library: opts.library || { type: 'image' },
});
frame.on('select', function () {
var selection = frame.state().get('selection');
var files = selection.map(function (att) {
var a = att.toJSON();
return { id: a.id, url: a.url, title: a.title || '' };
if (upload.maxSize && file.size > upload.maxSize) {
return Promise.reject(new Error('File is too large'));
}
var body = new FormData();
body.append('file', file);
return fetch(upload.url, {
method: 'POST',
credentials: 'same-origin',
headers: { 'X-WP-Nonce': cfg.restNonce || '' },
body: body,
}).then(function (response) {
return response.json().catch(function () { return {}; }).then(function (payload) {
if (!response.ok || !payload || !payload.success) {
throw new Error((payload && payload.message) || i18n.error || 'Upload failed');
}
return payload;
});
cb(opts.multiple ? files : files[0]);
});
frame.open();
},
pickUpload: function (kind, accept) {
return new Promise(function (resolve) {
var input = document.createElement('input');
input.type = 'file';
input.accept = accept;
input.addEventListener('change', function () {
resolve(input.files && input.files[0] ? input.files[0] : null);
}, { once: true });
input.click();
});
},
bindFeatured: function () {
@@ -431,24 +445,21 @@
return;
}
if (act === 'image') {
self.openMedia({ title: i18n.pickImage, multiple: false }, function (file) {
if (file) {
self.setFeatured('image', { url: file.url, id: file.id });
}
});
self.pickUpload('image', 'image/*').then(function (file) {
if (!file) return null;
return self.uploadFile('image', file);
}).then(function (file) {
if (file) self.setFeatured('image', { url: file.url, id: file.attachment_id });
}).catch(function (error) { alert(error.message || i18n.error || 'Upload failed'); });
return;
}
if (act === 'slide') {
self.openMedia({ title: i18n.pickImage, multiple: true }, function (files) {
if (files && files.length) {
self.setFeatured(
'slide',
files.map(function (f) {
return { url: f.url, id: f.id };
})
);
}
});
self.pickUpload('image', 'image/*').then(function (file) {
if (!file) return null;
return self.uploadFile('image', file);
}).then(function (file) {
if (file) self.setFeatured('slide', [{ url: file.url, id: file.attachment_id }]);
}).catch(function (error) { alert(error.message || i18n.error || 'Upload failed'); });
return;
}
if (act === 'video') {
@@ -456,9 +467,12 @@
if (!url) {
return;
}
self.openMedia({ title: i18n.pickVideo || 'Poster', multiple: false }, function (file) {
self.pickUpload('image', 'image/*').then(function (file) {
if (!file) return null;
return self.uploadFile('image', file);
}).then(function (file) {
self.setFeatured('video', { url: url, pic: file ? file.url : '' });
});
}).catch(function (error) { alert(error.message || i18n.error || 'Upload failed'); });
}
});
},
@@ -520,6 +534,32 @@
list.appendChild(node);
});
}
var uploadBtn = qs('[data-pay-download-upload]', box);
if (uploadBtn && list && tpl) {
uploadBtn.addEventListener('click', function () {
App.pickUpload('resource', '*/*').then(function (file) {
if (!file) return null;
uploadBtn.disabled = true;
return App.uploadFile('resource', file);
}).then(function (file) {
if (!file) return;
var node = tpl.content.cloneNode(true);
var idx = list.querySelectorAll('[data-pay-download-item]').length;
qsa('[data-name]', node).forEach(function (input) {
input.name = 'aksrt_pay[downloads][' + idx + '][' + input.getAttribute('data-name') + ']';
});
var url = qs('[data-pay-download-url]', node);
var label = qs('[data-pay-download-label]', node);
if (url) url.value = file.url || '';
if (label) label.value = file.name || '';
list.appendChild(node);
}).catch(function (error) {
alert(error.message || i18n.error || 'Upload failed');
}).finally(function () {
uploadBtn.disabled = false;
});
});
}
if (list) {
list.addEventListener('click', function (e) {
var rm = e.target.closest('[data-pay-download-remove]');
@@ -541,6 +581,15 @@
return;
}
e.preventDefault();
if (btn.hasAttribute('data-insert-upload-image')) {
App.pickUpload('image', 'image/*').then(function (file) {
if (!file) return null;
return App.uploadFile('image', file);
}).then(function (file) {
if (file && file.url) insertEditorContent('<img src="' + file.url.replace(/"/g, '&quot;') + '" alt="">');
}).catch(function (error) { alert(error.message || i18n.error || 'Upload failed'); });
return;
}
var type = btn.getAttribute('data-insert-shortcode');
if (type === 'hide') {
insertEditorContent('[hidecontent type="login"]' + (i18n.hidePlaceholder || '隐藏内容') + '[/hidecontent]');
-34
View File
@@ -194,40 +194,6 @@
var form = document.getElementById('profile-form');
if (!form) return;
var iconOptions = '<option value="">—</option>'
+ '<option value="github">GitHub</option>'
+ '<option value="google">Google</option>'
+ '<option value="x">X</option>'
+ '<option value="email">邮箱</option>'
+ '<option value="wechat">微信</option>'
+ '<option value="qq">QQ</option>'
+ '<option value="weibo">微博</option>'
+ '<option value="telegram">Telegram</option>'
+ '<option value="discord">Discord</option>'
+ '<option value="bilibili">Bilibili</option>'
+ '<option value="linkedin">LinkedIn</option>'
+ '<option value="facebook">Facebook</option>'
+ '<option value="youtube">YouTube</option>'
+ '<option value="instagram">Instagram</option>'
+ '<option value="web">网站</option>';
var addBtn = document.getElementById('add-contact-btn');
if (addBtn) {
addBtn.addEventListener('click', function() {
var list = document.getElementById('contacts-list');
var idx = list.children.length;
var div = document.createElement('div');
div.className = 'contact-row';
div.dataset.index = idx;
div.innerHTML =
'<select name="contacts[' + idx + '][icon]" class="profile-input contact-icon">' + iconOptions + '</select>' +
'<input type="hidden" name="contacts[' + idx + '][name]" value="">' +
'<input type="url" name="contacts[' + idx + '][url]" class="profile-input contact-url" placeholder="' + (i18n.contactUrlPlaceholder || '链接 URL') + '">' +
'<button type="button" class="contact-remove" onclick="this.parentElement.remove()" title="' + (i18n.deleteLabel || '删除') + '">&times;</button>';
list.appendChild(div);
});
}
form.addEventListener('submit', function(e) {
e.preventDefault();
var btn = document.getElementById('profile-save-btn');
+1 -2
View File
@@ -149,8 +149,7 @@ if (!function_exists('aksrt_is_avatar_upload_enabled')) {
* Whether local avatar uploads are enabled by attachment policy.
*/
function aksrt_is_avatar_upload_enabled(): bool {
$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
return !isset($settings['allow_avatar_upload']) || !empty($settings['allow_avatar_upload']);
return function_exists('aksrt_user_can') && aksrt_user_can('upload_avatar', get_current_user_id());
}
}
-2
View File
@@ -76,7 +76,6 @@ if (!function_exists('aksrt_comments_build_context')) :
'order' => 'desc',
'nested_depth' => 3,
'require_login' => false,
'allow_reply' => true,
'avatar_size' => 48,
'empty_text' => __('暂无评论,来做第一个评论的人吧~', 'aksrt'),
];
@@ -109,7 +108,6 @@ if (!function_exists('aksrt_comments_build_context')) :
'post_author_id' => (int) get_the_author_meta('ID'),
'avatar_size' => $avatar_size,
'max_depth' => max(1, (int) ($settings['nested_depth'] ?? 3)),
'allow_reply' => !empty($settings['allow_reply']),
'empty_text' => (string) ($settings['empty_text'] ?? __('暂无评论,来做第一个评论的人吧~', 'aksrt')),
'toolbar' => $toolbar,
'upload_enabled' => !empty($toolbar['upload_img']),
+1 -6
View File
@@ -775,13 +775,8 @@ if (!function_exists('aksrt_can_download')) {
return new WP_Error('aksrt_not_downloadable', __('该内容不是可下载资源。', 'aksrt'));
}
$paid_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_paid_content_settings()
: ['download_login' => true];
$uid = $user_id > 0 ? $user_id : get_current_user_id();
if (!empty($paid_settings['download_login']) && $uid <= 0) {
if (!function_exists('aksrt_user_can') || !aksrt_user_can('resource_download', $uid, (int) $p->ID)) {
$login_url = wp_login_url(get_permalink($p));
return new WP_Error('aksrt_need_login', __('请先登录。', 'aksrt'), ['login_url' => $login_url]);
}
+1 -5
View File
@@ -403,11 +403,7 @@ if (!function_exists('aksrt_can_download')) {
}
$uid = $user_id > 0 ? $user_id : get_current_user_id();
$paid_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_paid_content_settings()
: ['download_login' => true];
if (!empty($paid_settings['download_login']) && $uid <= 0) {
if (!function_exists('aksrt_user_can') || !aksrt_user_can('resource_download', $uid, (int) $p->ID)) {
return new WP_Error('aksrt_need_login', __('请先登录后再下载。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url(get_permalink($p)),
+70 -43
View File
@@ -7,41 +7,48 @@
defined('ABSPATH') || exit;
if (!function_exists('aksrt_normalize_user_contacts')) {
if (!function_exists('aksrt_get_user_profile_details')) {
/**
* Sanitize the editable public contact list.
* Read the fixed profile details. Social fields are deliberately kept in one
* structured meta value so public rendering can filter them server-side.
*
* @param mixed $raw_contacts Submitted contact rows.
* @return array<int, array{name:string,url:string,icon:string}>
* @return array{privacy:string,gender:string,website:string,qq:string,wechat:string,github:string}
*/
function aksrt_normalize_user_contacts($raw_contacts): array {
$contacts = [];
if (!is_array($raw_contacts)) {
return $contacts;
function aksrt_get_user_profile_details(int $user_id): array {
$raw = get_user_meta($user_id, 'aksrt_profile_details', true);
$raw = is_array($raw) ? $raw : [];
$privacy = (string) ($raw['privacy'] ?? 'private');
if (!in_array($privacy, ['private', 'public', 'members'], true)) {
$privacy = 'private';
}
$gender = (string) ($raw['gender'] ?? 'secret');
if (!in_array($gender, ['secret', 'male', 'female'], true)) {
$gender = 'secret';
}
foreach ($raw_contacts as $contact) {
if (!is_array($contact)) {
continue;
}
return [
'privacy' => $privacy,
'gender' => $gender,
'website' => esc_url_raw((string) ($raw['website'] ?? '')),
'qq' => sanitize_text_field((string) ($raw['qq'] ?? '')),
'wechat' => sanitize_text_field((string) ($raw['wechat'] ?? '')),
'github' => esc_url_raw((string) ($raw['github'] ?? '')),
];
}
}
$url = isset($contact['url']) ? esc_url_raw(wp_unslash($contact['url'])) : '';
$icon = isset($contact['icon']) ? sanitize_key(wp_unslash($contact['icon'])) : '';
$icon = preg_replace('/^(fab|fas|far)[\s-]*/', '', $icon);
$icon = preg_replace('/^fa[\s-]*/', '', (string) $icon);
$icon = trim(preg_replace('/^[\s-]+/', '', (string) $icon), '- ');
if (!$url || !$icon) {
continue;
}
$contacts[] = [
'name' => aksrt_icon_key_to_name($icon),
'url' => $url,
'icon' => $icon,
];
}
return $contacts;
if (!function_exists('aksrt_get_visible_user_profile_details')) {
/** @return array{gender:string,website:string,qq:string,wechat:string,github:string} */
function aksrt_get_visible_user_profile_details(int $user_id, int $viewer_id = 0): array {
$details = aksrt_get_user_profile_details($user_id);
$visible = $viewer_id === $user_id || $details['privacy'] === 'public' || ($details['privacy'] === 'members' && $viewer_id > 0);
return [
'gender' => $details['gender'] === 'secret' ? '' : $details['gender'],
'website' => $visible ? $details['website'] : '',
'qq' => $visible ? $details['qq'] : '',
'wechat' => $visible ? $details['wechat'] : '',
'github' => $visible ? $details['github'] : '',
];
}
}
@@ -79,6 +86,30 @@ if (!function_exists('aksrt_save_user_center_profile')) {
}
}
$privacy = isset($input['privacy']) ? sanitize_key(wp_unslash($input['privacy'])) : 'private';
if (!in_array($privacy, ['private', 'public', 'members'], true)) {
$errors[] = __('无效的隐私设置。', 'aksrt');
}
$gender = isset($input['gender']) ? sanitize_key(wp_unslash($input['gender'])) : 'secret';
if (!in_array($gender, ['secret', 'male', 'female'], true)) {
$errors[] = __('无效的性别设置。', 'aksrt');
}
$details = [
'privacy' => $privacy,
'gender' => $gender,
'website' => isset($input['website']) ? esc_url_raw(wp_unslash($input['website'])) : '',
'qq' => isset($input['qq']) ? sanitize_text_field(wp_unslash($input['qq'])) : '',
'wechat' => isset($input['wechat']) ? sanitize_text_field(wp_unslash($input['wechat'])) : '',
'github' => isset($input['github']) ? esc_url_raw(wp_unslash($input['github'])) : '',
];
foreach (['website', 'github'] as $field) {
if (!empty($input[$field]) && $details[$field] === '') {
$errors[] = sprintf(__('无效的%s链接。', 'aksrt'), $field === 'website' ? __('个人网站', 'aksrt') : 'GitHub');
}
}
if (mb_strlen($details['qq']) > 50 || mb_strlen($details['wechat']) > 100) {
$errors[] = __('社交资料长度超过限制。', 'aksrt');
}
if ($errors) {
return new WP_Error('aksrt_profile_invalid', implode('<br>', $errors));
}
@@ -89,8 +120,7 @@ if (!function_exists('aksrt_save_user_center_profile')) {
return $result;
}
}
update_user_meta($user_id, 'aksrt_user_contacts', aksrt_normalize_user_contacts($input['contacts'] ?? []));
update_user_meta($user_id, 'aksrt_profile_details', $details);
return true;
@@ -388,6 +418,8 @@ if (!function_exists('aksrt_user_center_cover_upload_config')) {
if (!$allow_bg_upload) {
return [];
}
$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$max_size = max(1, min(10, (int) ($settings['avatar_max_size'] ?? 2))) * MB_IN_BYTES;
return [
'enabled' => true,
'restUrl' => rest_url('aksrt/v1/user/background'),
@@ -396,11 +428,11 @@ if (!function_exists('aksrt_user_center_cover_upload_config')) {
'inputId' => 'aksrt-bg-file-input',
'imageId' => 'aksrt-cover-img',
'fieldName' => 'background',
'maxSize' => 2097152,
'maxSize' => $max_size,
'fallbackImage' => AKSRT_URI . '/assets/img/person_background.png',
'idleText' => __('编辑', 'aksrt'),
'idleText' => __('修改背景图', 'aksrt'),
'loadingText' => __('上传中…', 'aksrt'),
'tooLargeText' => __('图片大小不能超过 2MB', 'aksrt'),
'tooLargeText' => sprintf(__('图片大小不能超过 %dMB', 'aksrt'), (int) ($max_size / MB_IN_BYTES)),
'successText' => __('背景图已更新', 'aksrt'),
'errorText' => __('上传失败', 'aksrt'),
'networkErrorText' => __('网络错误,请重试', 'aksrt'),
@@ -418,10 +450,7 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
$user_center_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_user_center_settings()
: [
'tabs' => ['profile', 'comments', 'favorites'],
'avatar_upload' => true,
'cover_upload' => true,
'social_links' => true,
'tabs' => ['profile', 'comments', 'favorites'],
];
$tabs = aksrt_user_center_resolve_tabs();
@@ -439,11 +468,9 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
$comment_count = (int) ($dashboard['comment_count'] ?? get_comments(['user_id' => $user_id, 'count' => true, 'status' => 'approve']));
$favorite_post_ids = $dashboard['favorite_post_ids'] ?? [];
$favorite_count = (int) ($dashboard['favorite_count'] ?? count($favorite_post_ids));
$user_contacts = $dashboard['user_contacts'] ?? get_user_meta($user_id, 'aksrt_user_contacts', true);
$user_contacts = is_array($user_contacts) ? $user_contacts : [];
$user_background_url = (string) ($dashboard['background_url'] ?? get_user_meta($user_id, 'aksrt_background_image', true));
$allow_avatar_upload = !empty($user_center_settings['avatar_upload']);
$allow_bg_upload = !empty($user_center_settings['cover_upload']) && class_exists('\AKSRT\Settings\Settings') && aksrt_get_setting('allow_background_upload', false);
$allow_avatar_upload = function_exists('aksrt_user_can') && aksrt_user_can('upload_avatar', $user_id);
$allow_bg_upload = function_exists('aksrt_user_can') && aksrt_user_can('upload_profile_background', $user_id);
$messages_enabled = !class_exists('\AKSRT\Settings\Settings') || \AKSRT\Settings\Settings::message_enabled();
$follower_count = class_exists('\AKSRT\User\Follow')
? \AKSRT\User\Follow::get_instance()->get_follower_count($user_id)
@@ -501,7 +528,7 @@ if (!function_exists('aksrt_get_user_center_render_context')) {
'favorite_count' => $favorite_count,
'follower_count' => $follower_count,
'following_count' => $following_count,
'user_contacts' => $user_contacts,
'profile_details' => aksrt_get_user_profile_details($user_id),
'profile_signals' => $dashboard['profile_signals'] ?? [],
'user_background_url' => $user_background_url,
'allow_bg_upload' => $allow_bg_upload,
+5 -11
View File
@@ -23,7 +23,7 @@ defined('ABSPATH') || exit;
* @type int $comment_count Approved comment count.
* @type int $favorite_count Favorited post count.
* @type array $favorite_post_ids Array of favorited post IDs.
* @type array $user_contacts Social/contact links from user meta.
* @type array $profile_details Server-filtered fixed public profile fields.
* @type array $profile_signals Identity chips (verified, level, badges).
* @type array $profile_notice Ban/restriction notice, if any.
* @type string $background_url Cover image URL.
@@ -62,14 +62,9 @@ function aksrt_get_user_dashboard_view(int $user_id, array $args = []): array {
])
: [];
$user_contacts = get_user_meta($user_id, 'aksrt_user_contacts', true) ?: [];
if (!is_array($user_contacts)) {
$user_contacts = [];
}
$user_center_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_user_center_settings()
: ['social_links' => true];
$profile_details = function_exists('aksrt_get_visible_user_profile_details')
? aksrt_get_visible_user_profile_details($user_id, $viewer_id)
: ['gender' => '', 'website' => '', 'qq' => '', 'wechat' => '', 'github' => ''];
$background_url = get_user_meta($user_id, 'aksrt_background_image', true) ?: '';
@@ -98,8 +93,7 @@ function aksrt_get_user_dashboard_view(int $user_id, array $args = []): array {
'favorite_count' => $favorite_count,
'favorite_post_ids'=> $favorite_post_ids,
'recent_posts' => $recent_posts,
'user_contacts' => $user_contacts,
'social_links_on' => !empty($user_center_settings['social_links']),
'profile_details' => $profile_details,
'profile_signals' => $profile_signals,
'profile_notice' => $profile_notice,
'show_report' => $show_report,
+2 -7
View File
@@ -34,15 +34,11 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
'posts_per_page' => 10,
];
$user_center_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_user_center_settings()
: ['social_links' => true];
$post_count = (int) ($dashboard['post_count'] ?? 0);
$comment_count = (int) ($dashboard['comment_count'] ?? 0);
$favorite_post_ids = $dashboard['favorite_post_ids'] ?? [];
$favorite_count = (int) ($dashboard['favorite_count'] ?? 0);
$user_contacts = $dashboard['user_contacts'] ?? [];
$profile_details = $dashboard['profile_details'] ?? [];
$per_page = max(1, (int) ($user_home_settings['posts_per_page'] ?? 10));
$credits_enabled = !empty($dashboard['credits_enabled']);
$credit_balance = $dashboard['credit_balance'] ?? 0.0;
@@ -118,8 +114,7 @@ if (!function_exists('aksrt_get_user_home_page_context')) {
'dashboard' => $dashboard,
'is_own_profile' => $is_own_profile,
'user_home_settings' => $user_home_settings,
'user_center_settings' => $user_center_settings,
'user_contacts' => $user_contacts,
'profile_details' => $profile_details,
'post_count' => $post_count,
'comment_count' => $comment_count,
'favorite_post_ids' => $favorite_post_ids,
+1 -2
View File
@@ -151,7 +151,6 @@ if (!function_exists('aksrt_get_comment_footer_html')) :
*/
function aksrt_get_comment_footer_html(WP_Comment $comment, array $args, int $depth, array $settings = []): string {
$is_pending = '0' === (string) $comment->comment_approved;
$allow_reply = !empty($settings['allow_reply']);
// Time ago.
$absolute = date_i18n('Y-m-d H:i:s', strtotime((string) $comment->comment_date));
@@ -181,7 +180,7 @@ if (!function_exists('aksrt_get_comment_footer_html')) :
// Reply.
$reply_html = '';
if ($allow_reply && !$is_pending && $depth > 0) {
if (!$is_pending && $depth > 0) {
$max_depth = (int) ($args['max_depth'] ?? get_option('thread_comments_depth', 5));
$link = get_comment_reply_link(
array_merge(
@@ -1,34 +0,0 @@
<?php
/**
* User center profile form: contact rows block.
*
* Expects extracted $context vars.
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (empty($user_center_settings['social_links'])) {
return;
}
?>
<div class="widget-card">
<h3 class="widget-title"><?php esc_html_e('联系方式', 'aksrt'); ?></h3>
<p class="profile-hint profile-hint--top"><?php esc_html_e('添加你的社交链接,显示在公开用户主页', 'aksrt'); ?></p>
<div id="contacts-list">
<?php foreach ((array) $user_contacts as $i => $contact) : ?>
<div class="contact-row" data-index="<?php echo (int) $i; ?>">
<select name="contacts[<?php echo (int) $i; ?>][icon]" class="profile-input contact-icon">
<?php echo aksrt_contact_icon_options($contact['icon'] ?? ''); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</select>
<input type="hidden" name="contacts[<?php echo (int) $i; ?>][name]" value="">
<input type="url" name="contacts[<?php echo (int) $i; ?>][url]"
value="<?php echo esc_url($contact['url'] ?? ''); ?>"
class="profile-input contact-url" placeholder="<?php esc_attr_e('链接 URL', 'aksrt'); ?>">
<button type="button" class="contact-remove" onclick="this.parentElement.remove()" title="<?php esc_attr_e('删除', 'aksrt'); ?>">&times;</button>
</div>
<?php endforeach; ?>
</div>
<button type="button" class="btn-ghost btn-sm profile-add-contact" id="add-contact-btn">+ <?php esc_html_e('添加联系方式', 'aksrt'); ?></button>
</div>
@@ -9,6 +9,7 @@ defined('ABSPATH') || exit;
$context = is_array($args['context'] ?? null) ? $args['context'] : [];
extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
$profile_details = isset($profile_details) && is_array($profile_details) ? $profile_details : aksrt_get_user_profile_details((int) $user_id);
?>
<form class="profile-form" id="profile-form" method="post">
<?php wp_nonce_field('aksrt_profile_save', '_profile_nonce'); ?>
@@ -24,13 +25,39 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
</div>
<div class="widget-card">
<h3 class="widget-title"><?php esc_html_e('个人简介', 'aksrt'); ?></h3>
<h3 class="widget-title"><?php esc_html_e('个人签名', 'aksrt'); ?></h3>
<textarea name="description" id="profile-bio" class="profile-input profile-textarea"
rows="4" maxlength="500" placeholder="<?php esc_attr_e('介绍一下自己…', 'aksrt'); ?>"><?php echo esc_textarea($target_user->description); ?></textarea>
<p class="profile-hint"><?php esc_html_e('显示在你的公开用户主页上,最多 500 字', 'aksrt'); ?></p>
</div>
<?php require __DIR__ . '/form-contacts.php'; ?>
<div class="widget-card">
<h3 class="widget-title"><?php esc_html_e('隐私设置', 'aksrt'); ?></h3>
<select name="privacy" class="profile-input">
<option value="private" <?php selected($profile_details['privacy'], 'private'); ?>><?php esc_html_e('社交资料 所有人不可见', 'aksrt'); ?></option>
<option value="public" <?php selected($profile_details['privacy'], 'public'); ?>><?php esc_html_e('社交资料 所有人可见', 'aksrt'); ?></option>
<option value="members" <?php selected($profile_details['privacy'], 'members'); ?>><?php esc_html_e('社交资料 仅注册用户可见', 'aksrt'); ?></option>
</select>
</div>
<div class="widget-card">
<h3 class="widget-title"><?php esc_html_e('性别', 'aksrt'); ?></h3>
<select name="gender" class="profile-input">
<option value="secret" <?php selected($profile_details['gender'], 'secret'); ?>><?php esc_html_e('保密', 'aksrt'); ?></option>
<option value="male" <?php selected($profile_details['gender'], 'male'); ?>><?php esc_html_e('男', 'aksrt'); ?></option>
<option value="female" <?php selected($profile_details['gender'], 'female'); ?>><?php esc_html_e('女', 'aksrt'); ?></option>
</select>
</div>
<div class="widget-card">
<h3 class="widget-title"><?php esc_html_e('社交资料', 'aksrt'); ?></h3>
<div class="profile-form__fields">
<label><span><?php esc_html_e('个人网站', 'aksrt'); ?></span><input type="url" name="website" class="profile-input" value="<?php echo esc_attr($profile_details['website']); ?>" placeholder="https://"></label>
<label><span>QQ</span><input type="text" name="qq" class="profile-input" maxlength="50" value="<?php echo esc_attr($profile_details['qq']); ?>"></label>
<label><span><?php esc_html_e('微信', 'aksrt'); ?></span><input type="text" name="wechat" class="profile-input" maxlength="100" value="<?php echo esc_attr($profile_details['wechat']); ?>"></label>
<label><span>GitHub</span><input type="url" name="github" class="profile-input" value="<?php echo esc_attr($profile_details['github']); ?>" placeholder="https://github.com/"></label>
</div>
</div>
<div class="profile-form__submit">
@@ -14,13 +14,18 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
<div class="uc-header__cover">
<img id="aksrt-cover-img" class="uc-header__cover-img" src="<?php echo esc_url($user_background_url ? $user_background_url : get_theme_file_uri('assets/img/person_background.png')); ?>" alt="">
<div class="uc-header__cover-mask"></div>
<?php if ($allow_bg_upload) : ?>
<button type="button" class="uc-header__cover-upload" id="aksrt-upload-bg-btn" title="<?php esc_attr_e('编辑背景图', 'aksrt'); ?>">
<i class="fas fa-pen" aria-hidden="true"></i>
<span><?php esc_html_e('编辑', 'aksrt'); ?></span>
</button>
<input type="file" id="aksrt-bg-file-input" accept="image/jpeg,image/png,image/webp,image/gif" style="display:none">
<?php endif; ?>
<div class="aksrt-cover-menu" data-aksrt-menu>
<button type="button" class="aksrt-cover-menu__trigger" aria-haspopup="true" aria-expanded="false" aria-label="<?php esc_attr_e('个人主页操作', 'aksrt'); ?>">
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button>
<div class="aksrt-cover-menu__panel" role="menu" hidden>
<a role="menuitem" href="<?php echo esc_url(aksrt_get_user_home_url()); ?>"><i class="fas fa-home" aria-hidden="true"></i><?php esc_html_e('我的主页', 'aksrt'); ?></a>
<?php if ($allow_bg_upload) : ?>
<button type="button" role="menuitem" id="aksrt-upload-bg-btn"><i class="fas fa-image" aria-hidden="true"></i><span><?php esc_html_e('修改背景图', 'aksrt'); ?></span></button>
<?php endif; ?>
</div>
</div>
<?php if ($allow_bg_upload) : ?><input type="file" id="aksrt-bg-file-input" accept="image/jpeg,image/png,image/webp,image/gif" hidden><?php endif; ?>
</div>
<div class="uc-header__content">
<div class="uc-header__avatar">
@@ -72,9 +77,6 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
<?php endif; ?>
</a>
<?php endif; ?>
<a href="<?php echo esc_url(aksrt_get_user_home_url()); ?>" class="btn-ghost btn-sm">
<i class="fas fa-map-marker-alt" aria-hidden="true"></i> <?php esc_html_e('我的主页', 'aksrt'); ?>
</a>
</div>
</div>
</div>
+2 -2
View File
@@ -183,8 +183,8 @@ class ExtensionController extends ControllerBase {
'provider' => (string) ($captcha['provider'] ?? ''),
'configured' => !empty($captcha['configured']),
'scenes' => $scenes,
'download_required' => !empty($features['download_captcha']),
'download_active' => !empty($features['download_captcha']) && !empty($scenes['download']),
'download_required' => !empty($scenes['download']),
'download_active' => !empty($scenes['download']),
],
'content_audit' => [
'enabled' => !empty($services['audit']['enabled']),
-2
View File
@@ -182,9 +182,7 @@ class GlobalController extends ControllerBase {
$params = $this->get_json_params($request);
$allowed = [
'resource_cpt_enabled' => 'bool',
'download_login' => 'bool',
'download_credit' => 'bool',
'download_captcha' => 'bool',
'search_include_resource' => 'bool',
];
$sanitized = $this->sanitize_fields($params, $allowed);
+15 -8
View File
@@ -46,7 +46,7 @@ class PermissionsController extends ControllerBase {
'groups' => $this->build_capability_groups($defs, $caps, $columns),
'summary' => [
'cap_count' => count($defs),
'group_count' => 3,
'group_count' => 5,
'column_count' => count($columns),
],
],
@@ -114,6 +114,16 @@ class PermissionsController extends ControllerBase {
'title' => __('评论权限', 'aksrt'),
'items' => [],
],
'upload' => [
'key' => 'upload',
'title' => __('上传权限', 'aksrt'),
'items' => [],
],
'download' => [
'key' => 'download',
'title' => __('下载权限', 'aksrt'),
'items' => [],
],
];
foreach ($defs as $cap_id => $def) {
@@ -122,10 +132,13 @@ class PermissionsController extends ControllerBase {
$bucket_key = 'comment';
} elseif (str_starts_with($cap_id, 'new_post_')) {
$bucket_key = 'post';
} elseif (str_starts_with($cap_id, 'upload_')) {
$bucket_key = 'upload';
} elseif (str_starts_with($cap_id, 'resource_')) {
$bucket_key = 'download';
}
$excludes = $def['exclude_roles'] ?? [];
$only_roles = $def['roles'] ?? null;
$values = $caps[$cap_id] ?? [];
$visible_columns = [];
@@ -133,9 +146,6 @@ class PermissionsController extends ControllerBase {
if (in_array($column, $excludes, true)) {
continue;
}
if ($only_roles !== null && !in_array($column, $only_roles, true) && !in_array($column, ['all', 'logged', 'level', 'vip', 'auth'], true)) {
continue;
}
$visible_columns[] = $column;
}
@@ -157,9 +167,6 @@ class PermissionsController extends ControllerBase {
'level' => __('等级≥', 'aksrt'),
'vip' => __('VIP≥', 'aksrt'),
'auth' => __('已认证', 'aksrt'),
'moderator' => __('版主', 'aksrt'),
'plate_author' => __('超级版主', 'aksrt'),
'cat_moderator' => __('分区版主', 'aksrt'),
];
}
}
-2
View File
@@ -40,7 +40,6 @@ class SchemaPayment {
['label' => '正文底部', 'value' => 'bottom'],
['label' => '文章框底部', 'value' => 'box_bottom'],
]],
['key' => 'pay_free_logged_show', 'label' => '免费资源需登录后下载', 'type' => 'switch', 'default' => false],
]],
['key' => 'download', 'title' => '下载能力', 'fields' => [
['key' => 'payment_download_enable', 'label' => '启用下载能力', 'type' => 'switch', 'default' => false],
@@ -308,7 +307,6 @@ class SchemaPayment {
['label' => '正文底部', 'value' => 'bottom'],
['label' => '文章框底部', 'value' => 'box_bottom'],
]],
['key' => 'pay_free_logged_show', 'label' => '登录用户显示免费', 'type' => 'switch', 'default' => false],
['key' => 'payment_show_ip', 'label' => '显示用户IP', 'type' => 'switch', 'default' => false],
['key' => 'payment_show_device', 'label' => '显示设备信息', 'type' => 'switch', 'default' => false],
['key' => 'payment_show_order_no', 'label' => '显示订单号', 'type' => 'switch', 'default' => true],
+12 -90
View File
@@ -11,9 +11,9 @@ class SchemaPermissions {
public static function build(): array {
return [
'domain' => 'permissions',
'title' => '功能&权限',
'sections' => [
'domain' => 'permissions',
'title' => '功能与权限',
'sections' => [
'login' => [
'key' => 'login',
'title' => '人机验证',
@@ -54,94 +54,24 @@ class SchemaPermissions {
]],
],
],
'comment' => [
'key' => 'comment',
'title' => '评论权限',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'basic', 'title' => '基础权限', 'fields' => [
['key' => 'comment_enabled', 'label' => '全局评论开关', 'type' => 'switch', 'default' => true, 'help' => '关闭后全站禁用评论功能'],
['key' => 'comment_guest_allowed', 'label' => '允许游客评论', 'type' => 'switch', 'default' => true],
['key' => 'comment_min_length', 'label' => '评论最少字数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 100],
['key' => 'comment_max_length', 'label' => '评论最多字数', 'type' => 'number', 'default' => 1000, 'min' => 50, 'max' => 10000],
['key' => 'comment_cool_down_seconds', 'label' => '评论冷却时间(秒)', 'type' => 'number', 'default' => 15, 'min' => 0, 'max' => 300],
['key' => 'comment_max_links', 'label' => '最大链接数', 'type' => 'number', 'default' => 2, 'min' => 0, 'max' => 10],
['key' => 'comment_max_images', 'label' => '最大图片数', 'type' => 'number', 'default' => 3, 'min' => 0, 'max' => 20],
]],
['key' => 'sensitive', 'title' => '敏感词', 'fields' => [
['key' => 'comment_sensitive_words', 'label' => '敏感词列表', 'type' => 'textarea', 'default' => ''],
['key' => 'comment_sensitive_action', 'label' => '敏感词处理', 'type' => 'radio', 'default' => 'reject', 'options' => [
['label' => '拒绝评论', 'value' => 'reject'],
['label' => '替换为***', 'value' => 'replace'],
['label' => '转入审核', 'value' => 'pending'],
]],
['key' => 'comment_sensitive_log', 'label' => '记录敏感词日志', 'type' => 'switch', 'default' => true],
]],
['key' => 'anti-spam', 'title' => '反垃圾', 'fields' => [
['key' => 'comment_link_filter', 'label' => '链接过滤', 'type' => 'switch', 'default' => true, 'help' => '评论包含链接自动进入审核'],
['key' => 'comment_flood_protection', 'label' => '防刷评论', 'type' => 'switch', 'default' => true],
['key' => 'comment_flood_interval', 'label' => '防刷间隔(秒)', 'type' => 'number', 'default' => 10, 'min' => 3, 'max' => 60],
['key' => 'comment_max_per_hour', 'label' => '每小时最大评论数', 'type' => 'number', 'default' => 20, 'min' => 1, 'max' => 200],
]],
['key' => 'display', 'title' => '展示设置', 'fields' => [
['key' => 'comment_avatar_required', 'label' => '要求设置头像', 'type' => 'switch', 'default' => false],
['key' => 'comment_nickname_required', 'label' => '要求填写昵称', 'type' => 'switch', 'default' => true],
['key' => 'comment_email_required', 'label' => '要求填写邮箱', 'type' => 'switch', 'default' => true],
]],
],
],
'attachment' => [
'key' => 'attachment',
'title' => '附件权限',
'version' => '1.0.0',
'title' => '附件配置',
'version' => '1.1.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'profile', 'title' => '头像与封面', 'fields' => [
['key' => 'allow_avatar_upload', 'label' => '允许用户上传头像', 'type' => 'switch', 'default' => true],
['key' => 'allow_cover_upload', 'label' => '允许用户上传封面图', 'type' => 'switch', 'default' => true],
['key' => 'allow_background_upload', 'label' => '允许用户上传背景图', 'type' => 'switch', 'default' => false],
['key' => 'avatar_max_size', 'label' => '头像/封面大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
['key' => 'profile', 'title' => '用户资料卡背景图', 'fields' => [
['key' => 'avatar_max_size', 'label' => '头像/用户资料卡背景图大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
]],
['key' => 'content', 'title' => '内容上传', 'fields' => [
['key' => 'allow_submit_images', 'label' => '允许投稿插入图片', 'type' => 'switch', 'default' => true],
['key' => 'content', 'title' => '内容上传限制', 'fields' => [
['key' => 'submit_image_max_size', 'label' => '投稿图片大小上限 (MB)', 'type' => 'number', 'default' => 5, 'min' => 1, 'max' => 20],
['key' => 'allow_comment_images', 'label' => '允许评论插入图片', 'type' => 'switch', 'default' => false],
['key' => 'comment_image_max_size', 'label' => '评论图片大小上限 (MB)', 'type' => 'number', 'default' => 2, 'min' => 1, 'max' => 10],
]],
['key' => 'resource', 'title' => '资源附件', 'fields' => [
['key' => 'allow_resource_upload', 'label' => '允许资源附件上传', 'type' => 'switch', 'default' => true],
['key' => 'resource', 'title' => '资源附件限制', 'fields' => [
['key' => 'resource_max_size', 'label' => '资源附件大小上限 (MB)', 'type' => 'number', 'default' => 50, 'min' => 1, 'max' => 200],
]],
],
],
'download' => [
'key' => 'download',
'title' => '下载权限',
'version' => '1.0.0',
'option_key' => 'aksrt_settings',
'groups' => [
['key' => 'basic', 'title' => '下载设置', 'fields' => [
['key' => 'download_require_login', 'label' => '下载需要登录', 'type' => 'switch', 'default' => true],
['key' => 'download_require_credits', 'label' => '允许积分购买模式', 'type' => 'switch', 'default' => false, 'help' => '兼容字段;运行时以 cms_features.download_credit 为准。开启表示 pay_modo=积分 可收银台一次购买,不是按次扣费。'],
['key' => 'download_credits_cost', 'label' => '默认积分价参考', 'type' => 'number', 'default' => 1, 'min' => 0, 'max' => 100, 'help' => '仅运营参考/兼容;实际价格以文章付费设置为准。'],
['key' => 'download_daily_limit', 'label' => '每日下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '0为不限制'],
['key' => 'download_link_expire', 'label' => '下载链接有效期(分钟)', 'type' => 'number', 'default' => 30, 'min' => 5, 'max' => 1440],
]],
['key' => 'vip', 'title' => 'VIP 特权', 'fields' => [
['key' => 'download_vip_free', 'label' => 'VIP 免费下载', 'type' => 'switch', 'default' => false],
['key' => 'download_vip_daily_extra', 'label' => 'VIP每日额外次数', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 999],
]],
['key' => 'role', 'title' => '角色权限', 'fields' => [
['key' => 'download_role_limits', 'label' => '角色下载限制', 'type' => 'textarea', 'default' => '', 'help' => '格式: role|daily_limit,每行一条'],
['key' => 'download_role_size', 'label' => '角色文件大小限制', 'type' => 'textarea', 'default' => '', 'help' => '格式: role|max_size_mb,每行一条'],
]],
['key' => 'captcha', 'title' => '安全设置', 'fields' => [
['key' => 'download_captcha_hint', 'label' => '下载验证码', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '下载验证码门槛在「功能&权限 → 下载权限」页(cms_features.download_captcha)维护;服务商与场景在「人机验证」。'],
['key' => 'download_ip_limit', 'label' => 'IP下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '同一IP每小时最大下载次数,0为不限制'],
]],
],
],
'moderation' => [
'key' => 'moderation',
'title' => '内容审核',
@@ -159,9 +89,9 @@ class SchemaPermissions {
]],
['key' => 'ai', 'title' => 'AI 审核', 'fields' => [
['key' => 'ai_enabled', 'label' => '启用 AI 审核', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'enabled', 'value' => true]],
['key' => 'ai_api_hint', 'label' => '审核 API 配置', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => 'API 接入(百度/阿里云等)在「扩展 → API内容审核」中统一配置,此处仅控制审核范围和策略。', 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'ai_api_hint', 'label' => '审核 API 配置', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => 'API 接入在「扩展 → API 内容审核」中统一配置;此处仅控制审核范围和策略。', 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'ai_moderate_comments', 'label' => '用户评论', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'ai_moderate_background', 'label' => '用户资料背景', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'ai_moderate_background', 'label' => '用户资料卡背景图', 'type' => 'switch', 'default' => true, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'auto_approve_low_risk', 'label' => '低风险自动通过', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'auto_reject_high_risk', 'label' => '高风险自动拒绝', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
['key' => 'low_risk_max_score', 'label' => '低风险上限', 'type' => 'number', 'default' => 35, 'min' => 0, 'max' => 100, 'dependsOn' => ['key' => 'ai_enabled', 'value' => true]],
@@ -175,21 +105,13 @@ class SchemaPermissions {
['key' => 'rate_limit_min_interval', 'label' => '最小间隔(秒)', 'type' => 'number', 'default' => 8, 'min' => 1, 'max' => 300, 'dependsOn' => ['key' => 'rate_limit_enabled', 'value' => true]],
['key' => 'rate_limit_max_per_hour', 'label' => '每小时上限', 'type' => 'number', 'default' => 15, 'min' => 1, 'max' => 200, 'dependsOn' => ['key' => 'rate_limit_enabled', 'value' => true]],
]],
['key' => 'geoip', 'title' => 'GeoIP 拦截', 'fields' => [
['key' => 'geoip_enabled', 'label' => '启用 GeoIP 拦截', 'type' => 'switch', 'default' => false, 'dependsOn' => ['key' => 'enabled', 'value' => true]],
['key' => 'geoip_hint', 'label' => 'IP 数据源', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '复用「扩展 → IP归属地」中配置的数据源和 API Key。', 'dependsOn' => ['key' => 'geoip_enabled', 'value' => true]],
['key' => 'geoip_block_countries', 'label' => '屏蔽国家/地区', 'type' => 'custom', 'default' => [], 'sanitize_type' => 'array', 'dependsOn' => ['key' => 'geoip_enabled', 'value' => true]],
]],
],
],
'capabilities' => [
'key' => 'capabilities',
'title' => '基本权限',
'version' => '1.0.0',
'version' => '1.1.0',
'option_key' => 'aksrt_settings',
// Live editing uses PermissionsCapabilities.vue + /permissions/capabilities.
// Keep an empty schema contract here so domain build() does not fatally call
// a missing build_cap_matrix_groups() helper when loading login/moderation.
'groups' => [],
],
],
-3
View File
@@ -253,8 +253,6 @@ class SchemaPostList {
['key' => 'comment_avatar_size', 'label' => '头像大小', 'type' => 'number', 'default' => 48, 'min' => 24, 'max' => 96],
]],
['key' => 'permissions', 'title' => '评论权限', 'fields' => [
['key' => 'comment_require_login', 'label' => '评论需要登录', 'type' => 'switch', 'default' => false],
['key' => 'comment_allow_reply', 'label' => '允许回复', 'type' => 'switch', 'default' => true],
['key' => 'comment_close_comments', 'label' => '关闭评论', 'type' => 'switch', 'default' => false],
['key' => 'comment_edit_window', 'label' => '作者自助编辑时限(分钟)', 'type' => 'number', 'default' => 10, 'min' => 0, 'max' => 1440],
]],
@@ -262,7 +260,6 @@ class SchemaPostList {
['key' => 'comment_smilie', 'label' => 'Emoji表情', 'type' => 'switch', 'default' => true],
['key' => 'comment_code', 'label' => '代码插入', 'type' => 'switch', 'default' => true],
['key' => 'comment_img', 'label' => '图片上传', 'type' => 'switch', 'default' => true],
['key' => 'comment_upload_img', 'label' => '评论图片上传', 'type' => 'switch', 'default' => false],
]],
['key' => 'interaction', 'title' => '互动功能', 'fields' => [
['key' => 'comment_hot_threshold', 'label' => '神评点赞阈值', 'type' => 'number', 'default' => 10, 'min' => 1, 'max' => 999],
-2
View File
@@ -602,8 +602,6 @@ class SchemaUsers {
'groups' => [
['key' => 'center', 'title' => '用户中心', 'fields' => [
['key' => 'user_center_tabs', 'label' => '用户中心 Tab', 'type' => 'input', 'default' => 'profile,level,comments,favorites,vip,credits,revenue,auth', 'help' => '逗号分隔。level=我的等级;creator=创作中心;revenue=推广收益中心;auth=官方认证;等级、VIP、订单、创作中心、收益中心和认证会按功能开关自动注入'],
['key' => 'user_cover_upload', 'label' => '封面上传', 'type' => 'switch', 'default' => true],
['key' => 'user_social_links', 'label' => '社交链接', 'type' => 'switch', 'default' => true],
['key' => 'user_desc_std', 'label' => '默认签名', 'type' => 'input', 'default' => '这家伙很懒…', 'desc' => '用户未设置个人简介时显示的默认文案'],
['key' => 'user_city_s', 'label' => '显示城市(IP 归属地)', 'type' => 'switch', 'default' => true],
['key' => 'user_city_type', 'label' => '位置精度', 'type' => 'radio', 'default' => 'province', 'options' => [
+43 -1
View File
@@ -659,6 +659,9 @@ class UserController extends ControllerBase {
if ($user_id <= 0) {
return $this->error(__('请先登录', 'aksrt'), 401);
}
if (!function_exists('aksrt_user_can') || !aksrt_user_can('upload_comment_image', $user_id)) {
return $this->error(__('当前账号没有上传评论图片的权限', 'aksrt'), 403);
}
if (empty($_FILES['image']) && empty($_FILES['file'])) {
return $this->error(__('请选择要上传的图片', 'aksrt'), 400);
}
@@ -667,10 +670,49 @@ class UserController extends ControllerBase {
require_once ABSPATH . 'wp-admin/includes/image.php';
$file_key = !empty($_FILES['image']) ? 'image' : 'file';
$attachment_id = media_handle_upload($file_key, 0);
$file = $_FILES[$file_key];
$upload_error = isset($file['error']) ? (int) $file['error'] : UPLOAD_ERR_NO_FILE;
if ($upload_error !== UPLOAD_ERR_OK) {
return $this->error(__('图片文件上传失败,请重新选择', 'aksrt'), 400);
}
$settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$max_size_mb = max(1, min(10, (int) ($settings['comment_image_max_size'] ?? 2)));
$actual_size = !empty($file['tmp_name']) && is_readable($file['tmp_name']) ? (int) filesize($file['tmp_name']) : 0;
if ($actual_size <= 0 || $actual_size > $max_size_mb * MB_IN_BYTES) {
return $this->error(sprintf(__('图片大小必须在 1 字节至 %dMB 之间', 'aksrt'), $max_size_mb), 413);
}
$mimes = [
'jpg|jpeg|jpe' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
];
$checked = wp_check_filetype_and_ext($file['tmp_name'], $file['name'], $mimes);
$image_size = wp_getimagesize($file['tmp_name']);
if (empty($checked['type']) || empty($checked['ext']) || !wp_match_mime_types('image', $checked['type']) || !$image_size || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
return $this->error(__('仅支持有效的 JPG、PNG、GIF 或 WebP 图片', 'aksrt'), 415);
}
if (class_exists('\AKSRT\Extensions\ContentAudit')) {
$audit = \AKSRT\Extensions\ContentAudit::get_instance()->audit_image((string) $file['tmp_name']);
if (empty($audit['passed'])) {
return $this->error(__('图片未通过内容审核', 'aksrt'), 422);
}
}
$attachment_id = media_handle_upload($file_key, 0, [], [
'test_form' => false,
'mimes' => $mimes,
]);
if (is_wp_error($attachment_id)) {
return $this->error($attachment_id->get_error_message(), 400);
}
wp_update_post(['ID' => (int) $attachment_id, 'post_author' => $user_id]);
if (!wp_attachment_is_image((int) $attachment_id)) {
wp_delete_attachment((int) $attachment_id, true);
return $this->error(__('上传文件不是有效图片', 'aksrt'), 415);
}
$url = wp_get_attachment_url((int) $attachment_id);
if (!$url) {
return $this->error(__('上传失败', 'aksrt'), 500);
@@ -54,14 +54,11 @@ trait UserControllerProfileMediaOperations {
return $this->error(__('请先登录', 'aksrt'), 401);
}
$center_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_user_center_settings()
: ['avatar_upload' => true, 'cover_upload' => true];
if ($is_avatar && empty($center_settings['avatar_upload'])) {
return $this->error(__('头像上传功能未开启', 'aksrt'), 403);
}
if (!$is_avatar && (empty($center_settings['cover_upload']) || !aksrt_get_setting('allow_background_upload', false))) {
return $this->error(__('背景图上传功能未开启', 'aksrt'), 403);
$capability = $is_avatar ? 'upload_avatar' : 'upload_profile_background';
if (!function_exists('aksrt_user_can') || !aksrt_user_can($capability, $user_id)) {
return $this->error($is_avatar
? __('当前账号没有上传头像的权限', 'aksrt')
: __('当前账号没有上传用户资料卡背景图的权限', 'aksrt'), 403);
}
$file_key = '';
@@ -107,6 +104,12 @@ trait UserControllerProfileMediaOperations {
if (!$image_size || empty($image_size[0]) || empty($image_size[1]) || ((int) $image_size[0] * (int) $image_size[1]) > 40000000) {
return $this->error(__('图片无效或尺寸过大', 'aksrt'), 415);
}
if (!$is_avatar && class_exists('\AKSRT\Extensions\ContentAudit')) {
$audit = \AKSRT\Extensions\ContentAudit::get_instance()->audit_image((string) $file['tmp_name']);
if (empty($audit['passed'])) {
return $this->error(__('图片未通过内容审核', 'aksrt'), 422);
}
}
$attachment_id = media_handle_upload($file_key, 0, [], [
'test_form' => false,
+14 -1
View File
@@ -451,8 +451,21 @@ trait UserControllerReportOperations {
return $this->error('无效的举报类型');
}
$reporter_id = get_current_user_id();
if ($reporter_id <= 0) {
return $this->error(__('请先登录', 'aksrt'), 401);
}
if ($target_type === 'user') {
if ($target_id === $reporter_id) {
return $this->error(__('不能举报自己', 'aksrt'), 400);
}
if (!function_exists('aksrt_user_can') || !aksrt_user_can('user_report', $reporter_id, $target_id)) {
return $this->error(__('当前账号没有举报用户的权限', 'aksrt'), 403);
}
}
$report_id = \AKSRT\Messages\ReportCenter::get_instance()->submit(
get_current_user_id(),
$reporter_id,
$target_type,
$target_id,
$reason,
+9
View File
@@ -81,6 +81,15 @@ class Moderation {
*/
public function process(array $commentdata): array {
$actor = get_current_user_id();
if (!function_exists('aksrt_user_can') || !aksrt_user_can('comment_add', $actor, (int) ($commentdata['comment_post_ID'] ?? 0))) {
wp_die(
esc_html__('当前账号没有发表评论的权限。', 'aksrt'),
esc_html__('评论提交失败', 'aksrt'),
['response' => $actor > 0 ? 403 : 401, 'back_link' => true]
);
}
// 人机验证:覆盖 REST 与原生 wp-comments-post 两条提交合同(与审核开关无关)
if (function_exists('aksrt_require_captcha')) {
$payload = is_array($_POST ?? null) ? wp_unslash($_POST) : [];
+12 -9
View File
@@ -91,6 +91,15 @@ class Download {
}
public function check_rest_nonce(\WP_REST_Request $request) {
// The capability matrix can explicitly allow resource downloads for
// everyone. A WordPress REST nonce only exists for authenticated
// sessions, so requiring one from visitors would make the "all"
// column ineffective. Anonymous requests still pass the unified
// download gate (capability, payment and captcha) in rest_request().
if (get_current_user_id() <= 0) {
return true;
}
$nonce = $request->get_header('x-wp-nonce');
if (!$nonce) {
return new \WP_Error('aksrt_rest_no_nonce', __('缺少安全令牌', 'aksrt'), ['status' => 403]);
@@ -109,7 +118,6 @@ class Download {
}
$meta = function_exists('aksrt_get_download_box_meta') ? aksrt_get_download_box_meta($post) : [];
$paid_settings = \AKSRT\Settings\Settings::get_paid_content_settings();
$cost = aksrt_resource_download_cost($post);
$uid = get_current_user_id();
$priced = function_exists('aksrt_get_user_pay_price') ? aksrt_get_user_pay_price($post, $uid) : null;
@@ -122,10 +130,7 @@ class Download {
? Visibility::get_post_access_state($post, $uid)
: [];
$need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']);
if ($need_captcha && function_exists('aksrt_is_captcha_enabled_for') && !aksrt_is_captcha_enabled_for('download')) {
$need_captcha = false;
}
$need_captcha = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
return new \WP_REST_Response([
'post_id' => $post_id,
@@ -134,7 +139,7 @@ class Download {
'pay_modo' => $pay_modo,
'pay_limit' => (int) ($meta['pay_limit'] ?? 0),
'cost' => $cost,
'require_login' => !empty($paid_settings['download_login']) || !empty($meta['require_login']) || (!empty($paid_settings['free_logged_show']) && $cost <= 0 && $display_price <= 0),
'require_login' => !empty($access['need_login']),
'require_captcha' => $need_captcha,
'logged_in' => $uid > 0,
'balance' => $uid > 0 && class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::balance($uid) : 0,
@@ -181,7 +186,6 @@ class Download {
$gate = aksrt_can_download($post);
if (is_wp_error($gate)) return $gate;
$paid_settings = \AKSRT\Settings\Settings::get_paid_content_settings();
$uid = get_current_user_id();
$cost = aksrt_resource_download_cost($post);
@@ -193,8 +197,7 @@ class Download {
}
}
$need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']);
// 下载双开关:cms_features.download_captcha(或单资源强制)且场景未停用。
$need_captcha = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
if ($need_captcha && function_exists('aksrt_require_captcha')) {
$cap_payload = isset($params['captcha']) && is_array($params['captcha']) ? $params['captcha'] : [];
// 场景未启用时 require 直接放行;启用时强制校验 token。
+14 -28
View File
@@ -47,14 +47,6 @@ class Visibility {
: (($pay_modo === 'points')
? (float) ($pay_meta['points_price'] ?? 0)
: (float) ($pay_meta['price'] ?? 0));
$paid_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_paid_content_settings()
: [
'download_login' => true,
'download_credit' => true,
'download_captcha' => false,
];
$state = [
'post_id' => $resource_post instanceof \WP_Post ? (int) $resource_post->ID : 0,
'pay_type' => $pay_type,
@@ -134,12 +126,13 @@ class Visibility {
if ($allowed_member && $price <= 0) {
$download_cost = 0;
}
$can_download = function_exists('aksrt_user_can') && aksrt_user_can('resource_download', $uid, (int) $resource_post->ID);
$state['download_cost'] = max(0, $download_cost);
$state['need_login'] = $uid <= 0;
$state['need_login'] = !$can_download && $uid <= 0;
$state['need_purchase'] = $allowed_member && $price > 0 && !$has_paid_order;
$state['has_paid_access'] = $allowed_member && ($has_paid_order || $price <= 0);
$state['can_view'] = true;
$state['can_download'] = $allowed_member && ($uid > 0) && ($has_paid_order || $price <= 0);
$state['can_download'] = $can_download && $allowed_member && ($has_paid_order || $price <= 0);
if (!$allowed_member) {
$state['can_download'] = false;
$state['has_paid_access'] = false;
@@ -172,16 +165,14 @@ class Visibility {
}
$state['download_cost'] = max(0, $download_cost);
$free_logged_show = !empty($paid_settings['free_logged_show']);
$state['need_login'] = !empty($paid_settings['download_login'])
|| !empty($pay_meta['require_login'])
|| ($free_logged_show && $price <= 0);
$state['require_captcha'] = !empty($paid_settings['download_captcha']) || !empty($pay_meta['require_captcha']);
$can_download = function_exists('aksrt_user_can') && aksrt_user_can('resource_download', $uid, (int) $resource_post->ID);
$state['need_login'] = !$can_download && $uid <= 0;
$state['require_captcha'] = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
$state['need_purchase'] = !$has_paid_order && $price > 0;
$state['has_paid_access'] = $has_paid_order || $price <= 0 || $has_free_access;
$state['can_view'] = true;
$state['can_download'] = (!$state['need_login'] || $uid > 0) && ($has_paid_order || $price <= 0);
$state['can_download'] = $can_download && ($has_paid_order || $price <= 0);
return $state;
}
@@ -227,6 +218,13 @@ class Visibility {
$state = self::get_post_access_state($resource_post, $user_id);
$uid = $user_id > 0 ? $user_id : get_current_user_id();
if (!function_exists('aksrt_user_can') || !aksrt_user_can('resource_download', $uid, (int) $resource_post->ID)) {
return new \WP_Error('aksrt_download_forbidden', __('当前账号没有下载资源的权限。', 'aksrt'), [
'status' => $uid > 0 ? 403 : 401,
'login_url' => wp_login_url(get_permalink($resource_post)),
]);
}
if (($state['pay_type'] ?? '') !== \AKSRT\Core\DomainModel::PAY_TYPE_DOWNLOAD) {
return new \WP_Error('aksrt_not_downloadable', __('该内容未启用下载能力。', 'aksrt'));
}
@@ -238,19 +236,7 @@ class Visibility {
]);
}
// Free download + pay_free_logged_show (belt-and-suspenders with state.need_login).
$cost = (int) ($state['download_cost'] ?? 0);
$paid_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_paid_content_settings()
: ['download_credit' => true, 'free_logged_show' => false];
if ($cost <= 0 && !empty($paid_settings['free_logged_show']) && $uid <= 0) {
return new \WP_Error('aksrt_need_login', __('免费资源,请登录后下载。', 'aksrt'), [
'status' => 401,
'login_url' => wp_login_url(get_permalink($resource_post)),
]);
}
// Member-only (pay_limit).
if (!empty($state['need_vip']) && empty($state['has_paid_access']) && empty($state['can_download'])) {
$need_level = (int) ($state['pay_limit'] ?? 1);
+9
View File
@@ -266,6 +266,15 @@ class CommentActions {
], 403);
}
$user_id = get_current_user_id();
if (!function_exists('aksrt_user_can') || !aksrt_user_can('comment_add', $user_id, $post_id)) {
return new \WP_REST_Response([
'success' => false,
'code' => 'comment_forbidden',
'message' => __('当前账号没有发表评论的权限。', 'aksrt'),
], $user_id > 0 ? 403 : 401);
}
$nonce = (string) ($params['_aksrt_comment_nonce'] ?? $params['_wpnonce'] ?? '');
if ($nonce !== '' && !wp_verify_nonce($nonce, 'aksrt_comment_nonce') && !wp_verify_nonce($nonce, 'wp_rest')) {
return new \WP_REST_Response([
+112
View File
@@ -36,12 +36,124 @@ class FrontSubmit {
// Guest path (when enabled).
add_action('wp_ajax_nopriv_aksrt_submit_post', [$this, 'ajax_submit_post']);
add_action('rest_api_init', [$this, 'register_rest_routes']);
add_filter('body_class', [$this, 'body_class']);
add_filter('wp_robots', [$this, 'robots_noindex']);
add_filter('aksrt_floating_buttons_enabled', [$this, 'disable_chrome_on_submit']);
add_filter('aksrt_footer_tabbar_enabled', [$this, 'disable_chrome_on_submit']);
}
public function register_rest_routes(): void {
register_rest_route('aksrt/v1', '/front-submit/upload/(?P<kind>image|resource)', [
'methods' => 'POST',
'callback' => [$this, 'rest_upload_attachment'],
'permission_callback' => [$this, 'can_upload_attachment'],
'args' => [
'kind' => [
'validate_callback' => static function ($value): bool {
return in_array((string) $value, ['image', 'resource'], true);
},
],
],
]);
}
public function can_upload_attachment(\WP_REST_Request $request): bool {
$user_id = get_current_user_id();
if ($user_id <= 0) {
return false;
}
if (!$this->can_submit($user_id)) {
return false;
}
$kind = (string) $request->get_param('kind');
$cap = $kind === 'resource' ? 'upload_resource_attachment' : 'upload_submit_image';
return $this->user_can_cap($cap, $user_id);
}
public function rest_upload_attachment(\WP_REST_Request $request) {
$kind = (string) $request->get_param('kind');
$user_id = get_current_user_id();
$files = $request->get_file_params();
$file = $files['file'] ?? null;
if (!is_array($file) || empty($file['tmp_name']) || !empty($file['error'])) {
return new \WP_Error('aksrt_upload_missing_file', __('请选择要上传的文件。', 'aksrt'), ['status' => 400]);
}
$settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$max_mb = $kind === 'resource'
? max(1, min(200, (int) ($settings['resource_max_size'] ?? 50)))
: max(1, min(20, (int) ($settings['submit_image_max_size'] ?? 5)));
$max_size = $max_mb * MB_IN_BYTES;
$size = (int) ($file['size'] ?? 0);
if ($size <= 0 || $size > $max_size) {
return new \WP_Error(
'aksrt_upload_too_large',
sprintf(__('文件大小不能超过 %dMB。', 'aksrt'), $max_mb),
['status' => 400]
);
}
$filename = sanitize_file_name((string) ($file['name'] ?? ''));
$checked = wp_check_filetype_and_ext((string) $file['tmp_name'], $filename, get_allowed_mime_types());
$mime = (string) ($checked['type'] ?? '');
if ($filename === '' || $mime === '') {
return new \WP_Error('aksrt_upload_invalid_type', __('不支持的文件类型。', 'aksrt'), ['status' => 400]);
}
if ($kind === 'image') {
$image_info = @getimagesize((string) $file['tmp_name']);
if ($image_info === false || strpos($mime, 'image/') !== 0) {
return new \WP_Error('aksrt_upload_invalid_image', __('请上传有效的图片文件。', 'aksrt'), ['status' => 400]);
}
if (((int) $image_info[0] * (int) $image_info[1]) > 40000000) {
return new \WP_Error('aksrt_upload_image_too_large', __('图片尺寸过大。', 'aksrt'), ['status' => 400]);
}
if (class_exists('\AKSRT\Extensions\ContentAudit')) {
$audit = \AKSRT\Extensions\ContentAudit::get_instance()->audit_image((string) $file['tmp_name']);
if (empty($audit['passed'])) {
return new \WP_Error('aksrt_upload_image_rejected', __('图片未通过内容审核。', 'aksrt'), ['status' => 422]);
}
}
}
if (!function_exists('media_handle_upload')) {
require_once ABSPATH . 'wp-admin/includes/media.php';
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/image.php';
}
$_FILES['aksrt_front_submit_file'] = $file;
$attachment_id = media_handle_upload('aksrt_front_submit_file', 0, [], ['test_form' => false]);
unset($_FILES['aksrt_front_submit_file']);
if (is_wp_error($attachment_id)) {
return new \WP_Error('aksrt_upload_failed', $attachment_id->get_error_message(), ['status' => 400]);
}
$attachment_id = (int) $attachment_id;
wp_update_post(['ID' => $attachment_id, 'post_author' => $user_id]);
update_post_meta($attachment_id, '_aksrt_front_submit_upload_type', $kind);
update_post_meta($attachment_id, '_aksrt_front_submit_upload_user_id', $user_id);
$url = wp_get_attachment_url($attachment_id);
if (!$url) {
wp_delete_attachment($attachment_id, true);
return new \WP_Error('aksrt_upload_failed', __('上传后无法读取文件。', 'aksrt'), ['status' => 500]);
}
return new \WP_REST_Response([
'success' => true,
'attachment_id' => $attachment_id,
'url' => $url,
'name' => get_the_title($attachment_id),
'mime' => $mime,
], 201);
}
/**
* @return array<string, mixed>
*/
+36 -10
View File
@@ -48,9 +48,12 @@ trait FrontSubmitFeatured {
}
$video = esc_url_raw((string) ($payload['url'] ?? $payload['video'] ?? ''));
$pic = esc_url_raw((string) ($payload['pic'] ?? $payload['poster'] ?? $payload['cover'] ?? ''));
if ($pic !== '' && !$this->user_can_cap('upload_submit_image', $user_id)) {
$pic = '';
}
$post_meta['featured_video'] = $video;
$post_meta['featured_slide'] = '';
if ($pic !== '') {
if ($pic !== '' && $this->can_use_front_submit_image_url($pic, $user_id)) {
$post_meta['thumbnail_url'] = $pic;
$post_meta['cover_images'] = $pic;
$this->maybe_set_thumbnail_url($post_id, $pic);
@@ -70,22 +73,26 @@ trait FrontSubmitFeatured {
foreach ($ids as $item) {
if (is_array($item)) {
$u = esc_url_raw((string) ($item['url'] ?? ''));
if ($u !== '') {
$urls[] = $u;
} elseif (!empty($item['id'])) {
$src = wp_get_attachment_image_url((int) $item['id'], 'full');
$attachment_id = absint($item['id'] ?? 0);
if ($attachment_id > 0 && $this->can_use_front_submit_image($attachment_id, $user_id)) {
$src = wp_get_attachment_image_url($attachment_id, 'full');
if ($src) {
$urls[] = $src;
}
} elseif ($u !== '' && $this->user_can_cap('upload_submit_image', $user_id)) {
$urls[] = $u;
}
} elseif (is_numeric($item)) {
$src = wp_get_attachment_image_url((int) $item, 'full');
$attachment_id = (int) $item;
$src = $this->can_use_front_submit_image($attachment_id, $user_id)
? wp_get_attachment_image_url($attachment_id, 'full')
: false;
if ($src) {
$urls[] = $src;
}
} else {
$u = esc_url_raw((string) $item);
if ($u !== '') {
if ($u !== '' && $this->user_can_cap('upload_submit_image', $user_id)) {
$urls[] = $u;
}
}
@@ -93,7 +100,7 @@ trait FrontSubmitFeatured {
}
$post_meta['featured_slide'] = implode(',', $urls);
$post_meta['featured_video'] = '';
if (!empty($urls[0])) {
if (!empty($urls[0]) && $this->can_use_front_submit_image_url((string) $urls[0], $user_id)) {
$post_meta['thumbnail_url'] = $urls[0];
$post_meta['cover_images'] = $urls[0];
$this->maybe_set_thumbnail_url($post_id, $urls[0]);
@@ -106,6 +113,9 @@ trait FrontSubmitFeatured {
}
$url = esc_url_raw((string) ($payload['url'] ?? $payload['image'] ?? ''));
$id = absint($payload['id'] ?? 0);
if ($id > 0 && !$this->can_use_front_submit_image($id, $user_id)) {
return;
}
if ($url === '' && $id > 0) {
$src = wp_get_attachment_image_url($id, 'full');
if ($src) {
@@ -114,12 +124,12 @@ trait FrontSubmitFeatured {
}
$post_meta['featured_video'] = '';
$post_meta['featured_slide'] = '';
if ($url !== '') {
if ($url !== '' && $this->can_use_front_submit_image_url($url, $user_id)) {
$post_meta['thumbnail_url'] = $url;
$post_meta['cover_images'] = $url;
if ($id > 0) {
set_post_thumbnail($post_id, $id);
} else {
} elseif ($this->user_can_cap('upload_submit_image', $user_id)) {
$this->maybe_set_thumbnail_url($post_id, $url);
}
}
@@ -154,4 +164,20 @@ trait FrontSubmitFeatured {
}
$this->set_thumbnail_from_url($post_id, $url);
}
private function can_use_front_submit_image(int $attachment_id, int $user_id): bool {
if ($attachment_id <= 0 || !$this->user_can_cap('upload_submit_image', $user_id)) {
return false;
}
$attachment = get_post($attachment_id);
return $attachment instanceof \WP_Post
&& $attachment->post_type === 'attachment'
&& (int) $attachment->post_author === $user_id
&& wp_attachment_is_image($attachment_id);
}
private function can_use_front_submit_image_url(string $url, int $user_id): bool {
$attachment_id = attachment_url_to_postid($url);
return $attachment_id > 0 && $this->can_use_front_submit_image($attachment_id, $user_id);
}
}
+18 -3
View File
@@ -55,7 +55,7 @@ trait FrontSubmitPay {
}
}
$downloads = $this->normalize_front_downloads($raw['downloads'] ?? $data['pay_download'] ?? []);
$downloads = $this->normalize_front_downloads($raw['downloads'] ?? $data['pay_download'] ?? [], $user_id);
if (!empty($downloads) && $this->user_can_cap('new_post_pay_download', $user_id)) {
$pay_type = DomainModel::PAY_TYPE_DOWNLOAD;
$meta['downloads'] = $downloads;
@@ -163,7 +163,7 @@ trait FrontSubmitPay {
* @param mixed $raw
* @return array<int, array<string, string>>
*/
private function normalize_front_downloads($raw): array {
private function normalize_front_downloads($raw, int $user_id): array {
if (!is_array($raw)) {
return [];
}
@@ -178,16 +178,31 @@ trait FrontSubmitPay {
if ($url === '') {
continue;
}
$attachment_id = attachment_url_to_postid($url);
if ($attachment_id > 0 && !$this->can_use_front_submit_resource($attachment_id, $user_id)) {
continue;
}
$out[] = [
'label' => sanitize_text_field((string) ($row['label'] ?? $row['name'] ?? '')),
'url' => $url,
'type' => 'external',
'type' => $attachment_id > 0 ? 'direct' : 'external',
'extract_code' => sanitize_text_field((string) ($row['extract_code'] ?? $row['copy_val'] ?? $row['more'] ?? '')),
];
}
return DomainModel::sanitize_downloads($out);
}
private function can_use_front_submit_resource(int $attachment_id, int $user_id): bool {
if ($attachment_id <= 0 || !$this->user_can_cap('upload_resource_attachment', $user_id)) {
return false;
}
$attachment = get_post($attachment_id);
return $attachment instanceof \WP_Post
&& $attachment->post_type === 'attachment'
&& (int) $attachment->post_author === $user_id
&& get_post_meta($attachment_id, '_aksrt_front_submit_upload_type', true) === 'resource';
}
/**
* @param array<string, mixed> $meta
*/
+6 -2
View File
@@ -183,8 +183,12 @@ trait FrontSubmitSave {
// Legacy thumbnail URL fallback.
$thumbnail_url = esc_url_raw((string) ($data['thumbnail'] ?? ''));
if ($thumbnail_url !== '' && empty($data['featured_data'])) {
$this->set_thumbnail_from_url((int) $in_id, $thumbnail_url);
if ($thumbnail_url !== '' && empty($data['featured_data']) && $this->user_can_cap('upload_submit_image', $user_id)) {
$attachment_id = attachment_url_to_postid($thumbnail_url);
$attachment = $attachment_id > 0 ? get_post($attachment_id) : null;
if ($attachment instanceof \WP_Post && (int) $attachment->post_author === $user_id && wp_attachment_is_image($attachment_id)) {
set_post_thumbnail((int) $in_id, $attachment_id);
}
}
$post = get_post((int) $in_id);
+6 -1
View File
@@ -17,7 +17,12 @@ trait AuthReader {
/** 是否要求注册用户才能评论 */
public static function comment_requires_registration(): bool {
return !empty(self::get_value('comment_require_login', false)) && self::comment_enabled();
if (!self::comment_enabled()) {
return true;
}
$user_id = get_current_user_id();
return !class_exists(Capabilities::class) || !Capabilities::user_can('comment_add', $user_id);
}
/** 是否允许注册 */
+16 -18
View File
@@ -12,7 +12,7 @@ class Capabilities {
/**
* Capability definitions registry.
*
* @return array<string, array{label:string, roles?:string[], exclude_roles?:string[], default?:array<string,mixed>}>
* @return array<string, array{label:string, exclude_roles?:string[], default?:array<string,mixed>}>
*/
public static function cap_definitions(): array {
return [
@@ -20,16 +20,11 @@ class Capabilities {
'balance_transfer' => ['label' => __('余额转账给其他用户', 'aksrt'), 'exclude_roles' => ['all']],
'points_transfer' => ['label' => __('积分转账给其他用户', 'aksrt'), 'exclude_roles' => ['all']],
'balance_withdraw' => ['label' => __('余额提现', 'aksrt'), 'exclude_roles' => ['all']],
'set_user_ban' => ['label' => __('设置用户封禁状态', 'aksrt'), 'roles' => ['cat_moderator'], 'default' => ['cat_moderator' => true]],
'medal_manually_set' => ['label' => __('为用户授予徽章', 'aksrt'), 'roles' => ['cat_moderator'], 'default' => ['cat_moderator' => true]],
'new_post_add' => ['label' => __('发布新文章', 'aksrt'), 'default' => ['logged' => true]],
'new_post_audit_no' => ['label' => __('投稿无需审核直接发布', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_audit_no_manual' => ['label' => __('无需人工审核直接发布', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_upload_img' => ['label' => __('投稿允许上传图片', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_upload_video' => ['label' => __('投稿允许上传视频', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_iframe_video' => ['label' => __('投稿允许插入 iframe 视频', 'aksrt'), 'exclude_roles' => ['all'], 'default' => ['logged' => true]],
'new_post_upload_file' => ['label' => __('投稿允许上传文件', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_hide' => ['label' => __('投稿允许发布隐藏内容', 'aksrt'), 'default' => ['logged' => true]],
'new_post_image_cover' => ['label' => __('发帖允许设置图片封面', 'aksrt'), 'exclude_roles' => ['all']],
'new_post_slide_cover' => ['label' => __('允许设置幻灯片封面', 'aksrt'), 'exclude_roles' => ['all']],
@@ -40,18 +35,26 @@ class Capabilities {
'new_post_delete' => ['label' => __('删除自己发布的投稿', 'aksrt'), 'exclude_roles' => ['all']],
'comment_view' => ['label' => __('查看评论', 'aksrt'), 'default' => ['all' => true]],
'comment_add' => ['label' => __('发表评论', 'aksrt'), 'default' => ['logged' => true]],
'comment_edit' => ['label' => __('修改自己发布的评论', 'aksrt'), 'exclude_roles' => ['all'], 'default' => ['logged' => true]],
'comment_delete' => ['label' => __('删除自己发布的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_audit_no' => ['label' => __('评论无需审核直接发布', 'aksrt'), 'exclude_roles' => ['all']],
'comment_audit_no_manual' => ['label' => __('评论无需人工审核', 'aksrt'), 'exclude_roles' => ['all']],
'comment_set_topping_my_post' => ['label' => __('置顶自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all'], 'default' => ['moderator' => true, 'plate_author' => true, 'cat_moderator' => true]],
'comment_set_topping_other' => ['label' => __('置顶管理帖子下的评论', 'aksrt'), 'roles' => ['moderator', 'plate_author', 'cat_moderator'], 'default' => ['cat_moderator' => true]],
'comment_set_topping_my_post' => ['label' => __('置顶自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_set_topping_other' => ['label' => __('置顶其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_edit_my_post' => ['label' => __('修改自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_edit_other' => ['label' => __('修改管理帖子下的评论', 'aksrt'), 'roles' => ['moderator', 'plate_author', 'cat_moderator'], 'default' => ['cat_moderator' => true]],
'comment_audit_my_post' => ['label' => __('审核自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all'], 'default' => ['moderator' => true, 'plate_author' => true, 'cat_moderator' => true]],
'comment_audit_other' => ['label' => __('审核管理帖子下的评论', 'aksrt'), 'roles' => ['moderator', 'plate_author', 'cat_moderator'], 'default' => ['moderator' => true, 'plate_author' => true, 'cat_moderator' => true]],
'comment_edit_other' => ['label' => __('修改其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_audit_my_post' => ['label' => __('审核自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_audit_other' => ['label' => __('审核其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_delete_my_post' => ['label' => __('删除自己文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
'comment_delete_other' => ['label' => __('删除管理帖子下的评论', 'aksrt'), 'roles' => ['moderator', 'plate_author', 'cat_moderator'], 'default' => ['cat_moderator' => true]],
'comment_delete_other' => ['label' => __('删除其他文章下的评论', 'aksrt'), 'exclude_roles' => ['all']],
'upload_avatar' => ['label' => __('上传头像', 'aksrt'), 'default' => ['logged' => true]],
'upload_profile_background' => ['label' => __('上传用户资料卡背景图', 'aksrt'), 'default' => ['logged' => true]],
'upload_submit_image' => ['label' => __('投稿图片上传', 'aksrt'), 'exclude_roles' => ['all']],
'upload_comment_image' => ['label' => __('评论图片上传', 'aksrt'), 'exclude_roles' => ['all']],
'upload_resource_attachment'=> ['label' => __('资源附件上传', 'aksrt'), 'exclude_roles' => ['all']],
'resource_download' => ['label' => __('资源下载', 'aksrt'), 'default' => ['logged' => true]],
];
}
@@ -59,7 +62,7 @@ class Capabilities {
* All role columns in the capability matrix.
*/
public static function cap_role_columns(): array {
return ['all', 'logged', 'level', 'vip', 'auth', 'moderator', 'plate_author', 'cat_moderator'];
return ['all', 'logged', 'level', 'vip', 'auth'];
}
/**
@@ -77,14 +80,9 @@ class Capabilities {
foreach ($defs as $cap_id => $def) {
$defaults = $def['default'] ?? [];
$excludes = $def['exclude_roles'] ?? [];
$only_roles = $def['roles'] ?? null;
$row = [];
foreach ($cols as $col) {
if ($only_roles !== null && !in_array($col, $only_roles, true) && !in_array($col, ['all', 'logged', 'level', 'vip', 'auth'], true)) {
$row[$col] = false;
continue;
}
if (in_array($col, $excludes, true)) {
$row[$col] = false;
continue;
+1 -3
View File
@@ -121,9 +121,7 @@ trait CreditsPaymentGroups {
* Unified paid-content and download policy settings.
*
* @return array{
* download_login:bool,
* download_credit:bool,
* download_captcha:bool,
* default_cost:int,
* paid_post_types:string[],
* vip_free_download:bool,
@@ -194,7 +192,7 @@ trait CreditsPaymentGroups {
/**
* 获取订单设置
*
* @return array{prefix:string,expiry:int,show_user:bool,show_paycount:bool,no_logged_in:bool,cookie_day:int,no_logged_remind:bool,wait_float:bool,order_max_minutes:int,mark:string,box_position:string,free_logged_show:bool,show_ip:bool,show_device:bool,show_order_no:bool,show_create_time:bool,show_pay_time:bool,status_texts:array,service:array}
* @return array{prefix:string,expiry:int,show_user:bool,show_paycount:bool,no_logged_in:bool,cookie_day:int,no_logged_remind:bool,wait_float:bool,order_max_minutes:int,mark:string,box_position:string,show_ip:bool,show_device:bool,show_order_no:bool,show_create_time:bool,show_pay_time:bool,status_texts:array,service:array}
*/
public static function get_order_settings(): array {
return PaymentRuntime::get_order_settings();
-2
View File
@@ -137,9 +137,7 @@ class HomeCms {
public static function get_cms_features(): array {
$defaults = [
'resource_cpt_enabled' => true,
'download_login' => true,
'download_credit' => true,
'download_captcha' => false,
'search_include_resource' => true,
];
$s = Settings::get_all();
-4
View File
@@ -27,9 +27,7 @@ class PaymentRuntime {
}
return [
'download_login' => !empty($features['download_login']),
'download_credit' => !empty($features['download_credit']) && Settings::credits_enabled(),
'download_captcha' => !empty($features['download_captcha']),
'default_cost' => Settings::download_default_cost(),
'paid_post_types' => self::parse_slug_list((string) ($settings['payment_paid_post_types'] ?? 'post'), ['post']),
'vip_free_download' => !empty($settings['payment_vip_free_download']),
@@ -37,7 +35,6 @@ class PaymentRuntime {
'catalog_note' => $catalog_note,
'manual_delivery_note' => $manual_note !== '' ? $manual_note : __('付费或人工确认内容请在订单备注中记录交付方式。', 'aksrt'),
'no_logged_in' => !isset($settings['pay_no_logged_in']) || !empty($settings['pay_no_logged_in']),
'free_logged_show' => !empty($settings['pay_free_logged_show']),
'down_alone_page' => !empty($settings['payment_down_alone_page']),
'display' => [
'mark' => sanitize_text_field((string) ($settings['pay_mark'] ?? '付费')),
@@ -375,7 +372,6 @@ class PaymentRuntime {
'order_max_minutes' => $max_minutes,
'mark' => sanitize_text_field((string) ($s['pay_mark'] ?? '付费')),
'box_position' => $box_position,
'free_logged_show' => !empty($s['pay_free_logged_show']),
'show_ip' => !empty($s['payment_show_ip']),
'show_device' => !empty($s['payment_show_device']),
'show_order_no' => !isset($s['payment_show_order_no']) || !empty($s['payment_show_order_no']),
+1 -2
View File
@@ -461,14 +461,13 @@ class PostDisplay {
'per_page' => max(5, min(100, (int) ($settings['comment_per_page'] ?? 20))),
'nested_depth' => max(1, min(10, (int) ($settings['comment_nested_depth'] ?? 3))),
'require_login' => Settings::comment_requires_registration(),
'allow_reply' => !isset($settings['comment_allow_reply']) || !empty($settings['comment_allow_reply']),
'avatar_size' => max(24, min(96, (int) ($settings['comment_avatar_size'] ?? 48))),
'empty_text' => $empty_text !== '' ? $empty_text : __('暂无评论,来做第一个评论的人吧~', 'aksrt'),
'toolbar' => [
'smilie' => !isset($settings['comment_smilie']) || !empty($settings['comment_smilie']),
'code' => !isset($settings['comment_code']) || !empty($settings['comment_code']),
'image' => !isset($settings['comment_img']) || !empty($settings['comment_img']),
'upload_img' => !empty($settings['comment_upload_img']),
'upload_img' => function_exists('aksrt_user_can') && aksrt_user_can('upload_comment_image', get_current_user_id()),
],
'like_enabled' => !isset($settings['comment_like_s']) || !empty($settings['comment_like_s']),
'author_tag' => !isset($settings['comment_author_tag']) || !empty($settings['comment_author_tag']),
+2 -2
View File
@@ -573,7 +573,7 @@ class Settings {
/**
* Private user-center display settings.
*
* @return array{tabs:string[],avatar_upload:bool,cover_upload:bool,social_links:bool}
* @return array{tabs:string[]}
*/
public static function get_user_center_settings(): array {
return UserRuntime::get_user_center_settings();
@@ -705,7 +705,7 @@ class Settings {
/**
* Comment display and behavior settings.
*
* @return array{order:string,pagination_mode:string,nested_depth:int,require_login:bool,allow_reply:bool,avatar_size:int,empty_text:string}
* @return array{order:string,pagination_mode:string,nested_depth:int,require_login:bool,avatar_size:int,empty_text:string}
*/
public static function get_comment_display_settings(): array {
return PostDisplay::get_comment_display_settings();
+1 -4
View File
@@ -80,7 +80,7 @@ class UserRuntime {
/**
* Private user-center display settings.
*
* @return array{tabs:string[],avatar_upload:bool,cover_upload:bool,social_links:bool}
* @return array{tabs:string[]}
*/
public static function get_user_center_settings(): array {
$settings = Settings::get_all();
@@ -133,9 +133,6 @@ class UserRuntime {
return [
'tabs' => $normalized_tabs,
'avatar_upload' => !isset($settings['allow_avatar_upload']) || !empty($settings['allow_avatar_upload']),
'cover_upload' => !isset($settings['user_cover_upload']) || !empty($settings['user_cover_upload']),
'social_links' => !isset($settings['user_social_links']) || !empty($settings['user_social_links']),
];
}
-3
View File
@@ -409,9 +409,6 @@ class Badges {
if (user_can($actor, 'manage_options')) {
return true;
}
if (class_exists('\AKSRT\Settings\Capabilities')) {
return \AKSRT\Settings\Capabilities::user_can('medal_manually_set', $actor);
}
return false;
}
+5 -8
View File
@@ -113,14 +113,11 @@ class Center {
* 注册默认面板
*/
private function register_default_panels(): void {
$settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_user_center_settings()
: [
'tabs' => ['profile', 'comments', 'favorites'],
'avatar_upload' => true,
'cover_upload' => true,
'social_links' => true,
];
$settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_user_center_settings()
: [
'tabs' => ['profile', 'comments', 'favorites'],
];
$definitions = [
'profile' => [
-8
View File
@@ -56,14 +56,6 @@ $need_vip = $gate_code === 'aksrt_need_vip';
$priced = function_exists('aksrt_get_user_pay_price') ? aksrt_get_user_pay_price($post) : null;
$price = is_array($priced) ? (float) ($priced['amount'] ?? 0) : (float) ($pay_meta['price'] ?? 0);
$pay_modo = is_array($priced) ? ((($priced['modo'] ?? '0') === 'points') ? 'points' : '0') : ((($pay_meta['pay_modo'] ?? '0') === 'points') ? 'points' : '0');
$order_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_order_settings() : [];
$free_logged_show = !empty($order_settings['free_logged_show']);
if ($price <= 0 && $free_logged_show && !is_user_logged_in()) {
$can_dl = false;
$need_login = true;
$has_access = true; // free "access" but must login to download
}
$currency = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
$cash_symbol = '¥';
if (class_exists('\AKSRT\Settings\Settings')) {
+1 -2
View File
@@ -60,8 +60,7 @@ if ($is_logged_in) {
}
$currency_name = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_currency_name() : __('积分', 'aksrt');
$paid_settings = class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_paid_content_settings() : [];
$dl_requires_login = !empty($paid_settings['download_login']);
$dl_requires_login = function_exists('aksrt_user_can') && !aksrt_user_can('resource_download', get_current_user_id());
?>
<main id="main-content" class="site-main container" tabindex="-1">
+13 -3
View File
@@ -68,9 +68,9 @@ if (file_exists($js)) {
true
);
}
if (!empty($ctx['can_upload_img']) || !empty($ctx['can_cover_image']) || !empty($ctx['can_cover_slide'])) {
wp_enqueue_media();
}
$attachment_settings = class_exists('\\AKSRT\\Settings\\Settings') ? \AKSRT\Settings\Settings::get_all() : [];
$submit_image_max_size = max(1, min(20, (int) ($attachment_settings['submit_image_max_size'] ?? 5))) * MB_IN_BYTES;
$resource_max_size = max(1, min(200, (int) ($attachment_settings['resource_max_size'] ?? 50))) * MB_IN_BYTES;
wp_localize_script('aksrt-front-submit', 'aksrtSubmitData', [
'ajaxUrl' => admin_url('admin-ajax.php'),
@@ -83,6 +83,16 @@ if (file_exists($js)) {
'summary' => !empty($ctx['ai_available']),
'tags' => !empty($ctx['ai_available']),
],
'uploads' => [
'image' => !empty($ctx['can_upload_img']) ? [
'url' => esc_url_raw(rest_url('aksrt/v1/front-submit/upload/image')),
'maxSize' => $submit_image_max_size,
] : null,
'resource' => !empty($ctx['can_upload_file']) ? [
'url' => esc_url_raw(rest_url('aksrt/v1/front-submit/upload/resource')),
'maxSize' => $resource_max_size,
] : null,
],
'i18n' => [
'confirmLeave' => __('你有未保存的更改,确定要离开吗?', 'aksrt'),
'saving' => __('保存中...', 'aksrt'),
+2 -11
View File
@@ -43,7 +43,7 @@ $gate_data = $gate_blocked ? (array) $gate->get_error_data() : [];
$paid_settings = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_paid_content_settings()
: ['download_credit' => true, 'download_captcha' => false];
: ['download_credit' => true];
$access = class_exists('\AKSRT\Content\Visibility')
? \AKSRT\Content\Visibility::get_post_access_state($post_id)
@@ -56,10 +56,7 @@ $pay_modo = is_array($priced)
$user_amount = is_array($priced) ? (float) ($priced['amount'] ?? 0) : (float) ($meta['price'] ?? 0);
$cost = function_exists('aksrt_resource_download_cost') ? (int) aksrt_resource_download_cost($post_id) : 0;
$captcha_active = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']);
if ($captcha_active && function_exists('aksrt_is_captcha_enabled_for') && !aksrt_is_captcha_enabled_for('download')) {
$captcha_active = false;
}
$captcha_active = function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download');
$balance = (int) ($gate_data['balance'] ?? (is_user_logged_in() && class_exists('\AKSRT\Credits\Credits') ? \AKSRT\Credits\Credits::balance(get_current_user_id()) : 0));
$login_url = (string) ($gate_data['login_url'] ?? wp_login_url(get_permalink($post_id)));
$vip_url = function_exists('aksrt_get_user_center_tab_url')
@@ -84,13 +81,7 @@ if ($is_need_login && $allow_guest_buy) {
$gate_blocked = true;
}
// Zibll pay_free_logged_show: free downloads still require login.
$free_logged_show = !empty($paid_settings['free_logged_show']);
$price_amount = (float) ($price_label['amount'] ?? $user_amount);
if ($price_amount <= 0 && $free_logged_show && !is_user_logged_in()) {
$is_need_login = true;
$gate_blocked = true;
}
// Zibll down_alone_page: single entry to independent download page.
$alone_page = !empty($paid_settings['down_alone_page']) || !empty($paid_settings['download']['alone_page']);
+2 -2
View File
@@ -213,9 +213,9 @@ return [
'can_cover_image' => $cap('new_post_image_cover'),
'can_cover_slide' => $cap('new_post_slide_cover'),
'can_cover_video' => $cap('new_post_video_cover'),
'can_upload_img' => $cap('new_post_upload_img'),
'can_upload_img' => $cap('upload_submit_image'),
'can_upload_video' => $cap('new_post_upload_video'),
'can_upload_file' => $cap('new_post_upload_file'),
'can_upload_file' => $cap('upload_resource_attachment'),
'can_hide' => $cap('new_post_hide'),
'can_iframe' => $cap('new_post_iframe_video'),
];
+7 -5
View File
@@ -44,14 +44,11 @@ $is_can_new = !empty($ctx['is_can_new']);
<?php
$toolbar1 = 'formatselect,bold,italic,bullist,numlist,alignleft,aligncenter,alignright,link,blockquote,code,fullscreen';
$plugins = 'paste,link,lists,fullscreen,wordpress,wplink,charmap';
if (!empty($ctx['can_upload_img']) || !empty($ctx['can_upload_video']) || !empty($ctx['can_upload_file'])) {
// media_buttons handle uploads; keep toolbar clean.
}
$editor_settings = [
'textarea_name' => 'post_content',
'textarea_rows' => 20,
'editor_height' => wp_is_mobile() ? 460 : 470,
'media_buttons' => !empty($ctx['can_upload_img']) || !empty($ctx['can_upload_video']) || !empty($ctx['can_upload_file']),
'media_buttons' => false,
'default_editor' => 'tinymce',
'quicktags' => false,
'teeny' => false,
@@ -66,8 +63,13 @@ $is_can_new = !empty($ctx['is_can_new']);
?>
</div>
<?php if (!empty($ctx['can_hide']) || !empty($ctx['can_pay'])) : ?>
<?php if (!empty($ctx['can_hide']) || !empty($ctx['can_pay']) || !empty($ctx['can_upload_img'])) : ?>
<div class="aksrt-submit-shortcode-tools">
<?php if (!empty($ctx['can_upload_img'])) : ?>
<button type="button" class="but" data-insert-upload-image>
<i class="fas fa-image"></i> <?php esc_html_e('上传图片', 'aksrt'); ?>
</button>
<?php endif; ?>
<?php if (!empty($ctx['can_hide'])) : ?>
<button type="button" class="but" data-insert-shortcode="hide" title="<?php esc_attr_e('插入登录后可见', 'aksrt'); ?>">
<i class="fas fa-eye-slash"></i> <?php esc_html_e('登录可见', 'aksrt'); ?>
+14 -7
View File
@@ -117,9 +117,16 @@ $downloads = is_array($pay['downloads'] ?? null) ? $pay['downloads'] : [];
<div class="aksrt-submit-pay-downloads" data-pay-downloads>
<div class="aksrt-submit-pay__row">
<strong><?php esc_html_e('付费下载', 'aksrt'); ?></strong>
<button type="button" class="but hollow c-blue" data-pay-download-add>
<i class="fas fa-plus"></i> <?php esc_html_e('添加资源', 'aksrt'); ?>
</button>
<span>
<button type="button" class="but hollow c-blue" data-pay-download-add>
<i class="fas fa-plus"></i> <?php esc_html_e('添加资源', 'aksrt'); ?>
</button>
<?php if (!empty($ctx['can_upload_file'])) : ?>
<button type="button" class="but hollow" data-pay-download-upload>
<i class="fas fa-upload"></i> <?php esc_html_e('上传附件', 'aksrt'); ?>
</button>
<?php endif; ?>
</span>
</div>
<div class="aksrt-submit-pay-downloads__list" data-pay-download-list>
<?php foreach ($downloads as $i => $row) :
@@ -128,8 +135,8 @@ $downloads = is_array($pay['downloads'] ?? null) ? $pay['downloads'] : [];
$code = (string) ($row['extract_code'] ?? '');
?>
<div class="aksrt-submit-pay-downloads__item" data-pay-download-item>
<input type="url" class="aksrt-form-input" name="aksrt_pay[downloads][<?php echo (int) $i; ?>][url]" value="<?php echo esc_attr($url); ?>" placeholder="<?php esc_attr_e('资源链接', 'aksrt'); ?>">
<input type="text" class="aksrt-form-input" name="aksrt_pay[downloads][<?php echo (int) $i; ?>][label]" value="<?php echo esc_attr($label); ?>" placeholder="<?php esc_attr_e('名称', 'aksrt'); ?>">
<input type="url" class="aksrt-form-input" data-pay-download-url name="aksrt_pay[downloads][<?php echo (int) $i; ?>][url]" value="<?php echo esc_attr($url); ?>" placeholder="<?php esc_attr_e('资源链接', 'aksrt'); ?>">
<input type="text" class="aksrt-form-input" data-pay-download-label name="aksrt_pay[downloads][<?php echo (int) $i; ?>][label]" value="<?php echo esc_attr($label); ?>" placeholder="<?php esc_attr_e('名称', 'aksrt'); ?>">
<input type="text" class="aksrt-form-input" name="aksrt_pay[downloads][<?php echo (int) $i; ?>][extract_code]" value="<?php echo esc_attr($code); ?>" placeholder="<?php esc_attr_e('提取码/备注', 'aksrt'); ?>">
<button type="button" class="but c-red cir" data-pay-download-remove title="<?php esc_attr_e('删除', 'aksrt'); ?>"><i class="fas fa-trash"></i></button>
</div>
@@ -137,8 +144,8 @@ $downloads = is_array($pay['downloads'] ?? null) ? $pay['downloads'] : [];
</div>
<template data-pay-download-tpl>
<div class="aksrt-submit-pay-downloads__item" data-pay-download-item>
<input type="url" class="aksrt-form-input" data-name="url" placeholder="<?php esc_attr_e('资源链接', 'aksrt'); ?>">
<input type="text" class="aksrt-form-input" data-name="label" placeholder="<?php esc_attr_e('名称', 'aksrt'); ?>">
<input type="url" class="aksrt-form-input" data-name="url" data-pay-download-url placeholder="<?php esc_attr_e('资源链接', 'aksrt'); ?>">
<input type="text" class="aksrt-form-input" data-name="label" data-pay-download-label placeholder="<?php esc_attr_e('名称', 'aksrt'); ?>">
<input type="text" class="aksrt-form-input" data-name="extract_code" placeholder="<?php esc_attr_e('提取码/备注', 'aksrt'); ?>">
<button type="button" class="but c-red cir" data-pay-download-remove title="<?php esc_attr_e('删除', 'aksrt'); ?>"><i class="fas fa-trash"></i></button>
</div>
+33 -22
View File
@@ -35,11 +35,12 @@ $show_follow = !array_key_exists('follow', $settings) || !empty($settings['follo
$show_avatar = !array_key_exists('avatar', $settings) || !empty($settings['avatar']);
$show_name = !array_key_exists('name', $settings) || !empty($settings['name']);
$show_desc = !array_key_exists('desc', $settings) || !empty($settings['desc']);
$show_social = !array_key_exists('social', $settings) || !empty($settings['social']);
$show_message = !array_key_exists('message', $settings) || !empty($settings['message']);
$contacts = $dashboard['user_contacts'] ?? [];
$social_on = !empty($dashboard['social_links_on']);
$profile_details = is_array($dashboard['profile_details'] ?? null) ? $dashboard['profile_details'] : [];
// Gender is rendered inside the dialog but never creates a standalone menu
// action: without visible social details there is no "更多资料" item.
$has_more_details = !empty($profile_details['website']) || !empty($profile_details['qq']) || !empty($profile_details['wechat']) || !empty($profile_details['github']);
$signals = $dashboard['profile_signals'] ?? [];
$notice = $dashboard['profile_notice'] ?? null;
$show_report = !empty($dashboard['show_report']);
@@ -73,6 +74,18 @@ $pm_url = ($pm_enabled && $messages_url !== '')
</span>
</div>
<?php endif; ?>
<?php if ($has_more_details || ($show_report && !$is_own)) : ?>
<div class="aksrt-cover-menu aksrt-cover-menu--author" data-aksrt-menu>
<button type="button" class="aksrt-cover-menu__trigger" aria-haspopup="true" aria-expanded="false" aria-label="<?php esc_attr_e('个人主页操作', 'aksrt'); ?>"><i class="fas fa-ellipsis-h" aria-hidden="true"></i></button>
<div class="aksrt-cover-menu__panel" role="menu" hidden>
<?php if ($has_more_details) : ?><button type="button" role="menuitem" data-aksrt-profile-details><i class="fas fa-address-card" aria-hidden="true"></i><?php esc_html_e('更多资料', 'aksrt'); ?></button><?php endif; ?>
<?php if ($show_report && !$is_own) : ?>
<?php if (is_user_logged_in()) : ?><button type="button" role="menuitem" data-aksrt-report='{"target_type":"user","target_id":<?php echo (int) $user_id; ?>}'><i class="far fa-flag" aria-hidden="true"></i><?php esc_html_e('举报用户', 'aksrt'); ?></button>
<?php else : ?><a role="menuitem" href="<?php echo esc_url(aksrt_get_login_url($home_url)); ?>"><i class="far fa-flag" aria-hidden="true"></i><?php esc_html_e('举报用户', 'aksrt'); ?></a><?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
@@ -125,13 +138,6 @@ $pm_url = ($pm_enabled && $messages_url !== '')
</div>
<?php endif; ?>
<?php if ($show_social && !empty($contacts) && $social_on) : ?>
<div class="aksrt-author-hero__contacts">
<?php if (function_exists('aksrt_render_social_links')) : ?>
<?php aksrt_render_social_links($contacts); ?>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<div class="aksrt-author-hero__actions">
@@ -176,19 +182,24 @@ $pm_url = ($pm_enabled && $messages_url !== '')
<?php endif; ?>
<?php endif; ?>
<?php if ($show_report) : ?>
<?php if (is_user_logged_in()) : ?>
<button type="button" class="btn-ghost btn-sm aksrt-author-hero__report-btn" data-aksrt-report='{"target_type":"user","target_id":<?php echo (int) $user_id; ?>}' title="<?php esc_attr_e('举报该用户', 'aksrt'); ?>">
<i class="far fa-flag" aria-hidden="true"></i>
</button>
<?php else : ?>
<a href="<?php echo esc_url(aksrt_get_login_url($home_url)); ?>" class="btn-ghost btn-sm aksrt-author-hero__report-btn">
<i class="far fa-flag" aria-hidden="true"></i>
</a>
<?php endif; ?>
<?php endif; ?>
<?php if ($is_author && function_exists('aksrt_render_admin_quick_link')) { aksrt_render_admin_quick_link('page-display/user-home', __('用户主页设置', 'aksrt')); } ?>
</div>
</div>
</div>
<?php if ($has_more_details) : ?>
<div class="aksrt-profile-details-modal" id="aksrt-profile-details-modal" hidden role="dialog" aria-modal="true" aria-labelledby="aksrt-profile-details-title">
<div class="aksrt-profile-details-modal__backdrop" data-aksrt-modal-close></div>
<div class="aksrt-profile-details-modal__dialog" tabindex="-1">
<button type="button" class="aksrt-profile-details-modal__close" data-aksrt-modal-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>">&times;</button>
<h2 id="aksrt-profile-details-title"><?php esc_html_e('更多资料', 'aksrt'); ?></h2>
<dl>
<?php if (!empty($profile_details['gender'])) : ?><div><dt><?php esc_html_e('性别', 'aksrt'); ?></dt><dd><?php echo esc_html($profile_details['gender'] === 'male' ? __('男', 'aksrt') : __('女', 'aksrt')); ?></dd></div><?php endif; ?>
<?php if (!empty($profile_details['website'])) : ?><div><dt><?php esc_html_e('个人网站', 'aksrt'); ?></dt><dd><a href="<?php echo esc_url($profile_details['website']); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html($profile_details['website']); ?></a></dd></div><?php endif; ?>
<?php if (!empty($profile_details['github'])) : ?><div><dt>GitHub</dt><dd><a href="<?php echo esc_url($profile_details['github']); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html($profile_details['github']); ?></a></dd></div><?php endif; ?>
<?php if (!empty($profile_details['qq'])) : ?><div><dt>QQ</dt><dd><?php echo esc_html($profile_details['qq']); ?></dd></div><?php endif; ?>
<?php if (!empty($profile_details['wechat'])) : ?><div><dt><?php esc_html_e('微信', 'aksrt'); ?></dt><dd><?php echo esc_html($profile_details['wechat']); ?></dd></div><?php endif; ?>
</dl>
</div>
</div>
<?php endif; ?>
-1
View File
@@ -59,7 +59,6 @@ if ($options) {
// ── Delete AKSRT user meta ──
$meta_keys = [
'aksrt_user_contacts',
'aksrt_avatar_attachment_id',
'aksrt_background_attachment_id',
'aksrt_background_image',