feat(admin): rebuild page display settings

This commit is contained in:
2026-08-19 00:07:13 +08:00
parent 09b1e8fd35
commit 480e2bf07e
117 changed files with 1815 additions and 9081 deletions
+78 -4
View File
@@ -3,7 +3,15 @@
<n-message-provider>
<n-dialog-provider>
<n-notification-provider>
<n-layout v-if="!isMobile" class="aksrt-admin-layout" has-sider>
<div v-if="!isMobile" class="aksrt-csf-shell">
<header class="aksrt-csf-header">
<div>
<h1>AKSRT主题设置 <small>页面与功能配置</small></h1>
<p>欢迎使用 AKSRT 主题</p>
</div>
<span class="aksrt-csf-version">V{{ themeVersion }}</span>
</header>
<n-layout class="aksrt-admin-layout" has-sider>
<n-layout-sider
bordered
collapse-mode="width"
@@ -47,6 +55,7 @@
</div>
</n-layout>
</n-layout>
</div>
<div v-else class="aksrt-admin-mobile">
<MobileTopBar :title="currentPageTitle" @toggle="drawerActive = true" />
@@ -101,6 +110,7 @@ const collapsed = ref(false)
const pageError = ref(null)
const isMobile = ref(false)
const drawerActive = ref(false)
const themeVersion = window.aksrtAdminData?.version || '9.0'
function checkMobile() {
isMobile.value = window.innerWidth < 768
@@ -205,7 +215,7 @@ function handleMenuClick(key) {
display: flex;
align-items: center;
justify-content: center;
height: 60px;
height: 72px;
gap: 10px;
border-bottom: 1px solid var(--n-border-color, #efeff5);
}
@@ -222,7 +232,7 @@ function handleMenuClick(key) {
width: 28px;
height: 28px;
border-radius: 8px;
background: #18a058;
background: linear-gradient(135deg, #65bff7 10%, #0d67b9 100%);
color: #fff;
font-size: 12px;
font-weight: 700;
@@ -237,7 +247,71 @@ function handleMenuClick(key) {
}
.aksrt-admin-content__inner {
padding: 24px 28px;
padding: 30px;
}
.aksrt-csf-shell {
max-width: 1380px;
margin: 20px auto;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 0 20px rgba(82, 82, 82, .1);
}
.aksrt-csf-header {
position: relative;
display: flex;
min-height: 108px;
align-items: center;
justify-content: space-between;
overflow: hidden;
padding: 22px 34px;
color: #fff;
background: linear-gradient(135deg, #f97794 10%, #623aa2 100%);
}
.aksrt-csf-header::before {
position: absolute;
left: 10%;
bottom: -25px;
content: 'AKSRT';
color: rgba(255, 255, 255, .05);
font-size: 100px;
font-weight: 800;
font-style: oblique;
transform: rotate(-5deg) scaleX(1.5);
}
.aksrt-csf-header h1,
.aksrt-csf-header p {
position: relative;
z-index: 1;
margin: 0;
color: #fff;
}
.aksrt-csf-header h1 {
font-size: 26px;
}
.aksrt-csf-header h1 small {
margin-left: 8px;
font-size: 13px;
font-weight: 400;
}
.aksrt-csf-header p {
margin-top: 7px;
opacity: .78;
}
.aksrt-csf-version {
position: relative;
z-index: 1;
padding: 5px 12px;
border: 1px solid rgba(255, 255, 255, .3);
border-radius: 20px;
background: rgba(255, 255, 255, .1);
}
.aksrt-admin-mobile {
+4 -5
View File
@@ -19,12 +19,11 @@ function normalizeDomainKey(domain) {
/**
* Join REST base + endpoint correctly for both pretty and plain permalinks.
*
* Pretty: https://site/wp-json/aksrt/v1 + /banners?kind=x
* → https://site/wp-json/aksrt/v1/banners?kind=x
* Pretty: https://site/wp-json/aksrt/v1 + /settings/schema/global/common
* → https://site/wp-json/aksrt/v1/settings/schema/global/common
*
* Plain: https://site/index.php?rest_route=/aksrt/v1 + /banners?kind=x
* → https://site/index.php?rest_route=/aksrt/v1/banners&kind=x
* (must NOT produce rest_route=/aksrt/v1/banners?kind=x)
* Plain: https://site/index.php?rest_route=/aksrt/v1 + /settings/schema/global/common
* → https://site/index.php?rest_route=/aksrt/v1/settings/schema/global/common
*
* @param {string} baseUrl
* @param {string} endpoint path starting with /, may include ?query
-3
View File
@@ -7,9 +7,6 @@ export const globalApiMethods = {
saveMailSettings(data) { return this.request('/settings/mail', { method: 'POST', body: JSON.stringify(data) }) },
testMail(to) { return this.request('/mail/test', { method: 'POST', body: JSON.stringify({ to }) }) },
getFooterSettings() { return this.request('/settings/footer') },
saveFooterSettings(data) { return this.request('/settings/footer', { method: 'POST', body: JSON.stringify(data) }) },
getFooterTabbarSettings() { return this.request('/settings/footer-tabbar') },
saveFooterTabbarSettings(data) { return this.request('/settings/footer-tabbar', { method: 'POST', body: JSON.stringify(data) }) },
reorderFooterTabbar(ids) { return this.request('/settings/footer-tabbar/reorder', { method: 'POST', body: JSON.stringify({ ids }) }) },
+5 -13
View File
@@ -3,10 +3,6 @@ export const pageDisplayApiMethods = {
* 页面&显示 (Page Display)
* ================================================================ */
getHomepageSettings() { return this.request('/settings/homepage') },
saveHomepageSettings(data) { return this.request('/settings/homepage', { method: 'POST', body: JSON.stringify(data) }) },
getHomepageCategories() { return this.request('/homepage/categories') },
getCategoryRecommendSettings() { return this.request('/settings/category-recommend') },
saveCategoryRecommendSettings(data) { return this.request('/settings/category-recommend', { method: 'POST', body: JSON.stringify(data) }) },
getCategories(taxonomy = 'category') {
@@ -16,13 +12,9 @@ export const pageDisplayApiMethods = {
updateCategoryMeta(id, data) { return this.request(`/categories/${id}/meta`, { method: 'POST', body: JSON.stringify(data) }) },
searchUsers(search = '', number = 20) { return this.request(`/users/search?search=${encodeURIComponent(search)}&number=${number}`) },
getBanners() { return this.request('/banners') },
createBanner(data) { return this.request('/banners', { method: 'POST', body: JSON.stringify(data) }) },
updateBanner(id, data) { return this.request(`/banners/${id}`, { method: 'PUT', body: JSON.stringify(data) }) },
deleteBanner(id) { return this.request(`/banners/${id}`, { method: 'DELETE' }) },
getHomepageBanners() { return this.getBanners() },
createHomepageBanner(data) { return this.createBanner(data) },
updateHomepageBanner(id, data) { return this.updateBanner(id, data) },
deleteHomepageBanner(id) { return this.deleteBanner(id) },
searchPageDisplayOptions(source, query = '', include = []) {
const params = new URLSearchParams({ query })
include.forEach(value => params.append('include[]', value))
return this.request(`/settings/schema/page_display/search/${encodeURIComponent(source)}?${params}`)
},
}
+256 -28
View File
@@ -1,6 +1,6 @@
<template>
<div class="aksrt-field" :class="fieldClasses">
<div class="aksrt-field__label-row">
<div v-if="schema.label" class="aksrt-field__label-row">
<label class="aksrt-field__label">
{{ schema.label }}
<span v-if="schema.required" class="aksrt-field__required">*</span>
@@ -19,14 +19,14 @@
<!-- Switch -->
<n-switch
v-if="schema.type === 'switch'"
v-if="schema.type === 'switch' || schema.type === 'switcher'"
:value="modelValue"
@update:value="emit('update:modelValue', $event)"
/>
<!-- Input (text, email, url, password) -->
<n-input
v-else-if="schema.type === 'input' || schema.type === 'text'"
v-else-if="schema.type === 'input' || schema.type === 'text' || schema.type === 'url'"
:value="modelValue"
:placeholder="schema.placeholder || ''"
:type="schema.inputType || 'text'"
@@ -50,7 +50,7 @@
<!-- Number -->
<n-input-number
v-else-if="schema.type === 'number'"
v-else-if="schema.type === 'number' || schema.type === 'spinner'"
:value="modelValue"
:min="schema.min"
:max="schema.max"
@@ -62,16 +62,44 @@
/>
<!-- Select -->
<n-select
v-else-if="schema.type === 'select'"
:value="modelValue"
:options="normalizedOptions"
:placeholder="schema.placeholder || '请选择'"
:multiple="schema.multiple"
:clearable="schema.clearable !== false"
:disabled="schema.disabled"
@update:value="emit('update:modelValue', $event)"
/>
<div v-else-if="schema.type === 'select'" class="aksrt-field__ordered-select">
<n-select
:value="modelValue"
:options="normalizedOptions"
:placeholder="schema.placeholder || '请选择'"
:multiple="schema.multiple"
:clearable="schema.clearable !== false"
:disabled="schema.disabled"
@update:value="emit('update:modelValue', $event)"
/>
<div v-if="schema.multiple && schema.sortable && Array.isArray(modelValue)" class="aksrt-field__selected-order">
<div v-for="(value, index) in modelValue" :key="value" class="aksrt-field__selected-order-item">
<span>{{ selectedOptionLabel(normalizedOptions, value) }}</span>
<span><n-button text size="tiny" :disabled="index === 0" @click="moveSelectedValue(index, -1)">↑</n-button><n-button text size="tiny" :disabled="index === modelValue.length - 1" @click="moveSelectedValue(index, 1)">↓</n-button></span>
</div>
</div>
</div>
<div v-else-if="schema.type === 'async_select'" class="aksrt-field__ordered-select">
<n-select
:value="schema.multiple === true && !Array.isArray(modelValue) ? [] : modelValue"
:options="asyncOptions"
:loading="asyncLoading"
:placeholder="schema.placeholder || '输入关键词搜索内容'"
:multiple="schema.multiple === true"
filterable
remote
clearable
@search="handleAsyncSearch"
@update:value="emit('update:modelValue', $event)"
/>
<div v-if="schema.multiple && schema.sortable && Array.isArray(modelValue)" class="aksrt-field__selected-order">
<div v-for="(value, index) in modelValue" :key="value" class="aksrt-field__selected-order-item">
<span>{{ selectedOptionLabel(asyncOptions, value) }}</span>
<span><n-button text size="tiny" :disabled="index === 0" @click="moveSelectedValue(index, -1)">↑</n-button><n-button text size="tiny" :disabled="index === modelValue.length - 1" @click="moveSelectedValue(index, 1)">↓</n-button></span>
</div>
</div>
</div>
<!-- Taxonomy Select (async-loaded category/tag terms) -->
<n-select
@@ -157,6 +185,19 @@
@update:value="emit('update:modelValue', $event)"
/>
<div v-else-if="schema.type === 'palette'" class="aksrt-field__palette">
<button
v-for="opt in normalizedOptions"
:key="opt.value"
type="button"
class="aksrt-field__palette-item"
:class="{ 'is-active': modelValue === opt.value }"
:style="{ background: opt.label }"
:title="opt.value"
@click="emit('update:modelValue', opt.value)"
/>
</div>
<!-- Icon picker -->
<FaIconPicker
v-else-if="schema.type === 'icon'"
@@ -167,7 +208,7 @@
<!-- Media upload -->
<AksMediaField
v-else-if="schema.type === 'media'"
v-else-if="schema.type === 'media' || schema.type === 'upload'"
:model-value="modelValue"
:accept="schema.accept"
:placeholder="schema.placeholder || '选择图片'"
@@ -203,7 +244,7 @@
:title="schema.alertTitle"
:closable="false"
>
{{ schema.alertContent || modelValue }}
{{ schema.content || schema.alertContent || modelValue }}
</n-alert>
<!-- Pure HTML content (no form field, just display) -->
@@ -360,9 +401,9 @@
</div>
<!-- Fieldset (single nested object) -->
<div v-else-if="schema.type === 'fieldset'" class="aksrt-field__fieldset">
<div v-else-if="schema.type === 'fieldset' || schema.type === 'accordion'" class="aksrt-field__fieldset">
<AksField
v-for="field in (schema.fields || [])"
v-for="field in visibleNestedFields"
:key="field.key"
:schema="field"
:model-value="fieldsetValue(field)"
@@ -370,8 +411,29 @@
/>
</div>
<div v-else-if="schema.type === 'link'" class="aksrt-field__link">
<n-input :value="modelValue?.url || ''" placeholder="https://" @update:value="updateLink('url', $event)" />
<n-select :value="modelValue?.target || ''" :options="linkTargetOptions" @update:value="updateLink('target', $event)" />
<n-input :value="modelValue?.text || ''" placeholder="链接文字(可选)" @update:value="updateLink('text', $event)" />
</div>
<div v-else-if="schema.type === 'sortable'" class="aksrt-field__sortable">
<div v-for="(item, index) in sortableItems" :key="item.key" class="aksrt-field__sortable-item">
<div class="aksrt-field__sortable-head">
<strong>{{ item.label }}</strong>
<span>
<n-button text size="tiny" :disabled="index === 0" @click="moveSortable(index, -1)">↑</n-button>
<n-button text size="tiny" :disabled="index === sortableItems.length - 1" @click="moveSortable(index, 1)">↓</n-button>
</span>
</div>
<div class="aksrt-field__sortable-body">
<AksField v-for="field in item.fields || []" :key="field.key" :schema="field" :model-value="modelValue?.[item.key]?.[field.key] ?? field.default" @update:model-value="updateSortable(item.key, field.key, $event)" />
</div>
</div>
</div>
<!-- Repeater (array of objects) — inline expand (default) -->
<div v-else-if="schema.type === 'repeater' && schema.ui !== 'list-modal'" class="aksrt-field__repeater">
<div v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui !== 'list-modal'" class="aksrt-field__repeater">
<div
v-for="(item, index) in (modelValue || [])"
:key="item._id || index"
@@ -383,7 +445,7 @@
size="tiny"
type="error"
text
:disabled="schema.disabled"
:disabled="schema.disabled || reachedRepeaterMinimum"
@click="removeRepeaterItem(index)"
>
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
@@ -392,6 +454,7 @@
<div class="aksrt-field__repeater-fields">
<template v-for="field in (schema.fields || [])" :key="field.key">
<AksField
v-if="matchDependency(field.dependsOn, item)"
:schema="field"
:model-value="item[field.key]"
:disabled="schema.disabled"
@@ -403,16 +466,16 @@
<n-button
size="small"
dashed
:disabled="schema.disabled"
:disabled="schema.disabled || reachedRepeaterMaximum"
@click="addRepeaterItem"
>
<FaIcon icon="fa fas fa-plus" :size="12" style="margin-right: 6px" />
{{ schema.addText || '添加一项' }}
{{ schema.button_title || schema.addText || '添加一项' }}
</n-button>
</div>
<!-- Repeater list-modal: compact list + modal editor -->
<div v-else-if="schema.type === 'repeater' && schema.ui === 'list-modal'" class="aksrt-field__repeater aksrt-field__repeater--list-modal">
<div v-else-if="(schema.type === 'repeater' || schema.type === 'group') && schema.ui === 'list-modal'" class="aksrt-field__repeater aksrt-field__repeater--list-modal">
<div v-if="(modelValue || []).length" class="aksrt-field__repeater-list">
<div
v-for="(item, index) in (modelValue || [])"
@@ -452,7 +515,7 @@
size="tiny"
quaternary
type="error"
:disabled="schema.disabled"
:disabled="schema.disabled || reachedRepeaterMinimum"
@click="removeRepeaterItem(index)"
>
<FaIcon icon="fa fas fa-trash-alt" :size="12" />
@@ -488,11 +551,11 @@
size="small"
dashed
block
:disabled="schema.disabled"
:disabled="schema.disabled || reachedRepeaterMaximum"
@click="openRepeaterModal(-1)"
>
<FaIcon icon="fa fas fa-plus" :size="12" style="margin-right: 6px" />
{{ schema.addText || '添加一项' }}
{{ schema.button_title || schema.addText || '添加一项' }}
</n-button>
<n-modal
@@ -505,7 +568,7 @@
>
<div class="aksrt-field__repeater-modal-fields">
<AksField
v-for="field in (schema.fields || [])"
v-for="field in (schema.fields || []).filter(field => matchDependency(field.dependsOn, repeaterDraft))"
:key="field.key"
:schema="field"
:model-value="repeaterDraft[field.key]"
@@ -564,6 +627,10 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue'])
const repeaterLength = computed(() => Array.isArray(props.modelValue) ? props.modelValue.length : 0)
const reachedRepeaterMinimum = computed(() => repeaterLength.value <= Number(props.schema.min || 0))
const reachedRepeaterMaximum = computed(() => Number(props.schema.max || 0) > 0 && repeaterLength.value >= Number(props.schema.max))
function fieldsetValue(field) {
const current = props.modelValue && typeof props.modelValue === 'object' && !Array.isArray(props.modelValue)
? props.modelValue
@@ -584,12 +651,94 @@ const normalizedOptions = computed(() => {
return opts.map(opt => {
if (typeof opt === 'string') return { label: opt, value: opt }
if (typeof opt === 'object' && opt !== null) {
return { label: opt.label ?? opt.name ?? opt.value, value: opt.value ?? opt.label ?? opt.name }
return { ...opt, label: opt.label ?? opt.name ?? opt.value, value: opt.value ?? opt.label ?? opt.name }
}
return { label: String(opt), value: opt }
})
})
const visibleNestedFields = computed(() => (props.schema.fields || []).filter(field => matchDependency(field.dependsOn, props.modelValue)))
const linkTargetOptions = [{ label: '当前窗口', value: '' }, { label: '当前窗口', value: '_self' }, { label: '新窗口', value: '_blank' }]
function matchDependency(rule, sourceValue) {
if (!rule) return true
const source = sourceValue && typeof sourceValue === 'object' ? sourceValue : {}
if (rule.all) return rule.all.every(item => matchDependency(item, source))
if (rule.any) return rule.any.some(item => matchDependency(item, source))
const value = source[rule.key]
if (rule.operator === 'truthy') return !!value
if (rule.operator === 'falsy') return !value
if (rule.operator === 'not_empty') return Array.isArray(value) ? value.length > 0 : value !== '' && value != null
if (rule.operator === 'empty') return Array.isArray(value) ? value.length === 0 : value === '' || value == null
if (rule.operator === 'eq') return value === rule.value
if (rule.operator === 'neq') return value !== rule.value
return rule.value === undefined || value === rule.value
}
function updateLink(key, value) {
emit('update:modelValue', { ...(props.modelValue || {}), [key]: value })
}
function selectedOptionLabel(options, value) {
return options.find(option => option.value === value)?.label || String(value)
}
function moveSelectedValue(index, delta) {
if (!Array.isArray(props.modelValue)) return
const target = index + delta
if (target < 0 || target >= props.modelValue.length) return
const values = [...props.modelValue]
;[values[index], values[target]] = [values[target], values[index]]
emit('update:modelValue', values)
}
const sortableItems = computed(() => {
const definitions = new Map((props.schema.items || []).map(item => [item.key, item]))
const order = Object.keys(props.modelValue || {}).filter(key => definitions.has(key))
for (const key of definitions.keys()) if (!order.includes(key)) order.push(key)
return order.map(key => definitions.get(key))
})
function updateSortable(itemKey, fieldKey, value) {
const current = props.modelValue || {}
emit('update:modelValue', { ...current, [itemKey]: { ...(current[itemKey] || {}), [fieldKey]: value } })
}
function moveSortable(index, delta) {
const items = [...sortableItems.value]
const target = index + delta
if (target < 0 || target >= items.length) return
;[items[index], items[target]] = [items[target], items[index]]
const current = props.modelValue || {}
emit('update:modelValue', Object.fromEntries(items.map(item => [item.key, current[item.key] || {}])))
}
const asyncOptions = ref([])
const asyncLoading = ref(false)
let asyncTimer = null
async function loadAsyncOptions(query = '') {
if (props.schema.source === 'publish-types') {
asyncOptions.value = normalizedOptions.value
return
}
asyncLoading.value = true
try {
const selected = Array.isArray(props.modelValue) ? props.modelValue : (props.modelValue ? [props.modelValue] : [])
const response = await api.searchPageDisplayOptions(props.schema.source, query, query ? [] : selected)
asyncOptions.value = response?.data || []
} catch (error) {
asyncOptions.value = []
} finally {
asyncLoading.value = false
}
}
function handleAsyncSearch(query) {
clearTimeout(asyncTimer)
asyncTimer = setTimeout(() => loadAsyncOptions(query), 250)
}
// ============ Taxonomy Select (async) ============
@@ -621,6 +770,9 @@ onMounted(() => {
if (props.schema.type === 'user_select') {
loadInitialUsers()
}
if (props.schema.type === 'async_select') {
loadAsyncOptions()
}
})
// ============ User Select (async search) ============
@@ -740,12 +892,14 @@ function createRepeaterItem() {
}
function addRepeaterItem() {
if (reachedRepeaterMaximum.value) return
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
current.push(createRepeaterItem())
emit('update:modelValue', current)
}
function removeRepeaterItem(index) {
if (reachedRepeaterMinimum.value) return
const current = Array.isArray(props.modelValue) ? [...props.modelValue] : []
current.splice(index, 1)
emit('update:modelValue', current)
@@ -807,6 +961,7 @@ function getRepeaterPreviewDesc(item) {
}
function openRepeaterModal(index) {
if (index < 0 && reachedRepeaterMaximum.value) return
if (props.schema.disabled) return
if (index >= 0) {
const item = (props.modelValue || [])[index] || {}
@@ -998,6 +1153,74 @@ function confirmRepeaterModal() {
text-align: center;
}
.aksrt-field__palette {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.aksrt-field__palette-item {
width: 34px;
height: 26px;
padding: 0;
border: 3px solid #fff;
border-radius: 3px;
box-shadow: 0 0 0 1px #d8d8d8;
cursor: pointer;
}
.aksrt-field__palette-item.is-active {
box-shadow: 0 0 0 2px #2f8fce;
}
.aksrt-field__link {
display: grid;
grid-template-columns: minmax(220px, 1fr) 120px minmax(140px, .5fr);
gap: 8px;
}
.aksrt-field__sortable {
display: grid;
gap: 8px;
}
.aksrt-field__selected-order {
display: grid;
gap: 5px;
margin-top: 8px;
}
.aksrt-field__selected-order-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 9px;
border: 1px solid #e6e8eb;
border-radius: 3px;
background: #fafafa;
}
.aksrt-field__sortable-item {
border: 1px solid #e5e5e5;
background: #fff;
}
.aksrt-field__sortable-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: #f7f7f7;
cursor: move;
}
.aksrt-field__sortable-body {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
padding: 12px;
}
/* Repeater */
.aksrt-field__fieldset {
padding: 14px;
@@ -1059,6 +1282,11 @@ function confirmRepeaterModal() {
flex-direction: column;
align-items: flex-start;
}
.aksrt-field__link,
.aksrt-field__sortable-body {
grid-template-columns: 1fr;
}
}
/* Repeater list-modal */
@@ -172,8 +172,16 @@ function matchDependsOn(dependsOn) {
return !!props.formData[dependsOn]
}
if (typeof dependsOn === 'object') {
const { key, value, values, not } = dependsOn
if (Array.isArray(dependsOn.all)) return dependsOn.all.every(matchDependsOn)
if (Array.isArray(dependsOn.any)) return dependsOn.any.some(matchDependsOn)
const { key, value, values, not, operator } = dependsOn
const currentVal = props.formData[key]
if (operator === 'truthy') return !!currentVal
if (operator === 'falsy') return !currentVal
if (operator === 'not_empty') return Array.isArray(currentVal) ? currentVal.length > 0 : currentVal !== '' && currentVal != null
if (operator === 'empty') return Array.isArray(currentVal) ? currentVal.length === 0 : currentVal === '' || currentVal == null
if (operator === 'eq') return currentVal === value
if (operator === 'neq') return currentVal !== value
if (not) {
if (Array.isArray(values)) {
// eslint-disable-next-line eqeqeq
@@ -1,313 +0,0 @@
<template>
<div class="aksrt-module-canvas">
<div class="aksrt-module-canvas__toolbar">
<n-space>
<n-dropdown :options="addModuleOptions" trigger="click" @select="handleAddModule">
<n-button type="primary" size="small">
<template #icon><FaIcon icon="fa fas fa-plus" :size="12" /></template>
添加模块
</n-button>
</n-dropdown>
<n-button size="small" @click="$emit('pick-preset')">
<template #icon><FaIcon icon="fa fas fa-magic" :size="12" /></template>
应用预设
</n-button>
<n-button size="small" @click="$emit('export')">
<template #icon><FaIcon icon="fa fas fa-download" :size="12" /></template>
导出
</n-button>
<n-button size="small" @click="$emit('import')">
<template #icon><FaIcon icon="fa fas fa-upload" :size="12" /></template>
导入
</n-button>
</n-space>
</div>
<div v-if="modules.length === 0" class="aksrt-module-canvas__empty">
<FaIcon icon="fa fas fa-th-large" :size="32" color="#c0c4cc" />
<p>暂无首页模块,点击"添加模块"或"应用预设"开始配置。</p>
</div>
<div v-else class="aksrt-module-canvas__list">
<div
v-for="(mod, index) in modules"
:key="mod.id || index"
class="aksrt-module-canvas__item"
:class="{
'aksrt-module-canvas__item--selected': selectedIndex === index,
'aksrt-module-canvas__item--disabled': !mod.enabled,
'aksrt-module-canvas__item--dragging': dragIndex === index,
'aksrt-module-canvas__item--drag-over': dragOverIndex === index && dragOverIndex !== dragIndex,
}"
:draggable="true"
@click="$emit('select', index)"
@dragstart="handleDragStart(index, $event)"
@dragover.prevent="handleDragOver(index)"
@dragleave="handleDragLeave"
@drop.prevent="handleDrop(index)"
@dragend="handleDragEnd"
>
<div class="aksrt-module-canvas__item-handle" title="拖拽排序">
<FaIcon icon="fa fas fa-bars" :size="12" color="#c0c4cc" />
</div>
<div class="aksrt-module-canvas__item-icon">
<FaIcon :icon="getModuleIcon(mod.type)" :size="14" />
</div>
<div class="aksrt-module-canvas__item-info">
<span class="aksrt-module-canvas__item-label">{{ getModuleLabel(mod.type) }}</span>
<span v-if="mod.title" class="aksrt-module-canvas__item-title">{{ mod.title }}</span>
</div>
<div class="aksrt-module-canvas__item-actions">
<n-switch
:value="mod.enabled !== false"
size="small"
@update:value="(val) => $emit('toggle', index, val)"
@click.stop
/>
<n-button
size="tiny"
quaternary
title="复制"
@click.stop="$emit('copy', index)"
>
<FaIcon icon="fa fas fa-copy" :size="11" />
</n-button>
<n-button
size="tiny"
quaternary
type="error"
title="删除"
@click.stop="$emit('remove', index)"
>
<FaIcon icon="fa fas fa-trash-alt" :size="11" />
</n-button>
<n-button
size="tiny"
quaternary
:disabled="index === 0"
title="上移"
@click.stop="$emit('move', index, -1)"
>
<FaIcon icon="fa fas fa-chevron-up" :size="11" />
</n-button>
<n-button
size="tiny"
quaternary
:disabled="index === modules.length - 1"
title="下移"
@click.stop="$emit('move', index, 1)"
>
<FaIcon icon="fa fas fa-chevron-down" :size="11" />
</n-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { MODULE_LIBRARY } from '@/utils/home-module-schema'
import FaIcon from '@/components/form/FaIcon.vue'
const props = defineProps({
modules: { type: Array, default: () => [] },
selectedIndex: { type: Number, default: -1 },
})
const emit = defineEmits(['select', 'add', 'remove', 'copy', 'toggle', 'move', 'pick-preset', 'export', 'import'])
// Drag-and-drop state
const dragIndex = ref(-1)
const dragOverIndex = ref(-1)
function handleDragStart(index, event) {
dragIndex.value = index
event.dataTransfer.effectAllowed = 'move'
event.dataTransfer.setData('text/plain', String(index))
}
function handleDragOver(index) {
if (dragIndex.value !== index) {
dragOverIndex.value = index
}
}
function handleDragLeave() {
dragOverIndex.value = -1
}
function handleDrop(targetIndex) {
if (dragIndex.value >= 0 && dragIndex.value !== targetIndex) {
const offset = targetIndex - dragIndex.value
emit('move', dragIndex.value, offset)
}
dragIndex.value = -1
dragOverIndex.value = -1
}
function handleDragEnd() {
dragIndex.value = -1
dragOverIndex.value = -1
}
const addModuleOptions = computed(() =>
MODULE_LIBRARY.map(m => ({
label: m.label,
key: m.type,
icon: () => {
const iconMap = {
image: 'fa fas fa-image', rectangle: 'fa fas fa-square', folder: 'fa fas fa-folder',
switcher: 'fa fas fa-columns', trophy: 'fa fas fa-trophy', clock: 'fa fas fa-clock',
tag: 'fa fas fa-tag', link: 'fa fas fa-link', code: 'fa fas fa-code', fire: 'fa fas fa-fire',
grid: 'fa fas fa-th', 'font-colors': 'fa fas fa-font', 'th-large': 'fa fas fa-th-large',
swap: 'fa fas fa-arrows-alt-v', 'question-circle': 'fa fas fa-question-circle',
table: 'fa fas fa-table', star: 'fa fas fa-star', handshake: 'fa fas fa-handshake',
'shopping-bag': 'fa fas fa-shopping-bag',
}
return h('i', { class: iconMap[m.icon] || 'fa fas fa-cube', style: 'font-size:12px;margin-right:6px;' })
},
}))
)
function getModuleIcon(type) {
const meta = MODULE_LIBRARY.find(m => m.type === type)
const iconMap = {
image: 'fa fas fa-image', rectangle: 'fa fas fa-square', folder: 'fa fas fa-folder',
switcher: 'fa fas fa-columns', trophy: 'fa fas fa-trophy', clock: 'fa fas fa-clock',
tag: 'fa fas fa-tag', link: 'fa fas fa-link', code: 'fa fas fa-code', fire: 'fa fas fa-fire',
grid: 'fa fas fa-th', 'font-colors': 'fa fas fa-font', 'th-large': 'fa fas fa-th-large',
swap: 'fa fas fa-arrows-alt-v', 'question-circle': 'fa fas fa-question-circle',
table: 'fa fas fa-table', star: 'fa fas fa-star', handshake: 'fa fas fa-handshake',
'shopping-bag': 'fa fas fa-shopping-bag',
}
return iconMap[meta?.icon] || 'fa fas fa-cube'
}
function getModuleLabel(type) {
const meta = MODULE_LIBRARY.find(m => m.type === type)
return meta?.label || type
}
function handleAddModule(type) {
emit('add', type)
}
</script>
<script>
import { h } from 'vue'
export default {
// h is used in addModuleOptions computed icon renderer
}
</script>
<style scoped>
.aksrt-module-canvas__toolbar {
margin-bottom: 12px;
}
.aksrt-module-canvas__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
border: 2px dashed #e0e0e0;
border-radius: 8px;
color: #999;
gap: 12px;
}
.aksrt-module-canvas__empty p {
margin: 0;
font-size: 13px;
}
.aksrt-module-canvas__list {
display: flex;
flex-direction: column;
gap: 4px;
}
.aksrt-module-canvas__item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: 1px solid #f0f0f0;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
background: #fff;
}
.aksrt-module-canvas__item:hover {
border-color: #d0d0d0;
background: #fafafa;
}
.aksrt-module-canvas__item--selected {
border-color: #1890ff;
background: #e6f4ff;
}
.aksrt-module-canvas__item--dragging {
opacity: 0.5;
border-style: dashed;
}
.aksrt-module-canvas__item--drag-over {
border-color: #1890ff;
border-width: 2px;
background: #f0f7ff;
}
.aksrt-module-canvas__item--disabled {
opacity: 0.5;
}
.aksrt-module-canvas__item-handle {
cursor: grab;
padding: 2px;
}
.aksrt-module-canvas__item-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
background: #f5f7fa;
color: #666;
flex-shrink: 0;
}
.aksrt-module-canvas__item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
.aksrt-module-canvas__item-label {
font-size: 13px;
font-weight: 500;
color: #333;
}
.aksrt-module-canvas__item-title {
font-size: 11px;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.aksrt-module-canvas__item-actions {
display: flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
}
</style>
@@ -1,399 +0,0 @@
<template>
<div class="aksrt-module-inspector">
<div v-if="!module" class="aksrt-module-inspector__empty">
<FaIcon icon="fa fas fa-mouse-pointer" :size="24" color="#c0c4cc" />
<p>选择左侧模块查看和编辑配置</p>
</div>
<template v-else>
<div class="aksrt-module-inspector__header">
<FaIcon :icon="getModuleIcon(module.type)" :size="16" />
<span class="aksrt-module-inspector__type">{{ getModuleLabel(module.type) }}</span>
</div>
<n-scrollbar style="max-height: calc(100vh - 320px);">
<n-form
:model="localConfig"
label-placement="top"
size="small"
class="aksrt-module-inspector__form"
>
<!-- Common fields -->
<n-form-item label="模块标题">
<n-input
:value="module.title || ''"
placeholder="留空使用默认标题"
@update:value="(val) => updateField('title', val)"
/>
</n-form-item>
<n-form-item label="启用模块">
<n-switch
:value="module.enabled !== false"
@update:value="(val) => updateField('enabled', val)"
/>
</n-form-item>
<n-divider />
<!-- Type-specific fields from schema -->
<template v-if="metaFields.length > 0">
<n-form-item
v-for="field in metaFields"
:key="field.key"
:label="field.label"
>
<!-- Switch -->
<n-switch
v-if="field.type === 'switch'"
:value="localConfig[field.key] ?? field.default ?? false"
@update:value="(val) => updateConfig(field.key, val)"
/>
<!-- Number -->
<n-input-number
v-else-if="field.type === 'number'"
:value="localConfig[field.key] ?? field.default ?? 0"
:min="field.min"
:max="field.max"
style="width: 100%;"
@update:value="(val) => updateConfig(field.key, val)"
/>
<!-- Text -->
<n-input
v-else-if="field.type === 'text'"
:value="localConfig[field.key] ?? field.default ?? ''"
:placeholder="field.placeholder || ''"
@update:value="(val) => updateConfig(field.key, val)"
/>
<!-- Textarea -->
<n-input
v-else-if="field.type === 'textarea'"
type="textarea"
:value="localConfig[field.key] ?? field.default ?? ''"
:rows="field.rows || 3"
@update:value="(val) => updateConfig(field.key, val)"
/>
<!-- Select -->
<n-select
v-else-if="field.type === 'select'"
:value="localConfig[field.key] ?? field.default"
:options="field.options || []"
@update:value="(val) => updateConfig(field.key, val)"
/>
<!-- Radio -->
<n-radio-group
v-else-if="field.type === 'radio'"
:value="localConfig[field.key] ?? field.default"
@update:value="(val) => updateConfig(field.key, val)"
>
<n-space>
<n-radio
v-for="opt in field.options"
:key="opt.value"
:value="opt.value"
>
{{ opt.label }}
</n-radio>
</n-space>
</n-radio-group>
<!-- Category (single) -->
<n-select
v-else-if="field.type === 'cat'"
:value="localConfig[field.key] ?? field.default ?? 0"
:options="categoryOptions"
filterable
placeholder="选择分类"
@update:value="(val) => updateConfig(field.key, val)"
/>
<!-- Categories (multiple) -->
<n-select
v-else-if="field.type === 'cats'"
:value="localConfig[field.key] ?? field.default ?? []"
:options="categoryOptions"
multiple
filterable
placeholder="选择分类"
@update:value="(val) => updateConfig(field.key, val)"
/>
<!-- Media Picker -->
<div v-else-if="field.type === 'media'" class="aksrt-module-inspector__media">
<div v-if="localConfig[field.key]" class="aksrt-module-inspector__media-preview">
<img :src="localConfig[field.key]" alt="" />
<n-button size="tiny" quaternary type="error" @click="updateConfig(field.key, '')">
移除
</n-button>
</div>
<n-input
v-else
:value="localConfig[field.key] ?? ''"
placeholder="输入图片 URL 或点击下方选择"
@update:value="(val) => updateConfig(field.key, val)"
/>
<n-button size="tiny" quaternary @click="openMediaPicker(field.key)">
<template #icon><FaIcon icon="fa fas fa-image" :size="12" /></template>
从媒体库选择
</n-button>
</div>
<!-- Repeater -->
<div v-else-if="field.type === 'repeater'" class="aksrt-module-inspector__repeater">
<div
v-for="(item, idx) in (localConfig[field.key] ?? [])"
:key="idx"
class="aksrt-module-inspector__repeater-item"
>
<div class="aksrt-module-inspector__repeater-fields">
<n-input
v-for="sub in (field.fields || [])"
:key="sub.key"
:value="item[sub.key] ?? ''"
:placeholder="sub.placeholder || sub.label || ''"
size="small"
@update:value="(val) => updateRepeaterItem(field.key, idx, sub.key, val)"
/>
</div>
<n-button size="tiny" quaternary type="error" @click="removeRepeaterItem(field.key, idx)">
<FaIcon icon="fa fas fa-times" :size="10" />
</n-button>
</div>
<n-button size="tiny" quaternary @click="addRepeaterItem(field.key, field.fields)">
<template #icon><FaIcon icon="fa fas fa-plus" :size="10" /></template>
添加一项
</n-button>
</div>
</n-form-item>
</template>
<n-text v-else depth="3" style="font-size: 12px;">
此模块类型暂无可配置字段。
</n-text>
</n-form>
</n-scrollbar>
</template>
<!-- Image URL input modal (replaces native prompt) -->
<n-modal v-model:show="showImageUrlInput" preset="card" title="输入图片 URL" style="max-width: 400px;" :bordered="false">
<n-input v-model:value="imageUrlValue" placeholder="https://example.com/image.jpg" @keyup.enter="confirmImageUrl" />
<template #footer>
<n-space justify="end">
<n-button @click="showImageUrlInput = false">取消</n-button>
<n-button type="primary" @click="confirmImageUrl">确定</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script setup>
import { computed, ref, watch, onMounted } from 'vue'
import { MODULE_LIBRARY } from '@/utils/home-module-schema'
import FaIcon from '@/components/form/FaIcon.vue'
import api from '@/api/index.js'
const props = defineProps({
module: { type: Object, default: null },
moduleIndex: { type: Number, default: -1 },
})
const emit = defineEmits(['update'])
const categoryOptions = ref([])
const meta = computed(() => {
if (!props.module) return null
return MODULE_LIBRARY.find(m => m.type === props.module.type) || null
})
const metaFields = computed(() => {
return meta.value?.fields || []
})
const localConfig = computed(() => {
return props.module?.config || {}
})
function getModuleIcon(type) {
const m = MODULE_LIBRARY.find(x => x.type === type)
const iconMap = {
image: 'fa fas fa-image', rectangle: 'fa fas fa-square', folder: 'fa fas fa-folder',
switcher: 'fa fas fa-columns', trophy: 'fa fas fa-trophy', clock: 'fa fas fa-clock',
tag: 'fa fas fa-tag', link: 'fa fas fa-link', code: 'fa fas fa-code', fire: 'fa fas fa-fire',
grid: 'fa fas fa-th', 'font-colors': 'fa fas fa-font', 'th-large': 'fa fas fa-th-large',
swap: 'fa fas fa-arrows-alt-v', 'question-circle': 'fa fas fa-question-circle',
table: 'fa fas fa-table', star: 'fa fas fa-star', handshake: 'fa fas fa-handshake',
'shopping-bag': 'fa fas fa-shopping-bag',
}
return iconMap[m?.icon] || 'fa fas fa-cube'
}
function getModuleLabel(type) {
const m = MODULE_LIBRARY.find(x => x.type === type)
return m?.label || type
}
function updateField(key, value) {
emit('update', props.moduleIndex, key, value)
}
function updateConfig(key, value) {
const newConfig = { ...(props.module?.config || {}), [key]: value }
emit('update', props.moduleIndex, 'config', newConfig)
}
// Image URL input state (replaces native prompt)
const showImageUrlInput = ref(false)
const imageUrlFieldKey = ref('')
const imageUrlValue = ref('')
function openMediaPicker(fieldKey) {
if (!window.wp || !window.wp.media) {
// Fallback: show inline input instead of native prompt
imageUrlFieldKey.value = fieldKey
imageUrlValue.value = localConfig.value[fieldKey] || ''
showImageUrlInput.value = true
return
}
const frame = wp.media({
title: '选择图片',
button: { text: '使用此图片' },
multiple: false,
library: { type: 'image' },
})
frame.on('select', function () {
const attachment = frame.state().get('selection').first().toJSON()
updateConfig(fieldKey, attachment.url)
})
frame.open()
}
function confirmImageUrl() {
if (imageUrlValue.value.trim()) {
updateConfig(imageUrlFieldKey.value, imageUrlValue.value.trim())
}
showImageUrlInput.value = false
}
function addRepeaterItem(fieldKey, subFields) {
const current = [...(props.module?.config?.[fieldKey] || [])]
const newItem = {}
;(subFields || []).forEach(f => { newItem[f.key] = f.default ?? '' })
current.push(newItem)
updateConfig(fieldKey, current)
}
function removeRepeaterItem(fieldKey, index) {
const current = [...(props.module?.config?.[fieldKey] || [])]
current.splice(index, 1)
updateConfig(fieldKey, current)
}
function updateRepeaterItem(fieldKey, index, subKey, value) {
const current = [...(props.module?.config?.[fieldKey] || [])]
if (current[index]) {
current[index] = { ...current[index], [subKey]: value }
updateConfig(fieldKey, current)
}
}
async function loadCategories() {
try {
const res = await api.getHomepageCategories()
const cats = res?.data || []
categoryOptions.value = cats.map(c => ({
label: `${c.name} (${c.count})`,
value: c.term_id,
}))
} catch { /* non-critical */ }
}
onMounted(loadCategories)
</script>
<style scoped>
.aksrt-module-inspector__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
color: #999;
gap: 12px;
}
.aksrt-module-inspector__empty p {
margin: 0;
font-size: 13px;
}
.aksrt-module-inspector__header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 12px;
}
.aksrt-module-inspector__type {
font-size: 15px;
font-weight: 600;
color: #333;
}
.aksrt-module-inspector__form {
padding: 0 4px;
}
.aksrt-module-inspector__media {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.aksrt-module-inspector__media-preview {
display: flex;
align-items: center;
gap: 8px;
}
.aksrt-module-inspector__media-preview img {
width: 80px;
height: 60px;
object-fit: cover;
border-radius: 4px;
border: 1px solid #e8e8e8;
}
.aksrt-module-inspector__repeater {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.aksrt-module-inspector__repeater-item {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 8px;
border-radius: 6px;
background: #fafafa;
border: 1px solid #f0f0f0;
}
.aksrt-module-inspector__repeater-fields {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
</style>
@@ -1,143 +0,0 @@
<template>
<n-modal
:show="show"
title="应用预设模板"
preset="dialog"
style="width: 560px;"
:show-icon="false"
@close="$emit('close')"
>
<div class="aksrt-preset-picker">
<p class="aksrt-preset-picker__hint">
选择一个预设模板快速配置首页模块。应用预设将替换当前所有模块。
</p>
<div class="aksrt-preset-picker__list">
<div
v-for="preset in presets"
:key="preset.key"
class="aksrt-preset-picker__item"
:class="{ 'aksrt-preset-picker__item--selected': selectedKey === preset.key }"
@click="selectedKey = preset.key"
>
<div class="aksrt-preset-picker__item-header">
<strong>{{ preset.label }}</strong>
<n-tag size="small" type="info">{{ preset.modules.length }} 个模块</n-tag>
</div>
<p class="aksrt-preset-picker__item-desc">{{ preset.desc }}</p>
<div class="aksrt-preset-picker__item-modules">
<n-tag
v-for="(mod, i) in preset.modules"
:key="i"
size="small"
:bordered="false"
class="aksrt-preset-picker__module-tag"
>
{{ getModuleLabel(mod.type) }}
</n-tag>
</div>
</div>
</div>
</div>
<template #action>
<n-space>
<n-button @click="$emit('close')">取消</n-button>
<n-button
type="primary"
:disabled="!selectedKey"
@click="handleApply"
>
应用预设
</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup>
import { ref } from 'vue'
import { PRESET_TEMPLATES, MODULE_LIBRARY } from '@/utils/home-module-schema'
defineProps({
show: { type: Boolean, default: false },
})
const emit = defineEmits(['close', 'apply'])
const selectedKey = ref('')
const presets = PRESET_TEMPLATES
function getModuleLabel(type) {
const m = MODULE_LIBRARY.find(x => x.type === type)
return m?.label || type
}
function handleApply() {
const preset = presets.find(p => p.key === selectedKey.value)
if (preset) {
emit('apply', preset)
selectedKey.value = ''
}
}
</script>
<style scoped>
.aksrt-preset-picker__hint {
margin: 0 0 16px;
font-size: 13px;
color: #666;
line-height: 1.6;
}
.aksrt-preset-picker__list {
display: flex;
flex-direction: column;
gap: 8px;
}
.aksrt-preset-picker__item {
padding: 12px;
border: 1px solid #f0f0f0;
border-radius: 8px;
cursor: pointer;
transition: all 0.15s;
}
.aksrt-preset-picker__item:hover {
border-color: #d0d0d0;
background: #fafafa;
}
.aksrt-preset-picker__item--selected {
border-color: #1890ff;
background: #e6f4ff;
}
.aksrt-preset-picker__item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 4px;
}
.aksrt-preset-picker__item-header strong {
font-size: 14px;
color: #333;
}
.aksrt-preset-picker__item-desc {
margin: 0 0 8px;
font-size: 12px;
color: #888;
}
.aksrt-preset-picker__item-modules {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.aksrt-preset-picker__module-tag {
font-size: 11px;
}
</style>
@@ -1,119 +1,28 @@
import { renderFaIcon, schemaPage, customPage } from './helpers'
import { renderFaIcon, schemaPage } from './helpers'
const pages = [
['header-nav', '顶部导航栏'],
['header-components', '顶部多功能组件'],
['footer', '底部页脚'],
['homepage', '首页配置'],
['category', '分类页面'],
['tag', '标签页面'],
['user-home', '用户主页'],
['other', '其它页面'],
]
export const pageDisplaySection = {
key: 'page-display',
label: '页面&显示',
icon: renderFaIcon('fa fas fa-desktop'),
route: '/page-display',
children: [
schemaPage({
key: 'page-display-header-nav',
name: 'PageDisplayHeaderNav',
label: '顶部导航栏',
route: '/page-display/header-nav',
title: '顶部导航栏',
desc: '导航栏布局、菜单位置、LOGO 与搜索入口配置。',
canonicalDomain: 'page_display',
section: 'header-nav',
}),
schemaPage({
key: 'page-display-background',
name: 'PageDisplayBackground',
label: '全局背景',
route: '/page-display/background',
title: '全局背景',
desc: '配置站点全局背景图、背景色、暗色模式背景和遮罩。',
canonicalDomain: 'page_display',
section: 'background',
}),
schemaPage({
key: 'page-display-menu-decoration',
name: 'PageDisplayMenuDecoration',
label: '菜单装饰',
route: '/page-display/menu-decoration',
title: '菜单装饰',
desc: '配置导航菜单的图标、徽章、下拉样式和图文卡片子菜单。',
canonicalDomain: 'page_display',
section: 'menu-decoration',
}),
schemaPage({
key: 'page-display-header-components',
name: 'PageDisplayHeaderComponents',
label: '顶部多功能组件',
route: '/page-display/header-components',
title: '顶部多功能组件',
desc: '配置顶部组件显示范围、幻灯片内容、轮播参数、搜索覆盖层与图标卡片。',
canonicalDomain: 'page_display',
section: 'header-components',
}),
customPage({
key: 'page-display-banner',
name: 'PageDisplayBanner',
label: 'Banner 轮播',
route: '/page-display/banner',
title: 'Banner 管理',
desc: '管理首页内容流 Banner 幻灯片(图片、文案、跳转、启用状态)。',
component: 'BannerManager',
canonicalDomain: 'page_display',
section: 'banner',
}),
customPage({
key: 'page-display-footer',
name: 'PageDisplayFooter',
label: '底部页脚',
route: '/page-display/footer',
title: '底部页脚',
desc: '页脚布局、友链、版权信息配置。',
component: 'FooterSettings',
}),
customPage({
key: 'page-display-homepage',
name: 'PageDisplayHomepage',
label: '首页配置',
route: '/page-display/homepage',
title: '首页配置',
desc: '首页内容模型、主列表、分类 Tab 与首页交互配置。',
component: 'HomepageSettings',
}),
schemaPage({
key: 'page-display-category',
name: 'PageDisplayCategory',
label: '分类页面',
route: '/page-display/category',
title: '分类页面',
desc: '分类归档页的头图、简介、列表模式、侧栏配置。',
canonicalDomain: 'page_display',
section: 'category',
}),
schemaPage({
key: 'page-display-tag',
name: 'PageDisplayTag',
label: '标签页面',
route: '/page-display/tag',
title: '标签页面',
desc: '标签归档页的展示样式与配置。',
canonicalDomain: 'page_display',
section: 'tag',
}),
schemaPage({
key: 'page-display-user-home',
name: 'PageDisplayUserHome',
label: '用户主页',
route: '/page-display/user-home',
title: '用户主页',
desc: '用户公开主页的布局与展示配置。',
canonicalDomain: 'page_display',
section: 'user-home',
}),
schemaPage({
key: 'page-display-other',
name: 'PageDisplayOther',
label: '其它页面',
route: '/page-display/other',
title: '其它页面',
desc: '404 页面、搜索结果页等其它页面配置。',
canonicalDomain: 'page_display',
section: 'other',
}),
],
children: pages.map(([section, label]) => schemaPage({
key: `page-display-${section}`,
name: `PageDisplay${section.split('-').map(part => part[0].toUpperCase() + part.slice(1)).join('')}`,
label,
route: `/page-display/${section}`,
title: label,
canonicalDomain: 'page_display',
section,
})),
}
+17 -5
View File
@@ -130,6 +130,7 @@ const message = useMessage()
const showTestModal = ref(false)
const testLoading = ref(false)
const testResult = ref(null)
const serverSchema = ref(null)
/** Resolve domain/section from props or route meta (canonicalDomain + section, with legacy fallback) */
const resolvedDomain = computed(() => normalizeSchemaDomain(props.domain || route.meta?.canonicalDomain || route.meta?.schemaDomain || resolveDomainFromPath(route.path)))
@@ -150,7 +151,9 @@ function resolveSectionFromPath(path) {
/** Load schema from the Vue-side registry */
const pageMeta = computed(() => {
const schema = getSchema(resolvedDomain.value, resolvedSection.value)
const schema = resolvedDomain.value === 'page_display'
? serverSchema.value
: getSchema(resolvedDomain.value, resolvedSection.value)
if (schema && schema.groups && schema.groups.length > 0) {
return {
title: schema.title || '',
@@ -170,7 +173,9 @@ const pageMeta = computed(() => {
/** Default values from schema */
const defaults = computed(() => {
const schema = getSchema(resolvedDomain.value, resolvedSection.value)
const schema = resolvedDomain.value === 'page_display'
? serverSchema.value
: getSchema(resolvedDomain.value, resolvedSection.value)
if (!schema) return {}
const d = {}
for (const group of schema.groups || []) {
@@ -188,7 +193,13 @@ const {
formData, loading, saving, loadError, dirty,
save: doSave, reload,
} = useSectionForm({
fetch: () => api.getSchemaPage(resolvedDomain.value, resolvedSection.value),
fetch: async () => {
const response = await api.getSchemaPage(resolvedDomain.value, resolvedSection.value)
if (resolvedDomain.value === 'page_display') {
serverSchema.value = response?.schema || null
}
return response
},
submit: (data) => api.saveSchemaPage(resolvedDomain.value, resolvedSection.value, data),
defaults: defaults.value,
onSaved: () => {
@@ -426,6 +437,7 @@ function closeTestModal() {
watch([resolvedDomain, resolvedSection], ([nextDomain, nextSection], [prevDomain, prevSection]) => {
if (nextDomain !== prevDomain || nextSection !== prevSection) {
closeTestModal()
serverSchema.value = null
reload()
}
})
@@ -437,8 +449,8 @@ onBeforeUnmount(() => {
<style scoped>
.aksrt-schema-page {
max-width: 1100px;
margin: 0 auto;
max-width: none;
margin: 0;
}
.aksrt-schema-page__tool-actions {
@@ -1,403 +0,0 @@
<template>
<div class="aksrt-page-container content-page">
<PageHeader
title="Banner 管理"
desc="管理首页内容流 Banner 幻灯片(图片、文案、跳转、启用状态)。"
/>
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
<template #header>
<div class="aksrt-card-header-row">
<strong>Banner 显示开关</strong>
<n-button size="small" secondary @click="goHeaderComponents">顶部多功能组件(轮播参数)</n-button>
</div>
</template>
<n-form label-placement="left" label-width="180" class="aksrt-form">
<n-form-item label="显示首页 Banner">
<div class="aksrt-inline-setting">
<n-switch v-model:value="bannerEnabled" :disabled="settingsLoading || settingsSaving" />
<span class="aksrt-inline-setting__hint">关闭后,首页 Banner 区块将不再渲染。</span>
</div>
</n-form-item>
</n-form>
<div class="aksrt-inline-setting__actions">
<n-button type="primary" :loading="settingsSaving" :disabled="settingsLoading" @click="saveBannerSettings">保存 Banner 开关</n-button>
</div>
</n-card>
<n-spin :show="loading" style="width:100%; min-height:200px;">
<n-card class="aksrt-section-card">
<template #header>
<div class="aksrt-card-header-row">
<div class="aksrt-card-header-row__left">
<span class="header-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:18px;height:18px" aria-hidden="true">
<rect width="18" height="14" x="3" y="5" rx="2"/>
<path d="M3 10h18"/>
</svg>
</span>
<strong class="aksrt-card-header-row__title">Banner 列表 ({{ banners.length }})</strong>
</div>
<n-button type="primary" size="small" @click="openCreate">+ 添加 Banner</n-button>
</div>
</template>
<n-data-table
v-if="banners.length"
:columns="columns"
:data="banners"
:row-key="(row) => row.id"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无 Banner,添加后即可在首页使用。" />
</n-card>
</n-spin>
<n-modal
v-model:show="modalShow"
:mask-closable="false"
preset="card"
:title="editing.id ? '编辑 Banner' : '添加 Banner'"
class="aksrt-modal-card"
>
<n-form :model="editing" label-placement="top" class="aksrt-form">
<n-form-item label="标题">
<n-input v-model:value="editing.title" placeholder="开启文字后显示的主标题" />
</n-form-item>
<n-form-item label="副标题">
<n-input v-model:value="editing.subtitle" placeholder="开启文字后显示的副标题" />
</n-form-item>
<n-form-item label="Banner 图片">
<n-input-group>
<n-input v-model:value="editing.image_url" placeholder="https://..." />
<n-button secondary @click="pickImage">媒体库</n-button>
</n-input-group>
</n-form-item>
<div v-if="editing.image_url" class="aksrt-modal-preview">
<n-image :src="editing.image_url" width="100%" height="160" object-fit="cover" :preview-src="editing.image_url" />
</div>
<n-form-item label="跳转链接">
<n-input v-model:value="editing.link_url" placeholder="点击后跳转地址,可留空" />
</n-form-item>
<div class="aksrt-modal-grid">
<n-form-item label="显示文字">
<n-switch v-model:value="editing.show_text" />
</n-form-item>
<n-form-item label="启用">
<n-switch v-model:value="editing.enabled" />
</n-form-item>
<n-form-item label="显示元信息">
<n-switch v-model:value="editing.show_meta" />
</n-form-item>
<n-form-item label="排序">
<n-input-number v-model:value="editing.sort_order" :min="0" style="width:100%;" />
</n-form-item>
</div>
</n-form>
<template #footer>
<div class="aksrt-modal-footer">
<n-button @click="modalShow = false">取消</n-button>
<n-button type="primary" :loading="saving" @click="handleSave">保存</n-button>
</div>
</template>
</n-modal>
</div>
</template>
<script setup>
import { h, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { NButton, NImage, NTag, useDialog, useMessage } from 'naive-ui'
import api from '@/api'
import { openMediaPicker } from '@/utils/media'
import PageHeader from '@/components/PageHeader.vue'
const router = useRouter()
const message = useMessage()
const dialog = useDialog()
function goHeaderComponents() {
router.push('/page-display/header-components')
}
const loading = ref(true)
const saving = ref(false)
const settingsLoading = ref(false)
const settingsSaving = ref(false)
const banners = ref([])
const bannerEnabled = ref(true)
const modalShow = ref(false)
const editing = ref(emptyForm())
function emptyForm() {
return {
id: 0,
title: '',
subtitle: '',
image_url: '',
link_url: '',
show_text: true,
enabled: true,
show_meta: false,
sort_order: 0,
}
}
const columns = [
{
title: '预览',
key: 'image_url',
width: 90,
render(row) {
if (!row.image_url) {
return h('span', { style: 'color:#bbb;font-size:12px;' }, '无')
}
return h(NImage, {
src: row.image_url,
width: 64,
height: 36,
objectFit: 'cover',
previewSrc: row.image_url,
style: 'border-radius:4px;display:block;',
})
},
},
{
title: '标题',
key: 'title',
ellipsis: { tooltip: true },
render(row) {
return row.title || h('span', { style: 'color:#bbb;' }, '未命名')
},
},
{
title: '文字',
key: 'show_text',
width: 70,
className: 'aksrt-table-mobile-hide',
render(row) {
return h(NTag, { size: 'small', type: row.show_text ? 'info' : 'default' }, {
default: () => (row.show_text ? '显示' : '隐藏'),
})
},
},
{
title: '排序',
key: 'sort_order',
width: 64,
className: 'aksrt-table-mobile-hide',
},
{
title: '元信息',
key: 'show_meta',
width: 78,
className: 'aksrt-table-mobile-hide',
render(row) {
return h(NTag, { size: 'small', type: row.show_meta ? 'info' : 'default' }, {
default: () => (row.show_meta ? '显示' : '隐藏'),
})
},
},
{
title: '状态',
key: 'enabled',
width: 84,
render(row) {
return h(NTag, { size: 'small', type: row.enabled ? 'success' : 'default' }, {
default: () => (row.enabled ? '启用' : '禁用'),
})
},
},
{
title: '操作',
key: 'actions',
width: 130,
render(row) {
return h('div', { style: 'display:flex;gap:6px;' }, [
h(NButton, { size: 'tiny', onClick: () => openEdit(row) }, { default: () => '编辑' }),
h(NButton, { size: 'tiny', type: 'error', quaternary: true, onClick: () => handleDelete(row) }, { default: () => '删除' }),
])
},
},
]
async function loadBanners() {
try {
loading.value = true
const res = await api.getHomepageBanners()
if (res.code === 'success') {
banners.value = res.data || []
}
} catch (e) {
message.error(e.message || '加载失败')
} finally {
loading.value = false
}
}
async function loadBannerSettings() {
settingsLoading.value = true
try {
const res = await api.getHomepageSettings()
const data = res?.data || {}
bannerEnabled.value = data.homepage_module_banner_enabled !== false
} catch (e) {
message.error(e.message || '加载 Banner 开关失败')
} finally {
settingsLoading.value = false
}
}
async function saveBannerSettings() {
settingsSaving.value = true
try {
const res = await api.saveHomepageSettings({
homepage_module_banner_enabled: bannerEnabled.value,
})
if (res?.code === 'success') {
message.success('Banner 开关已保存')
return
}
throw new Error(res?.message || '保存 Banner 开关失败')
} catch (e) {
message.error(e.message || '保存 Banner 开关失败')
} finally {
settingsSaving.value = false
}
}
function openCreate() {
editing.value = emptyForm()
modalShow.value = true
}
function openEdit(row) {
editing.value = {
id: row.id,
title: row.title || '',
subtitle: row.subtitle || '',
image_url: row.image_url || '',
link_url: row.link_url || '',
show_text: row.show_text !== false,
enabled: !!row.enabled,
show_meta: row.show_meta === true,
sort_order: Number(row.sort_order) || 0,
}
modalShow.value = true
}
async function pickImage() {
try {
const media = await openMediaPicker({
title: '选择 Banner 图片',
buttonText: '使用这张 Banner 图',
})
if (media?.url) {
editing.value.image_url = media.url
}
} catch (e) {
message.error(e.message || '打开媒体库失败')
}
}
async function handleSave() {
if (!editing.value.image_url) {
message.warning('请填写 Banner 图片')
return
}
saving.value = true
try {
if (editing.value.id) {
await api.updateHomepageBanner(editing.value.id, editing.value)
message.success('已保存')
} else {
await api.createHomepageBanner(editing.value)
message.success('已添加')
}
modalShow.value = false
await loadBanners()
} catch (e) {
message.error(e.message || '保存失败')
} finally {
saving.value = false
}
}
function handleDelete(row) {
dialog.warning({
title: '确认删除',
content: `确定删除 Banner「${row.title || '未命名'}」?`,
positiveText: '删除',
negativeText: '取消',
onPositiveClick: async () => {
try {
await api.deleteHomepageBanner(row.id)
message.success('已删除')
await loadBanners()
} catch (e) {
message.error(e.message || '删除失败')
}
},
})
}
onMounted(() => {
loadBanners()
loadBannerSettings()
})
</script>
<style scoped>
.aksrt-inline-setting {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.aksrt-inline-setting__hint {
color: #7a7f8a;
font-size: 13px;
}
.aksrt-inline-setting__actions {
display: flex;
justify-content: flex-end;
}
.aksrt-modal-preview {
margin: -8px 0 18px;
border-radius: 8px;
overflow: hidden;
border: 1px solid #eef1f6;
}
.aksrt-modal-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 14px;
}
.aksrt-modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
@media (max-width: 768px) {
.aksrt-modal-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,986 +0,0 @@
<template>
<div class="aksrt-page-container content-page">
<PageHeader title="页脚配置" desc="管理页脚区域的联系方式、快速链接、更多链接、友情链接和版权信息。">
<template #actions>
<n-button type="primary" :loading="saving" @click="handleSave">保存</n-button>
</template>
</PageHeader>
<n-spin :show="loading" style="width: 100%; min-height: 200px;">
<n-tabs
v-model:value="activeTab"
type="line"
animated
class="aksrt-tabs"
>
<!-- ============== 品牌与Logo ============== -->
<n-tab-pane name="brand" tab="品牌与Logo">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<span class="section-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></span>
<h3>品牌列(第一列)</h3>
</div>
<n-form :model="form" label-placement="left" label-width="160" class="aksrt-form">
<n-form-item label="显示设备">
<n-checkbox-group v-model:value="form.footer_t1_m_s">
<n-space>
<n-checkbox value="pc" label="PC" />
<n-checkbox value="mobile" label="移动端" />
</n-space>
</n-checkbox-group>
</n-form-item>
<n-form-item label="页脚 Logo(日间)">
<div class="aksrt-media-row">
<n-input v-model:value="form.footer_t1_img" placeholder="图片 URL" readonly style="flex: 1;" />
<n-button size="small" @click="pickMedia('footer_t1_img')">选择</n-button>
<n-button size="small" quaternary type="error" :disabled="!form.footer_t1_img" @click="form.footer_t1_img = ''">清除</n-button>
</div>
<img v-if="form.footer_t1_img" :src="form.footer_t1_img" class="aksrt-media-preview" alt="footer logo day" />
</n-form-item>
<n-form-item label="页脚 Logo(夜间)">
<div class="aksrt-media-row">
<n-input v-model:value="form.footer_t1_img_dark" placeholder="图片 URL" readonly style="flex: 1;" />
<n-button size="small" @click="pickMedia('footer_t1_img_dark')">选择</n-button>
<n-button size="small" quaternary type="error" :disabled="!form.footer_t1_img_dark" @click="form.footer_t1_img_dark = ''">清除</n-button>
</div>
<img v-if="form.footer_t1_img_dark" :src="form.footer_t1_img_dark" class="aksrt-media-preview" alt="footer logo night" />
</n-form-item>
<n-form-item label="品牌标题">
<n-input v-model:value="form.footer_t1_t" placeholder="留空则使用站点名称" />
</n-form-item>
<n-form-item label="品牌列自定义代码">
<n-input v-model:value="form.fcode_t1_code" type="textarea" :rows="3" placeholder="可选,显示在品牌区域底部" />
</n-form-item>
</n-form>
</n-card>
</n-tab-pane>
<!-- ============== 联系方式 ============== -->
<n-tab-pane name="contacts" tab="联系方式">
<n-card class="aksrt-section-card">
<template #header>
<div class="aksrt-card-header-row">
<div class="aksrt-card-header-row__left">
<span class="header-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="12" y1="18" x2="12.01" y2="18"/></svg></span>
<strong class="aksrt-card-header-row__title">联系方式图标</strong>
</div>
<n-button type="primary" size="small" @click="addContact">+ 添加图标</n-button>
</div>
</template>
<p class="section-desc">页脚中的社交平台或联系入口。</p>
<n-data-table
v-if="form.contacts.length"
:columns="contactColumns"
:data="form.contacts"
:row-key="(row) => row._uid"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无联系方式图标" />
<n-divider>标准联系字段</n-divider>
<n-form :model="form" label-placement="left" label-width="160" class="aksrt-form">
<n-form-item label="显示设备">
<n-checkbox-group v-model:value="form.footer_contact_m_s">
<n-space>
<n-checkbox value="pc" label="PC" />
<n-checkbox value="mobile" label="移动端" />
</n-space>
</n-checkbox-group>
</n-form-item>
<n-form-item label="微信二维码">
<div class="aksrt-media-row">
<n-input v-model:value="form.footer_contact_wechat_img" placeholder="图片 URL" readonly style="flex: 1;" />
<n-button size="small" @click="pickMedia('footer_contact_wechat_img')">选择</n-button>
<n-button size="small" quaternary type="error" :disabled="!form.footer_contact_wechat_img" @click="form.footer_contact_wechat_img = ''">清除</n-button>
</div>
<img v-if="form.footer_contact_wechat_img" :src="form.footer_contact_wechat_img" class="aksrt-media-preview" alt="wechat qr" />
</n-form-item>
<n-form-item label="QQ">
<n-input v-model:value="form.footer_contact_qq" placeholder="QQ 号或链接" />
</n-form-item>
<n-form-item label="微博">
<n-input v-model:value="form.footer_contact_weibo" placeholder="微博链接" />
</n-form-item>
<n-form-item label="邮箱">
<n-input v-model:value="form.footer_contact_email" placeholder="邮箱地址" />
</n-form-item>
<n-form-item label="链接 HTML(fcode_t2_code_1)">
<n-input v-model:value="form.fcode_t2_code_1" type="textarea" :rows="3" placeholder="推荐直接写 <a href=&quot;/&quot;>首页</a> 等链接 HTML;为空时回退快速链接/友链/热门分类" />
</n-form-item>
<n-form-item label="链接下方自定义代码">
<n-input v-model:value="form.fcode_t2_code_2" type="textarea" :rows="2" placeholder="备案号等 HTML" />
</n-form-item>
</n-form>
<n-divider>更多联系项</n-divider>
<n-card class="aksrt-section-card" embedded>
<template #header>
<div class="aksrt-card-header-row">
<strong class="aksrt-card-header-row__title">扩展联系项</strong>
<n-button type="primary" size="small" @click="addContactMore">+ 添加</n-button>
</div>
</template>
<p class="section-desc">每项含描述、图标、链接、图片(hover 显示),用于补充标准联系方式。</p>
<n-data-table
v-if="form.footer_contact_more.length"
:columns="contactMoreColumns"
:data="form.footer_contact_more"
:row-key="(_, i) => i"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无扩展联系项" />
</n-card>
</n-card>
</n-tab-pane>
<!-- ============== 快速链接 ============== -->
<n-tab-pane name="quick" tab="快速链接">
<n-card class="aksrt-section-card">
<template #header>
<div class="aksrt-card-header-row">
<div class="aksrt-card-header-row__left">
<span class="header-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span>
<strong class="aksrt-card-header-row__title">快速链接</strong>
</div>
<n-button type="primary" size="small" @click="openLinkEditor('quick_links')">+ 添加链接</n-button>
</div>
</template>
<p class="section-desc">页脚第二列的常用导航链接。</p>
<n-data-table
v-if="form.quick_links.length"
:columns="createLinkColumns('quick_links')"
:data="form.quick_links"
:row-key="(row) => row.id"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无快速链接" />
</n-card>
</n-tab-pane>
<!-- ============== 更多链接 ============== -->
<n-tab-pane name="more" tab="更多链接">
<n-card class="aksrt-section-card">
<template #header>
<div class="aksrt-card-header-row">
<div class="aksrt-card-header-row__left">
<span class="header-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span>
<strong class="aksrt-card-header-row__title">更多链接</strong>
</div>
<n-button type="primary" size="small" @click="openLinkEditor('more_links')">+ 添加链接</n-button>
</div>
</template>
<p class="section-desc">页脚补充链接区域。</p>
<n-data-table
v-if="form.more_links.length"
:columns="createLinkColumns('more_links')"
:data="form.more_links"
:row-key="(row) => row.id"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无更多链接" />
</n-card>
</n-tab-pane>
<!-- ============== 友情链接 ============== -->
<n-tab-pane name="friends" tab="友情链接">
<n-card class="aksrt-section-card">
<template #header>
<div class="aksrt-card-header-row">
<div class="aksrt-card-header-row__left">
<span class="header-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg></span>
<strong class="aksrt-card-header-row__title">友情链接 ({{ friendLinks.length }})</strong>
</div>
<n-button type="primary" size="small" @click="openFriendEditor()">+ 添加友链</n-button>
</div>
</template>
<p class="section-desc">站点友情链接列表,支持 Logo 头像、名称、URL 与简短描述。</p>
<n-data-table
v-if="friendLinks.length"
:columns="friendColumns"
:data="friendLinks"
:row-key="(row) => row.id"
:bordered="false"
size="small"
/>
<n-empty v-else description="暂无友情链接" />
</n-card>
</n-tab-pane>
<!-- ============== 二维码图片组 ============== -->
<n-tab-pane name="mini_qr" tab="二维码图片组">
<n-card class="aksrt-section-card">
<template #header>
<div class="aksrt-card-header-row">
<div class="aksrt-card-header-row__left">
<span class="header-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg></span>
<strong class="aksrt-card-header-row__title">二维码图片组(最多 4 个)</strong>
</div>
<n-button type="primary" size="small" :disabled="form.footer_mini_img.length >= 4" @click="addMiniImg">+ 添加</n-button>
</div>
</template>
<p class="section-desc">显示在页脚第三列区域,每项含文字描述和二维码图片。</p>
<n-form :model="form" label-placement="left" label-width="120" class="aksrt-form">
<n-form-item label="显示设备">
<n-checkbox-group v-model:value="form.footer_mini_img_m_s">
<n-space>
<n-checkbox value="pc" label="PC" />
<n-checkbox value="mobile" label="移动端" />
</n-space>
</n-checkbox-group>
</n-form-item>
</n-form>
<div v-for="(item, idx) in form.footer_mini_img" :key="idx" class="aksrt-mini-img-row">
<n-space align="center" :size="12" style="width: 100%;">
<n-input v-model:value="item.text" placeholder="文字描述" style="width: 160px;" />
<div class="aksrt-media-row" style="flex: 1;">
<n-input v-model:value="item.img" placeholder="二维码图片 URL" readonly style="flex: 1;" />
<n-button size="small" @click="pickMiniImg(idx)">选择</n-button>
</div>
<n-button size="small" quaternary type="error" @click="removeMiniImg(idx)">删除</n-button>
</n-space>
<img v-if="item.img" :src="item.img" class="aksrt-media-preview aksrt-media-preview--sm" :alt="item.text || 'qr'" />
</div>
<n-empty v-if="!form.footer_mini_img.length" description="暂无二维码图片" />
</n-card>
</n-tab-pane>
<!-- ============== 自定义代码 ============== -->
<n-tab-pane name="custom_code" tab="自定义代码">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<span class="section-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg></span>
<h3>底部自定义代码</h3>
</div>
<p class="section-desc">插入在页脚底部区域(版权信息之后),支持 HTML。</p>
<n-form :model="form" label-placement="top" class="aksrt-form">
<n-form-item label="fcode_customize_code">
<n-input v-model:value="form.fcode_customize_code" type="textarea" :rows="6" placeholder="自定义 HTML 代码,显示在页脚最底部" />
</n-form-item>
</n-form>
</n-card>
</n-tab-pane>
<!-- ============== 版权与杂项 ============== -->
<n-tab-pane name="misc" tab="版权与杂项">
<n-card class="aksrt-section-card">
<div class="aksrt-section-header">
<span class="section-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
<h3>其他设置</h3>
</div>
<n-form :model="form" label-placement="left" label-width="140" class="aksrt-form">
<n-form-item label="页脚布局模板">
<n-radio-group v-model:value="form.layout_template">
<n-radio-button value="template_1">模板一(Zibll)</n-radio-button>
</n-radio-group>
</n-form-item>
<n-form-item label="热门分类数量">
<n-space align="center" :size="10">
<n-input-number v-model:value="form.hot_categories" :min="1" :max="20" style="width: 120px;" />
<span class="aksrt-hint-text">范围 1 - 20</span>
</n-space>
</n-form-item>
<n-form-item label="自定义版权文案">
<n-input
v-model:value="form.copyright_text"
type="textarea"
:rows="3"
placeholder="留空则使用默认版权信息"
/>
</n-form-item>
</n-form>
</n-card>
</n-tab-pane>
</n-tabs>
<div class="aksrt-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存页脚配置</n-button>
</div>
</n-spin>
<!-- ===== 联系方式 Modal ===== -->
<n-modal
v-model:show="showContactModal"
:mask-closable="false"
preset="card"
title="编辑联系方式图标"
class="aksrt-modal-card"
>
<n-form :model="editingContact" label-placement="top" class="aksrt-form">
<n-form-item label="图标类型">
<n-select v-model:value="editingContact.icon" :options="iconOptions" placeholder="选择图标" />
</n-form-item>
<n-form-item label="链接地址">
<n-input v-model:value="editingContact.url" placeholder="https://..." />
</n-form-item>
<n-form-item label="标题提示">
<n-input v-model:value="editingContact.title" placeholder="鼠标悬停提示文案" />
</n-form-item>
<n-form-item label="启用状态">
<n-switch v-model:value="editingContact.enabled" />
</n-form-item>
</n-form>
<template #footer>
<div class="aksrt-modal-footer">
<n-button @click="showContactModal = false">取消</n-button>
<n-button type="primary" @click="saveContact">保存</n-button>
</div>
</template>
</n-modal>
<!-- ===== 链接 Modal ===== -->
<n-modal
v-model:show="showLinkModal"
:mask-closable="false"
preset="card"
:title="editingLinkType === 'quick_links' ? '编辑快速链接' : '编辑更多链接'"
class="aksrt-modal-card"
>
<n-form :model="editingLink" label-placement="top" class="aksrt-form">
<n-form-item label="链接名称">
<n-input v-model:value="editingLink.name" placeholder="例如:首页" />
</n-form-item>
<n-form-item label="链接地址">
<n-input v-model:value="editingLink.url" placeholder="https:// 或 /path" />
</n-form-item>
<n-form-item label="启用状态">
<n-switch v-model:value="editingLink.enabled" />
</n-form-item>
</n-form>
<template #footer>
<div class="aksrt-modal-footer">
<n-button @click="showLinkModal = false">取消</n-button>
<n-button type="primary" @click="saveLink">保存</n-button>
</div>
</template>
</n-modal>
<!-- ===== 友情链接 Modal ===== -->
<n-modal
v-model:show="showFriendModal"
:mask-closable="false"
preset="card"
title="添加友情链接"
class="aksrt-modal-card"
>
<n-form :model="editingFriend" label-placement="top" class="aksrt-form">
<n-form-item label="站点名称">
<n-input v-model:value="editingFriend.name" placeholder="如:某人的博客" />
</n-form-item>
<n-form-item label="站点 URL">
<n-input v-model:value="editingFriend.url" placeholder="https://" />
</n-form-item>
<n-form-item label="Logo 图片 URL">
<n-input v-model:value="editingFriend.logo" placeholder="图标 URL(可选,方形 PNG 推荐)" />
</n-form-item>
<n-form-item label="描述">
<n-input v-model:value="editingFriend.description" placeholder="一句话简介(可选)" />
</n-form-item>
</n-form>
<template #footer>
<div class="aksrt-modal-footer">
<n-button @click="showFriendModal = false">取消</n-button>
<n-button type="primary" :loading="addingFriend" @click="saveFriend">保存</n-button>
</div>
</template>
</n-modal>
<!-- ===== 扩展联系项 Modal ===== -->
<n-modal
v-model:show="showContactMoreModal"
:mask-closable="false"
preset="card"
title="编辑扩展联系项"
class="aksrt-modal-card"
>
<n-form :model="editingContactMore" label-placement="top" class="aksrt-form">
<n-form-item label="描述">
<n-input v-model:value="editingContactMore.desc" placeholder="如:微信公众号" />
</n-form-item>
<n-form-item label="图标类名">
<n-input v-model:value="editingContactMore.icon" placeholder="如:fa fab fa-weixin" />
</n-form-item>
<n-form-item label="链接地址">
<n-input v-model:value="editingContactMore.link" placeholder="https://..." />
</n-form-item>
<n-form-item label="Hover 图片">
<n-input v-model:value="editingContactMore.img" placeholder="图片 URL(可选,hover 显示)" />
</n-form-item>
</n-form>
<template #footer>
<div class="aksrt-modal-footer">
<n-button @click="showContactMoreModal = false">取消</n-button>
<n-button type="primary" @click="saveContactMore">保存</n-button>
</div>
</template>
</n-modal>
</div>
</template>
<script setup>
import { h, onMounted, reactive, ref } from 'vue'
import { NAvatar, NButton, NTag, useDialog, useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
import { openMediaPicker } from '@/utils/media'
const message = useMessage()
const dialog = useDialog()
const loading = ref(true)
const saving = ref(false)
const activeTab = ref('contacts')
const form = reactive({
contacts: [],
quick_links: [],
more_links: [],
hot_categories: 5,
copyright_text: '',
layout_template: 'template_1',
// §2.3 三列结构化字段
footer_t1_m_s: ['pc', 'mobile'],
footer_t1_img: '',
footer_t1_img_dark: '',
footer_t1_t: '',
fcode_t1_code: '',
footer_contact_wechat_img: '',
footer_contact_qq: '',
footer_contact_weibo: '',
footer_contact_email: '',
footer_contact_m_s: ['pc', 'mobile'],
footer_contact_more: [],
fcode_t2_code_1: '',
fcode_t2_code_2: '',
footer_mini_img_m_s: ['pc', 'mobile'],
footer_mini_img: [],
fcode_customize_code: '',
})
// ============== 联系方式 ==============
const showContactModal = ref(false)
const editingContactIndex = ref(-1)
const editingContact = reactive({
icon: '',
url: '',
title: '',
enabled: true,
})
let contactUid = 0
const iconOptions = [
{ label: 'GitHub', value: 'github' },
{ label: 'X (Twitter)', value: 'x' },
{ label: 'Email', value: 'email' },
{ label: '微信', value: 'wechat' },
{ label: 'QQ', value: 'qq' },
{ label: '微博', value: 'weibo' },
{ label: 'Telegram', value: 'telegram' },
{ label: 'Discord', value: 'discord' },
{ label: 'Bilibili', value: 'bilibili' },
{ label: 'LinkedIn', value: 'linkedin' },
{ label: 'Facebook', value: 'facebook' },
{ label: 'YouTube', value: 'youtube' },
{ label: 'Instagram', value: 'instagram' },
{ label: 'RSS / 网站', value: 'web' },
]
function addContact() {
Object.assign(editingContact, {
icon: '',
url: '',
title: '',
enabled: true,
})
editingContactIndex.value = -1
showContactModal.value = true
}
function editContact(row, index) {
Object.assign(editingContact, {
icon: row.icon,
url: row.url,
title: row.title,
enabled: row.enabled,
})
editingContactIndex.value = index
showContactModal.value = true
}
function saveContact() {
if (!editingContact.icon || !editingContact.url) {
message.warning('请填写图标类型和链接地址')
return
}
const item = {
_uid: editingContactIndex.value >= 0 ? form.contacts[editingContactIndex.value]._uid : ++contactUid,
icon: editingContact.icon,
url: editingContact.url,
title: editingContact.title,
enabled: editingContact.enabled,
}
if (editingContactIndex.value >= 0) {
form.contacts[editingContactIndex.value] = item
} else {
form.contacts.push(item)
}
showContactModal.value = false
}
function deleteContact(index) {
dialog.warning({
title: '确认删除',
content: '确定删除这个联系方式图标吗?',
positiveText: '删除',
negativeText: '取消',
onPositiveClick: () => {
form.contacts.splice(index, 1)
},
})
}
const contactColumns = [
{
title: '平台',
key: 'icon',
width: 120,
render: (row) => iconOptions.find((item) => item.value === row.icon)?.label || row.icon,
},
{
title: '链接',
key: 'url',
ellipsis: { tooltip: true },
},
{
title: '提示',
key: 'title',
ellipsis: { tooltip: true },
className: 'aksrt-table-mobile-hide',
},
{
title: '状态',
key: 'enabled',
width: 80,
render: (row) => h(
NTag,
{ size: 'small', type: row.enabled ? 'success' : 'default' },
{ default: () => (row.enabled ? '启用' : '禁用') }
),
},
{
title: '操作',
key: 'actions',
width: 130,
render: (_, index) => h('div', { style: 'display:flex;gap:6px;' }, [
h(NButton, { size: 'tiny', onClick: () => editContact(form.contacts[index], index) }, { default: () => '编辑' }),
h(NButton, { size: 'tiny', type: 'error', quaternary: true, onClick: () => deleteContact(index) }, { default: () => '删除' }),
]),
},
]
// ============== 快速 / 更多链接 ==============
const showLinkModal = ref(false)
const editingLinkType = ref('quick_links')
const editingLinkIndex = ref(-1)
const editingLink = reactive({
id: '',
name: '',
url: '',
enabled: true,
sort_order: 0,
})
function openLinkEditor(type, row = null, index = -1) {
editingLinkType.value = type
editingLinkIndex.value = index
Object.assign(editingLink, row || {
id: '',
name: '',
url: '',
enabled: true,
sort_order: type === 'quick_links' ? form.quick_links.length : form.more_links.length,
})
showLinkModal.value = true
}
function saveLink() {
if (!editingLink.name || !editingLink.url) {
message.warning('请填写链接名称和链接地址')
return
}
const target = form[editingLinkType.value]
const item = {
id: editingLink.id || `link_${Date.now()}`,
name: editingLink.name,
url: editingLink.url,
enabled: editingLink.enabled,
sort_order: editingLink.sort_order ?? target.length,
}
if (editingLinkIndex.value >= 0) {
target[editingLinkIndex.value] = item
} else {
target.push(item)
}
showLinkModal.value = false
}
function deleteLink(type, index) {
dialog.warning({
title: '确认删除',
content: '确定删除这个链接吗?',
positiveText: '删除',
negativeText: '取消',
onPositiveClick: () => {
form[type].splice(index, 1)
},
})
}
function createLinkColumns(type) {
return [
{
title: '名称',
key: 'name',
width: 160,
},
{
title: 'URL',
key: 'url',
ellipsis: { tooltip: true },
},
{
title: '状态',
key: 'enabled',
width: 80,
render: (row) => h(
NTag,
{ size: 'small', type: row.enabled ? 'success' : 'default' },
{ default: () => (row.enabled ? '启用' : '禁用') }
),
},
{
title: '操作',
key: 'actions',
width: 130,
render: (_, index) => h('div', { style: 'display:flex;gap:6px;' }, [
h(NButton, { size: 'tiny', onClick: () => openLinkEditor(type, form[type][index], index) }, { default: () => '编辑' }),
h(NButton, { size: 'tiny', type: 'error', quaternary: true, onClick: () => deleteLink(type, index) }, { default: () => '删除' }),
]),
},
]
}
// ============== 友情链接 ==============
const friendLinks = ref([])
const showFriendModal = ref(false)
const addingFriend = ref(false)
const editingFriend = reactive({
name: '',
url: '',
logo: '',
description: '',
})
const friendColumns = [
{
title: 'Logo',
key: 'logo',
width: 60,
render: (row) => row.logo ? h(NAvatar, { src: row.logo, size: 32, round: true }) : h('span', { style: 'color:#bbb;font-size:12px;' }, '—'),
},
{ title: '名称', key: 'name', width: 140, ellipsis: { tooltip: true } },
{ title: 'URL', key: 'url', ellipsis: { tooltip: true } },
{ title: '描述', key: 'description', ellipsis: { tooltip: true }, className: 'aksrt-table-mobile-hide' },
{
title: '操作',
key: 'actions',
width: 90,
render: (row) => h(NButton, {
quaternary: true,
type: 'error',
size: 'tiny',
onClick: () => deleteFriend(row),
}, { default: () => '删除' }),
},
]
function openFriendEditor() {
Object.assign(editingFriend, {
name: '',
url: '',
logo: '',
description: '',
})
showFriendModal.value = true
}
async function loadFriendLinks() {
try {
const res = await api.getFriendLinks()
if (res.code === 'success') friendLinks.value = res.data || []
} catch (e) {
console.error('[FriendLinks] load failed:', e)
}
}
async function saveFriend() {
if (!editingFriend.name || !editingFriend.url) {
message.warning('请填写站点名称和 URL')
return
}
addingFriend.value = true
try {
const res = await api.addFriendLink({
name: editingFriend.name,
url: editingFriend.url,
logo: editingFriend.logo,
description: editingFriend.description,
sort_order: friendLinks.value.length,
})
if (res.code === 'success') {
message.success(res.message || '已添加')
showFriendModal.value = false
await loadFriendLinks()
}
} catch (e) {
message.error(e.message || '添加失败')
} finally {
addingFriend.value = false
}
}
function deleteFriend(row) {
dialog.warning({
title: '确认删除',
content: `确定删除友链「${row.name}」?`,
positiveText: '删除',
negativeText: '取消',
onPositiveClick: async () => {
try {
await api.deleteFriendLink(row.id)
message.success('已删除')
await loadFriendLinks()
} catch (e) {
message.error(e.message || '删除失败')
}
},
})
}
// ============== 加载 / 保存 ==============
async function loadSettings() {
loading.value = true
try {
const [footerRes] = await Promise.all([
api.getFooterSettings(),
loadFriendLinks(),
])
if (footerRes.code === 'success' && footerRes.data) {
const data = footerRes.data
form.contacts = (data.contacts || []).map((item) => ({ ...item, _uid: ++contactUid }))
form.quick_links = data.quick_links || []
form.more_links = data.more_links || []
form.hot_categories = data.hot_categories ?? 5
form.copyright_text = data.copyright_text || ''
form.layout_template = 'template_1'
// §2.3 三列结构化字段
form.footer_t1_m_s = Array.isArray(data.footer_t1_m_s) ? data.footer_t1_m_s : ['pc', 'mobile']
form.footer_t1_img = data.footer_t1_img || ''
form.footer_t1_img_dark = data.footer_t1_img_dark || ''
form.footer_t1_t = data.footer_t1_t || ''
form.fcode_t1_code = data.fcode_t1_code || ''
form.footer_contact_wechat_img = data.footer_contact_wechat_img || ''
form.footer_contact_qq = data.footer_contact_qq || ''
form.footer_contact_weibo = data.footer_contact_weibo || ''
form.footer_contact_email = data.footer_contact_email || ''
form.footer_contact_m_s = Array.isArray(data.footer_contact_m_s) ? data.footer_contact_m_s : ['pc', 'mobile']
form.footer_contact_more = Array.isArray(data.footer_contact_more) ? data.footer_contact_more : []
form.fcode_t2_code_1 = data.fcode_t2_code_1 || ''
form.fcode_t2_code_2 = data.fcode_t2_code_2 || ''
form.footer_mini_img_m_s = Array.isArray(data.footer_mini_img_m_s) ? data.footer_mini_img_m_s : ['pc', 'mobile']
form.footer_mini_img = Array.isArray(data.footer_mini_img) ? data.footer_mini_img : []
form.fcode_customize_code = data.fcode_customize_code || ''
}
} catch (error) {
console.error('[Footer] load failed:', error)
message.error(error?.message || '加载失败')
} finally {
loading.value = false
}
}
async function handleSave() {
saving.value = true
try {
const payload = {
contacts: form.contacts.map(({ _uid, ...rest }) => rest),
quick_links: form.quick_links,
more_links: form.more_links,
hot_categories: form.hot_categories,
copyright_text: form.copyright_text,
layout_template: form.layout_template,
// §2.3 三列结构化字段
footer_t1_m_s: form.footer_t1_m_s,
footer_t1_img: form.footer_t1_img,
footer_t1_img_dark: form.footer_t1_img_dark,
footer_t1_t: form.footer_t1_t,
fcode_t1_code: form.fcode_t1_code,
footer_contact_wechat_img: form.footer_contact_wechat_img,
footer_contact_qq: form.footer_contact_qq,
footer_contact_weibo: form.footer_contact_weibo,
footer_contact_email: form.footer_contact_email,
footer_contact_m_s: form.footer_contact_m_s,
footer_contact_more: form.footer_contact_more,
fcode_t2_code_1: form.fcode_t2_code_1,
fcode_t2_code_2: form.fcode_t2_code_2,
footer_mini_img_m_s: form.footer_mini_img_m_s,
footer_mini_img: form.footer_mini_img,
fcode_customize_code: form.fcode_customize_code,
}
const res = await api.saveFooterSettings(payload)
if (res.code === 'success') {
message.success(res.message || '保存成功')
} else {
message.error(res.message || '保存失败')
}
} catch (error) {
console.error('[Footer] save failed:', error)
message.error(error?.message || '保存失败')
} finally {
saving.value = false
}
}
// ============== 媒体选择 ==============
async function pickMedia(field) {
try {
const media = await openMediaPicker({ title: '选择图片' })
if (media?.url) {
form[field] = media.url
}
} catch (e) {
if (e.message) message.warning(e.message)
}
}
// ============== 扩展联系项 ==============
function addContactMore() {
form.footer_contact_more.push({ desc: '', icon: '', link: '', img: '' })
}
function removeContactMore(index) {
form.footer_contact_more.splice(index, 1)
}
const showContactMoreModal = ref(false)
const editingContactMoreIndex = ref(-1)
const editingContactMore = reactive({ desc: '', icon: '', link: '', img: '' })
function editContactMore(row, index) {
Object.assign(editingContactMore, row)
editingContactMoreIndex.value = index
showContactMoreModal.value = true
}
function saveContactMore() {
const item = { ...editingContactMore }
if (editingContactMoreIndex.value >= 0) {
form.footer_contact_more[editingContactMoreIndex.value] = item
}
showContactMoreModal.value = false
}
const contactMoreColumns = [
{ title: '描述', key: 'desc', width: 120 },
{ title: '图标', key: 'icon', width: 100 },
{ title: '链接', key: 'link', ellipsis: { tooltip: true } },
{ title: '图片', key: 'img', width: 60, render: (row) => row.img ? h('img', { src: row.img, style: 'width:32px;height:32px;object-fit:cover;border-radius:4px;' }) : h('span', { style: 'color:#bbb;' }, '—') },
{
title: '操作',
key: 'actions',
width: 130,
render: (_, index) => h('div', { style: 'display:flex;gap:6px;' }, [
h(NButton, { size: 'tiny', onClick: () => editContactMore(form.footer_contact_more[index], index) }, { default: () => '编辑' }),
h(NButton, { size: 'tiny', type: 'error', quaternary: true, onClick: () => removeContactMore(index) }, { default: () => '删除' }),
]),
},
]
// ============== 二维码图片组 ==============
function addMiniImg() {
if (form.footer_mini_img.length < 4) {
form.footer_mini_img.push({ text: '', img: '' })
}
}
function removeMiniImg(idx) {
form.footer_mini_img.splice(idx, 1)
}
async function pickMiniImg(idx) {
try {
const media = await openMediaPicker({ title: '选择二维码图片' })
if (media?.url) {
form.footer_mini_img[idx].img = media.url
}
} catch (e) {
if (e.message) message.warning(e.message)
}
}
onMounted(loadSettings)
</script>
<style scoped>
.aksrt-modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.aksrt-media-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
}
.aksrt-media-preview {
display: block;
max-width: 200px;
max-height: 80px;
margin-top: 8px;
border-radius: 6px;
border: 1px solid var(--n-border-color, #e0e0e6);
object-fit: contain;
}
.aksrt-media-preview--sm {
max-width: 64px;
max-height: 64px;
}
.aksrt-mini-img-row {
padding: 10px 0;
border-bottom: 1px solid var(--n-border-color, #e0e0e6);
}
.aksrt-mini-img-row:last-child {
border-bottom: none;
}
</style>
@@ -1,354 +0,0 @@
<template>
<div class="aksrt-page-container content-page">
<PageHeader
title="首页配置"
desc="配置首页主列表、分类 Tab 与基础交互。列表/卡片显示请在「文章列表」中设置。"
>
<template #actions>
<n-button type="primary" :loading="saving" @click="handleSave">保存设置</n-button>
</template>
</PageHeader>
<n-tabs v-model:value="activeTab" type="line" animated>
<n-tab-pane name="stream" tab="主列表 & 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>
<n-form :model="form" label-placement="left" label-width="160" class="aksrt-form">
<n-form-item label="内容说明">
<n-alert type="info" :show-icon="false">
首页主列表只使用普通文章。下载/付费是文章能力(meta),不是第二种内容类型。当前为固定骨架模式,不再使用旧版自由模块画布。
</n-alert>
</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="启用主列表">
<n-switch v-model:value="form.home_posts_list_s" />
<template #feedback><span class="aksrt-hint-text">首页主内容区保留 Zibll 式内容流,不建议关闭。</span></template>
</n-form-item>
<n-form-item label="显示标题">
<n-input v-model:value="form.index_list_title" placeholder="最新发布" />
</n-form-item>
<n-form-item label="主栏目默认排序">
<n-select v-model:value="form.home_list1_orderby_option.orderby" :options="allSortOptions" />
</n-form-item>
<n-form-item label="显示排序方式按钮">
<n-switch v-model:value="form.home_list1_orderby_s" />
</n-form-item>
<n-form-item v-if="form.home_list1_orderby_s" label="排序栏样式">
<n-radio-group v-model:value="form.home_list_sort_dropdown">
<n-space size="large">
<n-radio-button :value="false">内联链接</n-radio-button>
<n-radio-button :value="true">下拉菜单</n-radio-button>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item v-if="form.home_list1_orderby_s" label="排序方式选项">
<n-select
v-model:value="form.home_list1_orderby_option.options"
:options="allSortOptions"
multiple
placeholder="选择排序选项"
/>
</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="排除文章">
<n-input
v-model:value="excludePostIdsText"
type="textarea"
placeholder="逗号分隔的文章 ID,如 12,34,56"
:rows="2"
/>
<template #feedback><span class="aksrt-hint-text">主列表和首页 Tab 查询均排除这些文章。</span></template>
</n-form-item>
<n-form-item label="排除分类">
<n-input
v-model:value="excludeCategoryIdsText"
type="textarea"
placeholder="逗号分隔的分类 ID,如 5,8"
:rows="2"
/>
<template #feedback><span class="aksrt-hint-text">主列表排除这些分类,首页 Tab 不能绑定已排除分类。</span></template>
</n-form-item>
</n-form>
</n-card>
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
<template #header>
<div style="display:flex; align-items:center; justify-content:space-between;">
<strong>更多栏目</strong>
<n-button size="small" @click="addTab">+ 添加栏目</n-button>
</div>
</template>
<n-form :model="form" label-placement="left" label-width="160" class="aksrt-form" style="margin-bottom:16px;">
<n-form-item label="Tab 数量上限">
<n-input-number v-model:value="form.homepage_tab_limit" :min="1" :max="12" />
</n-form-item>
<n-form-item label="每 Tab 文章数">
<n-input-number v-model:value="form.homepage_tab_posts_per_page" :min="1" :max="20" />
</n-form-item>
</n-form>
<div v-if="form.home_lists.length === 0" class="homepage-settings__empty-tabs">
<n-text depth="3">暂无更多栏目,点击“添加栏目”创建分类 Tab。</n-text>
</div>
<div v-for="(tab, index) in form.home_lists" :key="index" class="homepage-settings__tab-row">
<div class="homepage-settings__tab-fields">
<n-select
:value="tab.term_id"
:options="categoryOptions"
placeholder="分类"
filterable
style="flex:2;"
@update:value="(val) => updateTab(index, 'term_id', val)"
/>
<n-input
:value="tab.title"
placeholder="自定义标题"
style="flex:1;"
@update:value="(val) => updateTab(index, 'title', val)"
/>
<n-select
:value="tab.orderby"
:options="allSortOptions"
style="flex:1;"
@update:value="(val) => updateTab(index, 'orderby', val)"
/>
<n-switch
:value="tab.enabled"
@update:value="(val) => updateTab(index, 'enabled', val)"
/>
<n-button size="small" quaternary type="error" @click="removeTab(index)">删除</n-button>
</div>
</div>
</n-card>
</n-spin>
</n-tab-pane>
<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>
<n-alert type="info" :show-icon="false">
首页文章列表/卡片模式、样式变体与缩略图比例统一由「文章&amp;列表 → 文章列表」中的 Zibll 全局字段控制。
</n-alert>
</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-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">
<template #header><strong>固定骨架说明</strong></template>
<n-space vertical>
<n-alert type="warning" :show-icon="false">
顶部多功能组件与 Banner 已作为固定首页骨架的一部分维护,不再由旧版首页模块编排驱动。
</n-alert>
<n-alert type="info" :show-icon="false">
如需调整首页首屏运营区,请前往对应管理页配置幻灯片内容或顶部组件参数。
</n-alert>
<n-space>
<n-button secondary type="primary" @click="router.push('/page-display/banner')">管理 Banner 轮播</n-button>
<n-button secondary @click="router.push('/page-display/header-components')">
顶部多功能组件
</n-button>
</n-space>
</n-space>
</n-card>
</n-spin>
</n-tab-pane>
</n-tabs>
<div class="aksrt-form-actions">
<n-button type="primary" :loading="saving" @click="handleSave">保存设置</n-button>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import api from '@/api'
import PageHeader from '@/components/PageHeader.vue'
const router = useRouter()
const message = useMessage()
const loading = ref(true)
const saving = ref(false)
const activeTab = ref('stream')
const categoryOptions = ref([])
const allSortOptions = [
{ label: '最近更新', value: 'modified' },
{ label: '发布时间', value: 'date' },
{ label: '浏览量', value: 'views' },
{ label: '点赞数', value: 'likes' },
{ label: '评论数', value: 'comments' },
{ label: '收藏数', value: 'favorites' },
{ label: '下载量', value: 'downloads' },
{ label: '随机', value: 'rand' },
]
const form = reactive({
home_posts_list_s: true,
home_list1_orderby_s: true,
home_list1_orderby_option: {
orderby: 'modified',
options: ['modified', 'date', 'views', 'likes', 'comments'],
},
index_list_title: '最新发布',
home_exclude_posts: [],
home_exclude_cats: [],
home_lists: [],
homepage_sidebar_mini_enabled: true,
homepage_sort_ajax_enabled: true,
homepage_sticky_tabs_enabled: true,
home_list_sort_dropdown: false,
homepage_tab_posts_per_page: 6,
homepage_tab_limit: 4,
})
const excludePostIdsText = computed({
get: () => (form.home_exclude_posts || []).join(','),
set: (val) => {
form.home_exclude_posts = val.split(',').map(s => parseInt(s.trim(), 10)).filter(n => n > 0)
},
})
const excludeCategoryIdsText = computed({
get: () => (form.home_exclude_cats || []).join(','),
set: (val) => {
form.home_exclude_cats = val.split(',').map(s => parseInt(s.trim(), 10)).filter(n => n > 0)
},
})
function addTab() {
form.home_lists.push({ term_id: 0, title: '', orderby: 'date', enabled: true })
}
function removeTab(index) {
form.home_lists.splice(index, 1)
}
function updateTab(index, field, value) {
form.home_lists[index][field] = value
}
async function loadCategories() {
try {
const res = await api.getHomepageCategories()
const cats = res?.data || []
categoryOptions.value = cats.map(c => ({
label: `${c.name} (${c.count})`,
value: c.term_id || c.id,
}))
} catch {
// non-critical
}
}
async function loadSettings() {
loading.value = true
try {
const res = await api.getHomepageSettings()
const d = res?.data || {}
form.home_posts_list_s = d.home_posts_list_s !== false
form.home_list1_orderby_s = d.home_list1_orderby_s !== false
const orderOpt = d.home_list1_orderby_option && typeof d.home_list1_orderby_option === 'object' ? d.home_list1_orderby_option : {}
form.home_list1_orderby_option = {
orderby: orderOpt.orderby || 'date',
options: Array.isArray(orderOpt.options) && orderOpt.options.length
? orderOpt.options
: ['modified', 'date', 'views', 'likes', 'comments'],
}
form.index_list_title = d.index_list_title || '最新发布'
form.home_exclude_posts = Array.isArray(d.home_exclude_posts) ? d.home_exclude_posts : []
form.home_exclude_cats = Array.isArray(d.home_exclude_cats) ? d.home_exclude_cats : []
form.home_lists = Array.isArray(d.home_lists) ? d.home_lists : []
form.homepage_sidebar_mini_enabled = d.homepage_sidebar_mini_enabled !== false
form.homepage_sort_ajax_enabled = d.homepage_sort_ajax_enabled !== false
form.homepage_sticky_tabs_enabled = d.homepage_sticky_tabs_enabled !== false
form.home_list_sort_dropdown = !!d.home_list_sort_dropdown
form.homepage_tab_posts_per_page = d.homepage_tab_posts_per_page || 6
form.homepage_tab_limit = d.homepage_tab_limit || 4
} catch (error) {
message.error(error.message || '加载设置失败')
} finally {
loading.value = false
}
}
async function handleSave() {
saving.value = true
try {
const payload = {
...form,
}
const res = await api.saveHomepageSettings(payload)
if (res?.code === 'success') {
message.success('首页设置已保存')
return
}
throw new Error(res?.message || '保存失败')
} catch (error) {
message.error(error.message || '保存失败')
} finally {
saving.value = false
}
}
onMounted(() => {
loadCategories()
loadSettings()
})
</script>
<style scoped>
.homepage-settings__empty-tabs {
padding: 24px;
text-align: center;
border: 1px dashed var(--n-border-color);
border-radius: 4px;
margin-bottom: 12px;
}
.homepage-settings__tab-row {
margin-bottom: 8px;
}
.homepage-settings__tab-fields {
display: flex;
gap: 8px;
align-items: center;
}
.aksrt-form-actions {
margin-top: 16px;
text-align: right;
}
</style>
+1 -1
View File
@@ -10,7 +10,7 @@
<FaIcon icon="fa fas fa-paint-brush" :size="16" />
<div>
<strong>全局设置</strong>
<p>Logo、背景、菜单装饰、自定义代码</p>
<p>Logo、导航栏、自定义代码</p>
<router-link to="/global/common">前往配置 →</router-link>
</div>
</div>
@@ -4,9 +4,6 @@ export const corePageLoaders = {
NativeOpsRedirect: () => import('@/pages/placeholder/NativeOpsRedirect.vue'),
FloatingButtonsSettings: () => import('@/pages/content/FloatingButtonsSettings.vue'),
FooterTabbarSettings: () => import('@/pages/content/FooterTabbarSettings.vue'),
FooterSettings: () => import('@/pages/content/FooterSettings.vue'),
HomepageSettings: () => import('@/pages/content/HomepageSettings.vue'),
BannerManager: () => import('@/pages/content/BannerManager.vue'),
PostPageSettings: () => import('@/pages/post-list/PostPageSettings.vue'),
CommentSettings: () => import('@/pages/post-list/CommentSettings.vue'),
PermissionsCapabilities: () => import('@/pages/permissions/PermissionsCapabilities.vue'),
+1 -2
View File
@@ -27,7 +27,6 @@
* }
*/
import { globalSchemas } from './sections/global/index.js'
import { pageDisplaySchemas } from './sections/page-display/index.js'
import { postListSchemas } from './sections/post-list/index.js'
import { permissionsSchemas } from './sections/permissions/index.js'
import { usersSchemas } from './sections/users/index.js'
@@ -58,7 +57,7 @@ export const CANONICAL_SCHEMA_DOMAINS = [
/** All schemas organized by domain */
export const ALL_SCHEMAS = Object.freeze({
global: globalSchemas,
page_display: pageDisplaySchemas,
page_display: Object.freeze({}),
post_list: postListSchemas,
permissions: permissionsSchemas,
users: usersSchemas,
@@ -17,7 +17,7 @@ export const searchSchema = {
search_cat_in: '',
search_more_cat_obj: [],
search_custom_key_s: true,
search_custom_key: ['title', 'content', 'excerpt', 'post_tag', 'forum_topic', 'shop_cat', 'shop_discount'],
search_custom_key: ['title', 'content', 'excerpt', 'category', 'post_tag'],
search_facets: {},
search_ms_s: false,
search_ms_opt: { api_url: 'http://127.0.0.1:7700', api_key: '', index_prefix: 'aksrt', synonyms: [{ key: 'wordpress', val: 'wp,wordp,wordpre' }] },
@@ -59,25 +59,16 @@ export const searchSchema = {
{ key: '404_search_s', label: '404 页面搜索', type: 'switch', default: true, help: '在 404 页面显示搜索功能' },
{ key: 'search_posts', label: '搜索框热门文章', type: 'switch', default: true, help: '在搜索面板中显示热门文章列表' },
{ key: 'search_no_page', label: '排除页面', type: 'switch', default: true, help: '搜索结果中排除页面(仅显示文章)' },
{ key: 'search_exclude_cats', label: '排除分类', type: 'taxonomy_select', taxonomy: ['category', 'shop_cat'], multiple: true, default: [], help: '搜索结果中排除的文章或商品分类' },
{ key: 'search_exclude_cats', label: '排除分类', type: 'taxonomy_select', taxonomy: 'category', multiple: true, default: [], help: '搜索结果中排除的文章分类' },
{ key: 'search_custom_key_s', label: '自定义匹配范围', type: 'switch', default: true, help: '启用后可配置搜索匹配字段范围' },
{ key: 'search_custom_key', label: '匹配字段', type: 'checkbox', default: ['title', 'content', 'excerpt', 'post_tag', 'forum_topic', 'shop_cat', 'shop_discount'], dependsOn: { key: 'search_custom_key_s', value: true }, options: [
{ key: 'search_custom_key', label: '匹配字段', type: 'checkbox', default: ['title', 'content', 'excerpt', 'category', 'post_tag'], dependsOn: { key: 'search_custom_key_s', value: true }, options: [
{ label: '标题', value: 'title' }, { label: '内容', value: 'content' }, { label: '简介', value: 'excerpt' },
{ label: '分类', value: 'category' }, { label: '标签', value: 'post_tag' }, { label: '专题', value: 'topics' },
{ label: '分类', value: 'category' }, { label: '标签', value: 'post_tag' },
{ label: '评论', value: 'coment' }, { label: '作者昵称', value: 'user' },
{ label: '[帖子]话题', value: 'forum_topic' }, { label: '[帖子]标签', value: 'forum_tag' },
{ label: '[商品]分类', value: 'shop_cat' }, { label: '[商品]特色标签', value: 'shop_tag' }, { label: '[商品]优惠活动', value: 'shop_discount' },
] },
{ key: 'search_facets', label: '搜索筛选', type: 'fieldset', sanitizeType: 'array', default: {}, dependsOn: { key: 'search_ms_s', value: false }, help: '搜索结果中显示的筛选列表;启用 Meilisearch 后由搜索引擎生成关联筛选。', fields: [
{ key: 'category', label: '[文章] 分类', type: 'taxonomy_select', taxonomy: 'category', multiple: true, default: [] },
{ key: 'topics', label: '[文章] 专题', type: 'taxonomy_select', taxonomy: 'topics', multiple: true, default: [] },
{ key: 'post_tag', label: '[文章] 标签', type: 'taxonomy_select', taxonomy: 'post_tag', multiple: true, default: [] },
{ key: 'plate_id', label: '[论坛帖子] 板块 ID', type: 'input', default: '' },
{ key: 'forum_topic', label: '[论坛帖子] 话题', type: 'taxonomy_select', taxonomy: 'forum_topic', multiple: true, default: [] },
{ key: 'forum_tag', label: '[论坛帖子] 标签', type: 'taxonomy_select', taxonomy: 'forum_tag', multiple: true, default: [] },
{ key: 'shop_cat', label: '[商品] 分类', type: 'taxonomy_select', taxonomy: 'shop_cat', multiple: true, default: [] },
{ key: 'shop_discount', label: '[商品] 优惠活动', type: 'taxonomy_select', taxonomy: 'shop_discount', multiple: true, default: [] },
{ key: 'shop_tag', label: '[商品] 特色标签', type: 'taxonomy_select', taxonomy: 'shop_tag', multiple: true, default: [] },
] },
],
},
@@ -1,778 +0,0 @@
/**
* Page Display domain schemas.
* Each key corresponds to a sub-page under the "页面&显示" section.
*/
/** Theme asset base for layout previews (admin SPA). */
function themeAsset(path) {
const base = (typeof window !== 'undefined' && window.aksrtAdminData && window.aksrtAdminData.themeUri)
? String(window.aksrtAdminData.themeUri).replace(/\/$/, '')
: ''
return base ? `${base}/${path.replace(/^\//, '')}` : path
}
/** Homepage sort options — keep in sync with AKSRT_Settings_Home_Cms::homepage_allowed_sorts() */
const sortOptions = [
{ label: '最近更新', value: 'modified' },
{ label: '发布时间', value: 'date' },
{ label: '浏览量', value: 'views' },
{ label: '点赞数', value: 'likes' },
{ label: '评论数', value: 'comments' },
{ label: '收藏数', value: 'favorites' },
{ label: '下载量', value: 'downloads' },
{ label: '随机', value: 'rand' },
]
export const pageDisplaySchemas = {
/**
* Header navigation settings
*/
'header-nav': {
key: 'page_display.header-nav',
title: '顶部导航栏',
icon: 'fa fas fa-bars',
description: '配置导航栏布局、菜单结构、搜索入口和用户入口。',
defaults: {
nav_fixed: true,
nav_scroll_down_hide: true,
header_layout: '1',
nav_pay_vip: true,
nav_user_pay_vip: true,
nav_newposts_btns: ['post'],
nav_user_newposts_btn: 'post',
nav_user_newposts_btn_text: '发布文章',
mobile_header_layout: 'center',
mobile_navbar_align: 'left',
nav_newposts: true,
nav_newposts_text: '<i class="fa fas fa-fw fa-pen"></i>发布',
nav_newposts_radius: true,
nav_newposts_class: 'jb-blue',
header_theme_custom: false,
header_theme_bg_custom: '',
header_theme_bg: '',
header_theme_color_custom: '',
},
groups: [
{
key: 'layout',
title: '导航栏布局',
icon: 'fa fas fa-th-large',
fields: [
{ key: 'nav_fixed', label: '导航栏一直固定在顶部', type: 'switch', default: true },
{ key: 'nav_scroll_down_hide', label: '下滚动隐藏,上滚动显示', type: 'switch', default: true, dependsOn: { key: 'nav_fixed', value: true } },
],
},
{
key: 'layout-type',
title: '布局类型',
icon: 'fa fas fa-columns',
fields: [
{
key: 'header_layout',
label: '电脑端布局',
type: 'image_select',
default: '1',
options: [
{ label: '布局 1(经典)', value: '1', image: themeAsset('assets/images/header-layout/header_layout_1.png') },
{ label: '布局 2(内联搜索)', value: '2', image: themeAsset('assets/images/header-layout/header_layout_2.png') },
{ label: '布局 3(紧凑)', value: '3', image: themeAsset('assets/images/header-layout/header_layout_3.png') },
],
help: '不同布局切换导航 DOM 结构(与 zibll image_select 一致)',
},
{
key: 'mobile_header_layout',
label: '移动端布局',
type: 'image_select',
default: 'center',
options: [
{ label: '居中(含搜索按钮)', value: 'center', image: themeAsset('assets/images/header-layout/mobile_header_layout_center.png') },
{ label: '靠左', value: 'left', image: themeAsset('assets/images/header-layout/mobile_header_layout_left.png') },
],
},
{
key: 'mobile_navbar_align',
label: '移动端菜单方向',
type: 'radio',
default: 'left',
options: [
{ label: '顶部(关闭手势)', value: 'top' },
{ label: '左侧滑出', value: 'left' },
{ label: '右侧滑出', value: 'right' },
],
},
],
},
{
key: 'newposts',
title: '发布按钮',
icon: 'fa fas fa-pen',
fields: [
{ key: 'nav_newposts', label: '启用发布按钮', type: 'switch', default: true },
{ key: 'nav_newposts_text', label: '按钮文案', type: 'input', default: '<i class="fa fas fa-fw fa-pen"></i>发布', dependsOn: { key: 'nav_newposts', value: true } },
{ key: 'nav_newposts_radius', label: '按钮圆角(胶囊)', type: 'switch', default: true, dependsOn: { key: 'nav_newposts', value: true } },
{ key: 'nav_newposts_class', label: '按钮颜色', type: 'select', default: 'jb-blue', options: [
{ label: '蓝色', value: 'jb-blue' },
{ label: '红色', value: 'jb-red' },
{ label: '绿色', value: 'jb-green' },
{ label: '黄色', value: 'jb-yellow' },
{ label: '紫色', value: 'jb-purple' },
{ label: '粉色', value: 'jb-pink' },
], dependsOn: { key: 'nav_newposts', value: true } },
{ key: 'nav_newposts_btns', label: '发布类型', type: 'checkbox', default: ['post'], options: [
{ label: '文章', value: 'post' },
], dependsOn: { key: 'nav_newposts', value: true } },
{ key: 'nav_user_newposts_btn', label: '用户卡发布类型', type: 'select', default: 'post', options: [
{ label: '文章', value: 'post' },
] },
{ key: 'nav_user_newposts_btn_text', label: '用户卡发布文案', type: 'input', default: '发布文章' },
{ key: 'nav_pay_vip', label: '导航栏开通会员按钮', type: 'switch', default: true },
{ key: 'nav_user_pay_vip', label: '用户框开通会员按钮', type: 'switch', default: true },
],
},
{
key: 'color',
title: '导航配色',
icon: 'fa fas fa-paint-brush',
fields: [
{ key: 'header_theme_custom', label: '自定义导航配色', type: 'switch', default: false, help: '启用后覆盖导航栏默认配色' },
{ key: 'header_theme_bg_custom', label: '导航背景色', type: 'color', default: '', dependsOn: { key: 'header_theme_custom', value: true } },
{ key: 'header_theme_bg', label: '导航背景色(备选)', type: 'color', default: '', dependsOn: { key: 'header_theme_custom', value: true } },
{ key: 'header_theme_color_custom', label: '导航文字色', type: 'color', default: '', dependsOn: { key: 'header_theme_custom', value: true } },
],
},
],
},
/**
* Category page settings
*/
category: {
key: 'page_display.category',
title: '分类页面',
icon: 'fa fas fa-folder-open',
description: '为分类归档页配置头图、简介、列表模式、筛选条和侧边栏。',
defaults: {
category_cover_enabled: true,
category_desc_enabled: true,
category_header_style: 'cover',
category_list_mode: 'grid',
category_filter_bar: true,
category_sidebar: true,
category_per_page: 12,
category_default_cover: '',
category_post_count: true,
category_orderby: 'modified',
},
groups: [
{
key: 'header',
title: '分类头图',
icon: 'fa fas fa-image',
fields: [
{ key: 'category_header_style', label: '头部风格', type: 'radio', default: 'cover', options: [
{ label: '封面图', value: 'cover' },
{ label: '简洁标题', value: 'simple' },
{ label: '隐藏', value: 'none' },
]},
{ key: 'category_cover_enabled', label: '显示封面图', type: 'switch', default: true, dependsOn: { key: 'category_header_style', value: 'cover' } },
{ key: 'category_desc_enabled', label: '显示分类简介', type: 'switch', default: true },
{ key: 'category_default_cover', label: '默认封面图', type: 'upload', default: '', help: '未设置分类封面时使用的默认图片' },
],
},
{
key: 'list',
title: '列表设置',
icon: 'fa fas fa-list',
fields: [
{ key: 'category_list_mode', label: '列表模式', type: 'radio', default: 'grid', options: [
{ label: '网格', value: 'grid' },
{ label: '列表', value: 'list' },
{ label: '紧凑', value: 'compact' },
]},
{ key: 'category_per_page', label: '每页文章数', type: 'number', default: 12, min: 4, max: 48 },
{ key: 'category_filter_bar', label: '筛选条', type: 'switch', default: true, help: '子分类筛选和排序' },
{ key: 'category_sidebar', label: '侧边栏', type: 'switch', default: true },
{ key: 'category_post_count', label: '显示文章数', type: 'switch', default: true, help: '在分类列表中显示每篇文章的浏览次数' },
],
},
{
key: 'orderby',
title: '排序设置',
icon: 'fa fas fa-sort',
fields: [
{ key: 'category_orderby', label: '默认排序', type: 'select', default: 'modified', options: [
{ label: '最近更新', value: 'modified' },
{ label: '发布时间', value: 'date' },
{ label: '浏览数', value: 'views' },
{ label: '评论数', value: 'comments' },
{ label: '点赞数', value: 'likes' },
]},
],
},
],
},
/**
* Tag page settings
*/
tag: {
key: 'page_display.tag',
title: '标签页面',
icon: 'fa fas fa-tags',
description: '为标签归档页配置展示样式和布局。',
defaults: {
tag_header_style: 'simple',
tag_default_cover: '',
tag_list_mode: 'grid',
tag_sidebar: true,
tag_per_page: 12,
tag_post_count: true,
tag_orderby: 'count',
},
groups: [
{
key: 'header',
title: '标签头图',
icon: 'fa fas fa-tag',
fields: [
{ key: 'tag_header_style', label: '头部风格', type: 'radio', default: 'simple', options: [
{ label: '简洁标题', value: 'simple' },
{ label: '封面图', value: 'cover' },
{ label: '隐藏', value: 'none' },
]},
{ key: 'tag_default_cover', label: '默认封面图', type: 'upload', default: '' },
],
},
{
key: 'list',
title: '列表设置',
icon: 'fa fas fa-list',
fields: [
{ key: 'tag_list_mode', label: '列表模式', type: 'radio', default: 'grid', options: [
{ label: '网格', value: 'grid' },
{ label: '列表', value: 'list' },
]},
{ key: 'tag_per_page', label: '每页文章数', type: 'number', default: 12, min: 4, max: 48 },
{ key: 'tag_sidebar', label: '侧边栏', type: 'switch', default: true },
{ key: 'tag_post_count', label: '显示文章数', type: 'switch', default: true },
],
},
{
key: 'orderby',
title: '排序设置',
icon: 'fa fas fa-sort',
fields: [
{ key: 'tag_orderby', label: '默认排序', type: 'select', default: 'count', options: [
{ label: '文章数', value: 'count' },
{ label: '名称', value: 'name' },
{ label: '最近更新', value: 'modified' },
]},
],
},
],
},
/**
* User homepage settings
*/
'user-home': {
key: 'page_display.user-home',
title: '用户主页',
icon: 'fa fas fa-user-circle',
description: '公开用户主页对齐 Zibll 作者页:通栏头图 + Tab 计数;交易/快捷入口仅在用户中心。',
defaults: {
user_home_tabs: 'posts,favorites,comments,follow',
user_home_tabs_order: 'posts,favorites,comments,follow',
user_home_tabs_pc_hide: '',
user_home_tabs_mobile_hide: '',
user_home_sidebar: false,
user_home_cover: true,
user_home_route_identifier: 'nicename',
user_home_follow: true,
user_home_posts_per_page: 10,
user_home_avatar: true,
user_home_name: true,
user_home_desc: true,
user_home_stats: false,
user_home_social: true,
author_main_tab_swiper: false,
user_home_message: true,
user_home_private: true,
user_home_paging_ajax: true,
},
groups: [
{
key: 'layout',
title: '主页布局',
icon: 'fa fas fa-th-large',
fields: [
{ key: 'user_home_cover', label: '封面区域', type: 'switch', default: true, help: '用户主页顶部封面图' },
{ key: 'user_home_route_identifier', label: '主页 URL 标识', type: 'select', default: 'nicename', options: [{ label: '用户名 URL slug', value: 'nicename' }, { label: '数字用户 ID', value: 'uid' }] },
{ key: 'user_home_sidebar', label: '侧边栏(已弃用)', type: 'switch', default: false, help: '公开主页为通栏布局(对齐 Zibll author);运行时强制关闭。交易服务请去用户中心。' },
{ key: 'user_home_follow', label: '关注按钮', type: 'switch', default: true },
{ key: 'user_home_posts_per_page', label: '每页文章数', type: 'number', default: 10, min: 5, max: 50 },
{ key: 'user_home_paging_ajax', label: 'AJAX 翻页', type: 'switch', default: true, help: '使用 AJAX 加载替代传统翻页' },
],
},
{
key: 'tabs',
title: 'Tab 配置',
icon: 'fa fas fa-columns',
fields: [
{ 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: '逗号分隔要在移动端隐藏的栏目' },
{ key: 'author_main_tab_swiper', label: '移动端滑动切换', type: 'switch', default: false, help: '移动端左右滑动切换栏目' },
],
},
{
key: 'display',
title: '展示设置',
icon: 'fa fas fa-eye',
fields: [
{ key: 'user_home_avatar', label: '显示头像', type: 'switch', default: true },
{ key: 'user_home_name', label: '显示用户名', type: 'switch', default: true },
{ key: 'user_home_desc', label: '显示个人简介', type: 'switch', default: true },
{ key: 'user_home_stats', label: '显示统计数据(已弃用)', type: 'switch', default: false, help: '计数已并入 Tab 角标,头图内不再重复文章/评论/收藏。' },
{ key: 'user_home_social', label: '显示社交链接', type: 'switch', default: true },
{ key: 'user_home_message', label: '私信/消息按钮', type: 'switch', default: true, help: '本人显示消息中心入口;访客显示私信入口' },
{ key: 'user_home_private', label: '留言按钮', type: 'switch', default: true, help: '预留开关,公开主页留言入口' },
],
},
],
},
/**
* Other pages settings (404, search results, etc.)
*/
other: {
key: 'page_display.other',
title: '其它页面',
icon: 'fa fas fa-file',
description: '配置 404 页面、搜索结果页等特殊页面的展示。',
defaults: {
page_header_style: '1',
page_header_cover_img: '',
page_404_search: true,
page_404_popular: true,
page_404_message: '页面未找到',
page_404_back: true,
page_search_sidebar: true,
page_search_per_page: 12,
page_author_cover: true,
page_author_sidebar: true,
page_author_posts_per_page: 12,
},
groups: [
{
key: 'page-header',
title: '统一页头',
icon: 'fa fas fa-heading',
fields: [
{ key: 'page_header_style', label: '页面标题样式', type: 'select', default: '1', options: [
{ label: '不显示', value: '' },
{ label: '简单', value: '1' },
{ label: '卡片', value: '2' },
{ label: '封面', value: '3' },
], help: '控制归档/搜索/页面的页头样式,单页可通过 post meta 覆盖' },
{ key: 'page_header_cover_img', label: '默认封面图', type: 'upload', default: '', help: '封面样式(样式3)的默认背景图,单页可覆盖' },
],
},
{
key: 'page_404',
title: '404 页面',
icon: 'fa fas fa-exclamation-triangle',
fields: [
{ key: 'page_404_message', label: '404 提示文字', type: 'input', default: '页面未找到' },
{ key: 'page_404_search', label: '显示搜索框', type: 'switch', default: true },
{ key: 'page_404_popular', label: '显示热门文章', type: 'switch', default: true },
{ key: 'page_404_back', label: '显示返回首页', type: 'switch', default: true },
],
},
{
key: 'search_results',
title: '搜索结果页',
icon: 'fa fas fa-search',
fields: [
{ key: 'page_search_sidebar', label: '搜索结果侧边栏', type: 'switch', default: true },
{ key: 'page_search_per_page', label: '每页结果数', type: 'number', default: 12, min: 4, max: 48 },
],
},
{
key: 'author',
title: '作者页面',
icon: 'fa fas fa-user',
fields: [
{ key: 'page_author_cover', label: '显示封面', type: 'switch', default: true },
{ key: 'page_author_sidebar', label: '显示侧边栏', type: 'switch', default: true },
{ key: 'page_author_posts_per_page', label: '每页文章数', type: 'number', default: 12, min: 4, max: 48 },
],
},
],
},
/**
* Global background settings
*/
background: {
key: 'page_display.background',
title: '全局背景',
icon: 'fa fas fa-image',
description: '配置站点全局背景图、背景色、暗色模式背景和遮罩。',
defaults: {
bg_color: '',
bg_image: '',
bg_size: '100%',
bg_position: 'center center',
bg_repeat: 'no-repeat',
bg_attachment: 'fixed',
bg_dark_color: '',
bg_dark_image: '',
bg_overlay: false,
bg_overlay_color: 'rgba(0, 0, 0, 0.3)',
page_bg_color: '',
page_bg_image: '',
},
groups: [
{
key: 'light',
title: '亮色背景',
icon: 'fa fas fa-sun',
fields: [
{ key: 'bg_color', label: '背景颜色', type: 'color', default: '', help: '留空则使用主题默认背景色' },
{ key: 'bg_image', label: '背景图片', type: 'media', help: '推荐使用高分辨率图片' },
{ key: 'bg_size', label: '图片尺寸', type: 'select', default: 'cover', options: [
{ label: '覆盖 (Cover)', value: 'cover' },
{ label: '包含 (Contain)', value: 'contain' },
{ label: '原始大小', value: 'auto' },
{ label: '拉伸填充', value: '100% 100%' },
{ label: '100% 宽度(旧版兼容)', value: '100%' },
]},
{ key: 'bg_position', label: '图片位置', type: 'select', default: 'center center', options: [
{ label: '居中', value: 'center center' },
{ label: '顶部居中', value: 'top center' },
{ label: '底部居中', value: 'bottom center' },
]},
{ key: 'bg_repeat', label: '图片重复', type: 'select', default: 'no-repeat', options: [
{ label: '不重复', value: 'no-repeat' },
{ label: '平铺', value: 'repeat' },
{ label: '水平平铺', value: 'repeat-x' },
{ label: '垂直平铺', value: 'repeat-y' },
]},
{ key: 'bg_attachment', label: '固定方式', type: 'select', default: 'fixed', options: [
{ label: '固定 (视差)', value: 'fixed' },
{ label: '随内容滚动', value: 'scroll' },
]},
],
},
{
key: 'dark',
title: '暗色背景',
icon: 'fa fas fa-moon',
fields: [
{ key: 'bg_dark_color', label: '暗色背景颜色', type: 'color', default: '', help: '留空则使用主题默认暗色背景' },
{ key: 'bg_dark_image', label: '暗色背景图片', type: 'media', help: '暗色模式下显示的背景图' },
],
},
{
key: 'overlay',
title: '遮罩设置',
icon: 'fa fas fa-clone',
fields: [
{ key: 'bg_overlay', label: '启用背景遮罩', type: 'switch', default: false, help: '在背景图上方叠加半透明遮罩,提升文字可读性' },
{ key: 'bg_overlay_color', label: '遮罩颜色', type: 'input', default: 'rgba(0, 0, 0, 0.3)', dependsOn: { key: 'bg_overlay', value: true }, help: '支持 rgba/hex 色值' },
],
},
{
key: 'page',
title: '页面背景',
icon: 'fa fas fa-file',
fields: [
{ key: 'page_bg_color', label: '内容区背景色', type: 'color', default: '', help: '主内容区域的背景色' },
{ key: 'page_bg_image', label: '内容区背景图', type: 'media' },
],
},
],
},
/**
* Header components
*/
'header-components': {
key: 'page_display.header-components',
title: '顶部多功能组件',
icon: 'fa fas fa-th',
description: 'Zibll 顶部幻灯、搜索覆盖层和图标卡片的原始字段契约。',
defaults: {
header_slider_show: [],
header_slider_show_type: '',
header_slider_nav_color: '#ffffff',
header_slider: [
{ background: themeAsset('assets/vendor/zibll/img/slider-bg.jpg'), link: { url: 'https://www.zibll.com/', target: '_blank' }, text: { desc: '', title: '' }, text_align: 'left-bottom', text_parallax: 30, text_size_m: 20, text_size_pc: 30 },
{ background: themeAsset('assets/vendor/zibll/img/user_t.jpg'), link: { url: 'https://www.zibll.com/', target: '_blank' }, text: { desc: '', title: '' }, text_align: 'left-bottom', text_parallax: 30, text_size_m: 20, text_size_pc: 30 },
],
header_slider_option: { direction: 'horizontal', loop: true, button: true, pagination: true, effect: 'slide', scale_height: false, auto_height: false, pc_height: 550, m_height: 280, spacebetween: 0, speed: 0, autoplay: true, interval: 6 },
header_slider_search_s: ['pc_s', 'm_s'],
header_slider_search_option: { show_keywords: true, popular_limit: 6, show_input_cat: false, in_cat: '', more_cats: [], show_type: false, in_type: 'post', before_html: '<div class="em16 font-bold mb10">更优雅的Wordpress主题</div>\n<div>这是导航栏幻灯片多功能组件,可在后台进行配置</div>', after_html: '' },
header_slider_card_s: ['pc_s', 'm_s'],
header_slider_card_option: { show_widget_bg: false, icon_radius4: false, cards: [
{ icon: 'zibsvg-vip_1', customize_icon: '', icon_class: 'c-yellow', link: {}, title: '图标卡片', desc: '这是一个图标卡片示例' },
{ icon: 'zibsvg-vip_2', customize_icon: '', icon_class: 'c-blue-2', link: {}, title: '原创作品', desc: '这是一个图标卡片示例' },
{ icon: 'zibsvg-hot', customize_icon: '', icon_class: 'jb-pink', link: {}, title: '灵感来源<badge class="ml6 jb-yellow">NEW</badge>', desc: '这是一个图标卡片示例' },
{ icon: 'fa fas fa-ioxhost', customize_icon: '', icon_class: 'jb-cyan', link: {}, title: '系统工具 <badge class="ml6 jb-blue">GO <span class="fa fas fa-angle-right em12"></span></badge>', desc: '这是一个图标卡片示例' },
] },
header_slider_filter_blur: ['pc_s', 'm_s'],
},
groups: [
{
key: 'visibility',
title: '显示规则',
icon: 'fa fas fa-eye',
fields: [
{ key: 'header_slider_show', label: '启用顶部多功能组件的页面', type: 'checkbox', default: [], options: [
{ label: '首页', value: 'home' },
{ label: '分类页', value: 'category' },
{ label: '标签页', value: 'tag' },
{ label: '用户页', value: 'author' },
{ label: '文章页', value: 'single' },
{ label: '搜索页', value: 'search' },
{ label: '其它页面', value: 'page' },
{ label: '专题页', value: 'topics' },
{ label: '论坛首页', value: 'forum_home' },
{ label: '论坛板块', value: 'forum_plate' },
{ label: '论坛帖子', value: 'forum_post' },
] },
{ key: 'header_slider_show_type', label: '设备显示规则', type: 'radio', default: '', options: [
{ label: '全部', value: '' },
{ label: '仅电脑', value: 'only_pc' },
{ label: '仅移动', value: 'only_sm' },
], dependsOn: { key: 'header_slider_show', not: [] } },
{ key: 'header_slider_nav_color', label: '导航栏文字颜色', type: 'color', default: '#ffffff', dependsOn: { key: 'header_slider_show', not: [] } },
],
},
{
key: 'slides',
title: '背景内容',
icon: 'fa fas fa-image',
fields: [
{ key: 'header_slider', label: '幻灯片项目', type: 'json', sanitizeType: 'array', default: [
{ background: themeAsset('assets/vendor/zibll/img/slider-bg.jpg'), link: { url: 'https://www.zibll.com/', target: '_blank' }, text: { desc: '', title: '' }, text_align: 'left-bottom', text_parallax: 30, text_size_m: 20, text_size_pc: 30 },
{ background: themeAsset('assets/vendor/zibll/img/user_t.jpg'), link: { url: 'https://www.zibll.com/', target: '_blank' }, text: { desc: '', title: '' }, text_align: 'left-bottom', text_parallax: 30, text_size_m: 20, text_size_pc: 30 },
], help: '原始数组字段:background、background_video、video、hide、link、image_layer、text。' },
],
},
{
key: 'slider-config',
title: '背景显示设置',
icon: 'fa fas fa-sliders-h',
fields: [
{ key: 'header_slider_option', label: '幻灯片参数', type: 'json', sanitizeType: 'array', default: { direction: 'horizontal', loop: true, button: true, pagination: true, effect: 'slide', scale_height: false, auto_height: false, pc_height: 550, m_height: 280, spacebetween: 0, speed: 0, autoplay: true, interval: 6 } },
],
},
{
key: 'search',
title: '搜索组件',
icon: 'fa fas fa-search',
fields: [
{ key: 'header_slider_search_s', label: '显示设备', type: 'checkbox', default: ['pc_s', 'm_s'], options: [{ label: 'PC 端显示', value: 'pc_s' }, { label: '移动端显示', value: 'm_s' }] },
{ key: 'header_slider_search_option', label: '搜索组件参数', type: 'json', sanitizeType: 'array', default: { show_keywords: true, popular_limit: 6, show_input_cat: false, in_cat: '', more_cats: [], show_type: false, in_type: 'post', before_html: '<div class="em16 font-bold mb10">更优雅的Wordpress主题</div>\n<div>这是导航栏幻灯片多功能组件,可在后台进行配置</div>', after_html: '' } },
],
},
{
key: 'cards',
title: '图标卡片组件',
icon: 'fa fas fa-th-large',
fields: [
{ key: 'header_slider_card_s', label: '显示设备', type: 'checkbox', default: ['pc_s', 'm_s'], options: [{ label: 'PC 端显示', value: 'pc_s' }, { label: '移动端显示', value: 'm_s' }] },
{ key: 'header_slider_card_option', label: '图标卡片参数', type: 'json', sanitizeType: 'array', default: { show_widget_bg: false, icon_radius4: false, cards: [
{ icon: 'zibsvg-vip_1', customize_icon: '', icon_class: 'c-yellow', link: {}, title: '图标卡片', desc: '这是一个图标卡片示例' },
{ icon: 'zibsvg-vip_2', customize_icon: '', icon_class: 'c-blue-2', link: {}, title: '原创作品', desc: '这是一个图标卡片示例' },
{ icon: 'zibsvg-hot', customize_icon: '', icon_class: 'jb-pink', link: {}, title: '灵感来源<badge class="ml6 jb-yellow">NEW</badge>', desc: '这是一个图标卡片示例' },
{ icon: 'fa fas fa-ioxhost', customize_icon: '', icon_class: 'jb-cyan', link: {}, title: '系统工具 <badge class="ml6 jb-blue">GO <span class="fa fas fa-angle-right em12"></span></badge>', desc: '这是一个图标卡片示例' },
] } },
{ key: 'header_slider_filter_blur', label: '组件高斯模糊特效', type: 'checkbox', default: ['pc_s', 'm_s'], options: [{ label: 'PC 端开启', value: 'pc_s' }, { label: '移动端开启', value: 'm_s' }] },
],
},
],
},
footer: {
key: 'page_display.footer',
title: '底部页脚',
icon: 'fa fas fa-paw',
description: '页脚布局、友链、版权信息配置。实际编辑界面在 FooterSettings.vue(customPage),此处为 REST 聚合描述。',
defaults: {
footer_columns: 3,
footer_copyright: true,
footer_icp: true,
footer_police: true,
footer_theme_custom: false,
footer_theme_bg_custom: '',
footer_theme_bg: '',
footer_theme_color_custom: '',
footer_contact_wechat_img: '',
footer_contact_qq: '',
footer_contact_weibo: '',
footer_contact_email: '',
footer_t1_m_s: ['pc', 'mobile'],
footer_t1_img: '',
footer_t1_img_dark: '',
footer_t1_t: '',
fcode_t1_code: '',
footer_contact_m_s: ['pc', 'mobile'],
footer_contact_more: '',
fcode_t2_code_1: '',
fcode_t2_code_2: '',
footer_mini_img_m_s: ['pc', 'mobile'],
footer_mini_img: '',
fcode_customize_code: '',
},
groups: [
{
key: 'layout',
title: '页脚布局',
icon: 'fa fas fa-columns',
fields: [
{ key: 'footer_columns', label: '页脚列数', type: 'number', default: 3, min: 1, max: 4 },
{ key: 'footer_copyright', label: '显示版权信息', type: 'switch', default: true },
{ key: 'footer_icp', label: '显示ICP备案', type: 'switch', default: true },
{ key: 'footer_police', label: '显示公安备案', type: 'switch', default: true },
],
},
{
key: 'color',
title: '页脚颜色',
icon: 'fa fas fa-paint-brush',
fields: [
{ key: 'footer_theme_custom', label: '自定义页脚颜色', type: 'switch', default: false },
{ key: 'footer_theme_bg_custom', label: '自定义背景色', type: 'color', default: '' },
{ key: 'footer_theme_bg', label: '背景图片', type: 'media', default: '' },
{ key: 'footer_theme_color_custom', label: '自定义文字颜色', type: 'color', default: '' },
],
},
{
key: 'contact',
title: '联系方式',
icon: 'fa fas fa-address-book',
fields: [
{ key: 'footer_contact_wechat_img', label: '微信二维码', type: 'media', default: '' },
{ key: 'footer_contact_qq', label: 'QQ', type: 'input', default: '' },
{ key: 'footer_contact_weibo', label: '微博', type: 'input', default: '' },
{ key: 'footer_contact_email', label: '邮箱', type: 'input', default: '' },
],
},
{
key: 'brand',
title: '品牌列(第一列)',
icon: 'fa fas fa-building',
fields: [
{ key: 'footer_t1_m_s', label: '显示设备', type: 'checkbox', default: ['pc', 'mobile'] },
{ key: 'footer_t1_img', label: '页脚 Logo(日间)', type: 'media', default: '' },
{ key: 'footer_t1_img_dark', label: '页脚 Logo(夜间)', type: 'media', default: '' },
{ key: 'footer_t1_t', label: '品牌标题', type: 'input', default: '' },
{ key: 'fcode_t1_code', label: '品牌列自定义代码', type: 'textarea', default: '' },
],
},
{
key: 'contact_ext',
title: '联系方式扩展',
icon: 'fa fas fa-phone',
fields: [
{ key: 'footer_contact_m_s', label: '显示设备', type: 'checkbox', default: ['pc', 'mobile'] },
{ key: 'footer_contact_more', label: '更多联系项', type: 'textarea', default: '' },
{ key: 'fcode_t2_code_1', label: '链接 HTML(fcode_t2_code_1)', type: 'textarea', default: '' },
{ key: 'fcode_t2_code_2', label: '链接下方自定义代码', type: 'textarea', default: '' },
],
},
{
key: 'mini_qr',
title: '二维码图片组(第三列)',
icon: 'fa fas fa-qrcode',
fields: [
{ key: 'footer_mini_img_m_s', label: '显示设备', type: 'checkbox', default: ['pc', 'mobile'] },
{ key: 'footer_mini_img', label: '二维码组', type: 'textarea', default: '' },
],
},
{
key: 'custom',
title: '底部自定义',
icon: 'fa fas fa-code',
fields: [
{ key: 'fcode_customize_code', label: '底部自定义代码', type: 'textarea', default: '' },
],
},
],
},
/**
* Homepage settings (already has a real Vue page)
*/
homepage: {
key: 'page_display.homepage',
title: '首页配置',
icon: 'fa fas fa-home',
description: '配置首页主列表、分类 Tab 与首页交互。文章列表视觉统一由文章列表设置控制。',
defaults: {
homepage_tabs: [],
homepage_sidebar_mini_enabled: true,
homepage_default_sort: 'date',
homepage_sort_ajax_enabled: true,
homepage_sticky_tabs_enabled: true,
home_posts_list_s: true,
home_list_title: '最新发布',
home_list_sort_enabled: true,
home_list_sort_options: ['modified', 'date', 'views', 'likes', 'comments'],
home_list_sort_dropdown: false,
home_exclude_post_ids: [],
home_exclude_category_ids: [],
homepage_tab_posts_per_page: 6,
homepage_tab_limit: 4,
},
groups: [
{
key: 'main-stream',
title: '主列表',
icon: 'fa fas fa-list',
fields: [
{ key: 'home_posts_list_s', label: '启用主列表', type: 'switch', default: true },
{ key: 'index_list_title', label: '区块标题', type: 'input', default: '最新发布', dependsOn: { key: 'home_posts_list_s', value: true } },
{ key: 'home_list1_orderby_option.orderby', label: '默认排序', type: 'select', default: 'date', dependsOn: { key: 'home_posts_list_s', value: true }, options: sortOptions },
{ key: 'home_list1_orderby_s', label: '启用排序栏', type: 'switch', default: true, dependsOn: { key: 'home_posts_list_s', value: true } },
{ key: 'home_list_sort_dropdown', label: '排序栏下拉模式', type: 'switch', default: false, dependsOn: { key: 'home_list1_orderby_s', value: true } },
{ key: 'home_list1_orderby_option', label: '可用排序选项', type: 'checkbox', default: ['modified', 'date', 'views', 'likes', 'comments'], dependsOn: { key: 'home_list1_orderby_s', value: true }, options: sortOptions },
],
},
{
key: 'exclude',
title: '排除规则',
icon: 'fa fas fa-filter',
fields: [
{ key: 'home_exclude_posts', label: '排除文章 ID', type: 'array', default: [], help: '主列表和 Tab 查询均排除这些文章。' },
{ key: 'home_exclude_cats', label: '排除分类 ID', type: 'array', default: [], help: '主列表排除这些分类。' },
],
},
{
key: 'tabs',
title: '首页 Tab',
icon: 'fa fas fa-tags',
fields: [
{ key: 'home_lists', label: '首页 Tab', type: 'array', default: [], help: '分类 Tab 由 HomepageSettings.vue 中的可视化编辑器维护。' },
{ key: 'homepage_tab_limit', label: 'Tab 数量上限', type: 'number', default: 4, min: 1, max: 12 },
{ key: 'homepage_tab_posts_per_page', label: '每 Tab 文章数', type: 'number', default: 6, min: 1, max: 20 },
{ key: 'homepage_sticky_tabs_enabled', label: 'Tab 吸顶', type: 'switch', default: true },
],
},
{
key: 'cards-sidebar',
title: '侧栏与交互',
icon: 'fa fas fa-columns',
fields: [
{ key: 'homepage_sidebar_mini_enabled', label: '迷你侧栏列表', type: 'switch', default: true },
{ key: 'homepage_sort_ajax_enabled', label: '排序栏 AJAX', type: 'switch', default: true },
],
},
],
},
}
+126 -1
View File
@@ -37,7 +37,114 @@
#aksrt-admin-app .aksrt-admin-layout {
min-height: 100vh;
background: #f5f7fb;
background: rgb(249, 250, 252);
}
#aksrt-admin-app .aksrt-csf-shell .n-layout-sider {
background: #fff;
border-right: 1px solid #f2f2f2;
}
#aksrt-admin-app .aksrt-csf-shell .n-menu-item-content {
border-bottom: 1px solid #f4f4f4;
border-radius: 0;
}
#aksrt-admin-app .aksrt-csf-shell .n-menu-item-content--selected,
#aksrt-admin-app .aksrt-csf-shell .n-menu-item-content:hover {
color: #0997f5;
background: rgb(249, 250, 252);
}
#aksrt-admin-app .aksrt-schema-page {
max-width: none;
}
#aksrt-admin-app .aksrt-form-renderer__header {
margin: -30px -30px 0;
padding: 18px 30px;
border-bottom: 1px solid #eee;
background: #fff;
}
#aksrt-admin-app .aksrt-form-renderer__title {
color: #0991f5;
font-size: 19px;
}
#aksrt-admin-app .aksrt-field-group {
margin: 0 -30px;
border: 0;
border-radius: 0;
background: transparent;
}
#aksrt-admin-app .aksrt-field-group__body {
padding: 0;
}
#aksrt-admin-app .aksrt-field {
display: grid;
grid-template-columns: minmax(150px, 22%) minmax(0, 1fr);
column-gap: 24px;
align-items: start;
margin: 0;
padding: 20px 30px;
border-bottom: 1px solid #eee;
}
#aksrt-admin-app .aksrt-field > :not(.aksrt-field__label-row):not(.aksrt-field__desc):not(.aksrt-field__error) {
grid-column: 2;
}
#aksrt-admin-app .aksrt-field__label-row {
grid-column: 1;
grid-row: 1;
margin: 4px 0 0;
}
#aksrt-admin-app .aksrt-field__label {
color: #0991f5;
font-weight: 600;
}
#aksrt-admin-app .aksrt-field__desc {
grid-column: 1;
margin: 26px 0 0;
}
#aksrt-admin-app .aksrt-field--notice {
display: block;
padding-top: 14px;
padding-bottom: 14px;
background: #fff8e5;
}
#aksrt-admin-app .aksrt-field__fieldset .aksrt-field,
#aksrt-admin-app .aksrt-field__repeater .aksrt-field,
#aksrt-admin-app .aksrt-field__sortable .aksrt-field {
padding: 12px 0;
border-bottom-color: #eee;
}
#aksrt-admin-app .aksrt-field__repeater-item,
#aksrt-admin-app .aksrt-field__fieldset {
border-radius: 2px;
background: #fff;
}
#aksrt-admin-app .aksrt-form-renderer__save-bar {
left: auto;
right: 0;
bottom: 0;
border-top: 1px solid #f2f2f2;
background: rgba(255, 255, 255, .96);
}
#aksrt-admin-app .aksrt-form-renderer__save-bar .n-button--primary-type {
border-radius: 30px;
background-image: linear-gradient(135deg, #65bff7 10%, #0d67b9 100%);
box-shadow: -1px 2px 10px rgba(1, 65, 139, .3);
}
#aksrt-admin-app .aksrt-page-container,
@@ -253,6 +360,24 @@
padding: 0;
}
#aksrt-admin-app .aksrt-form-renderer__header {
margin: -14px -12px 0;
padding: 14px 16px;
}
#aksrt-admin-app .aksrt-field-group {
margin: 0 -12px;
}
#aksrt-admin-app .aksrt-field {
display: block;
padding: 16px;
}
#aksrt-admin-app .aksrt-field__desc {
margin: 2px 0 8px;
}
#aksrt-admin-app .aksrt-page-title {
font-size: 18px;
margin-bottom: 8px;
@@ -1,491 +0,0 @@
/**
* AKSRT CMS Home Module Schema
*
* 定义每类模块的:
* - 元数据(label / icon / desc)
* - 默认 config
* - 表单字段(用于配置抽屉自动生成 UI)
*
* 字段类型 type:
* - text n-input
* - number n-input-number
* - select n-select (options)
* - radio n-radio-group (options)
* - switch n-switch
* - cats n-select (multiple, 动态加载分类)
* - cat n-select (single, 动态加载分类)
* - textarea n-input type=textarea
*/
export const MODULE_TYPES = [
'banner',
'category',
'tabs',
'ranking',
'recent',
'tag_cloud',
'friend_links',
'html',
'hotspot',
'category_recommend_cards',
'text_title',
'button_group',
'spacing',
'faq',
'data_grid',
'highlight_cards',
'partner_marquee',
'product_showcase',
'user',
'more',
]
export const MODULE_LIBRARY = [
{
type: 'banner',
label: 'Banner 轮播',
icon: 'rectangle',
desc: '固定首页骨架:正文区轮播条。幻灯片内容在「页面&显示 → Banner 轮播」维护。',
defaultConfig: {},
fields: [],
},
{
type: 'category',
label: '分类块',
icon: 'folder',
desc: '指定分类的最新内容网格',
defaultConfig: { cat_id: 0, count: 8, layout: 'card', columns: 4, title: '' },
fields: [
{ key: 'title', type: 'text', label: '标题(留空用分类名)' },
{ key: 'cat_id', type: 'cat', label: '分类' },
{ key: 'count', type: 'number', label: '数量', min: 1, max: 20 },
{ key: 'columns', type: 'number', label: '列数', min: 1, max: 6 },
{
key: 'layout', type: 'select', label: '布局',
options: [
{ label: '卡片', value: 'card' },
{ label: '列表', value: 'list' },
],
},
],
},
{
type: 'tabs',
label: '分类 Tabs',
icon: 'switcher',
desc: '多分类 Tab 切换',
defaultConfig: { cat_ids: [], count: 8, layout: 'card', title: '' },
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'cat_ids', type: 'cats', label: '分类列表' },
{ key: 'count', type: 'number', label: '每个 Tab 数量', min: 1, max: 20 },
{
key: 'layout', type: 'select', label: '布局',
options: [
{ label: '卡片', value: 'card' },
{ label: '列表', value: 'list' },
],
},
],
},
{
type: 'ranking',
label: '排行榜',
icon: 'trophy',
desc: '按下载量 / 阅读量 / 评论数排序(普通文章)',
defaultConfig: { metric: 'downloads', count: 6, title: '' },
fields: [
{ key: 'title', type: 'text', label: '标题' },
{
key: 'metric', type: 'radio', label: '排序依据',
options: [
{ label: '下载量', value: 'downloads' },
{ label: '阅读量', value: 'views' },
{ label: '评论数', value: 'comments' },
],
},
{ key: 'count', type: 'number', label: '数量', min: 1, max: 20 },
],
},
{
type: 'recent',
label: '最新文章',
icon: 'clock',
desc: '按时间倒序的最新文章',
defaultConfig: { count: 6, layout: 'card', title: '' },
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'count', type: 'number', label: '数量', min: 1, max: 20 },
{
key: 'layout', type: 'select', label: '布局',
options: [
{ label: '卡片', value: 'card' },
{ label: '列表', value: 'list' },
],
},
],
},
{
type: 'tag_cloud',
label: '标签云',
icon: 'tag',
desc: '按使用频率展示热门标签',
defaultConfig: { limit: 30, title: '' },
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'limit', type: 'number', label: '数量', min: 5, max: 100 },
],
},
{
type: 'friend_links',
label: '友情链接',
icon: 'link',
desc: '展示已配置的友链',
defaultConfig: { category: '', title: '' },
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'category', type: 'text', label: '分类筛选(可空)' },
],
},
{
type: 'html',
label: '自定义 HTML',
icon: 'code',
desc: '自由插入 HTML 片段(受 wp_kses_post 清洗)',
defaultConfig: { html: '', title: '' },
fields: [
{ key: 'title', type: 'text', label: '标题(可空)' },
{ key: 'html', type: 'textarea', label: 'HTML 内容', rows: 6 },
],
},
{
type: 'hotspot',
label: '热点推荐',
icon: 'fire',
desc: '文章推荐列表(纯文本,仅桌面端显示)',
defaultConfig: { count: 6, source: 'auto', title: '' },
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'count', type: 'number', label: '数量', min: 3, max: 20 },
{
key: 'source', type: 'select', label: '数据来源',
options: [
{ label: '自动推荐(推荐文章优先)', value: 'auto' },
{ label: '最新文章', value: 'recent' },
{ label: '最热文章(按阅读量)', value: 'views' },
],
},
],
},
{
type: 'category_recommend_cards',
label: '分类推荐卡片',
icon: 'grid',
desc: '4 张分类卡片 2×2 布局,含封面图、浏览量胶囊和文章列表',
defaultConfig: {
title: '分类推荐',
cat_ids: [],
show_views: true,
show_dates: true,
},
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'cat_ids', type: 'cats', label: '分类(按选择顺序对应卡片 1-4)' },
{ key: 'show_views', type: 'switch', label: '显示浏览量胶囊' },
{ key: 'show_dates', type: 'switch', label: '显示发布时间' },
],
},
{
type: 'text_title',
label: '标题文案',
icon: 'font-colors',
desc: '带标题、副标题和可选按钮的文案区块',
defaultConfig: {
title: '',
desc: '',
align: 'left',
style: 'default',
button_text: '',
button_url: '',
button_style: 'primary',
},
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'desc', type: 'textarea', label: '副标题/描述', rows: 2 },
{ key: 'align', type: 'radio', label: '对齐', options: [
{ label: '左对齐', value: 'left' },
{ label: '居中', value: 'center' },
{ label: '右对齐', value: 'right' },
]},
{ key: 'style', type: 'select', label: '样式', options: [
{ label: '默认', value: 'default' },
{ label: '带左边框', value: 'bordered' },
{ label: '带背景', value: 'bg' },
]},
{ key: 'button_text', type: 'text', label: '按钮文字(可空)' },
{ key: 'button_url', type: 'text', label: '按钮链接' },
{ key: 'button_style', type: 'select', label: '按钮样式', options: [
{ label: '主色', value: 'primary' },
{ label: '轮廓', value: 'outline' },
{ label: '文字', value: 'text' },
]},
],
},
{
type: 'button_group',
label: '按钮组',
icon: 'th-large',
desc: '多按钮横向排列,适合快捷入口',
defaultConfig: {
title: '',
buttons: [],
columns: 4,
style: 'card',
},
fields: [
{ key: 'title', type: 'text', label: '标题(可空)' },
{ key: 'columns', type: 'number', label: '每行列数', min: 2, max: 6 },
{ key: 'style', type: 'select', label: '按钮样式', options: [
{ label: '卡片', value: 'card' },
{ label: '图标', value: 'icon' },
{ label: '文字', value: 'text' },
]},
],
},
{
type: 'spacing',
label: '间距分隔',
icon: 'swap',
desc: '模块间间距或分隔线',
defaultConfig: {
height: 40,
divider: false,
divider_style: 'solid',
},
fields: [
{ key: 'height', type: 'number', label: '高度 (px)', min: 0, max: 200 },
{ key: 'divider', type: 'switch', label: '显示分隔线' },
{ key: 'divider_style', type: 'select', label: '分隔线样式', options: [
{ label: '实线', value: 'solid' },
{ label: '虚线', value: 'dashed' },
{ label: '点线', value: 'dotted' },
]},
],
},
{
type: 'faq',
label: '常见问题',
icon: 'question-circle',
desc: '折叠式 FAQ 列表',
defaultConfig: {
title: '常见问题',
items: [],
style: 'accordion',
},
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'style', type: 'radio', label: '样式', options: [
{ label: '手风琴', value: 'accordion' },
{ label: '卡片', value: 'card' },
]},
],
},
{
type: 'data_grid',
label: '数据网格',
icon: 'table',
desc: '多列数据卡片展示(统计、特征等)',
defaultConfig: {
title: '',
columns: 4,
items: [],
style: 'number',
},
fields: [
{ key: 'title', type: 'text', label: '标题(可空)' },
{ key: 'columns', type: 'number', label: '列数', min: 2, max: 6 },
{ key: 'style', type: 'select', label: '样式', options: [
{ label: '数字突出', value: 'number' },
{ label: '图标突出', value: 'icon' },
{ label: '简洁', value: 'minimal' },
]},
],
},
{
type: 'highlight_cards',
label: '高亮卡片',
icon: 'star',
desc: '带配色的特色卡片网格',
defaultConfig: {
title: '',
columns: 3,
cards: [],
},
fields: [
{ key: 'title', type: 'text', label: '标题(可空)' },
{ key: 'columns', type: 'number', label: '列数', min: 2, max: 4 },
],
},
{
type: 'partner_marquee',
label: '合作伙伴',
icon: 'handshake',
desc: 'Logo 滚动展示(合作伙伴/品牌墙)',
defaultConfig: {
title: '合作伙伴',
logos: [],
speed: 30,
pause_on_hover: true,
columns: 5,
},
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'speed', type: 'number', label: '滚动速度', min: 10, max: 100 },
{ key: 'pause_on_hover', type: 'switch', label: '鼠标悬停暂停' },
{ key: 'columns', type: 'number', label: '可见列数', min: 3, max: 8 },
],
},
{
type: 'product_showcase',
label: '产品展示',
icon: 'shopping-bag',
desc: '产品/商品卡片网格(为商城 Phase 预留)',
defaultConfig: {
title: '产品展示',
columns: 3,
count: 6,
source: 'manual',
show_price: true,
show_button: true,
},
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'columns', type: 'number', label: '列数', min: 2, max: 4 },
{ key: 'count', type: 'number', label: '数量', min: 1, max: 12 },
{ key: 'source', type: 'select', label: '数据来源', options: [
{ label: '手动配置', value: 'manual' },
{ label: '最新商品', value: 'latest' },
{ label: '热门商品', value: 'popular' },
]},
{ key: 'show_price', type: 'switch', label: '显示价格' },
{ key: 'show_button', type: 'switch', label: '显示购买按钮' },
],
},
{
type: 'user',
label: '用户卡片',
icon: 'user',
desc: '推荐用户卡片(头像、简介、关注按钮)',
defaultConfig: {
title: '推荐用户',
user_ids: [],
count: 4,
columns: 4,
order: 'recent',
show_stats: true,
},
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'count', type: 'number', label: '用户数量', min: 1, max: 12 },
{ key: 'columns', type: 'number', label: '列数', min: 2, max: 6 },
{ key: 'order', type: 'select', label: '排序', options: [
{ label: '最近注册', value: 'recent' },
{ label: '文章最多', value: 'posts' },
{ label: '手动指定', value: 'manual' },
]},
{ key: 'show_stats', type: 'switch', label: '显示统计数据' },
],
},
{
type: 'more',
label: '发现更多',
icon: 'th',
desc: '分类卡片网格(封面、文章数、最近文章)',
defaultConfig: {
title: '发现更多',
cat_ids: [],
count: 6,
columns: 3,
show_post_count: true,
show_latest_posts: true,
latest_count: 5,
},
fields: [
{ key: 'title', type: 'text', label: '标题' },
{ key: 'count', type: 'number', label: '分类数量', min: 1, max: 12 },
{ key: 'columns', type: 'number', label: '列数', min: 2, max: 4 },
{ key: 'show_post_count', type: 'switch', label: '显示文章数' },
{ key: 'show_latest_posts', type: 'switch', label: '显示最近文章' },
{ key: 'latest_count', type: 'number', label: '最近文章数', min: 1, max: 10 },
],
},
]
export function getModuleMeta(type) {
return MODULE_LIBRARY.find(m => m.type === type) || null
}
export function makeDefaultModule(type) {
const meta = getModuleMeta(type)
if (!meta) return null
return {
type,
config: JSON.parse(JSON.stringify(meta.defaultConfig || {})),
}
}
export const PRESET_TEMPLATES = [
{
key: 'resource_standard',
label: '资源站标准',
desc: 'Tabs + 最新文章 + 下载排行;头部幻灯由导航系统独立渲染',
modules: [
{ type: 'tabs', config: { count: 8, layout: 'card' } },
{ type: 'recent', config: { count: 8, layout: 'card' } },
{ type: 'ranking', config: { metric: 'downloads', count: 6 } },
],
},
{
key: 'news',
label: '资讯站',
desc: 'Banner(固定骨架)+ 多个分类块 + 标签云',
modules: [
{ type: 'banner', config: {} },
{ type: 'category', config: { count: 8, columns: 4, layout: 'card' } },
{ type: 'category', config: { count: 8, columns: 4, layout: 'card' } },
{ type: 'tag_cloud', config: { limit: 30 } },
],
},
{
key: 'mixed',
label: '混合站',
desc: '最新文章 + 多个分类块;头部幻灯由导航系统独立渲染',
modules: [
{ type: 'recent', config: { count: 6, layout: 'card' } },
{ type: 'category', config: { count: 6, columns: 3, layout: 'card' } },
{ type: 'category', config: { count: 6, columns: 3, layout: 'card' } },
],
},
{
key: 'landing',
label: '落地页',
desc: '标题 + 高亮卡片 + FAQ + 按钮组',
modules: [
{ type: 'text_title', config: { title: '为什么选择我们', align: 'center', style: 'default' } },
{ type: 'highlight_cards', config: { columns: 3 } },
{ type: 'spacing', config: { height: 40 } },
{ type: 'faq', config: { title: '常见问题' } },
{ type: 'button_group', config: { columns: 3, style: 'card' } },
],
},
{
key: 'corporate',
label: '企业站',
desc: '数据网格 + 产品展示 + 合作伙伴',
modules: [
{ type: 'data_grid', config: { title: '核心数据', columns: 4, style: 'number' } },
{ type: 'product_showcase', config: { title: '产品展示', columns: 3 } },
{ type: 'partner_marquee', config: { title: '合作伙伴', speed: 30 } },
],
},
]
@@ -11,14 +11,6 @@
}
}
/* CMS freeform: do NOT pull the whole container — nav pull is only on .slide-header
* (zibll body.nav-fixed .slide-header). Whole-container pull made homepage cards
* differ from other page types that share the same component. */
.home-modules > .header-components-container:first-child,
.home-modules--fullbleed > .header-components-container:first-child {
margin-top: 0;
}
/* Fixed homepage hero under-nav is owned by Zibll main.css. */
/* ============================================================
-16
View File
@@ -1,16 +0,0 @@
@import url("home-modules/base.css");
@import url("home-modules/tabs.css");
@import url("home-modules/tag-cloud.css");
@import url("home-modules/ranking.css");
@import url("home-modules/friend-links.css");
@import url("home-modules/html-module.css");
@import url("home-modules/responsive.css");
@import url("home-modules/text-title.css");
@import url("home-modules/button-group.css");
@import url("home-modules/spacing.css");
@import url("home-modules/faq.css");
@import url("home-modules/data-grid.css");
@import url("home-modules/highlight-cards.css");
@import url("home-modules/partner-marquee.css");
@import url("home-modules/product-showcase.css");
@import url("home-modules/user-cards.css");
-89
View File
@@ -1,89 +0,0 @@
/**
* Home Modules · CMS 形态首页模块系统样式
*
* 仅在 site_profile=cms 时(首页)加载。
*
* 宽度模型:
* - .home-modules > .home-module 锁定到 --container(默认 1280)
* - .home-modules > .hero 走 full-bleed(不在此处约束)
* - banner 模块的精细微调见 hero.css `.banner-slider.home-modules__banner`
*/
.home-modules {
display: flex;
flex-direction: column;
gap: var(--space-8, 40px);
padding-bottom: var(--space-8, 40px);
}
.home-modules--fullbleed {
width: 100%;
}
.home-modules--fullbleed > .home-module {
max-width: 100%;
padding-left: 0;
padding-right: 0;
}
/* 容器宽度统一:所有非 hero 的 home-module 都对齐到 --container */
.home-modules > .home-module {
width: 100%;
max-width: var(--container);
margin-left: auto;
margin-right: auto;
padding-left: 16px;
padding-right: 16px;
box-sizing: border-box;
}
@media (min-width: 768px) {
.home-modules > .home-module {
padding-left: 24px;
padding-right: 24px;
}
}
/* ============ 公共 header ============ */
.home-module__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3, 12px);
margin-bottom: var(--space-5, 20px);
padding-bottom: var(--space-3, 12px);
border-bottom: 1px solid var(--color-line-soft);
}
.home-module__title {
display: flex;
align-items: center;
gap: 8px;
font: 700 22px/1.2 var(--font-display);
margin: 0;
color: var(--color-ink);
}
.home-module__title i {
color: var(--color-primary);
}
.home-module__more {
display: inline-flex;
align-items: center;
gap: 4px;
font: 500 13px/1 var(--font-ui);
color: var(--color-primary);
text-decoration: none;
}
.home-module__more:hover {
color: var(--color-primary-active);
}
.home-module__admin-tools {
display: flex;
justify-content: flex-end;
margin-top: 14px;
}
@@ -1,47 +0,0 @@
/* Button Group */
.home-module--button-group {
padding: 32px 0;
}
.home-module__buttons {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
margin-top: 16px;
}
.aksrt-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 10px 24px;
border-radius: var(--radius-btn, 4px);
font-size: 14px;
font-weight: 500;
text-decoration: none;
transition: opacity 0.2s, transform 0.2s, box-shadow 0.2s, background 0.2s;
}
.aksrt-btn:hover {
opacity: 1;
transform: translateY(-1px);
}
.aksrt-btn--primary {
background: var(--color-primary);
color: #fff;
box-shadow: 0 8px 18px rgba(240, 68, 148, 0.22);
}
.aksrt-btn--outline {
background: var(--surface-1);
color: var(--color-primary);
border: 1px solid var(--color-primary);
}
.aksrt-btn--ghost {
background: var(--surface-4);
color: var(--color-ink);
}
@@ -1,45 +0,0 @@
/* Data Grid */
.home-module--data-grid {
padding: 32px 0;
}
.home-module__data-grid {
display: grid;
gap: 16px;
text-align: center;
}
.aksrt-data-cell {
padding: 24px 16px;
background: var(--surface-1, #fff);
border-radius: var(--main-radius, var(--radius, 8px));
border: none;
box-shadow: var(--shadow-sm);
transition: box-shadow 0.2s ease;
}
.aksrt-data-cell:hover {
box-shadow: var(--shadow);
transform: none;
}
.aksrt-data-cell i {
font-size: 24px;
color: var(--color-primary);
margin-bottom: 8px;
display: block;
}
.aksrt-data-cell__value {
font-size: 28px;
font-weight: 700;
color: var(--color-ink);
display: block;
margin-bottom: 4px;
}
.aksrt-data-cell__label {
font-size: 13px;
color: var(--color-ink-3);
}
-56
View File
@@ -1,56 +0,0 @@
/* FAQ */
.home-module--faq {
padding: 32px 0;
}
.home-module__faq-grid {
column-gap: 24px;
}
.aksrt-faq-item {
break-inside: avoid;
margin-bottom: 12px;
border: none;
border-radius: var(--main-radius, var(--radius, 8px));
box-shadow: var(--shadow-sm);
overflow: hidden;
background: var(--surface-1, #fff);
}
.aksrt-faq-item__question {
padding: 14px 16px;
font-size: 15px;
font-weight: 600;
cursor: pointer;
background: var(--surface-1, #fff);
list-style: none;
transition: color .18s ease, background .18s ease;
}
.aksrt-faq-item__question:hover {
color: var(--color-primary);
background: var(--surface-4);
}
.aksrt-faq-item__question::-webkit-details-marker {
display: none;
}
.aksrt-faq-item__question::after {
content: '+';
float: right;
font-weight: 400;
transition: transform 0.2s;
}
.aksrt-faq-item[open] .aksrt-faq-item__question::after {
content: '−';
}
.aksrt-faq-item__answer {
padding: 0 16px 14px;
font-size: 14px;
color: var(--color-ink-3);
line-height: 1.7;
}
@@ -1,56 +0,0 @@
/* ============ Friend Links ============ */
.home-module__friends-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: var(--space-3, 12px);
}
.home-module__friend-card {
display: flex;
align-items: center;
gap: var(--space-3, 12px);
padding: 12px;
border-radius: var(--main-radius, var(--radius, 8px));
border: none;
box-shadow: var(--shadow-sm);
background: var(--surface-1);
color: var(--color-ink);
text-decoration: none;
transition: border-color .18s ease, box-shadow .18s ease, transform .18s ease;
}
.home-module__friend-card:hover {
box-shadow: var(--shadow);
transform: translateY(-1px);
}
.home-module__friend-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
flex: 0 0 40px;
}
.home-module__friend-meta {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.home-module__friend-meta strong {
font: 600 14px/1.2 var(--font-ui);
}
.home-module__friend-meta span {
font-size: 12px;
color: var(--color-ink-3);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -1,58 +0,0 @@
/* Highlight Cards */
.home-module--highlight-cards {
padding: 32px 0;
}
.home-module__highlight-grid {
display: grid;
gap: 16px;
}
.aksrt-highlight-card {
padding: 24px;
background: var(--surface-1, #fff);
border-radius: var(--main-radius, var(--radius, 8px));
border: none;
box-shadow: var(--shadow-sm);
text-decoration: none;
color: inherit;
transition: box-shadow 0.2s ease;
}
.aksrt-highlight-card:hover {
box-shadow: var(--shadow);
transform: none;
}
.aksrt-highlight-card__image {
width: 100%;
border-radius: var(--radius-sm, 4px);
margin-bottom: 12px;
}
.aksrt-highlight-card__icon {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius-md, 8px);
background: var(--color-primary-softer);
color: var(--color-primary);
font-size: 20px;
margin-bottom: 12px;
}
.aksrt-highlight-card__title {
font-size: 16px;
font-weight: 600;
margin: 0 0 8px;
}
.aksrt-highlight-card__desc {
font-size: 13px;
color: var(--color-ink-3);
margin: 0;
line-height: 1.6;
}
@@ -1,12 +0,0 @@
/* ============ HTML Module ============ */
.home-module--html .home-module__html-body {
color: var(--color-ink);
font-size: 15px;
line-height: 1.75;
padding: 20px 22px;
background: var(--surface-1);
border: none;
border-radius: var(--radius-2xl, 16px);
box-shadow: var(--shadow-sm);
}
@@ -1,46 +0,0 @@
/* Partner Marquee */
.home-module--partner-marquee {
padding: 32px 0;
overflow: hidden;
}
.aksrt-marquee {
overflow: hidden;
width: 100%;
}
.aksrt-marquee__track {
display: flex;
gap: 32px;
width: max-content;
animation: aksrt-marquee-scroll var(--marquee-duration, 30s) linear infinite;
}
.aksrt-marquee__item {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 10px 18px;
border-radius: var(--main-radius, var(--radius, 8px));
background: var(--surface-1);
border: none;
box-shadow: var(--shadow-sm);
}
.aksrt-marquee__item img {
height: 40px;
width: auto;
opacity: 0.7;
transition: opacity 0.2s;
}
.aksrt-marquee__item:hover img {
opacity: 1;
}
@keyframes aksrt-marquee-scroll {
0% { transform: translateX(0); }
100% { transform: translateX(-50%); }
}
@@ -1,60 +0,0 @@
/* Product Showcase */
.home-module--product-showcase {
padding: 32px 0;
}
.home-module__product-grid {
display: grid;
gap: 16px;
}
.aksrt-product-card {
background: var(--surface-1, #fff);
border-radius: var(--radius, 8px);
border: none;
box-shadow: var(--shadow-sm);
overflow: hidden;
text-decoration: none;
color: inherit;
transition: box-shadow 0.2s ease;
}
.aksrt-product-card:hover {
box-shadow: var(--shadow);
}
.aksrt-product-card__image img {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: cover;
border-radius: var(--radius, 8px) var(--radius, 8px) 0 0;
display: block;
}
.aksrt-product-card__body {
padding: 15px;
}
.aksrt-product-card__title {
font-size: 16px;
font-weight: 600;
margin: 0 0 6px;
}
.aksrt-product-card__desc {
font-size: 13px;
color: var(--color-ink-3);
margin: 0 0 8px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.aksrt-product-card__price {
font-size: 18px;
font-weight: 700;
color: var(--color-error);
}
@@ -1,67 +0,0 @@
/* ============ Ranking ============ */
.home-module__ranking-list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: var(--space-3, 12px);
}
.home-module__ranking-item {
display: flex;
align-items: center;
gap: var(--space-3, 12px);
padding: 12px 14px;
background: var(--surface-1);
border: none;
border-radius: var(--main-radius, var(--radius, 8px));
box-shadow: var(--shadow-sm);
transition: box-shadow .18s ease, border-color .18s ease, transform .18s ease;
}
.home-module__ranking-item:hover {
box-shadow: var(--shadow);
transform: none;
}
.home-module__ranking-num {
flex: 0 0 32px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
background: var(--surface-2);
color: var(--color-ink-3);
font: 700 13px/1 var(--font-ui);
}
.home-module__ranking-item--top .home-module__ranking-num {
background: linear-gradient(135deg, #f59e0b 0%, #f97316 100%);
color: #fff;
box-shadow: 0 4px 8px rgba(245, 158, 11, .25);
}
.home-module__ranking-link {
flex: 1 1 auto;
color: var(--color-ink);
text-decoration: none;
font: 500 14px/1.4 var(--font-ui);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.home-module__ranking-link:hover {
color: var(--color-primary);
}
.home-module__ranking-value {
flex: 0 0 auto;
color: var(--color-ink-3);
font-variant-numeric: tabular-nums;
font: 500 12px/1 var(--font-ui);
}
@@ -1,278 +0,0 @@
/* ============ Responsive ============ */
@media (max-width: 640px) {
.home-modules {
gap: var(--space-7, 32px);
}
.home-module__header {
flex-wrap: wrap;
}
.home-module__title {
font-size: 18px;
}
}
/* ============ Hotspot 热点推荐 ============ */
/* 手机端隐藏 */
.hotspot--desktop-only {
display: none;
}
@media (min-width: 768px) {
.hotspot--desktop-only {
display: block;
}
}
.hotspot__list {
list-style: none;
margin: 0;
padding: 0;
}
.hotspot__item {
display: flex;
align-items: flex-start;
gap: var(--space-4, 16px);
padding: var(--space-4, 16px) 0;
border-bottom: 1px solid var(--color-line);
}
.hotspot__item:last-child {
border-bottom: none;
}
.hotspot__num {
flex: 0 0 32px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-avatar, 50%);
font: 700 13px/1 var(--font-ui);
background: var(--surface-2);
color: var(--color-ink-3);
}
.hotspot__num--top {
background: var(--color-primary);
color: var(--color-on-primary, #fff);
}
.hotspot__body {
flex: 1;
min-width: 0;
}
.hotspot__title {
display: block;
font: 600 15px/1.4 var(--font-ui);
color: var(--color-ink);
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color 0.18s ease;
}
.hotspot__title:hover {
color: var(--color-primary);
}
.hotspot__meta {
display: flex;
align-items: center;
gap: var(--space-3, 12px);
margin-top: var(--space-2, 8px);
font: 400 12px/1 var(--font-ui);
color: var(--color-ink-3);
}
.hotspot__cat {
display: inline-block;
padding: 2px 8px;
border-radius: var(--radius-pill, 999px);
background: var(--surface-2);
font-size: 11px;
font-weight: 500;
}
/* ============ 分类推荐卡片 ============ */
.cat-cards-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.cat-card {
background: var(--surface-2, #fff);
border-radius: 10px;
padding: 22px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.2s ease, border-color 0.2s ease;
}
.cat-card:hover {
box-shadow: var(--shadow-md);
}
/* 上部:封面 + 信息 */
.cat-card__top {
display: flex;
align-items: flex-start;
gap: 18px;
margin-bottom: 18px;
}
.cat-card__banner {
width: 145px;
height: 95px;
border-radius: 8px;
overflow: hidden;
position: relative;
flex-shrink: 0;
background: var(--surface-3, #f5f5f5);
}
.cat-card__cover {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.3s ease;
}
.cat-card:hover .cat-card__cover {
transform: scale(1.02);
}
/* 浏览量胶囊:红底白字 */
.cat-card__views {
position: absolute;
top: 8px;
right: 8px;
background: #f54343;
color: #fff;
font-size: 12px;
font-weight: 600;
padding: 2px 7px;
border-radius: 15px;
pointer-events: none;
z-index: 1;
}
.cat-card__info {
flex: 1;
height: 95px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.cat-card__name {
font-size: 19px;
font-weight: 600;
color: var(--color-ink);
line-height: 1.3;
}
.cat-card__desc {
font-size: 13px;
color: var(--color-ink-3);
margin-top: 4px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.cat-card__foot {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
color: var(--color-ink-4);
}
.cat-card__more {
color: var(--color-primary);
text-decoration: none;
font-weight: 500;
}
.cat-card__more:hover {
text-decoration: underline;
}
/* 下部:文章列表 */
.cat-card__list {
list-style: none;
margin: 0;
padding: 0;
}
.cat-card__list li {
display: flex;
align-items: flex-start;
font-size: 13.5px;
color: var(--color-ink);
line-height: 1.6;
padding: 5px 0;
}
.cat-card__list li::before {
content: "\25CF";
color: var(--color-primary);
font-size: 10px;
margin-right: 8px;
margin-top: 3px;
flex-shrink: 0;
}
.cat-card__list li a {
color: inherit;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
min-width: 0;
transition: color 0.15s ease;
}
.cat-card__list li a:hover {
color: var(--color-primary);
}
.cat-card__time {
color: var(--color-ink-4);
font-size: 12px;
margin-left: auto;
padding-left: 12px;
white-space: nowrap;
flex-shrink: 0;
}
/* 响应式 */
@media (max-width: 767px) {
.cat-cards-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.cat-card__name {
font-size: 16px;
}
.cat-card__info {
height: auto;
min-height: 95px;
}
}
/* ============================================================
* Phase 5: 运营型模块样式
* ============================================================ */
@@ -1,5 +0,0 @@
/* Spacing */
.home-module--spacing {
pointer-events: none;
}
-59
View File
@@ -1,59 +0,0 @@
/* ============ Tabs ============ */
.home-module__header--tabs {
flex-wrap: wrap;
border-bottom: 0;
padding-bottom: 0;
}
.home-module__tabs {
display: flex;
flex-wrap: wrap;
gap: var(--space-2, 8px);
border-bottom: 1px solid var(--color-line-soft);
margin-top: var(--space-3, 12px);
padding-bottom: 12px;
width: 100%;
}
.home-module__tab {
background: transparent;
border: 0;
padding: 9px 18px;
font: 500 14px/1 var(--font-ui);
color: var(--color-ink-3);
cursor: pointer;
border-radius: 999px;
border-bottom: 0;
transition: color .18s ease, border-color .18s ease, background .18s ease;
}
.home-module__tab:hover {
color: var(--color-ink);
background: var(--surface-4);
}
.home-module__tab.is-active {
color: var(--color-primary);
background: var(--color-primary-soft);
box-shadow: inset 0 0 0 1px rgba(240, 68, 148, 0.08);
}
.home-module__tab-panel {
margin-top: var(--space-5, 20px);
}
.index-tab-main .home-module__tab-panel.tab-pane[hidden],
.home-tab-content .home-module__tab-panel.tab-pane[hidden] {
display: none;
}
.home-module__tab-panel[hidden] {
display: none;
}
.home-module__empty {
color: var(--color-ink-3);
text-align: center;
padding: var(--space-7, 32px) 0;
}
@@ -1,34 +0,0 @@
/* ============ Tag Cloud ============ */
.home-module__tag-cloud {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: var(--space-2, 8px);
}
.home-module__tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: var(--radius-pill);
background: var(--surface-4);
color: var(--color-ink-2);
font: 500 13px/1 var(--font-ui);
text-decoration: none;
transition: background .18s ease, color .18s ease, box-shadow .18s ease;
}
.home-module__tag:hover {
background: var(--color-primary-soft);
color: var(--color-primary);
box-shadow: inset 0 0 0 1px rgba(240, 68, 148, 0.08);
}
.home-module__tag-count {
font-variant-numeric: tabular-nums;
opacity: .7;
}
@@ -1,17 +0,0 @@
/* Text Title */
.home-module--text-title {
padding: 24px 0;
}
.home-module--text-title .home-module__title {
font-size: 28px;
font-weight: 700;
margin: 0 0 8px;
}
.home-module--text-title .home-module__subtitle {
font-size: 15px;
color: var(--color-ink-3);
margin: 0;
}
@@ -1,180 +0,0 @@
/* ============ User Cards ============ */
.home-module__users-grid {
display: grid;
grid-template-columns: repeat(var(--cols, 4), 1fr);
gap: var(--space-4, 16px);
}
.home-module__user-card {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 20px 15px;
border-radius: var(--radius, 8px);
border: none;
background: var(--surface-1);
box-shadow: var(--shadow-sm);
transition: box-shadow 0.2s ease;
}
.home-module__user-card:hover {
box-shadow: var(--shadow);
}
.home-module__user-link {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
text-decoration: none;
color: var(--color-ink);
}
.home-module__user-avatar {
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
}
.home-module__user-name {
font: 600 14px/1.2 var(--font-ui);
}
.home-module__user-bio {
font-size: 12px;
color: var(--color-ink-3);
margin: 4px 0 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.home-module__user-stats {
display: flex;
gap: 12px;
margin-top: 8px;
font-size: 12px;
color: var(--color-ink-2);
}
.home-module__user-stat {
display: inline-flex;
align-items: center;
gap: 4px;
}
.home-module__user-stat i {
font-size: 11px;
color: var(--color-ink-3);
}
/* ============ More (Discover Categories) ============ */
.home-module__more-grid {
display: grid;
grid-template-columns: repeat(var(--cols, 3), 1fr);
gap: var(--space-4, 16px);
}
.home-module__more-card {
border-radius: var(--radius);
border: 1px solid var(--color-line);
background: var(--surface-1);
overflow: hidden;
transition: border-color .18s ease, transform .18s ease;
}
.home-module__more-card:hover {
border-color: var(--color-line);
transform: none;
}
.home-module__more-link {
display: block;
text-decoration: none;
color: inherit;
}
.home-module__more-cover {
aspect-ratio: 16 / 9;
overflow: hidden;
background: var(--color-fill-2);
}
.home-module__more-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.home-module__more-cover--placeholder {
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: var(--color-ink-3);
}
.home-module__more-body {
padding: 12px;
display: flex;
flex-direction: column;
gap: 2px;
}
.home-module__more-name {
font: 600 14px/1.3 var(--font-ui);
}
.home-module__more-count {
font-size: 12px;
color: var(--color-ink-3);
}
.home-module__more-posts {
list-style: none;
margin: 0;
padding: 0 12px 12px;
}
.home-module__more-posts li {
border-top: 1px solid var(--color-line-light, rgba(0,0,0,.06));
}
.home-module__more-posts a {
display: block;
padding: 6px 0;
font-size: 13px;
color: var(--color-ink-2);
text-decoration: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: color .15s ease;
}
.home-module__more-posts a:hover {
color: var(--color-primary);
}
/* 响应式 */
@media (max-width: 767px) {
.home-module__data-grid,
.home-module__highlight-grid,
.home-module__product-grid {
grid-template-columns: 1fr !important;
}
.home-module__faq-grid {
column-count: 1 !important;
}
.home-module__users-grid,
.home-module__more-grid {
grid-template-columns: 1fr 1fr !important;
}
}
@@ -127,22 +127,10 @@
margin-bottom: 20px;
}
.aksrt-homepage__content-zone--operated.home-modules,
.aksrt-homepage__content-zone--mid.home-modules,
.aksrt-homepage__content-zone--supplement.home-modules {
gap: 24px;
padding-bottom: 0;
}
.aksrt-homepage__content-zone--supplement {
padding-top: 4px;
}
.aksrt-homepage__content-zone--featured.home-modules {
gap: 24px;
padding-bottom: 0;
}
.aksrt-homepage__content-zone .widget-card:last-child,
.aksrt-homepage__aside .widget:last-child,
.aksrt-homepage__fluid-zone .widget-fluid:last-child {
-28
View File
@@ -593,38 +593,10 @@ body.site-layout-3 .sidebar,
}
}
/* CMS 模块在侧栏布局中的适配 */
.content-layout__primary--cms {
min-width: 0;
}
.content-layout__primary--cms .home-modules {
gap: var(--space-7, 32px);
}
.content-layout__primary--cms .home-modules > .home-module {
max-width: 100%;
padding-left: 0;
padding-right: 0;
}
/* ============================================================
* Phase 2 — 背景遮罩层级修正
* body.aksrt-bg-overlay 的 ::before 需要 z-index: -1 让内容在其上方
* ============================================================ */
body.aksrt-bg-overlay {
position: relative;
}
body.aksrt-bg-overlay::before {
z-index: -1;
}
body.aksrt-bg-overlay .site-wrapper {
position: relative;
z-index: 0;
}
/* ============================================================
* Phase 2 — 侧栏 widget 内链接增强
* ============================================================ */
-72
View File
@@ -1,72 +0,0 @@
/**
* AKSRT Theme Background System
*
* 全局背景图、夜间背景图、页面背景色/背景图。
* 所有选择器均带 aksrt- 命名空间。
*/
/* ========== 全局背景 ========== */
.aksrt-bg-body {
background-color: var(--aksrt-bg-color, transparent);
background-image: var(--aksrt-bg-image, none);
background-size: var(--aksrt-bg-size, cover);
background-position: var(--aksrt-bg-position, center center);
background-repeat: var(--aksrt-bg-repeat, no-repeat);
background-attachment: var(--aksrt-bg-attachment, fixed);
}
/* ========== 夜间模式背景 ========== */
.aksrt-bg-dark {
background-color: var(--aksrt-bg-dark-color, transparent);
background-image: var(--aksrt-bg-dark-image, none);
}
/* ========== 页面级背景 ========== */
.aksrt-bg-page {
background-color: var(--aksrt-page-bg-color, transparent);
background-image: var(--aksrt-page-bg-image, none);
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
}
/* ========== 模块级背景 ========== */
.aksrt-bg-module {
background-color: var(--aksrt-module-bg-color, transparent);
background-image: var(--aksrt-module-bg-image, none);
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
}
/* ========== 背景遮罩层(提升文字可读性)========== */
.aksrt-bg-overlay {
position: relative;
}
.aksrt-bg-overlay::before {
content: '';
position: absolute;
inset: 0;
background: var(--aksrt-bg-overlay-color, rgba(0, 0, 0, 0.3));
pointer-events: none;
z-index: 0;
}
.aksrt-bg-overlay > * {
position: relative;
z-index: 1;
}
/* ========== 暗色模式自动切换 ========== */
@media (prefers-color-scheme: dark) {
:root:not(.aksrt-light-mode) .aksrt-bg-body {
background-color: var(--aksrt-bg-dark-color, var(--aksrt-bg-color, transparent));
background-image: var(--aksrt-bg-dark-image, var(--aksrt-bg-image, none));
}
}
html.aksrt-dark-mode .aksrt-bg-body {
background-color: var(--aksrt-bg-dark-color, var(--aksrt-bg-color, transparent));
background-image: var(--aksrt-bg-dark-image, var(--aksrt-bg-image, none));
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

-52
View File
@@ -1,52 +0,0 @@
/**
* AKSRT CMS Home Modules
*
* - Tab 切换:[data-aksrt-tabs] / [data-tab-target] / [data-tab-id]
*/
(function () {
'use strict';
function initTabs(scope) {
const tablists = (scope || document).querySelectorAll('[data-aksrt-tabs]');
tablists.forEach(function (list) {
const buttons = list.querySelectorAll('.home-module__tab');
const section = list.closest('.home-module--tabs');
if (!section) return;
const panels = section.querySelectorAll('.home-module__tab-panel');
buttons.forEach(function (btn) {
btn.addEventListener('click', function () {
const target = btn.getAttribute('data-tab-target');
if (!target) return;
buttons.forEach(function (b) {
b.classList.toggle('is-active', b === btn);
b.setAttribute('aria-selected', b === btn ? 'true' : 'false');
});
panels.forEach(function (p) {
const isMatch = p.getAttribute('data-tab-id') === target;
p.classList.toggle('is-active', isMatch);
if (isMatch) {
p.removeAttribute('hidden');
} else {
p.setAttribute('hidden', '');
}
});
});
});
});
}
// 监听 postMessage:iframe 预览模式下接收 reload 请求
window.addEventListener('message', function (e) {
if (!e.data || e.data.type !== 'aksrt-preview-reload') return;
// 简单做:直接 reload
window.location.reload();
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { initTabs(); });
} else {
initTabs();
}
})();
+8 -52
View File
@@ -21,9 +21,8 @@
var state = {
filter_cat: "",
filter_tag: "",
sort: "latest",
page: 1,
cf: {}
sort: "date",
page: 1
};
var activeCat = bar.querySelector("[data-filter-cat].is-active");
@@ -31,12 +30,7 @@
var activeTag = bar.querySelector("[data-filter-tag].is-active");
if (activeTag) state.filter_tag = activeTag.dataset.filterTag || "";
var activeSort = bar.querySelector("[data-filter-sort].is-active");
if (activeSort) state.sort = activeSort.dataset.filterSort || "latest";
bar.querySelectorAll("[data-filter-cf].is-active").forEach(function (el) {
var dimKey = el.dataset.filterCf || "";
var dimVal = el.dataset.filterCfVal || "";
if (dimKey) state.cf[dimKey] = dimVal;
});
if (activeSort) state.sort = activeSort.dataset.filterSort || "date";
function buildUrl() {
var params = new URLSearchParams();
@@ -45,20 +39,8 @@
if (state.filter_cat) params.set("filter_cat", state.filter_cat);
if (state.filter_tag) params.set("filter_tag", state.filter_tag);
var sortMap = { oldest: { orderby: "date", order: "ASC" }, popular: { orderby: "comment_count", order: "DESC" } };
var s = sortMap[state.sort];
params.set("sort", s ? s.orderby : "date");
params.set("order", s ? s.order : "DESC");
params.set("sort", state.sort || "date");
params.set("page", String(state.page));
var cfActive = {};
var hasCf = false;
for (var cfK in state.cf) {
if (state.cf[cfK]) {
cfActive[cfK] = state.cf[cfK];
hasCf = true;
}
}
if (hasCf) params.set("custom_filters", JSON.stringify(cfActive));
return REST_BASE + "?" + params.toString();
}
@@ -70,13 +52,7 @@
el.classList.toggle("is-active", (el.dataset.filterTag || "") === state.filter_tag);
});
bar.querySelectorAll("[data-filter-sort]").forEach(function (el) {
el.classList.toggle("is-active", (el.dataset.filterSort || "latest") === state.sort);
});
bar.querySelectorAll("[data-filter-cf]").forEach(function (el) {
var dimKey = el.dataset.filterCf || "";
var dimVal = el.dataset.filterCfVal || "";
var isActive = dimKey && (state.cf[dimKey] || "") === dimVal;
el.classList.toggle("is-active", isActive);
el.classList.toggle("is-active", (el.dataset.filterSort || "date") === state.sort);
});
}
@@ -131,21 +107,9 @@
url.searchParams.delete("paged");
url.searchParams.delete("aksrt_filter_cat");
url.searchParams.delete("aksrt_filter_tag");
url.searchParams.forEach(function (v, k) {
if (k.indexOf("aksrt_cf_") === 0) url.searchParams.delete(k);
});
if (state.filter_cat) url.searchParams.set("aksrt_filter_cat", state.filter_cat);
if (state.filter_tag) url.searchParams.set("aksrt_filter_tag", state.filter_tag);
for (var cfK2 in state.cf) {
if (state.cf[cfK2]) url.searchParams.set("aksrt_cf_" + cfK2, state.cf[cfK2]);
}
if (state.sort === "oldest") {
url.searchParams.set("orderby", "date");
url.searchParams.set("order", "asc");
} else if (state.sort === "popular") {
url.searchParams.set("orderby", "comment_count");
url.searchParams.set("order", "desc");
}
if (state.sort) url.searchParams.set("orderby", state.sort);
if (state.page > 1) url.searchParams.set("paged", String(state.page));
history.pushState({ aksrtArchive: true, state: Object.assign({}, state) }, "", url.toString());
}
@@ -156,7 +120,7 @@
}
bar.addEventListener("click", function (e) {
var chip = e.target.closest("[data-filter-cat], [data-filter-tag], [data-filter-sort], [data-filter-cf]");
var chip = e.target.closest("[data-filter-cat], [data-filter-tag], [data-filter-sort]");
if (!chip) return;
e.preventDefault();
@@ -176,20 +140,12 @@
}
}
if (chip.dataset.filterSort !== undefined) {
var newSort = chip.dataset.filterSort || "latest";
var newSort = chip.dataset.filterSort || "date";
if (newSort !== state.sort) {
state.sort = newSort;
changed = true;
}
}
if (chip.dataset.filterCf !== undefined) {
var cfKey = chip.dataset.filterCf;
var cfVal = chip.dataset.filterCfVal || "";
if ((state.cf[cfKey] || "") !== cfVal) {
state.cf[cfKey] = cfVal;
changed = true;
}
}
if (changed) {
state.page = 1;