From 2203bc471c9adf429b916cf59daf80838a099318 Mon Sep 17 00:00:00 2001 From: wpdev Date: Fri, 31 Jul 2026 10:01:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20Settings=20=E5=88=86=E5=8D=80?= =?UTF-8?q?=E5=8D=B3=E6=99=82=E5=84=B2=E5=AD=98=EF=BC=88backport=20A=20v3.?= =?UTF-8?q?0.2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7 個設定分區各自加「儲存此區塊」按鈕,經 wp_ajax_wpdo_save_settings_section 以 AJAX 存檔,不再需要整頁 reload。 - 新增 ajax_save_settings_section() + 7 個 private save_section_*() - 新增 admin/assets/wpdo-settings.js(106 行)與 4 條 CSS 規則 - render_settings() 的 7 個 h3 各包上 .wpdo-settings-section[data-section] - 全頁送出按鈕改標「儲存全部設定」,原本的 POST handler 保持不變(漸進增強) hp-transient / wc-term-count 兩個分區的 toggle 仍由核心 admin 呈現(實作在 AddOn),只寫 wp_options,故不需 class_exists 守衛。 --- admin/assets/wpdo-admin.css | 28 +++++ admin/assets/wpdo-settings.js | 106 +++++++++++++++++++ admin/class-tmdo-admin.php | 186 +++++++++++++++++++++++++++++++++- 3 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 admin/assets/wpdo-settings.js diff --git a/admin/assets/wpdo-admin.css b/admin/assets/wpdo-admin.css index 78722e0..0c284cc 100644 --- a/admin/assets/wpdo-admin.css +++ b/admin/assets/wpdo-admin.css @@ -513,3 +513,31 @@ animation: none; } } + +/* ── Settings per-section instant save (v3.0.2 backport) ────────────── */ + +.wpdo-settings-section { + background: var(--color-surface-card, #fbf7f0); + border: 1px solid var(--color-border-soft, #e5dccd); + border-radius: var(--radius-md, 12px); + padding: var(--space-4, 16px) var(--space-6, 24px); + margin-bottom: var(--space-5, 20px); +} + +.wpdo-section-save-row { + display: flex; + align-items: center; + gap: var(--space-3, 12px); + padding-top: var(--space-3, 12px); +} + +.wpdo-section-save-status { + font-size: 0.9rem; + color: var(--color-success, #5a8c5a); + opacity: 0; + transition: opacity var(--duration-normal, 200ms) var(--ease-default, ease); +} + +.wpdo-section-save-status.visible { + opacity: 1; +} diff --git a/admin/assets/wpdo-settings.js b/admin/assets/wpdo-settings.js new file mode 100644 index 0000000..8cf7cc2 --- /dev/null +++ b/admin/assets/wpdo-settings.js @@ -0,0 +1,106 @@ +/** + * WP Data Optimizer — Settings per-section instant save. + * + * Each .wpdo-settings-section card has a "儲存此區塊" button that posts only + * that section's fields via AJAX, updating options without a full page reload. + * + * @since 3.0.2 + */ +/* global wpdoSettings */ +( function () { + 'use strict'; + + if ( typeof wpdoSettings === 'undefined' ) { + return; + } + + const { ajaxUrl, nonce, i18n } = wpdoSettings; + + /** + * Collect form fields within a section into a FormData object. + * Unchecked checkboxes are intentionally omitted (server does isset() ? '1' : '0'). + * + * @param {HTMLElement} section + * @returns {FormData} + */ + function collectFields( section ) { + const fd = new FormData(); + section.querySelectorAll( 'input, select, textarea' ).forEach( ( el ) => { + if ( ! el.name ) { + return; + } + if ( el.type === 'checkbox' ) { + if ( el.checked ) { + fd.append( el.name, el.value ); + } + } else if ( el.type === 'radio' ) { + if ( el.checked ) { + fd.append( el.name, el.value ); + } + } else { + fd.append( el.name, el.value ); + } + } ); + return fd; + } + + /** + * Show a transient status message, then fade it out after 2.5s. + * + * @param {HTMLElement} statusEl + * @param {string} message + * @param {boolean} isError + */ + function showStatus( statusEl, message, isError ) { + statusEl.textContent = message; + statusEl.style.color = isError ? 'var(--color-error, #b03d3d)' : 'var(--color-success, #5a8c5a)'; + statusEl.classList.add( 'visible' ); + clearTimeout( statusEl._wpdo_timer ); + statusEl._wpdo_timer = setTimeout( () => { + statusEl.classList.remove( 'visible' ); + }, 2500 ); + } + + document.addEventListener( 'click', function ( e ) { + const btn = e.target.closest( '.wpdo-section-save' ); + if ( ! btn ) { + return; + } + const section = btn.closest( '.wpdo-settings-section' ); + const statusEl = btn.nextElementSibling; + const sectionKey = section ? section.dataset.section : ''; + if ( ! sectionKey ) { + return; + } + + const originalLabel = btn.textContent; + btn.disabled = true; + btn.textContent = i18n.saving; + + const fd = collectFields( section ); + fd.append( 'action', 'wpdo_save_settings_section' ); + fd.append( '_ajax_nonce', nonce ); + fd.append( 'section', sectionKey ); + + fetch( ajaxUrl, { + method : 'POST', + credentials : 'same-origin', + body : fd, + } ) + .then( ( r ) => r.json() ) + .then( ( data ) => { + if ( data.success ) { + showStatus( statusEl, i18n.saved, false ); + } else { + showStatus( statusEl, i18n.error, true ); + } + } ) + .catch( () => { + showStatus( statusEl, i18n.error, true ); + } ) + .finally( () => { + btn.disabled = false; + btn.textContent = originalLabel; + } ); + } ); +} )(); diff --git a/admin/class-tmdo-admin.php b/admin/class-tmdo-admin.php index 13eaf4a..97846eb 100644 --- a/admin/class-tmdo-admin.php +++ b/admin/class-tmdo-admin.php @@ -44,6 +44,7 @@ class TMDO_Admin { add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) ); add_action( 'network_admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) ); add_action( 'wp_ajax_wpdo_admin_action', array( __CLASS__, 'handle_ajax' ) ); + add_action( 'wp_ajax_wpdo_save_settings_section', array( __CLASS__, 'ajax_save_settings_section' ) ); add_action( 'admin_notices', array( __CLASS__, 'maybe_nginx_backup_notice' ) ); } @@ -300,6 +301,28 @@ class TMDO_Admin { ), ) ); + + // Settings per-section instant save. + wp_enqueue_script( + 'wpdo-settings', + TMDO_URL . 'admin/assets/wpdo-settings.js', + array(), + TMDO_VERSION, + true + ); + wp_localize_script( + 'wpdo-settings', + 'wpdoSettings', + array( + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), + 'nonce' => wp_create_nonce( 'wpdo_save_settings' ), + 'i18n' => array( + 'saving' => __( '儲存中…', '2meet-data-optimizer' ), + 'saved' => __( '✓ 已儲存', '2meet-data-optimizer' ), + 'error' => __( '儲存失敗,請重試。', '2meet-data-optimizer' ), + ), + ) + ); } /** @@ -3211,6 +3234,7 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));' +

@@ -3311,6 +3335,12 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));' } ?> +
+ + +
+ +


