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:
kate522 committed 2026-08-13 00:24:54 +08:00
1 parent 0b06b2c7df
commit 8241570ca2
67 files changed
+780 -1127

No files matched your search

@@ -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>