fix(payment): 卡密管理页标题按钮单点输出并修正排序箭头下沉
- 标题右侧按钮原在页面头部与列表渲染函数各输出一次(实际渲染 3 个 page-title-action,「添加卡密」重复出现),改为只在页面头部的 tab switch 中 拼一次;render_list() 收为 render_list(string $s),只渲染过滤链接与表格, 搜索词在页面头部单次解析后传入,搜索提示仍按参考主题跟在按钮之后。 - .orderby-but 恢复参考主题写法(position: relative,不设 display)。 箭头是绝对定位元素且不写 top/left,位置取自容器的静态位置;容器一旦变成零 高度 inline-block,基线会退化为自身下边缘并与文字基线对齐,整组箭头下沉一 个字高。同时补回参考主题的 padding-right: 1em,并加注释锁定该约束。 - 清理死代码:行循环中 custom 分支的 $type_html 赋值总被下方类型链覆盖,已删除。 - 新增 tests/admin/charge-cards-page-contract.test.cjs(4 例)作为回归护栏, 并按 .gitignore 既有白名单惯例放开该文件。
This commit is contained in:
@@ -55,6 +55,9 @@
|
||||
!/tests/widget-system/circular-button-icon-margin-contract.test.cjs
|
||||
!/tests/widget-system/single-cover-height-contract.test.cjs
|
||||
!/tests/widget-system/frontend-lifecycle-contract.test.cjs
|
||||
!/tests/admin/
|
||||
/tests/admin/*
|
||||
!/tests/admin/charge-cards-page-contract.test.cjs
|
||||
!/tests/front-submit/
|
||||
/tests/front-submit/*
|
||||
!/tests/front-submit/video-cover-contract.test.cjs
|
||||
|
||||
@@ -51,8 +51,9 @@ class PayChargeCards {
|
||||
}
|
||||
|
||||
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : '';
|
||||
$s = isset($_REQUEST['s']) ? sanitize_text_field((string) wp_unslash($_REQUEST['s'])) : '';
|
||||
$page_title = __('卡密管理', 'aksrt');
|
||||
$head_but = '<a href="' . esc_url(add_query_arg('tab', 'add', $this->page_url)) . '" class="page-title-action">' . esc_html__('添加卡密', 'aksrt') . '</a>';
|
||||
$head_but = '';
|
||||
|
||||
// 页面级动作(参考主题在渲染前处理 add / delete)。
|
||||
$action = isset($_REQUEST['action']) ? sanitize_key((string) wp_unslash($_REQUEST['action'])) : '';
|
||||
@@ -62,6 +63,7 @@ class PayChargeCards {
|
||||
$this->handle_delete();
|
||||
}
|
||||
|
||||
// 标题右侧按钮是页面头部的一部分,只在此处输出一次(列表页同样如此,由 render_list 负责列表本体)。
|
||||
switch ($tab) {
|
||||
case 'add':
|
||||
$page_title = __('添加卡密', 'aksrt');
|
||||
@@ -72,6 +74,14 @@ class PayChargeCards {
|
||||
$page_title = __('导出卡密', 'aksrt');
|
||||
$head_but = '<a href="' . esc_url($this->page_url) . '" class="page-title-action">' . esc_html__('返回列表', 'aksrt') . '</a>';
|
||||
break;
|
||||
|
||||
default:
|
||||
$head_but = '<a href="' . esc_url(add_query_arg('tab', 'add', $this->page_url)) . '" class="page-title-action">' . esc_html__('添加卡密', 'aksrt') . '</a>';
|
||||
$head_but .= '<a href="' . esc_url(add_query_arg('tab', 'export', $this->page_url)) . '" class="page-title-action">' . esc_html__('导出卡密', 'aksrt') . '</a>';
|
||||
if ($s !== '') {
|
||||
$head_but .= '<div><div class="update-nag" style="margin: 10px 0 0;">' . sprintf(esc_html__('搜索 “%s” 的内容', 'aksrt'), esc_html($s)) . ' </div></div>';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
echo '<div class="wrap">';
|
||||
@@ -84,7 +94,7 @@ class PayChargeCards {
|
||||
} elseif ($tab === 'export') {
|
||||
$this->render_export_form();
|
||||
} else {
|
||||
$this->render_list();
|
||||
$this->render_list($s);
|
||||
}
|
||||
|
||||
echo '</div>';
|
||||
@@ -94,10 +104,12 @@ class PayChargeCards {
|
||||
* 列表
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
private function render_list(): void {
|
||||
/**
|
||||
* @param string $s 搜索词(由页面头部统一解析后传入,用于提示、过滤链接与查询)。
|
||||
*/
|
||||
private function render_list(string $s): void {
|
||||
$svc = \AKSRT\Payment\ChargeCard::get_instance();
|
||||
|
||||
$s = isset($_REQUEST['s']) ? sanitize_text_field((string) wp_unslash($_REQUEST['s'])) : '';
|
||||
$status = isset($_GET['status']) ? sanitize_key((string) wp_unslash($_GET['status'])) : '';
|
||||
$type = isset($_GET['type']) ? sanitize_key((string) wp_unslash($_GET['type'])) : '';
|
||||
$other = isset($_GET['other']) ? sanitize_text_field((string) wp_unslash($_GET['other'])) : '';
|
||||
@@ -116,15 +128,7 @@ class PayChargeCards {
|
||||
$type = '';
|
||||
}
|
||||
|
||||
// 标题右侧按钮与过滤链接。
|
||||
$head_but = '';
|
||||
if ($s !== '') {
|
||||
$head_but .= '<div><div class="update-nag" style="margin: 10px 0 0;">' . sprintf(esc_html__('搜索 “%s” 的内容', 'aksrt'), esc_html($s)) . ' </div></div>';
|
||||
} else {
|
||||
$head_but .= '<a href="' . esc_url(add_query_arg('tab', 'add', $this->page_url)) . '" class="page-title-action">' . esc_html__('添加卡密', 'aksrt') . '</a>';
|
||||
$head_but .= '<a href="' . esc_url(add_query_arg('tab', 'export', $this->page_url)) . '" class="page-title-action">' . esc_html__('导出卡密', 'aksrt') . '</a>';
|
||||
}
|
||||
|
||||
// 过滤链接(标题右侧按钮与搜索提示由页面头部统一输出)。
|
||||
$sub_but = [
|
||||
['name' => __('全部', 'aksrt'), 'href' => $this->page_url],
|
||||
];
|
||||
@@ -148,7 +152,6 @@ class PayChargeCards {
|
||||
</div>
|
||||
</form>';
|
||||
|
||||
echo $head_but; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。
|
||||
echo '<div class="order-header"><ul class="subsubsub">' . $but_html . '</ul>' . $search . '</div>';
|
||||
|
||||
$result = $svc->get_list([
|
||||
@@ -203,10 +206,9 @@ class PayChargeCards {
|
||||
$meta = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
// 自定义卡密不展示状态;其余类型按状态着色(类型链接统一在下方链表中生成)。
|
||||
$status_html = '';
|
||||
if ($msg['type'] === 'custom') {
|
||||
$type_html = '<a style="color:#a42ade" href="' . esc_url(add_query_arg('type', 'custom', $this->page_url)) . '">' . esc_html__('自定义卡密', 'aksrt') . '</a>';
|
||||
} else {
|
||||
if ($msg['type'] !== 'custom') {
|
||||
$status_html = '<span style="color: #3d7ffd;">' . esc_html__('未使用', 'aksrt') . '</span>';
|
||||
if ($msg['status'] === 'used') {
|
||||
$status_html = '<span style="color: #f93b3b;">' . esc_html__('已使用', 'aksrt') . '</span>';
|
||||
@@ -1029,8 +1031,11 @@ class PayChargeCards {
|
||||
<style>
|
||||
.order-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; margin-top: 8px; }
|
||||
.order-header .subsubsub { margin: 6px 0; }
|
||||
.orderby-but { position: relative; display: inline-block; margin-left: 4px; }
|
||||
.orderby-but > a { opacity: .4; position: absolute; transform: translateY(-3px); transition: .3s; }
|
||||
/* 排序箭头:容器必须保持 inline(不要设 display / margin)。
|
||||
箭头是绝对定位元素,其静态位置取自容器的行盒顶部;容器一旦变成零高度的 inline-block,
|
||||
基线会退化为自身下边缘并与文字基线对齐,箭头整体下沉一个字高、脱离表头文字。 */
|
||||
.orderby-but { position: relative; }
|
||||
.orderby-but > a { opacity: .4; position: absolute; transform: translateY(-3px); transition: .3s; padding-right: 1em; }
|
||||
.orderby-but > a + a { transform: translateY(6px); }
|
||||
.orderby-but:hover a { opacity: .6; }
|
||||
.orderby-but > a:hover { opacity: 1; }
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* 「卡密管理」后台页契约:
|
||||
* 1) 标题右侧按钮(page-title-action)由页面头部唯一输出,列表渲染不得再输出一次;
|
||||
* 2) 表头排序箭头宿主(.orderby-but)必须保持 inline,不能改成零高度 inline-block,
|
||||
* 否则绝对定位箭头的静态位置会落到文字基线上,整组箭头下沉一行字高。
|
||||
*/
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..');
|
||||
const page = fs.readFileSync(path.join(root, 'src/Admin/Native/PayChargeCards.php'), 'utf8');
|
||||
|
||||
const count = (src, pattern) => (src.match(pattern) || []).length;
|
||||
|
||||
const header = page.slice(
|
||||
page.indexOf('public function render_charge_cards_page('),
|
||||
page.indexOf('private function render_list(')
|
||||
);
|
||||
const list = page.slice(
|
||||
page.indexOf('private function render_list('),
|
||||
page.indexOf('private function render_add_form(')
|
||||
);
|
||||
|
||||
test('标题右侧按钮只在页面头部输出一次', () => {
|
||||
assert.ok(header.length > 0 && list.length > 0);
|
||||
|
||||
// 添加/导出两个 tab 的「返回列表」各一次,列表页的「添加卡密 / 导出卡密」各一次。
|
||||
assert.equal(count(header, /class="page-title-action"/g), 4);
|
||||
assert.doesNotMatch(list, /class="page-title-action"/);
|
||||
assert.equal(count(page, /class="page-title-action"/g), count(header, /class="page-title-action"/g));
|
||||
|
||||
// 列表页的两个入口按钮各自只有一处定义。
|
||||
assert.equal(count(page, /add_query_arg\('tab', 'add'/g), 1);
|
||||
assert.equal(count(page, /add_query_arg\('tab', 'export'/g), 1);
|
||||
assert.match(header, /esc_html__\('添加卡密', 'aksrt'\)/);
|
||||
assert.match(header, /esc_html__\('导出卡密', 'aksrt'\)/);
|
||||
});
|
||||
|
||||
test('搜索提示仍由页面头部输出且列表不再自建头部', () => {
|
||||
assert.equal(count(header, /update-nag/g), 1);
|
||||
assert.match(header, /搜索 “%s” 的内容/);
|
||||
assert.doesNotMatch(list, /update-nag/);
|
||||
assert.doesNotMatch(list, /\$head_but/);
|
||||
|
||||
// 搜索词在页面头部解析后传入列表,避免两处各解析一次。
|
||||
assert.match(page, /render_list\(\$s\)/);
|
||||
assert.match(list, /private function render_list\(string \$s\): void/);
|
||||
});
|
||||
|
||||
test('排序箭头宿主保持 inline,不使用零高度 inline-block 定位', () => {
|
||||
const host = page.match(/\.orderby-but \{[^}]*\}/);
|
||||
assert.ok(host, '.orderby-but 规则缺失');
|
||||
assert.match(host[0], /position:\s*relative/);
|
||||
assert.doesNotMatch(host[0], /display\s*:/);
|
||||
assert.doesNotMatch(host[0], /margin-left/);
|
||||
|
||||
const arrow = page.match(/\.orderby-but > a \{[^}]*\}/);
|
||||
assert.ok(arrow, '.orderby-but > a 规则缺失');
|
||||
assert.match(arrow[0], /position:\s*absolute/);
|
||||
assert.match(arrow[0], /padding-right:\s*1em/);
|
||||
|
||||
assert.match(page, /\.orderby-but > a \+ a \{ transform: translateY\(6px\); \}/);
|
||||
assert.match(header + list, /<span class="orderby-but">/);
|
||||
});
|
||||
|
||||
test('类型列链接每种类型只生成一次', () => {
|
||||
assert.equal(count(page, /\$type_html = '<a/g), 4);
|
||||
assert.equal(count(page, /add_query_arg\('type', 'custom'/g), 1);
|
||||
assert.match(list, /if \(\$msg\['type'\] !== 'custom'\) \{/);
|
||||
assert.doesNotMatch(list, /\$msg\['type'\] === 'custom'\)\s*\{\s*\$type_html/);
|
||||
});
|
||||
Reference in New Issue
Block a user