导入 AKSRT 主题开发源码
This commit is contained in:
1 parent
9681dd3d48
commit
1268a55dd0
595 files changed
+127705
No files matched your search
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader
|
||||
title="附件治理"
|
||||
desc="统一管理全站上传行为:头像、封面、投稿图片、评论图片、背景图和资源附件。"
|
||||
>
|
||||
<template #actions>
|
||||
<n-space>
|
||||
<n-button quaternary @click="go('/permissions/download')">下载权限</n-button>
|
||||
<n-button quaternary @click="goModeration">审核策略</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<n-spin :show="loading" style="width:100%; min-height:200px;">
|
||||
<!-- Summary -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>上传策略总览</h3>
|
||||
</div>
|
||||
<n-grid :cols="3" :x-gap="12" :y-gap="12">
|
||||
<n-grid-item v-for="item in summaryItems" :key="item.key">
|
||||
<div class="aksrt-attach__summary">
|
||||
<FaIcon :icon="item.icon" :size="16" :color="item.enabled ? '#52c41a' : '#8a909c'" />
|
||||
<div>
|
||||
<strong>{{ item.label }}</strong>
|
||||
<p>{{ item.enabled ? '已启用' : '已关闭' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</n-grid-item>
|
||||
</n-grid>
|
||||
</n-card>
|
||||
|
||||
<!-- Avatar & Cover -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<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">关闭后用户只能使用 Gravatar/Cravatar 自动头像。</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-input-number v-model:value="form.avatar_max_size" :min="1" :max="10" style="max-width:160px;" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- Content uploads -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<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>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- Resource attachments -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<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>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- AI Moderation -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>审核联动</h3>
|
||||
</div>
|
||||
<n-form :model="form" label-placement="left" label-width="200" class="aksrt-form">
|
||||
<n-form-item label="背景图接入 AI 审核">
|
||||
<n-switch v-model:value="form.ai_moderate_background" />
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">命中高风险时会阻断上传,并保留上一次背景图。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="投稿图片接入 AI 审核">
|
||||
<n-switch v-model:value="form.ai_moderate_submit" />
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">投稿中插入的图片也会经过内容安全审核。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</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, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import api from '@/api'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import FaIcon from '@/components/form/FaIcon.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const loading = ref(true)
|
||||
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 },
|
||||
])
|
||||
|
||||
function go(path) {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
function goModeration() { router.push('/permissions/moderation') }
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const [permissionRes, moderationRes] = await Promise.all([
|
||||
api.getPermissionsAttachmentSettings(),
|
||||
api.getPermissionsModerationSettings(),
|
||||
])
|
||||
|
||||
const permission = permissionRes?.data ?? permissionRes ?? {}
|
||||
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
|
||||
form.ai_moderate_submit = !!moderation.ai_moderate_submit
|
||||
|
||||
moderationSnapshot.value = { ...moderation }
|
||||
} catch (error) {
|
||||
message.error(error.message || '加载附件权限失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saving.value = true
|
||||
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({
|
||||
...moderationSnapshot.value,
|
||||
ai_moderate_background: form.ai_moderate_background,
|
||||
ai_moderate_submit: form.ai_moderate_submit,
|
||||
}),
|
||||
])
|
||||
moderationSnapshot.value = {
|
||||
...moderationSnapshot.value,
|
||||
ai_moderate_background: form.ai_moderate_background,
|
||||
ai_moderate_submit: form.ai_moderate_submit,
|
||||
}
|
||||
message.success('附件治理配置已保存')
|
||||
} catch (error) {
|
||||
message.error(error.message || '保存失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-attach__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.aksrt-attach__summary strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.aksrt-attach__summary p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: #8a909c;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,150 @@
|
||||
<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 disabledScenarios = Array.isArray(captcha.disabled_scenarios) ? captcha.disabled_scenarios : []
|
||||
|
||||
form.comment_require_login = !!comment.comment_require_login
|
||||
form.comment_allow_reply = comment.comment_allow_reply !== false
|
||||
|
||||
status.commentCaptchaEnabled = !!captcha.enabled && !disabledScenarios.includes('comment')
|
||||
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>
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader
|
||||
title="下载权限"
|
||||
desc="管理资源下载所需的登录、积分与验证码门槛,并查看当前默认下载成本。"
|
||||
>
|
||||
<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">关闭后,资源价格将只作为展示信息,不会真正扣除积分。</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 与白名单策略将在 Phase 10 统一收口。
|
||||
</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>
|
||||
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader
|
||||
title="基本权限 / 能力矩阵"
|
||||
desc="统一管理用户治理、投稿和评论的角色能力矩阵。"
|
||||
>
|
||||
<template #actions>
|
||||
<n-space>
|
||||
<n-button quaternary @click="resetDefaults">恢复默认</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<n-spin :show="loading" style="width: 100%; min-height: 240px;">
|
||||
<n-card class="aksrt-section-card" v-if="summary.cap_count">
|
||||
<div class="aksrt-section-header">
|
||||
<h3>矩阵概览</h3>
|
||||
</div>
|
||||
<n-space>
|
||||
<n-tag round type="info">能力项:{{ summary.cap_count }}</n-tag>
|
||||
<n-tag round type="info">分组:{{ summary.group_count }}</n-tag>
|
||||
<n-tag round type="info">角色列:{{ summary.column_count }}</n-tag>
|
||||
</n-space>
|
||||
</n-card>
|
||||
|
||||
<n-card v-for="group in groups" :key="group.key" class="aksrt-section-card">
|
||||
<template #header>
|
||||
<div class="aksrt-cap-matrix__header">
|
||||
<div>
|
||||
<h3 class="aksrt-cap-matrix__title">{{ group.title }}</h3>
|
||||
<p class="aksrt-cap-matrix__desc">共 {{ group.items.length }} 项能力</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<n-data-table
|
||||
:columns="buildColumns(group)"
|
||||
:data="group.items"
|
||||
:bordered="true"
|
||||
size="small"
|
||||
:single-line="false"
|
||||
:scroll-x="1200"
|
||||
/>
|
||||
</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 { h, onMounted, reactive, ref } from 'vue'
|
||||
import { NInputNumber, NSwitch, NTag } from 'naive-ui'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import api from '@/api'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
|
||||
const message = useMessage()
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const groups = ref([])
|
||||
const columnsMeta = ref({})
|
||||
const summary = reactive({
|
||||
cap_count: 0,
|
||||
group_count: 0,
|
||||
column_count: 0,
|
||||
})
|
||||
const userCaps = reactive({})
|
||||
const defaultsSnapshot = ref({})
|
||||
|
||||
onMounted(load)
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await api.getCapabilities()
|
||||
const data = res?.data || {}
|
||||
|
||||
columnsMeta.value = data.columns || {}
|
||||
groups.value = data.groups || []
|
||||
Object.assign(summary, data.summary || { cap_count: 0, group_count: 0, column_count: 0 })
|
||||
|
||||
const snapshot = {}
|
||||
for (const group of groups.value) {
|
||||
for (const item of group.items || []) {
|
||||
snapshot[item.cap_id] = { ...(item.values || {}) }
|
||||
}
|
||||
}
|
||||
|
||||
replaceReactive(userCaps, snapshot)
|
||||
defaultsSnapshot.value = JSON.parse(JSON.stringify(snapshot))
|
||||
} catch (error) {
|
||||
message.error(error?.message || '加载能力矩阵失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function replaceReactive(target, source) {
|
||||
Object.keys(target).forEach((key) => delete target[key])
|
||||
Object.entries(source).forEach(([key, value]) => {
|
||||
target[key] = { ...value }
|
||||
})
|
||||
}
|
||||
|
||||
function buildColumns(group) {
|
||||
const cols = [
|
||||
{
|
||||
title: '能力项',
|
||||
key: 'label',
|
||||
fixed: 'left',
|
||||
width: 280,
|
||||
render: (row) => h('div', { class: 'aksrt-cap-matrix__cap' }, [
|
||||
h('strong', row.label),
|
||||
h('code', row.cap_id),
|
||||
]),
|
||||
},
|
||||
]
|
||||
|
||||
const visibleColumns = new Set()
|
||||
for (const item of group.items || []) {
|
||||
for (const col of item.visible_columns || []) {
|
||||
visibleColumns.add(col)
|
||||
}
|
||||
}
|
||||
|
||||
for (const col of Object.keys(columnsMeta.value || {})) {
|
||||
if (!visibleColumns.has(col)) continue
|
||||
|
||||
cols.push({
|
||||
title: columnsMeta.value[col],
|
||||
key: col,
|
||||
width: col === 'level' || col === 'vip' ? 110 : 100,
|
||||
render: (row) => renderCapCell(row, col),
|
||||
})
|
||||
}
|
||||
|
||||
return cols
|
||||
}
|
||||
|
||||
function renderCapCell(row, col) {
|
||||
const hidden = !(row.visible_columns || []).includes(col)
|
||||
if (hidden) {
|
||||
return h('span', { class: 'aksrt-cap-matrix__empty' }, '—')
|
||||
}
|
||||
|
||||
if (!userCaps[row.cap_id]) {
|
||||
userCaps[row.cap_id] = { ...(row.values || {}) }
|
||||
}
|
||||
|
||||
if (col === 'level' || col === 'vip') {
|
||||
return h(NInputNumber, {
|
||||
value: userCaps[row.cap_id]?.[col] ?? -1,
|
||||
min: -1,
|
||||
max: col === 'level' ? 10 : 2,
|
||||
step: 1,
|
||||
size: 'small',
|
||||
style: 'width: 90px',
|
||||
onUpdateValue: (value) => updateCapValue(row.cap_id, col, value ?? -1),
|
||||
})
|
||||
}
|
||||
|
||||
return h(NSwitch, {
|
||||
value: !!userCaps[row.cap_id]?.[col],
|
||||
size: 'small',
|
||||
onUpdateValue: (value) => updateCapValue(row.cap_id, col, value),
|
||||
})
|
||||
}
|
||||
|
||||
function updateCapValue(capId, col, value) {
|
||||
if (!userCaps[capId]) {
|
||||
userCaps[capId] = {}
|
||||
}
|
||||
userCaps[capId][col] = value
|
||||
}
|
||||
|
||||
function resetDefaults() {
|
||||
replaceReactive(userCaps, JSON.parse(JSON.stringify(defaultsSnapshot.value || {})))
|
||||
message.info('已恢复默认矩阵')
|
||||
}
|
||||
|
||||
function expandAll() {
|
||||
message.info('当前矩阵为平铺展示,无需展开')
|
||||
}
|
||||
|
||||
function collapseAll() {
|
||||
message.info('当前矩阵为平铺展示,无需折叠')
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
saving.value = true
|
||||
try {
|
||||
await api.saveCapabilities(JSON.parse(JSON.stringify(userCaps)))
|
||||
defaultsSnapshot.value = JSON.parse(JSON.stringify(userCaps))
|
||||
message.success('能力矩阵已保存')
|
||||
} catch (error) {
|
||||
message.error(error?.message || '保存能力矩阵失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-cap-matrix__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.aksrt-cap-matrix__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.aksrt-cap-matrix__desc {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: #8a909c;
|
||||
}
|
||||
|
||||
.aksrt-cap-matrix__cap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.aksrt-cap-matrix__cap code {
|
||||
font-size: 11px;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.aksrt-cap-matrix__empty {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.aksrt-form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user