feat(users): govern profiles and access

Normalize fixed profile fields, profile actions, and the five-column capability matrix for comments, uploads, and downloads.
This commit is contained in:
2026-08-13 00:24:54 +08:00
parent 0b06b2c7df
commit 8241570ca2
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归属地」启用并勾选对应展示位置' },
{