From 5732be847e2e5effc2f22a3b7097b0ad015e0f87 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 30 Oct 2026 19:10:13 +0800 Subject: [PATCH] fix(admin): keep invite filters inside the list card Unify status filters, search, bulk actions and pagination in one invite list card. Remove the standalone filter summary and preserve usable search widths on small screens, with structural regression coverage. Co-Authored-By: Claude Fable 5 --- assets/css/admin/invite-codes.css | 11 ++++-- src/Admin/Native/UsersMenusInviteCode.php | 33 ++++++++--------- .../invite-admin-contract.test.cjs | 37 +++++++++++++++++++ 3 files changed, 60 insertions(+), 21 deletions(-) diff --git a/assets/css/admin/invite-codes.css b/assets/css/admin/invite-codes.css index c786c935..5efd51b7 100644 --- a/assets/css/admin/invite-codes.css +++ b/assets/css/admin/invite-codes.css @@ -2,10 +2,13 @@ .aksrt-inv-page [hidden] { display: none !important; } .aksrt-inv-page .aksrt-native-header__actions .button { display: inline-flex; align-items: center; gap: 5px; } .aksrt-inv-page .aksrt-native-header__actions .dashicons { margin: 0; font-size: 17px; line-height: 20px; } +.aksrt-inv-toolbar { padding: 14px 20px; border-bottom: 1px solid var(--an-border-soft); } +.aksrt-inv-toolbar__views { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; } .aksrt-inv-toolbar .subsubsub { margin: 0; padding: 0; box-shadow: none; } .aksrt-inv-toolbar .aksrt-native-filters { margin: 0; } -.aksrt-inv-filter-summary { display: flex; flex-wrap: wrap; gap: 8px 16px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--an-border-soft); overflow-wrap: anywhere; } -.aksrt-inv-selection { color: var(--an-text-2); font-size: 12px; font-variant-numeric: tabular-nums; } +.aksrt-inv-filter-remark { max-width: 240px; color: var(--an-text-2); font-size: 12px; overflow-wrap: anywhere; } +.aksrt-inv-bulk { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 12px 20px; border-bottom: 1px solid var(--an-border-soft); } +.aksrt-inv-selection { margin-right: auto; color: var(--an-text-2); font-size: 12px; font-variant-numeric: tabular-nums; } .aksrt-inv-page .aksrt-inv-table { min-width: 1000px; table-layout: fixed; } .aksrt-inv-table .column-code { width: 21%; } @@ -90,8 +93,10 @@ html.aksrt-inv-modal-open { overflow: hidden; } @media (max-width: 600px) { .aksrt-inv-page .aksrt-native-header__actions { width: 100%; } .aksrt-inv-page .aksrt-native-header__actions .button { flex: 1; justify-content: center; } + .aksrt-inv-toolbar, + .aksrt-inv-bulk { padding-left: 14px; padding-right: 14px; } .aksrt-inv-toolbar .aksrt-native-filters { width: 100%; } - .aksrt-inv-toolbar input[type="search"] { flex: 1; width: 0; min-width: 0; } + .aksrt-inv-toolbar input[type="search"] { flex: 1 1 calc(100% - 76px); width: 0; min-width: 0; } .aksrt-inv-dialog { max-height: calc(100dvh - 24px); } .aksrt-inv-dialog__head, .aksrt-inv-dialog__body, diff --git a/src/Admin/Native/UsersMenusInviteCode.php b/src/Admin/Native/UsersMenusInviteCode.php index 07a1a0a5..86ffc55f 100644 --- a/src/Admin/Native/UsersMenusInviteCode.php +++ b/src/Admin/Native/UsersMenusInviteCode.php @@ -81,40 +81,38 @@ trait UsersMenusInviteCode { $filters = ['status' => $status, 's' => $s, 'remark' => $remark, 'orderby' => $data['orderby'], 'desc' => $data['desc']]; $filter_url = add_query_arg($filters, $this_url); - NativeUi::card_open('', '', false, 'aksrt-inv-toolbar'); - echo '
    '; + NativeUi::card_open('', '', true, 'aksrt-inv-list-card'); + echo '
    '; + echo '

    ' . esc_html__('邀请码列表', 'aksrt') . '

    '; + echo '
      '; foreach (['all' => __('全部', 'aksrt'), 'unused' => __('未使用', 'aksrt'), 'used' => __('已使用', 'aksrt')] as $key => $label) { $url = add_query_arg('status', $key, $filter_url); echo '
    • ' . esc_html($label) . '
    • '; } - echo '
    '; + echo '
'; echo ''; foreach (array_diff_key($filters, ['s' => '']) as $key => $value) { echo ''; } echo ''; echo ''; - echo ''; - if ($s !== '' || $remark !== '' || $status !== 'all') { - echo '
'; - if ($s !== '') { - echo '' . esc_html(sprintf(__('关键词:%s', 'aksrt'), $s)) . ''; - } - if ($remark !== '') { - echo '' . esc_html(sprintf(__('标识:%s', 'aksrt'), $remark)) . ''; - } - echo '' . esc_html__('清除筛选', 'aksrt') . '
'; + echo ''; + if ($remark !== '') { + echo '' . esc_html(sprintf(__('标识:%s', 'aksrt'), $remark)) . ''; } - NativeUi::card_close(); + if ($s !== '' || $remark !== '' || $status !== 'all') { + echo '' . esc_html__('清除筛选', 'aksrt') . ''; + } + echo ''; echo '
'; echo ''; - $bulk = '
' + echo '
' . '' . '' . '
'; - NativeUi::table_card_open(__('邀请码列表', 'aksrt'), $bulk); + echo '
'; echo ''; $columns = [ @@ -171,8 +169,7 @@ trait UsersMenusInviteCode { 'type' => 'plain', 'prev_text' => '«', 'next_text' => '»', ]); } - echo ''; + echo ''; NativeUi::card_close(); - echo ''; } } diff --git a/tests/widget-system/invite-admin-contract.test.cjs b/tests/widget-system/invite-admin-contract.test.cjs index 8e5ef471..01cb6ce6 100644 --- a/tests/widget-system/invite-admin-contract.test.cjs +++ b/tests/widget-system/invite-admin-contract.test.cjs @@ -40,6 +40,43 @@ function hasBooleanAttr(tag, name) { assert.match(tag, new RegExp(`(?:\\s|^)${name}(?:\\s|>|$)`)); } +test('filters, selection, table and pagination share one list card', () => { + const { html } = harness(); + const cards = [...html.matchAll(/]*class="[^"]*\baksrt-native-card\b[^"]*"[^>]*>[\s\S]*?<\/section>/g)]; + assert.equal(cards.length, 1); + const card = cards[0][0]; + assert.match(card, /aksrt-inv-list-card/); + assert.match(card, /

邀请码列表<\/h2>/); + for (const marker of ['class="subsubsub"', 'class="aksrt-native-filters"', '清除筛选', 'class="aksrt-inv-bulk"', 'data-aksrt-inv-selection', 'aksrt-inv-table', 'class="tablenav bottom"']) { + assert.ok(card.includes(marker), `${marker} belongs to the list card`); + } + assert.doesNotMatch(html, /aksrt-inv-filter-summary|关键词:/); + assert.doesNotMatch(read('assets', 'css', 'admin', 'invite-codes.css'), /aksrt-inv-filter-summary/); +}); + +test('search and bulk actions remain independent sibling forms inside the list card', () => { + const { html } = harness(); + let depth = 0; + let count = 0; + for (const [tag] of html.matchAll(/<\/?form\b[^>]*>/g)) { + if (tag.startsWith('= 0 && depth <= 1, 'forms must never nest'); + } + assert.equal(depth, 0); + assert.equal(count, 4); + const searchEnd = html.indexOf('', html.indexOf('class="aksrt-native-filters"')); + const listStart = html.lastIndexOf('', listStart); + assert.ok(html.indexOf('class="tablenav bottom"', listStart) < listEnd); + assert.ok(listEnd < html.indexOf('', listStart)); +}); + test('page renders add and export dialogs beside, not inside, the list form', () => { const { html } = harness(); const listMarker = html.indexOf('data-aksrt-inv-list');

'; echo '