From 18951b01d67c3126c37fb9d01292aa7aaa3d9352 Mon Sep 17 00:00:00 2001 From: unknown Date: Fri, 30 Oct 2026 15:05:38 +0800 Subject: [PATCH] feat(admin): unify native admin pages on a shared modern UI Add NativeUi helper + native-ui.css (tokens mirror the SPA Naive UI theme) and move the pay center, orders, products, coupons, charge cards, income, withdrawals, tickets, invite codes, VIP, verify, messages and report/ban pages onto shared header, stat cards, cards, filters, badges and empty states. Remove inline styles and dead helpers; split PayChargeCards into sibling traits to stay under the file size limit. Co-Authored-By: Claude Fable 5 --- assets/css/admin/invite-codes.css | 49 ++ assets/css/admin/messages.css | 157 +--- assets/css/admin/native-ui.css | 573 +++++++++++++ assets/css/admin/tickets.css | 208 ++--- src/Admin/Native/NativeUi.php | 251 ++++++ src/Admin/Native/PayChargeCards.php | 801 +----------------- src/Admin/Native/PayChargeCardsAssets.php | 143 ++++ src/Admin/Native/PayChargeCardsForms.php | 436 ++++++++++ src/Admin/Native/PayChargeCardsList.php | 241 ++++++ src/Admin/Native/PayCoupons.php | 196 +++-- src/Admin/Native/PayIncome.php | 79 +- src/Admin/Native/PayMenus.php | 70 +- src/Admin/Native/PayOrders.php | 144 ++-- src/Admin/Native/PayProducts.php | 59 +- src/Admin/Native/PayWithdrawals.php | 74 +- src/Admin/Native/UsersMenus.php | 14 +- src/Admin/Native/UsersMenusInviteCode.php | 199 +++-- src/Admin/Native/UsersMenusMessages.php | 90 +- src/Admin/Native/UsersMenusTickets.php | 92 +- src/Admin/Native/UsersMenusTicketsCenter.php | 21 +- src/Admin/Native/UsersMenusTicketsDetail.php | 8 +- src/Admin/Native/UsersMenusTicketsList.php | 56 +- src/Admin/Native/UsersMenusVerify.php | 39 +- src/Admin/Native/UsersMenusVip.php | 41 +- tests/admin/charge-cards-dependency.test.cjs | 15 +- .../charge-cards-form-constraints.test.cjs | 10 +- .../admin/charge-cards-page-contract.test.cjs | 10 +- tests/admin/charge-cards-pagination.test.cjs | 10 +- tests/admin/pay-pages-pagination.test.cjs | 10 +- 29 files changed, 2503 insertions(+), 1593 deletions(-) create mode 100644 assets/css/admin/invite-codes.css create mode 100644 assets/css/admin/native-ui.css create mode 100644 src/Admin/Native/NativeUi.php create mode 100644 src/Admin/Native/PayChargeCardsAssets.php create mode 100644 src/Admin/Native/PayChargeCardsForms.php create mode 100644 src/Admin/Native/PayChargeCardsList.php diff --git a/assets/css/admin/invite-codes.css b/assets/css/admin/invite-codes.css new file mode 100644 index 00000000..344153b5 --- /dev/null +++ b/assets/css/admin/invite-codes.css @@ -0,0 +1,49 @@ +/* + * 邀请码管理(users.php?page=aksrt_invite_codes)页面专属样式。 + * + * 共享外壳/卡片/表格/徽标由 native-ui.css 承载;这里只留列表排序箭头、 + * 添加/导出表单的行布局与按钮组切换、复制提示 toast 等本页独有交互样式。 + * 仅在 users_page_aksrt_invite_codes 入队,规则限定在 .aksrt-inv-page 下。 + */ + +/* 排序箭头:默认弱化,悬停加强。 */ +.aksrt-inv-page .orderby-but { position: relative; display: inline-block; margin-left: 4px; } +.aksrt-inv-page .orderby-but > a { opacity: .4; position: absolute; transform: translateY(-3px); transition: .3s; } +.aksrt-inv-page .orderby-but > a + a { transform: translateY(6px); } +.aksrt-inv-page .orderby-but:hover a { opacity: .6; } +.aksrt-inv-page .orderby-but > a:hover { opacity: 1; } + +/* 复制邀请码:指针反馈。 */ +.aksrt-inv-page .aksrt-inv-code { cursor: pointer; } + +/* 三视图切换链接(列表 / 添加 / 导出):贴合页头下方。 */ +.aksrt-inv-page .aksrt-inv-switch-views { margin: -8px 0 12px; display: flex; flex-wrap: wrap; gap: 6px; } +.aksrt-inv-page .aksrt-inv-switch-views .page-title-action { display: inline-flex; align-items: center; gap: 4px; } + +/* 选项字段布局:左标题右控件,行间分隔线。 */ +.aksrt-inv-page .aksrt-inv-field { display: flex; align-items: flex-start; padding: 14px 0; border-bottom: 1px solid var(--an-border-soft); } +.aksrt-inv-page .aksrt-inv-field:last-child { border-bottom: 0; } +.aksrt-inv-page .aksrt-inv-field > label { flex: 0 0 170px; color: var(--an-text); font-weight: 600; padding-top: 4px; } +.aksrt-inv-page .aksrt-inv-field > .aksrt-inv-control { flex: 1; min-width: 0; } +.aksrt-inv-page .aksrt-inv-field .description { display: block; margin-top: 4px; } + +/* 添加方式按钮组(二选一)。 */ +.aksrt-inv-page .aksrt-inv-btnset label { display: inline-block; margin-right: -1px; padding: 4px 14px; color: var(--an-text-2); background: #fff; border: 1px solid var(--an-border); cursor: pointer; } +.aksrt-inv-page .aksrt-inv-btnset label:first-child { border-radius: var(--an-radius-sm) 0 0 var(--an-radius-sm); } +.aksrt-inv-page .aksrt-inv-btnset label:last-child { border-radius: 0 var(--an-radius-sm) var(--an-radius-sm) 0; } +.aksrt-inv-page .aksrt-inv-btnset input { display: none; } +.aksrt-inv-page .aksrt-inv-btnset label.on { color: #fff; background: var(--an-primary); border-color: var(--an-primary); } + +/* 开关式复选 / 奖励字段行 / 单位后缀。 */ +.aksrt-inv-page .aksrt-inv-switch { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; } +.aksrt-inv-page .aksrt-inv-switch input { margin: 0; } +.aksrt-inv-page .aksrt-inv-rewards { display: flex; flex-wrap: wrap; gap: 12px; } +.aksrt-inv-page .aksrt-inv-radios { margin-top: 8px; } +.aksrt-inv-page .aksrt-inv-radios label + label { margin-left: 12px; font-weight: 400; } +.aksrt-inv-page .aksrt-inv-viptime { margin-top: 8px; } +.aksrt-inv-page .aksrt-inv-mini { width: 80px; } +.aksrt-inv-page .aksrt-inv-unit { margin-left: 4px; color: var(--an-muted); } +.aksrt-inv-page .aksrt-inv-textarea { resize: both; } + +/* 复制成功悬浮提示。 */ +.aksrt-inv-toast { position: fixed; top: 42px; left: 50%; transform: translateX(-50%); display: none; padding: 8px 22px; color: #fff; font-size: 13px; background: rgba(0, 0, 0, .75); border-radius: 4px; z-index: 100100; } diff --git a/assets/css/admin/messages.css b/assets/css/admin/messages.css index 9f6b89e1..5a5e0c3a 100644 --- a/assets/css/admin/messages.css +++ b/assets/css/admin/messages.css @@ -1,118 +1,21 @@ -/* 消息管理(users.php?page=aksrt_messages)——仅在本页入队 */ +/* 消息管理(users.php?page=aksrt_messages)——仅本页入队。 + * 页面外壳 / 概览 / 折叠卡 / 表单栅格 / 筛选条 / 空状态均走 native-ui.css 的共享类, + * 本文件只保留消息列表专属样式:收件人模式切换、类型徽章、读取状态、用户单元格、列宽。 */ -.aksrt-messages-admin { - --aksrt-msg-border: #dcdcde; - --aksrt-msg-muted: #646970; - --aksrt-msg-soft: #f6f7f7; - --aksrt-msg-accent: #2271b1; -} - -.aksrt-messages-admin__muted { color: var(--aksrt-msg-muted); } -.aksrt-messages-admin__sub { margin-top: 3px; color: var(--aksrt-msg-muted); font-size: 12px; } -.aksrt-messages-admin__sub code { padding: 0 4px; font-size: 12px; background: var(--aksrt-msg-soft); } - -/* ---- 概览 ---- */ -.aksrt-messages-admin__stats { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; - margin: 16px 0 0; - padding: 0; - list-style: none; -} -.aksrt-messages-admin__stat { - display: flex; - align-items: center; - gap: 12px; - margin: 0; - padding: 14px 16px; - background: #fff; - border: 1px solid var(--aksrt-msg-border); - border-radius: 4px; -} -.aksrt-messages-admin__stat > .dashicons { - flex: none; - width: 36px; - height: 36px; - font-size: 20px; - line-height: 36px; - text-align: center; - color: var(--aksrt-msg-tone, var(--aksrt-msg-accent)); - background: color-mix(in srgb, var(--aksrt-msg-tone, var(--aksrt-msg-accent)) 12%, #fff); - border-radius: 50%; -} -.aksrt-messages-admin__stat div { display: flex; flex-direction: column; min-width: 0; } -.aksrt-messages-admin__stat strong { font-size: 20px; line-height: 1.3; } -.aksrt-messages-admin__stat div span { color: var(--aksrt-msg-muted); } -.aksrt-messages-admin__stat.is-unread { --aksrt-msg-tone: #d63638; } -.aksrt-messages-admin__stat.is-today { --aksrt-msg-tone: #00a32a; } -.aksrt-messages-admin__stat.is-recipients { --aksrt-msg-tone: #8c5bd0; } - -/* ---- 代发 ---- */ -.aksrt-messages-admin__compose { - margin: 16px 0 8px; - background: #fff; - border: 1px solid var(--aksrt-msg-border); - border-radius: 4px; -} -.aksrt-messages-admin__compose > summary { - display: flex; - align-items: center; - gap: 6px; - padding: 10px 14px; - cursor: pointer; - font-weight: 600; - color: var(--aksrt-msg-accent); - list-style: none; -} -.aksrt-messages-admin__compose > summary::-webkit-details-marker { display: none; } -.aksrt-messages-admin__compose[open] > summary { border-bottom: 1px solid var(--aksrt-msg-border); } -.aksrt-messages-admin__compose-form { margin: 0; padding: 14px; } +.aksrt-messages-admin__muted { color: var(--an-muted); } +.aksrt-messages-admin__sub { margin-top: 3px; color: var(--an-muted); font-size: 12px; } +.aksrt-messages-admin__sub code { padding: 0 4px; font-size: 12px; background: var(--an-bg-soft); } +/* ---- 代发:收件人模式切换(纯 CSS,无 JS 时按所选模式显隐字段) ---- */ .aksrt-messages-admin__modes { display: flex; gap: 16px; margin: 0 0 12px; padding: 0; border: 0; } .aksrt-messages-admin__modes legend { float: left; margin-right: 8px; font-weight: 600; } - -.aksrt-messages-admin__grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px 16px; -} -.aksrt-messages-admin__grid label { display: flex; flex-direction: column; gap: 4px; } -.aksrt-messages-admin__grid label > span { font-weight: 600; } -.aksrt-messages-admin__grid label > small { color: var(--aksrt-msg-muted); } -.aksrt-messages-admin__grid .is-wide { grid-column: 1 / -1; } -.aksrt-messages-admin__grid input, -.aksrt-messages-admin__grid select, -.aksrt-messages-admin__grid textarea { width: 100%; max-width: none; } - -/* 收件人模式切换:纯 CSS,无 JS 时两组字段都可见、按所选模式生效。 */ -.aksrt-messages-admin__compose-form:has(input[name="recipient_mode"][value="users"]:checked) .is-role, -.aksrt-messages-admin__compose-form:has(input[name="recipient_mode"][value="role"]:checked) .is-users { display: none; } - -.aksrt-messages-admin__compose-foot { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin: 12px 0 0; -} - -/* ---- 筛选 ---- */ -.aksrt-messages-admin__filters { - clear: both; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - padding: 8px 0 0; -} -.aksrt-messages-admin__filters input[type="text"], -.aksrt-messages-admin__filters input[type="search"] { min-width: 200px; } +.aksrt-messages-admin__compose form:has(input[name="recipient_mode"][value="users"]:checked) .is-role, +.aksrt-messages-admin__compose form:has(input[name="recipient_mode"][value="role"]:checked) .is-users { display: none; } /* ---- 列表 ---- */ -.aksrt-messages-admin__danger.button { color: #b32d2e; border-color: #b32d2e; } +.aksrt-messages-admin__danger.button { color: var(--an-danger); border-color: var(--an-danger); } .aksrt-messages-admin__danger.button:hover, -.aksrt-messages-admin__danger.button:focus { color: #fff; background: #b32d2e; border-color: #b32d2e; } +.aksrt-messages-admin__danger.button:focus { color: #fff; background: var(--an-danger); border-color: var(--an-danger); } .aksrt-messages-admin__table .column-title { width: 40%; } .aksrt-messages-admin__table .column-type { width: 12%; } @@ -122,19 +25,10 @@ .aksrt-messages-admin__table td { vertical-align: middle; } .aksrt-messages-admin__table .row-title { word-break: break-word; } .aksrt-messages-admin__table tr.is-unread .row-title { font-weight: 700; } -.aksrt-messages-admin__table tr.is-unread > th:first-child { box-shadow: inset 3px 0 0 var(--aksrt-msg-accent); } -.aksrt-messages-admin__desc { margin-top: 3px; color: #3c434a; word-break: break-word; } - -.aksrt-messages-admin__empty { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - padding: 32px 0; - color: var(--aksrt-msg-muted); -} -.aksrt-messages-admin__empty .dashicons { width: 32px; height: 32px; font-size: 32px; opacity: .5; } +.aksrt-messages-admin__table tr.is-unread > th:first-child { box-shadow: inset 3px 0 0 var(--an-primary); } +.aksrt-messages-admin__desc { margin-top: 3px; color: var(--an-text-2); word-break: break-word; } +/* 类型徽章(可点击筛选,按类型着色) */ .aksrt-msg-badge { display: inline-flex; align-items: center; @@ -144,32 +38,29 @@ line-height: 18px; white-space: nowrap; text-decoration: none; - color: var(--aksrt-msg-tone, #646970); - background: color-mix(in srgb, var(--aksrt-msg-tone, #646970) 12%, #fff); - border: 1px solid color-mix(in srgb, var(--aksrt-msg-tone, #646970) 30%, #fff); + color: var(--aksrt-msg-tone, var(--an-muted)); + background: color-mix(in srgb, var(--aksrt-msg-tone, var(--an-muted)) 12%, #fff); + border: 1px solid color-mix(in srgb, var(--aksrt-msg-tone, var(--an-muted)) 30%, #fff); } .aksrt-msg-badge:hover, -.aksrt-msg-badge:focus { color: var(--aksrt-msg-tone, #646970); border-color: var(--aksrt-msg-tone, #646970); } -.aksrt-msg-badge.is-info { --aksrt-msg-tone: #2271b1; } +.aksrt-msg-badge:focus { color: var(--aksrt-msg-tone, var(--an-muted)); border-color: var(--aksrt-msg-tone, var(--an-muted)); } +.aksrt-msg-badge.is-info { --aksrt-msg-tone: var(--an-info); } .aksrt-msg-badge.is-success { --aksrt-msg-tone: #00a32a; } -.aksrt-msg-badge.is-danger { --aksrt-msg-tone: #d63638; } +.aksrt-msg-badge.is-danger { --aksrt-msg-tone: var(--an-danger); } .aksrt-msg-badge.is-social { --aksrt-msg-tone: #8c5bd0; } +/* 读取状态(带阅读时间 title 提示) */ .aksrt-msg-state { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; white-space: nowrap; } .aksrt-msg-state::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; } -.aksrt-msg-state.is-unread { color: var(--aksrt-msg-accent); font-weight: 600; } -.aksrt-msg-state.is-read { color: var(--aksrt-msg-muted); } +.aksrt-msg-state.is-unread { color: var(--an-primary); font-weight: 600; } +.aksrt-msg-state.is-read { color: var(--an-muted); } +/* 用户单元格(点击按该用户筛选) */ .aksrt-msg-user { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; min-width: 0; text-decoration: none; } .aksrt-msg-user img { flex: none; border-radius: 50%; } .aksrt-msg-user span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -@media (max-width: 1100px) { - .aksrt-messages-admin__stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } -} @media (max-width: 782px) { - .aksrt-messages-admin__grid { grid-template-columns: minmax(0, 1fr); } - .aksrt-messages-admin__compose-foot { flex-direction: column; align-items: flex-start; } .aksrt-messages-admin__table .column-type, .aksrt-messages-admin__table .column-read { display: none; } } diff --git a/assets/css/admin/native-ui.css b/assets/css/admin/native-ui.css new file mode 100644 index 00000000..6e46b34f --- /dev/null +++ b/assets/css/admin/native-ui.css @@ -0,0 +1,573 @@ +/* + * AKSRT 原生后台运营页共享 UI(支付中心 / 用户子菜单)。 + * + * 只在 NativeUi::SLUGS 列出的页面入队,全部规则限定在 .aksrt-native 下, + * 不影响 WP 其它后台页。色板与圆角对齐 SPA 的 Naive UI 主题(App.vue)。 + * 标记由 src/Admin/Native/NativeUi.php 输出。 + */ + +.aksrt-native { + --an-primary: #18a058; + --an-primary-hover: #36ad6a; + --an-primary-soft: #e8f6ee; + --an-info: #2080f0; + --an-warning: #f0a020; + --an-danger: #d03050; + --an-text: #1f2329; + --an-text-2: #4e5969; + --an-muted: #86909c; + --an-border: #e5e6eb; + --an-border-soft: #f0f1f4; + --an-bg-soft: #f7f8fa; + --an-card: #fff; + --an-radius: 12px; + --an-radius-sm: 8px; + --an-shadow: 0 1px 2px rgba(15, 23, 42, .04), 0 4px 16px rgba(15, 23, 42, .04); + --an-shadow-hover: 0 2px 4px rgba(15, 23, 42, .06), 0 8px 24px rgba(15, 23, 42, .08); + + margin-right: 20px; + color: var(--an-text-2); + font-size: 13px; +} + +/* ---------- 页头 ---------- */ +.aksrt-native-header { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 12px 16px; + margin: 18px 0 16px; +} +.aksrt-native-header__main { min-width: 0; } +.aksrt-native .aksrt-native-header__title { + display: flex; + align-items: center; + gap: 10px; + margin: 0; + padding: 0; + color: var(--an-text); + font-size: 22px; + font-weight: 600; + line-height: 1.3; +} +.aksrt-native-header__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + color: var(--an-primary); + background: var(--an-primary-soft); + border-radius: 10px; +} +.aksrt-native-header__icon.dashicons:before { font-size: 20px; line-height: 36px; } +.aksrt-native-header__icon.dashicons { width: 36px; height: 36px; } +.aksrt-native-header__desc { margin: 6px 0 0; color: var(--an-muted); } +.aksrt-native-header__actions { display: flex; flex-wrap: wrap; gap: 8px; } + +/* WP 把 .notice 挪到首个 h1/h2 后;保持与卡片同宽同圆角。 */ +.aksrt-native .notice { margin: 0 0 16px; border-radius: var(--an-radius-sm); box-shadow: var(--an-shadow); } + +/* ---------- 标签导航(.subsubsub / .nav-tab-wrapper / .aksrt-native-tabs) ---------- */ +.aksrt-native .subsubsub, +.aksrt-native-tabs { + display: inline-flex; + flex-wrap: wrap; + float: none; + gap: 4px; + margin: 0 0 16px; + padding: 4px; + color: var(--an-muted); + background: var(--an-card); + border-radius: 10px; + box-shadow: var(--an-shadow); + font-size: 13px; +} +.aksrt-native .subsubsub li { margin: 0; font-size: 0; } +.aksrt-native .subsubsub li > * { font-size: 13px; } +.aksrt-native .subsubsub a, +.aksrt-native-tabs a { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + color: var(--an-text-2); + border-radius: 7px; + text-decoration: none; + line-height: 1.4; + transition: background .15s, color .15s; +} +.aksrt-native .subsubsub a:hover, +.aksrt-native-tabs a:hover { color: var(--an-primary); background: var(--an-bg-soft); } +.aksrt-native .subsubsub a.current, +.aksrt-native-tabs a.is-active, +.aksrt-native-tabs a[aria-current="page"] { + color: var(--an-primary); + background: var(--an-primary-soft); + font-weight: 600; +} +.aksrt-native .subsubsub a .count, +.aksrt-native-tabs .count { + padding: 0 6px; + color: inherit; + font-size: 12px; + font-weight: 400; + background: rgba(0, 0, 0, .05); + border-radius: 999px; +} +.aksrt-native .nav-tab-wrapper { + display: flex; + gap: 4px; + margin: 0 0 16px; + padding: 0; + border-bottom: 1px solid var(--an-border); +} +.aksrt-native .nav-tab { + margin: 0 0 -1px; + padding: 8px 14px; + color: var(--an-text-2); + background: transparent; + border: 0; + border-bottom: 2px solid transparent; + font-weight: 500; +} +.aksrt-native .nav-tab:hover { color: var(--an-primary); background: transparent; } +.aksrt-native .nav-tab-active, +.aksrt-native .nav-tab-active:hover { color: var(--an-primary); border-bottom-color: var(--an-primary); background: transparent; } +.aksrt-native-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; } +.aksrt-native-toolbar > .subsubsub, +.aksrt-native-toolbar > .aksrt-native-tabs { margin-bottom: 0; } + +/* ---------- 统计卡 ---------- */ +.aksrt-native-stats { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 12px; + margin: 0 0 16px; +} +.aksrt-native-stat { + --an-tone: var(--an-primary); + position: relative; + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; + padding: 16px 18px; + color: var(--an-text-2); + background: var(--an-card); + border-radius: var(--an-radius); + box-shadow: var(--an-shadow); + text-decoration: none; + overflow: hidden; + transition: box-shadow .2s, transform .2s; +} +.aksrt-native-stat::after { + content: ""; + position: absolute; + top: 0; + right: 0; + width: 72px; + height: 72px; + background: radial-gradient(circle at top right, color-mix(in srgb, var(--an-tone) 16%, transparent), transparent 70%); + pointer-events: none; +} +a.aksrt-native-stat:hover, +a.aksrt-native-stat:focus { color: var(--an-text-2); box-shadow: var(--an-shadow-hover); transform: translateY(-1px); } +a.aksrt-native-stat:focus { outline: 2px solid color-mix(in srgb, var(--an-tone) 40%, transparent); outline-offset: 1px; } +.aksrt-native-stat__label { display: flex; align-items: center; gap: 6px; color: var(--an-muted); font-size: 13px; } +.aksrt-native-stat__label .dashicons { width: 16px; height: 16px; color: var(--an-tone); font-size: 16px; } +.aksrt-native-stat__value { + color: var(--an-text); + font-size: 26px; + font-weight: 600; + line-height: 1.2; + font-variant-numeric: tabular-nums; + word-break: break-all; +} +.aksrt-native-stat__unit { margin-right: 2px; color: var(--an-muted); font-size: 14px; font-weight: 400; } +.aksrt-native-stat__hint { color: var(--an-muted); font-size: 12px; } +.aksrt-native-stat--success { --an-tone: var(--an-primary); } +.aksrt-native-stat--info { --an-tone: var(--an-info); } +.aksrt-native-stat--warning { --an-tone: var(--an-warning); } +.aksrt-native-stat--danger { --an-tone: var(--an-danger); } +.aksrt-native-stat--neutral { --an-tone: var(--an-muted); } +.aksrt-native-stat--warning .aksrt-native-stat__value { color: #c27c0e; } +.aksrt-native-stat--danger .aksrt-native-stat__value { color: var(--an-danger); } + +/* ---------- 卡片 ---------- */ +.aksrt-native-card { + margin: 0 0 16px; + background: var(--an-card); + border-radius: var(--an-radius); + box-shadow: var(--an-shadow); +} +.aksrt-native-card__head { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 8px 12px; + padding: 14px 20px; + border-bottom: 1px solid var(--an-border-soft); +} +.aksrt-native .aksrt-native-card__title { + position: relative; + margin: 0; + padding: 0 0 0 12px; + color: var(--an-text); + font-size: 15px; + font-weight: 600; + line-height: 1.4; +} +.aksrt-native-card__title::before { + content: ""; + position: absolute; + top: 3px; + bottom: 3px; + left: 0; + width: 4px; + background: var(--an-primary); + border-radius: 4px; +} +.aksrt-native-card__actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } +.aksrt-native-card__body { padding: 16px 20px; } +.aksrt-native-card__body > :first-child { margin-top: 0; } +.aksrt-native-card__body > :last-child { margin-bottom: 0; } +.aksrt-native-card--flush > .aksrt-native-card__body { padding: 0; } +.aksrt-native-card__foot { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 12px 20px; + border-top: 1px solid var(--an-border-soft); +} + +/* 折叠卡片:
… */ +.aksrt-native-collapse > summary { + display: flex; + align-items: center; + gap: 8px; + padding: 14px 20px; + color: var(--an-text); + font-size: 14px; + font-weight: 600; + cursor: pointer; + list-style: none; +} +.aksrt-native-collapse > summary::-webkit-details-marker { display: none; } +.aksrt-native-collapse > summary::after { + content: "\f347"; + margin-left: auto; + color: var(--an-muted); + font: normal 16px/1 dashicons; + transition: transform .2s; +} +.aksrt-native-collapse[open] > summary { border-bottom: 1px solid var(--an-border-soft); } +.aksrt-native-collapse[open] > summary::after { transform: rotate(180deg); } +.aksrt-native-collapse > summary .dashicons { color: var(--an-primary); } + +/* ---------- 筛选栏 ---------- */ +.aksrt-native-filters { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin: 0; +} +.aksrt-native-filters label { display: inline-flex; align-items: center; gap: 6px; color: var(--an-muted); } +.aksrt-native-filters input[type="search"] { min-width: 220px; } +.aksrt-native-filters__spacer { flex: 1 1 auto; } +.aksrt-native-card__body > .aksrt-native-filters + .aksrt-native-filters { margin-top: 10px; } + +/* ---------- 表单 ---------- */ +.aksrt-native input[type="text"], +.aksrt-native input[type="search"], +.aksrt-native input[type="number"], +.aksrt-native input[type="date"], +.aksrt-native input[type="datetime-local"], +.aksrt-native input[type="email"], +.aksrt-native input[type="url"], +.aksrt-native input[type="password"], +.aksrt-native select, +.aksrt-native textarea { + min-height: 32px; + border-color: var(--an-border); + border-radius: var(--an-radius-sm); + box-shadow: none; +} +.aksrt-native select { padding-right: 28px; } +.aksrt-native input:focus, +.aksrt-native select:focus, +.aksrt-native textarea:focus { + border-color: var(--an-primary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--an-primary) 20%, transparent); + outline: 0; +} +.aksrt-native-form-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 14px 16px; +} +.aksrt-native-form-grid .is-wide { grid-column: 1 / -1; } +.aksrt-native-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; } +.aksrt-native-field__label { color: var(--an-text); font-weight: 500; } +.aksrt-native-field__hint { color: var(--an-muted); font-size: 12px; } +.aksrt-native-field input, +.aksrt-native-field select, +.aksrt-native-field textarea { width: 100%; max-width: none; } +.aksrt-native-form-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 16px; } +.aksrt-native .form-table th { color: var(--an-text); font-weight: 500; } +.aksrt-native .form-table td, +.aksrt-native .form-table th { padding-top: 12px; padding-bottom: 12px; } + +/* ---------- 按钮 ---------- */ +.aksrt-native .button, +.aksrt-native .page-title-action { + min-height: 32px; + padding: 0 14px; + color: var(--an-text-2); + line-height: 30px; + background: #fff; + border-color: var(--an-border); + border-radius: var(--an-radius-sm); + transition: border-color .15s, color .15s, background .15s; +} +.aksrt-native .page-title-action { top: 0; display: inline-flex; align-items: center; } +.aksrt-native .button:hover, +.aksrt-native .button:focus, +.aksrt-native .page-title-action:hover { + color: var(--an-primary); + background: #fff; + border-color: var(--an-primary); + box-shadow: none; +} +.aksrt-native .button-primary, +.aksrt-native .button.button-primary { + color: #fff; + background: var(--an-primary); + border-color: var(--an-primary); +} +.aksrt-native .button-primary:hover, +.aksrt-native .button-primary:focus, +.aksrt-native .button.button-primary:hover { + color: #fff; + background: var(--an-primary-hover); + border-color: var(--an-primary-hover); +} +.aksrt-native .button-small, +.aksrt-native .button.button-small { min-height: 26px; padding: 0 10px; font-size: 12px; line-height: 24px; } +.aksrt-native .button-link-delete, +.aksrt-native .button.is-danger { color: var(--an-danger); } +.aksrt-native .button.is-danger:hover { color: #fff; background: var(--an-danger); border-color: var(--an-danger); } +.aksrt-native .button .dashicons { width: 16px; height: 16px; margin: 0 4px 0 -2px; font-size: 16px; line-height: inherit; vertical-align: -1px; } +.aksrt-native .submit { margin: 0; padding: 0; } + +/* ---------- 表格 ---------- */ +.aksrt-native-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; } +.aksrt-native .widefat, +.aksrt-native .wp-list-table { + border: 0; + border-radius: 0; + box-shadow: none; +} +.aksrt-native .aksrt-native-card .widefat { background: transparent; } +.aksrt-native .widefat thead th, +.aksrt-native .widefat thead td, +.aksrt-native .widefat tfoot th, +.aksrt-native .widefat tfoot td { + padding: 10px 12px; + color: var(--an-muted); + font-size: 12px; + font-weight: 500; + background: var(--an-bg-soft); + border-bottom: 1px solid var(--an-border-soft); +} +.aksrt-native .widefat thead th a { color: var(--an-muted); } +.aksrt-native .widefat td, +.aksrt-native .widefat tbody th { + padding: 12px; + color: var(--an-text-2); + vertical-align: middle; + border-bottom: 1px solid var(--an-border-soft); +} +.aksrt-native .widefat tbody tr:last-child > td, +.aksrt-native .widefat tbody tr:last-child > th { border-bottom: 0; } +.aksrt-native .striped > tbody > :nth-child(odd) { background: transparent; } +.aksrt-native .widefat tbody tr { transition: background .15s; } +.aksrt-native .widefat tbody tr:hover { background: #fafbfc; } +.aksrt-native .widefat .check-column { width: 2.4em; padding: 12px 0 12px 12px; vertical-align: middle; } +.aksrt-native .widefat thead .check-column { padding-top: 10px; padding-bottom: 10px; } +.aksrt-native .widefat .check-column input { margin: 0; } +.aksrt-native .widefat code, +.aksrt-native-mono { + padding: 1px 6px; + color: var(--an-text); + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 12px; + background: var(--an-bg-soft); + border-radius: 4px; +} +.aksrt-native .row-actions { color: var(--an-muted); } +.aksrt-native .row-title, +.aksrt-native .widefat strong { color: var(--an-text); } +.aksrt-native-card > .aksrt-native-card__body > .widefat:first-child thead th:first-child, +.aksrt-native-card--flush .widefat thead > tr > :first-child { padding-left: 20px; } +.aksrt-native-card--flush .widefat tbody > tr > :first-child { padding-left: 20px; } +.aksrt-native-card--flush .widefat thead > tr > :last-child, +.aksrt-native-card--flush .widefat tbody > tr > :last-child { padding-right: 20px; } +.aksrt-native-card--flush .widefat thead th:first-child, +.aksrt-native-card--flush .widefat thead td:first-child { border-top-left-radius: 0; } + +/* 行内操作表单:单元格里的一组小控件 */ +.aksrt-native-inline { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin: 0; +} +.aksrt-native-inline + .aksrt-native-inline { margin-top: 6px; } +.aksrt-native-inline input[type="text"] { min-width: 0; width: 140px; } + +/* 用户单元格:头像 + 名称 + 次要信息 */ +.aksrt-native-user { display: inline-flex; align-items: center; gap: 8px; min-width: 0; color: var(--an-text); text-decoration: none; } +.aksrt-native-user img { flex: none; border-radius: 50%; } +.aksrt-native-user__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.aksrt-native-user__meta { display: block; color: var(--an-muted); font-size: 12px; } + +/* ---------- 徽标 ---------- */ +.aksrt-native-badge { + --an-tone: var(--an-muted); + display: inline-flex; + align-items: center; + gap: 5px; + padding: 1px 8px; + color: var(--an-tone); + font-size: 12px; + font-weight: 500; + line-height: 20px; + white-space: nowrap; + background: color-mix(in srgb, var(--an-tone) 10%, #fff); + border-radius: 999px; +} +.aksrt-native-badge.has-dot::before { + content: ""; + width: 6px; + height: 6px; + background: currentColor; + border-radius: 50%; +} +.aksrt-native-badge--success { --an-tone: var(--an-primary); } +.aksrt-native-badge--info { --an-tone: var(--an-info); } +.aksrt-native-badge--warning { --an-tone: #c27c0e; } +.aksrt-native-badge--danger { --an-tone: var(--an-danger); } +.aksrt-native-badge--neutral { --an-tone: #6b7785; } +.aksrt-native-badge--primary { --an-tone: #7c4dff; } + +/* ---------- 空状态 / 提示 ---------- */ +.aksrt-native-empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 40px 16px; + color: var(--an-muted); + text-align: center; +} +.aksrt-native-empty .dashicons { width: 40px; height: 40px; color: #c9cdd4; font-size: 40px; } +.aksrt-native-empty__title { color: var(--an-text-2); font-size: 14px; font-weight: 500; } +.aksrt-native .widefat td.aksrt-native-empty-cell { padding: 0; } +.aksrt-native-muted, +.aksrt-native .description { color: var(--an-muted); } +.aksrt-native-text--success { color: var(--an-primary); } +.aksrt-native-text--info { color: var(--an-info); } +.aksrt-native-text--warning { color: #c27c0e; } +.aksrt-native-text--danger { color: var(--an-danger); } +.aksrt-native-clamp { max-height: 40px; overflow: hidden; } +/* 举报&禁封申诉页:宽表 + 排序箭头 */ +.aksrt-native-ban .aksrt-native-table-wrap > table { min-width: 1000px; } +.aksrt-native-ban .orderby-but { position: relative; display: inline-block; width: 0; height: 0; margin-left: 4px; } +.aksrt-native-ban .orderby-but > a { position: absolute; left: 0; bottom: .725em; padding-right: 1em; opacity: .4; transform: translateY(50%); transition: opacity .3s; } +.aksrt-native-ban .orderby-but > a + a { bottom: .075em; } +.aksrt-native-ban .orderby-but > a:hover { opacity: 1; } +.aksrt-native-ban .aksrt-native-toolbar + .subsubsub { float: none; margin: 10px 0 0; } +.aksrt-native-ban textarea { width: 95%; max-width: 500px; } +.aksrt-native-amount { color: var(--an-text); font-weight: 600; font-variant-numeric: tabular-nums; } +.aksrt-native-amount.is-positive { color: var(--an-primary); } +.aksrt-native-amount.is-negative { color: var(--an-danger); } +.aksrt-native-callout { + margin: 0 0 16px; + padding: 12px 16px; + color: var(--an-text-2); + background: var(--an-bg-soft); + border-left: 3px solid var(--an-info); + border-radius: var(--an-radius-sm); +} +.aksrt-native-callout.is-warning { background: #fff8eb; border-left-color: var(--an-warning); } +.aksrt-native-callout.is-danger { background: #fdf0f2; border-left-color: var(--an-danger); } + +/* 快捷入口网格 */ +.aksrt-native-links { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 10px; + margin: 0; + padding: 0; + list-style: none; +} +.aksrt-native-links li { margin: 0; } +.aksrt-native-links a { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; + color: var(--an-text); + background: var(--an-bg-soft); + border-radius: var(--an-radius-sm); + text-decoration: none; + transition: background .15s, color .15s; +} +.aksrt-native-links a:hover { color: var(--an-primary); background: var(--an-primary-soft); } +.aksrt-native-links a::after { content: "\f345"; margin-left: auto; color: var(--an-muted); font: normal 14px/1 dashicons; } +.aksrt-native-links .dashicons { color: var(--an-primary); } + +/* ---------- 分页条(AdminListPagination / paginate_links) ---------- */ +.aksrt-native .tablenav { height: auto; margin: 0; padding: 12px 20px; } +.aksrt-native .aksrt-native-card .tablenav.bottom { border-top: 1px solid var(--an-border-soft); } +.aksrt-native .tablenav-pages { float: right; margin: 0; color: var(--an-muted); } +.aksrt-native .tablenav-pages .button, +.aksrt-native .tablenav-pages .page-numbers { + min-width: 30px; + min-height: 30px; + padding: 0 8px; + line-height: 28px; + text-align: center; +} +.aksrt-native .tablenav-pages .page-numbers { + display: inline-block; + color: var(--an-text-2); + border: 1px solid var(--an-border); + border-radius: var(--an-radius-sm); + text-decoration: none; +} +.aksrt-native .tablenav-pages .page-numbers.current { color: #fff; background: var(--an-primary); border-color: var(--an-primary); } +.aksrt-native .tablenav-pages .page-numbers.dots { border-color: transparent; } +.aksrt-native .tablenav-pages .button.disabled { color: #c9cdd4 !important; background: var(--an-bg-soft) !important; border-color: var(--an-border-soft) !important; } +.aksrt-native .tablenav .actions { padding: 0; } +.aksrt-native .tablenav::after { content: ""; display: table; clear: both; } + +@media (max-width: 782px) { + .aksrt-native { margin-right: 10px; } + .aksrt-native-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } + .aksrt-native-stat { padding: 12px 14px; } + .aksrt-native-stat__value { font-size: 20px; } + .aksrt-native-card__head, + .aksrt-native-card__body, + .aksrt-native-collapse > summary { padding-left: 14px; padding-right: 14px; } + .aksrt-native .tablenav { padding: 10px 14px; } + .aksrt-native .tablenav-pages { float: none; } + .aksrt-native-filters input[type="search"] { min-width: 0; flex: 1 1 100%; } +} diff --git a/assets/css/admin/tickets.css b/assets/css/admin/tickets.css index bb7613dd..d6f8e00f 100644 --- a/assets/css/admin/tickets.css +++ b/assets/css/admin/tickets.css @@ -1,77 +1,14 @@ -/* 工单管理(users.php?page=aksrt_tickets)——仅在本页入队 */ - -.aksrt-tickets-admin { - --aksrt-tk-border: #dcdcde; - --aksrt-tk-muted: #646970; - --aksrt-tk-soft: #f6f7f7; - --aksrt-tk-accent: #2271b1; -} - -.aksrt-tickets-admin__muted { color: var(--aksrt-tk-muted); font-weight: 400; } - -/* ---- 代客提交 ---- */ -.aksrt-tickets-admin__create { - margin: 16px 0 8px; - background: #fff; - border: 1px solid var(--aksrt-tk-border); - border-radius: 4px; -} -.aksrt-tickets-admin__create > summary { - display: flex; - align-items: center; - gap: 6px; - padding: 10px 14px; - cursor: pointer; - font-weight: 600; - color: var(--aksrt-tk-accent); - list-style: none; -} -.aksrt-tickets-admin__create > summary::-webkit-details-marker { display: none; } -.aksrt-tickets-admin__create[open] > summary { border-bottom: 1px solid var(--aksrt-tk-border); } -.aksrt-tickets-admin__create-form, -.aksrt-tickets-admin__create-empty { padding: 14px; margin: 0; } -.aksrt-tickets-admin__create-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px 16px; -} -.aksrt-tickets-admin__create-grid label { display: flex; flex-direction: column; gap: 4px; } -.aksrt-tickets-admin__create-grid label > span { font-weight: 600; } -.aksrt-tickets-admin__create-grid .is-wide { grid-column: 1 / -1; } -.aksrt-tickets-admin__create-grid input, -.aksrt-tickets-admin__create-grid select, -.aksrt-tickets-admin__create-grid textarea { width: 100%; max-width: none; } -.aksrt-tickets-admin__create-foot { - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; - margin: 12px 0 0; -} - -/* ---- 视图与筛选 ---- */ -.aksrt-tickets-admin__views { margin-bottom: 4px; } -.aksrt-ticket-dot { - display: inline-block; - width: 8px; - height: 8px; - margin-right: 4px; - border-radius: 50%; - background: var(--aksrt-ticket-color, #8c8f94); - vertical-align: 1px; -} -.aksrt-tickets-admin__filters { - clear: both; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 6px; - padding: 8px 0; -} -.aksrt-tickets-admin__filters input[type="search"] { min-width: 220px; } +/* + * 工单管理(users.php?page=aksrt_tickets)——仅在本页入队。 + * + * 页面外壳 / 页头 / 卡片 / 折叠创建卡 / 筛选栏 / 表格 / 空态 / 分页 / 徽标基形 + * 由 native-ui.css(.aksrt-native)统一承载;本文件只保留工单专属部分: + * 列表行状态、按状态上色的徽标与小圆点(--aksrt-ticket-color)、催单徽标、 + * 行内展开的详情面板(对话气泡 / 引用 / 附件 / 信息卡 / 处理表单 / 内部备注)。 + * 颜色与圆角继承 .aksrt-native 的 --an-* 令牌。 + */ /* ---- 列表 ---- */ -.aksrt-tickets-admin__table-wrap { overflow-x: auto; } .aksrt-tickets-admin__table { min-width: 860px; table-layout: fixed; } .aksrt-tickets-admin__table .column-title { width: 34%; } .aksrt-tickets-admin__table .column-status { width: 10%; } @@ -79,34 +16,36 @@ .aksrt-tickets-admin__table .column-admin { width: 11%; } .aksrt-tickets-admin__table .column-user { width: 15%; } .aksrt-tickets-admin__table .column-time { width: 19%; } -.aksrt-tickets-admin__table td { vertical-align: middle; } .aksrt-tickets-admin__table .row-title { word-break: break-word; } -.aksrt-tickets-admin__sub { margin-top: 3px; color: var(--aksrt-tk-muted); font-size: 12px; } -.aksrt-tickets-admin__sub code { padding: 0 4px; font-size: 12px; background: var(--aksrt-tk-soft); } -.aksrt-tickets-admin__row.is-awaiting td:first-child { box-shadow: inset 3px 0 0 #d63638; } -.aksrt-tickets-admin__row.is-open td { background: #f0f6fc; } -.aksrt-tickets-admin__detail-row > td { padding: 0 !important; background: var(--aksrt-tk-soft) !important; } -.aksrt-tickets-admin__empty { - display: flex; - flex-direction: column; - align-items: center; - gap: 4px; - padding: 32px 0; - color: var(--aksrt-tk-muted); +.aksrt-tickets-admin__sub { margin-top: 3px; color: var(--an-muted); font-size: 12px; } +.aksrt-tickets-admin__sub code { padding: 0 4px; font-size: 12px; } +.aksrt-native .aksrt-tickets-admin__row.is-awaiting td:first-child { box-shadow: inset 3px 0 0 var(--an-danger); } +.aksrt-native .aksrt-tickets-admin__row.is-open td { background: var(--an-primary-soft); } +.aksrt-native .aksrt-tickets-admin__detail-row > td { padding: 0 !important; background: var(--an-bg-soft) !important; } + +/* 状态小圆点(状态视图链接前缀) */ +.aksrt-ticket-dot { + display: inline-block; + width: 8px; + height: 8px; + margin-right: 4px; + border-radius: 50%; + background: var(--aksrt-ticket-color, var(--an-muted)); + vertical-align: 1px; } +/* 按状态上色的徽标(--aksrt-ticket-color 由状态配置驱动,不走 NativeUi 色调) */ .aksrt-ticket-badge { display: inline-flex; align-items: center; gap: 5px; - padding: 2px 8px; + padding: 1px 8px; border-radius: 999px; font-size: 12px; - line-height: 18px; + line-height: 20px; white-space: nowrap; - color: var(--aksrt-ticket-color, #646970); - background: color-mix(in srgb, var(--aksrt-ticket-color, #646970) 12%, #fff); - border: 1px solid color-mix(in srgb, var(--aksrt-ticket-color, #646970) 30%, #fff); + color: var(--aksrt-ticket-color, var(--an-muted)); + background: color-mix(in srgb, var(--aksrt-ticket-color, var(--an-muted)) 10%, #fff); } .aksrt-ticket-badge::before { content: ""; @@ -116,45 +55,60 @@ background: currentColor; } -.aksrt-ticket-user { display: inline-flex; align-items: center; gap: 6px; min-width: 0; text-decoration: none; } +/* 催单徽标:列表标题旁 + 详情信息卡。 */ +.aksrt-ticket-urge { + display: inline-block; + padding: 0 6px; + color: var(--an-danger); + font-size: 12px; + line-height: 18px; + background: #fdf0f2; + border: 1px solid color-mix(in srgb, var(--an-danger) 30%, #fff); + border-radius: 9px; + vertical-align: middle; +} + +/* 用户 chip(列表行) */ +.aksrt-ticket-user { display: inline-flex; align-items: center; gap: 6px; min-width: 0; color: var(--an-text); text-decoration: none; } .aksrt-ticket-user img { flex: none; border-radius: 50%; } .aksrt-ticket-user span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -/* ---- 详情 ---- */ +/* ---- 详情面板(行内展开) ---- */ .aksrt-ticket-detail { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 16px; - padding: 16px; - border-top: 1px solid var(--aksrt-tk-border); - border-bottom: 1px solid var(--aksrt-tk-border); + padding: 16px 20px; + border-top: 1px solid var(--an-border); + border-bottom: 1px solid var(--an-border); } .aksrt-ticket-detail__main, .aksrt-ticket-detail__side { display: flex; flex-direction: column; gap: 12px; min-width: 0; } -.aksrt-ticket-detail__heading { display: block; margin: 0 0 8px; font-size: 13px; font-weight: 600; } -.aksrt-ticket-detail__empty { margin: 0; color: var(--aksrt-tk-muted); } +.aksrt-ticket-detail__heading { display: block; margin: 0 0 8px; color: var(--an-text); font-size: 13px; font-weight: 600; } +.aksrt-ticket-detail__empty { margin: 0; color: var(--an-muted); } .aksrt-ticket-detail__origin, .aksrt-ticket-detail__reply, .aksrt-ticket-detail__card, .aksrt-ticket-detail__reply-form { padding: 12px 14px; - background: #fff; - border: 1px solid var(--aksrt-tk-border); - border-radius: 4px; + background: var(--an-card); + border: 1px solid var(--an-border); + border-radius: var(--an-radius-sm); } -.aksrt-ticket-detail__origin { border-left: 3px solid var(--aksrt-tk-accent); } +.aksrt-ticket-detail__origin { border-left: 3px solid var(--an-primary); } .aksrt-ticket-detail__msg-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } .aksrt-ticket-detail__msg-head img { border-radius: 50%; } -.aksrt-ticket-detail__msg-head time { margin-left: auto; color: var(--aksrt-tk-muted); font-size: 12px; } +.aksrt-ticket-detail__msg-head strong { color: var(--an-text); } +.aksrt-ticket-detail__msg-head time { margin-left: auto; color: var(--an-muted); font-size: 12px; } .aksrt-ticket-detail__body { word-break: break-word; } .aksrt-ticket-detail__body > p:first-child { margin-top: 0; } .aksrt-ticket-detail__body > p:last-child { margin-bottom: 0; } .aksrt-ticket-detail__replies { display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; list-style: none; } .aksrt-ticket-detail__reply { margin: 0; } -.aksrt-ticket-detail__reply.is-admin { margin-left: 32px; background: #f0f6fc; border-color: #c5d9ed; } +.aksrt-ticket-detail__reply.is-admin { margin-left: 32px; background: var(--an-primary-soft); border-color: color-mix(in srgb, var(--an-primary) 24%, #fff); } .aksrt-ticket-detail__reply.is-user { margin-right: 32px; } /* 系统记录(催单等):居中细条,与对话气泡区分。 */ .aksrt-ticket-detail__system { @@ -162,28 +116,16 @@ justify-content: center; gap: 8px; margin: 0; - color: var(--aksrt-tk-muted); + color: var(--an-muted); font-size: 12px; } -.aksrt-ticket-detail__system span { color: #b32d2e; } -/* 催单徽标:列表标题旁 + 详情信息卡。 */ -.aksrt-ticket-urge { - display: inline-block; - padding: 0 6px; - color: #b32d2e; - font-size: 12px; - line-height: 18px; - background: #fcf0f1; - border: 1px solid #f0c3c5; - border-radius: 9px; - vertical-align: middle; -} +.aksrt-ticket-detail__system span { color: var(--an-danger); } .aksrt-ticket-detail__quote { margin: 0 0 8px; padding: 6px 10px; - color: var(--aksrt-tk-muted); - background: var(--aksrt-tk-soft); - border-left: 3px solid var(--aksrt-tk-border); + color: var(--an-muted); + background: var(--an-bg-soft); + border-left: 3px solid var(--an-border); white-space: pre-line; } @@ -194,25 +136,25 @@ width: 72px; height: 72px; object-fit: cover; - border: 1px solid var(--aksrt-tk-border); - border-radius: 4px; + border: 1px solid var(--an-border); + border-radius: var(--an-radius-sm); } .aksrt-ticket-detail__reply-form textarea { width: 100%; } .aksrt-ticket-detail__reply-foot { display: flex; align-items: center; gap: 8px; margin-top: 8px; } .aksrt-ticket-detail__reply-foot select { max-width: 240px; } -.aksrt-ticket-detail__counter { margin-left: auto; color: var(--aksrt-tk-muted); font-size: 12px; } -.aksrt-ticket-detail__counter.is-over { color: #d63638; } +.aksrt-ticket-detail__counter { margin-left: auto; color: var(--an-muted); font-size: 12px; } +.aksrt-ticket-detail__counter.is-over { color: var(--an-danger); } .aksrt-ticket-detail__closed { display: flex; align-items: center; gap: 6px; margin: 0; padding: 10px 14px; - color: var(--aksrt-tk-muted); - background: #fff; - border: 1px dashed var(--aksrt-tk-border); - border-radius: 4px; + color: var(--an-muted); + background: var(--an-card); + border: 1px dashed var(--an-border); + border-radius: var(--an-radius-sm); } .aksrt-ticket-detail__info { @@ -221,8 +163,8 @@ gap: 8px 12px; margin: 0; } -.aksrt-ticket-detail__info dt { color: var(--aksrt-tk-muted); } -.aksrt-ticket-detail__info dd { margin: 0; word-break: break-all; } +.aksrt-ticket-detail__info dt { color: var(--an-muted); } +.aksrt-ticket-detail__info dd { margin: 0; color: var(--an-text); word-break: break-all; } .aksrt-ticket-detail__status-form { display: flex; flex-direction: column; gap: 8px; } .aksrt-ticket-detail__status-form label { display: flex; flex-direction: column; gap: 3px; } @@ -230,9 +172,10 @@ .aksrt-ticket-detail__status-form .button { align-self: flex-start; } .aksrt-ticket-detail__notes { display: flex; flex-direction: column; gap: 8px; margin: 0 0 10px; padding: 0; list-style: none; } -.aksrt-ticket-detail__notes li { margin: 0; padding: 8px 10px; background: #fcf9e8; border-left: 3px solid #dba617; word-break: break-word; } +.aksrt-ticket-detail__notes li { margin: 0; padding: 8px 10px; background: #fffbe8; border-left: 3px solid var(--an-warning); word-break: break-word; } .aksrt-ticket-detail__note-head { display: flex; justify-content: space-between; gap: 8px; margin-bottom: 4px; font-size: 12px; } -.aksrt-ticket-detail__note-head time { color: var(--aksrt-tk-muted); } +.aksrt-ticket-detail__note-head strong { color: var(--an-text); } +.aksrt-ticket-detail__note-head time { color: var(--an-muted); } .aksrt-ticket-detail__note-form { display: flex; flex-direction: column; gap: 6px; } .aksrt-ticket-detail__note-form textarea { width: 100%; } .aksrt-ticket-detail__note-form .button { align-self: flex-start; } @@ -241,7 +184,6 @@ .aksrt-ticket-detail { grid-template-columns: minmax(0, 1fr); } } @media (max-width: 782px) { - .aksrt-tickets-admin__create-grid { grid-template-columns: minmax(0, 1fr); } .aksrt-ticket-detail__reply.is-admin, .aksrt-ticket-detail__reply.is-user { margin-left: 0; margin-right: 0; } } diff --git a/src/Admin/Native/NativeUi.php b/src/Admin/Native/NativeUi.php new file mode 100644 index 00000000..881451a1 --- /dev/null +++ b/src/Admin/Native/NativeUi.php @@ -0,0 +1,251 @@ +'; + } + + public static function page_close(): void { + echo ''; + } + + /** + * 页头:图标 + 标题 + 说明 + 右侧操作。 + * + * @param array $actions + */ + public static function header(string $title, string $description = '', string $icon = '', array $actions = []): void { + echo '
'; + // h1 让 WP 把 admin_notices 挪到标题之后。 + echo '

'; + if ($icon !== '') { + echo ''; + } + echo esc_html($title) . '

'; + if ($description !== '') { + echo '

' . esc_html($description) . '

'; + } + echo '
'; + if ($actions !== []) { + echo '
'; + foreach ($actions as $action) { + echo self::button_link($action['label'], $action['url'], !empty($action['primary']), (string) ($action['icon'] ?? '')); + } + echo '
'; + } + echo '
'; + // WP 会把 .notice 插在首个 .wrap h1 之后;这里留一个固定落点。 + echo '
'; + } + + public static function button_link(string $label, string $url, bool $primary = false, string $icon = ''): string { + $class = $primary ? 'button button-primary' : 'button'; + $icon_html = $icon !== '' ? '' : ''; + return '' . $icon_html . esc_html($label) . ''; + } + + /* ------------------------------------------------------------------ + * 统计卡 + * ----------------------------------------------------------------*/ + + /** + * @param array $items + */ + public static function stats(array $items): void { + if ($items === []) { + return; + } + echo '
'; + foreach ($items as $item) { + self::stat($item); + } + echo '
'; + } + + /** @param array{label:string,value:string|int|float,url?:string,tone?:string,icon?:string,hint?:string,unit?:string} $item */ + public static function stat(array $item): void { + $tone = self::tone((string) ($item['tone'] ?? 'success')); + $url = (string) ($item['url'] ?? ''); + $tag = $url !== '' ? 'a' : 'div'; + $attr = $url !== '' ? ' href="' . esc_url($url) . '"' : ''; + + echo '<' . $tag . ' class="aksrt-native-stat aksrt-native-stat--' . esc_attr($tone) . '"' . $attr . '>'; + echo ''; + if (!empty($item['icon'])) { + echo ''; + } + echo esc_html($item['label']) . ''; + echo ''; + if (!empty($item['unit'])) { + echo '' . esc_html((string) $item['unit']) . ''; + } + echo esc_html((string) $item['value']) . ''; + if (!empty($item['hint'])) { + echo '' . esc_html((string) $item['hint']) . ''; + } + echo ''; + } + + /* ------------------------------------------------------------------ + * 卡片 + * ----------------------------------------------------------------*/ + + /** + * 打开卡片。$actions_html 为调用方已转义的右侧操作区片段。 + * $flush=true 时正文无内边距(承载整表)。 + */ + public static function card_open(string $title = '', string $actions_html = '', bool $flush = false, string $modifier = ''): void { + $class = 'aksrt-native-card' . ($flush ? ' aksrt-native-card--flush' : '') . ($modifier !== '' ? ' ' . $modifier : ''); + echo '
'; + if ($title !== '' || $actions_html !== '') { + echo '
'; + if ($title !== '') { + echo '

' . esc_html($title) . '

'; + } + if ($actions_html !== '') { + echo '
' . $actions_html . '
'; + } + echo '
'; + } + echo '
'; + } + + public static function card_close(): void { + echo '
'; + } + + /** 表格卡:整表铺满卡片,带横向滚动容器。 */ + public static function table_card_open(string $title = '', string $actions_html = ''): void { + self::card_open($title, $actions_html, true); + echo '
'; + } + + /** 关闭表格卡;分页条应在 table-wrap 关闭后、卡片关闭前输出,故拆成两步。 */ + public static function table_wrap_close(): void { + echo '
'; + } + + /* ------------------------------------------------------------------ + * 徽标 / 空状态 + * ----------------------------------------------------------------*/ + + public static function badge(string $label, string $tone = 'neutral', bool $dot = true): string { + $class = 'aksrt-native-badge aksrt-native-badge--' . self::tone($tone) . ($dot ? ' has-dot' : ''); + return '' . esc_html($label) . ''; + } + + /** + * 按状态键取徽标。$map: 状态键 => [文案, 色调];未命中时原样输出灰色徽标。 + * + * @param array $map + */ + public static function status_badge(string $status, array $map): string { + if (isset($map[$status])) { + return self::badge($map[$status][0], $map[$status][1]); + } + return self::badge($status !== '' ? $status : '—', 'neutral'); + } + + public static function empty_state(string $title, string $hint = '', string $icon = 'dashicons-portfolio'): string { + $html = '
'; + $html .= ''; + $html .= '' . esc_html($title) . ''; + if ($hint !== '') { + $html .= '' . esc_html($hint) . ''; + } + return $html . '
'; + } + + /** 表格内的空状态整行。 */ + public static function empty_row(int $colspan, string $title, string $hint = ''): string { + return '' + . self::empty_state($title, $hint) + . ''; + } + + /** 用户单元格:头像 + 名称 + #ID。 */ + public static function user_cell(int $user_id, string $fallback = ''): string { + $user = $user_id > 0 ? get_userdata($user_id) : false; + if (!$user) { + return '' . esc_html($fallback !== '' ? $fallback : ($user_id > 0 ? '#' . $user_id : '—')) . ''; + } + $avatar = function_exists('aksrt_get_avatar_url') ? (string) aksrt_get_avatar_url($user_id, 28) : (string) get_avatar_url($user_id, ['size' => 28]); + return '' + . ($avatar !== '' ? '' : '') + . '' . esc_html($user->display_name) . '' + . '#' . esc_html((string) $user_id) . ''; + } + + private static function tone(string $tone): string { + return in_array($tone, self::TONES, true) ? $tone : 'neutral'; + } +} diff --git a/src/Admin/Native/PayChargeCards.php b/src/Admin/Native/PayChargeCards.php index 63911d10..6547a69e 100644 --- a/src/Admin/Native/PayChargeCards.php +++ b/src/Admin/Native/PayChargeCards.php @@ -8,6 +8,12 @@ * 卡密类型与参考主题一致:余额充值 / 会员兑换 / 积分兑换 / 自定义卡密; * 本主题内部类型映射:balance / vip / credits / custom。 * 差异点:AKSRT 无商城发货(shipped)子系统,故不含「已售」标记与链接;删除取代参考主题的批量操作语义一致。 + * + * 外壳与配色走 NativeUi(.aksrt-native),页面专属交互样式仍由 print_assets() 输出。 + * 类体按关注点拆成 flat sibling traits(PSR-4 要求 trait 与宿主类同目录,不能放子目录): + * - PayChargeCardsList 列表渲染 + 翻页参数 + * - PayChargeCardsAssets 页面专属内联样式/脚本 + * - PayChargeCardsForms 添加/导出表单 + 依赖显隐助手 */ namespace AKSRT\Admin\Native; @@ -16,6 +22,10 @@ defined('ABSPATH') || exit; class PayChargeCards { + use PayChargeCardsList; + use PayChargeCardsAssets; + use PayChargeCardsForms; + public const CAPABILITY = 'manage_options'; public const PAGE_SLUG = 'aksrt_charge_cards'; public const NONCE = 'aksrt_card_pass_page'; @@ -45,8 +55,10 @@ class PayChargeCards { $this->page_url = admin_url('admin.php?page=' . self::PAGE_SLUG); if (!class_exists('\AKSRT\Payment\ChargeCard')) { - echo '

' . esc_html__('卡密管理', 'aksrt') . '

'; - echo '

' . esc_html__('卡密模块未初始化。', 'aksrt') . '

'; + NativeUi::page_open(); + echo '

' . esc_html__('卡密管理', 'aksrt') . '

'; + echo '

' . esc_html__('卡密模块未初始化。', 'aksrt') . '

'; + NativeUi::page_close(); return; } @@ -84,10 +96,11 @@ class PayChargeCards { break; } - echo '
'; + NativeUi::page_open(); $this->print_assets(); echo '

' . esc_html($page_title) . '

'; echo $head_but; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。 + echo '
'; if ($tab === 'add') { $this->render_add_form(); @@ -97,463 +110,7 @@ class PayChargeCards { $this->render_list($s); } - echo '
'; - } - - /* ------------------------------------------------------------------ - * 列表 - * ------------------------------------------------------------------ */ - - /** - * @param string $s 搜索词(由页面头部统一解析后传入,用于提示、过滤链接与查询)。 - */ - private function render_list(string $s): void { - $svc = \AKSRT\Payment\ChargeCard::get_instance(); - - $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'])) : ''; - $orderby = isset($_GET['orderby']) ? sanitize_key((string) wp_unslash($_GET['orderby'])) : 'modified_time'; - $order = isset($_GET['desc']) ? (strtoupper((string) wp_unslash($_GET['desc'])) === 'ASC' ? 'ASC' : 'DESC') : 'DESC'; - $paged = max(1, (int) ($_GET['paged'] ?? 1)); - // 每页条数默认值取自共享助手;ice_perpage 仅作为 URL 参数被尊重,无界面控件。 - $per_page = max(1, min(100, (int) ($_REQUEST['ice_perpage'] ?? AdminListPagination::DEFAULT_PER_PAGE))); - - // 参考主题:status=0 表示未使用;本主题规范状态为 active。 - if ($status === '0' || $status === 'active') { - $status = 'active'; - } elseif (!in_array($status, ['used', 'expired', 'disabled'], true)) { - $status = ''; - } - if (!in_array($type, ['balance', 'vip', 'credits', 'custom'], true)) { - $type = ''; - } - - // 过滤链接(标题右侧按钮与搜索提示由页面头部统一输出)。 - $sub_but = [ - ['name' => __('全部', 'aksrt'), 'href' => $this->page_url], - ]; - if ($s === '') { - $sub_but[] = ['name' => __('未使用', 'aksrt'), 'href' => add_query_arg('status', '0', $this->page_url)]; - $sub_but[] = ['name' => __('已使用', 'aksrt'), 'href' => add_query_arg('status', 'used', $this->page_url)]; - foreach ($this->type_labels() as $_k => $_v) { - $sub_but[] = ['name' => $_v, 'href' => add_query_arg('type', $_k, $this->page_url)]; - } - } - $but_html = ''; - foreach ($sub_but as $but) { - $but_html .= '
  • ' . esc_html($but['name']) . '
  • | '; - } - $but_html = substr($but_html, 0, -2); - - $search = '
    -
    - - -
    -
    '; - - // 当前筛选参数(不含 paged)。分页链接显式带上它,不再依赖 add_query_arg() - // 默认取 $_SERVER['REQUEST_URI'](链接会随环境漂移,且列条件容易丢)。 - $query_args = $this->list_query_args($status, $type, $other, $s, $orderby, $order, $per_page); - - echo '
      ' . $but_html . '
    ' . $search . '
    '; - - $filters = [ - 'status' => $status, - 'type' => $type, - 'other' => $other, - 'search' => $s, - 'orderby' => $orderby, - 'order' => $order, - ]; - - $result = $svc->get_list($filters + ['limit' => $per_page, 'offset' => ($paged - 1) * $per_page]); - $items = (array) ($result['items'] ?? []); - $count_all = (int) ($result['total'] ?? 0); - - // 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外, - // 表现为「空白表格 + 一个分页控件都没有」的死胡同,只能靠手改 URL 退出。 - // 这里回退到最后一页重查一次,保证越界页码也能拿到数据与分页条。 - $max_page = max(1, (int) ($result['max_pages'] ?? 1)); - if (!$items && $count_all > 0 && $paged > $max_page) { - $paged = $max_page; - $result = $svc->get_list($filters + ['limit' => $per_page, 'offset' => ($paged - 1) * $per_page]); - $items = (array) ($result['items'] ?? []); - $count_all = (int) ($result['total'] ?? $count_all); - } - - if (!$items) { - echo '
    '; - echo ''; - echo ''; - echo '
    ' . esc_html__('未找到对应内容,或暂无内容', 'aksrt') . '
    '; - return; - } - - // 表头(与参考主题同列、同排序链接)。 - $theads = [ - ['width' => '6%', 'orderby' => 'card', 'name' => __('卡号', 'aksrt')], - ['width' => '10%', 'orderby' => 'password', 'name' => __('密码', 'aksrt')], - ['width' => '5%', 'orderby' => 'type', 'name' => __('类型', 'aksrt')], - ['width' => '5%', 'orderby' => 'create_time', 'name' => __('创建时间', 'aksrt')], - ['width' => '5%', 'orderby' => 'modified_time', 'name' => __('更新时间', 'aksrt')], - ['width' => '5%', 'orderby' => 'status', 'name' => __('状态', 'aksrt')], - ['width' => '8%', 'orderby' => 'other', 'name' => __('标识', 'aksrt')], - ]; - - $thead_th = ''; - foreach ($theads as $thead) { - $orderby_links = ''; - if ($thead['orderby']) { - $orderby_url = add_query_arg('orderby', $thead['orderby']); - $orderby_links .= ''; - $orderby_links .= ''; - $orderby_links = '' . $orderby_links . ''; - } - $thead_th .= '' . esc_html($thead['name']) . $orderby_links . ''; - } - - $tbody = ''; - foreach ($items as $msg) { - $meta = []; - if (!empty($msg['meta'])) { - $decoded = json_decode((string) $msg['meta'], true); - $meta = is_array($decoded) ? $decoded : []; - } - - // 自定义卡密不展示状态;其余类型按状态着色(类型链接统一在下方链表中生成)。 - $status_html = ''; - if ($msg['type'] !== 'custom') { - $status_html = '' . esc_html__('未使用', 'aksrt') . ''; - if ($msg['status'] === 'used') { - $status_html = '' . esc_html__('已使用', 'aksrt') . ''; - if (!empty($msg['order_number'])) { - $order_link_url = admin_url('admin.php?page=aksrt_orders&s=' . urlencode((string) $msg['order_number'])); - $status_html .= ' [' . esc_html__('查看', 'aksrt') . ']'; - } - } elseif ($msg['status'] === 'expired') { - $status_html = '' . esc_html__('已过期', 'aksrt') . ''; - } elseif ($msg['status'] === 'disabled') { - $status_html = '' . esc_html__('已禁用', 'aksrt') . ''; - } - } - - $other_a = ''; - if (!empty($msg['note'])) { - $other_a = '' . esc_html($msg['note']) . ''; - } - - if ($msg['type'] === 'vip') { - $type_html = '' . esc_html__('会员兑换', 'aksrt') . ''; - $level = (int) ($meta['level'] ?? 1); - $type_html .= '
    ' . esc_html($this->vip_level_name($level)) . ':' . esc_html($this->vip_time_text($meta)); - } elseif ($msg['type'] === 'credits') { - $type_html = '' . esc_html__('积分兑换', 'aksrt') . ''; - $type_html .= '
    ' . esc_html__('积分:', 'aksrt') . esc_html((string) (int) ($meta['points'] ?? 0)); - } elseif ($msg['type'] === 'balance') { - $type_html = '' . esc_html__('余额充值', 'aksrt') . ''; - $type_html .= '
    ' . esc_html__('金额:', 'aksrt') . esc_html($this->balance_amount_text($meta)); - } else { - $type_html = '' . esc_html__('自定义卡密', 'aksrt') . ''; - } - - $card_name = (string) ($meta['custom_name']['card_name'] ?? ''); - $password_name = (string) ($meta['custom_name']['password_name'] ?? ''); - - $tbody .= ''; - $tbody .= ' - - '; - $tbody .= "" . esc_html($card_name) . ' ' . esc_html((string) $msg['card']) . ''; - $tbody .= "" . esc_html($password_name) . ' ' . esc_html((string) $msg['password']) . ''; - $tbody .= "$type_html"; - $tbody .= '' . esc_html((string) $msg['created_at']) . ''; - $tbody .= '' . esc_html((string) ($msg['used_at'] ?: $msg['created_at'])) . ''; - $tbody .= "$status_html"; - $tbody .= "$other_a"; - $tbody .= ''; - } - - echo '
    '; - echo '
    '; - wp_nonce_field(self::NONCE); - echo '
    - - -
    '; - - echo '
    '; - echo ''; - echo '' . $thead_th . ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。 - echo '' . $tbody . ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。 - echo '
    '; - echo '
    '; - echo '
    '; - echo '
    '; - - AdminListPagination::render(self::PAGE_SLUG, $count_all, $paged, $per_page, $query_args); - } - - /* ------------------------------------------------------------------ - * 添加卡密 - * ------------------------------------------------------------------ */ - - private function render_add_form(): void { - $type_labels = $this->type_labels(); - ?> -

    -
      -
    • -
    • -
    -
    - - - - field_row( - __('卡密类型', 'aksrt'), - $this->button_set('type', $type_labels, 'balance'), - [] - ); - $this->field_row( - __('添加方式', 'aksrt'), - $this->button_set('add_type', [ - 'auto' => __('系统自动生成', 'aksrt'), - 'import' => __('导入卡密', 'aksrt'), - ], 'auto'), - [] - ); - - // 自动生成 - $this->field_row( - __('生成数量', 'aksrt'), - $this->spinner('auto_num', 20, __('张', 'aksrt')), - [['key' => 'add_type', 'op' => '==', 'val' => 'auto']], - __('需要生成多少张卡密(1 - 1000 的整数,单次生成数量太多可能会对服务器性能造成影响)', 'aksrt') - ); - $this->field_row( - __('标识', 'aksrt'), - '', - [['key' => 'add_type', 'op' => '==', 'val' => 'auto']], - __('对生成的卡密做标记标识,方便后期查找管理', 'aksrt') . '
    ' . sprintf(esc_html__('注意:标识中不能有%1$s字符,系统已占用', 'aksrt'), 'shipped') - ); - $this->field_row( - __('面额', 'aksrt'), - '', - [ - ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], - ['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'], - ], - __('单张卡密的面额', 'aksrt') - ); - $this->field_row( - __('兑换会员', 'aksrt'), - __('会员等级', 'aksrt') . ' ' . $this->button_set('vip_exchange[level]', [ - '1' => $this->vip_level_name(1), - '2' => $this->vip_level_name(2), - ], '1'), - [ - ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], - ['key' => 'type', 'op' => '==', 'val' => 'vip'], - ] - ); - $this->field_row( - ' ', - __('兑换会员时长', 'aksrt') . '
    ', - [ - ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], - ['key' => 'type', 'op' => '==', 'val' => 'vip'], - ], - __('填', 'aksrt') . 'Permanent' . __('为永久', 'aksrt') - ); - $this->field_row( - ' ', - __('时长单位', 'aksrt') . ' ' . $this->button_set('vip_exchange[unit]', [ - 'day' => __('天', 'aksrt'), - 'month' => __('个月', 'aksrt'), - ], 'month'), - [ - ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], - ['key' => 'type', 'op' => '==', 'val' => 'vip'], - ['key' => 'vip_exchange[time]', 'op' => '!=', 'val' => 'Permanent'], - ] - ); - $this->field_row( - __('自定义显示名称', 'aksrt'), - __('卡号显示名称', 'aksrt') . '

    ' . - __('卡密显示名称', 'aksrt') . '
    ', - [ - ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], - ['key' => 'type', 'op' => '==', 'val' => 'custom'], - ] - ); - $this->field_row( - __('高级选项', 'aksrt'), - '', - [['key' => 'add_type', 'op' => '==', 'val' => 'auto']] - ); - $this->field_row( - __('自定义卡号位数', 'aksrt'), - $this->spinner('auto_rand_number_limit', 20, __('位数', 'aksrt')), - [ - ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], - ['key' => 'auto_top_s', 'op' => 'checked'], - ] - ); - $this->field_row( - __('自定义密码位数', 'aksrt'), - $this->spinner('auto_rand_password_limit', 35, __('位数', 'aksrt')), - [ - ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], - ['key' => 'auto_top_s', 'op' => 'checked'], - ], - __('自定义自动生成的长度(不能太短,太短可能会出现重复)如果启用了单密码模式,可以将卡号位数设置为0', 'aksrt') - ); - - // 导入提示统一放在「卡密数据」正上方:切换卡密类型/添加方式时, - // 命中的那一条紧邻示例数据与输入框,便于对照填写。 - $this->field_row( - __('单密码模式', 'aksrt'), - '', - [ - ['key' => 'add_type', 'op' => '==', 'val' => 'import'], - ['key' => 'type', 'op' => '==', 'val' => 'custom'], - ] - ); - $this->submessage_row( - [ - ['key' => 'add_type', 'op' => '!=', 'val' => 'auto'], - ['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'], - ], - '

    ' . esc_html__('导入卡密', 'aksrt') . '

    -
      -
    • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('卡号 密码 面额 标识', 'aksrt') . '
    • -
    • ' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
    • -
    • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
    • ' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '
    • -
    -

    ' . esc_html__('数据示例', 'aksrt') . '

    -
    4849977843617 3w20V4Qe7dRhAIoPYum6KSA 50 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('50' . $this->currency_unit())) . '
    -
    6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq 100 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('100' . $this->currency_unit())) . '
    ' - ); - $this->submessage_row( - [ - ['key' => 'add_type', 'op' => '!=', 'val' => 'auto'], - ['key' => 'type', 'op' => '==', 'val' => 'vip'], - ], - '

    ' . esc_html__('导入会员兑换卡密', 'aksrt') . '

    -
      -
    • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('卡号 密码 会员参数 标识', 'aksrt') . '
    • -
    • ' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
    • -
    • ' . esc_html__('会员参数格式为:', 'aksrt') . 'level=1&time=5&unit=day
    • -
    • ' . esc_html__('参数说明:level为等级,只能为1或2。time为时长,填Permanent或数字。unit为时长单位,只能填day(天)或者month(月)', 'aksrt') . '
    • -
    • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
    • ' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '
    • -
    -

    ' . esc_html__('数据示例', 'aksrt') . '

    -
    4849977843617 3w20V4Qe7dRhAIoPYum6KSA level=1&time=5&unit=day ' . esc_html__('5天一级会员', 'aksrt') . '
    -
    6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq level=2&time=Permanent ' . esc_html__('永久2级会员', 'aksrt') . '
    - ' - ); - $this->submessage_row( - [ - ['key' => 'add_type', 'op' => '==', 'val' => 'import'], - ['key' => 'type', 'op' => '==', 'val' => 'custom'], - ['key' => 'only_pass_s', 'op' => 'unchecked'], - ], - '

    ' . esc_html__('自定义卡密导入', 'aksrt') . '

    -
      -
    • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('卡号 密码 标识 自定义参数', 'aksrt') . '
    • -
    • ' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
    • -
    • ' . esc_html__('自定义参数格式为:', 'aksrt') . 'card_name=授权号&password_name=授权密码,' . esc_html__('card_name为卡号的显示名称,password_name为卡密的显示名称,可以留空', 'aksrt') . '
    • -
    • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
    • -
    -

    ' . esc_html__('数据示例', 'aksrt') . '

    -
    4849977843617 3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '
    -
    6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '
    -
    111111@qq.com 11assssddsds ' . esc_html__('某某账号', 'aksrt') . ' card_name=' . esc_html__('账号', 'aksrt') . '&password_name=' . esc_html__('密码', 'aksrt') . '
    ' - ); - $this->submessage_row( - [ - ['key' => 'add_type', 'op' => '==', 'val' => 'import'], - ['key' => 'type', 'op' => '==', 'val' => 'custom'], - ['key' => 'only_pass_s', 'op' => 'checked'], - ], - '

    ' . esc_html__('自定义卡密导入(单密码模式)', 'aksrt') . '

    -
      -
    • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('密码 标识 自定义参数', 'aksrt') . '
    • -
    • ' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
    • -
    • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
    • -
    • ' . esc_html__('自定义参数格式为:', 'aksrt') . 'password_name=授权密码,' . esc_html__('password_name为卡密的显示名称,可以留空', 'aksrt') . '
    • -
    -

    ' . esc_html__('数据示例', 'aksrt') . '

    -
    3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '
    -
    SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '
    ' - ); - $this->field_row( - __('卡密数据', 'aksrt'), - '', - [['key' => 'add_type', 'op' => '==', 'val' => 'import']] - ); - $this->field_row( - __('自定义分隔符号', 'aksrt'), - '', - [['key' => 'add_type', 'op' => '==', 'val' => 'import']], - __('卡号和密码之间分割符号(默认为空格分割)', 'aksrt') - ); - ?> - - -
    - -
    - - - field_row( - __('卡密类型', 'aksrt'), - $this->button_set('type', $this->type_labels(), 'balance'), - [] - ); - $this->field_row( - __('选择状态', 'aksrt'), - $this->button_set('status', [ - 'all' => __('全部', 'aksrt'), - '0' => __('未使用', 'aksrt'), - 'used' => __('已使用', 'aksrt'), - ], 'all'), - [['key' => 'type', 'op' => '!=', 'val' => 'custom']] - ); - $this->field_row( - __('导出格式', 'aksrt'), - $this->button_set('export_format', [ - 'text' => __('文本文档', 'aksrt'), - 'xls' => __('Excel表格', 'aksrt'), - ], 'xls'), - [] - ); - $this->field_row( - ' ', - __('分隔符号', 'aksrt') . '
    ', - [['key' => 'export_format', 'op' => '==', 'val' => 'text']], - __('卡号和密码之间分割符号(默认为空格分割)', 'aksrt') - ); - ?> - - -
    - page_notice(__('删除完成', 'aksrt'), sprintf(__('已删除%s个卡密', 'aksrt'), (string) $delete_i)); } - /* ------------------------------------------------------------------ - * 渲染辅助 - * ------------------------------------------------------------------ */ - - /** - * @param array $deps - */ - private function field_row(string $title, string $html, array $deps = [], string $desc = ''): void { - $dep_attr = $deps ? ' data-dep="' . esc_attr((string) wp_json_encode($deps)) . '"' : ''; - $hidden = ($deps && !$this->deps_visible($deps)) ? ' style="display:none;"' : ''; - echo '' . esc_html($title) . '' . $html; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。 - if ($desc !== '') { - echo '

    ' . wp_kses_post($desc) . '

    '; - } - echo ''; - } - - /** - * @param array $deps - */ - private function submessage_row(array $deps, string $content): void { - $dep_attr = ' data-dep="' . esc_attr((string) wp_json_encode($deps)) . '"'; - $hidden = $this->deps_visible($deps) ? '' : ' style="display:none;"'; - // 提示块**不要**挂 WP 的 .notice / .notice-warning:wp-admin/js/common.js 在 DOM - // 就绪时会执行 - // $('div.updated, div.error, div.notice').not('.inline, .below-h2').insertAfter($headerEnd) - // 把页面上所有「非 inline」的 notice 整块搬到 .wp-header-end(不存在时取 .wrap 内首个 - // h1/h2)之后。提示块一旦带上 notice,就会被搬到页面顶部、脱离「添加方式—卡密数据」 - // 之间的字段区,而且搬迁发生在求值之后,行内的显隐逻辑无从纠正。 - // 外观改由 .aksrt-submsg 自带(见 print_assets),与参考主题 CSF content 字段一致。 - // data-dep 与初始隐藏状态同时挂在 和内层提示块上:提示块若脱离所在行,仍能自行显隐。 - echo '' . $content . ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。 - } - - /** - * 服务端按表单默认值预判依赖可见性(求值规则与行内 JS 完全一致)。 - * 仅在表单渲染默认值(添加方式=系统自动生成、类型=余额)下,把不满足依赖的行标记为初始隐藏, - * 避免四条导入提示首帧铺开。 - * - * 注意:已知键必须覆盖 field_row/submessage_row 用到的所有依赖键,否则该键的判定会与 JS 不一致, - * 出现「服务端判可见、JS 判隐藏」之类的错配(例如自定义卡密的两条导入提示同时铺开)。 - * - * @param array $deps - */ - private function deps_visible(array $deps): bool { - foreach ($deps as $dep) { - $key = (string) ($dep['key'] ?? ''); - $val = (string) ($dep['val'] ?? ''); - $op = (string) ($dep['op'] ?? ''); - - if (!$this->is_known_dep_key($key)) { - // 未登记的键:不做服务端判定,交 JS 处理。 - continue; - } - - $v = (string) $this->default_form_value($key); - - switch ($op) { - case '==': - if ($v !== $val) { - return false; - } - break; - case '!=': - if ($v === $val) { - return false; - } - break; - case 'any': - if (!in_array($v, explode(',', $val), true)) { - return false; - } - break; - case 'checked': - if ($v === '') { - return false; - } - break; - case 'unchecked': - if ($v !== '') { - return false; - } - break; - } - } - return true; - } - - /** - * 可在服务端求值的字段键(须完整覆盖表单依赖中使用的键,与 default_form_value 保持一致)。 - */ - private function is_known_dep_key(string $key): bool { - static $known = [ - 'add_type', - 'type', - 'auto_top_s', - 'only_pass_s', - 'vip_exchange[time]', - 'export_format', - 'status', - ]; - return in_array($key, $known, true); - } - - /** - * 表单字段的初始默认值(须与 field_row/button_set/spinner 渲染的默认值保持同步)。 - */ - private function default_form_value(string $key): string { - switch ($key) { - case 'add_type': - return 'auto'; - case 'type': - return 'balance'; - case 'export_format': - return 'xls'; - case 'status': - return 'all'; - // 未勾选的 checkbox/switcher 与空文本,在依赖求值中均等价于空字符串。 - case 'auto_top_s': - case 'only_pass_s': - case 'vip_exchange[time]': - return ''; - default: - return ''; - } - } - - /** - * button_set:radio 按钮组(参考主题 CSF button_set 的原生替代)。 - * - * @param array $options - */ - private function button_set(string $name, array $options, string $current): string { - $html = '
    '; - foreach ($options as $value => $label) { - $html .= ''; - } - return $html . '
    '; - } - - /** - * 数字输入(参考主题 CSF spinner / number 字段的原生替代)。 - * - * **一律不写原生 min / max / step 约束属性,只写 `step="any"`:** - * - 本表单的行是条件显隐的(`field_row()` 会给不满足依赖的 `` 打上 `display:none`)。 - * HTML5 约束校验会检查**隐藏**控件,而隐藏控件无法聚焦、浏览器弹不出提示气泡, - * 于是直接**静默拒绝提交**(控制台只有一句 "An invalid form control ... is not focusable"), - * 页面毫无反应 —— 用户看到的就是「确认提交按钮不起作用」。 - * - `step="any"` 不能省:`type="number"` 缺省 `step=1`,会退回整数网格,把非整数输入判为非法, - * 同样触发上面的静默拒绝。 - * - * 取值范围不在标记里表达,由服务端单点约束:生成数量见 `handle_add()` 的 (0, 1000] 判定, - * 卡号 / 密码位数见 `ChargeCard::generate_cards()`。 - */ - private function spinner(string $name, int $value, string $unit): string { - return '' - . ' ' . esc_html($unit) . ''; - } - - private function page_notice(string $title, string $msg, string $type = 'success'): void { - $html = ''; - $html .= $title ? '

    ' . esc_html($title) . '

    ' : ''; - $html .= $msg ? '

    ' . esc_html($msg) . '

    ' : ''; - - if ($html) { - echo '
    ' . $html . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput - } - } - - /** - * 列表筛选参数(不含 paged / page),用于构造翻页链接与「每页显示」表单。 - * 显式列条件而不是依赖 add_query_arg() 默认取 $_SERVER['REQUEST_URI']: - * 后者把链接绑到当前请求上,一旦请求形态变化(POST、代理改写)就会丢条件。 - * - * @return array - */ - private function list_query_args(string $status, string $type, string $other, string $s, string $orderby, string $order, int $per_page): array { - $args = []; - if ($status !== '') { - $args['status'] = $status; - } - if ($type !== '') { - $args['type'] = $type; - } - if ($other !== '') { - $args['other'] = $other; - } - if ($s !== '') { - $args['s'] = $s; - } - if ($orderby !== '' && $orderby !== 'modified_time') { - $args['orderby'] = $orderby; - } - if ($order === 'ASC') { - $args['desc'] = 'ASC'; - } - if ($per_page !== AdminListPagination::DEFAULT_PER_PAGE) { - $args['ice_perpage'] = (string) $per_page; - } - return $args; - } - - /** 行内样式与脚本:依赖显隐 + 点击复制。 */ - private function print_assets(): void { - ?> - - - + + + type_labels(); + ?> +

    +
      +
    • +
    • +
    +
    + + + + field_row( + __('卡密类型', 'aksrt'), + $this->button_set('type', $type_labels, 'balance'), + [] + ); + $this->field_row( + __('添加方式', 'aksrt'), + $this->button_set('add_type', [ + 'auto' => __('系统自动生成', 'aksrt'), + 'import' => __('导入卡密', 'aksrt'), + ], 'auto'), + [] + ); + + // 自动生成 + $this->field_row( + __('生成数量', 'aksrt'), + $this->spinner('auto_num', 20, __('张', 'aksrt')), + [['key' => 'add_type', 'op' => '==', 'val' => 'auto']], + __('需要生成多少张卡密(1 - 1000 的整数,单次生成数量太多可能会对服务器性能造成影响)', 'aksrt') + ); + $this->field_row( + __('标识', 'aksrt'), + '', + [['key' => 'add_type', 'op' => '==', 'val' => 'auto']], + __('对生成的卡密做标记标识,方便后期查找管理', 'aksrt') . '
    ' . sprintf(esc_html__('注意:标识中不能有%1$s字符,系统已占用', 'aksrt'), 'shipped') + ); + $this->field_row( + __('面额', 'aksrt'), + '', + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], + ['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'], + ], + __('单张卡密的面额', 'aksrt') + ); + $this->field_row( + __('兑换会员', 'aksrt'), + __('会员等级', 'aksrt') . ' ' . $this->button_set('vip_exchange[level]', [ + '1' => $this->vip_level_name(1), + '2' => $this->vip_level_name(2), + ], '1'), + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], + ['key' => 'type', 'op' => '==', 'val' => 'vip'], + ] + ); + $this->field_row( + ' ', + __('兑换会员时长', 'aksrt') . '
    ', + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], + ['key' => 'type', 'op' => '==', 'val' => 'vip'], + ], + __('填', 'aksrt') . 'Permanent' . __('为永久', 'aksrt') + ); + $this->field_row( + ' ', + __('时长单位', 'aksrt') . ' ' . $this->button_set('vip_exchange[unit]', [ + 'day' => __('天', 'aksrt'), + 'month' => __('个月', 'aksrt'), + ], 'month'), + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], + ['key' => 'type', 'op' => '==', 'val' => 'vip'], + ['key' => 'vip_exchange[time]', 'op' => '!=', 'val' => 'Permanent'], + ] + ); + $this->field_row( + __('自定义显示名称', 'aksrt'), + __('卡号显示名称', 'aksrt') . '

    ' . + __('卡密显示名称', 'aksrt') . '
    ', + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], + ['key' => 'type', 'op' => '==', 'val' => 'custom'], + ] + ); + $this->field_row( + __('高级选项', 'aksrt'), + '', + [['key' => 'add_type', 'op' => '==', 'val' => 'auto']] + ); + $this->field_row( + __('自定义卡号位数', 'aksrt'), + $this->spinner('auto_rand_number_limit', 20, __('位数', 'aksrt')), + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], + ['key' => 'auto_top_s', 'op' => 'checked'], + ] + ); + $this->field_row( + __('自定义密码位数', 'aksrt'), + $this->spinner('auto_rand_password_limit', 35, __('位数', 'aksrt')), + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'auto'], + ['key' => 'auto_top_s', 'op' => 'checked'], + ], + __('自定义自动生成的长度(不能太短,太短可能会出现重复)如果启用了单密码模式,可以将卡号位数设置为0', 'aksrt') + ); + + // 导入提示统一放在「卡密数据」正上方:切换卡密类型/添加方式时, + // 命中的那一条紧邻示例数据与输入框,便于对照填写。 + $this->field_row( + __('单密码模式', 'aksrt'), + '', + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'import'], + ['key' => 'type', 'op' => '==', 'val' => 'custom'], + ] + ); + $this->submessage_row( + [ + ['key' => 'add_type', 'op' => '!=', 'val' => 'auto'], + ['key' => 'type', 'op' => 'any', 'val' => 'balance,credits'], + ], + '

    ' . esc_html__('导入卡密', 'aksrt') . '

    +
      +
    • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('卡号 密码 面额 标识', 'aksrt') . '
    • +
    • ' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
    • +
    • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
    • ' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '
    • +
    +

    ' . esc_html__('数据示例', 'aksrt') . '

    +
    4849977843617 3w20V4Qe7dRhAIoPYum6KSA 50 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('50' . $this->currency_unit())) . '
    +
    6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq 100 ' . sprintf(esc_html__('%1$s余额', 'aksrt'), esc_html('100' . $this->currency_unit())) . '
    ' + ); + $this->submessage_row( + [ + ['key' => 'add_type', 'op' => '!=', 'val' => 'auto'], + ['key' => 'type', 'op' => '==', 'val' => 'vip'], + ], + '

    ' . esc_html__('导入会员兑换卡密', 'aksrt') . '

    +
      +
    • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('卡号 密码 会员参数 标识', 'aksrt') . '
    • +
    • ' . esc_html__('卡号、密码、面额、标识默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
    • +
    • ' . esc_html__('会员参数格式为:', 'aksrt') . 'level=1&time=5&unit=day
    • +
    • ' . esc_html__('参数说明:level为等级,只能为1或2。time为时长,填Permanent或数字。unit为时长单位,只能填day(天)或者month(月)', 'aksrt') . '
    • +
    • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
    • ' . esc_html__('如果使用的是单密码模式,卡号部分随便填什么都行,但不能留空', 'aksrt') . '
    • +
    +

    ' . esc_html__('数据示例', 'aksrt') . '

    +
    4849977843617 3w20V4Qe7dRhAIoPYum6KSA level=1&time=5&unit=day ' . esc_html__('5天一级会员', 'aksrt') . '
    +
    6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq level=2&time=Permanent ' . esc_html__('永久2级会员', 'aksrt') . '
    + ' + ); + $this->submessage_row( + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'import'], + ['key' => 'type', 'op' => '==', 'val' => 'custom'], + ['key' => 'only_pass_s', 'op' => 'unchecked'], + ], + '

    ' . esc_html__('自定义卡密导入', 'aksrt') . '

    +
      +
    • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('卡号 密码 标识 自定义参数', 'aksrt') . '
    • +
    • ' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
    • +
    • ' . esc_html__('自定义参数格式为:', 'aksrt') . 'card_name=授权号&password_name=授权密码,' . esc_html__('card_name为卡号的显示名称,password_name为卡密的显示名称,可以留空', 'aksrt') . '
    • +
    • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
    • +
    +

    ' . esc_html__('数据示例', 'aksrt') . '

    +
    4849977843617 3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '
    +
    6484027668705 SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' card_name=' . esc_html__('授权号', 'aksrt') . '&password_name=' . esc_html__('授权码', 'aksrt') . '
    +
    111111@qq.com 11assssddsds ' . esc_html__('某某账号', 'aksrt') . ' card_name=' . esc_html__('账号', 'aksrt') . '&password_name=' . esc_html__('密码', 'aksrt') . '
    ' + ); + $this->submessage_row( + [ + ['key' => 'add_type', 'op' => '==', 'val' => 'import'], + ['key' => 'type', 'op' => '==', 'val' => 'custom'], + ['key' => 'only_pass_s', 'op' => 'checked'], + ], + '

    ' . esc_html__('自定义卡密导入(单密码模式)', 'aksrt') . '

    +
      +
    • ' . esc_html__('一行一个卡密,单行格式为:', 'aksrt') . '' . esc_html__('密码 标识 自定义参数', 'aksrt') . '
    • +
    • ' . esc_html__('默认使用空格分割,您可以在下方自定义分割符号,与您的数据对应即可', 'aksrt') . '
    • +
    • ' . esc_html__('单次导入数量太多可能会对服务器性能造成影响', 'aksrt') . '
    • +
    • ' . esc_html__('自定义参数格式为:', 'aksrt') . 'password_name=授权密码,' . esc_html__('password_name为卡密的显示名称,可以留空', 'aksrt') . '
    • +
    +

    ' . esc_html__('数据示例', 'aksrt') . '

    +
    3w20V4Qe7dRhAIoPYum6KSA ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '
    +
    SkdJZ5vWcpqPxrbs3pqPcpq ' . esc_html__('平台卡密01', 'aksrt') . ' password_name=' . esc_html__('授权码', 'aksrt') . '
    ' + ); + $this->field_row( + __('卡密数据', 'aksrt'), + '', + [['key' => 'add_type', 'op' => '==', 'val' => 'import']] + ); + $this->field_row( + __('自定义分隔符号', 'aksrt'), + '', + [['key' => 'add_type', 'op' => '==', 'val' => 'import']], + __('卡号和密码之间分割符号(默认为空格分割)', 'aksrt') + ); + ?> + + +
    + +
    + + + field_row( + __('卡密类型', 'aksrt'), + $this->button_set('type', $this->type_labels(), 'balance'), + [] + ); + $this->field_row( + __('选择状态', 'aksrt'), + $this->button_set('status', [ + 'all' => __('全部', 'aksrt'), + '0' => __('未使用', 'aksrt'), + 'used' => __('已使用', 'aksrt'), + ], 'all'), + [['key' => 'type', 'op' => '!=', 'val' => 'custom']] + ); + $this->field_row( + __('导出格式', 'aksrt'), + $this->button_set('export_format', [ + 'text' => __('文本文档', 'aksrt'), + 'xls' => __('Excel表格', 'aksrt'), + ], 'xls'), + [] + ); + $this->field_row( + ' ', + __('分隔符号', 'aksrt') . '
    ', + [['key' => 'export_format', 'op' => '==', 'val' => 'text']], + __('卡号和密码之间分割符号(默认为空格分割)', 'aksrt') + ); + ?> + + +
    + $deps + */ + private function field_row(string $title, string $html, array $deps = [], string $desc = ''): void { + $dep_attr = $deps ? ' data-dep="' . esc_attr((string) wp_json_encode($deps)) . '"' : ''; + $hidden = ($deps && !$this->deps_visible($deps)) ? ' style="display:none;"' : ''; + echo '' . esc_html($title) . '' . $html; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。 + if ($desc !== '') { + echo '

    ' . wp_kses_post($desc) . '

    '; + } + echo ''; + } + + /** + * @param array $deps + */ + private function submessage_row(array $deps, string $content): void { + $dep_attr = ' data-dep="' . esc_attr((string) wp_json_encode($deps)) . '"'; + $hidden = $this->deps_visible($deps) ? '' : ' style="display:none;"'; + // 提示块**不要**挂 WP 的 .notice / .notice-warning:wp-admin/js/common.js 在 DOM + // 就绪时会执行 + // $('div.updated, div.error, div.notice').not('.inline, .below-h2').insertAfter($headerEnd) + // 把页面上所有「非 inline」的 notice 整块搬到 .wp-header-end(不存在时取 .wrap 内首个 + // h1/h2)之后。提示块一旦带上 notice,就会被搬到页面顶部、脱离「添加方式—卡密数据」 + // 之间的字段区,而且搬迁发生在求值之后,行内的显隐逻辑无从纠正。 + // 外观改由 .aksrt-submsg 自带(见 print_assets),与参考主题 CSF content 字段一致。 + // data-dep 与初始隐藏状态同时挂在 和内层提示块上:提示块若脱离所在行,仍能自行显隐。 + echo '' . $content . ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 文案已转义。 + } + + /** + * 服务端按表单默认值预判依赖可见性(求值规则与行内 JS 完全一致)。 + * 仅在表单渲染默认值(添加方式=系统自动生成、类型=余额)下,把不满足依赖的行标记为初始隐藏, + * 避免四条导入提示首帧铺开。 + * + * 注意:已知键必须覆盖 field_row/submessage_row 用到的所有依赖键,否则该键的判定会与 JS 不一致, + * 出现「服务端判可见、JS 判隐藏」之类的错配(例如自定义卡密的两条导入提示同时铺开)。 + * + * @param array $deps + */ + private function deps_visible(array $deps): bool { + foreach ($deps as $dep) { + $key = (string) ($dep['key'] ?? ''); + $val = (string) ($dep['val'] ?? ''); + $op = (string) ($dep['op'] ?? ''); + + if (!$this->is_known_dep_key($key)) { + // 未登记的键:不做服务端判定,交 JS 处理。 + continue; + } + + $v = (string) $this->default_form_value($key); + + switch ($op) { + case '==': + if ($v !== $val) { + return false; + } + break; + case '!=': + if ($v === $val) { + return false; + } + break; + case 'any': + if (!in_array($v, explode(',', $val), true)) { + return false; + } + break; + case 'checked': + if ($v === '') { + return false; + } + break; + case 'unchecked': + if ($v !== '') { + return false; + } + break; + } + } + return true; + } + + /** + * 可在服务端求值的字段键(须完整覆盖表单依赖中使用的键,与 default_form_value 保持一致)。 + */ + private function is_known_dep_key(string $key): bool { + static $known = [ + 'add_type', + 'type', + 'auto_top_s', + 'only_pass_s', + 'vip_exchange[time]', + 'export_format', + 'status', + ]; + return in_array($key, $known, true); + } + + /** + * 表单字段的初始默认值(须与 field_row/button_set/spinner 渲染的默认值保持同步)。 + */ + private function default_form_value(string $key): string { + switch ($key) { + case 'add_type': + return 'auto'; + case 'type': + return 'balance'; + case 'export_format': + return 'xls'; + case 'status': + return 'all'; + // 未勾选的 checkbox/switcher 与空文本,在依赖求值中均等价于空字符串。 + case 'auto_top_s': + case 'only_pass_s': + case 'vip_exchange[time]': + return ''; + default: + return ''; + } + } + + /** + * button_set:radio 按钮组(参考主题 CSF button_set 的原生替代)。 + * + * @param array $options + */ + private function button_set(string $name, array $options, string $current): string { + $html = '
    '; + foreach ($options as $value => $label) { + $html .= ''; + } + return $html . '
    '; + } + + /** + * 数字输入(参考主题 CSF spinner / number 字段的原生替代)。 + * + * **一律不写原生 min / max / step 约束属性,只写 `step="any"`:** + * - 本表单的行是条件显隐的(`field_row()` 会给不满足依赖的 `` 打上 `display:none`)。 + * HTML5 约束校验会检查**隐藏**控件,而隐藏控件无法聚焦、浏览器弹不出提示气泡, + * 于是直接**静默拒绝提交**(控制台只有一句 "An invalid form control ... is not focusable"), + * 页面毫无反应 —— 用户看到的就是「确认提交按钮不起作用」。 + * - `step="any"` 不能省:`type="number"` 缺省 `step=1`,会退回整数网格,把非整数输入判为非法, + * 同样触发上面的静默拒绝。 + * + * 取值范围不在标记里表达,由服务端单点约束:生成数量见 `handle_add()` 的 (0, 1000] 判定, + * 卡号 / 密码位数见 `ChargeCard::generate_cards()`。 + */ + private function spinner(string $name, int $value, string $unit): string { + return '' + . ' ' . esc_html($unit) . ''; + } + + private function page_notice(string $title, string $msg, string $type = 'success'): void { + $html = ''; + $html .= $title ? '

    ' . esc_html($title) . '

    ' : ''; + $html .= $msg ? '

    ' . esc_html($msg) . '

    ' : ''; + + if ($html) { + echo '
    ' . $html . '
    '; // phpcs:ignore WordPress.Security.EscapeOutput + } + } +} diff --git a/src/Admin/Native/PayChargeCardsList.php b/src/Admin/Native/PayChargeCardsList.php new file mode 100644 index 00000000..80fc0903 --- /dev/null +++ b/src/Admin/Native/PayChargeCardsList.php @@ -0,0 +1,241 @@ + __('全部', 'aksrt'), 'href' => $this->page_url], + ]; + if ($s === '') { + $sub_but[] = ['name' => __('未使用', 'aksrt'), 'href' => add_query_arg('status', '0', $this->page_url)]; + $sub_but[] = ['name' => __('已使用', 'aksrt'), 'href' => add_query_arg('status', 'used', $this->page_url)]; + foreach ($this->type_labels() as $_k => $_v) { + $sub_but[] = ['name' => $_v, 'href' => add_query_arg('type', $_k, $this->page_url)]; + } + } + $but_html = ''; + foreach ($sub_but as $but) { + $but_html .= '
  • ' . esc_html($but['name']) . '
  • | '; + } + $but_html = substr($but_html, 0, -2); + + $search = '
    +
    + + +
    +
    '; + + // 当前筛选参数(不含 paged)。分页链接显式带上它,不再依赖 add_query_arg() + // 默认取 $_SERVER['REQUEST_URI'](链接会随环境漂移,且列条件容易丢)。 + $query_args = $this->list_query_args($status, $type, $other, $s, $orderby, $order, $per_page); + + echo '
      ' . $but_html . '
    ' . $search . '
    '; + + $filters = [ + 'status' => $status, + 'type' => $type, + 'other' => $other, + 'search' => $s, + 'orderby' => $orderby, + 'order' => $order, + ]; + + $result = $svc->get_list($filters + ['limit' => $per_page, 'offset' => ($paged - 1) * $per_page]); + $items = (array) ($result['items'] ?? []); + $count_all = (int) ($result['total'] ?? 0); + + // 页码越界(手工改 URL、或筛选后总数变小)会让 offset 落到数据之外, + // 表现为「空白表格 + 一个分页控件都没有」的死胡同,只能靠手改 URL 退出。 + // 这里回退到最后一页重查一次,保证越界页码也能拿到数据与分页条。 + $max_page = max(1, (int) ($result['max_pages'] ?? 1)); + if (!$items && $count_all > 0 && $paged > $max_page) { + $paged = $max_page; + $result = $svc->get_list($filters + ['limit' => $per_page, 'offset' => ($paged - 1) * $per_page]); + $items = (array) ($result['items'] ?? []); + $count_all = (int) ($result['total'] ?? $count_all); + } + + if (!$items) { + NativeUi::table_card_open(); + echo ''; + echo '' . NativeUi::empty_row(1, esc_html__('未找到对应内容,或暂无内容', 'aksrt')) . ''; + echo '
    '; + NativeUi::table_wrap_close(); + NativeUi::card_close(); + return; + } + + // 表头(与参考主题同列、同排序链接)。 + $theads = [ + ['width' => '6%', 'orderby' => 'card', 'name' => __('卡号', 'aksrt')], + ['width' => '10%', 'orderby' => 'password', 'name' => __('密码', 'aksrt')], + ['width' => '5%', 'orderby' => 'type', 'name' => __('类型', 'aksrt')], + ['width' => '5%', 'orderby' => 'create_time', 'name' => __('创建时间', 'aksrt')], + ['width' => '5%', 'orderby' => 'modified_time', 'name' => __('更新时间', 'aksrt')], + ['width' => '5%', 'orderby' => 'status', 'name' => __('状态', 'aksrt')], + ['width' => '8%', 'orderby' => 'other', 'name' => __('标识', 'aksrt')], + ]; + + $thead_th = ''; + foreach ($theads as $thead) { + $orderby_links = ''; + if ($thead['orderby']) { + $orderby_url = add_query_arg('orderby', $thead['orderby']); + $orderby_links .= ''; + $orderby_links .= ''; + $orderby_links = '' . $orderby_links . ''; + } + $thead_th .= '' . esc_html($thead['name']) . $orderby_links . ''; + } + + $tbody = ''; + foreach ($items as $msg) { + $meta = []; + if (!empty($msg['meta'])) { + $decoded = json_decode((string) $msg['meta'], true); + $meta = is_array($decoded) ? $decoded : []; + } + + // 自定义卡密不展示状态;其余类型按状态着色(类型链接统一在下方链表中生成)。 + $status_html = ''; + if ($msg['type'] !== 'custom') { + $status_html = '' . esc_html__('未使用', 'aksrt') . ''; + if ($msg['status'] === 'used') { + $status_html = '' . esc_html__('已使用', 'aksrt') . ''; + if (!empty($msg['order_number'])) { + $order_link_url = admin_url('admin.php?page=aksrt_orders&s=' . urlencode((string) $msg['order_number'])); + $status_html .= ' [' . esc_html__('查看', 'aksrt') . ']'; + } + } elseif ($msg['status'] === 'expired') { + $status_html = '' . esc_html__('已过期', 'aksrt') . ''; + } elseif ($msg['status'] === 'disabled') { + $status_html = '' . esc_html__('已禁用', 'aksrt') . ''; + } + } + + $other_a = ''; + if (!empty($msg['note'])) { + $other_a = '' . esc_html($msg['note']) . ''; + } + + if ($msg['type'] === 'vip') { + $type_html = '' . esc_html__('会员兑换', 'aksrt') . ''; + $level = (int) ($meta['level'] ?? 1); + $type_html .= '
    ' . esc_html($this->vip_level_name($level)) . ':' . esc_html($this->vip_time_text($meta)); + } elseif ($msg['type'] === 'credits') { + $type_html = '' . esc_html__('积分兑换', 'aksrt') . ''; + $type_html .= '
    ' . esc_html__('积分:', 'aksrt') . esc_html((string) (int) ($meta['points'] ?? 0)); + } elseif ($msg['type'] === 'balance') { + $type_html = '' . esc_html__('余额充值', 'aksrt') . ''; + $type_html .= '
    ' . esc_html__('金额:', 'aksrt') . esc_html($this->balance_amount_text($meta)); + } else { + $type_html = '' . esc_html__('自定义卡密', 'aksrt') . ''; + } + + $card_name = (string) ($meta['custom_name']['card_name'] ?? ''); + $password_name = (string) ($meta['custom_name']['password_name'] ?? ''); + + $tbody .= ''; + $tbody .= ' + + '; + $tbody .= "" . esc_html($card_name) . ' ' . esc_html((string) $msg['card']) . ''; + $tbody .= "" . esc_html($password_name) . ' ' . esc_html((string) $msg['password']) . ''; + $tbody .= "$type_html"; + $tbody .= '' . esc_html((string) $msg['created_at']) . ''; + $tbody .= '' . esc_html((string) ($msg['used_at'] ?: $msg['created_at'])) . ''; + $tbody .= "$status_html"; + $tbody .= "$other_a"; + $tbody .= ''; + } + + echo '
    '; + wp_nonce_field(self::NONCE); + echo '
    + + +
    '; + + NativeUi::table_card_open(); + echo ''; + echo '' . $thead_th . ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。 + echo '' . $tbody . ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 已转义。 + echo '
    '; + NativeUi::table_wrap_close(); + AdminListPagination::render(self::PAGE_SLUG, $count_all, $paged, $per_page, $query_args); + NativeUi::card_close(); + echo '
    '; + } + + /** + * 列表筛选参数(不含 paged / page),用于构造翻页链接与「每页显示」表单。 + * 显式列条件而不是依赖 add_query_arg() 默认取 $_SERVER['REQUEST_URI']: + * 后者把链接绑到当前请求上,一旦请求形态变化(POST、代理改写)就会丢条件。 + * + * @return array + */ + private function list_query_args(string $status, string $type, string $other, string $s, string $orderby, string $order, int $per_page): array { + $args = []; + if ($status !== '') { + $args['status'] = $status; + } + if ($type !== '') { + $args['type'] = $type; + } + if ($other !== '') { + $args['other'] = $other; + } + if ($s !== '') { + $args['s'] = $s; + } + if ($orderby !== '' && $orderby !== 'modified_time') { + $args['orderby'] = $orderby; + } + if ($order === 'ASC') { + $args['desc'] = 'ASC'; + } + if ($per_page !== AdminListPagination::DEFAULT_PER_PAGE) { + $args['ice_perpage'] = (string) $per_page; + } + return $args; + } +} diff --git a/src/Admin/Native/PayCoupons.php b/src/Admin/Native/PayCoupons.php index 8b46a33c..4a423758 100644 --- a/src/Admin/Native/PayCoupons.php +++ b/src/Admin/Native/PayCoupons.php @@ -34,11 +34,18 @@ class PayCoupons { public function render_page(): void { $this->ensure_cap(); - $this->render_header(__('优惠码', 'aksrt')); + + NativeUi::page_open(); + NativeUi::header( + __('优惠码', 'aksrt'), + __('创建、批量生成与管理结算优惠码。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt'), + 'dashicons-tickets-alt' + ); + $this->render_flash(); if (!class_exists('\AKSRT\Payment\Coupon')) { echo '

    ' . esc_html__('优惠码模块未初始化。', 'aksrt') . '

    '; - $this->render_footer(); + NativeUi::page_close(); return; } @@ -56,7 +63,8 @@ class PayCoupons { $this->render_batch_form(); $this->render_generated_dump(); $this->render_list($svc); - $this->render_footer(); + + NativeUi::page_close(); } /** @@ -70,83 +78,119 @@ class PayCoupons { $expires = (string) ($row['expires_at'] ?? ''); $expires_local = $expires !== '' ? str_replace(' ', 'T', substr($expires, 0, 16)) : ''; - echo '

    ' . esc_html(sprintf( + $title = sprintf( /* translators: %d coupon id */ __('编辑优惠码 #%d', 'aksrt'), $id - )) . '

    '; - echo '

    ← ' . esc_html__('返回列表', 'aksrt') . '

    '; - echo '
    '; + ); + $back = NativeUi::button_link(__('返回列表', 'aksrt'), $this->page_url(), false, 'dashicons-arrow-left-alt'); + + NativeUi::card_open($title, $back); + echo ''; echo ''; echo ''; wp_nonce_field('aksrt_pay_update_coupon_' . $id); - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - $this->render_scope_fields($scope_type, implode(',', $scope_ids)); - echo '

    '; + echo '
    '; + $this->field(__('代码', 'aksrt'), ''); + $this->field(__('类型', 'aksrt'), $this->type_select((string) ($row['type'] ?? ''))); + $this->field(__('面值', 'aksrt'), ''); + $this->field(__('最低消费', 'aksrt'), ''); + $this->field(__('最高优惠(百分比用)', 'aksrt'), ''); + $this->field(__('总次数(0=不限)', 'aksrt'), ''); + $this->field(__('每人限次(0=不限)', 'aksrt'), ''); + $this->scope_fields($scope_type, implode(',', $scope_ids)); + $this->field(__('过期时间', 'aksrt'), ''); + echo '
    '; + echo '
    '; submit_button(__('保存修改', 'aksrt'), 'primary', '', false); - echo '
    '; + echo '
    '; + echo ''; + NativeUi::card_close(); } private function render_create_form(): void { - echo '

    ' . esc_html__('新建优惠码', 'aksrt') . '

    '; - echo '
    '; + NativeUi::card_open(__('新建优惠码', 'aksrt')); + echo ''; echo ''; wp_nonce_field('aksrt_pay_create_coupon'); - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - $this->render_scope_fields('', ''); - echo '

    '; + echo '
    '; + $this->field(__('代码', 'aksrt'), ''); + $this->field(__('类型', 'aksrt'), $this->type_select('')); + $this->field(__('面值', 'aksrt'), ''); + $this->field(__('最低消费', 'aksrt'), ''); + $this->field(__('最高优惠(百分比用)', 'aksrt'), ''); + $this->field(__('总次数(0=不限)', 'aksrt'), ''); + $this->field(__('每人限次(0=不限)', 'aksrt'), ''); + $this->scope_fields('', ''); + $this->field(__('过期时间', 'aksrt'), ''); + echo '
    '; + echo '
    '; submit_button(__('创建', 'aksrt'), 'primary', '', false); + echo '
    '; echo '
    '; + NativeUi::card_close(); } private function render_batch_form(): void { - echo '

    ' . esc_html__('批量生成优惠码', 'aksrt') . '

    '; - echo '
    '; + echo '
    '; + echo '' . esc_html__('批量生成优惠码', 'aksrt') . ''; + echo '
    '; + echo ''; echo ''; wp_nonce_field('aksrt_pay_batch_coupons'); - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - echo '

    '; - $this->render_scope_fields('', ''); - echo '

    '; + echo '
    '; + $this->field(__('数量', 'aksrt'), ''); + $this->field(__('长度', 'aksrt'), ''); + $this->field(__('前缀', 'aksrt'), ''); + $this->field(__('类型', 'aksrt'), $this->type_select('fixed')); + $this->field(__('面值', 'aksrt'), ''); + $this->field(__('最低消费', 'aksrt'), ''); + $this->field(__('最高优惠', 'aksrt'), ''); + $this->field(__('总次数(0=不限)', 'aksrt'), ''); + $this->field(__('每人限次', 'aksrt'), ''); + $this->scope_fields('', ''); + $this->field(__('过期时间', 'aksrt'), ''); + echo '
    '; + echo '
    '; submit_button(__('批量生成', 'aksrt'), 'secondary', '', false); + echo '
    '; echo ''; + echo '
    '; + echo '
    '; } - private function render_scope_fields(string $selected_type, string $ids_text): void { - echo '

    '; - echo '

    '; + $select .= ''; + $this->field(__('适用范围', 'aksrt'), $select); + $this->field( + __('范围 ID', 'aksrt'), + '' + ); } private function render_generated_dump(): void { @@ -161,9 +205,9 @@ class PayCoupons { $generated_codes = get_transient('aksrt_pay_coupon_codes_' . get_current_user_id()); if (is_array($generated_codes) && $generated_codes) { delete_transient('aksrt_pay_coupon_codes_' . get_current_user_id()); - echo '

    ' . esc_html__('本次生成优惠码(请及时复制保存):', 'aksrt') . '

    '; + NativeUi::card_open(__('本次生成优惠码(请及时复制保存)', 'aksrt')); echo ''; - echo '
    '; + NativeUi::card_close(); } } @@ -176,21 +220,22 @@ class PayCoupons { 'fixed' => __('固定金额', 'aksrt'), 'percent' => __('折扣百分比', 'aksrt'), ]; - $status_labels = [ - 'active' => __('可用', 'aksrt'), - 'disabled' => __('已禁用', 'aksrt'), + $status_map = [ + 'active' => [__('可用', 'aksrt'), 'success'], + 'disabled' => [__('已禁用', 'aksrt'), 'neutral'], ]; - echo '
    '; - echo ''; - echo ''; + $filters .= ' '; - echo ' '; - submit_button(__('筛选', 'aksrt'), 'secondary', '', false); - echo '
    '; + $filters .= ''; + $filters .= ''; + $filters .= ''; + $filters .= ''; $result = $svc->get_list([ 'status' => $status, @@ -215,11 +260,12 @@ class PayCoupons { $items = $result['items'] ?? []; } + NativeUi::table_card_open(__('优惠码列表', 'aksrt'), $filters); echo ''; echo ''; echo ''; if (!$items) { - echo ''; + echo NativeUi::empty_row(10, __('暂无优惠码', 'aksrt'), __('在上方创建或批量生成优惠码。', 'aksrt')); // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。 } foreach ($items as $row) { $id = (int) ($row['id'] ?? 0); @@ -233,26 +279,26 @@ class PayCoupons { echo ''; echo ''; echo ''; - echo ''; + echo ''; // phpcs:ignore WordPress.Security.EscapeOutput -- 内部构造,已转义。 echo ''; - echo ''; + echo ''; } // 翻页链接显式带上当前筛选条件(不含 paged),口径与上方筛选表单一致。 $query_args = array_filter([ @@ -263,7 +309,9 @@ class PayCoupons { }); echo '
    ID' . esc_html__('代码', 'aksrt') . '' . esc_html__('类型', 'aksrt') . '' . esc_html__('面值', 'aksrt') . '' . esc_html__('门槛/封顶', 'aksrt') . '' . esc_html__('范围', 'aksrt') . '' . esc_html__('已用/上限/每人', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('过期', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
    ' . esc_html__('暂无优惠码', 'aksrt') . '
    ' . esc_html((string) ($row['min_amount'] ?? 0)) . ' / ' . esc_html((string) ($row['max_discount'] ?? 0)) . '' . esc_html($this->format_scope_label($row['product_scope'] ?? [])) . '' . esc_html((string) ((int) ($row['used_count'] ?? 0)) . '/' . (string) ((int) ($row['usage_limit'] ?? 0)) . '/' . (string) ((int) ($row['per_user_limit'] ?? 0))) . '' . esc_html($status_labels[$st] ?? $st) . '' . NativeUi::status_badge($st, $status_map) . '' . esc_html((string) ($row['expires_at'] ?? '')) . ''; + echo '
    '; echo '' . esc_html__('编辑', 'aksrt') . ''; if ($st === 'active') { - echo '
    '; + echo ''; echo ''; echo ''; wp_nonce_field('aksrt_pay_disable_coupon_' . $id); submit_button(__('禁用', 'aksrt'), 'small', '', false); echo '
    '; } elseif ($st === 'disabled') { - echo '
    '; + echo ''; echo ''; echo ''; wp_nonce_field('aksrt_pay_enable_coupon_' . $id); submit_button(__('启用', 'aksrt'), 'small', '', false); echo '
    '; } - echo '
    '; + NativeUi::table_wrap_close(); AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args); + NativeUi::card_close(); } public function handle_create_coupon(): void { @@ -440,16 +488,6 @@ class PayCoupons { } } - private function render_header(string $title): void { - echo '
    '; - echo '

    ' . esc_html($title) . '

    '; - $this->render_flash(); - } - - private function render_footer(): void { - echo '
    '; - } - private function render_flash(): void { if (empty($_GET['aksrt_pay_msg'])) { return; diff --git a/src/Admin/Native/PayIncome.php b/src/Admin/Native/PayIncome.php index d10cb3ec..fc6f28b2 100644 --- a/src/Admin/Native/PayIncome.php +++ b/src/Admin/Native/PayIncome.php @@ -31,11 +31,13 @@ class PayIncome { public function render_income_page(): void { $this->ensure_cap(); - $this->render_header(__('创作分成', 'aksrt')); + NativeUi::page_open(); + NativeUi::header(__('创作分成', 'aksrt'), '', 'dashicons-chart-area'); + $this->render_flash(); if (!class_exists('\AKSRT\Payment\Revenue')) { echo '

    ' . esc_html__('分成模块未初始化。', 'aksrt') . '

    '; - $this->render_footer(); + NativeUi::page_close(); return; } @@ -67,24 +69,17 @@ class PayIncome { return '0.00'; } }; - echo '

    ' . esc_html(sprintf( - __('现金:合计 ¥%1$s · 待结算 ¥%2$s · 已结算 ¥%3$s;积分:合计 %4$s · 待结算 %5$s · 已结算 %6$s', 'aksrt'), - $cash_amount($cash_stats['total'] ?? '0'), - $cash_amount($cash_stats['pending'] ?? '0'), - $cash_amount($cash_stats['settled'] ?? '0'), - number_format_i18n(max(0, (int) ($points_stats['total'] ?? 0))), - number_format_i18n(max(0, (int) ($points_stats['pending'] ?? 0))), - number_format_i18n(max(0, (int) ($points_stats['settled'] ?? 0))) - )) . '

    '; + NativeUi::stats([ + ['label' => __('现金合计', 'aksrt'), 'value' => $cash_amount($cash_stats['total'] ?? '0'), 'unit' => '¥', 'icon' => 'dashicons-chart-area', 'tone' => 'info'], + ['label' => __('现金待结算', 'aksrt'), 'value' => $cash_amount($cash_stats['pending'] ?? '0'), 'unit' => '¥', 'icon' => 'dashicons-clock', 'tone' => 'warning'], + ['label' => __('现金已结算', 'aksrt'), 'value' => $cash_amount($cash_stats['settled'] ?? '0'), 'unit' => '¥', 'icon' => 'dashicons-yes-alt', 'tone' => 'success'], + ['label' => __('积分合计', 'aksrt'), 'value' => number_format_i18n(max(0, (int) ($points_stats['total'] ?? 0))), 'icon' => 'dashicons-star-filled', 'tone' => 'info'], + ['label' => __('积分待结算', 'aksrt'), 'value' => number_format_i18n(max(0, (int) ($points_stats['pending'] ?? 0))), 'icon' => 'dashicons-clock', 'tone' => 'warning'], + ['label' => __('积分已结算', 'aksrt'), 'value' => number_format_i18n(max(0, (int) ($points_stats['settled'] ?? 0))), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'], + ]); - echo '
    '; - echo ''; - echo ''; - wp_nonce_field('aksrt_pay_batch_settle_income'); - submit_button(__('批量结算选中记录', 'aksrt'), 'secondary', '', false); - echo '
    '; - - echo '
    '; + NativeUi::card_open(__('筛选', 'aksrt')); + echo ''; echo ''; echo ''; + echo ''; + wp_nonce_field('aksrt_pay_batch_settle_income'); + submit_button(__('批量结算选中记录', 'aksrt'), 'secondary', '', false); + echo '
    '; + $batch_actions = ob_get_clean(); + + NativeUi::table_card_open(__('分成记录', 'aksrt'), $batch_actions); echo ''; echo ''; echo ''; if (!$items) { - echo ''; + echo NativeUi::empty_row(8, __('暂无分成记录', 'aksrt')); } + $status_map = [ + 'pending' => [__('待结算', 'aksrt'), 'warning'], + 'settled' => [__('已结算', 'aksrt'), 'success'], + ]; foreach ($items as $row) { $id = (int) ($row['id'] ?? 0); $user_id = (int) ($row['author_id'] ?? 0); - $user = $user_id > 0 ? get_userdata($user_id) : null; $st = (string) ($row['status'] ?? ''); echo ''; echo ''; - echo ''; - echo ''; + echo ''; + echo ''; $is_points = (string) ($row['currency'] ?? 'CNY') === 'points'; $row_currency = $is_points ? __('积分', 'aksrt') : '¥'; $row_amount = $is_points ? number_format_i18n(max(0, (int) ($row['amount'] ?? 0))) : $cash_amount($row['amount'] ?? '0'); - echo ''; - echo ''; + echo ''; + echo ''; echo ''; echo ''; } echo '
    ID' . esc_html__('用户', 'aksrt') . '' . esc_html__('订单号', 'aksrt') . '' . esc_html__('金额', 'aksrt') . '' . esc_html__('状态', 'aksrt') . '' . esc_html__('时间', 'aksrt') . '' . esc_html__('操作', 'aksrt') . '
    ' . esc_html__('暂无分成记录', 'aksrt') . '
    ' . ($st === 'pending' ? '' : '') . '' . esc_html((string) $id) . '' . ($user ? esc_html($user->display_name) : esc_html((string) $user_id)) . '' . esc_html((string) ($row['order_number'] ?? '')) . '' . esc_html((string) $id) . '' . NativeUi::user_cell($user_id) . '' . esc_html((string) ($row['order_number'] ?? '')) . '' . esc_html($row_amount . ' ' . $row_currency) . '' . esc_html($st) . '' . esc_html($row_amount . ' ' . $row_currency) . '' . NativeUi::status_badge($st, $status_map) . '' . esc_html((string) ($row['created_at'] ?? '')) . ''; if ($st === 'pending') { - echo '
    '; + echo ''; echo ''; echo ''; echo ''; @@ -183,13 +193,16 @@ class PayIncome { submit_button(__('结算', 'aksrt'), 'small', '', false); echo '
    '; } else { - echo '—'; + echo '—'; } echo '
    '; + NativeUi::table_wrap_close(); AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args); - $this->render_footer(); + NativeUi::card_close(); + + NativeUi::page_close(); } public function handle_settle_income(): void { @@ -246,16 +259,6 @@ class PayIncome { } } - private function render_header(string $title): void { - echo '
    '; - echo '

    ' . esc_html($title) . '

    '; - $this->render_flash(); - } - - private function render_footer(): void { - echo '
    '; - } - private function render_flash(): void { if (empty($_GET['aksrt_pay_msg'])) { return; diff --git a/src/Admin/Native/PayMenus.php b/src/Admin/Native/PayMenus.php index 03147255..e00f8f34 100644 --- a/src/Admin/Native/PayMenus.php +++ b/src/Admin/Native/PayMenus.php @@ -26,6 +26,7 @@ class PayMenus { return; } + NativeUi::boot(); add_action('admin_menu', [$this, 'register_menus']); add_action('admin_notices', [$this, 'pending_notices']); // Domain admin_post handlers are registered by dedicated modules. @@ -169,22 +170,22 @@ class PayMenus { } } - private function render_header(string $title): void { - echo '
    '; - echo '

    ' . esc_html($title) . '

    '; - } - - private function render_footer(): void { - echo '
    '; - } - private function page_url(string $slug, array $args = []): string { return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); } + /** 快捷入口图标,键同 pages()。 */ + private const PAGE_ICONS = [ + 'orders' => 'dashicons-list-view', + 'products' => 'dashicons-media-document', + 'coupons' => 'dashicons-tickets-alt', + 'charge_cards' => 'dashicons-id-alt', + 'income' => 'dashicons-chart-pie', + 'withdraw' => 'dashicons-money-alt', + ]; + public function render_overview_page(): void { $this->ensure_cap(); - $this->render_header(__('支付中心', 'aksrt')); $order_stats = class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_stats() @@ -196,36 +197,39 @@ class PayMenus { ? \AKSRT\Payment\ChargeCard::get_instance()->get_stats() : []; - echo '

    ' . esc_html__('运营数据入口。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt') . '

    '; + NativeUi::page_open(); + NativeUi::header( + __('支付中心', 'aksrt'), + __('运营数据入口。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt'), + 'dashicons-cart', + [[ + 'label' => __('打开支付配置(SPA)', 'aksrt'), + 'url' => admin_url('admin.php?page=aksrt-settings#/payment/gateway'), + 'icon' => 'dashicons-admin-generic', + ]] + ); - echo '
    '; - $this->stat_card(__('订单总数', 'aksrt'), (string) (int) ($order_stats['total'] ?? 0), $this->page_url('aksrt_orders')); - $this->stat_card(__('待支付', 'aksrt'), (string) (int) ($order_stats['pending'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'pending'])); - $this->stat_card(__('已支付', 'aksrt'), (string) (int) ($order_stats['paid'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'paid'])); - $this->stat_card(__('营收', 'aksrt'), (string) ($order_stats['revenue'] ?? '0.00'), $this->page_url('aksrt_orders')); - $this->stat_card(__('待审提现', 'aksrt'), (string) (int) ($withdraw_stats['pending'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'pending'])); - $this->stat_card(__('可用卡密', 'aksrt'), (string) (int) ($card_stats['active'] ?? 0), $this->page_url('aksrt_charge_cards', ['status' => 'active'])); - echo '
    '; + NativeUi::stats([ + ['label' => __('订单总数', 'aksrt'), 'value' => (int) ($order_stats['total'] ?? 0), 'url' => $this->page_url('aksrt_orders'), 'icon' => 'dashicons-list-view', 'tone' => 'info'], + ['label' => __('待支付', 'aksrt'), 'value' => (int) ($order_stats['pending'] ?? 0), 'url' => $this->page_url('aksrt_orders', ['status' => 'pending']), 'icon' => 'dashicons-clock', 'tone' => 'warning'], + ['label' => __('已支付', 'aksrt'), 'value' => (int) ($order_stats['paid'] ?? 0), 'url' => $this->page_url('aksrt_orders', ['status' => 'paid']), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'], + ['label' => __('营收', 'aksrt'), 'value' => (string) ($order_stats['revenue'] ?? '0.00'), 'url' => $this->page_url('aksrt_orders'), 'icon' => 'dashicons-chart-line', 'tone' => 'success'], + ['label' => __('待审提现', 'aksrt'), 'value' => (int) ($withdraw_stats['pending'] ?? 0), 'url' => $this->page_url('aksrt_withdraw', ['status' => 'pending']), 'icon' => 'dashicons-money-alt', 'tone' => 'danger'], + ['label' => __('可用卡密', 'aksrt'), 'value' => (int) ($card_stats['active'] ?? 0), 'url' => $this->page_url('aksrt_charge_cards', ['status' => 'active']), 'icon' => 'dashicons-id-alt', 'tone' => 'info'], + ]); - echo '

    ' . esc_html__('快捷入口', 'aksrt') . '

      '; - foreach ($this->pages() as $page) { + NativeUi::card_open(__('快捷入口', 'aksrt')); + echo ''; + NativeUi::card_close(); - $spa = admin_url('admin.php?page=aksrt-settings#/payment/gateway'); - echo '

      ' . esc_html__('打开支付配置(SPA)', 'aksrt') . '

      '; - - $this->render_footer(); - } - - private function stat_card(string $label, string $value, string $url): void { - echo ''; - echo '
      ' . esc_html($label) . '
      '; - echo '
      ' . esc_html($value) . '
      '; - echo '
      '; + NativeUi::page_close(); } } diff --git a/src/Admin/Native/PayOrders.php b/src/Admin/Native/PayOrders.php index 6e54fdc3..4a5e349e 100644 --- a/src/Admin/Native/PayOrders.php +++ b/src/Admin/Native/PayOrders.php @@ -34,18 +34,20 @@ class PayOrders { public function render_orders_page(): void { $this->ensure_cap(); - $this->render_header(__('订单明细', 'aksrt')); + NativeUi::page_open(); + NativeUi::header(__('订单明细', 'aksrt'), '', 'dashicons-list-view'); + $this->render_flash(); if (!class_exists('\AKSRT\Payment\OrderRepository')) { echo '

      ' . esc_html__('订单服务不可用。', 'aksrt') . '

      '; - $this->render_footer(); + NativeUi::page_close(); return; } $detail_order = isset($_GET['order']) ? sanitize_text_field((string) wp_unslash($_GET['order'])) : ''; if ($detail_order !== '') { $this->render_order_detail_page($detail_order); - $this->render_footer(); + NativeUi::page_close(); return; } @@ -139,8 +141,7 @@ class PayOrders { }); // Type tabs. - echo '
        '; - $tab_bits = []; + echo '
        '; foreach ($type_tabs as $tkey => $tlabel) { $url = $this->page_url('aksrt_orders', array_filter( ['product_type' => $tkey !== '' ? $tkey : null] + $query_args, @@ -148,15 +149,13 @@ class PayOrders { return $value !== null; } )); - $cls = ($product_type === $tkey) ? 'current' : ''; - $tab_bits[] = '
      • ' . esc_html($tlabel) . '
      • '; + $cls = ($product_type === $tkey) ? ' is-active' : ''; + echo '' . esc_html($tlabel) . ''; } - echo implode(' | ', $tab_bits); - echo '
      '; + echo ''; // Payment method quick tabs. - echo '
        '; - $m_bits = []; + echo '
        '; foreach ($method_tabs as $mkey => $mlabel) { $url = $this->page_url('aksrt_orders', array_filter( ['payment_method' => $mkey !== '' ? $mkey : null] + $query_args, @@ -164,13 +163,13 @@ class PayOrders { return $value !== null; } )); - $cls = ($pay_method === $mkey) ? 'current' : ''; - $m_bits[] = '
      • ' . esc_html($mlabel) . '
      • '; + $cls = ($pay_method === $mkey) ? ' is-active' : ''; + echo '' . esc_html($mlabel) . ''; } - echo implode(' | ', $m_bits); - echo '
      '; + echo ''; - echo '
      '; + NativeUi::card_open(__('筛选', 'aksrt')); + echo ''; echo ''; if ($product_type !== '') { echo ''; @@ -213,21 +212,23 @@ class PayOrders { } if ($date_from !== '') $filter_bits[] = sprintf(__('开始日期=%s', 'aksrt'), $date_from); if ($date_to !== '') $filter_bits[] = sprintf(__('结束日期=%s', 'aksrt'), $date_to); - echo ' ' . esc_html(implode(' · ', $filter_bits)) . ''; + echo ' ' . esc_html(implode(' · ', $filter_bits)) . ''; echo ' ' . esc_html__('清除过滤', 'aksrt') . ''; } echo '
      '; // Closed-order cleanup. - echo '

      '; - echo '

      '; + echo ''; echo ''; echo ''; wp_nonce_field('aksrt_pay_clear_closed_orders'); submit_button(__('清理 14 天前关闭订单', 'aksrt'), 'secondary', '', false); echo '
      '; - echo '

      '; + NativeUi::card_close(); + $status_map = $this->status_badge_map($labels); + + NativeUi::table_card_open(__('订单明细', 'aksrt')); echo ''; echo ''; echo ''; @@ -240,15 +241,13 @@ class PayOrders { echo ''; if (!$items) { - echo ''; + echo NativeUi::empty_row(7, __('暂无订单', 'aksrt')); } foreach ($items as $row) { $order_number = (string) ($row['order_number'] ?? ''); $user_id = (int) ($row['user_id'] ?? 0); - $user = $user_id > 0 ? get_userdata($user_id) : null; $status_key = (string) ($row['status'] ?? ''); - $status_label = $labels[$status_key] ?? $status_key; $product_id = (int) ($row['product_id'] ?? 0); $product_type = sanitize_key((string) ($row['product_type'] ?? '')); $type_labels = \AKSRT\Core\DomainModel::order_product_type_labels(); @@ -259,17 +258,17 @@ class PayOrders { } echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
      ' . esc_html__('订单号', 'aksrt') . '
      ' . esc_html__('暂无订单', 'aksrt') . '
      ' . esc_html($order_number) . '' . ($user ? esc_html($user->display_name . ' (#' . $user_id . ')') : esc_html((string) $user_id)) . '' . esc_html($product_title !== '' ? $product_title : ($type_labels[$product_type] ?? $product_type)) . '
      ' . esc_html($type_labels[$product_type] ?? $product_type) . ($product_id > 0 ? ' #' . esc_html((string) $product_id) : '') . '
      ' . esc_html($this->order_amount($row)) . '
      ' . esc_html((string) ($row['payment_method'] ?? '')) . '
      ' . esc_html($status_label) . '' . esc_html((string) ($row['created_at'] ?? '')) . '
      ' . esc_html((string) ($row['paid_at'] ?? '') ?: '—') . '
      '; + echo '' . esc_html($order_number) . '' . NativeUi::user_cell($user_id) . '' . esc_html($product_title !== '' ? $product_title : ($type_labels[$product_type] ?? $product_type)) . '
      ' . esc_html($type_labels[$product_type] ?? $product_type) . ($product_id > 0 ? ' #' . esc_html((string) $product_id) : '') . '
      ' . esc_html($this->order_amount($row)) . '
      ' . esc_html((string) ($row['payment_method'] ?? '')) . '
      ' . NativeUi::status_badge($status_key, $status_map) . '' . esc_html((string) ($row['created_at'] ?? '')) . '
      ' . esc_html((string) ($row['paid_at'] ?? '') ?: '—') . '
      '; echo '' . esc_html__('详情', 'aksrt') . ''; // 已支付是已退款的唯一前置状态。 if ($status_key === \AKSRT\Core\DomainModel::ORDER_STATUS_PAID) { - echo '
      '; + echo ''; echo ''; echo ''; wp_nonce_field('aksrt_pay_refund_order_' . $order_number); @@ -278,7 +277,7 @@ class PayOrders { } $this->render_order_quick_status_buttons($order_number, $status_key); if ($status_key === 'closed') { - echo ''; + echo ''; echo ''; echo ''; wp_nonce_field('aksrt_pay_delete_order_' . $order_number); @@ -290,10 +289,31 @@ class PayOrders { } echo '
      '; - + NativeUi::table_wrap_close(); AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args); + NativeUi::card_close(); - $this->render_footer(); + NativeUi::page_close(); + } + + /** + * 订单状态键 → [文案, 色调] 映射,供 NativeUi::status_badge() 用。 + * + * @param array $labels DomainModel::order_status_labels() + * @return array + */ + private function status_badge_map(array $labels): array { + $tones = [ + \AKSRT\Core\DomainModel::ORDER_STATUS_PENDING => 'warning', + \AKSRT\Core\DomainModel::ORDER_STATUS_PAID => 'success', + \AKSRT\Core\DomainModel::ORDER_STATUS_REFUNDED => 'info', + \AKSRT\Core\DomainModel::ORDER_STATUS_CLOSED => 'neutral', + ]; + $map = []; + foreach ($labels as $key => $label) { + $map[$key] = [(string) $label, $tones[$key] ?? 'neutral']; + } + return $map; } private function render_order_detail_page(string $order_number): void { @@ -319,43 +339,49 @@ class PayOrders { $admin_note = (string) $order['note']; } - echo '

      ← ' . esc_html__('返回订单列表', 'aksrt') . '

      '; + $back = NativeUi::button_link(__('返回订单列表', 'aksrt'), $this->page_url('aksrt_orders'), false, 'dashicons-arrow-left-alt'); - echo ''; + NativeUi::card_open(sprintf(__('订单 %s', 'aksrt'), $order_number), $back); + echo '
      '; $rows = [ - __('订单号', 'aksrt') => $order_number, - __('用户', 'aksrt') => $user ? $user->display_name . ' (#' . $user_id . ')' : (string) $user_id, - __('商品类型', 'aksrt') => \AKSRT\Core\DomainModel::order_product_type_labels()[(string) ($order['product_type'] ?? '')] ?? (string) ($order['product_type'] ?? ''), - __('商品 ID', 'aksrt') => (string) ($order['product_id'] ?? ''), - __('金额', 'aksrt') => $this->order_amount($order), - __('状态', 'aksrt') => $labels[$status] ?? $status, - __('支付方式', 'aksrt') => (string) ($order['payment_method'] ?? ''), - __('渠道交易号', 'aksrt') => (string) ($order['payment_transaction_id'] ?? ''), - __('创建时间', 'aksrt') => (string) ($order['created_at'] ?? ''), - __('支付时间', 'aksrt') => (string) ($order['paid_at'] ?? ''), - __('买家备注', 'aksrt') => $customer_note, + __('订单号', 'aksrt') => esc_html($order_number), + __('用户', 'aksrt') => $user_id > 0 ? NativeUi::user_cell($user_id) : esc_html((string) $user_id), + __('商品类型', 'aksrt') => esc_html(\AKSRT\Core\DomainModel::order_product_type_labels()[(string) ($order['product_type'] ?? '')] ?? (string) ($order['product_type'] ?? '')), + __('商品 ID', 'aksrt') => esc_html((string) ($order['product_id'] ?? '')), + __('金额', 'aksrt') => esc_html($this->order_amount($order)), + __('状态', 'aksrt') => NativeUi::status_badge($status, $this->status_badge_map($labels)), + __('支付方式', 'aksrt') => esc_html((string) ($order['payment_method'] ?? '')), + __('渠道交易号', 'aksrt') => esc_html((string) ($order['payment_transaction_id'] ?? '')), + __('创建时间', 'aksrt') => esc_html((string) ($order['created_at'] ?? '')), + __('支付时间', 'aksrt') => esc_html((string) ($order['paid_at'] ?? '')), + __('买家备注', 'aksrt') => esc_html($customer_note), ]; foreach ($rows as $label => $value) { - echo ''; + echo ''; } if (!empty($context)) { - echo ''; + echo ''; } echo '
      ' . esc_html($label) . '' . esc_html((string) $value) . '
      ' . esc_html($label) . '' . $value . '
      ' . esc_html__('附加上下文', 'aksrt') . '
      ' . esc_html(wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '
      ' . esc_html__('附加上下文', 'aksrt') . '
      ' . esc_html(wp_json_encode($context, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '
      '; + NativeUi::card_close(); - echo '

      ' . esc_html__('运营备注', 'aksrt') . '

      '; - echo ''; + NativeUi::card_open(__('运营备注', 'aksrt')); + echo ''; echo ''; echo ''; wp_nonce_field('aksrt_pay_update_order_note_' . $order_number); - echo '

      '; + echo '

      '; + echo '
      '; submit_button(__('保存备注', 'aksrt'), 'secondary', '', false); + echo '
      '; echo ''; + NativeUi::card_close(); - echo '

      ' . esc_html__('更新状态', 'aksrt') . '

      '; - echo '
      '; + NativeUi::card_open(__('更新状态', 'aksrt')); + echo '
      '; $this->render_order_quick_status_buttons($order_number, $status, true); echo '
      '; + NativeUi::card_close(); } public function handle_update_order_status(): void { @@ -537,16 +563,6 @@ class PayOrders { return $this->cash_decimal((string) ($order['amount'] ?? '0')) . ' ' . strtoupper((string) ($order['currency'] ?? 'CNY')); } - private function render_header(string $title): void { - echo '
      '; - echo '

      ' . esc_html($title) . '

      '; - $this->render_flash(); - } - - private function render_footer(): void { - echo '
      '; - } - private function render_flash(): void { if (empty($_GET['aksrt_pay_msg'])) { return; diff --git a/src/Admin/Native/PayProducts.php b/src/Admin/Native/PayProducts.php index e876a748..7e8acd50 100644 --- a/src/Admin/Native/PayProducts.php +++ b/src/Admin/Native/PayProducts.php @@ -27,11 +27,12 @@ class PayProducts { public function render_products_page(): void { $this->ensure_cap(); - $this->render_header(__('付费文章', 'aksrt')); + NativeUi::page_open(); + NativeUi::header(__('付费文章', 'aksrt'), '', 'dashicons-media-document'); if (!class_exists('\AKSRT\Payment\OrderRepository') || !class_exists('\AKSRT\Core\DomainModel')) { echo '

      ' . esc_html__('订单服务不可用。', 'aksrt') . '

      '; - $this->render_footer(); + NativeUi::page_close(); return; } @@ -80,6 +81,7 @@ class PayProducts { $this->render_filters($pay_type, $search, $type_labels); + NativeUi::table_card_open(__('付费文章', 'aksrt')); echo ''; echo ''; echo ''; @@ -95,7 +97,7 @@ class PayProducts { echo ''; if (!$posts) { - echo ''; + echo NativeUi::empty_row(10, __('暂无付费文章。', 'aksrt')); } foreach ($posts as $post) { @@ -146,7 +148,7 @@ class PayProducts { echo ''; echo ''; echo ''; - echo '
      ' . esc_html__('商品', 'aksrt') . '
      ' . esc_html__('暂无付费文章。', 'aksrt') . '
      ' . esc_html((string) $db_sales) . '' . esc_html((string) max(0, $meta_sales)) . '' . esc_html($revenue) . ''; + echo ''; if ($edit_url) { echo '' . esc_html__('编辑', 'aksrt') . ''; } @@ -168,8 +170,10 @@ class PayProducts { }); echo '
      '; + NativeUi::table_wrap_close(); AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args); - $this->render_footer(); + NativeUi::card_close(); + NativeUi::page_close(); } /** @@ -314,7 +318,22 @@ class PayProducts { * @param array $type_labels */ private function render_filters(string $pay_type, string $search, array $type_labels): void { - echo '
      '; + echo '
        '; + $base = $this->page_url(self::PAGE_SLUG); + $all_class = $pay_type === '' ? 'is-active' : ''; + echo '
      • ' . esc_html__('全部', 'aksrt') . '
      • '; + foreach ($type_labels as $key => $label) { + $url = $this->page_url(self::PAGE_SLUG, array_filter([ + 'pay_type' => $key, + 's' => $search !== '' ? $search : null, + ])); + $cls = $pay_type === $key ? 'is-active' : ''; + echo '
      • ' . esc_html($label) . '
      • '; + } + echo '
      '; + + NativeUi::card_open(__('筛选', 'aksrt')); + echo ''; echo ''; echo ' '; - echo ' '; + echo ''; + echo ''; submit_button(__('筛选', 'aksrt'), 'secondary', '', false); echo '
      '; - - echo '
        '; - $base = $this->page_url(self::PAGE_SLUG); - $all_class = $pay_type === '' ? 'current' : ''; - echo '
      • ' . esc_html__('全部', 'aksrt') . '
      • '; - foreach ($type_labels as $key => $label) { - $url = $this->page_url(self::PAGE_SLUG, array_filter([ - 'pay_type' => $key, - 's' => $search !== '' ? $search : null, - ])); - $cls = $pay_type === $key ? 'current' : ''; - echo ' |
      • ' . esc_html($label) . '
      • '; - } - echo '
      '; echo '

      ' . esc_html__('本页统计内容付费文章(product_type=resource),不含商城实体商品。订单销量来自已支付/已发货/已完成订单;Meta 销量为缓存计数。', 'aksrt') . '

      '; + NativeUi::card_close(); } private function ensure_cap(): void { @@ -361,15 +367,6 @@ class PayProducts { } } - private function render_header(string $title): void { - echo '
      '; - echo '

      ' . esc_html($title) . '

      '; - } - - private function render_footer(): void { - echo '
      '; - } - private function page_url(string $slug, array $args = []): string { return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); } diff --git a/src/Admin/Native/PayWithdrawals.php b/src/Admin/Native/PayWithdrawals.php index 1b81f46d..b331f474 100644 --- a/src/Admin/Native/PayWithdrawals.php +++ b/src/Admin/Native/PayWithdrawals.php @@ -30,11 +30,13 @@ class PayWithdrawals { public function render_withdrawals_page(): void { $this->ensure_cap(); - $this->render_header(__('提现审核', 'aksrt')); + NativeUi::page_open(); + NativeUi::header(__('提现审核', 'aksrt'), '', 'dashicons-money-alt'); + $this->render_flash(); if (!class_exists('\AKSRT\Payment\Withdrawal')) { echo '

      ' . esc_html__('提现模块未初始化。', 'aksrt') . '

      '; - $this->render_footer(); + NativeUi::page_close(); return; } @@ -53,18 +55,17 @@ class PayWithdrawals { $retry_key = ''; } - echo '

      ' . esc_html(sprintf( - /* translators: 1: total 2: pending 3: completed 4: paid */ - __('共 %1$d 条 · 待处理 %2$d · 处理中 %3$d · 已完成 %4$d · 已拒绝 %5$d · 已打款 %6$s', 'aksrt'), - (int) ($stats['total'] ?? 0), - (int) ($stats['pending'] ?? 0), - (int) ($stats['processing'] ?? 0), - (int) ($stats['completed'] ?? 0), - (int) ($stats['rejected'] ?? 0), - (string) ($stats['total_paid'] ?? '0.00') - )) . '

      '; + NativeUi::stats([ + ['label' => __('总申请', 'aksrt'), 'value' => (int) ($stats['total'] ?? 0), 'icon' => 'dashicons-money-alt', 'tone' => 'info'], + ['label' => __('待处理', 'aksrt'), 'value' => (int) ($stats['pending'] ?? 0), 'icon' => 'dashicons-clock', 'tone' => 'warning'], + ['label' => __('处理中', 'aksrt'), 'value' => (int) ($stats['processing'] ?? 0), 'icon' => 'dashicons-update', 'tone' => 'info'], + ['label' => __('已完成', 'aksrt'), 'value' => (int) ($stats['completed'] ?? 0), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'], + ['label' => __('已拒绝', 'aksrt'), 'value' => (int) ($stats['rejected'] ?? 0), 'icon' => 'dashicons-no-alt', 'tone' => 'danger'], + ['label' => __('已打款', 'aksrt'), 'value' => (string) ($stats['total_paid'] ?? '0.00'), 'unit' => '¥', 'icon' => 'dashicons-chart-line', 'tone' => 'success'], + ]); - echo '
      '; + NativeUi::card_open(__('筛选', 'aksrt')); + echo ''; echo ''; echo ''; echo ''; echo ''; @@ -210,10 +219,11 @@ class PayWithdrawals { } echo ''; - + NativeUi::table_wrap_close(); AdminListPagination::render(self::PAGE_SLUG, $total, $page_num, $per_page, $query_args); + NativeUi::card_close(); - $this->render_footer(); + NativeUi::page_close(); } public function handle_update_withdrawal(): void { @@ -247,28 +257,12 @@ class PayWithdrawals { exit; } - /* ------------------------------------------------------------------ - * Income - * ----------------------------------------------------------------*/ - - - private function ensure_cap(): void { if (!current_user_can(self::CAPABILITY)) { wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); } } - private function render_header(string $title): void { - echo '
      '; - echo '

      ' . esc_html($title) . '

      '; - $this->render_flash(); - } - - private function render_footer(): void { - echo '
      '; - } - private function render_flash(): void { if (empty($_GET['aksrt_pay_msg'])) { return; diff --git a/src/Admin/Native/UsersMenus.php b/src/Admin/Native/UsersMenus.php index 1f9d2840..e9842069 100644 --- a/src/Admin/Native/UsersMenus.php +++ b/src/Admin/Native/UsersMenus.php @@ -32,6 +32,7 @@ class UsersMenus { return; } + NativeUi::boot(); add_action('admin_menu', [$this, 'register_menus']); add_action('admin_notices', [$this, 'pending_notices']); add_action('admin_post_aksrt_users_send_message', [$this, 'handle_send_message']); @@ -44,6 +45,7 @@ class UsersMenus { add_action('admin_post_aksrt_ticket_update', [$this, 'handle_ticket_update']); add_action('admin_enqueue_scripts', [$this, 'enqueue_ticket_assets']); add_action('admin_enqueue_scripts', [$this, 'enqueue_message_assets']); + add_action('admin_enqueue_scripts', [$this, 'enqueue_invite_assets']); } public function register_menus(): void { @@ -223,14 +225,22 @@ class UsersMenus { public function render_ban_page(): void { $this->ensure_cap(); - echo '

      ' . esc_html__('用户禁封申诉及用户举报管理', 'aksrt') . '

      '; + NativeUi::page_open('aksrt-native-ban'); + NativeUi::header( + __('用户禁封申诉及用户举报管理', 'aksrt'), + __('处理用户举报与禁封申诉工单。禁封 / 解封操作在前台封禁弹窗完成。', 'aksrt'), + 'dashicons-shield' + ); $this->flash(); // 与参考主题一致:后台仅承载「申诉&举报」工单处理;禁封/解封操作 // 全部在前台完成(用户主页封面 / 评论区 / 文章作者栏三点菜单 → // 封禁弹窗,REST /user-ban/modal + /user-ban/set)。 + // render_tickets_tab() 自带筛选控件 + 表格 + 分页,直接交给 .aksrt-native + // 作用域下的共享样式(.subsubsub / .widefat / .button / .tablenav)接管。 $this->render_tickets_tab(); - echo '
      '; + + NativeUi::page_close(); } diff --git a/src/Admin/Native/UsersMenusInviteCode.php b/src/Admin/Native/UsersMenusInviteCode.php index f1ff35c6..953a9f20 100644 --- a/src/Admin/Native/UsersMenusInviteCode.php +++ b/src/Admin/Native/UsersMenusInviteCode.php @@ -14,6 +14,17 @@ defined('ABSPATH') || exit; trait UsersMenusInviteCode { + /** 页面专属样式(排序箭头 / 按钮组 / 字段行 / 复制提示),只在本页入队。 */ + public function enqueue_invite_assets(string $hook): void { + if ($hook !== 'users_page_aksrt_invite_codes') { + return; + } + $css = '/assets/css/admin/invite-codes.css'; + if (is_file(AKSRT_DIR . $css)) { + wp_enqueue_style('aksrt-admin-invite-codes', AKSRT_URI . $css, [], (string) filemtime(AKSRT_DIR . $css)); + } + } + public function render_invite_codes_page(): void { if (!current_user_can('edit_users')) { wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); @@ -27,11 +38,13 @@ trait UsersMenusInviteCode { } $nonce = wp_create_nonce('aksrt_invite_admin'); + NativeUi::page_open('aksrt-inv-page'); + // 功能未启用提示(置顶橙色提醒)。 $settings = $service->registration_settings(); if (empty($settings['enabled'])) { $settings_url = admin_url('admin.php?page=aksrt-settings'); - echo '

      ' + echo '

      ' . esc_html__('注意:邀请码功能暂未启用。您可以在', 'aksrt') . ' ' . esc_html__('登录注册设置', 'aksrt') . ' ' . esc_html__('中启用此功能', 'aksrt') @@ -46,43 +59,17 @@ trait UsersMenusInviteCode { echo '

      ' . esc_html($inv_title) . '

      ' . ($inv_msg !== '' ? '

      ' . esc_html($inv_msg) . '

      ' : '') . '
      '; } + + if ($tab === '') { + $this->render_invite_codes_list($service, $this_url, $nonce); + } elseif ($tab === 'add') { + $this->render_invite_codes_add($this_url, $nonce); + } else { + $this->render_invite_codes_export($this_url, $nonce); + } + + NativeUi::page_close(); ?> -
      - - - render_invite_codes_list($service, $this_url, $nonce); ?> - - render_invite_codes_add($this_url, $nonce); ?> - - render_invite_codes_export($this_url, $nonce); ?> - -
      '; - - $this->ticket_pagenavi($total, self::TICKETS_PER_PAGE); } /** 分页(对齐参考主题 zibpay_admin_pagenavi:« ‹ 第X页共Y页 › » + 每页显示数) */ @@ -309,22 +307,22 @@ trait UsersMenusTickets { $is_processing = isset($_GET['action']) && sanitize_key((string) wp_unslash($_GET['action'])) === 'process' && isset($_GET['id']) && (string) wp_unslash($_GET['id']) === $kind . '-' . (string) $row['id']; if ($is_processing) { - $status_html = '' . esc_html__('正在处理', 'aksrt') . ''; + $status_html = NativeUi::badge(__('正在处理', 'aksrt'), 'warning'); } elseif ($status === 'pending') { // JS 切换展开(无整页刷新);无 JS 时 href 整页兜底。 $status_html = '' . esc_html__('立即处理', 'aksrt') . ''; } elseif ($status === 'resolved') { $status_html = $kind === 'appeal' - ? '' . esc_html__('已批准', 'aksrt') . '' - : '' . esc_html__('处理完成', 'aksrt') . ''; + ? NativeUi::badge(__('已批准', 'aksrt'), 'success') + : NativeUi::badge(__('处理完成', 'aksrt'), 'info'); } else { - $status_html = '' . esc_html__('已拒绝', 'aksrt') . ''; + $status_html = NativeUi::badge(__('已拒绝', 'aksrt'), 'danger'); } // 类型列(对齐参考主题:不良举报红 / 禁封申诉绿) $type_html = $kind === 'report' - ? '' . esc_html__('不良举报', 'aksrt') . '' - : '' . esc_html__('禁封申诉', 'aksrt') . ''; + ? NativeUi::badge(__('不良举报', 'aksrt'), 'danger', false) + : NativeUi::badge(__('禁封申诉', 'aksrt'), 'success', false); // 提交用户列(显示名链接 + 管理|查看 row-actions) $user = $user_id > 0 ? get_userdata($user_id) : null; @@ -345,7 +343,7 @@ trait UsersMenusTickets { echo '' . $status_html . ''; echo '' . $type_html . ''; echo '' . $user_html . ''; - echo '
      ' . $desc_html . '
      '; + echo '
      ' . $desc_html . '
      '; echo '' . esc_html((string) $row['time']) . ''; echo '' . esc_html((string) ($row['updated'] !== '' ? $row['updated'] : '—')) . ''; echo ''; @@ -358,9 +356,9 @@ trait UsersMenusTickets { $link = $this->ticket_target_link($target_type, $target_id); if ($target_type === 'user') { - $html = '
      ' . esc_html__('举报用户:', 'aksrt') . ($link !== '' ? $link : esc_html('#' . $target_id)) . '
      '; + $html = '
      ' . esc_html__('举报用户:', 'aksrt') . ($link !== '' ? $link : esc_html('#' . $target_id)) . '
      '; } else { - $html = '
      ' . esc_html__('举报内容:', 'aksrt') . ($link !== '' ? $link : esc_html($target_type . ' #' . $target_id)) . '
      '; + $html = '
      ' . esc_html__('举报内容:', 'aksrt') . ($link !== '' ? $link : esc_html($target_type . ' #' . $target_id)) . '
      '; } $html .= '
      ' . esc_html(sprintf(__('举报原因:%s', 'aksrt'), (string) ($r['reason'] ?? ''))) . '
      '; if (!empty($r['category'])) { @@ -393,7 +391,7 @@ trait UsersMenusTickets { if ($ban) { $end = !empty($ban['expires_at']) ? (string) $ban['expires_at'] : __('永久', 'aksrt'); - $html = '
      ' . esc_html(sprintf(__('禁封状态:%s', 'aksrt'), __('封禁中(禁止登录)', 'aksrt'))) . '
      '; + $html = '
      ' . esc_html(sprintf(__('禁封状态:%s', 'aksrt'), __('封禁中(禁止登录)', 'aksrt'))) . '
      '; $html .= '
      ' . esc_html(sprintf(__('禁封原因:%s', 'aksrt'), (string) ($ban['reason'] ?? ''))) . '
      '; $html .= '
      ' . esc_html(sprintf(__('开始时间:%s', 'aksrt'), (string) ($ban['created_at'] ?? ''))) . '
      '; $html .= '
      ' . esc_html(sprintf(__('结束时间:%s', 'aksrt'), $end)) . '
      '; @@ -401,24 +399,24 @@ trait UsersMenusTickets { } if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_SUSPEND)) { - return '
      ' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('停权中', 'aksrt'))) . '
      '; + return '
      ' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('停权中', 'aksrt'))) . '
      '; } if ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_MUTE)) { - return '
      ' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('禁言中', 'aksrt'))) . '
      '; + return '
      ' . esc_html(sprintf(__('限制状态:%s', 'aksrt'), __('禁言中', 'aksrt'))) . '
      '; } // 对齐参考主题:申诉列表行中用户已无禁封 → 显示已解封并自动结案 if ($context === 'appeal_row' && class_exists('\AKSRT\User\BanAppeal') && \AKSRT\User\BanAppeal::get_status($user_id) === 'pending') { update_user_meta($user_id, \AKSRT\User\BanAppeal::META_STATUS, 'approved'); - return '
      ' . esc_html__('此用户已解封', 'aksrt') . '
      '; + return '
      ' . esc_html__('此用户已解封', 'aksrt') . '
      '; } // 对齐参考主题:申诉处理表单中用户已解封的提示 if ($context === 'appeal_form') { - return '
      ' . esc_html__('当前用户已解封', 'aksrt') . '
      '; + return '
      ' . esc_html__('当前用户已解封', 'aksrt') . '
      '; } - return '
      ' . esc_html__('用户状态正常', 'aksrt') . '
      '; + return '
      ' . esc_html__('用户状态正常', 'aksrt') . '
      '; } private function ticket_target_link(string $target_type, int $target_id): string { @@ -480,7 +478,7 @@ trait UsersMenusTickets { // 与参考主题一致:跳转到前台该用户公开主页,经封面右下角三点 // 菜单的「禁封此用户」打开封禁弹窗完成处理(后台不提供禁封表单)。 $ban_url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($target_id) : get_author_posts_url($target_id); - echo '
      ' . esc_html__('用户状态正常', 'aksrt') . '
      '; + echo '
      ' . esc_html__('用户状态正常', 'aksrt') . '
      '; echo '

      ' . esc_html__('请查阅相关资料以及举证,如举报事实清楚,可以', 'aksrt'); echo ' ' . esc_html__('点击此处', 'aksrt') . ' '; echo esc_html__('对用户做禁封处理,并将处理结果填写至下方', 'aksrt') . '

      '; @@ -489,7 +487,7 @@ trait UsersMenusTickets { } echo '' . esc_html__('处理说明', 'aksrt') . ''; - echo ''; + echo ''; echo '

      ' . esc_html__('此处内容会反馈给举报人', 'aksrt') . '

      '; echo ''; @@ -518,12 +516,12 @@ trait UsersMenusTickets { echo ''; echo '' . esc_html__('处理方式', 'aksrt') . ''; - echo '

      '; - echo '

      '; + echo '

      '; + echo '

      '; echo ''; echo '' . esc_html__('处理留言', 'aksrt') . ''; - echo ''; + echo ''; echo '

      ' . esc_html__('如需给用户留言请填写此处,如果拒绝请填写拒绝原因', 'aksrt') . '

      '; echo ''; diff --git a/src/Admin/Native/UsersMenusTicketsCenter.php b/src/Admin/Native/UsersMenusTicketsCenter.php index e0627d83..86589d70 100644 --- a/src/Admin/Native/UsersMenusTicketsCenter.php +++ b/src/Admin/Native/UsersMenusTicketsCenter.php @@ -54,19 +54,24 @@ trait UsersMenusTicketsCenter { wp_die(esc_html__('权限不足', 'aksrt')); } - echo '
      '; - echo '

      ' . esc_html__('工单管理', 'aksrt') . '

      '; - printf( - '%s', - esc_url(admin_url('admin.php?page=aksrt-settings#/users/ticket')), - esc_html__('工单配置(部门 / 状态 / 预设回复)', 'aksrt') + // 保留 aksrt-tickets-admin 根类:admin-tickets.js 以它界定提交防抖与 + // 展开工单滚动聚焦的作用域;aksrt-native 提供共享运营页样式令牌。 + NativeUi::page_open('aksrt-tickets-admin'); + NativeUi::header( + __('工单管理', 'aksrt'), + __('处理用户工单。部门 / 状态 / 预设回复的配置在主题设置 SPA。', 'aksrt'), + 'dashicons-tickets-alt', + [[ + 'label' => __('工单配置(部门 / 状态 / 预设回复)', 'aksrt'), + 'url' => admin_url('admin.php?page=aksrt-settings#/users/ticket'), + 'icon' => 'dashicons-admin-generic', + ]] ); - echo '
      '; $this->render_ticket_notices(); $this->render_ticket_create_form(); $this->render_ticket_list_tab(); - echo '
      '; + NativeUi::page_close(); } /** 写操作后的 redirect 提示:成功绿色、失败红色。 */ diff --git a/src/Admin/Native/UsersMenusTicketsDetail.php b/src/Admin/Native/UsersMenusTicketsDetail.php index a1702716..bed7960f 100644 --- a/src/Admin/Native/UsersMenusTicketsDetail.php +++ b/src/Admin/Native/UsersMenusTicketsDetail.php @@ -93,7 +93,7 @@ trait UsersMenusTicketsDetail { $html .= '
      '; $html .= '

      ' . esc_html__('内部备注', 'aksrt') - . ' ' . esc_html__('用户不可见', 'aksrt') . '

      '; + . ' ' . esc_html__('用户不可见', 'aksrt') . ''; if ($notes === []) { $html .= '

      ' . esc_html__('暂无备注', 'aksrt') . '

      '; } else { @@ -158,7 +158,7 @@ trait UsersMenusTicketsDetail { __('部门', 'aksrt') => esc_html((string) $ticket['ticket_type_name'] !== '' ? (string) $ticket['ticket_type_name'] : '—'), __('跟进人', 'aksrt') => (int) $ticket['admin_id'] > 0 ? esc_html((string) $ticket['admin_name']) - : '' . esc_html__('未指派', 'aksrt') . '', + : '' . esc_html__('未指派', 'aksrt') . '', ]; if ((string) $ticket['order_number'] !== '') { $rows[__('关联订单', 'aksrt')] = '' . esc_html((string) $ticket['order_number']) . ''; @@ -170,7 +170,7 @@ trait UsersMenusTicketsDetail { /* translators: %d urge count */ __('%d 次', 'aksrt'), (int) $ticket['urge_count'] - )) . ' ' . esc_html(sprintf( + )) . ' ' . esc_html(sprintf( /* translators: %s last urge time */ __('最近 %s', 'aksrt'), (string) $ticket['last_urge_time'] @@ -268,7 +268,7 @@ trait UsersMenusTicketsDetail { return '
      ' . ($avatar !== '' ? '' : '') . '' . esc_html($name !== '' ? $name : '#' . $user_id) . '' - . '' . esc_html($role) . '' + . '' . esc_html($role) . '' . '
      '; } diff --git a/src/Admin/Native/UsersMenusTicketsList.php b/src/Admin/Native/UsersMenusTicketsList.php index c39cdf66..28cb1a21 100644 --- a/src/Admin/Native/UsersMenusTicketsList.php +++ b/src/Admin/Native/UsersMenusTicketsList.php @@ -13,17 +13,18 @@ defined('ABSPATH') || exit; trait UsersMenusTicketsList { - /** 代客提交:原生
      折叠,失败回跳时自动展开。 */ + /** 代客提交:共享折叠卡片,失败回跳时自动展开。 */ private function render_ticket_create_form(): void { $types = \AKSRT\Ticket\TicketTypeService::all(); $failed = isset($_GET['aksrt_ticket_msg']) && sanitize_key((string) wp_unslash($_GET['aksrt_ticket_msg'])) === 'create_failed'; - echo '
      '; + echo '
      '; echo '' . esc_html__('代客提交工单', 'aksrt') . ''; + echo '
      '; if ($types === []) { printf( - '

      %s %s

      ', + '

      %s %s

    ', esc_html__('尚未配置工单部门,无法创建工单。', 'aksrt'), esc_url(admin_url('admin.php?page=aksrt-settings#/users/ticket')), esc_html__('前往配置', 'aksrt') @@ -31,43 +32,43 @@ trait UsersMenusTicketsList { return; } - echo $this->ticket_form_open('create', 0, 'aksrt-tickets-admin__create-form'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $this->ticket_form_open('create', 0); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped wp_nonce_field('aksrt_ticket_create_0'); - echo '
    '; + echo '
    '; printf( - '', + '', esc_html__('用户 ID', 'aksrt'), esc_attr__('工单归属用户', 'aksrt') ); - echo ''; printf( - '', + '', esc_html__('标题', 'aksrt'), \AKSRT\Ticket\TicketSettings::title_max() ); if (\AKSRT\Ticket\TicketSettings::order_link_enabled()) { printf( - '', + '', esc_html__('关联订单', 'aksrt'), esc_attr__('可选', 'aksrt') ); } printf( - '', + '', esc_html__('问题描述', 'aksrt'), \AKSRT\Ticket\TicketSettings::content_max() ); echo '
    '; printf( - '

    %s

    ', + '
    %s
    ', esc_html__('创建后状态为「已回复」并由你跟进,用户会收到通知。', 'aksrt'), esc_html__('创建工单', 'aksrt') ); - echo ''; + echo '
    '; } private function render_ticket_list_tab(): void { @@ -84,8 +85,7 @@ trait UsersMenusTicketsList { ], static fn($value): bool => $value !== '' && $value !== 0); $view_url = $this->ticket_page_url(array_merge($filters, $paged > 1 ? ['paged' => $paged] : [])); - $this->render_ticket_status_views($status); - $this->render_ticket_filter_form($status, $type_id, $s); + $this->render_ticket_toolbar($status, $type_id, $s); $repo = \AKSRT\Ticket\TicketRepository::get_instance(); $result = $repo->paginate([ @@ -98,7 +98,7 @@ trait UsersMenusTicketsList { $items = $result['items']; $total = $result['total']; - echo '
    '; + NativeUi::table_card_open(__('工单列表', 'aksrt')); echo ''; echo ''; echo ''; @@ -113,17 +113,14 @@ trait UsersMenusTicketsList { $hint = $filters === [] ? __('暂无进行中的工单,已关闭工单可在「已关闭」视图查看。', 'aksrt') : __('当前筛选条件下没有工单。', 'aksrt'); - printf( - '', - esc_html__('暂无工单', 'aksrt'), - esc_html($hint) - ); + echo NativeUi::empty_row(6, __('暂无工单', 'aksrt'), $hint); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } foreach ($items as $ticket) { $this->render_ticket_center_row($ticket, $view_url, $open_id === (int) $ticket['id']); } - echo '
    ' . esc_html__('工单', 'aksrt') . '
    %s%s
    '; + echo ''; + NativeUi::table_wrap_close(); \AKSRT\Admin\Native\AdminListPagination::render( self::TICKET_PAGE_SLUG, @@ -133,6 +130,17 @@ trait UsersMenusTicketsList { $filters, 'users.php' ); + NativeUi::card_close(); + } + + /** 状态视图 + 部门/关键词筛选,合成一张工具条卡片。 */ + private function render_ticket_toolbar(string $status, int $type_id, string $s): void { + NativeUi::card_open(); + echo '
    '; + $this->render_ticket_status_views($status); + $this->render_ticket_filter_form($status, $type_id, $s); + echo '
    '; + NativeUi::card_close(); } /** 状态视图:未关闭(默认)+ 各进行中状态 + 完结态,均带计数。 */ @@ -152,7 +160,7 @@ trait UsersMenusTicketsList { } array_unshift($views, ['', __('未关闭', 'aksrt'), $open_total, '']); - echo '