@@ -3387,6 +3417,12 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));' OR option_name LIKE '\\_transient\\_timeout\\_wpdo\\_hp\\_pm\\_%'" ); ?> +

+ + +
+
+

\', $value) 把 TTL cache 寫進 wp_postmeta(每個 hp_listing publish 觸發 8-16 個 transient row)。本 filter 在 metadata 層攔截並重新路由到 wp_options(native transient API),HivePress 完全無感,wp_postmeta 保持乾淨。Filter 是 metadata 層運作,不依賴 mode promote。', '2meet-data-optimizer' ); ?> @@ -3477,6 +3513,12 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));' OR meta_key IN ('_hp_price','_hp_status','_hp_featured','_hp_verified','_hp_view_count','_thumbnail_id','_edit_lock','_edit_last')" ); ?> +

+ + +
+
+

@@ -3557,6 +3599,12 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));' WHERE option_name LIKE '\\_transient\\_wpdo\\_wc\\_termcount\\_%'" ); ?> +

+ + +
+
+

cache rows,重新路由到 wp_options(native transient 結構)。WC 自身 cache 失效邏輯不變(每次新增/刪除 product 時會重算寫入),僅儲存位置改變。Read 路徑亦會優先從 wp_options 讀回,cache miss 才 fall-through 至 wp_termmeta(向後相容)。', '2meet-data-optimizer' ); ?> @@ -3619,6 +3667,12 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));' ? TMDO_Term_Comment_Misc_Bucket::count_rows( 'comment' ) : 0; ?> +

+ + +
+
+

@@ -3665,6 +3719,12 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));'

+
+ + +
+
+

@@ -3703,10 +3763,134 @@ wpdo.getListings({ per_page: 3 }).then(r => console.log(r));' - +
+ + +
+
+ 'save_section_notifications', + 'entity-bridge' => 'save_section_entity_bridge', + 'hp-transient' => 'save_section_hp_transient', + 'garbage-filter' => 'save_section_garbage_filter', + 'wc-term-count' => 'save_section_wc_term_count', + 'misc-bucket' => 'save_section_misc_bucket', + 'automator' => 'save_section_automator', + ); + if ( ! isset( $handlers[ $section ] ) ) { + wp_send_json_error( 'invalid_section', 400 ); + } + self::{$handlers[ $section ]}(); + wp_send_json_success( array( 'message' => __( '設定已儲存。', '2meet-data-optimizer' ) ) ); + } } // Boot admin hooks.