feat: remove forum feature (Phase 1)

This commit is contained in:
2026-08-02 20:02:44 +08:00
parent f3e4cdd9cf
commit 2bbd7c01a3
173 changed files with 51 additions and 27360 deletions
+1 -2
View File
@@ -94,8 +94,7 @@ class ApiClient {
async request(endpointOrConfig, options = {}) {
// Dual-signature support:
// request('/forum/overview')
// request({ url: '/aksrt/v1/forum/topics', method: 'GET', params: { limit: 20 } })
// request('/users/1/restrictions')
// request({ url: '/aksrt/v1/users/1/restrictions', method: 'GET' })
let endpoint, method, headers, params, body, rest
@@ -1,111 +0,0 @@
import { renderFaIcon, schemaPage } from './helpers'
// Forum SPA = config only. Ops moved to native CPT menus (Phase 4).
export const forumSection = {
key: 'forum',
label: '社区&论坛',
icon: renderFaIcon('fas fa-comments'),
route: '/forum',
children: [
schemaPage({
key: 'forum-settings-basic',
name: 'ForumSettingsBasic',
label: '基础设置',
route: '/forum/settings',
title: '论坛基础设置',
desc: '论坛总开关、管理员和名称定义。',
canonicalDomain: 'forum',
section: 'basic',
}),
schemaPage({
key: 'forum-settings-names',
name: 'ForumSettingsNames',
label: '名称定义',
route: '/forum/settings/names',
title: '名称定义',
desc: '自定义论坛各实体的显示名称。',
canonicalDomain: 'forum',
section: 'names',
}),
schemaPage({
key: 'forum-settings-homepage',
name: 'ForumSettingsHomepage',
label: '首页设置',
route: '/forum/settings/homepage',
title: '论坛首页设置',
desc: '首页 Tab 引擎、快速发帖入口和移动端交互配置。',
canonicalDomain: 'forum',
section: 'homepage',
}),
schemaPage({
key: 'forum-settings-board-page',
name: 'ForumSettingsBoardPage',
label: '版块页面',
route: '/forum/settings/board-page',
title: '版块页面设置',
desc: '版块页布局、Tab 栏目和移动端交互配置。',
canonicalDomain: 'forum',
section: 'board_page',
}),
schemaPage({
key: 'forum-settings-post-page',
name: 'ForumSettingsPostPage',
label: '帖子页面',
route: '/forum/settings/post-page',
title: '帖子页面设置',
desc: '面包屑、评分、高度限制和封面配置。',
canonicalDomain: 'forum',
section: 'post_page',
}),
schemaPage({
key: 'forum-settings-reply',
name: 'ForumSettingsReply',
label: '回复评论',
route: '/forum/settings/reply',
title: '回复评论设置',
desc: '回复翻页方式、工具栏功能和快捷回复配置。',
canonicalDomain: 'forum',
section: 'reply',
}),
schemaPage({
key: 'forum-settings-permissions',
name: 'ForumSettingsPermissions',
label: '权限配置',
route: '/forum/settings/permissions',
title: '论坛权限配置',
desc: '发帖限制、帖子付费和版块付费关注。',
canonicalDomain: 'forum',
section: 'permissions',
}),
schemaPage({
key: 'forum-settings-other',
name: 'ForumSettingsOther',
label: '其它设置',
route: '/forum/settings/other',
title: '论坛其它设置',
desc: '投票、版主申请说明、标题字数和 URL 别名。',
canonicalDomain: 'forum',
section: 'other',
}),
schemaPage({
key: 'forum-settings-upload',
name: 'ForumSettingsUpload',
label: '上传权限',
route: '/forum/settings/upload',
title: '上传权限设置',
desc: '图片、视频和文件上传大小与格式限制。',
canonicalDomain: 'forum',
section: 'upload',
}),
schemaPage({
key: 'forum-settings-credits',
name: 'ForumSettingsCredits',
label: '积分设置',
route: '/forum/settings/credits',
title: '论坛积分设置',
desc: '论坛行为积分奖励矩阵和每日上限。',
canonicalDomain: 'forum',
section: 'credits',
}),
],
}
@@ -4,8 +4,6 @@ import { postListSection } from './post-list'
import { permissionsSection } from './permissions'
import { usersSection } from './users'
import { paymentSection } from './payment'
import { shopSection } from './shop'
import { forumSection } from './forum'
import { extensionsSection } from './extensions'
import { themeInfoSection } from './theme-info'
import { docsSection } from './docs'
@@ -18,8 +16,6 @@ const SECTION_DEFAULT_CHILDREN = {
permissions: 'permissions-comment',
users: 'users-auth',
payment: 'payment-paid',
shop: 'shop-basic',
forum: 'forum-settings-basic',
extensions: 'extensions-ip',
tools: 'tools-import-export',
'theme-info': 'theme-info-dashboard',
@@ -47,8 +43,6 @@ export const ADMIN_SECTIONS = [
attachSectionDefaultRoute(permissionsSection),
attachSectionDefaultRoute(usersSection),
attachSectionDefaultRoute(paymentSection),
attachSectionDefaultRoute(shopSection),
attachSectionDefaultRoute(forumSection),
attachSectionDefaultRoute(extensionsSection),
attachSectionDefaultRoute(toolsSection),
attachSectionDefaultRoute(themeInfoSection),
@@ -372,7 +372,6 @@ const typeOptions = [
{ label: '主题切换', value: 'theme_mode' },
{ label: '页面二维码', value: 'qrcode' },
{ label: '发布入口', value: 'add' },
{ label: '购物车', value: 'shop_cart' },
{ label: 'QQ 客服', value: 'service_qq' },
{ label: '微信客服', value: 'service_wechat' },
{ label: '会员入口', value: 'pay_vip' },
@@ -397,7 +396,7 @@ const addPresetOptions = computed(() => {
})
// pay_vip always opens the site VIP modal on the front-end — no link required.
const needsLink = computed(() => ['custom', 'add', 'shop_cart'].includes(editingItem.type) && !editingItem.hover)
const needsLink = computed(() => ['custom', 'add'].includes(editingItem.type) && !editingItem.hover)
// Custom hover HTML only; VIP uses modal, not free-form hover content.
const supportsHover = computed(() => editingItem.type === 'custom')
const supportsActionMode = computed(() => ['service_qq', 'service_wechat'].includes(editingItem.type))
@@ -444,7 +443,6 @@ function applyTypeDefaults(type) {
service_wechat: { icon: 'fab fa-weixin', icon_dark: '', icon_light: '', text: '微信', desc: '扫码添加微信', link: '', hover: '', action_mode: 'link', qr_size: 120 },
pay_vip: { icon: 'fas fa-gem', icon_dark: '', icon_light: '', text: '会员', desc: '开通/升级会员弹窗', link: '', hover: '', action_mode: 'link', qr_size: 120 },
add: { icon: 'fas fa-plus', icon_dark: '', icon_light: '', text: '发布', desc: '发布内容', link: '', hover: '', action_mode: 'link', qr_size: 120 },
shop_cart: { icon: 'fas fa-shopping-cart', icon_dark: '', icon_light: '', text: '购物车', desc: '购物车入口', link: '', hover: '', action_mode: 'link', qr_size: 120 },
}
return map[type] || map.custom
}
@@ -186,8 +186,8 @@
</n-radio-group>
</n-form-item>
<n-form-item v-if="['link', 'msg', 'pay_vip', 'shop_cart'].includes(editingItem.type)" label="链接地址">
<n-input v-model:value="editingItem.link" :placeholder="editingItem.type === 'msg' ? '留空使用默认消息中心地址' : editingItem.type === 'pay_vip' ? '留空使用默认会员页地址' : editingItem.type === 'shop_cart' ? '留空使用默认购物车地址' : 'https:// 或 /path'" />
<n-form-item v-if="['link', 'msg', 'pay_vip'].includes(editingItem.type)" label="链接地址">
<n-input v-model:value="editingItem.link" :placeholder="editingItem.type === 'msg' ? '留空使用默认消息中心地址' : editingItem.type === 'pay_vip' ? '留空使用默认会员页地址' : 'https:// 或 /path'" />
</n-form-item>
<n-form-item v-if="editingItem.type === 'link'" label="新窗口打开">
@@ -312,7 +312,6 @@ const typeOptions = [
{ label: '搜索', value: 'search' },
{ label: '消息中心', value: 'msg' },
{ label: '开通会员', value: 'pay_vip' },
{ label: '购物车', value: 'shop_cart' },
]
const typeLabelMap = {
@@ -325,7 +324,6 @@ const typeLabelMap = {
search: '搜索',
msg: '消息中心',
pay_vip: '开通会员',
shop_cart: '购物车',
}
const typeDefaults = {
@@ -338,7 +336,6 @@ const typeDefaults = {
search: { icon: 'fas fa-search', text: '搜索' },
msg: { icon: 'fas fa-bell', text: '消息' },
pay_vip: { icon: 'fas fa-crown', text: '会员' },
shop_cart: { icon: 'fas fa-shopping-cart', text: '购物车' },
}
const enabledPreviewItems = computed(() => form.items.filter((item) => item.enabled))
@@ -360,7 +357,6 @@ function getItemSummary(item) {
if (item.type === 'auth') return '登录用户显示我的,游客显示登录'
if (item.type === 'msg') return '跳转到消息中心页面'
if (item.type === 'pay_vip') return item.link || '跳转到会员开通页'
if (item.type === 'shop_cart') return item.link || '跳转到购物车页面'
return item.link || '自定义链接按钮'
}
@@ -405,7 +401,7 @@ function applyTypeDefaults(type, force = false) {
const preset = typeDefaults[type] || typeDefaults.link
if (force || !editingItem.icon) editingItem.icon = preset.icon
if (force || !editingItem.text) editingItem.text = preset.text
if (!['link', 'msg', 'pay_vip', 'shop_cart'].includes(type)) editingItem.link = ''
if (!['link', 'msg', 'pay_vip'].includes(type)) editingItem.link = ''
if (type !== 'category') editingItem.category_id = null
if (type !== 'search') editingItem.search_default_type = 'post'
if (type !== 'add') editingItem.add_items = []
@@ -483,7 +479,7 @@ function saveItem() {
type: editingItem.type,
icon: editingItem.icon,
text: editingItem.text,
link: ['link', 'msg', 'pay_vip', 'shop_cart'].includes(editingItem.type) ? editingItem.link : '',
link: ['link', 'msg', 'pay_vip'].includes(editingItem.type) ? editingItem.link : '',
badge: (editingItem.badge || '').trim(),
badge_mode: editingItem.badge_mode,
category_id: editingItem.type === 'category' ? (editingItem.category_id || 0) : 0,
@@ -1,195 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="版块管理" desc="创建、编辑和维护论坛版块信息,并观察版主申请积压情况。" />
<n-card size="small" class="aksrt-forum-boards__section">
<n-space justify="space-between" align="center">
<div class="aksrt-forum-boards__summary">共 {{ boards.length }} 个版块</div>
<n-button type="primary" @click="openCreate">新增版块</n-button>
</n-space>
</n-card>
<n-card size="small" class="aksrt-forum-boards__section">
<n-data-table
:columns="columns"
:data="boards"
:loading="loading"
:bordered="false"
size="small"
:row-key="(row) => row.id"
/>
</n-card>
<n-modal :show="modalVisible" preset="card" style="width: 520px" :title="editingId ? '编辑版块' : '新增版块'" @update:show="modalVisible = $event">
<n-form ref="formRef" :model="form" :rules="rules" label-placement="top">
<n-form-item label="版块名称" path="name">
<n-input v-model:value="form.name" placeholder="输入版块名称" />
</n-form-item>
<n-form-item label="版块描述" path="description">
<n-input v-model:value="form.description" type="textarea" :rows="3" placeholder="描述主题方向和讨论边界" />
</n-form-item>
<n-form-item label="图标" path="icon">
<n-input v-model:value="form.icon" placeholder="例如:fas fa-comments" />
</n-form-item>
<n-form-item label="排序" path="sort_order">
<n-input-number v-model:value="form.sort_order" :min="0" style="width: 100%" />
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="modalVisible = false">取消</n-button>
<n-button type="primary" :loading="saving" @click="handleSave">保存</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script setup>
import { computed, h, onMounted, reactive, ref } from 'vue'
import { NButton, NSpace, NTag, useDialog, useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api/index.js'
const message = useMessage()
const dialog = useDialog()
const loading = ref(false)
const saving = ref(false)
const boards = ref([])
const modalVisible = ref(false)
const editingId = ref(null)
const formRef = ref(null)
const form = reactive({
name: '',
description: '',
icon: 'fas fa-comments',
sort_order: 0,
})
const rules = {
name: { required: true, message: '请输入版块名称', trigger: 'blur' },
}
const columns = computed(() => [
{ title: '排序', key: 'sort_order', width: 70 },
{ title: '版块名称', key: 'name', minWidth: 180 },
{ title: '描述', key: 'description', ellipsis: { tooltip: true } },
{
title: '治理状态',
key: 'governance',
width: 180,
render: (row) => h(NSpace, { size: 'small' }, () => [
h(NTag, { size: 'small', type: 'info' }, () => `${row.post_count || 0} 主题`),
h(NTag, { size: 'small', type: 'success' }, () => `${row.moderator_count || 0} 版主`),
h(NTag, { size: 'small', type: (row.pending_application_count || 0) > 0 ? 'warning' : 'default' }, () => `${row.pending_application_count || 0} 申请`),
]),
},
{
title: '操作',
key: 'actions',
width: 150,
render: (row) => h(NSpace, { size: 'small' }, () => [
h(NButton, { size: 'tiny', quaternary: true, type: 'info', onClick: () => openEdit(row) }, () => '编辑'),
h(NButton, { size: 'tiny', quaternary: true, type: 'error', onClick: () => handleDelete(row) }, () => '删除'),
]),
},
])
function resetForm() {
form.name = ''
form.description = ''
form.icon = 'fas fa-comments'
form.sort_order = boards.value.length
}
async function loadBoards() {
loading.value = true
try {
const res = await api.request('/forum/boards')
boards.value = res?.data?.items || []
} catch (error) {
message.error(error?.message || '加载版块列表失败')
} finally {
loading.value = false
}
}
function openCreate() {
editingId.value = null
resetForm()
modalVisible.value = true
}
function openEdit(row) {
editingId.value = row.id
form.name = row.name || ''
form.description = row.description || ''
form.icon = row.icon || 'fas fa-comments'
form.sort_order = row.sort_order || 0
modalVisible.value = true
}
async function handleSave() {
try {
await formRef.value?.validate()
} catch {
return
}
saving.value = true
try {
if (editingId.value) {
await api.request(`/forum/boards/${editingId.value}`, {
method: 'POST',
data: { ...form },
})
message.success('版块已更新')
} else {
await api.request('/forum/boards', {
method: 'POST',
data: { ...form },
})
message.success('版块已创建')
}
modalVisible.value = false
await loadBoards()
} catch (error) {
message.error(error?.message || '版块保存失败')
} finally {
saving.value = false
}
}
function handleDelete(row) {
dialog.warning({
title: '确认删除版块',
content: `确定删除“${row.name}”吗?如果仍有主题,请先完成迁移。`,
positiveText: '删除',
negativeText: '取消',
onPositiveClick: async () => {
try {
await api.request(`/forum/boards/${row.id}`, { method: 'DELETE' })
message.success('版块已删除')
await loadBoards()
} catch (error) {
message.error(error?.message || '版块删除失败')
}
},
})
}
onMounted(loadBoards)
</script>
<style scoped>
.aksrt-forum-boards__section {
margin-bottom: 16px;
}
.aksrt-forum-boards__summary {
color: #6b7280;
font-size: 13px;
}
</style>
@@ -1,223 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="论坛概览" desc="从待审核主题、版主申请和版块状态三个视角观察论坛治理负载。" />
<div class="aksrt-forum-dashboard__stats">
<div v-for="card in statCards" :key="card.key" class="aksrt-forum-dashboard__stat-card">
<div class="aksrt-forum-dashboard__stat-icon" :style="{ background: card.color }">
<i :class="card.icon"></i>
</div>
<div class="aksrt-forum-dashboard__stat-info">
<div class="aksrt-forum-dashboard__stat-value">{{ stats[card.key] || 0 }}</div>
<div class="aksrt-forum-dashboard__stat-label">{{ card.label }}</div>
</div>
</div>
</div>
<div class="aksrt-forum-dashboard__grid">
<n-card size="small" class="aksrt-forum-dashboard__section">
<template #header>
<div class="aksrt-forum-dashboard__section-head">
<strong>待审核主题</strong>
<n-button size="small" quaternary type="primary" @click="goTopics">进入主题治理</n-button>
</div>
</template>
<n-data-table
v-if="pendingTopics.length"
:columns="topicColumns"
:data="pendingTopics"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无待审核主题" />
</n-card>
<n-card size="small" class="aksrt-forum-dashboard__section">
<template #header>
<div class="aksrt-forum-dashboard__section-head">
<strong>待处理版主申请</strong>
<n-button size="small" quaternary type="primary" @click="goApplications">进入申请处理</n-button>
</div>
</template>
<n-data-table
v-if="pendingApplications.length"
:columns="applicationColumns"
:data="pendingApplications"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无待处理申请" />
</n-card>
</div>
<n-card size="small" class="aksrt-forum-dashboard__section">
<template #header>
<div class="aksrt-forum-dashboard__section-head">
<strong>版块概况</strong>
<n-button size="small" quaternary type="primary" @click="goBoards">进入版块管理</n-button>
</div>
</template>
<n-data-table
v-if="boards.length"
:columns="boardColumns"
:data="boards"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无版块" />
</n-card>
</div>
</template>
<script setup>
import { h, onMounted, ref } from 'vue'
import { NButton, NTag, useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api/index.js'
const message = useMessage()
const stats = ref({})
const boards = ref([])
const pendingTopics = ref([])
const pendingApplications = ref([])
const statCards = [
{ key: 'board_count', label: '版块数', icon: 'fas fa-th-large', color: '#dbeafe' },
{ key: 'topic_count', label: '公开主题', icon: 'fas fa-list-alt', color: '#dcfce7' },
{ key: 'pending_topic_count', label: '待审核主题', icon: 'fas fa-hourglass-half', color: '#fef3c7' },
{ key: 'pending_application_count', label: '待处理申请', icon: 'fas fa-user-shield', color: '#fee2e2' },
]
const topicColumns = [
{ title: '标题', key: 'title', minWidth: 260 },
{ title: '作者', key: 'author_name', width: 110, render: (row) => row.author?.display_name || '-' },
{ title: '版块', key: 'board_name', width: 120, render: (row) => row.boards?.[0]?.name || '-' },
{ title: '时间', key: 'date', width: 160 },
{
title: '操作',
key: 'actions',
width: 100,
render: (row) => h(NButton, { size: 'small', quaternary: true, type: 'info', onClick: () => goTopics() }, () => '治理'),
},
]
const applicationColumns = [
{ title: '版块', key: 'board_name', width: 140 },
{ title: '申请人', key: 'display_name', width: 140 },
{ title: '理由', key: 'reason', ellipsis: { tooltip: true } },
{ title: '时间', key: 'created_at', width: 160 },
{
title: '状态',
key: 'status',
width: 100,
render: (row) => h(NTag, { size: 'small', type: 'warning' }, () => row.status_label || row.status || '-'),
},
]
const boardColumns = [
{ title: '版块名称', key: 'name', minWidth: 180 },
{ title: '主题数', key: 'post_count', width: 90 },
{ title: '版主数', key: 'moderator_count', width: 90 },
{ title: '待处理申请', key: 'pending_application_count', width: 120 },
]
async function loadOverview() {
try {
const res = await api.request('/forum/overview')
const data = res?.data || {}
stats.value = data.stats || {}
boards.value = data.boards || []
pendingTopics.value = data.pending_topics || []
pendingApplications.value = data.pending_applications || []
} catch (error) {
message.error(error?.message || '加载论坛概览失败')
}
}
function goBoards() {
window.location.hash = '#/forum/boards'
}
function goTopics() {
window.location.hash = '#/forum/topics'
}
function goApplications() {
window.location.hash = '#/forum/moderator-applications'
}
onMounted(loadOverview)
</script>
<style scoped>
.aksrt-forum-dashboard__stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.aksrt-forum-dashboard__stat-card {
display: flex;
align-items: center;
gap: 16px;
padding: 18px;
border-radius: 12px;
background: #fff;
border: 1px solid #eef2f7;
}
.aksrt-forum-dashboard__stat-icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
color: #1d4ed8;
font-size: 18px;
}
.aksrt-forum-dashboard__stat-info {
display: flex;
flex-direction: column;
}
.aksrt-forum-dashboard__stat-value {
color: #111827;
font-size: 24px;
font-weight: 700;
}
.aksrt-forum-dashboard__stat-label {
color: #6b7280;
font-size: 13px;
}
.aksrt-forum-dashboard__grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 16px;
}
.aksrt-forum-dashboard__section {
margin-bottom: 16px;
}
.aksrt-forum-dashboard__section-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
@media (max-width: 1023px) {
.aksrt-forum-dashboard__grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,201 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="版主申请处理" desc="处理前台版主申请,完成审核、通过和拒绝反馈闭环。" />
<n-card size="small" class="aksrt-forum-apps__section">
<n-space wrap>
<n-input v-model:value="filters.keyword" clearable placeholder="搜索版块 / 用户 / 理由" style="width: 220px" @keyup.enter="loadData" />
<n-select v-model:value="filters.board_id" :options="boardOptions" clearable placeholder="版块" style="width: 180px" />
<n-select v-model:value="filters.status" :options="statusOptions" clearable placeholder="状态" style="width: 160px" />
<n-button type="primary" @click="loadData">查询</n-button>
<n-button @click="resetFilters">重置</n-button>
</n-space>
</n-card>
<n-card size="small" class="aksrt-forum-apps__section">
<n-data-table
:columns="columns"
:data="items"
:loading="loading"
:bordered="false"
size="small"
/>
</n-card>
<n-modal :show="reviewVisible" preset="card" title="处理版主申请" style="width: 520px" @update:show="reviewVisible = $event">
<n-space vertical :size="14">
<n-alert type="info" :show-icon="false">
<div>{{ activeApplication?.display_name || '-' }} / {{ activeApplication?.board_name || '-' }}</div>
<div class="aksrt-forum-apps__reason">{{ activeApplication?.reason || '暂无申请理由' }}</div>
</n-alert>
<n-radio-group v-model:value="reviewForm.status">
<n-radio-button value="approved">通过</n-radio-button>
<n-radio-button value="rejected">拒绝</n-radio-button>
</n-radio-group>
<n-input v-model:value="reviewForm.note" type="textarea" :rows="4" placeholder="记录审核结论和补充说明" />
</n-space>
<template #footer>
<n-space justify="end">
<n-button @click="reviewVisible = false">取消</n-button>
<n-button type="primary" :loading="reviewing" @click="submitReview">提交处理</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script setup>
import { h, onMounted, reactive, ref } from 'vue'
import { NButton, NTag, useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api/index.js'
const message = useMessage()
const loading = ref(false)
const items = ref([])
const boardOptions = ref([{ label: '全部版块', value: null }])
const statusOptions = ref([{ label: '全部状态', value: null }])
const reviewVisible = ref(false)
const reviewing = ref(false)
const activeApplication = ref(null)
const filters = reactive({
keyword: '',
board_id: null,
status: null,
})
const reviewForm = reactive({
status: 'approved',
note: '',
})
const columns = [
{
title: '版块 / 用户',
key: 'board_name',
minWidth: 220,
render: (row) => h('div', { class: 'aksrt-forum-apps__identity' }, [
h('div', { class: 'aksrt-forum-apps__board' }, row.board_name || '-'),
h('div', { class: 'aksrt-forum-apps__user' }, `${row.display_name || '-'} · ${row.user_email || '-'}`),
]),
},
{ title: '申请理由', key: 'reason', ellipsis: { tooltip: true } },
{
title: '状态',
key: 'status',
width: 110,
render: (row) => h(NTag, { size: 'small', type: resolveStatusType(row.status) }, () => row.status_label || row.status || '-'),
},
{ title: '申请时间', key: 'created_at', width: 160 },
{ title: '处理时间', key: 'reviewed_at', width: 160, render: (row) => row.reviewed_at || '-' },
{
title: '操作',
key: 'actions',
width: 160,
render: (row) => {
if (row.status !== 'pending') {
return '-'
}
return h('div', { style: 'display:flex;gap:6px;' }, [
h(NButton, { size: 'small', quaternary: true, type: 'success', onClick: () => openReview(row, 'approved') }, () => '通过'),
h(NButton, { size: 'small', quaternary: true, type: 'error', onClick: () => openReview(row, 'rejected') }, () => '拒绝'),
])
},
},
]
function resolveStatusType(status) {
if (status === 'approved') return 'success'
if (status === 'rejected') return 'error'
return 'warning'
}
async function loadData() {
loading.value = true
try {
const res = await api.request('/forum/moderator/applications', {
params: {
keyword: filters.keyword || '',
board_id: filters.board_id || '',
status: filters.status || '',
},
})
const data = res?.data || {}
items.value = data.items || []
boardOptions.value = [{ label: '全部版块', value: null }, ...((data.options?.boards || []).map((item) => ({ label: item.name, value: item.id })))]
statusOptions.value = data.options?.status || [{ label: '全部状态', value: null }]
} catch (error) {
message.error(error?.message || '加载版主申请失败')
} finally {
loading.value = false
}
}
function resetFilters() {
filters.keyword = ''
filters.board_id = null
filters.status = null
loadData()
}
function openReview(row, status) {
activeApplication.value = row
reviewForm.status = status
reviewForm.note = row.review_note || ''
reviewVisible.value = true
}
async function submitReview() {
if (!activeApplication.value?.id) {
return
}
reviewing.value = true
try {
await api.request(`/forum/moderator/applications/${activeApplication.value.id}/review`, {
method: 'POST',
data: {
status: reviewForm.status,
note: reviewForm.note,
},
})
message.success('申请状态已更新')
reviewVisible.value = false
activeApplication.value = null
await loadData()
} catch (error) {
message.error(error?.message || '申请处理失败')
} finally {
reviewing.value = false
}
}
onMounted(loadData)
</script>
<style scoped>
.aksrt-forum-apps__section {
margin-bottom: 16px;
}
.aksrt-forum-apps__identity {
display: flex;
flex-direction: column;
gap: 4px;
}
.aksrt-forum-apps__board {
color: #111827;
font-weight: 600;
}
.aksrt-forum-apps__user,
.aksrt-forum-apps__reason {
color: #6b7280;
font-size: 12px;
}
</style>
@@ -1,363 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="版主管理" desc="按版块维护版主名单,支持角色分层和权限档位。" />
<n-grid cols="1 s:2 m:4" :x-gap="16" :y-gap="16" responsive="screen" style="margin-bottom:16px;">
<n-grid-item>
<n-statistic label="总版主数" :value="stats.total_moderators" />
</n-grid-item>
<n-grid-item>
<n-statistic label="总版块数" :value="stats.total_boards" />
</n-grid-item>
<n-grid-item>
<n-statistic label="待处理申请" :value="stats.pending_applications">
<template #suffix>
<n-tag v-if="stats.pending_applications > 0" type="warning" size="small">待处理</n-tag>
</template>
</n-statistic>
</n-grid-item>
<n-grid-item>
<n-statistic label="高级版主" :value="stats.senior_moderators" />
</n-grid-item>
</n-grid>
<n-card size="small" class="aksrt-forum-mod__toolbar">
<n-space justify="space-between" align="center">
<span class="aksrt-forum-mod__summary">已加载 {{ boards.length }} 个版块</span>
<n-space>
<n-button @click="goApplications">查看版主申请</n-button>
<n-button type="primary" @click="showBatchModal = true">批量管理</n-button>
</n-space>
</n-space>
</n-card>
<n-card v-for="board in boards" :key="board.board_id" size="small" class="aksrt-forum-mod__section">
<template #header>
<div class="aksrt-forum-mod__header">
<strong>{{ board.board_name }}</strong>
<n-space size="small">
<n-tag size="small" type="info">{{ board.topic_count }} 主题</n-tag>
<n-tag size="small" :type="board.pending_application_count > 0 ? 'warning' : 'default'">
{{ board.pending_application_count }} 申请待处理
</n-tag>
</n-space>
</div>
</template>
<!-- 版主列表 -->
<div class="aksrt-forum-mod__list">
<div v-for="moderator in board.moderators" :key="moderator.id" class="aksrt-forum-mod__item">
<n-avatar :src="moderator.avatar_url" :size="36" round />
<div class="aksrt-forum-mod__info">
<div class="aksrt-forum-mod__name-row">
<span class="aksrt-forum-mod__name">{{ moderator.display_name }}</span>
<n-tag :type="getRoleTagType(moderator.role)" size="tiny">{{ getRoleLabel(moderator.role) }}</n-tag>
</div>
<span class="aksrt-forum-mod__email">{{ moderator.user_email }}</span>
<div v-if="moderator.permissions?.length" class="aksrt-forum-mod__permissions">
<n-tag v-for="perm in moderator.permissions" :key="perm" size="tiny" type="default">{{ getPermissionLabel(perm) }}</n-tag>
</div>
</div>
<n-space size="small">
<n-select
v-model:value="moderator.role"
:options="roleOptions"
size="tiny"
style="width:100px;"
@update:value="handleRoleChange(board.board_id, moderator.id, $event)"
/>
<n-button size="tiny" quaternary type="error" @click="handleRemove(board.board_id, moderator.id)">移除</n-button>
</n-space>
</div>
<n-empty v-if="board.moderators.length === 0" description="暂无版主" size="small" />
</div>
<!-- 添加版主 -->
<div class="aksrt-forum-mod__add">
<n-input
v-model:value="board._newUser"
size="small"
style="width:140px"
placeholder="用户 ID"
@keyup.enter="handleAdd(board)"
/>
<n-select
v-model:value="board._newRole"
:options="roleOptions"
size="small"
style="width:100px;"
placeholder="角色"
/>
<n-button size="small" type="primary" :disabled="!board._newUser" @click="handleAdd(board)">添加版主</n-button>
</div>
</n-card>
<n-empty v-if="boards.length === 0 && !loading" description="暂无论坛版块" />
<!-- 批量管理弹窗 -->
<n-modal v-model:show="showBatchModal" preset="card" title="批量管理版主" style="max-width:560px;" :bordered="false">
<n-form label-placement="top">
<n-form-item label="选择版块">
<n-select v-model:value="batchForm.board_id" :options="boardOptions" placeholder="选择版块" />
</n-form-item>
<n-form-item label="操作">
<n-radio-group v-model:value="batchForm.action">
<n-space>
<n-radio value="add">批量添加</n-radio>
<n-radio value="remove">批量移除</n-radio>
<n-radio value="update_role">批量修改角色</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="用户 ID 列表">
<n-input v-model:value="batchForm.user_ids" type="textarea" :rows="3" placeholder="每行一个用户 ID" />
</n-form-item>
<n-form-item v-if="batchForm.action === 'update_role'" label="目标角色">
<n-select v-model:value="batchForm.target_role" :options="roleOptions" />
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="showBatchModal = false">取消</n-button>
<n-button type="primary" :loading="batchLoading" @click="handleBatch">执行</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api/index.js'
const message = useMessage()
const loading = ref(false)
const boards = ref([])
const stats = reactive({ total_moderators: 0, total_boards: 0, pending_applications: 0, senior_moderators: 0 })
// 批量管理
const showBatchModal = ref(false)
const batchLoading = ref(false)
const batchForm = reactive({ board_id: null, action: 'add', user_ids: '', target_role: 'moderator' })
// 角色选项
const roleOptions = [
{ label: '超级版主', value: 'super_moderator' },
{ label: '高级版主', value: 'senior_moderator' },
{ label: '版主', value: 'moderator' },
{ label: '实习版主', value: 'trainee' },
]
// 权限选项
const permissionLabels = {
manage_topics: '管理主题',
manage_comments: '管理评论',
manage_users: '管理用户',
pin_topics: '置顶主题',
close_topics: '关闭主题',
move_topics: '移动主题',
delete_topics: '删除主题',
ban_users: '封禁用户',
}
const boardOptions = computed(() => boards.value.map(b => ({ label: b.board_name, value: b.board_id })))
function getRoleLabel(role) {
const opt = roleOptions.find(r => r.value === role)
return opt?.label || role || '版主'
}
function getRoleTagType(role) {
const map = { super_moderator: 'error', senior_moderator: 'warning', moderator: 'info', trainee: 'default' }
return map[role] || 'default'
}
function getPermissionLabel(perm) {
return permissionLabels[perm] || perm
}
async function loadModerators() {
loading.value = true
try {
const res = await api.request('/forum/moderators')
boards.value = (res?.data?.boards || []).map(item => ({
...item,
_newUser: '',
_newRole: 'moderator',
}))
// 计算统计
stats.total_boards = boards.value.length
stats.total_moderators = boards.value.reduce((sum, b) => sum + (b.moderators?.length || 0), 0)
stats.pending_applications = boards.value.reduce((sum, b) => sum + (b.pending_application_count || 0), 0)
stats.senior_moderators = boards.value.reduce((sum, b) =>
sum + (b.moderators?.filter(m => ['super_moderator', 'senior_moderator'].includes(m.role))?.length || 0), 0)
} catch (error) {
message.error(error?.message || '加载版主列表失败')
} finally {
loading.value = false
}
}
async function handleAdd(board) {
const userId = parseInt(board._newUser || '', 10)
if (!userId || userId <= 0) {
message.warning('请输入有效的用户 ID')
return
}
try {
await api.request('/forum/moderators', {
method: 'POST',
data: {
board_id: board.board_id,
user_id: userId,
role: board._newRole || 'moderator',
action: 'add',
},
})
message.success('版主已添加')
board._newUser = ''
board._newRole = 'moderator'
await loadModerators()
} catch (error) {
message.error(error?.message || '添加版主失败')
}
}
async function handleRemove(boardId, userId) {
try {
await api.request('/forum/moderators', {
method: 'POST',
data: { board_id: boardId, user_id: userId, action: 'remove' },
})
message.success('版主已移除')
await loadModerators()
} catch (error) {
message.error(error?.message || '移除版主失败')
}
}
async function handleRoleChange(boardId, userId, newRole) {
try {
await api.request('/forum/moderators', {
method: 'POST',
data: { board_id: boardId, user_id: userId, role: newRole, action: 'update_role' },
})
message.success('角色已更新')
await loadModerators()
} catch (error) {
message.error(error?.message || '角色更新失败')
}
}
async function handleBatch() {
const userIds = batchForm.user_ids.split('\n').map(s => parseInt(s.trim(), 10)).filter(n => n > 0)
if (!batchForm.board_id || userIds.length === 0) {
message.warning('请选择版块并输入用户 ID')
return
}
batchLoading.value = true
try {
await api.request('/forum/moderators/batch', {
method: 'POST',
data: {
board_id: batchForm.board_id,
user_ids: userIds,
action: batchForm.action,
target_role: batchForm.target_role,
},
})
message.success('批量操作完成')
showBatchModal.value = false
batchForm.user_ids = ''
await loadModerators()
} catch (error) {
message.error(error?.message || '批量操作失败')
} finally {
batchLoading.value = false
}
}
function goApplications() {
window.location.hash = '#/forum/moderator-applications'
}
onMounted(loadModerators)
</script>
<style scoped>
.aksrt-forum-mod__toolbar,
.aksrt-forum-mod__section {
margin-bottom: 16px;
}
.aksrt-forum-mod__summary {
color: var(--color-ink-3);
font-size: 13px;
}
.aksrt-forum-mod__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.aksrt-forum-mod__list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 12px;
}
.aksrt-forum-mod__item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
border-radius: 8px;
background: var(--surface-2);
border: 1px solid var(--color-line-soft);
}
.aksrt-forum-mod__info {
flex: 1;
min-width: 0;
}
.aksrt-forum-mod__name-row {
display: flex;
align-items: center;
gap: 6px;
}
.aksrt-forum-mod__name {
display: block;
color: var(--color-ink);
font-weight: 600;
}
.aksrt-forum-mod__email {
display: block;
color: var(--color-ink-3);
font-size: 12px;
}
.aksrt-forum-mod__permissions {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.aksrt-forum-mod__add {
display: flex;
gap: 8px;
align-items: center;
padding-top: 12px;
border-top: 1px solid var(--color-line-soft);
}
</style>
@@ -1,186 +0,0 @@
<template>
<div class="aksrt-page-container">
<PageHeader title="主题治理" desc="集中处理论坛主题的审核、可见性、版块移动和治理状态。" />
<n-card size="small" class="aksrt-forum-topics__section">
<n-space wrap>
<n-input v-model:value="filters.keyword" clearable placeholder="搜索标题 / 作者 / 版块" style="width: 220px" @keyup.enter="loadData(1)" />
<n-select v-model:value="filters.board_id" :options="boardOptions" clearable placeholder="版块" style="width: 180px" />
<n-select v-model:value="filters.status" :options="statusOptions" placeholder="帖子状态" style="width: 160px" />
<n-select v-model:value="filters.review_status" :options="reviewOptions" clearable placeholder="审核状态" style="width: 160px" />
<n-select v-model:value="filters.visibility" :options="visibilityOptions" clearable placeholder="可见性" style="width: 160px" />
<n-button type="primary" @click="loadData(1)">查询</n-button>
<n-button @click="resetFilters">重置</n-button>
</n-space>
</n-card>
<n-card size="small" class="aksrt-forum-topics__section">
<n-data-table
:columns="columns"
:data="items"
:loading="loading"
:bordered="false"
size="small"
:pagination="pagination"
@update:page="handlePageChange"
/>
</n-card>
<TopicModerationDrawer
v-model:show="detailVisible"
:topic-id="detailTopicId"
@updated="loadData(page)"
/>
</div>
</template>
<script setup>
import { h, onMounted, reactive, ref } from 'vue'
import { NButton, NTag, useMessage } from 'naive-ui'
import PageHeader from '@/components/PageHeader.vue'
import api from '@/api/index.js'
import TopicModerationDrawer from './components/TopicModerationDrawer.vue'
const message = useMessage()
const loading = ref(false)
const page = ref(1)
const items = ref([])
const detailVisible = ref(false)
const detailTopicId = ref(null)
const boardOptions = ref([])
const statusOptions = ref([{ label: '全部', value: 'all' }])
const reviewOptions = ref([])
const visibilityOptions = ref([])
const filters = reactive({
keyword: '',
board_id: null,
status: 'all',
review_status: null,
visibility: null,
})
const pagination = reactive({
page: 1,
pageSize: 20,
itemCount: 0,
})
const columns = [
{
title: '主题',
key: 'title',
minWidth: 280,
render: (row) => h('div', { class: 'aksrt-forum-topics__title-cell' }, [
h('div', { class: 'aksrt-forum-topics__title' }, row.title || '-'),
h('div', { class: 'aksrt-forum-topics__subtitle' }, [
row.boards?.[0]?.name || '未分配版块',
' · ',
row.author?.display_name || '未知作者',
]),
]),
},
{
title: '可见性',
key: 'visibility',
width: 110,
render: (row) => h(NTag, { size: 'small', type: row.visibility?.key === 'public' ? 'default' : 'warning' }, () => row.visibility?.label || '-'),
},
{
title: '审核',
key: 'review_status',
width: 110,
render: (row) => h(NTag, { size: 'small', type: resolveReviewType(row.review_status?.key) }, () => row.review_status?.label || '-'),
},
{ title: '回复', key: 'comment_count', width: 80 },
{ title: '浏览', key: 'views', width: 80 },
{ title: '发布时间', key: 'date', width: 160 },
{
title: '操作',
key: 'actions',
width: 110,
render: (row) => h(NButton, { size: 'small', quaternary: true, type: 'info', onClick: () => openDetail(row) }, () => '治理'),
},
]
function resolveReviewType(status) {
if (status === 'approved') return 'success'
if (status === 'rejected') return 'error'
return 'warning'
}
async function loadData(nextPage = 1) {
loading.value = true
page.value = nextPage
pagination.page = nextPage
try {
const res = await api.request('/forum/topics', {
params: {
page: nextPage,
per_page: pagination.pageSize,
keyword: filters.keyword || '',
board_id: filters.board_id || '',
status: filters.status || 'all',
review_status: filters.review_status || '',
visibility: filters.visibility || '',
},
})
const data = res?.data || {}
items.value = data.items || []
pagination.itemCount = data.total || 0
boardOptions.value = [{ label: '全部版块', value: null }, ...((data.options?.boards || []).map((item) => ({ label: item.name, value: item.id })))]
statusOptions.value = data.options?.status || [{ label: '全部', value: 'all' }]
reviewOptions.value = [{ label: '全部审核', value: null }, ...(data.options?.review || [])]
visibilityOptions.value = [{ label: '全部可见性', value: null }, ...(data.options?.visibility || [])]
} catch (error) {
message.error(error?.message || '加载主题列表失败')
} finally {
loading.value = false
}
}
function handlePageChange(nextPage) {
loadData(nextPage)
}
function resetFilters() {
filters.keyword = ''
filters.board_id = null
filters.status = 'all'
filters.review_status = null
filters.visibility = null
loadData(1)
}
function openDetail(row) {
detailTopicId.value = row.id
detailVisible.value = true
}
onMounted(() => loadData(1))
</script>
<style scoped>
.aksrt-forum-topics__section {
margin-bottom: 16px;
}
.aksrt-forum-topics__title-cell {
display: flex;
flex-direction: column;
gap: 4px;
}
.aksrt-forum-topics__title {
color: #111827;
font-weight: 600;
}
.aksrt-forum-topics__subtitle {
color: #6b7280;
font-size: 12px;
}
</style>
@@ -1,284 +0,0 @@
<template>
<n-drawer :show="show" :width="620" @update:show="$emit('update:show', $event)">
<n-drawer-content title="主题治理" closable>
<n-spin :show="loading">
<template v-if="detail.topic">
<n-descriptions bordered :column="1" size="small" label-placement="left">
<n-descriptions-item label="主题标题">{{ detail.topic.title || '-' }}</n-descriptions-item>
<n-descriptions-item label="所属版块">{{ primaryBoardName }}</n-descriptions-item>
<n-descriptions-item label="作者">{{ detail.topic.author?.display_name || '-' }}</n-descriptions-item>
<n-descriptions-item label="可见性">{{ detail.topic.visibility?.label || '-' }}</n-descriptions-item>
<n-descriptions-item label="审核状态">{{ detail.topic.review_status?.label || '-' }}</n-descriptions-item>
<n-descriptions-item label="主题状态">{{ detail.topic.status || '-' }}</n-descriptions-item>
<n-descriptions-item label="置顶">{{ stickySummary }}</n-descriptions-item>
<n-descriptions-item label="精华">{{ detail.topic.featured ? '是' : '否' }}</n-descriptions-item>
<n-descriptions-item label="帖子类型">{{ bbsTypeLabel }}</n-descriptions-item>
<n-descriptions-item v-if="detail.topic.bbs_type === 'question'" label="提问状态">{{ questionStatusLabel }}</n-descriptions-item>
<n-descriptions-item label="收藏">{{ detail.topic.favorite_count ?? 0 }}</n-descriptions-item>
<n-descriptions-item label="评分">{{ detail.topic.score ?? 0 }}</n-descriptions-item>
<n-descriptions-item label="专题">{{ detail.topic.special_topic || '-' }}</n-descriptions-item>
<n-descriptions-item label="投票">{{ voteSummary }}</n-descriptions-item>
<n-descriptions-item label="治理备注">{{ detail.topic.moderation_note || '-' }}</n-descriptions-item>
</n-descriptions>
<div class="aksrt-topic-drawer__excerpt">
<div class="aksrt-topic-drawer__section-title">内容摘要</div>
<p>{{ detail.topic.excerpt || '暂无摘要' }}</p>
</div>
<div class="aksrt-topic-drawer__actions">
<div class="aksrt-topic-drawer__section-title">执行治理动作</div>
<n-space vertical :size="12">
<n-select v-model:value="form.action" :options="actionOptions" />
<n-select
v-if="form.action === 'review'"
v-model:value="form.review_status"
:options="detail.options?.review || []"
placeholder="选择审核状态"
/>
<n-select
v-if="form.action === 'move_board'"
v-model:value="form.target_board_id"
:options="boardOptions"
placeholder="选择目标版块"
/>
<n-select
v-if="form.action === 'visibility'"
v-model:value="form.visibility"
:options="detail.options?.visibility || []"
placeholder="选择可见性"
/>
<n-select
v-if="form.action === 'sticky'"
v-model:value="form.sticky_value"
:options="stickyLevelOptions"
placeholder="选择置顶级别"
/>
<n-select
v-if="form.action === 'question_status'"
v-model:value="form.question_status_value"
:options="questionStatusOptions"
placeholder="选择提问状态"
/>
<n-select
v-if="booleanActions.includes(form.action) && form.action !== 'sticky'"
v-model:value="form.value"
:options="booleanOptions"
placeholder="选择状态"
/>
<n-input
v-model:value="form.note"
type="textarea"
:rows="3"
placeholder="记录本次处理原因,便于后续审计"
/>
<n-space>
<n-button type="primary" :loading="submitting" @click="handleSubmit">执行</n-button>
<n-button @click="loadDetail">刷新详情</n-button>
</n-space>
</n-space>
</div>
</template>
<n-empty v-else description="暂无主题详情" />
</n-spin>
</n-drawer-content>
</n-drawer>
</template>
<script setup>
import { computed, reactive, ref, watch } from 'vue'
import { useMessage } from 'naive-ui'
import api from '@/api/index.js'
const props = defineProps({
show: { type: Boolean, default: false },
topicId: { type: [Number, String], default: null },
})
const emit = defineEmits(['update:show', 'updated'])
const message = useMessage()
const loading = ref(false)
const submitting = ref(false)
const detail = ref({ topic: null, options: { boards: [], visibility: [], review: [] } })
// sticky 现在是 0~3 分级选择,不再是布尔动作(§7.3 置顶分级)。
const booleanActions = ['featured', 'locked', 'close', 'hide']
const form = reactive({
action: 'review',
review_status: 'pending',
target_board_id: null,
visibility: 'public',
value: '1',
sticky_value: 0,
question_status_value: 0,
note: '',
})
const actionOptions = [
{ label: '审核状态', value: 'review' },
{ label: '移动版块', value: 'move_board' },
{ label: '修改可见性', value: 'visibility' },
{ label: '置顶分级', value: 'sticky' },
{ label: '精华切换', value: 'featured' },
{ label: '锁定切换', value: 'locked' },
{ label: '关闭帖子', value: 'close' },
{ label: '隐藏帖子', value: 'hide' },
{ label: '提问状态', value: 'question_status' },
]
const booleanOptions = [
{ label: '启用', value: '1' },
{ label: '关闭', value: '0' },
]
// 置顶分级选项(对标 zibll topping 0/1/2/3)。
const stickyLevelOptions = [
{ label: '不置顶', value: 0 },
{ label: '版块置顶', value: 1 },
{ label: '超级置顶', value: 2 },
{ label: '全局置顶', value: 3 },
]
const questionStatusOptions = [
{ label: '未解决', value: 0 },
{ label: '已解决', value: 1 },
]
const primaryBoardName = computed(() => detail.value?.topic?.boards?.[0]?.name || '-')
const boardOptions = computed(() => (detail.value?.options?.boards || []).map((item) => ({
label: item.name,
value: item.id,
})))
// 置顶摘要:优先用后端返回的 sticky_label,否则按 sticky_level 映射。
const stickySummary = computed(() => {
const topic = detail.value?.topic
if (!topic) return '-'
const label = topic.sticky_label
if (label) return label
const level = topic.sticky_level ?? 0
const found = stickyLevelOptions.find((opt) => opt.value === level)
return found ? found.label : '不置顶'
})
const voteSummary = computed(() => {
const vote = detail.value?.topic?.vote
if (!vote?.enabled) {
return '未开启'
}
return `${vote.total_votes || 0} 票 / ${vote.multiple ? '多选' : '单选'}`
})
const bbsTypeLabel = computed(() => {
const t = detail.value?.topic?.bbs_type
return t === 'question' ? '提问' : '标准帖子'
})
const questionStatusLabel = computed(() => {
const s = detail.value?.topic?.question_status
return s === 1 ? '已解决' : '未解决'
})
watch(() => props.show, async (visible) => {
if (visible && props.topicId) {
await loadDetail()
} else if (!visible) {
detail.value = { topic: null, options: { boards: [], visibility: [], review: [] } }
resetForm()
}
})
function resetForm() {
form.action = 'review'
form.review_status = detail.value?.topic?.review_status?.key || 'pending'
form.target_board_id = detail.value?.topic?.boards?.[0]?.id || null
form.visibility = detail.value?.topic?.visibility?.key || 'public'
form.value = '1'
form.sticky_value = detail.value?.topic?.sticky_level ?? 0
form.question_status_value = detail.value?.topic?.question_status ?? 0
form.note = ''
}
async function loadDetail() {
if (!props.topicId) {
return
}
loading.value = true
try {
const res = await api.request(`/forum/topics/${props.topicId}`)
detail.value = res?.data || { topic: null, options: { boards: [], visibility: [], review: [] } }
resetForm()
} catch (error) {
message.error(error?.message || '加载主题详情失败')
} finally {
loading.value = false
}
}
async function handleSubmit() {
if (!props.topicId) {
return
}
const payload = {
action: form.action,
note: form.note,
}
if (form.action === 'review') {
payload.review_status = form.review_status
} else if (form.action === 'move_board') {
payload.target_board_id = form.target_board_id
} else if (form.action === 'visibility') {
payload.visibility = form.visibility
} else if (form.action === 'sticky') {
// 置顶分级:发送 0~3 整数。
payload.value = form.sticky_value
} else if (form.action === 'question_status') {
payload.value = form.question_status_value
} else {
payload.value = form.value === '1'
}
submitting.value = true
try {
await api.request(`/forum/topics/${props.topicId}/moderate`, {
method: 'POST',
data: payload,
})
message.success('治理动作已执行')
await loadDetail()
emit('updated')
} catch (error) {
message.error(error?.message || '治理动作执行失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.aksrt-topic-drawer__excerpt,
.aksrt-topic-drawer__actions {
margin-top: 18px;
}
.aksrt-topic-drawer__section-title {
margin-bottom: 8px;
font-weight: 600;
color: #1f2937;
}
.aksrt-topic-drawer__excerpt p {
margin: 0;
color: #4b5563;
line-height: 1.7;
}
</style>
@@ -64,11 +64,11 @@ onMounted(async () => {
loading.value = true
try {
const [gwRes, ovRes] = await Promise.all([
api.request('/shop/gateways').catch(() => null),
api.request('/payment/overview').catch(() => null),
])
if (gwRes?.data?.gateways) {
gateways.value = gwRes.data.gateways
if (gwRes?.data?.gateway?.gateways) {
const gwList = Object.entries(gwRes.data.gateway.gateways).map(([id, enabled]) => ({ id, name: id, configured: !!enabled }))
gateways.value = gwList
}
if (ovRes?.data?.gateway) {
overview.value = ovRes.data.gateway
@@ -91,7 +91,6 @@ const guides = [
{ key: 'meilisearch', title: 'Meilisearch', desc: '配置搜索索引与同步策略', url: '#/global/search' },
{ key: 'i18n', title: '多语言翻译', desc: '检查语言切换与术语配置', url: '#/global/i18n' },
{ key: 'cdn', title: '静态资源与 CDN', desc: '核对前端资源地址与缓存策略', url: '#/global/common' },
{ key: 'forum', title: '论坛体系', desc: '检查版块、话题与版主管理', url: '#/forum' },
{ key: 'ip-geo', title: 'IP 归属地', desc: '验证 IP 查询服务与密钥配置', url: '#/extensions/security' },
{ key: 'payment', title: '支付与会员', desc: '核对网关、会员与充值配置', url: '#/payment/gateway' },
{ key: 'credits', title: '积分与签到', desc: '检查积分规则、签到与邀请奖励', url: '#/users/signin' },
@@ -130,7 +130,6 @@ export default {
{ key: 'system_notice', desc: '系统公告通知' },
{ key: 'order_paid', desc: '订单支付通知' },
{ key: 'credits_change', desc: '积分变动通知' },
{ key: 'forum_reply', desc: '论坛回复通知' },
];
const allowedTypes = ref([]);
@@ -1,2 +0,0 @@
// Forum ops Vue pages removed (Phase 4). Config uses SchemaPage.
export const forumPageLoaders = {}
@@ -2,8 +2,6 @@ import { corePageLoaders } from './core'
import { usersPageLoaders } from './users'
import { paymentPageLoaders } from './payment'
import { toolsPageLoaders } from './tools'
import { shopPageLoaders } from './shop'
import { forumPageLoaders } from './forum'
import { extensionsPageLoaders } from './extensions'
export const componentLoaders = {
@@ -11,7 +9,5 @@ export const componentLoaders = {
...usersPageLoaders,
...paymentPageLoaders,
...toolsPageLoaders,
...shopPageLoaders,
...forumPageLoaders,
...extensionsPageLoaders,
}
-12
View File
@@ -76,18 +76,6 @@ function createSectionRedirectRoutes() {
* Bookmarks under #/shop/products etc. land on NativeOpsRedirect.
*/
const NATIVE_OPS_REDIRECTS = [
{ path: '/shop/products', title: '商品管理', nativeAdminPath: 'edit.php?post_type=aksrt_product' },
{ path: '/shop/orders', title: '订单管理', nativeAdminPath: 'admin.php?page=aksrt_orders' },
{ path: '/shop/after-sale', title: '售后管理', nativeAdminPath: 'admin.php?page=aksrt_after_sale' },
{ path: '/shop/shipping', title: '物流管理', nativeAdminPath: 'admin.php?page=aksrt_shipping' },
{ path: '/shop/coupons', title: '优惠券', nativeAdminPath: 'admin.php?page=aksrt_coupons' },
{ path: '/shop/withdrawals', title: '提现管理', nativeAdminPath: 'admin.php?page=aksrt_withdraw' },
{ path: '/shop/dashboard', title: '商城概览', nativeAdminPath: 'admin.php?page=aksrt_pay' },
{ path: '/forum/boards', title: '板块管理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' },
{ path: '/forum/topics', title: '主题治理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_topic' },
{ path: '/forum/moderators', title: '版主管理', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' },
{ path: '/forum/moderator-applications', title: '版主申请', nativeAdminPath: 'edit.php?post_type=aksrt_forum_plate' },
{ path: '/forum/dashboard', title: '论坛概览', nativeAdminPath: 'edit.php?post_type=aksrt_forum_topic' },
{ path: '/payment/charge-card', title: '充值卡', nativeAdminPath: 'admin.php?page=aksrt_charge_cards' },
{ path: '/payment/revenue', title: '创作分成', nativeAdminPath: 'admin.php?page=aksrt_income' },
{ path: '/payment/rebate', title: '返佣管理', nativeAdminPath: 'admin.php?page=aksrt_rebate' },
-6
View File
@@ -34,8 +34,6 @@ import { usersSchemas } from './sections/users/index.js'
import { paymentSchemas } from './sections/payment/index.js'
import { extensionsSchemas } from './sections/extensions/index.js'
import { toolsSchemas } from './sections/tools/index.js'
import { shopSchemas } from './sections/shop/index.js'
import { forumSchemas } from './sections/forum/index.js'
export function normalizeSchemaDomain(domain = '') {
const value = String(domain).trim()
@@ -55,8 +53,6 @@ export const CANONICAL_SCHEMA_DOMAINS = [
'payment',
'extensions',
'tools',
'shop',
'forum',
]
/** All schemas organized by domain */
@@ -69,8 +65,6 @@ export const ALL_SCHEMAS = Object.freeze({
payment: paymentSchemas,
extensions: extensionsSchemas,
tools: toolsSchemas,
shop: shopSchemas,
forum: forumSchemas,
})
/**
@@ -42,7 +42,6 @@ export const auditSchema = {
fields: [
{ key: 'content_audit_text_enabled', label: '文本审核', type: 'switch', default: true, dependsOn: { key: 'content_audit_enabled', value: true } },
{ key: 'content_audit_image_enabled', label: '图片审核', type: 'switch', default: false, dependsOn: { key: 'content_audit_enabled', value: true } },
{ key: 'audit_bbs_term', label: '审核论坛分类', type: 'switch', default: false, dependsOn: { key: 'content_audit_enabled', value: true } },
],
},
{
@@ -1,28 +0,0 @@
export const basicSchema = {
key: 'forum.basic',
title: '基础设置',
icon: 'fas fa-power-off',
description: '论坛总开关与管理员配置。',
defaults: {
bbs_s: true,
bbs_admin_users: [],
},
groups: [
{
key: 'switch',
title: '功能开关',
icon: 'fas fa-toggle-on',
fields: [
{ key: 'bbs_s', label: '启用论坛', type: 'switch', default: true, help: '关闭后论坛页面将返回 404' },
],
},
{
key: 'admin',
title: '论坛管理',
icon: 'fas fa-user-shield',
fields: [
{ key: 'bbs_admin_users', label: '论坛管理员', type: 'user_select', multiple: true, default: [], help: '拥有跨版块全部治理权限,绕过 ban' },
],
},
],
}
@@ -1,31 +0,0 @@
export const boardPageSchema = {
key: 'forum.board_page',
title: '版块页面',
icon: 'fas fa-th-large',
description: '版块页布局、Tab 栏目和移动端交互配置。',
defaults: {
bbs_plate_top_info_s: true,
bbs_plate_tab: [
{ label: '全部', sort: 'date' },
{ label: '推荐', sort: 'featured' },
{ label: '最新回复', sort: 'last_reply' },
{ label: '热门', sort: 'hot' },
{ label: '精华', sort: 'essence' },
],
bbs_plate_tab_active_index: 0,
bbs_plate_tab_swiper: true,
},
groups: [
{
key: 'layout',
title: '版块页布局',
icon: 'fas fa-layer-group',
fields: [
{ key: 'bbs_plate_top_info_s', label: '版块信息卡', type: 'switch', default: true, help: '版块页顶部显示版块详情卡片' },
{ key: 'bbs_plate_tab', label: '版块栏目', type: 'group', default: [{ label: '全部', sort: 'date' }, { label: '推荐', sort: 'featured' }, { label: '最新回复', sort: 'last_reply' }, { label: '热门', sort: 'hot' }, { label: '精华', sort: 'essence' }] },
{ key: 'bbs_plate_tab_active_index', label: '默认选中 Tab', type: 'number', default: 0, min: 0 },
{ key: 'bbs_plate_tab_swiper', label: '移动端滑动切换', type: 'switch', default: true },
],
},
],
}
@@ -1,37 +0,0 @@
export const creditsSchema = {
key: 'forum.credits',
title: '积分设置',
icon: 'fas fa-coins',
description: '论坛行为积分奖励矩阵和每日积分上限。',
defaults: {
bbs_credits_post_new: 5,
bbs_credits_plate_new: 10,
bbs_credits_comment_new: 2,
bbs_credits_comment_like: 1,
bbs_credits_score_extra: 3,
bbs_credits_essence: 10,
bbs_credits_posts_hot: 5,
bbs_hot_recommend_threshold: 500,
bbs_credits_adopted: 20,
bbs_credits_daily_max: 50,
},
groups: [
{
key: 'earn',
title: '积分奖励',
icon: 'fas fa-gift',
fields: [
{ key: 'bbs_credits_post_new', label: '发帖奖励', type: 'number', default: 5, min: 0 },
{ key: 'bbs_credits_plate_new', label: '建版奖励', type: 'number', default: 10, min: 0 },
{ key: 'bbs_credits_comment_new', label: '回复奖励', type: 'number', default: 2, min: 0 },
{ key: 'bbs_credits_comment_like', label: '评论被赞', type: 'number', default: 1, min: 0 },
{ key: 'bbs_credits_score_extra', label: '被加分', type: 'number', default: 3, min: 0 },
{ key: 'bbs_credits_essence', label: '被设精华', type: 'number', default: 10, min: 0 },
{ key: 'bbs_credits_posts_hot', label: '成为热帖', type: 'number', default: 5, min: 0 },
{ key: 'bbs_hot_recommend_threshold', label: '热帖推荐分阈值', type: 'number', default: 500, min: 1, help: '推荐分达到该值触发热帖奖励与勋章累计' },
{ key: 'bbs_credits_adopted', label: '被采纳', type: 'number', default: 20, min: 0 },
{ key: 'bbs_credits_daily_max', label: '每日积分上限', type: 'number', default: 50, min: 0, help: '0 = 不限' },
],
},
],
}
@@ -1,29 +0,0 @@
export const homepageSchema = {
key: 'forum.homepage',
title: '首页设置',
icon: 'fas fa-home',
description: '论坛首页 Tab 引擎、快速发帖入口和移动端交互配置。',
defaults: {
bbs_home_tab: [
{ type: 'follow', label: '关注' },
{ type: 'synthesis', label: '综合' },
{ type: 'plate', label: '版块' },
],
bbs_home_tab_active_index: 1,
bbs_home_tab_quick_posts_s: true,
bbs_home_tab_swiper: true,
},
groups: [
{
key: 'tabs',
title: '首页 Tab',
icon: 'fas fa-columns',
fields: [
{ key: 'bbs_home_tab', label: '首页栏目', type: 'group', default: [{ type: 'follow', label: '关注' }, { type: 'synthesis', label: '综合' }, { type: 'plate', label: '版块' }], help: '可拖拽排序,支持 follow / synthesis / plate / tabs 类型' },
{ key: 'bbs_home_tab_active_index', label: '默认选中 Tab', type: 'number', default: 1, min: 0 },
{ key: 'bbs_home_tab_quick_posts_s', label: '快速发帖', type: 'switch', default: true, help: '首页顶部显示快速发帖入口' },
{ key: 'bbs_home_tab_swiper', label: '移动端滑动切换', type: 'switch', default: true },
],
},
],
}
@@ -1,31 +0,0 @@
/**
* Forum domain schemas.
*
* 12 sections matching PHP schema_forum():
* basic / names / homepage / board_page / post_page / reply /
* permissions / other / upload / credits
*/
import { basicSchema } from './basic.js'
import { namesSchema } from './names.js'
import { homepageSchema } from './homepage.js'
import { boardPageSchema } from './board-page.js'
import { postPageSchema } from './post-page.js'
import { replySchema } from './reply.js'
import { permissionsSchema } from './permissions.js'
import { otherSchema } from './other.js'
import { uploadSchema } from './upload.js'
import { creditsSchema } from './credits.js'
export const forumSchemas = {
basic: basicSchema,
names: namesSchema,
homepage: homepageSchema,
board_page: boardPageSchema,
post_page: postPageSchema,
reply: replySchema,
permissions: permissionsSchema,
other: otherSchema,
upload: uploadSchema,
credits: creditsSchema,
}
@@ -1,41 +0,0 @@
export const namesSchema = {
key: 'forum.names',
title: '名称定义',
icon: 'fas fa-language',
description: '自定义论坛各实体的显示名称(12 词条)。',
defaults: {
bbs_forum_display_name: '论坛',
bbs_plate_display_name: '版块',
bbs_posts_display_name: '帖子',
bbs_topic_display_name: '话题',
bbs_tag_display_name: '标签',
bbs_comment_display_name: '回复',
bbs_question_badge_name: '提问',
bbs_question_ok_badge_name: '已解决',
bbs_cat_moderator_name: '分区版主',
bbs_plate_author_name: '超级版主',
bbs_plate_moderator_name: '版主',
bbs_plate_follow_name: '关注',
},
groups: [
{
key: 'display_names',
title: '名词自定义',
icon: 'fas fa-tags',
fields: [
{ key: 'bbs_forum_display_name', label: '论坛', type: 'input', default: '论坛' },
{ key: 'bbs_plate_display_name', label: '版块', type: 'input', default: '版块' },
{ key: 'bbs_posts_display_name', label: '帖子', type: 'input', default: '帖子' },
{ key: 'bbs_topic_display_name', label: '话题', type: 'input', default: '话题' },
{ key: 'bbs_tag_display_name', label: '标签', type: 'input', default: '标签' },
{ key: 'bbs_comment_display_name', label: '回复', type: 'input', default: '回复' },
{ key: 'bbs_question_badge_name', label: '提问徽章', type: 'input', default: '提问' },
{ key: 'bbs_question_ok_badge_name', label: '已解决徽章', type: 'input', default: '已解决' },
{ key: 'bbs_cat_moderator_name', label: '分区版主', type: 'input', default: '分区版主' },
{ key: 'bbs_plate_author_name', label: '超级版主', type: 'input', default: '超级版主' },
{ key: 'bbs_plate_moderator_name', label: '版主', type: 'input', default: '版主' },
{ key: 'bbs_plate_follow_name', label: '关注', type: 'input', default: '关注' },
],
},
],
}
@@ -1,64 +0,0 @@
export const otherSchema = {
key: 'forum.other',
title: '其它设置',
icon: 'fas fa-cog',
description: '投票、版主申请说明、标题字数限制和 URL 别名配置。',
defaults: {
bbs_vote_number_type: 'percent',
bbs_vote_max: 8,
bbs_apply_moderator_desc: '欢迎有热情的用户申请成为版主,协助管理社区秩序。',
bbs_term_thumb: '',
bbs_post_lists_hide_pay_s: false,
bbs_post_title_strlen_limit: 30,
bbs_t_placeholder: '请输入标题',
bbs_c_placeholder: '请输入内容',
bbs_rewrite_suffix_html_s: true,
bbs_plate_rewrite_slug: 'board',
bbs_posts_rewrite_slug: 'topic',
bbs_posts_edit_rewrite_slug: 'topic-edit',
bbs_posts_show_in_rest_s: false,
},
groups: [
{
key: 'vote',
title: '投票',
icon: 'fas fa-poll',
fields: [
{ key: 'bbs_vote_number_type', label: '投票显示', type: 'select', default: 'percent', options: [{ label: '百分比', value: 'percent' }, { label: '票数', value: 'number' }] },
{ key: 'bbs_vote_max', label: '选项上限', type: 'number', default: 8, min: 2, max: 20 },
],
},
{
key: 'moderator_apply',
title: '版主申请',
icon: 'fas fa-user-plus',
fields: [
{ key: 'bbs_apply_moderator_desc', label: '申请说明', type: 'textarea', default: '欢迎有热情的用户申请成为版主,协助管理社区秩序。' },
],
},
{
key: 'misc',
title: '杂项',
icon: 'fas fa-sliders-h',
fields: [
{ key: 'bbs_term_thumb', label: '标签默认缩略图', type: 'media', default: '' },
{ key: 'bbs_post_lists_hide_pay_s', label: '列表隐藏付费标记', type: 'switch', default: false },
{ key: 'bbs_post_title_strlen_limit', label: '标题字数限制', type: 'number', default: 30, min: 5, max: 100, help: '最小 5,最大 100' },
{ key: 'bbs_t_placeholder', label: '标题占位符', type: 'input', default: '请输入标题' },
{ key: 'bbs_c_placeholder', label: '内容占位符', type: 'input', default: '请输入内容' },
],
},
{
key: 'url',
title: 'URL / 别名',
icon: 'fas fa-link',
fields: [
{ key: 'bbs_rewrite_suffix_html_s', label: '.html 后缀', type: 'switch', default: true },
{ key: 'bbs_plate_rewrite_slug', label: '版块别名', type: 'input', default: 'board' },
{ key: 'bbs_posts_rewrite_slug', label: '帖子别名', type: 'input', default: 'topic' },
{ key: 'bbs_posts_edit_rewrite_slug', label: '编辑别名', type: 'input', default: 'topic-edit' },
{ key: 'bbs_posts_show_in_rest_s', label: '古腾堡编辑器', type: 'switch', default: false, help: '开启后帖子可使用 Gutenberg' },
],
},
],
}
@@ -1,64 +0,0 @@
export const permissionsSchema = {
key: 'forum.permissions',
title: '权限配置',
icon: 'fas fa-lock',
description: '发帖限制、按钮显示、帖子付费和版块付费关注配置。',
defaults: {
bbs_posts_add_limit_opt_max: 10,
bbs_plate_add_limit_opt_max: 3,
bbs_show_new_plate: true,
bbs_show_new_topic: true,
bbs_show_apply_moderator: true,
bbs_post_pay_hide_type_s: true,
bbs_post_pay_vip_price_s: true,
bbs_post_pay_vip_1_discount: 80,
bbs_post_pay_vip_2_discount: 50,
bbs_post_pay_price_limit: 1000,
bbs_plate_follow_pay_s: false,
bbs_plate_follow_pay_price: 10,
bbs_plate_follow_pay_type: 'money',
},
groups: [
{
key: 'posting',
title: '发帖限制',
icon: 'fas fa-ban',
fields: [
{ key: 'bbs_posts_add_limit_opt_max', label: '每日发帖上限', type: 'number', default: 10, min: 0, help: '0 = 不限' },
{ key: 'bbs_plate_add_limit_opt_max', label: '建版数量上限', type: 'number', default: 3, min: 0 },
],
},
{
key: 'display_btns',
title: '按钮显示',
icon: 'fas fa-eye',
fields: [
{ key: 'bbs_show_new_plate', label: '始终显示建版按钮', type: 'switch', default: true },
{ key: 'bbs_show_new_topic', label: '始终显示发帖按钮', type: 'switch', default: true },
{ key: 'bbs_show_apply_moderator', label: '始终显示申请版主', type: 'switch', default: true },
],
},
{
key: 'paid_content',
title: '帖子付费',
icon: 'fas fa-money-bill-wave',
fields: [
{ key: 'bbs_post_pay_hide_type_s', label: '启用付费内容', type: 'switch', default: true },
{ key: 'bbs_post_pay_vip_price_s', label: 'VIP 特价', type: 'switch', default: true, dependsOn: { key: 'bbs_post_pay_hide_type_s', value: true } },
{ key: 'bbs_post_pay_vip_1_discount', label: '一级 VIP 折扣 (%)', type: 'number', default: 80, min: 0, max: 100, dependsOn: { key: 'bbs_post_pay_vip_price_s', value: true } },
{ key: 'bbs_post_pay_vip_2_discount', label: '二级 VIP 折扣 (%)', type: 'number', default: 50, min: 0, max: 100, dependsOn: { key: 'bbs_post_pay_vip_price_s', value: true } },
{ key: 'bbs_post_pay_price_limit', label: '最高定价', type: 'number', default: 1000, min: 1, dependsOn: { key: 'bbs_post_pay_hide_type_s', value: true } },
],
},
{
key: 'plate_follow',
title: '版块付费关注',
icon: 'fas fa-heart',
fields: [
{ key: 'bbs_plate_follow_pay_s', label: '启用付费关注', type: 'switch', default: false },
{ key: 'bbs_plate_follow_pay_price', label: '关注价格', type: 'number', default: 10, min: 0, dependsOn: { key: 'bbs_plate_follow_pay_s', value: true } },
{ key: 'bbs_plate_follow_pay_type', label: '支付方式', type: 'select', default: 'money', options: [{ label: '现金', value: 'money' }, { label: '积分', value: 'points' }], dependsOn: { key: 'bbs_plate_follow_pay_s', value: true } },
],
},
],
}
@@ -1,60 +0,0 @@
export const postPageSchema = {
key: 'forum.post_page',
title: '帖子页面',
icon: 'fas fa-file-alt',
description: '帖子页面包屑、评分、高度限制、封面比例和视频静音配置。',
defaults: {
bbs_breadcrumbs_s: true,
bbs_breadcrumbs_home: true,
bbs_breadcrumbs_bbs_home: true,
bbs_breadcrumbs_bbs_home_name: '论坛',
bbs_breadcrumbs_plate_cat: true,
bbs_score_extra_max: 5,
bbs_score_deduct_max: 3,
bbs_posts_maxheight_s: false,
bbs_posts_maxheight: 800,
bbs_posts_cover_ratio: '16:9',
bbs_posts_cover_video_mute: true,
},
groups: [
{
key: 'breadcrumbs',
title: '面包屑',
icon: 'fas fa-angle-double-right',
fields: [
{ key: 'bbs_breadcrumbs_s', label: '显示面包屑', type: 'switch', default: true },
{ key: 'bbs_breadcrumbs_home', label: '显示首页', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } },
{ key: 'bbs_breadcrumbs_bbs_home', label: '显示论坛首页', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } },
{ key: 'bbs_breadcrumbs_bbs_home_name', label: '论坛首页名称', type: 'input', default: '论坛', dependsOn: { key: 'bbs_breadcrumbs_bbs_home', value: true } },
{ key: 'bbs_breadcrumbs_plate_cat', label: '显示分区', type: 'switch', default: true, dependsOn: { key: 'bbs_breadcrumbs_s', value: true } },
],
},
{
key: 'score',
title: '评分设置',
icon: 'fas fa-star',
fields: [
{ key: 'bbs_score_extra_max', label: '加分上限', type: 'number', default: 5, min: 1, max: 50 },
{ key: 'bbs_score_deduct_max', label: '扣分上限', type: 'number', default: 3, min: 1, max: 50 },
],
},
{
key: 'display',
title: '内容显示',
icon: 'fas fa-expand-arrows-alt',
fields: [
{ key: 'bbs_posts_maxheight_s', label: '内容高度限制', type: 'switch', default: false },
{ key: 'bbs_posts_maxheight', label: '最大高度 (px)', type: 'number', default: 800, min: 300, max: 3000, dependsOn: { key: 'bbs_posts_maxheight_s', value: true } },
],
},
{
key: 'cover',
title: '封面设置',
icon: 'fas fa-image',
fields: [
{ key: 'bbs_posts_cover_ratio', label: '封面比例', type: 'select', default: '16:9', options: [{ label: '16:9', value: '16:9' }, { label: '4:3', value: '4:3' }, { label: '1:1', value: '1:1' }, { label: '3:4', value: '3:4' }] },
{ key: 'bbs_posts_cover_video_mute', label: '视频封面静音', type: 'switch', default: true },
],
},
],
}
@@ -1,44 +0,0 @@
export const replySchema = {
key: 'forum.reply',
title: '回复评论',
icon: 'fas fa-reply-all',
description: '回复翻页、工具栏功能、表情、代码和快捷回复配置。',
defaults: {
bbs_reply_paginate_type: 'ajax',
bbs_reply_paginate_ias_s: true,
bbs_reply_paginate_ias_max: 3,
bbs_comment_smilie: true,
bbs_comment_code: true,
bbs_comment_img: true,
bbs_comment_upload_img: true,
bbs_comment_quick_s: true,
bbs_comment_quick_often: '不错,学习了\n感谢分享\n期待更新',
bbs_comment_placeholder: '写下你的回复…',
},
groups: [
{
key: 'paginate',
title: '翻页',
icon: 'fas fa-chevron-circle-right',
fields: [
{ key: 'bbs_reply_paginate_type', label: '翻页方式', type: 'select', default: 'ajax', options: [{ label: 'AJAX 加载', value: 'ajax' }, { label: '传统翻页', value: 'page' }] },
{ key: 'bbs_reply_paginate_ias_s', label: '无限滚动', type: 'switch', default: true },
{ key: 'bbs_reply_paginate_ias_max', label: '自动加载页数', type: 'number', default: 3, min: 0, max: 10, dependsOn: { key: 'bbs_reply_paginate_ias_s', value: true } },
],
},
{
key: 'toolbar',
title: '回复工具栏',
icon: 'fas fa-toolbox',
fields: [
{ key: 'bbs_comment_smilie', label: '表情', type: 'switch', default: true },
{ key: 'bbs_comment_code', label: '代码', type: 'switch', default: true },
{ key: 'bbs_comment_img', label: '插图', type: 'switch', default: true },
{ key: 'bbs_comment_upload_img', label: '上传图片', type: 'switch', default: true },
{ key: 'bbs_comment_quick_s', label: '快捷回复', type: 'switch', default: true },
{ key: 'bbs_comment_quick_often', label: '预设短语', type: 'textarea', default: '不错,学习了\n感谢分享\n期待更新', help: '每行一条', dependsOn: { key: 'bbs_comment_quick_s', value: true } },
{ key: 'bbs_comment_placeholder', label: '回复占位符', type: 'input', default: '写下你的回复…' },
],
},
],
}
@@ -1,27 +0,0 @@
export const uploadSchema = {
key: 'forum.upload',
title: '上传权限',
icon: 'fas fa-cloud-upload-alt',
description: '论坛帖子图片、视频和文件的上传大小与格式限制。',
defaults: {
bbs_upload_img_size: 2048,
bbs_image_upload_multiple: 9,
bbs_upload_video_size: 20480,
bbs_upload_file_size: 10240,
bbs_upload_file_mimes: 'jpg,jpeg,png,gif,mp4,zip,rar,pdf',
},
groups: [
{
key: 'limits',
title: '上传限制',
icon: 'fas fa-weight-hanging',
fields: [
{ key: 'bbs_upload_img_size', label: '图片大小 (KB)', type: 'number', default: 2048, min: 100 },
{ key: 'bbs_image_upload_multiple', label: '批量上传数', type: 'number', default: 9, min: 1, max: 20 },
{ key: 'bbs_upload_video_size', label: '视频大小 (KB)', type: 'number', default: 20480, min: 1024 },
{ key: 'bbs_upload_file_size', label: '文件大小 (KB)', type: 'number', default: 10240, min: 100 },
{ key: 'bbs_upload_file_mimes', label: '允许格式', type: 'input', default: 'jpg,jpeg,png,gif,mp4,zip,rar,pdf', help: '逗号分隔' },
],
},
],
}
@@ -25,8 +25,6 @@ export const searchSchema = {
search_type_user: false,
search_type_category: true,
search_type_tag: true,
search_type_product: false,
search_type_topic: false,
search_default_type: 'post',
search_ms_enabled: false,
search_ms_api_url: '',
@@ -63,8 +61,6 @@ export const searchSchema = {
{ key: 'search_type_user', label: '用户', type: 'switch', default: false },
{ key: 'search_type_category', label: '分类', type: 'switch', default: true },
{ key: 'search_type_tag', label: '标签', type: 'switch', default: true },
{ key: 'search_type_product', label: '商品', type: 'switch', default: false },
{ key: 'search_type_topic', label: '帖子', type: 'switch', default: false },
{
key: 'search_default_type',
label: '默认类型',
@@ -75,8 +71,6 @@ export const searchSchema = {
{ label: '用户', value: 'user' },
{ label: '分类', value: 'category' },
{ label: '标签', value: 'tag' },
{ label: '商品', value: 'product' },
{ label: '帖子', value: 'topic' },
],
},
],
@@ -136,15 +136,9 @@ export const pageDisplaySchemas = {
], dependsOn: { key: 'header_newposts_enabled', value: true } },
{ key: 'header_newposts_types', label: '发布类型', type: 'checkbox', default: ['post'], options: [
{ label: '文章', value: 'post' },
{ label: '帖子', value: 'topic' },
{ label: '商品', value: 'product' },
{ label: '版块', value: 'plate' },
], dependsOn: { key: 'header_newposts_enabled', value: true }, help: '未开启的模块不会显示' },
{ key: 'header_user_newposts_btn', label: '用户卡发布类型', type: 'select', default: 'post', options: [
{ label: '文章', value: 'post' },
{ label: '帖子', value: 'topic' },
{ label: '商品', value: 'product' },
{ label: '版块', value: 'plate' },
] },
{ key: 'header_user_newposts_text', label: '用户卡发布文案', type: 'input', default: '发布内容' },
{ key: 'header_nav_pay_vip', label: '导航栏开通会员按钮', type: 'switch', default: true, help: '顶部导航显示开通会员' },
@@ -349,7 +343,7 @@ export const pageDisplaySchemas = {
title: 'Tab 配置',
icon: 'fas fa-columns',
fields: [
{ key: 'user_home_tabs', label: '显示的 Tab', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔;默认 posts,favorites,comments,follow(粉丝)。可选 credits,product,plate,forum' },
{ key: 'user_home_tabs', label: '显示的 Tab', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔;默认 posts,favorites,comments,follow(粉丝)。可选 credits' },
{ key: 'user_home_tabs_order', label: 'Tab 显示顺序', type: 'input', default: 'posts,favorites,comments,follow', help: '逗号分隔,控制栏目顺序' },
{ key: 'user_home_tabs_pc_hide', label: 'PC 端隐藏', type: 'input', default: '', help: '逗号分隔要在 PC 端隐藏的栏目' },
{ key: 'user_home_tabs_mobile_hide', label: '移动端隐藏', type: 'input', default: '', help: '逗号分隔要在移动端隐藏的栏目' },
@@ -669,9 +663,6 @@ export const pageDisplaySchemas = {
{ label: '用户页', value: 'author' },
{ label: '文章页', value: 'single' },
{ label: '搜索页', value: 'search' },
{ label: '[论坛]首页', value: 'forum_home' },
{ label: '[论坛]板块页面', value: 'forum_plate' },
{ label: '[论坛]帖子页面', value: 'forum_post' },
{ label: '其它页面', value: 'page' },
], help: '选择启用顶部多功能组件的页面类型,一个都不选则为关闭此功能' },
{ key: 'header_components_device_visibility', label: '设备显示规则', type: 'radio', default: '', options: [
@@ -717,13 +708,10 @@ export const pageDisplaySchemas = {
{ key: 'header_components_search_popular_limit', label: '关键词最多显示', type: 'number', default: 6, min: 2, max: 50, dependsOn: { key: 'header_components_search_show_keywords', value: true } },
{ key: 'header_components_search_show_input_cat', label: '分类搜索', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '显示分类搜索选择' },
{ key: 'header_components_search_in_cat', label: '默认搜索的分类', type: 'input', default: '', dependsOn: { key: 'header_components_search_show_input_cat', value: true }, help: '分类 ID,可留空' },
{ key: 'header_components_search_show_type', label: '显示类型选择', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '显示切换搜索类型的按钮(文章、用户、版块、帖子、商品)' },
{ key: 'header_components_search_show_type', label: '显示类型选择', type: 'switch', default: false, dependsOn: { key: 'header_components_search_enabled', value: true }, help: '显示切换搜索类型的按钮(文章、用户)' },
{ key: 'header_components_search_in_type', label: '默认搜索类型', type: 'radio', default: 'post', dependsOn: { key: 'header_components_search_show_type', value: true }, options: [
{ label: '文章', value: 'post' },
{ label: '用户', value: 'user' },
{ label: '商品', value: 'product' },
{ label: '帖子', value: 'forum' },
{ label: '版块', value: 'plate' },
]},
{
key: 'header_components_search_before_html',
@@ -7,16 +7,10 @@ export const MEDAL_GET_TYPES = [
{ label: '注册天数', value: 'registration_time' },
{ label: '累计签到天数', value: 'checkin_all_day' },
{ label: '发布文章数', value: 'new_post' },
{ label: '论坛发帖数', value: 'bbs_post' },
{ label: '评论数', value: 'comment' },
{ label: '粉丝数', value: 'followed' },
{ label: '文章获赞', value: 'like' },
{ label: '评论获赞', value: 'comment_like' },
{ label: '回复被采纳', value: 'bbs_adopt' },
{ label: '神评次数', value: 'bbs_comment_hot' },
{ label: '热帖次数', value: 'bbs_post_hot' },
{ label: '精华帖数', value: 'bbs_essence' },
{ label: '帖子评分累计', value: 'bbs_score' },
{ label: '内容阅读量', value: 'views' },
{ label: '创作分成累计(元)', value: 'pay_income' },
{ label: '推广佣金累计(元)', value: 'pay_rebate' },
@@ -19,13 +19,6 @@ export const creditsSchema = {
points_free_comment_new: 1,
points_free_comment_like: 1,
points_free_followed: 1,
points_free_bbs_posts_new: 5,
points_free_bbs_reply: 2,
points_free_bbs_essence: 10,
points_free_bbs_adopt: 20,
points_free_bbs_posts_hot: 5,
points_free_bbs_comment_hot: 5,
points_free_bbs_score_extra: 3,
points_earn_invite: 10,
points_spend_comment: 0,
points_spend_download: 0,
@@ -74,13 +67,6 @@ export const creditsSchema = {
{ key: 'points_free_comment_new', label: '发表评论', type: 'number', default: 1, min: 0, max: 100 },
{ key: 'points_free_comment_like', label: '评论被赞', type: 'number', default: 1, min: 0, max: 100 },
{ key: 'points_free_followed', label: '被关注', type: 'number', default: 1, min: 0, max: 100 },
{ key: 'points_free_bbs_posts_new', label: '论坛发帖', type: 'number', default: 5, min: 0, max: 1000 },
{ key: 'points_free_bbs_reply', label: '论坛回复', type: 'number', default: 2, min: 0, max: 1000 },
{ key: 'points_free_bbs_essence', label: '帖子加精', type: 'number', default: 10, min: 0, max: 1000 },
{ key: 'points_free_bbs_adopt', label: '回复被采纳', type: 'number', default: 20, min: 0, max: 1000 },
{ key: 'points_free_bbs_posts_hot', label: '热帖奖励', type: 'number', default: 5, min: 0, max: 1000 },
{ key: 'points_free_bbs_comment_hot', label: '神评奖励', type: 'number', default: 5, min: 0, max: 1000 },
{ key: 'points_free_bbs_score_extra', label: '帖子加分', type: 'number', default: 3, min: 0, max: 1000 },
{ key: 'points_earn_invite', label: '邀请注册奖励', type: 'number', default: 10, min: 0, max: 1000, help: '邀请不计入免费日上限' },
],
},
@@ -11,14 +11,6 @@ export const INTEGRAL_CATALOG = [
{ key: 'post_favorite', label: '文章被收藏', default: 2, group: '文章' },
{ key: 'comment_new', label: '发表评论', default: 2, group: '文章' },
{ key: 'comment_like', label: '评论获赞', default: 1, group: '文章' },
{ key: 'bbs_posts_new', label: '发布帖子', default: 3, group: '论坛' },
{ key: 'bbs_score_extra', label: '帖子被加分', default: 1, group: '论坛' },
{ key: 'bbs_essence', label: '帖子评为精华', default: 2, group: '论坛' },
{ key: 'bbs_posts_hot', label: '帖子成为热门', default: 2, group: '论坛' },
{ key: 'bbs_plate_new', label: '创建版块', default: 2, group: '论坛' },
{ key: 'bbs_plate_hot', label: '版块成为热门', default: 2, group: '论坛' },
{ key: 'bbs_adopt', label: '回答被采纳', default: 2, group: '论坛' },
{ key: 'bbs_comment_hot', label: '评论成为神评', default: 2, group: '论坛' },
]
export function defaultLevelOpt(max = 10) {