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:
@@ -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',
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 白名单与收银台策略见「支付 & 付费」相关页面。
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
@@ -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归属地」启用并勾选对应展示位置' },
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
@@ -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, '"') + '" 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]');
|
||||
|
||||
@@ -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 || '删除') + '">×</button>';
|
||||
list.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var btn = document.getElementById('profile-save-btn');
|
||||
|
||||
Reference in New Issue
Block a user