From b94c820361377d5bd7c8678d3720da6de3b05935 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 24 Sep 2026 20:27:30 +0800 Subject: [PATCH] feat(pay): mirror reference resource row with copy, icon and reorder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post editor's download row only exposed address / button text / remark, leaving copy_key, copy_val and class as hidden inputs with no UI, so the reference copy area, custom button icon, text and color were unreachable. The submission popup already emitted those keys, which made the two write paths diverge from anything the editor could produce. Expose the full reference field set (地址/备注/复制名称/复制内容/图标/文案/ 颜色) as an accordion card with drag reordering, add the one genuinely new `icon` key through aksrt_sanitize_icon_value(), and teach both frontend download renderers to consume the fields they previously ignored. Co-Authored-By: Claude Sonnet 4.6 --- assets/css/admin/pay-meta-box.css | 195 +++++++++++++++++- assets/css/frontend/resource-meta/part.css | 24 +++ assets/js/admin-pay-meta-box.js | 151 +++++++++++++- inc/helpers/aksrt-pay.php | 7 +- src/Core/DomainModel.php | 28 ++- src/Core/ResourceMetaBox.php | 91 +++++++- src/Core/Setup.php | 1 + templates/parts/resource-download-box.php | 38 ++-- .../download-direct-contract.test.cjs | 11 +- .../post-editor-reference-contract.test.cjs | 45 ++++ 10 files changed, 554 insertions(+), 37 deletions(-) diff --git a/assets/css/admin/pay-meta-box.css b/assets/css/admin/pay-meta-box.css index 210a8342..025c7cfa 100644 --- a/assets/css/admin/pay-meta-box.css +++ b/assets/css/admin/pay-meta-box.css @@ -63,8 +63,7 @@ .aksrt-pay-meta__inline, .aksrt-pay-meta__number, .aksrt-pay-meta__upload, -.aksrt-pay-meta__buttons, -.aksrt-pay-meta__group--inline { +.aksrt-pay-meta__buttons { display: flex; align-items: center; gap: 7px; @@ -120,20 +119,202 @@ display: grid; gap: 7px; margin: 0 0 10px; - padding: 10px; + padding: 0; + overflow: hidden; border: 1px solid #e3e5e8; border-radius: 4px; background: #f8f9fa; } .aksrt-pay-meta__group--inline { - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto; + display: flex; + align-items: center; + gap: 7px; + margin: 0 0 10px; + padding: 10px; + border: 1px solid #e3e5e8; + border-radius: 4px; + background: #f8f9fa; } -.aksrt-pay-meta__group > .button-link-delete { +.aksrt-pay-meta__group-head { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + margin: 0; + background: #fff; + border-bottom: 1px solid #e3e5e8; +} + +.aksrt-pay-meta__group.is-open .aksrt-pay-meta__group-head { + border-bottom-color: #e3e5e8; +} + +.aksrt-pay-meta__group-drag { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + color: #8c8f94; + cursor: move; + border-radius: 3px; +} + +.aksrt-pay-meta__group-drag:hover { + color: #1d2327; + background: #f0f0f1; +} + +.aksrt-pay-meta__group-drag .dashicons { + font-size: 18px; + width: 18px; + height: 18px; +} + +.aksrt-pay-meta__group-toggle { + display: flex; + flex: 1; + align-items: center; + gap: 6px; + min-width: 0; + padding: 0; + font-size: 13px; + font-weight: 600; + color: #1d2327; + text-align: left; + background: none; + border: 0; + box-shadow: none; + cursor: pointer; +} + +.aksrt-pay-meta__group-toggle:focus { + outline: 1px solid #2271b1; + outline-offset: 2px; +} + +.aksrt-pay-meta__group-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.aksrt-pay-meta__group-caret { + flex: 0 0 auto; + color: #8c8f94; + font-size: 16px; + width: 16px; + height: 16px; + transition: transform 0.15s ease; +} + +.aksrt-pay-meta__group.is-open .aksrt-pay-meta__group-caret { + transform: rotate(180deg); +} + +.aksrt-pay-meta__group-body { + display: none; + gap: 10px; + padding: 12px 10px; +} + +.aksrt-pay-meta__group.is-open .aksrt-pay-meta__group-body { + display: grid; +} + +.aksrt-pay-meta__group-body > .button-link-delete { justify-self: start; } +.aksrt-pay-meta__group-body > .description { + margin: -4px 0 0; + color: #777; + font-size: 12px; + line-height: 1.55; +} + +.aksrt-pay-meta__group-field { + display: grid; + gap: 4px; +} + +.aksrt-pay-meta__group-field > label { + color: #1d2327; + font-size: 12px; + font-weight: 600; +} + +.aksrt-pay-meta__group-field--split { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 10px; +} + +.aksrt-pay-meta__group-field--split > div { + display: grid; + gap: 4px; +} + +.aksrt-pay-meta__group-field input[type="text"], +.aksrt-pay-meta__group-field input[type="url"], +.aksrt-pay-meta__group-field textarea { + width: 100%; +} + +.aksrt-pay-meta__group-field .aksrt-icon-picker { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.aksrt-pay-meta__group-field .aksrt-icon-picker__preview { + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + font-size: 16px; + color: #1d2327; + background: #fff; + border: 1px solid #dcdcde; + border-radius: 3px; +} + +.aksrt-pay-meta__group-field .aksrt-icon-picker__value { + flex: 1; + min-width: 160px; +} + +.aksrt-pay-meta__class-swatches { + display: flex; + flex-wrap: wrap; + gap: 5px; +} + +.aksrt-pay-meta__class-swatch { + width: 26px; + height: 22px; + padding: 0; + border: 2px solid #fff; + border-radius: 3px; + box-shadow: 0 0 0 1px #dcdcde; + cursor: pointer; +} + +.aksrt-pay-meta__class-swatch:hover { + box-shadow: 0 0 0 1px #8c8f94; +} + +.aksrt-pay-meta__class-swatch.is-active { + box-shadow: 0 0 0 2px #2271b1; +} + +.aksrt-pay-meta__group.is-dragging { + opacity: 0.5; +} + .aksrt-pay-meta__notice { margin: 16px 18px; padding: 12px 14px; @@ -157,6 +338,10 @@ } .aksrt-pay-meta__group--inline { + flex-wrap: wrap; + } + + .aksrt-pay-meta__group-field--split { grid-template-columns: 1fr; } } diff --git a/assets/css/frontend/resource-meta/part.css b/assets/css/frontend/resource-meta/part.css index fca8faeb..b8e05418 100644 --- a/assets/css/frontend/resource-meta/part.css +++ b/assets/css/frontend/resource-meta/part.css @@ -1071,6 +1071,30 @@ background: linear-gradient(135deg, #333333 0%, #1a1a1a 100%); } +/* 后台自定义按钮配色:元素同时带 legacy .but 的 b-*/jb-* 类, + 它们提供 --this-bg / --this-color,这里读取即可覆盖云服务渐变 */ +.resource-download-box__btn--custom { + background: var(--this-bg, var(--color-primary, #3B82F6)); + color: var(--this-color, #fff); +} + +.resource-download-box__btn--custom:hover { + background: var(--this-bg, var(--color-primary-active, #2563EB)); + color: var(--this-color, #fff); + filter: brightness(0.94); +} + +.resource-download-box__btn--custom .resource-download-box__service-badge { + background: rgb(255 255 255 / 25%); +} + +/* 移动端下载条的 hover 规则作用域更窄,同作用域再声明一次以保持自定义色 */ +.resource-mobile-download-bar .resource-download-box__btn--custom:hover { + background: var(--this-bg, var(--color-primary-active, #2563EB)); + color: var(--this-color, #fff); + filter: brightness(0.94); +} + /* 云服务徽章 */ .resource-download-box__service-badge { display: inline-block; diff --git a/assets/js/admin-pay-meta-box.js b/assets/js/admin-pay-meta-box.js index 4e1c5aa5..d8e07560 100644 --- a/assets/js/admin-pay-meta-box.js +++ b/assets/js/admin-pay-meta-box.js @@ -4,6 +4,9 @@ var root = document.querySelector('[data-aksrt-pay-meta]'); if (!root) return; + var cfg = window.aksrtPayMetaBox || {}; + var rowTitleFallback = (cfg.i18n && cfg.i18n.downloadUrl) || '下载地址'; + function selected(name) { var input = root.querySelector('input[name="aksrt_pay[' + name + ']"]:checked'); return input ? input.value : ''; @@ -79,6 +82,74 @@ frame.open(); } + function rowTitle(row) { + var label = row.querySelector('[data-group-label]'); + var url = row.querySelector('[data-group-url]'); + var text = label ? String(label.value || '').trim() : ''; + if (text === '') { + text = url ? String(url.value || '').trim() : ''; + } + return text !== '' ? text : rowTitleFallback; + } + + function syncRowTitle(row) { + var title = row.querySelector('[data-group-title]'); + if (title) title.textContent = rowTitle(row); + } + + function syncRowColor(row) { + var input = row.querySelector('[data-group-class-input]'); + if (!input) return; + var value = String(input.value || ''); + row.querySelectorAll('[data-group-class]').forEach(function (swatch) { + var active = swatch.getAttribute('data-group-class') === value; + swatch.classList.toggle('is-active', active); + swatch.setAttribute('aria-pressed', active ? 'true' : 'false'); + }); + } + + // Restore contiguous input names so PHP receives an ordered array after any + // reorder / removal instead of relying on DOM order. Anchored to this list's + // own group so sibling groups (attributes, video_episode) are never touched. + function reindexRows(list) { + var group = list.getAttribute('data-group-list') || '[^\\]]+'; + var pattern = new RegExp('aksrt_pay\\[' + group + '\\]\\[\\d+\\]'); + list.querySelectorAll('[data-group-row]').forEach(function (row, index) { + row.querySelectorAll('[name]').forEach(function (input) { + input.name = input.name.replace(pattern, 'aksrt_pay[' + group + '][' + index + ']'); + }); + }); + } + + function toggleRow(row, force) { + var open = typeof force === 'boolean' ? force : !row.classList.contains('is-open'); + row.classList.toggle('is-open', open); + var toggle = row.querySelector('[data-group-toggle]'); + if (toggle) toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); + } + + function initRow(row) { + syncRowTitle(row); + syncRowColor(row); + var drag = row.querySelector('[data-group-drag]'); + if (drag) drag.draggable = true; + } + + function dragAfterElement(list, y) { + var rows = Array.prototype.slice.call(list.querySelectorAll('[data-group-row]:not(.is-dragging)')); + var best = null; + var bestOffset = -Infinity; + rows.forEach(function (row) { + var box = row.getBoundingClientRect(); + var offset = y - box.top - box.height / 2; + if (offset < 0 && offset > bestOffset) { + bestOffset = offset; + best = row; + } + }); + return best; + } + function addGroup(button) { var id = button.getAttribute('data-group-add'); var list = root.querySelector('[data-group-list="' + id + '"]'); @@ -86,6 +157,11 @@ if (!list || !template) return; var index = list.querySelectorAll('[data-group-row]').length; list.insertAdjacentHTML('beforeend', template.innerHTML.replaceAll('__INDEX__', String(index))); + var added = list.lastElementChild; + if (added && added.hasAttribute('data-group-row')) { + initRow(added); + toggleRow(added, false); + } } root.addEventListener('click', function (event) { @@ -122,15 +198,88 @@ addGroup(add); return; } + var toggle = target.closest('[data-group-toggle]'); + if (toggle) { + event.preventDefault(); + var toggleRowEl = toggle.closest('[data-group-row]'); + if (toggleRowEl) toggleRow(toggleRowEl); + return; + } + var swatch = target.closest('[data-group-class]'); + if (swatch) { + event.preventDefault(); + var swatchRow = swatch.closest('[data-group-row]'); + var classInput = swatchRow && swatchRow.querySelector('[data-group-class-input]'); + if (classInput) { + classInput.value = swatch.getAttribute('data-group-class') || ''; + syncRowColor(swatchRow); + } + return; + } var remove = target.closest('[data-group-remove]'); if (remove) { event.preventDefault(); var row = remove.closest('[data-group-row]'); + var list = remove.closest('[data-group-list]'); if (row) row.remove(); + if (list) reindexRows(list); } }); - root.addEventListener('change', syncDependencies); + root.addEventListener('change', function (event) { + syncDependencies(); + var row = event.target.closest && event.target.closest('[data-group-row]'); + if (row && (event.target.hasAttribute('data-group-url') || event.target.hasAttribute('data-group-label'))) { + syncRowTitle(row); + } + }); + + root.addEventListener('input', function (event) { + var row = event.target.closest('[data-group-row]'); + if (!row) return; + if (event.target.hasAttribute('data-group-label') || event.target.hasAttribute('data-group-url')) { + syncRowTitle(row); + } + }); + + root.addEventListener('dragstart', function (event) { + var row = event.target.closest && event.target.closest('[data-group-row]'); + if (!row) return; + row.classList.add('is-dragging'); + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move'; + try { + event.dataTransfer.setData('text/plain', ''); + } catch (error) { /* older IE */ } + } + }); + + root.addEventListener('dragend', function (event) { + var row = event.target.closest && event.target.closest('[data-group-row]'); + if (!row) return; + row.classList.remove('is-dragging'); + var list = row.closest('[data-group-list]'); + if (list) reindexRows(list); + }); + + root.addEventListener('dragover', function (event) { + var list = event.target.closest ? event.target.closest('[data-group-list]') : null; + if (!list) return; + var dragging = list.querySelector('[data-group-row].is-dragging'); + if (!dragging) return; + event.preventDefault(); + var after = dragAfterElement(list, event.clientY); + if (after == null) { + list.appendChild(dragging); + } else if (after !== dragging) { + list.insertBefore(dragging, after); + } + }); + + root.querySelectorAll('[data-group-list]').forEach(function (list) { + list.querySelectorAll('[data-group-row]').forEach(initRow); + reindexRows(list); + }); renderGallery(); syncDependencies(); })(); diff --git a/inc/helpers/aksrt-pay.php b/inc/helpers/aksrt-pay.php index 0712fced..2d50dd6b 100644 --- a/inc/helpers/aksrt-pay.php +++ b/inc/helpers/aksrt-pay.php @@ -1478,13 +1478,18 @@ if (!function_exists('aksrt_get_paid_download_actions_html')) { $service_key = sanitize_html_class((string) ($service['key'] ?? 'other')) ?: 'other'; $button_class = trim((string) ($download['class'] ?? '')); $button_class = $button_class !== '' ? $button_class : 'b-theme ' . $service_key; + $icon = trim((string) ($download['icon'] ?? '')); + $icon_html = $icon !== '' && function_exists('aksrt_icon_markup') ? aksrt_icon_markup($icon) : ''; + if ($icon_html === '') { + $icon_html = ''; + } $copy_value = trim((string) ($download['copy_val'] ?? '')); $copy_key = trim((string) ($download['copy_key'] ?? '')); $copy_label = $extract_code !== '' ? $extract_code : ($copy_key !== '' ? $copy_key : __('更多内容', 'aksrt')); $buttons .= '
'; $buttons .= ''; - $buttons .= '' . esc_html($label) . ''; + $buttons .= $icon_html . esc_html($label) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped if ($copy_value !== '') { $buttons .= ''; } elseif ($extract_code !== '') { diff --git a/src/Core/DomainModel.php b/src/Core/DomainModel.php index e7cc2517..15f7fabd 100644 --- a/src/Core/DomainModel.php +++ b/src/Core/DomainModel.php @@ -24,6 +24,21 @@ class DomainModel { public const RESOURCE_PAY_META_MIGRATED = 'aksrt_resource_pay_meta_migrated'; + /** + * Allowed download-button color classes (legacy .but swatch system). + * + * Shared by the sanitizer and the meta box palette so the two can not drift. + * + * @var array + */ + public const DOWNLOAD_BUTTON_CLASSES = [ + '', + 'c-red', 'c-yellow', 'c-cyan', 'c-blue', 'c-green', 'c-purple', + 'b-red', 'b-yellow', 'b-cyan', 'b-blue', 'b-green', 'b-purple', + 'jb-red', 'jb-pink', 'jb-yellow', 'jb-cyan', 'jb-blue', 'jb-green', 'jb-purple', + 'jb-vip1', 'jb-vip2', + ]; + public const ORDER_PRODUCT_RESOURCE = 'resource'; public const ORDER_PRODUCT_VIP = 'vip'; public const ORDER_PRODUCT_RECHARGE = 'recharge'; @@ -174,7 +189,7 @@ class DomainModel { /** * @param mixed $value - * @return array + * @return array */ public static function sanitize_downloads($value): array { if (!is_array($value)) { @@ -182,13 +197,7 @@ class DomainModel { } $allowed_types = ['direct', 'net_disk', 'external']; - $allowed_classes = [ - '', - 'c-red', 'c-yellow', 'c-cyan', 'c-blue', 'c-green', 'c-purple', - 'b-red', 'b-yellow', 'b-cyan', 'b-blue', 'b-green', 'b-purple', - 'jb-red', 'jb-pink', 'jb-yellow', 'jb-cyan', 'jb-blue', 'jb-green', 'jb-purple', - 'jb-vip1', 'jb-vip2', - ]; + $allowed_classes = self::DOWNLOAD_BUTTON_CLASSES; $downloads = []; foreach ($value as $row) { @@ -239,6 +248,9 @@ class DomainModel { 'extract_code' => $extract_code, 'copy_key' => isset($row['copy_key']) ? sanitize_text_field((string) $row['copy_key']) : '', 'copy_val' => isset($row['copy_val']) ? sanitize_text_field((string) $row['copy_val']) : '', + 'icon' => isset($row['icon']) && function_exists('aksrt_sanitize_icon_value') + ? aksrt_sanitize_icon_value((string) $row['icon']) + : (isset($row['icon']) ? sanitize_text_field((string) $row['icon']) : ''), 'class' => isset($row['class']) && in_array($row['class'], $allowed_classes, true) ? (string) $row['class'] : '', diff --git a/src/Core/ResourceMetaBox.php b/src/Core/ResourceMetaBox.php index 57d49d93..50a03f62 100644 --- a/src/Core/ResourceMetaBox.php +++ b/src/Core/ResourceMetaBox.php @@ -42,14 +42,24 @@ class ResourceMetaBox { } wp_enqueue_media(); + if (function_exists('aksrt_icon_picker_assets')) { + aksrt_icon_picker_assets(['aksrt-pay-meta-box']); + } $css = AKSRT_DIR . '/assets/css/admin/pay-meta-box.css'; $js = AKSRT_DIR . '/assets/js/admin-pay-meta-box.js'; if (is_file($css)) { - wp_enqueue_style('aksrt-pay-meta-box', AKSRT_URI . '/assets/css/admin/pay-meta-box.css', [], (string) filemtime($css)); + wp_enqueue_style('aksrt-pay-meta-box', AKSRT_URI . '/assets/css/admin/pay-meta-box.css', ['aksrt-icon-picker'], (string) filemtime($css)); } if (is_file($js)) { - wp_enqueue_script('aksrt-pay-meta-box', AKSRT_URI . '/assets/js/admin-pay-meta-box.js', [], (string) filemtime($js), true); + wp_enqueue_script('aksrt-pay-meta-box', AKSRT_URI . '/assets/js/admin-pay-meta-box.js', ['aksrt-icon-picker'], (string) filemtime($js), true); } + wp_localize_script('aksrt-pay-meta-box', 'aksrtPayMetaBox', [ + 'buttonClasses' => DomainModel::DOWNLOAD_BUTTON_CLASSES, + 'i18n' => [ + 'resource' => __('资源', 'aksrt'), + 'downloadUrl' => __('下载地址', 'aksrt'), + ], + ]); } public function render(\WP_Post $post): void { @@ -133,7 +143,7 @@ class ResourceMetaBox {
- group('downloads', __('资源下载', 'aksrt'), (array) ($meta['downloads'] ?? []), 'download_row', __('添加资源', 'aksrt')); ?> + group('downloads', __('资源下载', 'aksrt'), (array) ($meta['downloads'] ?? []), 'download_row', __('添加资源', 'aksrt'), __('每个资源一张卡片,点击标题展开配置;拖动卡片左上角的手柄可调整下载源的显示顺序。', 'aksrt')); ?> group('attributes', __('资源属性', 'aksrt'), (array) ($meta['attributes'] ?? []), 'attribute_row', __('添加属性', 'aksrt')); ?> text('demo_url', __('演示地址', 'aksrt'), (string) ($meta['demo_url'] ?? ''), '', 'https://', 'url'); ?>
@@ -199,8 +209,81 @@ class ResourceMetaBox { /** @param int|string $index */ private function download_row($index, array $row): void { + $url = (string) ($row['url'] ?? ''); + $label = (string) ($row['label'] ?? ''); + $extract_code = (string) ($row['extract_code'] ?? ''); + $copy_key = (string) ($row['copy_key'] ?? ''); + $copy_val = (string) ($row['copy_val'] ?? ''); + $icon = (string) ($row['icon'] ?? ''); + $class = (string) ($row['class'] ?? ''); + $name = 'aksrt_pay[downloads][' . $index . ']'; + $title = $label !== '' ? $label : ($url !== '' ? $url : __('下载地址', 'aksrt')); ?> -
+
+
+ + + +
+
+
+ +
+ + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+

+
+ + 'fa fas fa-download'] + ); + } else { + ?> + + +
+
+ + +
+
+ +
+ + + +
+ +

+
+
+
['type' => 'string'], 'copy_key' => ['type' => 'string'], 'copy_val' => ['type' => 'string'], + 'icon' => ['type' => 'string'], 'class' => ['type' => 'string'], ], ], diff --git a/templates/parts/resource-download-box.php b/templates/parts/resource-download-box.php index 49aac7f7..ad94f5e4 100644 --- a/templates/parts/resource-download-box.php +++ b/templates/parts/resource-download-box.php @@ -333,10 +333,7 @@ if ($is_need_purchase && $user_amount > 0) {