feat(pay): mirror reference resource row with copy, icon and reorder

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 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 20:27:30 +08:00
co-authored by Claude Sonnet 4.6
parent ab495854d5
commit b94c820361
10 changed files with 554 additions and 37 deletions
+190 -5
View File
@@ -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;
}
}
@@ -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;
+150 -1
View File
@@ -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();
})();
+6 -1
View File
@@ -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 = '<i class="fa fas fa-download" aria-hidden="true"></i>';
}
$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 .= '<div class="but-download flex ac">';
$buttons .= '<a class="mr10 but ' . esc_attr($button_class) . '" href="' . esc_url($url) . '" target="_blank" rel="nofollow noopener">';
$buttons .= '<i class="fa fas fa-download" aria-hidden="true"></i>' . esc_html($label) . '</a>';
$buttons .= $icon_html . esc_html($label) . '</a>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
if ($copy_value !== '') {
$buttons .= '<button type="button" class="but" data-clipboard-tag="' . esc_attr($copy_key !== '' ? $copy_key : __('内容', 'aksrt')) . '" data-clipboard-text="' . esc_attr($copy_value) . '">' . esc_html($copy_label) . '</button>';
} elseif ($extract_code !== '') {
+20 -8
View File
@@ -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<int, string>
*/
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<int, array{label:string,url:string,type:string,extract_code:string,copy_key:string,copy_val:string,class:string,attachment_id:int}>
* @return array<int, array{label:string,url:string,type:string,extract_code:string,copy_key:string,copy_val:string,icon:string,class:string,attachment_id:int}>
*/
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']
: '',
+87 -4
View File
@@ -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 {
</div>
<div data-pay-type="download">
<?php $this->group('downloads', __('资源下载', 'aksrt'), (array) ($meta['downloads'] ?? []), 'download_row', __('添加资源', 'aksrt')); ?>
<?php $this->group('downloads', __('资源下载', 'aksrt'), (array) ($meta['downloads'] ?? []), 'download_row', __('添加资源', 'aksrt'), __('每个资源一张卡片,点击标题展开配置;拖动卡片左上角的手柄可调整下载源的显示顺序。', 'aksrt')); ?>
<?php $this->group('attributes', __('资源属性', 'aksrt'), (array) ($meta['attributes'] ?? []), 'attribute_row', __('添加属性', 'aksrt')); ?>
<?php $this->text('demo_url', __('演示地址', 'aksrt'), (string) ($meta['demo_url'] ?? ''), '', 'https://', 'url'); ?>
</div>
@@ -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'));
?>
<div class="aksrt-pay-meta__group" data-group-row><div class="aksrt-pay-meta__upload"><input type="url" name="aksrt_pay[downloads][<?php echo esc_attr((string) $index); ?>][url]" value="<?php echo esc_attr((string) ($row['url'] ?? '')); ?>" placeholder="<?php esc_attr_e('上传文件或输入下载地址', 'aksrt'); ?>" data-media-input><button type="button" class="button" data-media-select data-library=""><?php esc_html_e('选择', 'aksrt'); ?></button></div><input type="text" name="aksrt_pay[downloads][<?php echo esc_attr((string) $index); ?>][label]" value="<?php echo esc_attr((string) ($row['label'] ?? '')); ?>" placeholder="<?php esc_attr_e('按钮文案', 'aksrt'); ?>"><textarea rows="1" name="aksrt_pay[downloads][<?php echo esc_attr((string) $index); ?>][extract_code]" placeholder="<?php esc_attr_e('资源备注、提取码或解压密码', 'aksrt'); ?>"><?php echo esc_textarea((string) ($row['extract_code'] ?? '')); ?></textarea><input type="hidden" name="aksrt_pay[downloads][<?php echo esc_attr((string) $index); ?>][copy_key]" value="<?php echo esc_attr((string) ($row['copy_key'] ?? '')); ?>"><input type="hidden" name="aksrt_pay[downloads][<?php echo esc_attr((string) $index); ?>][copy_val]" value="<?php echo esc_attr((string) ($row['copy_val'] ?? '')); ?>"><input type="hidden" name="aksrt_pay[downloads][<?php echo esc_attr((string) $index); ?>][class]" value="<?php echo esc_attr((string) ($row['class'] ?? '')); ?>"><button type="button" class="button-link-delete" data-group-remove><?php esc_html_e('移除', 'aksrt'); ?></button></div>
<div class="aksrt-pay-meta__group is-open" data-group-row>
<div class="aksrt-pay-meta__group-head">
<span class="aksrt-pay-meta__group-drag" data-group-drag title="<?php esc_attr_e('拖拽排序', 'aksrt'); ?>"><span class="dashicons dashicons-move"></span></span>
<button type="button" class="aksrt-pay-meta__group-toggle" data-group-toggle aria-expanded="true">
<span class="aksrt-pay-meta__group-title" data-group-title><?php echo esc_html($title); ?></span>
<span class="aksrt-pay-meta__group-caret dashicons dashicons-arrow-down"></span>
</button>
<button type="button" class="button-link-delete" data-group-remove><?php esc_html_e('移除', 'aksrt'); ?></button>
</div>
<div class="aksrt-pay-meta__group-body">
<div class="aksrt-pay-meta__group-field">
<label><?php esc_html_e('下载地址', 'aksrt'); ?></label>
<div class="aksrt-pay-meta__upload">
<input type="url" name="<?php echo esc_attr($name); ?>[url]" value="<?php echo esc_attr($url); ?>" placeholder="<?php esc_attr_e('上传文件或输入下载地址', 'aksrt'); ?>" data-media-input data-group-url>
<button type="button" class="button" data-media-select data-library=""><?php esc_html_e('选择', 'aksrt'); ?></button>
</div>
</div>
<div class="aksrt-pay-meta__group-field">
<label><?php esc_html_e('资源备注', 'aksrt'); ?></label>
<textarea rows="1" name="<?php echo esc_attr($name); ?>[extract_code]" placeholder="<?php esc_attr_e('按钮旁边的额外内容,例如提取码、解压密码等', 'aksrt'); ?>"><?php echo esc_textarea($extract_code); ?></textarea>
</div>
<div class="aksrt-pay-meta__group-field aksrt-pay-meta__group-field--split">
<div>
<label><?php esc_html_e('复制的名称', 'aksrt'); ?></label>
<input type="text" name="<?php echo esc_attr($name); ?>[copy_key]" value="<?php echo esc_attr($copy_key); ?>" placeholder="<?php esc_attr_e('例如:提取码', 'aksrt'); ?>">
</div>
<div>
<label><?php esc_html_e('复制的内容', 'aksrt'); ?></label>
<input type="text" name="<?php echo esc_attr($name); ?>[copy_val]" value="<?php echo esc_attr($copy_val); ?>" placeholder="<?php esc_attr_e('例如:a1b2', 'aksrt'); ?>">
</div>
</div>
<p class="description"><?php esc_html_e('为“资源备注”按钮添加点击复制功能,请设置复制名称和复制内容。', 'aksrt'); ?></p>
<div class="aksrt-pay-meta__group-field">
<label><?php esc_html_e('自定义按钮图标', 'aksrt'); ?></label>
<?php
if (function_exists('aksrt_icon_picker_field')) {
echo aksrt_icon_picker_field( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$name . '[icon]',
'aksrt-pay-download-icon-' . $index,
$icon,
['placeholder' => 'fa fas fa-download']
);
} else {
?>
<input type="text" name="<?php echo esc_attr($name); ?>[icon]" value="<?php echo esc_attr($icon); ?>" placeholder="fa fas fa-download">
<?php
}
?>
</div>
<div class="aksrt-pay-meta__group-field">
<label><?php esc_html_e('自定义按钮文案', 'aksrt'); ?></label>
<textarea rows="1" name="<?php echo esc_attr($name); ?>[label]" placeholder="<?php esc_attr_e('例如:立即下载', 'aksrt'); ?>" data-group-label><?php echo esc_textarea($label); ?></textarea>
</div>
<div class="aksrt-pay-meta__group-field">
<label><?php esc_html_e('自定义按钮颜色', 'aksrt'); ?></label>
<div class="aksrt-pay-meta__class-swatches" data-group-classes>
<?php foreach (DomainModel::DOWNLOAD_BUTTON_CLASSES as $swatch) : ?>
<button type="button" class="aksrt-pay-meta__class-swatch <?php echo $swatch !== '' ? esc_attr($swatch) : 'b-theme'; ?><?php echo $swatch === $class ? ' is-active' : ''; ?>" data-group-class="<?php echo esc_attr($swatch); ?>" aria-label="<?php echo $swatch !== '' ? esc_attr($swatch) : esc_attr__('默认', 'aksrt'); ?>" aria-pressed="<?php echo $swatch === $class ? 'true' : 'false'; ?>"></button>
<?php endforeach; ?>
</div>
<input type="hidden" name="<?php echo esc_attr($name); ?>[class]" value="<?php echo esc_attr($class); ?>" data-group-class-input>
<p class="description"><?php esc_html_e('按钮图标、文案、颜色默认均会自动获取,建议为空即可。', 'aksrt'); ?></p>
</div>
</div>
</div>
<?php
}
+1
View File
@@ -186,6 +186,7 @@ class Setup {
'extract_code' => ['type' => 'string'],
'copy_key' => ['type' => 'string'],
'copy_val' => ['type' => 'string'],
'icon' => ['type' => 'string'],
'class' => ['type' => 'string'],
],
],
+22 -16
View File
@@ -333,10 +333,7 @@ if ($is_need_purchase && $user_amount > 0) {
<?php else : ?>
<ul class="resource-download-box__list">
<?php foreach ($downloads as $idx => $row) :
$label = isset($row['label']) ? (string) $row['label'] : '';
if ($label === '') {
$label = sprintf(__('下载源 %d', 'aksrt'), $idx + 1);
}
$label = isset($row['label']) ? trim((string) $row['label']) : '';
$type = isset($row['type']) ? (string) $row['type'] : 'direct';
$url = isset($row['url']) ? (string) $row['url'] : '';
if ($url === '') {
@@ -345,47 +342,56 @@ if ($is_need_purchase && $user_amount > 0) {
}
$extract_code = isset($row['extract_code']) ? trim((string) $row['extract_code']) : '';
$has_extract = $extract_code !== '';
$copy_key = isset($row['copy_key']) ? trim((string) $row['copy_key']) : '';
$copy_val = isset($row['copy_val']) ? trim((string) $row['copy_val']) : '';
$icon = isset($row['icon']) ? trim((string) $row['icon']) : '';
$button_class = isset($row['class']) ? trim((string) $row['class']) : '';
$has_copy = $copy_val !== '';
$service = function_exists('aksrt_detect_cloud_service') ? aksrt_detect_cloud_service($url) : ['key' => 'other', 'name' => $label, 'color' => '#6b7280'];
if (strpos($label, '下载源') === 0 && $service['key'] !== 'other') {
$label = $service['name'];
if ($label === '') {
$label = strpos($service['name'], '本地') === 0 ? sprintf(__('下载源 %d', 'aksrt'), $idx + 1) : (string) $service['name'];
}
$icon_markup = $icon !== '' && function_exists('aksrt_icon_markup') ? aksrt_icon_markup($icon) : '';
if ($is_need_login) {
$btn_href = $login_url;
$btn_text = __('登录后下载', 'aksrt');
$btn_icon = 'fa-sign-in-alt';
$btn_icon = $icon_markup !== '' ? $icon_markup : '<i class="fa fa-sign-in-alt" aria-hidden="true"></i>';
$btn_extra_class = '';
$btn_modifier = ' resource-download-box__btn--login';
$btn_target = '';
$btn_rel = '';
} else {
$btn_href = $url;
$btn_text = __('立即下载', 'aksrt');
$btn_icon = 'fa-download';
$btn_modifier = $has_paid_access ? ' resource-download-box__btn--owned' : '';
$btn_text = $label;
$btn_icon = $icon_markup !== '' ? $icon_markup : '<i class="fa fa-download" aria-hidden="true"></i>';
$btn_extra_class = $button_class !== '' ? ' ' . $button_class : '';
$btn_modifier = ($has_paid_access ? ' resource-download-box__btn--owned' : '') . ($button_class !== '' ? ' resource-download-box__btn--custom' : '');
$btn_target = ' target="_blank"';
$btn_rel = ' rel="nofollow noopener"';
}
?>
<li class="resource-download-box__item">
<a
class="resource-download-box__btn btn-primary resource-download-box__btn--service-<?php echo esc_attr($service['key']); ?><?php echo esc_attr($btn_modifier); ?>"
class="resource-download-box__btn btn-primary resource-download-box__btn--service-<?php echo esc_attr($service['key']); ?><?php echo esc_attr($btn_modifier); ?><?php echo esc_attr($btn_extra_class); ?>"
href="<?php echo esc_url($btn_href); ?>"<?php echo $btn_target; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo $btn_rel; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
data-source-index="<?php echo esc_attr((string) $idx); ?>"
data-source-type="<?php echo esc_attr($type); ?>"
style="--service-color: <?php echo esc_attr($service['color']); ?>"
>
<i class="fa <?php echo esc_attr($btn_icon); ?>" aria-hidden="true"></i>
<?php echo $btn_icon; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<span><?php echo esc_html($btn_text); ?></span>
<span class="resource-download-box__service-badge" style="background: <?php echo esc_attr($service['color']); ?>">
<?php echo esc_html($service['name']); ?>
</span>
</a>
<?php if ($has_extract) : ?>
<?php if ($has_extract || $has_copy) : ?>
<button type="button" class="resource-download-box__btn resource-download-box__btn--extract"
data-clipboard-tag="<?php esc_attr_e('提取码', 'aksrt'); ?>"
data-clipboard-text="<?php echo esc_attr($extract_code); ?>"
><?php echo esc_html($extract_code); ?></button>
data-clipboard-tag="<?php echo esc_attr($has_copy ? ($copy_key !== '' ? $copy_key : __('内容', 'aksrt')) : __('提取码', 'aksrt')); ?>"
data-clipboard-text="<?php echo esc_attr($has_copy ? $copy_val : $extract_code); ?>"
><?php echo esc_html($has_extract ? $extract_code : ($copy_key !== '' ? $copy_key : __('复制内容', 'aksrt'))); ?></button>
<?php endif; ?>
</li>
<?php endforeach; ?>
@@ -42,8 +42,15 @@ test('the download box renders direct anchors with inline extract codes', () =>
assert.match(box, /\$btn_rel = ' rel="nofollow noopener"'/);
// Extract codes surface inline next to the anchor, no popup involved.
assert.match(box, /if \(\$has_extract\) : \?>/);
assert.match(box, /data-clipboard-text="<\?php echo esc_attr\(\$extract_code\); \?>/);
assert.match(box, /if \(\$has_extract \|\| \$has_copy\) : \?>/);
assert.match(box, /\$has_copy = \$copy_val !== '';/);
assert.match(box, /data-clipboard-text="<\?php echo esc_attr\(\$has_copy \? \$copy_val : \$extract_code\); \?>/);
assert.match(box, /data-clipboard-tag="<\?php echo esc_attr\(\$has_copy \?/);
// Custom icon / label / color from the pay meta feed the anchor.
assert.match(box, /aksrt_icon_markup\(\$icon\)/);
assert.match(box, /\$btn_text = \$label;/);
assert.match(box, /resource-download-box__btn--custom/);
// The login gate keeps its own anchor.
assert.match(box, /\$btn_href = \$login_url;/);
@@ -105,6 +105,51 @@ test('front submit paid resources use the reference modal editor and compact lis
assert.doesNotMatch(downloads, /shop|forum|bbs_/i);
});
test('the post editor resource row mirrors the reference seven fields with icon support', () => {
const meta = read('src/Core/ResourceMetaBox.php');
const model = read('src/Core/DomainModel.php');
const setup = read('src/Core/Setup.php');
['下载地址', '资源备注', '复制的名称', '复制的内容', '自定义按钮图标', '自定义按钮文案', '自定义按钮颜色'].forEach((label) => {
assert.ok(meta.includes(label), `missing label: ${label}`);
});
// copy_key / copy_val / class must be real inputs, not hidden-only leftovers.
['url', 'extract_code', 'copy_key', 'copy_val', 'icon', 'label', 'class'].forEach((field) => {
assert.match(meta, new RegExp(`\\[${field}\\]`));
assert.match(model, new RegExp(`'${field}'`));
});
// copy_key / copy_val must be real visible inputs, not hidden-only leftovers.
// The color field legitimately keeps a hidden store behind the swatch buttons.
assert.doesNotMatch(meta, /type="hidden"[^>]*name="[^"]*\[(?:copy_key|copy_val)\]"/);
// Accordion + drag reorder affordances.
assert.match(meta, /data-group-toggle/);
assert.match(meta, /data-group-drag/);
assert.match(meta, /aksrt-icon-picker_assets\(|aksrt_icon_picker_field\(/);
// The swatch palette and the sanitizer share one class whitelist.
assert.match(meta, /DomainModel::DOWNLOAD_BUTTON_CLASSES/);
assert.match(model, /const DOWNLOAD_BUTTON_CLASSES/);
const js = read('assets/js/admin-pay-meta-box.js');
assert.match(js, /dragstart/);
assert.match(js, /dragover/);
assert.match(js, /reindexRows/);
// Reindexing rewrites the row index for every field and is scoped to its own
// group, so dragging a download row can never renumber attributes/episodes.
const reindex = (group, name, index) =>
name.replace(new RegExp(`aksrt_pay\\[${group}\\]\\[\\d+\\]`), `aksrt_pay[${group}][${index}]`);
['url', 'label', 'extract_code', 'copy_key', 'copy_val', 'icon', 'class'].forEach((field) => {
assert.equal(reindex('downloads', `aksrt_pay[downloads][0][${field}]`, 3), `aksrt_pay[downloads][3][${field}]`);
});
assert.equal(reindex('downloads', 'aksrt_pay[downloads][5][url]', 1), 'aksrt_pay[downloads][1][url]');
assert.equal(reindex('downloads', 'aksrt_pay[attributes][0][key]', 9), 'aksrt_pay[attributes][0][key]');
assert.equal(reindex('downloads', 'aksrt_pay[video_episode][1][title]', 9), 'aksrt_pay[video_episode][1][title]');
assert.match(setup, /'icon' => \['type' => 'string'\]/);
});
test('front submit paid-content prices and switch use the reference alignment rules', () => {
const pay = read('templates/parts/submit/sidebar-pay.php');
const submitCss = read('assets/css/frontend/pages/submit.css');