fix(admin): harden hero/banner round-trip and operator cross-links

Align enabled-meta semantics with frontend, allow truly empty titles on
update, use router navigation for manager deep-links, and document fixed
skeleton ownership in module schema copy.
This commit is contained in:
2026-07-13 22:31:12 +08:00
parent dcc82a39e7
commit 24c135aeec
6 changed files with 64 additions and 23 deletions
@@ -2,11 +2,16 @@
<div class="aksrt-page-container content-page">
<PageHeader
title="Banner 管理"
desc="管理首页 Banner 数据,供固定首页和相关展示区域调用;可控制是否显示文字。"
desc="管理首页内容流 Banner 幻灯片(图片、文案、跳转、启用状态)。"
/>
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
<template #header><strong>Banner 显示开关</strong></template>
<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">
@@ -111,13 +116,19 @@
<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)
+13 -2
View File
@@ -1,9 +1,14 @@
<template>
<div class="aksrt-page-container content-page">
<PageHeader title="Hero 轮播管理" desc="管理首页 Hero 轮播数据,供固定首页和相关展示区域调用;标题、副标题可留空。" />
<PageHeader title="Hero 轮播管理" desc="管理首页 Hero 幻灯片内容(图片/视频、文案、图层、跳转)。标题、副标题可留空。" />
<n-card class="aksrt-section-card" style="margin-bottom:16px;">
<template #header><strong>Hero 显示开关</strong></template>
<template #header>
<div class="aksrt-card-header-row">
<strong>Hero 显示开关</strong>
<n-button size="small" secondary @click="goHeaderComponents">顶部多功能组件(搜索/卡片/轮播参数)</n-button>
</div>
</template>
<n-form label-placement="left" label-width="160" class="aksrt-form">
<n-form-item label="显示首页 Hero">
<div class="aksrt-inline-setting">
@@ -196,13 +201,19 @@
<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)
@@ -184,9 +184,9 @@
如需调整首页首屏运营区,请前往对应管理页配置幻灯片内容或顶部组件参数。
</n-alert>
<n-space>
<n-button tag="a" href="#/page-display/hero" secondary type="primary">管理 Hero 轮播</n-button>
<n-button tag="a" href="#/page-display/banner" secondary type="primary">管理 Banner 轮播</n-button>
<n-button tag="a" href="#/page-display/header-components" secondary>
<n-button secondary type="primary" @click="router.push('/page-display/hero')">管理 Hero 轮播</n-button>
<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>
@@ -204,10 +204,12 @@
<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)
+20
View File
@@ -178,6 +178,26 @@
padding: 24px 0;
}
#aksrt-admin-app .aksrt-card-header-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
width: 100%;
flex-wrap: wrap;
}
#aksrt-admin-app .aksrt-card-header-row__left {
display: inline-flex;
align-items: center;
gap: 8px;
min-width: 0;
}
#aksrt-admin-app .aksrt-card-header-row__title {
font-weight: 600;
}
/* ==================== 卡片 header 行(标题 + 按钮) ==================== */
#aksrt-admin-app .aksrt-card-header-row {
display: flex;
+5 -5
View File
@@ -46,7 +46,7 @@ export const MODULE_LIBRARY = [
type: 'hero',
label: 'Hero 首屏',
icon: 'image',
desc: '大图轮播首屏,支持叠加搜索框和图标卡片',
desc: '固定首页骨架:大图轮播首屏。幻灯片内容在「页面&显示 → Hero 轮播」维护;搜索/卡片参数见「顶部多功能组件」。',
defaultConfig: { with_search: true, show_cards: false, card_columns: 4, icon_cards: [] },
fields: [
{ key: 'with_search', type: 'switch', label: '显示搜索覆盖框' },
@@ -58,7 +58,7 @@ export const MODULE_LIBRARY = [
type: 'banner',
label: 'Banner 轮播',
icon: 'rectangle',
desc: '正文区轮播条',
desc: '固定首页骨架:正文区轮播条。幻灯片内容在「页面&显示 → Banner 轮播」维护。',
defaultConfig: {},
fields: [],
},
@@ -478,7 +478,7 @@ export const PRESET_TEMPLATES = [
{
key: 'resource_standard',
label: '资源站标准',
desc: 'Hero + Tabs + 最新资源 + 下载排行',
desc: 'Hero(固定骨架)+ Tabs + 最新资源 + 下载排行',
modules: [
{ type: 'hero', config: {} },
{ type: 'tabs', config: { count: 8, layout: 'grid-cms' } },
@@ -489,7 +489,7 @@ export const PRESET_TEMPLATES = [
{
key: 'news',
label: '资讯站',
desc: 'Banner + 多个分类块 + 标签云',
desc: 'Banner(固定骨架)+ 多个分类块 + 标签云',
modules: [
{ type: 'banner', config: {} },
{ type: 'category', config: { count: 8, columns: 4, layout: 'card' } },
@@ -500,7 +500,7 @@ export const PRESET_TEMPLATES = [
{
key: 'mixed',
label: '混合站',
desc: 'Hero + 最新文章 + 多个分类块',
desc: 'Hero(固定骨架)+ 最新文章 + 多个分类块',
modules: [
{ type: 'hero', config: {} },
{ type: 'recent', config: { post_type: 'post', count: 6, layout: 'card' } },
@@ -94,7 +94,8 @@ class AKSRT_Admin_Page_Display_Controller extends AKSRT_Admin_Controller_Base {
'link_target' => get_post_meta($post->ID, '_link_target', true) ?: '_self',
'text_align' => get_post_meta($post->ID, '_text_align', true) ?: 'left',
'device_visibility' => get_post_meta($post->ID, '_device_visibility', true) ?: 'all',
'enabled' => get_post_meta($post->ID, '_enabled', true) === '1',
// Match frontend aksrt_get_banners(): missing _enabled means enabled.
'enabled' => get_post_meta($post->ID, '_enabled', true) !== '0',
'show_text' => get_post_meta($post->ID, '_show_text', true) !== '0',
'show_meta' => get_post_meta($post->ID, '_show_meta', true) === '1',
'kind' => get_post_meta($post->ID, '_aksrt_kind', true) ?: 'hero',
@@ -102,7 +103,7 @@ class AKSRT_Admin_Page_Display_Controller extends AKSRT_Admin_Controller_Base {
'text_size_pc' => get_post_meta($post->ID, '_text_size_pc', true) ?: '',
'text_size_m' => get_post_meta($post->ID, '_text_size_m', true) ?: '',
'text_parallax' => get_post_meta($post->ID, '_text_parallax', true) === '1',
'layers' => is_string($layers_json) && $layers_json !== '' ? json_decode($layers_json, true) : [],
'layers' => is_string($layers_json) && $layers_json !== '' ? (json_decode($layers_json, true) ?: []) : [],
];
}, $posts);
return rest_ensure_response(['code' => 'success', 'data' => $banners]);
@@ -113,6 +114,7 @@ class AKSRT_Admin_Page_Display_Controller extends AKSRT_Admin_Controller_Base {
$kind = $this->normalize_banner_kind($params['kind'] ?? 'hero');
$title = sanitize_text_field($params['title'] ?? '');
$post_id = wp_insert_post([
// Title is optional in admin UI; keep a non-empty WP title for list readability.
'post_title' => '' !== $title ? $title : ('banner' === $kind ? 'Banner Slide' : 'Hero Slide'),
'post_type' => 'aksrt_banner',
'post_status' => 'publish',
@@ -139,7 +141,7 @@ class AKSRT_Admin_Page_Display_Controller extends AKSRT_Admin_Controller_Base {
update_post_meta($post_id, '_text_size_m', sanitize_text_field($params['text_size_m'] ?? ''));
update_post_meta($post_id, '_text_parallax', !empty($params['text_parallax']) ? '1' : '0');
if (isset($params['layers']) && is_array($params['layers'])) {
update_post_meta($post_id, '_layers', wp_json_encode($params['layers']));
update_post_meta($post_id, '_layers', wp_json_encode(array_values($params['layers'])));
}
return rest_ensure_response(['code' => 'success', 'data' => ['id' => $post_id], 'message' => __('幻灯片已创建', 'aksrt')]);
@@ -158,13 +160,8 @@ class AKSRT_Admin_Page_Display_Controller extends AKSRT_Admin_Controller_Base {
$params = $this->get_json_params($request);
$kind = $this->normalize_banner_kind($params['kind'] ?? get_post_meta($id, '_aksrt_kind', true) ?: 'hero');
// Optional title: empty is allowed (UI shows “未命名”); do not 422.
$title = sanitize_text_field($params['title'] ?? '');
if ($title === '') {
$existing = get_the_title($id);
$title = is_string($existing) && $existing !== ''
? $existing
: ('banner' === $kind ? 'Banner Slide' : 'Hero Slide');
}
wp_update_post([
'ID' => $id,
@@ -180,8 +177,8 @@ class AKSRT_Admin_Page_Display_Controller extends AKSRT_Admin_Controller_Base {
update_post_meta($id, '_link_target', in_array($params['link_target'] ?? '', ['_blank', '_self'], true) ? $params['link_target'] : '_self');
update_post_meta($id, '_text_align', in_array($params['text_align'] ?? '', ['left', 'center', 'right'], true) ? $params['text_align'] : 'left');
update_post_meta($id, '_device_visibility', in_array($params['device_visibility'] ?? '', ['all', 'pc', 'mobile'], true) ? $params['device_visibility'] : 'all');
update_post_meta($id, '_enabled', !empty($params['enabled']) ? '1' : '0');
update_post_meta($id, '_show_text', !isset($params['show_text']) || !empty($params['show_text']) ? '1' : '0');
update_post_meta($id, '_enabled', !array_key_exists('enabled', $params) || !empty($params['enabled']) ? '1' : '0');
update_post_meta($id, '_show_text', !array_key_exists('show_text', $params) || !empty($params['show_text']) ? '1' : '0');
update_post_meta($id, '_show_meta', !empty($params['show_meta']) ? '1' : '0');
update_post_meta($id, '_text_size_pc', sanitize_text_field($params['text_size_pc'] ?? ''));
update_post_meta($id, '_text_size_m', sanitize_text_field($params['text_size_m'] ?? ''));