diff --git a/assets/admin/src/constants/sections/page-display.js b/assets/admin/src/constants/sections/page-display.js index 7bb773ce..9430faf1 100644 --- a/assets/admin/src/constants/sections/page-display.js +++ b/assets/admin/src/constants/sections/page-display.js @@ -86,16 +86,6 @@ export const pageDisplaySection = { desc: '首页内容模型、主列表、分类 Tab 与首页交互配置。', component: 'HomepageSettings', }), - schemaPage({ - key: 'page-display-float-btn', - name: 'PageDisplayFloatBtn', - label: '悬浮按钮', - route: '/page-display/float-btn', - title: '悬浮按钮', - desc: '悬浮按钮的显示规则、样式与滚动隐藏设置。', - canonicalDomain: 'page_display', - section: 'float-btn', - }), schemaPage({ key: 'page-display-category', name: 'PageDisplayCategory', diff --git a/assets/admin/src/pages/content/HeroManager.vue b/assets/admin/src/pages/content/HeroManager.vue index 4963f9fb..23e1779b 100644 --- a/assets/admin/src/pages/content/HeroManager.vue +++ b/assets/admin/src/pages/content/HeroManager.vue @@ -5,21 +5,13 @@ - - -
- - 关闭后,首页 Hero 区块将不再渲染。 -
-
-
-
- 保存 Hero 开关 -
+

+ 幻灯片内容在此维护。页面类型启用、设备规则、搜索覆盖层与图标卡片请到「顶部多功能组件」配置;全不选页面类型即为关闭。 +

@@ -216,10 +208,7 @@ function goHeaderComponents() { } const loading = ref(true) const saving = ref(false) -const settingsLoading = ref(false) -const settingsSaving = ref(false) const slides = ref([]) -const heroEnabled = ref(true) const modalShow = ref(false) const editing = ref(emptyForm()) @@ -348,36 +337,7 @@ async function loadSlides() { } } -async function loadHeroSettings() { - settingsLoading.value = true - try { - const res = await api.getHomepageSettings() - const data = res?.data || {} - heroEnabled.value = data.homepage_module_hero_enabled !== false - } catch (e) { - message.error(e.message || '加载 Hero 开关失败') - } finally { - settingsLoading.value = false - } -} -async function saveHeroSettings() { - settingsSaving.value = true - try { - const res = await api.saveHomepageSettings({ - homepage_module_hero_enabled: heroEnabled.value, - }) - if (res?.code === 'success') { - message.success('Hero 开关已保存') - return - } - throw new Error(res?.message || '保存 Hero 开关失败') - } catch (e) { - message.error(e.message || '保存 Hero 开关失败') - } finally { - settingsSaving.value = false - } -} function openCreate() { editing.value = emptyForm() @@ -495,7 +455,6 @@ function handleDelete(row) { onMounted(() => { loadSlides() - loadHeroSettings() }) diff --git a/assets/admin/src/schema/sections/page-display/index.js b/assets/admin/src/schema/sections/page-display/index.js index 20b2e71c..28db59bf 100644 --- a/assets/admin/src/schema/sections/page-display/index.js +++ b/assets/admin/src/schema/sections/page-display/index.js @@ -626,14 +626,17 @@ export const pageDisplaySchemas = { { key: 'header_components_cards_enabled', label: '启用图标卡片', type: 'switch', default: false, help: '在顶部组件区域显示图标卡片入口' }, { key: 'header_components_show_on', label: '按页面类型启用', type: 'checkbox', default: ['home'], options: [ { label: '首页', value: 'home' }, - { label: '文章', value: 'single' }, - { label: '页面', value: 'page' }, - { label: '分类', value: 'category' }, - { label: '标签', value: 'tag' }, - { label: '搜索', value: 'search' }, - { label: '归档', value: 'archive' }, - { label: '专题', value: 'special' }, - ], help: '选择哪些页面类型显示顶部组件,不选则关闭' }, + { label: '专题页', value: 'topics' }, + { label: '分类页', value: 'category' }, + { label: '标签页', value: 'tag' }, + { label: '用户页', value: 'author' }, + { label: '文章页', value: 'single' }, + { label: '搜索页', value: 'search' }, + { label: '[论坛]首页', value: 'forum_home' }, + { label: '[论坛]板块页面', value: 'forum_plate' }, + { label: '[论坛]帖子页面', value: 'forum_post' }, + { label: '其它页面', value: 'page' }, + ], help: '选择启用顶部多功能组件的页面类型,一个都不选则为关闭此功能' }, { key: 'header_components_device_visibility', label: '设备显示规则', type: 'radio', default: '', options: [ { label: '全部', value: '' }, { label: '仅电脑', value: 'only_pc' }, @@ -742,74 +745,6 @@ export const pageDisplaySchemas = { ], }, - /** - * Floating button settings - */ - 'float-btn': { - key: 'page_display.float-btn', - title: '悬浮按钮', - icon: 'fas fa-circle-chevron-up', - description: '配置页面悬浮按钮的显示规则、样式和滚动隐藏行为。', - defaults: { - float_btn_style: 'round', - float_btn_position: 'bottom', - float_btn_scroll_hide: false, - float_btn_scroll_down_hide: true, - float_btn_max: 5, - float_btn_size: 44, - float_btn_spacing: 10, - float_btn_shadow: true, - float_btn_animation: true, - float_btn_opacity: 100, - float_btn_active_color: '', - float_btn_bg_color: '', - float_btn_pc_s: true, - float_btn_m_s: true, - float_btn_filter_css: '', - }, - groups: [ - { - key: 'basic', - title: '悬浮按钮设置', - icon: 'fas fa-circle-notch', - fields: [ - { - key: 'float_btn_style', - label: '按钮风格', - type: 'radio', - default: 'round', - options: [ - { label: '圆形', value: 'round' }, - { label: '方形', value: 'square' }, - ], - }, - { - key: 'float_btn_position', - label: '按钮位置', - type: 'radio', - default: 'bottom', - options: [ - { label: '底部', value: 'bottom' }, - { label: '居中', value: 'center' }, - ], - }, - { key: 'float_btn_scroll_hide', label: '滚动时隐藏', type: 'switch', default: false }, - { key: 'float_btn_scroll_down_hide', label: '下滑时隐藏', type: 'switch', default: true }, - { key: 'float_btn_max', label: '最大显示数', type: 'number', default: 5, min: 1, max: 10 }, - { key: 'float_btn_size', label: '按钮尺寸', type: 'number', default: 44, min: 32, max: 64 }, - { key: 'float_btn_spacing', label: '按钮间距', type: 'number', default: 10, min: 4, max: 24 }, - { key: 'float_btn_shadow', label: '显示阴影', type: 'switch', default: true }, - { key: 'float_btn_animation', label: '悬停动画', type: 'switch', default: true }, - { key: 'float_btn_opacity', label: '按钮透明度', type: 'number', default: 100, min: 30, max: 100 }, - { key: 'float_btn_active_color', label: '激活颜色', type: 'color', default: '' }, - { key: 'float_btn_bg_color', label: '背景颜色', type: 'color', default: '' }, - { key: 'float_btn_pc_s', label: 'PC端显示', type: 'switch', default: true }, - { key: 'float_btn_m_s', label: '移动端显示', type: 'switch', default: true }, - { key: 'float_btn_filter_css', label: '过滤 CSS', type: 'textarea', default: '' }, - ], - }, - ], - }, footer: { key: 'page_display.footer', title: '底部页脚', diff --git a/assets/js/core/article.js b/assets/js/core/article.js index f4e02461..b8dd91c7 100644 --- a/assets/js/core/article.js +++ b/assets/js/core/article.js @@ -157,85 +157,116 @@ return null; } + function collectActionButtons(scope, selector) { + scope = scope || document; + var list = []; + if (!scope) return list; + if (scope.nodeType === 1 && scope.matches && scope.matches(selector)) { + list.push(scope); + } + if (typeof scope.querySelectorAll === "function") { + var found = scope.querySelectorAll(selector); + for (var i = 0; i < found.length; i++) list.push(found[i]); + } else if (scope.nodeType === 9) { + var all = document.querySelectorAll(selector); + for (var j = 0; j < all.length; j++) list.push(all[j]); + } + // also keep legacy id hooks + var legacyId = selector.indexOf("action-like") >= 0 ? "action-like" : (selector.indexOf("action-favorite") >= 0 ? "action-favorite" : ""); + if (legacyId) { + var legacy = findInScope(scope, legacyId); + if (legacy && list.indexOf(legacy) === -1) list.push(legacy); + } + return list; + } + function initInteractionBar(root) { var scope = root || document; - // Element roots (AJAX rehydrate) do not implement getElementById - var likeBtn = findInScope(scope, "action-like"); - var favBtn = findInScope(scope, "action-favorite"); + // Bind all like/favorite controls (in-content + footer tabbar extend). + var likeBtns = collectActionButtons(scope, ".action-like[data-post-id]"); + var favBtns = collectActionButtons(scope, ".action-favorite[data-post-id]"); - if (likeBtn && likeBtn.dataset.aksrtInteractionBound) return; - if (favBtn && favBtn.dataset.aksrtInteractionBound) return; - - if (likeBtn) { - var likePostId = likeBtn.dataset.postId ? String(likeBtn.dataset.postId) : ""; - - function applyLikeState(isActive, count) { - likeBtn.dataset.initialActive = isActive ? "1" : "0"; - likeBtn.classList.toggle("is-active", isActive); - var likeIcon = likeBtn.querySelector("i"); - if (likeIcon) { - likeIcon.className = isActive ? "fas fa-heart" : "far fa-heart"; - } - if ("number" === typeof count && !isNaN(count)) { - setBaseStatCount("likes", count); - } else { - updateStatCounts("likes", getBaseStatCount("likes")); - } - } - - applyLikeState(likeBtn.dataset.initialActive === "1", getBaseStatCount("likes")); - - likeBtn.addEventListener("click", function () { - if (!likePostId || likeBtn.disabled) return; - var nextState = likeBtn.dataset.initialActive !== "1"; - setButtonBusy(likeBtn, true); - requestInteraction("aksrt_toggle_like", likePostId, nextState).then(function (data) { - applyLikeState(!!data.active, parseInt(data.count, 10) || 0); - showToast(data.active ? "感谢你的点赞" : "已取消点赞", "success"); - }).catch(function (error) { - showToast(error && error.message ? error.message : "点赞失败", "error"); - }).finally(function () { - setButtonBusy(likeBtn, false); - }); - }); + function paintLike(btn, isActive) { + btn.dataset.initialActive = isActive ? "1" : "0"; + btn.classList.toggle("is-active", isActive); + var icon = btn.querySelector("i"); + if (icon) icon.className = isActive ? "fas fa-heart" : "far fa-heart"; } - if (favBtn) { - var favoritePostId = favBtn.dataset.postId ? String(favBtn.dataset.postId) : ""; - - function applyFavoriteState(isActive, count) { - favBtn.dataset.initialActive = isActive ? "1" : "0"; - favBtn.classList.toggle("is-active", isActive); - var favoriteIcon = favBtn.querySelector("i"); - if (favoriteIcon) { - favoriteIcon.className = isActive ? "fas fa-star" : "far fa-star"; - } - if ("number" === typeof count && !isNaN(count)) { - setBaseStatCount("favorites", count); - } else { - updateStatCounts("favorites", getBaseStatCount("favorites")); - } - } - - applyFavoriteState(favBtn.dataset.initialActive === "1", getBaseStatCount("favorites")); - - favBtn.addEventListener("click", function () { - if (!favoritePostId || favBtn.disabled) return; - var nextState = favBtn.dataset.initialActive !== "1"; - setButtonBusy(favBtn, true); - requestInteraction("aksrt_toggle_favorite", favoritePostId, nextState).then(function (data) { - applyFavoriteState(!!data.active, parseInt(data.count, 10) || 0); - showToast(data.active ? "收藏成功" : "已取消收藏", "success"); - }).catch(function (error) { - showToast(error && error.message ? error.message : "收藏失败", "error"); - }).finally(function () { - setButtonBusy(favBtn, false); - }); - }); + function paintFavorite(btn, isActive) { + btn.dataset.initialActive = isActive ? "1" : "0"; + btn.classList.toggle("is-active", isActive); + var icon = btn.querySelector("i"); + if (icon) icon.className = isActive ? "fas fa-star" : "far fa-star"; } - if (likeBtn) likeBtn.dataset.aksrtInteractionBound = "1"; - if (favBtn) favBtn.dataset.aksrtInteractionBound = "1"; + function applyLikeState(isActive, count) { + for (var i = 0; i < likeBtns.length; i++) paintLike(likeBtns[i], isActive); + if ("number" === typeof count && !isNaN(count)) { + setBaseStatCount("likes", count); + } else { + updateStatCounts("likes", getBaseStatCount("likes")); + } + } + + function applyFavoriteState(isActive, count) { + for (var i = 0; i < favBtns.length; i++) paintFavorite(favBtns[i], isActive); + if ("number" === typeof count && !isNaN(count)) { + setBaseStatCount("favorites", count); + } else { + updateStatCounts("favorites", getBaseStatCount("favorites")); + } + } + + if (likeBtns.length) { + var seedLike = likeBtns[0]; + if (!seedLike.dataset.aksrtInteractionBound) { + applyLikeState(seedLike.dataset.initialActive === "1", getBaseStatCount("likes")); + likeBtns.forEach(function (likeBtn) { + if (likeBtn.dataset.aksrtInteractionBound) return; + likeBtn.dataset.aksrtInteractionBound = "1"; + likeBtn.addEventListener("click", function () { + var postId = likeBtn.dataset.postId ? String(likeBtn.dataset.postId) : ""; + if (!postId || likeBtn.disabled) return; + var nextState = likeBtn.dataset.initialActive !== "1"; + setButtonBusy(likeBtn, true); + requestInteraction("aksrt_toggle_like", postId, nextState).then(function (data) { + applyLikeState(!!data.active, parseInt(data.count, 10) || 0); + showToast(data.active ? "感谢你的点赞" : "已取消点赞", "success"); + }).catch(function (error) { + showToast(error && error.message ? error.message : "点赞失败", "error"); + }).finally(function () { + setButtonBusy(likeBtn, false); + }); + }); + }); + } + } + + if (favBtns.length) { + var seedFav = favBtns[0]; + if (!seedFav.dataset.aksrtInteractionBound) { + applyFavoriteState(seedFav.dataset.initialActive === "1", getBaseStatCount("favorites")); + favBtns.forEach(function (favBtn) { + if (favBtn.dataset.aksrtInteractionBound) return; + favBtn.dataset.aksrtInteractionBound = "1"; + favBtn.addEventListener("click", function () { + var postId = favBtn.dataset.postId ? String(favBtn.dataset.postId) : ""; + if (!postId || favBtn.disabled) return; + var nextState = favBtn.dataset.initialActive !== "1"; + setButtonBusy(favBtn, true); + requestInteraction("aksrt_toggle_favorite", postId, nextState).then(function (data) { + applyFavoriteState(!!data.active, parseInt(data.count, 10) || 0); + showToast(data.active ? "收藏成功" : "已取消收藏", "success"); + }).catch(function (error) { + showToast(error && error.message ? error.message : "收藏失败", "error"); + }).finally(function () { + setButtonBusy(favBtn, false); + }); + }); + }); + } + } } window.AksrtFrontend.register("aksrt-search-clear", function () { initSearchClear(); }); diff --git a/assets/js/post-share.js b/assets/js/post-share.js index 7765328c..dd0234b3 100644 --- a/assets/js/post-share.js +++ b/assets/js/post-share.js @@ -123,8 +123,9 @@ } function initShareDropdown() { - var trigger = document.getElementById('action-share-trigger'); var dropdown = document.getElementById('share-dropdown'); + var triggers = Array.prototype.slice.call(document.querySelectorAll('#action-share-trigger, .action-share[data-type="share"], .footer-tabbar .action-share')).filter(function (el, idx, arr) { return arr.indexOf(el) === idx; }); + var trigger = triggers[0] || null; if (!trigger || !dropdown || dropdown.dataset.aksrtShareBound === '1') return; dropdown.dataset.aksrtShareBound = '1'; @@ -160,6 +161,16 @@ toggleDropdown(); }); + triggers.slice(1).forEach(function (extraTrigger) { + extraTrigger.addEventListener('click', function (event) { + event.preventDefault(); + event.stopPropagation(); + // Reuse primary trigger click semantics by synthetic toggle + trigger.click(); + }); + }); + + document.addEventListener('click', function (event) { if (!trigger.contains(event.target) && !dropdown.contains(event.target)) { closeDropdown(); diff --git a/footer.php b/footer.php index 36b787d0..0a37041a 100644 --- a/footer.php +++ b/footer.php @@ -471,27 +471,38 @@ if ($floating_enabled) : - + -