Zibll 主线收口:统一 single 并清退 compat 资源路径
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<div class="aksrt-page-container content-page">
|
||||
<PageHeader
|
||||
title="首页配置"
|
||||
desc="配置首页内容模型、模块编排、分类 Tab、卡片样式和交互方式。"
|
||||
desc="按 Zibll 首页主线配置内容模型、Tab、主列表、卡片样式与基础交互。"
|
||||
>
|
||||
<template #actions>
|
||||
<n-button type="primary" :loading="saving" @click="handleSave">保存设置</n-button>
|
||||
@@ -10,65 +10,39 @@
|
||||
</PageHeader>
|
||||
|
||||
<n-tabs v-model:value="activeTab" type="line" animated>
|
||||
<!-- Module Workstation Tab -->
|
||||
<n-tab-pane name="modules" tab="模块编排">
|
||||
<div class="aksrt-homepage__workstation">
|
||||
<div class="aksrt-homepage__canvas-panel">
|
||||
<ModuleCanvas
|
||||
:modules="homeModules"
|
||||
:selected-index="selectedModuleIndex"
|
||||
@select="selectedModuleIndex = $event"
|
||||
@add="handleAddModule"
|
||||
@remove="handleRemoveModule"
|
||||
@copy="handleCopyModule"
|
||||
@toggle="handleToggleModule"
|
||||
@move="handleMoveModule"
|
||||
@pick-preset="showPresetPicker = true"
|
||||
@export="handleExportModules"
|
||||
@import="showImportDialog = true"
|
||||
/>
|
||||
</div>
|
||||
<div class="aksrt-homepage__inspector-panel">
|
||||
<ModuleInspector
|
||||
:module="selectedModule"
|
||||
:module-index="selectedModuleIndex"
|
||||
@update="handleModuleUpdate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- Main Stream & Tabs Tab -->
|
||||
<n-tab-pane name="stream" tab="主列表 & Tab">
|
||||
<n-spin :show="loading" style="width:100%; min-height:200px;">
|
||||
<!-- Content Model -->
|
||||
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
|
||||
<template #header><strong>内容模型</strong></template>
|
||||
<template #header><strong>首页模型</strong></template>
|
||||
<n-form :model="form" label-placement="left" label-width="160" class="aksrt-form">
|
||||
<n-form-item label="首页内容模型">
|
||||
<n-radio-group v-model:value="form.homepage_content_model">
|
||||
<n-space vertical>
|
||||
<n-radio value="mixed">
|
||||
<n-text>混合模式</n-text>
|
||||
<n-text depth="3" style="margin-left:8px;">文章 + 资源(默认)</n-text>
|
||||
<n-text>混合内容流</n-text>
|
||||
<n-text depth="3" style="margin-left:8px;">文章 + 资源,贴近 Zibll 首页主流呈现。</n-text>
|
||||
</n-radio>
|
||||
<n-radio value="post">
|
||||
<n-text>纯文章</n-text>
|
||||
<n-text depth="3" style="margin-left:8px;">仅标准文章</n-text>
|
||||
<n-text>纯文章流</n-text>
|
||||
<n-text depth="3" style="margin-left:8px;">仅标准文章,适合纯资讯站点。</n-text>
|
||||
</n-radio>
|
||||
</n-space>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-form-item label="首页说明">
|
||||
<n-alert type="info" :show-icon="false">
|
||||
当前首页已切换为固定骨架模式,不再使用旧版自由模块画布、导入导出预设或历史模块桥接。
|
||||
</n-alert>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- Main Stream -->
|
||||
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
|
||||
<template #header><strong>主列表(最新发布)</strong></template>
|
||||
<template #header><strong>主列表</strong></template>
|
||||
<n-form :model="form" label-placement="left" label-width="160" class="aksrt-form">
|
||||
<n-form-item label="启用主列表">
|
||||
<n-switch v-model:value="form.home_posts_list_enabled" />
|
||||
<template #feedback><span class="aksrt-hint-text">关闭后首页将不显示文章列表(仅显示模块)。</span></template>
|
||||
<template #feedback><span class="aksrt-hint-text">首页主内容区保留 Zibll 式内容流,不建议关闭。</span></template>
|
||||
</n-form-item>
|
||||
<n-form-item label="区块标题">
|
||||
<n-input v-model:value="form.home_list_title" placeholder="最新发布" />
|
||||
@@ -98,7 +72,6 @@
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- Exclusions -->
|
||||
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
|
||||
<template #header><strong>排除规则</strong></template>
|
||||
<n-form :model="form" label-placement="left" label-width="160" class="aksrt-form">
|
||||
@@ -109,7 +82,7 @@
|
||||
placeholder="逗号分隔的文章 ID,如 12,34,56"
|
||||
:rows="2"
|
||||
/>
|
||||
<template #feedback><span class="aksrt-hint-text">主列表和 Tab 查询均排除这些文章。</span></template>
|
||||
<template #feedback><span class="aksrt-hint-text">主列表和首页 Tab 查询均排除这些文章。</span></template>
|
||||
</n-form-item>
|
||||
<n-form-item label="排除分类 ID">
|
||||
<n-input
|
||||
@@ -118,12 +91,11 @@
|
||||
placeholder="逗号分隔的分类 ID,如 5,8"
|
||||
:rows="2"
|
||||
/>
|
||||
<template #feedback><span class="aksrt-hint-text">主列表排除这些分类。Tab 不能绑定已排除的分类。</span></template>
|
||||
<template #feedback><span class="aksrt-hint-text">主列表排除这些分类,首页 Tab 不能绑定已排除分类。</span></template>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- Tabs -->
|
||||
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
|
||||
<template #header>
|
||||
<div style="display:flex; align-items:center; justify-content:space-between;">
|
||||
@@ -141,7 +113,7 @@
|
||||
</n-form>
|
||||
|
||||
<div v-if="form.homepage_tabs.length === 0" class="homepage-settings__empty-tabs">
|
||||
<n-text depth="3">暂无 Tab 配置,点击"添加 Tab"添加。</n-text>
|
||||
<n-text depth="3">暂无 Tab 配置,点击“添加 Tab”创建首页分类切换。</n-text>
|
||||
</div>
|
||||
|
||||
<div v-for="(tab, index) in form.homepage_tabs" :key="index" class="homepage-settings__tab-row">
|
||||
@@ -177,8 +149,7 @@
|
||||
</n-spin>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- Cards & Sidebar Tab -->
|
||||
<n-tab-pane name="cards" tab="卡片 & 侧栏">
|
||||
<n-tab-pane name="cards" tab="卡片 & 交互">
|
||||
<n-spin :show="loading" style="width:100%; min-height:200px;">
|
||||
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
|
||||
<template #header><strong>卡片样式</strong></template>
|
||||
@@ -210,74 +181,36 @@
|
||||
</n-card>
|
||||
|
||||
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
|
||||
<template #header><strong>侧边栏</strong></template>
|
||||
<template #header><strong>交互设置</strong></template>
|
||||
<n-form :model="form" label-placement="left" label-width="160" class="aksrt-form">
|
||||
<n-form-item label="排序栏 AJAX">
|
||||
<n-switch v-model:value="form.homepage_sort_ajax_enabled" />
|
||||
<template #feedback><span class="aksrt-hint-text">关闭后排序链接使用整页跳转。</span></template>
|
||||
</n-form-item>
|
||||
<n-form-item label="Tab 吸顶">
|
||||
<n-switch v-model:value="form.homepage_sticky_tabs_enabled" />
|
||||
</n-form-item>
|
||||
<n-form-item label="迷你侧栏列表">
|
||||
<n-switch v-model:value="form.homepage_sidebar_mini_enabled" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
|
||||
<template #header><strong>交互设置</strong></template>
|
||||
<n-form :model="form" label-placement="left" label-width="160" class="aksrt-form">
|
||||
<n-form-item label="排序栏 AJAX">
|
||||
<n-switch v-model:value="form.homepage_sort_ajax_enabled" />
|
||||
<template #feedback><span class="aksrt-hint-text">关闭后排序链接将使用整页跳转。</span></template>
|
||||
</n-form-item>
|
||||
<n-form-item label="Tab 吸顶">
|
||||
<n-switch v-model:value="form.homepage_sticky_tabs_enabled" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- Module Status -->
|
||||
<n-card class="aksrt-section-card">
|
||||
<template #header><strong>模块状态</strong></template>
|
||||
<n-form label-placement="left" label-width="160" class="aksrt-form">
|
||||
<n-form-item label="Hero 首屏">
|
||||
<n-tag type="info" size="small">在 Hero 页面管理</n-tag>
|
||||
<n-text depth="3" style="margin-left:8px;">首页 Hero 开关已从首页设置中移出。</n-text>
|
||||
</n-form-item>
|
||||
<n-form-item label="Banner 轮播">
|
||||
<n-tag type="info" size="small">在 Banner 页面管理</n-tag>
|
||||
<n-text depth="3" style="margin-left:8px;">首页 Banner 开关已从首页设置中移出。</n-text>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #header><strong>固定骨架说明</strong></template>
|
||||
<n-space vertical>
|
||||
<n-alert type="warning" :show-icon="false">
|
||||
Hero 与 Banner 已作为固定首页骨架的一部分维护,不再由旧版首页模块编排驱动。
|
||||
</n-alert>
|
||||
<n-alert type="info" :show-icon="false">
|
||||
如需调整首页首屏运营区,请前往对应 Hero / Banner 管理页,而不是返回旧模块工作台。
|
||||
</n-alert>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</n-spin>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
<!-- Preset Picker Dialog -->
|
||||
<ModulePresetPicker
|
||||
:show="showPresetPicker"
|
||||
@close="showPresetPicker = false"
|
||||
@apply="handleApplyPreset"
|
||||
/>
|
||||
|
||||
<!-- Import Dialog -->
|
||||
<n-modal
|
||||
v-model:show="showImportDialog"
|
||||
title="导入模块配置"
|
||||
preset="dialog"
|
||||
style="width: 480px;"
|
||||
:show-icon="false"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="importJson"
|
||||
type="textarea"
|
||||
:rows="8"
|
||||
placeholder="粘贴导出的 JSON 配置..."
|
||||
/>
|
||||
<template #action>
|
||||
<n-space>
|
||||
<n-button @click="showImportDialog = false">取消</n-button>
|
||||
<n-button type="primary" :disabled="!importJson.trim()" @click="handleImportModules">导入</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<div class="aksrt-form-actions">
|
||||
<n-button type="primary" :loading="saving" @click="handleSave">保存设置</n-button>
|
||||
</div>
|
||||
@@ -285,34 +218,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import api from '@/api'
|
||||
import PageHeader from '@/components/PageHeader.vue'
|
||||
import ModuleCanvas from '@/components/homepage/ModuleCanvas.vue'
|
||||
import ModuleInspector from '@/components/homepage/ModuleInspector.vue'
|
||||
import ModulePresetPicker from '@/components/homepage/ModulePresetPicker.vue'
|
||||
import { makeDefaultModule } from '@/utils/home-module-schema'
|
||||
|
||||
const message = useMessage()
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const activeTab = ref('modules')
|
||||
const activeTab = ref('stream')
|
||||
const categoryOptions = ref([])
|
||||
const showPresetPicker = ref(false)
|
||||
const showImportDialog = ref(false)
|
||||
const importJson = ref('')
|
||||
|
||||
// Module workstation state
|
||||
const homeModules = ref([])
|
||||
const selectedModuleIndex = ref(-1)
|
||||
|
||||
const selectedModule = computed(() => {
|
||||
if (selectedModuleIndex.value >= 0 && selectedModuleIndex.value < homeModules.value.length) {
|
||||
return homeModules.value[selectedModuleIndex.value]
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const allSortOptions = [
|
||||
{ label: '最近更新', value: 'modified' },
|
||||
@@ -361,116 +276,6 @@ const excludeCategoryIdsText = computed({
|
||||
},
|
||||
})
|
||||
|
||||
// Module operations
|
||||
function handleAddModule(type) {
|
||||
const newMod = makeDefaultModule(type)
|
||||
if (newMod) {
|
||||
newMod.id = 'mod_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6)
|
||||
newMod.enabled = true
|
||||
newMod.title = ''
|
||||
homeModules.value.push(newMod)
|
||||
selectedModuleIndex.value = homeModules.value.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveModule(index) {
|
||||
homeModules.value.splice(index, 1)
|
||||
if (selectedModuleIndex.value >= homeModules.value.length) {
|
||||
selectedModuleIndex.value = homeModules.value.length - 1
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopyModule(index) {
|
||||
const src = homeModules.value[index]
|
||||
if (!src) return
|
||||
const copy = JSON.parse(JSON.stringify(src))
|
||||
copy.id = 'mod_' + Date.now() + '_' + Math.random().toString(36).slice(2, 6)
|
||||
homeModules.value.splice(index + 1, 0, copy)
|
||||
selectedModuleIndex.value = index + 1
|
||||
}
|
||||
|
||||
function handleToggleModule(index, val) {
|
||||
if (homeModules.value[index]) {
|
||||
homeModules.value[index].enabled = val
|
||||
}
|
||||
}
|
||||
|
||||
function handleMoveModule(index, direction) {
|
||||
const newIndex = index + direction
|
||||
if (newIndex < 0 || newIndex >= homeModules.value.length) return
|
||||
const temp = homeModules.value[index]
|
||||
homeModules.value[index] = homeModules.value[newIndex]
|
||||
homeModules.value[newIndex] = temp
|
||||
// Trigger reactivity
|
||||
homeModules.value = [...homeModules.value]
|
||||
selectedModuleIndex.value = newIndex
|
||||
}
|
||||
|
||||
function handleModuleUpdate(index, key, value) {
|
||||
if (homeModules.value[index]) {
|
||||
if (key === 'config') {
|
||||
homeModules.value[index].config = value
|
||||
} else {
|
||||
homeModules.value[index][key] = value
|
||||
}
|
||||
// Trigger reactivity
|
||||
homeModules.value = [...homeModules.value]
|
||||
}
|
||||
}
|
||||
|
||||
function handleApplyPreset(preset) {
|
||||
homeModules.value = preset.modules.map((m, i) => ({
|
||||
id: 'mod_' + Date.now() + '_' + i,
|
||||
type: m.type,
|
||||
config: JSON.parse(JSON.stringify(m.config || {})),
|
||||
enabled: true,
|
||||
title: '',
|
||||
}))
|
||||
selectedModuleIndex.value = -1
|
||||
showPresetPicker.value = false
|
||||
message.success(`已应用预设「${preset.label}」,共 ${preset.modules.length} 个模块`)
|
||||
}
|
||||
|
||||
function handleExportModules() {
|
||||
const json = JSON.stringify(homeModules.value, null, 2)
|
||||
navigator.clipboard.writeText(json).then(() => {
|
||||
message.success('模块配置已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
// Fallback: show in a dialog
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'homepage-modules.json'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
message.success('模块配置已导出')
|
||||
})
|
||||
}
|
||||
|
||||
function handleImportModules() {
|
||||
try {
|
||||
const parsed = JSON.parse(importJson.value)
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('JSON 必须是数组格式')
|
||||
}
|
||||
homeModules.value = parsed.map((m, i) => ({
|
||||
id: m.id || 'mod_' + Date.now() + '_' + i,
|
||||
type: m.type,
|
||||
config: m.config || {},
|
||||
enabled: m.enabled !== false,
|
||||
title: m.title || '',
|
||||
}))
|
||||
selectedModuleIndex.value = -1
|
||||
showImportDialog.value = false
|
||||
importJson.value = ''
|
||||
message.success(`已导入 ${homeModules.value.length} 个模块`)
|
||||
} catch (err) {
|
||||
message.error('导入失败: ' + (err.message || 'JSON 格式错误'))
|
||||
}
|
||||
}
|
||||
|
||||
// Tab operations
|
||||
function addTab() {
|
||||
form.homepage_tabs.push({ term_id: 0, title: '', orderby: 'date', enabled: true })
|
||||
}
|
||||
@@ -483,7 +288,6 @@ function updateTab(index, field, value) {
|
||||
form.homepage_tabs[index][field] = value
|
||||
}
|
||||
|
||||
// Data loading
|
||||
async function loadCategories() {
|
||||
try {
|
||||
const res = await api.getHomepageCategories()
|
||||
@@ -492,7 +296,9 @@ async function loadCategories() {
|
||||
label: `${c.name} (${c.count})`,
|
||||
value: c.term_id,
|
||||
}))
|
||||
} catch { /* non-critical */ }
|
||||
} catch {
|
||||
// non-critical
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
@@ -522,15 +328,6 @@ async function loadSettings() {
|
||||
form.home_exclude_category_ids = Array.isArray(d.home_exclude_category_ids) ? d.home_exclude_category_ids : []
|
||||
form.homepage_tab_posts_per_page = d.homepage_tab_posts_per_page || 6
|
||||
form.homepage_tab_limit = d.homepage_tab_limit || 4
|
||||
|
||||
// Load modules
|
||||
homeModules.value = Array.isArray(d.homepage_modules) ? d.homepage_modules.map((m, i) => ({
|
||||
id: m.id || 'mod_' + i,
|
||||
type: m.type,
|
||||
config: m.config || {},
|
||||
enabled: m.enabled !== false,
|
||||
title: m.title || '',
|
||||
})) : []
|
||||
} catch (error) {
|
||||
message.error(error.message || '加载设置失败')
|
||||
} finally {
|
||||
@@ -543,13 +340,6 @@ async function handleSave() {
|
||||
try {
|
||||
const payload = {
|
||||
...form,
|
||||
homepage_modules: homeModules.value.map(m => ({
|
||||
id: m.id,
|
||||
type: m.type,
|
||||
config: m.config,
|
||||
enabled: m.enabled !== false,
|
||||
title: m.title || '',
|
||||
})),
|
||||
}
|
||||
|
||||
const res = await api.saveHomepageSettings(payload)
|
||||
@@ -572,26 +362,6 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.aksrt-homepage__workstation {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.aksrt-homepage__canvas-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksrt-homepage__inspector-panel {
|
||||
width: 360px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.homepage-settings__empty-tabs {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
|
||||
@@ -57,7 +57,7 @@ export const orderSchema = {
|
||||
icon: 'fas fa-eye',
|
||||
fields: [
|
||||
{ key: 'payment_mark', label: '付费标记', type: 'input', default: '付费' },
|
||||
{ key: 'payment_box_position', label: '付费框位置', type: 'radio', default: 'top', options: [{ label: '文章模块上', value: 'box_top' }, { label: '内容顶', value: 'top' }, { label: '内容底', value: 'bottom' }, { label: '模块下', value: 'box_bottom' }] },
|
||||
{ key: 'payment_box_position', label: '付费框位置', type: 'radio', default: 'top', options: [{ label: '内容顶部(正文前)', value: 'top' }, { label: '内容底部(正文后)', value: 'bottom' }] },
|
||||
{ key: 'payment_free_logged_show', label: '登录用户显示免费', type: 'switch', default: false },
|
||||
{ key: 'payment_show_ip', label: '显示用户IP', type: 'switch', default: false },
|
||||
{ key: 'payment_show_device', label: '显示设备信息', type: 'switch', default: false },
|
||||
|
||||
@@ -51,7 +51,7 @@ export const paidSchema = {
|
||||
{ key: 'payment_currency_symbol', label: '货币符号', type: 'input', default: '¥', help: '商品价格前缀,如 ¥、$、¥' },
|
||||
{ key: 'payment_paid_show_user', label: '显示购买用户', type: 'switch', default: true },
|
||||
{ key: 'payment_paid_show_paycount', label: '显示购买次数', type: 'switch', default: true },
|
||||
{ key: 'payment_paid_box_position', label: '付费框位置', type: 'radio', default: 'top', options: [{ label: '文章模块上', value: 'box_top' }, { label: '内容顶', value: 'top' }, { label: '内容底', value: 'bottom' }, { label: '模块下', value: 'box_bottom' }] },
|
||||
{ key: 'payment_paid_box_position', label: '付费框位置', type: 'radio', default: 'top', options: [{ label: '内容顶部(正文前)', value: 'top' }, { label: '内容底部(正文后)', value: 'bottom' }] },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user